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