diff options
| author | s-ol <s+removethis@s-ol.nu> | 2026-04-16 15:29:50 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2026-05-14 14:46:13 +0000 |
| commit | 5925137f3b41cb3a87f098604ef482bced91341e (patch) | |
| tree | 613ee305358e319bab5d88a85cc923b4870d73f1 /src | |
| parent | refactor (diff) | |
| download | wgsl-view-5925137f3b41cb3a87f098604ef482bced91341e.tar.gz wgsl-view-5925137f3b41cb3a87f098604ef482bced91341e.zip | |
tsv integration, split binaries
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/tsv_video_stream.rs | 185 | ||||
| -rw-r--r-- | src/bin/tsv_view.rs (renamed from src/main.rs) | 166 | ||||
| -rw-r--r-- | src/bin/wgsl_render.rs | 281 | ||||
| -rw-r--r-- | src/gpu.rs | 141 | ||||
| -rw-r--r-- | src/lib.rs | 5 | ||||
| -rw-r--r-- | src/osc.rs | 60 | ||||
| -rw-r--r-- | src/renderer.rs | 122 | ||||
| -rw-r--r-- | src/uniform.rs | 456 |
8 files changed, 1158 insertions, 258 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/main.rs b/src/bin/tsv_view.rs index 3ce4ffc..885a8c4 100644 --- a/src/main.rs +++ b/src/bin/tsv_view.rs @@ -1,13 +1,8 @@ -mod osc; -mod renderer; -mod uniform; -mod window; - use std::sync::Arc; -use osc::{OscCommand, OscServer}; -use renderer::Renderer; -use window::{PreviewWindow, ScaleMode}; +use ash::vk; +use wgsl_view::gpu; +use wgsl_view::window::{PreviewWindow, ScaleMode}; use winit::application::ApplicationHandler; use winit::event::{ElementState, KeyEvent, WindowEvent}; @@ -17,74 +12,61 @@ use winit::keyboard::{Key, NamedKey}; struct AppState { device: wgpu::Device, queue: wgpu::Queue, - renderer: Renderer, preview: PreviewWindow, + client: texture_share_vk_client::VkClient, + fence: vk::Fence, + canvas: wgpu::Texture, + name: String, } struct App { - width: u32, - height: u32, scale_mode: ScaleMode, - osc: OscServer, + name: String, state: Option<AppState>, } -impl App { - fn handle_osc(&mut self) { - 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); - } +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; } - OscCommand::Uniform { path, args } => { - if let Err(e) = state.renderer.set_uniform(&path, &args) { - log::warn!("uniform error: {}", e); - } + 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); -impl ApplicationHandler for App { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { 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)), + .with_title(format!("tsv-view: {}", self.name)) + .with_inner_size(winit::dpi::LogicalSize::new(width, 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 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 @@ -94,23 +76,46 @@ impl ApplicationHandler for App { .copied() .unwrap_or(surface_caps.formats[0]); - let renderer = Renderer::new(&device, &queue, self.width, self.height); + 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, - renderer.canvas_view(), - self.width, - self.height, + &canvas_view, + width, + height, self.scale_mode, ); self.state = Some(AppState { device, queue, - renderer, preview, + client, + fence, + canvas, + name: self.name.clone(), }); } @@ -144,9 +149,22 @@ impl ApplicationHandler for App { state.preview.resize(&state.device, size.width, size.height); } WindowEvent::RedrawRequested => { - self.handle_osc(); - let state = self.state.as_mut().expect("gpu not initialized"); - state.renderer.render(&state.device, &state.queue); + // 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); } _ => {} @@ -160,43 +178,41 @@ impl ApplicationHandler for App { } } +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 width = 1920u32; - let mut height = 1080u32; let mut scale_mode = ScaleMode::Contain; - let mut port = 9000u16; + let mut name = "wgsl-view".to_string(); 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"), "--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), + s => panic!("unknown scale mode: {s}"), } } - other => panic!("unknown argument: {}", other), + 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 { - width, - height, scale_mode, - osc, + name, state: None, }; 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); + } +} diff --git a/src/gpu.rs b/src/gpu.rs new file mode 100644 index 0000000..e6529f9 --- /dev/null +++ b/src/gpu.rs @@ -0,0 +1,141 @@ +use std::time::Duration; + +use ash::vk; +use texture_share_vk_base::vk_device::VkDevice; +use texture_share_vk_base::vk_entry::VkEntry; +use texture_share_vk_base::vk_instance::VkInstance; +use texture_share_vk_base::vk_setup::VkSetup; +use texture_share_vk_client::VkClient; + +const TSV_SERVER_EXECUTABLE: &str = "/usr/bin/texture-share-vk-server"; +const TSV_SERVER_SOCKET_PATH: &str = "/tmp/vk_server/vk_server.sock"; +const TSV_SERVER_LOCK_PATH: &str = "/tmp/vk_server/vk_server.lock"; +const TSV_SHMEM_PREFIX: &str = "shmem_img_"; + +/// Creates a wgpu instance configured for Vulkan. +pub fn create_instance() -> wgpu::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, + }) +} + +/// Requests a wgpu adapter, optionally compatible with a surface. +pub fn create_adapter( + instance: &wgpu::Instance, + surface: Option<&wgpu::Surface<'_>>, +) -> wgpu::Adapter { + pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: surface, + force_fallback_adapter: false, + })) + .expect("find vulkan adapter") +} + +/// Requests a wgpu device + queue. +pub fn create_device(adapter: &wgpu::Adapter) -> (wgpu::Device, wgpu::Queue) { + pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { + label: Some("device"), + ..Default::default() + })) + .expect("create device") +} + +/// Builds a texture-share-vk VkSetup by importing raw Vulkan handles from a wgpu device. +/// The returned VkSetup does NOT own the Vulkan objects (import_only = true). +fn import_vk_setup(device: &wgpu::Device) -> VkSetup { + // Extract raw handles from the wgpu HAL device + let (raw_instance, raw_device, raw_physical_device, raw_queue, queue_family_index, queue_index) = unsafe { + let hal = device + .as_hal::<wgpu_hal::api::Vulkan>() + .expect("vulkan HAL device"); + + let instance_shared = hal.shared_instance(); + let raw_instance = instance_shared.raw_instance().handle(); + let raw_device = hal.raw_device().handle(); + let raw_physical_device = hal.raw_physical_device(); + let raw_queue = hal.raw_queue(); + let queue_family_index = hal.queue_family_index(); + let queue_index = hal.queue_index(); + + ( + raw_instance, + raw_device, + raw_physical_device, + raw_queue, + queue_family_index, + queue_index, + ) + }; + + let vk_entry = Box::new(VkEntry::new().expect("vulkan entry")); + let vk_instance = + VkInstance::import_vk(Some(vk_entry), raw_instance, true).expect("import vulkan instance"); + let vk_device = VkDevice::import_vk( + &vk_instance, + raw_device, + raw_physical_device, + raw_queue, + queue_family_index, + queue_index, + true, + ) + .expect("import vulkan device"); + + VkSetup::new(vk_instance, vk_device) +} + +/// Creates a VkClient connected to the texture-share-vk server, +/// launching the server if needed. +pub fn create_tsv_client(device: &wgpu::Device) -> VkClient { + let vk_setup = import_vk_setup(device); + let timeout = Duration::from_secs(2); + + VkClient::new_with_server_launch( + TSV_SERVER_SOCKET_PATH, + Box::new(vk_setup), + Duration::from_secs(1), + TSV_SERVER_EXECUTABLE, + TSV_SERVER_LOCK_PATH, + TSV_SERVER_SOCKET_PATH, + TSV_SHMEM_PREFIX, + timeout, + timeout, + timeout, + timeout, + timeout, + ) + .expect("connect to texture-share-vk server") +} + +/// Image format for texture-share-vk, matching RGBA8. +pub const TSV_FORMAT: texture_share_vk_base::ipc::platform::img_data::ImgFormat = + texture_share_vk_base::ipc::platform::img_data::ImgFormat::R8G8B8A8; + +/// Extracts the raw vk::Image handle from a wgpu Texture. +/// +/// # Safety +/// The returned handle must not outlive the texture. +pub unsafe fn raw_image(texture: &wgpu::Texture) -> vk::Image { + let hal = texture + .as_hal::<wgpu_hal::api::Vulkan>() + .expect("vulkan HAL texture"); + hal.raw_handle() +} + +/// Creates a vk::Fence on the VkClient's device. +pub fn create_fence(client: &VkClient) -> vk::Fence { + let device = &client.get_vk_setup().device.device; + let create_info = vk::FenceCreateInfo::default(); + unsafe { device.create_fence(&create_info, None) }.expect("create fence") +} + +/// Destroys a vk::Fence. +pub fn destroy_fence(client: &VkClient, fence: vk::Fence) { + let device = &client.get_vk_setup().device.device; + unsafe { device.destroy_fence(fence, None) }; +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..22a55d8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod gpu; +pub mod osc; +pub mod renderer; +pub mod uniform; +pub mod window; @@ -1,11 +1,6 @@ use std::net::UdpSocket; -use rosc::{OscMessage, OscPacket, OscType}; - -pub enum OscCommand { - Shader(String), - Uniform { path: String, args: Vec<OscType> }, -} +use rosc::{OscMessage, OscPacket}; pub struct OscServer { socket: UdpSocket, @@ -23,15 +18,19 @@ impl OscServer { }) } - pub fn poll(&mut self, mut on_command: impl FnMut(OscCommand)) { + /// Drains all pending OSC messages, calling `on_message` for each. + /// Returns `true` if any messages were dispatched. + pub fn poll(&mut self, mut on_message: impl FnMut(OscMessage)) -> bool { + let mut received = false; 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)) => dispatch(packet, &mut on_command), + Ok((_, packet)) => dispatch(packet, &mut on_message), Err(e) => log::warn!("OSC decode error: {}", e), } + received = true; } Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break, Err(e) => { @@ -40,34 +39,37 @@ impl OscServer { } } } + received } -} -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); + /// Blocks until at least one OSC message arrives, then drains all pending. + pub fn recv(&mut self, mut on_message: impl FnMut(OscMessage)) -> bool { + self.socket.set_nonblocking(false).expect("set blocking"); + match self.socket.recv_from(&mut self.buf) { + Ok((size, _addr)) => { + let data = &self.buf[..size]; + match rosc::decoder::decode_udp(data) { + Ok((_, packet)) => dispatch(packet, &mut on_message), + Err(e) => log::warn!("OSC decode error: {}", e), + } + } + Err(e) => { + log::warn!("OSC recv error: {}", e); } } + self.socket.set_nonblocking(true).expect("set nonblocking"); + self.poll(on_message); + true } } -fn dispatch_message(msg: OscMessage, on_command: &mut impl FnMut(OscCommand)) { - let path = &msg.addr; - - if path == "/shader" { - match &msg.args[..] { - [OscType::String(code)] => on_command(OscCommand::Shader(code.clone())), - _ => log::warn!("/shader: expected single string argument"), +fn dispatch(packet: OscPacket, on_message: &mut impl FnMut(OscMessage)) { + match packet { + OscPacket::Message(msg) => on_message(msg), + OscPacket::Bundle(bundle) => { + for p in bundle.content { + dispatch(p, on_message); + } } - } 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 index e3a2acd..454e81a 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,5 +1,4 @@ -use crate::uniform::{UniformCache, UniformError}; -use rosc::OscType; +use crate::uniform::UniformCache; const CANVAS_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb; @@ -32,33 +31,34 @@ fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> { "; pub struct Renderer { + canvas: wgpu::Texture, canvas_view: wgpu::TextureView, vertex_module: wgpu::ShaderModule, vertex_bgl: wgpu::BindGroupLayout, vertex_bg: wgpu::BindGroup, render_pipeline: wgpu::RenderPipeline, - uniform_cache: UniformCache, + uniforms: 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 canvas = 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 + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + let canvas_view = canvas.create_view(&Default::default()); let vertex_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("vertex_shader"), @@ -101,106 +101,54 @@ impl Renderer { bytemuck::cast_slice(&[width as f32, height as f32]), ); - let mut uniform_cache = UniformCache::new(); + let mut uniforms = UniformCache::new(); let render_pipeline = build_pipeline( device, &vertex_module, &vertex_bgl, DEFAULT_FRAGMENT, - &mut uniform_cache, + &mut uniforms, ) .expect("default shader"); Self { + canvas, canvas_view, vertex_module, vertex_bgl, vertex_bg, render_pipeline, - uniform_cache, + uniforms, } } + pub fn canvas_texture(&self) -> &wgpu::Texture { + &self.canvas + } + pub fn canvas_view(&self) -> &wgpu::TextureView { &self.canvas_view } + pub fn uniforms(&mut self) -> &mut UniformCache { + &mut self.uniforms + } + 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, + &mut self.uniforms, )?; 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); + self.uniforms.flush(queue); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); @@ -225,7 +173,7 @@ impl Renderer { pass.set_pipeline(&self.render_pipeline); pass.set_bind_group(0, &self.vertex_bg, &[]); - if let Some(ref bg) = self.uniform_cache.bind_group { + if let Some(ref bg) = self.uniforms.bind_group { pass.set_bind_group(1, bg, &[]); } pass.draw(0..4, 0..1); diff --git a/src/uniform.rs b/src/uniform.rs index c6886b4..57a0a4b 100644 --- a/src/uniform.rs +++ b/src/uniform.rs @@ -397,12 +397,196 @@ pub fn types_compatible( } } -/// Manages uniform buffer data for all `var<uniform>` globals in a shader. +/// A texture input slot, backed by a wgpu::Texture that can be fed from TSV. +pub struct TextureSlot { + name: String, + binding: u32, + tsv_name: Option<String>, + tsv_registered: bool, + texture: wgpu::Texture, + view: wgpu::TextureView, + width: u32, + height: u32, + view_dimension: wgpu::TextureViewDimension, + sample_type: wgpu::TextureSampleType, + multisampled: bool, +} + +impl TextureSlot { + fn new( + device: &wgpu::Device, + name: String, + binding: u32, + view_dimension: wgpu::TextureViewDimension, + sample_type: wgpu::TextureSampleType, + multisampled: bool, + ) -> Self { + let (texture, view) = create_input_texture(device, &name, 1, 1); + Self { + name, + binding, + tsv_name: None, + tsv_registered: false, + texture, + view, + width: 1, + height: 1, + view_dimension, + sample_type, + multisampled, + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn tsv_name(&self) -> Option<&str> { + self.tsv_name.as_deref() + } + + pub fn set_tsv_name(&mut self, name: String) { + if self.tsv_name.as_deref() != Some(&name) { + self.tsv_name = Some(name); + self.tsv_registered = false; + } + } + + pub fn tsv_registered(&self) -> bool { + self.tsv_registered + } + + pub fn set_tsv_registered(&mut self) { + self.tsv_registered = true; + } + + pub fn texture(&self) -> &wgpu::Texture { + &self.texture + } + + pub fn width(&self) -> u32 { + self.width + } + + pub fn height(&self) -> u32 { + self.height + } + + /// Recreate the texture at new dimensions. Returns true if the size actually changed. + pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) -> bool { + if self.width == width && self.height == height { + return false; + } + let (texture, view) = create_input_texture(device, &self.name, width, height); + self.texture = texture; + self.view = view; + self.width = width; + self.height = height; + true + } +} + +fn create_input_texture( + device: &wgpu::Device, + label: &str, + width: u32, + height: u32, +) -> (wgpu::Texture, wgpu::TextureView) { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some(label), + 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 view = texture.create_view(&Default::default()); + (texture, view) +} + +pub struct SamplerSlot { + name: String, + binding: u32, + sampler: wgpu::Sampler, + binding_type: wgpu::SamplerBindingType, +} + +impl SamplerSlot { + pub fn name(&self) -> &str { + &self.name + } + + /// Recreate the sampler with new filter and address modes. + pub fn configure( + &mut self, + device: &wgpu::Device, + filter: wgpu::FilterMode, + address_mode: wgpu::AddressMode, + ) { + self.sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some(&self.name), + mag_filter: filter, + min_filter: filter, + mipmap_filter: match filter { + wgpu::FilterMode::Linear => wgpu::MipmapFilterMode::Linear, + wgpu::FilterMode::Nearest => wgpu::MipmapFilterMode::Nearest, + }, + address_mode_u: address_mode, + address_mode_v: address_mode, + address_mode_w: address_mode, + ..Default::default() + }); + } +} + +fn image_view_dimension(dim: naga::ImageDimension, arrayed: bool) -> wgpu::TextureViewDimension { + match (dim, arrayed) { + (naga::ImageDimension::D1, false) => wgpu::TextureViewDimension::D1, + (naga::ImageDimension::D2, false) => wgpu::TextureViewDimension::D2, + (naga::ImageDimension::D2, true) => wgpu::TextureViewDimension::D2Array, + (naga::ImageDimension::D3, false) => wgpu::TextureViewDimension::D3, + (naga::ImageDimension::Cube, false) => wgpu::TextureViewDimension::Cube, + (naga::ImageDimension::Cube, true) => wgpu::TextureViewDimension::CubeArray, + _ => wgpu::TextureViewDimension::D2, + } +} + +fn image_sample_type(class: naga::ImageClass) -> wgpu::TextureSampleType { + match class { + naga::ImageClass::Sampled { kind, .. } => match kind { + naga::ScalarKind::Float => wgpu::TextureSampleType::Float { filterable: true }, + naga::ScalarKind::Sint => wgpu::TextureSampleType::Sint, + naga::ScalarKind::Uint => wgpu::TextureSampleType::Uint, + _ => wgpu::TextureSampleType::Float { filterable: true }, + }, + naga::ImageClass::Depth { .. } => wgpu::TextureSampleType::Depth, + naga::ImageClass::Storage { .. } | naga::ImageClass::External => { + wgpu::TextureSampleType::Float { filterable: false } + } + } +} + +fn image_multisampled(class: naga::ImageClass) -> bool { + matches!( + class, + naga::ImageClass::Sampled { multi: true, .. } | naga::ImageClass::Depth { multi: true } + ) +} + +/// Manages uniform buffer data and texture/sampler bindings for all globals in a shader. pub struct UniformCache { pub module: naga::Module, pub layouter: Layouter, uniforms: HashMap<String, UniformMember>, buffers: Vec<BufferState>, + textures: Vec<TextureSlot>, + samplers: Vec<SamplerSlot>, pub bind_group_layout: Option<wgpu::BindGroupLayout>, pub bind_group: Option<wgpu::BindGroup>, dirty: bool, @@ -416,12 +600,17 @@ struct UniformMember { } struct BufferState { - group: u32, binding: u32, data: Vec<u8>, gpu_buffer: Option<wgpu::Buffer>, } +impl Default for UniformCache { + fn default() -> Self { + Self::new() + } +} + impl UniformCache { pub fn new() -> Self { Self { @@ -429,13 +618,16 @@ impl UniformCache { layouter: Layouter::default(), uniforms: HashMap::new(), buffers: Vec::new(), + textures: Vec::new(), + samplers: Vec::new(), bind_group_layout: None, bind_group: None, dirty: false, } } - /// Rebuild from a new shader module, transferring compatible uniform values. + /// Rebuild from a new shader module, transferring compatible uniform values + /// and texture associations. pub fn refresh(&mut self, mut new_module: naga::Module, device: &wgpu::Device) { ensure_component_types(&mut new_module); let mut new_layouter = Layouter::default(); @@ -444,43 +636,108 @@ impl UniformCache { 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); + let old_textures = std::mem::take(&mut self.textures); let mut new_uniforms: HashMap<String, UniformMember> = HashMap::new(); let mut new_buffers: Vec<BufferState> = Vec::new(); + let mut new_textures: Vec<TextureSlot> = Vec::new(); + let mut new_samplers: Vec<SamplerSlot> = Vec::new(); for (_handle, var) in new_module.global_variables.iter() { - if var.space != naga::AddressSpace::Uniform { - continue; - } let binding = match &var.binding { Some(b) => b, None => continue, }; - let layout = new_layouter[var.ty]; - let buffer_idx = new_buffers.len(); + match var.space { + naga::AddressSpace::Uniform => { + let layout = new_layouter[var.ty]; + let buffer_idx = new_buffers.len(); - let data = vec![0u8; layout.size as usize]; - new_buffers.push(BufferState { - group: binding.group, - binding: binding.binding, - data, - gpu_buffer: None, - }); + let data = vec![0u8; layout.size as usize]; + new_buffers.push(BufferState { + binding: binding.binding, + data, + gpu_buffer: None, + }); - if let Some(ref name) = var.name { - new_uniforms.insert( - name.clone(), - UniformMember { - buffer_idx, - ty: var.ty, - offset: 0, - size: layout.size as usize, - }, - ); + if let Some(ref name) = var.name { + new_uniforms.insert( + name.clone(), + UniformMember { + buffer_idx, + ty: var.ty, + offset: 0, + size: layout.size as usize, + }, + ); + } + } + naga::AddressSpace::Handle => { + let ty_inner = &new_module.types[var.ty].inner; + match *ty_inner { + naga::TypeInner::Image { + dim, + arrayed, + class, + } => { + let name = var.name.clone().unwrap_or_default(); + let view_dimension = image_view_dimension(dim, arrayed); + let sample_type = image_sample_type(class); + let multisampled = image_multisampled(class); + + let mut slot = TextureSlot::new( + device, + name.clone(), + binding.binding, + view_dimension, + sample_type, + multisampled, + ); + + // Transfer TSV association from old texture with same name + if let Some(old) = old_textures.iter().find(|t| t.name == name) { + if old.view_dimension == slot.view_dimension + && old.sample_type == slot.sample_type + { + slot.tsv_name = old.tsv_name.clone(); + slot.tsv_registered = old.tsv_registered; + if old.width > 1 || old.height > 1 { + slot.resize(device, old.width, old.height); + } + } + } + + new_textures.push(slot); + } + naga::TypeInner::Sampler { comparison } => { + let binding_type = if comparison { + wgpu::SamplerBindingType::Comparison + } else { + wgpu::SamplerBindingType::Filtering + }; + let name = var.name.clone().unwrap_or_default(); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some(&name), + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + new_samplers.push(SamplerSlot { + name, + binding: binding.binding, + sampler, + binding_type, + }); + } + _ => {} + } + } + _ => {} } } + // Transfer compatible buffer data from old shader for (name, new_member) in &new_uniforms { if let Some(old_member) = old_uniforms.get(name) { if old_member.size == new_member.size @@ -495,9 +752,10 @@ impl UniformCache { } } + // Create GPU buffers for buf in &mut new_buffers { let gpu_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some(&format!("uniform@{}:{}", buf.group, buf.binding)), + label: Some(&format!("uniform@{}", buf.binding)), size: buf.data.len() as u64, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, @@ -505,54 +763,118 @@ impl UniformCache { buf.gpu_buffer = Some(gpu_buffer); } - let bind_group_layout = if !new_buffers.is_empty() { - let entries: Vec<wgpu::BindGroupLayoutEntry> = new_buffers - .iter() - .map(|buf| wgpu::BindGroupLayoutEntry { - binding: buf.binding, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }) - .collect(); - Some( - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("uniform_bind_group_layout"), - entries: &entries, - }), - ) - } else { - None - }; - - let bind_group = bind_group_layout.as_ref().map(|layout| { - let entries: Vec<wgpu::BindGroupEntry> = new_buffers - .iter() - .map(|buf| wgpu::BindGroupEntry { - binding: buf.binding, - resource: buf.gpu_buffer.as_ref().unwrap().as_entire_binding(), - }) - .collect(); - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("uniform_bind_group"), - layout, - entries: &entries, - }) - }); - self.module = new_module; self.layouter = new_layouter; self.uniforms = new_uniforms; self.buffers = new_buffers; - self.bind_group_layout = bind_group_layout; - self.bind_group = bind_group; + self.textures = new_textures; + self.samplers = new_samplers; + self.build_bind_group(device); self.dirty = true; } + pub fn texture_slot_mut(&mut self, name: &str) -> Option<&mut TextureSlot> { + self.textures.iter_mut().find(|t| t.name == name) + } + + pub fn texture_slots_mut(&mut self) -> &mut [TextureSlot] { + &mut self.textures + } + + pub fn sampler_slot_mut(&mut self, name: &str) -> Option<&mut SamplerSlot> { + self.samplers.iter_mut().find(|s| s.name == name) + } + + /// Rebuild the bind group (e.g. after a texture resize). + pub fn rebuild_bind_group(&mut self, device: &wgpu::Device) { + self.build_bind_group(device); + } + + fn build_bind_group(&mut self, device: &wgpu::Device) { + let has_bindings = + !self.buffers.is_empty() || !self.textures.is_empty() || !self.samplers.is_empty(); + + if !has_bindings { + self.bind_group_layout = None; + self.bind_group = None; + return; + } + + let mut layout_entries: Vec<wgpu::BindGroupLayoutEntry> = Vec::new(); + + for buf in &self.buffers { + layout_entries.push(wgpu::BindGroupLayoutEntry { + binding: buf.binding, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }); + } + + for tex in &self.textures { + layout_entries.push(wgpu::BindGroupLayoutEntry { + binding: tex.binding, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: tex.sample_type, + view_dimension: tex.view_dimension, + multisampled: tex.multisampled, + }, + count: None, + }); + } + + for smp in &self.samplers { + layout_entries.push(wgpu::BindGroupLayoutEntry { + binding: smp.binding, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(smp.binding_type), + count: None, + }); + } + + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("uniform_bind_group_layout"), + entries: &layout_entries, + }); + + let mut bg_entries: Vec<wgpu::BindGroupEntry> = Vec::new(); + + for buf in &self.buffers { + bg_entries.push(wgpu::BindGroupEntry { + binding: buf.binding, + resource: buf.gpu_buffer.as_ref().unwrap().as_entire_binding(), + }); + } + + for tex in &self.textures { + bg_entries.push(wgpu::BindGroupEntry { + binding: tex.binding, + resource: wgpu::BindingResource::TextureView(&tex.view), + }); + } + + for smp in &self.samplers { + bg_entries.push(wgpu::BindGroupEntry { + binding: smp.binding, + resource: wgpu::BindingResource::Sampler(&smp.sampler), + }); + } + + let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("uniform_bind_group"), + layout: &bgl, + entries: &bg_entries, + }); + + self.bind_group_layout = Some(bgl); + self.bind_group = Some(bg); + } + pub fn get(&mut self, name: &str) -> Option<UniformRef<'_>> { // Split the borrow: get member info first, then borrow buffer data let member_info = self.uniforms.get(name)?; |
