diff options
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/tsv_video_stream.rs | 185 | ||||
| -rw-r--r-- | src/bin/tsv_view.rs | 220 | ||||
| -rw-r--r-- | src/bin/wgsl_render.rs | 281 |
3 files changed, 686 insertions, 0 deletions
diff --git a/src/bin/tsv_video_stream.rs b/src/bin/tsv_video_stream.rs new file mode 100644 index 0000000..d68f4b4 --- /dev/null +++ b/src/bin/tsv_video_stream.rs @@ -0,0 +1,185 @@ +use std::io::Read; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; + +use ash::vk; +use wgsl_view::gpu; + +/// Options that ffmpeg supports but ffprobe does not. +const FFMPEG_ONLY_OPTIONS: &[&str] = &["-stream_loop"]; + +fn filter_probe_args(ff_args: &[String]) -> Vec<String> { + let mut out = Vec::new(); + let mut skip_next = false; + for arg in ff_args { + if skip_next { + skip_next = false; + continue; + } + if FFMPEG_ONLY_OPTIONS.contains(&arg.as_str()) { + skip_next = true; + continue; + } + out.push(arg.clone()); + } + out +} + +fn probe_video(ff_args: &[String]) -> (u32, u32, f64) { + let probe_args = filter_probe_args(ff_args); + let output = Command::new("ffprobe") + .args(["-v", "error", "-select_streams", "v:0"]) + .args(["-show_entries", "stream=width,height,r_frame_rate"]) + .args(["-of", "csv=p=0"]) + .args(&probe_args) + .output() + .expect("failed to run ffprobe"); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + panic!("ffprobe failed: {stderr}"); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let line = stdout.trim(); + let parts: Vec<&str> = line.split(',').collect(); + if parts.len() < 3 { + panic!("unexpected ffprobe output: {line}"); + } + + let width: u32 = parts[0].parse().expect("parse width"); + let height: u32 = parts[1].parse().expect("parse height"); + + let fps: f64 = if let Some((num, den)) = parts[2].split_once('/') { + let n: f64 = num.parse().expect("parse fps numerator"); + let d: f64 = den.parse().expect("parse fps denominator"); + n / d + } else { + parts[2].parse().expect("parse fps") + }; + + (width, height, fps) +} + +fn main() { + env_logger::init(); + + let args: Vec<String> = std::env::args().skip(1).collect(); + let sep = args.iter().position(|a| a == "--"); + + let (own_args, ff_args) = match sep { + Some(i) => (&args[..i], &args[i + 1..]), + None => { + eprintln!("usage: tsv-video-stream [--name NAME] -- <ffmpeg input args>"); + std::process::exit(1); + } + }; + + let mut name = "tsv-video-stream".to_string(); + + let mut i = 0; + while i < own_args.len() { + match own_args[i].as_str() { + "--name" => { + name = own_args[i + 1].clone(); + i += 2; + } + other => panic!("unknown argument: {other}"), + } + } + + let (width, height, fps) = probe_video(ff_args); + log::info!("{width}x{height} @ {fps:.2}fps, tsv image: {name}"); + + 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 texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("video_frame"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + let frame_size = (width * height * 4) as usize; + let frame_time = Duration::from_secs_f64(1.0 / fps); + + let mut ffmpeg = Command::new("ffmpeg") + .args(["-v", "quiet"]) + .args(ff_args) + .args(["-f", "rawvideo", "-pix_fmt", "rgba", "pipe:1"]) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .expect("failed to start ffmpeg"); + + let stdout = ffmpeg.stdout.take().unwrap(); + let mut reader = std::io::BufReader::new(stdout); + let mut frame_buf = vec![0u8; frame_size]; + + loop { + let frame_start = Instant::now(); + + if reader.read_exact(&mut frame_buf).is_err() { + break; + } + + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &frame_buf, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(width * 4), + rows_per_image: None, + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + // flush the pending write_texture command and wait for GPU completion + queue.submit([]); + device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); + + let raw = unsafe { gpu::raw_image(&texture) }; + if let Err(e) = client.send_image( + &name, + raw, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + fence, + ) { + log::warn!("send_image error: {e}"); + } + + let elapsed = frame_start.elapsed(); + if elapsed < frame_time { + thread::sleep(frame_time - elapsed); + } + } + + ffmpeg.wait().ok(); +} diff --git a/src/bin/tsv_view.rs b/src/bin/tsv_view.rs new file mode 100644 index 0000000..885a8c4 --- /dev/null +++ b/src/bin/tsv_view.rs @@ -0,0 +1,220 @@ +use std::sync::Arc; + +use ash::vk; +use wgsl_view::gpu; +use wgsl_view::window::{PreviewWindow, ScaleMode}; + +use winit::application::ApplicationHandler; +use winit::event::{ElementState, KeyEvent, WindowEvent}; +use winit::event_loop::EventLoop; +use winit::keyboard::{Key, NamedKey}; + +struct AppState { + device: wgpu::Device, + queue: wgpu::Queue, + preview: PreviewWindow, + client: texture_share_vk_client::VkClient, + fence: vk::Fence, + canvas: wgpu::Texture, + name: String, +} + +struct App { + scale_mode: ScaleMode, + name: String, + state: Option<AppState>, +} + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + let instance = gpu::create_instance(); + + // Create a temporary headless device to connect to TSV and read image dimensions + let temp_adapter = gpu::create_adapter(&instance, None); + let (temp_device, _temp_queue) = gpu::create_device(&temp_adapter); + let mut client = gpu::create_tsv_client(&temp_device); + + let (width, height) = match client.find_image_data(&self.name, true) { + Ok(Some((_lock, data))) => (data.width, data.height), + Ok(None) => { + log::error!("tsv image '{}' not found", self.name); + event_loop.exit(); + return; + } + Err(e) => { + log::error!("failed to find tsv image '{}': {e}", self.name); + event_loop.exit(); + return; + } + }; + log::info!("found tsv image '{}': {width}x{height}", self.name); + + drop(client); + drop(temp_device); + + let window = Arc::new( + event_loop + .create_window( + winit::window::Window::default_attributes() + .with_title(format!("tsv-view: {}", self.name)) + .with_inner_size(winit::dpi::LogicalSize::new(width, height)), + ) + .expect("create window"), + ); + + let surface = instance + .create_surface(window.clone()) + .expect("create surface"); + let adapter = gpu::create_adapter(&instance, Some(&surface)); + let (device, queue) = gpu::create_device(&adapter); + + 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 mut client = gpu::create_tsv_client(&device); + client.find_image(&self.name, true).expect("find tsv image"); + + let fence = gpu::create_fence(&client); + + let canvas = device.create_texture(&wgpu::TextureDescriptor { + label: Some("tsv_canvas"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + + let canvas_view = canvas.create_view(&Default::default()); + let preview = PreviewWindow::new( + &device, + window, + surface, + surface_format, + &canvas_view, + width, + height, + self.scale_mode, + ); + + self.state = Some(AppState { + device, + queue, + preview, + client, + fence, + canvas, + name: self.name.clone(), + }); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + _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: + KeyEvent { + logical_key, + state: ElementState::Pressed, + .. + }, + .. + } => 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) => { + state.preview.resize(&state.device, size.width, size.height); + } + WindowEvent::RedrawRequested => { + // Receive the latest frame from texture-share-vk. + // Use UNDEFINED as orig_layout: the image content is fully overwritten by recv, + // and wgpu may not have transitioned it yet. + let canvas_image = unsafe { gpu::raw_image(&state.canvas) }; + match state.client.recv_image( + &state.name, + canvas_image, + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + state.fence, + ) { + Err(e) => log::warn!("recv_image error: {e}"), + Ok(Some(())) => log::debug!("got frame"), + Ok(None) => {} + } + + state.preview.draw(&state.device, &state.queue); + } + _ => {} + } + } + + fn about_to_wait(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) { + if let Some(ref state) = self.state { + state.preview.window().request_redraw(); + } + } +} + +impl Drop for AppState { + fn drop(&mut self) { + gpu::destroy_fence(&self.client, self.fence); + } +} + +fn main() { + env_logger::init(); + + let mut args = std::env::args().skip(1); + let mut scale_mode = ScaleMode::Contain; + let mut name = "wgsl-view".to_string(); + + while let Some(arg) = args.next() { + match arg.as_str() { + "--name" => name = args.next().expect("--name VALUE"), + "--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 event_loop = EventLoop::new().expect("create event loop"); + event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); + + let mut app = App { + scale_mode, + name, + state: None, + }; + + event_loop.run_app(&mut app).expect("run"); +} 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); + } +} |
