diff --git a/apps/typegpu-docs/tests/individual-example-tests/game-of-life.test.ts b/apps/typegpu-docs/tests/individual-example-tests/game-of-life.test.ts index d60895463d..ced2640a42 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/game-of-life.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/game-of-life.test.ts @@ -241,6 +241,8 @@ describe('game of life example', () => { textureStore(next, gid.xy, vec4u(u32(select(0i, 1i, nextAlive)), 0u, 0u, 0u)); } + requires readonly_and_readwrite_storage_textures; + struct fullScreenTriangle_Output { @builtin(position) pos: vec4f, @location(0) uv: vec2f, diff --git a/apps/typegpu-docs/tests/individual-example-tests/jump-flood-distance.test.ts b/apps/typegpu-docs/tests/individual-example-tests/jump-flood-distance.test.ts index ddbdce02a1..4b62c8cce7 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/jump-flood-distance.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/jump-flood-distance.test.ts @@ -22,7 +22,9 @@ describe('jump flood (distance) example', () => { ); expect(shaderCodes).toMatchInlineSnapshot(` - "@group(0) @binding(0) var sizeUniform: vec3u; + "requires readonly_and_readwrite_storage_textures; + + @group(0) @binding(0) var sizeUniform: vec3u; @group(1) @binding(1) var writeView: texture_storage_2d; @@ -47,6 +49,8 @@ describe('jump flood (distance) example', () => { wrappedCallback(id.x, id.y, id.z); } + requires readonly_and_readwrite_storage_textures; + @group(0) @binding(0) var sizeUniform: vec3u; @group(0) @binding(1) var offsetUniform: i32; @@ -258,6 +262,8 @@ describe('jump flood (distance) example', () => { wrappedCallback(id.x, id.y, id.z); } + requires readonly_and_readwrite_storage_textures; + @group(0) @binding(0) var sizeUniform: vec3u; @group(1) @binding(1) var readView: texture_storage_2d; diff --git a/apps/typegpu-docs/tests/individual-example-tests/jump-flood-voronoi.test.ts b/apps/typegpu-docs/tests/individual-example-tests/jump-flood-voronoi.test.ts index 2b47d416e9..615e248401 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/jump-flood-voronoi.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/jump-flood-voronoi.test.ts @@ -137,6 +137,8 @@ describe('jump flood (voronoi) example', () => { return textureSample(floodTexture, sampler_1, _arg_0.uv); } + requires readonly_and_readwrite_storage_textures; + @group(0) @binding(0) var sizeUniform: vec3u; @group(0) @binding(1) var offsetUniform: i32; diff --git a/apps/typegpu-docs/tests/individual-example-tests/point-light-shadow.test.ts b/apps/typegpu-docs/tests/individual-example-tests/point-light-shadow.test.ts index fa3b82931d..89730a6a8e 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/point-light-shadow.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/point-light-shadow.test.ts @@ -22,7 +22,9 @@ describe('point light shadow example', () => { ); expect(shaderCodes).toMatchInlineSnapshot(` - "var viewProjectionMatrix: mat4x4f; + "requires immediate_address_space; + + var viewProjectionMatrix: mat4x4f; struct vertexDepth_Output { @builtin(position) pos: vec4f, diff --git a/apps/typegpu-docs/tests/individual-example-tests/slime-mold-3d.test.ts b/apps/typegpu-docs/tests/individual-example-tests/slime-mold-3d.test.ts index ac9e3437fb..17ec5ae0ef 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/slime-mold-3d.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/slime-mold-3d.test.ts @@ -159,6 +159,8 @@ describe('slime mold 3d example', () => { textureStore(newState, gid.xyz, vec4f(newValue, 0f, 0f, 1f)); } + requires readonly_and_readwrite_storage_textures; + fn hash(value: u32) -> u32 { { var x = (value ^ (value >> 17u)); diff --git a/apps/typegpu-docs/tests/individual-example-tests/slime-mold.test.ts b/apps/typegpu-docs/tests/individual-example-tests/slime-mold.test.ts index 81f5ab0193..12029b6d4c 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/slime-mold.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/slime-mold.test.ts @@ -105,6 +105,8 @@ describe('slime mold example', () => { wrappedCallback(id.x, id.y, id.z); } + requires readonly_and_readwrite_storage_textures; + @group(1) @binding(0) var oldState: texture_storage_2d; struct Params { @@ -228,6 +230,8 @@ describe('slime mold example', () => { textureStore(newState, gid.xy, vec4f(newColor, 1f)); } + requires readonly_and_readwrite_storage_textures; + fn hash(value: u32) -> u32 { { var x = (value ^ (value >> 17u)); diff --git a/apps/typegpu-docs/tests/individual-example-tests/uniformity.test.ts b/apps/typegpu-docs/tests/individual-example-tests/uniformity.test.ts index 05742970b1..7c1612b332 100644 --- a/apps/typegpu-docs/tests/individual-example-tests/uniformity.test.ts +++ b/apps/typegpu-docs/tests/individual-example-tests/uniformity.test.ts @@ -23,7 +23,9 @@ describe('uniformity test example', () => { ); expect(shaderCodes).toMatchInlineSnapshot(` - "struct fullScreenTriangle_Output { + "requires readonly_and_readwrite_storage_textures; + + struct fullScreenTriangle_Output { @builtin(position) pos: vec4f, @location(0) uv: vec2f, } diff --git a/packages/typegpu/src/core/resolve/resolveData.ts b/packages/typegpu/src/core/resolve/resolveData.ts index f5428fb819..ae408170f2 100644 --- a/packages/typegpu/src/core/resolve/resolveData.ts +++ b/packages/typegpu/src/core/resolve/resolveData.ts @@ -275,6 +275,9 @@ export function resolveData(ctx: ResolutionCtx, data: AnyData): string { } if (isWgslStorageTexture(data)) { + if (data.access !== 'write-only') { + ctx.requireFeatures.add('readonly_and_readwrite_storage_textures'); + } return `${data.type}<${data.format}, ${accessModeMap[data.access]}>`; } diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index c183d04cdd..e6368e6f06 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -54,6 +54,7 @@ import type { StackLayer, ShaderStage, Wgsl, + WgslLanguageExtension, } from './types.ts'; import { CodegenState, isSelfResolvable, NormalState, type FunctionArgument } from './types.ts'; import type { WgslEnableExtension } from './wgslExtensions.ts'; @@ -453,6 +454,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { // -- public readonly enableExtensions: WgslEnableExtension[] | undefined; + public readonly requireFeatures = new Set(); public expectedType: BaseData | undefined; #usedImmediate: TgpuImmediateVar | undefined; @@ -479,6 +481,7 @@ export class ResolutionCtxImpl implements ResolutionCtx { ); } this.#usedImmediate = immediate; + this.requireFeatures.add('immediate_address_space'); } get usedImmediate(): TgpuImmediateVar | undefined { @@ -1251,6 +1254,11 @@ export function resolve(item: Wgsl, options: ResolutionCtxImplOptions): Resoluti code = `${extensions.join('\n')}\n\n${code}`; } + if (ctx.requireFeatures.size > 0) { + const features = [...ctx.requireFeatures].join(', '); + code = `requires ${features};\n\n${code}`; + } + let declarations = ctx.declarations.map(({ name, code: declarationCode }) => ({ name, code: bindingReplacements.reduce( diff --git a/packages/typegpu/src/tgsl/wgslGenerator.ts b/packages/typegpu/src/tgsl/wgslGenerator.ts index 9569541d32..ee42f08b39 100644 --- a/packages/typegpu/src/tgsl/wgslGenerator.ts +++ b/packages/typegpu/src/tgsl/wgslGenerator.ts @@ -2204,6 +2204,9 @@ ${stringifyNode(statement)}`); expr, asComptime(mutated.value.base) !== undefined, ); + if (mutated.value.components.length > 1) { + this.ctx.requireFeatures.add('swizzle_assignment'); + } } else { throwIfImmutable(mutated.origin, expr, asComptime(mutated) !== undefined); } diff --git a/packages/typegpu/src/types.ts b/packages/typegpu/src/types.ts index 3c66e1031f..0371660b41 100644 --- a/packages/typegpu/src/types.ts +++ b/packages/typegpu/src/types.ts @@ -266,6 +266,23 @@ export class SimulationState { export type ExecState = NormalState | CodegenState | SimulationState; +export type WgslLanguageExtension = + | 'readonly_and_readwrite_storage_textures' + | 'packed_4x8_integer_dot_product' + | 'unrestricted_pointer_parameters' + | 'pointer_composite_access' + | 'uniform_buffer_standard_layout' + | 'subgroup_id' + | 'subgroup_uniformity' + | 'texture_and_sampler_let' + | 'texture_formats_tier1' + | 'linear_indexing' + | 'immediate_address_space' + | 'fragment_depth' + | 'buffer_view' + | 'swizzle_assignment' + | 'atomic_vec2u_min_max'; + /** * Passed into each resolvable item. All items in a tree share a resolution ctx, * but there can be layers added and removed from the item stack when going down @@ -279,6 +296,7 @@ export interface ResolutionCtx { readonly pre: string; readonly mode: ExecState; readonly enableExtensions: WgslEnableExtension[] | undefined; + readonly requireFeatures: Set; readonly gen: ShaderGenerator; /** diff --git a/packages/typegpu/tests/immediates.test.ts b/packages/typegpu/tests/immediates.test.ts index ab122b5568..9573152561 100644 --- a/packages/typegpu/tests/immediates.test.ts +++ b/packages/typegpu/tests/immediates.test.ts @@ -28,7 +28,9 @@ describe('tgpu.immediateVar', () => { const fn1 = tgpu.fn([], d.f32)(() => level.$); expect(tgpu.resolve([fn1])).toMatchInlineSnapshot(` - "var level: f32; + "requires immediate_address_space; + + var level: f32; fn fn1() -> f32 { return level; @@ -36,12 +38,21 @@ describe('tgpu.immediateVar', () => { `); }); + it('includes a require', () => { + const level = tgpu['~unstable'].immediateVar(d.f32); + const fn = tgpu.fn([], d.f32)(() => level.$); + + expect(tgpu.resolve([fn])).toContain('requires immediate_address_space;'); + }); + it('does not emit the default value as an initializer', () => { const level = tgpu['~unstable'].immediateVar(d.f32, 0.5); const fn1 = tgpu.fn([], d.f32)(() => level.$); expect(tgpu.resolve([fn1])).toMatchInlineSnapshot(` - "var level: f32; + "requires immediate_address_space; + + var level: f32; fn fn1() -> f32 { return level; @@ -62,7 +73,9 @@ describe('tgpu.immediateVar', () => { }); expect(tgpu.resolve([fn1])).toMatchInlineSnapshot(` - "struct Params { + "requires immediate_address_space; + + struct Params { intensity: f32, color: vec3f, } @@ -88,7 +101,9 @@ describe('tgpu.immediateVar', () => { }); expect(tgpu.resolve([fn1])).toMatchInlineSnapshot(` - "var offset: vec4f; + "requires immediate_address_space; + + var offset: vec4f; fn fn1() -> vec2f { let o = (&offset); @@ -123,14 +138,18 @@ describe('tgpu.immediateVar', () => { return slot.$ ? first.$ : second.$; }); expect(tgpu.resolve([choose.with(slot, true)])).toMatchInlineSnapshot(` - "var first: f32; + "requires immediate_address_space; + + var first: f32; fn choose() -> f32 { return first; }" `); expect(tgpu.resolve([choose.with(slot, false)])).toMatchInlineSnapshot(` - "var second: f32; + "requires immediate_address_space; + + var second: f32; fn choose() -> f32 { return second; @@ -144,7 +163,9 @@ describe('tgpu.immediateVar', () => { const outer = tgpu.fn([], d.f32)(() => inner() + level.$); expect(tgpu.resolve([outer])).toMatchInlineSnapshot(` - "var level: f32; + "requires immediate_address_space; + + var level: f32; fn inner() -> f32 { return level; @@ -727,7 +748,9 @@ describe('tgpu.immediateVar', () => { .with(level, levelImmediate); expect(tgpu.resolve([fn1])).toMatchInlineSnapshot(` - "var levelImmediate: f32; + "requires immediate_address_space; + + var levelImmediate: f32; fn fn1() -> f32 { return levelImmediate; diff --git a/packages/typegpu/tests/indent.test.ts b/packages/typegpu/tests/indent.test.ts index 0c76ab660e..8aad448d72 100644 --- a/packages/typegpu/tests/indent.test.ts +++ b/packages/typegpu/tests/indent.test.ts @@ -174,7 +174,9 @@ describe('indents', () => { }); expect(tgpu.resolve([main])).toMatchInlineSnapshot(` - "@group(0) @binding(2) var counter: u32; + "requires readonly_and_readwrite_storage_textures; + + @group(0) @binding(2) var counter: u32; fn incrementCounter() { counter += 1u; diff --git a/packages/typegpu/tests/namespace.test.ts b/packages/typegpu/tests/namespace.test.ts index 43958bc22d..44971ad3b1 100644 --- a/packages/typegpu/tests/namespace.test.ts +++ b/packages/typegpu/tests/namespace.test.ts @@ -30,7 +30,9 @@ describe('tgpu.namespace', () => { const fn = tgpu.fn([], d.f32)(() => level.$); expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "var immediate_1: f32; + "requires immediate_address_space; + + var immediate_1: f32; fn fn_1() -> f32 { return immediate_1; diff --git a/packages/typegpu/tests/requires.test.ts b/packages/typegpu/tests/requires.test.ts new file mode 100644 index 0000000000..220bde0212 --- /dev/null +++ b/packages/typegpu/tests/requires.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it } from 'vitest'; +import { tgpu, d, std } from 'typegpu'; + +describe('requires directive', () => { + it('requires readonly_and_readwrite_storage_textures for read-only storage textures', () => { + const layout = tgpu.bindGroupLayout({ + texture: { storageTexture: d.textureStorage2d('r32float', 'read-only') }, + }); + + const fn = () => { + 'use gpu'; + return std.textureLoad(layout.$.texture, d.vec2i()); + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "requires readonly_and_readwrite_storage_textures; + + @group(0) @binding(0) var texture: texture_storage_2d; + + fn fn_1() -> vec4f { + return textureLoad(texture, vec2i()); + }" + `); + }); + + it('requires readonly_and_readwrite_storage_textures for read-write storage textures', () => { + const layout = tgpu.bindGroupLayout({ + texture: { storageTexture: d.textureStorage2d('r32float', 'read-write') }, + }); + + const fn = () => { + 'use gpu'; + return std.textureLoad(layout.$.texture, d.vec2i()); + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "requires readonly_and_readwrite_storage_textures; + + @group(0) @binding(0) var texture: texture_storage_2d; + + fn fn_1() -> vec4f { + return textureLoad(texture, vec2i()); + }" + `); + }); + + it('requires immediate_address_space', () => { + const level = tgpu['~unstable'].immediateVar(d.u32); + + const fn = () => { + 'use gpu'; + return level.$; + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "requires immediate_address_space; + + var level: u32; + + fn fn_1() -> u32 { + return level; + }" + `); + }); + + it('requires swizzle_assignment', () => { + const fn = () => { + 'use gpu'; + const vec = d.vec4f(); + vec.xy = d.vec2f(1, 2); + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "requires swizzle_assignment; + + fn fn_1() { + var vec = vec4f(); + vec.xy = vec2f(1, 2); + }" + `); + }); + + it('combines multiple requires into a single directive', () => { + const level = tgpu['~unstable'].immediateVar(d.u32); + const layout = tgpu.bindGroupLayout({ + texture: { storageTexture: d.textureStorage2d('r32uint', 'read-write') }, + }); + + const fn = () => { + 'use gpu'; + return std.textureLoad(layout.$.texture, d.vec2i(level.$)); + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "requires immediate_address_space, readonly_and_readwrite_storage_textures; + + var level: u32; + + @group(0) @binding(0) var texture: texture_storage_2d; + + fn fn_1() -> vec4u { + return textureLoad(texture, vec2i(i32(level))); + }" + `); + }); + + it('does not clash with extensions', () => { + const level = tgpu['~unstable'].immediateVar(d.u32); + + const fn = () => { + 'use gpu'; + const x = d.f16(); + return level.$; + }; + + expect(tgpu.resolve([fn], { enableExtensions: ['f16'] })).toMatchInlineSnapshot(` + "requires immediate_address_space; + + enable f16; + + var level: u32; + + fn fn_1() -> u32 { + const x = 0h; + return level; + }" + `); + }); +}); diff --git a/packages/typegpu/tests/std/texture/textureLoad.test.ts b/packages/typegpu/tests/std/texture/textureLoad.test.ts index 1729d4a78a..261f8c7e52 100644 --- a/packages/typegpu/tests/std/texture/textureLoad.test.ts +++ b/packages/typegpu/tests/std/texture/textureLoad.test.ts @@ -135,7 +135,9 @@ describe('textureLoad', () => { }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var store1d: texture_storage_1d; + "requires readonly_and_readwrite_storage_textures; + + @group(0) @binding(0) var store1d: texture_storage_1d; @group(0) @binding(1) var store2d: texture_storage_2d; diff --git a/packages/typegpu/tests/std/texture/textureQueries.test.ts b/packages/typegpu/tests/std/texture/textureQueries.test.ts index 81927490e0..4383c4c477 100644 --- a/packages/typegpu/tests/std/texture/textureQueries.test.ts +++ b/packages/typegpu/tests/std/texture/textureQueries.test.ts @@ -51,7 +51,9 @@ describe('texture query and gather-compare builtins', () => { }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "@group(0) @binding(0) var sampledArray: texture_2d_array; + "requires readonly_and_readwrite_storage_textures; + + @group(0) @binding(0) var sampledArray: texture_2d_array; @group(0) @binding(1) var cubeArray: texture_cube_array; diff --git a/packages/typegpu/tests/swizzleAssignment.test.ts b/packages/typegpu/tests/swizzleAssignment.test.ts index 104fc9b100..b528116731 100644 --- a/packages/typegpu/tests/swizzleAssignment.test.ts +++ b/packages/typegpu/tests/swizzleAssignment.test.ts @@ -110,7 +110,9 @@ describe('Swizzle assignment', () => { }; expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { + "requires swizzle_assignment; + + fn fn_1() { var vec = vec4f(); vec.xy = vec2f(1, 2); vec.wzy = vec3f(1, 2, 3); @@ -127,7 +129,9 @@ describe('Swizzle assignment', () => { }; expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { + "requires swizzle_assignment; + + fn fn_1() { var vec = vec4f(); vec.xy += 1; }" @@ -157,7 +161,9 @@ describe('Swizzle assignment', () => { }; expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` - "fn fn_1() { + "requires swizzle_assignment; + + fn fn_1() { var vec = vec4f(); vec.xy += 1; }"