summaryrefslogtreecommitdiffstats
path: root/src
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
parentrefactor (diff)
downloadwgsl-view-5925137f3b41cb3a87f098604ef482bced91341e.tar.gz
wgsl-view-5925137f3b41cb3a87f098604ef482bced91341e.zip
tsv integration, split binaries
Diffstat (limited to 'src')
-rw-r--r--src/bin/tsv_video_stream.rs185
-rw-r--r--src/bin/tsv_view.rs (renamed from src/main.rs)166
-rw-r--r--src/bin/wgsl_render.rs281
-rw-r--r--src/gpu.rs141
-rw-r--r--src/lib.rs5
-rw-r--r--src/osc.rs60
-rw-r--r--src/renderer.rs122
-rw-r--r--src/uniform.rs456
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;
diff --git a/src/osc.rs b/src/osc.rs
index 6759704..f9a8df1 100644
--- a/src/osc.rs
+++ b/src/osc.rs
@@ -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)?;