a deliberately stupid space heater that wastes electricity on fire shaders and prime numbers
1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::sync::Arc;
4use std::time::Instant;
5
6use winit::dpi::PhysicalSize;
7use winit::window::Window;
8
9use crate::shadertoy_adapter;
10
11// Uniform buffer matching the shader's Uniforms struct
12#[repr(C)]
13#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
14pub struct Uniforms {
15 pub time: f32,
16 pub _pad0: f32,
17 pub resolution: [f32; 2],
18 pub intensity: f32,
19 pub _pad1: [f32; 3],
20}
21
22fn create_intermediate_texture(
23 device: &wgpu::Device,
24 width: u32,
25 height: u32,
26 label: &str,
27) -> (wgpu::Texture, wgpu::TextureView) {
28 let texture = device.create_texture(&wgpu::TextureDescriptor {
29 label: Some(label),
30 size: wgpu::Extent3d {
31 width: width.max(1),
32 height: height.max(1),
33 depth_or_array_layers: 1,
34 },
35 mip_level_count: 1,
36 sample_count: 1,
37 dimension: wgpu::TextureDimension::D2,
38 format: wgpu::TextureFormat::Rgba16Float,
39 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
40 view_formats: &[],
41 });
42 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
43 (texture, view)
44}
45
46pub struct GpuState {
47 surface: wgpu::Surface<'static>,
48 device: wgpu::Device,
49 queue: wgpu::Queue,
50 config: wgpu::SurfaceConfiguration,
51
52 // Pipelines
53 fire_pipeline: wgpu::RenderPipeline,
54 blur_pipeline: wgpu::RenderPipeline,
55 composite_pipeline: wgpu::RenderPipeline,
56
57 // Intermediate textures
58 fire_texture: wgpu::Texture,
59 fire_view: wgpu::TextureView,
60 blur_texture: wgpu::Texture,
61 blur_view: wgpu::TextureView,
62
63 // Shared resources
64 sampler: wgpu::Sampler,
65 uniform_buffer: wgpu::Buffer,
66
67 // Bind group layouts (needed for recreating bind groups on resize)
68 #[allow(dead_code)]
69 fire_bg_layout: wgpu::BindGroupLayout,
70 blur_bg_layout: wgpu::BindGroupLayout,
71 composite_bg_layout: wgpu::BindGroupLayout,
72
73 // Bind groups
74 fire_bind_group: wgpu::BindGroup,
75 blur_bind_group: wgpu::BindGroup,
76 composite_bind_group: wgpu::BindGroup,
77
78 start_time: Instant,
79 pub size: PhysicalSize<u32>,
80}
81
82impl GpuState {
83 fn create_blur_bind_group(
84 device: &wgpu::Device,
85 layout: &wgpu::BindGroupLayout,
86 uniform_buffer: &wgpu::Buffer,
87 fire_view: &wgpu::TextureView,
88 sampler: &wgpu::Sampler,
89 ) -> wgpu::BindGroup {
90 device.create_bind_group(&wgpu::BindGroupDescriptor {
91 label: Some("blur_bind_group"),
92 layout,
93 entries: &[
94 wgpu::BindGroupEntry {
95 binding: 0,
96 resource: uniform_buffer.as_entire_binding(),
97 },
98 wgpu::BindGroupEntry {
99 binding: 1,
100 resource: wgpu::BindingResource::TextureView(fire_view),
101 },
102 wgpu::BindGroupEntry {
103 binding: 2,
104 resource: wgpu::BindingResource::Sampler(sampler),
105 },
106 ],
107 })
108 }
109
110 fn create_composite_bind_group(
111 device: &wgpu::Device,
112 layout: &wgpu::BindGroupLayout,
113 uniform_buffer: &wgpu::Buffer,
114 fire_view: &wgpu::TextureView,
115 blur_view: &wgpu::TextureView,
116 sampler: &wgpu::Sampler,
117 ) -> wgpu::BindGroup {
118 device.create_bind_group(&wgpu::BindGroupDescriptor {
119 label: Some("composite_bind_group"),
120 layout,
121 entries: &[
122 wgpu::BindGroupEntry {
123 binding: 0,
124 resource: uniform_buffer.as_entire_binding(),
125 },
126 wgpu::BindGroupEntry {
127 binding: 1,
128 resource: wgpu::BindingResource::TextureView(fire_view),
129 },
130 wgpu::BindGroupEntry {
131 binding: 2,
132 resource: wgpu::BindingResource::TextureView(blur_view),
133 },
134 wgpu::BindGroupEntry {
135 binding: 3,
136 resource: wgpu::BindingResource::Sampler(sampler),
137 },
138 ],
139 })
140 }
141
142 pub async fn new(window: Arc<Window>) -> Self {
143 let size = window.inner_size();
144
145 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
146 backends: wgpu::Backends::all(),
147 ..Default::default()
148 });
149
150 let surface = instance.create_surface(window.clone()).unwrap();
151
152 let adapter = instance
153 .request_adapter(&wgpu::RequestAdapterOptions {
154 power_preference: wgpu::PowerPreference::HighPerformance,
155 compatible_surface: Some(&surface),
156 force_fallback_adapter: false,
157 })
158 .await
159 .expect("Failed to find a suitable GPU adapter");
160
161 log::info!("Using GPU: {:?}", adapter.get_info().name);
162
163 let (device, queue) = adapter
164 .request_device(
165 &wgpu::DeviceDescriptor {
166 label: Some("heatslop_device"),
167 required_features: wgpu::Features::empty(),
168 required_limits: wgpu::Limits::default(),
169 ..Default::default()
170 },
171 None,
172 )
173 .await
174 .expect("Failed to create device");
175
176 let surface_caps = surface.get_capabilities(&adapter);
177 let surface_format = surface_caps
178 .formats
179 .iter()
180 .find(|f| f.is_srgb())
181 .copied()
182 .unwrap_or(surface_caps.formats[0]);
183
184 let config = wgpu::SurfaceConfiguration {
185 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
186 format: surface_format,
187 width: size.width.max(1),
188 height: size.height.max(1),
189 present_mode: wgpu::PresentMode::AutoNoVsync,
190 alpha_mode: surface_caps.alpha_modes[0],
191 view_formats: vec![],
192 desired_maximum_frame_latency: 2,
193 };
194 surface.configure(&device, &config);
195
196 // ---- Shaders ----
197 // Vertex shader: single WGSL module, override constant controls Y-flip
198 let vert = device.create_shader_module(wgpu::ShaderModuleDescriptor {
199 label: Some("vert"),
200 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
201 "shaders/fullscreen.wgsl"
202 ))),
203 });
204 let flip_off = HashMap::from([("0".to_string(), 0.0_f64)]);
205 let flip_on = HashMap::from([("0".to_string(), 1.0_f64)]);
206
207 // Fragment shaders: original Shadertoy code composed via adapter
208 let fire_src = shadertoy_adapter::fire_frag();
209 let fire_frag = device.create_shader_module(wgpu::ShaderModuleDescriptor {
210 label: Some("fire_frag"),
211 source: wgpu::ShaderSource::Glsl {
212 shader: Cow::Owned(fire_src),
213 stage: naga::ShaderStage::Fragment,
214 defines: Default::default(),
215 },
216 });
217 let blur_src = shadertoy_adapter::blur_frag();
218 let blur_frag = device.create_shader_module(wgpu::ShaderModuleDescriptor {
219 label: Some("blur_frag"),
220 source: wgpu::ShaderSource::Glsl {
221 shader: Cow::Owned(blur_src),
222 stage: naga::ShaderStage::Fragment,
223 defines: Default::default(),
224 },
225 });
226 let composite_src = shadertoy_adapter::composite_frag();
227 let composite_frag = device.create_shader_module(wgpu::ShaderModuleDescriptor {
228 label: Some("composite_frag"),
229 source: wgpu::ShaderSource::Glsl {
230 shader: Cow::Owned(composite_src),
231 stage: naga::ShaderStage::Fragment,
232 defines: Default::default(),
233 },
234 });
235
236 // ---- Uniform buffer ----
237 let uniforms = Uniforms {
238 time: 0.0,
239 _pad0: 0.0,
240 resolution: [size.width as f32, size.height as f32],
241 intensity: 1.0,
242 _pad1: [0.0; 3],
243 };
244 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
245 label: Some("uniform_buffer"),
246 size: std::mem::size_of::<Uniforms>() as u64,
247 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
248 mapped_at_creation: false,
249 });
250 queue.write_buffer(&uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
251
252 // ---- Sampler ----
253 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
254 label: Some("tex_sampler"),
255 address_mode_u: wgpu::AddressMode::ClampToEdge,
256 address_mode_v: wgpu::AddressMode::ClampToEdge,
257 mag_filter: wgpu::FilterMode::Linear,
258 min_filter: wgpu::FilterMode::Linear,
259 ..Default::default()
260 });
261
262 // ---- Intermediate textures ----
263 let (fire_texture, fire_view) =
264 create_intermediate_texture(&device, size.width, size.height, "fire_texture");
265 let (blur_texture, blur_view) =
266 create_intermediate_texture(&device, size.width, size.height, "blur_texture");
267
268 // ---- Bind group layouts ----
269
270 // Fire: just uniforms
271 let fire_bg_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
272 label: Some("fire_bg_layout"),
273 entries: &[wgpu::BindGroupLayoutEntry {
274 binding: 0,
275 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
276 ty: wgpu::BindingType::Buffer {
277 ty: wgpu::BufferBindingType::Uniform,
278 has_dynamic_offset: false,
279 min_binding_size: None,
280 },
281 count: None,
282 }],
283 });
284
285 // Blur: uniforms + input texture + sampler
286 let blur_bg_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
287 label: Some("blur_bg_layout"),
288 entries: &[
289 wgpu::BindGroupLayoutEntry {
290 binding: 0,
291 visibility: wgpu::ShaderStages::FRAGMENT,
292 ty: wgpu::BindingType::Buffer {
293 ty: wgpu::BufferBindingType::Uniform,
294 has_dynamic_offset: false,
295 min_binding_size: None,
296 },
297 count: None,
298 },
299 wgpu::BindGroupLayoutEntry {
300 binding: 1,
301 visibility: wgpu::ShaderStages::FRAGMENT,
302 ty: wgpu::BindingType::Texture {
303 sample_type: wgpu::TextureSampleType::Float { filterable: true },
304 view_dimension: wgpu::TextureViewDimension::D2,
305 multisampled: false,
306 },
307 count: None,
308 },
309 wgpu::BindGroupLayoutEntry {
310 binding: 2,
311 visibility: wgpu::ShaderStages::FRAGMENT,
312 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
313 count: None,
314 },
315 ],
316 });
317
318 // Composite: uniforms + fire texture + blur texture + sampler
319 let composite_bg_layout =
320 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
321 label: Some("composite_bg_layout"),
322 entries: &[
323 wgpu::BindGroupLayoutEntry {
324 binding: 0,
325 visibility: wgpu::ShaderStages::FRAGMENT,
326 ty: wgpu::BindingType::Buffer {
327 ty: wgpu::BufferBindingType::Uniform,
328 has_dynamic_offset: false,
329 min_binding_size: None,
330 },
331 count: None,
332 },
333 wgpu::BindGroupLayoutEntry {
334 binding: 1,
335 visibility: wgpu::ShaderStages::FRAGMENT,
336 ty: wgpu::BindingType::Texture {
337 sample_type: wgpu::TextureSampleType::Float { filterable: true },
338 view_dimension: wgpu::TextureViewDimension::D2,
339 multisampled: false,
340 },
341 count: None,
342 },
343 wgpu::BindGroupLayoutEntry {
344 binding: 2,
345 visibility: wgpu::ShaderStages::FRAGMENT,
346 ty: wgpu::BindingType::Texture {
347 sample_type: wgpu::TextureSampleType::Float { filterable: true },
348 view_dimension: wgpu::TextureViewDimension::D2,
349 multisampled: false,
350 },
351 count: None,
352 },
353 wgpu::BindGroupLayoutEntry {
354 binding: 3,
355 visibility: wgpu::ShaderStages::FRAGMENT,
356 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
357 count: None,
358 },
359 ],
360 });
361
362 // ---- Bind groups ----
363 let fire_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
364 label: Some("fire_bind_group"),
365 layout: &fire_bg_layout,
366 entries: &[wgpu::BindGroupEntry {
367 binding: 0,
368 resource: uniform_buffer.as_entire_binding(),
369 }],
370 });
371
372 let blur_bind_group = Self::create_blur_bind_group(
373 &device,
374 &blur_bg_layout,
375 &uniform_buffer,
376 &fire_view,
377 &sampler,
378 );
379
380 let composite_bind_group = Self::create_composite_bind_group(
381 &device,
382 &composite_bg_layout,
383 &uniform_buffer,
384 &fire_view,
385 &blur_view,
386 &sampler,
387 );
388
389 // ---- Pipeline layouts ----
390 let fire_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
391 label: Some("fire_pipeline_layout"),
392 bind_group_layouts: &[&fire_bg_layout],
393 push_constant_ranges: &[],
394 });
395 let blur_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
396 label: Some("blur_pipeline_layout"),
397 bind_group_layouts: &[&blur_bg_layout],
398 push_constant_ranges: &[],
399 });
400 let composite_pipeline_layout =
401 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
402 label: Some("composite_pipeline_layout"),
403 bind_group_layouts: &[&composite_bg_layout],
404 push_constant_ranges: &[],
405 });
406
407 // ---- Render pipelines ----
408
409 // Fire pipeline: renders to Rgba16Float intermediate texture
410 let fire_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
411 label: Some("fire_pipeline"),
412 layout: Some(&fire_pipeline_layout),
413 vertex: wgpu::VertexState {
414 module: &vert,
415 entry_point: Some("main"),
416 buffers: &[],
417 compilation_options: wgpu::PipelineCompilationOptions {
418 constants: &flip_off,
419 ..Default::default()
420 },
421 },
422 fragment: Some(wgpu::FragmentState {
423 module: &fire_frag,
424 entry_point: Some("main"),
425 targets: &[Some(wgpu::ColorTargetState {
426 format: wgpu::TextureFormat::Rgba16Float,
427 blend: Some(wgpu::BlendState::REPLACE),
428 write_mask: wgpu::ColorWrites::ALL,
429 })],
430 compilation_options: Default::default(),
431 }),
432 primitive: wgpu::PrimitiveState {
433 topology: wgpu::PrimitiveTopology::TriangleList,
434 cull_mode: None,
435 ..Default::default()
436 },
437 depth_stencil: None,
438 multisample: wgpu::MultisampleState::default(),
439 multiview: None,
440 cache: None,
441 });
442
443 // Blur pipeline: renders to Rgba16Float intermediate texture
444 let blur_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
445 label: Some("blur_pipeline"),
446 layout: Some(&blur_pipeline_layout),
447 vertex: wgpu::VertexState {
448 module: &vert,
449 entry_point: Some("main"),
450 buffers: &[],
451 compilation_options: wgpu::PipelineCompilationOptions {
452 constants: &flip_on,
453 ..Default::default()
454 },
455 },
456 fragment: Some(wgpu::FragmentState {
457 module: &blur_frag,
458 entry_point: Some("main"),
459 targets: &[Some(wgpu::ColorTargetState {
460 format: wgpu::TextureFormat::Rgba16Float,
461 blend: Some(wgpu::BlendState::REPLACE),
462 write_mask: wgpu::ColorWrites::ALL,
463 })],
464 compilation_options: Default::default(),
465 }),
466 primitive: wgpu::PrimitiveState {
467 topology: wgpu::PrimitiveTopology::TriangleList,
468 cull_mode: None,
469 ..Default::default()
470 },
471 depth_stencil: None,
472 multisample: wgpu::MultisampleState::default(),
473 multiview: None,
474 cache: None,
475 });
476
477 // Composite pipeline: renders to swapchain (sRGB surface)
478 let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
479 label: Some("composite_pipeline"),
480 layout: Some(&composite_pipeline_layout),
481 vertex: wgpu::VertexState {
482 module: &vert,
483 entry_point: Some("main"),
484 buffers: &[],
485 compilation_options: wgpu::PipelineCompilationOptions {
486 constants: &flip_on,
487 ..Default::default()
488 },
489 },
490 fragment: Some(wgpu::FragmentState {
491 module: &composite_frag,
492 entry_point: Some("main"),
493 targets: &[Some(wgpu::ColorTargetState {
494 format: surface_format,
495 blend: Some(wgpu::BlendState::REPLACE),
496 write_mask: wgpu::ColorWrites::ALL,
497 })],
498 compilation_options: Default::default(),
499 }),
500 primitive: wgpu::PrimitiveState {
501 topology: wgpu::PrimitiveTopology::TriangleList,
502 cull_mode: None,
503 ..Default::default()
504 },
505 depth_stencil: None,
506 multisample: wgpu::MultisampleState::default(),
507 multiview: None,
508 cache: None,
509 });
510
511 Self {
512 surface,
513 device,
514 queue,
515 config,
516 fire_pipeline,
517 blur_pipeline,
518 composite_pipeline,
519 fire_texture,
520 fire_view,
521 blur_texture,
522 blur_view,
523 sampler,
524 uniform_buffer,
525 fire_bg_layout,
526 blur_bg_layout,
527 composite_bg_layout,
528 fire_bind_group,
529 blur_bind_group,
530 composite_bind_group,
531 start_time: Instant::now(),
532 size,
533 }
534 }
535
536 pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
537 if new_size.width > 0 && new_size.height > 0 {
538 self.size = new_size;
539 self.config.width = new_size.width;
540 self.config.height = new_size.height;
541 self.surface.configure(&self.device, &self.config);
542
543 // Recreate intermediate textures at new size
544 let (ft, fv) = create_intermediate_texture(
545 &self.device,
546 new_size.width,
547 new_size.height,
548 "fire_texture",
549 );
550 self.fire_texture = ft;
551 self.fire_view = fv;
552
553 let (bt, bv) = create_intermediate_texture(
554 &self.device,
555 new_size.width,
556 new_size.height,
557 "blur_texture",
558 );
559 self.blur_texture = bt;
560 self.blur_view = bv;
561
562 // Recreate bind groups that reference the textures
563 self.blur_bind_group = Self::create_blur_bind_group(
564 &self.device,
565 &self.blur_bg_layout,
566 &self.uniform_buffer,
567 &self.fire_view,
568 &self.sampler,
569 );
570
571 self.composite_bind_group = Self::create_composite_bind_group(
572 &self.device,
573 &self.composite_bg_layout,
574 &self.uniform_buffer,
575 &self.fire_view,
576 &self.blur_view,
577 &self.sampler,
578 );
579 }
580 }
581
582 pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
583 let elapsed = self.start_time.elapsed().as_secs_f32();
584
585 // Update uniforms
586 let uniforms = Uniforms {
587 time: elapsed,
588 _pad0: 0.0,
589 resolution: [self.size.width as f32, self.size.height as f32],
590 intensity: 1.0,
591 _pad1: [0.0; 3],
592 };
593 self.queue
594 .write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
595
596 let output = self.surface.get_current_texture()?;
597 let output_view = output
598 .texture
599 .create_view(&wgpu::TextureViewDescriptor::default());
600
601 let mut encoder = self
602 .device
603 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
604 label: Some("render_encoder"),
605 });
606
607 // ---- Pass 1: Fire generation → fire_texture ----
608 {
609 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
610 label: Some("fire_pass"),
611 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
612 view: &self.fire_view,
613 resolve_target: None,
614 ops: wgpu::Operations {
615 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
616 store: wgpu::StoreOp::Store,
617 },
618 })],
619 depth_stencil_attachment: None,
620 timestamp_writes: None,
621 occlusion_query_set: None,
622 });
623 pass.set_pipeline(&self.fire_pipeline);
624 pass.set_bind_group(0, &self.fire_bind_group, &[]);
625 pass.draw(0..3, 0..1);
626 }
627
628 // ---- Pass 2: Gaussian blur of fire → blur_texture ----
629 {
630 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
631 label: Some("blur_pass"),
632 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
633 view: &self.blur_view,
634 resolve_target: None,
635 ops: wgpu::Operations {
636 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
637 store: wgpu::StoreOp::Store,
638 },
639 })],
640 depth_stencil_attachment: None,
641 timestamp_writes: None,
642 occlusion_query_set: None,
643 });
644 pass.set_pipeline(&self.blur_pipeline);
645 pass.set_bind_group(0, &self.blur_bind_group, &[]);
646 pass.draw(0..3, 0..1);
647 }
648
649 // ---- Pass 3: Composite (fire + bloom) → screen ----
650 {
651 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
652 label: Some("composite_pass"),
653 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
654 view: &output_view,
655 resolve_target: None,
656 ops: wgpu::Operations {
657 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
658 store: wgpu::StoreOp::Store,
659 },
660 })],
661 depth_stencil_attachment: None,
662 timestamp_writes: None,
663 occlusion_query_set: None,
664 });
665 pass.set_pipeline(&self.composite_pipeline);
666 pass.set_bind_group(0, &self.composite_bind_group, &[]);
667 pass.draw(0..3, 0..1);
668 }
669
670 self.queue.submit(std::iter::once(encoder.finish()));
671 output.present();
672
673 Ok(())
674 }
675}