Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,6 @@ function makePipelines(
outputGridMutable: TgpuBufferMutable<GridData>,
) {
const initWorldAction = root['~unstable']
.with(inputGridSlot, outputGridMutable)
.with(outputGridSlot, outputGridMutable)
.prepareDispatch((xu, yu) => {
'use gpu';
Expand Down
1 change: 0 additions & 1 deletion packages/typegpu/src/core/function/tgpuFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,6 @@ function createBoundFunction<ImplSchema extends AnyFn>(
value: unknown,
): TgpuFn<ImplSchema> {
return createBoundFunction(fn, [
...pairs,
[isAccessor(slot) ? slot.slot : slot, value],
]);
},
Expand Down
64 changes: 24 additions & 40 deletions packages/typegpu/src/resolutionCtx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ import type {
ItemLayer,
ItemStateStack,
ResolutionCtx,
StackLayer,
Wgsl,
} from './types.ts';
import { CodegenState, isSelfResolvable, NormalState } from './types.ts';
import type { WgslExtension } from './wgslExtensions.ts';
import { hasTinyestMetadata } from './shared/meta.ts';
import { getName, hasTinyestMetadata } from './shared/meta.ts';

/**
* Inserted into bind group entry definitions that belong
Expand All @@ -90,23 +91,8 @@ export type ResolutionCtxImplOptions = {
readonly namespace: Namespace;
};

type SlotBindingLayer = {
type: 'slotBinding';
bindingMap: WeakMap<TgpuSlot<unknown>, unknown>;
};

type BlockScopeLayer = {
type: 'blockScope';
declarations: Map<string, Snippet>;
};

class ItemStateStackImpl implements ItemStateStack {
private _stack: (
| ItemLayer
| SlotBindingLayer
| FunctionScopeLayer
| BlockScopeLayer
)[] = [];
private _stack: StackLayer[] = [];
private _itemDepth = 0;

get itemDepth(): number {
Expand All @@ -133,21 +119,14 @@ class ItemStateStackImpl implements ItemStateStack {
});
}

popItem() {
this.pop('item');
}

pushSlotBindings(pairs: SlotValuePair<unknown>[]) {
this._stack.push({
type: 'slotBinding',
bindingMap: new WeakMap(pairs),
usedSet: new WeakSet(),
});
}

popSlotBindings() {
this.pop('slotBinding');
}

pushFunctionScope(
args: Snippet[],
argAliases: Record<string, Snippet>,
Expand All @@ -167,31 +146,26 @@ class ItemStateStackImpl implements ItemStateStack {
return scope;
}

popFunctionScope() {
this.pop('functionScope');
}

pushBlockScope() {
this._stack.push({
type: 'blockScope',
declarations: new Map(),
});
}

popBlockScope() {
this.pop('blockScope');
}

pop(type?: (typeof this._stack)[number]['type']) {
pop<T extends StackLayer['type']>(type: T): Extract<StackLayer, { type: T }>;
pop(): StackLayer | undefined;
pop(type?: StackLayer['type']) {
const layer = this._stack[this._stack.length - 1];
if (!layer || (type && layer.type !== type)) {
throw new Error(`Internal error, expected a ${type} layer to be on top.`);
}

this._stack.pop();
const poppedValue = this._stack.pop();
if (type === 'item') {
this._itemDepth--;
}
return poppedValue;
}

readSlot<T>(slot: TgpuSlot<T>): T | undefined {
Expand All @@ -204,6 +178,7 @@ class ItemStateStackImpl implements ItemStateStack {
const boundValue = layer.bindingMap.get(slot);

if (boundValue !== undefined) {
layer.usedSet.add(slot);
return boundValue as T;
}
} else if (
Expand Down Expand Up @@ -433,7 +408,7 @@ export class ResolutionCtxImpl implements ResolutionCtx {
}

popBlockScope() {
this._itemStateStack.popBlockScope();
this._itemStateStack.pop('blockScope');
}

generateLog(args: Snippet[]): Snippet {
Expand Down Expand Up @@ -487,7 +462,7 @@ export class ResolutionCtxImpl implements ResolutionCtx {
returnType,
};
} finally {
this._itemStateStack.popFunctionScope();
this._itemStateStack.pop('functionScope');
}
}

Expand Down Expand Up @@ -537,7 +512,16 @@ export class ResolutionCtxImpl implements ResolutionCtx {
try {
return callback();
} finally {
this._itemStateStack.popSlotBindings();
const usedSlots = this._itemStateStack.pop('slotBinding').usedSet;
pairs.forEach((pair) => {
if (!usedSlots.has(pair[0])) {
console.warn(
`Slot '${getName(pair[0])}' with value '${
pair[1]
}' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.`,
);
}
});
}
}

Expand Down Expand Up @@ -624,7 +608,7 @@ export class ResolutionCtxImpl implements ResolutionCtx {

throw new ResolutionError(err, [derived]);
} finally {
this._itemStateStack.popItem();
this._itemStateStack.pop('item');
}
}

Expand Down Expand Up @@ -695,7 +679,7 @@ export class ResolutionCtxImpl implements ResolutionCtx {

throw new ResolutionError(err, [item]);
} finally {
this._itemStateStack.popItem();
this._itemStateStack.pop('item');
}
}

Expand Down
26 changes: 21 additions & 5 deletions packages/typegpu/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,30 @@ export type FunctionScopeLayer = {
reportedReturnTypes: Set<AnyData>;
};

export type SlotBindingLayer = {
type: 'slotBinding';
bindingMap: WeakMap<TgpuSlot<unknown>, unknown>;
usedSet: WeakSet<TgpuSlot<unknown>>;
};

export type BlockScopeLayer = {
type: 'blockScope';
declarations: Map<string, Snippet>;
};

export type StackLayer =
| ItemLayer
| SlotBindingLayer
| FunctionScopeLayer
| BlockScopeLayer;

export interface ItemStateStack {
readonly itemDepth: number;
readonly topItem: ItemLayer;
readonly topFunctionScope: FunctionScopeLayer | undefined;

pushItem(): void;
popItem(): void;
pushSlotBindings(pairs: SlotValuePair<unknown>[]): void;
popSlotBindings(): void;
pushFunctionScope(
args: Snippet[],
argAliases: Record<string, Snippet>,
Expand All @@ -126,10 +141,11 @@ export interface ItemStateStack {
returnType: AnyData | undefined,
externalMap: Record<string, unknown>,
): FunctionScopeLayer;
popFunctionScope(): void;
pushBlockScope(): void;
popBlockScope(): void;
pop(type?: 'functionScope' | 'blockScope' | 'slotBinding' | 'item'): void;

pop<T extends StackLayer['type']>(type: T): Extract<StackLayer, { type: T }>;
pop(): StackLayer | undefined;

readSlot<T>(slot: TgpuSlot<T>): T | undefined;
getSnippetById(id: string): Snippet | undefined;
defineBlockVariable(id: string, snippet: Snippet): void;
Expand Down
155 changes: 153 additions & 2 deletions packages/typegpu/tests/slot.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect } from 'vitest';
import { describe, expect, vi } from 'vitest';
import * as d from '../src/data/index.ts';
import * as std from '../src/std/index.ts';
import tgpu from '../src/index.ts';
import * as std from '../src/std/index.ts';
import { it } from './utils/extendedIt.ts';
import { asWgsl } from './utils/parseResolved.ts';

Expand Down Expand Up @@ -363,4 +363,155 @@ describe('tgpu.slot', () => {
}"
`);
});

it('warns when the slot is unused in WGSL-implemented functions', () => {
using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

const colorSlot = tgpu.slot<d.v3f>();

const getColor = tgpu.fn([], d.vec3f)`() { return vec3f(); }`;

const main = getColor.with(colorSlot, d.vec3f(0, 1, 0));

tgpu.resolve({ externals: { main } });

expect(warnSpy).toHaveBeenCalledWith(
"Slot 'colorSlot' with value 'vec3f(0, 1, 0)' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.",
);
});

it('warns when the slot is unused in TGSL-implemented functions', () => {
using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

const colorSlot = tgpu.slot<d.v3f>();

const getColor = tgpu.fn([], d.vec3f)(() => {
return d.vec3f();
});

const main = getColor.with(colorSlot, d.vec3f(0, 1, 0));

tgpu.resolve({ externals: { main } });

expect(warnSpy).toHaveBeenCalledWith(
"Slot 'colorSlot' with value 'vec3f(0, 1, 0)' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.",
);
});

it('warns when the slot is unused in WGSL-implemented pipeline', ({ root }) => {
using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

const colorSlot = tgpu.slot<d.v3f>();

const computeFn = tgpu['~unstable'].computeFn({
workgroupSize: [1, 1, 1],
in: { gid: d.builtin.globalInvocationId },
})`{ }`;

const pipeline = root['~unstable']
.with(colorSlot, d.vec3f(1, 0, 1))
.withCompute(computeFn)
.createPipeline();

tgpu.resolve({ externals: { pipeline } });

expect(warnSpy).toHaveBeenCalledWith(
"Slot 'colorSlot' with value 'vec3f(1, 0, 1)' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.",
);
});

it('warns when the slot is unused in TGSL-implemented pipeline', ({ root }) => {
using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

const colorSlot = tgpu.slot<d.v3f>();

const computeFn = tgpu['~unstable'].computeFn({
workgroupSize: [1, 1, 1],
in: { gid: d.builtin.globalInvocationId },
})(() => {});

const pipeline = root['~unstable']
.with(colorSlot, d.vec3f(1, 0, 1))
.withCompute(computeFn)
.createPipeline();

tgpu.resolve({ externals: { pipeline } });

expect(warnSpy).toHaveBeenCalledWith(
"Slot 'colorSlot' with value 'vec3f(1, 0, 1)' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.",
);
});

it('distinguishes different slot usages', () => {
using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

const colorSlot = tgpu.slot<d.v3f>();

const getColorUsingSlot = tgpu
.fn([], d.vec3f)(() => {
return colorSlot.$;
})
.with(colorSlot, d.vec3f(0, 1, 0));

const getColor = tgpu
.fn([], d.vec3f)(() => {
return d.vec3f();
})
.with(colorSlot, d.vec3f(2, 1, 0));

const main = tgpu.fn([])(() => {
getColorUsingSlot();
getColor();
});
tgpu.resolve({ externals: { main } });

expect(warnSpy).toHaveBeenCalledWith(
"Slot 'colorSlot' with value 'vec3f(2, 1, 0)' was provided in a 'with' method despite not being utilized during resolution. Please verify that this slot was intended for use and that, in case of WGSL-implemented functions, it is properly declared with the '$uses' method.",
);
});

it('does not warn in nested case', () => {
using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

const sizeSlot = tgpu.slot<number>();

const getSize = tgpu.fn([], d.f32)(() => sizeSlot.$);

const main = tgpu.fn([], d.f32)(() => getSize()).with(sizeSlot, 1);

tgpu.resolve({ externals: { main } });

expect(warnSpy).toHaveBeenCalledTimes(0);
});

it('warns exactly as many times as there are unused slots', () => {
using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

const sizeSlot = tgpu.slot<number>();
const colorSlot = tgpu.slot<typeof RED | typeof GREEN>();
const shapeSlot = tgpu.slot<0 | 1 | 2>();

const getSize = tgpu.fn([], d.f32)`() { return sizeSlot; }`
.$uses({ sizeSlot })
.with(sizeSlot, 1)
.with(colorSlot, RED)
.with(shapeSlot, 2);

tgpu.resolve({ externals: { getSize } });

expect(warnSpy).toHaveBeenCalledTimes(2);
});

it('does not warn when default value is used', () => {
using warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

const sizeSlot = tgpu.slot<number>(7);

const getSize = tgpu.fn([], d.f32)`() { return sizeSlot; }`
.$uses({ sizeSlot });

tgpu.resolve({ externals: { getSize } });

expect(warnSpy).toHaveBeenCalledTimes(0);
});
});
Loading