summaryrefslogtreecommitdiffstats
path: root/src/bin/wgsl_render.rs
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2026-04-16 15:29:50 +0000
committers-ol <s+removethis@s-ol.nu>2026-05-14 14:46:13 +0000
commit5925137f3b41cb3a87f098604ef482bced91341e (patch)
tree613ee305358e319bab5d88a85cc923b4870d73f1 /src/bin/wgsl_render.rs
parentrefactor (diff)
downloadwgsl-view-5925137f3b41cb3a87f098604ef482bced91341e.tar.gz
wgsl-view-5925137f3b41cb3a87f098604ef482bced91341e.zip
tsv integration, split binaries
Diffstat (limited to 'src/bin/wgsl_render.rs')
-rw-r--r--src/bin/wgsl_render.rs281
1 files changed, 281 insertions, 0 deletions
diff --git a/src/bin/wgsl_render.rs b/src/bin/wgsl_render.rs
new file mode 100644
index 0000000..1860b7c
--- /dev/null
+++ b/src/bin/wgsl_render.rs
@@ -0,0 +1,281 @@
+use std::thread;
+use std::time::{Duration, Instant};
+
+use ash::vk;
+use rosc::OscType;
+use wgsl_view::gpu;
+use wgsl_view::osc::OscServer;
+use wgsl_view::renderer::Renderer;
+use wgsl_view::uniform::UniformError;
+
+fn main() {
+ env_logger::init();
+
+ let mut args = std::env::args().skip(1);
+ let mut width = 1920u32;
+ let mut height = 1080u32;
+ let mut port = 9000u16;
+ let mut name = "wgsl-view".to_string();
+ let mut fps = 60u32;
+ let mut continuous = false;
+
+ 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"),
+ "--name" => name = args.next().expect("--name VALUE"),
+ "--fps" => fps = args.next().expect("--fps VALUE").parse().expect("u32"),
+ "--continuous" => continuous = true,
+ other => panic!("unknown argument: {other}"),
+ }
+ }
+
+ let instance = gpu::create_instance();
+ let adapter = gpu::create_adapter(&instance, None);
+ let (device, queue) = gpu::create_device(&adapter);
+
+ let mut client = gpu::create_tsv_client(&device);
+ client
+ .init_image(&name, width, height, gpu::TSV_FORMAT, true)
+ .expect("init tsv image");
+
+ let fence = gpu::create_fence(&client);
+ let mut renderer = Renderer::new(&device, &queue, width, height);
+ let mut osc = OscServer::new(&format!("0.0.0.0:{port}")).expect("bind OSC socket");
+
+ let frame_time = Duration::from_secs_f64(1.0 / fps as f64);
+ log::info!(
+ "rendering {width}x{height}{}, tsv image: {name}",
+ if continuous {
+ format!(" at {fps}fps")
+ } else {
+ " on OSC input".to_string()
+ }
+ );
+
+ loop {
+ let frame_start = Instant::now();
+
+ let handle_msg = |msg: rosc::OscMessage| {
+ let path = &msg.addr;
+
+ if path == "/shader" {
+ match &msg.args[..] {
+ [OscType::String(code)] => {
+ if let Err(e) = renderer.load_shader(&device, code) {
+ log::error!("shader error: {e}");
+ }
+ }
+ _ => log::warn!("/shader: expected single string argument"),
+ }
+ } else if let Some(rest) = path.strip_prefix("/uniform/") {
+ if let Err(e) = set_uniform(renderer.uniforms(), rest, &msg.args) {
+ log::warn!("uniform error: {e}");
+ }
+ } else if let Some(tex_name) = path.strip_prefix("/texture/") {
+ match &msg.args[..] {
+ [OscType::String(tsv_name)] => {
+ match renderer.uniforms().texture_slot_mut(tex_name) {
+ Some(slot) => {
+ slot.set_tsv_name(tsv_name.clone());
+ log::info!("texture '{tex_name}' → tsv '{tsv_name}'");
+ }
+ None => log::warn!("texture '{tex_name}' not found"),
+ }
+ }
+ _ => log::warn!("/texture/{tex_name}: expected string (TSV image name)"),
+ }
+ } else if let Some(smp_name) = path.strip_prefix("/sampler/") {
+ match &msg.args[..] {
+ [OscType::String(filter), OscType::String(clamp)] => {
+ if let Err(e) = configure_sampler(
+ renderer.uniforms(),
+ &device,
+ smp_name,
+ filter,
+ clamp,
+ ) {
+ log::warn!("sampler error: {e}");
+ }
+ }
+ _ => log::warn!("/sampler/{smp_name}: expected two strings (filter, clamp)"),
+ }
+ } else {
+ log::debug!("unhandled OSC: {path}");
+ }
+ };
+
+ let dirty = if continuous {
+ osc.poll(handle_msg);
+ true
+ } else {
+ osc.recv(handle_msg)
+ };
+
+ if dirty {
+ refresh_textures(renderer.uniforms(), &device, &mut client, fence);
+ renderer.render(&device, &queue);
+ device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
+
+ let canvas_image = unsafe { gpu::raw_image(renderer.canvas_texture()) };
+ if let Err(e) = client.send_image(
+ &name,
+ canvas_image,
+ vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
+ vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
+ fence,
+ ) {
+ log::warn!("send_image error: {e}");
+ }
+ }
+
+ if continuous {
+ let elapsed = frame_start.elapsed();
+ if elapsed < frame_time {
+ thread::sleep(frame_time - elapsed);
+ }
+ }
+ }
+}
+
+/// Set a uniform value from OSC args, navigating the path.
+fn set_uniform(
+ cache: &mut wgsl_view::uniform::UniformCache,
+ 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 = 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(())
+}
+
+/// Configure a named sampler from string filter/clamp mode names.
+fn configure_sampler(
+ cache: &mut wgsl_view::uniform::UniformCache,
+ device: &wgpu::Device,
+ name: &str,
+ filter: &str,
+ clamp: &str,
+) -> Result<(), String> {
+ let filter_mode = match filter {
+ "linear" => wgpu::FilterMode::Linear,
+ "nearest" => wgpu::FilterMode::Nearest,
+ _ => return Err(format!("unknown filter mode '{filter}' (linear|nearest)")),
+ };
+ let address_mode = match clamp {
+ "clamp" => wgpu::AddressMode::ClampToEdge,
+ "repeat" => wgpu::AddressMode::Repeat,
+ "mirror" => wgpu::AddressMode::MirrorRepeat,
+ _ => return Err(format!("unknown clamp mode '{clamp}' (clamp|repeat|mirror)")),
+ };
+ let slot = cache
+ .sampler_slot_mut(name)
+ .ok_or_else(|| format!("sampler '{name}' not found"))?;
+ slot.configure(device, filter_mode, address_mode);
+ cache.rebuild_bind_group(device);
+ log::info!("sampler '{name}' → {filter}, {clamp}");
+ Ok(())
+}
+
+/// Refresh all texture inputs from TSV shared images.
+fn refresh_textures(
+ cache: &mut wgsl_view::uniform::UniformCache,
+ device: &wgpu::Device,
+ client: &mut texture_share_vk_client::VkClient,
+ fence: vk::Fence,
+) {
+ let mut needs_rebind = false;
+
+ for slot in cache.texture_slots_mut() {
+ let tsv_name = match slot.tsv_name() {
+ Some(n) => n.to_string(),
+ None => continue,
+ };
+
+ if !slot.tsv_registered() {
+ if let Err(e) = client.find_image(&tsv_name, true) {
+ log::debug!("tsv find '{tsv_name}': {e}");
+ continue;
+ }
+
+ match client.find_image_data(&tsv_name, true) {
+ Ok(Some((_lock, data))) => {
+ if slot.resize(device, data.width, data.height) {
+ needs_rebind = true;
+ }
+ }
+ _ => continue,
+ }
+
+ slot.set_tsv_registered();
+ }
+
+ let raw = unsafe { gpu::raw_image(slot.texture()) };
+ match client.recv_image(
+ &tsv_name,
+ raw,
+ vk::ImageLayout::UNDEFINED,
+ vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
+ fence,
+ ) {
+ Ok(Some(())) => log::trace!("texture '{}' updated from '{tsv_name}'", slot.name()),
+ Ok(None) => {}
+ Err(e) => log::warn!("recv_image '{tsv_name}': {e}"),
+ }
+ }
+
+ if needs_rebind {
+ cache.rebuild_bind_group(device);
+ }
+}