@forgeax/engine-rhi 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-rhi",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -26,7 +26,7 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "dependencies": {
29
- "@forgeax/engine-types": "0.1.3",
29
+ "@forgeax/engine-types": "0.1.6",
30
30
  "@webgpu/types": "^0.1.71"
31
31
  },
32
32
  "forgeax": {
@@ -34,13 +34,17 @@ import type {
34
34
  BindGroupLayoutDescriptor,
35
35
  BufferDescriptor,
36
36
  CommandEncoderDescriptor,
37
+ PipelineLayout,
37
38
  RenderPassColorAttachment,
38
39
  RenderPassDepthStencilAttachment,
39
40
  RenderPassDescriptor,
40
41
  RenderPassTimestampWrites,
41
42
  RenderPipelineDescriptor,
43
+ RenderPipelineFragmentState,
44
+ RenderPipelineVertexState,
42
45
  SamplerDescriptor,
43
46
  TextureDescriptor,
47
+ TextureFormat,
44
48
  TextureView,
45
49
  } from '../index';
46
50
 
@@ -140,18 +144,31 @@ describe('MVP-1.1 - 5 descriptors mirror @webgpu/types', () => {
140
144
  | 'multisample'
141
145
  | 'fragment';
142
146
  expectTypeOf<keyof RenderPipelineDescriptor>().toEqualTypeOf<RppKeys>();
143
- expectTypeOf<ValueOf<RenderPipelineDescriptor, 'vertex'>>().toEqualTypeOf<
144
- ValueOf<GPURenderPipelineDescriptor, 'vertex'>
145
- >();
146
- expectTypeOf<ValueOf<RenderPipelineDescriptor, 'fragment'>>().toEqualTypeOf<
147
- ValueOf<GPURenderPipelineDescriptor, 'fragment'>
148
- >();
147
+ expectTypeOf<
148
+ ValueOf<RenderPipelineDescriptor, 'vertex'>
149
+ >().toEqualTypeOf<RenderPipelineVertexState>();
150
+ expectTypeOf<
151
+ ValueOf<RenderPipelineDescriptor, 'fragment'>
152
+ >().toEqualTypeOf<RenderPipelineFragmentState>();
149
153
  expectTypeOf<ValueOf<RenderPipelineDescriptor, 'layout'>>().toEqualTypeOf<
150
- ValueOf<GPURenderPipelineDescriptor, 'layout'>
154
+ 'auto' | PipelineLayout
151
155
  >();
152
156
  });
153
157
  });
154
158
 
159
+ describe('M4 RHI descriptor mirror', () => {
160
+ it('uses the spec TextureFormat union for texture descriptors', () => {
161
+ expectTypeOf<NonNullable<TextureDescriptor['format']>>().toEqualTypeOf<TextureFormat>();
162
+ const invalid: TextureDescriptor = {
163
+ // @ts-expect-error -- RHI descriptors reject arbitrary format strings.
164
+ format: 'not-a-texture-format',
165
+ size: { width: 1, height: 1 },
166
+ usage: 0,
167
+ };
168
+ void invalid;
169
+ });
170
+ });
171
+
155
172
  // w8: 4 new descriptors (D-S5) - field NAMES align byte-for-byte with spec;
156
173
  // the `view` field TYPE is tightened to `Texture` (spec has
157
174
  // `GPUTexture | GPUTextureView`).
@@ -18,7 +18,7 @@
18
18
  // dataLayout is Pick<GPUTexelCopyBufferLayout, ...>.
19
19
  // (b) copyExternalImageToTexture(source, destination, copySize) where:
20
20
  // source is Pick<GPUCopyExternalImageSourceInfo, ...> (spec rename);
21
- // destination is Pick<GPUCopyExternalImageDestInfo, ...> (spec rename).
21
+ // destination is the opaque-handle ExternalImageTextureDestination.
22
22
  // (c) r12-lint Pick<GPU*Descriptor count >= 20 follow-on (verified by w22
23
23
  // gate, not this test-d).
24
24
  //
@@ -30,7 +30,16 @@
30
30
  /// <reference types="@webgpu/types" />
31
31
 
32
32
  import { describe, expectTypeOf, it } from 'vitest';
33
- import type { Buffer, Result, RhiError, RhiQueue, Texture, TextureView } from '../index';
33
+ import type {
34
+ Buffer,
35
+ ExternalImageTextureDestination,
36
+ Result,
37
+ RhiError,
38
+ RhiQueue,
39
+ Texture,
40
+ TextureView,
41
+ TextureWriteDestination,
42
+ } from '../index';
34
43
 
