@forgeax/engine-rhi-webgpu 0.1.20 → 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,12 +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
- When `device.caps.timestampQuery` is true, `RhiCommandEncoder.writeTimestamp`
55
- must find and successfully call the raw `GPUCommandEncoder.writeTimestamp`
56
- with the mapped query set and index. A missing or throwing raw method throws a
57
- structured `RhiError` with code `webgpu-runtime-error`; the shim never treats
58
- that write as a no-op or synthesizes timestamp ticks. Capability-disabled
59
- devices retain the existing `feature-not-enabled` diagnostic path.
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.
61
+ When `device.caps.timestampQuery` is true, render and compute pass descriptors
62
+ carry the mapped query set and timestamp indices. The WebGPU adapter does not
63
+ expose the removed command-encoder timestamp entry; callers that need an
64
+ encoder-position marker use an empty timestamp-enabled compute pass on the same
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.
60
68
 
61
69
  ## Query forwarding contract
62
70
 
@@ -68,7 +76,7 @@ The shim forwards the query surface from [`@forgeax/engine-rhi`](../rhi):
68
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) |
69
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) |
70
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) |
71
- | 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) |
72
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) |
73
81
 
74
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":"AA6BA,OAAO,KAAK,EAuBV,gBAAgB,EAIhB,SAAS,EAaV,MAAM,qBAAqB,CAAC;AA8L7B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAE/E;AAsrCD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,SAAS,GAAG;IACnD,MAAM,EAAE,SAAS,CAAC;IAClB;6CACyC;IACzC,GAAG,EAAE,SAAS,CAAC;CAChB,CAicA;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(
@@ -129,26 +254,6 @@ function resolveTimestampQueries(args) {
129
254
  );
130
255
  }
131
256
  }
132
- function writeTimestamp(args) {
133
- const encoderWithTimestamp = args.rawEncoder;
134
- if (typeof encoderWithTimestamp.writeTimestamp !== "function") {
135
- throw new RhiError({
136
- code: "webgpu-runtime-error",
137
- expected: "underlying GPUCommandEncoder.writeTimestamp to be callable",
138
- hint: "timestamp-query is advertised but the raw encoder has no writeTimestamp method"
139
- });
140
- }
141
- try {
142
- encoderWithTimestamp.writeTimestamp(args.rawQuerySet, args.queryIndex);
143
- } catch (error) {
144
- const message = error instanceof Error ? error.message : String(error);
145
- throw new RhiError({
146
- code: "webgpu-runtime-error",
147
- expected: "underlying GPUCommandEncoder.writeTimestamp to succeed",
148
- hint: `writeTimestamp raised: ${message}`
149
- });
150
- }
151
- }
152
257
 
153
258
  // src/device.ts
154
259
  function mirror(src, keys) {
@@ -203,6 +308,9 @@ var CP_KEYS = ["label", "layout", "compute"];
203
308
  var QS_KEYS = ["label", "type", "count"];
204
309
  var QUERY_SET_COUNT_LIMIT = 4096;
205
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;
206
314
  var BUFFER_RAW_MAP = /* @__PURE__ */ new WeakMap();
207
315
  var TEXTURE_VIEW_RAW_MAP = /* @__PURE__ */ new WeakMap();
208
316
  var ENCODER_STATE = /* @__PURE__ */ new WeakMap();
@@ -578,7 +686,7 @@ function mirrorRenderPipelineDescriptor(desc) {
578
686
  ...fragment === void 0 ? {} : { fragment }
579
687
  };
580
688
  }
