@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/src/device.ts CHANGED
@@ -14,7 +14,8 @@
14
14
  // exposes the spec Promise without a second cache.
15
15
  //
16
16
  // w3 / w5 / w6 (feat-20260508-rhi-surface-completion, co-commit):
17
- // - createCommandEncoder + 12 RhiCommandEncoder methods + 3 mixin (w3)
17
+ // - createCommandEncoder + 12 RhiCommandEncoder spec methods plus the
18
+ // ForgeaX encodeEmptyComputePass compound operation + 3 mixin (w3)
18
19
  // - 17 RhiRenderPassEncoder spec stable methods + 1 setBindGroup overload +
19
20
  // 3 placeholders (executeBundles / beginOcclusionQuery / endOcclusionQuery) (w5)
20
21
  // - Queue.submit / writeBuffer real implementation + bounds validation (w6)
@@ -33,6 +34,7 @@ import type {
33
34
  BindGroupLayout,
34
35
  BindGroupLayoutDescriptor,
35
36
  Buffer,
37
+ BufferCopyDestination,
36
38
  BufferDescriptor,
37
39
  CanvasConfiguration,
38
40
  CommandBuffer,
@@ -63,6 +65,7 @@ import type {
63
65
  Sampler,
64
66
  SamplerDescriptor,
65
67
  Texture,
68
+ TextureCopySource,
66
69
  TextureDescriptor,
67
70
  TextureView,
68
71
  TextureViewDescriptor,
@@ -75,7 +78,8 @@ import {
75
78
  queueWriteBufferOutOfBounds,
76
79
  renderPassNotEnded,
77
80
  } from './errors';
78
- import { resolveTimestampQueries, writeTimestamp } from './internal/timestamp-query';
81
+ import { probeR32FloatCapability } from './internal/r32float-capability';
82
+ import { resolveTimestampQueries } from './internal/timestamp-query';
79
83
 
80
84
  /**
81
85
  * Mirror forgeax `?: T | undefined` descriptor onto the spec GPUXxxDescriptor
@@ -167,6 +171,12 @@ const QUERY_SET_COUNT_LIMIT = 4096;
167
171
  * forward recording calls to the underlying GPUCommandEncoder.
168
172
  */
169
173
  const RAW_DEVICE_MAP: WeakMap<RhiDevice, GPUDevice> = new WeakMap();
174
+ const RAW_DEVICE_GENERATION_MAP: WeakMap<GPUDevice, number> = new WeakMap();
175
+ const R32FLOAT_PROBE_CACHE: WeakMap<
176
+ RhiDevice,
177
+ Promise<Result<import('@forgeax/engine-rhi').RhiTextureFormatCapabilityReceipt, RhiError>>
178
+ > = new WeakMap();
179
+ let NEXT_DEVICE_GENERATION = 1;
170
180
  const BUFFER_RAW_MAP: WeakMap<Buffer, GPUBuffer> = new WeakMap();
171
181
  const TEXTURE_VIEW_RAW_MAP: WeakMap<TextureView, GPUTextureView> = new WeakMap();
172
182
  const ENCODER_STATE: WeakMap<RhiCommandEncoder, EncoderState> = new WeakMap();
@@ -812,11 +822,7 @@ function mirrorRenderPipelineDescriptor(
812
822
  * - render-pass-not-ended is detected by tracking activePass; finish()
813
823
  * while a pass has not been end()-ed returns the structured error.
814
824
  */
815
- function makeCommandEncoder(
816
- rawEncoder: GPUCommandEncoder,
817
- caps: { readonly timestampQuery: boolean },
818
- fireFeatureNotEnabled: (featureName: string, hint: string) => void,
819
- ): RhiCommandEncoder {
825
+ function makeCommandEncoder(rawEncoder: GPUCommandEncoder): RhiCommandEncoder {
820
826
  function mirrorComputePassDescriptor(
821
827
  desc: ComputePassDescriptor | undefined,
822
828
  ): GPUComputePassDescriptor | undefined {
@@ -890,6 +896,12 @@ function makeCommandEncoder(
890
896
  };
891
897
  return pass;
892
898
  },
899
+ encodeEmptyComputePass(desc: ComputePassDescriptor): void {
900
+ const state = ENCODER_STATE.get(enc);
901
+ throwIfFinished(state);
902
+ const rawPass = rawEncoder.beginComputePass(mirrorComputePassDescriptor(desc));
903
+ rawPass.end();
904
+ },
893
905
  copyBufferToBuffer(
894
906
  source: Buffer,
895
907
  arg2: number | Buffer,
@@ -933,19 +945,27 @@ function makeCommandEncoder(
933
945
  rawEncoder.copyBufferToTexture(rawSrc, destination, copySize);
934
946
  },
935
947
  copyTextureToBuffer(
936
- source: GPUTexelCopyTextureInfo,
937
- destination: GPUTexelCopyBufferInfo,
948
+ source: GPUTexelCopyTextureInfo | TextureCopySource,
949
+ destination: GPUTexelCopyBufferInfo | BufferCopyDestination,
938
950
  copySize: GPUExtent3DStrict,
939
951
  ): void {
940
952
  const state = ENCODER_STATE.get(enc);
941
953
  throwIfFinished(state);
942
- const rawDst = {
943
- ...destination,
954
+ const rawSource: GPUTexelCopyTextureInfo = {
955
+ texture: source.texture as unknown as GPUTexture,
956
+ };
957
+ if (source.mipLevel !== undefined) rawSource.mipLevel = source.mipLevel;
958
+ if (source.origin !== undefined) rawSource.origin = source.origin;
959
+ if (source.aspect !== undefined) rawSource.aspect = source.aspect;
960
+ const rawDst: GPUTexelCopyBufferInfo = {
944
961
  buffer:
945
962
  BUFFER_RAW_MAP.get(destination.buffer as unknown as Buffer) ??
946
963
  (destination.buffer as unknown as GPUBuffer),
947
964
  };
948
- rawEncoder.copyTextureToBuffer(source, rawDst, copySize);
965
+ if (destination.offset !== undefined) rawDst.offset = destination.offset;
966
+ if (destination.bytesPerRow !== undefined) rawDst.bytesPerRow = destination.bytesPerRow;
967
+ if (destination.rowsPerImage !== undefined) rawDst.rowsPerImage = destination.rowsPerImage;
968
+ rawEncoder.copyTextureToBuffer(rawSource, rawDst, copySize);
949
969
  },
950
970
  copyTextureToTexture(
951
971
  source: GPUTexelCopyTextureInfo,
@@ -1045,25 +1065,6 @@ function makeCommandEncoder(
1045
1065
  insertDebugMarker(markerLabel: string): void {
1046
1066
  rawEncoder.insertDebugMarker(markerLabel);
1047
1067
  },
1048
- writeTimestamp(querySet: QuerySet, queryIndex: number): void {
1049
- // M5 / K-3 (research §2.4): timestamp-query feature gate. spec
1050
- // writeTimestamp returns void; the forgeax form keeps the void shape
1051
- // and fans out 'feature-not-enabled' through the engine onError
1052
- // channel rather than wrapping in Result. When the capability is true,
1053
- // a missing or throwing raw write is a structured runtime failure so a
1054
- // Render capture cannot publish a fabricated interval.
1055
- const state = ENCODER_STATE.get(enc);
1056
- throwIfFinished(state);
1057
- if (caps.timestampQuery !== true) {
1058
- fireFeatureNotEnabled(
1059
- 'timestamp-query',
1060
- 'check device.caps.timestampQuery before calling writeTimestamp',
1061
- );
1062
- return;
1063
- }
1064
- const rawQs = QUERY_SET_RAW_MAP.get(querySet) ?? (querySet as unknown as GPUQuerySet);
1065
- writeTimestamp({ rawEncoder, rawQuerySet: rawQs, queryIndex });
1066
- },
1067
1068
  finish(): Result<CommandBuffer, RhiError> {
1068
1069
  const state = ENCODER_STATE.get(enc);
1069
1070
  if (state === undefined) {
@@ -1502,11 +1503,27 @@ export function makeRhiDevice(rawDevice: GPUDevice): {
1502
1503
  const limits = rawDevice.limits as RhiLimits;
1503
1504
 
1504
1505
  const queue: RhiQueue = makeQueue(rawDevice.queue);
1506
+ const deviceGeneration =
1507
+ RAW_DEVICE_GENERATION_MAP.get(rawDevice) ??
1508
+ (() => {
1509
+ const generation = NEXT_DEVICE_GENERATION++;
1510
+ RAW_DEVICE_GENERATION_MAP.set(rawDevice, generation);
1511
+ return generation;
1512
+ })();
1505
1513
 
1506
1514
  const device: RhiDevice = {
1507
1515
  caps,
1508
1516
  features,
1509
1517
  limits,
1518
+ probeTextureFormatCapability(): Promise<
1519
+ Result<import('@forgeax/engine-rhi').RhiTextureFormatCapabilityReceipt, RhiError>
1520
+ > {
1521
+ const cached = R32FLOAT_PROBE_CACHE.get(device);
1522
+ if (cached !== undefined) return cached;
1523
+ const probe = probeR32FloatCapability(rawDevice, deviceGeneration);
1524
+ R32FLOAT_PROBE_CACHE.set(device, probe);
1525
+ return probe;
1526
+ },
1510
1527
  queue,
1511
1528
  lost: rawDevice.lost as unknown as Promise<{
1512
1529
  readonly reason: 'destroyed' | 'unknown';
@@ -1915,29 +1932,7 @@ export function makeRhiDevice(rawDevice: GPUDevice): {
1915
1932
  : rawDevice.createCommandEncoder(
1916
1933
  mirror(desc, ENC_KEYS) as unknown as GPUCommandEncoderDescriptor,
1917
1934
  );
1918
- // M5 / w39: pass caps.timestampQuery + an onError-style fan-out so
1919
- // writeTimestamp can fire 'feature-not-enabled' through the engine
1920
- // channel when caps.timestampQuery is false (K-3: spec writeTimestamp
1921
- // returns void; the forgeax form keeps that shape).
1922
- //
1923
- // Round 3 fix-up F-P3-3: the forgeax RhiDevice
1924
- // does not expose `onError` directly (charter proposition 5: keep
1925
- // RHI math-free + listener-free), so the shim writes a structured
1926
- // diagnostic to `console.error` matching the RhiError shape. The
1927
- // engine layer subscribes through `Renderer.onError` and fans the
1928
- // same RhiError out; this keeps the unsupported capability observable
1929
- // for pure-RHI consumers (mock unit tests, dawn-real-gpu probes) that
1930
- // never instantiate a Renderer.
1931
- const fireFeatureNotEnabled = (featureName: string, hint: string): void => {
1932
- // Diagnostic channel (a) of the K-9 double-channel pattern: default
1933
- // console.error so AI consumers running headless / mock paths still
1934
- // observe the unsupported capability without subscribing to a
1935
- // listener. The capability-disabled entry remains non-throwing.
1936
- console.error(
1937
- `[RhiError feature-not-enabled] expected: device.features.has('${featureName}') === true; hint: ${hint}`,
1938
- );
1939
- };
1940
- return ok(makeCommandEncoder(rawEnc, caps, fireFeatureNotEnabled));
1935
+ return ok(makeCommandEncoder(rawEnc));
1941
1936
  },
1942
1937
  // fix-f3: synchronous createShaderModule placeholder removed; the
1943
1938
  // shader-compile-failed path lives in the top-level async factory
@@ -1,88 +1,120 @@
1
- import { RhiError } from '@forgeax/engine-rhi';
2
1
  import { describe, expect, it } from 'vitest';
3
2
  import { createMockGpu } from '../../__tests__/__mocks__/gpu-device';
4
3
  import { makeRhiDevice } from '../../device';
5
4
 
6
- async function timestampEncoder(writeTimestamp?: (querySet: unknown, queryIndex: number) => void) {
7
- const gpu = createMockGpu();
8
- const adapter = await gpu.requestAdapter();
9
- if (adapter === null) throw new Error('mock adapter should exist');
10
- const raw = await adapter.requestDevice();
11
- const features = raw.features as unknown as Set<GPUFeatureName>;
12
- features.add('timestamp-query');
13
- const originalCreateCommandEncoder = raw.createCommandEncoder.bind(raw);
14
- raw.createCommandEncoder = (descriptor) => {
15
- const encoder = originalCreateCommandEncoder(descriptor) as unknown as Record<string, unknown>;
16
- if (writeTimestamp !== undefined) encoder.writeTimestamp = writeTimestamp;
17
- return encoder as unknown as ReturnType<typeof raw.createCommandEncoder>;
18
- };
19
- const { device } = makeRhiDevice(raw as unknown as GPUDevice);
20
- const querySet = device.createQuerySet({ type: 'timestamp', count: 2 });
21
- if (!querySet.ok) throw new Error('timestamp query set should be created');
22
- const encoder = device.createCommandEncoder();
23
- if (!encoder.ok) throw new Error('command encoder should be created');
24
- return { encoder: encoder.value, querySet: querySet.value };
25
- }
26
-
27
- describe('timestamp query raw write seam', () => {
28
- it('forwards a callable raw writeTimestamp exactly once', async () => {
29
- const calls: Array<{ querySet: unknown; queryIndex: number }> = [];
30
- const { encoder, querySet } = await timestampEncoder((rawQuerySet, queryIndex) => {
31
- calls.push({ querySet: rawQuerySet, queryIndex });
5
+ describe('timestamp query pass descriptor forwarding', () => {
6
+ it('maps an opaque QuerySet in a compute-pass timestampWrites descriptor', async () => {
7
+ const gpu = createMockGpu();
8
+ const adapter = await gpu.requestAdapter();
9
+ if (adapter === null) throw new Error('mock adapter should exist');
10
+ const raw = await adapter.requestDevice();
11
+ (raw.features as unknown as Set<GPUFeatureName>).add('timestamp-query');
12
+ let rawQuerySet: unknown;
13
+ const originalCreateQuerySet = raw.createQuerySet.bind(raw);
14
+ raw.createQuerySet = (descriptor) => {
15
+ const result = originalCreateQuerySet(descriptor);
16
+ rawQuerySet = result;
17
+ return result;
18
+ };
19
+ let captured: GPUComputePassDescriptor | undefined;
20
+ const originalCreateCommandEncoder = raw.createCommandEncoder.bind(raw);
21
+ raw.createCommandEncoder = (descriptor) => {
22
+ const encoder = originalCreateCommandEncoder(descriptor) as unknown as Record<
23
+ string,
24
+ unknown
25
+ >;
26
+ const originalBegin = encoder.beginComputePass as (
27
+ passDescriptor?: GPUComputePassDescriptor,
28
+ ) => unknown;
29
+ encoder.beginComputePass = (passDescriptor?: GPUComputePassDescriptor) => {
30
+ captured = passDescriptor;
31
+ return originalBegin.call(encoder, passDescriptor);
32
+ };
33
+ return encoder as unknown as ReturnType<typeof raw.createCommandEncoder>;
34
+ };
35
+ const { device } = makeRhiDevice(raw as unknown as GPUDevice);
36
+ const querySet = device.createQuerySet({ type: 'timestamp', count: 2 });
37
+ expect(querySet.ok).toBe(true);
38
+ if (!querySet.ok) return;
39
+ const encoder = device.createCommandEncoder();
40
+ expect(encoder.ok).toBe(true);
41
+ if (!encoder.ok) return;
42
+ const pass = encoder.value.beginComputePass({
43
+ label: 'timestamp-pass',
44
+ timestampWrites: {
45
+ querySet: querySet.value,
46
+ beginningOfPassWriteIndex: 0,
47
+ endOfPassWriteIndex: 1,
48
+ },
32
49
  });
33
-
34
- encoder.writeTimestamp(querySet, 1);
35
-
36
- expect(calls).toHaveLength(1);
37
- expect(calls[0]?.querySet).toBeDefined();
38
- expect(calls[0]?.queryIndex).toBe(1);
39
- });
40
-
41
- it('returns a structured refusal when capability-positive raw writeTimestamp is missing', async () => {
42
- const { encoder, querySet } = await timestampEncoder();
43
-
44
- expect(() => encoder.writeTimestamp(querySet, 0)).toThrow(RhiError);
45
- try {
46
- encoder.writeTimestamp(querySet, 0);
47
- } catch (error) {
48
- expect(error).toMatchObject({
49
- code: 'webgpu-runtime-error',
50
- expected: 'underlying GPUCommandEncoder.writeTimestamp to be callable',
51
- });
52
- expect((error as RhiError).hint).toContain('timestamp-query');
53
- }
54
- });
55
-
56
- it('returns a structured refusal when raw writeTimestamp throws', async () => {
57
- const { encoder, querySet } = await timestampEncoder(() => {
58
- throw new Error('raw timestamp failure');
50
+ pass.end();
51
+ expect(captured).toMatchObject({
52
+ label: 'timestamp-pass',
53
+ timestampWrites: {
54
+ beginningOfPassWriteIndex: 0,
55
+ endOfPassWriteIndex: 1,
56
+ },
59
57
  });
60
-
61
- expect(() => encoder.writeTimestamp(querySet, 0)).toThrow(RhiError);
62
- try {
63
- encoder.writeTimestamp(querySet, 0);
64
- } catch (error) {
65
- expect(error).toMatchObject({
66
- code: 'webgpu-runtime-error',
67
- expected: 'underlying GPUCommandEncoder.writeTimestamp to succeed',
68
- });
69
- expect((error as RhiError).hint).toContain('raw timestamp failure');
70
- }
58
+ expect(rawQuerySet).toBeDefined();
59
+ expect(captured?.timestampWrites?.querySet).toBe(rawQuerySet);
71
60
  });
72
61
 
73
- it('refuses timestamp writes after encoder.finish with a lifecycle error', async () => {
74
- const { encoder, querySet } = await timestampEncoder(() => {});
75
- const finish = encoder.finish();
76
- expect(finish.ok).toBe(true);
77
-
78
- expect(() => encoder.writeTimestamp(querySet, 0)).toThrow(RhiError);
79
- try {
80
- encoder.writeTimestamp(querySet, 0);
81
- } catch (error) {
82
- expect(error).toMatchObject({
83
- code: 'command-encoder-finished',
84
- expected: 'command encoder must not be finished before recording new commands',
85
- });
86
- }
62
+ it('maps an opaque QuerySet in a render-pass timestampWrites descriptor', async () => {
63
+ const gpu = createMockGpu();
64
+ const adapter = await gpu.requestAdapter();
65
+ if (adapter === null) throw new Error('mock adapter should exist');
66
+ const raw = await adapter.requestDevice();
67
+ (raw.features as unknown as Set<GPUFeatureName>).add('timestamp-query');
68
+ let captured: GPURenderPassDescriptor | undefined;
69
+ const originalCreateCommandEncoder = raw.createCommandEncoder.bind(raw);
70
+ raw.createCommandEncoder = (descriptor) => {
71
+ const encoder = originalCreateCommandEncoder(descriptor) as unknown as Record<
72
+ string,
73
+ unknown
74
+ >;
75
+ const originalBegin = encoder.beginRenderPass as (
76
+ passDescriptor: GPURenderPassDescriptor,
77
+ ) => unknown;
78
+ encoder.beginRenderPass = (passDescriptor: GPURenderPassDescriptor) => {
79
+ captured = passDescriptor;
80
+ return originalBegin.call(encoder, passDescriptor);
81
+ };
82
+ return encoder as unknown as ReturnType<typeof raw.createCommandEncoder>;
83
+ };
84
+ const { device } = makeRhiDevice(raw as unknown as GPUDevice);
85
+ const querySet = device.createQuerySet({ type: 'timestamp', count: 2 });
86
+ expect(querySet.ok).toBe(true);
87
+ if (!querySet.ok) return;
88
+ const texture = device.createTexture({
89
+ size: [1, 1, 1],
90
+ format: 'rgba8unorm',
91
+ usage: 0x10,
92
+ });
93
+ expect(texture.ok).toBe(true);
94
+ if (!texture.ok) return;
95
+ const view = device.createTextureView(texture.value, {});
96
+ expect(view.ok).toBe(true);
97
+ if (!view.ok) return;
98
+ const encoder = device.createCommandEncoder();
99
+ expect(encoder.ok).toBe(true);
100
+ if (!encoder.ok) return;
101
+ const pass = encoder.value.beginRenderPass({
102
+ label: 'timestamp-raster',
103
+ colorAttachments: [{ view: view.value, loadOp: 'clear', storeOp: 'store' }],
104
+ timestampWrites: {
105
+ querySet: querySet.value,
106
+ beginningOfPassWriteIndex: 0,
107
+ endOfPassWriteIndex: 1,
108
+ },
109
+ });
110
+ pass.end();
111
+ expect(captured).toMatchObject({
112
+ label: 'timestamp-raster',
113
+ timestampWrites: {
114
+ beginningOfPassWriteIndex: 0,
115
+ endOfPassWriteIndex: 1,
116
+ },
117
+ });
118
+ expect(captured?.timestampWrites?.querySet).toBeDefined();
87
119
  });
88
120
  });
@@ -0,0 +1,153 @@
1
+ import {
2
+ createUnavailableR32FloatReceipt,
3
+ err,
4
+ ok,
5
+ R32FLOAT_PROBE_STAGES,
6
+ type Result,
7
+ RhiError,
8
+ type RhiTextureFormatCapabilityReceipt,
9
+ type RhiTextureFormatProbeStage,
10
+ validateR32FloatReceipt,
11
+ } from '@forgeax/engine-rhi';
12
+
13
+ const TEXTURE_BINDING = 0x04;
14
+ const STORAGE_BINDING = 0x08;
15
+ const COPY_SRC = 0x01;
16
+ const COPY_DST = 0x02;
17
+ const MAP_READ = 0x0001;
18
+ const BUFFER_COPY_DST = 0x0008;
19
+
20
+ type RawDevice = GPUDevice;
21
+
22
+ /** Execute the complete real-device r32float profile without exposing raw handles. */
23
+ export async function probeR32FloatCapability(
24
+ rawDevice: RawDevice,
25
+ deviceGeneration: number,
26
+ ): Promise<Result<RhiTextureFormatCapabilityReceipt, RhiError>> {
27
+ let stage: RhiTextureFormatProbeStage = 'texture-create';
28
+ let texture: GPUTexture | undefined;
29
+ let readback: GPUBuffer | undefined;
30
+ rawDevice.pushErrorScope('validation');
31
+ try {
32
+ texture = rawDevice.createTexture({
33
+ size: { width: 2, height: 2, depthOrArrayLayers: 1 },
34
+ format: 'r32float',
35
+ mipLevelCount: 2,
36
+ usage: TEXTURE_BINDING | STORAGE_BINDING | COPY_SRC | COPY_DST,
37
+ });
38
+
39
+ stage = 'mip-view';
40
+ const sourceView = texture.createView({ baseMipLevel: 0, mipLevelCount: 1 });
41
+ const destinationView = texture.createView({ baseMipLevel: 1, mipLevelCount: 1 });
42
+
43
+ stage = 'sampled-storage-bind-group';
44
+ const layout = rawDevice.createBindGroupLayout({
45
+ entries: [
46
+ {
47
+ binding: 0,
48
+ visibility: 4,
49
+ texture: { sampleType: 'unfilterable-float', viewDimension: '2d' },
50
+ },
51
+ {
52
+ binding: 1,
53
+ visibility: 4,
54
+ storageTexture: { access: 'write-only', format: 'r32float', viewDimension: '2d' },
55
+ },
56
+ ],
57
+ });
58
+ const bindGroup = rawDevice.createBindGroup({
59
+ layout,
60
+ entries: [
61
+ { binding: 0, resource: sourceView },
62
+ { binding: 1, resource: destinationView },
63
+ ],
64
+ });
65
+
66
+ stage = 'pipeline-bind';
67
+ const shader = rawDevice.createShaderModule({
68
+ code: `
69
+ @group(0) @binding(0) var source: texture_2d<f32>;
70
+ @group(0) @binding(1) var destination: texture_storage_2d<r32float, write>;
71
+ @compute @workgroup_size(1) fn main() {
72
+ let value = textureLoad(source, vec2i(0, 0), 0).r;
73
+ textureStore(destination, vec2i(0, 0), vec4f(value));
74
+ }
75
+ `,
76
+ });
77
+ const pipeline = rawDevice.createComputePipeline({
78
+ layout: rawDevice.createPipelineLayout({ bindGroupLayouts: [layout] }),
79
+ compute: { module: shader, entryPoint: 'main' },
80
+ });
81
+
82
+ const encoder = rawDevice.createCommandEncoder({ label: 'r32float-profile' });
83
+ const pass = encoder.beginComputePass();
84
+ pass.setPipeline(pipeline);
85
+ pass.setBindGroup(0, bindGroup);
86
+ pass.dispatchWorkgroups(1);
87
+ pass.end();
88
+
89
+ stage = 'finish';
90
+ readback = rawDevice.createBuffer({ size: 256, usage: MAP_READ | BUFFER_COPY_DST });
91
+ encoder.copyTextureToBuffer(
92
+ { texture, mipLevel: 1 },
93
+ { buffer: readback, bytesPerRow: 256, rowsPerImage: 1 },
94
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
95
+ );
96
+ const commandBuffer = encoder.finish();
97
+
98
+ stage = 'submit';
99
+ rawDevice.queue.submit([commandBuffer]);
100
+
101
+ stage = 'completion';
102
+ await rawDevice.queue.onSubmittedWorkDone();
103
+ const validationError = await rawDevice.popErrorScope();
104
+ if (validationError !== null) {
105
+ throw new Error(`WebGPU validation: ${validationError.message}`);
106
+ }
107
+
108
+ stage = 'readback';
109
+ await readback.mapAsync(MAP_READ);
110
+ const values = Array.from(new Float32Array(readback.getMappedRange(0, 4).slice(0)));
111
+ readback.unmap();
112
+ const receipt: RhiTextureFormatCapabilityReceipt = {
113
+ profile: 'r32float-mip-sampled-storage',
114
+ verdict: 'admitted',
115
+ evidence: 'real',
116
+ deviceGeneration,
117
+ stages: R32FLOAT_PROBE_STAGES.map((entry) => ({
118
+ stage: entry,
119
+ verdict: 'admitted',
120
+ evidence: 'real',
121
+ })),
122
+ sampleType: 'unfilterable-float',
123
+ usages: ['texture-binding', 'storage-binding', 'copy-src'],
124
+ readback: { byteLength: 4, values },
125
+ probeExecutions: 1,
126
+ };
127
+ const validated = validateR32FloatReceipt(receipt);
128
+ return validated.ok ? ok(validated.value) : validated;
129
+ } catch (cause) {
130
+ const message = cause instanceof Error ? cause.message : String(cause);
131
+ const receipt = createUnavailableR32FloatReceipt({
132
+ deviceGeneration,
133
+ failedStage: stage,
134
+ detail: message,
135
+ });
136
+ void receipt;
137
+ return err(
138
+ new RhiError({
139
+ code: 'rhi-texture-format-capability-unavailable',
140
+ expected: `r32float profile stage ${stage} to complete on the live WebGPU device`,
141
+ hint: 'retain fallback-only rendering and inspect the device validation error',
142
+ detail: {
143
+ stage,
144
+ deviceGeneration,
145
+ reason: message,
146
+ },
147
+ }),
148
+ );
149
+ } finally {
150
+ texture?.destroy();
151
+ readback?.destroy();
152
+ }
153
+ }
@@ -30,30 +30,3 @@ export function resolveTimestampQueries(args: {
30
30
  );
31
31
  }
32
32
  }
33
-
34
- export function writeTimestamp(args: {
35
- rawEncoder: GPUCommandEncoder;
36
- rawQuerySet: GPUQuerySet;
37
- queryIndex: number;
38
- }): void {
39
- const encoderWithTimestamp = args.rawEncoder as unknown as {
40
- writeTimestamp?: (querySet: GPUQuerySet, queryIndex: number) => void;
41
- };
42
- if (typeof encoderWithTimestamp.writeTimestamp !== 'function') {
43
- throw new RhiError({
44
- code: 'webgpu-runtime-error',
45
- expected: 'underlying GPUCommandEncoder.writeTimestamp to be callable',
46
- hint: 'timestamp-query is advertised but the raw encoder has no writeTimestamp method',
47
- });
48
- }
49
- try {
50
- encoderWithTimestamp.writeTimestamp(args.rawQuerySet, args.queryIndex);
51
- } catch (error) {
52
- const message = error instanceof Error ? error.message : String(error);
53
- throw new RhiError({
54
- code: 'webgpu-runtime-error',
55
- expected: 'underlying GPUCommandEncoder.writeTimestamp to succeed',
56
- hint: `writeTimestamp raised: ${message}`,
57
- });
58
- }
59
- }