@aardworx/wombat.rendering 0.19.16 → 0.19.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aardworx/wombat.rendering",
3
- "version": "0.19.16",
3
+ "version": "0.19.18",
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",
@@ -88,10 +88,25 @@ function samplerDescriptorFromState(
88
88
  if (state.addressW !== undefined) (desc as { addressModeW?: GPUAddressMode }).addressModeW = addr(state.addressW);
89
89
  if (state.minLod !== undefined) (desc as { lodMinClamp?: number }).lodMinClamp = state.minLod;
90
90
  if (state.maxLod !== undefined) (desc as { lodMaxClamp?: number }).lodMaxClamp = state.maxLod;
91
- // NOT applied: `comparison` (a compare sampler needs a sampler_comparison
92
- // binding separate feature) and `mipLodBias` (no WebGPU counterpart).
91
+ // `comparison` is applied by the caller when the binding is a
92
+ // sampler_comparison (a compare function on a filtering sampler would
93
+ // fail WebGPU validation). `mipLodBias` has no WebGPU counterpart.
93
94
  return desc;
94
95
  }
96
+
97
+ /** FShade ComparisonFunction case name → GPUCompareFunction. */
98
+ function compareFunctionOf(name: string): GPUCompareFunction {
99
+ switch (name) {
100
+ case "Never": return "never";
101
+ case "Less": return "less";
102
+ case "Equal": return "equal";
103
+ case "LessOrEqual": return "less-equal";
104
+ case "Greater": return "greater";
105
+ case "GreaterOrEqual": return "greater-equal";
106
+ case "NotEqual": return "not-equal";
107
+ default: return "always";
108
+ }
109
+ }
95
110
  import type { RenderObject } from "../core/renderObject.js";
96
111
  import { asAttributeProvider, asUniformProvider } from "../core/provider.js";
97
112
  import type { HeapDrawSpec, HeapTextureSet } from "./heapScene.js";
