summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2026-04-22 13:35:18 +0000
committers-ol <s+removethis@s-ol.nu>2026-05-14 14:46:13 +0000
commit9fd2105289ccc5a7c12af56bad4d33ef768d0f16 (patch)
treea4e6ff2c64fd0f1d4c73d74b3d62327630ff6206 /src
parentadd tsv-video-buffer (diff)
downloadwgsl-view-9fd2105289ccc5a7c12af56bad4d33ef768d0f16.tar.gz
wgsl-view-9fd2105289ccc5a7c12af56bad4d33ef768d0f16.zip
samplers/textures as resources
Diffstat (limited to 'src')
-rw-r--r--src/bin/wgsl_render.rs122
-rw-r--r--src/gpu.rs34
-rw-r--r--src/osc.rs47
-rw-r--r--src/renderer.rs74
-rw-r--r--src/uniform.rs405
5 files changed, 500 insertions, 182 deletions
diff --git a/src/bin/wgsl_render.rs b/src/bin/wgsl_render.rs
index 29b5d5d..da28816 100644
--- a/src/bin/wgsl_render.rs
+++ b/src/bin/wgsl_render.rs
@@ -61,52 +61,35 @@ fn main() {
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"),
+ let handle_msg = |msg: rosc::OscMessage| -> Result<(), String> {
+ let path: Vec<_> = msg.addr.strip_prefix("/").unwrap().split('/').collect();
+
+ match (&path[..], &msg.args[..]) {
+ (["shader"], [OscType::String(code)]) => renderer.load_shader(&device, code),
+ (["uniform", rest @ ..], args) => {
+ set_uniform(renderer.uniforms(), rest, args).map_err(|e| e.to_string())
+ }
+ (["texture", id], [OscType::String(tsv_name)]) => {
+ renderer.create_texture_resource(&device, id, tsv_name);
+ Ok(())
}
- } 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}");
+ (["sampler", id], [OscType::String(filter), OscType::String(clamp)]) => {
+ let (filter_mode, address_mode) = parse_sampler_modes(filter, clamp)?;
+ renderer.create_sampler_resource(&device, id, filter_mode, address_mode);
+ Ok(())
}
- } 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)"),
+ (["texture", id, "destroy"], []) => {
+ renderer.destroy_texture_resource(&device, id);
+ Ok(())
}
- } 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)"),
+ (["sampler", id, "destroy"], []) => {
+ renderer.destroy_sampler_resource(&device, id);
+ Ok(())
}
- } else {
- log::debug!("unhandled OSC: {path}");
+ (["binding", name], [OscType::String(target)]) => {
+ bind_resource(&mut renderer, &device, name, target)
+ }
+ _ => Err(format!("unhandled OSC message {} {:?}", msg.addr, msg.args)),
}
};
@@ -118,7 +101,7 @@ fn main() {
};
if dirty {
- gpu::refresh_textures(renderer.uniforms(), &device, &mut client, fence);
+ renderer.refresh_textures(&device, &mut client, fence);
renderer.render(&device, &queue);
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
@@ -146,13 +129,13 @@ fn main() {
/// Set a uniform value from OSC args, navigating the path.
fn set_uniform(
cache: &mut wgsl_view::uniform::UniformCache,
- path: &str,
+ path: &[&str],
args: &[OscType],
-) -> Result<(), Box<dyn std::error::Error>> {
- let mut parts = path.split('/');
- let name = parts.next().ok_or("missing uniform name")?;
+) -> Result<(), UniformError> {
+ let mut parts = path.iter();
+ let name = parts.next().ok_or(UniformError::NotFound)?;
- let mut uref = cache.get(name).ok_or("uniform not found")?;
+ let mut uref = cache.get(name).ok_or(UniformError::NotFound)?;
for component in parts {
uref = uref.field(component)?;
}
@@ -198,38 +181,43 @@ fn set_uniform(
.collect::<Result<_, _>>()?;
uref.set_u32(&values)?;
}
- _ => return Err(Box::new(UniformError::TypeMismatch)),
+ _ => return Err(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,
+fn parse_sampler_modes(
filter: &str,
- clamp: &str,
-) -> Result<(), String> {
+ address: &str,
+) -> Result<(wgpu::FilterMode, wgpu::AddressMode), String> {
let filter_mode = match filter {
"linear" => wgpu::FilterMode::Linear,
"nearest" => wgpu::FilterMode::Nearest,
- _ => return Err(format!("unknown filter mode '{filter}' (linear|nearest)")),
+ _ => return Err(format!("unknown filter mode '{filter}'")),
};
- let address_mode = match clamp {
+ let address_mode = match address {
"clamp" => wgpu::AddressMode::ClampToEdge,
"repeat" => wgpu::AddressMode::Repeat,
"mirror" => wgpu::AddressMode::MirrorRepeat,
- _ => return Err(format!("unknown clamp mode '{clamp}' (clamp|repeat|mirror)")),
+ _ => return Err(format!("unknown address mode mode '{address}'")),
};
- 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(())
+ Ok((filter_mode, address_mode))
}
-
+fn bind_resource(
+ renderer: &mut Renderer,
+ device: &wgpu::Device,
+ name: &str,
+ target: &str,
+) -> Result<(), String> {
+ if let Some(id) = target.strip_prefix("/texture/") {
+ renderer.bind_texture_resource(device, name, id)?;
+ Ok(())
+ } else if let Some(id) = target.strip_prefix("/sampler/") {
+ renderer.bind_sampler_resource(device, name, id)?;
+ Ok(())
+ } else {
+ Err(format!("invalid binding target '{target}'"))
+ }
+}
diff --git a/src/gpu.rs b/src/gpu.rs
index 993e524..edcc073 100644
--- a/src/gpu.rs
+++ b/src/gpu.rs
@@ -178,20 +178,20 @@ pub fn destroy_fence(client: &VkClient, fence: vk::Fence) {
/// Refresh all texture inputs from TSV shared images.
/// Handles format detection and texture recreation internally.
pub fn refresh_textures(
- cache: &mut crate::uniform::UniformCache,
+ pools: &mut crate::uniform::ResourcePools,
device: &wgpu::Device,
client: &mut VkClient,
fence: vk::Fence,
-) {
+) -> bool {
let mut needs_rebind = false;
- for slot in cache.texture_slots_mut() {
- let tsv_name = match slot.tsv_name() {
+ for texture in pools.textures_mut() {
+ let tsv_name = match texture.tsv_name() {
Some(n) => n.to_string(),
None => continue,
};
- if !slot.tsv_registered() {
+ if !texture.tsv_registered() {
if let Err(e) = client.find_image(&tsv_name, true) {
log::debug!("tsv find '{tsv_name}': {e}");
continue;
@@ -200,11 +200,14 @@ pub fn refresh_textures(
match client.find_image_data(&tsv_name, true) {
Ok(Some((_lock, data))) => {
let format = img_format_to_wgpu(data.format);
- if slot.resize(
+ let view_dimension =
+ img_type_to_view_dimension(data.image_type, data.depth_or_array_layers);
+ if texture.resize(
device,
data.width,
data.height,
data.depth_or_array_layers,
+ view_dimension,
format,
) {
needs_rebind = true;
@@ -213,10 +216,10 @@ pub fn refresh_textures(
_ => continue,
}
- slot.set_tsv_registered();
+ texture.set_tsv_registered();
}
- let raw = unsafe { raw_image(slot.texture()) };
+ let raw = unsafe { raw_image(texture.texture()) };
match client.recv_image(
&tsv_name,
raw,
@@ -224,13 +227,22 @@ pub fn refresh_textures(
vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
fence,
) {
- Ok(Some(())) => log::trace!("texture '{}' updated from '{tsv_name}'", slot.name()),
+ Ok(Some(())) => log::trace!("texture '{}' updated from '{tsv_name}'", texture.id()),
Ok(None) => {}
Err(e) => log::warn!("recv_image '{tsv_name}': {e}"),
}
}
- if needs_rebind {
- cache.rebuild_bind_group(device);
+ needs_rebind
+}
+
+fn img_type_to_view_dimension(
+ image_type: ImgType,
+ depth_or_array_layers: u32,
+) -> wgpu::TextureViewDimension {
+ match image_type {
+ ImgType::D3 => wgpu::TextureViewDimension::D3,
+ ImgType::D2 if depth_or_array_layers > 1 => wgpu::TextureViewDimension::D2Array,
+ _ => wgpu::TextureViewDimension::D2,
}
}
diff --git a/src/osc.rs b/src/osc.rs
index f9a8df1..fdbcb0e 100644
--- a/src/osc.rs
+++ b/src/osc.rs
@@ -1,4 +1,4 @@
-use std::net::UdpSocket;
+use std::{fmt::Display, net::UdpSocket};
use rosc::{OscMessage, OscPacket};
@@ -20,16 +20,24 @@ impl OscServer {
/// 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 {
+ pub fn poll<E: Display>(
+ &mut self,
+ mut on_message: impl FnMut(OscMessage) -> Result<(), E>,
+ ) -> 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_message),
- Err(e) => log::warn!("OSC decode error: {}", e),
- }
+ let res = rosc::decoder::decode_udp(data)
+ .map_err(|e| format!("OSC edecode error: {}", e))
+ .and_then(|(_, packet)| {
+ dispatch(packet, &mut on_message).map_err(|e| e.to_string())
+ });
+ if let Err(e) = res {
+ log::warn!("{}", e);
+ };
+
received = true;
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
@@ -43,15 +51,22 @@ impl OscServer {
}
/// Blocks until at least one OSC message arrives, then drains all pending.
- pub fn recv(&mut self, mut on_message: impl FnMut(OscMessage)) -> bool {
+ pub fn recv<E: Display>(
+ &mut self,
+ mut on_message: impl FnMut(OscMessage) -> Result<(), E>,
+ ) -> 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),
- }
+ let res = rosc::decoder::decode_udp(data)
+ .map_err(|e| format!("OSC edecode error: {}", e))
+ .and_then(|(_, packet)| {
+ dispatch(packet, &mut on_message).map_err(|e| e.to_string())
+ });
+ if let Err(e) = res {
+ log::warn!("{}", e);
+ };
}
Err(e) => {
log::warn!("OSC recv error: {}", e);
@@ -63,13 +78,17 @@ impl OscServer {
}
}
-fn dispatch(packet: OscPacket, on_message: &mut impl FnMut(OscMessage)) {
+fn dispatch<E>(
+ packet: OscPacket,
+ on_message: &mut impl FnMut(OscMessage) -> Result<(), E>,
+) -> Result<(), E> {
match packet {
- OscPacket::Message(msg) => on_message(msg),
+ OscPacket::Message(msg) => on_message(msg)?,
OscPacket::Bundle(bundle) => {
for p in bundle.content {
- dispatch(p, on_message);
+ dispatch(p, on_message)?;
}
}
}
+ Ok(())
}
diff --git a/src/renderer.rs b/src/renderer.rs
index 454e81a..3717945 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -1,4 +1,5 @@
-use crate::uniform::UniformCache;
+use crate::gpu;
+use crate::uniform::{ResourcePools, UniformCache};
const CANVAS_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
@@ -37,6 +38,7 @@ pub struct Renderer {
vertex_bgl: wgpu::BindGroupLayout,
vertex_bg: wgpu::BindGroup,
render_pipeline: wgpu::RenderPipeline,
+ resources: ResourcePools,
uniforms: UniformCache,
}
@@ -101,12 +103,14 @@ impl Renderer {
bytemuck::cast_slice(&[width as f32, height as f32]),
);
+ let resources = ResourcePools::new();
let mut uniforms = UniformCache::new();
let render_pipeline = build_pipeline(
device,
&vertex_module,
&vertex_bgl,
DEFAULT_FRAGMENT,
+ &resources,
&mut uniforms,
)
.expect("default shader");
@@ -118,6 +122,7 @@ impl Renderer {
vertex_bgl,
vertex_bg,
render_pipeline,
+ resources,
uniforms,
}
}
@@ -140,6 +145,7 @@ impl Renderer {
&self.vertex_module,
&self.vertex_bgl,
source,
+ &self.resources,
&mut self.uniforms,
)?;
self.render_pipeline = pipeline;
@@ -147,6 +153,70 @@ impl Renderer {
Ok(())
}
+ pub fn create_texture_resource(&mut self, device: &wgpu::Device, id: &str, tsv_name: &str) {
+ self.resources.create_texture(device, id, tsv_name);
+ self.uniforms.rebuild_bind_group(device, &self.resources);
+ }
+
+ pub fn destroy_texture_resource(&mut self, device: &wgpu::Device, id: &str) {
+ let removed = self.resources.destroy_texture(id);
+ let unbound = self.uniforms.unbind_texture_resource(id);
+ if removed || unbound {
+ self.uniforms.rebuild_bind_group(device, &self.resources);
+ }
+ }
+
+ pub fn create_sampler_resource(
+ &mut self,
+ device: &wgpu::Device,
+ id: &str,
+ filter: wgpu::FilterMode,
+ address_mode: wgpu::AddressMode,
+ ) {
+ self.resources
+ .create_sampler(device, id, filter, address_mode);
+ self.uniforms.rebuild_bind_group(device, &self.resources);
+ }
+
+ pub fn destroy_sampler_resource(&mut self, device: &wgpu::Device, id: &str) {
+ let removed = self.resources.destroy_sampler(id);
+ let unbound = self.uniforms.unbind_sampler_resource(id);
+ if removed || unbound {
+ self.uniforms.rebuild_bind_group(device, &self.resources);
+ }
+ }
+
+ pub fn bind_texture_resource(
+ &mut self,
+ device: &wgpu::Device,
+ name: &str,
+ id: &str,
+ ) -> Result<(), String> {
+ self.uniforms
+ .bind_texture_resource(device, &self.resources, name, id)
+ }
+
+ pub fn bind_sampler_resource(
+ &mut self,
+ device: &wgpu::Device,
+ name: &str,
+ id: &str,
+ ) -> Result<(), String> {
+ self.uniforms
+ .bind_sampler_resource(device, &self.resources, name, id)
+ }
+
+ pub fn refresh_textures(
+ &mut self,
+ device: &wgpu::Device,
+ client: &mut texture_share_vk_client::VkClient,
+ fence: ash::vk::Fence,
+ ) {
+ if gpu::refresh_textures(&mut self.resources, device, client, fence) {
+ self.uniforms.rebuild_bind_group(device, &self.resources);
+ }
+ }
+
pub fn render(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
self.uniforms.flush(queue);
@@ -188,6 +258,7 @@ fn build_pipeline(
vertex_module: &wgpu::ShaderModule,
vertex_bgl: &wgpu::BindGroupLayout,
fragment_source: &str,
+ resources: &ResourcePools,
cache: &mut UniformCache,
) -> Result<wgpu::RenderPipeline, String> {
let module = naga::front::wgsl::parse_str(fragment_source)
@@ -201,6 +272,7 @@ fn build_pipeline(
.map_err(|e| format!("WGSL validation error: {e}"))?;
cache.refresh(module, device);
+ cache.rebuild_bind_group(device, resources);
let fragment_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("fragment_shader"),
diff --git a/src/uniform.rs b/src/uniform.rs
index 44fb541..8f8b172 100644
--- a/src/uniform.rs
+++ b/src/uniform.rs
@@ -397,10 +397,9 @@ pub fn types_compatible(
}
}
-/// A texture input slot, backed by a wgpu::Texture that can be fed from TSV.
-pub struct TextureSlot {
- name: String,
- binding: u32,
+/// A pooled TSV texture resource.
+pub struct TextureResource {
+ id: String,
tsv_name: Option<String>,
tsv_registered: bool,
texture: wgpu::Texture,
@@ -414,23 +413,18 @@ pub struct TextureSlot {
multisampled: bool,
}
-impl TextureSlot {
- fn new(
- device: &wgpu::Device,
- name: String,
- binding: u32,
- view_dimension: wgpu::TextureViewDimension,
- sample_type: wgpu::TextureSampleType,
- multisampled: bool,
- ) -> Self {
+impl TextureResource {
+ pub fn new(device: &wgpu::Device, id: String, tsv_name: String) -> Self {
let format = wgpu::TextureFormat::Rgba8UnormSrgb;
+ let view_dimension = wgpu::TextureViewDimension::D2;
+ let sample_type = wgpu::TextureSampleType::Float { filterable: true };
+ let multisampled = false;
let dimension = view_dimension_to_texture_dimension(view_dimension);
let (texture, view) =
- create_input_texture(device, &name, 1, 1, 1, dimension, view_dimension, format);
+ create_input_texture(device, &id, 1, 1, 1, dimension, view_dimension, format);
Self {
- name,
- binding,
- tsv_name: None,
+ id,
+ tsv_name: Some(tsv_name),
tsv_registered: false,
texture,
view,
@@ -444,8 +438,8 @@ impl TextureSlot {
}
}
- pub fn name(&self) -> &str {
- &self.name
+ pub fn id(&self) -> &str {
+ &self.id
}
pub fn tsv_name(&self) -> Option<&str> {
@@ -471,6 +465,22 @@ impl TextureSlot {
&self.texture
}
+ pub fn view(&self) -> &wgpu::TextureView {
+ &self.view
+ }
+
+ pub fn view_dimension(&self) -> wgpu::TextureViewDimension {
+ self.view_dimension
+ }
+
+ pub fn sample_type(&self) -> wgpu::TextureSampleType {
+ self.sample_type
+ }
+
+ pub fn multisampled(&self) -> bool {
+ self.multisampled
+ }
+
pub fn width(&self) -> u32 {
self.width
}
@@ -486,24 +496,26 @@ impl TextureSlot {
width: u32,
height: u32,
depth_or_array_layers: u32,
+ view_dimension: wgpu::TextureViewDimension,
format: wgpu::TextureFormat,
) -> bool {
if self.width == width
&& self.height == height
&& self.depth_or_array_layers == depth_or_array_layers
+ && self.view_dimension == view_dimension
&& self.format == format
{
return false;
}
- let dimension = view_dimension_to_texture_dimension(self.view_dimension);
+ let dimension = view_dimension_to_texture_dimension(view_dimension);
let (texture, view) = create_input_texture(
device,
- &self.name,
+ &self.id,
width,
height,
depth_or_array_layers,
dimension,
- self.view_dimension,
+ view_dimension,
format,
);
self.texture = texture;
@@ -512,6 +524,9 @@ impl TextureSlot {
self.width = width;
self.height = height;
self.depth_or_array_layers = depth_or_array_layers;
+ self.view_dimension = view_dimension;
+ self.sample_type = wgpu::TextureSampleType::Float { filterable: true };
+ self.multisampled = false;
true
}
}
@@ -558,16 +573,32 @@ fn create_input_texture(
(texture, view)
}
-pub struct SamplerSlot {
- name: String,
- binding: u32,
+pub struct SamplerResource {
+ id: String,
sampler: wgpu::Sampler,
- binding_type: wgpu::SamplerBindingType,
}
-impl SamplerSlot {
- pub fn name(&self) -> &str {
- &self.name
+impl SamplerResource {
+ pub fn new(
+ device: &wgpu::Device,
+ id: String,
+ filter: wgpu::FilterMode,
+ address_mode: wgpu::AddressMode,
+ ) -> Self {
+ let mut this = Self {
+ sampler: create_sampler(device, &id, filter, address_mode),
+ id,
+ };
+ this.configure(device, filter, address_mode);
+ this
+ }
+
+ pub fn id(&self) -> &str {
+ &self.id
+ }
+
+ pub fn sampler(&self) -> &wgpu::Sampler {
+ &self.sampler
}
/// Recreate the sampler with new filter and address modes.
@@ -577,22 +608,111 @@ impl SamplerSlot {
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()
- });
+ self.sampler = create_sampler(device, &self.id, filter, address_mode);
+ }
+}
+
+fn create_sampler(
+ device: &wgpu::Device,
+ label: &str,
+ filter: wgpu::FilterMode,
+ address_mode: wgpu::AddressMode,
+) -> wgpu::Sampler {
+ device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some(label),
+ 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()
+ })
+}
+
+pub struct ResourcePools {
+ textures: HashMap<String, TextureResource>,
+ samplers: HashMap<String, SamplerResource>,
+}
+
+impl Default for ResourcePools {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl ResourcePools {
+ pub fn new() -> Self {
+ Self {
+ textures: HashMap::new(),
+ samplers: HashMap::new(),
+ }
+ }
+
+ pub fn create_texture(&mut self, device: &wgpu::Device, id: &str, tsv_name: &str) {
+ self.textures.insert(
+ id.to_string(),
+ TextureResource::new(device, id.to_string(), tsv_name.to_string()),
+ );
+ }
+
+ pub fn destroy_texture(&mut self, id: &str) -> bool {
+ self.textures.remove(id).is_some()
+ }
+
+ pub fn texture(&self, id: &str) -> Option<&TextureResource> {
+ self.textures.get(id)
+ }
+
+ pub fn texture_mut(&mut self, id: &str) -> Option<&mut TextureResource> {
+ self.textures.get_mut(id)
+ }
+
+ pub fn textures_mut(&mut self) -> impl Iterator<Item = &mut TextureResource> {
+ self.textures.values_mut()
+ }
+
+ pub fn create_sampler(
+ &mut self,
+ device: &wgpu::Device,
+ id: &str,
+ filter: wgpu::FilterMode,
+ address_mode: wgpu::AddressMode,
+ ) {
+ self.samplers.insert(
+ id.to_string(),
+ SamplerResource::new(device, id.to_string(), filter, address_mode),
+ );
+ }
+
+ pub fn destroy_sampler(&mut self, id: &str) -> bool {
+ self.samplers.remove(id).is_some()
+ }
+
+ pub fn sampler(&self, id: &str) -> Option<&SamplerResource> {
+ self.samplers.get(id)
}
}
+struct TextureBindingSlot {
+ name: String,
+ binding: u32,
+ view_dimension: wgpu::TextureViewDimension,
+ sample_type: wgpu::TextureSampleType,
+ multisampled: bool,
+ resource_id: Option<String>,
+}
+
+struct SamplerBindingSlot {
+ name: String,
+ binding: u32,
+ binding_type: wgpu::SamplerBindingType,
+ resource_id: Option<String>,
+}
+
fn image_view_dimension(dim: naga::ImageDimension, arrayed: bool) -> wgpu::TextureViewDimension {
match (dim, arrayed) {
(naga::ImageDimension::D1, false) => wgpu::TextureViewDimension::D1,
@@ -633,8 +753,8 @@ pub struct UniformCache {
pub layouter: Layouter,
uniforms: HashMap<String, UniformMember>,
buffers: Vec<BufferState>,
- textures: Vec<TextureSlot>,
- samplers: Vec<SamplerSlot>,
+ textures: Vec<TextureBindingSlot>,
+ samplers: Vec<SamplerBindingSlot>,
pub bind_group_layout: Option<wgpu::BindGroupLayout>,
pub bind_group: Option<wgpu::BindGroup>,
dirty: bool,
@@ -685,11 +805,12 @@ impl UniformCache {
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 old_samplers = std::mem::take(&mut self.samplers);
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();
+ let mut new_textures: Vec<TextureBindingSlot> = Vec::new();
+ let mut new_samplers: Vec<SamplerBindingSlot> = Vec::new();
for (_handle, var) in new_module.global_variables.iter() {
let binding = match &var.binding {
@@ -734,34 +855,21 @@ impl UniformCache {
let sample_type = image_sample_type(class);
let multisampled = image_multisampled(class);
- let mut slot = TextureSlot::new(
- device,
- name.clone(),
- binding.binding,
+ let mut slot = TextureBindingSlot {
+ name: name.clone(),
+ binding: binding.binding,
view_dimension,
sample_type,
multisampled,
- );
+ resource_id: None,
+ };
- // 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
+ && old.multisampled == slot.multisampled
{
- slot.tsv_name = old.tsv_name.clone();
- slot.tsv_registered = old.tsv_registered;
- if old.width > 1
- || old.height > 1
- || old.depth_or_array_layers > 1
- {
- slot.resize(
- device,
- old.width,
- old.height,
- old.depth_or_array_layers,
- old.format,
- );
- }
+ slot.resource_id = old.resource_id.clone();
}
}
@@ -774,18 +882,18 @@ impl UniformCache {
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 {
+ let mut slot = SamplerBindingSlot {
name,
binding: binding.binding,
- sampler,
binding_type,
- });
+ resource_id: None,
+ };
+ if let Some(old) = old_samplers.iter().find(|s| {
+ s.name == slot.name && s.binding_type == slot.binding_type
+ }) {
+ slot.resource_id = old.resource_id.clone();
+ }
+ new_samplers.push(slot);
}
_ => {}
}
@@ -826,28 +934,77 @@ impl UniformCache {
self.buffers = new_buffers;
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 bind_texture_resource(
+ &mut self,
+ device: &wgpu::Device,
+ pools: &ResourcePools,
+ name: &str,
+ resource_id: &str,
+ ) -> Result<(), String> {
+ if pools.texture(resource_id).is_none() {
+ return Err(format!("texture '{resource_id}' not found"));
+ }
+ let slot = self
+ .textures
+ .iter_mut()
+ .find(|t| t.name == name)
+ .ok_or_else(|| format!("texture binding '{name}' not found"))?;
+ slot.resource_id = Some(resource_id.to_string());
+ self.build_bind_group(device, pools);
+ Ok(())
}
- pub fn texture_slots_mut(&mut self) -> &mut [TextureSlot] {
- &mut self.textures
+ pub fn bind_sampler_resource(
+ &mut self,
+ device: &wgpu::Device,
+ pools: &ResourcePools,
+ name: &str,
+ resource_id: &str,
+ ) -> Result<(), String> {
+ if pools.sampler(resource_id).is_none() {
+ return Err(format!("sampler '{resource_id}' not found"));
+ }
+ let slot = self
+ .samplers
+ .iter_mut()
+ .find(|s| s.name == name)
+ .ok_or_else(|| format!("sampler binding '{name}' not found"))?;
+ slot.resource_id = Some(resource_id.to_string());
+ self.build_bind_group(device, pools);
+ Ok(())
}
- pub fn sampler_slot_mut(&mut self, name: &str) -> Option<&mut SamplerSlot> {
- self.samplers.iter_mut().find(|s| s.name == name)
+ pub fn unbind_texture_resource(&mut self, id: &str) -> bool {
+ let mut changed = false;
+ for slot in &mut self.textures {
+ if slot.resource_id.as_deref() == Some(id) {
+ slot.resource_id = None;
+ changed = true;
+ }
+ }
+ changed
+ }
+
+ pub fn unbind_sampler_resource(&mut self, id: &str) -> bool {
+ let mut changed = false;
+ for slot in &mut self.samplers {
+ if slot.resource_id.as_deref() == Some(id) {
+ slot.resource_id = None;
+ changed = true;
+ }
+ }
+ changed
}
/// 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);
+ pub fn rebuild_bind_group(&mut self, device: &wgpu::Device, pools: &ResourcePools) {
+ self.build_bind_group(device, pools);
}
- fn build_bind_group(&mut self, device: &wgpu::Device) {
+ fn build_bind_group(&mut self, device: &wgpu::Device, pools: &ResourcePools) {
let has_bindings =
!self.buffers.is_empty() || !self.textures.is_empty() || !self.samplers.is_empty();
@@ -900,6 +1057,29 @@ impl UniformCache {
});
let mut bg_entries: Vec<wgpu::BindGroupEntry> = Vec::new();
+ let fallback_textures_and_views: Vec<_> = self
+ .textures
+ .iter()
+ .map(|tex| create_placeholder_texture(device, tex))
+ .collect();
+ let fallback_samplers: Vec<_> = self
+ .samplers
+ .iter()
+ .map(|smp| {
+ if smp.binding_type == wgpu::SamplerBindingType::Comparison {
+ device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("comparison_sampler_fallback"),
+ compare: Some(wgpu::CompareFunction::LessEqual),
+ ..Default::default()
+ })
+ } else {
+ device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("sampler_fallback"),
+ ..Default::default()
+ })
+ }
+ })
+ .collect();
for buf in &self.buffers {
bg_entries.push(wgpu::BindGroupEntry {
@@ -908,17 +1088,36 @@ impl UniformCache {
});
}
- for tex in &self.textures {
+ for (idx, tex) in self.textures.iter().enumerate() {
+ let resource = tex
+ .resource_id
+ .as_deref()
+ .and_then(|id| pools.texture(id))
+ .filter(|res| texture_resource_matches(tex, res));
+ let texture_view = if let Some(resource) = resource {
+ resource.view()
+ } else {
+ &fallback_textures_and_views[idx].1
+ };
bg_entries.push(wgpu::BindGroupEntry {
binding: tex.binding,
- resource: wgpu::BindingResource::TextureView(&tex.view),
+ resource: wgpu::BindingResource::TextureView(texture_view),
});
}
- for smp in &self.samplers {
+ for (idx, smp) in self.samplers.iter().enumerate() {
+ let sampler = if smp.binding_type == wgpu::SamplerBindingType::Comparison {
+ &fallback_samplers[idx]
+ } else if let Some(resource) =
+ smp.resource_id.as_deref().and_then(|id| pools.sampler(id))
+ {
+ resource.sampler()
+ } else {
+ &fallback_samplers[idx]
+ };
bg_entries.push(wgpu::BindGroupEntry {
binding: smp.binding,
- resource: wgpu::BindingResource::Sampler(&smp.sampler),
+ resource: wgpu::BindingResource::Sampler(sampler),
});
}
@@ -962,3 +1161,31 @@ impl UniformCache {
self.dirty = false;
}
}
+
+fn texture_resource_matches(slot: &TextureBindingSlot, resource: &TextureResource) -> bool {
+ slot.view_dimension == resource.view_dimension()
+ && slot.sample_type == resource.sample_type()
+ && slot.multisampled == resource.multisampled()
+}
+
+fn create_placeholder_texture(
+ device: &wgpu::Device,
+ slot: &TextureBindingSlot,
+) -> (wgpu::Texture, wgpu::TextureView) {
+ let format = match slot.sample_type {
+ wgpu::TextureSampleType::Float { .. } => wgpu::TextureFormat::Rgba8UnormSrgb,
+ wgpu::TextureSampleType::Sint => wgpu::TextureFormat::Rgba8Sint,
+ wgpu::TextureSampleType::Uint => wgpu::TextureFormat::Rgba8Uint,
+ wgpu::TextureSampleType::Depth => wgpu::TextureFormat::Depth32Float,
+ };
+ create_input_texture(
+ device,
+ &format!("placeholder_{}", slot.name),
+ 1,
+ 1,
+ 1,
+ view_dimension_to_texture_dimension(slot.view_dimension),
+ slot.view_dimension,
+ format,
+ )
+}