@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 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
- shim never treats it as a no-op or synthesizes timestamp ticks. Capability-
60
- disabled devices retain the existing refusal path.
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
- | Command-encoder `writeTimestamp` | Capability-positive devices require a callable raw method; missing/throwing raw methods surface `webgpu-runtime-error` | [`device.ts`](./src/device.ts#L1048), [unit coverage](./src/__tests__/rhi-webgpu.unit.test.ts#L394) |
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=r32float-capability-generation.integration.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=r32float-format-capability.browser.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=r32float-format-capability.dawn.test.d.ts.map
@@ -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":""}
@@ -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;AA8L7B;;;;;;;;;;;;;;;;;;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,CA2aA;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"}
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) {