summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2026-04-22 11:52:07 +0000
committers-ol <s+removethis@s-ol.nu>2026-05-14 15:00:13 +0000
commit76e48cc66816a1ec064c12a7181b7751d1c783b2 (patch)
tree7368d6ab24518d25eea7e6cf1bf8559519121cb7 /src
parentrebind outdated textures (diff)
downloadwgsl-view-76e48cc66816a1ec064c12a7181b7751d1c783b2.tar.gz
wgsl-view-76e48cc66816a1ec064c12a7181b7751d1c783b2.zip
switch to WESL shaders
Diffstat (limited to 'src')
-rw-r--r--src/bin/wgsl_render.rs115
-rw-r--r--src/lib.rs1
-rw-r--r--src/osc.rs30
-rw-r--r--src/renderer.rs160
-rw-r--r--src/uniform.rs57
-rw-r--r--src/wesl.rs86
6 files changed, 296 insertions, 153 deletions
diff --git a/src/bin/wgsl_render.rs b/src/bin/wgsl_render.rs
index 2288366..bcfb393 100644
--- a/src/bin/wgsl_render.rs
+++ b/src/bin/wgsl_render.rs
@@ -1,3 +1,4 @@
+use std::error::Error;
use std::thread;
use std::time::{Duration, Instant};
@@ -6,7 +7,6 @@ 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();
@@ -68,38 +68,44 @@ fn main() {
loop {
let frame_start = Instant::now();
- let handle_msg = |msg: rosc::OscMessage| -> Result<(), String> {
+ let handle_msg = |msg: rosc::OscMessage| -> Result<(), Box<dyn Error>> {
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())
+ // create shaders
+ (["module", module_name], [OscType::String(code)]) => {
+ renderer.load_module(module_name, code)?
}
+ (["entrypoint"], [OscType::String(module_name)]) => {
+ renderer.compile(&device, module_name)?
+ }
+ // bind values
+ (["uniform", module, item, rest @ ..], args) => {
+ renderer.set_uniform(module, item, rest, args)?
+ }
+ (["binding", module, item], [OscType::String(target)]) => {
+ renderer.set_binding(&device, module, item, target)?
+ }
+ // create/destroy texture resources
(["texture", id], [OscType::String(tsv_name)]) => {
renderer.uniforms().create_texture(&device, id, tsv_name);
- Ok(())
}
+ (["texture", id, "destroy"], []) => {
+ renderer.uniforms().destroy_texture(&device, id);
+ }
+ // create/destroy sampler resources
(["sampler", id], [OscType::String(filter), OscType::String(clamp)]) => {
let (filter_mode, address_mode) = parse_sampler_modes(filter, clamp)?;
renderer
.uniforms()
.create_sampler(&device, id, filter_mode, address_mode);
- Ok(())
- }
- (["texture", id, "destroy"], []) => {
- renderer.uniforms().destroy_texture(&device, id);
- Ok(())
}
(["sampler", id, "destroy"], []) => {
renderer.uniforms().destroy_sampler(&device, id);
- Ok(())
}
- (["binding", name], [OscType::String(target)]) => {
- bind_resource(renderer.uniforms(), &device, name, target)
- }
- _ => Err(format!("unhandled OSC message {} {:?}", msg.addr, msg.args)),
+ _ => Err(format!("unhandled OSC message {} {:?}", msg.addr, msg.args))?,
}
+ Ok(())
};
let dirty = if continuous {
@@ -136,66 +142,6 @@ fn main() {
}
/// Set a uniform value from OSC args, navigating the path.
-fn set_uniform(
- cache: &mut wgsl_view::uniform::UniformCache,
- path: &[&str],
- args: &[OscType],
-) -> Result<(), UniformError> {
- let mut parts = path.iter();
- let name = parts.next().ok_or(UniformError::NotFound)?;
-
- let mut uref = cache.get(name).ok_or(UniformError::NotFound)?;
- 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(UniformError::TypeMismatch),
- }
-
- Ok(())
-}
-
fn parse_sampler_modes(
filter: &str,
address: &str,
@@ -213,20 +159,3 @@ fn parse_sampler_modes(
};
Ok((filter_mode, address_mode))
}
-
-fn bind_resource(
- uniforms: &mut wgsl_view::uniform::UniformCache,
- device: &wgpu::Device,
- name: &str,
- target: &str,
-) -> Result<(), String> {
- if let Some(id) = target.strip_prefix("/texture/") {
- uniforms.bind_texture(device, name, id)?;
- Ok(())
- } else if let Some(id) = target.strip_prefix("/sampler/") {
- uniforms.bind_sampler(device, name, id)?;
- Ok(())
- } else {
- Err(format!("invalid binding target '{target}'"))
- }
-}
diff --git a/src/lib.rs b/src/lib.rs
index 57a1042..157a9b2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,4 +3,5 @@ pub mod hap;
pub mod osc;
pub mod renderer;
pub mod uniform;
+mod wesl;
pub mod window;
diff --git a/src/osc.rs b/src/osc.rs
index fdbcb0e..6288e2f 100644
--- a/src/osc.rs
+++ b/src/osc.rs
@@ -1,4 +1,4 @@
-use std::{fmt::Display, net::UdpSocket};
+use std::{error::Error, net::UdpSocket};
use rosc::{OscMessage, OscPacket};
@@ -20,9 +20,9 @@ impl OscServer {
/// Drains all pending OSC messages, calling `on_message` for each.
/// Returns `true` if any messages were dispatched.
- pub fn poll<E: Display>(
+ pub fn poll(
&mut self,
- mut on_message: impl FnMut(OscMessage) -> Result<(), E>,
+ mut on_message: impl FnMut(OscMessage) -> Result<(), Box<dyn Error>>,
) -> bool {
let mut received = false;
loop {
@@ -30,19 +30,17 @@ impl OscServer {
Ok((size, _addr)) => {
let data = &self.buf[..size];
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())
- });
+ .map_err(Box::from)
+ .and_then(|(_, packet)| dispatch(packet, &mut on_message));
if let Err(e) = res {
- log::warn!("{}", e);
+ log::error!("{}", e);
};
received = true;
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
Err(e) => {
- log::warn!("OSC recv error: {}", e);
+ log::error!("OSC recv error: {}", e);
break;
}
}
@@ -51,25 +49,23 @@ impl OscServer {
}
/// Blocks until at least one OSC message arrives, then drains all pending.
- pub fn recv<E: Display>(
+ pub fn recv(
&mut self,
- mut on_message: impl FnMut(OscMessage) -> Result<(), E>,
+ mut on_message: impl FnMut(OscMessage) -> Result<(), Box<dyn Error>>,
) -> 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];
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())
- });
+ .map_err(Box::from)
+ .and_then(|(_, packet)| dispatch(packet, &mut on_message));
if let Err(e) = res {
- log::warn!("{}", e);
+ log::error!("{}", e);
};
}
Err(e) => {
- log::warn!("OSC recv error: {}", e);
+ log::error!("OSC recv error: {}", e);
}
}
self.socket.set_nonblocking(true).expect("set nonblocking");
diff --git a/src/renderer.rs b/src/renderer.rs
index 155c318..c6f1973 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -1,4 +1,11 @@
-use crate::uniform::UniformCache;
+use std::error::Error;
+use std::str::FromStr;
+
+use rosc::OscType;
+use wesl::{Mangler, ModulePath};
+
+use crate::uniform::{UniformCache, UniformError};
+use crate::wesl::ShaderSources;
const CANVAS_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
@@ -30,6 +37,24 @@ fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
}
";
+#[derive(Debug, thiserror::Error)]
+pub enum ShaderError {
+ #[error("error resolving module")]
+ InvalidModulePath,
+
+ #[error("error setting uniform '{0}': {1}")]
+ UniformError(String, crate::uniform::UniformError),
+
+ #[error("uniform '{0}' not found")]
+ UniformNotFound(String),
+
+ #[error("error binding resource to '{0}': {1}")]
+ BindError(String, crate::uniform::BindError),
+
+ #[error("error compiling shader: {0}")]
+ CompileError(#[from] wesl::Error),
+}
+
pub struct Renderer {
canvas: wgpu::Texture,
canvas_view: wgpu::TextureView,
@@ -37,6 +62,7 @@ pub struct Renderer {
vertex_bgl: wgpu::BindGroupLayout,
vertex_bg: wgpu::BindGroup,
render_pipeline: wgpu::RenderPipeline,
+ shader_sources: ShaderSources,
uniforms: UniformCache,
}
@@ -102,11 +128,19 @@ impl Renderer {
);
let mut uniforms = UniformCache::new();
+ let mut shader_sources = ShaderSources::default();
+ shader_sources.set_module(wesl::ModulePath::new_root(), DEFAULT_FRAGMENT);
+
+ let initial_fragment = shader_sources
+ .compile(&wesl::ModulePath::new_root())
+ .expect("default shader");
+ log::info!("frag source:\n {initial_fragment}");
+
let render_pipeline = build_pipeline(
device,
&vertex_module,
&vertex_bgl,
- DEFAULT_FRAGMENT,
+ &initial_fragment,
&mut uniforms,
)
.expect("default shader");
@@ -118,6 +152,7 @@ impl Renderer {
vertex_bgl,
vertex_bg,
render_pipeline,
+ shader_sources,
uniforms,
}
}
@@ -126,24 +161,115 @@ impl Renderer {
&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(
+ pub fn set_uniform(
+ &mut self,
+ module: &str,
+ item: &str,
+ rest: &[&str],
+ args: &[OscType],
+ ) -> Result<(), ShaderError> {
+ let path = ModulePath::from_str(module).map_err(|_| ShaderError::InvalidModulePath)?;
+ let name = self.shader_sources.mangler().mangle(&path, item);
+
+ let mut uref = self
+ .uniforms
+ .get(&name)
+ .ok_or(ShaderError::UniformNotFound(name))?;
+
+ let lift_err = |e| ShaderError::UniformError(item.to_string(), e);
+
+ for component in rest.iter() {
+ uref = uref.field(component).map_err(lift_err)?;
+ }
+
+ let scalar = uref.leaf_scalar().map_err(lift_err)?;
+ match scalar.kind {
+ naga::ScalarKind::Float => 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<Vec<_>, _>>()
+ .and_then(|values| uref.set_f32(&values)),
+ naga::ScalarKind::Sint => 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<Vec<_>, _>>()
+ .and_then(|values| uref.set_i32(&values)),
+ naga::ScalarKind::Uint => 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<Vec<_>, _>>()
+ .and_then(|values| uref.set_u32(&values)),
+ _ => Err(UniformError::TypeMismatch),
+ }
+ .map_err(lift_err)
+ }
+
+ pub fn set_binding(
+ &mut self,
+ device: &wgpu::Device,
+ module: &str,
+ item: &str,
+ target: &str,
+ ) -> Result<(), ShaderError> {
+ let path = ModulePath::from_str(module).map_err(|_| ShaderError::InvalidModulePath)?;
+ let name = self.shader_sources.mangler().mangle(&path, item);
+
+ if let Some(id) = target.strip_prefix("/texture/") {
+ self.uniforms.bind_texture(device, &name, id)
+ } else if let Some(id) = target.strip_prefix("/sampler/") {
+ self.uniforms.bind_sampler(device, &name, id)
+ } else {
+ Err(crate::uniform::BindError::ResourceNotFound(
+ target.to_string(),
+ ))
+ }
+ .map_err(|e| ShaderError::BindError(name, e))
+ }
+
+ pub fn load_module(&mut self, module_name: &str, source: &str) -> Result<(), ShaderError> {
+ let path = ModulePath::from_str(module_name).map_err(|_| ShaderError::InvalidModulePath)?;
+ self.shader_sources.set_module(path, source);
+ Ok(())
+ }
+
+ pub fn compile(
+ &mut self,
+ device: &wgpu::Device,
+ module_name: &str,
+ ) -> Result<(), Box<dyn Error>> {
+ let path = ModulePath::from_str(module_name).map_err(|_| ShaderError::InvalidModulePath)?;
+ let fragment_source = self.shader_sources.compile(&path)?;
+ log::info!("frag source:\n {fragment_source}");
+
+ self.render_pipeline = build_pipeline(
device,
&self.vertex_module,
&self.vertex_bgl,
- source,
+ &fragment_source,
&mut self.uniforms,
)?;
- self.render_pipeline = pipeline;
- log::info!("shader loaded");
Ok(())
}
@@ -189,16 +315,14 @@ fn build_pipeline(
vertex_bgl: &wgpu::BindGroupLayout,
fragment_source: &str,
cache: &mut UniformCache,
-) -> Result<wgpu::RenderPipeline, String> {
- let module = naga::front::wgsl::parse_str(fragment_source)
- .map_err(|e| format!("WGSL parse error: {e}"))?;
+) -> Result<wgpu::RenderPipeline, Box<dyn Error>> {
+ let module = naga::front::wgsl::parse_str(fragment_source)?;
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
- .validate(&module)
- .map_err(|e| format!("WGSL validation error: {e}"))?;
+ .validate(&module)?;
cache.refresh(module, device);
cache.rebuild_bind_group(device);
@@ -225,13 +349,13 @@ fn build_pipeline(
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: vertex_module,
- entry_point: Some("vs_main"),
+ entry_point: None,
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &fragment_module,
- entry_point: Some("fs_main"),
+ entry_point: None,
targets: &[Some(wgpu::ColorTargetState {
format: CANVAS_FORMAT,
blend: Some(wgpu::BlendState::REPLACE),
diff --git a/src/uniform.rs b/src/uniform.rs
index fefd67f..db01e0d 100644
--- a/src/uniform.rs
+++ b/src/uniform.rs
@@ -11,29 +11,28 @@ pub struct UniformRef<'a> {
pub data: &'a mut [u8],
}
-#[derive(Debug)]
+#[derive(Debug, thiserror::Error)]
pub enum UniformError {
+ #[error("can't be indexed (further)")]
NotIndexable,
- IndexOutOfBounds,
- NotFound,
+ #[error("index '{0}' out of bounds")]
+ IndexOutOfBounds(usize),
+ #[error("member '{0}' not found")]
+ MemberNotFound(String),
+ #[error("mismatched value types")]
TypeMismatch,
+ #[error("invalid number of OSC values")]
SizeMismatch,
}
-impl std::fmt::Display for UniformError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::NotIndexable => write!(f, "type is not indexable"),
- Self::IndexOutOfBounds => write!(f, "index out of bounds"),
- Self::NotFound => write!(f, "member not found"),
- Self::TypeMismatch => write!(f, "scalar type mismatch"),
- Self::SizeMismatch => write!(f, "value count mismatch"),
- }
- }
+#[derive(Debug, thiserror::Error)]
+pub enum BindError {
+ #[error("binding point '{0}' not found")]
+ NotFound(String),
+ #[error("bindable resource '{0}' not found")]
+ ResourceNotFound(String),
}
-impl std::error::Error for UniformError {}
-
impl<'a> UniformRef<'a> {
/// Navigate by path component: struct member by name, or swizzle/numeric index otherwise.
pub fn field(self, name: &str) -> Result<UniformRef<'a>, UniformError> {
@@ -51,7 +50,7 @@ impl<'a> UniformRef<'a> {
});
}
}
- return Err(UniformError::NotFound);
+ return Err(UniformError::MemberNotFound(name.to_string()));
}
let i = match name {
@@ -70,7 +69,7 @@ impl<'a> UniformRef<'a> {
match *ty_inner {
naga::TypeInner::Vector { size, scalar } => {
if i >= size as usize {
- return Err(UniformError::IndexOutOfBounds);
+ return Err(UniformError::IndexOutOfBounds(i));
}
let w = scalar.width as usize;
let scalar_ty = find_or_expect_scalar_type(self.module, scalar);
@@ -87,7 +86,7 @@ impl<'a> UniformRef<'a> {
scalar,
} => {
if i >= columns as usize {
- return Err(UniformError::IndexOutOfBounds);
+ return Err(UniformError::IndexOutOfBounds(i));
}
let col_ty = find_or_expect_vector_type(self.module, rows, scalar);
let col_layout = self.layouter[col_ty];
@@ -106,7 +105,7 @@ impl<'a> UniformRef<'a> {
_ => return Err(UniformError::NotIndexable),
};
if i >= len {
- return Err(UniformError::IndexOutOfBounds);
+ return Err(UniformError::IndexOutOfBounds(i));
}
let elem_size = self.layouter[base].size as usize;
Ok(UniformRef {
@@ -733,6 +732,10 @@ impl UniformCache {
None => continue,
};
+ if let Some(ref name) = var.name {
+ log::info!("found uniform '{name}'");
+ }
+
match var.space {
naga::AddressSpace::Uniform => {
let layout = new_layouter[var.ty];
@@ -855,6 +858,7 @@ impl UniformCache {
// --- Resource pool operations ---
pub fn create_texture(&mut self, device: &wgpu::Device, id: &str, tsv_name: &str) {
+ log::info!("creating texture {id} (tsv {tsv_name})");
self.texture_pool.insert(
id.to_string(),
TextureResource::new(device, id.to_string(), tsv_name.to_string()),
@@ -863,6 +867,7 @@ impl UniformCache {
}
pub fn destroy_texture(&mut self, device: &wgpu::Device, id: &str) {
+ log::info!("destroying texture {id}");
let removed = self.texture_pool.remove(id).is_some();
let unbound = self.unbind_texture_resource(id);
if removed || unbound {
@@ -881,6 +886,7 @@ impl UniformCache {
filter: wgpu::FilterMode,
address_mode: wgpu::AddressMode,
) {
+ log::info!("creating sampler {id} ({filter:?} {address_mode:?})");
self.sampler_pool.insert(
id.to_string(),
SamplerResource::new(device, id.to_string(), filter, address_mode),
@@ -889,6 +895,7 @@ impl UniformCache {
}
pub fn destroy_sampler(&mut self, device: &wgpu::Device, id: &str) {
+ log::info!("destroying sampler {id}");
let removed = self.sampler_pool.remove(id).is_some();
let unbound = self.unbind_sampler_resource(id);
if removed || unbound {
@@ -901,15 +908,15 @@ impl UniformCache {
device: &wgpu::Device,
slot_name: &str,
resource_id: &str,
- ) -> Result<(), String> {
+ ) -> Result<(), BindError> {
if !self.texture_pool.contains_key(resource_id) {
- return Err(format!("texture '{resource_id}' not found"));
+ return Err(BindError::ResourceNotFound(resource_id.to_string()));
}
let slot = self
.texture_slots
.iter_mut()
.find(|t| t.name == slot_name)
- .ok_or_else(|| format!("texture binding '{slot_name}' not found in shader"))?;
+ .ok_or(BindError::NotFound(slot_name.to_string()))?;
slot.resource_id = Some(resource_id.to_string());
self.build_bind_group(device);
Ok(())
@@ -920,15 +927,15 @@ impl UniformCache {
device: &wgpu::Device,
slot_name: &str,
resource_id: &str,
- ) -> Result<(), String> {
+ ) -> Result<(), BindError> {
if !self.sampler_pool.contains_key(resource_id) {
- return Err(format!("sampler '{resource_id}' not found"));
+ return Err(BindError::ResourceNotFound(resource_id.to_string()));
}
let slot = self
.sampler_slots
.iter_mut()
.find(|s| s.name == slot_name)
- .ok_or_else(|| format!("sampler binding '{slot_name}' not found in shader"))?;
+ .ok_or(BindError::NotFound(slot_name.to_string()))?;
slot.resource_id = Some(resource_id.to_string());
self.build_bind_group(device);
Ok(())
diff --git a/src/wesl.rs b/src/wesl.rs
new file mode 100644
index 0000000..446c915
--- /dev/null
+++ b/src/wesl.rs
@@ -0,0 +1,86 @@
+use std::borrow::Cow;
+use std::iter;
+use std::path::PathBuf;
+
+use wesl::{
+ syntax::PathOrigin, EscapeMangler, FileResolver, Mangler, ModulePath, ResolveError, Resolver,
+ VirtualResolver, Wesl,
+};
+
+pub struct ShaderSources {
+ virtual_resolver: VirtualResolver<'static>,
+ file_resolver: Option<FileResolver>,
+ mangler: EscapeMangler,
+}
+
+impl Default for ShaderSources {
+ fn default() -> Self {
+ Self::new(Some("wesl-lib".into()))
+ }
+}
+
+impl ShaderSources {
+ pub fn new(lib_dir: Option<PathBuf>) -> Self {
+ Self {
+ virtual_resolver: VirtualResolver::new(),
+ file_resolver: lib_dir.map(FileResolver::new),
+ mangler: Default::default(),
+ }
+ }
+
+ pub fn set_module(&mut self, module: ModulePath, source: impl Into<String>) {
+ self.virtual_resolver
+ .add_module(module, Cow::Owned(source.into()));
+ }
+
+ pub fn compile(&mut self, root_module: &ModulePath) -> Result<String, wesl::Error> {
+ log::info!("--- all modules ---");
+ for (path, text) in self.virtual_resolver.modules() {
+ log::info!("==== {path}:\n{text}");
+ }
+ log::info!("--- end modules ---");
+
+ Wesl::new_barebones()
+ .set_custom_resolver(&*self)
+ .set_custom_mangler(self.mangler.clone())
+ .set_options(wesl::CompileOptions {
+ mangle_root: true,
+ ..Default::default()
+ })
+ .compile(root_module)
+ .map(|compiled| compiled.to_string())
+ }
+
+ pub fn mangler(&self) -> &impl Mangler {
+ &self.mangler
+ }
+}
+
+impl Resolver for ShaderSources {
+ fn resolve_source<'a>(&'a self, path: &ModulePath) -> Result<Cow<'a, str>, ResolveError> {
+ match path.origin {
+ PathOrigin::Absolute => self.virtual_resolver.resolve_source(path),
+ PathOrigin::Package(ref pname) => {
+ if let Some(ref files) = self.file_resolver {
+ let fpath = ModulePath {
+ origin: PathOrigin::Absolute,
+ components: iter::once(pname)
+ .chain(path.components.iter())
+ .cloned()
+ .collect(),
+ };
+ files.resolve_source(&fpath)
+ } else {
+ Err(ResolveError::ModuleNotFound(
+ path.clone(),
+ "no library director found".into(),
+ ))
+ }
+ }
+ _ => Err(ResolveError::ModuleNotFound(
+ path.clone(),
+ "can't resolve relative module".into(),
+ )),
+ }
+ }
+}