@forgeax/engine-render 0.1.19 → 0.1.20
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/.tsbuildinfo +1 -1
- package/dist/assembly/factory.d.ts +16 -5
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/{chunk-RJRUS55L.mjs → chunk-4XINDLAY.mjs} +267 -186
- package/dist/chunk-4XINDLAY.mjs.map +1 -0
- package/dist/{chunk-YTQ5C7YQ.mjs → chunk-CDTWTHZR.mjs} +176 -8
- package/dist/chunk-CDTWTHZR.mjs.map +1 -0
- package/dist/{chunk-IPXCABEP.mjs → chunk-M2MP4ZOU.mjs} +2 -2
- package/dist/chunk-M2MP4ZOU.mjs.map +1 -0
- package/dist/{chunk-LOSO4CTV.mjs → chunk-TBQJOK2L.mjs} +67 -3
- package/dist/chunk-TBQJOK2L.mjs.map +1 -0
- package/dist/components/light-helpers.d.ts.map +1 -1
- package/dist/components/spot-light.d.ts +12 -0
- package/dist/components/spot-light.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +2281 -164
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +19 -2
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/errors/render.d.ts +69 -1
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/gpu-usage.d.ts +2 -0
- package/dist/gpu-usage.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/internal.mjs +1 -1
- package/dist/light-buffer-layout.d.ts +20 -12
- package/dist/light-buffer-layout.d.ts.map +1 -1
- package/dist/pbr-pipeline.d.ts +6 -3
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
- package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-profile.d.ts +13 -0
- package/dist/pipeline/standard-profile.d.ts.map +1 -1
- package/dist/record/frame-lighting.d.ts +2 -0
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +37 -1
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +3 -2
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/gpu-timing.d.ts +32 -0
- package/dist/record/gpu-timing.d.ts.map +1 -0
- package/dist/record/main-pass-material.d.ts +1 -1
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +5 -1
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +11 -3
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/render-contract.d.ts +66 -1
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-data.d.ts +10 -6
- package/dist/render-data.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +36 -0
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +55 -3
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +7 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/renderer-inspect.d.ts +29 -0
- package/dist/renderer-inspect.d.ts.map +1 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/temporal/frame.d.ts +44 -0
- package/dist/temporal/frame.d.ts.map +1 -0
- package/dist/temporal/index.mjs +3 -3
- package/dist/typed-render-graph-primitives.d.ts +6 -1
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/typed-shadow-atlas.d.ts +10 -0
- package/dist/typed-shadow-atlas.d.ts.map +1 -0
- package/dist/typed-shadow-passes.d.ts.map +1 -1
- package/dist/volume/capability.d.ts +57 -0
- package/dist/volume/capability.d.ts.map +1 -0
- package/dist/volume/component.d.ts +61 -0
- package/dist/volume/component.d.ts.map +1 -0
- package/dist/volume/extract.d.ts +11 -0
- package/dist/volume/extract.d.ts.map +1 -0
- package/dist/volume/inspection.d.ts +38 -0
- package/dist/volume/inspection.d.ts.map +1 -0
- package/dist/volume/passes.d.ts +38 -0
- package/dist/volume/passes.d.ts.map +1 -0
- package/dist/volume/recovery.d.ts +38 -0
- package/dist/volume/recovery.d.ts.map +1 -0
- package/dist/volume/resources.d.ts +57 -0
- package/dist/volume/resources.d.ts.map +1 -0
- package/dist/volume/temporal.d.ts +35 -0
- package/dist/volume/temporal.d.ts.map +1 -0
- package/package.json +20 -20
- package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -14
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
- package/src/__tests__/render-error-code-owner.test-d.ts +5 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +28 -0
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +1 -1
- package/src/__tests__/skinned-shadow-caster.test.ts +21 -0
- package/src/__tests__/standard-profile-oracle.unit.test.ts +7 -5
- package/src/__tests__/volume-optics-oracle.unit.test.ts +85 -0
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +655 -0
- package/src/__tests__/volumetric-fog-component.unit.test.ts +71 -0
- package/src/__tests__/volumetric-fog-composite-stage-readback.dawn.test.ts +376 -0
- package/src/__tests__/volumetric-fog-errors.unit.test.ts +51 -0
- package/src/__tests__/volumetric-fog-inspection.unit.test.ts +77 -0
- package/src/__tests__/volumetric-fog-light-capability.unit.test.ts +19 -0
- package/src/__tests__/volumetric-fog-light-consumers.integration.test.ts +18 -0
- package/src/__tests__/volumetric-fog-light-errors.unit.test.ts +17 -0
- package/src/__tests__/volumetric-fog-light-topology.unit.test.ts +21 -0
- package/src/__tests__/volumetric-fog-light-transaction.integration.test.ts +53 -0
- package/src/__tests__/volumetric-fog-m9-watcher.integration.test.ts +27 -0
- package/src/__tests__/volumetric-fog-point-light-owner.integration.test.ts +44 -0
- package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +49 -0
- package/src/__tests__/volumetric-fog-point-spot-contract.unit.test.ts +28 -0
- package/src/__tests__/volumetric-fog-point-spot-lkg.integration.test.ts +65 -0
- package/src/__tests__/volumetric-fog-point-spot-selection.integration.test.ts +49 -0
- package/src/__tests__/volumetric-fog-point-spot-temporal.unit.test.ts +55 -0
- package/src/__tests__/volumetric-fog-projector-contract.unit.test.ts +22 -0
- package/src/__tests__/volumetric-fog-projector-inspection.unit.test.ts +12 -0
- package/src/__tests__/volumetric-fog-projector-surface-volume.integration.test.ts +11 -0
- package/src/__tests__/volumetric-fog-projector-transaction.integration.test.ts +26 -0
- package/src/__tests__/volumetric-fog-recovery.integration.test.ts +47 -0
- package/src/__tests__/volumetric-fog-selected-light-resolution.integration.test.ts +63 -0
- package/src/__tests__/volumetric-fog-spot-closure.integration.test.ts +26 -0
- package/src/__tests__/volumetric-fog-spot-contract.test-d.ts +21 -0
- package/src/__tests__/volumetric-fog-spot-contract.unit.test.ts +27 -0
- package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +14 -0
- package/src/__tests__/volumetric-fog-spot-optics.integration.test.ts +29 -0
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +550 -0
- package/src/__tests__/volumetric-fog-temporal-selected-light.unit.test.ts +33 -0
- package/src/__tests__/volumetric-fog-temporal.unit.test.ts +45 -0
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +237 -0
- package/src/__tests__/volumetric-fog-transparent.integration.test.ts +27 -0
- package/src/assembly/factory.ts +271 -15
- package/src/components/light-helpers.ts +4 -0
- package/src/components/spot-light.ts +22 -3
- package/src/device/gpu-residency.ts +168 -58
- package/src/errors/render.ts +119 -1
- package/src/gpu-usage.ts +2 -0
- package/src/index.ts +38 -1
- package/src/light-buffer-layout.ts +59 -26
- package/src/pbr-pipeline.ts +33 -10
- package/src/pipeline/standard-forward-lane.ts +20 -0
- package/src/pipeline/standard-pipeline.ts +20 -0
- package/src/pipeline/standard-profile.ts +23 -0
- package/src/record/frame-lighting.ts +103 -89
- package/src/record/frame-snapshot.ts +41 -1
- package/src/record/frame-targets.ts +6 -2
- package/src/record/frame.ts +908 -5
- package/src/record/gpu-timing.ts +224 -0
- package/src/record/main-pass-material.ts +6 -2
- package/src/record/render-context.ts +5 -0
- package/src/record/shadow-pass.ts +87 -38
- package/src/record/typed-frame-graph.ts +143 -8
- package/src/render-contract.ts +72 -0
- package/src/render-data.ts +50 -22
- package/src/render-pipeline.ts +44 -0
- package/src/render-system-extract.ts +329 -3
- package/src/render-system.ts +79 -0
- package/src/renderer-inspect.ts +66 -0
- package/src/scene/render-scene.ts +2 -0
- package/src/temporal/frame.ts +128 -0
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +2 -3
- package/src/typed-render-graph-primitives.ts +32 -2
- package/src/typed-shadow-atlas.ts +17 -0
- package/src/typed-shadow-passes.ts +6 -5
- package/src/volume/capability.ts +173 -0
- package/src/volume/component.ts +110 -0
- package/src/volume/extract.ts +21 -0
- package/src/volume/inspection.ts +78 -0
- package/src/volume/passes.ts +680 -0
- package/src/volume/recovery.ts +86 -0
- package/src/volume/resources.ts +236 -0
- package/src/volume/temporal.ts +89 -0
- package/dist/chunk-IPXCABEP.mjs.map +0 -1
- package/dist/chunk-LOSO4CTV.mjs.map +0 -1
- package/dist/chunk-RJRUS55L.mjs.map +0 -1
- package/dist/chunk-YTQ5C7YQ.mjs.map +0 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { Buffer, QuerySet, RhiCommandEncoder, RhiDevice, RhiError } from '@forgeax/engine-rhi';
|
|
2
|
+
import { ok, type Result } from '@forgeax/engine-types';
|
|
3
|
+
import {
|
|
4
|
+
GPU_BUFFER_USAGE_COPY_DST,
|
|
5
|
+
GPU_BUFFER_USAGE_COPY_SRC,
|
|
6
|
+
GPU_BUFFER_USAGE_MAP_READ,
|
|
7
|
+
GPU_BUFFER_USAGE_QUERY_RESOLVE,
|
|
8
|
+
} from '../gpu-usage';
|
|
9
|
+
|
|
10
|
+
const MAX_VOLUME_PASSES = 4;
|
|
11
|
+
const QUERY_COUNT = MAX_VOLUME_PASSES * 2;
|
|
12
|
+
const QUERY_RESULT_BYTES = 8;
|
|
13
|
+
const READBACK_BYTES = QUERY_COUNT * QUERY_RESULT_BYTES;
|
|
14
|
+
const PASS_NAMES = [
|
|
15
|
+
'volume-inject',
|
|
16
|
+
'volume-integrate',
|
|
17
|
+
'volume-temporal',
|
|
18
|
+
'volume-composite',
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export type VolumeTimingObservation =
|
|
22
|
+
| {
|
|
23
|
+
readonly status: 'ready';
|
|
24
|
+
readonly unit: 'ms';
|
|
25
|
+
readonly totalMs: number;
|
|
26
|
+
readonly passes: readonly { readonly name: string; readonly milliseconds: number }[];
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
readonly status: 'unavailable';
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function unavailable(reason: string): VolumeTimingObservation {
|
|
34
|
+
return Object.freeze({ status: 'unavailable', reason });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Renderer-owned timestamp capture for the authored volume graph. Query
|
|
39
|
+
* resources are private to one accepted submit and never cross the receipt
|
|
40
|
+
* boundary. This publishes raw renderer timings; a benchmark runner owns
|
|
41
|
+
* runner class, frame-count, and threshold qualification.
|
|
42
|
+
*/
|
|
43
|
+
export class GpuTimingCapture {
|
|
44
|
+
readonly #device: RhiDevice;
|
|
45
|
+
readonly #querySet: QuerySet;
|
|
46
|
+
readonly #resolveBuffer: Buffer;
|
|
47
|
+
readonly #readbackBuffer: Buffer;
|
|
48
|
+
readonly #periodNanoseconds: number;
|
|
49
|
+
readonly #started = new Map<string, number>();
|
|
50
|
+
readonly #finished = new Map<string, number>();
|
|
51
|
+
#resolved = false;
|
|
52
|
+
#submitted = false;
|
|
53
|
+
#discarded = false;
|
|
54
|
+
#disposed = false;
|
|
55
|
+
#observation: Promise<VolumeTimingObservation> | undefined;
|
|
56
|
+
|
|
57
|
+
private constructor(
|
|
58
|
+
device: RhiDevice,
|
|
59
|
+
querySet: QuerySet,
|
|
60
|
+
resolveBuffer: Buffer,
|
|
61
|
+
readbackBuffer: Buffer,
|
|
62
|
+
periodNanoseconds: number,
|
|
63
|
+
) {
|
|
64
|
+
this.#device = device;
|
|
65
|
+
this.#querySet = querySet;
|
|
66
|
+
this.#resolveBuffer = resolveBuffer;
|
|
67
|
+
this.#readbackBuffer = readbackBuffer;
|
|
68
|
+
this.#periodNanoseconds = periodNanoseconds;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static create(device: RhiDevice): Result<GpuTimingCapture | undefined, RhiError> {
|
|
72
|
+
const period = device.caps.timestampPeriodNanoseconds;
|
|
73
|
+
if (!device.caps.timestampQuery || period === null || !Number.isFinite(period) || period <= 0) {
|
|
74
|
+
return ok(undefined);
|
|
75
|
+
}
|
|
76
|
+
const querySet = device.createQuerySet({
|
|
77
|
+
label: 'volume-timing',
|
|
78
|
+
type: 'timestamp',
|
|
79
|
+
count: QUERY_COUNT,
|
|
80
|
+
});
|
|
81
|
+
if (!querySet.ok) return querySet;
|
|
82
|
+
const resolveBuffer = device.createBuffer({
|
|
83
|
+
label: 'volume-timing.resolve',
|
|
84
|
+
size: READBACK_BYTES,
|
|
85
|
+
usage: GPU_BUFFER_USAGE_QUERY_RESOLVE | GPU_BUFFER_USAGE_COPY_SRC,
|
|
86
|
+
});
|
|
87
|
+
if (!resolveBuffer.ok) {
|
|
88
|
+
device.destroyQuerySet(querySet.value);
|
|
89
|
+
return resolveBuffer;
|
|
90
|
+
}
|
|
91
|
+
const readbackBuffer = device.createBuffer({
|
|
92
|
+
label: 'volume-timing.readback',
|
|
93
|
+
size: READBACK_BYTES,
|
|
94
|
+
usage: GPU_BUFFER_USAGE_MAP_READ | GPU_BUFFER_USAGE_COPY_DST,
|
|
95
|
+
});
|
|
96
|
+
if (!readbackBuffer.ok) {
|
|
97
|
+
device.destroyBuffer(resolveBuffer.value);
|
|
98
|
+
device.destroyQuerySet(querySet.value);
|
|
99
|
+
return readbackBuffer;
|
|
100
|
+
}
|
|
101
|
+
return ok(
|
|
102
|
+
new GpuTimingCapture(
|
|
103
|
+
device,
|
|
104
|
+
querySet.value,
|
|
105
|
+
resolveBuffer.value,
|
|
106
|
+
readbackBuffer.value,
|
|
107
|
+
period,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
beforePass(name: string, encoder: RhiCommandEncoder): void {
|
|
113
|
+
const index = PASS_NAMES.indexOf(name as (typeof PASS_NAMES)[number]);
|
|
114
|
+
if (index < 0 || this.#started.has(name)) return;
|
|
115
|
+
this.#started.set(name, index);
|
|
116
|
+
encoder.writeTimestamp(this.#querySet, index * 2);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
afterPass(name: string, encoder: RhiCommandEncoder): void {
|
|
120
|
+
const index = this.#started.get(name);
|
|
121
|
+
if (index === undefined || this.#finished.has(name)) return;
|
|
122
|
+
this.#finished.set(name, index);
|
|
123
|
+
encoder.writeTimestamp(this.#querySet, index * 2 + 1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
resolve(encoder: RhiCommandEncoder): Result<void, RhiError> {
|
|
127
|
+
if (this.#started.size === 0) {
|
|
128
|
+
this.#resolved = true;
|
|
129
|
+
return ok(undefined);
|
|
130
|
+
}
|
|
131
|
+
const resolved = encoder.resolveQuerySet(
|
|
132
|
+
this.#querySet,
|
|
133
|
+
0,
|
|
134
|
+
QUERY_COUNT,
|
|
135
|
+
this.#resolveBuffer,
|
|
136
|
+
0,
|
|
137
|
+
);
|
|
138
|
+
if (!resolved.ok) return resolved;
|
|
139
|
+
encoder.copyBufferToBuffer(this.#resolveBuffer, this.#readbackBuffer, READBACK_BYTES);
|
|
140
|
+
this.#resolved = true;
|
|
141
|
+
return ok(undefined);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
markSubmitted(): void {
|
|
145
|
+
if (!this.#discarded) this.#submitted = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
discard(): void {
|
|
149
|
+
if (this.#discarded) return;
|
|
150
|
+
this.#discarded = true;
|
|
151
|
+
this.#resolved = true;
|
|
152
|
+
this.#submitted = false;
|
|
153
|
+
this.#dispose();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
observation(): Promise<VolumeTimingObservation> {
|
|
157
|
+
if (this.#observation !== undefined) return this.#observation;
|
|
158
|
+
if (this.#discarded || !this.#resolved || !this.#submitted || this.#started.size === 0) {
|
|
159
|
+
this.#observation = Promise.resolve(
|
|
160
|
+
unavailable('timestamp capture did not reach an accepted volume submit'),
|
|
161
|
+
);
|
|
162
|
+
this.#dispose();
|
|
163
|
+
return this.#observation;
|
|
164
|
+
}
|
|
165
|
+
this.#observation = this.#readbackBuffer
|
|
166
|
+
.mapAsync(GPU_BUFFER_USAGE_MAP_READ)
|
|
167
|
+
.then((mapped) => {
|
|
168
|
+
if (!mapped.ok) return unavailable(`timestamp readback failed: ${mapped.error.code}`);
|
|
169
|
+
const range = mapped.value.getMappedRange();
|
|
170
|
+
if (!range.ok) {
|
|
171
|
+
mapped.value.unmap();
|
|
172
|
+
return unavailable(`timestamp mapping failed: ${range.error.code}`);
|
|
173
|
+
}
|
|
174
|
+
const values = new DataView(range.value);
|
|
175
|
+
const decoded = [...this.#finished.entries()]
|
|
176
|
+
.sort((a, b) => a[1] - b[1])
|
|
177
|
+
.map(([name, index]) => ({
|
|
178
|
+
name,
|
|
179
|
+
begin: values.getBigUint64(index * 2 * QUERY_RESULT_BYTES, true),
|
|
180
|
+
end: values.getBigUint64((index * 2 + 1) * QUERY_RESULT_BYTES, true),
|
|
181
|
+
}));
|
|
182
|
+
if (decoded.some(({ end, begin }) => end < begin)) {
|
|
183
|
+
mapped.value.unmap();
|
|
184
|
+
this.#dispose();
|
|
185
|
+
return unavailable('timestamp query returned an end before its begin');
|
|
186
|
+
}
|
|
187
|
+
const passes = decoded.map(({ name, begin, end }) => ({
|
|
188
|
+
name,
|
|
189
|
+
milliseconds: (Number(end - begin) * this.#periodNanoseconds) / 1_000_000,
|
|
190
|
+
}));
|
|
191
|
+
const firstBegin = decoded.reduce(
|
|
192
|
+
(first, sample) => (sample.begin < first ? sample.begin : first),
|
|
193
|
+
decoded[0]?.begin ?? 0n,
|
|
194
|
+
);
|
|
195
|
+
const lastEnd = decoded.reduce(
|
|
196
|
+
(last, sample) => (sample.end > last ? sample.end : last),
|
|
197
|
+
decoded[0]?.end ?? 0n,
|
|
198
|
+
);
|
|
199
|
+
const totalMs = (Number(lastEnd - firstBegin) * this.#periodNanoseconds) / 1_000_000;
|
|
200
|
+
mapped.value.unmap();
|
|
201
|
+
this.#dispose();
|
|
202
|
+
if (
|
|
203
|
+
!Number.isFinite(totalMs) ||
|
|
204
|
+
passes.some((pass) => !Number.isFinite(pass.milliseconds))
|
|
205
|
+
) {
|
|
206
|
+
return unavailable('timestamp query returned a non-finite duration');
|
|
207
|
+
}
|
|
208
|
+
return Object.freeze({ status: 'ready', unit: 'ms', totalMs, passes });
|
|
209
|
+
})
|
|
210
|
+
.catch((cause: unknown) => {
|
|
211
|
+
this.#dispose();
|
|
212
|
+
return unavailable(`timestamp readback failed: ${String(cause)}`);
|
|
213
|
+
});
|
|
214
|
+
return this.#observation;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#dispose(): void {
|
|
218
|
+
if (this.#disposed) return;
|
|
219
|
+
this.#disposed = true;
|
|
220
|
+
this.#device.destroyBuffer(this.#resolveBuffer);
|
|
221
|
+
this.#device.destroyBuffer(this.#readbackBuffer);
|
|
222
|
+
this.#device.destroyQuerySet(this.#querySet);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -414,10 +414,14 @@ const DEFAULT_STANDARD_TEXTURE_COORDINATES = new Float32Array([
|
|
|
414
414
|
|
|
415
415
|
/** Derives the logical-content UV scale for an uploaded material texture. */
|
|
416
416
|
export function materialTextureUvScale(
|
|
417
|
-
texture: Pick<TextureAsset, '
|
|
417
|
+
texture: Pick<TextureAsset, 'shape' | 'format'> | undefined,
|
|
418
418
|
): readonly [number, number] {
|
|
419
419
|
if (texture === undefined) return [1, 1];
|
|
420
|
-
return deriveTextureExtent(
|
|
420
|
+
return deriveTextureExtent(
|
|
421
|
+
texture.format,
|
|
422
|
+
texture.shape.extent.width,
|
|
423
|
+
texture.shape.extent.height,
|
|
424
|
+
).uvScale;
|
|
421
425
|
}
|
|
422
426
|
|
|
423
427
|
function materialTextureForField(
|
|
@@ -57,6 +57,7 @@ import type {
|
|
|
57
57
|
RenderDebugOverlay,
|
|
58
58
|
RenderPipelineContext,
|
|
59
59
|
RenderRecordPhase,
|
|
60
|
+
VolumetricFogShaderSources,
|
|
60
61
|
} from '../render-contract';
|
|
61
62
|
import type {
|
|
62
63
|
DispatchEntry,
|
|
@@ -236,6 +237,8 @@ export interface PipelineState {
|
|
|
236
237
|
/** Surface route committed by configureSurface for inspection provenance. */
|
|
237
238
|
readonly surfaceProfile?: 'dual-view' | 'raw-only';
|
|
238
239
|
readonly viewBindGroupLayout: BindGroupLayout;
|
|
240
|
+
/** Device capability gate for the optional SpotLight projector binding. */
|
|
241
|
+
readonly projectorAvailable?: boolean;
|
|
239
242
|
readonly materialBindGroupLayout: BindGroupLayout;
|
|
240
243
|
readonly meshBindGroupLayout: BindGroupLayout;
|
|
241
244
|
readonly viewUniformBuffer: Buffer;
|
|
@@ -302,6 +305,8 @@ export interface RenderSystemInternals extends RenderSystemRuntime {
|
|
|
302
305
|
label: string,
|
|
303
306
|
) => RhiRenderPipeline | null)
|
|
304
307
|
| undefined;
|
|
308
|
+
/** Composed renderer utility sources installed atomically at build-ready. */
|
|
309
|
+
volumetricFogShaders?: VolumetricFogShaderSources | undefined;
|
|
305
310
|
}
|
|
306
311
|
|
|
307
312
|
export interface _StandardForwardSceneView {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type RhiQueue,
|
|
8
8
|
type RhiRenderPassEncoder,
|
|
9
9
|
} from '@forgeax/engine-rhi';
|
|
10
|
-
import type { PassSelector } from '@forgeax/engine-types';
|
|
10
|
+
import type { MaterialRenderState, PassSelector } from '@forgeax/engine-types';
|
|
11
11
|
import { GpuBuffer } from '../gpu-resource';
|
|
12
12
|
import {
|
|
13
13
|
GPU_BUFFER_USAGE_COPY_DST,
|
|
@@ -81,6 +81,7 @@ function ensureTypedShadowViewBg(
|
|
|
81
81
|
const { runtime, frameState, pipelineState } = c;
|
|
82
82
|
const shadowSampler = pipelineState.perPassResources.shadowSampler;
|
|
83
83
|
if (shadowSampler === null) return null;
|
|
84
|
+
const projectorAvailable = pipelineState.projectorAvailable !== false;
|
|
84
85
|
try {
|
|
85
86
|
return getOrCreateFromChain(
|
|
86
87
|
frameState.viewBindGroupCache,
|
|
@@ -94,6 +95,9 @@ function ensureTypedShadowViewBg(
|
|
|
94
95
|
pipelineState.shadowParamsBuffer,
|
|
95
96
|
pipelineState.shadowCasterCascadeBuffer,
|
|
96
97
|
pipelineState.shadowFallbackTextureView,
|
|
98
|
+
...(projectorAvailable
|
|
99
|
+
? [pipelineState.defaultWhiteTextureView, pipelineState.defaultSampler]
|
|
100
|
+
: []),
|
|
97
101
|
pipelineState.pointsLinesViewBuffer ?? pipelineState.viewUniformBuffer,
|
|
98
102
|
],
|
|
99
103
|
variant,
|
|
@@ -165,6 +169,21 @@ function ensureTypedShadowViewBg(
|
|
|
165
169
|
},
|
|
166
170
|
},
|
|
167
171
|
},
|
|
172
|
+
...(projectorAvailable
|
|
173
|
+
? [
|
|
174
|
+
{
|
|
175
|
+
binding: 11,
|
|
176
|
+
resource: {
|
|
177
|
+
kind: 'textureView' as const,
|
|
178
|
+
value: pipelineState.defaultWhiteTextureView,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
binding: 12,
|
|
183
|
+
resource: { kind: 'sampler' as const, value: pipelineState.defaultSampler },
|
|
184
|
+
},
|
|
185
|
+
]
|
|
186
|
+
: []),
|
|
168
187
|
],
|
|
169
188
|
});
|
|
170
189
|
if (!created.ok) throw created.error;
|
|
@@ -181,11 +200,26 @@ function ensureTypedShadowViewBg(
|
|
|
181
200
|
}
|
|
182
201
|
}
|
|
183
202
|
|
|
184
|
-
|
|
185
|
-
|
|
203
|
+
interface ShadowDispatch {
|
|
204
|
+
readonly materialShaderId: string;
|
|
205
|
+
readonly renderState: MaterialRenderState | undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
type ShadowDispatchMap = ReadonlyMap<number, ReadonlyMap<number, ShadowDispatch>>;
|
|
209
|
+
|
|
210
|
+
function shadowShaderMap(c: _InternalRenderPipelineContext): ShadowDispatchMap {
|
|
211
|
+
const shaders = new Map<number, Map<number, ShadowDispatch>>();
|
|
186
212
|
for (const entry of c.dispatch) {
|
|
187
213
|
if (entry.tags.LightMode === 'ShadowCaster' && entry.materialShaderId !== undefined) {
|
|
188
|
-
shaders.
|
|
214
|
+
let byMaterial = shaders.get(entry.renderableIndex);
|
|
215
|
+
if (byMaterial === undefined) {
|
|
216
|
+
byMaterial = new Map<number, ShadowDispatch>();
|
|
217
|
+
shaders.set(entry.renderableIndex, byMaterial);
|
|
218
|
+
}
|
|
219
|
+
byMaterial.set(entry.materialHandle, {
|
|
220
|
+
materialShaderId: entry.materialShaderId,
|
|
221
|
+
renderState: entry.renderState,
|
|
222
|
+
});
|
|
189
223
|
}
|
|
190
224
|
}
|
|
191
225
|
return shaders;
|
|
@@ -488,8 +522,8 @@ export function buildMatchedMaterialHandlesByRenderable(
|
|
|
488
522
|
* groups (@group 0/1) are already set by the caller; this loop owns @group
|
|
489
523
|
* 2/3 + the vertex/index/pipeline de-dup state. Receives the explicit
|
|
490
524
|
* `_InternalRenderPipelineContext` (`c`) plus the caller-resolved shadow
|
|
491
|
-
* pipeline, mesh bind group, pass-selector match set, and per-
|
|
492
|
-
*
|
|
525
|
+
* pipeline, mesh bind group, pass-selector match set, and per-material
|
|
526
|
+
* ShadowCaster dispatch map so no cross-function mutable state is introduced.
|
|
493
527
|
*/
|
|
494
528
|
function recordShadowCasterDraws(
|
|
495
529
|
c: _InternalRenderPipelineContext,
|
|
@@ -497,7 +531,7 @@ function recordShadowCasterDraws(
|
|
|
497
531
|
shadowPipeline: RenderPipeline,
|
|
498
532
|
shadowMeshBindGroup: BindGroup,
|
|
499
533
|
matchedIndices: Set<number> | null,
|
|
500
|
-
|
|
534
|
+
shadowDispatchByRenderableIdx: ShadowDispatchMap,
|
|
501
535
|
): void {
|
|
502
536
|
const { runtime, pipelineState, validatedOrdered } = c;
|
|
503
537
|
// M-3 / w12: vertexBuffer/indexBuffer state locals migrate to GpuBuffer
|
|
@@ -520,41 +554,14 @@ function recordShadowCasterDraws(
|
|
|
520
554
|
// feat-20260609 M2: skip entities that don't match the pass selector.
|
|
521
555
|
if (matchedIndices !== null && !matchedIndices.has(entry.renderableIndex)) continue;
|
|
522
556
|
|
|
523
|
-
// bug-20260619-csm RC-3 (AC-10, D-3): resolve the per-entity shadow
|
|
524
|
-
// PSO from its ShadowCaster shader id. Default casters keep the
|
|
525
|
-
// vertex-only `forgeax::default-shadow-caster` PSO bound above; a
|
|
526
|
-
// material with a custom ShadowCaster shader (cutout alpha-test) gets
|
|
527
|
-
// its own fragment-carrying PSO so `discard` runs in the depth pass.
|
|
528
|
-
const entryShadowShaderId = shadowShaderByRenderableIdx.get(entry.renderableIndex);
|
|
529
|
-
let entryShadowPipeline = shadowPipeline;
|
|
530
557
|
if (entry.source.skin !== undefined) {
|
|
531
558
|
const skinPipeline = skinnedShadowPipeline(c, entry);
|
|
532
559
|
if (skinPipeline === null) continue;
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
) {
|
|
538
|
-
// Custom ShadowCaster PSO; same cache path as the default above
|
|
539
|
-
// (passKind 'shadow-caster'). On a cache miss (async build in
|
|
540
|
-
// flight / build failure) fall back to the default PSO so the
|
|
541
|
-
// caster still writes depth rather than dropping its draw.
|
|
542
|
-
entryShadowPipeline =
|
|
543
|
-
runtime.getMaterialShaderPipeline?.(
|
|
544
|
-
entryShadowShaderId,
|
|
545
|
-
false, // isHdr — shadow depth pass is always LDR
|
|
546
|
-
undefined, // renderState
|
|
547
|
-
'triangle-list', // topology — shadow PSO targets triangle-list
|
|
548
|
-
undefined, // indexFormat
|
|
549
|
-
undefined, // variantSet — shadow caster has no variant axes
|
|
550
|
-
'shadow-caster', // passKind
|
|
551
|
-
) ?? shadowPipeline;
|
|
552
|
-
}
|
|
553
|
-
if (entryShadowPipeline !== shadowLastPipeline && entryShadowPipeline !== null) {
|
|
554
|
-
shadowPass.setPipeline(entryShadowPipeline);
|
|
555
|
-
shadowLastPipeline = entryShadowPipeline;
|
|
560
|
+
if (skinPipeline !== shadowLastPipeline) {
|
|
561
|
+
shadowPass.setPipeline(skinPipeline);
|
|
562
|
+
shadowLastPipeline = skinPipeline;
|
|
563
|
+
}
|
|
556
564
|
}
|
|
557
|
-
|
|
558
565
|
// feat-20260604-mesh-topology-debug-draw M5 / w14 (AC-09, D-A6): the
|
|
559
566
|
// shadow caster PSO is triangle-list; it only projects triangle faces.
|
|
560
567
|
// line-list / line-strip / point-list meshes have no surface to cast a
|
|
@@ -708,6 +715,48 @@ function recordShadowCasterDraws(
|
|
|
708
715
|
if (sm.topology !== 'triangle-list' && sm.topology !== 'triangle-strip') {
|
|
709
716
|
continue;
|
|
710
717
|
}
|
|
718
|
+
|
|
719
|
+
// Resolve the ShadowCaster PSO for the material bound to this submesh.
|
|
720
|
+
// Built-in materials normally use the shared vertex-only caster, but an
|
|
721
|
+
// authored `cullMode:'none'`/`frontFace` must still reach that PSO so
|
|
722
|
+
// two-sided casters (such as the Three.js teapot) populate the atlas
|
|
723
|
+
// from both faces. Custom ShadowCaster shaders retain their own path.
|
|
724
|
+
const submeshMaterial = entry.source.materials[sm.materialSlot] ?? entry.source.material;
|
|
725
|
+
const shadowDispatch = shadowDispatchByRenderableIdx
|
|
726
|
+
.get(entry.renderableIndex)
|
|
727
|
+
?.get(submeshMaterial.materialHandle ?? 0);
|
|
728
|
+
const entryShadowShaderId = shadowDispatch?.materialShaderId;
|
|
729
|
+
const entryShadowRenderState = shadowDispatch?.renderState;
|
|
730
|
+
// A skinned caster's pipeline was selected above together with its
|
|
731
|
+
// two-binding mesh/palette group. Keep that pairing for every submesh;
|
|
732
|
+
// resetting to the static shadow pipeline here would bind the skin
|
|
733
|
+
// group against `pbr-pl` and Dawn rejects the draw at submit time.
|
|
734
|
+
let entryShadowPipeline =
|
|
735
|
+
entry.source.skin !== undefined ? shadowLastPipeline : shadowPipeline;
|
|
736
|
+
if (
|
|
737
|
+
entry.source.skin === undefined &&
|
|
738
|
+
entryShadowShaderId !== undefined &&
|
|
739
|
+
(entryShadowShaderId !== 'forgeax::default-shadow-caster' ||
|
|
740
|
+
entryShadowRenderState !== undefined)
|
|
741
|
+
) {
|
|
742
|
+
// On a cache miss (async build in flight / build failure), fall back
|
|
743
|
+
// to the default PSO so the caster still writes depth rather than
|
|
744
|
+
// dropping its draw.
|
|
745
|
+
entryShadowPipeline =
|
|
746
|
+
runtime.getMaterialShaderPipeline?.(
|
|
747
|
+
entryShadowShaderId,
|
|
748
|
+
false, // isHdr — shadow depth pass is always LDR
|
|
749
|
+
entryShadowRenderState,
|
|
750
|
+
'triangle-list', // topology — shadow PSO targets triangle-list
|
|
751
|
+
undefined, // indexFormat
|
|
752
|
+
undefined, // variantSet — shadow caster has no variant axes
|
|
753
|
+
'shadow-caster', // passKind
|
|
754
|
+
) ?? shadowPipeline;
|
|
755
|
+
}
|
|
756
|
+
if (entryShadowPipeline !== shadowLastPipeline && entryShadowPipeline !== null) {
|
|
757
|
+
shadowPass.setPipeline(entryShadowPipeline);
|
|
758
|
+
shadowLastPipeline = entryShadowPipeline;
|
|
759
|
+
}
|
|
711
760
|
if (entry.mesh.indexed) {
|
|
712
761
|
shadowPass.drawIndexed(sm.indexCount, shadowInstanceCount, sm.indexOffset, 0, 0);
|
|
713
762
|
} else {
|