From a97e1dc431c061c81b49128c97914f83e24dea2c Mon Sep 17 00:00:00 2001 From: reczkok Date: Fri, 10 Jan 2025 10:11:36 +0100 Subject: [PATCH 01/12] lil work --- .../typegpu/src/core/resolve/resolveData.ts | 19 +++++++++++++++++++ .../vertexLayout/connectAttributesToShader.ts | 6 +++--- packages/typegpu/src/data/dataTypes.ts | 4 +++- packages/typegpu/src/data/unstruct.ts | 11 +++++++++++ packages/typegpu/src/data/vertexFormatData.ts | 2 +- 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/typegpu/src/core/resolve/resolveData.ts b/packages/typegpu/src/core/resolve/resolveData.ts index 1f8bc1b2b..8e0078d01 100644 --- a/packages/typegpu/src/core/resolve/resolveData.ts +++ b/packages/typegpu/src/core/resolve/resolveData.ts @@ -1,3 +1,4 @@ +import { type Unstruct, formatToWGSLType } from '../../data'; import { getAttributesString } from '../../data/attributes'; import type { AnyWgslData, @@ -27,6 +28,7 @@ import type { } from '../../data/wgslTypes'; import { assertExhaustive } from '../../shared/utilityTypes'; import type { ResolutionCtx } from '../../types'; +import { isAttribute } from '../vertexLayout/connectAttributesToShader'; /** * Schemas for which their `type` property directly @@ -101,6 +103,23 @@ ${Object.entries(struct.propTypes) return id; } +function resolveUnstruct(ctx: ResolutionCtx, unstruct: Unstruct) { + const id = ctx.names.makeUnique(unstruct.label); + + ctx.addDeclaration(` +struct ${id} { +${Object.entries(unstruct.propTypes) + .map((prop) => + isAttribute(prop[1]) + ? resolveStructProperty(ctx, [prop[0], formatToWGSLType[prop[1].format]]) + : resolveStructProperty(ctx, prop), + ) + .join('')}\ +}\n`); + + return id; +} + function resolveArray(ctx: ResolutionCtx, array: WgslArray) { const element = ctx.resolve(array.elementType as AnyWgslData); diff --git a/packages/typegpu/src/core/vertexLayout/connectAttributesToShader.ts b/packages/typegpu/src/core/vertexLayout/connectAttributesToShader.ts index f56fdecbd..609cddedc 100644 --- a/packages/typegpu/src/core/vertexLayout/connectAttributesToShader.ts +++ b/packages/typegpu/src/core/vertexLayout/connectAttributesToShader.ts @@ -16,9 +16,9 @@ export interface ConnectAttributesToShaderResult { bufferDefinitions: GPUVertexBufferLayout[]; } -function isAttribute( - value: unknown | T, -): value is T { +export function isAttribute< + T extends TgpuVertexAttrib & INTERNAL_TgpuVertexAttrib, +>(value: unknown | T): value is T { return typeof (value as T)?.format === 'string'; } diff --git a/packages/typegpu/src/data/dataTypes.ts b/packages/typegpu/src/data/dataTypes.ts index 03803fa14..a42e59c75 100644 --- a/packages/typegpu/src/data/dataTypes.ts +++ b/packages/typegpu/src/data/dataTypes.ts @@ -1,3 +1,4 @@ +import type { TgpuNamable } from '../namable'; import type { Infer, InferRecord } from '../shared/repr'; import { vertexFormats } from '../shared/vertexFormat'; import type { PackedData } from './vertexFormatData'; @@ -33,7 +34,8 @@ export interface Unstruct< string, wgsl.BaseWgslData >, -> { +> extends TgpuNamable { + readonly label?: string | undefined; readonly type: 'unstruct'; readonly propTypes: TProps; readonly '~repr': InferRecord; diff --git a/packages/typegpu/src/data/unstruct.ts b/packages/typegpu/src/data/unstruct.ts index 13020b20c..226ea0b7b 100644 --- a/packages/typegpu/src/data/unstruct.ts +++ b/packages/typegpu/src/data/unstruct.ts @@ -37,9 +37,20 @@ export const unstruct = >( class UnstructImpl> implements Unstruct { + private _label: string | undefined; + public readonly type = 'unstruct'; /** Type-token, not available at runtime */ public readonly '~repr'!: InferRecord; constructor(public readonly propTypes: TProps) {} + + get label() { + return this._label; + } + + $name(label: string) { + this._label = label; + return this; + } } diff --git a/packages/typegpu/src/data/vertexFormatData.ts b/packages/typegpu/src/data/vertexFormatData.ts index 9a3da1be4..682bec991 100644 --- a/packages/typegpu/src/data/vertexFormatData.ts +++ b/packages/typegpu/src/data/vertexFormatData.ts @@ -30,7 +30,7 @@ class TgpuVertexFormatDataImpl constructor(public readonly type: T) {} } -const formatToWGSLType = { +export const formatToWGSLType = { uint8: u32, uint8x2: vec2u, uint8x4: vec4u, From 4f9c387833fd107ee7aefbad64415dfa1fbc565f Mon Sep 17 00:00:00 2001 From: reczkok Date: Wed, 15 Jan 2025 10:58:35 +0100 Subject: [PATCH 02/12] temp test --- packages/typegpu/tests/resolve.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/typegpu/tests/resolve.test.ts b/packages/typegpu/tests/resolve.test.ts index e77587a20..aaa1c1f4d 100644 --- a/packages/typegpu/tests/resolve.test.ts +++ b/packages/typegpu/tests/resolve.test.ts @@ -174,4 +174,18 @@ describe('tgpu resolve', () => { `), ); }); + + it('should resolve an unstruct to its corresponding struct', () => { + const vertexInfo = d.unstruct({ + color: d.snorm8x4, + colorHDR: d.unorm10_10_10_2, + position2d: d.float16x2, + }); + + const resolved = tgpu.resolve({ + template: 'fn foo() { var v: vertexInfo; }', + externals: { vertexInfo }, + names: 'strict', + }); + }); }); From 98892cc11d139d1cd0264b55cc69b09b2a98892b Mon Sep 17 00:00:00 2001 From: reczkok Date: Thu, 23 Jan 2025 16:29:31 +0100 Subject: [PATCH 03/12] fix loose data resolution --- .../typegpu/src/core/resolve/resolveData.ts | 45 ++++++++++++++++--- packages/typegpu/src/resolutionCtx.ts | 3 +- packages/typegpu/src/types.ts | 3 +- packages/typegpu/tests/resolve.test.ts | 14 ++++++ 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/packages/typegpu/src/core/resolve/resolveData.ts b/packages/typegpu/src/core/resolve/resolveData.ts index 75106a6ee..a0d370bb9 100644 --- a/packages/typegpu/src/core/resolve/resolveData.ts +++ b/packages/typegpu/src/core/resolve/resolveData.ts @@ -1,4 +1,10 @@ -import { type Unstruct, formatToWGSLType } from '../../data'; +import { + type AnyData, + type Disarray, + type Unstruct, + formatToWGSLType, + isLooseData, +} from '../../data'; import { getAttributesString } from '../../data/attributes'; import type { AnyWgslData, @@ -171,6 +177,18 @@ function resolveArray(ctx: ResolutionCtx, array: WgslArray) { : `array<${element}, ${array.elementCount}>`; } +function resolveDisarray(ctx: ResolutionCtx, disarray: Disarray) { + const element = ctx.resolve( + isAttribute(disarray.elementType) + ? formatToWGSLType[disarray.elementType.format] + : (disarray.elementType as AnyWgslData), + ); + + return disarray.elementCount === 0 + ? `array<${element}>` + : `array<${element}, ${disarray.elementCount}>`; +} + /** * Resolves a WGSL data-type schema to a string. * @param ctx - The resolution context. @@ -178,12 +196,25 @@ function resolveArray(ctx: ResolutionCtx, array: WgslArray) { * * @returns The resolved data-type string. */ -export function resolveData( - ctx: ResolutionCtx, - data: AnyWgslData | Unstruct, -): string { - if (data.type === 'unstruct') { - return resolveUnstruct(ctx, data); +export function resolveData(ctx: ResolutionCtx, data: AnyData): string { + if (isLooseData(data)) { + if (data.type === 'unstruct') { + return resolveUnstruct(ctx, data); + } + + if (data.type === 'disarray') { + return resolveDisarray(ctx, data); + } + + if (data.type === 'loose-decorated') { + return ctx.resolve( + isAttribute(data.inner) + ? formatToWGSLType[data.inner.format] + : data.inner, + ); + } + + return ctx.resolve(formatToWGSLType[data.type]); } if (isIdentityType(data)) { diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index a96440382..3ebb2da1f 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -8,6 +8,7 @@ import { isDerived, isSlot, } from './core/slot/slotTypes'; +import { isLooseData } from './data'; import { type AnyWgslData, type BaseWgslData, @@ -465,7 +466,7 @@ class ResolutionCtxImpl implements ResolutionCtx { // If we got here, no item with the given slot-to-value combo exists in cache yet let result: string; - if (isWgslData(item)) { + if (isWgslData(item) || isLooseData(item)) { result = resolveData(this, item); } else if (isDerived(item) || isSlot(item)) { result = this.resolve(item); diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index eb28e503a..e154dc09b 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -19,6 +19,7 @@ import { import type { TgpuExternalTexture } from './core/texture/externalTexture'; import type { TgpuAnyTextureView, TgpuTexture } from './core/texture/texture'; import type { TgpuVar } from './core/variable/tgpuVariable'; +import type { AnyData } from './data'; import { type AnyMatInstance, type AnyVecInstance, @@ -52,7 +53,7 @@ export type ResolvableObject = | TgpuVar | AnyVecInstance | AnyMatInstance - | AnyWgslData + | AnyData // biome-ignore lint/suspicious/noExplicitAny: | TgpuFn; diff --git a/packages/typegpu/tests/resolve.test.ts b/packages/typegpu/tests/resolve.test.ts index 526aa9c64..04f920fa7 100644 --- a/packages/typegpu/tests/resolve.test.ts +++ b/packages/typegpu/tests/resolve.test.ts @@ -187,5 +187,19 @@ describe('tgpu resolve', () => { externals: { vertexInfo }, names: 'strict', }); + + expect(parse(resolved)).toEqual( + parse(` + struct vertexInfo { + color: vec4f, + colorHDR: vec4f, + position2d: vec2f, + } + + fn foo() { + var v: vertexInfo; + } + `), + ); }); }); From 85845f6aeccee5dbf208e9edc23e8f554a6e02de Mon Sep 17 00:00:00 2001 From: reczkok Date: Thu, 23 Jan 2025 16:44:06 +0100 Subject: [PATCH 04/12] fix imports --- packages/typegpu/src/core/resolve/resolveData.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/typegpu/src/core/resolve/resolveData.ts b/packages/typegpu/src/core/resolve/resolveData.ts index a0d370bb9..3ed24b432 100644 --- a/packages/typegpu/src/core/resolve/resolveData.ts +++ b/packages/typegpu/src/core/resolve/resolveData.ts @@ -1,11 +1,11 @@ +import { getAttributesString } from '../../data/attributes'; import { type AnyData, type Disarray, type Unstruct, - formatToWGSLType, isLooseData, -} from '../../data'; -import { getAttributesString } from '../../data/attributes'; +} from '../../data/dataTypes'; +import { formatToWGSLType } from '../../data/vertexFormatData'; import type { AnyWgslData, BaseWgslData, From 70cf6465c9eb5d4d7a8c221b34bc4855dde3872b Mon Sep 17 00:00:00 2001 From: reczkok Date: Thu, 23 Jan 2025 16:48:58 +0100 Subject: [PATCH 05/12] fix game of life import --- .../src/content/examples/simulation/game-of-life/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/typegpu-docs/src/content/examples/simulation/game-of-life/index.ts b/apps/typegpu-docs/src/content/examples/simulation/game-of-life/index.ts index c10989463..223248374 100644 --- a/apps/typegpu-docs/src/content/examples/simulation/game-of-life/index.ts +++ b/apps/typegpu-docs/src/content/examples/simulation/game-of-life/index.ts @@ -1,5 +1,5 @@ +import tgpu from 'typegpu'; import * as d from 'typegpu/data'; -import tgpu from 'typegpu/experimental'; const root = await tgpu.init(); const device = root.device; @@ -87,12 +87,12 @@ const squareBuffer = root .createBuffer(d.arrayOf(d.u32, 8), [0, 0, 1, 0, 0, 1, 1, 1]) .$usage('vertex'); -const squareVertexLayout = tgpu.vertexLayout( +const squareVertexLayout = tgpu['~unstable'].vertexLayout( (n: number) => d.arrayOf(d.location(1, d.vec2u), n), 'vertex', ); -const cellsVertexLayout = tgpu.vertexLayout( +const cellsVertexLayout = tgpu['~unstable'].vertexLayout( (n: number) => d.arrayOf(d.location(0, d.u32), n), 'instance', ); From 1d61eb3545aa88c0cbaf2295cc41190f39d0652e Mon Sep 17 00:00:00 2001 From: reczkok Date: Thu, 23 Jan 2025 16:59:25 +0100 Subject: [PATCH 06/12] use root.unwrap on a vertex layout in boids --- .../examples/simulation/boids/index.ts | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts b/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts index c59e003dc..cf251f4ca 100644 --- a/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts +++ b/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts @@ -199,13 +199,10 @@ const paramsBuffer = root .$usage('storage'); const triangleVertexBuffer = root - .createBuffer(d.arrayOf(d.f32, 6), [ - 0.0, - triangleSize, - -triangleSize / 2, - -triangleSize / 2, - triangleSize / 2, - -triangleSize / 2, + .createBuffer(d.arrayOf(d.vec2f, 3), [ + d.vec2f(0.0, triangleSize), + d.vec2f(-triangleSize / 2, -triangleSize / 2), + d.vec2f(triangleSize / 2, -triangleSize / 2), ]) .$usage('vertex'); @@ -276,24 +273,19 @@ const computeModule = root.device.createShaderModule({ }), }); +const vertexLayout = tgpu['~unstable'].vertexLayout((n) => + d.arrayOf(d.location(0, d.vec2f), n), +); + +console.log(root.unwrap(vertexLayout)); + const pipeline = root.device.createRenderPipeline({ layout: root.device.createPipelineLayout({ bindGroupLayouts: [root.unwrap(renderBindGroupLayout)], }), vertex: { module: renderModule, - buffers: [ - { - arrayStride: 2 * 4, - attributes: [ - { - shaderLocation: 0, - offset: 0, - format: 'float32x2' as const, - }, - ], - }, - ], + buffers: [root.unwrap(vertexLayout)], }, fragment: { module: renderModule, From 0ad9f26288ccf2539f155ada407029ba13cf5122 Mon Sep 17 00:00:00 2001 From: reczkok Date: Fri, 24 Jan 2025 10:22:51 +0100 Subject: [PATCH 07/12] more loose data resolution tests --- packages/typegpu/tests/resolve.test.ts | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/packages/typegpu/tests/resolve.test.ts b/packages/typegpu/tests/resolve.test.ts index e9b5472d7..f7ab2af94 100644 --- a/packages/typegpu/tests/resolve.test.ts +++ b/packages/typegpu/tests/resolve.test.ts @@ -201,6 +201,83 @@ describe('tgpu resolve', () => { ); }); + it('should resolve an unstruct with a disarray to its corresponding struct', () => { + const vertexInfo = d.unstruct({ + color: d.snorm8x4, + colorHDR: d.unorm10_10_10_2, + position2d: d.float16x2, + extra: d.disarrayOf(d.snorm8x4, 16), + }); + + const resolved = tgpu.resolve({ + template: 'fn foo() { var v: vertexInfo; }', + externals: { vertexInfo }, + names: 'strict', + }); + + expect(parse(resolved)).toEqual( + parse(` + struct vertexInfo { + color: vec4f, + colorHDR: vec4f, + position2d: vec2f, + extra: array, + } + fn foo() { var v: vertexInfo; } + `), + ); + }); + + it('should resolve an unstruct with a complex nested structure', () => { + const vertexInfo = d.unstruct({ + color: d.snorm8x4, + colorHDR: d.unorm10_10_10_2, + position2d: d.float16x2, + extra: d + .unstruct({ + a: d.snorm8, + b: d.snorm8x4, + c: d.float16x2, + }) + .$name('extra'), + more: d.disarrayOf( + d.unstruct({ a: d.snorm8, b: d.snorm8x4 }).$name('more'), + 16, + ), + }); + + const resolved = tgpu.resolve({ + template: 'fn foo() { var v: vertexInfo; }', + externals: { vertexInfo }, + names: 'strict', + }); + + expect(parse(resolved)).toEqual( + parse(` + struct extra { + a: f32, + b: vec4f, + c: vec2f, + } + + struct more { + a: f32, + b: vec4f, + } + + struct vertexInfo { + color: vec4f, + colorHDR: vec4f, + position2d: vec2f, + extra: extra, + more: array, + } + + fn foo() { var v: vertexInfo; } + `), + ); + }); + it('should resolve object externals and replace their usages in template', () => { const getColor = tgpu['~unstable'] .fn([], d.vec3f) From eeea19606b4d62fb7c4061486fd4d3b9e21e2af3 Mon Sep 17 00:00:00 2001 From: reczkok Date: Fri, 24 Jan 2025 13:07:14 +0100 Subject: [PATCH 08/12] add tests for unwrapping vertex layouts --- .../src/core/vertexLayout/vertexLayout.ts | 6 +- packages/typegpu/tests/root.test.ts | 59 +++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/packages/typegpu/src/core/vertexLayout/vertexLayout.ts b/packages/typegpu/src/core/vertexLayout/vertexLayout.ts index 6afc33bf3..bf77de6ce 100644 --- a/packages/typegpu/src/core/vertexLayout/vertexLayout.ts +++ b/packages/typegpu/src/core/vertexLayout/vertexLayout.ts @@ -211,9 +211,9 @@ class TgpuVertexLayoutImpl } // check if all attributes have custom locations - const allAttributesHaveCustomLocations = - Object.values(this._customLocationMap).length === - Object.keys(this.attrib).length; + const allAttributesHaveCustomLocations = Object.keys(this.attrib).every( + (key) => this._customLocationMap[key] !== undefined, + ); if (!allAttributesHaveCustomLocations) { throw new Error( diff --git a/packages/typegpu/tests/root.test.ts b/packages/typegpu/tests/root.test.ts index e4b98cf8e..6e32003c0 100644 --- a/packages/typegpu/tests/root.test.ts +++ b/packages/typegpu/tests/root.test.ts @@ -1,4 +1,5 @@ import { describe, expect, vi } from 'vitest'; +import tgpu from '../src'; import * as d from '../src/data'; import { it } from './utils/extendedIt'; @@ -100,6 +101,64 @@ describe('TgpuRoot', () => { const buffer = root.createBuffer(d.u32, rawBuffer); expect(root.unwrap(buffer)).toBe(rawBuffer); }); + + it('should return the correct GPUVertexBufferLayout for a simple vertex layout', ({ + root, + }) => { + const vertexLayout = tgpu['~unstable'].vertexLayout( + (n) => d.arrayOf(d.location(0, d.vec2u), n), + 'vertex', + ); + + expect(root.unwrap(vertexLayout)).toEqual({ + arrayStride: 8, + stepMode: 'vertex', + attributes: [ + { + format: 'uint32x2', + offset: 0, + shaderLocation: 0, + }, + ], + }); + }); + + it('should return the correct GPUVertexBufferLayout for a complex vertex layout', ({ + root, + }) => { + const VertexData = d.unstruct({ + position: d.location(0, d.float32x3), + color: d.location(1, d.unorm10_10_10_2), + something: d.location(2, d.u32), + }); + + const vertexLayout = tgpu['~unstable'].vertexLayout( + (n) => d.disarrayOf(VertexData, n), + 'instance', + ); + + expect(root.unwrap(vertexLayout)).toEqual({ + arrayStride: 20, + stepMode: 'instance', + attributes: [ + { + format: 'float32x3', + offset: 0, + shaderLocation: 0, + }, + { + format: 'unorm10-10-10-2', + offset: 12, + shaderLocation: 1, + }, + { + format: 'uint32', + offset: 16, + shaderLocation: 2, + }, + ], + }); + }); }); // TODO: Adapt the tests to the new API From 490ac7244af43a88fca55ccd1a0c6b10e706c91a Mon Sep 17 00:00:00 2001 From: reczkok Date: Mon, 27 Jan 2025 12:20:31 +0100 Subject: [PATCH 09/12] add docs --- apps/typegpu-docs/astro.config.mjs | 5 + .../src/content/docs/fundamentals/roots.mdx | 8 +- .../docs/fundamentals/vertex-layouts.mdx | 187 ++++++++++++++++++ 3 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx diff --git a/apps/typegpu-docs/astro.config.mjs b/apps/typegpu-docs/astro.config.mjs index 209bd9df6..45f9eb75b 100644 --- a/apps/typegpu-docs/astro.config.mjs +++ b/apps/typegpu-docs/astro.config.mjs @@ -128,6 +128,11 @@ export default defineConfig({ slug: 'fundamentals/resolve', badge: { text: '0.3' }, }, + { + label: 'Vertex Layouts', + slug: 'fundamentals/vertex-layouts', + badge: { text: '0.3.3' }, + }, DEV && { label: 'Slots', slug: 'fundamentals/slots', diff --git a/apps/typegpu-docs/src/content/docs/fundamentals/roots.mdx b/apps/typegpu-docs/src/content/docs/fundamentals/roots.mdx index b14da3654..a8ac74928 100644 --- a/apps/typegpu-docs/src/content/docs/fundamentals/roots.mdx +++ b/apps/typegpu-docs/src/content/docs/fundamentals/roots.mdx @@ -55,9 +55,13 @@ untyped value of a typed resource, use the `root.unwrap` function. | `root.unwrap(resource: TgpuBuffer)` | Returns a `GPUBuffer`. | | `root.unwrap(resource: TgpuBindGroupLayout)` | Returns a `GPUBindGroupLayout`. | | `root.unwrap(resource: TgpuBindGroup)` | Returns a `GPUBindGroup`. | +| `root.unwrap(resource: TgpuVertexLayout)` | Returns a `GPUVertexBufferLayout`. | {/* | `root.unwrap(resource: TgpuTexture)` | Returns a `GPUTexture`. | */} {/* | `root.unwrap(resource: TgpuReadonlyTexture \| TgpuWriteonlyTexture \| TgpuMutableTexture \| TgpuSampledTexture)` | Returns a `GPUTextureView`. | */} +:::note +To unwrap a `TgpuVertexLayout` make sure that all its attributes are marked with the approperiate location. +::: ## Destroying resources @@ -86,7 +90,7 @@ import React from 'react'; function SceneView() { const ref = useWebGPU(({ context, device, presentationFormat }) => { const root = tgpu.initFromDevice({ device }); - + // create all resources... }); @@ -135,4 +139,4 @@ class GameObject { // create all resources... } } -``` \ No newline at end of file +``` diff --git a/apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx b/apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx new file mode 100644 index 000000000..8102e9447 --- /dev/null +++ b/apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx @@ -0,0 +1,187 @@ +--- +title: Vertex Layouts +description: A guide on how to create and use typed vertex layouts +--- + +Typed vertex layouts are a way to describe the structure of vertex data in a typed manner. They are used to create a single source of truth for vertex data, which can be used in combination with [`tgpu.resolve`](/TypeGPU/fundamentals/resolve) and [`root.unwrap`](/TypeGPU/fundamentals/roots) to easily create and manage vertex buffers. + +## Creating a vertex layout + +To create a vertex layout, use the `tgpu.vertexLayout` function. It takes a function that takes a number and returns an array containing any data type, and a string that describes the type of the vertex layout. It can be either `vertex` or `instance` (default is `vertex`). + +```ts +import tgpu from 'typegpu'; +import * as d from 'typegpu/data'; + +const ParticleGeometry = d.struct({ + tilt: d.f32, + angle: d.f32, + color: d.vec4f, +}); + +const geometryLayout = tgpu + .vertexLayout((n: number) => d.arrayOf(ParticleGeometry, n), 'instance'); +``` + +## Utilizing loose schemas with vertex layouts + +The above example is great if the vertex buffer will also be used as a storage or uniform buffer. However, if you're only interested in the vertex data, you can use a loose schema instead. +It is not restricted by alignment rules and can utilize many useful [vertex formats](https://www.w3.org/TR/webgpu/#vertex-formats). To create loose schemas, use `d.unstruct` instead of `d.struct` and `d.disarrayOf` instead of `d.arrayOf`. +Inside of loose schemas you can use vertex formats as well as the usual data types. + +```ts +const LooseParticleGeometry = d.unstruct({ + tilt: d.f32, + angle: d.f32, + color: d.unorm8x4, // 4x8-bit unsigned normalized +}); +``` + +The size of `LooseParticleGeometry` will be 12 bytes, compared to 32 bytes of `ParticleGeometry`. This can be useful when you're working with large amounts of vertex data and want to save memory. + +:::tip[Aligning loose schemas] +Sometimes you might want to align the data in a loose schema due to external requirements or performance reasons. +You can do this by using the `d.align` function, just like in normal schemas. Even though loose schemas don't have alignment requirements, they will still respect any alignment you specify. + +```ts +const LooseParticleGeometry = d.unstruct({ + tilt: d.f32, + angle: d.f32, + color: d.align(16, d.unorm8x4), + // 4x8-bit unsigned normalized aligned to 16 bytes +}); +``` + +This will align the `color` field to 16 bytes, making the size of `LooseParticleGeometry` 20 bytes. +::: + +## Using vertex layouts + +You can utilize [`root.unwrap`](/TypeGPU/fundamentals/roots) to get the raw `GPUVertexBufferLayout` from a typed vertex layout. It will automatically calculate the stride and attributes for you, according to the vertex layout you provided. + +:::caution +Make sure that all attributes in the vertex layout are marked with the appropriate location. You can use the `d.location` function to specify the location of each attribute. +If you don't do this, the unwrapping will fail at runtime. +::: + +```ts +const ParticleGeometry = d.struct({ + tilt: d.location(0, d.f32), + angle: d.location(1, d.f32), + color: d.location(2, d.vec4f), +}); + +const geometryLayout = tgpu + .vertexLayout((n: number) => d.arrayOf(ParticleGeometry, n), 'instance'); + +const geometry = root.unwrap(geometryLayout); + +console.log(geometry); +//{ +// "arrayStride": 32, +// "stepMode": "instance", +// "attributes": [ +// { +// "format": "float32", +// "offset": 0, +// "shaderLocation": 0 +// }, +// { +// "format": "float32", +// "offset": 4, +// "shaderLocation": 1 +// }, +// { +// "format": "float32x4", +// "offset": 16, +// "shaderLocation": 2 +// } +// ] +//} +``` + +This will return a `GPUVertexBufferLayout` that can be used when creating a render pipeline. + +```diff lang=ts +const renderPipeline = device.createRenderPipeline({ + layout: device.createPipelineLayout({ + bindGroupLayouts: [root.unwrap(bindGroupLayout)], + }), + primitive: { + topology: 'triangle-strip', + }, + vertex: { + module: renderShader, +- buffers: [ +- { +- arrayStride: 32, +- stepMode: 'instance', +- attributes: [ +- { +- format: 'float32', +- offset: 0, +- shaderLocation: 0, +- }, +- { +- format: 'float32', +- offset: 4, +- shaderLocation: 1, +- }, +- { +- format: 'float32x4', +- offset: 16, +- shaderLocation: 2, +- }, +- ], +- }, +- ], ++ buffers: [root.unwrap(geometryLayout)], + }, + fragment: { + ... + }, +}); +``` + +If you are using a loose schema, you can now resolve it to get it's WGSL representation. + +```ts +const LooseParticleGeometry = d.unstruct({ + tilt: d.location(0, d.f32), + angle: d.location(1, d.f32), + color: d.location(2, d.unorm8x4), +}); + +const sampleShader = ` + @vertex + fn main(particleGeometry: LooseParticleGeometry) -> @builtin(position) pos: vec4f { + return vec4f( + particleGeometry.tilt, + particleGeometry.angle, + particleGeometry.color.rgb, + 1.0 + ); + } +`; + +const wgslDefinition = tgpu.resolve({ + template: sampleShader, + externals: { LooseParticleGeometry } +}); +console.log(wgslDefinition); +// struct LooseParticleGeometry_0 { +// @location(0) tilt: f32, +// @location(1) angle: f32, +// @location(2) color: vec4f, +// } +// +// @vertex +// fn main(particleGeometry: LooseParticleGeometry_0) -> @builtin(position) pos: vec4f { +// return vec4f( +// particleGeometry.tilt, +// particleGeometry.angle, +// particleGeometry.color.rgb, +// 1.0 +// ); +// } +``` From 55814713f93f6214a13588a5a7aa2058e563b784 Mon Sep 17 00:00:00 2001 From: reczkok Date: Mon, 27 Jan 2025 12:22:08 +0100 Subject: [PATCH 10/12] hide for now --- apps/typegpu-docs/astro.config.mjs | 2 +- .../src/content/docs/fundamentals/vertex-layouts.mdx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/typegpu-docs/astro.config.mjs b/apps/typegpu-docs/astro.config.mjs index 45f9eb75b..21e5b6d72 100644 --- a/apps/typegpu-docs/astro.config.mjs +++ b/apps/typegpu-docs/astro.config.mjs @@ -128,7 +128,7 @@ export default defineConfig({ slug: 'fundamentals/resolve', badge: { text: '0.3' }, }, - { + DEV && { label: 'Vertex Layouts', slug: 'fundamentals/vertex-layouts', badge: { text: '0.3.3' }, diff --git a/apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx b/apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx index 8102e9447..b59931b32 100644 --- a/apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx +++ b/apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx @@ -1,6 +1,7 @@ --- title: Vertex Layouts description: A guide on how to create and use typed vertex layouts +draft: true --- Typed vertex layouts are a way to describe the structure of vertex data in a typed manner. They are used to create a single source of truth for vertex data, which can be used in combination with [`tgpu.resolve`](/TypeGPU/fundamentals/resolve) and [`root.unwrap`](/TypeGPU/fundamentals/roots) to easily create and manage vertex buffers. From 53bcec83232f72e982a035701051fc782a48d37c Mon Sep 17 00:00:00 2001 From: reczkok <66403540+reczkok@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:49:45 +0100 Subject: [PATCH 11/12] Apply suggestions from code review Co-authored-by: Marcin Hawryluk <70582973+mhawryluk@users.noreply.github.com> --- apps/typegpu-docs/src/content/docs/fundamentals/roots.mdx | 2 +- .../src/content/docs/fundamentals/vertex-layouts.mdx | 3 ++- .../src/content/examples/simulation/boids/index.ts | 1 - packages/typegpu/src/core/resolve/resolveData.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/typegpu-docs/src/content/docs/fundamentals/roots.mdx b/apps/typegpu-docs/src/content/docs/fundamentals/roots.mdx index a8ac74928..31b7d74b2 100644 --- a/apps/typegpu-docs/src/content/docs/fundamentals/roots.mdx +++ b/apps/typegpu-docs/src/content/docs/fundamentals/roots.mdx @@ -60,7 +60,7 @@ untyped value of a typed resource, use the `root.unwrap` function. {/* | `root.unwrap(resource: TgpuReadonlyTexture \| TgpuWriteonlyTexture \| TgpuMutableTexture \| TgpuSampledTexture)` | Returns a `GPUTextureView`. | */} :::note -To unwrap a `TgpuVertexLayout` make sure that all its attributes are marked with the approperiate location. +To unwrap a `TgpuVertexLayout` make sure that all its attributes are marked with the appropriate location. ::: ## Destroying resources diff --git a/apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx b/apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx index b59931b32..799d58511 100644 --- a/apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx +++ b/apps/typegpu-docs/src/content/docs/fundamentals/vertex-layouts.mdx @@ -144,7 +144,7 @@ const renderPipeline = device.createRenderPipeline({ }); ``` -If you are using a loose schema, you can now resolve it to get it's WGSL representation. +If you are using a loose schema, you can now resolve it to get its WGSL representation. ```ts const LooseParticleGeometry = d.unstruct({ @@ -169,6 +169,7 @@ const wgslDefinition = tgpu.resolve({ template: sampleShader, externals: { LooseParticleGeometry } }); + console.log(wgslDefinition); // struct LooseParticleGeometry_0 { // @location(0) tilt: f32, diff --git a/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts b/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts index cf251f4ca..0321072e1 100644 --- a/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts +++ b/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts @@ -277,7 +277,6 @@ const vertexLayout = tgpu['~unstable'].vertexLayout((n) => d.arrayOf(d.location(0, d.vec2f), n), ); -console.log(root.unwrap(vertexLayout)); const pipeline = root.device.createRenderPipeline({ layout: root.device.createPipelineLayout({ diff --git a/packages/typegpu/src/core/resolve/resolveData.ts b/packages/typegpu/src/core/resolve/resolveData.ts index 3ed24b432..8e5d7bc2b 100644 --- a/packages/typegpu/src/core/resolve/resolveData.ts +++ b/packages/typegpu/src/core/resolve/resolveData.ts @@ -150,7 +150,7 @@ ${Object.entries(unstruct.propTypes) ? resolveStructProperty(ctx, [prop[0], formatToWGSLType[prop[1].format]]) : resolveStructProperty(ctx, prop), ) - .join('')}\ + .join('')} }\n`); return id; From ddbab233af8f78f3a779a70ff4d80b1e031fd6c8 Mon Sep 17 00:00:00 2001 From: reczkok Date: Wed, 29 Jan 2025 13:10:44 +0100 Subject: [PATCH 12/12] more review fixes --- .../examples/simulation/boids/index.ts | 1 - .../examples/simulation/game-of-life/index.ts | 12 +++------ .../src/core/vertexLayout/vertexLayout.ts | 27 +++++++++++-------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts b/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts index 0321072e1..1efda59f6 100644 --- a/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts +++ b/apps/typegpu-docs/src/content/examples/simulation/boids/index.ts @@ -277,7 +277,6 @@ const vertexLayout = tgpu['~unstable'].vertexLayout((n) => d.arrayOf(d.location(0, d.vec2f), n), ); - const pipeline = root.device.createRenderPipeline({ layout: root.device.createPipelineLayout({ bindGroupLayouts: [root.unwrap(renderBindGroupLayout)], diff --git a/apps/typegpu-docs/src/content/examples/simulation/game-of-life/index.ts b/apps/typegpu-docs/src/content/examples/simulation/game-of-life/index.ts index 223248374..fad943f91 100644 --- a/apps/typegpu-docs/src/content/examples/simulation/game-of-life/index.ts +++ b/apps/typegpu-docs/src/content/examples/simulation/game-of-life/index.ts @@ -23,7 +23,7 @@ let timestep = 4; let swap = false; let paused = false; -const layoutCompute = { +const bindGroupLayoutCompute = tgpu.bindGroupLayout({ size: { storage: d.vec2u, access: 'readonly', @@ -36,16 +36,12 @@ const layoutCompute = { storage: (arrayLength: number) => d.arrayOf(d.u32, arrayLength), access: 'mutable', }, -} as const; - -const groupLayout = { +}); +const bindGroupLayoutRender = tgpu.bindGroupLayout({ size: { uniform: d.vec2u, }, -} as const; - -const bindGroupLayoutCompute = tgpu.bindGroupLayout(layoutCompute); -const bindGroupLayoutRender = tgpu.bindGroupLayout(groupLayout); +}); const computeShader = device.createShaderModule({ code: tgpu.resolve({ diff --git a/packages/typegpu/src/core/vertexLayout/vertexLayout.ts b/packages/typegpu/src/core/vertexLayout/vertexLayout.ts index bf77de6ce..630a655a5 100644 --- a/packages/typegpu/src/core/vertexLayout/vertexLayout.ts +++ b/packages/typegpu/src/core/vertexLayout/vertexLayout.ts @@ -197,6 +197,15 @@ class TgpuVertexLayoutImpl // If defaultAttribEntry is in the custom location map, // it means that the vertex layout is based on a single attribute if (this._customLocationMap[defaultAttribEntry] !== undefined) { + if ( + typeof this.attrib.format !== 'string' || + typeof this.attrib.offset !== 'number' + ) { + throw new Error( + 'Single attribute vertex layouts must have a format and offset.', + ); + } + return { arrayStride: this.stride, stepMode: this.stepMode, @@ -206,7 +215,7 @@ class TgpuVertexLayoutImpl offset: this.attrib.offset, shaderLocation: this._customLocationMap[defaultAttribEntry], }, - ] as unknown as GPUVertexAttribute[], + ], }; } @@ -225,16 +234,12 @@ class TgpuVertexLayoutImpl arrayStride: this.stride, stepMode: this.stepMode, attributes: [ - ...Object.entries(this.attrib).map(([key, attrib]) => { - return { - //@ts-ignore - format: attrib.format, - //@ts-ignore - offset: attrib.offset, - shaderLocation: this._customLocationMap[key], - }; - }), - ] as unknown as GPUVertexAttribute[], + ...Object.entries(this.attrib).map(([key, attrib]) => ({ + format: attrib.format, + offset: attrib.offset, + shaderLocation: this._customLocationMap[key], + })), + ] as GPUVertexAttribute[], }; }