@forgeax/engine-rhi-webgpu 0.1.21 → 0.1.23
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/README.md +11 -4
- package/dist/__tests__/r32float-capability-generation.integration.test.d.ts +2 -0
- package/dist/__tests__/r32float-capability-generation.integration.test.d.ts.map +1 -0
- package/dist/__tests__/r32float-format-capability.browser.test.d.ts +2 -0
- package/dist/__tests__/r32float-format-capability.browser.test.d.ts.map +1 -0
- package/dist/__tests__/r32float-format-capability.dawn.test.d.ts +2 -0
- package/dist/__tests__/r32float-format-capability.dawn.test.d.ts.map +1 -0
- package/dist/device.d.ts.map +1 -1
- package/dist/index.mjs +141 -1
- package/dist/index.mjs.map +1 -1
- package/dist/internal/__tests__/timestamp-query.unit.test.d.ts +2 -0
- package/dist/internal/__tests__/timestamp-query.unit.test.d.ts.map +1 -0
- package/dist/internal/r32float-capability.d.ts +6 -0
- package/dist/internal/r32float-capability.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/dawn-real-gpu.dawn.test.ts +168 -0
- package/src/__tests__/r32float-capability-generation.integration.test.ts +35 -0
- package/src/__tests__/r32float-format-capability.browser.test.ts +22 -0
- package/src/__tests__/r32float-format-capability.dawn.test.ts +31 -0
- package/src/__tests__/rhi-webgpu.unit.test.ts +64 -0
- package/src/device.ts +23 -0
- package/src/internal/__tests__/timestamp-query.unit.test.ts +120 -0
- package/src/internal/r32float-capability.ts +153 -0
- package/dist/.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -51,13 +51,20 @@ Real-GPU integration is in [`src/__tests__/dawn-real-gpu.dawn.test.ts`](./src/__
|
|
|
51
51
|
|
|
52
52
|
## Timestamp write contract
|
|
53
53
|
|
|
54
|
+
Timestamp capture uses the WebGPU pass descriptor `timestampWrites` field on
|
|
55
|
+
`beginRenderPass` / `beginComputePass`. The shim maps the opaque ForgeaX
|
|
56
|
+
`QuerySet` to the native `GPUQuerySet` at the pass boundary. The obsolete
|
|
57
|
+
command-encoder `writeTimestamp` entry is intentionally not exposed: current
|
|
58
|
+
Dawn rejects that method even when `timestamp-query` is advertised. A device
|
|
59
|
+
without a usable timestamp period remains capability-negative and produces no
|
|
60
|
+
synthetic ticks.
|
|
54
61
|
When `device.caps.timestampQuery` is true, render and compute pass descriptors
|
|
55
62
|
carry the mapped query set and timestamp indices. The WebGPU adapter does not
|
|
56
63
|
expose the removed command-encoder timestamp entry; callers that need an
|
|
57
64
|
encoder-position marker use an empty timestamp-enabled compute pass on the same
|
|
58
|
-
encoder. A marker failure is a structured `webgpu-runtime-error`; the
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
encoder. A marker failure is a structured `webgpu-runtime-error`; the shim
|
|
66
|
+
never treats it as a no-op or synthesizes timestamp ticks. Capability-disabled
|
|
67
|
+
devices retain the existing refusal path.
|
|
61
68
|
|
|
62
69
|
## Query forwarding contract
|
|
63
70
|
|
|
@@ -69,7 +76,7 @@ The shim forwards the query surface from [`@forgeax/engine-rhi`](../rhi):
|
|
|
69
76
|
| `occlusionQuerySet` + `beginOcclusionQuery` / `endOcclusionQuery` | The pass owns the query-set state; missing set, out-of-range index, nested begin, and unmatched end produce structured errors | [`device.ts`](./src/device.ts#L542), [Dawn state/round-trip coverage](./src/__tests__/dawn-real-gpu.dawn.test.ts#L619) |
|
|
70
77
|
| `resolveQuerySet` | Validates 256-byte destination alignment, `QUERY_RESOLVE` usage, range, and destination size before raw forwarding | [`device.ts`](./src/device.ts#L966), [Dawn coverage](./src/__tests__/dawn-real-gpu.dawn.test.ts#L820) |
|
|
71
78
|
| Timestamp pass writes | `timestampWrites` maps opaque `QuerySet` handles to raw `GPUQuerySet`; a capability-negative device refuses creation and does not synthesize ticks | [`device.ts`](./src/device.ts#L745), [Dawn admission/readback coverage](./src/__tests__/dawn-real-gpu.dawn.test.ts#L1233) |
|
|
72
|
-
|
|
|
79
|
+
| Pass descriptor `timestampWrites` | Capability-positive devices map opaque query sets at the real render/compute pass boundary; unavailable periods remain fail-closed | [`device.ts`](./src/device.ts#L745), [unit coverage](./src/internal/__tests__/timestamp-query.unit.test.ts) |
|
|
73
80
|
| `executeBundles` | Still unavailable: the public RHI has no constructible `RenderBundle` handle or creation path, so the shim returns `rhi-not-available` | [`device.ts`](./src/device.ts#L533) |
|
|
74
81
|
|
|
75
82
|
> [!NOTE]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r32float-capability-generation.integration.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/r32float-capability-generation.integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r32float-format-capability.browser.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/r32float-format-capability.browser.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r32float-format-capability.dawn.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/r32float-format-capability.dawn.test.ts"],"names":[],"mappings":""}
|
package/dist/device.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAwBV,gBAAgB,EAIhB,SAAS,EAcV,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAwBV,gBAAgB,EAIhB,SAAS,EAcV,MAAM,qBAAqB,CAAC;AAqM7B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAE/E;AA6qCD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,SAAS,GAAG;IACnD,MAAM,EAAE,SAAS,CAAC;IAClB;6CACyC;IACzC,GAAG,EAAE,SAAS,CAAC;CAChB,CA2bA;AA0BD;;iFAEiF;AACjF,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,aAAa,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACvD,WAAW,IAAI,IAAI,CAAC;IACpB,gBAAgB,IAAI,sBAAsB,GAAG,IAAI,CAAC;IAClD,iBAAiB,IAAI,UAAU,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,oBAAoB,GAAG,gBAAgB,CAqGpF"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { err, RhiError, ok } from '@forgeax/engine-rhi';
|
|
1
|
+
import { err, RhiError, ok, R32FLOAT_PROBE_STAGES, validateR32FloatReceipt, createUnavailableR32FloatReceipt } from '@forgeax/engine-rhi';
|
|
2
2
|
export { RhiError as RhiErrorClass, err, ok } from '@forgeax/engine-rhi';
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
@@ -108,6 +108,131 @@ function queueWriteBufferOutOfBounds(args) {
|
|
|
108
108
|
})
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
|
+
var TEXTURE_BINDING = 4;
|
|
112
|
+
var STORAGE_BINDING = 8;
|
|
113
|
+
var COPY_SRC = 1;
|
|
114
|
+
var COPY_DST = 2;
|
|
115
|
+
var MAP_READ = 1;
|
|
116
|
+
var BUFFER_COPY_DST = 8;
|
|
117
|
+
async function probeR32FloatCapability(rawDevice, deviceGeneration) {
|
|
118
|
+
let stage = "texture-create";
|
|
119
|
+
let texture;
|
|
120
|
+
let readback;
|
|
121
|
+
rawDevice.pushErrorScope("validation");
|
|
122
|
+
try {
|
|
123
|
+
texture = rawDevice.createTexture({
|
|
124
|
+
size: { width: 2, height: 2, depthOrArrayLayers: 1 },
|
|
125
|
+
format: "r32float",
|
|
126
|
+
mipLevelCount: 2,
|
|
127
|
+
usage: TEXTURE_BINDING | STORAGE_BINDING | COPY_SRC | COPY_DST
|
|
128
|
+
});
|
|
129
|
+
stage = "mip-view";
|
|
130
|
+
const sourceView = texture.createView({ baseMipLevel: 0, mipLevelCount: 1 });
|
|
131
|
+
const destinationView = texture.createView({ baseMipLevel: 1, mipLevelCount: 1 });
|
|
132
|
+
stage = "sampled-storage-bind-group";
|
|
133
|
+
const layout = rawDevice.createBindGroupLayout({
|
|
134
|
+
entries: [
|
|
135
|
+
{
|
|
136
|
+
binding: 0,
|
|
137
|
+
visibility: 4,
|
|
138
|
+
texture: { sampleType: "unfilterable-float", viewDimension: "2d" }
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
binding: 1,
|
|
142
|
+
visibility: 4,
|
|
143
|
+
storageTexture: { access: "write-only", format: "r32float", viewDimension: "2d" }
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
});
|
|
147
|
+
const bindGroup = rawDevice.createBindGroup({
|
|
148
|
+
layout,
|
|
149
|
+
entries: [
|
|
150
|
+
{ binding: 0, resource: sourceView },
|
|
151
|
+
{ binding: 1, resource: destinationView }
|
|
152
|
+
]
|
|
153
|
+
});
|
|
154
|
+
stage = "pipeline-bind";
|
|
155
|
+
const shader = rawDevice.createShaderModule({
|
|
156
|
+
code: `
|
|
157
|
+
@group(0) @binding(0) var source: texture_2d<f32>;
|
|
158
|
+
@group(0) @binding(1) var destination: texture_storage_2d<r32float, write>;
|
|
159
|
+
@compute @workgroup_size(1) fn main() {
|
|
160
|
+
let value = textureLoad(source, vec2i(0, 0), 0).r;
|
|
161
|
+
textureStore(destination, vec2i(0, 0), vec4f(value));
|
|
162
|
+
}
|
|
163
|
+
`
|
|
164
|
+
});
|
|
165
|
+
const pipeline = rawDevice.createComputePipeline({
|
|
166
|
+
layout: rawDevice.createPipelineLayout({ bindGroupLayouts: [layout] }),
|
|
167
|
+
compute: { module: shader, entryPoint: "main" }
|
|
168
|
+
});
|
|
169
|
+
const encoder = rawDevice.createCommandEncoder({ label: "r32float-profile" });
|
|
170
|
+
const pass = encoder.beginComputePass();
|
|
171
|
+
pass.setPipeline(pipeline);
|
|
172
|
+
pass.setBindGroup(0, bindGroup);
|
|
173
|
+
pass.dispatchWorkgroups(1);
|
|
174
|
+
pass.end();
|
|
175
|
+
stage = "finish";
|
|
176
|
+
readback = rawDevice.createBuffer({ size: 256, usage: MAP_READ | BUFFER_COPY_DST });
|
|
177
|
+
encoder.copyTextureToBuffer(
|
|
178
|
+
{ texture, mipLevel: 1 },
|
|
179
|
+
{ buffer: readback, bytesPerRow: 256, rowsPerImage: 1 },
|
|
180
|
+
{ width: 1, height: 1, depthOrArrayLayers: 1 }
|
|
181
|
+
);
|
|
182
|
+
const commandBuffer = encoder.finish();
|
|
183
|
+
stage = "submit";
|
|
184
|
+
rawDevice.queue.submit([commandBuffer]);
|
|
185
|
+
stage = "completion";
|
|
186
|
+
await rawDevice.queue.onSubmittedWorkDone();
|
|
187
|
+
const validationError = await rawDevice.popErrorScope();
|
|
188
|
+
if (validationError !== null) {
|
|
189
|
+
throw new Error(`WebGPU validation: ${validationError.message}`);
|
|
190
|
+
}
|
|
191
|
+
stage = "readback";
|
|
192
|
+
await readback.mapAsync(MAP_READ);
|
|
193
|
+
const values = Array.from(new Float32Array(readback.getMappedRange(0, 4).slice(0)));
|
|
194
|
+
readback.unmap();
|
|
195
|
+
const receipt = {
|
|
196
|
+
profile: "r32float-mip-sampled-storage",
|
|
197
|
+
verdict: "admitted",
|
|
198
|
+
evidence: "real",
|
|
199
|
+
deviceGeneration,
|
|
200
|
+
stages: R32FLOAT_PROBE_STAGES.map((entry) => ({
|
|
201
|
+
stage: entry,
|
|
202
|
+
verdict: "admitted",
|
|
203
|
+
evidence: "real"
|
|
204
|
+
})),
|
|
205
|
+
sampleType: "unfilterable-float",
|
|
206
|
+
usages: ["texture-binding", "storage-binding", "copy-src"],
|
|
207
|
+
readback: { byteLength: 4, values },
|
|
208
|
+
probeExecutions: 1
|
|
209
|
+
};
|
|
210
|
+
const validated = validateR32FloatReceipt(receipt);
|
|
211
|
+
return validated.ok ? ok(validated.value) : validated;
|
|
212
|
+
} catch (cause) {
|
|
213
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
214
|
+
createUnavailableR32FloatReceipt({
|
|
215
|
+
deviceGeneration,
|
|
216
|
+
failedStage: stage,
|
|
217
|
+
detail: message
|
|
218
|
+
});
|
|
219
|
+
return err(
|
|
220
|
+
new RhiError({
|
|
221
|
+
code: "rhi-texture-format-capability-unavailable",
|
|
222
|
+
expected: `r32float profile stage ${stage} to complete on the live WebGPU device`,
|
|
223
|
+
hint: "retain fallback-only rendering and inspect the device validation error",
|
|
224
|
+
detail: {
|
|
225
|
+
stage,
|
|
226
|
+
deviceGeneration,
|
|
227
|
+
reason: message
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
} finally {
|
|
232
|
+
texture?.destroy();
|
|
233
|
+
readback?.destroy();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
111
236
|
function resolveTimestampQueries(args) {
|
|
112
237
|
try {
|
|
113
238
|
args.rawEncoder.resolveQuerySet(
|
|
@@ -183,6 +308,9 @@ var CP_KEYS = ["label", "layout", "compute"];
|
|
|
183
308
|
var QS_KEYS = ["label", "type", "count"];
|
|
184
309
|
var QUERY_SET_COUNT_LIMIT = 4096;
|
|
185
310
|
var RAW_DEVICE_MAP = /* @__PURE__ */ new WeakMap();
|
|
311
|
+
var RAW_DEVICE_GENERATION_MAP = /* @__PURE__ */ new WeakMap();
|
|
312
|
+
var R32FLOAT_PROBE_CACHE = /* @__PURE__ */ new WeakMap();
|
|
313
|
+
var NEXT_DEVICE_GENERATION = 1;
|
|
186
314
|
var BUFFER_RAW_MAP = /* @__PURE__ */ new WeakMap();
|
|
187
315
|
var TEXTURE_VIEW_RAW_MAP = /* @__PURE__ */ new WeakMap();
|
|
188
316
|
var ENCODER_STATE = /* @__PURE__ */ new WeakMap();
|
|
@@ -1030,10 +1158,22 @@ function makeRhiDevice(rawDevice) {
|
|
|
1030
1158
|
const features = rawDevice.features;
|
|
1031
1159
|
const limits = rawDevice.limits;
|
|
1032
1160
|
const queue = makeQueue(rawDevice.queue);
|
|
1161
|
+
const deviceGeneration = RAW_DEVICE_GENERATION_MAP.get(rawDevice) ?? (() => {
|
|
1162
|
+
const generation = NEXT_DEVICE_GENERATION++;
|
|
1163
|
+
RAW_DEVICE_GENERATION_MAP.set(rawDevice, generation);
|
|
1164
|
+
return generation;
|
|
1165
|
+
})();
|
|
1033
1166
|
const device = {
|
|
1034
1167
|
caps,
|
|
1035
1168
|
features,
|
|
1036
1169
|
limits,
|
|
1170
|
+
probeTextureFormatCapability() {
|
|
1171
|
+
const cached = R32FLOAT_PROBE_CACHE.get(device);
|
|
1172
|
+
if (cached !== void 0) return cached;
|
|
1173
|
+
const probe = probeR32FloatCapability(rawDevice, deviceGeneration);
|
|
1174
|
+
R32FLOAT_PROBE_CACHE.set(device, probe);
|
|
1175
|
+
return probe;
|
|
1176
|
+
},
|
|
1037
1177
|
queue,
|
|
1038
1178
|
lost: rawDevice.lost,
|
|
1039
1179
|
createBuffer(desc) {
|