@forgeax/engine-rhi-webgpu 0.1.2
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/LICENSE +202 -0
- package/README.md +124 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/__mocks__/gpu-device.d.ts +233 -0
- package/dist/__tests__/__mocks__/gpu-device.d.ts.map +1 -0
- package/dist/__tests__/dawn-real-gpu.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn-real-gpu.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/queue-write-range.browser.test.d.ts +2 -0
- package/dist/__tests__/queue-write-range.browser.test.d.ts.map +1 -0
- package/dist/__tests__/rgba16float-live-probe.browser.test.d.ts +2 -0
- package/dist/__tests__/rgba16float-live-probe.browser.test.d.ts.map +1 -0
- package/dist/__tests__/rgba16float-live-probe.d.ts +12 -0
- package/dist/__tests__/rgba16float-live-probe.d.ts.map +1 -0
- package/dist/__tests__/rgba16float-live-probe.dawn.test.d.ts +2 -0
- package/dist/__tests__/rgba16float-live-probe.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-webgpu.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-webgpu.unit.test.d.ts.map +1 -0
- package/dist/device.d.ts +62 -0
- package/dist/device.d.ts.map +1 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +183 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1704 -0
- package/dist/index.mjs.map +1 -0
- 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/error-translation.d.ts +16 -0
- package/dist/internal/error-translation.d.ts.map +1 -0
- package/dist/internal/timestamp-query.d.ts +39 -0
- package/dist/internal/timestamp-query.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/__tests__/__mocks__/gpu-device.ts +564 -0
- package/src/__tests__/dawn-real-gpu.dawn.test.ts +1488 -0
- package/src/__tests__/queue-write-range.browser.test.ts +60 -0
- package/src/__tests__/rgba16float-live-probe.browser.test.ts +13 -0
- package/src/__tests__/rgba16float-live-probe.dawn.test.ts +15 -0
- package/src/__tests__/rgba16float-live-probe.ts +44 -0
- package/src/__tests__/rhi-webgpu.unit.test.ts +2409 -0
- package/src/device.ts +1974 -0
- package/src/errors.ts +145 -0
- package/src/index.ts +599 -0
- package/src/internal/__tests__/timestamp-query.unit.test.ts +88 -0
- package/src/internal/error-translation.ts +187 -0
- package/src/internal/timestamp-query.ts +167 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
// Minimal mock GPU device fixture for @forgeax/engine-rhi-webgpu unit tests.
|
|
2
|
+
//
|
|
3
|
+
// Strategy (decision S-2 / OQ-1 path (b) / research F-6):
|
|
4
|
+
// - hand-written minimal mock object (pure TS, zero native dependency).
|
|
5
|
+
// - does NOT attempt a spec-compliant full GPUDevice (webgpu-utils author
|
|
6
|
+
// evaluated that as higher-cost than maintaining a puppeteer launcher,
|
|
7
|
+
// and impossible to keep up with spec drift).
|
|
8
|
+
// - covers the entry-point set used by the shim tests:
|
|
9
|
+
// requestAdapter / requestDevice / device.createBuffer / createTexture /
|
|
10
|
+
// createSampler / createBindGroupLayout / createRenderPipeline /
|
|
11
|
+
// createShaderModule / createCommandEncoder + queue submit / writeBuffer
|
|
12
|
+
// + failure injection switches.
|
|
13
|
+
//
|
|
14
|
+
// Design points:
|
|
15
|
+
// - `MockGpu` implements the GPU interface subset required by the shim's
|
|
16
|
+
// `gpu?: GPU` provider seam (research F-6 webgpu-utils + CTS consensus).
|
|
17
|
+
// - All createX calls forward the descriptor into `__captured` so the
|
|
18
|
+
// field-by-field passthrough assertions can read it back.
|
|
19
|
+
// - `failures` controls error injection for the device/shader paths.
|
|
20
|
+
// - `compilerMessages` lets shader-compile tests pump the full 6 fields of
|
|
21
|
+
// GPUCompilationMessage (OQ-P2: message / type / lineNum / linePos /
|
|
22
|
+
// offset / length).
|
|
23
|
+
//
|
|
24
|
+
// w3 / w5 / w6 (feat-20260508-rhi-surface-completion): added
|
|
25
|
+
// MockCommandEncoder + MockRenderPassEncoder + MockComputePassEncoder +
|
|
26
|
+
// MockCommandBuffer + MockBuffer.size + MockQueue.submit / writeBuffer.
|
|
27
|
+
|
|
28
|
+
/// <reference types="@webgpu/types" />
|
|
29
|
+
|
|
30
|
+
/** Failure-injection switches consumed by the device path tests. */
|
|
31
|
+
export interface MockFailures {
|
|
32
|
+
/** `requestAdapter` returns null (research F-5 single null channel). */
|
|
33
|
+
adapterNull?: boolean;
|
|
34
|
+
/** `requestDevice` rejects with `OperationError` (spec behaviour for an
|
|
35
|
+
* unsupported feature). */
|
|
36
|
+
requestDeviceFeatureNotEnabled?: boolean;
|
|
37
|
+
/** `requestDevice` rejects with `OperationError` (spec behaviour for an
|
|
38
|
+
* out-of-range limit). */
|
|
39
|
+
requestDeviceLimitExceeded?: boolean;
|
|
40
|
+
/** `createShaderModule` returns a module whose `getCompilationInfo()`
|
|
41
|
+
* yields error-typed messages. */
|
|
42
|
+
shaderCompileMessages?: readonly GPUCompilationMessage[];
|
|
43
|
+
/** `createShaderModule` returns a module whose `getCompilationInfo()`
|
|
44
|
+
* rejects — models the GPU instance being dropped mid-await (device
|
|
45
|
+
* destroyed / page teardown while the async query is in flight). */
|
|
46
|
+
getCompilationInfoRejects?: boolean;
|
|
47
|
+
/** `createRenderPipeline` throws synchronously with this message. */
|
|
48
|
+
renderPipelineError?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Mock-capture event union; consumed by passthrough assertions. */
|
|
52
|
+
export type MockCapture =
|
|
53
|
+
| { kind: 'requestAdapter'; options: GPURequestAdapterOptions | undefined }
|
|
54
|
+
| { kind: 'requestDevice'; options: GPUDeviceDescriptor | undefined }
|
|
55
|
+
| {
|
|
56
|
+
kind: 'writeBuffer';
|
|
57
|
+
buffer: MockBuffer;
|
|
58
|
+
bufferOffset: number;
|
|
59
|
+
data: ArrayBufferView | ArrayBuffer;
|
|
60
|
+
dataOffset: number | undefined;
|
|
61
|
+
size: number | undefined;
|
|
62
|
+
}
|
|
63
|
+
| { kind: 'createBuffer'; descriptor: GPUBufferDescriptor }
|
|
64
|
+
| { kind: 'createTexture'; descriptor: GPUTextureDescriptor }
|
|
65
|
+
| { kind: 'createSampler'; descriptor: GPUSamplerDescriptor | undefined }
|
|
66
|
+
| { kind: 'createBindGroupLayout'; descriptor: GPUBindGroupLayoutDescriptor }
|
|
67
|
+
| { kind: 'createBindGroup'; descriptor: GPUBindGroupDescriptor }
|
|
68
|
+
| { kind: 'createPipelineLayout'; descriptor: GPUPipelineLayoutDescriptor }
|
|
69
|
+
| { kind: 'createRenderPipeline'; descriptor: GPURenderPipelineDescriptor }
|
|
70
|
+
| { kind: 'createShaderModule'; descriptor: GPUShaderModuleDescriptor }
|
|
71
|
+
| {
|
|
72
|
+
kind: 'createTextureView';
|
|
73
|
+
sourceDescriptor: GPUTextureDescriptor;
|
|
74
|
+
descriptor: GPUTextureViewDescriptor | undefined;
|
|
75
|
+
}
|
|
76
|
+
| { kind: 'createComputePipeline'; descriptor: GPUComputePipelineDescriptor }
|
|
77
|
+
| { kind: 'createQuerySet'; descriptor: GPUQuerySetDescriptor };
|
|
78
|
+
|
|
79
|
+
/** Unique brand symbol so captured handles cannot be confused with real GPU* objects. */
|
|
80
|
+
const MOCK_BRAND: unique symbol = Symbol('mock-gpu-handle');
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Mock texture handle: exposes spec readonly fields the shim consumes for
|
|
84
|
+
* createTextureView cross-resource validation (research §1.1: format must be
|
|
85
|
+
* in source.format ∪ source.viewFormats; usage must be a subset of
|
|
86
|
+
* source.usage). The shim retrieves these via its own WeakMap (filled at
|
|
87
|
+
* createTexture time), but the mock surfaces createView so the shim can
|
|
88
|
+
* forward and capture descriptors verbatim.
|
|
89
|
+
*/
|
|
90
|
+
export interface MockTexture {
|
|
91
|
+
readonly [MOCK_BRAND]: 'texture';
|
|
92
|
+
readonly format: GPUTextureFormat;
|
|
93
|
+
readonly usage: GPUTextureUsageFlags;
|
|
94
|
+
readonly viewFormats: readonly GPUTextureFormat[];
|
|
95
|
+
createView(descriptor?: GPUTextureViewDescriptor | undefined): MockTextureView;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Mock texture view handle. */
|
|
99
|
+
export interface MockTextureView {
|
|
100
|
+
readonly [MOCK_BRAND]: 'texture-view';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Mock device exposed subset (covers only the surface the shim uses). */
|
|
104
|
+
export interface MockDevice {
|
|
105
|
+
readonly features: GPUSupportedFeatures;
|
|
106
|
+
readonly limits: GPUSupportedLimits;
|
|
107
|
+
readonly lost: Promise<GPUDeviceLostInfo>;
|
|
108
|
+
readonly queue: MockQueue;
|
|
109
|
+
createBuffer(descriptor: GPUBufferDescriptor): MockBuffer;
|
|
110
|
+
createTexture(descriptor: GPUTextureDescriptor): MockTexture;
|
|
111
|
+
createSampler(descriptor?: GPUSamplerDescriptor | undefined): {
|
|
112
|
+
readonly [MOCK_BRAND]: 'sampler';
|
|
113
|
+
};
|
|
114
|
+
createBindGroupLayout(descriptor: GPUBindGroupLayoutDescriptor): { readonly [MOCK_BRAND]: 'bgl' };
|
|
115
|
+
createBindGroup(descriptor: GPUBindGroupDescriptor): { readonly [MOCK_BRAND]: 'bg' };
|
|
116
|
+
createPipelineLayout(descriptor: GPUPipelineLayoutDescriptor): {
|
|
117
|
+
readonly [MOCK_BRAND]: 'pl';
|
|
118
|
+
};
|
|
119
|
+
createRenderPipeline(descriptor: GPURenderPipelineDescriptor): {
|
|
120
|
+
readonly [MOCK_BRAND]: 'render-pipeline';
|
|
121
|
+
};
|
|
122
|
+
createComputePipeline(descriptor: GPUComputePipelineDescriptor): {
|
|
123
|
+
readonly [MOCK_BRAND]: 'compute-pipeline';
|
|
124
|
+
};
|
|
125
|
+
createQuerySet(descriptor: GPUQuerySetDescriptor): {
|
|
126
|
+
readonly [MOCK_BRAND]: 'query-set';
|
|
127
|
+
};
|
|
128
|
+
createShaderModule(descriptor: GPUShaderModuleDescriptor): {
|
|
129
|
+
readonly [MOCK_BRAND]: 'shader';
|
|
130
|
+
getCompilationInfo(): Promise<GPUCompilationInfo>;
|
|
131
|
+
};
|
|
132
|
+
createCommandEncoder(descriptor?: GPUCommandEncoderDescriptor | undefined): MockCommandEncoder;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Mock queue: tracks submit / writeBuffer for assertions; M5 / w37 adds
|
|
136
|
+
* writeTexture / copyExternalImageToTexture / onSubmittedWorkDone. */
|
|
137
|
+
export interface MockQueue {
|
|
138
|
+
readonly [MOCK_BRAND]: 'queue';
|
|
139
|
+
submit(commandBuffers: Iterable<MockCommandBuffer>): void;
|
|
140
|
+
writeBuffer(
|
|
141
|
+
buffer: MockBuffer,
|
|
142
|
+
bufferOffset: number,
|
|
143
|
+
data: ArrayBufferView | ArrayBuffer,
|
|
144
|
+
dataOffset?: number,
|
|
145
|
+
size?: number,
|
|
146
|
+
): void;
|
|
147
|
+
writeTexture(
|
|
148
|
+
destination: unknown,
|
|
149
|
+
data: ArrayBufferView | ArrayBuffer,
|
|
150
|
+
dataLayout: unknown,
|
|
151
|
+
size: unknown,
|
|
152
|
+
): void;
|
|
153
|
+
copyExternalImageToTexture(source: unknown, destination: unknown, copySize: unknown): void;
|
|
154
|
+
onSubmittedWorkDone(): Promise<undefined>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Mock buffer; carries `size` so writeBuffer bounds-checking has a value.
|
|
158
|
+
* Extended in M5 / w35 with mapping surface (mapAsync / getMappedRange /
|
|
159
|
+
* unmap / mapState) so the shim Buffer wrapper can drive the validation
|
|
160
|
+
* paths (research §4.1 / §4.2 / §4.4). */
|
|
161
|
+
export interface MockBuffer {
|
|
162
|
+
readonly [MOCK_BRAND]: 'buffer';
|
|
163
|
+
readonly size: number;
|
|
164
|
+
mapState: 'unmapped' | 'pending' | 'mapped';
|
|
165
|
+
mapAsync(mode: number, offset?: number, size?: number): Promise<void>;
|
|
166
|
+
getMappedRange(offset?: number, size?: number): ArrayBuffer;
|
|
167
|
+
unmap(): void;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Mock command encoder: returned by device.createCommandEncoder. */
|
|
171
|
+
export interface MockCommandEncoder {
|
|
172
|
+
readonly [MOCK_BRAND]: 'command-encoder';
|
|
173
|
+
beginRenderPass(descriptor: GPURenderPassDescriptor): MockRenderPassEncoder;
|
|
174
|
+
beginComputePass(descriptor?: GPUComputePassDescriptor): MockComputePassEncoder;
|
|
175
|
+
copyBufferToBuffer(...args: unknown[]): void;
|
|
176
|
+
copyBufferToTexture(source: unknown, destination: unknown, copySize: unknown): void;
|
|
177
|
+
copyTextureToBuffer(source: unknown, destination: unknown, copySize: unknown): void;
|
|
178
|
+
copyTextureToTexture(source: unknown, destination: unknown, copySize: unknown): void;
|
|
179
|
+
clearBuffer(buffer: MockBuffer, offset?: number, size?: number): void;
|
|
180
|
+
resolveQuerySet(...args: unknown[]): void;
|
|
181
|
+
pushDebugGroup(label: string): void;
|
|
182
|
+
popDebugGroup(): void;
|
|
183
|
+
insertDebugMarker(label: string): void;
|
|
184
|
+
finish(descriptor?: GPUCommandBufferDescriptor): MockCommandBuffer;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Mock render pass encoder. */
|
|
188
|
+
export interface MockRenderPassEncoder {
|
|
189
|
+
readonly [MOCK_BRAND]: 'render-pass-encoder';
|
|
190
|
+
setPipeline(pipeline: unknown): void;
|
|
191
|
+
setBindGroup(...args: unknown[]): void;
|
|
192
|
+
setIndexBuffer(...args: unknown[]): void;
|
|
193
|
+
setVertexBuffer(...args: unknown[]): void;
|
|
194
|
+
draw(...args: unknown[]): void;
|
|
195
|
+
drawIndexed(...args: unknown[]): void;
|
|
196
|
+
drawIndirect(...args: unknown[]): void;
|
|
197
|
+
drawIndexedIndirect(...args: unknown[]): void;
|
|
198
|
+
setViewport(...args: unknown[]): void;
|
|
199
|
+
setScissorRect(...args: unknown[]): void;
|
|
200
|
+
setBlendConstant(...args: unknown[]): void;
|
|
201
|
+
setStencilReference(...args: unknown[]): void;
|
|
202
|
+
pushDebugGroup(label: string): void;
|
|
203
|
+
popDebugGroup(): void;
|
|
204
|
+
insertDebugMarker(label: string): void;
|
|
205
|
+
end(): void;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Mock compute pass encoder. */
|
|
209
|
+
export interface MockComputePassEncoder {
|
|
210
|
+
readonly [MOCK_BRAND]: 'compute-pass-encoder';
|
|
211
|
+
setPipeline(pipeline: unknown): void;
|
|
212
|
+
setBindGroup(...args: unknown[]): void;
|
|
213
|
+
dispatchWorkgroups(...args: unknown[]): void;
|
|
214
|
+
dispatchWorkgroupsIndirect(...args: unknown[]): void;
|
|
215
|
+
end(): void;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Mock command buffer (returned by encoder.finish). */
|
|
219
|
+
export interface MockCommandBuffer {
|
|
220
|
+
readonly [MOCK_BRAND]: 'command-buffer';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Minimal mock `GPU` provider injected via the shim's `gpu?: GPU` provider
|
|
225
|
+
* seam (research F-6 webgpu-utils + CTS consensus).
|
|
226
|
+
*
|
|
227
|
+
* `__captured` / `__failures` are observation points exclusive to the test
|
|
228
|
+
* side; the shim does not read them.
|
|
229
|
+
*/
|
|
230
|
+
export interface MockGpu {
|
|
231
|
+
/** Test observation: createX / requestX call-site descriptors in order. */
|
|
232
|
+
readonly __captured: MockCapture[];
|
|
233
|
+
/** Failure-injection switches for the test side. */
|
|
234
|
+
readonly __failures: MockFailures;
|
|
235
|
+
requestAdapter(options?: GPURequestAdapterOptions | undefined): Promise<MockAdapter | null>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Mock adapter subset. */
|
|
239
|
+
export interface MockAdapter {
|
|
240
|
+
readonly features: GPUSupportedFeatures;
|
|
241
|
+
readonly limits: GPUSupportedLimits;
|
|
242
|
+
requestDevice(descriptor?: GPUDeviceDescriptor | undefined): Promise<MockDevice>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Default features set (empty); use failures.requestDeviceFeatureNotEnabled
|
|
246
|
+
* to trigger the unsupported-feature path. */
|
|
247
|
+
function makeFeatures(initial: readonly GPUFeatureName[] = []): GPUSupportedFeatures {
|
|
248
|
+
return new Set(initial) as unknown as GPUSupportedFeatures;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Default limits with non-zero values (MVP-1.2 runtime non-empty assertion
|
|
253
|
+
* only checks `typeof !== 'undefined'`).
|
|
254
|
+
*
|
|
255
|
+
* Field set aligns with `@webgpu/types` v0.1.69 GPUSupportedLimits 35 fields;
|
|
256
|
+
* this mock uses spec defaults (research F-1 / W3C CR 3.6). Cast to
|
|
257
|
+
* GPUSupportedLimits to tolerate upstream dts additions.
|
|
258
|
+
*/
|
|
259
|
+
function makeLimits(): GPUSupportedLimits {
|
|
260
|
+
return {
|
|
261
|
+
maxTextureDimension1D: 8192,
|
|
262
|
+
maxTextureDimension2D: 8192,
|
|
263
|
+
maxTextureDimension3D: 2048,
|
|
264
|
+
maxTextureArrayLayers: 256,
|
|
265
|
+
maxBindGroups: 4,
|
|
266
|
+
maxBindGroupsPlusVertexBuffers: 24,
|
|
267
|
+
maxBindingsPerBindGroup: 1000,
|
|
268
|
+
maxDynamicUniformBuffersPerPipelineLayout: 8,
|
|
269
|
+
maxDynamicStorageBuffersPerPipelineLayout: 4,
|
|
270
|
+
maxSampledTexturesPerShaderStage: 16,
|
|
271
|
+
maxSamplersPerShaderStage: 16,
|
|
272
|
+
maxStorageBuffersPerShaderStage: 8,
|
|
273
|
+
maxStorageTexturesPerShaderStage: 4,
|
|
274
|
+
maxUniformBuffersPerShaderStage: 12,
|
|
275
|
+
maxUniformBufferBindingSize: 65536,
|
|
276
|
+
maxStorageBufferBindingSize: 134217728,
|
|
277
|
+
minUniformBufferOffsetAlignment: 256,
|
|
278
|
+
minStorageBufferOffsetAlignment: 256,
|
|
279
|
+
maxVertexBuffers: 8,
|
|
280
|
+
maxBufferSize: 268435456,
|
|
281
|
+
maxVertexAttributes: 16,
|
|
282
|
+
maxVertexBufferArrayStride: 2048,
|
|
283
|
+
maxInterStageShaderVariables: 16,
|
|
284
|
+
maxColorAttachments: 8,
|
|
285
|
+
maxColorAttachmentBytesPerSample: 32,
|
|
286
|
+
maxComputeWorkgroupStorageSize: 16384,
|
|
287
|
+
maxComputeInvocationsPerWorkgroup: 256,
|
|
288
|
+
maxComputeWorkgroupSizeX: 256,
|
|
289
|
+
maxComputeWorkgroupSizeY: 256,
|
|
290
|
+
maxComputeWorkgroupSizeZ: 64,
|
|
291
|
+
maxComputeWorkgroupsPerDimension: 65535,
|
|
292
|
+
} as unknown as GPUSupportedLimits;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Construct a minimal mock device; captures call-site descriptors into `captured`. */
|
|
296
|
+
function makeDevice(captured: MockCapture[], failures: MockFailures): MockDevice {
|
|
297
|
+
const compileMsgs = failures.shaderCompileMessages;
|
|
298
|
+
const compilationInfoRejects = failures.getCompilationInfoRejects === true;
|
|
299
|
+
const lost = new Promise<GPUDeviceLostInfo>(() => {
|
|
300
|
+
/* never settles - device.lost two-track test uses caller-injected mock */
|
|
301
|
+
});
|
|
302
|
+
// M5 / w37: track FIFO order of onSubmittedWorkDone calls so the unit
|
|
303
|
+
// tests can assert ordering constraint #1 (research §5.2).
|
|
304
|
+
let osWdCounter = 0;
|
|
305
|
+
const queue: MockQueue = {
|
|
306
|
+
[MOCK_BRAND]: 'queue' as const,
|
|
307
|
+
submit(_commandBuffers): void {
|
|
308
|
+
// No-op; w6 unit tests assert the shim wiring + bounds error paths
|
|
309
|
+
// before the call would reach the real GPU layer.
|
|
310
|
+
},
|
|
311
|
+
writeBuffer(buffer, bufferOffset, data, dataOffset, size): void {
|
|
312
|
+
// No-op; bounds checks happen in the shim before this is reached.
|
|
313
|
+
captured.push({ kind: 'writeBuffer', buffer, bufferOffset, data, dataOffset, size });
|
|
314
|
+
},
|
|
315
|
+
writeTexture(_destination, _data, _dataLayout, _size): void {
|
|
316
|
+
// No-op; alignment validation happens in the shim before this is hit.
|
|
317
|
+
},
|
|
318
|
+
copyExternalImageToTexture(_source, _destination, _copySize): void {
|
|
319
|
+
// No-op; mock surface for type / API contract assertions.
|
|
320
|
+
},
|
|
321
|
+
onSubmittedWorkDone(): Promise<undefined> {
|
|
322
|
+
// Mirror FIFO ordering by chaining microtasks (await each previous
|
|
323
|
+
// call's resolution before resolving this one).
|
|
324
|
+
const order = ++osWdCounter;
|
|
325
|
+
return new Promise<undefined>((resolve) => {
|
|
326
|
+
// Schedule resolution at microtask depth N so the N-th call
|
|
327
|
+
// settles after the (N-1)-th call (research §5.2 constraint #1).
|
|
328
|
+
const chain = (depth: number): void => {
|
|
329
|
+
if (depth === 0) {
|
|
330
|
+
resolve(undefined);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
queueMicrotask(() => chain(depth - 1));
|
|
334
|
+
};
|
|
335
|
+
chain(order);
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
return {
|
|
340
|
+
features: makeFeatures(),
|
|
341
|
+
limits: makeLimits(),
|
|
342
|
+
lost,
|
|
343
|
+
queue,
|
|
344
|
+
createBuffer(descriptor) {
|
|
345
|
+
captured.push({ kind: 'createBuffer', descriptor });
|
|
346
|
+
const sizeField =
|
|
347
|
+
typeof descriptor.size === 'number' ? descriptor.size : Number.MAX_SAFE_INTEGER;
|
|
348
|
+
// M5 / w35: track mapState so the shim Buffer wrapper can detect the
|
|
349
|
+
// F-8 three-row faults (already-mapped re-mapAsync / detach after unmap
|
|
350
|
+
// / mode-usage mismatch all ride 'webgpu-runtime-error', research §4.2
|
|
351
|
+
// step 1 / 9 + §4.4 detach semantics).
|
|
352
|
+
let backing: ArrayBuffer | null =
|
|
353
|
+
descriptor.mappedAtCreation === true ? new ArrayBuffer(sizeField) : null;
|
|
354
|
+
const buf: MockBuffer = {
|
|
355
|
+
[MOCK_BRAND]: 'buffer' as const,
|
|
356
|
+
size: sizeField,
|
|
357
|
+
mapState: descriptor.mappedAtCreation === true ? 'mapped' : 'unmapped',
|
|
358
|
+
mapAsync(_mode, _offset, _size): Promise<void> {
|
|
359
|
+
buf.mapState = 'mapped';
|
|
360
|
+
backing = new ArrayBuffer(sizeField);
|
|
361
|
+
return Promise.resolve();
|
|
362
|
+
},
|
|
363
|
+
getMappedRange(offset = 0, size?): ArrayBuffer {
|
|
364
|
+
if (backing === null) {
|
|
365
|
+
throw new Error('mock: getMappedRange before mapAsync');
|
|
366
|
+
}
|
|
367
|
+
const len = size ?? backing.byteLength - offset;
|
|
368
|
+
return backing.slice(offset, offset + len);
|
|
369
|
+
},
|
|
370
|
+
unmap(): void {
|
|
371
|
+
backing = null;
|
|
372
|
+
buf.mapState = 'unmapped';
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
return buf;
|
|
376
|
+
},
|
|
377
|
+
createTexture(descriptor) {
|
|
378
|
+
captured.push({ kind: 'createTexture', descriptor });
|
|
379
|
+
const sourceDescriptor = descriptor;
|
|
380
|
+
const tex: MockTexture = {
|
|
381
|
+
[MOCK_BRAND]: 'texture' as const,
|
|
382
|
+
format: descriptor.format,
|
|
383
|
+
usage: descriptor.usage,
|
|
384
|
+
viewFormats: descriptor.viewFormats === undefined ? [] : [...descriptor.viewFormats],
|
|
385
|
+
createView(viewDescriptor) {
|
|
386
|
+
captured.push({
|
|
387
|
+
kind: 'createTextureView',
|
|
388
|
+
sourceDescriptor,
|
|
389
|
+
descriptor: viewDescriptor,
|
|
390
|
+
});
|
|
391
|
+
return { [MOCK_BRAND]: 'texture-view' as const };
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
return tex;
|
|
395
|
+
},
|
|
396
|
+
createSampler(descriptor) {
|
|
397
|
+
captured.push({ kind: 'createSampler', descriptor });
|
|
398
|
+
return { [MOCK_BRAND]: 'sampler' as const };
|
|
399
|
+
},
|
|
400
|
+
createBindGroupLayout(descriptor) {
|
|
401
|
+
captured.push({ kind: 'createBindGroupLayout', descriptor });
|
|
402
|
+
return { [MOCK_BRAND]: 'bgl' as const };
|
|
403
|
+
},
|
|
404
|
+
createBindGroup(descriptor) {
|
|
405
|
+
captured.push({ kind: 'createBindGroup', descriptor });
|
|
406
|
+
return { [MOCK_BRAND]: 'bg' as const };
|
|
407
|
+
},
|
|
408
|
+
createPipelineLayout(descriptor) {
|
|
409
|
+
captured.push({ kind: 'createPipelineLayout', descriptor });
|
|
410
|
+
return { [MOCK_BRAND]: 'pl' as const };
|
|
411
|
+
},
|
|
412
|
+
createRenderPipeline(descriptor) {
|
|
413
|
+
captured.push({ kind: 'createRenderPipeline', descriptor });
|
|
414
|
+
if (failures.renderPipelineError !== undefined) {
|
|
415
|
+
throw new Error(failures.renderPipelineError);
|
|
416
|
+
}
|
|
417
|
+
return { [MOCK_BRAND]: 'render-pipeline' as const };
|
|
418
|
+
},
|
|
419
|
+
createComputePipeline(descriptor) {
|
|
420
|
+
captured.push({ kind: 'createComputePipeline', descriptor });
|
|
421
|
+
return { [MOCK_BRAND]: 'compute-pipeline' as const };
|
|
422
|
+
},
|
|
423
|
+
createQuerySet(descriptor) {
|
|
424
|
+
captured.push({ kind: 'createQuerySet', descriptor });
|
|
425
|
+
// Mock surfaces .count + .type (spec readonly fields) so the shim's
|
|
426
|
+
// resolveQuerySet bounds check (research §2.3) and beginOcclusionQuery
|
|
427
|
+
// queryIndex bounds check (research §2.1) can read real values without
|
|
428
|
+
// routing through the dawn-real-gpu path.
|
|
429
|
+
return {
|
|
430
|
+
[MOCK_BRAND]: 'query-set' as const,
|
|
431
|
+
count: descriptor.count,
|
|
432
|
+
type: descriptor.type,
|
|
433
|
+
} as unknown as { readonly [MOCK_BRAND]: 'query-set' };
|
|
434
|
+
},
|
|
435
|
+
createShaderModule(descriptor) {
|
|
436
|
+
captured.push({ kind: 'createShaderModule', descriptor });
|
|
437
|
+
return {
|
|
438
|
+
[MOCK_BRAND]: 'shader' as const,
|
|
439
|
+
getCompilationInfo() {
|
|
440
|
+
if (compilationInfoRejects) {
|
|
441
|
+
return Promise.reject(
|
|
442
|
+
new DOMException('Instance dropped error in getCompilationInfo', 'OperationError'),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
return Promise.resolve({
|
|
446
|
+
messages: compileMsgs ?? [],
|
|
447
|
+
} as GPUCompilationInfo);
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
},
|
|
451
|
+
createCommandEncoder(_descriptor) {
|
|
452
|
+
const encoder: MockCommandEncoder = {
|
|
453
|
+
[MOCK_BRAND]: 'command-encoder' as const,
|
|
454
|
+
beginRenderPass(_descriptor) {
|
|
455
|
+
const pass: MockRenderPassEncoder = {
|
|
456
|
+
[MOCK_BRAND]: 'render-pass-encoder' as const,
|
|
457
|
+
setPipeline() {},
|
|
458
|
+
setBindGroup() {},
|
|
459
|
+
setIndexBuffer() {},
|
|
460
|
+
setVertexBuffer() {},
|
|
461
|
+
draw() {},
|
|
462
|
+
drawIndexed() {},
|
|
463
|
+
drawIndirect() {},
|
|
464
|
+
drawIndexedIndirect() {},
|
|
465
|
+
setViewport() {},
|
|
466
|
+
setScissorRect() {},
|
|
467
|
+
setBlendConstant() {},
|
|
468
|
+
setStencilReference() {},
|
|
469
|
+
pushDebugGroup() {},
|
|
470
|
+
popDebugGroup() {},
|
|
471
|
+
insertDebugMarker() {},
|
|
472
|
+
end() {},
|
|
473
|
+
};
|
|
474
|
+
return pass;
|
|
475
|
+
},
|
|
476
|
+
beginComputePass(_descriptor) {
|
|
477
|
+
const pass: MockComputePassEncoder = {
|
|
478
|
+
[MOCK_BRAND]: 'compute-pass-encoder' as const,
|
|
479
|
+
setPipeline() {},
|
|
480
|
+
setBindGroup() {},
|
|
481
|
+
dispatchWorkgroups() {},
|
|
482
|
+
dispatchWorkgroupsIndirect() {},
|
|
483
|
+
end() {},
|
|
484
|
+
};
|
|
485
|
+
return pass;
|
|
486
|
+
},
|
|
487
|
+
copyBufferToBuffer() {},
|
|
488
|
+
copyBufferToTexture() {},
|
|
489
|
+
copyTextureToBuffer() {},
|
|
490
|
+
copyTextureToTexture() {},
|
|
491
|
+
clearBuffer() {},
|
|
492
|
+
resolveQuerySet() {},
|
|
493
|
+
pushDebugGroup() {},
|
|
494
|
+
popDebugGroup() {},
|
|
495
|
+
insertDebugMarker() {},
|
|
496
|
+
finish(_descriptor) {
|
|
497
|
+
return { [MOCK_BRAND]: 'command-buffer' as const };
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
return encoder;
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Create a MockGpu test entry point.
|
|
507
|
+
*
|
|
508
|
+
* @example
|
|
509
|
+
* const gpu = createMockGpu();
|
|
510
|
+
* gpu.__failures.adapterNull = true;
|
|
511
|
+
* const r = await rhiWebgpu.requestDevice({ gpu });
|
|
512
|
+
* expect(r.ok).toBe(false);
|
|
513
|
+
*/
|
|
514
|
+
export function createMockGpu(failures: MockFailures = {}): MockGpu {
|
|
515
|
+
const captured: MockCapture[] = [];
|
|
516
|
+
const __failures: MockFailures = { ...failures };
|
|
517
|
+
return {
|
|
518
|
+
__captured: captured,
|
|
519
|
+
__failures,
|
|
520
|
+
async requestAdapter(options) {
|
|
521
|
+
captured.push({ kind: 'requestAdapter', options });
|
|
522
|
+
if (__failures.adapterNull === true) return null;
|
|
523
|
+
return {
|
|
524
|
+
features: makeFeatures(),
|
|
525
|
+
limits: makeLimits(),
|
|
526
|
+
async requestDevice(deviceOptions) {
|
|
527
|
+
captured.push({ kind: 'requestDevice', options: deviceOptions });
|
|
528
|
+
if (__failures.requestDeviceFeatureNotEnabled === true) {
|
|
529
|
+
const e = new Error('mock: requested feature not supported by adapter');
|
|
530
|
+
e.name = 'OperationError';
|
|
531
|
+
throw e;
|
|
532
|
+
}
|
|
533
|
+
if (__failures.requestDeviceLimitExceeded === true) {
|
|
534
|
+
const e = new Error('mock: requested limit exceeds adapter capability');
|
|
535
|
+
e.name = 'OperationError';
|
|
536
|
+
throw e;
|
|
537
|
+
}
|
|
538
|
+
return makeDevice(captured, __failures);
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Build a 6-field GPUCompilationMessage (OQ-P2 locked field set).
|
|
547
|
+
* Used by shader-compile-failed path tests asserting detail.compilerMessages.
|
|
548
|
+
*/
|
|
549
|
+
export function makeShaderError(
|
|
550
|
+
partial: Partial<GPUCompilationMessage> = {},
|
|
551
|
+
): GPUCompilationMessage {
|
|
552
|
+
// GPUCompilationMessage in @webgpu/types v0.1.69 has a private __brand
|
|
553
|
+
// placeholder (implementation-exclusive brand); user code constructs a
|
|
554
|
+
// plain object and casts to GPUCompilationMessage. Same shape as the
|
|
555
|
+
// webgpu-utils / CTS test fixtures (research F-6).
|
|
556
|
+
return {
|
|
557
|
+
message: partial.message ?? 'unexpected token',
|
|
558
|
+
type: partial.type ?? 'error',
|
|
559
|
+
lineNum: partial.lineNum ?? 1,
|
|
560
|
+
linePos: partial.linePos ?? 1,
|
|
561
|
+
offset: partial.offset ?? 0,
|
|
562
|
+
length: partial.length ?? 0,
|
|
563
|
+
} as unknown as GPUCompilationMessage;
|
|
564
|
+
}
|