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