a deliberately stupid space heater that wastes electricity on fire shaders and prime numbers
1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
2use std::sync::Arc;
3use std::time::Instant;
4
5use winit::application::ApplicationHandler;
6use winit::dpi::PhysicalSize;
7use winit::event::WindowEvent;
8use winit::event_loop::ActiveEventLoop;
9use winit::window::{Window, WindowId};
10
11use crate::cpu;
12use crate::gpu::GpuState;
13
14pub struct App {
15 window: Option<Arc<Window>>,
16 gpu: Option<GpuState>,
17 running: Arc<AtomicBool>,
18 primes_found: Arc<AtomicU64>,
19 numbers_tested: Arc<AtomicU64>,
20 cpu_threads: Vec<std::thread::JoinHandle<()>>,
21 frame_count: u64,
22 last_fps_time: Instant,
23 fps: f64,
24 last_tested_count: u64,
25 tests_per_sec: f64,
26}
27
28impl App {
29 pub fn new() -> Self {
30 Self {
31 window: None,
32 gpu: None,
33 running: Arc::new(AtomicBool::new(true)),
34 primes_found: Arc::new(AtomicU64::new(0)),
35 numbers_tested: Arc::new(AtomicU64::new(0)),
36 cpu_threads: Vec::new(),
37 frame_count: 0,
38 last_fps_time: Instant::now(),
39 fps: 0.0,
40 last_tested_count: 0,
41 tests_per_sec: 0.0,
42 }
43 }
44
45 fn spawn_cpu_workers(&mut self) {
46 let num_threads = num_cpus::get();
47 log::info!("Spawning {} CPU threads for prime search", num_threads);
48
49 for i in 0..num_threads {
50 let running = self.running.clone();
51 let primes_found = self.primes_found.clone();
52 let numbers_tested = self.numbers_tested.clone();
53 let handle = std::thread::Builder::new()
54 .name(format!("prime-worker-{}", i))
55 .spawn(move || {
56 cpu::prime_worker(
57 running,
58 primes_found,
59 numbers_tested,
60 i as u64,
61 num_threads as u64,
62 );
63 })
64 .expect("Failed to spawn CPU worker thread");
65 self.cpu_threads.push(handle);
66 }
67 }
68
69 fn update_title(&mut self) {
70 self.frame_count += 1;
71 let now = Instant::now();
72 let elapsed = now.duration_since(self.last_fps_time).as_secs_f64();
73
74 let current_tested = self.numbers_tested.load(Ordering::Relaxed);
75 let current_primes = self.primes_found.load(Ordering::Relaxed);
76
77 if elapsed >= 0.5 {
78 self.fps = self.frame_count as f64 / elapsed;
79 let delta = current_tested.saturating_sub(self.last_tested_count);
80 self.tests_per_sec = delta as f64 / elapsed;
81 self.last_tested_count = current_tested;
82 self.frame_count = 0;
83 self.last_fps_time = now;
84 }
85
86 let num_cores = num_cpus::get();
87
88 let title = format!(
89 "\u{1F525} HEATSLOP \u{2014} GPU: {:.0} fps | CPU: {} cores \u{00D7} primes \u{2014} {} found, {} tests/s | Stay warm!",
90 self.fps,
91 num_cores,
92 format_number(current_primes),
93 format_number(self.tests_per_sec as u64),
94 );
95
96 if let Some(window) = &self.window {
97 window.set_title(&title);
98 }
99 }
100}
101
102fn format_number(n: u64) -> String {
103 if n >= 1_000_000_000 {
104 format!("{:.2}B", n as f64 / 1_000_000_000.0)
105 } else if n >= 1_000_000 {
106 format!("{:.2}M", n as f64 / 1_000_000.0)
107 } else if n >= 1_000 {
108 format!("{:.1}K", n as f64 / 1_000.0)
109 } else {
110 format!("{}", n)
111 }
112}
113
114impl ApplicationHandler for App {
115 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
116 if self.window.is_some() {
117 return;
118 }
119
120 let window_attrs = Window::default_attributes()
121 .with_title("\u{1F525} HEATSLOP \u{2014} Starting up...")
122 .with_inner_size(PhysicalSize::new(1280u32, 720u32));
123
124 let window = Arc::new(event_loop.create_window(window_attrs).unwrap());
125
126 let gpu = pollster::block_on(GpuState::new(window.clone()));
127
128 self.window = Some(window);
129 self.gpu = Some(gpu);
130
131 // Start CPU workers
132 self.spawn_cpu_workers();
133 }
134
135 fn window_event(
136 &mut self,
137 event_loop: &ActiveEventLoop,
138 _window_id: WindowId,
139 event: WindowEvent,
140 ) {
141 match event {
142 WindowEvent::CloseRequested => {
143 log::info!("Shutting down...");
144 self.running.store(false, Ordering::Relaxed);
145 event_loop.exit();
146 }
147 WindowEvent::Resized(new_size) => {
148 if let Some(gpu) = &mut self.gpu {
149 gpu.resize(new_size);
150 }
151 }
152 WindowEvent::RedrawRequested => {
153 self.update_title();
154
155 if let Some(gpu) = &mut self.gpu {
156 match gpu.render() {
157 Ok(_) => {}
158 Err(wgpu::SurfaceError::Lost) => {
159 let size = gpu.size;
160 gpu.resize(size);
161 }
162 Err(wgpu::SurfaceError::OutOfMemory) => {
163 log::error!("Out of GPU memory!");
164 event_loop.exit();
165 }
166 Err(e) => {
167 log::warn!("Render error: {:?}", e);
168 }
169 }
170 }
171
172 // Request another frame immediately (no vsync = max GPU throughput)
173 if let Some(window) = &self.window {
174 window.request_redraw();
175 }
176 }
177 WindowEvent::KeyboardInput { event, .. } => {
178 if event.physical_key
179 == winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::Escape)
180 {
181 self.running.store(false, Ordering::Relaxed);
182 event_loop.exit();
183 }
184 }
185 _ => {}
186 }
187 }
188}
189
190impl Drop for App {
191 fn drop(&mut self) {
192 self.running.store(false, Ordering::Relaxed);
193 for handle in self.cpu_threads.drain(..) {
194 let _ = handle.join();
195 }
196 }
197}