@aardworx/wombat.rendering 0.1.1 → 0.2.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/resources/preparedRenderObject.d.ts +35 -3
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +195 -84
- 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/resources/preparedRenderObject.ts +271 -85
- 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,
|
|
@@ -56,8 +63,6 @@ interface VertexBindingInfo {
|
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
function vertexBindingsFor(iface: ProgramInterface): VertexBindingInfo[] {
|
|
59
|
-
// `iface.attributes` is already filtered to non-builtin vertex inputs
|
|
60
|
-
// by wombat.shader's interface builder.
|
|
61
66
|
return iface.attributes.map(a => ({
|
|
62
67
|
name: a.name,
|
|
63
68
|
slot: a.location,
|
|
@@ -66,19 +71,127 @@ function vertexBindingsFor(iface: ProgramInterface): VertexBindingInfo[] {
|
|
|
66
71
|
}));
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Plain pipeline-state snapshot — what we evaluate avals into per-frame.
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
interface BlendComponentSnap {
|
|
79
|
+
readonly operation: GPUBlendOperation;
|
|
80
|
+
readonly srcFactor: GPUBlendFactor;
|
|
81
|
+
readonly dstFactor: GPUBlendFactor;
|
|
82
|
+
}
|
|
83
|
+
interface BlendSnap {
|
|
84
|
+
readonly color: BlendComponentSnap;
|
|
85
|
+
readonly alpha: BlendComponentSnap;
|
|
86
|
+
readonly writeMask: number;
|
|
87
|
+
}
|
|
88
|
+
interface StencilFaceSnap {
|
|
89
|
+
readonly compare: GPUCompareFunction;
|
|
90
|
+
readonly failOp: GPUStencilOperation;
|
|
91
|
+
readonly depthFailOp: GPUStencilOperation;
|
|
92
|
+
readonly passOp: GPUStencilOperation;
|
|
93
|
+
}
|
|
94
|
+
interface StencilSnap {
|
|
95
|
+
readonly enabled: boolean;
|
|
96
|
+
readonly readMask: number;
|
|
97
|
+
readonly writeMask: number;
|
|
98
|
+
readonly front: StencilFaceSnap;
|
|
99
|
+
readonly back: StencilFaceSnap;
|
|
100
|
+
}
|
|
101
|
+
interface PipelineSnap {
|
|
102
|
+
readonly topology: GPUPrimitiveTopology;
|
|
103
|
+
readonly cullMode: import("../core/index.js").CullMode;
|
|
104
|
+
readonly frontFace: import("../core/index.js").FrontFace;
|
|
105
|
+
readonly depthBias: { readonly constant: number; readonly slopeScale: number; readonly clamp: number } | undefined;
|
|
106
|
+
readonly depthWrite: boolean | undefined;
|
|
107
|
+
readonly depthCompare: GPUCompareFunction | undefined;
|
|
108
|
+
readonly depthClamp: boolean | undefined;
|
|
109
|
+
readonly stencil: StencilSnap | undefined;
|
|
110
|
+
readonly blends: ReadonlyArray<readonly [string, BlendSnap]> | undefined;
|
|
111
|
+
readonly alphaToCoverage: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function snapshotPipeline(
|
|
115
|
+
ps: import("../core/index.js").PipelineState,
|
|
116
|
+
token: AdaptiveToken,
|
|
117
|
+
): PipelineSnap {
|
|
118
|
+
const r = ps.rasterizer;
|
|
119
|
+
const depthBias = r.depthBias !== undefined ? r.depthBias.getValue(token) : undefined;
|
|
120
|
+
let stencil: StencilSnap | undefined;
|
|
121
|
+
if (ps.stencil !== undefined) {
|
|
122
|
+
const s = ps.stencil;
|
|
123
|
+
stencil = {
|
|
124
|
+
enabled: s.enabled.getValue(token),
|
|
125
|
+
readMask: s.readMask.getValue(token),
|
|
126
|
+
writeMask: s.writeMask.getValue(token),
|
|
127
|
+
front: {
|
|
128
|
+
compare: s.front.compare.getValue(token),
|
|
129
|
+
failOp: s.front.failOp.getValue(token),
|
|
130
|
+
depthFailOp: s.front.depthFailOp.getValue(token),
|
|
131
|
+
passOp: s.front.passOp.getValue(token),
|
|
132
|
+
},
|
|
133
|
+
back: {
|
|
134
|
+
compare: s.back.compare.getValue(token),
|
|
135
|
+
failOp: s.back.failOp.getValue(token),
|
|
136
|
+
depthFailOp: s.back.depthFailOp.getValue(token),
|
|
137
|
+
passOp: s.back.passOp.getValue(token),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
let blends: ReadonlyArray<readonly [string, BlendSnap]> | undefined;
|
|
142
|
+
if (ps.blends !== undefined) {
|
|
143
|
+
const m = ps.blends.getValue(token);
|
|
144
|
+
const arr: [string, BlendSnap][] = [];
|
|
145
|
+
for (const [k, b] of m) {
|
|
146
|
+
arr.push([k, {
|
|
147
|
+
color: {
|
|
148
|
+
operation: b.color.operation.getValue(token),
|
|
149
|
+
srcFactor: b.color.srcFactor.getValue(token),
|
|
150
|
+
dstFactor: b.color.dstFactor.getValue(token),
|
|
151
|
+
},
|
|
152
|
+
alpha: {
|
|
153
|
+
operation: b.alpha.operation.getValue(token),
|
|
154
|
+
srcFactor: b.alpha.srcFactor.getValue(token),
|
|
155
|
+
dstFactor: b.alpha.dstFactor.getValue(token),
|
|
156
|
+
},
|
|
157
|
+
writeMask: b.writeMask.getValue(token),
|
|
158
|
+
}]);
|
|
159
|
+
}
|
|
160
|
+
arr.sort((x, y) => (x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0));
|
|
161
|
+
blends = arr;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
topology: r.topology.getValue(token),
|
|
165
|
+
cullMode: r.cullMode.getValue(token),
|
|
166
|
+
frontFace: r.frontFace.getValue(token),
|
|
167
|
+
depthBias,
|
|
168
|
+
depthWrite: ps.depth?.write.getValue(token),
|
|
169
|
+
depthCompare: ps.depth?.compare.getValue(token),
|
|
170
|
+
depthClamp: ps.depth?.clamp?.getValue(token),
|
|
171
|
+
stencil,
|
|
172
|
+
blends,
|
|
173
|
+
alphaToCoverage: ps.alphaToCoverage?.getValue(token) ?? false,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function pipelineKeyOf(snap: PipelineSnap): string {
|
|
178
|
+
return JSON.stringify(snap);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function colorTargetsForSnap(
|
|
70
182
|
iface: ProgramInterface,
|
|
71
183
|
sig: FramebufferSignature,
|
|
72
|
-
blends:
|
|
184
|
+
blends: ReadonlyArray<readonly [string, BlendSnap]> | undefined,
|
|
73
185
|
): GPUColorTargetState[] {
|
|
74
186
|
const out: GPUColorTargetState[] = [];
|
|
187
|
+
const blendMap = blends !== undefined ? new Map<string, BlendSnap>(blends as readonly (readonly [string, BlendSnap])[]) : undefined;
|
|
75
188
|
for (const o of iface.fragmentOutputs) {
|
|
76
189
|
const fmt = sig.colors.tryFind(o.name);
|
|
77
190
|
if (fmt === undefined) {
|
|
78
191
|
throw new Error(`prepareRenderObject: fragment output "${o.name}" has no matching signature attachment`);
|
|
79
192
|
}
|
|
80
193
|
const target: GPUColorTargetState = { format: fmt };
|
|
81
|
-
const blend =
|
|
194
|
+
const blend = blendMap?.get(o.name);
|
|
82
195
|
if (blend !== undefined) {
|
|
83
196
|
target.blend = {
|
|
84
197
|
color: { operation: blend.color.operation, srcFactor: blend.color.srcFactor, dstFactor: blend.color.dstFactor },
|
|
@@ -91,27 +204,27 @@ function colorTargetsFor(
|
|
|
91
204
|
return out;
|
|
92
205
|
}
|
|
93
206
|
|
|
94
|
-
function
|
|
207
|
+
function depthStencilStateForSnap(
|
|
95
208
|
sig: FramebufferSignature,
|
|
96
|
-
|
|
209
|
+
snap: PipelineSnap,
|
|
97
210
|
): GPUDepthStencilState | undefined {
|
|
98
211
|
if (sig.depthStencil === undefined) return undefined;
|
|
99
|
-
if (
|
|
212
|
+
if (snap.depthCompare === undefined && snap.depthWrite === undefined && snap.stencil === undefined) return undefined;
|
|
100
213
|
const out: GPUDepthStencilState = {
|
|
101
214
|
format: sig.depthStencil.format,
|
|
102
|
-
depthWriteEnabled:
|
|
103
|
-
depthCompare:
|
|
215
|
+
depthWriteEnabled: snap.depthWrite ?? false,
|
|
216
|
+
depthCompare: snap.depthCompare ?? "always",
|
|
104
217
|
};
|
|
105
|
-
if (
|
|
106
|
-
out.stencilFront =
|
|
107
|
-
out.stencilBack =
|
|
108
|
-
out.stencilReadMask =
|
|
109
|
-
out.stencilWriteMask =
|
|
218
|
+
if (snap.stencil !== undefined && snap.stencil.enabled) {
|
|
219
|
+
out.stencilFront = snap.stencil.front;
|
|
220
|
+
out.stencilBack = snap.stencil.back;
|
|
221
|
+
out.stencilReadMask = snap.stencil.readMask;
|
|
222
|
+
out.stencilWriteMask = snap.stencil.writeMask;
|
|
110
223
|
}
|
|
111
|
-
if (
|
|
112
|
-
out.depthBias =
|
|
113
|
-
out.depthBiasSlopeScale =
|
|
114
|
-
out.depthBiasClamp =
|
|
224
|
+
if (snap.depthBias !== undefined) {
|
|
225
|
+
out.depthBias = snap.depthBias.constant;
|
|
226
|
+
out.depthBiasSlopeScale = snap.depthBias.slopeScale;
|
|
227
|
+
out.depthBiasClamp = snap.depthBias.clamp;
|
|
115
228
|
}
|
|
116
229
|
return out;
|
|
117
230
|
}
|
|
@@ -124,9 +237,6 @@ function sampleTypeFor(type: Type): GPUTextureSampleType {
|
|
|
124
237
|
if (s.kind === "Int") return s.signed ? "sint" : "uint";
|
|
125
238
|
return "float";
|
|
126
239
|
}
|
|
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
240
|
return "float";
|
|
131
241
|
}
|
|
132
242
|
|
|
@@ -168,8 +278,7 @@ function buildGroups(device: GPUDevice, descs: readonly EntryDesc[][]): GroupDes
|
|
|
168
278
|
return out;
|
|
169
279
|
}
|
|
170
280
|
|
|
171
|
-
// Per-handle identity counter for the bind-group cache key.
|
|
172
|
-
// handles have no built-in id; we attach one via a WeakMap.
|
|
281
|
+
// Per-handle identity counter for the bind-group cache key.
|
|
173
282
|
const handleIds = new WeakMap<object, number>();
|
|
174
283
|
let nextHandleId = 1;
|
|
175
284
|
function handleId(h: object): number {
|
|
@@ -182,14 +291,75 @@ function handleId(h: object): number {
|
|
|
182
291
|
// PreparedRenderObject
|
|
183
292
|
// ---------------------------------------------------------------------------
|
|
184
293
|
|
|
294
|
+
interface PipelineBuildContext {
|
|
295
|
+
readonly device: GPUDevice;
|
|
296
|
+
readonly iface: ProgramInterface;
|
|
297
|
+
readonly signature: FramebufferSignature;
|
|
298
|
+
readonly vertexLayouts: readonly GPUVertexBufferLayout[];
|
|
299
|
+
readonly bindGroupLayouts: readonly GPUBindGroupLayout[];
|
|
300
|
+
readonly vsSource: string;
|
|
301
|
+
readonly fsSource: string;
|
|
302
|
+
readonly vsEntry: string;
|
|
303
|
+
readonly fsEntry: string;
|
|
304
|
+
readonly vsSourceMap: import("@aardworx/wombat.shader/ir").SourceMap | null | undefined;
|
|
305
|
+
readonly fsSourceMap: import("@aardworx/wombat.shader/ir").SourceMap | null | undefined;
|
|
306
|
+
readonly label?: string | undefined;
|
|
307
|
+
readonly effectId?: string | undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function buildPipelineForSnap(
|
|
311
|
+
ctx: PipelineBuildContext,
|
|
312
|
+
snap: PipelineSnap,
|
|
313
|
+
): GPURenderPipeline {
|
|
314
|
+
const colorTargets = colorTargetsForSnap(ctx.iface, ctx.signature, snap.blends);
|
|
315
|
+
const ds = depthStencilStateForSnap(ctx.signature, snap);
|
|
316
|
+
const multisample: GPUMultisampleState | undefined =
|
|
317
|
+
ctx.signature.sampleCount > 1 || snap.alphaToCoverage
|
|
318
|
+
? {
|
|
319
|
+
count: ctx.signature.sampleCount,
|
|
320
|
+
...(snap.alphaToCoverage ? { alphaToCoverageEnabled: true } : {}),
|
|
321
|
+
}
|
|
322
|
+
: undefined;
|
|
323
|
+
const desc: CompileRenderPipelineDescription = {
|
|
324
|
+
...(ctx.label !== undefined ? { label: ctx.label } : {}),
|
|
325
|
+
...(ctx.effectId !== undefined ? { effectId: ctx.effectId } : {}),
|
|
326
|
+
vertexShaderSource: ctx.vsSource,
|
|
327
|
+
fragmentShaderSource: ctx.fsSource,
|
|
328
|
+
...(ctx.vsSourceMap ? { vertexSourceMap: ctx.vsSourceMap } : {}),
|
|
329
|
+
...(ctx.fsSourceMap ? { fragmentSourceMap: ctx.fsSourceMap } : {}),
|
|
330
|
+
vertexEntryPoint: ctx.vsEntry,
|
|
331
|
+
fragmentEntryPoint: ctx.fsEntry,
|
|
332
|
+
vertexBufferLayouts: ctx.vertexLayouts,
|
|
333
|
+
bindGroupLayouts: ctx.bindGroupLayouts,
|
|
334
|
+
colorTargets,
|
|
335
|
+
primitive: {
|
|
336
|
+
topology: snap.topology,
|
|
337
|
+
...(snap.cullMode !== "none" ? { cullMode: snap.cullMode } : {}),
|
|
338
|
+
frontFace: snap.frontFace,
|
|
339
|
+
...(snap.depthClamp === true ? { unclippedDepth: true } : {}),
|
|
340
|
+
},
|
|
341
|
+
...(ds !== undefined ? { depthStencil: ds } : {}),
|
|
342
|
+
...(multisample !== undefined ? { multisample } : {}),
|
|
343
|
+
};
|
|
344
|
+
return compileRenderPipeline(ctx.device, desc);
|
|
345
|
+
}
|
|
346
|
+
|
|
185
347
|
export class PreparedRenderObject {
|
|
186
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Most recently resolved pipeline. Set by `update(token)`. Reading
|
|
350
|
+
* this before `update` was ever called yields a sentinel object
|
|
351
|
+
* suitable only for identity comparison; the actual pipeline is
|
|
352
|
+
* resolved on the first `update`/`record` call.
|
|
353
|
+
*/
|
|
354
|
+
pipeline: GPURenderPipeline;
|
|
187
355
|
readonly groups: readonly GroupDesc[];
|
|
188
356
|
|
|
189
|
-
// Per-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
357
|
+
// Per-PreparedRO pipeline cache keyed by snapshot string.
|
|
358
|
+
private readonly _pipelineCache = new Map<string, GPURenderPipeline>();
|
|
359
|
+
private _pipelineCtx: PipelineBuildContext;
|
|
360
|
+
private _pipelineState: import("../core/index.js").PipelineState;
|
|
361
|
+
|
|
362
|
+
// Per-group bind group cache.
|
|
193
363
|
private readonly _bindGroupCache: { key: string | null; group: GPUBindGroup | null }[];
|
|
194
364
|
|
|
195
365
|
constructor(
|
|
@@ -201,13 +371,20 @@ export class PreparedRenderObject {
|
|
|
201
371
|
private readonly indices: aval<import("../core/index.js").BufferView> | undefined,
|
|
202
372
|
groups: readonly GroupDesc[],
|
|
203
373
|
private readonly drawCall: aval<import("../core/index.js").DrawCall>,
|
|
204
|
-
|
|
374
|
+
pipelineCtx: PipelineBuildContext,
|
|
375
|
+
pipelineState: import("../core/index.js").PipelineState,
|
|
205
376
|
) {
|
|
206
|
-
this.pipeline = pipeline;
|
|
207
377
|
this.groups = groups;
|
|
378
|
+
this._pipelineCtx = pipelineCtx;
|
|
379
|
+
this._pipelineState = pipelineState;
|
|
208
380
|
this._bindGroupCache = groups.map(() => ({ key: null, group: null }));
|
|
381
|
+
// Sentinel pipeline — replaced on the first `update(token)`.
|
|
382
|
+
this.pipeline = SENTINEL_PIPELINE;
|
|
209
383
|
}
|
|
210
384
|
|
|
385
|
+
/** Internal — exposed for tests. The PipelineState the RO is reading. */
|
|
386
|
+
get pipelineState(): import("../core/index.js").PipelineState { return this._pipelineState; }
|
|
387
|
+
|
|
211
388
|
acquire(): void {
|
|
212
389
|
for (const r of this.vertexBuffers.values()) r.acquire();
|
|
213
390
|
if (this.indexBuffer !== undefined) this.indexBuffer.acquire();
|
|
@@ -220,15 +397,42 @@ export class PreparedRenderObject {
|
|
|
220
397
|
for (const g of this.groups) for (const e of g.entries) e.resource.release();
|
|
221
398
|
}
|
|
222
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Resolve the pipeline for the current values of the
|
|
402
|
+
* pipeline-influencing avals. Updates `this.pipeline` so that the
|
|
403
|
+
* surrounding sort / record path can read it without forcing.
|
|
404
|
+
*/
|
|
405
|
+
update(token: AdaptiveToken): void {
|
|
406
|
+
const snap = snapshotPipeline(this._pipelineState, token);
|
|
407
|
+
const key = pipelineKeyOf(snap);
|
|
408
|
+
let pipeline = this._pipelineCache.get(key);
|
|
409
|
+
if (pipeline === undefined) {
|
|
410
|
+
pipeline = buildPipelineForSnap(this._pipelineCtx, snap);
|
|
411
|
+
this._pipelineCache.set(key, pipeline);
|
|
412
|
+
}
|
|
413
|
+
this.pipeline = pipeline;
|
|
414
|
+
}
|
|
415
|
+
|
|
223
416
|
record(pass: GPURenderPassEncoder, token: AdaptiveToken): void {
|
|
417
|
+
// Resolve current pipeline. If `update` was already called by the
|
|
418
|
+
// walker this is a no-op cache hit; otherwise we still pick the
|
|
419
|
+
// right pipeline for the token-evaluated values.
|
|
420
|
+
this.update(token);
|
|
224
421
|
pass.setPipeline(this.pipeline);
|
|
225
422
|
|
|
423
|
+
// Per-frame state — does NOT influence the pipeline cache key.
|
|
424
|
+
const ps = this._pipelineState;
|
|
425
|
+
if (ps.blendConstant !== undefined) {
|
|
426
|
+
const c = ps.blendConstant.getValue(token);
|
|
427
|
+
pass.setBlendConstant(c);
|
|
428
|
+
}
|
|
429
|
+
if (ps.stencil !== undefined) {
|
|
430
|
+
const ref = ps.stencil.reference.getValue(token);
|
|
431
|
+
pass.setStencilReference(ref);
|
|
432
|
+
}
|
|
433
|
+
|
|
226
434
|
for (let gi = 0; gi < this.groups.length; gi++) {
|
|
227
435
|
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
436
|
let cacheKey = "";
|
|
233
437
|
const resolved: { binding: number; resource: GPUBindingResource }[] = [];
|
|
234
438
|
for (const e of g.entries) {
|
|
@@ -243,10 +447,6 @@ export class PreparedRenderObject {
|
|
|
243
447
|
case "tex": {
|
|
244
448
|
const tex = e.resource.getValue(token);
|
|
245
449
|
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
450
|
resolved.push({ binding: e.binding, resource: tex.createView() });
|
|
251
451
|
break;
|
|
252
452
|
}
|
|
@@ -316,9 +516,6 @@ export function prepareRenderObject(
|
|
|
316
516
|
const iface = effect.interface;
|
|
317
517
|
const vertexBindings = vertexBindingsFor(iface);
|
|
318
518
|
|
|
319
|
-
// Vertex + instance buffers — same shape; only stepMode differs.
|
|
320
|
-
// Each shader-required attribute is looked up in obj.vertexAttributes
|
|
321
|
-
// first (per-vertex) then obj.instanceAttributes (per-instance).
|
|
322
519
|
const vertexBuffers = new Map<string, AdaptiveResource<GPUBuffer>>();
|
|
323
520
|
const vertexLayouts: GPUVertexBufferLayout[] = [];
|
|
324
521
|
for (const vb of vertexBindings) {
|
|
@@ -352,12 +549,14 @@ export function prepareRenderObject(
|
|
|
352
549
|
usage: BufferUsage.INDEX,
|
|
353
550
|
...(opts.label !== undefined ? { label: `${opts.label}.indices` } : {}),
|
|
354
551
|
});
|
|
552
|
+
// Construction-time read to discover the index format. The
|
|
553
|
+
// BufferView aval is expected to settle on a stable format —
|
|
554
|
+
// changing index format would require a fresh PreparedRO.
|
|
355
555
|
const initial = obj.indices.force();
|
|
356
556
|
indexFormat = initial.indexFormat
|
|
357
557
|
?? (initial.format === "uint16" ? "uint16" : "uint32");
|
|
358
558
|
}
|
|
359
559
|
|
|
360
|
-
// Per-group entry collection. Use `slot` (real shape) instead of `binding`.
|
|
361
560
|
const slotOf = (e: { group: number; slot: number }) => e.slot;
|
|
362
561
|
const groupOf = (e: { group: number }) => e.group;
|
|
363
562
|
const maxGroup = Math.max(
|
|
@@ -371,9 +570,6 @@ export function prepareRenderObject(
|
|
|
371
570
|
for (let g = 0; g <= maxGroup; g++) perGroup.push([]);
|
|
372
571
|
|
|
373
572
|
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
573
|
const merged = mergeUniformInputs(obj.uniforms, effect.avalBindings, ub);
|
|
378
574
|
const block = ubAsBlock(ub);
|
|
379
575
|
const res = prepareUniformBuffer(device, block, merged, {
|
|
@@ -412,42 +608,30 @@ export function prepareRenderObject(
|
|
|
412
608
|
|
|
413
609
|
const groups = buildGroups(device, perGroup);
|
|
414
610
|
|
|
415
|
-
// Pipeline
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
611
|
+
// Pipeline — set up the build context and produce the initial
|
|
612
|
+
// GPURenderPipeline. We snapshot via AdaptiveToken.top here; that's
|
|
613
|
+
// a one-time construction-boundary read, NOT inside an adaptive
|
|
614
|
+
// computation. Subsequent recompiles happen inside `update(token)`.
|
|
615
|
+
const vsStage = effect.stages.find(st => st.stage === "vertex");
|
|
616
|
+
const fsStage = effect.stages.find(st => st.stage === "fragment");
|
|
419
617
|
if (vsStage === undefined) throw new Error("prepareRenderObject: CompiledEffect has no vertex stage");
|
|
420
618
|
if (fsStage === undefined) throw new Error("prepareRenderObject: CompiledEffect has no fragment stage");
|
|
421
619
|
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
620
|
+
const pipelineCtx: PipelineBuildContext = {
|
|
621
|
+
device,
|
|
622
|
+
iface,
|
|
623
|
+
signature,
|
|
624
|
+
vertexLayouts,
|
|
625
|
+
bindGroupLayouts: groups.map(g => g.layout),
|
|
626
|
+
vsSource: vsStage.source,
|
|
627
|
+
fsSource: fsStage.source,
|
|
628
|
+
vsEntry: vsStage.entryName,
|
|
629
|
+
fsEntry: fsStage.entryName,
|
|
630
|
+
vsSourceMap: vsStage.sourceMap,
|
|
631
|
+
fsSourceMap: fsStage.sourceMap,
|
|
431
632
|
...(opts.label !== undefined ? { label: opts.label } : {}),
|
|
432
633
|
...(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
634
|
};
|
|
450
|
-
const pipeline = compileRenderPipeline(device, pipelineDesc);
|
|
451
635
|
|
|
452
636
|
return new PreparedRenderObject(
|
|
453
637
|
device,
|
|
@@ -455,10 +639,20 @@ export function prepareRenderObject(
|
|
|
455
639
|
indexBuffer, indexFormat, obj.indices,
|
|
456
640
|
groups,
|
|
457
641
|
obj.drawCall,
|
|
458
|
-
|
|
642
|
+
pipelineCtx,
|
|
643
|
+
obj.pipelineState,
|
|
459
644
|
);
|
|
460
645
|
}
|
|
461
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Sentinel pipeline placeholder. PreparedRenderObject's `pipeline`
|
|
649
|
+
* field is non-null for ergonomic typing; before the first
|
|
650
|
+
* `update(token)` it points at this sentinel which is only used as
|
|
651
|
+
* a sort-rank identity (never bound to a render pass — `record`
|
|
652
|
+
* always calls `update` first).
|
|
653
|
+
*/
|
|
654
|
+
const SENTINEL_PIPELINE = { __sentinel: "PreparedRenderObject.pipeline" } as unknown as GPURenderPipeline;
|
|
655
|
+
|
|
462
656
|
// ---------------------------------------------------------------------------
|
|
463
657
|
// Uniform input merging — `obj.uniforms` wins; `avalBindings` is fallback.
|
|
464
658
|
// ---------------------------------------------------------------------------
|
|
@@ -475,8 +669,6 @@ function mergeUniformInputs(
|
|
|
475
669
|
const getter = bindings[f.name];
|
|
476
670
|
if (getter === undefined) continue;
|
|
477
671
|
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
672
|
if (isAval(v)) merged = merged.add(f.name, v as aval<unknown>);
|
|
481
673
|
else merged = merged.add(f.name, cval(v));
|
|
482
674
|
}
|
|
@@ -487,12 +679,6 @@ function isAval(v: unknown): boolean {
|
|
|
487
679
|
return typeof v === "object" && v !== null && typeof (v as { getValue?: unknown }).getValue === "function";
|
|
488
680
|
}
|
|
489
681
|
|
|
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
682
|
function ubAsBlock(ub: import("../core/index.js").UniformBlockInfo): import("../core/index.js").UniformBlockInfo {
|
|
497
683
|
return ub;
|
|
498
684
|
}
|
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
|
}
|