@@ -391,8 +406,16 @@ export function renderObjectToHeapSpec(
391
406
  // builder, carried through the IR) overrides the scene's default sampler.
392
407
  const stateBinding = schema.samplers.find(b => b.state !== undefined);
393
408
  if (stateBinding?.state !== undefined) {
409
+ const desc = samplerDescriptorFromState(stateBinding.state);
410
+ if (stateBinding.wgslType === "sampler_comparison") {
411
+ // Shadow sampler: the binding requires a compare function. WebGPU
412
+ // also forbids anisotropy unless all filters are linear; keep the
413
+ // state's choice (samplerDescriptorFromState already clamps).
414
+ (desc as { compare?: GPUCompareFunction }).compare =
415
+ compareFunctionOf(stateBinding.state.comparison ?? "LessOrEqual");
416
+ }
394
417
  distinctSamplerAvals.add(
395
- AVal.constant(ISampler.fromDescriptor(samplerDescriptorFromState(stateBinding.state))) as aval<ISampler>,
418
+ AVal.constant(ISampler.fromDescriptor(desc)) as aval<ISampler>,
396
419
  );
397
420
  } else {
398
421
  ro.samplers.iter((_n, av) => { distinctSamplerAvals.add(av as aval<ISampler>); });
@@ -247,15 +247,18 @@ function buildSchema(iface: ProgramInterface): HeapEffectSchema {
247
247
  const fragmentOutputs = iface.fragmentOutputs.map(o => ({
248
248
  name: o.name, location: o.location, wgslType: irTypeToWgsl(o.type),
249
249
  }));
250
- // Texture/sampler bindings post-DCE. The IR Type for textures is a
251
- // structured kind we don't fully model here yet — for v1 every
252
- // texture maps to "texture_2d<f32>" and every sampler to plain
253
- // "sampler". Specialise as multi-kind support lands in the DSL.
250
+ // Texture/sampler bindings post-DCE. Comparison (shadow) samplers pair
251
+ // with depth textures: the legalise pass marks both with
252
+ // `comparison: true` — map them to "texture_depth_2d" /
253
+ // "sampler_comparison". Everything else stays the v1 2D-float kind;
254
+ // specialise further as multi-kind support lands in the DSL.
255
+ const isComparison = (t: unknown): boolean =>
256
+ typeof t === "object" && t !== null && (t as { comparison?: boolean }).comparison === true;
254
257
  const textures: HeapTextureBinding[] = iface.textures.map(t => ({
255
- name: t.name, wgslType: "texture_2d<f32>",
258
+ name: t.name, wgslType: isComparison(t.type) ? "texture_depth_2d" : "texture_2d<f32>",
256
259
  }));
257
260
  const samplers: HeapSamplerBinding[] = iface.samplers.map(s => ({
258
- name: s.name, wgslType: "sampler",
261
+ name: s.name, wgslType: isComparison(s.type) ? "sampler_comparison" : "sampler",
259
262
  ...(s.state !== undefined ? { state: s.state } : {}),
260
263
  }));
261
264
  return { attributes, uniforms, varyings, fragmentOutputs, textures, samplers };
@@ -23,6 +23,17 @@ import { GrowBuffer, ALIGN16, DEFAULT_MAX_BUFFER_BYTES } from "./growBuffer.js";
23
23
  import { Freelist } from "./freelist.js";
24
24
  import { ChunkedAttributeArena, ChunkedIndexAllocator } from "./chunkedArena.js";
25
25
 
26
+ // ─── Debug toggles ─────────────────────────────────────────────────────
27
+
28
+ /** Opt-in O(live-allocs) overlap validation on every alloc. Set
29
+ * `globalThis.__wombatDebugAllocOverlap = true` BEFORE scene creation
30
+ * to enable. Default OFF: the scan made scene build O(n²) — 65 s for a
31
+ * 20 k-object scene, renderer-crash territory at 68 k. The O(1)
32
+ * release-side checks (double-free, size mismatch) stay always-on. */
33
+ const DEBUG_ALLOC_OVERLAP: boolean =
34
+ (globalThis as Record<string, unknown> & typeof globalThis)
35
+ .__wombatDebugAllocOverlap === true;
36
+
26
37
  // ─── Per-allocation header layout ──────────────────────────────────────
27
38
 
28
39
  /** Per-allocation header: (u32 typeId, u32 length). Data follows
@@ -485,19 +496,20 @@ export class AttributeArena {
485
496
  this.buf.setUsed(this.cursor);
486
497
  }
487
498
  }
488
- /** Throws if the just-returned alloc overlaps any live allocation. */
499
+ /** Tracks the alloc; with `__wombatDebugAllocOverlap` set, also
500
+ * throws if it overlaps any live allocation (O(live) per alloc —
501
+ * made scene build O(n²) when always-on, hence opt-in). */
489
502
  private recordAlloc(off: number, size: number, source: string): void {
490
- // Check against every existing live alloc — O(N). Cheap while N
491
- // is small (per-tile demos run with ~hundreds of allocs). Wire
492
- // off behind an env-flag if it ever becomes a hot path.
493
- for (const [liveOff, liveSize] of this.liveAllocs) {
503
+ if (DEBUG_ALLOC_OVERLAP) {
494
504
  // [a, a+sa) vs [b, b+sb) overlap iff a < b+sb && b < a+sa
495
- if (off < liveOff + liveSize && liveOff < off + size) {
496
- throw new Error(
497
- `AttributeArena.tryAlloc(${source}): returned [${off},${off + size}) ` +
498
- `(size=${size}) overlaps live alloc at [${liveOff},${liveOff + liveSize}) ` +
499
- `(size=${liveSize}). Allocator is handing out shared memory!`,
500
- );
505
+ for (const [liveOff, liveSize] of this.liveAllocs) {
506
+ if (off < liveOff + liveSize && liveOff < off + size) {
507
+ throw new Error(
508
+ `AttributeArena.tryAlloc(${source}): returned [${off},${off + size}) ` +
509
+ `(size=${size}) overlaps live alloc at [${liveOff},${liveOff + liveSize}) ` +
510
+ `(size=${liveSize}). Allocator is handing out shared memory!`,
511
+ );
512
+ }
501
513
  }
502
514
  }
503
515
  this.liveAllocs.set(off, size);
@@ -566,13 +578,15 @@ export class IndexAllocator {
566
578
  return off;
567
579
  }
568
580
  private recordAlloc(off: number, size: number, source: string): void {
569
- for (const [liveOff, liveSize] of this.liveAllocs) {
570
- if (off < liveOff + liveSize && liveOff < off + size) {
571
- throw new Error(
572
- `IndexAllocator.tryAlloc(${source}): returned [${off},${off + size}) ` +
573
- `(size=${size}) overlaps live alloc at [${liveOff},${liveOff + liveSize}) ` +
574
- `(size=${liveSize}). Index allocator is handing out shared memory!`,
575
- );
581
+ if (DEBUG_ALLOC_OVERLAP) {
582
+ for (const [liveOff, liveSize] of this.liveAllocs) {
583
+ if (off < liveOff + liveSize && liveOff < off + size) {
584
+ throw new Error(
585
+ `IndexAllocator.tryAlloc(${source}): returned [${off},${off + size}) ` +
586
+ `(size=${size}) overlaps live alloc at [${liveOff},${liveOff + liveSize}) ` +
587
+ `(size=${liveSize}). Index allocator is handing out shared memory!`,
588
+ );
589
+ }
576
590
  }
577
591
  }
578
592
  this.liveAllocs.set(off, size);
@@ -1273,7 +1273,10 @@ export function buildHeapScene(
1273
1273
  interface BglEntry { bgl: GPUBindGroupLayout; pipelineLayout: GPUPipelineLayout }
1274
1274
  const bglCache = new Map<string, BglEntry>();
1275
1275
  function getBgl(layout: BucketLayout, withAtlasArrays: boolean): BglEntry {
1276
- const key = `t${layout.textureBindings.length}|s${layout.samplerBindings.length}|a${withAtlasArrays ? 1 : 0}`;
1276
+ const key =
1277
+ `t${layout.textureBindings.map(t => t.wgslType).join(",")}` +
1278
+ `|s${layout.samplerBindings.map(s => s.wgslType).join(",")}` +
1279
+ `|a${withAtlasArrays ? 1 : 0}`;
1277
1280
  let e = bglCache.get(key);
1278
1281
  if (e !== undefined) return e;
1279
1282
  // Heap data buffers are read by both stages: FS uniform-via-varying
@@ -1294,13 +1297,13 @@ export function buildHeapScene(
1294
1297
  for (const t of layout.textureBindings) {
1295
1298
  entries.push({
1296
1299
  binding: t.binding, visibility: GPUShaderStage.FRAGMENT,
1297
- texture: { sampleType: "float" },
1300
+ texture: { sampleType: t.wgslType === "texture_depth_2d" ? "depth" : "float" },
1298
1301
  });
1299
1302
  }
1300
1303
  for (const s of layout.samplerBindings) {
1301
1304
  entries.push({
1302
1305
  binding: s.binding, visibility: GPUShaderStage.FRAGMENT,
1303
- sampler: { type: "filtering" },
1306
+ sampler: { type: s.wgslType === "sampler_comparison" ? "comparison" : "filtering" },
1304
1307
  });
1305
1308
  }
1306
1309
  if (withAtlasArrays) {