Something went wrong. Try again.
a deliberately stupid space heater that wastes electricity on fire shaders and prime numbers
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676use std::borrow::Cow;use std::collections::HashMap;use std::sync::Arc;use std::time::Instant;
use winit::dpi::PhysicalSize;use winit::window::Window;
use crate::shadertoy_adapter;
// Uniform buffer matching the shader's Uniforms struct#[repr(C)]#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]pub struct Uniforms { pub time: f32, pub _pad0: f32, pub resolution: [f32; 2], pub intensity: f32, pub _pad1: [f32; 3],}
fn create_intermediate_texture( device: &wgpu::Device, width: u32, height: u32, label: &str,) -> (wgpu::Texture, wgpu::TextureView) { let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some(label), size: wgpu::Extent3d { width: width.max(1), height: height.max(1), depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba16Float, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); (texture, view)}
pub struct GpuState { surface: wgpu::Surface<'static>, device: wgpu::Device, queue: wgpu::Queue, config: wgpu::SurfaceConfiguration,
// Pipelines fire_pipeline: wgpu::RenderPipeline, blur_pipeline: wgpu::RenderPipeline, composite_pipeline: wgpu::RenderPipeline,
// Intermediate textures fire_texture: wgpu::Texture, fire_view: wgpu::TextureView, blur_texture: wgpu::Texture, blur_view: wgpu::TextureView,
// Shared resources sampler: wgpu::Sampler, uniform_buffer: wgpu::Buffer,
// Bind group layouts (needed for recreating bind groups on resize) #[allow(dead_code)] fire_bg_layout: wgpu::BindGroupLayout, blur_bg_layout: wgpu::BindGroupLayout, composite_bg_layout: wgpu::BindGroupLayout,
// Bind groups fire_bind_group: wgpu::BindGroup, blur_bind_group: wgpu::BindGroup, composite_bind_group: wgpu::BindGroup,
start_time: Instant, pub size: PhysicalSize<u32>,}
impl GpuState { fn create_blur_bind_group( device: &wgpu::Device, layout: &wgpu::BindGroupLayout, uniform_buffer: &wgpu::Buffer, fire_view: &wgpu::TextureView, sampler: &wgpu::Sampler, ) -> wgpu::BindGroup { device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("blur_bind_group"), layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: uniform_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(fire_view), }, wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(sampler), }, ], }) }
fn create_composite_bind_group( device: &wgpu::Device, layout: &wgpu::BindGroupLayout, uniform_buffer: &wgpu::Buffer, fire_view: &wgpu::TextureView, blur_view: &wgpu::TextureView, sampler: &wgpu::Sampler, ) -> wgpu::BindGroup { device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("composite_bind_group"), layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: uniform_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(fire_view), }, wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(blur_view), }, wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(sampler), }, ], }) }
pub async fn new(window: Arc<Window>) -> Self { let size = window.inner_size();
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { backends: wgpu::Backends::all(), ..Default::default() });
let surface = instance.create_surface(window.clone()).unwrap();
let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface: Some(&surface), force_fallback_adapter: false, }) .await .expect("Failed to find a suitable GPU adapter");
log::info!("Using GPU: {:?}", adapter.get_info().name);
let (device, queue) = adapter .request_device( &wgpu::DeviceDescriptor { label: Some("heatslop_device"), required_features: wgpu::Features::empty(), required_limits: wgpu::Limits::default(), ..Default::default() }, None, ) .await .expect("Failed to create device");
let surface_caps = surface.get_capabilities(&adapter); let surface_format = surface_caps .formats .iter() .find(|f| f.is_srgb()) .copied() .unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface_format, width: size.width.max(1), height: size.height.max(1), present_mode: wgpu::PresentMode::AutoNoVsync, alpha_mode: surface_caps.alpha_modes[0], view_formats: vec![], desired_maximum_frame_latency: 2, }; surface.configure(&device, &config);
// ---- Shaders ---- // Vertex shader: single WGSL module, override constant controls Y-flip let vert = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("vert"), source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!( "shaders/fullscreen.wgsl" ))), }); let flip_off = HashMap::from([("0".to_string(), 0.0_f64)]); let flip_on = HashMap::from([("0".to_string(), 1.0_f64)]);
// Fragment shaders: original Shadertoy code composed via adapter let fire_src = shadertoy_adapter::fire_frag(); let fire_frag = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("fire_frag"), source: wgpu::ShaderSource::Glsl { shader: Cow::Owned(fire_src), stage: naga::ShaderStage::Fragment, defines: Default::default(), }, }); let blur_src = shadertoy_adapter::blur_frag(); let blur_frag = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("blur_frag"), source: wgpu::ShaderSource::Glsl { shader: Cow::Owned(blur_src), stage: naga::ShaderStage::Fragment, defines: Default::default(), }, }); let composite_src = shadertoy_adapter::composite_frag(); let composite_frag = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("composite_frag"), source: wgpu::ShaderSource::Glsl { shader: Cow::Owned(composite_src), stage: naga::ShaderStage::Fragment, defines: Default::default(), }, });
// ---- Uniform buffer ---- let uniforms = Uniforms { time: 0.0, _pad0: 0.0, resolution: [size.width as f32, size.height as f32], intensity: 1.0, _pad1: [0.0; 3], }; let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("uniform_buffer"), size: std::mem::size_of::<Uniforms>() as u64, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); queue.write_buffer(&uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
// ---- Sampler ---- let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("tex_sampler"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, ..Default::default() });
// ---- Intermediate textures ---- let (fire_texture, fire_view) = create_intermediate_texture(&device, size.width, size.height, "fire_texture"); let (blur_texture, blur_view) = create_intermediate_texture(&device, size.width, size.height, "blur_texture");
// ---- Bind group layouts ----
// Fire: just uniforms let fire_bg_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("fire_bg_layout"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }], });
// Blur: uniforms + input texture + sampler let blur_bg_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("blur_bg_layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], });
// Composite: uniforms + fire texture + blur texture + sampler let composite_bg_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("composite_bg_layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 3, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], });
// ---- Bind groups ---- let fire_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("fire_bind_group"), layout: &fire_bg_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform_buffer.as_entire_binding(), }], });
let blur_bind_group = Self::create_blur_bind_group( &device, &blur_bg_layout, &uniform_buffer, &fire_view, &sampler, );
let composite_bind_group = Self::create_composite_bind_group( &device, &composite_bg_layout, &uniform_buffer, &fire_view, &blur_view, &sampler, );
// ---- Pipeline layouts ---- let fire_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("fire_pipeline_layout"), bind_group_layouts: &[&fire_bg_layout], push_constant_ranges: &[], }); let blur_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("blur_pipeline_layout"), bind_group_layouts: &[&blur_bg_layout], push_constant_ranges: &[], }); let composite_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("composite_pipeline_layout"), bind_group_layouts: &[&composite_bg_layout], push_constant_ranges: &[], });
// ---- Render pipelines ----
// Fire pipeline: renders to Rgba16Float intermediate texture let fire_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("fire_pipeline"), layout: Some(&fire_pipeline_layout), vertex: wgpu::VertexState { module: &vert, entry_point: Some("main"), buffers: &[], compilation_options: wgpu::PipelineCompilationOptions { constants: &flip_off, ..Default::default() }, }, fragment: Some(wgpu::FragmentState { module: &fire_frag, entry_point: Some("main"), targets: &[Some(wgpu::ColorTargetState { format: wgpu::TextureFormat::Rgba16Float, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: Default::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, cull_mode: None, ..Default::default() }, depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview: None, cache: None, });
// Blur pipeline: renders to Rgba16Float intermediate texture let blur_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("blur_pipeline"), layout: Some(&blur_pipeline_layout), vertex: wgpu::VertexState { module: &vert, entry_point: Some("main"), buffers: &[], compilation_options: wgpu::PipelineCompilationOptions { constants: &flip_on, ..Default::default() }, }, fragment: Some(wgpu::FragmentState { module: &blur_frag, entry_point: Some("main"), targets: &[Some(wgpu::ColorTargetState { format: wgpu::TextureFormat::Rgba16Float, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: Default::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, cull_mode: None, ..Default::default() }, depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview: None, cache: None, });
// Composite pipeline: renders to swapchain (sRGB surface) let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("composite_pipeline"), layout: Some(&composite_pipeline_layout), vertex: wgpu::VertexState { module: &vert, entry_point: Some("main"), buffers: &[], compilation_options: wgpu::PipelineCompilationOptions { constants: &flip_on, ..Default::default() }, }, fragment: Some(wgpu::FragmentState { module: &composite_frag, entry_point: Some("main"), targets: &[Some(wgpu::ColorTargetState { format: surface_format, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: Default::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, cull_mode: None, ..Default::default() }, depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview: None, cache: None, });
Self { surface, device, queue, config, fire_pipeline, blur_pipeline, composite_pipeline, fire_texture, fire_view, blur_texture, blur_view, sampler, uniform_buffer, fire_bg_layout, blur_bg_layout, composite_bg_layout, fire_bind_group, blur_bind_group, composite_bind_group, start_time: Instant::now(), size, } }
pub fn resize(&mut self, new_size: PhysicalSize<u32>) { if new_size.width > 0 && new_size.height > 0 { self.size = new_size; self.config.width = new_size.width; self.config.height = new_size.height; self.surface.configure(&self.device, &self.config);
// Recreate intermediate textures at new size let (ft, fv) = create_intermediate_texture( &self.device, new_size.width, new_size.height, "fire_texture", ); self.fire_texture = ft; self.fire_view = fv;
let (bt, bv) = create_intermediate_texture( &self.device, new_size.width, new_size.height, "blur_texture", ); self.blur_texture = bt; self.blur_view = bv;
// Recreate bind groups that reference the textures self.blur_bind_group = Self::create_blur_bind_group( &self.device, &self.blur_bg_layout, &self.uniform_buffer, &self.fire_view, &self.sampler, );
self.composite_bind_group = Self::create_composite_bind_group( &self.device, &self.composite_bg_layout, &self.uniform_buffer, &self.fire_view, &self.blur_view, &self.sampler, ); } }
pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> { let elapsed = self.start_time.elapsed().as_secs_f32();
// Update uniforms let uniforms = Uniforms { time: elapsed, _pad0: 0.0, resolution: [self.size.width as f32, self.size.height as f32], intensity: 1.0, _pad1: [0.0; 3], }; self.queue .write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
let output = self.surface.get_current_texture()?; let output_view = output .texture .create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("render_encoder"), });
// ---- Pass 1: Fire generation → fire_texture ---- { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("fire_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &self.fire_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }); pass.set_pipeline(&self.fire_pipeline); pass.set_bind_group(0, &self.fire_bind_group, &[]); pass.draw(0..3, 0..1); }
// ---- Pass 2: Gaussian blur of fire → blur_texture ---- { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("blur_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &self.blur_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }); pass.set_pipeline(&self.blur_pipeline); pass.set_bind_group(0, &self.blur_bind_group, &[]); pass.draw(0..3, 0..1); }
// ---- Pass 3: Composite (fire + bloom) → screen ---- { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("composite_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &output_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }); pass.set_pipeline(&self.composite_pipeline); pass.set_bind_group(0, &self.composite_bind_group, &[]); pass.draw(0..3, 0..1); }
self.queue.submit(std::iter::once(encoder.finish())); output.present();
Ok(()) }}