diff --git a/Cargo.lock b/Cargo.lock index 4a9afd4..d984210 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -603,7 +603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -618,6 +618,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "find-msvc-tools" version = "0.1.5" @@ -1381,7 +1390,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1812,6 +1821,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -2095,7 +2117,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2262,6 +2284,7 @@ dependencies = [ "env_logger", "futures", "ouroboros", + "png", "shared", "spirv-builder", "wgpu", @@ -2524,7 +2547,7 @@ dependencies = [ "getrandom", "once_cell", "rustix 1.1.2", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3240,7 +3263,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a2b349a..fce835f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" default = ["use-compiled-tools"] use-installed-tools = ["spirv-builder/use-installed-tools"] use-compiled-tools = ["spirv-builder/use-compiled-tools"] +headless = ["png"] [dependencies] shared = { path = "shared" } @@ -24,6 +25,7 @@ winit = "0.30.12" bytemuck = "1.20.0" env_logger = "0.11.6" ouroboros = "0.18.5" +png = { version = "0.17", optional = true } [build-dependencies] spirv-builder.workspace = true diff --git a/src/headless.rs b/src/headless.rs new file mode 100644 index 0000000..a622a80 --- /dev/null +++ b/src/headless.rs @@ -0,0 +1,205 @@ +use futures::executor::block_on; +use shared::ShaderConstants; +use std::error::Error; +use wgpu::{self, InstanceDescriptor}; +use wgpu::{include_spirv, include_spirv_raw}; + +pub fn run() -> Result<(), Box> { + let width = 1280u32; + let height = 720u32; + + let instance = wgpu::Instance::new(&InstanceDescriptor::default()); + let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: None, + force_fallback_adapter: false, + }))?; + + let mut required_features = wgpu::Features::PUSH_CONSTANTS; + if adapter + .features() + .contains(wgpu::Features::SPIRV_SHADER_PASSTHROUGH) + { + required_features |= wgpu::Features::SPIRV_SHADER_PASSTHROUGH; + } + + let required_limits = wgpu::Limits { + max_push_constant_size: std::mem::size_of::() as u32, + ..Default::default() + }; + let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor { + label: None, + required_features, + required_limits, + ..Default::default() + }))?; + + let shader_module = if device + .features() + .contains(wgpu::Features::SPIRV_SHADER_PASSTHROUGH) + { + let x = include_spirv_raw!(env!("shadertoys_shaders.spv")); + unsafe { device.create_shader_module_passthrough(x) } + } else { + device.create_shader_module(include_spirv!(env!("shadertoys_shaders.spv"))) + }; + + let texture_format = wgpu::TextureFormat::Rgba8UnormSrgb; + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[], + push_constant_ranges: &[wgpu::PushConstantRange { + stages: wgpu::ShaderStages::VERTEX_FRAGMENT, + range: 0..std::mem::size_of::() as u32, + }], + }); + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader_module, + entry_point: Some("main_vs"), + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: Some("main_fs"), + targets: &[Some(wgpu::ColorTargetState { + format: texture_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("headless render target"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: texture_format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let bytes_per_pixel = 4u32; + let unpadded_bytes_per_row = width * bytes_per_pixel; + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align; + let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("headless output buffer"), + size: (padded_bytes_per_row * height) as u64, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + let push_constants = ShaderConstants { + width, + height, + time: 0.0, + cursor_x: 0.0, + cursor_y: 0.0, + drag_start_x: 0.0, + drag_start_y: 0.0, + drag_end_x: 0.0, + drag_end_y: 0.0, + mouse_left_pressed: 0, + mouse_left_clicked: 0, + }; + + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &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, + }); + rpass.set_viewport(0.0, 0.0, width as f32, height as f32, 0.0, 1.0); + rpass.set_pipeline(&render_pipeline); + rpass.set_push_constants( + wgpu::ShaderStages::VERTEX_FRAGMENT, + 0, + bytemuck::bytes_of(&push_constants), + ); + rpass.draw(0..3, 0..1); + } + encoder.copy_texture_to_buffer( + wgpu::TexelCopyTextureInfo { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyBufferInfo { + buffer: &output_buffer, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(padded_bytes_per_row), + rows_per_image: Some(height), + }, + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + queue.submit(Some(encoder.finish())); + + let buffer_slice = output_buffer.slice(..); + let (tx, rx) = std::sync::mpsc::channel(); + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + tx.send(result).unwrap(); + }); + device.poll(wgpu::PollType::Wait)?; + rx.recv()??; + + let data = buffer_slice.get_mapped_range(); + let output_path = "output.png"; + let file = std::fs::File::create(output_path)?; + let w = &mut std::io::BufWriter::new(file); + let mut encoder = png::Encoder::new(w, width, height); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + encoder.set_source_srgb(png::SrgbRenderingIntent::Perceptual); + let mut writer = encoder.write_header()?; + // Remove row padding + let mut unpadded = Vec::with_capacity((unpadded_bytes_per_row * height) as usize); + for row in 0..height as usize { + let start = row * padded_bytes_per_row as usize; + let end = start + unpadded_bytes_per_row as usize; + unpadded.extend_from_slice(&data[start..end]); + } + writer.write_image_data(&unpadded)?; + drop(data); + output_buffer.unmap(); + + eprintln!("Rendered frame to {output_path}"); + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index fa3e53d..065df30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,361 +1,19 @@ -use futures::executor::block_on; -use ouroboros::self_referencing; -use shared::ShaderConstants; -use std::error::Error; -use std::time::Instant; -use wgpu::{self, InstanceDescriptor}; -use wgpu::{include_spirv, include_spirv_raw}; -use winit::application::ApplicationHandler; -use winit::dpi::LogicalSize; -use winit::event::{ElementState, MouseButton, WindowEvent}; -use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; -use winit::keyboard::NamedKey; -use winit::window::{Window, WindowAttributes, WindowId}; - -#[self_referencing] -struct WindowSurface { - window: Box, - #[borrows(window)] - #[covariant] - surface: wgpu::Surface<'this>, -} - -struct ShaderToyApp { - device: Option, - queue: Option, - window_surface: Option, - config: Option, - render_pipeline: Option, - shader_module: Option, - close_requested: bool, - start: Instant, - // Mouse state. - cursor_x: f32, - cursor_y: f32, - drag_start_x: f32, - drag_start_y: f32, - drag_end_x: f32, - drag_end_y: f32, - mouse_left_pressed: bool, - mouse_left_clicked: bool, -} - -impl Default for ShaderToyApp { - fn default() -> Self { - Self { - device: None, - queue: None, - window_surface: None, - config: None, - render_pipeline: None, - shader_module: None, - close_requested: false, - start: Instant::now(), - cursor_x: 0.0, - cursor_y: 0.0, - drag_start_x: 0.0, - drag_start_y: 0.0, - drag_end_x: 0.0, - drag_end_y: 0.0, - mouse_left_pressed: false, - mouse_left_clicked: false, - } - } -} - -impl ShaderToyApp { - async fn init(&mut self, event_loop: &ActiveEventLoop) -> Result<(), Box> { - let window_attributes = WindowAttributes::default() - .with_title("Rust GPU - wgpu") - .with_inner_size(LogicalSize::new(1280.0, 720.0)); - let window_box = event_loop.create_window(window_attributes)?; - let mut instance_flags = wgpu::InstanceFlags::default(); - // Turn off validation as the shaders are trusted. - instance_flags.remove(wgpu::InstanceFlags::VALIDATION); - // Disable debugging info to speed things up. - instance_flags.remove(wgpu::InstanceFlags::DEBUG); - let instance = wgpu::Instance::new(&InstanceDescriptor { - flags: instance_flags, - ..Default::default() - }); - - let window_surface = WindowSurfaceBuilder { - window: Box::new(window_box), - surface_builder: |window| { - instance - .create_surface(window) - .expect("Failed to create surface") - }, - } - .build(); - - let window_size = window_surface.borrow_window().inner_size(); - let surface = window_surface.borrow_surface(); +#[cfg(feature = "headless")] +mod headless; - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: Some(surface), - force_fallback_adapter: false, - }) - .await?; - let mut required_features = wgpu::Features::PUSH_CONSTANTS; - if adapter - .features() - .contains(wgpu::Features::SPIRV_SHADER_PASSTHROUGH) - { - required_features |= wgpu::Features::SPIRV_SHADER_PASSTHROUGH; - } +#[cfg(not(feature = "headless"))] +mod windowed; - const MAX_PUSH_CONSTANT_SIZE: u32 = { - let v = size_of::(); - // Not sure if this is portable on Metal or DX12 in the first place... - assert!( - v <= 128, - "Push constant larger than the minimum that Vulkan requires, may not be portable!" - ); - v as u32 - }; - let required_limits = wgpu::Limits { - max_push_constant_size: MAX_PUSH_CONSTANT_SIZE, - ..Default::default() - }; - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor { - label: None, - required_features, - required_limits, - ..Default::default() - }) - .await?; - let shader_module = if device - .features() - .contains(wgpu::Features::SPIRV_SHADER_PASSTHROUGH) - { - let x = include_spirv_raw!(env!("shadertoys_shaders.spv")); - unsafe { device.create_shader_module_passthrough(x) } - } else { - device.create_shader_module(include_spirv!(env!("shadertoys_shaders.spv"))) - }; - let swapchain_format = surface.get_capabilities(&adapter).formats[0]; - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: None, - bind_group_layouts: &[], - push_constant_ranges: &[wgpu::PushConstantRange { - stages: wgpu::ShaderStages::VERTEX_FRAGMENT, - range: 0..std::mem::size_of::() as u32, - }], - }); - let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: None, - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &shader_module, - entry_point: Some("main_vs"), - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader_module, - entry_point: Some("main_fs"), - targets: &[Some(wgpu::ColorTargetState { - format: swapchain_format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - cache: None, - }); - let config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: swapchain_format, - width: window_size.width, - height: window_size.height, - present_mode: wgpu::PresentMode::Fifo, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![], - desired_maximum_frame_latency: Default::default(), - }; - surface.configure(&device, &config); - - self.device = Some(device); - self.queue = Some(queue); - self.window_surface = Some(window_surface); - self.config = Some(config); - self.render_pipeline = Some(render_pipeline); - self.shader_module = Some(shader_module); - self.start = Instant::now(); - Ok(()) - } - - fn render(&mut self) { - let window_surface = match &self.window_surface { - Some(ws) => ws, - None => return, - }; - - let window = window_surface.borrow_window(); - let current_size = window.inner_size(); - let surface = window_surface.borrow_surface(); - let device = self.device.as_ref().unwrap(); - let queue = self.queue.as_ref().unwrap(); - let frame = match surface.get_current_texture() { - Ok(frame) => frame, - Err(e) => { - eprintln!("Failed to acquire texture: {e:?}"); - return; - } - }; - let view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - { - let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &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, - }); - rpass.set_viewport( - 0.0, - 0.0, - current_size.width as f32, - current_size.height as f32, - 0.0, - 1.0, - ); - let push_constants = shared::ShaderConstants { - width: current_size.width, - height: current_size.height, - time: self.start.elapsed().as_secs_f32(), - cursor_x: self.cursor_x, - cursor_y: self.cursor_y, - drag_start_x: self.drag_start_x, - drag_start_y: self.drag_start_y, - drag_end_x: self.drag_end_x, - drag_end_y: self.drag_end_y, - mouse_left_pressed: self.mouse_left_pressed as u32, - mouse_left_clicked: self.mouse_left_clicked as u32, - }; - self.mouse_left_clicked = false; - rpass.set_pipeline(self.render_pipeline.as_ref().unwrap()); - rpass.set_push_constants( - wgpu::ShaderStages::VERTEX_FRAGMENT, - 0, - bytemuck::bytes_of(&push_constants), - ); - rpass.draw(0..3, 0..1); - } - queue.submit(Some(encoder.finish())); - frame.present(); - } -} - -impl ApplicationHandler for ShaderToyApp { - fn resumed(&mut self, event_loop: &ActiveEventLoop) { - if let Err(e) = block_on(self.init(event_loop)) { - eprintln!("Initialization error: {e}"); - event_loop.exit(); - } - } - - fn window_event( - &mut self, - event_loop: &ActiveEventLoop, - _window_id: WindowId, - event: WindowEvent, - ) { - match event { - WindowEvent::CloseRequested => self.close_requested = true, - WindowEvent::Resized(new_size) => { - if let Some(config) = self.config.as_mut() { - config.width = new_size.width; - config.height = new_size.height; - if let Some(ws) = &self.window_surface { - let surface = ws.borrow_surface(); - if let Some(device) = self.device.as_ref() { - surface.configure(device, config); - } - } - } - } - WindowEvent::CursorMoved { position, .. } => { - self.cursor_x = position.x as f32; - self.cursor_y = position.y as f32; - if self.mouse_left_pressed { - self.drag_end_x = self.cursor_x; - self.drag_end_y = self.cursor_y; - } - } - WindowEvent::MouseInput { - state, - button: MouseButton::Left, - .. - } => { - self.mouse_left_pressed = state == ElementState::Pressed; - if self.mouse_left_pressed { - self.drag_start_x = self.cursor_x; - self.drag_start_y = self.cursor_y; - self.drag_end_x = self.cursor_x; - self.drag_end_y = self.cursor_y; - self.mouse_left_clicked = true; - } - } - WindowEvent::MouseWheel { - delta: winit::event::MouseScrollDelta::LineDelta(x, y), - .. - } => { - self.drag_end_x = x * 0.1; - self.drag_end_y = y * 0.1; - } - WindowEvent::KeyboardInput { event, .. } - if event.logical_key == NamedKey::Escape - && event.state == ElementState::Pressed => - { - self.close_requested = true; - } - WindowEvent::RedrawRequested => self.render(), - _ => {} - } - if self.close_requested { - event_loop.exit(); - } else if let Some(ws) = &self.window_surface { - ws.borrow_window().request_redraw(); - } - event_loop.set_control_flow(ControlFlow::Poll); - } +use std::error::Error; - fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { - if self.close_requested { - event_loop.exit(); - } else if let Some(ws) = &self.window_surface { - ws.borrow_window().request_redraw(); - } - event_loop.set_control_flow(ControlFlow::Poll); - } +#[cfg(feature = "headless")] +fn main() -> Result<(), Box> { + env_logger::init(); + headless::run() } +#[cfg(not(feature = "headless"))] fn main() -> Result<(), Box> { env_logger::init(); - let event_loop = EventLoop::new()?; - let mut app = ShaderToyApp::default(); - event_loop.run_app(&mut app).map_err(Into::into) + windowed::run() } diff --git a/src/windowed.rs b/src/windowed.rs new file mode 100644 index 0000000..3a7ef6b --- /dev/null +++ b/src/windowed.rs @@ -0,0 +1,360 @@ +use futures::executor::block_on; +use ouroboros::self_referencing; +use shared::ShaderConstants; +use std::error::Error; +use std::time::Instant; +use wgpu::{self, InstanceDescriptor}; +use wgpu::{include_spirv, include_spirv_raw}; +use winit::application::ApplicationHandler; +use winit::dpi::LogicalSize; +use winit::event::{ElementState, MouseButton, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::keyboard::NamedKey; +use winit::window::{Window, WindowAttributes, WindowId}; + +#[self_referencing] +struct WindowSurface { + window: Box, + #[borrows(window)] + #[covariant] + surface: wgpu::Surface<'this>, +} + +struct ShaderToyApp { + device: Option, + queue: Option, + window_surface: Option, + config: Option, + render_pipeline: Option, + shader_module: Option, + close_requested: bool, + start: Instant, + // Mouse state. + cursor_x: f32, + cursor_y: f32, + drag_start_x: f32, + drag_start_y: f32, + drag_end_x: f32, + drag_end_y: f32, + mouse_left_pressed: bool, + mouse_left_clicked: bool, +} + +impl Default for ShaderToyApp { + fn default() -> Self { + Self { + device: None, + queue: None, + window_surface: None, + config: None, + render_pipeline: None, + shader_module: None, + close_requested: false, + start: Instant::now(), + cursor_x: 0.0, + cursor_y: 0.0, + drag_start_x: 0.0, + drag_start_y: 0.0, + drag_end_x: 0.0, + drag_end_y: 0.0, + mouse_left_pressed: false, + mouse_left_clicked: false, + } + } +} + +impl ShaderToyApp { + async fn init(&mut self, event_loop: &ActiveEventLoop) -> Result<(), Box> { + let window_attributes = WindowAttributes::default() + .with_title("Rust GPU - wgpu") + .with_inner_size(LogicalSize::new(1280.0, 720.0)); + let window_box = event_loop.create_window(window_attributes)?; + let mut instance_flags = wgpu::InstanceFlags::default(); + // Turn off validation as the shaders are trusted. + instance_flags.remove(wgpu::InstanceFlags::VALIDATION); + // Disable debugging info to speed things up. + instance_flags.remove(wgpu::InstanceFlags::DEBUG); + let instance = wgpu::Instance::new(&InstanceDescriptor { + flags: instance_flags, + ..Default::default() + }); + + let window_surface = WindowSurfaceBuilder { + window: Box::new(window_box), + surface_builder: |window| { + instance + .create_surface(window) + .expect("Failed to create surface") + }, + } + .build(); + + let window_size = window_surface.borrow_window().inner_size(); + let surface = window_surface.borrow_surface(); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(surface), + force_fallback_adapter: false, + }) + .await?; + let mut required_features = wgpu::Features::PUSH_CONSTANTS; + if adapter + .features() + .contains(wgpu::Features::SPIRV_SHADER_PASSTHROUGH) + { + required_features |= wgpu::Features::SPIRV_SHADER_PASSTHROUGH; + } + + const MAX_PUSH_CONSTANT_SIZE: u32 = { + let v = size_of::(); + // Not sure if this is portable on Metal or DX12 in the first place... + assert!( + v <= 128, + "Push constant larger than the minimum that Vulkan requires, may not be portable!" + ); + v as u32 + }; + let required_limits = wgpu::Limits { + max_push_constant_size: MAX_PUSH_CONSTANT_SIZE, + ..Default::default() + }; + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + label: None, + required_features, + required_limits, + ..Default::default() + }) + .await?; + let shader_module = if device + .features() + .contains(wgpu::Features::SPIRV_SHADER_PASSTHROUGH) + { + let x = include_spirv_raw!(env!("shadertoys_shaders.spv")); + unsafe { device.create_shader_module_passthrough(x) } + } else { + device.create_shader_module(include_spirv!(env!("shadertoys_shaders.spv"))) + }; + let swapchain_format = surface.get_capabilities(&adapter).formats[0]; + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[], + push_constant_ranges: &[wgpu::PushConstantRange { + stages: wgpu::ShaderStages::VERTEX_FRAGMENT, + range: 0..std::mem::size_of::() as u32, + }], + }); + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader_module, + entry_point: Some("main_vs"), + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: Some("main_fs"), + targets: &[Some(wgpu::ColorTargetState { + format: swapchain_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: window_size.width, + height: window_size.height, + present_mode: wgpu::PresentMode::Fifo, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![], + desired_maximum_frame_latency: Default::default(), + }; + surface.configure(&device, &config); + + self.device = Some(device); + self.queue = Some(queue); + self.window_surface = Some(window_surface); + self.config = Some(config); + self.render_pipeline = Some(render_pipeline); + self.shader_module = Some(shader_module); + self.start = Instant::now(); + Ok(()) + } + + fn render(&mut self) { + let window_surface = match &self.window_surface { + Some(ws) => ws, + None => return, + }; + + let window = window_surface.borrow_window(); + let current_size = window.inner_size(); + let surface = window_surface.borrow_surface(); + let device = self.device.as_ref().unwrap(); + let queue = self.queue.as_ref().unwrap(); + let frame = match surface.get_current_texture() { + Ok(frame) => frame, + Err(e) => { + eprintln!("Failed to acquire texture: {e:?}"); + return; + } + }; + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &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, + }); + rpass.set_viewport( + 0.0, + 0.0, + current_size.width as f32, + current_size.height as f32, + 0.0, + 1.0, + ); + let push_constants = shared::ShaderConstants { + width: current_size.width, + height: current_size.height, + time: self.start.elapsed().as_secs_f32(), + cursor_x: self.cursor_x, + cursor_y: self.cursor_y, + drag_start_x: self.drag_start_x, + drag_start_y: self.drag_start_y, + drag_end_x: self.drag_end_x, + drag_end_y: self.drag_end_y, + mouse_left_pressed: self.mouse_left_pressed as u32, + mouse_left_clicked: self.mouse_left_clicked as u32, + }; + self.mouse_left_clicked = false; + rpass.set_pipeline(self.render_pipeline.as_ref().unwrap()); + rpass.set_push_constants( + wgpu::ShaderStages::VERTEX_FRAGMENT, + 0, + bytemuck::bytes_of(&push_constants), + ); + rpass.draw(0..3, 0..1); + } + queue.submit(Some(encoder.finish())); + frame.present(); + } +} + +impl ApplicationHandler for ShaderToyApp { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + if let Err(e) = block_on(self.init(event_loop)) { + eprintln!("Initialization error: {e}"); + event_loop.exit(); + } + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + match event { + WindowEvent::CloseRequested => self.close_requested = true, + WindowEvent::Resized(new_size) => { + if let Some(config) = self.config.as_mut() { + config.width = new_size.width; + config.height = new_size.height; + if let Some(ws) = &self.window_surface { + let surface = ws.borrow_surface(); + if let Some(device) = self.device.as_ref() { + surface.configure(device, config); + } + } + } + } + WindowEvent::CursorMoved { position, .. } => { + self.cursor_x = position.x as f32; + self.cursor_y = position.y as f32; + if self.mouse_left_pressed { + self.drag_end_x = self.cursor_x; + self.drag_end_y = self.cursor_y; + } + } + WindowEvent::MouseInput { + state, + button: MouseButton::Left, + .. + } => { + self.mouse_left_pressed = state == ElementState::Pressed; + if self.mouse_left_pressed { + self.drag_start_x = self.cursor_x; + self.drag_start_y = self.cursor_y; + self.drag_end_x = self.cursor_x; + self.drag_end_y = self.cursor_y; + self.mouse_left_clicked = true; + } + } + WindowEvent::MouseWheel { + delta: winit::event::MouseScrollDelta::LineDelta(x, y), + .. + } => { + self.drag_end_x = x * 0.1; + self.drag_end_y = y * 0.1; + } + WindowEvent::KeyboardInput { event, .. } + if event.logical_key == NamedKey::Escape + && event.state == ElementState::Pressed => + { + self.close_requested = true; + } + WindowEvent::RedrawRequested => self.render(), + _ => {} + } + if self.close_requested { + event_loop.exit(); + } else if let Some(ws) = &self.window_surface { + ws.borrow_window().request_redraw(); + } + event_loop.set_control_flow(ControlFlow::Poll); + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if self.close_requested { + event_loop.exit(); + } else if let Some(ws) = &self.window_surface { + ws.borrow_window().request_redraw(); + } + event_loop.set_control_flow(ControlFlow::Poll); + } +} + +pub fn run() -> Result<(), Box> { + let event_loop = EventLoop::new()?; + let mut app = ShaderToyApp::default(); + event_loop.run_app(&mut app).map_err(Into::into) +}