@aardworx/wombat.rendering 0.1.1 → 0.3.0
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/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/pipelineState.d.ts +93 -23
- package/dist/core/pipelineState.d.ts.map +1 -1
- package/dist/core/pipelineState.js +83 -1
- package/dist/core/pipelineState.js.map +1 -1
- package/dist/core/renderObject.d.ts +11 -4
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts +37 -3
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +274 -91
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/runtime/scenePass.d.ts.map +1 -1
- package/dist/runtime/scenePass.js +6 -1
- package/dist/runtime/scenePass.js.map +1 -1
- package/package.json +1 -1
- package/src/core/index.ts +10 -1
- package/src/core/pipelineState.ts +178 -24
- package/src/core/renderObject.ts +11 -4
- package/src/resources/preparedRenderObject.ts +358 -92
- package/src/runtime/scenePass.ts +6 -1
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
// Reads the wombat.shader `ProgramInterface` for everything needed
|
|
5
5
|
// to lay out vertex buffers, bind groups, color targets, and draw
|
|
6
6
|
// state.
|
|
7
|
+
//
|
|
8
|
+
// Pipeline state is fully reactive: the pipeline-influencing avals
|
|
9
|
+
// (rasterizer / depth / stencil ops / blends / alphaToCoverage) are
|
|
10
|
+
// snapshotted at token-evaluation time into a cache key; the
|
|
11
|
+
// resulting `GPURenderPipeline` is cached per-key in the
|
|
12
|
+
// PreparedRenderObject so flipping a non-pipeline-affecting field
|
|
13
|
+
// (`stencilReference`, `blendConstant`) does NOT trigger a recompile.
|
|
7
14
|
|
|
8
15
|
import {
|
|
9
16
|
AdaptiveResource,
|
|
@@ -15,9 +22,11 @@ import {
|
|
|
15
22
|
import {
|
|
16
23
|
type AdaptiveToken,
|
|
17
24
|
type aval,
|
|
18
|
-
|
|
25
|
+
HashMap,
|
|
26
|
+
AVal,
|
|
19
27
|
cval,
|
|
20
28
|
} from "@aardworx/wombat.adaptive";
|
|
29
|
+
import type { BufferView } from "../core/bufferView.js";
|
|
21
30
|
import type { Type } from "@aardworx/wombat.shader/ir";
|
|
22
31
|
import { prepareAdaptiveBuffer } from "./adaptiveBuffer.js";
|
|
23
32
|
import { prepareAdaptiveTexture } from "./adaptiveTexture.js";
|
|
@@ -56,8 +65,6 @@ interface VertexBindingInfo {
|
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
function vertexBindingsFor(iface: ProgramInterface): VertexBindingInfo[] {
|
|
59
|
-
// `iface.attributes` is already filtered to non-builtin vertex inputs
|
|
60
|
-
// by wombat.shader's interface builder.
|
|
61
68
|
return iface.attributes.map(a => ({
|
|
62
69
|
name: a.name,
|
|
63
70
|
slot: a.location,
|
|
@@ -66,19 +73,127 @@ function vertexBindingsFor(iface: ProgramInterface): VertexBindingInfo[] {
|
|
|
66
73
|
}));
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Plain pipeline-state snapshot — what we evaluate avals into per-frame.
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
interface BlendComponentSnap {
|
|
81
|
+
readonly operation: GPUBlendOperation;
|
|
82
|
+
readonly srcFactor: GPUBlendFactor;
|
|
83
|
+
readonly dstFactor: GPUBlendFactor;
|
|
84
|
+
}
|
|
85
|
+
interface BlendSnap {
|
|
86
|
+
readonly color: BlendComponentSnap;
|
|
87
|
+
readonly alpha: BlendComponentSnap;
|
|
88
|
+
readonly writeMask: number;
|
|
89
|
+
}
|
|
90
|
+
interface StencilFaceSnap {
|
|
91
|
+
readonly compare: GPUCompareFunction;
|
|
92
|
+
readonly failOp: GPUStencilOperation;
|
|
93
|
+
readonly depthFailOp: GPUStencilOperation;
|
|
94
|
+
readonly passOp: GPUStencilOperation;
|
|
95
|
+
}
|
|
96
|
+
interface StencilSnap {
|
|
97
|
+
readonly enabled: boolean;
|
|
98
|
+
readonly readMask: number;
|
|
99
|
+
readonly writeMask: number;
|
|
100
|
+
readonly front: StencilFaceSnap;
|
|
101
|
+
readonly back: StencilFaceSnap;
|
|
102
|
+
}
|
|
103
|
+
interface PipelineSnap {
|
|
104
|
+
readonly topology: GPUPrimitiveTopology;
|
|
105
|
+
readonly cullMode: import("../core/index.js").CullMode;
|
|
106
|
+
readonly frontFace: import("../core/index.js").FrontFace;
|
|
107
|
+
readonly depthBias: { readonly constant: number; readonly slopeScale: number; readonly clamp: number } | undefined;
|
|
108
|
+
readonly depthWrite: boolean | undefined;
|
|
109
|
+
readonly depthCompare: GPUCompareFunction | undefined;
|
|
110
|
+
readonly depthClamp: boolean | undefined;
|
|
111
|
+
readonly stencil: StencilSnap | undefined;
|
|
112
|
+
readonly blends: ReadonlyArray<readonly [string, BlendSnap]> | undefined;
|
|
113
|
+
readonly alphaToCoverage: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function snapshotPipeline(
|
|
117
|
+
ps: import("../core/index.js").PipelineState,
|
|
118
|
+
token: AdaptiveToken,
|
|
119
|
+
): PipelineSnap {
|
|
120
|
+
const r = ps.rasterizer;
|
|
121
|
+
const depthBias = r.depthBias !== undefined ? r.depthBias.getValue(token) : undefined;
|
|
122
|
+
let stencil: StencilSnap | undefined;
|
|
123
|
+
if (ps.stencil !== undefined) {
|
|
124
|
+
const s = ps.stencil;
|
|
125
|
+
stencil = {
|
|
126
|
+
enabled: s.enabled.getValue(token),
|
|
127
|
+
readMask: s.readMask.getValue(token),
|
|
128
|
+
writeMask: s.writeMask.getValue(token),
|
|
129
|
+
front: {
|
|
130
|
+
compare: s.front.compare.getValue(token),
|
|
131
|
+
failOp: s.front.failOp.getValue(token),
|
|
132
|
+
depthFailOp: s.front.depthFailOp.getValue(token),
|
|
133
|
+
passOp: s.front.passOp.getValue(token),
|
|
134
|
+
},
|
|
135
|
+
back: {
|
|
136
|
+
compare: s.back.compare.getValue(token),
|
|
137
|
+
failOp: s.back.failOp.getValue(token),
|
|
138
|
+
depthFailOp: s.back.depthFailOp.getValue(token),
|
|
139
|
+
passOp: s.back.passOp.getValue(token),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
let blends: ReadonlyArray<readonly [string, BlendSnap]> | undefined;
|
|
144
|
+
if (ps.blends !== undefined) {
|
|
145
|
+
const m = ps.blends.getValue(token);
|
|
146
|
+
const arr: [string, BlendSnap][] = [];
|
|
147
|
+
for (const [k, b] of m) {
|
|
148
|
+
arr.push([k, {
|
|
149
|
+
color: {
|
|
150
|
+
operation: b.color.operation.getValue(token),
|
|
151
|
+
srcFactor: b.color.srcFactor.getValue(token),
|
|
152
|
+
dstFactor: b.color.dstFactor.getValue(token),
|
|
153
|
+
},
|
|
154
|
+
alpha: {
|
|
155
|
+
operation: b.alpha.operation.getValue(token),
|
|
156
|
+
srcFactor: b.alpha.srcFactor.getValue(token),
|
|
157
|
+
dstFactor: b.alpha.dstFactor.getValue(token),
|
|
158
|
+
},
|
|
159
|
+
writeMask: b.writeMask.getValue(token),
|
|
160
|
+
}]);
|
|
161
|
+
}
|
|
162
|
+
arr.sort((x, y) => (x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0));
|
|
163
|
+
blends = arr;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
topology: r.topology.getValue(token),
|
|
167
|
+
cullMode: r.cullMode.getValue(token),
|
|
168
|
+
frontFace: r.frontFace.getValue(token),
|
|
169
|
+
depthBias,
|
|
170
|
+
depthWrite: ps.depth?.write.getValue(token),
|
|
171
|
+
depthCompare: ps.depth?.compare.getValue(token),
|
|
172
|
+
depthClamp: ps.depth?.clamp?.getValue(token),
|
|
173
|
+
stencil,
|
|
174
|
+
blends,
|
|
175
|
+
alphaToCoverage: ps.alphaToCoverage?.getValue(token) ?? false,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function pipelineKeyOf(snap: PipelineSnap): string {
|
|
180
|
+
return JSON.stringify(snap);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function colorTargetsForSnap(
|
|
70
184
|
iface: ProgramInterface,
|
|
71
185
|
sig: FramebufferSignature,
|
|
72
|
-
blends:
|
|
186
|
+
blends: ReadonlyArray<readonly [string, BlendSnap]> | undefined,
|
|
73
187
|
): GPUColorTargetState[] {
|
|
74
188
|
const out: GPUColorTargetState[] = [];
|
|
189
|
+
const blendMap = blends !== undefined ? new Map<string, BlendSnap>(blends as readonly (readonly [string, BlendSnap])[]) : undefined;
|
|
75
190
|
for (const o of iface.fragmentOutputs) {
|
|
76
191
|
const fmt = sig.colors.tryFind(o.name);
|
|
77
192
|
if (fmt === undefined) {
|
|
78
193
|
throw new Error(`prepareRenderObject: fragment output "${o.name}" has no matching signature attachment`);
|
|
79
194
|
}
|
|
80
195
|
const target: GPUColorTargetState = { format: fmt };
|
|
81
|
-
const blend =
|
|
196
|
+
const blend = blendMap?.get(o.name);
|
|
82
197
|
if (blend !== undefined) {
|
|
83
198
|
target.blend = {
|
|
84
199
|
color: { operation: blend.color.operation, srcFactor: blend.color.srcFactor, dstFactor: blend.color.dstFactor },
|
|
@@ -91,27 +206,27 @@ function colorTargetsFor(
|
|
|
91
206
|
return out;
|
|
92
207
|
}
|
|
93
208
|
|
|
94
|
-
function
|
|
209
|
+
function depthStencilStateForSnap(
|
|
95
210
|
sig: FramebufferSignature,
|
|
96
|
-
|
|
211
|
+
snap: PipelineSnap,
|
|
97
212
|
): GPUDepthStencilState | undefined {
|
|
98
213
|
if (sig.depthStencil === undefined) return undefined;
|
|
99
|
-
if (
|
|
214
|
+
if (snap.depthCompare === undefined && snap.depthWrite === undefined && snap.stencil === undefined) return undefined;
|
|
100
215
|
const out: GPUDepthStencilState = {
|
|
101
216
|
format: sig.depthStencil.format,
|
|
102
|
-
depthWriteEnabled:
|
|
103
|
-
depthCompare:
|
|
217
|
+
depthWriteEnabled: snap.depthWrite ?? false,
|
|
218
|
+
depthCompare: snap.depthCompare ?? "always",
|
|
104
219
|
};
|
|
105
|
-
if (
|
|
106
|
-
out.stencilFront =
|
|
107
|
-
out.stencilBack =
|
|
108
|
-
out.stencilReadMask =
|
|
109
|
-
out.stencilWriteMask =
|
|
220
|
+
if (snap.stencil !== undefined && snap.stencil.enabled) {
|
|
221
|
+
out.stencilFront = snap.stencil.front;
|
|
222
|
+
out.stencilBack = snap.stencil.back;
|
|
223
|
+
out.stencilReadMask = snap.stencil.readMask;
|
|
224
|
+
out.stencilWriteMask = snap.stencil.writeMask;
|
|
110
225
|
}
|
|
111
|
-
if (
|
|
112
|
-
out.depthBias =
|
|
113
|
-
out.depthBiasSlopeScale =
|
|
114
|
-
out.depthBiasClamp =
|
|
226
|
+
if (snap.depthBias !== undefined) {
|
|
227
|
+
out.depthBias = snap.depthBias.constant;
|
|
228
|
+
out.depthBiasSlopeScale = snap.depthBias.slopeScale;
|
|
229
|
+
out.depthBiasClamp = snap.depthBias.clamp;
|
|
115
230
|
}
|
|
116
231
|
return out;
|
|
117
232
|
}
|
|
@@ -124,9 +239,6 @@ function sampleTypeFor(type: Type): GPUTextureSampleType {
|
|
|
124
239
|
if (s.kind === "Int") return s.signed ? "sint" : "uint";
|
|
125
240
|
return "float";
|
|
126
241
|
}
|
|
127
|
-
// Storage textures don't have a sampleType in the binding-layout
|
|
128
|
-
// sense; this code path is only for sampled textures. If we see
|
|
129
|
-
// anything else default to filterable float.
|
|
130
242
|
return "float";
|
|
131
243
|
}
|
|
132
244
|
|
|
@@ -168,8 +280,7 @@ function buildGroups(device: GPUDevice, descs: readonly EntryDesc[][]): GroupDes
|
|
|
168
280
|
return out;
|
|
169
281
|
}
|
|
170
282
|
|
|
171
|
-
// Per-handle identity counter for the bind-group cache key.
|
|
172
|
-
// handles have no built-in id; we attach one via a WeakMap.
|
|
283
|
+
// Per-handle identity counter for the bind-group cache key.
|
|
173
284
|
const handleIds = new WeakMap<object, number>();
|
|
174
285
|
let nextHandleId = 1;
|
|
175
286
|
function handleId(h: object): number {
|
|
@@ -182,14 +293,75 @@ function handleId(h: object): number {
|
|
|
182
293
|
// PreparedRenderObject
|
|
183
294
|
// ---------------------------------------------------------------------------
|
|
184
295
|
|
|
296
|
+
interface PipelineBuildContext {
|
|
297
|
+
readonly device: GPUDevice;
|
|
298
|
+
readonly iface: ProgramInterface;
|
|
299
|
+
readonly signature: FramebufferSignature;
|
|
300
|
+
readonly vertexLayouts: readonly GPUVertexBufferLayout[];
|
|
301
|
+
readonly bindGroupLayouts: readonly GPUBindGroupLayout[];
|
|
302
|
+
readonly vsSource: string;
|
|
303
|
+
readonly fsSource: string;
|
|
304
|
+
readonly vsEntry: string;
|
|
305
|
+
readonly fsEntry: string;
|
|
306
|
+
readonly vsSourceMap: import("@aardworx/wombat.shader/ir").SourceMap | null | undefined;
|
|
307
|
+
readonly fsSourceMap: import("@aardworx/wombat.shader/ir").SourceMap | null | undefined;
|
|
308
|
+
readonly label?: string | undefined;
|
|
309
|
+
readonly effectId?: string | undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildPipelineForSnap(
|
|
313
|
+
ctx: PipelineBuildContext,
|
|
314
|
+
snap: PipelineSnap,
|
|
315
|
+
): GPURenderPipeline {
|
|
316
|
+
const colorTargets = colorTargetsForSnap(ctx.iface, ctx.signature, snap.blends);
|
|
317
|
+
const ds = depthStencilStateForSnap(ctx.signature, snap);
|
|
318
|
+
const multisample: GPUMultisampleState | undefined =
|
|
319
|
+
ctx.signature.sampleCount > 1 || snap.alphaToCoverage
|
|
320
|
+
? {
|
|
321
|
+
count: ctx.signature.sampleCount,
|
|
322
|
+
...(snap.alphaToCoverage ? { alphaToCoverageEnabled: true } : {}),
|
|
323
|
+
}
|
|
324
|
+
: undefined;
|
|
325
|
+
const desc: CompileRenderPipelineDescription = {
|
|
326
|
+
...(ctx.label !== undefined ? { label: ctx.label } : {}),
|
|
327
|
+
...(ctx.effectId !== undefined ? { effectId: ctx.effectId } : {}),
|
|
328
|
+
vertexShaderSource: ctx.vsSource,
|
|
329
|
+
fragmentShaderSource: ctx.fsSource,
|
|
330
|
+
...(ctx.vsSourceMap ? { vertexSourceMap: ctx.vsSourceMap } : {}),
|
|
331
|
+
...(ctx.fsSourceMap ? { fragmentSourceMap: ctx.fsSourceMap } : {}),
|
|
332
|
+
vertexEntryPoint: ctx.vsEntry,
|
|
333
|
+
fragmentEntryPoint: ctx.fsEntry,
|
|
334
|
+
vertexBufferLayouts: ctx.vertexLayouts,
|
|
335
|
+
bindGroupLayouts: ctx.bindGroupLayouts,
|
|
336
|
+
colorTargets,
|
|
337
|
+
primitive: {
|
|
338
|
+
topology: snap.topology,
|
|
339
|
+
...(snap.cullMode !== "none" ? { cullMode: snap.cullMode } : {}),
|
|
340
|
+
frontFace: snap.frontFace,
|
|
341
|
+
...(snap.depthClamp === true ? { unclippedDepth: true } : {}),
|
|
342
|
+
},
|
|
343
|
+
...(ds !== undefined ? { depthStencil: ds } : {}),
|
|
344
|
+
...(multisample !== undefined ? { multisample } : {}),
|
|
345
|
+
};
|
|
346
|
+
return compileRenderPipeline(ctx.device, desc);
|
|
347
|
+
}
|
|
348
|
+
|
|
185
349
|
export class PreparedRenderObject {
|
|
186
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Most recently resolved pipeline. Set by `update(token)`. Reading
|
|
352
|
+
* this before `update` was ever called yields a sentinel object
|
|
353
|
+
* suitable only for identity comparison; the actual pipeline is
|
|
354
|
+
* resolved on the first `update`/`record` call.
|
|
355
|
+
*/
|
|
356
|
+
pipeline: GPURenderPipeline;
|
|
187
357
|
readonly groups: readonly GroupDesc[];
|
|
188
358
|
|
|
189
|
-
// Per-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
359
|
+
// Per-PreparedRO pipeline cache keyed by snapshot string.
|
|
360
|
+
private readonly _pipelineCache = new Map<string, GPURenderPipeline>();
|
|
361
|
+
private _pipelineCtx: PipelineBuildContext;
|
|
362
|
+
private _pipelineState: import("../core/index.js").PipelineState;
|
|
363
|
+
|
|
364
|
+
// Per-group bind group cache.
|
|
193
365
|
private readonly _bindGroupCache: { key: string | null; group: GPUBindGroup | null }[];
|
|
194
366
|
|
|
195
367
|
constructor(
|
|
@@ -201,13 +373,21 @@ export class PreparedRenderObject {
|
|
|
201
373
|
private readonly indices: aval<import("../core/index.js").BufferView> | undefined,
|
|
202
374
|
groups: readonly GroupDesc[],
|
|
203
375
|
private readonly drawCall: aval<import("../core/index.js").DrawCall>,
|
|
204
|
-
|
|
376
|
+
pipelineCtx: PipelineBuildContext,
|
|
377
|
+
pipelineState: import("../core/index.js").PipelineState,
|
|
378
|
+
private readonly validity: aval<boolean>,
|
|
205
379
|
) {
|
|
206
|
-
this.pipeline = pipeline;
|
|
207
380
|
this.groups = groups;
|
|
381
|
+
this._pipelineCtx = pipelineCtx;
|
|
382
|
+
this._pipelineState = pipelineState;
|
|
208
383
|
this._bindGroupCache = groups.map(() => ({ key: null, group: null }));
|
|
384
|
+
// Sentinel pipeline — replaced on the first `update(token)`.
|
|
385
|
+
this.pipeline = SENTINEL_PIPELINE;
|
|
209
386
|
}
|
|
210
387
|
|
|
388
|
+
/** Internal — exposed for tests. The PipelineState the RO is reading. */
|
|
389
|
+
get pipelineState(): import("../core/index.js").PipelineState { return this._pipelineState; }
|
|
390
|
+
|
|
211
391
|
acquire(): void {
|
|
212
392
|
for (const r of this.vertexBuffers.values()) r.acquire();
|
|
213
393
|
if (this.indexBuffer !== undefined) this.indexBuffer.acquire();
|
|
@@ -220,15 +400,59 @@ export class PreparedRenderObject {
|
|
|
220
400
|
for (const g of this.groups) for (const e of g.entries) e.resource.release();
|
|
221
401
|
}
|
|
222
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Resolve the pipeline for the current values of the
|
|
405
|
+
* pipeline-influencing avals. Updates `this.pipeline` so that the
|
|
406
|
+
* surrounding sort / record path can read it without forcing.
|
|
407
|
+
*/
|
|
408
|
+
update(token: AdaptiveToken): void {
|
|
409
|
+
// Pull validity into the upstream walker's dependency set so a
|
|
410
|
+
// key-set change on the reactive vertex-attribute aval propagates
|
|
411
|
+
// even when the shape happens not to influence the pipeline cache.
|
|
412
|
+
this.validity.getValue(token);
|
|
413
|
+
const snap = snapshotPipeline(this._pipelineState, token);
|
|
414
|
+
const key = pipelineKeyOf(snap);
|
|
415
|
+
let pipeline = this._pipelineCache.get(key);
|
|
416
|
+
if (pipeline === undefined) {
|
|
417
|
+
pipeline = buildPipelineForSnap(this._pipelineCtx, snap);
|
|
418
|
+
this._pipelineCache.set(key, pipeline);
|
|
419
|
+
}
|
|
420
|
+
this.pipeline = pipeline;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private _warnedMissing = false;
|
|
424
|
+
|
|
223
425
|
record(pass: GPURenderPassEncoder, token: AdaptiveToken): void {
|
|
426
|
+
// Validity gate: when the reactive attribute set fails to cover
|
|
427
|
+
// the shader's required-input names, warn-once and skip the draw.
|
|
428
|
+
// Reading validity here keeps the dependency on the outer attr
|
|
429
|
+
// avals chained into the consumer's tracking.
|
|
430
|
+
if (!this.validity.getValue(token)) {
|
|
431
|
+
if (!this._warnedMissing) {
|
|
432
|
+
console.warn("[wombat.rendering] PreparedRenderObject.record: vertex-attribute set is missing one or more shader-required names; skipping draw.");
|
|
433
|
+
this._warnedMissing = true;
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// Resolve current pipeline. If `update` was already called by the
|
|
438
|
+
// walker this is a no-op cache hit; otherwise we still pick the
|
|
439
|
+
// right pipeline for the token-evaluated values.
|
|
440
|
+
this.update(token);
|
|
224
441
|
pass.setPipeline(this.pipeline);
|
|
225
442
|
|
|
443
|
+
// Per-frame state — does NOT influence the pipeline cache key.
|
|
444
|
+
const ps = this._pipelineState;
|
|
445
|
+
if (ps.blendConstant !== undefined) {
|
|
446
|
+
const c = ps.blendConstant.getValue(token);
|
|
447
|
+
pass.setBlendConstant(c);
|
|
448
|
+
}
|
|
449
|
+
if (ps.stencil !== undefined) {
|
|
450
|
+
const ref = ps.stencil.reference.getValue(token);
|
|
451
|
+
pass.setStencilReference(ref);
|
|
452
|
+
}
|
|
453
|
+
|
|
226
454
|
for (let gi = 0; gi < this.groups.length; gi++) {
|
|
227
455
|
const g = this.groups[gi]!;
|
|
228
|
-
// Build a cache key from the resolved GPU handle identities;
|
|
229
|
-
// resources may be re-allocated by their AdaptiveResource on
|
|
230
|
-
// capacity growth or shape change, so we need to key on the
|
|
231
|
-
// CURRENT handles, not on the AdaptiveResource references.
|
|
232
456
|
let cacheKey = "";
|
|
233
457
|
const resolved: { binding: number; resource: GPUBindingResource }[] = [];
|
|
234
458
|
for (const e of g.entries) {
|
|
@@ -243,10 +467,6 @@ export class PreparedRenderObject {
|
|
|
243
467
|
case "tex": {
|
|
244
468
|
const tex = e.resource.getValue(token);
|
|
245
469
|
cacheKey += `${e.binding}:t${handleId(tex)};`;
|
|
246
|
-
// We create a new view per record because views are cheap
|
|
247
|
-
// and texture identity already keys the cache. (A
|
|
248
|
-
// real-perf-tuned implementation would also cache the
|
|
249
|
-
// view per texture.)
|
|
250
470
|
resolved.push({ binding: e.binding, resource: tex.createView() });
|
|
251
471
|
break;
|
|
252
472
|
}
|
|
@@ -316,21 +536,47 @@ export function prepareRenderObject(
|
|
|
316
536
|
const iface = effect.interface;
|
|
317
537
|
const vertexBindings = vertexBindingsFor(iface);
|
|
318
538
|
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
539
|
+
// Construction-boundary read of the outer attribute avals. We use
|
|
540
|
+
// these initial maps ONLY to determine per-binding step-mode
|
|
541
|
+
// (vertex vs instance) — that is structural and baked into the
|
|
542
|
+
// GPU pipeline's vertex-buffer layout, so it cannot be reactive
|
|
543
|
+
// without re-preparation. The buffers themselves AND the rest of
|
|
544
|
+
// the map (its key set) remain fully reactive on the live path.
|
|
545
|
+
const vertexAttributesAval = obj.vertexAttributes;
|
|
546
|
+
const instanceAttributesAval = obj.instanceAttributes
|
|
547
|
+
?? (AVal.constant(emptyAttrMap()) as aval<HashMap<string, aval<BufferView>>>);
|
|
548
|
+
const initialVertex = vertexAttributesAval.force();
|
|
549
|
+
const initialInstance = obj.instanceAttributes !== undefined
|
|
550
|
+
? obj.instanceAttributes.force()
|
|
551
|
+
: emptyAttrMap();
|
|
552
|
+
|
|
322
553
|
const vertexBuffers = new Map<string, AdaptiveResource<GPUBuffer>>();
|
|
323
554
|
const vertexLayouts: GPUVertexBufferLayout[] = [];
|
|
324
555
|
for (const vb of vertexBindings) {
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
556
|
+
const inVertex = initialVertex.tryFind(vb.name) !== undefined;
|
|
557
|
+
const inInstance = initialInstance.tryFind(vb.name) !== undefined;
|
|
558
|
+
if (!inVertex && !inInstance) {
|
|
559
|
+
throw new Error(`prepareRenderObject: missing vertex attribute "${vb.name}"`);
|
|
560
|
+
}
|
|
561
|
+
const stepMode: GPUVertexStepMode = inInstance && !inVertex ? "instance" : "vertex";
|
|
330
562
|
const stride = vb.byteSize !== undefined && vb.byteSize > 0
|
|
331
563
|
? vb.byteSize
|
|
332
564
|
: vertexFormatStride(vb.format);
|
|
333
|
-
|
|
565
|
+
|
|
566
|
+
// Reactive lookup: chain (outer map aval) → (inner BufferView aval)
|
|
567
|
+
// → buffer. AVal.bind preserves the dependency on the outer map
|
|
568
|
+
// so a key-set change re-runs the lookup without losing tracking.
|
|
569
|
+
// Missing-name on a frame is signalled via a sentinel that record()
|
|
570
|
+
// detects and skips the draw with a warn-once.
|
|
571
|
+
const bufAval = AVal.bind(vertexAttributesAval, vMap => {
|
|
572
|
+
const found = vMap.tryFind(vb.name);
|
|
573
|
+
if (found !== undefined) return found.map(view => view.buffer);
|
|
574
|
+
return AVal.bind(instanceAttributesAval, iMap => {
|
|
575
|
+
const f2 = iMap.tryFind(vb.name);
|
|
576
|
+
if (f2 !== undefined) return f2.map(view => view.buffer);
|
|
577
|
+
return AVal.constant(MISSING_BUFFER);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
334
580
|
const res = prepareAdaptiveBuffer(device, bufAval, {
|
|
335
581
|
usage: BufferUsage.VERTEX,
|
|
336
582
|
...(opts.label !== undefined ? { label: `${opts.label}.${vb.name}` } : {}),
|
|
@@ -343,6 +589,19 @@ export function prepareRenderObject(
|
|
|
343
589
|
};
|
|
344
590
|
}
|
|
345
591
|
|
|
592
|
+
// Validity aval: depends on the outer attribute avals so any key-set
|
|
593
|
+
// change re-runs the check. Used by record() to skip draws when the
|
|
594
|
+
// shader's required-input set isn't satisfied (warn-once).
|
|
595
|
+
const requiredNames = vertexBindings.map(b => b.name);
|
|
596
|
+
const validityAval: aval<boolean> = AVal.zip(vertexAttributesAval, instanceAttributesAval).map(
|
|
597
|
+
(vMap, iMap) => {
|
|
598
|
+
for (const n of requiredNames) {
|
|
599
|
+
if (vMap.tryFind(n) === undefined && iMap.tryFind(n) === undefined) return false;
|
|
600
|
+
}
|
|
601
|
+
return true;
|
|
602
|
+
},
|
|
603
|
+
);
|
|
604
|
+
|
|
346
605
|
// Index buffer — `indexFormat` from BufferView wins; defaults to uint32.
|
|
347
606
|
let indexBuffer: AdaptiveResource<GPUBuffer> | undefined;
|
|
348
607
|
let indexFormat: GPUIndexFormat | undefined;
|
|
@@ -352,12 +611,14 @@ export function prepareRenderObject(
|
|
|
352
611
|
usage: BufferUsage.INDEX,
|
|
353
612
|
...(opts.label !== undefined ? { label: `${opts.label}.indices` } : {}),
|
|
354
613
|
});
|
|
614
|
+
// Construction-time read to discover the index format. The
|
|
615
|
+
// BufferView aval is expected to settle on a stable format —
|
|
616
|
+
// changing index format would require a fresh PreparedRO.
|
|
355
617
|
const initial = obj.indices.force();
|
|
356
618
|
indexFormat = initial.indexFormat
|
|
357
619
|
?? (initial.format === "uint16" ? "uint16" : "uint32");
|
|
358
620
|
}
|
|
359
621
|
|
|
360
|
-
// Per-group entry collection. Use `slot` (real shape) instead of `binding`.
|
|
361
622
|
const slotOf = (e: { group: number; slot: number }) => e.slot;
|
|
362
623
|
const groupOf = (e: { group: number }) => e.group;
|
|
363
624
|
const maxGroup = Math.max(
|
|
@@ -371,9 +632,6 @@ export function prepareRenderObject(
|
|
|
371
632
|
for (let g = 0; g <= maxGroup; g++) perGroup.push([]);
|
|
372
633
|
|
|
373
634
|
for (const ub of iface.uniformBlocks) {
|
|
374
|
-
// Merge user-supplied uniforms (`obj.uniforms`) with effect-bound
|
|
375
|
-
// avals (`compiledEffect.avalBindings`). User entries win on name
|
|
376
|
-
// conflict — the rendering caller is the source of truth.
|
|
377
635
|
const merged = mergeUniformInputs(obj.uniforms, effect.avalBindings, ub);
|
|
378
636
|
const block = ubAsBlock(ub);
|
|
379
637
|
const res = prepareUniformBuffer(device, block, merged, {
|
|
@@ -412,42 +670,30 @@ export function prepareRenderObject(
|
|
|
412
670
|
|
|
413
671
|
const groups = buildGroups(device, perGroup);
|
|
414
672
|
|
|
415
|
-
// Pipeline
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
673
|
+
// Pipeline — set up the build context and produce the initial
|
|
674
|
+
// GPURenderPipeline. We snapshot via AdaptiveToken.top here; that's
|
|
675
|
+
// a one-time construction-boundary read, NOT inside an adaptive
|
|
676
|
+
// computation. Subsequent recompiles happen inside `update(token)`.
|
|
677
|
+
const vsStage = effect.stages.find(st => st.stage === "vertex");
|
|
678
|
+
const fsStage = effect.stages.find(st => st.stage === "fragment");
|
|
419
679
|
if (vsStage === undefined) throw new Error("prepareRenderObject: CompiledEffect has no vertex stage");
|
|
420
680
|
if (fsStage === undefined) throw new Error("prepareRenderObject: CompiledEffect has no fragment stage");
|
|
421
681
|
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
682
|
+
const pipelineCtx: PipelineBuildContext = {
|
|
683
|
+
device,
|
|
684
|
+
iface,
|
|
685
|
+
signature,
|
|
686
|
+
vertexLayouts,
|
|
687
|
+
bindGroupLayouts: groups.map(g => g.layout),
|
|
688
|
+
vsSource: vsStage.source,
|
|
689
|
+
fsSource: fsStage.source,
|
|
690
|
+
vsEntry: vsStage.entryName,
|
|
691
|
+
fsEntry: fsStage.entryName,
|
|
692
|
+
vsSourceMap: vsStage.sourceMap,
|
|
693
|
+
fsSourceMap: fsStage.sourceMap,
|
|
431
694
|
...(opts.label !== undefined ? { label: opts.label } : {}),
|
|
432
695
|
...(opts.effectId !== undefined ? { effectId: opts.effectId } : {}),
|
|
433
|
-
vertexShaderSource: vsStage.source,
|
|
434
|
-
fragmentShaderSource: fsStage.source,
|
|
435
|
-
...(vsStage.sourceMap ? { vertexSourceMap: vsStage.sourceMap } : {}),
|
|
436
|
-
...(fsStage.sourceMap ? { fragmentSourceMap: fsStage.sourceMap } : {}),
|
|
437
|
-
vertexEntryPoint: vsStage.entryName,
|
|
438
|
-
fragmentEntryPoint: fsStage.entryName,
|
|
439
|
-
vertexBufferLayouts: vertexLayouts,
|
|
440
|
-
bindGroupLayouts: groups.map(g => g.layout),
|
|
441
|
-
colorTargets,
|
|
442
|
-
primitive: {
|
|
443
|
-
topology: obj.pipelineState.rasterizer.topology,
|
|
444
|
-
...(obj.pipelineState.rasterizer.cullMode !== "none" ? { cullMode: obj.pipelineState.rasterizer.cullMode } : {}),
|
|
445
|
-
frontFace: obj.pipelineState.rasterizer.frontFace,
|
|
446
|
-
},
|
|
447
|
-
...(ds !== undefined ? { depthStencil: ds } : {}),
|
|
448
|
-
...(multisample !== undefined ? { multisample } : {}),
|
|
449
696
|
};
|
|
450
|
-
const pipeline = compileRenderPipeline(device, pipelineDesc);
|
|
451
697
|
|
|
452
698
|
return new PreparedRenderObject(
|
|
453
699
|
device,
|
|
@@ -455,10 +701,38 @@ export function prepareRenderObject(
|
|
|
455
701
|
indexBuffer, indexFormat, obj.indices,
|
|
456
702
|
groups,
|
|
457
703
|
obj.drawCall,
|
|
458
|
-
|
|
704
|
+
pipelineCtx,
|
|
705
|
+
obj.pipelineState,
|
|
706
|
+
validityAval,
|
|
459
707
|
);
|
|
460
708
|
}
|
|
461
709
|
|
|
710
|
+
/**
|
|
711
|
+
* Sentinel host-buffer returned by the per-attribute lookup aval when
|
|
712
|
+
* the named attribute is absent from a frame's outer attribute map.
|
|
713
|
+
* The PreparedRO's validity aval flips false in the same situation;
|
|
714
|
+
* record() observes the validity flip and skips the draw before any
|
|
715
|
+
* sentinel buffer is ever bound.
|
|
716
|
+
*/
|
|
717
|
+
const MISSING_BUFFER: import("../core/index.js").IBuffer = {
|
|
718
|
+
kind: "host",
|
|
719
|
+
data: new Uint8Array(4),
|
|
720
|
+
sizeBytes: 4,
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
function emptyAttrMap(): HashMap<string, aval<BufferView>> {
|
|
724
|
+
return HashMap.empty<string, aval<BufferView>>();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Sentinel pipeline placeholder. PreparedRenderObject's `pipeline`
|
|
729
|
+
* field is non-null for ergonomic typing; before the first
|
|
730
|
+
* `update(token)` it points at this sentinel which is only used as
|
|
731
|
+
* a sort-rank identity (never bound to a render pass — `record`
|
|
732
|
+
* always calls `update` first).
|
|
733
|
+
*/
|
|
734
|
+
const SENTINEL_PIPELINE = { __sentinel: "PreparedRenderObject.pipeline" } as unknown as GPURenderPipeline;
|
|
735
|
+
|
|
462
736
|
// ---------------------------------------------------------------------------
|
|
463
737
|
// Uniform input merging — `obj.uniforms` wins; `avalBindings` is fallback.
|
|
464
738
|
// ---------------------------------------------------------------------------
|
|
@@ -475,8 +749,6 @@ function mergeUniformInputs(
|
|
|
475
749
|
const getter = bindings[f.name];
|
|
476
750
|
if (getter === undefined) continue;
|
|
477
751
|
const v = getter();
|
|
478
|
-
// wombat.shader's avalBindings store either an `aval<T>` or a
|
|
479
|
-
// raw value; wrap the raw case as a constant aval.
|
|
480
752
|
if (isAval(v)) merged = merged.add(f.name, v as aval<unknown>);
|
|
481
753
|
else merged = merged.add(f.name, cval(v));
|
|
482
754
|
}
|
|
@@ -487,12 +759,6 @@ function isAval(v: unknown): boolean {
|
|
|
487
759
|
return typeof v === "object" && v !== null && typeof (v as { getValue?: unknown }).getValue === "function";
|
|
488
760
|
}
|
|
489
761
|
|
|
490
|
-
/**
|
|
491
|
-
* Adapter — wombat.shader's `UniformBlockInfo` and our internal
|
|
492
|
-
* UBO packer expect the same field shape (`offset`, `size`, etc.),
|
|
493
|
-
* so this is just a structural pass-through. Keeps the call sites
|
|
494
|
-
* type-clean.
|
|
495
|
-
*/
|
|
496
762
|
function ubAsBlock(ub: import("../core/index.js").UniformBlockInfo): import("../core/index.js").UniformBlockInfo {
|
|
497
763
|
return ub;
|
|
498
764
|
}
|
package/src/runtime/scenePass.ts
CHANGED
|
@@ -79,7 +79,12 @@ class LeafWalker extends NodeWalker {
|
|
|
79
79
|
this.prepared.acquire();
|
|
80
80
|
ctx.stats.prepareCount++;
|
|
81
81
|
}
|
|
82
|
-
update(): void {
|
|
82
|
+
update(token: AdaptiveToken): void {
|
|
83
|
+
// Resolve the current pipeline so the upstream sort comparator
|
|
84
|
+
// sees a stable identity for this frame's pipeline-influencing
|
|
85
|
+
// aval values.
|
|
86
|
+
this.prepared.update(token);
|
|
87
|
+
}
|
|
83
88
|
emit(out: PreparedRenderObject[]): void { out.push(this.prepared); }
|
|
84
89
|
dispose(): void { this.prepared.release(); }
|
|
85
90
|
}
|