diff options
| author | s-ol <s+removethis@s-ol.nu> | 2026-04-16 10:24:24 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2026-05-14 14:46:13 +0000 |
| commit | 6a67c23a5029aceea1498f892c3424946b152cdc (patch) | |
| tree | 5d1b2a7283818e8807caaa211d22641b467386ba | |
| parent | initial commit (diff) | |
| download | wgsl-view-6a67c23a5029aceea1498f892c3424946b152cdc.tar.gz wgsl-view-6a67c23a5029aceea1498f892c3424946b152cdc.zip | |
refactor
| -rw-r--r-- | src/main.rs | 500 | ||||
| -rw-r--r-- | src/osc.rs | 140 | ||||
| -rw-r--r-- | src/renderer.rs | 303 | ||||
| -rw-r--r-- | src/uniform.rs | 133 | ||||
| -rw-r--r-- | src/window.rs | 245 | ||||
| -rwxr-xr-x | test.sh | 2 |
6 files changed, 774 insertions, 549 deletions
diff --git a/src/main.rs b/src/main.rs index a024ad3..3ce4ffc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,362 +1,117 @@ -use std::sync::Arc; -use winit::{ - application::ApplicationHandler, - event::WindowEvent, - event_loop::EventLoop, - keyboard::{KeyCode, PhysicalKey}, - window::Window, -}; - mod osc; +mod renderer; mod uniform; +mod window; -const VERTEX_SHADER: &str = "\ -@group(0) @binding(0) var<uniform> _wgsl_resolution: vec2<f32>; +use std::sync::Arc; -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, - @location(0) uv: vec2<f32>, - @location(1) resolution: vec2<f32>, -}; +use osc::{OscCommand, OscServer}; +use renderer::Renderer; +use window::{PreviewWindow, ScaleMode}; -@vertex -fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { - var uv = vec2<f32>(f32(vi % 2u), f32(vi / 2u)); - var out: VertexOutput; - out.clip_position = vec4<f32>(2.0 * uv - 1.0, 0.0, 1.0); - out.uv = vec2<f32>(uv.x, 1.0 - uv.y); - out.resolution = _wgsl_resolution; - return out; -} -"; +use winit::application::ApplicationHandler; +use winit::event::{ElementState, KeyEvent, WindowEvent}; +use winit::event_loop::EventLoop; +use winit::keyboard::{Key, NamedKey}; -const DEFAULT_FRAGMENT: &str = "\ -@fragment -fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> { - let check = floor(uv * 10.0); - return vec4<f32>(vec3<f32>(fract((check.x + check.y) / 2.0)), 1.0); -} -"; - -struct GpuState { - window: Arc<Window>, - surface: wgpu::Surface<'static>, +struct AppState { device: wgpu::Device, queue: wgpu::Queue, - config: wgpu::SurfaceConfiguration, - surface_format: wgpu::TextureFormat, - vertex_module: wgpu::ShaderModule, - vertex_bind_group_layout: wgpu::BindGroupLayout, - vertex_bind_group: wgpu::BindGroup, - resolution_buffer: wgpu::Buffer, - render_pipeline: wgpu::RenderPipeline, - uniform_cache: uniform::UniformCache, + renderer: Renderer, + preview: PreviewWindow, } struct App { - gpu: Option<GpuState>, - osc_server: osc::OscServer, -} - -fn load_shader( - device: &wgpu::Device, - surface_format: wgpu::TextureFormat, - vertex_module: &wgpu::ShaderModule, - vertex_bgl: &wgpu::BindGroupLayout, - fragment_source: &str, - cache: &mut uniform::UniformCache, -) -> Result<wgpu::RenderPipeline, String> { - let module = naga::front::wgsl::parse_str(fragment_source) - .map_err(|e| format!("WGSL parse error: {}", e))?; - - naga::valid::Validator::new( - naga::valid::ValidationFlags::all(), - naga::valid::Capabilities::all(), - ) - .validate(&module) - .map_err(|e| format!("WGSL validation error: {}", e))?; - - cache.refresh(module, device); - - let fragment_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("fragment_shader"), - source: wgpu::ShaderSource::Wgsl(fragment_source.into()), - }); - - let mut bind_group_layouts: Vec<Option<&wgpu::BindGroupLayout>> = vec![Some(vertex_bgl)]; - if let Some(ref bgl) = cache.bind_group_layout { - bind_group_layouts.push(Some(bgl)); - } - - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("pipeline_layout"), - bind_group_layouts: &bind_group_layouts, - immediate_size: 0, - }); - - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("render_pipeline"), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: vertex_module, - entry_point: Some("vs_main"), - buffers: &[], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &fragment_module, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleStrip, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview_mask: None, - cache: None, - }); - - Ok(pipeline) + width: u32, + height: u32, + scale_mode: ScaleMode, + osc: OscServer, + state: Option<AppState>, } impl App { - fn new() -> Self { - let osc_server = osc::OscServer::new("0.0.0.0:9000").expect("bind OSC socket"); - Self { - gpu: None, - osc_server, - } - } - - fn init_gpu(&mut self, window: Arc<Window>) { - let gpu = pollster::block_on(async { - let size = window.inner_size(); - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::VULKAN, - flags: wgpu::InstanceFlags::default(), - memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), - backend_options: wgpu::BackendOptions::default(), - display: None, - }); - - let surface = instance - .create_surface(window.clone()) - .expect("create surface"); - - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - compatible_surface: Some(&surface), - force_fallback_adapter: false, - }) - .await - .expect("find adapter"); - - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor::default()) - .await - .expect("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::Fifo, - alpha_mode: surface_caps.alpha_modes[0], - view_formats: vec![], - desired_maximum_frame_latency: 2, - }; - surface.configure(&device, &config); - - let mut cache = uniform::UniformCache::new(); - let vertex_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("vertex_shader"), - source: wgpu::ShaderSource::Wgsl(VERTEX_SHADER.into()), - }); - - let resolution_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("resolution_buffer"), - size: 8, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let vertex_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("vertex_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - }); - - let vertex_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("vertex_bind_group"), - layout: &vertex_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: resolution_buffer.as_entire_binding(), - }], - }); - - let pipeline = load_shader( - &device, - surface_format, - &vertex_module, - &vertex_bind_group_layout, - DEFAULT_FRAGMENT, - &mut cache, - ) - .expect("default shader should compile"); - - GpuState { - window, - surface, - device, - queue, - config, - surface_format, - vertex_module, - vertex_bind_group_layout, - vertex_bind_group, - resolution_buffer, - render_pipeline: pipeline, - uniform_cache: cache, - } - }); - - self.gpu = Some(gpu); - } - fn handle_osc(&mut self) { - let gpu = self.gpu.as_mut().expect("gpu initialized"); - - let commands = self.osc_server.poll(&mut gpu.uniform_cache); - for cmd in commands { - match cmd { - osc::OscCommand::Shader(code) => { - match load_shader( - &gpu.device, - gpu.surface_format, - &gpu.vertex_module, - &gpu.vertex_bind_group_layout, - &code, - &mut gpu.uniform_cache, - ) { - Ok(pipeline) => { - gpu.render_pipeline = pipeline; - log::info!("shader loaded successfully"); - } - Err(e) => log::error!("{}", e), - } + let state = self.state.as_mut().expect("gpu not initialized"); + self.osc.poll(|cmd| match cmd { + OscCommand::Shader(source) => { + if let Err(e) = state.renderer.load_shader(&state.device, &source) { + log::error!("shader error: {}", e); } } - } - } - - fn render(&mut self) { - let gpu = self.gpu.as_mut().expect("gpu initialized"); - - gpu.uniform_cache.flush(&gpu.queue); - - let output = match gpu.surface.get_current_texture() { - wgpu::CurrentSurfaceTexture::Success(t) - | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t, - wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => { - gpu.surface.configure(&gpu.device, &gpu.config); - return; - } - other => { - log::error!("surface error: {:?}", other); - return; - } - }; - - let view = output - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - let mut encoder = gpu - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("render_encoder"), - }); - - { - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("render_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - depth_slice: None, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - - pass.set_pipeline(&gpu.render_pipeline); - let resolution = [gpu.config.width as f32, gpu.config.height as f32]; - gpu.queue - .write_buffer(&gpu.resolution_buffer, 0, bytemuck::cast_slice(&resolution)); - pass.set_bind_group(0, &gpu.vertex_bind_group, &[]); - if let Some(ref bg) = gpu.uniform_cache.bind_group { - pass.set_bind_group(1, bg, &[]); + OscCommand::Uniform { path, args } => { + if let Err(e) = state.renderer.set_uniform(&path, &args) { + log::warn!("uniform error: {}", e); + } } - pass.draw(0..4, 0..1); - } - - gpu.queue.submit(std::iter::once(encoder.finish())); - output.present(); + }); } } impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - if self.gpu.is_none() { - let window = Arc::new( - event_loop - .create_window(Window::default_attributes().with_title("wgsl-view")) - .expect("create window"), - ); - self.init_gpu(window); - } + let window = Arc::new( + event_loop + .create_window( + winit::window::Window::default_attributes() + .with_title("wgsl-view") + .with_inner_size(winit::dpi::LogicalSize::new(self.width, self.height)), + ) + .expect("create window"), + ); + + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::VULKAN, + flags: wgpu::InstanceFlags::default(), + backend_options: wgpu::BackendOptions::default(), + memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), + display: None, + }); + + let surface = instance + .create_surface(window.clone()) + .expect("create surface"); + + let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + })) + .expect("find adapter"); + + let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { + label: Some("device"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + ..Default::default() + })) + .expect("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 renderer = Renderer::new(&device, &queue, self.width, self.height); + let preview = PreviewWindow::new( + &device, + window, + surface, + surface_format, + renderer.canvas_view(), + self.width, + self.height, + self.scale_mode, + ); + + self.state = Some(AppState { + device, + queue, + renderer, + preview, + }); } fn window_event( @@ -365,44 +120,85 @@ impl ApplicationHandler for App { _window_id: winit::window::WindowId, event: WindowEvent, ) { + let state = self.state.as_mut().expect("gpu not initialized"); + match event { WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::KeyboardInput { event: - winit::event::KeyEvent { - physical_key: PhysicalKey::Code(KeyCode::Escape), - state: winit::event::ElementState::Pressed, + KeyEvent { + logical_key, + state: ElementState::Pressed, .. }, .. - } => event_loop.exit(), + } => match logical_key { + Key::Named(NamedKey::Escape) => event_loop.exit(), + Key::Character(ref c) if c.as_str() == "c" => { + state.preview.scale_mode = state.preview.scale_mode.cycle(); + log::info!("scale mode: {:?}", state.preview.scale_mode); + } + _ => {} + }, WindowEvent::Resized(size) => { - let gpu = self.gpu.as_mut().expect("gpu initialized"); - gpu.config.width = size.width.max(1); - gpu.config.height = size.height.max(1); - gpu.surface.configure(&gpu.device, &gpu.config); + state.preview.resize(&state.device, size.width, size.height); } WindowEvent::RedrawRequested => { self.handle_osc(); - self.render(); + let state = self.state.as_mut().expect("gpu not initialized"); + state.renderer.render(&state.device, &state.queue); + state.preview.draw(&state.device, &state.queue); } _ => {} } } fn about_to_wait(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) { - self.gpu - .as_ref() - .expect("gpu initialized") - .window - .request_redraw(); + if let Some(ref state) = self.state { + state.preview.window().request_redraw(); + } } } fn main() { env_logger::init(); + + let mut args = std::env::args().skip(1); + let mut width = 1920u32; + let mut height = 1080u32; + let mut scale_mode = ScaleMode::Contain; + let mut port = 9000u16; + + while let Some(arg) = args.next() { + match arg.as_str() { + "--width" => width = args.next().expect("--width VALUE").parse().expect("u32"), + "--height" => height = args.next().expect("--height VALUE").parse().expect("u32"), + "--port" => port = args.next().expect("--port VALUE").parse().expect("u16"), + "--scale" => { + scale_mode = match args.next().expect("--scale VALUE").as_str() { + "contain" => ScaleMode::Contain, + "cover" => ScaleMode::Cover, + "center" => ScaleMode::Center, + "natural" => ScaleMode::Natural, + s => panic!("unknown scale mode: {}", s), + } + } + other => panic!("unknown argument: {}", other), + } + } + + let osc = OscServer::new(&format!("0.0.0.0:{}", port)).expect("bind OSC socket"); + let event_loop = EventLoop::new().expect("create event loop"); event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); - let mut app = App::new(); - event_loop.run_app(&mut app).expect("run event loop"); + + let mut app = App { + width, + height, + scale_mode, + osc, + state: None, + }; + + event_loop.run_app(&mut app).expect("run"); } @@ -2,17 +2,16 @@ use std::net::UdpSocket; use rosc::{OscMessage, OscPacket, OscType}; -use crate::uniform::{UniformCache, UniformError, UniformRef}; +pub enum OscCommand { + Shader(String), + Uniform { path: String, args: Vec<OscType> }, +} pub struct OscServer { socket: UdpSocket, buf: Vec<u8>, } -pub enum OscCommand { - Shader(String), -} - impl OscServer { pub fn new(addr: &str) -> std::io::Result<Self> { let socket = UdpSocket::bind(addr)?; @@ -24,16 +23,13 @@ impl OscServer { }) } - pub fn poll(&mut self, cache: &mut UniformCache) -> Vec<OscCommand> { - let mut commands = Vec::new(); + pub fn poll(&mut self, mut on_command: impl FnMut(OscCommand)) { loop { match self.socket.recv_from(&mut self.buf) { Ok((size, _addr)) => { let data = &self.buf[..size]; match rosc::decoder::decode_udp(data) { - Ok((_, packet)) => { - self.handle_packet(packet, cache, &mut commands); - } + Ok((_, packet)) => dispatch(packet, &mut on_command), Err(e) => log::warn!("OSC decode error: {}", e), } } @@ -44,118 +40,34 @@ impl OscServer { } } } - commands - } - - fn handle_packet( - &self, - packet: OscPacket, - cache: &mut UniformCache, - commands: &mut Vec<OscCommand>, - ) { - match packet { - OscPacket::Message(msg) => self.handle_message(msg, cache, commands), - OscPacket::Bundle(bundle) => { - for p in bundle.content { - self.handle_packet(p, cache, commands); - } - } - } } +} - fn handle_message( - &self, - msg: OscMessage, - cache: &mut UniformCache, - commands: &mut Vec<OscCommand>, - ) { - let path = &msg.addr; - - if path == "/shader" { - match &msg.args[..] { - [OscType::String(code)] => commands.push(OscCommand::Shader(code.clone())), - _ => log::warn!("/shader: unexpected arguments"), - } - } else if let Some(rest) = path.strip_prefix("/uniform/") { - if let Err(e) = handle_uniform(rest, &msg.args, cache) { - log::warn!("error handling {}: {}", path, e); +fn dispatch(packet: OscPacket, on_command: &mut impl FnMut(OscCommand)) { + match packet { + OscPacket::Message(msg) => dispatch_message(msg, on_command), + OscPacket::Bundle(bundle) => { + for p in bundle.content { + dispatch(p, on_command); } - } else { - log::debug!("unhandled OSC message: {}", path); } } } -fn handle_uniform( - path: &str, - args: &[OscType], - cache: &mut UniformCache, -) -> Result<(), Box<dyn std::error::Error>> { - let mut parts = path.split('/'); - let name = parts.next().ok_or("missing uniform name")?; - - let mut uref = cache.get(name).ok_or("uniform not found")?; - - for component in parts { - let i = match component { - "x" | "r" | "s" => 0, - "y" | "g" | "t" => 1, - "z" | "b" | "p" => 2, - "w" | "a" | "q" => 3, - s => s.parse::<usize>()?, - }; - uref = uref.index(i)?; - } - - write_osc_values(uref, args)?; - Ok(()) -} - -/// Write OSC argument values into a UniformRef, converting OSC types -/// to match the uniform's expected scalar type. -fn write_osc_values(uref: UniformRef<'_>, args: &[OscType]) -> Result<(), UniformError> { - if args.is_empty() { - return Err(UniformError::SizeMismatch); - } +fn dispatch_message(msg: OscMessage, on_command: &mut impl FnMut(OscCommand)) { + let path = &msg.addr; - let scalar = uref.leaf_scalar()?; - match scalar.kind { - naga::ScalarKind::Float => { - let values: Result<Vec<f32>, _> = args - .iter() - .map(|a| match a { - OscType::Float(f) => Ok(*f), - OscType::Double(d) => Ok(*d as f32), - OscType::Int(i) => Ok(*i as f32), - _ => Err(UniformError::TypeMismatch), - }) - .collect(); - uref.set_f32(&values?) - } - naga::ScalarKind::Sint => { - let values: Result<Vec<i32>, _> = args - .iter() - .map(|a| match a { - OscType::Int(i) => Ok(*i), - OscType::Float(f) => Ok(*f as i32), - OscType::Double(d) => Ok(*d as i32), - _ => Err(UniformError::TypeMismatch), - }) - .collect(); - uref.set_i32(&values?) - } - naga::ScalarKind::Uint => { - let values: Result<Vec<u32>, _> = args - .iter() - .map(|a| match a { - OscType::Int(i) => Ok(*i as u32), - OscType::Float(f) => Ok(*f as u32), - OscType::Double(d) => Ok(*d as u32), - _ => Err(UniformError::TypeMismatch), - }) - .collect(); - uref.set_u32(&values?) + if path == "/shader" { + match &msg.args[..] { + [OscType::String(code)] => on_command(OscCommand::Shader(code.clone())), + _ => log::warn!("/shader: expected single string argument"), } - _ => Err(UniformError::TypeMismatch), + } else if let Some(rest) = path.strip_prefix("/uniform/") { + on_command(OscCommand::Uniform { + path: rest.to_string(), + args: msg.args, + }); + } else { + log::debug!("unhandled OSC message: {}", path); } } diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..e3a2acd --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,303 @@ +use crate::uniform::{UniformCache, UniformError}; +use rosc::OscType; + +const CANVAS_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb; + +const VERTEX_SHADER: &str = "\ +@group(0) @binding(0) var<uniform> _wgsl_resolution: vec2<f32>; + +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) uv: vec2<f32>, + @location(1) resolution: vec2<f32>, +}; + +@vertex +fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { + var uv = vec2<f32>(f32(vi % 2u), f32(vi / 2u)); + var out: VertexOutput; + out.clip_position = vec4<f32>(2.0 * uv - 1.0, 0.0, 1.0); + out.uv = vec2<f32>(uv.x, 1.0 - uv.y); + out.resolution = _wgsl_resolution; + return out; +} +"; + +const DEFAULT_FRAGMENT: &str = "\ +@fragment +fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> { + let check = floor(uv * 10.0); + return vec4<f32>(vec3<f32>(fract((check.x + check.y) / 2.0)), 1.0); +} +"; + +pub struct Renderer { + canvas_view: wgpu::TextureView, + vertex_module: wgpu::ShaderModule, + vertex_bgl: wgpu::BindGroupLayout, + vertex_bg: wgpu::BindGroup, + render_pipeline: wgpu::RenderPipeline, + uniform_cache: UniformCache, +} + +impl Renderer { + pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) -> Self { + let canvas_view = device + .create_texture(&wgpu::TextureDescriptor { + label: Some("canvas"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: CANVAS_FORMAT, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }) + .create_view(&Default::default()); + + let vertex_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("vertex_shader"), + source: wgpu::ShaderSource::Wgsl(VERTEX_SHADER.into()), + }); + + let resolution_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("resolution"), + size: 8, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let vertex_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let vertex_bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &vertex_bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: resolution_buffer.as_entire_binding(), + }], + }); + + queue.write_buffer( + &resolution_buffer, + 0, + bytemuck::cast_slice(&[width as f32, height as f32]), + ); + + let mut uniform_cache = UniformCache::new(); + let render_pipeline = build_pipeline( + device, + &vertex_module, + &vertex_bgl, + DEFAULT_FRAGMENT, + &mut uniform_cache, + ) + .expect("default shader"); + + Self { + canvas_view, + vertex_module, + vertex_bgl, + vertex_bg, + render_pipeline, + uniform_cache, + } + } + + pub fn canvas_view(&self) -> &wgpu::TextureView { + &self.canvas_view + } + + pub fn load_shader(&mut self, device: &wgpu::Device, source: &str) -> Result<(), String> { + let pipeline = build_pipeline( + device, + &self.vertex_module, + &self.vertex_bgl, + source, + &mut self.uniform_cache, + )?; + self.render_pipeline = pipeline; + log::info!("shader loaded"); + Ok(()) + } + + pub fn set_uniform( + &mut self, + path: &str, + args: &[OscType], + ) -> Result<(), Box<dyn std::error::Error>> { + let mut parts = path.split('/'); + let name = parts.next().ok_or("missing uniform name")?; + + let mut uref = self.uniform_cache.get(name).ok_or("uniform not found")?; + + for component in parts { + uref = uref.field(component)?; + } + + let scalar = uref.leaf_scalar()?; + match scalar.kind { + naga::ScalarKind::Float => { + let values: Vec<f32> = args + .iter() + .map(|a| match a { + OscType::Float(f) => Ok(*f), + OscType::Double(d) => Ok(*d as f32), + OscType::Int(i) => Ok(*i as f32), + OscType::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }), + _ => Err(UniformError::TypeMismatch), + }) + .collect::<Result<_, _>>()?; + uref.set_f32(&values)?; + } + naga::ScalarKind::Sint => { + let values: Vec<i32> = args + .iter() + .map(|a| match a { + OscType::Int(i) => Ok(*i), + OscType::Float(f) => Ok(*f as i32), + OscType::Double(d) => Ok(*d as i32), + OscType::Bool(b) => Ok(if *b { 1 } else { 0 }), + _ => Err(UniformError::TypeMismatch), + }) + .collect::<Result<_, _>>()?; + uref.set_i32(&values)?; + } + naga::ScalarKind::Uint => { + let values: Vec<u32> = args + .iter() + .map(|a| match a { + OscType::Int(i) => Ok(*i as u32), + OscType::Float(f) => Ok(*f as u32), + OscType::Double(d) => Ok(*d as u32), + OscType::Bool(b) => Ok(if *b { 1 } else { 0 }), + _ => Err(UniformError::TypeMismatch), + }) + .collect::<Result<_, _>>()?; + uref.set_u32(&values)?; + } + _ => return Err(Box::new(UniformError::TypeMismatch)), + } + + Ok(()) + } + + pub fn render(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { + self.uniform_cache.flush(queue); + + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &self.canvas_view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + pass.set_pipeline(&self.render_pipeline); + pass.set_bind_group(0, &self.vertex_bg, &[]); + if let Some(ref bg) = self.uniform_cache.bind_group { + pass.set_bind_group(1, bg, &[]); + } + pass.draw(0..4, 0..1); + } + + queue.submit([encoder.finish()]); + } +} + +fn build_pipeline( + device: &wgpu::Device, + vertex_module: &wgpu::ShaderModule, + vertex_bgl: &wgpu::BindGroupLayout, + fragment_source: &str, + cache: &mut UniformCache, +) -> Result<wgpu::RenderPipeline, String> { + let module = naga::front::wgsl::parse_str(fragment_source) + .map_err(|e| format!("WGSL parse error: {e}"))?; + + naga::valid::Validator::new( + naga::valid::ValidationFlags::all(), + naga::valid::Capabilities::all(), + ) + .validate(&module) + .map_err(|e| format!("WGSL validation error: {e}"))?; + + cache.refresh(module, device); + + let fragment_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("fragment_shader"), + source: wgpu::ShaderSource::Wgsl(fragment_source.into()), + }); + + let mut bind_group_layouts: Vec<Option<&wgpu::BindGroupLayout>> = vec![Some(vertex_bgl)]; + if let Some(ref bgl) = cache.bind_group_layout { + bind_group_layouts.push(Some(bgl)); + } + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &bind_group_layouts, + immediate_size: 0, + }); + + Ok( + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: vertex_module, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fragment_module, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: CANVAS_FORMAT, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + multisample: Default::default(), + multiview_mask: None, + cache: None, + }), + ) +} diff --git a/src/uniform.rs b/src/uniform.rs index 5a66396..c6886b4 100644 --- a/src/uniform.rs +++ b/src/uniform.rs @@ -15,6 +15,7 @@ pub struct UniformRef<'a> { pub enum UniformError { NotIndexable, IndexOutOfBounds, + NotFound, TypeMismatch, SizeMismatch, } @@ -24,6 +25,7 @@ impl std::fmt::Display for UniformError { match self { Self::NotIndexable => write!(f, "type is not indexable"), Self::IndexOutOfBounds => write!(f, "index out of bounds"), + Self::NotFound => write!(f, "member not found"), Self::TypeMismatch => write!(f, "scalar type mismatch"), Self::SizeMismatch => write!(f, "value count mismatch"), } @@ -33,28 +35,50 @@ impl std::fmt::Display for UniformError { impl std::error::Error for UniformError {} impl<'a> UniformRef<'a> { - /// Index into a compound type, returning a narrower UniformRef. - /// - /// - Vector: index selects a scalar component - /// - Matrix: index selects a column (vector) - /// - Array: index selects an element - /// - Struct: index selects a member by position + /// Navigate by path component: struct member by name, or swizzle/numeric index otherwise. + pub fn field(self, name: &str) -> Result<UniformRef<'a>, UniformError> { + let ty_inner = &self.module.types[self.ty].inner; + if let naga::TypeInner::Struct { ref members, .. } = *ty_inner { + for member in members { + if member.name.as_deref() == Some(name) { + let offset = member.offset as usize; + let size = self.layouter[member.ty].size as usize; + return Ok(UniformRef { + module: self.module, + layouter: self.layouter, + ty: member.ty, + data: &mut self.data[offset..offset + size], + }); + } + } + return Err(UniformError::NotFound); + } + + let i = match name { + "x" | "r" | "s" => 0, + "y" | "g" | "t" => 1, + "z" | "b" | "p" => 2, + "w" | "a" | "q" => 3, + s => s.parse::<usize>().map_err(|_| UniformError::NotIndexable)?, + }; + self.index(i) + } + + /// Index into a vector, matrix, or array by position. pub fn index(self, i: usize) -> Result<UniformRef<'a>, UniformError> { let ty_inner = &self.module.types[self.ty].inner; match *ty_inner { naga::TypeInner::Vector { size, scalar } => { - let count = size as usize; - if i >= count { + if i >= size as usize { return Err(UniformError::IndexOutOfBounds); } - let scalar_size = scalar.width as usize; - let offset = i * scalar_size; + let w = scalar.width as usize; let scalar_ty = find_or_expect_scalar_type(self.module, scalar); Ok(UniformRef { module: self.module, layouter: self.layouter, ty: scalar_ty, - data: &mut self.data[offset..offset + scalar_size], + data: &mut self.data[i * w..][..w], }) } naga::TypeInner::Matrix { @@ -62,20 +86,18 @@ impl<'a> UniformRef<'a> { rows, scalar, } => { - let col_count = columns as usize; - if i >= col_count { + if i >= columns as usize { return Err(UniformError::IndexOutOfBounds); } let col_ty = find_or_expect_vector_type(self.module, rows, scalar); let col_layout = self.layouter[col_ty]; - let stride = col_layout.to_stride(); - let offset = i * stride as usize; + let stride = col_layout.to_stride() as usize; let size = col_layout.size as usize; Ok(UniformRef { module: self.module, layouter: self.layouter, ty: col_ty, - data: &mut self.data[offset..offset + size], + data: &mut self.data[i * stride..][..size], }) } naga::TypeInner::Array { base, size, stride } => { @@ -87,26 +109,11 @@ impl<'a> UniformRef<'a> { return Err(UniformError::IndexOutOfBounds); } let elem_size = self.layouter[base].size as usize; - let offset = i * stride as usize; Ok(UniformRef { module: self.module, layouter: self.layouter, ty: base, - data: &mut self.data[offset..offset + elem_size], - }) - } - naga::TypeInner::Struct { ref members, .. } => { - if i >= members.len() { - return Err(UniformError::IndexOutOfBounds); - } - let member = &members[i]; - let offset = member.offset as usize; - let size = self.layouter[member.ty].size as usize; - Ok(UniformRef { - module: self.module, - layouter: self.layouter, - ty: member.ty, - data: &mut self.data[offset..offset + size], + data: &mut self.data[i * stride as usize..][..elem_size], }) } _ => Err(UniformError::NotIndexable), @@ -353,42 +360,23 @@ pub fn types_compatible( ) -> bool { let a = &a_mod.types[a_ty].inner; let b = &b_mod.types[b_ty].inner; + + // Uniform types (Scalar, Vector, Matrix) contain no handles or pointers, + // so TypeInner's derived PartialEq works directly. Only Array and Struct + // need recursion because they contain Handle<Type> from different arenas. match (a, b) { - (naga::TypeInner::Scalar(a), naga::TypeInner::Scalar(b)) => a == b, - ( - naga::TypeInner::Vector { - size: as_, - scalar: asc, - }, - naga::TypeInner::Vector { - size: bs, - scalar: bsc, - }, - ) => as_ == bs && asc == bsc, - ( - naga::TypeInner::Matrix { - columns: ac, - rows: ar, - scalar: asc, - }, - naga::TypeInner::Matrix { - columns: bc, - rows: br, - scalar: bsc, - }, - ) => ac == bc && ar == br && asc == bsc, ( naga::TypeInner::Array { base: ab, - size: naga::ArraySize::Constant(an), - .. + size: asize, + stride: astride, }, naga::TypeInner::Array { base: bb, - size: naga::ArraySize::Constant(bn), - .. + size: bsize, + stride: bstride, }, - ) => an == bn && types_compatible(a_mod, *ab, b_mod, *bb), + ) => asize == bsize && astride == bstride && types_compatible(a_mod, *ab, b_mod, *bb), ( naga::TypeInner::Struct { members: am, @@ -405,13 +393,11 @@ pub fn types_compatible( a.offset == b.offset && types_compatible(a_mod, a.ty, b_mod, b.ty) }) } - _ => false, + _ => a == b, } } /// Manages uniform buffer data for all `var<uniform>` globals in a shader. -/// -/// Struct members are exposed as individually addressable named uniforms. pub struct UniformCache { pub module: naga::Module, pub layouter: Layouter, @@ -430,7 +416,6 @@ struct UniformMember { } struct BufferState { - /// @group and @binding group: u32, binding: u32, data: Vec<u8>, @@ -456,7 +441,7 @@ impl UniformCache { let mut new_layouter = Layouter::default(); new_layouter.update(new_module.to_ctx()).unwrap(); - let old_module = std::mem::replace(&mut self.module, naga::Module::default()); + let old_module = std::mem::take(&mut self.module); let old_uniforms = std::mem::take(&mut self.uniforms); let old_buffers = std::mem::take(&mut self.buffers); @@ -472,7 +457,6 @@ impl UniformCache { None => continue, }; - let ty_inner = &new_module.types[var.ty].inner; let layout = new_layouter[var.ty]; let buffer_idx = new_buffers.len(); @@ -484,22 +468,7 @@ impl UniformCache { gpu_buffer: None, }); - if let naga::TypeInner::Struct { ref members, .. } = *ty_inner { - for member in members { - if let Some(ref name) = member.name { - let member_size = new_layouter[member.ty].size as usize; - new_uniforms.insert( - name.clone(), - UniformMember { - buffer_idx, - ty: member.ty, - offset: member.offset as usize, - size: member_size, - }, - ); - } - } - } else if let Some(ref name) = var.name { + if let Some(ref name) = var.name { new_uniforms.insert( name.clone(), UniformMember { diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 0000000..e385ecb --- /dev/null +++ b/src/window.rs @@ -0,0 +1,245 @@ +use std::sync::Arc; + +#[derive(Clone, Copy, Debug)] +pub enum ScaleMode { + Contain, + Cover, + Center, + Natural, +} + +impl ScaleMode { + pub fn cycle(self) -> Self { + match self { + Self::Contain => Self::Cover, + Self::Cover => Self::Center, + Self::Center => Self::Natural, + Self::Natural => Self::Contain, + } + } +} + +const BLIT_SHADER: &str = "\ +@group(0) @binding(0) var t: texture_2d<f32>; +@group(0) @binding(1) var s: sampler; + +struct VertexOutput { + @builtin(position) position: vec4<f32>, + @location(0) uv: vec2<f32>, +}; + +@vertex +fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { + var uv = vec2<f32>(f32(vi % 2u), f32(vi / 2u)); + var out: VertexOutput; + out.position = vec4<f32>(2.0 * uv - 1.0, 0.0, 1.0); + out.uv = vec2<f32>(uv.x, 1.0 - uv.y); + return out; +} + +@fragment +fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> { + return textureSample(t, s, uv); +} +"; + +pub struct PreviewWindow { + window: Arc<winit::window::Window>, + surface: wgpu::Surface<'static>, + surface_config: wgpu::SurfaceConfiguration, + blit_pipeline: wgpu::RenderPipeline, + blit_bind_group: wgpu::BindGroup, + canvas_width: u32, + canvas_height: u32, + pub scale_mode: ScaleMode, +} + +impl PreviewWindow { + #[allow(clippy::too_many_arguments)] + pub fn new( + device: &wgpu::Device, + window: Arc<winit::window::Window>, + surface: wgpu::Surface<'static>, + surface_format: wgpu::TextureFormat, + canvas_view: &wgpu::TextureView, + canvas_width: u32, + canvas_height: u32, + scale_mode: ScaleMode, + ) -> Self { + let size = window.inner_size(); + let surface_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::AutoVsync, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(device, &surface_config); + + let sampler_nearest = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("blit_sampler"), + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let blit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("blit_bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let blit_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("blit_bind_group"), + layout: &blit_bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(canvas_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler_nearest), + }, + ], + }); + + let blit_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("blit_shader"), + source: wgpu::ShaderSource::Wgsl(BLIT_SHADER.into()), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("blit_pipeline_layout"), + bind_group_layouts: &[Some(&blit_bgl)], + immediate_size: 0, + }); + + let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("blit_pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &blit_module, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &blit_module, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + Self { + window, + surface, + surface_config, + blit_pipeline, + blit_bind_group, + canvas_width, + canvas_height, + scale_mode, + } + } + + pub fn window(&self) -> &winit::window::Window { + &self.window + } + + pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) { + if width == 0 || height == 0 { + return; + } + self.surface_config.width = width; + self.surface_config.height = height; + self.surface.configure(device, &self.surface_config); + } + + pub fn draw(&self, device: &wgpu::Device, queue: &wgpu::Queue) { + let frame = match self.surface.get_current_texture() { + wgpu::CurrentSurfaceTexture::Success(frame) + | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame, + _ => return, + }; + + let view = frame.texture.create_view(&Default::default()); + + let w_scale = self.surface_config.width as f32 / self.canvas_width as f32; + let h_scale = self.surface_config.height as f32 / self.canvas_height as f32; + let scale = match self.scale_mode { + ScaleMode::Contain => w_scale.min(h_scale), + ScaleMode::Cover => w_scale.max(h_scale), + ScaleMode::Center => 1.0, + ScaleMode::Natural => 1.0 / (1.0 / w_scale.min(h_scale)).ceil(), + }; + + let vp_w = self.canvas_width as f32 * scale; + let vp_h = self.canvas_height as f32 * scale; + let vp_x = (self.surface_config.width as f32 - vp_w) / 2.0; + let vp_y = (self.surface_config.height as f32 - vp_h) / 2.0; + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("blit_encoder"), + }); + + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("blit_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + pass.set_viewport(vp_x, vp_y, vp_w, vp_h, 0.0, 1.0); + pass.set_pipeline(&self.blit_pipeline); + pass.set_bind_group(0, &self.blit_bind_group, &[]); + pass.draw(0..4, 0..1); + } + + queue.submit(std::iter::once(encoder.finish())); + frame.present(); + } +} @@ -23,7 +23,7 @@ sleep 0.5 echo "Animating time uniform..." t=0.0 while true; do - oscsend $HOST $PORT /uniform/time f "$t" + oscsend $HOST $PORT /uniform/u/time f "$t" t=$(echo "$t + 0.016" | bc) sleep 0.016 done |
