use crate::error::{ViewportError, ViewportResult};
use crate::renderer::shader_hashes::SHADERS;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum DeformStage {
ObjectSpace,
WorldSpace,
}
#[allow(dead_code)]
pub(crate) const REGISTRY_SLOT_COUNT: usize =
crate::resources::mesh_sidecar::deform::DEFORM_SLOT_COUNT;
pub(crate) const REGISTRY_INTERNAL_SLOT_START: usize =
crate::resources::mesh_sidecar::deform::DEFORM_INTERNAL_SLOT_START;
pub const DEFORM_SLOT_COUNT_PUB: usize = REGISTRY_INTERNAL_SLOT_START;
pub const DEFORM_PARAMS_PER_SLOT_PUB: usize =
crate::resources::mesh_sidecar::deform::DEFORM_PARAMS_PER_SLOT;
#[derive(Clone, Debug)]
pub struct DeformerDesc {
pub name: &'static str,
pub stage: DeformStage,
pub priority: i32,
pub wgsl_body: String,
pub per_vertex_stride: u32,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct DeformerId(pub(crate) usize);
impl DeformerId {
pub fn slot(&self) -> usize {
self.0
}
}
#[derive(Clone, Debug)]
pub(crate) struct StoredDeformer {
pub desc: DeformerDesc,
pub slot: usize,
}
pub(crate) const MESH_FAMILY_SHADERS: &[&str] = &[
"mesh.wgsl",
"mesh_oit.wgsl",
"mesh_instanced.wgsl",
"mesh_instanced_oit.wgsl",
"shadow.wgsl",
"outline_mask.wgsl",
];
fn shader_source(name: &str) -> Option<&'static str> {
SHADERS.iter().find(|s| s.name == name).map(|s| s.source)
}
fn build_stage_calls(stored: &[StoredDeformer], stage: DeformStage) -> String {
let mut entries: Vec<&StoredDeformer> =
stored.iter().filter(|d| d.desc.stage == stage).collect();
entries.sort_by(|a, b| {
a.desc
.priority
.cmp(&b.desc.priority)
.then_with(|| a.desc.name.cmp(b.desc.name))
});
let mut out = String::new();
for d in entries {
out.push_str(&format!(
" if ((ctx.flags & (1u << {slot}u)) != 0u) {{\n var ctx_{name} = ctx;\n ctx_{name}.slot = {slot}u;\n out = {name}__deform(out, ctx_{name});\n }}\n",
slot = d.slot,
name = d.desc.name,
));
}
out
}
fn rewrite_marker(source: &str, marker: &str, body: &str) -> Option<String> {
let open = format!("// <viewport-deform-slots:{marker}>");
let close = format!("// </viewport-deform-slots:{marker}>");
let open_idx = source.find(&open)?;
let close_idx = source.find(&close)?;
if close_idx < open_idx {
return None;
}
let mut out = String::with_capacity(source.len() + body.len());
out.push_str(&source[..open_idx + open.len()]);
out.push('\n');
out.push_str(body);
out.push_str(&source[close_idx..]);
Some(out)
}
fn identifier_prefix(name: &str, body: &str) -> String {
let mut decls: Vec<String> = Vec::new();
for tok in ["fn ", "struct ", "alias ", "const ", "var "] {
let mut search_from = 0;
while let Some(rel) = body[search_from..].find(tok) {
let i = search_from + rel + tok.len();
let rest = &body[i..];
let end = rest
.find(|c: char| !(c.is_alphanumeric() || c == '_'))
.unwrap_or(rest.len());
if end > 0 {
let ident = rest[..end].to_string();
if !ident.is_empty() && !decls.contains(&ident) {
decls.push(ident);
}
}
search_from = i + end;
}
}
decls.sort_by_key(|s| std::cmp::Reverse(s.len()));
let mut out = body.to_string();
for ident in &decls {
let prefixed = format!("{name}__{ident}");
out = replace_whole_word(&out, ident, &prefixed);
}
out
}
fn replace_whole_word(haystack: &str, needle: &str, replacement: &str) -> String {
if needle.is_empty() {
return haystack.to_string();
}
let bytes = haystack.as_bytes();
let needle_bytes = needle.as_bytes();
let mut out = String::with_capacity(haystack.len());
let mut i = 0;
while i < bytes.len() {
if i + needle_bytes.len() <= bytes.len()
&& &bytes[i..i + needle_bytes.len()] == needle_bytes
{
let before_ok = i == 0 || {
let c = bytes[i - 1] as char;
!(c.is_alphanumeric() || c == '_')
};
let after_idx = i + needle_bytes.len();
let after_ok = after_idx == bytes.len() || {
let c = bytes[after_idx] as char;
!(c.is_alphanumeric() || c == '_')
};
if before_ok && after_ok {
out.push_str(replacement);
i = after_idx;
continue;
}
}
out.push(bytes[i] as char);
i += 1;
}
out
}
pub(crate) fn compose_shader(base: &str, stored: &[StoredDeformer]) -> String {
let mut bodies = String::new();
for d in stored {
bodies.push_str(&format!(
"\n// deformer: {name} (slot {slot}, stage {stage:?})\n",
name = d.desc.name,
slot = d.slot,
stage = d.desc.stage,
));
bodies.push_str(&identifier_prefix(d.desc.name, &d.desc.wgsl_body));
bodies.push('\n');
}
let anchor = "fn viewport_deform_object_space(";
let with_bodies = match base.find(anchor) {
Some(idx) => {
let mut s = String::with_capacity(base.len() + bodies.len());
s.push_str(&base[..idx]);
s.push_str(&bodies);
s.push_str(&base[idx..]);
s
}
None => return base.to_string(),
};
let object_calls = build_stage_calls(stored, DeformStage::ObjectSpace);
let world_calls = build_stage_calls(stored, DeformStage::WorldSpace);
let after_obj = rewrite_marker(&with_bodies, "object", &object_calls).unwrap_or(with_bodies);
rewrite_marker(&after_obj, "world", &world_calls).unwrap_or(after_obj)
}
pub(crate) fn allocate_slot(stored: &[StoredDeformer]) -> ViewportResult<usize> {
let mut used = 0u32;
for d in stored {
used |= 1u32 << d.slot;
}
for slot in 0..REGISTRY_INTERNAL_SLOT_START {
if used & (1u32 << slot) == 0 {
return Ok(slot);
}
}
Err(ViewportError::DeformSlotsExhausted {
max: REGISTRY_INTERNAL_SLOT_START,
})
}
#[allow(dead_code)]
pub(crate) fn allocate_internal_slot(stored: &[StoredDeformer]) -> ViewportResult<usize> {
let mut used = 0u32;
for d in stored {
used |= 1u32 << d.slot;
}
for slot in REGISTRY_INTERNAL_SLOT_START..REGISTRY_SLOT_COUNT {
if used & (1u32 << slot) == 0 {
return Ok(slot);
}
}
Err(ViewportError::DeformSlotsExhausted {
max: REGISTRY_SLOT_COUNT - REGISTRY_INTERNAL_SLOT_START,
})
}
pub(crate) fn validate_name(stored: &[StoredDeformer], name: &str) -> ViewportResult<()> {
if name.is_empty()
|| !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
|| name
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(true)
{
return Err(ViewportError::DeformShaderInvalid {
reason: "deformer name must be a non-empty WGSL identifier (letters, digits, underscore; not starting with a digit)".to_string(),
});
}
if stored.iter().any(|d| d.desc.name == name) {
return Err(ViewportError::DeformNameTaken {
name: name.to_string(),
});
}
Ok(())
}
pub(crate) fn validate_with_wgpu(
device: &wgpu::Device,
label: &str,
source: &str,
) -> ViewportResult<wgpu::ShaderModule> {
device.push_error_scope(wgpu::ErrorFilter::Validation);
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(label),
source: wgpu::ShaderSource::Wgsl(source.into()),
});
let captured = block_on_simple(device.pop_error_scope());
if let Some(err) = captured {
return Err(ViewportError::DeformShaderInvalid {
reason: format!("{label}: {err}"),
});
}
Ok(module)
}
pub(crate) fn lookup_source(name: &str) -> Option<&'static str> {
shader_source(name)
}
fn block_on_simple<F: std::future::Future>(mut fut: F) -> F::Output {
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
const VTABLE: RawWakerVTable = RawWakerVTable::new(
|_| RawWaker::new(std::ptr::null(), &VTABLE),
|_| {},
|_| {},
|_| {},
);
let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
let mut cx = Context::from_waker(&waker);
let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
loop {
if let Poll::Ready(v) = fut.as_mut().poll(&mut cx) {
return v;
}
std::thread::yield_now();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn desc(name: &'static str, stage: DeformStage, priority: i32, body: &str) -> StoredDeformer {
StoredDeformer {
desc: DeformerDesc {
name,
stage,
priority,
wgsl_body: body.to_string(),
per_vertex_stride: 4,
},
slot: 0,
}
}
#[test]
fn allocate_slot_walks_lowest_free_index() {
let mut stored = Vec::<StoredDeformer>::new();
assert_eq!(allocate_slot(&stored).unwrap(), 0);
let mut d = desc("a", DeformStage::ObjectSpace, 0, "");
d.slot = 0;
stored.push(d);
assert_eq!(allocate_slot(&stored).unwrap(), 1);
let mut d = desc("b", DeformStage::ObjectSpace, 0, "");
d.slot = 2;
stored.push(d);
assert_eq!(allocate_slot(&stored).unwrap(), 1);
}
#[test]
fn allocate_slot_returns_exhausted_when_host_range_full() {
let mut stored = Vec::<StoredDeformer>::new();
for slot in 0..REGISTRY_INTERNAL_SLOT_START {
let mut d = desc(
Box::leak(format!("d{slot}").into_boxed_str()),
DeformStage::ObjectSpace,
0,
"",
);
d.slot = slot;
stored.push(d);
}
let err = allocate_slot(&stored).unwrap_err();
assert!(matches!(
err,
ViewportError::DeformSlotsExhausted { max } if max == REGISTRY_INTERNAL_SLOT_START
));
}
#[test]
fn allocate_internal_slot_uses_reserved_range() {
let stored = Vec::<StoredDeformer>::new();
let slot = allocate_internal_slot(&stored).unwrap();
assert!(slot >= REGISTRY_INTERNAL_SLOT_START && slot < REGISTRY_SLOT_COUNT);
}
#[test]
fn validate_name_rejects_duplicate() {
let mut stored = Vec::<StoredDeformer>::new();
stored.push(desc("wind", DeformStage::WorldSpace, 0, ""));
let err = validate_name(&stored, "wind").unwrap_err();
assert!(matches!(err, ViewportError::DeformNameTaken { .. }));
}
#[test]
fn validate_name_rejects_invalid_identifier() {
let stored = Vec::<StoredDeformer>::new();
for bad in &["", "1wind", "wind!", " wind"] {
assert!(matches!(
validate_name(&stored, bad),
Err(ViewportError::DeformShaderInvalid { .. })
));
}
assert!(validate_name(&stored, "wind_2").is_ok());
}
#[test]
fn identifier_prefix_renames_locals() {
let body = "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex {\n return v;\n}\nfn helper() -> f32 { return 1.0; }\n";
let out = identifier_prefix("wind", body);
assert!(out.contains("fn wind__deform("));
assert!(out.contains("fn wind__helper()"));
assert!(out.contains("DeformVertex"));
assert!(!out.contains("wind__DeformVertex"));
}
#[test]
fn build_stage_calls_orders_by_priority_then_name() {
let mut stored = vec![
desc("z_late", DeformStage::ObjectSpace, 10, ""),
desc("a_early", DeformStage::ObjectSpace, -5, ""),
desc("m_mid", DeformStage::ObjectSpace, 0, ""),
desc("world_only", DeformStage::WorldSpace, 0, ""),
];
for (i, d) in stored.iter_mut().enumerate() {
d.slot = i;
}
let obj = build_stage_calls(&stored, DeformStage::ObjectSpace);
let pos_a = obj.find("a_early__deform").unwrap();
let pos_m = obj.find("m_mid__deform").unwrap();
let pos_z = obj.find("z_late__deform").unwrap();
assert!(pos_a < pos_m);
assert!(pos_m < pos_z);
assert!(!obj.contains("world_only__deform"));
let w = build_stage_calls(&stored, DeformStage::WorldSpace);
assert!(w.contains("world_only__deform"));
}
#[test]
fn compose_shader_zero_deformers_is_identity_passthrough() {
let Some(src) = lookup_source("mesh.wgsl") else {
return;
};
let composed = compose_shader(src, &[]);
assert!(composed.contains("// <viewport-deform-slots:object>"));
assert!(!composed.contains("__deform("));
}
#[test]
fn compose_shader_splices_one_body_and_wires_call() {
let Some(src) = lookup_source("mesh.wgsl") else {
return;
};
let mut d = desc(
"wind",
DeformStage::WorldSpace,
0,
"fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex {\n return v;\n}\n",
);
d.slot = 2;
let composed = compose_shader(src, &std::slice::from_ref(&d));
assert!(composed.contains("fn wind__deform("));
assert!(composed.contains("out = wind__deform(out, ctx_wind);"));
assert!(composed.contains("ctx.flags & (1u << 2u)"));
assert!(composed.contains("ctx_wind.slot = 2u;"));
assert!(composed.contains("DeformVertex"));
}
}