From 21f8cf46ecf180c7e5707fc940a543e6c1c8d516 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:47:50 +0200 Subject: [PATCH 01/16] Remove dependency of shorthand on usage --- .../src/core/buffer/bufferShorthand.ts | 79 ++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/packages/typegpu/src/core/buffer/bufferShorthand.ts b/packages/typegpu/src/core/buffer/bufferShorthand.ts index 3f86942dcf..5b5d5c4330 100644 --- a/packages/typegpu/src/core/buffer/bufferShorthand.ts +++ b/packages/typegpu/src/core/buffer/bufferShorthand.ts @@ -1,10 +1,21 @@ -import type { ResolvedSnippet } from '../../data/snippet.ts'; +import { schemaCallWrapper } from '../../data/schemaCallWrapper.ts'; +import { snip, type ResolvedSnippet } from '../../data/snippet.ts'; import type { BaseData } from '../../data/wgslTypes.ts'; +import { IllegalBufferAccessError } from '../../errors.ts'; +import { getExecMode, isInsideTgpuFn } from '../../execMode.ts'; import type { StorageFlag } from '../../extension.ts'; import { getName, setName, type TgpuNamable } from '../../shared/meta.ts'; import type { Infer, InferGPU, InferInput, InferPatch, InferPartial } from '../../shared/repr.ts'; -import { $getNameForward, $gpuValueOf, $internal, $resolve } from '../../shared/symbols.ts'; +import { + $getNameForward, + $gpuValueOf, + $internal, + $ownSnippet, + $resolve, +} from '../../shared/symbols.ts'; +import { assertExhaustive } from '../../shared/utilityTypes.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; +import { valueProxyHandler } from '../valueProxyUtils.ts'; import type { BufferWriteOptions, TgpuBuffer, UniformFlag } from './buffer.ts'; import type { TgpuBufferUsage } from './bufferUsage.ts'; @@ -92,8 +103,6 @@ export class TgpuBufferShorthandImpl< readonly buffer: TgpuBuffer & (TType extends 'mutable' | 'readonly' ? StorageFlag : UniformFlag); - readonly #usage: TgpuBufferUsage; - constructor( resourceType: TType, buffer: TgpuBuffer & (TType extends 'mutable' | 'readonly' ? StorageFlag : UniformFlag), @@ -101,8 +110,6 @@ export class TgpuBufferShorthandImpl< this.resourceType = resourceType; this.buffer = buffer; this[$getNameForward] = buffer; - // oxlint-disable-next-line typescript/no-explicit-any -- too complex a type - this.#usage = (this.buffer as any).as(this.resourceType); } $name(label: string): this { @@ -128,11 +135,49 @@ export class TgpuBufferShorthandImpl< } get [$gpuValueOf](): InferGPU { - return this.#usage.$; + const dataType = this.buffer.dataType; + const usage = this.resourceType; + + return new Proxy( + { + [$internal]: true, + get [$ownSnippet]() { + return snip(this, dataType, usage); + }, + [$resolve]: (ctx) => ctx.resolve(this), + toString: () => `${usage}:${getName(this) ?? ''}.$`, + }, + valueProxyHandler, + ) as InferGPU; } get $(): InferGPU { - return this.#usage.$; + const mode = getExecMode(); + const insideTgpuFn = isInsideTgpuFn(); + + if (mode.type === 'normal') { + throw new IllegalBufferAccessError( + insideTgpuFn + ? `Cannot access ${String( + this.buffer, + )}. TypeGPU functions that depends on GPU resources need to be part of a compute dispatch, draw call or simulation` + : '.$ is inaccessible during normal JS execution. Try `.read()`', + ); + } + + if (mode.type === 'codegen') { + return this[$gpuValueOf]; + } + + if (mode.type === 'simulate') { + if (!mode.buffers.has(this.buffer)) { + // Not initialized yet + mode.buffers.set(this.buffer, schemaCallWrapper(this.buffer.dataType, this.buffer.initial)); + } + return mode.buffers.get(this.buffer) as InferGPU; + } + + return assertExhaustive(mode, 'bufferUsage.ts#TgpuFixedBufferImpl/$'); } get value(): InferGPU { @@ -144,6 +189,22 @@ export class TgpuBufferShorthandImpl< } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - return ctx.resolve(this.#usage); + const dataType = this.buffer.dataType; + const id = ctx.makeUniqueIdentifier(getName(this), 'global'); + const { group, binding } = ctx.allocateFixedEntry( + this.resourceType === 'uniform' + ? { uniform: dataType } + : { storage: dataType, access: this.resourceType }, + this.buffer, + ); + + return ctx.gen.declareGlobalVar({ + group, + binding, + scope: this.resourceType, + id, + dataType, + init: undefined, + }); } } From 2fba4f5eb694b5265ecdaecb35555c20e42fa72e Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:02:32 +0200 Subject: [PATCH 02/16] Add 'usage' on bufferShorthand --- .../typegpu-gl/tests/webglFallback.test.ts | 2 +- .../src/core/buffer/bufferShorthand.ts | 33 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/typegpu-gl/tests/webglFallback.test.ts b/packages/typegpu-gl/tests/webglFallback.test.ts index 6bf5da8552..fda462d487 100644 --- a/packages/typegpu-gl/tests/webglFallback.test.ts +++ b/packages/typegpu-gl/tests/webglFallback.test.ts @@ -55,7 +55,7 @@ describe('TgpuRootWebGL - createUniform', () => { const uniform = root.createUniform(d.vec4f); expect(uniform).toBeDefined(); - expect(uniform.resourceType).toBe('uniform'); + expect(uniform.usage).toBe('uniform'); expect(gl.createBuffer).toHaveBeenCalled(); }); diff --git a/packages/typegpu/src/core/buffer/bufferShorthand.ts b/packages/typegpu/src/core/buffer/bufferShorthand.ts index 5b5d5c4330..a98b22bdba 100644 --- a/packages/typegpu/src/core/buffer/bufferShorthand.ts +++ b/packages/typegpu/src/core/buffer/bufferShorthand.ts @@ -11,13 +11,13 @@ import { $gpuValueOf, $internal, $ownSnippet, + $repr, $resolve, } from '../../shared/symbols.ts'; import { assertExhaustive } from '../../shared/utilityTypes.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; import type { BufferWriteOptions, TgpuBuffer, UniformFlag } from './buffer.ts'; -import type { TgpuBufferUsage } from './bufferUsage.ts'; // ---------- // Public API @@ -40,7 +40,8 @@ interface TgpuBufferShorthandBase extends TgpuNamable { } export interface TgpuMutable extends TgpuBufferShorthandBase { - readonly resourceType: 'mutable'; + readonly resourceType: 'buffer-shorthand'; + readonly usage: 'mutable'; readonly buffer: TgpuBuffer & StorageFlag; // Accessible on the GPU @@ -50,10 +51,13 @@ export interface TgpuMutable extends TgpuBufferShort value: InferGPU; $: InferGPU; // --- + + readonly [$repr]: Infer; } export interface TgpuReadonly extends TgpuBufferShorthandBase { - readonly resourceType: 'readonly'; + readonly resourceType: 'buffer-shorthand'; + readonly usage: 'readonly'; readonly buffer: TgpuBuffer & StorageFlag; // Accessible on the GPU @@ -63,10 +67,13 @@ export interface TgpuReadonly extends TgpuBufferShor readonly value: InferGPU; readonly $: InferGPU; // --- + + readonly [$repr]: Infer; } export interface TgpuUniform extends TgpuBufferShorthandBase { - readonly resourceType: 'uniform'; + readonly resourceType: 'buffer-shorthand'; + readonly usage: 'uniform'; readonly buffer: TgpuBuffer & UniformFlag; // Accessible on the GPU @@ -76,6 +83,8 @@ export interface TgpuUniform extends TgpuBufferShort readonly value: InferGPU; readonly $: InferGPU; // --- + + readonly [$repr]: Infer; } export type TgpuBufferShorthand = @@ -99,15 +108,15 @@ export class TgpuBufferShorthandImpl< > implements SelfResolvable { readonly [$internal] = true; readonly [$getNameForward]: object; - readonly resourceType: TType; + readonly usage: TType; readonly buffer: TgpuBuffer & (TType extends 'mutable' | 'readonly' ? StorageFlag : UniformFlag); constructor( - resourceType: TType, + usage: TType, buffer: TgpuBuffer & (TType extends 'mutable' | 'readonly' ? StorageFlag : UniformFlag), ) { - this.resourceType = resourceType; + this.usage = usage; this.buffer = buffer; this[$getNameForward] = buffer; } @@ -136,7 +145,7 @@ export class TgpuBufferShorthandImpl< get [$gpuValueOf](): InferGPU { const dataType = this.buffer.dataType; - const usage = this.resourceType; + const usage = this.usage; return new Proxy( { @@ -185,23 +194,21 @@ export class TgpuBufferShorthandImpl< } toString(): string { - return `${this.resourceType}BufferShorthand:${getName(this) ?? ''}`; + return `${this.usage}BufferShorthand:${getName(this) ?? ''}`; } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { const dataType = this.buffer.dataType; const id = ctx.makeUniqueIdentifier(getName(this), 'global'); const { group, binding } = ctx.allocateFixedEntry( - this.resourceType === 'uniform' - ? { uniform: dataType } - : { storage: dataType, access: this.resourceType }, + this.usage === 'uniform' ? { uniform: dataType } : { storage: dataType, access: this.usage }, this.buffer, ); return ctx.gen.declareGlobalVar({ group, binding, - scope: this.resourceType, + scope: this.usage, id, dataType, init: undefined, From 8909877fa41793e52a88e4b85cc1047f29e2643b Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:49:24 +0200 Subject: [PATCH 03/16] Return shorthand instead of usage from mutable --- .../src/core/buffer/bufferShorthand.ts | 42 ++++++++++++++++--- .../typegpu/src/core/buffer/bufferUsage.ts | 9 ++-- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/typegpu/src/core/buffer/bufferShorthand.ts b/packages/typegpu/src/core/buffer/bufferShorthand.ts index a98b22bdba..aaad7314c1 100644 --- a/packages/typegpu/src/core/buffer/bufferShorthand.ts +++ b/packages/typegpu/src/core/buffer/bufferShorthand.ts @@ -51,8 +51,6 @@ export interface TgpuMutable extends TgpuBufferShort value: InferGPU; $: InferGPU; // --- - - readonly [$repr]: Infer; } export interface TgpuReadonly extends TgpuBufferShorthandBase { @@ -67,8 +65,6 @@ export interface TgpuReadonly extends TgpuBufferShor readonly value: InferGPU; readonly $: InferGPU; // --- - - readonly [$repr]: Infer; } export interface TgpuUniform extends TgpuBufferShorthandBase { @@ -83,8 +79,6 @@ export interface TgpuUniform extends TgpuBufferShort readonly value: InferGPU; readonly $: InferGPU; // --- - - readonly [$repr]: Infer; } export type TgpuBufferShorthand = @@ -106,11 +100,15 @@ export class TgpuBufferShorthandImpl< TType extends 'mutable' | 'readonly' | 'uniform', TData extends BaseData, > implements SelfResolvable { + /** Type-token, not available at runtime */ + declare readonly [$repr]: Infer; + readonly [$internal] = true; readonly [$getNameForward]: object; readonly usage: TType; readonly buffer: TgpuBuffer & (TType extends 'mutable' | 'readonly' ? StorageFlag : UniformFlag); + readonly resourceType = 'buffer-shorthand'; constructor( usage: TType, @@ -189,10 +187,42 @@ export class TgpuBufferShorthandImpl< return assertExhaustive(mode, 'bufferUsage.ts#TgpuFixedBufferImpl/$'); } + set $(value: InferGPU) { + const mode = getExecMode(); + const insideTgpuFn = isInsideTgpuFn(); + + if (mode.type === 'normal') { + throw new IllegalBufferAccessError( + insideTgpuFn + ? `Cannot access ${String( + this.buffer, + )}. TypeGPU functions that depends on GPU resources need to be part of a compute dispatch, draw call or simulation` + : '.$ is inaccessible during normal JS execution. Try `.write()`', + ); + } + + if (mode.type === 'codegen') { + // The WGSL generator handles buffer assignment, and does not defer to + // whatever's being assigned to generate the WGSL. + throw new Error('Unreachable bufferUsage.ts#TgpuFixedBufferImpl/$'); + } + + if (mode.type === 'simulate') { + mode.buffers.set(this.buffer, value); + return; + } + + assertExhaustive(mode, 'bufferUsage.ts#TgpuFixedBufferImpl/$'); + } + get value(): InferGPU { return this.$; } + set value(value: InferGPU) { + this.$ = value; + } + toString(): string { return `${this.usage}BufferShorthand:${getName(this) ?? ''}`; } diff --git a/packages/typegpu/src/core/buffer/bufferUsage.ts b/packages/typegpu/src/core/buffer/bufferUsage.ts index 66adcdcbd1..b97cdb37e1 100644 --- a/packages/typegpu/src/core/buffer/bufferUsage.ts +++ b/packages/typegpu/src/core/buffer/bufferUsage.ts @@ -20,6 +20,7 @@ import type { LayoutMembership } from '../../tgpuBindGroupLayout.ts'; import type { BindableBufferUsage, ResolutionCtx, SelfResolvable } from '../../types.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; import type { TgpuBuffer, UniformFlag } from './buffer.ts'; +import { TgpuBufferShorthandImpl, type TgpuMutable } from './bufferShorthand.ts'; // ---------- // Public API @@ -288,12 +289,12 @@ export class TgpuLaidOutBufferImpl, - TgpuFixedBufferImpl + TgpuBufferShorthandImpl<'mutable', BaseData> >(); export function mutable( buffer: TgpuBuffer & StorageFlag, -): TgpuBufferMutable & TgpuFixedBufferUsage { +): TgpuMutable { if (!isUsableAsStorage(buffer)) { throw new Error( `Cannot call as('mutable') on ${buffer}, as it is not allowed to be used as storage. To allow it, call .$usage('storage') when creating the buffer.`, @@ -302,10 +303,10 @@ export function mutable( let usage = mutableUsageMap.get(buffer); if (!usage) { - usage = new TgpuFixedBufferImpl('mutable', buffer); + usage = new TgpuBufferShorthandImpl('mutable', buffer); mutableUsageMap.set(buffer, usage); } - return usage as unknown as TgpuBufferMutable & TgpuFixedBufferUsage; + return usage as unknown as TgpuMutable; } const readonlyUsageMap = new WeakMap< From d4430d1fbd761b98533c7451fe785661f5be2669 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:52:26 +0200 Subject: [PATCH 04/16] Return readonly instead of usage --- packages/typegpu/src/core/buffer/bufferUsage.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/typegpu/src/core/buffer/bufferUsage.ts b/packages/typegpu/src/core/buffer/bufferUsage.ts index b97cdb37e1..5df126b003 100644 --- a/packages/typegpu/src/core/buffer/bufferUsage.ts +++ b/packages/typegpu/src/core/buffer/bufferUsage.ts @@ -20,7 +20,7 @@ import type { LayoutMembership } from '../../tgpuBindGroupLayout.ts'; import type { BindableBufferUsage, ResolutionCtx, SelfResolvable } from '../../types.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; import type { TgpuBuffer, UniformFlag } from './buffer.ts'; -import { TgpuBufferShorthandImpl, type TgpuMutable } from './bufferShorthand.ts'; +import { TgpuBufferShorthandImpl, type TgpuMutable, type TgpuReadonly } from './bufferShorthand.ts'; // ---------- // Public API @@ -311,12 +311,12 @@ export function mutable( const readonlyUsageMap = new WeakMap< TgpuBuffer, - TgpuFixedBufferImpl + TgpuBufferShorthandImpl<'readonly', BaseData> >(); export function readonly( buffer: TgpuBuffer & StorageFlag, -): TgpuBufferReadonly & TgpuFixedBufferUsage { +): TgpuReadonly { if (!isUsableAsStorage(buffer)) { throw new Error( `Cannot call as('readonly') on ${buffer}, as it is not allowed to be used as storage. To allow it, call .$usage('storage') when creating the buffer.`, @@ -325,10 +325,10 @@ export function readonly( let usage = readonlyUsageMap.get(buffer); if (!usage) { - usage = new TgpuFixedBufferImpl('readonly', buffer); + usage = new TgpuBufferShorthandImpl('readonly', buffer); readonlyUsageMap.set(buffer, usage); } - return usage as unknown as TgpuBufferReadonly & TgpuFixedBufferUsage; + return usage as unknown as TgpuReadonly; } const uniformUsageMap = new WeakMap< From d17a724334506c5201eaf8fcdad499543b94903a Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:52:47 +0200 Subject: [PATCH 05/16] Return uniform instead of usage --- packages/typegpu/src/core/buffer/bufferUsage.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/typegpu/src/core/buffer/bufferUsage.ts b/packages/typegpu/src/core/buffer/bufferUsage.ts index 5df126b003..6130796a7f 100644 --- a/packages/typegpu/src/core/buffer/bufferUsage.ts +++ b/packages/typegpu/src/core/buffer/bufferUsage.ts @@ -20,7 +20,12 @@ import type { LayoutMembership } from '../../tgpuBindGroupLayout.ts'; import type { BindableBufferUsage, ResolutionCtx, SelfResolvable } from '../../types.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; import type { TgpuBuffer, UniformFlag } from './buffer.ts'; -import { TgpuBufferShorthandImpl, type TgpuMutable, type TgpuReadonly } from './bufferShorthand.ts'; +import { + TgpuBufferShorthandImpl, + type TgpuMutable, + type TgpuReadonly, + type TgpuUniform, +} from './bufferShorthand.ts'; // ---------- // Public API @@ -333,12 +338,12 @@ export function readonly( const uniformUsageMap = new WeakMap< TgpuBuffer, - TgpuFixedBufferImpl + TgpuBufferShorthandImpl<'uniform', BaseData> >(); export function uniform( buffer: TgpuBuffer & UniformFlag, -): TgpuBufferUniform & TgpuFixedBufferUsage { +): TgpuUniform { if (!isUsableAsUniform(buffer)) { throw new Error( `Cannot call as('uniform') on ${buffer}, as it is not allowed to be used as a uniform. To allow it, call .$usage('uniform') when creating the buffer.`, @@ -347,8 +352,8 @@ export function uniform( let usage = uniformUsageMap.get(buffer); if (!usage) { - usage = new TgpuFixedBufferImpl('uniform', buffer); + usage = new TgpuBufferShorthandImpl('uniform', buffer); uniformUsageMap.set(buffer, usage); } - return usage as unknown as TgpuBufferUniform & TgpuFixedBufferUsage; + return usage as unknown as TgpuUniform; } From 2c1175e98a1fe9483cbfff3a60ea5a712cd6da26 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:43:41 +0200 Subject: [PATCH 06/16] Add $repr token to shorthands --- packages/typegpu/src/core/buffer/bufferShorthand.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/typegpu/src/core/buffer/bufferShorthand.ts b/packages/typegpu/src/core/buffer/bufferShorthand.ts index aaad7314c1..b66bcc0b46 100644 --- a/packages/typegpu/src/core/buffer/bufferShorthand.ts +++ b/packages/typegpu/src/core/buffer/bufferShorthand.ts @@ -37,6 +37,9 @@ interface TgpuBufferShorthandBase extends TgpuNamable { // Accessible on the GPU readonly [$gpuValueOf]: InferGPU; // --- + + /** Type-token, not available at runtime */ + readonly [$repr]: Infer; } export interface TgpuMutable extends TgpuBufferShorthandBase { @@ -51,6 +54,9 @@ export interface TgpuMutable extends TgpuBufferShort value: InferGPU; $: InferGPU; // --- + + /** Type-token, not available at runtime */ + readonly [$repr]: Infer; } export interface TgpuReadonly extends TgpuBufferShorthandBase { @@ -65,6 +71,9 @@ export interface TgpuReadonly extends TgpuBufferShor readonly value: InferGPU; readonly $: InferGPU; // --- + + /** Type-token, not available at runtime */ + readonly [$repr]: Infer; } export interface TgpuUniform extends TgpuBufferShorthandBase { From a366bf031fe863399133db5d4ea83ceae45d65ad Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:44:11 +0200 Subject: [PATCH 07/16] Deprecate TgpuBuffer* --- .../simulation/fluid-double-buffering/index.ts | 10 +++++----- packages/typegpu/src/core/buffer/buffer.ts | 17 +++++------------ packages/typegpu/src/core/buffer/bufferUsage.ts | 9 +++++++++ 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts index 06b40b268a..35fac0e2f3 100644 --- a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts @@ -1,5 +1,5 @@ import { randf } from '@typegpu/noise'; -import tgpu, { d, std, type TgpuBufferMutable, type TgpuBufferReadonly } from 'typegpu'; +import tgpu, { d, std, type TgpuMutable, type TgpuReadonly } from 'typegpu'; import { defineControls } from '../../common/defineControls.ts'; const MAX_GRID_SIZE = 1024; @@ -22,8 +22,8 @@ const BoxObstacle = d.struct({ const gridSize = 256; -const inputGridSlot = tgpu.slot | TgpuBufferMutable>(); -const outputGridSlot = tgpu.slot>(); +const inputGridSlot = tgpu.slot | TgpuMutable>(); +const outputGridSlot = tgpu.slot>(); const MAX_OBSTACLES = 4; const BoxObstacleArray = d.arrayOf(BoxObstacle, MAX_OBSTACLES); @@ -409,8 +409,8 @@ const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ canvas, alphaMode: 'premultiplied' }); function makePipelines( - inputGridReadonly: TgpuBufferReadonly, - outputGridMutable: TgpuBufferMutable, + inputGridReadonly: TgpuReadonly, + outputGridMutable: TgpuMutable, ) { const initWorldPipeline = root .with(outputGridSlot, outputGridMutable) diff --git a/packages/typegpu/src/core/buffer/buffer.ts b/packages/typegpu/src/core/buffer/buffer.ts index bd26adc6b7..1abddadee7 100644 --- a/packages/typegpu/src/core/buffer/buffer.ts +++ b/packages/typegpu/src/core/buffer/buffer.ts @@ -22,17 +22,10 @@ import { $internal } from '../../shared/symbols.ts'; import type { Prettify, UnionToIntersection } from '../../shared/utilityTypes.ts'; import { isGPUBuffer } from '../../types.ts'; import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts'; -import { - mutable, - readonly, - type TgpuBufferMutable, - type TgpuBufferReadonly, - type TgpuBufferUniform, - type TgpuFixedBufferUsage, - uniform, -} from './bufferUsage.ts'; +import { mutable, readonly, uniform } from './bufferUsage.ts'; import { calculateOffsets, readFromArrayBuffer, writeToArrayBuffer } from '../../data/dataIO.ts'; import { patchArrayBuffer } from '../../data/partialIO.ts'; +import type { TgpuMutable, TgpuReadonly, TgpuUniform } from './bufferShorthand.ts'; // ---------- // Public API @@ -83,9 +76,9 @@ type ViewUsages> = | (boolean extends TBuffer['usableAsStorage'] ? never : 'readonly' | 'mutable'); type UsageTypeToBufferUsage = { - uniform: TgpuBufferUniform & TgpuFixedBufferUsage; - mutable: TgpuBufferMutable & TgpuFixedBufferUsage; - readonly: TgpuBufferReadonly & TgpuFixedBufferUsage; + uniform: TgpuUniform; + mutable: TgpuMutable; + readonly: TgpuReadonly; }; const usageToUsageConstructor = { uniform, mutable, readonly }; diff --git a/packages/typegpu/src/core/buffer/bufferUsage.ts b/packages/typegpu/src/core/buffer/bufferUsage.ts index 6130796a7f..647980415a 100644 --- a/packages/typegpu/src/core/buffer/bufferUsage.ts +++ b/packages/typegpu/src/core/buffer/bufferUsage.ts @@ -51,6 +51,9 @@ export interface TgpuBufferUsage< }; } +/** + * @deprecated use TgpuUniform instead. + */ export interface TgpuBufferUniform extends TgpuBufferUsage< TData, 'uniform' @@ -62,6 +65,9 @@ export interface TgpuBufferUniform extends TgpuBufferUsa readonly $: InferGPU; } +/** + * @deprecated use TgpuReadonly instead. + */ export interface TgpuBufferReadonly extends TgpuBufferUsage< TData, 'readonly' @@ -77,6 +83,9 @@ export interface TgpuFixedBufferUsage extends TgpuNamabl readonly buffer: TgpuBuffer; } +/** + * @deprecated use TgpuMutable instead. + */ export interface TgpuBufferMutable extends TgpuBufferUsage< TData, 'mutable' From f679da9eb809c734fa7e76aea931d711dc8be747 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:03:40 +0200 Subject: [PATCH 08/16] Remove fixed buffer impl --- packages/typegpu/src/core/buffer/buffer.ts | 8 +- .../typegpu/src/core/buffer/bufferUsage.ts | 157 ++---------------- packages/typegpu/src/types.ts | 3 + 3 files changed, 19 insertions(+), 149 deletions(-) diff --git a/packages/typegpu/src/core/buffer/buffer.ts b/packages/typegpu/src/core/buffer/buffer.ts index 1abddadee7..a5949f5a85 100644 --- a/packages/typegpu/src/core/buffer/buffer.ts +++ b/packages/typegpu/src/core/buffer/buffer.ts @@ -75,7 +75,7 @@ type ViewUsages> = | (boolean extends TBuffer['usableAsUniform'] ? never : 'uniform') | (boolean extends TBuffer['usableAsStorage'] ? never : 'readonly' | 'mutable'); -type UsageTypeToBufferUsage = { +type UsageTypeToBufferShorthand = { uniform: TgpuUniform; mutable: TgpuMutable; readonly: TgpuReadonly; @@ -134,7 +134,7 @@ export interface TgpuBuffer extends TgpuNamable { ): this & UnionToIntersection>; $addFlags(flags: GPUBufferUsageFlags): this; - as>(usage: T): UsageTypeToBufferUsage[T]; + as>(usage: T): UsageTypeToBufferShorthand[T]; compileWriter(): void; write(data: InferInput, options?: BufferWriteOptions): void; @@ -442,8 +442,8 @@ class TgpuBufferImpl implements TgpuBuffer { return res; } - as>(usage: T): UsageTypeToBufferUsage[T] { - return usageToUsageConstructor[usage]?.(this as never) as UsageTypeToBufferUsage[T]; + as>(usage: T): UsageTypeToBufferShorthand[T] { + return usageToUsageConstructor[usage]?.(this as never) as UsageTypeToBufferShorthand[T]; } destroy() { diff --git a/packages/typegpu/src/core/buffer/bufferUsage.ts b/packages/typegpu/src/core/buffer/bufferUsage.ts index 647980415a..2cf461a1b9 100644 --- a/packages/typegpu/src/core/buffer/bufferUsage.ts +++ b/packages/typegpu/src/core/buffer/bufferUsage.ts @@ -1,21 +1,11 @@ -import { schemaCallWrapper } from '../../data/schemaCallWrapper.ts'; import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; import { type AnyWgslData, type BaseData } from '../../data/wgslTypes.ts'; -import { IllegalBufferAccessError } from '../../errors.ts'; -import { getExecMode, inCodegenMode, isInsideTgpuFn } from '../../execMode.ts'; +import { inCodegenMode } 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'; -import { - $getNameForward, - $gpuValueOf, - $internal, - $ownSnippet, - $repr, - $resolve, -} from '../../shared/symbols.ts'; -import { assertExhaustive } from '../../shared/utilityTypes.ts'; +import { $gpuValueOf, $internal, $ownSnippet, $repr, $resolve } from '../../shared/symbols.ts'; import type { LayoutMembership } from '../../tgpuBindGroupLayout.ts'; import type { BindableBufferUsage, ResolutionCtx, SelfResolvable } from '../../types.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; @@ -101,143 +91,20 @@ export function isUsableAsUniform>( // Implementation // -------------- -class TgpuFixedBufferImpl - implements TgpuBufferUsage, SelfResolvable, TgpuFixedBufferUsage -{ - /** Type-token, not available at runtime */ - declare readonly [$repr]: Infer; - - readonly resourceType = 'buffer-usage' as const; - readonly usage: TUsage; - readonly buffer: TgpuBuffer; - readonly [$internal]: { readonly dataType: TData }; - readonly [$getNameForward]: TgpuBuffer; - - constructor(usage: TUsage, buffer: TgpuBuffer) { - this.usage = usage; - this.buffer = buffer; - this[$internal] = { dataType: buffer.dataType }; - this[$getNameForward] = buffer; - } - - $name(label: string) { - setName(this, label); - return this; - } - - [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const dataType = this.buffer.dataType; - const id = ctx.makeUniqueIdentifier(getName(this), 'global'); - const { group, binding } = ctx.allocateFixedEntry( - this.usage === 'uniform' ? { uniform: dataType } : { storage: dataType, access: this.usage }, - this.buffer, - ); - - return ctx.gen.declareGlobalVar({ - group, - binding, - scope: this.usage, - id, - dataType, - init: undefined, - }); - } - - toString(): string { - return `${this.usage}:${getName(this) ?? ''}`; - } - - get [$gpuValueOf](): InferGPU { - const dataType = this.buffer.dataType; - const usage = this.usage; - - return new Proxy( - { - [$internal]: true, - get [$ownSnippet]() { - return snip(this, dataType, usage); - }, - [$resolve]: (ctx) => ctx.resolve(this), - toString: () => `${this.usage}:${getName(this) ?? ''}.$`, - }, - valueProxyHandler, - ) as InferGPU; - } - - get $(): InferGPU { - const mode = getExecMode(); - const insideTgpuFn = isInsideTgpuFn(); - - if (mode.type === 'normal') { - throw new IllegalBufferAccessError( - insideTgpuFn - ? `Cannot access ${String( - this.buffer, - )}. TypeGPU functions that depends on GPU resources need to be part of a compute dispatch, draw call or simulation` - : '.$ is inaccessible during normal JS execution. Try `.read()`', - ); - } - - if (mode.type === 'codegen') { - return this[$gpuValueOf]; - } - - if (mode.type === 'simulate') { - if (!mode.buffers.has(this.buffer)) { - // Not initialized yet - mode.buffers.set(this.buffer, schemaCallWrapper(this.buffer.dataType, this.buffer.initial)); - } - return mode.buffers.get(this.buffer) as InferGPU; - } - - return assertExhaustive(mode, 'bufferUsage.ts#TgpuFixedBufferImpl/$'); - } - - get value(): InferGPU { - return this.$; - } - - set $(value: InferGPU) { - const mode = getExecMode(); - const insideTgpuFn = isInsideTgpuFn(); - - if (mode.type === 'normal') { - throw new IllegalBufferAccessError( - insideTgpuFn - ? `Cannot access ${String( - this.buffer, - )}. TypeGPU functions that depends on GPU resources need to be part of a compute dispatch, draw call or simulation` - : '.$ is inaccessible during normal JS execution. Try `.write()`', - ); - } - - if (mode.type === 'codegen') { - // The WGSL generator handles buffer assignment, and does not defer to - // whatever's being assigned to generate the WGSL. - throw new Error('Unreachable bufferUsage.ts#TgpuFixedBufferImpl/$'); - } - - if (mode.type === 'simulate') { - mode.buffers.set(this.buffer, value); - return; - } - - assertExhaustive(mode, 'bufferUsage.ts#TgpuFixedBufferImpl/$'); - } - - set value(value: InferGPU) { - this.$ = value; - } -} - -export class TgpuLaidOutBufferImpl - implements TgpuBufferUsage, SelfResolvable -{ +/** + * A class representing a buffer accessed via a BindGroupLayout. + * Compared to a regular buffer, it's missing read/write methods, + * but it holds a membership. + */ +export class TgpuLaidOutBufferImpl< + TData extends BaseData, + TUsage extends BindableBufferUsage, +> implements SelfResolvable { /** Type-token, not available at runtime */ declare readonly [$repr]: Infer; readonly [$internal]: { readonly dataType: TData }; - readonly resourceType = 'buffer-usage' as const; + readonly resourceType = 'laid-out-buffer' as const; readonly usage: TUsage; readonly dataType: TData; diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index 86c40a3467..dc80abb36d 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -442,6 +442,9 @@ export function isGPUBuffer(value: unknown): value is GPUBuffer { return !!value && typeof value === 'object' && 'getMappedRange' in value && 'mapAsync' in value; } +/** + * @deprecated Use isBufferShorthand instead. + */ export function isBufferUsage( value: unknown, ): value is From 6c22fe5d0746060b27e1d1de8fcba9699dbe088c Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:02:08 +0200 Subject: [PATCH 09/16] Remove stray type occurrences --- packages/typegpu/src/core/buffer/bufferUsage.ts | 7 +------ packages/typegpu/src/core/slot/slotTypes.ts | 3 --- packages/typegpu/src/types.ts | 2 -- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/typegpu/src/core/buffer/bufferUsage.ts b/packages/typegpu/src/core/buffer/bufferUsage.ts index 2cf461a1b9..6dd89ccdd7 100644 --- a/packages/typegpu/src/core/buffer/bufferUsage.ts +++ b/packages/typegpu/src/core/buffer/bufferUsage.ts @@ -2,7 +2,6 @@ import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; import { type AnyWgslData, type BaseData } from '../../data/wgslTypes.ts'; import { inCodegenMode } 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'; import { $gpuValueOf, $internal, $ownSnippet, $repr, $resolve } from '../../shared/symbols.ts'; @@ -21,7 +20,7 @@ import { // Public API // ---------- -export interface TgpuBufferUsage< +interface TgpuBufferUsage< TData extends BaseData = BaseData, TUsage extends BindableBufferUsage = BindableBufferUsage, > { @@ -69,10 +68,6 @@ export interface TgpuBufferReadonly extends TgpuBufferUs readonly $: InferGPU; } -export interface TgpuFixedBufferUsage extends TgpuNamable { - readonly buffer: TgpuBuffer; -} - /** * @deprecated use TgpuMutable instead. */ diff --git a/packages/typegpu/src/core/slot/slotTypes.ts b/packages/typegpu/src/core/slot/slotTypes.ts index 8fce92c90d..a5e5ae0579 100644 --- a/packages/typegpu/src/core/slot/slotTypes.ts +++ b/packages/typegpu/src/core/slot/slotTypes.ts @@ -5,7 +5,6 @@ import type { GPUValueOf, Infer, InferGPU } from '../../shared/repr.ts'; import { $gpuValueOf, $internal, $providing } from '../../shared/symbols.ts'; import type { UnwrapRuntimeConstructor } from '../../tgpuBindGroupLayout.ts'; import type { TgpuBufferShorthand } from '../buffer/bufferShorthand.ts'; -import type { TgpuBufferUsage } from '../buffer/bufferUsage.ts'; import type { TgpuConst } from '../constant/tgpuConstant.ts'; import type { Withable } from '../root/rootTypes.ts'; import type { TgpuTextureView } from '../texture/texture.ts'; @@ -68,7 +67,6 @@ export interface TgpuAccessor extends TgpuNamable type DataAccessorIn = | (() => DataAccessorIn) - | TgpuBufferUsage | TgpuBufferShorthand | TgpuVar | TgpuConst @@ -101,7 +99,6 @@ export interface TgpuMutableAccessor extends Tgpu type MutableDataAccessorIn = | (() => Infer | MutableDataAccessorIn) - | TgpuBufferUsage | TgpuBufferShorthand | TgpuVar; diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index dc80abb36d..084d3903e7 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -4,7 +4,6 @@ import type { TgpuBufferMutable, TgpuBufferReadonly, TgpuBufferUniform, - TgpuBufferUsage, } from './core/buffer/bufferUsage.ts'; import type { TgpuConst } from './core/constant/tgpuConstant.ts'; import type { TgpuDeclare } from './core/declare/tgpuDeclare.ts'; @@ -51,7 +50,6 @@ import { ShaderGenerator } from './tgsl/shaderGenerator.ts'; export type ResolvableObject = | SelfResolvable - | TgpuBufferUsage | TgpuConst | TgpuDeclare | TgpuBindGroupLayout From 2127b255c1853ae9aabc2d20f3d60b8215d276a0 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:37:40 +0200 Subject: [PATCH 10/16] Revert the resourceType & usage split --- .../typegpu-gl/tests/webglFallback.test.ts | 2 +- .../src/core/buffer/bufferShorthand.ts | 24 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/typegpu-gl/tests/webglFallback.test.ts b/packages/typegpu-gl/tests/webglFallback.test.ts index fda462d487..6bf5da8552 100644 --- a/packages/typegpu-gl/tests/webglFallback.test.ts +++ b/packages/typegpu-gl/tests/webglFallback.test.ts @@ -55,7 +55,7 @@ describe('TgpuRootWebGL - createUniform', () => { const uniform = root.createUniform(d.vec4f); expect(uniform).toBeDefined(); - expect(uniform.usage).toBe('uniform'); + expect(uniform.resourceType).toBe('uniform'); expect(gl.createBuffer).toHaveBeenCalled(); }); diff --git a/packages/typegpu/src/core/buffer/bufferShorthand.ts b/packages/typegpu/src/core/buffer/bufferShorthand.ts index b66bcc0b46..36b4fecb03 100644 --- a/packages/typegpu/src/core/buffer/bufferShorthand.ts +++ b/packages/typegpu/src/core/buffer/bufferShorthand.ts @@ -43,8 +43,7 @@ interface TgpuBufferShorthandBase extends TgpuNamable { } export interface TgpuMutable extends TgpuBufferShorthandBase { - readonly resourceType: 'buffer-shorthand'; - readonly usage: 'mutable'; + readonly resourceType: 'mutable'; readonly buffer: TgpuBuffer & StorageFlag; // Accessible on the GPU @@ -60,8 +59,7 @@ export interface TgpuMutable extends TgpuBufferShort } export interface TgpuReadonly extends TgpuBufferShorthandBase { - readonly resourceType: 'buffer-shorthand'; - readonly usage: 'readonly'; + readonly resourceType: 'readonly'; readonly buffer: TgpuBuffer & StorageFlag; // Accessible on the GPU @@ -77,8 +75,7 @@ export interface TgpuReadonly extends TgpuBufferShor } export interface TgpuUniform extends TgpuBufferShorthandBase { - readonly resourceType: 'buffer-shorthand'; - readonly usage: 'uniform'; + readonly resourceType: 'uniform'; readonly buffer: TgpuBuffer & UniformFlag; // Accessible on the GPU @@ -114,16 +111,15 @@ export class TgpuBufferShorthandImpl< readonly [$internal] = true; readonly [$getNameForward]: object; - readonly usage: TType; readonly buffer: TgpuBuffer & (TType extends 'mutable' | 'readonly' ? StorageFlag : UniformFlag); - readonly resourceType = 'buffer-shorthand'; + readonly resourceType: TType; constructor( usage: TType, buffer: TgpuBuffer & (TType extends 'mutable' | 'readonly' ? StorageFlag : UniformFlag), ) { - this.usage = usage; + this.resourceType = usage; this.buffer = buffer; this[$getNameForward] = buffer; } @@ -152,7 +148,7 @@ export class TgpuBufferShorthandImpl< get [$gpuValueOf](): InferGPU { const dataType = this.buffer.dataType; - const usage = this.usage; + const usage = this.resourceType; return new Proxy( { @@ -233,21 +229,23 @@ export class TgpuBufferShorthandImpl< } toString(): string { - return `${this.usage}BufferShorthand:${getName(this) ?? ''}`; + return `${this.resourceType}BufferShorthand:${getName(this) ?? ''}`; } [$resolve](ctx: ResolutionCtx): ResolvedSnippet { const dataType = this.buffer.dataType; const id = ctx.makeUniqueIdentifier(getName(this), 'global'); const { group, binding } = ctx.allocateFixedEntry( - this.usage === 'uniform' ? { uniform: dataType } : { storage: dataType, access: this.usage }, + this.resourceType === 'uniform' + ? { uniform: dataType } + : { storage: dataType, access: this.resourceType }, this.buffer, ); return ctx.gen.declareGlobalVar({ group, binding, - scope: this.usage, + scope: this.resourceType, id, dataType, init: undefined, From 9260c707941dd295a3e7113300c6c72af988e3e4 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:46:35 +0200 Subject: [PATCH 11/16] Move constructors to bufferShorthand --- packages/typegpu/src/core/buffer/buffer.ts | 12 ++- .../src/core/buffer/bufferShorthand.ts | 75 ++++++++++++++++++- .../typegpu/src/core/buffer/bufferUsage.ts | 66 ---------------- 3 files changed, 82 insertions(+), 71 deletions(-) diff --git a/packages/typegpu/src/core/buffer/buffer.ts b/packages/typegpu/src/core/buffer/buffer.ts index a5949f5a85..e79084db46 100644 --- a/packages/typegpu/src/core/buffer/buffer.ts +++ b/packages/typegpu/src/core/buffer/buffer.ts @@ -22,10 +22,16 @@ import { $internal } from '../../shared/symbols.ts'; import type { Prettify, UnionToIntersection } from '../../shared/utilityTypes.ts'; import { isGPUBuffer } from '../../types.ts'; import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts'; -import { mutable, readonly, uniform } from './bufferUsage.ts'; import { calculateOffsets, readFromArrayBuffer, writeToArrayBuffer } from '../../data/dataIO.ts'; import { patchArrayBuffer } from '../../data/partialIO.ts'; -import type { TgpuMutable, TgpuReadonly, TgpuUniform } from './bufferShorthand.ts'; +import { + mutable, + readonly, + uniform, + type TgpuMutable, + type TgpuReadonly, + type TgpuUniform, +} from './bufferShorthand.ts'; // ---------- // Public API @@ -443,7 +449,7 @@ class TgpuBufferImpl implements TgpuBuffer { } as>(usage: T): UsageTypeToBufferShorthand[T] { - return usageToUsageConstructor[usage]?.(this as never) as UsageTypeToBufferShorthand[T]; + return usageToUsageConstructor[usage](this as never) as UsageTypeToBufferShorthand[T]; } destroy() { diff --git a/packages/typegpu/src/core/buffer/bufferShorthand.ts b/packages/typegpu/src/core/buffer/bufferShorthand.ts index 36b4fecb03..75cf8c85ce 100644 --- a/packages/typegpu/src/core/buffer/bufferShorthand.ts +++ b/packages/typegpu/src/core/buffer/bufferShorthand.ts @@ -1,9 +1,9 @@ import { schemaCallWrapper } from '../../data/schemaCallWrapper.ts'; import { snip, type ResolvedSnippet } from '../../data/snippet.ts'; -import type { BaseData } from '../../data/wgslTypes.ts'; +import type { AnyWgslData, BaseData } from '../../data/wgslTypes.ts'; import { IllegalBufferAccessError } from '../../errors.ts'; import { getExecMode, isInsideTgpuFn } from '../../execMode.ts'; -import type { StorageFlag } from '../../extension.ts'; +import { isUsableAsStorage, type StorageFlag } from '../../extension.ts'; import { getName, setName, type TgpuNamable } from '../../shared/meta.ts'; import type { Infer, InferGPU, InferInput, InferPatch, InferPartial } from '../../shared/repr.ts'; import { @@ -18,6 +18,7 @@ import { assertExhaustive } from '../../shared/utilityTypes.ts'; import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; import { valueProxyHandler } from '../valueProxyUtils.ts'; import type { BufferWriteOptions, TgpuBuffer, UniformFlag } from './buffer.ts'; +import { isUsableAsUniform } from './bufferUsage.ts'; // ---------- // Public API @@ -252,3 +253,73 @@ export class TgpuBufferShorthandImpl< }); } } + +// -------------- +// Constructors +// -------------- + +const mutableUsageMap = new WeakMap< + TgpuBuffer, + TgpuBufferShorthandImpl<'mutable', BaseData> +>(); + +export function mutable( + buffer: TgpuBuffer & StorageFlag, +): TgpuMutable { + if (!isUsableAsStorage(buffer)) { + throw new Error( + `Cannot call as('mutable') on ${buffer}, as it is not allowed to be used as storage. To allow it, call .$usage('storage') when creating the buffer.`, + ); + } + + let usage = mutableUsageMap.get(buffer); + if (!usage) { + usage = new TgpuBufferShorthandImpl('mutable', buffer); + mutableUsageMap.set(buffer, usage); + } + return usage as unknown as TgpuMutable; +} + +const readonlyUsageMap = new WeakMap< + TgpuBuffer, + TgpuBufferShorthandImpl<'readonly', BaseData> +>(); + +export function readonly( + buffer: TgpuBuffer & StorageFlag, +): TgpuReadonly { + if (!isUsableAsStorage(buffer)) { + throw new Error( + `Cannot call as('readonly') on ${buffer}, as it is not allowed to be used as storage. To allow it, call .$usage('storage') when creating the buffer.`, + ); + } + + let usage = readonlyUsageMap.get(buffer); + if (!usage) { + usage = new TgpuBufferShorthandImpl('readonly', buffer); + readonlyUsageMap.set(buffer, usage); + } + return usage as unknown as TgpuReadonly; +} + +const uniformUsageMap = new WeakMap< + TgpuBuffer, + TgpuBufferShorthandImpl<'uniform', BaseData> +>(); + +export function uniform( + buffer: TgpuBuffer & UniformFlag, +): TgpuUniform { + if (!isUsableAsUniform(buffer)) { + throw new Error( + `Cannot call as('uniform') on ${buffer}, as it is not allowed to be used as a uniform. To allow it, call .$usage('uniform') when creating the buffer.`, + ); + } + + let usage = uniformUsageMap.get(buffer); + if (!usage) { + usage = new TgpuBufferShorthandImpl('uniform', buffer); + uniformUsageMap.set(buffer, usage); + } + return usage as unknown as TgpuUniform; +} diff --git a/packages/typegpu/src/core/buffer/bufferUsage.ts b/packages/typegpu/src/core/buffer/bufferUsage.ts index 6dd89ccdd7..081dc8bc6b 100644 --- a/packages/typegpu/src/core/buffer/bufferUsage.ts +++ b/packages/typegpu/src/core/buffer/bufferUsage.ts @@ -162,69 +162,3 @@ export class TgpuLaidOutBufferImpl< return this.$; } } - -const mutableUsageMap = new WeakMap< - TgpuBuffer, - TgpuBufferShorthandImpl<'mutable', BaseData> ->(); - -export function mutable( - buffer: TgpuBuffer & StorageFlag, -): TgpuMutable { - if (!isUsableAsStorage(buffer)) { - throw new Error( - `Cannot call as('mutable') on ${buffer}, as it is not allowed to be used as storage. To allow it, call .$usage('storage') when creating the buffer.`, - ); - } - - let usage = mutableUsageMap.get(buffer); - if (!usage) { - usage = new TgpuBufferShorthandImpl('mutable', buffer); - mutableUsageMap.set(buffer, usage); - } - return usage as unknown as TgpuMutable; -} - -const readonlyUsageMap = new WeakMap< - TgpuBuffer, - TgpuBufferShorthandImpl<'readonly', BaseData> ->(); - -export function readonly( - buffer: TgpuBuffer & StorageFlag, -): TgpuReadonly { - if (!isUsableAsStorage(buffer)) { - throw new Error( - `Cannot call as('readonly') on ${buffer}, as it is not allowed to be used as storage. To allow it, call .$usage('storage') when creating the buffer.`, - ); - } - - let usage = readonlyUsageMap.get(buffer); - if (!usage) { - usage = new TgpuBufferShorthandImpl('readonly', buffer); - readonlyUsageMap.set(buffer, usage); - } - return usage as unknown as TgpuReadonly; -} - -const uniformUsageMap = new WeakMap< - TgpuBuffer, - TgpuBufferShorthandImpl<'uniform', BaseData> ->(); - -export function uniform( - buffer: TgpuBuffer & UniformFlag, -): TgpuUniform { - if (!isUsableAsUniform(buffer)) { - throw new Error( - `Cannot call as('uniform') on ${buffer}, as it is not allowed to be used as a uniform. To allow it, call .$usage('uniform') when creating the buffer.`, - ); - } - - let usage = uniformUsageMap.get(buffer); - if (!usage) { - usage = new TgpuBufferShorthandImpl('uniform', buffer); - uniformUsageMap.set(buffer, usage); - } - return usage as unknown as TgpuUniform; -} From 8dccd3a0724ca25290bff7470820aeedf2527889 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:48:28 +0200 Subject: [PATCH 12/16] Move laid out buffer to another file --- .../typegpu/src/core/buffer/bufferUsage.ts | 99 +------------------ .../typegpu/src/core/buffer/laidOutBuffer.ts | 85 ++++++++++++++++ packages/typegpu/src/tgpuBindGroupLayout.ts | 2 +- 3 files changed, 89 insertions(+), 97 deletions(-) create mode 100644 packages/typegpu/src/core/buffer/laidOutBuffer.ts diff --git a/packages/typegpu/src/core/buffer/bufferUsage.ts b/packages/typegpu/src/core/buffer/bufferUsage.ts index 081dc8bc6b..fa53eac334 100644 --- a/packages/typegpu/src/core/buffer/bufferUsage.ts +++ b/packages/typegpu/src/core/buffer/bufferUsage.ts @@ -1,20 +1,8 @@ -import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; -import { type AnyWgslData, type BaseData } from '../../data/wgslTypes.ts'; -import { inCodegenMode } from '../../execMode.ts'; -import { isUsableAsStorage, type StorageFlag } from '../../extension.ts'; -import { getName, setName } from '../../shared/meta.ts'; +import { type BaseData } from '../../data/wgslTypes.ts'; import type { Infer, InferGPU } from '../../shared/repr.ts'; -import { $gpuValueOf, $internal, $ownSnippet, $repr, $resolve } from '../../shared/symbols.ts'; -import type { LayoutMembership } from '../../tgpuBindGroupLayout.ts'; -import type { BindableBufferUsage, ResolutionCtx, SelfResolvable } from '../../types.ts'; -import { valueProxyHandler } from '../valueProxyUtils.ts'; +import { $gpuValueOf, $internal, $repr } from '../../shared/symbols.ts'; +import type { BindableBufferUsage } from '../../types.ts'; import type { TgpuBuffer, UniformFlag } from './buffer.ts'; -import { - TgpuBufferShorthandImpl, - type TgpuMutable, - type TgpuReadonly, - type TgpuUniform, -} from './bufferShorthand.ts'; // ---------- // Public API @@ -81,84 +69,3 @@ export function isUsableAsUniform>( ): buffer is T & UniformFlag { return !!buffer.usableAsUniform; } - -// -------------- -// Implementation -// -------------- - -/** - * A class representing a buffer accessed via a BindGroupLayout. - * Compared to a regular buffer, it's missing read/write methods, - * but it holds a membership. - */ -export class TgpuLaidOutBufferImpl< - TData extends BaseData, - TUsage extends BindableBufferUsage, -> implements SelfResolvable { - /** Type-token, not available at runtime */ - declare readonly [$repr]: Infer; - - readonly [$internal]: { readonly dataType: TData }; - readonly resourceType = 'laid-out-buffer' as const; - readonly usage: TUsage; - readonly dataType: TData; - - readonly #membership: LayoutMembership; - - constructor(usage: TUsage, dataType: TData, membership: LayoutMembership) { - this[$internal] = { dataType }; - this.usage = usage; - this.dataType = dataType; - this.#membership = membership; - setName(this, membership.key); - } - - [$resolve](ctx: ResolutionCtx): ResolvedSnippet { - const id = ctx.makeUniqueIdentifier(getName(this), 'global'); - const group = ctx.allocateLayoutEntry(this.#membership.layout); - - return ctx.gen.declareGlobalVar({ - group, - binding: this.#membership.idx, - scope: this.usage, - id, - dataType: this.dataType, - init: undefined, - }); - } - - toString(): string { - return `${this.usage}:${getName(this) ?? ''}`; - } - - get [$gpuValueOf](): InferGPU { - const schema = this.dataType; - const usage = this.usage; - - return new Proxy( - { - [$internal]: true, - get [$ownSnippet]() { - return snip(this, schema, usage); - }, - [$resolve]: (ctx) => ctx.resolve(this), - toString: () => `${this.usage}:${getName(this) ?? ''}.$`, - }, - valueProxyHandler, - ) as InferGPU; - } - - get $(): InferGPU { - if (inCodegenMode()) { - return this[$gpuValueOf]; - } - - throw new Error( - 'Direct access to buffer values is possible only as part of a compute dispatch or draw call. Try .read() or .write() instead', - ); - } - - get value(): InferGPU { - return this.$; - } -} diff --git a/packages/typegpu/src/core/buffer/laidOutBuffer.ts b/packages/typegpu/src/core/buffer/laidOutBuffer.ts new file mode 100644 index 0000000000..d9dd33568a --- /dev/null +++ b/packages/typegpu/src/core/buffer/laidOutBuffer.ts @@ -0,0 +1,85 @@ +import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; +import { type BaseData } from '../../data/wgslTypes.ts'; +import { inCodegenMode } from '../../execMode.ts'; +import { getName, setName } from '../../shared/meta.ts'; +import type { Infer, InferGPU } from '../../shared/repr.ts'; +import { $gpuValueOf, $internal, $ownSnippet, $repr, $resolve } from '../../shared/symbols.ts'; +import type { LayoutMembership } from '../../tgpuBindGroupLayout.ts'; +import type { BindableBufferUsage, ResolutionCtx, SelfResolvable } from '../../types.ts'; +import { valueProxyHandler } from '../valueProxyUtils.ts'; +/** + * A class representing a buffer accessed via a BindGroupLayout. + * Compared to a regular buffer, it's missing read/write methods, + * but it holds a membership. + */ +export class TgpuLaidOutBufferImpl< + TData extends BaseData, + TUsage extends BindableBufferUsage, +> implements SelfResolvable { + /** Type-token, not available at runtime */ + declare readonly [$repr]: Infer; + + readonly [$internal]: { readonly dataType: TData }; + readonly resourceType = 'laid-out-buffer' as const; + readonly usage: TUsage; + readonly dataType: TData; + + readonly #membership: LayoutMembership; + + constructor(usage: TUsage, dataType: TData, membership: LayoutMembership) { + this[$internal] = { dataType }; + this.usage = usage; + this.dataType = dataType; + this.#membership = membership; + setName(this, membership.key); + } + + [$resolve](ctx: ResolutionCtx): ResolvedSnippet { + const id = ctx.makeUniqueIdentifier(getName(this), 'global'); + const group = ctx.allocateLayoutEntry(this.#membership.layout); + + return ctx.gen.declareGlobalVar({ + group, + binding: this.#membership.idx, + scope: this.usage, + id, + dataType: this.dataType, + init: undefined, + }); + } + + toString(): string { + return `${this.usage}:${getName(this) ?? ''}`; + } + + get [$gpuValueOf](): InferGPU { + const schema = this.dataType; + const usage = this.usage; + + return new Proxy( + { + [$internal]: true, + get [$ownSnippet]() { + return snip(this, schema, usage); + }, + [$resolve]: (ctx) => ctx.resolve(this), + toString: () => `${this.usage}:${getName(this) ?? ''}.$`, + }, + valueProxyHandler, + ) as InferGPU; + } + + get $(): InferGPU { + if (inCodegenMode()) { + return this[$gpuValueOf]; + } + + throw new Error( + 'Direct access to buffer values is possible only as part of a compute dispatch or draw call. Try .read() or .write() instead', + ); + } + + get value(): InferGPU { + return this.$; + } +} diff --git a/packages/typegpu/src/tgpuBindGroupLayout.ts b/packages/typegpu/src/tgpuBindGroupLayout.ts index 77a9b57f13..a152be9c97 100644 --- a/packages/typegpu/src/tgpuBindGroupLayout.ts +++ b/packages/typegpu/src/tgpuBindGroupLayout.ts @@ -4,7 +4,6 @@ import { type TgpuBufferMutable, type TgpuBufferReadonly, type TgpuBufferUniform, - TgpuLaidOutBufferImpl, } from './core/buffer/bufferUsage.ts'; import { isComparisonSampler, @@ -56,6 +55,7 @@ import type { Default, NullableToOptional, Prettify } from './shared/utilityType import type { TgpuShaderStage } from './types.ts'; import type { Unwrapper } from './unwrapper.ts'; import type { WgslComparisonSampler, WgslSampler } from './data/sampler.ts'; +import { TgpuLaidOutBufferImpl } from './core/buffer/laidOutBuffer.ts'; // ---------- // Public API From ee319a57467d3cd25b4b21219430c3467e647361 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:12:54 +0200 Subject: [PATCH 13/16] Add TODOs --- packages/typegpu/src/core/buffer/bufferUsage.ts | 2 ++ packages/typegpu/tests/bufferUsage.test.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/typegpu/src/core/buffer/bufferUsage.ts b/packages/typegpu/src/core/buffer/bufferUsage.ts index fa53eac334..cd76ee78f7 100644 --- a/packages/typegpu/src/core/buffer/bufferUsage.ts +++ b/packages/typegpu/src/core/buffer/bufferUsage.ts @@ -4,6 +4,8 @@ import { $gpuValueOf, $internal, $repr } from '../../shared/symbols.ts'; import type { BindableBufferUsage } from '../../types.ts'; import type { TgpuBuffer, UniformFlag } from './buffer.ts'; +// TODO(#2666) - remove this file + // ---------- // Public API // ---------- diff --git a/packages/typegpu/tests/bufferUsage.test.ts b/packages/typegpu/tests/bufferUsage.test.ts index aaa3a671e1..b7b887983c 100644 --- a/packages/typegpu/tests/bufferUsage.test.ts +++ b/packages/typegpu/tests/bufferUsage.test.ts @@ -4,6 +4,7 @@ import { d, tgpu } from '../src/index.js'; import type { Infer } from '../src/shared/repr.ts'; import { it } from 'typegpu-testing-utility'; +// TODO(#2666) - remove this file describe('TgpuBufferUniform', () => { it('represents a `number` value', ({ root }) => { const buffer = root.createBuffer(d.f32).$usage('uniform'); From 39854593d3cdf8a6f419a6ecda35281427bb33c7 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:26:29 +0200 Subject: [PATCH 14/16] self review --- packages/typegpu/src/core/buffer/bufferShorthand.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/typegpu/src/core/buffer/bufferShorthand.ts b/packages/typegpu/src/core/buffer/bufferShorthand.ts index 75cf8c85ce..c1aa82fa33 100644 --- a/packages/typegpu/src/core/buffer/bufferShorthand.ts +++ b/packages/typegpu/src/core/buffer/bufferShorthand.ts @@ -112,9 +112,9 @@ export class TgpuBufferShorthandImpl< readonly [$internal] = true; readonly [$getNameForward]: object; + readonly resourceType: TType; readonly buffer: TgpuBuffer & (TType extends 'mutable' | 'readonly' ? StorageFlag : UniformFlag); - readonly resourceType: TType; constructor( usage: TType, @@ -190,7 +190,7 @@ export class TgpuBufferShorthandImpl< return mode.buffers.get(this.buffer) as InferGPU; } - return assertExhaustive(mode, 'bufferUsage.ts#TgpuFixedBufferImpl/$'); + return assertExhaustive(mode, 'bufferShorthand.ts#TgpuBufferShorthandImpl/$'); } set $(value: InferGPU) { @@ -210,7 +210,7 @@ export class TgpuBufferShorthandImpl< if (mode.type === 'codegen') { // The WGSL generator handles buffer assignment, and does not defer to // whatever's being assigned to generate the WGSL. - throw new Error('Unreachable bufferUsage.ts#TgpuFixedBufferImpl/$'); + throw new Error('Unreachable bufferShorthand.ts#TgpuBufferShorthandImpl/$'); } if (mode.type === 'simulate') { @@ -218,7 +218,7 @@ export class TgpuBufferShorthandImpl< return; } - assertExhaustive(mode, 'bufferUsage.ts#TgpuFixedBufferImpl/$'); + assertExhaustive(mode, 'bufferShorthand.ts#TgpuBufferShorthandImpl/$'); } get value(): InferGPU { From 295a509d263ae42ece0910c2937d64014fb9f8b5 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:32:29 +0200 Subject: [PATCH 15/16] Move uniform usage tests --- .../typegpu/tests/bufferShorthands.test.ts | 127 +++++++++++++++++- packages/typegpu/tests/bufferUsage.test.ts | 113 ---------------- 2 files changed, 120 insertions(+), 120 deletions(-) diff --git a/packages/typegpu/tests/bufferShorthands.test.ts b/packages/typegpu/tests/bufferShorthands.test.ts index 90e7896048..b19625181b 100644 --- a/packages/typegpu/tests/bufferShorthands.test.ts +++ b/packages/typegpu/tests/bufferShorthands.test.ts @@ -1,13 +1,14 @@ import { describe, expect, expectTypeOf } from 'vitest'; import * as d from 'typegpu/data'; import { it } from 'typegpu-testing-utility'; -import type { - StorageFlag, - TgpuBuffer, - TgpuMutable, - TgpuReadonly, - TgpuUniform, - UniformFlag, +import { + tgpu, + type StorageFlag, + type TgpuBuffer, + type TgpuMutable, + type TgpuReadonly, + type TgpuUniform, + type UniformFlag, } from 'typegpu'; import { attest } from '@ark/attest'; @@ -140,6 +141,118 @@ describe('root.createReadonly', () => { }); describe('root.createUniform', () => { + it('represents a `number` value', ({ root }) => { + const buffer = root.createBuffer(d.f32).$usage('uniform'); + const uniform = buffer.as('uniform'); + + expectTypeOf>().toEqualTypeOf(); + }); + + it('resolves to buffer binding in code', ({ root }) => { + const buffer = root.createBuffer(d.f32).$usage('uniform').$name('param'); + const uniform = buffer.as('uniform'); + + const main = tgpu.fn([])`() { let y = hello; }`.$uses({ hello: uniform }); + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "@group(0) @binding(0) var param: f32; + + fn main() { let y = param; }" + `); + }); + + it('resolves to buffer binding in tgsl functions', ({ root }) => { + const buffer = root.createBuffer(d.f32).$usage('uniform').$name('param'); + const uniform = buffer.as('uniform'); + + const func = tgpu.fn([])(() => { + const x = uniform.$; + }); + + expect(tgpu.resolve([func])).toMatchInlineSnapshot(` + "@group(0) @binding(0) var param: f32; + + fn func() { + let x = param; + }" + `); + }); + + it('allows accessing fields in a struct stored in its buffer', ({ root }) => { + const Boid = d.struct({ + pos: d.vec3f, + vel: d.vec3u, + }); + + const buffer = root.createBuffer(Boid).$usage('uniform').$name('boid'); + const uniform = buffer.as('uniform'); + + const func = tgpu.fn([])(() => { + const pos = uniform.$.pos; + const velX = uniform.$.vel.x; + }); + + expect(tgpu.resolve([func])).toMatchInlineSnapshot(` + "struct Boid { + pos: vec3f, + vel: vec3u, + } + + @group(0) @binding(0) var boid: Boid; + + fn func() { + let pos = (&boid.pos); + let velX = boid.vel.x; + }" + `); + }); + + it('forbids assignment to uniforms', ({ root }) => { + const myUniform = root.createUniform(d.vec2u); + const myFn = () => { + 'use gpu'; + // @ts-expect-error: .$ is a readonly property + myUniform.$ = d.vec2u(); + }; + + expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:myFn + - fn*:myFn(): 'myUniform.$ = d.vec2u()' is invalid, because uniform buffers cannot be mutated.] + `); + }); + + it('forbids assignment to uniform props', ({ root }) => { + const myUniform = root.createUniform(d.vec2u); + const myFn = () => { + 'use gpu'; + myUniform.$.x = 1; + }; + + expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:myFn + - fn*:myFn(): 'myUniform.$.x = 1' is invalid, because uniform buffers cannot be mutated.] + `); + }); + + it('allows creating shorthands only for buffers allowing them', ({ root }) => { + root.createBuffer(d.u32, 2).$usage('uniform').as('uniform'); + root.createBuffer(d.u32, 2).$usage('uniform', 'storage').as('uniform'); + root.createBuffer(d.u32, 2).$usage('uniform', 'vertex').as('uniform'); + // @ts-expect-error + expect(() => root.createBuffer(d.u32, 2).as('uniform')).toThrow(); + expect(() => + root + .createBuffer(d.u32, 2) + .$usage('storage') + // @ts-expect-error + .as('uniform'), + ).toThrow(); + }); + it('creates a uniform', ({ root }) => { const foo = root.createUniform(d.f32); diff --git a/packages/typegpu/tests/bufferUsage.test.ts b/packages/typegpu/tests/bufferUsage.test.ts index ee25b6845d..9510350de4 100644 --- a/packages/typegpu/tests/bufferUsage.test.ts +++ b/packages/typegpu/tests/bufferUsage.test.ts @@ -5,119 +5,6 @@ import type { Infer } from 'typegpu/data'; import { it } from 'typegpu-testing-utility'; // TODO(#2666) - remove this file -describe('TgpuBufferUniform', () => { - it('represents a `number` value', ({ root }) => { - const buffer = root.createBuffer(d.f32).$usage('uniform'); - const uniform = buffer.as('uniform'); - - expectTypeOf>().toEqualTypeOf(); - }); - - it('resolves to buffer binding in code', ({ root }) => { - const buffer = root.createBuffer(d.f32).$usage('uniform').$name('param'); - const uniform = buffer.as('uniform'); - - const main = tgpu.fn([])`() { let y = hello; }`.$uses({ hello: uniform }); - - expect(tgpu.resolve([main])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var param: f32; - - fn main() { let y = param; }" - `); - }); - - it('resolves to buffer binding in tgsl functions', ({ root }) => { - const buffer = root.createBuffer(d.f32).$usage('uniform').$name('param'); - const uniform = buffer.as('uniform'); - - const func = tgpu.fn([])(() => { - const x = uniform.$; - }); - - expect(tgpu.resolve([func])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var param: f32; - - fn func() { - let x = param; - }" - `); - }); - - it('allows accessing fields in a struct stored in its buffer', ({ root }) => { - const Boid = d.struct({ - pos: d.vec3f, - vel: d.vec3u, - }); - - const buffer = root.createBuffer(Boid).$usage('uniform').$name('boid'); - const uniform = buffer.as('uniform'); - - const func = tgpu.fn([])(() => { - const pos = uniform.$.pos; - const velX = uniform.$.vel.x; - }); - - expect(tgpu.resolve([func])).toMatchInlineSnapshot(` - "struct Boid { - pos: vec3f, - vel: vec3u, - } - - @group(0) @binding(0) var boid: Boid; - - fn func() { - let pos = (&boid.pos); - let velX = boid.vel.x; - }" - `); - }); - - it('forbids assignment to uniforms', ({ root }) => { - const myUniform = root.createUniform(d.vec2u); - const myFn = () => { - 'use gpu'; - // @ts-expect-error: .$ is a readonly property - myUniform.$ = d.vec2u(); - }; - - expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn*:myFn - - fn*:myFn(): 'myUniform.$ = d.vec2u()' is invalid, because uniform buffers cannot be mutated.] - `); - }); - - it('forbids assignment to uniform props', ({ root }) => { - const myUniform = root.createUniform(d.vec2u); - const myFn = () => { - 'use gpu'; - myUniform.$.x = 1; - }; - - expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn*:myFn - - fn*:myFn(): 'myUniform.$.x = 1' is invalid, because uniform buffers cannot be mutated.] - `); - }); - - it('allows creating bufferUsages only for buffers allowing them', ({ root }) => { - root.createBuffer(d.u32, 2).$usage('uniform').as('uniform'); - root.createBuffer(d.u32, 2).$usage('uniform', 'storage').as('uniform'); - root.createBuffer(d.u32, 2).$usage('uniform', 'vertex').as('uniform'); - // @ts-expect-error - expect(() => root.createBuffer(d.u32, 2).as('uniform')).toThrow(); - expect(() => - root - .createBuffer(d.u32, 2) - .$usage('storage') - // @ts-expect-error - .as('uniform'), - ).toThrow(); - }); -}); describe('TgpuBufferMutable', () => { it('represents a `number` value', ({ root }) => { From ef942b66e2044e7b4dc3c8e4d923cc505ccf09b7 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:40:54 +0200 Subject: [PATCH 16/16] Move remaining tests --- .../typegpu/tests/bufferShorthands.test.ts | 254 +++++++++++++++++ packages/typegpu/tests/bufferUsage.test.ts | 265 ------------------ 2 files changed, 254 insertions(+), 265 deletions(-) delete mode 100644 packages/typegpu/tests/bufferUsage.test.ts diff --git a/packages/typegpu/tests/bufferShorthands.test.ts b/packages/typegpu/tests/bufferShorthands.test.ts index b19625181b..bf66b045e7 100644 --- a/packages/typegpu/tests/bufferShorthands.test.ts +++ b/packages/typegpu/tests/bufferShorthands.test.ts @@ -13,6 +13,108 @@ import { import { attest } from '@ark/attest'; describe('root.createMutable', () => { + it('represents a `number` value', ({ root }) => { + const buffer = root.createBuffer(d.f32).$usage('storage'); + const mutable = buffer.as('mutable'); + + expectTypeOf>().toEqualTypeOf(); + }); + + it('resolves to buffer binding in code', ({ root }) => { + const buffer = root.createBuffer(d.f32).$usage('storage').$name('param'); + const mutable = buffer.as('mutable'); + + const main = tgpu.fn([])`() { let y = hello; }`.$uses({ hello: mutable }); + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "@group(0) @binding(0) var param: f32; + + fn main() { let y = param; }" + `); + }); + + it('resolves to buffer binding in tgsl functions', ({ root }) => { + const buffer = root.createBuffer(d.f32).$usage('storage').$name('param'); + const mutable = buffer.as('mutable'); + + const func = tgpu.fn([])(() => { + const x = mutable.$; + }); + + expect(tgpu.resolve([func])).toMatchInlineSnapshot(` + "@group(0) @binding(0) var param: f32; + + fn func() { + let x = param; + }" + `); + }); + + it('allows accessing fields in a struct stored in its buffer', ({ root }) => { + const Boid = d + .struct({ + pos: d.vec3f, + vel: d.vec3u, + }) + .$name('Boid'); + + const buffer = root.createBuffer(Boid).$usage('storage').$name('boid'); + const mutable = buffer.as('mutable'); + + const func = tgpu.fn([])(() => { + const pos = mutable.$.pos; + const velX = mutable.$.vel.x; + }); + + expect(tgpu.resolve([func])).toMatchInlineSnapshot(` + "struct Boid { + pos: vec3f, + vel: vec3u, + } + + @group(0) @binding(0) var boid: Boid; + + fn func() { + let pos = (&boid.pos); + let velX = boid.vel.x; + }" + `); + }); + + describe('simulate mode', () => { + it('allows accessing .$ in simulate mode', ({ root }) => { + const buffer = root.createBuffer(d.u32, 0).$usage('storage'); + const mutable = buffer.as('mutable'); + + const incrementThreeTimes = tgpu.fn([])(() => { + mutable.$++; + mutable.$++; + mutable.$++; + }); + + const result = tgpu['~unstable'].simulate(() => { + incrementThreeTimes(); + return mutable.$; + }); + expect(result.value).toBe(3); + }); + }); + + it('allows creating bufferUsages only for buffers allowing them', ({ root }) => { + root.createBuffer(d.u32, 2).$usage('storage').as('mutable'); + root.createBuffer(d.u32, 2).$usage('storage', 'uniform').as('mutable'); + root.createBuffer(d.u32, 2).$usage('vertex', 'storage').as('mutable'); + // @ts-expect-error + expect(() => root.createBuffer(d.u32, 2).as('mutable')).toThrow(); + expect(() => + root + .createBuffer(d.u32, 2) + .$usage('uniform') + // @ts-expect-error + .as('mutable'), + ).toThrow(); + }); + it('creates a mutable', ({ root }) => { const foo = root.createMutable(d.f32); @@ -77,6 +179,158 @@ describe('root.createMutable', () => { }); describe('root.createReadonly', () => { + it('represents a `number` value', ({ root }) => { + const buffer = root.createBuffer(d.f32).$usage('storage'); + const readonly = buffer.as('readonly'); + + expectTypeOf>().toEqualTypeOf(); + }); + + it('resolves to buffer binding in code', ({ root }) => { + const buffer = root.createBuffer(d.f32).$usage('storage').$name('param'); + const readonly = buffer.as('readonly'); + + const main = tgpu.fn([])`() { let y = hello; }`.$uses({ hello: readonly }); + + expect(tgpu.resolve([main])).toMatchInlineSnapshot(` + "@group(0) @binding(0) var param: f32; + + fn main() { let y = param; }" + `); + }); + + it('resolves to buffer binding in TGSL functions', ({ root }) => { + const paramBuffer = root.createBuffer(d.f32).$usage('storage'); + const paramReadonly = paramBuffer.as('readonly'); + + const func = tgpu.fn([])(() => { + const x = paramReadonly.$; + }); + + expect(tgpu.resolve([func])).toMatchInlineSnapshot(` + "@group(0) @binding(0) var paramBuffer: f32; + + fn func() { + let x = paramBuffer; + }" + `); + }); + + it('allows accessing fields in a struct stored in its buffer', ({ root }) => { + const Boid = d.struct({ + pos: d.vec3f, + vel: d.vec3u, + }); + + const boidBuffer = root.createBuffer(Boid).$usage('storage').$name('boid'); + const boidReadonly = boidBuffer.as('readonly'); + + const func = tgpu.fn([])(() => { + const pos = boidReadonly.$.pos; + const velX = boidReadonly.$.vel.x; + }); + + expect(tgpu.resolve([func])).toMatchInlineSnapshot(` + "struct Boid { + pos: vec3f, + vel: vec3u, + } + + @group(0) @binding(0) var boid: Boid; + + fn func() { + let pos = (&boid.pos); + let velX = boid.vel.x; + }" + `); + }); + + it('cannot be accessed via .$ top-level', ({ root }) => { + const buffer = root.createBuffer(d.f32).$usage('storage'); + const readonly = buffer.as('readonly'); + + expect(() => readonly.$).toThrowErrorMatchingInlineSnapshot( + `[Error: .$ is inaccessible during normal JS execution. Try \`.read()\`]`, + ); + }); + + it('cannot be accessed via .$ in a function called top-level', ({ root }) => { + const fooBuffer = root.createBuffer(d.f32).$usage('storage'); + const readonly = fooBuffer.as('readonly'); + + const foo = tgpu.fn( + [], + d.f32, + )(() => { + return readonly.$; // accessing GPU resource + }); + + expect(() => foo()).toThrowErrorMatchingInlineSnapshot(` + [Error: Execution of the following tree failed: + - fn:foo: Cannot access buffer:fooBuffer. TypeGPU functions that depends on GPU resources need to be part of a compute dispatch, draw call or simulation] + `); + }); + + it('forbids assignment to readonlys', ({ root }) => { + const myReadonly = root.createReadonly(d.vec2u); + const myFn = () => { + 'use gpu'; + // @ts-expect-error + myReadonly.$ = d.vec2u(); + }; + + expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:myFn + - fn*:myFn(): 'myReadonly.$ = d.vec2u()' is invalid, because readonly buffers cannot be mutated.] + `); + }); + + it('forbids assignment to readonly props', ({ root }) => { + const myReadonly = root.createReadonly(d.vec2u); + const myFn = () => { + 'use gpu'; + myReadonly.$.x = 1; + }; + + expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fn*:myFn + - fn*:myFn(): 'myReadonly.$.x = 1' is invalid, because readonly buffers cannot be mutated.] + `); + }); + + describe('simulate mode', () => { + it('allows accessing .$ in simulate mode', ({ root }) => { + const buffer = root.createBuffer(d.f32, 123).$usage('storage'); + const readonly = buffer.as('readonly'); + + const foo = tgpu.fn([])(() => { + return readonly.$; // accessing GPU resource + }); + + const result = tgpu['~unstable'].simulate(foo); + expect(result.value).toBe(123); + }); + }); + + it('allows creating bufferUsages only for buffers allowing them', ({ root }) => { + root.createBuffer(d.u32, 2).$usage('storage').as('readonly'); + root.createBuffer(d.u32, 2).$usage('storage', 'uniform').as('readonly'); + root.createBuffer(d.u32, 2).$usage('storage', 'vertex').as('readonly'); + // @ts-expect-error + expect(() => root.createBuffer(d.u32, 2).as('readonly')).toThrow(); + expect(() => + root + .createBuffer(d.u32, 2) + .$usage('uniform') + // @ts-expect-error + .as('readonly'), + ).toThrow(); + }); + it('creates a readonly', ({ root }) => { const foo = root.createReadonly(d.f32); diff --git a/packages/typegpu/tests/bufferUsage.test.ts b/packages/typegpu/tests/bufferUsage.test.ts deleted file mode 100644 index 9510350de4..0000000000 --- a/packages/typegpu/tests/bufferUsage.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { describe, expect, expectTypeOf } from 'vitest'; - -import { d, tgpu } from 'typegpu'; -import type { Infer } from 'typegpu/data'; -import { it } from 'typegpu-testing-utility'; - -// TODO(#2666) - remove this file - -describe('TgpuBufferMutable', () => { - it('represents a `number` value', ({ root }) => { - const buffer = root.createBuffer(d.f32).$usage('storage'); - const mutable = buffer.as('mutable'); - - expectTypeOf>().toEqualTypeOf(); - }); - - it('resolves to buffer binding in code', ({ root }) => { - const buffer = root.createBuffer(d.f32).$usage('storage').$name('param'); - const mutable = buffer.as('mutable'); - - const main = tgpu.fn([])`() { let y = hello; }`.$uses({ hello: mutable }); - - expect(tgpu.resolve([main])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var param: f32; - - fn main() { let y = param; }" - `); - }); - - it('resolves to buffer binding in tgsl functions', ({ root }) => { - const buffer = root.createBuffer(d.f32).$usage('storage').$name('param'); - const mutable = buffer.as('mutable'); - - const func = tgpu.fn([])(() => { - const x = mutable.$; - }); - - expect(tgpu.resolve([func])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var param: f32; - - fn func() { - let x = param; - }" - `); - }); - - it('allows accessing fields in a struct stored in its buffer', ({ root }) => { - const Boid = d - .struct({ - pos: d.vec3f, - vel: d.vec3u, - }) - .$name('Boid'); - - const buffer = root.createBuffer(Boid).$usage('storage').$name('boid'); - const mutable = buffer.as('mutable'); - - const func = tgpu.fn([])(() => { - const pos = mutable.$.pos; - const velX = mutable.$.vel.x; - }); - - expect(tgpu.resolve([func])).toMatchInlineSnapshot(` - "struct Boid { - pos: vec3f, - vel: vec3u, - } - - @group(0) @binding(0) var boid: Boid; - - fn func() { - let pos = (&boid.pos); - let velX = boid.vel.x; - }" - `); - }); - - describe('simulate mode', () => { - it('allows accessing .$ in simulate mode', ({ root }) => { - const buffer = root.createBuffer(d.u32, 0).$usage('storage'); - const mutable = buffer.as('mutable'); - - const incrementThreeTimes = tgpu.fn([])(() => { - mutable.$++; - mutable.$++; - mutable.$++; - }); - - const result = tgpu['~unstable'].simulate(() => { - incrementThreeTimes(); - return mutable.$; - }); - expect(result.value).toBe(3); - }); - }); - - it('allows creating bufferUsages only for buffers allowing them', ({ root }) => { - root.createBuffer(d.u32, 2).$usage('storage').as('mutable'); - root.createBuffer(d.u32, 2).$usage('storage', 'uniform').as('mutable'); - root.createBuffer(d.u32, 2).$usage('vertex', 'storage').as('mutable'); - // @ts-expect-error - expect(() => root.createBuffer(d.u32, 2).as('mutable')).toThrow(); - expect(() => - root - .createBuffer(d.u32, 2) - .$usage('uniform') - // @ts-expect-error - .as('mutable'), - ).toThrow(); - }); -}); - -describe('TgpuBufferReadonly', () => { - it('represents a `number` value', ({ root }) => { - const buffer = root.createBuffer(d.f32).$usage('storage'); - const readonly = buffer.as('readonly'); - - expectTypeOf>().toEqualTypeOf(); - }); - - it('resolves to buffer binding in code', ({ root }) => { - const buffer = root.createBuffer(d.f32).$usage('storage').$name('param'); - const readonly = buffer.as('readonly'); - - const main = tgpu.fn([])`() { let y = hello; }`.$uses({ hello: readonly }); - - expect(tgpu.resolve([main])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var param: f32; - - fn main() { let y = param; }" - `); - }); - - it('resolves to buffer binding in TGSL functions', ({ root }) => { - const paramBuffer = root.createBuffer(d.f32).$usage('storage'); - const paramReadonly = paramBuffer.as('readonly'); - - const func = tgpu.fn([])(() => { - const x = paramReadonly.$; - }); - - expect(tgpu.resolve([func])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var paramBuffer: f32; - - fn func() { - let x = paramBuffer; - }" - `); - }); - - it('allows accessing fields in a struct stored in its buffer', ({ root }) => { - const Boid = d.struct({ - pos: d.vec3f, - vel: d.vec3u, - }); - - const boidBuffer = root.createBuffer(Boid).$usage('storage').$name('boid'); - const boidReadonly = boidBuffer.as('readonly'); - - const func = tgpu.fn([])(() => { - const pos = boidReadonly.$.pos; - const velX = boidReadonly.$.vel.x; - }); - - expect(tgpu.resolve([func])).toMatchInlineSnapshot(` - "struct Boid { - pos: vec3f, - vel: vec3u, - } - - @group(0) @binding(0) var boid: Boid; - - fn func() { - let pos = (&boid.pos); - let velX = boid.vel.x; - }" - `); - }); - - it('cannot be accessed via .$ top-level', ({ root }) => { - const buffer = root.createBuffer(d.f32).$usage('storage'); - const readonly = buffer.as('readonly'); - - expect(() => readonly.$).toThrowErrorMatchingInlineSnapshot( - `[Error: .$ is inaccessible during normal JS execution. Try \`.read()\`]`, - ); - }); - - it('cannot be accessed via .$ in a function called top-level', ({ root }) => { - const fooBuffer = root.createBuffer(d.f32).$usage('storage'); - const readonly = fooBuffer.as('readonly'); - - const foo = tgpu.fn( - [], - d.f32, - )(() => { - return readonly.$; // accessing GPU resource - }); - - expect(() => foo()).toThrowErrorMatchingInlineSnapshot(` - [Error: Execution of the following tree failed: - - fn:foo: Cannot access buffer:fooBuffer. TypeGPU functions that depends on GPU resources need to be part of a compute dispatch, draw call or simulation] - `); - }); - - it('forbids assignment to readonlys', ({ root }) => { - const myReadonly = root.createReadonly(d.vec2u); - const myFn = () => { - 'use gpu'; - // @ts-expect-error - myReadonly.$ = d.vec2u(); - }; - - expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn*:myFn - - fn*:myFn(): 'myReadonly.$ = d.vec2u()' is invalid, because readonly buffers cannot be mutated.] - `); - }); - - it('forbids assignment to readonly props', ({ root }) => { - const myReadonly = root.createReadonly(d.vec2u); - const myFn = () => { - 'use gpu'; - myReadonly.$.x = 1; - }; - - expect(() => tgpu.resolve([myFn])).toThrowErrorMatchingInlineSnapshot(` - [Error: Resolution of the following tree failed: - - - - fn*:myFn - - fn*:myFn(): 'myReadonly.$.x = 1' is invalid, because readonly buffers cannot be mutated.] - `); - }); - - describe('simulate mode', () => { - it('allows accessing .$ in simulate mode', ({ root }) => { - const buffer = root.createBuffer(d.f32, 123).$usage('storage'); - const readonly = buffer.as('readonly'); - - const foo = tgpu.fn([])(() => { - return readonly.$; // accessing GPU resource - }); - - const result = tgpu['~unstable'].simulate(foo); - expect(result.value).toBe(123); - }); - }); - - it('allows creating bufferUsages only for buffers allowing them', ({ root }) => { - root.createBuffer(d.u32, 2).$usage('storage').as('readonly'); - root.createBuffer(d.u32, 2).$usage('storage', 'uniform').as('readonly'); - root.createBuffer(d.u32, 2).$usage('storage', 'vertex').as('readonly'); - // @ts-expect-error - expect(() => root.createBuffer(d.u32, 2).as('readonly')).toThrow(); - expect(() => - root - .createBuffer(d.u32, 2) - .$usage('uniform') - // @ts-expect-error - .as('readonly'), - ).toThrow(); - }); -});