35
44
  /** Strict structural equality helper. */
36
45
  type Equals<X, Y> =
@@ -42,15 +51,11 @@ describe('AC-08 — RhiQueue.writeTexture Pick<spec> narrow', () => {
42
51
  expectTypeOf<Method>().toBeFunction();
43
52
  });
44
53
 
45
- it('writeTexture destination param strictly equals Pick<GPUTexelCopyTextureInfo, ...> (no extra fields)', () => {
54
+ it('writeTexture destination keeps the opaque Texture resource boundary', () => {
46
55
  type DestParam = Parameters<RhiQueue['writeTexture']>[0];
47
- type ExpectedShape = Pick<
48
- GPUTexelCopyTextureInfo,
49
- 'texture' | 'mipLevel' | 'origin' | 'aspect'
50
- >;
51
- // Strict equality — DestParam has EXACTLY the four spec fields. AI users
52
- // passing extra forgeax-invented fields trip TS2375 at call site;
53
- // missing required `texture` field trips TS2741.
56
+ type ExpectedShape = TextureWriteDestination;
57
+ // Strict equality — the descriptor has exactly the four spec fields, but
58
+ // its resource is the RHI-opaque Texture rather than GPUTexture.
54
59
  type IsExact = Equals<DestParam, ExpectedShape>;
55
60
  expectTypeOf<IsExact>().toEqualTypeOf<true>();
56
61
  });
@@ -82,13 +87,9 @@ describe('AC-08 — RhiQueue.copyExternalImageToTexture Pick<spec> narrow + spec
82
87
  expectTypeOf<IsExact>().toEqualTypeOf<true>();
83
88
  });
84
89
 
85
- it('destination param strictly equals Pick<GPUCopyExternalImageDestInfo, ...> (spec rename D-P5)', () => {
90
+ it('destination param keeps the RHI Texture opaque (D-P5)', () => {
86
91
  type DestParam = Parameters<RhiQueue['copyExternalImageToTexture']>[1];
87
- type ExpectedShape = Pick<
88
- GPUCopyExternalImageDestInfo,
89
- 'texture' | 'mipLevel' | 'origin' | 'aspect' | 'colorSpace' | 'premultipliedAlpha'
90
- >;
91
- type IsExact = Equals<DestParam, ExpectedShape>;
92
+ type IsExact = Equals<DestParam, ExternalImageTextureDestination>;
92
93
  expectTypeOf<IsExact>().toEqualTypeOf<true>();
93
94
  });
94
95
 
package/src/index.ts CHANGED
@@ -56,7 +56,7 @@ import type {
56
56
  FilterMode,
57
57
  TextureFormat,
58
58
  } from '@forgeax/engine-types';
59
- import { err, ok, type Result, RhiError } from './errors';
59
+ import type { Result, RhiError } from './errors';
60
60
 
61
61
  // ============================================================================
62
62
  // 14 opaque handles (MVP-1.3)
@@ -211,6 +211,25 @@ export interface Texture {
211
211
  readonly [RhiTextureBrand]: void;
212
212
  }
213
213
 
214
+ /** RHI-owned destination for queue.writeTexture. The resource handle remains opaque. */
215
+ export interface TextureWriteDestination {
216
+ readonly texture: Texture;
217
+ readonly mipLevel?: number | undefined;
218
+ readonly origin?: GPUOrigin3D | undefined;
219
+ readonly aspect?: GPUTextureAspect | undefined;
220
+ }
221
+
222
+ /** RHI-owned destination for queue.copyExternalImageToTexture. */
223
+ export type ExternalImageTextureDestination = Omit<
224
+ Pick<
225
+ GPUCopyExternalImageDestInfo,
226
+ 'texture' | 'mipLevel' | 'origin' | 'aspect' | 'colorSpace' | 'premultipliedAlpha'
227
+ >,
228
+ 'texture'
229
+ > & {
230
+ readonly texture: Texture;
231
+ };
232
+
214
233
  declare const RhiTextureViewBrand: unique symbol;
215
234
  /** GPU texture view opaque handle. */
216
235
  export interface TextureView {
@@ -588,14 +607,36 @@ export type PipelineLayoutDescriptor = ExplicitUndefined<
588
607
  bindGroupLayouts: Iterable<BindGroupLayout>;
589
608
  };
590
609
 
591
- /** GPU render pipeline descriptor. Field set strictly matches
592
- * GPURenderPipelineDescriptor. */
610
+ /** GPU render pipeline vertex stage with an opaque forgeax shader module. */
611
+ export type RenderPipelineVertexState = ExplicitUndefined<
612
+ Omit<GPUVertexState, 'module' | 'buffers'>
613
+ > & {
614
+ module: ShaderModule;
615
+ buffers: NonNullable<GPUVertexState['buffers']>;
616
+ };
617
+
618
+ /** GPU render pipeline fragment stage with an opaque forgeax shader module. */
619
+ export type RenderPipelineFragmentState = ExplicitUndefined<
620
+ Omit<GPUFragmentState, 'module' | 'targets'>
621
+ > & {
622
+ module: ShaderModule;
623
+ targets: NonNullable<GPUFragmentState['targets']>;
624
+ };
625
+
626
+ /** GPU render pipeline descriptor with opaque forgeax layout and shader handles. */
593
627
  export type RenderPipelineDescriptor = ExplicitUndefined<
594
- Pick<
595
- GPURenderPipelineDescriptor,
596
- 'label' | 'layout' | 'vertex' | 'primitive' | 'depthStencil' | 'multisample' | 'fragment'
628
+ Omit<
629
+ Pick<
630
+ GPURenderPipelineDescriptor,
631
+ 'label' | 'layout' | 'vertex' | 'primitive' | 'depthStencil' | 'multisample' | 'fragment'
632
+ >,
633
+ 'layout' | 'vertex' | 'fragment'
597
634
  >
598
- >;
635
+ > & {
636
+ layout: 'auto' | PipelineLayout;
637
+ vertex: RenderPipelineVertexState;
638
+ fragment?: RenderPipelineFragmentState | undefined;
639
+ };
599
640
 
600
641
  // ============================================================================
601
642
  // 4 new descriptors (feat-20260508-rhi-surface-completion w8 / D-S5,
@@ -1606,14 +1647,7 @@ export interface RhiDevice {
1606
1647
  /** GPU command queue - writeBuffer / submit + M5 writeTexture /
1607
1648
  * copyExternalImageToTexture / onSubmittedWorkDone. */
1608
1649
  export interface RhiQueue {
1609
- /**
1610
- * Direct write to a buffer (POD + ArrayBufferView, math-free).
1611
- *
1612
- * For typed-array sources, dataOffset and size are element counts, matching
1613
- * GPUQueue.writeBuffer. For ArrayBuffer and DataView sources they are byte
1614
- * counts. Every backend and the debug tape normalize the selected source
1615
- * bytes through resolveBufferWriteRange before validation or recording.
1616
- */
1650
+ /** Direct write to a buffer (POD + ArrayBufferView, math-free). */
1617
1651
  writeBuffer(
1618
1652
  buffer: Buffer,
1619
1653
  bufferOffset: number,
@@ -1643,7 +1677,7 @@ export interface RhiQueue {
1643
1677
  * }
1644
1678
  */
1645
1679
  writeTexture(
1646
- destination: Pick<GPUTexelCopyTextureInfo, 'texture' | 'mipLevel' | 'origin' | 'aspect'>,
1680
+ destination: TextureWriteDestination,
1647
1681
  data: ArrayBufferView | ArrayBuffer,
1648
1682
  dataLayout: Pick<GPUTexelCopyBufferLayout, 'offset' | 'bytesPerRow' | 'rowsPerImage'>,
1649
1683
  size: GPUExtent3DStrict,
@@ -1662,10 +1696,7 @@ export interface RhiQueue {
1662
1696
  */
1663
1697
  copyExternalImageToTexture(
1664
1698
  source: Pick<GPUCopyExternalImageSourceInfo, 'source' | 'origin' | 'flipY'>,
1665
- destination: Pick<
1666
- GPUCopyExternalImageDestInfo,
1667
- 'texture' | 'mipLevel' | 'origin' | 'aspect' | 'colorSpace' | 'premultipliedAlpha'
1668
- >,
1699
+ destination: ExternalImageTextureDestination,
1669
1700
  copySize: GPUExtent3DStrict,
1670
1701
  ): Result<void, RhiError>;
1671
1702
  /** Submit command buffers (single-use). */
@@ -1721,7 +1752,7 @@ export interface RhiCommandEncoder {
1721
1752
  * @example
1722
1753
  * const pass = encoder.beginRenderPass({ colorAttachments: [{ ... }] });
1723
1754
  */
1724
- beginRenderPass(desc: GPURenderPassDescriptor): RhiRenderPassEncoder;
1755
+ beginRenderPass(desc: RenderPassDescriptor): RhiRenderPassEncoder;
1725
1756
  /** Begin compute pass.
1726
1757
  *
1727
1758
  * Spec anchor: [@webgpu/types.GPUCommandEncoder.beginComputePass].
@@ -2156,157 +2187,6 @@ export { err, ok, RhiError, validateDrawArgs } from './errors';
2156
2187
  // re-export common descriptor-related aliases for single-entry consumption.
2157
2188
  export type { AddressMode, CompareFunction, FilterMode, TextureFormat };
2158
2189
 
2159
- export interface Rgba16floatRenderTargetProbe {
2160
- readonly format: 'rgba16float';
2161
- readonly pixel: readonly [number, number, number, number];
2162
- readonly timestampQuery: boolean;
2163
- }
2164
-
2165
- function halfToFloat(value: number): number {
2166
- const sign = (value & 0x8000) === 0 ? 1 : -1;
2167
- const exponent = (value >>> 10) & 0x1f;
2168
- const mantissa = value & 0x03ff;
2169
- if (exponent === 0) return sign * 2 ** -14 * (mantissa / 1024);
2170
- if (exponent === 0x1f) return mantissa === 0 ? sign * Number.POSITIVE_INFINITY : Number.NaN;
2171
- return sign * 2 ** (exponent - 15) * (1 + mantissa / 1024);
2172
- }
2173
-
2174
- /** Execute an rgba16float attachment clear and texture-to-buffer readback on a live device. */
2175
- export async function probeRgba16floatRenderTarget(
2176
- device: RhiDevice,
2177
- ): Promise<Result<Rgba16floatRenderTargetProbe, RhiError>> {
2178
- if (!device.caps.rgba16floatRenderable) {
2179
- return err(
2180
- new RhiError({
2181
- code: 'feature-not-enabled',
2182
- expected: 'device.caps.rgba16floatRenderable is true',
2183
- hint: 'select a device that can render rgba16float attachments',
2184
- }),
2185
- );
2186
- }
2187
- const texture = device.createTexture({
2188
- label: 'rgba16float-render-target-probe',
2189
- size: { width: 1, height: 1, depthOrArrayLayers: 1 },
2190
- mipLevelCount: 1,
2191
- sampleCount: 1,
2192
- dimension: '2d',
2193
- format: 'rgba16float',
2194
- usage: 0x10 | 0x01,
2195
- textureBindingViewDimension: undefined,
2196
- viewFormats: [],
2197
- });
2198
- if (!texture.ok) return texture;
2199
- const readback = device.createBuffer({
2200
- label: 'rgba16float-render-target-probe-readback',
2201
- size: 256,
2202
- usage: 0x01 | 0x08,
2203
- mappedAtCreation: false,
2204
- });
2205
- if (!readback.ok) {
2206
- device.destroyTexture(texture.value);
2207
- return readback;
2208
- }
2209
- const view = device.createTextureView(texture.value, {});
2210
- if (!view.ok) {
2211
- device.destroyBuffer(readback.value);
2212
- device.destroyTexture(texture.value);
2213
- return view;
2214
- }
2215
- const encoder = device.createCommandEncoder({ label: 'rgba16float-render-target-probe' });
2216
- if (!encoder.ok) {
2217
- device.destroyBuffer(readback.value);
2218
- device.destroyTexture(texture.value);
2219
- return encoder;
2220
- }
2221
- try {
2222
- const pass = encoder.value.beginRenderPass({
2223
- label: 'rgba16float-render-target-probe',
2224
- colorAttachments: [
2225
- {
2226
- view: view.value as unknown as GPUTextureView,
2227
- clearValue: { r: 0.25, g: 0.5, b: 0.75, a: 1 },
2228
- loadOp: 'clear',
2229
- storeOp: 'store',
2230
- },
2231
- ],
2232
- });
2233
- pass.end();
2234
- encoder.value.copyTextureToBuffer(
2235
- {
2236
- texture: texture.value as unknown as GPUTexture,
2237
- mipLevel: 0,
2238
- origin: { x: 0, y: 0, z: 0 },
2239
- },
2240
- {
2241
- buffer: readback.value as unknown as GPUBuffer,
2242
- offset: 0,
2243
- bytesPerRow: 256,
2244
- rowsPerImage: 1,
2245
- },
2246
- { width: 1, height: 1, depthOrArrayLayers: 1 },
2247
- );
2248
- } catch (cause) {
2249
- device.destroyBuffer(readback.value);
2250
- device.destroyTexture(texture.value);
2251
- return err(
2252
- new RhiError({
2253
- code: 'webgpu-runtime-error',
2254
- expected: 'rgba16float render pass and texture-to-buffer copy encode successfully',
2255
- hint: cause instanceof Error ? cause.message : String(cause),
2256
- }),
2257
- );
2258
- }
2259
- const command = encoder.value.finish();
2260
- if (!command.ok) {
2261
- device.destroyBuffer(readback.value);
2262
- device.destroyTexture(texture.value);
2263
- return command;
2264
- }
2265
- const submitted = device.queue.submit([command.value]);
2266
- if (!submitted.ok) {
2267
- device.destroyBuffer(readback.value);
2268
- device.destroyTexture(texture.value);
2269
- return submitted;
2270
- }
2271
- try {
2272
- await device.queue.onSubmittedWorkDone();
2273
- } catch (cause) {
2274
- device.destroyBuffer(readback.value);
2275
- device.destroyTexture(texture.value);
2276
- return err(
2277
- new RhiError({
2278
- code: 'webgpu-runtime-error',
2279
- expected: 'rgba16float probe submission completes',
2280
- hint: cause instanceof Error ? cause.message : String(cause),
2281
- }),
2282
- );
2283
- }
2284
- const mapped = await readback.value.mapAsync(0x01);
2285
- if (!mapped.ok) {
2286
- device.destroyBuffer(readback.value);
2287
- device.destroyTexture(texture.value);
2288
- return mapped;
2289
- }
2290
- const range = mapped.value.getMappedRange(0, 8);
2291
- if (!range.ok) {
2292
- mapped.value.unmap();
2293
- device.destroyBuffer(readback.value);
2294
- device.destroyTexture(texture.value);
2295
- return range;
2296
- }
2297
- const words = new Uint16Array(range.value.slice(0));
2298
- const pixel = [
2299
- halfToFloat(words[0] ?? 0),
2300
- halfToFloat(words[1] ?? 0),
2301
- halfToFloat(words[2] ?? 0),
2302
- halfToFloat(words[3] ?? 0),
2303
- ] as const;
2304
- mapped.value.unmap();
2305
- device.destroyBuffer(readback.value);
2306
- device.destroyTexture(texture.value);
2307
- return ok({ format: 'rgba16float', pixel, timestampQuery: device.caps.timestampQuery });
2308
- }
2309
-
2310
2190
  // ============================================================================
2311
2191
  // Descriptor builder helpers (feat-20260612-point-light-shadows-urp-hdrp M0)
2312
2192
  // ============================================================================
@@ -2361,8 +2241,6 @@ export function comparisonSamplerDescriptor(): SamplerDescriptor {
2361
2241
  };
2362
2242
  }
2363
2243
 
2364
- export { type BufferWriteRange, resolveBufferWriteRange } from './queue-writes';
2365
-
2366
2244
  /**
2367
2245
  * Build a TextureViewDescriptor to view a single cube layer + face as a 2D
2368
2246
  * depth attachment during shadow-caster rendering.
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=queue-writes.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"queue-writes.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/queue-writes.unit.test.ts"],"names":[],"mappings":""}
@@ -1,20 +0,0 @@
1
- /**
2
- * Resolve the source range of a queue.writeBuffer call.
3
- *
4
- * WebGPU defines dataOffset and size in elements for typed-array sources, but
5
- * in bytes for ArrayBuffer and DataView sources. Keeping that conversion in
6
- * the shared RHI package makes the browser, native, and debug paths agree.
7
- */
8
- export interface BufferWriteRange {
9
- readonly sourceOffset: number;
10
- readonly sourceSize: number;
11
- readonly byteOffset: number;
12
- readonly byteSize: number;
13
- readonly bytes: Uint8Array;
14
- }
15
- /**
16
- * Return the exact source bytes selected by a queue.writeBuffer range.
17
- * Undefined means the supplied element range is invalid.
18
- */
19
- export declare function resolveBufferWriteRange(data: ArrayBufferView | ArrayBuffer, dataOffset?: number, size?: number): BufferWriteRange | undefined;
20
- //# sourceMappingURL=queue-writes.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"queue-writes.d.ts","sourceRoot":"","sources":["../src/queue-writes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAeD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,eAAe,GAAG,WAAW,EACnC,UAAU,CAAC,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,MAAM,GACZ,gBAAgB,GAAG,SAAS,CAyB9B"}
@@ -1,30 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { resolveBufferWriteRange } from '../queue-writes';
4
-
5
- describe('resolveBufferWriteRange', () => {
6
- it('uses typed-array elements and preserves a non-zero view byteOffset', () => {
7
- const source = new Float32Array([10, 20, 30, 40, 50]);
8
- const view = source.subarray(1);
9
- const range = resolveBufferWriteRange(view, 1, 2);
10
-
11
- expect(range).toBeDefined();
12
- expect(range?.byteOffset).toBe(4);
13
- expect(range?.byteSize).toBe(8);
14
- expect([...((range?.bytes ?? new Uint8Array()) as Uint8Array)]).toEqual([
15
- ...new Uint8Array(view.buffer, view.byteOffset + 4, 8),
16
- ]);
17
- });
18
-
19
- it('uses bytes for ArrayBuffer and DataView sources', () => {
20
- const source = new ArrayBuffer(8);
21
- const view = new DataView(source, 2, 4);
22
- expect(resolveBufferWriteRange(source, 2, 3)?.byteSize).toBe(3);
23
- expect(resolveBufferWriteRange(view, 1, 2)?.byteOffset).toBe(1);
24
- expect(resolveBufferWriteRange(view, 1, 2)?.byteSize).toBe(2);
25
- });
26
-
27
- it('rejects an element range outside the source view', () => {
28
- expect(resolveBufferWriteRange(new Float32Array(2), 1, 2)).toBeUndefined();
29
- });
30
- });
@@ -1,62 +0,0 @@
1
- /**
2
- * Resolve the source range of a queue.writeBuffer call.
3
- *
4
- * WebGPU defines dataOffset and size in elements for typed-array sources, but
5
- * in bytes for ArrayBuffer and DataView sources. Keeping that conversion in
6
- * the shared RHI package makes the browser, native, and debug paths agree.
7
- */
8
- export interface BufferWriteRange {
9
- readonly sourceOffset: number;
10
- readonly sourceSize: number;
11
- readonly byteOffset: number;
12
- readonly byteSize: number;
13
- readonly bytes: Uint8Array;
14
- }
15
-
16
- function elementSize(data: ArrayBufferView | ArrayBuffer): number {
17
- if (data instanceof ArrayBuffer) return 1;
18
- const bytesPerElement = (data as ArrayBufferView & { BYTES_PER_ELEMENT?: number })
19
- .BYTES_PER_ELEMENT;
20
- return typeof bytesPerElement === 'number' && bytesPerElement > 0 ? bytesPerElement : 1;
21
- }
22
-
23
- function sourceByteView(data: ArrayBufferView | ArrayBuffer): Uint8Array {
24
- return data instanceof ArrayBuffer
25
- ? new Uint8Array(data)
26
- : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
27
- }
28
-
29
- /**
30
- * Return the exact source bytes selected by a queue.writeBuffer range.
31
- * Undefined means the supplied element range is invalid.
32
- */
33
- export function resolveBufferWriteRange(
34
- data: ArrayBufferView | ArrayBuffer,
35
- dataOffset?: number,
36
- size?: number,
37
- ): BufferWriteRange | undefined {
38
- const bytes = sourceByteView(data);
39
- const bytesPerElement = elementSize(data);
40
- const elementCount = bytes.byteLength / bytesPerElement;
41
- const sourceOffset = dataOffset ?? 0;
42
- const sourceSize = size ?? elementCount - sourceOffset;
43
- if (
44
- !Number.isInteger(sourceOffset) ||
45
- !Number.isInteger(sourceSize) ||
46
- sourceOffset < 0 ||
47
- sourceSize < 0 ||
48
- sourceOffset > elementCount ||
49
- sourceSize > elementCount - sourceOffset
50
- ) {
51
- return undefined;
52
- }
53
- const byteOffset = sourceOffset * bytesPerElement;
54
- const byteSize = sourceSize * bytesPerElement;
55
- return {
56
- sourceOffset,
57
- sourceSize,
58
- byteOffset,
59
- byteSize,
60
- bytes: bytes.subarray(byteOffset, byteOffset + byteSize),
61
- };
62
- }