@aardworx/wombat.rendering 0.9.16 → 0.9.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -891,7 +891,14 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
891
891
  // right per-effect helper at draw time. Atlas-binding shape follows
892
892
  // from `family.drawHeaderUnion.atlasTextureBindings.size > 0`.
893
893
  void texIdOf; // retained for future per-bucket diagnostics
894
- function findOrCreateBucket(effect, _textures, pipelineState) {
894
+ function findOrCreateBucket(effect, _textures, pipelineState,
895
+ /**
896
+ * Optional precomputed descriptor. addDrawImpl passes this when
897
+ * the spec carries `modeRules` so the bucket key reflects the
898
+ * rule-evaluated value (not the raw PS aval values). When omitted
899
+ * the descriptor is snapshotted from PS as before.
900
+ */
901
+ precomputedDescriptor) {
895
902
  if (!familyBuilt) {
896
903
  throw new Error("heapScene: findOrCreateBucket called before family build");
897
904
  }
@@ -904,13 +911,25 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
904
911
  // (the actual value-set in the descriptor), so identical-value
905
912
  // cvals collapse to ONE bucket. See docs/derived-modes.md and
906
913
  // tests/heap-multi-pipeline-bucket.test.ts.
907
- const psDescriptor = snapshotDescriptor(pipelineState, sig);
914
+ const psDescriptor = precomputedDescriptor ?? snapshotDescriptor(pipelineState, sig);
908
915
  const psModeKey = encodeModeKey(psDescriptor);
909
916
  const bk = `family#${fam.schema.id}|mk#${psModeKey.toString(16)}`;
910
917
  const existing = bucketByKey.get(bk);
911
918
  if (existing !== undefined)
912
919
  return existing;
913
- const ps = resolvePipelineState(pipelineState);
920
+ // If a precomputed descriptor is supplied (rule-evaluated path),
921
+ // use its post-rule axis values so the pipeline reflects what the
922
+ // RO actually wants. Otherwise fall through to PS-aval forcing.
923
+ const ps = precomputedDescriptor !== undefined
924
+ ? {
925
+ topology: precomputedDescriptor.topology,
926
+ cullMode: precomputedDescriptor.cullMode,
927
+ frontFace: precomputedDescriptor.frontFace,
928
+ ...(precomputedDescriptor.depth !== undefined
929
+ ? { depth: { write: precomputedDescriptor.depth.write, compare: precomputedDescriptor.depth.compare } }
930
+ : {}),
931
+ }
932
+ : resolvePipelineState(pipelineState);
914
933
  const layout = fam.schema.drawHeaderUnion;
915
934
  const isAtlasBucket = layout.atlasTextureBindings.size > 0;
916
935
  const vsModule = fam.vsModule;
@@ -1185,7 +1204,23 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
1185
1204
  const perInstanceAttributes = spec.instanceAttributes !== undefined
1186
1205
  ? new Set(Object.keys(spec.instanceAttributes))
1187
1206
  : new Set();
1188
- const bucket = findOrCreateBucket(spec.effect, spec.textures, spec.pipelineState);
1207
+ // If the spec has modeRules, pre-snapshot the rule-evaluated
1208
+ // descriptor so the bucket key + pipeline reflect post-rule
1209
+ // values. Without this, two ROs with identical PS but different
1210
+ // rule outputs would collide on the same bucket.
1211
+ let precomputedDescriptor;
1212
+ if (spec.modeRules !== undefined) {
1213
+ const uAvals = new Map();
1214
+ for (const [name, val] of Object.entries(spec.inputs)) {
1215
+ uAvals.set(name, asAval(val));
1216
+ }
1217
+ precomputedDescriptor = snapshotDescriptor(spec.pipelineState, sig, {
1218
+ modeRules: spec.modeRules,
1219
+ uniformAvals: uAvals,
1220
+ token: outerTok,
1221
+ });
1222
+ }
1223
+ const bucket = findOrCreateBucket(spec.effect, spec.textures, spec.pipelineState, precomputedDescriptor);
1189
1224
  const fam = familyFor(spec.effect);
1190
1225
  const effectFields = fam.fieldsForEffect.get(spec.effect.id);
1191
1226
  // Indices live in their own INDEX-usage buffer (WebGPU constraint).
@@ -1357,7 +1392,22 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
1357
1392
  // skipSubscribe: this tracker doesn't install its own callbacks.
1358
1393
  // The scene-level `modeAvalToDrawIds` does it once per unique aval
1359
1394
  // (the 20k-ROs-share-one-cullCval optimization).
1360
- const tracker = new ModeKeyTracker(spec.pipelineState, sig, () => { dirtyModeKeyDrawIds.add(drawId); }, { skipSubscribe: true });
1395
+ // If the spec has modeRules, pre-build a uniforms-aval map the
1396
+ // rule closures will read against; the tracker auto-discovers
1397
+ // which avals each rule reads and surfaces them via forEachLeaf.
1398
+ let uniformAvals;
1399
+ if (spec.modeRules !== undefined) {
1400
+ const m = new Map();
1401
+ for (const [name, val] of Object.entries(spec.inputs)) {
1402
+ m.set(name, asAval(val));
1403
+ }
1404
+ uniformAvals = m;
1405
+ }
1406
+ const tracker = new ModeKeyTracker(spec.pipelineState, sig, () => { dirtyModeKeyDrawIds.add(drawId); }, {
1407
+ skipSubscribe: true,
1408
+ ...(spec.modeRules !== undefined ? { modeRules: spec.modeRules } : {}),
1409
+ ...(uniformAvals !== undefined ? { uniformAvals } : {}),
1410
+ });
1361
1411
  drawIdToModeTracker[drawId] = tracker;
1362
1412
  tracker.forEachLeaf((av) => subscribeModeLeaf(av, drawId));
1363
1413
  // ─── §7 derived-uniforms registration ────────────────────────────