@@ -11,6 +11,12 @@ use super::nodes;
1111/// Populated from disk by the resolver and passed to `compile()`.
1212pub type FunctionRegistry = HashMap < String , MaterialFunction > ;
1313
14+ /// How many distinct named parameters one master graph can declare. Must
15+ /// match the array size in the WGSL `SurfaceGraphParams` declaration and
16+ /// `surface_ext::SURFACE_GRAPH_PARAM_SLOTS`. Bumping this requires updating
17+ /// all three locations together.
18+ pub const MAX_PARAMETER_SLOTS : usize = 32 ;
19+
1420/// Sanitize a function name into a WGSL-identifier-safe string.
1521fn safe_fn_ident ( name : & str ) -> String {
1622 let mut out = String :: with_capacity ( name. len ( ) ) ;
@@ -144,10 +150,17 @@ struct Ctx<'a> {
144150 uses_cube_0 : bool ,
145151 uses_array_0 : bool ,
146152 uses_volume_0 : bool ,
147- /// Named parameters discovered while walking the graph. Order is the order
148- /// nodes were visited during recursion — stable across compiles of an
149- /// unchanged graph, which matters for instance override stability.
153+ /// Named parameters discovered while walking the graph. The `Vec`'s
154+ /// position is the slot index in `material_params.slots[N]` — codegen
155+ /// emits reads keyed on that index, and the resolver writes the
156+ /// corresponding default (or instance override) into the same slot.
157+ /// Names are deduped: two `param/float` nodes with the same name share
158+ /// one slot so changing one override updates every reader.
150159 parameters : Vec < MaterialParam > ,
160+ /// Name → slot index, mirroring `parameters` ordering. Lets the codegen
161+ /// look up an already-allocated slot in O(1) when the same parameter
162+ /// name appears on multiple nodes.
163+ parameter_slots : HashMap < String , usize > ,
151164}
152165
153166impl < ' a > Ctx < ' a > {
@@ -190,7 +203,34 @@ impl<'a> Ctx<'a> {
190203 uses_array_0 : false ,
191204 uses_volume_0 : false ,
192205 parameters : Vec :: new ( ) ,
206+ parameter_slots : HashMap :: new ( ) ,
207+ }
208+ }
209+
210+ /// Allocate (or reuse) a parameter slot by name. The first call for a
211+ /// given name appends to `parameters`; subsequent calls return the
212+ /// existing slot. Saturates at the last slot if a graph exceeds the
213+ /// uniform buffer's capacity — every read past the cap collides on
214+ /// slot N-1, which is wrong but won't UB-trap the GPU. The compile
215+ /// emits a warning so the user can split the master.
216+ fn intern_parameter ( & mut self , name : & str , kind : ParamKind , default : graph:: PinValue ) -> usize {
217+ if let Some ( & slot) = self . parameter_slots . get ( name) {
218+ return slot;
219+ }
220+ let slot = self . parameters . len ( ) ;
221+ if slot >= MAX_PARAMETER_SLOTS {
222+ // Once we hit the cap, every subsequent unique name aliases the
223+ // last slot. We still record the parameter so tooling can list
224+ // it, but the actual reads will collide.
225+ return MAX_PARAMETER_SLOTS - 1 ;
193226 }
227+ self . parameters . push ( MaterialParam {
228+ name : name. to_string ( ) ,
229+ kind,
230+ default,
231+ } ) ;
232+ self . parameter_slots . insert ( name. to_string ( ) , slot) ;
233+ slot
194234 }
195235
196236 fn next_var ( & mut self , prefix : & str ) -> String {
@@ -495,24 +535,23 @@ impl<'a> Ctx<'a> {
495535
496536 // ── Parameters ──────────────────────────────────────────
497537 //
498- // Stage 2: each `param/*` node bakes its authored default into the
499- // shader as a literal AND records the (name, kind, default) so
500- // downstream tooling can build material instances. Stage 3 will
501- // swap the literal for a uniform read so instances can override
502- // without recompiling the master.
538+ // Each `param/*` node reads from `material_params.slots[N]`
539+ // where `N` is the slot index allocated by `intern_parameter`.
540+ // The resolver writes the master's authored default into that
541+ // slot when building the master GraphMaterial; material
542+ // instances reuse the master's compiled shader and overwrite
543+ // the same slot with their per-instance value.
503544 "param/float" => {
504545 let name = param_name ( node, "FloatParam" ) ;
505546 let default = match node. input_values . get ( "default" ) {
506547 Some ( PinValue :: Float ( f) ) => * f,
507548 _ => 0.0 ,
508549 } ;
509- self . parameters . push ( MaterialParam {
510- name,
511- kind : ParamKind :: Float ,
512- default : PinValue :: Float ( default) ,
513- } ) ;
550+ let slot = self . intern_parameter ( & name, ParamKind :: Float , PinValue :: Float ( default) ) ;
514551 let v = self . next_var ( "param_f" ) ;
515- self . emit ( format ! ( " let {v} = {:.6};" , default ) ) ;
552+ self . emit ( format ! (
553+ " let {v} = material_params.slots[{slot}].x;"
554+ ) ) ;
516555 self . set_out ( id, "value" , v) ;
517556 }
518557 "param/color" => {
@@ -521,16 +560,9 @@ impl<'a> Ctx<'a> {
521560 Some ( PinValue :: Color ( c) ) => * c,
522561 _ => [ 1.0 , 1.0 , 1.0 , 1.0 ] ,
523562 } ;
524- self . parameters . push ( MaterialParam {
525- name,
526- kind : ParamKind :: Color ,
527- default : PinValue :: Color ( default) ,
528- } ) ;
563+ let slot = self . intern_parameter ( & name, ParamKind :: Color , PinValue :: Color ( default) ) ;
529564 let v = self . next_var ( "param_c" ) ;
530- self . emit ( format ! (
531- " let {v} = vec4<f32>({:.6}, {:.6}, {:.6}, {:.6});" ,
532- default [ 0 ] , default [ 1 ] , default [ 2 ] , default [ 3 ]
533- ) ) ;
565+ self . emit ( format ! ( " let {v} = material_params.slots[{slot}];" ) ) ;
534566 self . set_out ( id, "value" , v) ;
535567 }
536568 "param/vec2" => {
@@ -539,15 +571,10 @@ impl<'a> Ctx<'a> {
539571 Some ( PinValue :: Vec2 ( v) ) => * v,
540572 _ => [ 0.0 , 0.0 ] ,
541573 } ;
542- self . parameters . push ( MaterialParam {
543- name,
544- kind : ParamKind :: Vec2 ,
545- default : PinValue :: Vec2 ( default) ,
546- } ) ;
574+ let slot = self . intern_parameter ( & name, ParamKind :: Vec2 , PinValue :: Vec2 ( default) ) ;
547575 let v = self . next_var ( "param_v2" ) ;
548576 self . emit ( format ! (
549- " let {v} = vec2<f32>({:.6}, {:.6});" ,
550- default [ 0 ] , default [ 1 ]
577+ " let {v} = material_params.slots[{slot}].xy;"
551578 ) ) ;
552579 self . set_out ( id, "value" , v) ;
553580 }
@@ -557,15 +584,10 @@ impl<'a> Ctx<'a> {
557584 Some ( PinValue :: Vec3 ( v) ) => * v,
558585 _ => [ 0.0 , 0.0 , 0.0 ] ,
559586 } ;
560- self . parameters . push ( MaterialParam {
561- name,
562- kind : ParamKind :: Vec3 ,
563- default : PinValue :: Vec3 ( default) ,
564- } ) ;
587+ let slot = self . intern_parameter ( & name, ParamKind :: Vec3 , PinValue :: Vec3 ( default) ) ;
565588 let v = self . next_var ( "param_v3" ) ;
566589 self . emit ( format ! (
567- " let {v} = vec3<f32>({:.6}, {:.6}, {:.6});" ,
568- default [ 0 ] , default [ 1 ] , default [ 2 ]
590+ " let {v} = material_params.slots[{slot}].xyz;"
569591 ) ) ;
570592 self . set_out ( id, "value" , v) ;
571593 }
@@ -575,16 +597,9 @@ impl<'a> Ctx<'a> {
575597 Some ( PinValue :: Vec4 ( v) ) => * v,
576598 _ => [ 0.0 , 0.0 , 0.0 , 0.0 ] ,
577599 } ;
578- self . parameters . push ( MaterialParam {
579- name,
580- kind : ParamKind :: Vec4 ,
581- default : PinValue :: Vec4 ( default) ,
582- } ) ;
600+ let slot = self . intern_parameter ( & name, ParamKind :: Vec4 , PinValue :: Vec4 ( default) ) ;
583601 let v = self . next_var ( "param_v4" ) ;
584- self . emit ( format ! (
585- " let {v} = vec4<f32>({:.6}, {:.6}, {:.6}, {:.6});" ,
586- default [ 0 ] , default [ 1 ] , default [ 2 ] , default [ 3 ]
587- ) ) ;
602+ self . emit ( format ! ( " let {v} = material_params.slots[{slot}];" ) ) ;
588603 self . set_out ( id, "value" , v) ;
589604 }
590605 "param/bool" => {
@@ -593,15 +608,10 @@ impl<'a> Ctx<'a> {
593608 Some ( PinValue :: Bool ( b) ) => * b,
594609 _ => false ,
595610 } ;
596- self . parameters . push ( MaterialParam {
597- name,
598- kind : ParamKind :: Bool ,
599- default : PinValue :: Bool ( default) ,
600- } ) ;
611+ let slot = self . intern_parameter ( & name, ParamKind :: Bool , PinValue :: Bool ( default) ) ;
601612 let v = self . next_var ( "param_b" ) ;
602613 self . emit ( format ! (
603- " let {v} = {};" ,
604- if default { "true" } else { "false" }
614+ " let {v} = material_params.slots[{slot}].x > 0.5;"
605615 ) ) ;
606616 self . set_out ( id, "value" , v) ;
607617 }
@@ -2602,6 +2612,13 @@ fn texture_bindings_wgsl(ctx: &Ctx) -> String {
26022612 s. push_str ( "@group(3) @binding(112) var volume_0: texture_3d<f32>;\n " ) ;
26032613 s. push_str ( "@group(3) @binding(113) var volume_0_sampler: sampler;\n " ) ;
26042614 }
2615+ // Always declare the parameter UBO at @binding(118) — the
2616+ // `AsBindGroup` derive on `SurfaceGraphExt` requires this slot to be
2617+ // bound regardless of whether the graph reads any params, otherwise
2618+ // wgpu rejects the pipeline at draw time. Keep the slot count in sync
2619+ // with `surface_ext::SURFACE_GRAPH_PARAM_SLOTS`.
2620+ s. push_str ( "struct SurfaceGraphParams { slots: array<vec4<f32>, 32>, }\n " ) ;
2621+ s. push_str ( "@group(3) @binding(118) var<uniform> material_params: SurfaceGraphParams;\n " ) ;
26052622 s
26062623}
26072624
0 commit comments