@forgeax/engine-rhi-webgpu 0.1.3 → 0.1.6

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.
@@ -1,24 +1,4 @@
1
1
  import { type Result, RhiError } from '@forgeax/engine-rhi';
2
- export type TimestampQueryHealthCode = 'timestamp-query-unsupported' | 'timestamp-query-encoder-missing' | 'timestamp-query-generation-mismatch';
3
- export interface TimestampQueryHealth {
4
- readonly status: 'ready' | 'fault';
5
- readonly code: TimestampQueryHealthCode | undefined;
6
- readonly expected: string;
7
- readonly hint: string;
8
- readonly deviceGeneration: number;
9
- readonly expectedGeneration: number | undefined;
10
- }
11
- /**
12
- * Keep timestamp capability and device-generation checks structured. This is
13
- * also the health vocabulary used by recovery callers; a missing capability
14
- * or stale encoder is never reported as a fabricated timing interval.
15
- */
16
- export declare function timestampQueryHealth(input: {
17
- readonly capability: boolean;
18
- readonly encoderWriteTimestamp: boolean;
19
- readonly deviceGeneration: number;
20
- readonly expectedGeneration?: number | undefined;
21
- }): TimestampQueryHealth;
22
2
  export declare function resolveTimestampQueries(args: {
23
3
  rawEncoder: GPUCommandEncoder;
24
4
  rawQuerySet: GPUQuerySet;
@@ -26,14 +6,10 @@ export declare function resolveTimestampQueries(args: {
26
6
  queryCount: number;
27
7
  rawDestination: GPUBuffer;
28
8
  destinationOffset: number;
29
- readonly deviceGeneration?: number;
30
- readonly expectedGeneration?: number;
31
9
  }): Result<void, RhiError>;
32
10
  export declare function writeTimestamp(args: {
33
11
  rawEncoder: GPUCommandEncoder;
34
12
  rawQuerySet: GPUQuerySet;
35
13
  queryIndex: number;
36
- readonly deviceGeneration?: number;
37
- readonly expectedGeneration?: number;
38
14
  }): void;
39
15
  //# sourceMappingURL=timestamp-query.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"timestamp-query.d.ts","sourceRoot":"","sources":["../../src/internal/timestamp-query.ts"],"names":[],"mappings":"AAEA,OAAO,EAAW,KAAK,MAAM,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAErE,MAAM,MAAM,wBAAwB,GAChC,6BAA6B,GAC7B,iCAAiC,GACjC,qCAAqC,CAAC;AAE1C,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;CACjD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAC;IACxC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClD,GAAG,oBAAoB,CA0CvB;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,UAAU,EAAE,iBAAiB,CAAC;IAC9B,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,SAAS,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC,GAAG,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAuCzB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,UAAU,EAAE,iBAAiB,CAAC;IAC9B,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC,GAAG,IAAI,CAsCP"}
1
+ {"version":3,"file":"timestamp-query.d.ts","sourceRoot":"","sources":["../../src/internal/timestamp-query.ts"],"names":[],"mappings":"AAEA,OAAO,EAAW,KAAK,MAAM,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAErE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,UAAU,EAAE,iBAAiB,CAAC;IAC9B,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,SAAS,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;CAC3B,GAAG,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAoBzB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,UAAU,EAAE,iBAAiB,CAAC;IAC9B,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,IAAI,CAqBP"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-rhi-webgpu",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,8 +22,8 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "dependencies": {
25
- "@forgeax/engine-rhi": "0.1.3",
26
- "@forgeax/engine-types": "0.1.3",
25
+ "@forgeax/engine-rhi": "0.1.6",
26
+ "@forgeax/engine-types": "0.1.6",
27
27
  "@webgpu/types": "^0.1.71"
28
28
  },
29
29
  "forgeax": {
@@ -52,14 +52,6 @@ export interface MockFailures {
52
52
  export type MockCapture =
53
53
  | { kind: 'requestAdapter'; options: GPURequestAdapterOptions | undefined }
54
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
55
  | { kind: 'createBuffer'; descriptor: GPUBufferDescriptor }
64
56
  | { kind: 'createTexture'; descriptor: GPUTextureDescriptor }
65
57
  | { kind: 'createSampler'; descriptor: GPUSamplerDescriptor | undefined }
@@ -308,9 +300,8 @@ function makeDevice(captured: MockCapture[], failures: MockFailures): MockDevice
308
300
  // No-op; w6 unit tests assert the shim wiring + bounds error paths
309
301
  // before the call would reach the real GPU layer.
310
302
  },
311
- writeBuffer(buffer, bufferOffset, data, dataOffset, size): void {
303
+ writeBuffer(_buffer, _bufferOffset, _data, _dataOffset, _size): void {
312
304
  // No-op; bounds checks happen in the shim before this is reached.
313
- captured.push({ kind: 'writeBuffer', buffer, bufferOffset, data, dataOffset, size });
314
305
  },
315
306
  writeTexture(_destination, _data, _dataLayout, _size): void {
316
307
  // No-op; alignment validation happens in the shim before this is hit.
@@ -61,49 +61,6 @@ async function requestRhiDevice(): Promise<RhiDevice | undefined> {
61
61
  return deviceResult.value;
62
62
  }
63
63
 
64
- describe('dawn-real-gpu - queue.writeBuffer typed range', () => {
65
- it('reads back exactly the selected typed-array elements', async () => {
66
- const device = await requestRhiDevice();
67
- if (device === undefined) return;
68
- const targetResult = device.createBuffer({
69
- label: 'dawn-write-range-target',
70
- size: 32,
71
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
72
- });
73
- const readbackResult = device.createBuffer({
74
- label: 'dawn-write-range-readback',
75
- size: 32,
76
- usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
77
- });
78
- expect(targetResult.ok && readbackResult.ok).toBe(true);
79
- if (!targetResult.ok || !readbackResult.ok) return;
80
-
81
- const source = new Float32Array([10, 20, 30, 40, 50]);
82
- const write = device.queue.writeBuffer(targetResult.value, 0, source, 1, 2);
83
- expect(write.ok).toBe(true);
84
- if (!write.ok) return;
85
-
86
- const encoderResult = device.createCommandEncoder({ label: 'dawn-write-range-copy' });
87
- expect(encoderResult.ok).toBe(true);
88
- if (!encoderResult.ok) return;
89
- encoderResult.value.copyBufferToBuffer(targetResult.value, 0, readbackResult.value, 0, 32);
90
- const finish = encoderResult.value.finish();
91
- expect(finish.ok).toBe(true);
92
- if (!finish.ok) return;
93
- expect(device.queue.submit([finish.value]).ok).toBe(true);
94
- await device.queue.onSubmittedWorkDone();
95
-
96
- const mapped = await readbackResult.value.mapAsync(GPUMapMode.READ);
97
- expect(mapped.ok).toBe(true);
98
- if (!mapped.ok) return;
99
- const range = mapped.value.getMappedRange();
100
- expect(range.ok).toBe(true);
101
- if (!range.ok) return;
102
- expect(Array.from(new Float32Array(range.value.slice(0)))).toEqual([20, 30, 0, 0, 0, 0, 0, 0]);
103
- mapped.value.unmap();
104
- });
105
- });
106
-
107
64
  function reportTimestampEvidence(evidence: Record<string, unknown>): void {
108
65
  // biome-ignore lint/suspicious/noConsole: the dawn admission record is the test evidence output
109
66
  console.log(JSON.stringify(evidence));
@@ -685,7 +685,7 @@ import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gp
685
685
  const desc: RenderPipelineDescriptor = {
686
686
  label: 'rpp',
687
687
  layout: 'auto',
688
- vertex: { module: shader.value as unknown as GPUShaderModule, entryPoint: 'v' },
688
+ vertex: { module: shader.value, entryPoint: 'v', buffers: [] },
689
689
  fragment: undefined,
690
690
  };
691
691
  const out = device.createRenderPipeline(desc);
@@ -703,12 +703,17 @@ import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gp
703
703
  if (!r.ok) throw new Error('mock requestDevice failed');
704
704
  const device = r.value;
705
705
 
706
+ const shader = await createShaderModule(device, {
707
+ code: '@vertex fn v() -> @builtin(position) vec4f { return vec4f(0); }',
708
+ });
709
+ if (!shader.ok) throw new Error('mock shader creation failed');
706
710
  const out = device.createRenderPipeline({
707
711
  label: 'invalid-rpp',
708
712
  layout: 'auto',
709
713
  vertex: {
710
- module: {} as unknown as GPUShaderModule,
714
+ module: shader.value,
711
715
  entryPoint: 'v',
716
+ buffers: [],
712
717
  },
713
718
  fragment: undefined,
714
719
  });
@@ -1653,45 +1658,6 @@ import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gp
1653
1658
  expect(out.error.code).toBe('queue-write-buffer-out-of-bounds');
1654
1659
  }
1655
1660
  });
1656
-
1657
- it('interprets Float32Array dataOffset and size as elements for bounds', async () => {
1658
- const gpu = createMockGpu();
1659
- const r = await requestDevice({ gpu });
1660
- if (!r.ok) throw new Error('mock requestDevice should not fail');
1661
- const device = r.value as unknown as DeviceLike;
1662
- const bufResult = device.createBuffer({ size: 8, usage: 0 });
1663
- if (!bufResult.ok) throw new Error('createBuffer should succeed');
1664
- const data = new Float32Array(4);
1665
- const out = device.queue.writeBuffer(bufResult.value, 0, data, 1, 3);
1666
- expect(out.ok).toBe(false);
1667
- if (!out.ok) expect(out.error.code).toBe('queue-write-buffer-out-of-bounds');
1668
- });
1669
-
1670
- it('forwards a non-zero Float32 view and element range without byte reinterpretation', async () => {
1671
- const gpu = createMockGpu();
1672
- const r = await requestDevice({ gpu });
1673
- if (!r.ok) throw new Error('mock requestDevice should not fail');
1674
- const device = r.value as unknown as DeviceLike;
1675
- const bufResult = device.createBuffer({ size: 16, usage: 0 });
1676
- if (!bufResult.ok) throw new Error('createBuffer should succeed');
1677
- const source = new Float32Array([10, 20, 30, 40, 50]);
1678
- const view = source.subarray(1);
1679
- const out = device.queue.writeBuffer(bufResult.value, 0, view, 1, 2);
1680
- expect(out.ok).toBe(true);
1681
- const event = [...gpu.__captured].reverse().find((entry) => entry.kind === 'writeBuffer');
1682
- expect(event?.kind).toBe('writeBuffer');
1683
- if (event?.kind !== 'writeBuffer') return;
1684
- expect(event.data).toBe(view);
1685
- expect(event.dataOffset).toBe(1);
1686
- expect(event.size).toBe(2);
1687
- if (!ArrayBuffer.isView(event.data)) return;
1688
- const selected = new Uint8Array(
1689
- event.data.buffer,
1690
- event.data.byteOffset + (event.dataOffset ?? 0) * Float32Array.BYTES_PER_ELEMENT,
1691
- (event.size ?? 0) * Float32Array.BYTES_PER_ELEMENT,
1692
- );
1693
- expect([...selected]).toEqual([...new Uint8Array(source.buffer, 2 * 4, 8)]);
1694
- });
1695
1661
  });
1696
1662
 
1697
1663
  // ---------------------------------------------------------------------------
package/src/device.ts CHANGED
@@ -40,11 +40,13 @@ import type {
40
40
  ComputePassDescriptor,
41
41
  ComputePipeline,
42
42
  ComputePipelineDescriptor,
43
+ ExternalImageTextureDestination,
43
44
  MappedBuffer,
44
45
  PipelineLayout,
45
46
  PipelineLayoutDescriptor,
46
47
  QuerySet,
47
48
  QuerySetDescriptor,
49
+ RenderPassDescriptor,
48
50
  RenderPipeline,
49
51
  RenderPipelineDescriptor,
50
52
  Result,
@@ -64,8 +66,9 @@ import type {
64
66
  TextureDescriptor,
65
67
  TextureView,
66
68
  TextureViewDescriptor,
69
+ TextureWriteDestination,
67
70
  } from '@forgeax/engine-rhi';
68
- import { err, ok, RhiError as RhiErrorClass, resolveBufferWriteRange } from '@forgeax/engine-rhi';
71
+ import { err, ok, RhiError as RhiErrorClass } from '@forgeax/engine-rhi';
69
72
  import {
70
73
  commandEncoderFinished,
71
74
  queueSubmitFailed,
@@ -132,15 +135,6 @@ const BGL_KEYS = ['label', 'entries'] as const;
132
135
  // BG_KEYS removed in w20 — createBindGroup now uses tagged-union dispatch
133
136
  // over RhiBindingResource 4 kinds (not field-mirror passthrough).
134
137
  const PL_KEYS = ['label', 'bindGroupLayouts'] as const;
135
- const RPP_KEYS = [
136
- 'label',
137
- 'layout',
138
- 'vertex',
139
- 'primitive',
140
- 'depthStencil',
141
- 'multisample',
142
- 'fragment',
143
- ] as const;
144
138
  const ENC_KEYS = ['label'] as const;
145
139
  const TEXTURE_VIEW_KEYS = [
146
140
  'label',
@@ -174,6 +168,7 @@ const QUERY_SET_COUNT_LIMIT = 4096;
174
168
  */
175
169
  const RAW_DEVICE_MAP: WeakMap<RhiDevice, GPUDevice> = new WeakMap();
176
170
  const BUFFER_RAW_MAP: WeakMap<Buffer, GPUBuffer> = new WeakMap();
171
+ const TEXTURE_VIEW_RAW_MAP: WeakMap<TextureView, GPUTextureView> = new WeakMap();
177
172
  const ENCODER_STATE: WeakMap<RhiCommandEncoder, EncoderState> = new WeakMap();
178
173
  const PASS_STATE: WeakMap<RhiRenderPassEncoder, PassState> = new WeakMap();
179
174
  const COMMAND_BUFFER_RAW_MAP: WeakMap<CommandBuffer, GPUCommandBuffer> = new WeakMap();
@@ -271,7 +266,7 @@ const QUERY_RESOLVE_ALIGNMENT = 256;
271
266
  * The `_internal_` prefix + `@internal` JSDoc tag mark this as engine-internal
272
267
  * plumbing; the only sanctioned consumer is
273
268
  * `apps/hello/triangle/src/main.ts:96` which threads the raw GPUDevice into
274
- * `RendererOptions.rawDeviceForContextConfigure` so the canvas
269
+ * the host's internal canvas-device configuration slot so the canvas
275
270
  * `GPUCanvasContext.configure({device})` slot keeps working
276
271
  * (GPUCanvasContext is outside the RHI surface). Every other engine path
277
272
  * goes through the RHI interface.
@@ -510,12 +505,7 @@ function makeRenderPassEncoder(
510
505
  rawPass.setViewport(x, y, w, h, minDepth, maxDepth);
511
506
  },
512
507
  setScissorRect(x: number, y: number, w: number, h: number): void {
513
- const setScissorRect = (
514
- rawPass as unknown as {
515
- readonly setScissorRect?: (x: number, y: number, w: number, h: number) => void;
516
- }
517
- ).setScissorRect;
518
- setScissorRect?.call(rawPass, x, y, w, h);
508
+ rawPass.setScissorRect(x, y, w, h);
519
509
  },
520
510
  setBlendConstant(color: GPUColor): void {
521
511
  rawPass.setBlendConstant(color);
@@ -692,6 +682,123 @@ function throwIfFinished(state: EncoderState | undefined): void {
692
682
  }
693
683
  }
694
684
 
685
+ function rawTextureView(view: TextureView): GPUTextureView {
686
+ return TEXTURE_VIEW_RAW_MAP.get(view) ?? (view as unknown as GPUTextureView);
687
+ }
688
+
689
+ function mirrorRenderPassDescriptor(desc: RenderPassDescriptor): GPURenderPassDescriptor {
690
+ const colorAttachments: Array<GPURenderPassColorAttachment | null> = [];
691
+ for (const attachment of desc.colorAttachments) {
692
+ if (attachment === null || attachment === undefined) {
693
+ colorAttachments.push(null);
694
+ continue;
695
+ }
696
+ if (attachment.loadOp === undefined || attachment.storeOp === undefined) {
697
+ throw new TypeError('RHI render-pass color attachments require loadOp and storeOp');
698
+ }
699
+ colorAttachments.push({
700
+ view: rawTextureView(attachment.view),
701
+ ...(attachment.depthSlice === undefined ? {} : { depthSlice: attachment.depthSlice }),
702
+ ...(attachment.resolveTarget === undefined
703
+ ? {}
704
+ : { resolveTarget: rawTextureView(attachment.resolveTarget) }),
705
+ ...(attachment.clearValue === undefined ? {} : { clearValue: attachment.clearValue }),
706
+ loadOp: attachment.loadOp,
707
+ storeOp: attachment.storeOp,
708
+ });
709
+ }
710
+
711
+ return {
712
+ ...(desc.label === undefined ? {} : { label: desc.label }),
713
+ colorAttachments,
714
+ ...(desc.depthStencilAttachment === undefined
715
+ ? {}
716
+ : {
717
+ depthStencilAttachment: {
718
+ view: rawTextureView(desc.depthStencilAttachment.view),
719
+ ...(desc.depthStencilAttachment.depthClearValue === undefined
720
+ ? {}
721
+ : { depthClearValue: desc.depthStencilAttachment.depthClearValue }),
722
+ ...(desc.depthStencilAttachment.depthLoadOp === undefined
723
+ ? {}
724
+ : { depthLoadOp: desc.depthStencilAttachment.depthLoadOp }),
725
+ ...(desc.depthStencilAttachment.depthStoreOp === undefined
726
+ ? {}
727
+ : { depthStoreOp: desc.depthStencilAttachment.depthStoreOp }),
728
+ ...(desc.depthStencilAttachment.depthReadOnly === undefined
729
+ ? {}
730
+ : { depthReadOnly: desc.depthStencilAttachment.depthReadOnly }),
731
+ ...(desc.depthStencilAttachment.stencilClearValue === undefined
732
+ ? {}
733
+ : { stencilClearValue: desc.depthStencilAttachment.stencilClearValue }),
734
+ ...(desc.depthStencilAttachment.stencilLoadOp === undefined
735
+ ? {}
736
+ : { stencilLoadOp: desc.depthStencilAttachment.stencilLoadOp }),
737
+ ...(desc.depthStencilAttachment.stencilStoreOp === undefined
738
+ ? {}
739
+ : { stencilStoreOp: desc.depthStencilAttachment.stencilStoreOp }),
740
+ ...(desc.depthStencilAttachment.stencilReadOnly === undefined
741
+ ? {}
742
+ : { stencilReadOnly: desc.depthStencilAttachment.stencilReadOnly }),
743
+ },
744
+ }),
745
+ ...(desc.occlusionQuerySet === undefined
746
+ ? {}
747
+ : {
748
+ occlusionQuerySet:
749
+ QUERY_SET_RAW_MAP.get(desc.occlusionQuerySet) ??
750
+ (desc.occlusionQuerySet as unknown as GPUQuerySet),
751
+ }),
752
+ ...(desc.timestampWrites === undefined
753
+ ? {}
754
+ : {
755
+ timestampWrites: {
756
+ querySet:
757
+ QUERY_SET_RAW_MAP.get(desc.timestampWrites.querySet) ??
758
+ (desc.timestampWrites.querySet as unknown as GPUQuerySet),
759
+ ...(desc.timestampWrites.beginningOfPassWriteIndex === undefined
760
+ ? {}
761
+ : { beginningOfPassWriteIndex: desc.timestampWrites.beginningOfPassWriteIndex }),
762
+ ...(desc.timestampWrites.endOfPassWriteIndex === undefined
763
+ ? {}
764
+ : { endOfPassWriteIndex: desc.timestampWrites.endOfPassWriteIndex }),
765
+ },
766
+ }),
767
+ ...(desc.maxDrawCount === undefined ? {} : { maxDrawCount: desc.maxDrawCount }),
768
+ };
769
+ }
770
+
771
+ function mirrorRenderPipelineDescriptor(
772
+ desc: RenderPipelineDescriptor,
773
+ ): GPURenderPipelineDescriptor {
774
+ const vertex: GPUVertexState = {
775
+ module: desc.vertex.module as unknown as GPUShaderModule,
776
+ ...(desc.vertex.buffers === undefined ? {} : { buffers: Array.from(desc.vertex.buffers) }),
777
+ ...(desc.vertex.entryPoint === undefined ? {} : { entryPoint: desc.vertex.entryPoint }),
778
+ ...(desc.vertex.constants === undefined ? {} : { constants: desc.vertex.constants }),
779
+ };
780
+ const fragment: GPUFragmentState | undefined =
781
+ desc.fragment === undefined
782
+ ? undefined
783
+ : {
784
+ module: desc.fragment.module as unknown as GPUShaderModule,
785
+ targets: Array.from(desc.fragment.targets),
786
+ ...(desc.fragment.entryPoint === undefined
787
+ ? {}
788
+ : { entryPoint: desc.fragment.entryPoint }),
789
+ ...(desc.fragment.constants === undefined ? {} : { constants: desc.fragment.constants }),
790
+ };
791
+ return {
792
+ ...(desc.label === undefined ? {} : { label: desc.label }),
793
+ layout: desc.layout === 'auto' ? 'auto' : (desc.layout as unknown as GPUPipelineLayout),
794
+ vertex,
795
+ ...(desc.primitive === undefined ? {} : { primitive: desc.primitive }),
796
+ ...(desc.depthStencil === undefined ? {} : { depthStencil: desc.depthStencil }),
797
+ ...(desc.multisample === undefined ? {} : { multisample: desc.multisample }),
798
+ ...(fragment === undefined ? {} : { fragment }),
799
+ };
800
+ }
801
+
695
802
  /**
696
803
  * Build a RhiCommandEncoder around a raw GPUCommandEncoder (w3).
697
804
  *
@@ -736,17 +843,16 @@ function makeCommandEncoder(
736
843
  }
737
844
 
738
845
  const enc: RhiCommandEncoder = {
739
- beginRenderPass(desc: GPURenderPassDescriptor): RhiRenderPassEncoder {
846
+ beginRenderPass(desc: RenderPassDescriptor): RhiRenderPassEncoder {
740
847
  const state = ENCODER_STATE.get(enc);
741
848
  throwIfFinished(state);
742
- const rawPass = rawEncoder.beginRenderPass(desc);
849
+ const rawPass = rawEncoder.beginRenderPass(mirrorRenderPassDescriptor(desc));
743
850
  // Extract occlusionQuerySet from the descriptor (research §2.2:
744
851
  // RPDesc.occlusionQuerySet is the injection point; the render pass
745
852
  // PassState.[[occlusion_query_set]] mirrors it). The forgeax
746
853
  // RenderPassDescriptor declares occlusionQuerySet as `QuerySet |
747
854
  // undefined`; the raw GPU descriptor is structurally compatible.
748
- const occQs = (desc as unknown as { occlusionQuerySet?: unknown }).occlusionQuerySet;
749
- const occlusionQuerySet = occQs === undefined || occQs === null ? null : (occQs as QuerySet);
855
+ const occlusionQuerySet = desc.occlusionQuerySet ?? null;
750
856
  const pass = makeRenderPassEncoder(rawPass, enc, occlusionQuerySet);
751
857
  if (state !== undefined) state.activePass = pass;
752
858
  return pass;
@@ -1214,7 +1320,6 @@ function makeQueue(rawQueue: GPUQueue): RhiQueue {
1214
1320
  size?: number | undefined,
1215
1321
  ): Result<void, RhiError> {
1216
1322
  const rawBuffer = BUFFER_RAW_MAP.get(buffer) ?? (buffer as unknown as GPUBuffer);
1217
- const range = resolveBufferWriteRange(data, dataOffset, size);
1218
1323
  const bufferSize =
1219
1324
  typeof (rawBuffer as { size?: number }).size === 'number'
1220
1325
  ? (rawBuffer as { size: number }).size
@@ -1223,13 +1328,17 @@ function makeQueue(rawQueue: GPUQueue): RhiQueue {
1223
1328
  if (bufferOffset % 4 !== 0) {
1224
1329
  return queueWriteBufferOutOfBounds({
1225
1330
  offset: bufferOffset,
1226
- byteLength: range?.byteSize ?? 0,
1331
+ byteLength:
1332
+ data instanceof ArrayBuffer ? data.byteLength : (data as ArrayBufferView).byteLength,
1227
1333
  bufferSize,
1228
1334
  });
1229
1335
  }
1230
- const writeSize = range?.byteSize ?? 0;
1336
+ const dataByteLength =
1337
+ data instanceof ArrayBuffer ? data.byteLength : (data as ArrayBufferView).byteLength;
1338
+ const writeStart = dataOffset ?? 0;
1339
+ const writeSize = size ?? dataByteLength - writeStart;
1231
1340
  // Bounds validation: bufferOffset + writeSize <= bufferSize.
1232
- if (range === undefined || bufferOffset + writeSize > bufferSize) {
1341
+ if (bufferOffset + writeSize > bufferSize) {
1233
1342
  return queueWriteBufferOutOfBounds({
1234
1343
  offset: bufferOffset,
1235
1344
  byteLength: writeSize,
@@ -1242,15 +1351,15 @@ function makeQueue(rawQueue: GPUQueue): RhiQueue {
1242
1351
  rawBuffer,
1243
1352
  bufferOffset,
1244
1353
  data as GPUAllowSharedBufferSource,
1245
- range.sourceOffset,
1246
- range.sourceSize,
1354
+ writeStart,
1355
+ size,
1247
1356
  );
1248
1357
  } else if (dataOffset !== undefined) {
1249
1358
  rawQueue.writeBuffer(
1250
1359
  rawBuffer,
1251
1360
  bufferOffset,
1252
1361
  data as GPUAllowSharedBufferSource,
1253
- range.sourceOffset,
1362
+ writeStart,
1254
1363
  );
1255
1364
  } else {
1256
1365
  rawQueue.writeBuffer(rawBuffer, bufferOffset, data as GPUAllowSharedBufferSource);
@@ -1289,7 +1398,7 @@ function makeQueue(rawQueue: GPUQueue): RhiQueue {
1289
1398
  }
1290
1399
  },
1291
1400
  writeTexture(
1292
- destination: GPUTexelCopyTextureInfo,
1401
+ destination: TextureWriteDestination,
1293
1402
  data: ArrayBufferView | ArrayBuffer,
1294
1403
  dataLayout: GPUTexelCopyBufferLayout,
1295
1404
  size: GPUExtent3DStrict,
@@ -1309,8 +1418,14 @@ function makeQueue(rawQueue: GPUQueue): RhiQueue {
1309
1418
  // structural compatibility lets us forward verbatim with a cast.
1310
1419
  // The data + size casts cover @webgpu/types polymorphism that the
1311
1420
  // forgeax form normalises to the strict spec subset.
1421
+ const rawDestination: GPUTexelCopyTextureInfo = {
1422
+ texture: destination.texture as unknown as GPUTexture,
1423
+ };
1424
+ if (destination.mipLevel !== undefined) rawDestination.mipLevel = destination.mipLevel;
1425
+ if (destination.origin !== undefined) rawDestination.origin = destination.origin;
1426
+ if (destination.aspect !== undefined) rawDestination.aspect = destination.aspect;
1312
1427
  rawQueue.writeTexture(
1313
- destination,
1428
+ rawDestination,
1314
1429
  data as unknown as GPUAllowSharedBufferSource,
1315
1430
  dataLayout,
1316
1431
  size as unknown as GPUExtent3D,
@@ -1329,11 +1444,24 @@ function makeQueue(rawQueue: GPUQueue): RhiQueue {
1329
1444
  },
1330
1445
  copyExternalImageToTexture(
1331
1446
  source: GPUCopyExternalImageSourceInfo,
1332
- destination: GPUCopyExternalImageDestInfo,
1447
+ destination: ExternalImageTextureDestination,
1333
1448
  copySize: GPUExtent3DStrict,
1334
1449
  ): Result<void, RhiError> {
1335
1450
  try {
1336
- rawQueue.copyExternalImageToTexture(source, destination, copySize);
1451
+ rawQueue.copyExternalImageToTexture(
1452
+ source,
1453
+ {
1454
+ texture: destination.texture as unknown as GPUTexture,
1455
+ ...(destination.mipLevel === undefined ? {} : { mipLevel: destination.mipLevel }),
1456
+ ...(destination.origin === undefined ? {} : { origin: destination.origin }),
1457
+ ...(destination.aspect === undefined ? {} : { aspect: destination.aspect }),
1458
+ ...(destination.colorSpace === undefined ? {} : { colorSpace: destination.colorSpace }),
1459
+ ...(destination.premultipliedAlpha === undefined
1460
+ ? {}
1461
+ : { premultipliedAlpha: destination.premultipliedAlpha }),
1462
+ },
1463
+ copySize,
1464
+ );
1337
1465
  return ok(undefined);
1338
1466
  } catch (e) {
1339
1467
  const message = e instanceof Error ? e.message : String(e);
@@ -1538,7 +1666,9 @@ export function makeRhiDevice(rawDevice: GPUDevice): {
1538
1666
  const rawView = rawTexture.createView(
1539
1667
  mirror(desc, TEXTURE_VIEW_KEYS) as unknown as GPUTextureViewDescriptor,
1540
1668
  );
1541
- return ok(rawView as unknown as TextureView);
1669
+ const handle = rawView as unknown as TextureView;
1670
+ TEXTURE_VIEW_RAW_MAP.set(handle, rawView);
1671
+ return ok(handle);
1542
1672
  } catch (e) {
1543
1673
  const message = e instanceof Error ? e.message : String(e);
1544
1674
  return err(
@@ -1638,9 +1768,7 @@ export function makeRhiDevice(rawDevice: GPUDevice): {
1638
1768
  },
1639
1769
  createRenderPipeline(desc: RenderPipelineDescriptor): Result<RenderPipeline, RhiError> {
1640
1770
  try {
1641
- const out = rawDevice.createRenderPipeline(
1642
- mirror(desc, RPP_KEYS) as unknown as GPURenderPipelineDescriptor,
1643
- );
1771
+ const out = rawDevice.createRenderPipeline(mirrorRenderPipelineDescriptor(desc));
1644
1772
  return ok(out as unknown as RenderPipeline);
1645
1773
  } catch (e) {
1646
1774
  const message = e instanceof Error ? e.message : String(e);