diff --git a/apps/typegpu-docs/src/content/docs/apis/functions/index.mdx b/apps/typegpu-docs/src/content/docs/apis/functions/index.mdx index febc6d2524..fdadb8fce3 100644 --- a/apps/typegpu-docs/src/content/docs/apis/functions/index.mdx +++ b/apps/typegpu-docs/src/content/docs/apis/functions/index.mdx @@ -483,6 +483,22 @@ Another thing to note is that there is no need to specify locations of the argum as TypeGPU tries to assign locations automatically. If you wish to, you can assign the locations manually with the `d.location` decorator. +:::note +WGSL requires integer inter-stage values (`i32`, `u32`, `vec*i`, `vec*u`) to use flat interpolation. +When an entry function is defined with a shell, its vertex outputs and fragment inputs have to specify it explicitly, +otherwise resolution fails with an error. + +```ts +const vertexOutput = { + position: d.builtin.position, + instanceIndex: d.interpolate('flat', d.u32), +}; +``` + +Entry functions passed directly to `root.createRenderPipeline` without a shell infer their varyings, +so TypeGPU adds `@interpolate(flat)` to integer varyings automatically. +::: + During WGSL generation, TypeGPU automatically generates structs corresponding to the passed `IORecord`s. In WGSL-implemented functions, input and output structs of the given function can be referenced as `In` and `Out` respectively. Headers in WGSL implementations must be omitted, all input values are accessible through the struct named `in`. diff --git a/packages/typegpu-gl/tests/glslGenerator.test.ts b/packages/typegpu-gl/tests/glslGenerator.test.ts index 89cd6f54a4..32c2c330c9 100644 --- a/packages/typegpu-gl/tests/glslGenerator.test.ts +++ b/packages/typegpu-gl/tests/glslGenerator.test.ts @@ -782,8 +782,8 @@ describe('GlslGenerator - entry point generation with JS functions', () => { const vertFn = tgpu.vertexFn({ out: { position: d.builtin.position, - x: d.u32, - y: d.u32, + x: d.interpolate('flat', d.u32), + y: d.interpolate('flat', d.u32), }, })(() => { 'use gpu'; @@ -822,8 +822,8 @@ describe('GlslGenerator - entry point generation with JS functions', () => { const vertFn = tgpu.vertexFn({ out: { position: d.builtin.position, - x: d.u32, - y: d.u32, + x: d.interpolate('flat', d.u32), + y: d.interpolate('flat', d.u32), }, })(() => { 'use gpu'; @@ -869,8 +869,8 @@ describe('GlslGenerator - entry point generation with JS functions', () => { const vertFn = tgpu.vertexFn({ out: { position: d.builtin.position, - x: d.u32, - y: d.u32, + x: d.interpolate('flat', d.u32), + y: d.interpolate('flat', d.u32), }, })(() => { 'use gpu'; diff --git a/packages/typegpu/src/core/function/autoIO.ts b/packages/typegpu/src/core/function/autoIO.ts index b35a092b1a..5d0b2defe6 100644 --- a/packages/typegpu/src/core/function/autoIO.ts +++ b/packages/typegpu/src/core/function/autoIO.ts @@ -86,7 +86,12 @@ export class AutoFragmentFn implements SelfResolvable { setName(impl, 'fragmentFn'); } this.#core = createFnCore(impl, 'fragment'); - this.autoIn = new AutoStruct({ ...builtinFragmentIn, ...varyings }, undefined, locations); + this.autoIn = new AutoStruct( + { ...builtinFragmentIn, ...varyings }, + undefined, + locations, + /* autoInterpolateIntegers */ true, + ); setName(this.autoIn, 'FragmentIn'); this.autoOut = new AutoStruct(builtinFragmentOut, vec4f); setName(this.autoOut, 'FragmentOut'); @@ -134,7 +139,12 @@ export class AutoVertexFn implements SelfResolvable { this.#core = createFnCore(impl, 'vertex'); this.autoIn = new AutoStruct({ ...builtinVertexIn, ...attribs }, undefined, locations); setName(this.autoIn, 'VertexIn'); - this.autoOut = new AutoStruct(builtinVertexOut, undefined); + this.autoOut = new AutoStruct( + builtinVertexOut, + undefined, + undefined, + /* autoInterpolateIntegers */ true, + ); setName(this.autoOut, 'VertexOut'); } diff --git a/packages/typegpu/src/core/function/ioSchema.ts b/packages/typegpu/src/core/function/ioSchema.ts index 9d3fe80726..f670207295 100644 --- a/packages/typegpu/src/core/function/ioSchema.ts +++ b/packages/typegpu/src/core/function/ioSchema.ts @@ -2,12 +2,23 @@ import { type Decorate, type HasCustomLocation, type IsBuiltin, + interpolate, location, } from '../../data/attributes.ts'; import { isBuiltin } from '../../data/attributes.ts'; import { getCustomLocation, isData } from '../../data/dataTypes.ts'; import { INTERNAL_createStruct } from '../../data/struct.ts'; -import { type BaseData, isVoid, type Location, type WgslStruct } from '../../data/wgslTypes.ts'; +import { + type BaseData, + type FlatInterpolatableData, + isDecorated, + isInteger, + isIntegerVec, + isInterpolateAttrib, + isVoid, + type Location, + type WgslStruct, +} from '../../data/wgslTypes.ts'; import type { SeparatedEntryArgs } from './fnTypes.ts'; export type WithLocations> = { @@ -31,6 +42,7 @@ export type IOLayoutToSchema = T extends BaseData export function withLocations( members: Record | undefined, locations: Record = {}, + autoInterpolateIntegers = false, ): Record { let nextLocation = 0; const usedCustomLocations = new Set(); @@ -47,7 +59,10 @@ export function withLocations( usedCustomLocations.add(customLocation); } - return [key, member] as const; + return [ + key, + autoInterpolateIntegers ? withFlatInterpolationForInteger(member) : member, + ] as const; }) .map(([key, member]) => { if (isBuiltin(member)) { @@ -105,19 +120,55 @@ export function separateAllAsPositional(schema: Record): Separ export function createIoSchema>( layout: T, locations: Record = {}, + autoInterpolateIntegers = false, ) { - return ( - isData(layout) - ? isVoid(layout) - ? layout - : isBuiltin(layout) - ? layout - : getCustomLocation(layout) !== undefined - ? layout - : location(0, layout) - : INTERNAL_createStruct( - withLocations(layout as Record, locations), - /* isAbstruct */ false, - ) + if (isData(layout)) { + if (isVoid(layout) || isBuiltin(layout)) { + return layout as unknown as IOLayoutToSchema; + } + + return ( + getCustomLocation(layout) !== undefined ? layout : location(0, layout) + ) as IOLayoutToSchema; + } + + return INTERNAL_createStruct( + withLocations(layout as Record, locations, autoInterpolateIntegers), + /* isAbstruct */ false, ) as IOLayoutToSchema; } + +function needsFlatInterpolation(data: BaseData): boolean { + if (isBuiltin(data) || (isDecorated(data) && data.attribs.some(isInterpolateAttrib))) { + return false; + } + + const inner = isDecorated(data) ? data.inner : data; + return isInteger(inner) || isIntegerVec(inner); +} + +function withFlatInterpolationForInteger(data: BaseData): BaseData { + return needsFlatInterpolation(data) ? interpolate('flat', data as FlatInterpolatableData) : data; +} + +/** + * WGSL requires integer inter-stage values to be flat interpolated. Interpolation is only + * inferred for shellless entry functions, so shells have to specify it explicitly. + */ +export function assertIntegerVaryingsInterpolated( + layout: BaseData | Record | undefined, + location: string, +) { + if (layout === undefined || isData(layout)) { + // A single value is either a builtin or a fragment output, neither are varyings + return; + } + + for (const [key, member] of Object.entries(layout)) { + if (needsFlatInterpolation(member)) { + throw new Error( + `Integer value "${key}" in ${location} requires flat interpolation. Wrap its schema in d.interpolate('flat', ...) or d.interpolate('flat, either', ...).`, + ); + } + } +} diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index 7409cbdbb5..19b3b5822e 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -25,7 +25,12 @@ import { shaderStageSlot } from '../slot/internalSlots.ts'; import type { AnyAutoCustoms, AutoFragmentIn, AutoFragmentOut } from './autoIO.ts'; import { createFnCore, type FnCore } from './fnCore.ts'; import type { BaseIOData, Implementation, InferIO, IOLayout, IORecord } from './fnTypes.ts'; -import { createIoSchema, type IOLayoutToSchema, separateBuiltins } from './ioSchema.ts'; +import { + assertIntegerVaryingsInterpolated, + createIoSchema, + type IOLayoutToSchema, + separateBuiltins, +} from './ioSchema.ts'; import { stripTemplate } from './templateUtils.ts'; // ---------- @@ -209,6 +214,10 @@ function createFragmentFn( }, [$resolve](ctx: ResolutionCtx): ResolvedSnippet { + assertIntegerVaryingsInterpolated( + shell.in, + `fragmentFn (${getName(this) ?? ''}) input`, + ); const entryInput = separateBuiltins(shell.in ?? {}, ctx.varyingLocations ?? {}); if (entryInput.dataSchema && isNamable(entryInput.dataSchema)) { diff --git a/packages/typegpu/src/core/function/tgpuVertexFn.ts b/packages/typegpu/src/core/function/tgpuVertexFn.ts index 68b7bee7d3..e0d2d4c388 100644 --- a/packages/typegpu/src/core/function/tgpuVertexFn.ts +++ b/packages/typegpu/src/core/function/tgpuVertexFn.ts @@ -16,7 +16,12 @@ import type { IORecord, SeparatedEntryArgs, } from './fnTypes.ts'; -import { createIoSchema, type IOLayoutToSchema, separateAllAsPositional } from './ioSchema.ts'; +import { + assertIntegerVaryingsInterpolated, + createIoSchema, + type IOLayoutToSchema, + separateAllAsPositional, +} from './ioSchema.ts'; import { stripTemplate } from './templateUtils.ts'; // ---------- @@ -177,6 +182,10 @@ function createVertexFn( }, [$resolve](ctx: ResolutionCtx): ResolvedSnippet { + assertIntegerVaryingsInterpolated( + shell.out, + `vertexFn (${getName(this) ?? ''}) output`, + ); const outputWithLocation = createIoSchema(shell.out, ctx.varyingLocations).$name( `${getName(this) ?? ''}_Output`, ); diff --git a/packages/typegpu/src/data/autoStruct.ts b/packages/typegpu/src/data/autoStruct.ts index d08186bad5..ee00d22d24 100644 --- a/packages/typegpu/src/data/autoStruct.ts +++ b/packages/typegpu/src/data/autoStruct.ts @@ -34,6 +34,7 @@ export class AutoStruct implements BaseData, SelfResolvable { #locations: Record | undefined; #cachedStruct: WgslStruct | undefined; #typeForExtraProps: BaseData | undefined; + readonly #autoInterpolateIntegers: boolean; static { AutoStruct.prototype[$internal] = {}; @@ -44,11 +45,13 @@ export class AutoStruct implements BaseData, SelfResolvable { validProps: Record, typeForExtraProps: BaseData | undefined, locations?: Record, + autoInterpolateIntegers = false, ) { this.#validProps = validProps; this.#typeForExtraProps = typeForExtraProps; this.#allocated = {}; this.#locations = locations; + this.#autoInterpolateIntegers = autoInterpolateIntegers; this.#usedWgslKeys = new Set(); } @@ -102,6 +105,7 @@ export class AutoStruct implements BaseData, SelfResolvable { }), ), this.#locations, + this.#autoInterpolateIntegers, ); const ownName = getName(this); // Passing the given name forward diff --git a/packages/typegpu/tests/renderPipeline.test.ts b/packages/typegpu/tests/renderPipeline.test.ts index df36265dbe..660361a185 100644 --- a/packages/typegpu/tests/renderPipeline.test.ts +++ b/packages/typegpu/tests/renderPipeline.test.ts @@ -153,6 +153,193 @@ describe('render pipeline behavior', () => { }); describe('resolve', () => { + it('keeps explicit interpolation of integer varyings in shells', ({ root }) => { + const vertexMain = tgpu.vertexFn({ + out: { + count: d.interpolate('flat', d.u32), + coordinates: d.location(4, d.interpolate('flat', d.vec2i)), + tagged: d.interpolate('flat, either', d.u32), + position: d.builtin.position, + }, + })`{ return Out(); }`; + + const fragmentMain = tgpu.fragmentFn({ + in: { + count: d.interpolate('flat', d.u32), + coordinates: d.location(4, d.interpolate('flat', d.vec2i)), + tagged: d.interpolate('flat, either', d.u32), + }, + out: d.vec4f, + })(({ count, coordinates, tagged }) => { + 'use gpu'; + return d.vec4f(d.f32(count), d.f32(coordinates.x), d.f32(tagged), 1); + }); + + const pipeline = root.createRenderPipeline({ + vertex: vertexMain, + fragment: fragmentMain, + targets: { format: 'r8unorm' }, + }); + + expect(tgpu.resolve([pipeline])).toMatchInlineSnapshot(` + "struct vertexMain_Output { + @location(0) @interpolate(flat) count: u32, + @location(4) @interpolate(flat) coordinates: vec2i, + @location(1) @interpolate(flat, either) tagged: u32, + @builtin(position) position: vec4f, + } + + @vertex fn vertexMain() -> vertexMain_Output { return vertexMain_Output(); } + + struct fragmentMain_Input { + @location(0) @interpolate(flat) count: u32, + @location(4) @interpolate(flat) coordinates: vec2i, + @location(1) @interpolate(flat, either) tagged: u32, + } + + @fragment fn fragmentMain(_arg_0: fragmentMain_Input) -> @location(0) vec4f { + return vec4f(f32(_arg_0.count), f32(_arg_0.coordinates.x), f32(_arg_0.tagged), 1f); + }" + `); + }); + + it('automatically uses flat interpolation for inferred integer varyings', ({ root }) => { + const pipeline = root.createRenderPipeline({ + vertex: () => { + 'use gpu'; + return { $position: d.vec4f(), count: d.u32(1) }; + }, + fragment: ({ count, $primitiveIndex }) => { + 'use gpu'; + return d.vec4f(d.f32(count + $primitiveIndex)); + }, + targets: { format: 'r8unorm' }, + }); + + const resolved = tgpu.resolve([pipeline]); + + expect(resolved.match(/@location\(0\) @interpolate\(flat\) count: u32/g)).toHaveLength(2); + expect(resolved).toContain('@builtin(primitive_index) primitiveIndex: u32'); + expect(resolved).not.toContain('@interpolate(flat) @builtin(primitive_index)'); + expect(resolved).toMatchInlineSnapshot(` + "struct VertexOut { + @builtin(position) position: vec4f, + @location(0) @interpolate(flat) count: u32, + } + + @vertex fn vertex() -> VertexOut { + return VertexOut(vec4f(), 1u); + } + + struct FragmentIn { + @location(0) @interpolate(flat) count: u32, + @builtin(primitive_index) primitiveIndex: u32, + } + + @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f { + return vec4f(f32((_arg_0.count + _arg_0.primitiveIndex))); + }" + `); + }); + + it('does not flat interpolate integer vertex inputs or fragment outputs', ({ root }) => { + const vertexMain = tgpu.vertexFn({ + in: { index: d.u32 }, + out: { position: d.builtin.position }, + })(({ index }) => { + 'use gpu'; + return { position: d.vec4f(d.f32(index), 0, 0, 1) }; + }); + + const fragmentMain = tgpu.fragmentFn({ out: d.vec4u })(() => d.vec4u(1)); + const pipeline = root.createRenderPipeline({ + vertex: vertexMain, + fragment: fragmentMain, + targets: { format: 'rgba8uint' }, + }); + + const resolved = tgpu.resolve([pipeline]); + + expect(resolved).toContain('@location(0) index: u32'); + expect(resolved).not.toContain('@location(0) @interpolate(flat) index: u32'); + expect(resolved).toContain('@fragment fn fragmentMain() -> @location(0) vec4u'); + expect(resolved).not.toContain( + '@fragment fn fragmentMain() -> @location(0) @interpolate(flat) vec4u', + ); + expect(resolved).toMatchInlineSnapshot(` + "struct vertexMain_Output { + @builtin(position) position: vec4f, + } + + @vertex fn vertexMain(@location(0) index: u32) -> vertexMain_Output { + return vertexMain_Output(vec4f(f32(index), 0f, 0f, 1f)); + } + + @fragment fn fragmentMain() -> @location(0) vec4u { + return vec4u(1); + }" + `); + }); + + it('throws when a vertex shell output has an integer without interpolation', () => { + const vertexMain = tgpu.vertexFn({ + out: { count: d.u32, position: d.builtin.position }, + })`{ return Out(); }`; + + expect(() => tgpu.resolve([vertexMain])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - vertexFn:vertexMain: Integer value "count" in vertexFn (vertexMain) output requires flat interpolation. Wrap its schema in d.interpolate('flat', ...) or d.interpolate('flat, either', ...).] + `); + }); + + it('throws when a fragment shell input has an integer without interpolation', () => { + const fragmentMain = tgpu.fragmentFn({ + in: { coordinates: d.location(4, d.vec2i) }, + out: d.vec4f, + })`{ return Out(f32(in.coordinates.x)); }`; + + expect(() => tgpu.resolve([fragmentMain])).toThrowErrorMatchingInlineSnapshot(` + [Error: Resolution of the following tree failed: + - + - fragmentFn:fragmentMain: Integer value "coordinates" in fragmentFn (fragmentMain) input requires flat interpolation. Wrap its schema in d.interpolate('flat', ...) or d.interpolate('flat, either', ...).] + `); + }); + + it('keeps explicit interpolation when a vertex shell is paired with a shellless fragment', ({ + root, + }) => { + const vertexMain = tgpu.vertexFn({ + out: { position: d.builtin.position, count: d.interpolate('flat, either', d.u32) }, + })`{ return Out(); }`; + + const pipeline = root.createRenderPipeline({ + vertex: vertexMain, + fragment: ({ count }) => { + 'use gpu'; + return d.vec4f(d.f32(count)); + }, + targets: { format: 'r8unorm' }, + }); + + expect(tgpu.resolve([pipeline])).toMatchInlineSnapshot(` + "struct vertexMain_Output { + @builtin(position) position: vec4f, + @location(0) @interpolate(flat, either) count: u32, + } + + @vertex fn vertexMain() -> vertexMain_Output { return vertexMain_Output(); } + + struct FragmentIn { + @location(0) @interpolate(flat, either) count: u32, + } + + @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f { + return vec4f(f32(_arg_0.count)); + }" + `); + }); + it('resolves with correct locations when pairing up a vertex and a fragment function', ({ root, }) => { @@ -162,7 +349,7 @@ describe('render pipeline behavior', () => { bar: d.vec3f, baz: d.location(0, d.vec3f), baz2: d.location(5, d.f32), - baz3: d.u32, + baz3: d.interpolate('flat', d.u32), pos: d.builtin.position, }, })(() => ({ @@ -176,7 +363,7 @@ describe('render pipeline behavior', () => { const fragmentMain = tgpu.fragmentFn({ in: { - baz3: d.u32, + baz3: d.interpolate('flat', d.u32), bar: d.vec3f, foo: d.location(2, d.vec3f), baz2: d.f32, @@ -196,7 +383,7 @@ describe('render pipeline behavior', () => { @location(1) bar: vec3f, @location(0) baz: vec3f, @location(5) baz2: f32, - @location(3) baz3: u32, + @location(3) @interpolate(flat) baz3: u32, @builtin(position) pos: vec4f, } @@ -220,14 +407,14 @@ describe('render pipeline behavior', () => { position: d.builtin.position, baz: d.location(0, d.vec3f), baz2: d.location(5, d.f32), - baz3: d.u32, + baz3: d.interpolate('flat', d.u32), }, })`{ return Out(); }`; const fragmentMain = tgpu.fragmentFn({ in: { position: d.builtin.position, - baz3: d.u32, + baz3: d.interpolate('flat', d.u32), bar: d.vec3f, foo: d.location(2, d.vec3f), baz2: d.f32, @@ -248,13 +435,13 @@ describe('render pipeline behavior', () => { @builtin(position) position: vec4f, @location(0) baz: vec3f, @location(5) baz2: f32, - @location(3) baz3: u32, + @location(3) @interpolate(flat) baz3: u32, } @vertex fn vertexMain() -> vertexMain_Output { return vertexMain_Output(); } struct fragmentMain_Input { - @location(3) baz3: u32, + @location(3) @interpolate(flat) baz3: u32, @location(1) bar: vec3f, @location(2) foo: vec3f, @location(5) baz2: f32, @@ -868,7 +1055,7 @@ describe('root.createRenderPipeline', () => { expect(tgpu.resolve([pipeline])).toMatchInlineSnapshot(` "struct VertexOut { @builtin(position) position: vec4f, - @location(0) prop: i32, + @location(0) @interpolate(flat) prop: i32, } @vertex fn vertex() -> VertexOut { @@ -876,7 +1063,7 @@ describe('root.createRenderPipeline', () => { } struct FragmentIn { - @location(0) prop: i32, + @location(0) @interpolate(flat) prop: i32, } @fragment fn fragment(_arg_0: FragmentIn) -> @location(0) vec4f {