Skip to content

Commit d90c703

Browse files
feat: Limit overflow suggestions (#2146)
1 parent b5050ed commit d90c703

7 files changed

Lines changed: 284 additions & 0 deletions

File tree

packages/typegpu/src/core/pipeline/computePipeline.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { TgpuComputeFn } from '../function/tgpuComputeFn.ts';
2525
import { namespace } from '../resolve/namespace.ts';
2626
import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts';
2727
import type { TgpuSlot } from '../slot/slotTypes.ts';
28+
import { warnIfOverflow } from './limitsOverflow.ts';
2829
import {
2930
createWithPerformanceCallback,
3031
createWithTimestampWrites,
@@ -207,6 +208,11 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline {
207208

208209
const missingBindGroups = new Set(memo.usedBindGroupLayouts);
209210

211+
warnIfOverflow(
212+
memo.usedBindGroupLayouts,
213+
this[$internal].root.device.limits,
214+
);
215+
210216
memo.usedBindGroupLayouts.forEach((layout, idx) => {
211217
if (memo.catchall && idx === memo.catchall[0]) {
212218
// Catch-all
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { TgpuBindGroupLayout } from '../../tgpuBindGroupLayout.ts';
2+
3+
export function warnIfOverflow(
4+
layouts: TgpuBindGroupLayout[],
5+
limits: GPUSupportedLimits,
6+
) {
7+
const entries = Object.values(layouts)
8+
.flatMap((layout) => Object.values(layout.entries))
9+
.filter((entry) => entry !== null);
10+
11+
const uniform = entries.filter((entry) => 'uniform' in entry).length;
12+
const storage = entries.filter((entry) => 'storage' in entry).length;
13+
14+
if (uniform > limits.maxUniformBuffersPerShaderStage) {
15+
console.warn(
16+
`Total number of uniform buffers (${uniform}) exceeds maxUniformBuffersPerShaderStage (${limits.maxUniformBuffersPerShaderStage}). Consider:
17+
1. Grouping some of the uniforms into one using 'd.struct',
18+
2. Increasing the limit when requesting a device or creating a root.`,
19+
);
20+
}
21+
22+
if (storage > limits.maxStorageBuffersPerShaderStage) {
23+
console.warn(
24+
`Total number of storage buffers (${storage}) exceeds maxStorageBuffersPerShaderStage (${limits.maxStorageBuffersPerShaderStage}).`,
25+
);
26+
}
27+
}

packages/typegpu/src/core/pipeline/renderPipeline.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ import {
8787
} from '../vertexLayout/vertexLayout.ts';
8888
import { connectAttachmentToShader } from './connectAttachmentToShader.ts';
8989
import { connectTargetsToShader } from './connectTargetsToShader.ts';
90+
import { warnIfOverflow } from './limitsOverflow.ts';
9091
import {
9192
createWithPerformanceCallback,
9293
createWithTimestampWrites,
@@ -657,6 +658,11 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline {
657658

658659
const missingBindGroups = new Set(memo.usedBindGroupLayouts);
659660

661+
warnIfOverflow(
662+
memo.usedBindGroupLayouts,
663+
this[$internal].root.device.limits,
664+
);
665+
660666
memo.usedBindGroupLayouts.forEach((layout, idx) => {
661667
if (memo.catchall && idx === memo.catchall[0]) {
662668
// Catch-all

packages/typegpu/tests/computePipeline.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,4 +546,72 @@ describe('TgpuComputePipeline', () => {
546546
}"
547547
`);
548548
});
549+
550+
it('warns when buffer limits are exceeded', ({ root }) => {
551+
using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(
552+
() => {},
553+
);
554+
555+
const uniform1 = root.createUniform(d.u32);
556+
const uniform2 = root.createUniform(d.u32);
557+
const uniform3 = root.createUniform(d.u32);
558+
const uniform4 = root.createUniform(d.u32);
559+
const uniform5 = root.createUniform(d.u32);
560+
const uniform6 = root.createUniform(d.u32);
561+
const uniform7 = root.createUniform(d.u32);
562+
const uniform8 = root.createUniform(d.u32);
563+
const uniform9 = root.createUniform(d.u32);
564+
const uniform10 = root.createUniform(d.u32);
565+
const uniform11 = root.createUniform(d.u32);
566+
const uniform12 = root.createUniform(d.u32);
567+
const uniform13 = root.createUniform(d.u32);
568+
569+
const readonly1 = root.createReadonly(d.u32);
570+
const readonly2 = root.createReadonly(d.u32);
571+
const readonly3 = root.createReadonly(d.u32);
572+
const readonly4 = root.createReadonly(d.u32);
573+
const readonly5 = root.createReadonly(d.u32);
574+
const readonly6 = root.createReadonly(d.u32);
575+
const readonly7 = root.createReadonly(d.u32);
576+
const readonly8 = root.createReadonly(d.u32);
577+
const readonly9 = root.createReadonly(d.u32);
578+
579+
const pipeline = root.createGuardedComputePipeline(() => {
580+
'use gpu';
581+
let a = d.u32();
582+
a = uniform1.$;
583+
a = uniform2.$;
584+
a = uniform3.$;
585+
a = uniform4.$;
586+
a = uniform5.$;
587+
a = uniform6.$;
588+
a = uniform7.$;
589+
a = uniform8.$;
590+
a = uniform9.$;
591+
a = uniform10.$;
592+
a = uniform11.$;
593+
a = uniform12.$;
594+
a = uniform13.$;
595+
a = readonly1.$;
596+
a = readonly2.$;
597+
a = readonly3.$;
598+
a = readonly4.$;
599+
a = readonly5.$;
600+
a = readonly6.$;
601+
a = readonly7.$;
602+
a = readonly8.$;
603+
a = readonly9.$;
604+
});
605+
606+
pipeline.dispatchThreads();
607+
608+
expect(consoleWarnSpy).toHaveBeenCalledWith(
609+
`Total number of uniform buffers (14) exceeds maxUniformBuffersPerShaderStage (12). Consider:
610+
1. Grouping some of the uniforms into one using 'd.struct',
611+
2. Increasing the limit when requesting a device or creating a root.`,
612+
);
613+
expect(consoleWarnSpy).toHaveBeenCalledWith(
614+
`Total number of storage buffers (9) exceeds maxStorageBuffersPerShaderStage (8).`,
615+
);
616+
});
549617
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { describe, expect, vi } from 'vitest';
2+
import { it } from './utils/extendedIt.ts';
3+
import tgpu, { d } from '../src/index.ts';
4+
import { warnIfOverflow } from '../src/core/pipeline/limitsOverflow.ts';
5+
6+
describe('warnIfOverflow', () => {
7+
const limits = {
8+
maxUniformBuffersPerShaderStage: 2,
9+
maxStorageBuffersPerShaderStage: 1,
10+
// missing props may be added as necessary
11+
} as GPUSupportedLimits;
12+
13+
it('does not warn when no limits are exceeded', () => {
14+
using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(
15+
() => {},
16+
);
17+
18+
const layout = tgpu.bindGroupLayout({
19+
uniform1: { uniform: d.f32 },
20+
uniform2: { uniform: d.f32 },
21+
storage1: { storage: d.f32 },
22+
});
23+
24+
warnIfOverflow([layout], limits);
25+
26+
expect(consoleWarnSpy).toHaveBeenCalledTimes(0);
27+
});
28+
29+
it('warns for uniforms', () => {
30+
using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(
31+
() => {},
32+
);
33+
34+
const layout = tgpu.bindGroupLayout({
35+
uniform1: { uniform: d.f32 },
36+
uniform2: { uniform: d.f32 },
37+
uniform3: { uniform: d.f32 },
38+
});
39+
40+
warnIfOverflow([layout], limits);
41+
42+
expect(consoleWarnSpy).toHaveBeenCalledWith(
43+
`Total number of uniform buffers (3) exceeds maxUniformBuffersPerShaderStage (2). Consider:
44+
1. Grouping some of the uniforms into one using 'd.struct',
45+
2. Increasing the limit when requesting a device or creating a root.`,
46+
);
47+
});
48+
49+
it('warns for storages', () => {
50+
using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(
51+
() => {},
52+
);
53+
54+
const layout = tgpu.bindGroupLayout({
55+
storage1: { storage: d.f32 },
56+
storage2: { storage: d.f32 },
57+
});
58+
59+
warnIfOverflow([layout], limits);
60+
61+
expect(consoleWarnSpy).toHaveBeenCalledWith(
62+
`Total number of storage buffers (2) exceeds maxStorageBuffersPerShaderStage (1).`,
63+
);
64+
});
65+
66+
it('warns when resources are split among layouts', () => {
67+
using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(
68+
() => {},
69+
);
70+
71+
const layout1 = tgpu.bindGroupLayout({
72+
uniform1: { uniform: d.f32 },
73+
});
74+
75+
const layout2 = tgpu.bindGroupLayout({
76+
uniform2: { uniform: d.f32 },
77+
});
78+
79+
const layout3 = tgpu.bindGroupLayout({
80+
uniform3: { uniform: d.f32 },
81+
});
82+
83+
warnIfOverflow([layout1, layout2, layout3], limits);
84+
85+
expect(consoleWarnSpy).toHaveBeenCalledWith(
86+
`Total number of uniform buffers (3) exceeds maxUniformBuffersPerShaderStage (2). Consider:
87+
1. Grouping some of the uniforms into one using 'd.struct',
88+
2. Increasing the limit when requesting a device or creating a root.`,
89+
);
90+
});
91+
});

packages/typegpu/tests/renderPipeline.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,88 @@ describe('root.withVertex(...).withFragment(...)', () => {
10571057
},
10581058
});
10591059
});
1060+
1061+
it('warns when buffer limits are exceeded', ({ root }) => {
1062+
using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(
1063+
() => {},
1064+
);
1065+
1066+
const uniform1 = root.createUniform(d.u32);
1067+
const uniform2 = root.createUniform(d.u32);
1068+
const uniform3 = root.createUniform(d.u32);
1069+
const uniform4 = root.createUniform(d.u32);
1070+
const uniform5 = root.createUniform(d.u32);
1071+
const uniform6 = root.createUniform(d.u32);
1072+
const uniform7 = root.createUniform(d.u32);
1073+
const uniform8 = root.createUniform(d.u32);
1074+
const uniform9 = root.createUniform(d.u32);
1075+
const uniform10 = root.createUniform(d.u32);
1076+
const uniform11 = root.createUniform(d.u32);
1077+
const uniform12 = root.createUniform(d.u32);
1078+
const uniform13 = root.createUniform(d.u32);
1079+
1080+
const readonly1 = root.createReadonly(d.u32);
1081+
const readonly2 = root.createReadonly(d.u32);
1082+
const readonly3 = root.createReadonly(d.u32);
1083+
const readonly4 = root.createReadonly(d.u32);
1084+
const readonly5 = root.createReadonly(d.u32);
1085+
const readonly6 = root.createReadonly(d.u32);
1086+
const readonly7 = root.createReadonly(d.u32);
1087+
const readonly8 = root.createReadonly(d.u32);
1088+
const readonly9 = root.createReadonly(d.u32);
1089+
1090+
const vertexFn = tgpu['~unstable'].vertexFn({
1091+
out: { pos: d.builtin.position },
1092+
})('');
1093+
1094+
const fragmentFn = tgpu['~unstable'].fragmentFn({ out: d.vec4f })(() => {
1095+
let a = d.u32();
1096+
a = uniform1.$;
1097+
a = uniform2.$;
1098+
a = uniform3.$;
1099+
a = uniform4.$;
1100+
a = uniform5.$;
1101+
a = uniform6.$;
1102+
a = uniform7.$;
1103+
a = uniform8.$;
1104+
a = uniform9.$;
1105+
a = uniform10.$;
1106+
a = uniform11.$;
1107+
a = uniform12.$;
1108+
a = uniform13.$;
1109+
a = readonly1.$;
1110+
a = readonly2.$;
1111+
a = readonly3.$;
1112+
a = readonly4.$;
1113+
a = readonly5.$;
1114+
a = readonly6.$;
1115+
a = readonly7.$;
1116+
a = readonly8.$;
1117+
a = readonly9.$;
1118+
1119+
return d.vec4f();
1120+
});
1121+
1122+
const pipeline = root
1123+
.withVertex(vertexFn)
1124+
.withFragment(fragmentFn, { format: 'rgba8unorm' })
1125+
.createPipeline();
1126+
1127+
pipeline.withColorAttachment({
1128+
loadOp: 'load',
1129+
storeOp: 'store',
1130+
view: {} as unknown as GPUTextureView,
1131+
}).draw(3);
1132+
1133+
expect(consoleWarnSpy).toHaveBeenCalledWith(
1134+
`Total number of uniform buffers (13) exceeds maxUniformBuffersPerShaderStage (12). Consider:
1135+
1. Grouping some of the uniforms into one using 'd.struct',
1136+
2. Increasing the limit when requesting a device or creating a root.`,
1137+
);
1138+
expect(consoleWarnSpy).toHaveBeenCalledWith(
1139+
`Total number of storage buffers (9) exceeds maxStorageBuffersPerShaderStage (8).`,
1140+
);
1141+
});
10601142
});
10611143

10621144
describe('root.createRenderPipeline', () => {

packages/typegpu/tests/utils/extendedIt.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ const mockDevice = {
137137
writeBuffer: vi.fn(),
138138
writeTexture: vi.fn(),
139139
},
140+
limits: {
141+
maxUniformBuffersPerShaderStage: 12,
142+
maxStorageBuffersPerShaderStage: 8,
143+
},
140144
destroy: vi.fn(),
141145
};
142146

0 commit comments

Comments
 (0)