summaryrefslogtreecommitdiffstats
path: root/src/bin
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/tsv_video_stream.rs185
-rw-r--r--src/bin/tsv_view.rs220
-rw-r--r--src/bin/wgsl_render.rs281
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);
+ }
+}