@aardworx/wombat.rendering 0.19.15 → 0.19.17

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.15",
3
+ "version": "0.19.17",
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",
@@ -35,7 +35,17 @@ import { ISampler } from "../core/sampler.js";
35
35
  /** Map shader-defined sampler state (FShade Filter/WrapMode names) to a
36
36
  * GPUSamplerDescriptor (heap path). */
37
37
  function samplerDescriptorFromState(
38
- state: { readonly filter: string; readonly addressU: string; readonly addressV: string },
38
+ state: {
39
+ readonly filter: string;
40
+ readonly addressU: string;
41
+ readonly addressV: string;
42
+ readonly addressW?: string;
43
+ readonly comparison?: string;
44
+ readonly maxAnisotropy?: number;
45
+ readonly minLod?: number;
46
+ readonly maxLod?: number;
47
+ readonly mipLodBias?: number;
48
+ },
39
49
  ): GPUSamplerDescriptor {
40
50
  // FShade WrapMode names. WebGPU has no Border / MirrorOnce — map them to
41
51
  // their closest modes (clamp-to-edge / mirror-repeat).
@@ -66,11 +76,36 @@ function samplerDescriptorFromState(
66
76
  case "MinLinearMagPoint": mag = "nearest"; break;
67
77
  default: break; // unknown → all-linear
68
78
  }
69
- return {
79
+ // Explicit maxAnisotropy overrides the Anisotropic-filter default.
80
+ // WebGPU validation: maxAnisotropy > 1 requires all-linear filtering.
81
+ if (state.maxAnisotropy !== undefined) aniso = state.maxAnisotropy;
82
+ if (min !== "linear" || mag !== "linear" || mip !== "linear") aniso = 1;
83
+ const desc: GPUSamplerDescriptor = {
70
84
  magFilter: mag, minFilter: min, mipmapFilter: mip,
71
85
  addressModeU: addr(state.addressU), addressModeV: addr(state.addressV),
72
86
  maxAnisotropy: aniso,
73
87
  };
88
+ if (state.addressW !== undefined) (desc as { addressModeW?: GPUAddressMode }).addressModeW = addr(state.addressW);
89
+ if (state.minLod !== undefined) (desc as { lodMinClamp?: number }).lodMinClamp = state.minLod;
90
+ if (state.maxLod !== undefined) (desc as { lodMaxClamp?: number }).lodMaxClamp = state.maxLod;
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.
94
+ return desc;
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
+ }
74
109
  }
75
110
  import type { RenderObject } from "../core/renderObject.js";
76
111
  import { asAttributeProvider, asUniformProvider } from "../core/provider.js";
@@ -371,8 +406,16 @@ export function renderObjectToHeapSpec(
371
406
  // builder, carried through the IR) overrides the scene's default sampler.
372
407
  const stateBinding = schema.samplers.find(b => b.state !== undefined);
373
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
+ }
374
417
  distinctSamplerAvals.add(
375
- AVal.constant(ISampler.fromDescriptor(samplerDescriptorFromState(stateBinding.state))) as aval<ISampler>,
418
+ AVal.constant(ISampler.fromDescriptor(desc)) as aval<ISampler>,
376
419
  );
377
420
  } else {
378
421
  ro.samplers.iter((_n, av) => { distinctSamplerAvals.add(av as aval<ISampler>); });
@@ -52,9 +52,20 @@ export interface HeapSamplerBinding {
52
52
  readonly name: string;
53
53
  /** WGSL type — "sampler" or "sampler_comparison". */
54
54
  readonly wgslType: string;
55
- /** Shader-defined sampler state (filter/address) from the IR; the adapter
56
- * builds the GPUSamplerDescriptor from it, overriding the scene default. */
57
- readonly state?: { readonly filter: string; readonly addressU: string; readonly addressV: string } | undefined;
55
+ /** Shader-defined sampler state (filter/address/lod/anisotropy) from the
56
+ * IR; the adapter builds the GPUSamplerDescriptor from it, overriding the
57
+ * scene default. */
58
+ readonly state?: {
59
+ readonly filter: string;
60
+ readonly addressU: string;
61
+ readonly addressV: string;
62
+ readonly addressW?: string;
63
+ readonly comparison?: string;
64
+ readonly maxAnisotropy?: number;
65
+ readonly minLod?: number;
66
+ readonly maxLod?: number;
67
+ readonly mipLodBias?: number;
68
+ } | undefined;
58
69
  }
59
70
 
60
71
  export interface HeapEffectSchema {
@@ -236,15 +247,18 @@ function buildSchema(iface: ProgramInterface): HeapEffectSchema {
236
247
  const fragmentOutputs = iface.fragmentOutputs.map(o => ({
237
248
  name: o.name, location: o.location, wgslType: irTypeToWgsl(o.type),
238
249
  }));
239
- // Texture/sampler bindings post-DCE. The IR Type for textures is a
240
- // structured kind we don't fully model here yet — for v1 every
241
- // texture maps to "texture_2d<f32>" and every sampler to plain
242
- // "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;
243
257
  const textures: HeapTextureBinding[] = iface.textures.map(t => ({
244
- name: t.name, wgslType: "texture_2d<f32>",
258
+ name: t.name, wgslType: isComparison(t.type) ? "texture_depth_2d" : "texture_2d<f32>",
245
259
  }));
246
260
  const samplers: HeapSamplerBinding[] = iface.samplers.map(s => ({
247
- name: s.name, wgslType: "sampler",
261
+ name: s.name, wgslType: isComparison(s.type) ? "sampler_comparison" : "sampler",
248
262
  ...(s.state !== undefined ? { state: s.state } : {}),
249
263
  }));
250
264
  return { attributes, uniforms, varyings, fragmentOutputs, textures, samplers };
@@ -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) {