a deliberately stupid space heater that wastes electricity on fire shaders and prime numbers
at main 675 lines 25 kB view raw
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}