viewport_lib/resources/mesh_sidecar/
registry.rs1use crate::error::{ViewportError, ViewportResult};
14use crate::renderer::shader_hashes::SHADERS;
15
16#[derive(Copy, Clone, Debug, Eq, PartialEq)]
18pub enum DeformStage {
19 ObjectSpace,
21 WorldSpace,
23}
24
25#[allow(dead_code)]
30pub(crate) const REGISTRY_SLOT_COUNT: usize =
31 crate::resources::mesh_sidecar::deform::DEFORM_SLOT_COUNT;
32
33pub(crate) const REGISTRY_INTERNAL_SLOT_START: usize =
39 crate::resources::mesh_sidecar::deform::DEFORM_INTERNAL_SLOT_START;
40
41pub const DEFORM_SLOT_COUNT_PUB: usize = REGISTRY_INTERNAL_SLOT_START;
43
44pub const DEFORM_PARAMS_PER_SLOT_PUB: usize =
47 crate::resources::mesh_sidecar::deform::DEFORM_PARAMS_PER_SLOT;
48
49#[derive(Clone, Debug)]
123pub struct DeformerDesc {
124 pub name: &'static str,
128 pub stage: DeformStage,
130 pub priority: i32,
132 pub wgsl_body: String,
136 pub per_vertex_stride: u32,
138}
139
140#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
143pub struct DeformerId(pub(crate) usize);
144
145impl DeformerId {
146 pub fn slot(&self) -> usize {
148 self.0
149 }
150}
151
152#[derive(Clone, Debug)]
155pub(crate) struct StoredDeformer {
156 pub desc: DeformerDesc,
157 pub slot: usize,
158}
159
160pub(crate) const MESH_FAMILY_SHADERS: &[&str] = &[
162 "mesh.wgsl",
163 "mesh_oit.wgsl",
164 "mesh_instanced.wgsl",
165 "mesh_instanced_oit.wgsl",
166 "shadow.wgsl",
167 "outline_mask.wgsl",
168];
169
170fn shader_source(name: &str) -> Option<&'static str> {
171 SHADERS.iter().find(|s| s.name == name).map(|s| s.source)
172}
173
174fn build_stage_calls(stored: &[StoredDeformer], stage: DeformStage) -> String {
184 let mut entries: Vec<&StoredDeformer> =
185 stored.iter().filter(|d| d.desc.stage == stage).collect();
186 entries.sort_by(|a, b| {
187 a.desc
188 .priority
189 .cmp(&b.desc.priority)
190 .then_with(|| a.desc.name.cmp(b.desc.name))
191 });
192 let mut out = String::new();
193 for d in entries {
194 out.push_str(&format!(
195 " 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",
196 slot = d.slot,
197 name = d.desc.name,
198 ));
199 }
200 out
201}
202
203fn rewrite_marker(source: &str, marker: &str, body: &str) -> Option<String> {
205 let open = format!("// <viewport-deform-slots:{marker}>");
206 let close = format!("// </viewport-deform-slots:{marker}>");
207 let open_idx = source.find(&open)?;
208 let close_idx = source.find(&close)?;
209 if close_idx < open_idx {
210 return None;
211 }
212 let mut out = String::with_capacity(source.len() + body.len());
213 out.push_str(&source[..open_idx + open.len()]);
214 out.push('\n');
215 out.push_str(body);
216 out.push_str(&source[close_idx..]);
217 Some(out)
218}
219
220fn identifier_prefix(name: &str, body: &str) -> String {
226 let mut decls: Vec<String> = Vec::new();
227 for tok in ["fn ", "struct ", "alias ", "const ", "var "] {
228 let mut search_from = 0;
229 while let Some(rel) = body[search_from..].find(tok) {
230 let i = search_from + rel + tok.len();
231 let rest = &body[i..];
232 let end = rest
233 .find(|c: char| !(c.is_alphanumeric() || c == '_'))
234 .unwrap_or(rest.len());
235 if end > 0 {
236 let ident = rest[..end].to_string();
237 if !ident.is_empty() && !decls.contains(&ident) {
238 decls.push(ident);
239 }
240 }
241 search_from = i + end;
242 }
243 }
244 decls.sort_by_key(|s| std::cmp::Reverse(s.len()));
247 let mut out = body.to_string();
248 for ident in &decls {
249 let prefixed = format!("{name}__{ident}");
250 out = replace_whole_word(&out, ident, &prefixed);
251 }
252 out
253}
254
255fn replace_whole_word(haystack: &str, needle: &str, replacement: &str) -> String {
256 if needle.is_empty() {
257 return haystack.to_string();
258 }
259 let bytes = haystack.as_bytes();
260 let needle_bytes = needle.as_bytes();
261 let mut out = String::with_capacity(haystack.len());
262 let mut i = 0;
263 while i < bytes.len() {
264 if i + needle_bytes.len() <= bytes.len()
265 && &bytes[i..i + needle_bytes.len()] == needle_bytes
266 {
267 let before_ok = i == 0 || {
268 let c = bytes[i - 1] as char;
269 !(c.is_alphanumeric() || c == '_')
270 };
271 let after_idx = i + needle_bytes.len();
272 let after_ok = after_idx == bytes.len() || {
273 let c = bytes[after_idx] as char;
274 !(c.is_alphanumeric() || c == '_')
275 };
276 if before_ok && after_ok {
277 out.push_str(replacement);
278 i = after_idx;
279 continue;
280 }
281 }
282 out.push(bytes[i] as char);
283 i += 1;
284 }
285 out
286}
287
288pub(crate) fn compose_shader(base: &str, stored: &[StoredDeformer]) -> String {
293 let mut bodies = String::new();
294 for d in stored {
295 bodies.push_str(&format!(
296 "\n// deformer: {name} (slot {slot}, stage {stage:?})\n",
297 name = d.desc.name,
298 slot = d.slot,
299 stage = d.desc.stage,
300 ));
301 bodies.push_str(&identifier_prefix(d.desc.name, &d.desc.wgsl_body));
302 bodies.push('\n');
303 }
304
305 let anchor = "fn viewport_deform_object_space(";
308 let with_bodies = match base.find(anchor) {
309 Some(idx) => {
310 let mut s = String::with_capacity(base.len() + bodies.len());
311 s.push_str(&base[..idx]);
312 s.push_str(&bodies);
313 s.push_str(&base[idx..]);
314 s
315 }
316 None => return base.to_string(),
317 };
318
319 let object_calls = build_stage_calls(stored, DeformStage::ObjectSpace);
320 let world_calls = build_stage_calls(stored, DeformStage::WorldSpace);
321
322 let after_obj = rewrite_marker(&with_bodies, "object", &object_calls).unwrap_or(with_bodies);
323 rewrite_marker(&after_obj, "world", &world_calls).unwrap_or(after_obj)
324}
325
326pub(crate) fn allocate_slot(stored: &[StoredDeformer]) -> ViewportResult<usize> {
331 let mut used = 0u32;
332 for d in stored {
333 used |= 1u32 << d.slot;
334 }
335 for slot in 0..REGISTRY_INTERNAL_SLOT_START {
336 if used & (1u32 << slot) == 0 {
337 return Ok(slot);
338 }
339 }
340 Err(ViewportError::DeformSlotsExhausted {
341 max: REGISTRY_INTERNAL_SLOT_START,
342 })
343}
344
345#[allow(dead_code)]
350pub(crate) fn allocate_internal_slot(stored: &[StoredDeformer]) -> ViewportResult<usize> {
351 let mut used = 0u32;
352 for d in stored {
353 used |= 1u32 << d.slot;
354 }
355 for slot in REGISTRY_INTERNAL_SLOT_START..REGISTRY_SLOT_COUNT {
356 if used & (1u32 << slot) == 0 {
357 return Ok(slot);
358 }
359 }
360 Err(ViewportError::DeformSlotsExhausted {
361 max: REGISTRY_SLOT_COUNT - REGISTRY_INTERNAL_SLOT_START,
362 })
363}
364
365pub(crate) fn validate_name(stored: &[StoredDeformer], name: &str) -> ViewportResult<()> {
367 if name.is_empty()
368 || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
369 || name
370 .chars()
371 .next()
372 .map(|c| c.is_ascii_digit())
373 .unwrap_or(true)
374 {
375 return Err(ViewportError::DeformShaderInvalid {
376 reason: "deformer name must be a non-empty WGSL identifier (letters, digits, underscore; not starting with a digit)".to_string(),
377 });
378 }
379 if stored.iter().any(|d| d.desc.name == name) {
380 return Err(ViewportError::DeformNameTaken {
381 name: name.to_string(),
382 });
383 }
384 Ok(())
385}
386
387pub(crate) fn validate_with_wgpu(
390 device: &wgpu::Device,
391 label: &str,
392 source: &str,
393) -> ViewportResult<wgpu::ShaderModule> {
394 device.push_error_scope(wgpu::ErrorFilter::Validation);
395 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
396 label: Some(label),
397 source: wgpu::ShaderSource::Wgsl(source.into()),
398 });
399 let captured = block_on_simple(device.pop_error_scope());
400 if let Some(err) = captured {
401 return Err(ViewportError::DeformShaderInvalid {
402 reason: format!("{label}: {err}"),
403 });
404 }
405 Ok(module)
406}
407
408pub(crate) fn lookup_source(name: &str) -> Option<&'static str> {
411 shader_source(name)
412}
413
414fn block_on_simple<F: std::future::Future>(mut fut: F) -> F::Output {
419 use std::pin::Pin;
420 use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
421
422 const VTABLE: RawWakerVTable = RawWakerVTable::new(
423 |_| RawWaker::new(std::ptr::null(), &VTABLE),
424 |_| {},
425 |_| {},
426 |_| {},
427 );
428 let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
429 let mut cx = Context::from_waker(&waker);
430 let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
432 loop {
433 if let Poll::Ready(v) = fut.as_mut().poll(&mut cx) {
434 return v;
435 }
436 std::thread::yield_now();
437 }
438}
439
440#[cfg(test)]
445mod tests {
446 use super::*;
447
448 fn desc(name: &'static str, stage: DeformStage, priority: i32, body: &str) -> StoredDeformer {
449 StoredDeformer {
450 desc: DeformerDesc {
451 name,
452 stage,
453 priority,
454 wgsl_body: body.to_string(),
455 per_vertex_stride: 4,
456 },
457 slot: 0,
458 }
459 }
460
461 #[test]
462 fn allocate_slot_walks_lowest_free_index() {
463 let mut stored = Vec::<StoredDeformer>::new();
464 assert_eq!(allocate_slot(&stored).unwrap(), 0);
465 let mut d = desc("a", DeformStage::ObjectSpace, 0, "");
466 d.slot = 0;
467 stored.push(d);
468 assert_eq!(allocate_slot(&stored).unwrap(), 1);
469 let mut d = desc("b", DeformStage::ObjectSpace, 0, "");
470 d.slot = 2;
471 stored.push(d);
472 assert_eq!(allocate_slot(&stored).unwrap(), 1);
473 }
474
475 #[test]
476 fn allocate_slot_returns_exhausted_when_host_range_full() {
477 let mut stored = Vec::<StoredDeformer>::new();
478 for slot in 0..REGISTRY_INTERNAL_SLOT_START {
479 let mut d = desc(
480 Box::leak(format!("d{slot}").into_boxed_str()),
481 DeformStage::ObjectSpace,
482 0,
483 "",
484 );
485 d.slot = slot;
486 stored.push(d);
487 }
488 let err = allocate_slot(&stored).unwrap_err();
489 assert!(matches!(
490 err,
491 ViewportError::DeformSlotsExhausted { max } if max == REGISTRY_INTERNAL_SLOT_START
492 ));
493 }
494
495 #[test]
496 fn allocate_internal_slot_uses_reserved_range() {
497 let stored = Vec::<StoredDeformer>::new();
498 let slot = allocate_internal_slot(&stored).unwrap();
499 assert!(slot >= REGISTRY_INTERNAL_SLOT_START && slot < REGISTRY_SLOT_COUNT);
500 }
501
502 #[test]
503 fn validate_name_rejects_duplicate() {
504 let mut stored = Vec::<StoredDeformer>::new();
505 stored.push(desc("wind", DeformStage::WorldSpace, 0, ""));
506 let err = validate_name(&stored, "wind").unwrap_err();
507 assert!(matches!(err, ViewportError::DeformNameTaken { .. }));
508 }
509
510 #[test]
511 fn validate_name_rejects_invalid_identifier() {
512 let stored = Vec::<StoredDeformer>::new();
513 for bad in &["", "1wind", "wind!", " wind"] {
514 assert!(matches!(
515 validate_name(&stored, bad),
516 Err(ViewportError::DeformShaderInvalid { .. })
517 ));
518 }
519 assert!(validate_name(&stored, "wind_2").is_ok());
520 }
521
522 #[test]
523 fn identifier_prefix_renames_locals() {
524 let body = "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex {\n return v;\n}\nfn helper() -> f32 { return 1.0; }\n";
525 let out = identifier_prefix("wind", body);
526 assert!(out.contains("fn wind__deform("));
527 assert!(out.contains("fn wind__helper()"));
528 assert!(out.contains("DeformVertex"));
530 assert!(!out.contains("wind__DeformVertex"));
531 }
532
533 #[test]
534 fn build_stage_calls_orders_by_priority_then_name() {
535 let mut stored = vec![
536 desc("z_late", DeformStage::ObjectSpace, 10, ""),
537 desc("a_early", DeformStage::ObjectSpace, -5, ""),
538 desc("m_mid", DeformStage::ObjectSpace, 0, ""),
539 desc("world_only", DeformStage::WorldSpace, 0, ""),
540 ];
541 for (i, d) in stored.iter_mut().enumerate() {
542 d.slot = i;
543 }
544 let obj = build_stage_calls(&stored, DeformStage::ObjectSpace);
545 let pos_a = obj.find("a_early__deform").unwrap();
546 let pos_m = obj.find("m_mid__deform").unwrap();
547 let pos_z = obj.find("z_late__deform").unwrap();
548 assert!(pos_a < pos_m);
549 assert!(pos_m < pos_z);
550 assert!(!obj.contains("world_only__deform"));
551 let w = build_stage_calls(&stored, DeformStage::WorldSpace);
552 assert!(w.contains("world_only__deform"));
553 }
554
555 #[test]
556 fn compose_shader_zero_deformers_is_identity_passthrough() {
557 let Some(src) = lookup_source("mesh.wgsl") else {
558 return;
560 };
561 let composed = compose_shader(src, &[]);
562 assert!(composed.contains("// <viewport-deform-slots:object>"));
563 assert!(!composed.contains("__deform("));
565 }
566
567 #[test]
568 fn compose_shader_splices_one_body_and_wires_call() {
569 let Some(src) = lookup_source("mesh.wgsl") else {
570 return;
571 };
572 let mut d = desc(
573 "wind",
574 DeformStage::WorldSpace,
575 0,
576 "fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex {\n return v;\n}\n",
577 );
578 d.slot = 2;
579 let composed = compose_shader(src, &std::slice::from_ref(&d));
580 assert!(composed.contains("fn wind__deform("));
581 assert!(composed.contains("out = wind__deform(out, ctx_wind);"));
583 assert!(composed.contains("ctx.flags & (1u << 2u)"));
584 assert!(composed.contains("ctx_wind.slot = 2u;"));
585 assert!(composed.contains("DeformVertex"));
587 }
588}