Skip to content

Commit 7be93f6

Browse files
feat: Expose schema read-write APIs (#2411)
1 parent 687616e commit 7be93f6

8 files changed

Lines changed: 333 additions & 106 deletions

File tree

apps/typegpu-docs/src/content/docs/apis/buffers.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,11 @@ If you pass an unmapped buffer, the data will be written to the buffer using `GP
236236
If you passed your own buffer to the `root.createBuffer` function, you need to ensure it has the `GPUBufferUsage.COPY_DST` usage flag if you want to write to it using the `write` method.
237237
:::
238238

239+
:::tip
240+
To write to, patch, or read from an `ArrayBuffer` directly, TypeGPU provides [a dedicated API](/TypeGPU/apis/data-schemas/#arraybuffer-io) for that purpose.
241+
The same API is used internally for buffer writes.
242+
:::
243+
239244
### Permissive write inputs
240245

241246
`.write()` accepts several equivalent forms — you don't need to construct typed instances:

apps/typegpu-docs/src/content/docs/apis/data-schemas.mdx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,3 +577,49 @@ fn myFn(){
577577
s2.n = 1; // s1 is not modified
578578
}
579579
```
580+
581+
## ArrayBuffer IO
582+
583+
TypeGPU exports functions for reading and writing to a raw `ArrayBuffer`.
584+
Signatures are analogous to `buffer.write`, `buffer.patch` and `buffer.read`,
585+
although each one additionally requires a data schema.
586+
587+
```ts twoslash
588+
import { d, patchArrayBuffer, readFromArrayBuffer, writeToArrayBuffer } from 'typegpu';
589+
// ---cut---
590+
const buffer = new ArrayBuffer(16);
591+
592+
// update entire buffer
593+
writeToArrayBuffer(buffer, d.vec4u, d.vec4u(1, 2, 3, 4));
594+
```
595+
596+
```ts twoslash
597+
import { d, patchArrayBuffer, readFromArrayBuffer, writeToArrayBuffer } from 'typegpu';
598+
// ---cut---
599+
const buffer = new ArrayBuffer(64);
600+
const Numbers = d.arrayOf(d.u32, 16);
601+
602+
// update a slice
603+
const layout = d.memoryLayoutOf(Numbers, (a) => a[4]);
604+
writeToArrayBuffer(buffer, Numbers, [4, 5, 6, 7], { startOffset: layout.offset });
605+
```
606+
607+
```ts twoslash
608+
import { d, patchArrayBuffer, readFromArrayBuffer, writeToArrayBuffer } from 'typegpu';
609+
// ---cut---
610+
const buffer = new ArrayBuffer(64);
611+
const Boid = d.struct({ pos: d.vec2u, id: d.u32 });
612+
const Boids = d.arrayOf(Boid, 4);
613+
614+
// patch
615+
patchArrayBuffer(buffer, Boids, { 2: { pos: d.vec2u() } });
616+
```
617+
618+
```ts twoslash
619+
import { d, patchArrayBuffer, readFromArrayBuffer, writeToArrayBuffer } from 'typegpu';
620+
// ---cut---
621+
const buffer = new ArrayBuffer(64);
622+
623+
// read
624+
const mat = readFromArrayBuffer(buffer, d.mat4x4f);
625+
```

packages/typegpu/src/core/buffer/buffer.ts

Lines changed: 17 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import { BufferReader, BufferWriter, getSystemEndianness } from 'typed-binary';
21
import { getCompiledWriter } from '../../data/compiledIO.ts';
3-
import { readData, writeData } from '../../data/dataIO.ts';
42
import type { AnyData } from '../../data/dataTypes.ts';
5-
import {
6-
type WriteInstruction,
7-
convertPartialToPatch,
8-
getPatchInstructions,
9-
} from '../../data/partialIO.ts';
3+
import { convertPartialToPatch, getPatchInstructions } from '../../data/partialIO.ts';
104
import { sizeOf } from '../../data/sizeOf.ts';
115
import type { BaseData } from '../../data/wgslTypes.ts';
12-
import { isWgslArray, isWgslData } from '../../data/wgslTypes.ts';
6+
import { isWgslData } from '../../data/wgslTypes.ts';
137
import type { StorageFlag } from '../../extension.ts';
148
import type { TgpuNamable } from '../../shared/meta.ts';
159
import { getName, setName } from '../../shared/meta.ts';
@@ -37,8 +31,8 @@ import {
3731
type TgpuFixedBufferUsage,
3832
uniform,
3933
} from './bufferUsage.ts';
40-
import { alignmentOf } from '../../data/alignmentOf.ts';
41-
import { roundUp } from '../../mathUtils.ts';
34+
import { calculateOffsets, readFromArrayBuffer, writeToArrayBuffer } from '../../data/dataIO.ts';
35+
import { patchArrayBuffer } from '../../data/partialIO.ts';
4236

4337
// ----------
4438
// Public API
@@ -193,8 +187,6 @@ export function isUsableAsIndex<T extends TgpuBuffer<BaseData>>(
193187
// --------------
194188
// Implementation
195189
// --------------
196-
const endianness = getSystemEndianness();
197-
198190
class TgpuBufferImpl<TData extends BaseData> implements TgpuBuffer<TData> {
199191
readonly [$internal] = true;
200192
readonly resourceType = 'buffer';
@@ -261,7 +253,7 @@ class TgpuBufferImpl<TData extends BaseData> implements TgpuBuffer<TData> {
261253
if (this.#initialCallback) {
262254
this.#initialCallback(this);
263255
} else if (this.initial) {
264-
this.#writeToTarget(this.#getMappedRange(), this.initial);
256+
writeToArrayBuffer(this.#getMappedRange(), this.dataType, this.initial);
265257
}
266258
this.#unmapBuffer();
267259
}
@@ -354,77 +346,10 @@ class TgpuBufferImpl<TData extends BaseData> implements TgpuBuffer<TData> {
354346
getCompiledWriter(this.dataType);
355347
}
356348

357-
#writeToTarget(
358-
target: ArrayBuffer,
359-
data: InferInput<TData> | ArrayBuffer,
360-
options?: BufferWriteOptions,
361-
): void {
362-
const startOffset = options?.startOffset ?? 0;
363-
const endOffset = options?.endOffset ?? target.byteLength;
364-
365-
// Fast path: raw byte copy, user guarantees the padded layout
366-
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
367-
const src =
368-
data instanceof ArrayBuffer
369-
? new Uint8Array(data)
370-
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
371-
const regionSize = endOffset - startOffset;
372-
if (src.byteLength !== regionSize) {
373-
console.warn(
374-
`Buffer size mismatch: expected ${regionSize} bytes, got ${src.byteLength}. ` +
375-
(src.byteLength < regionSize ? 'Data truncated.' : 'Excess ignored.'),
376-
);
377-
}
378-
const copyLen = Math.min(src.byteLength, regionSize);
379-
new Uint8Array(target).set(src.subarray(0, copyLen), startOffset);
380-
return;
381-
}
382-
383-
const dataView = new DataView(target);
384-
const isLittleEndian = endianness === 'little';
385-
386-
const compiledWriter = getCompiledWriter(this.dataType);
387-
388-
if (compiledWriter) {
389-
try {
390-
compiledWriter(dataView, startOffset, data, isLittleEndian, endOffset);
391-
return;
392-
} catch (error) {
393-
console.error(
394-
`Error when using compiled writer for buffer ${
395-
getName(this) ?? '<unnamed>'
396-
} - this is likely a bug, please submit an issue at https://github.com/software-mansion/TypeGPU/issues\nUsing fallback writer instead.`,
397-
error,
398-
);
399-
}
400-
}
401-
402-
const writer = new BufferWriter(target);
403-
writer.seekTo(startOffset);
404-
writeData(writer, this.dataType, data as Infer<TData>);
405-
}
406-
407349
write(data: InferInput<TData>, options?: BufferWriteOptions): void;
408350
write(data: ArrayBuffer, options?: BufferWriteOptions): void;
409351
write(data: InferInput<TData> | ArrayBuffer, options?: BufferWriteOptions): void {
410352
const gpuBuffer = this.buffer;
411-
const bufferSize = sizeOf(this.dataType);
412-
const startOffset = options?.startOffset ?? 0;
413-
414-
let naturalSize: number | undefined = undefined;
415-
if (isWgslArray(this.dataType) && Array.isArray(data)) {
416-
const arrayData = data as unknown[];
417-
naturalSize =
418-
arrayData.length *
419-
roundUp(sizeOf(this.dataType.elementType), alignmentOf(this.dataType.elementType));
420-
} else if (ArrayBuffer.isView(data) || data instanceof ArrayBuffer) {
421-
naturalSize = data.byteLength;
422-
}
423-
const naturalEndOffset =
424-
naturalSize !== undefined ? Math.min(startOffset + naturalSize, bufferSize) : undefined;
425-
426-
const endOffset = options?.endOffset ?? naturalEndOffset ?? bufferSize;
427-
const size = endOffset - startOffset;
428353

429354
if (gpuBuffer.mapState === 'mapped') {
430355
const mapped = this.#getMappedRange();
@@ -433,43 +358,34 @@ class TgpuBufferImpl<TData extends BaseData> implements TgpuBuffer<TData> {
433358
// via arrayBuffer. Nothing to do here
434359
return;
435360
}
436-
this.#writeToTarget(mapped, data, options);
361+
writeToArrayBuffer(mapped, this.dataType, data, options);
437362
return;
438363
}
439364

440365
// If the caller already wrote directly into #hostBuffer via
441366
// arrayBuffer, skip the redundant copy, the data is already in place.
442367
if (!(data instanceof ArrayBuffer && data === this.#hostBuffer)) {
443-
this.#writeToTarget(this.#hostBuffer, data, options);
368+
writeToArrayBuffer(this.#hostBuffer, this.dataType, data, options);
444369
}
370+
371+
const { startOffset, endOffset } = calculateOffsets(options, this.dataType, data);
372+
const size = endOffset - startOffset;
373+
445374
this.#device.queue.writeBuffer(gpuBuffer, startOffset, this.#hostBuffer, startOffset, size);
446375
}
447376

448377
/** @deprecated Use {@link patch} instead. */
449378
public writePartial(data: InferPartial<TData>): void {
450-
this.#applyInstructions(
451-
getPatchInstructions(
452-
this.dataType,
453-
convertPartialToPatch(this.dataType, data),
454-
this.#hostBuffer,
455-
),
456-
);
379+
this.patch(convertPartialToPatch(this.dataType, data) as InferPatch<TData>);
457380
}
458381

459382
public patch(data: InferPatch<TData>): void {
460-
this.#applyInstructions(getPatchInstructions(this.dataType, data, this.#hostBuffer));
461-
}
462-
463-
#applyInstructions(instructions: WriteInstruction[]): void {
464383
const gpuBuffer = this.buffer;
465384

466385
if (gpuBuffer.mapState === 'mapped') {
467-
const mappedRange = this.#getMappedRange();
468-
const mappedView = new Uint8Array(mappedRange);
469-
for (const { data, gpuOffset } of instructions) {
470-
mappedView.set(data, gpuOffset);
471-
}
386+
patchArrayBuffer(this.#getMappedRange(), this.dataType, data);
472387
} else {
388+
const instructions = getPatchInstructions(this.dataType, data, this.#hostBuffer);
473389
for (const { data, gpuOffset } of instructions) {
474390
this.#device.queue.writeBuffer(gpuBuffer, gpuOffset, data);
475391
}
@@ -504,14 +420,12 @@ class TgpuBufferImpl<TData extends BaseData> implements TgpuBuffer<TData> {
504420
const gpuBuffer = this.buffer;
505421

506422
if (gpuBuffer.mapState === 'mapped') {
507-
const mapped = this.#getMappedRange();
508-
return readData(new BufferReader(mapped), this.dataType);
423+
return readFromArrayBuffer(this.#getMappedRange(), this.dataType);
509424
}
510425

511426
if (gpuBuffer.usage & GPUBufferUsage.MAP_READ) {
512427
await gpuBuffer.mapAsync(GPUMapMode.READ);
513-
const mapped = this.#getMappedRange();
514-
const res = readData(new BufferReader(mapped), this.dataType);
428+
const res = readFromArrayBuffer(this.#getMappedRange(), this.dataType);
515429
this.#unmapBuffer();
516430
return res;
517431
}
@@ -527,7 +441,7 @@ class TgpuBufferImpl<TData extends BaseData> implements TgpuBuffer<TData> {
527441
this.#device.queue.submit([commandEncoder.finish()]);
528442
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, sizeOf(this.dataType));
529443

530-
const res = readData(new BufferReader(stagingBuffer.getMappedRange()), this.dataType);
444+
const res = readFromArrayBuffer(stagingBuffer.getMappedRange(), this.dataType);
531445

532446
stagingBuffer.unmap();
533447
stagingBuffer.destroy();

packages/typegpu/src/data/dataIO.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import type { ISerialInput, ISerialOutput } from 'typed-binary';
2-
import type { Infer, InferRecord } from '../shared/repr.ts';
1+
import {
2+
BufferReader,
3+
BufferWriter,
4+
getSystemEndianness,
5+
type ISerialInput,
6+
type ISerialOutput,
7+
} from 'typed-binary';
8+
import type { Infer, InferInput, InferRecord } from '../shared/repr.ts';
39
import alignIO from './alignIO.ts';
410
import { alignmentOf, customAlignmentOf } from './alignmentOf.ts';
511
import type { AnyConcreteData, AnyData, Disarray, LooseDecorated, Unstruct } from './dataTypes.ts';
@@ -20,6 +26,11 @@ import {
2026
vec4u,
2127
} from './vector.ts';
2228
import type * as wgsl from './wgslTypes.ts';
29+
import { isWgslArray, type BaseData } from './wgslTypes.ts';
30+
import type { BufferWriteOptions } from '../core/buffer/buffer.ts';
31+
import { getCompiledWriter } from './compiledIO.ts';
32+
import { getName } from '../shared/meta.ts';
33+
import { roundUp } from '../mathUtils.ts';
2334

2435
type DataWriter<TSchema extends wgsl.BaseData> = (
2536
output: ISerialOutput,
@@ -788,3 +799,82 @@ export function readData<TData extends wgsl.BaseData>(
788799

789800
return reader(input, schema);
790801
}
802+
803+
const endianness = getSystemEndianness();
804+
805+
export function calculateOffsets<T extends BaseData>(
806+
options: BufferWriteOptions | undefined,
807+
schema: T,
808+
data: InferInput<T> | ArrayBuffer,
809+
): { startOffset: number; endOffset: number } {
810+
const bufferSize = sizeOf(schema);
811+
const startOffset = options?.startOffset ?? 0;
812+
let naturalSize: number | undefined = undefined;
813+
if (isWgslArray(schema) && Array.isArray(data)) {
814+
const arrayData = data as unknown[];
815+
naturalSize =
816+
arrayData.length * roundUp(sizeOf(schema.elementType), alignmentOf(schema.elementType));
817+
} else if (ArrayBuffer.isView(data) || data instanceof ArrayBuffer) {
818+
naturalSize = data.byteLength;
819+
}
820+
const naturalEndOffset =
821+
naturalSize !== undefined ? Math.min(startOffset + naturalSize, bufferSize) : undefined;
822+
823+
const endOffset = options?.endOffset ?? naturalEndOffset ?? bufferSize;
824+
825+
return { startOffset, endOffset };
826+
}
827+
828+
export function writeToArrayBuffer<T extends BaseData>(
829+
buffer: ArrayBuffer,
830+
schema: T,
831+
data: InferInput<T> | ArrayBuffer,
832+
options?: BufferWriteOptions,
833+
) {
834+
const { startOffset, endOffset } = calculateOffsets(options, schema, data);
835+
836+
// Fast path: raw byte copy, user guarantees the padded layout
837+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
838+
const src =
839+
data instanceof ArrayBuffer
840+
? new Uint8Array(data)
841+
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
842+
const regionSize = endOffset - startOffset;
843+
if (src.byteLength !== regionSize) {
844+
console.warn(
845+
`Buffer size mismatch: expected ${regionSize} bytes, got ${src.byteLength}. ` +
846+
(src.byteLength < regionSize ? 'Data truncated.' : 'Excess ignored.'),
847+
);
848+
}
849+
const copyLen = Math.min(src.byteLength, regionSize);
850+
new Uint8Array(buffer).set(src.subarray(0, copyLen), startOffset);
851+
return;
852+
}
853+
854+
const dataView = new DataView(buffer);
855+
const isLittleEndian = endianness === 'little';
856+
857+
const compiledWriter = getCompiledWriter(schema);
858+
859+
if (compiledWriter) {
860+
try {
861+
compiledWriter(dataView, startOffset, data, isLittleEndian, endOffset);
862+
return;
863+
} catch (error) {
864+
console.error(
865+
`Error when using compiled writer for data type '${
866+
schema.type
867+
}' (${getName(schema) ?? 'unnamed'}) - this is likely a bug, please submit an issue at https://github.com/software-mansion/TypeGPU/issues\nUsing fallback writer instead.`,
868+
error,
869+
);
870+
}
871+
}
872+
873+
const writer = new BufferWriter(buffer);
874+
writer.seekTo(startOffset);
875+
writeData(writer, schema, data as Infer<T>);
876+
}
877+
878+
export function readFromArrayBuffer<T extends BaseData>(buffer: ArrayBuffer, schema: T): Infer<T> {
879+
return readData(new BufferReader(buffer), schema);
880+
}

0 commit comments

Comments
 (0)