581
- function makeCommandEncoder(rawEncoder, caps, fireFeatureNotEnabled) {
689
+ function makeCommandEncoder(rawEncoder) {
582
690
  function mirrorComputePassDescriptor(desc) {
583
691
  if (desc === void 0) return void 0;
584
692
  const out = mirror(desc, ["label"]);
@@ -631,6 +739,12 @@ function makeCommandEncoder(rawEncoder, caps, fireFeatureNotEnabled) {
631
739
  };
632
740
  return pass;
633
741
  },
742
+ encodeEmptyComputePass(desc) {
743
+ const state = ENCODER_STATE.get(enc);
744
+ throwIfFinished(state);
745
+ const rawPass = rawEncoder.beginComputePass(mirrorComputePassDescriptor(desc));
746
+ rawPass.end();
747
+ },
634
748
  copyBufferToBuffer(source, arg2, arg3, arg4, arg5) {
635
749
  const state = ENCODER_STATE.get(enc);
636
750
  throwIfFinished(state);
@@ -657,11 +771,19 @@ function makeCommandEncoder(rawEncoder, caps, fireFeatureNotEnabled) {
657
771
  copyTextureToBuffer(source, destination, copySize) {
658
772
  const state = ENCODER_STATE.get(enc);
659
773
  throwIfFinished(state);
774
+ const rawSource = {
775
+ texture: source.texture
776
+ };
777
+ if (source.mipLevel !== void 0) rawSource.mipLevel = source.mipLevel;
778
+ if (source.origin !== void 0) rawSource.origin = source.origin;
779
+ if (source.aspect !== void 0) rawSource.aspect = source.aspect;
660
780
  const rawDst = {
661
- ...destination,
662
781
  buffer: BUFFER_RAW_MAP.get(destination.buffer) ?? destination.buffer
663
782
  };
664
- rawEncoder.copyTextureToBuffer(source, rawDst, copySize);
783
+ if (destination.offset !== void 0) rawDst.offset = destination.offset;
784
+ if (destination.bytesPerRow !== void 0) rawDst.bytesPerRow = destination.bytesPerRow;
785
+ if (destination.rowsPerImage !== void 0) rawDst.rowsPerImage = destination.rowsPerImage;
786
+ rawEncoder.copyTextureToBuffer(rawSource, rawDst, copySize);
665
787
  },
666
788
  copyTextureToTexture(source, destination, copySize) {
667
789
  const state = ENCODER_STATE.get(enc);
@@ -741,19 +863,6 @@ function makeCommandEncoder(rawEncoder, caps, fireFeatureNotEnabled) {
741
863
  insertDebugMarker(markerLabel) {
742
864
  rawEncoder.insertDebugMarker(markerLabel);
743
865
  },
744
- writeTimestamp(querySet, queryIndex) {
745
- const state = ENCODER_STATE.get(enc);
746
- throwIfFinished(state);
747
- if (caps.timestampQuery !== true) {
748
- fireFeatureNotEnabled(
749
- "timestamp-query",
750
- "check device.caps.timestampQuery before calling writeTimestamp"
751
- );
752
- return;
753
- }
754
- const rawQs = QUERY_SET_RAW_MAP.get(querySet) ?? querySet;
755
- writeTimestamp({ rawEncoder, rawQuerySet: rawQs, queryIndex });
756
- },
757
866
  finish() {
758
867
  const state = ENCODER_STATE.get(enc);
759
868
  if (state === void 0) {
@@ -1049,10 +1158,22 @@ function makeRhiDevice(rawDevice) {
1049
1158
  const features = rawDevice.features;
1050
1159
  const limits = rawDevice.limits;
1051
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
+ })();
1052
1166
  const device = {
1053
1167
  caps,
1054
1168
  features,
1055
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
+ },
1056
1177
  queue,
1057
1178
  lost: rawDevice.lost,
1058
1179
  createBuffer(desc) {
@@ -1383,12 +1504,7 @@ function makeRhiDevice(rawDevice) {
1383
1504
  const rawEnc = desc === void 0 ? rawDevice.createCommandEncoder() : rawDevice.createCommandEncoder(
1384
1505
  mirror(desc, ENC_KEYS)
1385
1506
  );
1386
- const fireFeatureNotEnabled = (featureName, hint) => {
1387
- console.error(
1388
- `[RhiError feature-not-enabled] expected: device.features.has('${featureName}') === true; hint: ${hint}`
1389
- );
1390
- };
1391
- return ok(makeCommandEncoder(rawEnc, caps, fireFeatureNotEnabled));
1507
+ return ok(makeCommandEncoder(rawEnc));
1392
1508
  }
1393
1509
  // fix-f3: synchronous createShaderModule placeholder removed; the
1394
1510
  // shader-compile-failed path lives in the top-level async factory