From 615b4d81d7f0f1e180c0f077820431233eb1e5e4 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 7 Aug 2025 12:41:07 +0200 Subject: [PATCH 01/13] Make unstructs callable --- packages/typegpu/src/data/unstruct.ts | 19 +++++-- packages/typegpu/src/data/utils.ts | 23 +++++++-- packages/typegpu/tests/unstruct.test.ts | 67 ++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/packages/typegpu/src/data/unstruct.ts b/packages/typegpu/src/data/unstruct.ts index e892b8b29f..012a5d1765 100644 --- a/packages/typegpu/src/data/unstruct.ts +++ b/packages/typegpu/src/data/unstruct.ts @@ -1,6 +1,7 @@ import { getName, setName } from '../shared/meta.ts'; import { $internal } from '../shared/symbols.ts'; import type { Unstruct } from './dataTypes.ts'; +import { schemaCloneWrapper, schemaDefaultWrapper } from './utils.ts'; import type { BaseData } from './wgslTypes.ts'; // ---------- @@ -28,11 +29,21 @@ import type { BaseData } from './wgslTypes.ts'; export function unstruct>( properties: TProps, ): Unstruct { - const unstruct = (props: T) => props; - Object.setPrototypeOf(unstruct, UnstructImpl); - unstruct.propTypes = properties; + // In the schema call, create and return a deep copy + // by wrapping all the values in corresponding schema calls. + const unstructSchema = (instanceProps?: TProps) => + Object.fromEntries( + Object.entries(properties).map(([key, schema]) => [ + key, + instanceProps + ? schemaCloneWrapper(schema, instanceProps[key]) + : schemaDefaultWrapper(schema), + ]), + ); + Object.setPrototypeOf(unstructSchema, UnstructImpl); + unstructSchema.propTypes = properties; - return unstruct as unknown as Unstruct; + return unstructSchema as unknown as Unstruct; } // -------------- diff --git a/packages/typegpu/src/data/utils.ts b/packages/typegpu/src/data/utils.ts index 4a50780aa8..45c33532ff 100644 --- a/packages/typegpu/src/data/utils.ts +++ b/packages/typegpu/src/data/utils.ts @@ -1,12 +1,20 @@ +import { formatToWGSLType } from './vertexFormatData'; + /** * A wrapper for `schema(item)` call. * Throws an error if the schema is not callable. */ export function schemaCloneWrapper(schema: unknown, item: T): T { + const maybeType = (schema as { type: string })?.type; + try { - return (schema as unknown as ((item: T) => T))(item); - } catch { - const maybeType = (schema as { type: string })?.type; + // TgpuVertexFormatData are not callable + const cloningSchema = maybeType in formatToWGSLType + ? formatToWGSLType[maybeType as keyof typeof formatToWGSLType] + : schema; + return (cloningSchema as unknown as ((item: T) => T))(item); + } catch (e) { + console.log(e); throw new Error( `Schema of type ${ maybeType ?? '' @@ -20,10 +28,15 @@ export function schemaCloneWrapper(schema: unknown, item: T): T { * Throws an error if the schema is not callable. */ export function schemaDefaultWrapper(schema: unknown): T { + const maybeType = (schema as { type: string })?.type; + try { - return (schema as unknown as (() => T))(); + // TgpuVertexFormatData are not callable + const cloningSchema = maybeType in formatToWGSLType + ? formatToWGSLType[maybeType as keyof typeof formatToWGSLType] + : schema; + return (cloningSchema as unknown as (() => T))(); } catch { - const maybeType = (schema as { type: string })?.type; throw new Error( `Schema of type ${maybeType ?? ''} is not callable.`, ); diff --git a/packages/typegpu/tests/unstruct.test.ts b/packages/typegpu/tests/unstruct.test.ts index 3ff3e543c3..a54f02f81d 100644 --- a/packages/typegpu/tests/unstruct.test.ts +++ b/packages/typegpu/tests/unstruct.test.ts @@ -1,5 +1,5 @@ import { BufferReader, BufferWriter } from 'typed-binary'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, expectTypeOf, it } from 'vitest'; import { readData, writeData } from '../src/data/dataIO.ts'; import * as d from '../src/data/index.ts'; @@ -234,4 +234,69 @@ describe('d.unstruct', () => { expect(data.c.x).toBeCloseTo(-0.25); expect(data.c.y).toBeCloseTo(0.25); }); + + it('can be called to create an object', () => { + const TestStruct = d.unstruct({ + x: d.u32, + y: d.uint32x3, + }); + + const obj = TestStruct({ x: 1, y: d.vec3u(1, 2, 3) }); + + expect(obj).toStrictEqual({ x: 1, y: d.vec3u(1, 2, 3) }); + expectTypeOf(obj).toEqualTypeOf<{ x: number; y: d.v3u }>(); + }); + + it('cannot be called with invalid properties', () => { + const TestStruct = d.unstruct({ + x: d.u32, + y: d.uint32x3, + }); + + // @ts-expect-error + (() => TestStruct({ x: 1, z: 2 })); + }); + + it('can be called to create a deep copy of other struct', () => { + const schema = d.unstruct({ + nested: d.unstruct({ prop1: d.float32x2, prop2: d.u32 }), + }); + const instance = schema({ nested: { prop1: d.vec2f(1, 2), prop2: 21 } }); + + const clone = schema(instance); + + expect(clone).toStrictEqual(instance); + expect(clone).not.toBe(instance); + expect(clone.nested).not.toBe(instance.nested); + expect(clone.nested.prop1).not.toBe(instance.nested.prop1); + }); + + it('can be called to strip extra properties of a struct', () => { + const schema = d.unstruct({ prop1: d.vec2f, prop2: d.u32 }); + const instance = { prop1: d.vec2f(1, 2), prop2: 21, prop3: 'extra' }; + + const clone = schema(instance); + + expect(clone).toStrictEqual({ prop1: d.vec2f(1, 2), prop2: 21 }); + }); + + it('can be called to create a default value', () => { + const schema = d.unstruct({ + nested: d.unstruct({ prop1: d.vec2f, prop2: d.u32 }), + }); + + const defaultStruct = schema(); + + expect(defaultStruct).toStrictEqual({ + nested: { prop1: d.vec2f(), prop2: d.u32() }, + }); + }); + + // it('can be called to create a default value with nested array', () => { + // const schema = d.unstruct({ arr: d.disarrayOf(d.u32, 1) }); + + // const defaultStruct = schema(); + + // expect(defaultStruct).toStrictEqual({ arr: [0] }); + // }); }); From e3e328910c13b78edd8d36a99c23745c9dfa293c Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 7 Aug 2025 12:44:21 +0200 Subject: [PATCH 02/13] Update unstruct type --- packages/typegpu/src/data/dataTypes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/typegpu/src/data/dataTypes.ts b/packages/typegpu/src/data/dataTypes.ts index 76949514bd..de61d0fdde 100644 --- a/packages/typegpu/src/data/dataTypes.ts +++ b/packages/typegpu/src/data/dataTypes.ts @@ -72,6 +72,7 @@ export interface Unstruct< TProps extends Record = any, > extends wgsl.BaseData, TgpuNamable { (props: Prettify>): Prettify>; + (): Prettify>; readonly type: 'unstruct'; readonly propTypes: TProps; From 414a45b0e585313398a40f3e0c03f076a723df99 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 7 Aug 2025 12:52:07 +0200 Subject: [PATCH 03/13] Refactor unstruct tests --- packages/typegpu/tests/unstruct.test.ts | 39 +++++++++++++------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/typegpu/tests/unstruct.test.ts b/packages/typegpu/tests/unstruct.test.ts index a54f02f81d..a91193c82a 100644 --- a/packages/typegpu/tests/unstruct.test.ts +++ b/packages/typegpu/tests/unstruct.test.ts @@ -133,7 +133,7 @@ describe('d.unstruct', () => { expect(data.c.z).toBeCloseTo(3.0); }); - it('properly writes and reads data with nested structs', () => { + it('properly writes and reads data with nested unstructs', () => { const s = d.unstruct({ a: d.unorm8x2, b: d.align(16, d.snorm16x2), @@ -181,7 +181,7 @@ describe('d.unstruct', () => { const a = d.disarrayOf(s, 8); expect(d.sizeOf(s)).toBe(12); - // since the struct is aligned to 16 bytes, the array stride should be 16 not 12 + // since the unstruct is aligned to 16 bytes, the array stride should be 16 not 12 expect(d.sizeOf(a)).toBe(16 * 8); const buffer = new ArrayBuffer(d.sizeOf(a)); @@ -236,34 +236,35 @@ describe('d.unstruct', () => { }); it('can be called to create an object', () => { - const TestStruct = d.unstruct({ + const TestUnstruct = d.unstruct({ x: d.u32, y: d.uint32x3, }); - const obj = TestStruct({ x: 1, y: d.vec3u(1, 2, 3) }); + const obj = TestUnstruct({ x: 1, y: d.vec3u(1, 2, 3) }); expect(obj).toStrictEqual({ x: 1, y: d.vec3u(1, 2, 3) }); expectTypeOf(obj).toEqualTypeOf<{ x: number; y: d.v3u }>(); }); it('cannot be called with invalid properties', () => { - const TestStruct = d.unstruct({ + const TestUnstruct = d.unstruct({ x: d.u32, y: d.uint32x3, }); // @ts-expect-error - (() => TestStruct({ x: 1, z: 2 })); + (() => TestUnstruct({ x: 1, z: 2 })); }); - it('can be called to create a deep copy of other struct', () => { - const schema = d.unstruct({ - nested: d.unstruct({ prop1: d.float32x2, prop2: d.u32 }), + it('can be called to create a deep copy of other unstruct', () => { + const NestedUnstruct = d.unstruct({ prop1: d.float32x2, prop2: d.u32 }); + const TestUnstruct = d.unstruct({ nested: NestedUnstruct }); + const instance = TestUnstruct({ + nested: { prop1: d.vec2f(1, 2), prop2: 21 }, }); - const instance = schema({ nested: { prop1: d.vec2f(1, 2), prop2: 21 } }); - const clone = schema(instance); + const clone = TestUnstruct(instance); expect(clone).toStrictEqual(instance); expect(clone).not.toBe(instance); @@ -271,31 +272,31 @@ describe('d.unstruct', () => { expect(clone.nested.prop1).not.toBe(instance.nested.prop1); }); - it('can be called to strip extra properties of a struct', () => { - const schema = d.unstruct({ prop1: d.vec2f, prop2: d.u32 }); + it('can be called to strip extra properties of a unstruct', () => { + const TestUnstruct = d.unstruct({ prop1: d.vec2f, prop2: d.u32 }); const instance = { prop1: d.vec2f(1, 2), prop2: 21, prop3: 'extra' }; - const clone = schema(instance); + const clone = TestUnstruct(instance); expect(clone).toStrictEqual({ prop1: d.vec2f(1, 2), prop2: 21 }); }); it('can be called to create a default value', () => { - const schema = d.unstruct({ + const TestUnstruct = d.unstruct({ nested: d.unstruct({ prop1: d.vec2f, prop2: d.u32 }), }); - const defaultStruct = schema(); + const defaultStruct = TestUnstruct(); expect(defaultStruct).toStrictEqual({ nested: { prop1: d.vec2f(), prop2: d.u32() }, }); }); - // it('can be called to create a default value with nested array', () => { - // const schema = d.unstruct({ arr: d.disarrayOf(d.u32, 1) }); + // it('can be called to create a default value with nested disarray', () => { + // const TestUnstruct = d.unstruct({ arr: d.disarrayOf(d.u32, 1) }); - // const defaultStruct = schema(); + // const defaultStruct = TestUnstruct(); // expect(defaultStruct).toStrictEqual({ arr: [0] }); // }); From 0a27975247555cf5f197ece0dd7934393a57f132 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 7 Aug 2025 13:30:56 +0200 Subject: [PATCH 04/13] Merge schemaDefaultWrapper and schemaCloneWrapper into schemaCallWrapper --- .../typegpu/src/core/buffer/bufferUsage.ts | 11 +++--- packages/typegpu/src/core/function/tgpuFn.ts | 6 ++-- packages/typegpu/src/data/array.ts | 7 ++-- packages/typegpu/src/data/struct.ts | 6 ++-- packages/typegpu/src/data/unstruct.ts | 6 ++-- packages/typegpu/src/data/utils.ts | 35 +++++-------------- packages/typegpu/src/data/vector.ts | 2 +- 7 files changed, 24 insertions(+), 49 deletions(-) diff --git a/packages/typegpu/src/core/buffer/bufferUsage.ts b/packages/typegpu/src/core/buffer/bufferUsage.ts index c57697eb65..6bf5597241 100644 --- a/packages/typegpu/src/core/buffer/bufferUsage.ts +++ b/packages/typegpu/src/core/buffer/bufferUsage.ts @@ -1,7 +1,9 @@ import type { AnyData } from '../../data/dataTypes.ts'; +import { schemaCallWrapper } from '../../data/utils.ts'; import type { AnyWgslData, BaseData } from '../../data/wgslTypes.ts'; -import { isUsableAsStorage, type StorageFlag } from '../../extension.ts'; +import { IllegalBufferAccessError } from '../../errors.ts'; import { getExecMode, inCodegenMode, isInsideTgpuFn } from '../../execMode.ts'; +import { isUsableAsStorage, type StorageFlag } from '../../extension.ts'; import type { TgpuNamable } from '../../shared/meta.ts'; import { getName, setName } from '../../shared/meta.ts'; import type { Infer, InferGPU } from '../../shared/repr.ts'; @@ -12,6 +14,7 @@ import { $repr, $wgslDataType, } from '../../shared/symbols.ts'; +import { assertExhaustive } from '../../shared/utilityTypes.ts'; import type { LayoutMembership } from '../../tgpuBindGroupLayout.ts'; import type { BindableBufferUsage, @@ -20,9 +23,6 @@ import type { } from '../../types.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; import type { TgpuBuffer, UniformFlag } from './buffer.ts'; -import { schemaCloneWrapper, schemaDefaultWrapper } from '../../data/utils.ts'; -import { assertExhaustive } from '../../shared/utilityTypes.ts'; -import { IllegalBufferAccessError } from '../../errors.ts'; // ---------- // Public API @@ -163,8 +163,7 @@ class TgpuFixedBufferImpl< if (!mode.buffers.has(this.buffer)) { // Not initialized yet mode.buffers.set( this.buffer, - schemaCloneWrapper(this.buffer.dataType, this.buffer.initial) ?? - schemaDefaultWrapper(this.buffer.dataType), + schemaCallWrapper(this.buffer.dataType, this.buffer.initial), ); } return mode.buffers.get(this.buffer) as InferGPU; diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index a680766898..fd5f48bef4 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -1,6 +1,6 @@ import { type AnyData, UnknownData } from '../../data/dataTypes.ts'; -import { schemaCloneWrapper } from '../../data/utils.ts'; import { snip } from '../../data/snippet.ts'; +import { schemaCallWrapper } from '../../data/utils.ts'; import { Void } from '../../data/wgslTypes.ts'; import { ExecutionError } from '../../errors.ts'; import { provideInsideTgpuFn } from '../../execMode.ts'; @@ -32,6 +32,7 @@ import { type TgpuAccessor, type TgpuSlot, } from '../slot/slotTypes.ts'; +import { createDualImpl } from './dualImpl.ts'; import { createFnCore, type FnCore } from './fnCore.ts'; import type { AnyFn, @@ -41,7 +42,6 @@ import type { InheritArgNames, } from './fnTypes.ts'; import { stripTemplate } from './templateUtils.ts'; -import { createDualImpl } from './dualImpl.ts'; // ---------- // Public API @@ -223,7 +223,7 @@ function createFn( } const castAndCopiedArgs = args.map((arg, index) => - schemaCloneWrapper(shell.argTypes[index], arg) + schemaCallWrapper(shell.argTypes[index], arg) ) as InferArgs>; return implementation(...castAndCopiedArgs); diff --git a/packages/typegpu/src/data/array.ts b/packages/typegpu/src/data/array.ts index d1c14eebad..ec8272fae2 100644 --- a/packages/typegpu/src/data/array.ts +++ b/packages/typegpu/src/data/array.ts @@ -1,6 +1,6 @@ import { $internal } from '../shared/symbols.ts'; import { sizeOf } from './sizeOf.ts'; -import { schemaCloneWrapper, schemaDefaultWrapper } from './utils.ts'; +import { schemaCallWrapper } from './utils.ts'; import type { AnyWgslData, WgslArray } from './wgslTypes.ts'; // ---------- @@ -33,10 +33,7 @@ export function arrayOf( return Array.from( { length: elementCount }, - (_, i) => - elements - ? schemaCloneWrapper(elementType, elements[i]) - : schemaDefaultWrapper(elementType), + (_, i) => schemaCallWrapper(elementType, elements?.[i]), ); }; Object.setPrototypeOf(arraySchema, WgslArrayImpl); diff --git a/packages/typegpu/src/data/struct.ts b/packages/typegpu/src/data/struct.ts index 80869bfb99..5582e03cc8 100644 --- a/packages/typegpu/src/data/struct.ts +++ b/packages/typegpu/src/data/struct.ts @@ -1,6 +1,6 @@ import { getName, setName } from '../shared/meta.ts'; import { $internal } from '../shared/symbols.ts'; -import { schemaCloneWrapper, schemaDefaultWrapper } from './utils.ts'; +import { schemaCallWrapper } from './utils.ts'; import type { AnyWgslData, WgslStruct } from './wgslTypes.ts'; // ---------- @@ -27,9 +27,7 @@ export function struct>( Object.fromEntries( Object.entries(props).map(([key, schema]) => [ key, - instanceProps - ? schemaCloneWrapper(schema, instanceProps[key]) - : schemaDefaultWrapper(schema), + schemaCallWrapper(schema, instanceProps?.[key]), ]), ); Object.setPrototypeOf(structSchema, WgslStructImpl); diff --git a/packages/typegpu/src/data/unstruct.ts b/packages/typegpu/src/data/unstruct.ts index 012a5d1765..6091c13b0e 100644 --- a/packages/typegpu/src/data/unstruct.ts +++ b/packages/typegpu/src/data/unstruct.ts @@ -1,7 +1,7 @@ import { getName, setName } from '../shared/meta.ts'; import { $internal } from '../shared/symbols.ts'; import type { Unstruct } from './dataTypes.ts'; -import { schemaCloneWrapper, schemaDefaultWrapper } from './utils.ts'; +import { schemaCallWrapper } from './utils.ts'; import type { BaseData } from './wgslTypes.ts'; // ---------- @@ -35,9 +35,7 @@ export function unstruct>( Object.fromEntries( Object.entries(properties).map(([key, schema]) => [ key, - instanceProps - ? schemaCloneWrapper(schema, instanceProps[key]) - : schemaDefaultWrapper(schema), + schemaCallWrapper(schema, instanceProps?.[key]), ]), ); Object.setPrototypeOf(unstructSchema, UnstructImpl); diff --git a/packages/typegpu/src/data/utils.ts b/packages/typegpu/src/data/utils.ts index 45c33532ff..fab271a770 100644 --- a/packages/typegpu/src/data/utils.ts +++ b/packages/typegpu/src/data/utils.ts @@ -1,20 +1,23 @@ import { formatToWGSLType } from './vertexFormatData'; /** - * A wrapper for `schema(item)` call. + * A wrapper for `schema(item)` or `schema()` call. + * If the schema is a TgpuVertexFormatData, it instead calls the corresponding constructible schema. * Throws an error if the schema is not callable. */ -export function schemaCloneWrapper(schema: unknown, item: T): T { +export function schemaCallWrapper(schema: unknown, item?: T): T { const maybeType = (schema as { type: string })?.type; try { // TgpuVertexFormatData are not callable - const cloningSchema = maybeType in formatToWGSLType + const callSchema = (maybeType in formatToWGSLType ? formatToWGSLType[maybeType as keyof typeof formatToWGSLType] - : schema; - return (cloningSchema as unknown as ((item: T) => T))(item); + : schema) as unknown as ((item?: T) => T); + if (item === undefined) { + return callSchema(); + } + return callSchema(item); } catch (e) { - console.log(e); throw new Error( `Schema of type ${ maybeType ?? '' @@ -22,23 +25,3 @@ export function schemaCloneWrapper(schema: unknown, item: T): T { ); } } - -/** - * A wrapper for `schema()` call. - * Throws an error if the schema is not callable. - */ -export function schemaDefaultWrapper(schema: unknown): T { - const maybeType = (schema as { type: string })?.type; - - try { - // TgpuVertexFormatData are not callable - const cloningSchema = maybeType in formatToWGSLType - ? formatToWGSLType[maybeType as keyof typeof formatToWGSLType] - : schema; - return (cloningSchema as unknown as (() => T))(); - } catch { - throw new Error( - `Schema of type ${maybeType ?? ''} is not callable.`, - ); - } -} diff --git a/packages/typegpu/src/data/vector.ts b/packages/typegpu/src/data/vector.ts index 3b83e37399..32acab24e8 100644 --- a/packages/typegpu/src/data/vector.ts +++ b/packages/typegpu/src/data/vector.ts @@ -1,7 +1,7 @@ import { createDualImpl } from '../core/function/dualImpl.ts'; import { $repr } from '../shared/symbols.ts'; -import { snip } from './snippet.ts'; import { bool, f16, f32, i32, u32 } from './numeric.ts'; +import { snip } from './snippet.ts'; import { Vec2bImpl, Vec2fImpl, From bcbdf951488ac62b113e9e1795c49dff1f97b965 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 7 Aug 2025 15:31:19 +0200 Subject: [PATCH 05/13] Make disarray callable --- packages/typegpu/src/data/disarray.ts | 75 +++++++++++++-------------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/packages/typegpu/src/data/disarray.ts b/packages/typegpu/src/data/disarray.ts index a01bd72401..f52edb0931 100644 --- a/packages/typegpu/src/data/disarray.ts +++ b/packages/typegpu/src/data/disarray.ts @@ -1,16 +1,6 @@ -import type { - Infer, - InferPartial, - IsValidVertexSchema, -} from '../shared/repr.ts'; import { $internal } from '../shared/symbols.ts'; -import type { - $invalidSchemaReason, - $repr, - $reprPartial, - $validVertexSchema, -} from '../shared/symbols.ts'; import type { AnyData, Disarray } from './dataTypes.ts'; +import { schemaCallWrapper } from './utils.ts'; // ---------- // Public API @@ -31,42 +21,49 @@ import type { AnyData, Disarray } from './dataTypes.ts'; * const disarray = d.disarrayOf(d.align(16, d.vec3f), 3); * * @param elementType The type of elements in the array. - * @param count The number of elements in the array. + * @param elementCount The number of elements in the array. */ export function disarrayOf( elementType: TElement, - count: number, + elementCount: number, ): Disarray { - return new DisarrayImpl(elementType, count); + // In the schema call, create and return a deep copy + // by wrapping all the values in `elementType` schema calls. + const disarraySchema = (elements?: TElement[]) => { + if (elements && elements.length !== elementCount) { + throw new Error( + `Array schema of ${elementCount} elements of type ${elementType.type} called with ${elements.length} arguments.`, + ); + } + + return Array.from( + { length: elementCount }, + (_, i) => schemaCallWrapper(elementType, elements?.[i]), + ); + }; + Object.setPrototypeOf(disarraySchema, DisarrayImpl); + + disarraySchema.elementType = elementType; + + if (!Number.isInteger(elementCount) || elementCount < 0) { + throw new Error( + `Cannot create array schema with invalid element count: ${elementCount}.`, + ); + } + disarraySchema.elementCount = elementCount; + + return disarraySchema as unknown as Disarray; } // -------------- // Implementation // -------------- -class DisarrayImpl implements Disarray { - public readonly [$internal] = true; - public readonly type = 'disarray'; - - // Type-tokens, not available at runtime - declare readonly [$repr]: Infer[]; - declare readonly [$reprPartial]: { - idx: number; - value: InferPartial; - }[]; - declare readonly [$validVertexSchema]: IsValidVertexSchema; - declare readonly [$invalidSchemaReason]: - Disarray[typeof $invalidSchemaReason]; - // --- +const DisarrayImpl = { + [$internal]: true, + type: 'disarray', - constructor( - public readonly elementType: TElement, - public readonly elementCount: number, - ) { - if (!Number.isInteger(elementCount) || elementCount < 0) { - throw new Error( - `Cannot create disarray schema with invalid element count: ${elementCount}.`, - ); - } - } -} + toString(this: Disarray): string { + return `disarrayOf(${this.elementType}, ${this.elementCount})`; + }, +}; From e9926e94b1b2f55b856e2963c43fd527aa7d7994 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 7 Aug 2025 16:08:06 +0200 Subject: [PATCH 06/13] Add disarray tests --- packages/typegpu/src/data/array.ts | 2 +- packages/typegpu/src/data/dataTypes.ts | 2 + packages/typegpu/src/data/disarray.ts | 2 +- packages/typegpu/tests/array.test.ts | 6 +-- packages/typegpu/tests/disarray.test.ts | 68 ++++++++++++++++++++++++- packages/typegpu/tests/unstruct.test.ts | 10 ++-- 6 files changed, 79 insertions(+), 11 deletions(-) diff --git a/packages/typegpu/src/data/array.ts b/packages/typegpu/src/data/array.ts index ec8272fae2..4e05235f44 100644 --- a/packages/typegpu/src/data/array.ts +++ b/packages/typegpu/src/data/array.ts @@ -27,7 +27,7 @@ export function arrayOf( const arraySchema = (elements?: TElement[]) => { if (elements && elements.length !== elementCount) { throw new Error( - `Array schema of ${elementCount} elements of type ${elementType.type} called with ${elements.length} arguments.`, + `Array schema of ${elementCount} elements of type ${elementType.type} called with ${elements.length} argument(s).`, ); } diff --git a/packages/typegpu/src/data/dataTypes.ts b/packages/typegpu/src/data/dataTypes.ts index de61d0fdde..6d0f7cf05c 100644 --- a/packages/typegpu/src/data/dataTypes.ts +++ b/packages/typegpu/src/data/dataTypes.ts @@ -44,6 +44,8 @@ export type TgpuDualFn unknown> = */ export interface Disarray extends wgsl.BaseData { + (elements: Infer[]): Infer[]; + (): Infer[]; readonly type: 'disarray'; readonly elementCount: number; readonly elementType: TElement; diff --git a/packages/typegpu/src/data/disarray.ts b/packages/typegpu/src/data/disarray.ts index f52edb0931..6a0d19bafe 100644 --- a/packages/typegpu/src/data/disarray.ts +++ b/packages/typegpu/src/data/disarray.ts @@ -32,7 +32,7 @@ export function disarrayOf( const disarraySchema = (elements?: TElement[]) => { if (elements && elements.length !== elementCount) { throw new Error( - `Array schema of ${elementCount} elements of type ${elementType.type} called with ${elements.length} arguments.`, + `Disarray schema of ${elementCount} elements of type ${elementType.type} called with ${elements.length} argument(s).`, ); } diff --git a/packages/typegpu/tests/array.test.ts b/packages/typegpu/tests/array.test.ts index 7822f9eaae..061282a3c3 100644 --- a/packages/typegpu/tests/array.test.ts +++ b/packages/typegpu/tests/array.test.ts @@ -99,7 +99,7 @@ describe('array', () => { expectTypeOf(obj).toEqualTypeOf(); }); - it('cannot be called with invalid properties', () => { + it('cannot be called with invalid elements', () => { const ArraySchema = d.arrayOf(d.u32, 4); // @ts-expect-error @@ -131,10 +131,10 @@ describe('array', () => { const ArraySchema = d.arrayOf(d.u32, 2); expect(() => ArraySchema([1])).toThrowErrorMatchingInlineSnapshot( - '[Error: Array schema of 2 elements of type u32 called with 1 arguments.]', + `[Error: Array schema of 2 elements of type u32 called with 1 argument(s).]`, ); expect(() => ArraySchema([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( - '[Error: Array schema of 2 elements of type u32 called with 3 arguments.]', + `[Error: Array schema of 2 elements of type u32 called with 3 argument(s).]`, ); }); diff --git a/packages/typegpu/tests/disarray.test.ts b/packages/typegpu/tests/disarray.test.ts index 51f59cdf94..1e8aa9c2b8 100644 --- a/packages/typegpu/tests/disarray.test.ts +++ b/packages/typegpu/tests/disarray.test.ts @@ -1,5 +1,5 @@ import { BufferReader, BufferWriter } from 'typed-binary'; -import { describe, expect } from 'vitest'; +import { describe, expect, expectTypeOf } from 'vitest'; import { readData, writeData } from '../src/data/dataIO.ts'; import * as d from '../src/data/index.ts'; import { it } from './utils/extendedIt.ts'; @@ -115,4 +115,70 @@ describe('disarray', () => { writeData(new BufferWriter(buffer), TestArray, value); expect(readData(new BufferReader(buffer), TestArray)).toStrictEqual(value); }); + + it('can be called to create a disarray', () => { + const DisarraySchema = d.disarrayOf(d.uint16x2, 2); + + const obj = DisarraySchema([d.vec2u(1, 2), d.vec2u(3, 4)]); + + expect(obj).toStrictEqual([d.vec2u(1, 2), d.vec2u(3, 4)]); + expectTypeOf(obj).toEqualTypeOf(); + }); + + it('cannot be called with invalid elements', () => { + const DisarraySchema = d.disarrayOf(d.unorm16x2, 2); + + // @ts-expect-error + (() => DisarraySchema([d.vec2f(), d.vec3f()])); + // @ts-expect-error + (() => DisarraySchema([d.vec3f(), d.vec3f()])); + }); + + it('can be called to create a deep copy of other disarray', () => { + const InnerSchema = d.disarrayOf(d.uint16x2, 2); + const OuterSchema = d.disarrayOf(InnerSchema, 1); + const instance = OuterSchema([InnerSchema([d.vec2u(1, 2), d.vec2u()])]); + + const clone = OuterSchema(instance); + + expect(clone).toStrictEqual(instance); + expect(clone).not.toBe(instance); + expect(clone[0]).not.toBe(instance[0]); + expect(clone[0]).not.toBe(clone[1]); + expect(clone[0]?.[0]).not.toBe(instance[0]?.[0]); + expect(clone[0]?.[0]).toStrictEqual(d.vec2u(1, 2)); + }); + + it('throws when invalid number of arguments', () => { + const DisarraySchema = d.disarrayOf(d.float32x2, 2); + + expect(() => DisarraySchema([d.vec2f()])) + .toThrowErrorMatchingInlineSnapshot( + `[Error: Disarray schema of 2 elements of type float32x2 called with 1 argument(s).]`, + ); + expect(() => DisarraySchema([d.vec2f(), d.vec2f(), d.vec2f()])) + .toThrowErrorMatchingInlineSnapshot( + `[Error: Disarray schema of 2 elements of type float32x2 called with 3 argument(s).]`, + ); + }); + + it('can be called to create a default value', () => { + const DisarraySchema = d.disarrayOf(d.float32x3, 2); + + const defaultDisarray = DisarraySchema(); + + expect(defaultDisarray).toStrictEqual([d.vec3f(), d.vec3f()]); + }); + + it('can be called to create a default value with nested unstruct', () => { + const UnstructSchema = d.unstruct({ vec: d.float32x3 }); + const DisarraySchema = d.disarrayOf(UnstructSchema, 2); + + const defaultDisarray = DisarraySchema(); + + expect(defaultDisarray).toStrictEqual([ + { vec: d.vec3f() }, + { vec: d.vec3f() }, + ]); + }); }); diff --git a/packages/typegpu/tests/unstruct.test.ts b/packages/typegpu/tests/unstruct.test.ts index a91193c82a..b5ac309838 100644 --- a/packages/typegpu/tests/unstruct.test.ts +++ b/packages/typegpu/tests/unstruct.test.ts @@ -293,11 +293,11 @@ describe('d.unstruct', () => { }); }); - // it('can be called to create a default value with nested disarray', () => { - // const TestUnstruct = d.unstruct({ arr: d.disarrayOf(d.u32, 1) }); + it('can be called to create a default value with nested disarray', () => { + const TestUnstruct = d.unstruct({ arr: d.disarrayOf(d.uint16, 1) }); - // const defaultStruct = TestUnstruct(); + const defaultStruct = TestUnstruct(); - // expect(defaultStruct).toStrictEqual({ arr: [0] }); - // }); + expect(defaultStruct).toStrictEqual({ arr: [0] }); + }); }); From 84e9f6d9a1c241fdfdaad47f85c3375c74da983f Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 7 Aug 2025 16:10:19 +0200 Subject: [PATCH 07/13] =?UTF-8?q?=F0=9F=A6=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/typegpu/src/data/utils.ts | 2 +- packages/typegpu/tests/array.test.ts | 4 ++-- packages/typegpu/tests/disarray.test.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/typegpu/src/data/utils.ts b/packages/typegpu/src/data/utils.ts index fab271a770..82d7bed1fb 100644 --- a/packages/typegpu/src/data/utils.ts +++ b/packages/typegpu/src/data/utils.ts @@ -1,4 +1,4 @@ -import { formatToWGSLType } from './vertexFormatData'; +import { formatToWGSLType } from './vertexFormatData.ts'; /** * A wrapper for `schema(item)` or `schema()` call. diff --git a/packages/typegpu/tests/array.test.ts b/packages/typegpu/tests/array.test.ts index 061282a3c3..203d5299ff 100644 --- a/packages/typegpu/tests/array.test.ts +++ b/packages/typegpu/tests/array.test.ts @@ -131,10 +131,10 @@ describe('array', () => { const ArraySchema = d.arrayOf(d.u32, 2); expect(() => ArraySchema([1])).toThrowErrorMatchingInlineSnapshot( - `[Error: Array schema of 2 elements of type u32 called with 1 argument(s).]`, + '[Error: Array schema of 2 elements of type u32 called with 1 argument(s).]', ); expect(() => ArraySchema([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( - `[Error: Array schema of 2 elements of type u32 called with 3 argument(s).]`, + '[Error: Array schema of 2 elements of type u32 called with 3 argument(s).]', ); }); diff --git a/packages/typegpu/tests/disarray.test.ts b/packages/typegpu/tests/disarray.test.ts index 1e8aa9c2b8..500fe44174 100644 --- a/packages/typegpu/tests/disarray.test.ts +++ b/packages/typegpu/tests/disarray.test.ts @@ -154,11 +154,11 @@ describe('disarray', () => { expect(() => DisarraySchema([d.vec2f()])) .toThrowErrorMatchingInlineSnapshot( - `[Error: Disarray schema of 2 elements of type float32x2 called with 1 argument(s).]`, + '[Error: Disarray schema of 2 elements of type float32x2 called with 1 argument(s).]', ); expect(() => DisarraySchema([d.vec2f(), d.vec2f(), d.vec2f()])) .toThrowErrorMatchingInlineSnapshot( - `[Error: Disarray schema of 2 elements of type float32x2 called with 3 argument(s).]`, + '[Error: Disarray schema of 2 elements of type float32x2 called with 3 argument(s).]', ); }); From f1db7952cfeb18d5b81a1ae56c9ec169a9c8ce5a Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 7 Aug 2025 16:51:52 +0200 Subject: [PATCH 08/13] Change `schemaCallWrapper` arg type from `unknown` to `AnyData` --- packages/typegpu/src/core/function/tgpuFn.ts | 2 +- packages/typegpu/src/data/unstruct.ts | 4 ++-- packages/typegpu/src/data/utils.ts | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/typegpu/src/core/function/tgpuFn.ts b/packages/typegpu/src/core/function/tgpuFn.ts index aa18baefe6..ed87cec167 100644 --- a/packages/typegpu/src/core/function/tgpuFn.ts +++ b/packages/typegpu/src/core/function/tgpuFn.ts @@ -223,7 +223,7 @@ function createFn( } const castAndCopiedArgs = args.map((arg, index) => - schemaCallWrapper(shell.argTypes[index], arg) + schemaCallWrapper(shell.argTypes[index] as unknown as AnyData, arg) ) as InferArgs>; return implementation(...castAndCopiedArgs); diff --git a/packages/typegpu/src/data/unstruct.ts b/packages/typegpu/src/data/unstruct.ts index 6091c13b0e..2cede97c64 100644 --- a/packages/typegpu/src/data/unstruct.ts +++ b/packages/typegpu/src/data/unstruct.ts @@ -1,6 +1,6 @@ import { getName, setName } from '../shared/meta.ts'; import { $internal } from '../shared/symbols.ts'; -import type { Unstruct } from './dataTypes.ts'; +import type { AnyData, Unstruct } from './dataTypes.ts'; import { schemaCallWrapper } from './utils.ts'; import type { BaseData } from './wgslTypes.ts'; @@ -35,7 +35,7 @@ export function unstruct>( Object.fromEntries( Object.entries(properties).map(([key, schema]) => [ key, - schemaCallWrapper(schema, instanceProps?.[key]), + schemaCallWrapper(schema as AnyData, instanceProps?.[key]), ]), ); Object.setPrototypeOf(unstructSchema, UnstructImpl); diff --git a/packages/typegpu/src/data/utils.ts b/packages/typegpu/src/data/utils.ts index 82d7bed1fb..2ec2cdee8e 100644 --- a/packages/typegpu/src/data/utils.ts +++ b/packages/typegpu/src/data/utils.ts @@ -1,3 +1,4 @@ +import { AnyData } from './index.ts'; import { formatToWGSLType } from './vertexFormatData.ts'; /** @@ -5,7 +6,7 @@ import { formatToWGSLType } from './vertexFormatData.ts'; * If the schema is a TgpuVertexFormatData, it instead calls the corresponding constructible schema. * Throws an error if the schema is not callable. */ -export function schemaCallWrapper(schema: unknown, item?: T): T { +export function schemaCallWrapper(schema: AnyData, item?: T): T { const maybeType = (schema as { type: string })?.type; try { From daa54c9d49daa413d8dfc86997d1d1781d13bbd0 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 7 Aug 2025 17:01:58 +0200 Subject: [PATCH 09/13] Add tests for `schemaCallWrapper` --- packages/typegpu/tests/data/utils.test.ts | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 packages/typegpu/tests/data/utils.test.ts diff --git a/packages/typegpu/tests/data/utils.test.ts b/packages/typegpu/tests/data/utils.test.ts new file mode 100644 index 0000000000..1acc90bfd0 --- /dev/null +++ b/packages/typegpu/tests/data/utils.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import * as d from '../../src/data/index.ts'; +import { schemaCallWrapper } from '../../src/data/utils.ts'; + +describe('schemaCallWrapper', () => { + it('throws when the schema is not callable', () => { + expect(() => schemaCallWrapper(d.Void)) + .toThrowErrorMatchingInlineSnapshot( + `[Error: Schema of type void is not callable or was called with invalid arguments.]`, + ); + }); + + it('calls schema without arguments', () => { + const TestStruct = d.struct({ v: d.vec2f }); + + expect(schemaCallWrapper(TestStruct)).toStrictEqual({ v: d.vec2f() }); + }); + + it('calls schema with arguments', () => { + const TestStruct = d.struct({ v: d.vec2f }); + const testInstance = { v: d.vec2f(1, 2), u: d.vec3u() }; + + expect(schemaCallWrapper(TestStruct, testInstance)) + .toStrictEqual({ v: d.vec2f(1, 2) }); + }); + + it('works with loose data', () => { + const TestUnstruct = d.unstruct({ v: d.float32x3 }); + const testInstance = { v: d.vec3f(1, 2, 3), u: d.vec3u() }; + + expect(schemaCallWrapper(TestUnstruct, testInstance)) + .toStrictEqual({ v: d.vec3f(1, 2, 3) }); + }); +}); From 86ce21af2d08ecdeaff3aa2535c8fac16d89c13f Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Thu, 7 Aug 2025 17:04:28 +0200 Subject: [PATCH 10/13] =?UTF-8?q?=F0=9F=A6=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/typegpu/src/data/utils.ts | 2 +- packages/typegpu/tests/data/utils.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typegpu/src/data/utils.ts b/packages/typegpu/src/data/utils.ts index 2ec2cdee8e..a3e1d0a86a 100644 --- a/packages/typegpu/src/data/utils.ts +++ b/packages/typegpu/src/data/utils.ts @@ -1,4 +1,4 @@ -import { AnyData } from './index.ts'; +import type { AnyData } from './index.ts'; import { formatToWGSLType } from './vertexFormatData.ts'; /** diff --git a/packages/typegpu/tests/data/utils.test.ts b/packages/typegpu/tests/data/utils.test.ts index 1acc90bfd0..0157fbb3ac 100644 --- a/packages/typegpu/tests/data/utils.test.ts +++ b/packages/typegpu/tests/data/utils.test.ts @@ -6,7 +6,7 @@ describe('schemaCallWrapper', () => { it('throws when the schema is not callable', () => { expect(() => schemaCallWrapper(d.Void)) .toThrowErrorMatchingInlineSnapshot( - `[Error: Schema of type void is not callable or was called with invalid arguments.]`, + '[Error: Schema of type void is not callable or was called with invalid arguments.]', ); }); From b873cb0adc20ec4dd9cad8b7e37c3e094ebd9260 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:15:42 +0200 Subject: [PATCH 11/13] Update packages/typegpu/src/data/disarray.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/typegpu/src/data/disarray.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/src/data/disarray.ts b/packages/typegpu/src/data/disarray.ts index 6a0d19bafe..facbc69339 100644 --- a/packages/typegpu/src/data/disarray.ts +++ b/packages/typegpu/src/data/disarray.ts @@ -47,7 +47,7 @@ export function disarrayOf( if (!Number.isInteger(elementCount) || elementCount < 0) { throw new Error( - `Cannot create array schema with invalid element count: ${elementCount}.`, + `Cannot create disarray schema with invalid element count: ${elementCount}.`, ); } disarraySchema.elementCount = elementCount; From bdd0065b3358086d5fbc06fcb4d95d7143d98b25 Mon Sep 17 00:00:00 2001 From: Aleksander Katan Date: Fri, 8 Aug 2025 13:31:23 +0200 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=A6=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/typegpu/src/data/struct.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/src/data/struct.ts b/packages/typegpu/src/data/struct.ts index 28f24221ea..5badb81854 100644 --- a/packages/typegpu/src/data/struct.ts +++ b/packages/typegpu/src/data/struct.ts @@ -1,6 +1,6 @@ import { getName, setName } from '../shared/meta.ts'; import { $internal } from '../shared/symbols.ts'; -import { AnyData } from './index.ts'; +import type { AnyData } from './index.ts'; import { schemaCallWrapper } from './utils.ts'; import type { AnyWgslData, BaseData, WgslStruct } from './wgslTypes.ts'; From e5e4d9ab1105fc8ca61f83a78a55d9c0dd86efb6 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:23:22 +0200 Subject: [PATCH 13/13] Update packages/typegpu/src/data/struct.ts Co-authored-by: Iwo Plaza --- packages/typegpu/src/data/struct.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/src/data/struct.ts b/packages/typegpu/src/data/struct.ts index 5badb81854..2bb2465ab2 100644 --- a/packages/typegpu/src/data/struct.ts +++ b/packages/typegpu/src/data/struct.ts @@ -1,6 +1,6 @@ import { getName, setName } from '../shared/meta.ts'; import { $internal } from '../shared/symbols.ts'; -import type { AnyData } from './index.ts'; +import type { AnyData } from './dataTypes.ts'; import { schemaCallWrapper } from './utils.ts'; import type { AnyWgslData, BaseData, WgslStruct } from './wgslTypes.ts';