summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2026-04-16 10:24:24 +0000
committers-ol <s+removethis@s-ol.nu>2026-05-14 14:46:13 +0000
commit6a67c23a5029aceea1498f892c3424946b152cdc (patch)
tree5d1b2a7283818e8807caaa211d22641b467386ba
parentinitial commit (diff)
downloadwgsl-view-6a67c23a5029aceea1498f892c3424946b152cdc.tar.gz
wgsl-view-6a67c23a5029aceea1498f892c3424946b152cdc.zip
refactor
-rw-r--r--src/main.rs500
-rw-r--r--src/osc.rs140
-rw-r--r--src/renderer.rs303
-rw-r--r--src/uniform.rs133
-rw-r--r--src/window.rs245
-rwxr-xr-xtest.sh2
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");
}
diff --git a/src/osc.rs b/src/osc.rs
index 73f0f35..6759704 100644
--- a/src/osc.rs
+++ b/src/osc.rs
@@ -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();
+ }
+}
diff --git a/test.sh b/test.sh
index 39ae836..54465bc 100755
--- a/test.sh
+++ b/test.sh
@@ -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