@aardworx/wombat.rendering 0.21.10 → 0.21.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aardworx/wombat.rendering",
3
- "version": "0.21.10",
3
+ "version": "0.21.12",
4
4
  "description": "WebGPU rendering layer for the Wombat TypeScript stack — RenderObject/RenderTask/runtime + window glue, port of Aardvark.Rendering's lower layers on top of WebGPU and wombat.shader.",
5
5
  "license": "MIT",
6
6
  "author": "krauthaufen",
@@ -72,6 +72,11 @@ interface VertexBindingInfo {
72
72
  readonly slot: number;
73
73
  readonly format: GPUVertexFormat;
74
74
  readonly byteSize: number;
75
+ /** Byte offset to apply at `setVertexBuffer` time (the group's
76
+ * common base — per-attribute layout offsets are RELATIVE to it).
77
+ * Undefined on legacy construction paths → fall back to the
78
+ * view's own offset. */
79
+ readonly baseOffset?: number;
75
80
  }
76
81
 
77
82
  function vertexBindingsFor(iface: ProgramInterface): VertexBindingInfo[] {
@@ -527,7 +532,7 @@ export class PreparedRenderObject {
527
532
  if (res === undefined) throw new Error(`PreparedRenderObject.record: missing vertex buffer for "${vb.name}"`);
528
533
  const view = this.vertexViews.get(vb.name);
529
534
  const buf = res.getValue(token);
530
- const offset = view !== undefined ? BufferView.offsetOf(view) : 0;
535
+ const offset = vb.baseOffset ?? (view !== undefined ? BufferView.offsetOf(view) : 0);
531
536
  if (offset > 0) {
532
537
  // size omitted → WebGPU uses (buffer.size − offset).
533
538
  pass.setVertexBuffer(vb.slot, buf, offset);
@@ -602,6 +607,8 @@ export function prepareRenderObject(
602
607
  readonly stepMode: GPUVertexStepMode;
603
608
  readonly stride: number;
604
609
  readonly offset: number;
610
+ /** Tight byte size of one element (for the slot-fit rule). */
611
+ readonly byteSizeOf: number;
605
612
  readonly format: GPUVertexFormat;
606
613
  readonly view: BufferView;
607
614
  }
@@ -631,6 +638,7 @@ export function prepareRenderObject(
631
638
  name: vb.name,
632
639
  shaderLocation: vb.slot,
633
640
  stepMode, stride, offset,
641
+ byteSizeOf: view.elementType.byteSize,
634
642
  format, view,
635
643
  });
636
644
  }
@@ -645,6 +653,8 @@ export function prepareRenderObject(
645
653
  readonly stride: number;
646
654
  readonly stepMode: GPUVertexStepMode;
647
655
  readonly view: BufferView;
656
+ /** Byte offset the slot is BOUND at (setVertexBuffer). */
657
+ readonly base: number;
648
658
  readonly bufferRes: AdaptiveResource<GPUBuffer>;
649
659
  readonly members: ResolvedBinding[];
650
660
  }
@@ -659,9 +669,19 @@ export function prepareRenderObject(
659
669
  let nextSlot = 0;
660
670
  const vertexBuffers = new Map<string, AdaptiveResource<GPUBuffer>>();
661
671
  const vertexViews = new Map<string, BufferView>();
662
- for (const r of resolved) {
672
+ // WebGPU validates `attribute.offset + formatSize <= arrayStride`
673
+ // (stride > 0), so a view whose offset does not fit RELATIVE to the
674
+ // group's base must live in its OWN slot, bound with a buffer
675
+ // offset at `setVertexBuffer` time — the strided-views idiom
676
+ // (SegA = P[i] at offset 0, SegB = P[i+1] at offset 12, stride 12)
677
+ // depends on this. Group key therefore includes the byte offset the
678
+ // slot is BOUND at (`base`); attributes whose offset equals the
679
+ // base share the slot with relative offset 0, interleaved layouts
680
+ // (offsets 0/16/32/48 within stride 64) join the base-0 group as
681
+ // before via the fit rule below.
682
+ const addToGroup = (r: ResolvedBinding, base: number): void => {
663
683
  const bufId = idOf(r.view.buffer as unknown as object);
664
- const key = `${bufId}|${r.stride}|${r.stepMode}`;
684
+ const key = `${bufId}|${r.stride}|${r.stepMode}|${base}`;
665
685
  let group = vbGroups.get(key);
666
686
  if (!group) {
667
687
  const bufferRes = prepareAdaptiveBuffer(device, r.view.buffer, {
@@ -670,13 +690,33 @@ export function prepareRenderObject(
670
690
  });
671
691
  group = {
672
692
  key, slot: nextSlot++, stride: r.stride, stepMode: r.stepMode,
673
- view: r.view, bufferRes, members: [],
693
+ view: r.view, base, bufferRes, members: [],
674
694
  };
675
695
  vbGroups.set(key, group);
676
696
  }
677
697
  group.members.push(r);
678
698
  vertexBuffers.set(r.name, group.bufferRes);
679
699
  vertexViews.set(r.name, r.view);
700
+ };
701
+ const fitsAt = (r: ResolvedBinding, base: number): boolean =>
702
+ r.stride === 0
703
+ ? r.offset === base
704
+ : r.offset >= base && r.offset - base + r.byteSizeOf <= r.stride;
705
+ for (const r of resolved) {
706
+ // Prefer an existing group of this (buffer, stride, stepMode)
707
+ // whose base the attribute's offset fits relative to; else open a
708
+ // group based at the attribute's own offset (relative offset 0 —
709
+ // always valid).
710
+ const bufId = idOf(r.view.buffer as unknown as object);
711
+ let placed = false;
712
+ for (const g of vbGroups.values()) {
713
+ if (g.key.startsWith(`${bufId}|${r.stride}|${r.stepMode}|`) && fitsAt(r, g.base)) {
714
+ addToGroup(r, g.base);
715
+ placed = true;
716
+ break;
717
+ }
718
+ }
719
+ if (!placed) addToGroup(r, r.offset);
680
720
  }
681
721
  const vertexLayouts: GPUVertexBufferLayout[] = [];
682
722
  for (const g of vbGroups.values()) {
@@ -685,7 +725,7 @@ export function prepareRenderObject(
685
725
  stepMode: g.stepMode,
686
726
  attributes: g.members.map((m) => ({
687
727
  shaderLocation: m.shaderLocation,
688
- offset: m.offset,
728
+ offset: m.offset - g.base,
689
729
  format: m.format,
690
730
  })),
691
731
  };
@@ -709,6 +749,7 @@ export function prepareRenderObject(
709
749
  slot: g.slot,
710
750
  format: head.format,
711
751
  byteSize: 0,
752
+ baseOffset: g.base,
712
753
  });
713
754
  }
714
755
  vertexBindings = drawTimeBindings;
@@ -224,7 +224,9 @@ export function isHeapEligible(ro: RenderObject): aval<boolean> {
224
224
  for (const av of textures) if (!isHeapServableTexture(av.getValue(token))) return false;
225
225
  for (const av of samplers) if (!isHeapServableSampler(av.getValue(token))) return false;
226
226
  const dc = ro.drawCall.getValue(token);
227
- if (dc.instanceCount < 1) return false;
227
+ // 0 is heap-legal (the scan kernel emits indexCount × 0 = nothing,
228
+ // matching what legacy draws) — only negative counts are malformed.
229
+ if (dc.instanceCount < 0) return false;
228
230
  if (dc.firstInstance !== 0) return false;
229
231
  if (dc.kind === "indexed") {
230
232
  if (ro.indices === undefined) return false;
@@ -1732,7 +1732,18 @@ export function buildHeapScene(
1732
1732
  // `colorTargets` here silently dropped blends/write-masks from every
1733
1733
  // heap pipeline: transparent heap leaves painted opaque, and the OIT
1734
1734
  // pick passes' write-mask-0 "Colors" leaked color onto the composite.
1735
+ // Locations the family's fragment shader actually writes — a
1736
+ // signature target the shader has NO output for must set
1737
+ // writeMask 0 (WebGPU: non-zero writeMask without a fragment
1738
+ // output is a validation error; e.g. a non-picking leaf in a
1739
+ // Colors+pickId pass writes only Colors).
1740
+ const writtenLocations = new Set<number>();
1741
+ for (const e of fam.schema.effects) {
1742
+ const sc = fam.schema.perEffectSchema.get(e)!;
1743
+ for (const fo of sc.fragmentOutputs) writtenLocations.add(fo.location);
1744
+ }
1735
1745
  const targets: GPUColorTargetState[] = colorTargets.map((t, i) => {
1746
+ if (!writtenLocations.has(i)) return { format: t.format, writeMask: 0 };
1736
1747
  const a = desc.attachments[i];
1737
1748
  if (a === undefined) return { format: t.format };
1738
1749
  return {