Skip to content

Commit 8097236

Browse files
authored
Add CALL_INDIRECT (#163)
* Base addition of CALL_INDIRECT * Fix indirect binding issues * Add documentation * Fix metadata exclusion list * Report errors for pipeline creation failures
1 parent 0289a4c commit 8097236

File tree

9 files changed

+161
-18
lines changed

9 files changed

+161
-18
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,4 @@ The files will then be available at `out/1/artifact/artifact.zip`.
6464
* Run `gh act -P ubuntu-latest=catthehacker/ubuntu:full-latest -j 'build' --artifact-server-path ./out`
6565
* This will create a file at `out/1/artifact/artifact.zip`
6666
* Extracting the zip file will provide a directory from which you can host the website with any web server
67-
* As an example, you can use vscode's live server extension to host the website by opening `index.html`
68-
67+
* As an example, you can use vscode's live server extension to host the website by opening `index.html`

engine/playground-render-canvas/src/RenderCanvas.vue

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -430,9 +430,34 @@ async function execFrame(timeMS: number, currentDisplayMode: ShaderType, playgro
430430
throw new Error("pipeline is undefined");
431431
}
432432
pass.setPipeline(pipeline.pipeline);
433+
433434
// Determine the workgroup size based on the size of the buffer or texture.
434435
let size: [number, number, number];
435-
if (command.type == "RESOURCE_BASED") {
436+
if (command.type == "INDIRECT") {
437+
// validate buffer
438+
if (!allocatedResources.has(command.bufferName)) {
439+
emit("logError", "Error when dispatching " + command.fnName + ". Indirect buffer not found: " + command.bufferName);
440+
pass.end();
441+
return false;
442+
}
443+
const indirectBuffer = allocatedResources.get(command.bufferName);
444+
if (!(indirectBuffer instanceof GPUBuffer)) {
445+
emit("logError", "Error when dispatching " + command.fnName + ". Indirect resource is not a buffer: " + command.bufferName);
446+
pass.end();
447+
return false;
448+
}
449+
450+
try {
451+
pass.dispatchWorkgroupsIndirect(indirectBuffer, command.offset);
452+
} catch (e) {
453+
emit("logError", "Failed to perform indirect dispatch for " + command.fnName + ": " + (e as Error).message);
454+
pass.end();
455+
return false;
456+
}
457+
458+
pass.end();
459+
continue; // Exit early since indirect dispatches are handled specially
460+
} else if (command.type == "RESOURCE_BASED") {
436461
if (!allocatedResources.has(command.resourceName)) {
437462
console.error("Error when dispatching " + command.fnName + ". Resource not found: " + command.resourceName);
438463
pass.end();
@@ -556,7 +581,12 @@ function safeSet<T extends GPUObjectBase>(map: Map<string, T>, key: string, valu
556581
map.set(key, value);
557582
};
558583
559-
async function processResourceCommands(resourceBindings: Bindings, resourceCommands: ResourceCommand[], uniformSize: number) {
584+
async function processResourceCommands(
585+
resourceBindings: Bindings,
586+
resourceCommands: ResourceCommand[],
587+
resourceMetadata: { [k: string]: ResourceMetadata },
588+
uniformSize: number
589+
) {
560590
let allocatedResources: Map<string, GPUObjectBase> = new Map();
561591
562592
safeSet(allocatedResources, "uniformInput", device.createBuffer({ size: uniformSize, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }));
@@ -575,7 +605,7 @@ async function processResourceCommands(resourceBindings: Bindings, resourceComma
575605
576606
const buffer = device.createBuffer({
577607
size: parsedCommand.count * elementSize,
578-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
608+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | (resourceMetadata[resourceName]?.indirect ? GPUBufferUsage.INDIRECT : 0),
579609
});
580610
581611
safeSet(allocatedResources, resourceName, buffer);
@@ -749,7 +779,7 @@ async function processResourceCommands(resourceBindings: Bindings, resourceComma
749779
// Create GPU buffer
750780
const buffer = device.createBuffer({
751781
size: bufferSize,
752-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
782+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | (resourceMetadata[resourceName]?.indirect ? GPUBufferUsage.INDIRECT : 0),
753783
});
754784
755785
// Upload data to GPU buffer (only the aligned portion)
@@ -773,7 +803,7 @@ async function processResourceCommands(resourceBindings: Bindings, resourceComma
773803
774804
const buffer = device.createBuffer({
775805
size: parsedCommand.count * elementSize,
776-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
806+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | (resourceMetadata[resourceName]?.indirect ? GPUBufferUsage.INDIRECT : 0),
777807
});
778808
779809
const floatArray = new Float32Array(parsedCommand.count);
@@ -858,6 +888,31 @@ async function processResourceCommands(resourceBindings: Bindings, resourceComma
858888
return allocatedResources;
859889
}
860890
891+
type ResourceMetadata = {
892+
indirect?: boolean,
893+
excludeBinding: string[],
894+
}
895+
896+
function getResourceMetadata(compiledCode: CompiledPlayground): { [k: string]: ResourceMetadata } {
897+
const metadata = {};
898+
899+
for (const resourceName of Object.keys(compiledCode.shader.layout)) {
900+
metadata[resourceName] = {
901+
indirect: false,
902+
excludeBinding: [],
903+
};
904+
}
905+
906+
for (const callCommand of compiledCode.callCommands) {
907+
if (callCommand.type === 'INDIRECT') {
908+
metadata[callCommand.bufferName].indirect = true;
909+
metadata[callCommand.bufferName].excludeBinding.push(callCommand.fnName);
910+
}
911+
}
912+
913+
return metadata;
914+
}
915+
861916
function onRun(runCompiledCode: CompiledPlayground) {
862917
if (!device) return;
863918
@@ -886,17 +941,41 @@ function onRun(runCompiledCode: CompiledPlayground) {
886941
887942
const module = device.createShaderModule({ code: compiledCode.shader.code });
888943
944+
const resourceMetadata = getResourceMetadata(compiledCode);
945+
889946
for (const callCommand of compiledCode.callCommands) {
890947
const entryPoint = callCommand.fnName;
891948
const pipeline = new ComputePipeline(device);
949+
950+
const entryPointReflection = compiledCode.shader.reflection.entryPoints.find(e => e.name === entryPoint);
951+
if (!entryPointReflection) {
952+
throw new Error(`Entry point ${entryPoint} not found in reflection data`);
953+
}
954+
955+
const pipelineBindings: Bindings = {};
956+
for (const param in compiledCode.shader.layout) {
957+
if (resourceMetadata[param]?.excludeBinding.includes(entryPoint)) {
958+
continue;
959+
}
960+
pipelineBindings[param] = compiledCode.shader.layout[param];
961+
}
962+
892963
// create a pipeline resource 'signature' based on the bindings found in the program.
893-
pipeline.createPipelineLayout(compiledCode.shader.layout);
894-
pipeline.createPipeline(module, entryPoint);
964+
pipeline.createPipelineLayout(pipelineBindings);
965+
let pipelineCreationResult = await pipeline.createPipeline(module, entryPoint);
966+
if (pipelineCreationResult.succ == false) {
967+
throw new Error(`Failed to create pipeline for entry point "${entryPoint}":\n${pipelineCreationResult.message}`);
968+
}
895969
pipeline.setThreadGroupSize(compiledCode.shader.threadGroupSizes[entryPoint]);
896970
computePipelines.push(pipeline);
897971
}
898972
899-
allocatedResources = await processResourceCommands(compiledCode.shader.layout, compiledCode.resourceCommands, compiledCode.uniformSize);
973+
allocatedResources = await processResourceCommands(
974+
compiledCode.shader.layout,
975+
compiledCode.resourceCommands,
976+
resourceMetadata,
977+
compiledCode.uniformSize
978+
);
900979
901980
if (!passThroughPipeline) {
902981
passThroughPipeline = new GraphicsPipeline(device);

engine/playground-render-canvas/src/compute.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { Bindings } from "slang-playground-shared";
1+
import type { Bindings, Result } from "slang-playground-shared";
22

33
export class ComputePipeline {
44
pipeline: GPUComputePipeline | undefined;
55
pipelineLayout: GPUPipelineLayout | "auto" | undefined;
66

7-
device;
7+
device: GPUDevice;
88
bindGroup: GPUBindGroup | undefined;
99

1010
// thread group size (array of 3 integers)
@@ -39,16 +39,31 @@ export class ComputePipeline {
3939
this.pipelineLayout = layout;
4040
}
4141

42-
createPipeline(shaderModule: GPUShaderModule, entryPoint: string) {
42+
async createPipeline(shaderModule: GPUShaderModule, entryPoint: string): Promise<Result<undefined>> {
4343
if (this.pipelineLayout == undefined)
4444
throw new Error("Cannot create pipeline without layout");
45+
46+
this.device.pushErrorScope("validation");
4547
const pipeline = this.device.createComputePipeline({
4648
label: 'compute pipeline',
4749
layout: this.pipelineLayout,
4850
compute: { module: shaderModule, entryPoint },
4951
});
5052

53+
const error = await this.device.popErrorScope();
54+
if (error) {
55+
return {
56+
succ: false,
57+
message: error.message,
58+
}
59+
}
60+
5161
this.pipeline = pipeline;
62+
63+
return {
64+
succ: true,
65+
result: undefined,
66+
}
5267
}
5368

5469
createBindGroup(allocatedResources: Map<string, GPUObjectBase>) {

engine/shared/src/playgroundInterface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ export type CallCommand = {
211211
fnName: string,
212212
size: number[],
213213
callOnce?: boolean,
214+
} | {
215+
type: "INDIRECT",
216+
fnName: string,
217+
bufferName: string,
218+
offset: number,
219+
callOnce?: boolean,
214220
};
215221

216222
export type PlaygroundRun = {

engine/slang-compilation-engine/media/playgroundDocumentation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ Dispatch a compute pass using the resource size to determine the work-group size
8585
Dispatch a compute pass with the given grid of threads.
8686
The number of work-groups will be determined by dividing by the number of threads per work-group and rounding up.
8787

88+
### `[playground::CALL_INDIRECT("BUFFER-NAME", 0)]`
89+
90+
Dispatch a compute pass with an indirect command buffer and an offset in bytes.
91+
8892
### `[playground::CALL::ONCE]`
8993

9094
Only dispatch the compute pass once at the start of rendering. Should be used in addition to another CALL command.

engine/slang-compilation-engine/src/playgroundCompiler.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,21 @@ function parseCallCommands(reflection: ReflectionJSON): Result<CallCommand[]> {
384384
fnName,
385385
size: attribute.arguments as number[]
386386
};
387+
} else if (attribute.name === "playground_CALL_INDIRECT") {
388+
if (callCommand != null) {
389+
return {
390+
succ: false,
391+
message: `Multiple CALL commands found for ${fnName}`,
392+
};
393+
}
394+
const bufferName = attribute.arguments[0] as string;
395+
const offset = attribute.arguments[1] as number;
396+
callCommand = {
397+
type: "INDIRECT",
398+
fnName,
399+
bufferName,
400+
offset,
401+
};
387402
} else if (attribute.name === "playground_CALL_ONCE") {
388403
if (callOnce) {
389404
return {

engine/slang-compilation-engine/src/slang/playground.slang

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,15 @@ public struct playground_CALL_SIZE_OFAttribute
195195
string resourceName;
196196
};
197197

198+
// Dispatch a compute pass by indirect call: use a buffer name and an integer offset
199+
// (the buffer is expected to contain entrypoint indices or dispatch parameters).
200+
[__AttributeUsage(_AttributeTargets.Function)]
201+
public struct playground_CALL_INDIRECTAttribute
202+
{
203+
string bufferName;
204+
int offset;
205+
};
206+
198207
// Only dispatch the compute pass once at the start of rendering.
199208
[__AttributeUsage(_AttributeTargets.Function)]
200209
public struct playground_CALL_ONCEAttribute

public/demos/painting.slang

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import playground;
22

3-
const static int MAX_BRUSH_SIZE = 16;
3+
static const int MAX_BRUSH_SIZE = 64;
4+
static const uint THREAD_COUNT = 8;
45

56
[playground::BLACK_SCREEN(1.0, 1.0)]
67
RWTexture2D<float> tex_red;
@@ -9,7 +10,10 @@ RWTexture2D<float> tex_green;
910
[playground::BLACK_SCREEN(1.0, 1.0)]
1011
RWTexture2D<float> tex_blue;
1112

12-
[playground::SLIDER(10.0, 4.0, 16.0)]
13+
[playground::ZEROS(8)]
14+
RWStructuredBuffer<uint> indirectBuffer;
15+
16+
[playground::SLIDER(10.0, 4.0, 64.0)]
1317
uniform float brush_size;
1418
[playground::COLOR_PICK(1.0, 0.0, 1.0)]
1519
uniform float3 color;
@@ -18,14 +22,24 @@ uniform float3 color;
1822
uniform float4 mousePosition;
1923

2024
[shader("compute")]
21-
[numthreads(8, 8, 1)]
22-
[playground::CALL(MAX_BRUSH_SIZE, MAX_BRUSH_SIZE, 1)]
25+
[numthreads(1, 1, 1)]
26+
[playground::CALL(1, 1, 1)]
27+
void update(uint2 dispatchThreadId: SV_DispatchThreadID)
28+
{
29+
indirectBuffer[0] = uint(2.0 * brush_size + THREAD_COUNT + 1.0) / THREAD_COUNT;
30+
indirectBuffer[1] = uint(2.0 * brush_size + THREAD_COUNT + 1.0) / THREAD_COUNT;
31+
indirectBuffer[2] = 1;
32+
}
33+
34+
[shader("compute")]
35+
[numthreads(THREAD_COUNT, THREAD_COUNT, 1)]
36+
[playground::CALL_INDIRECT("indirectBuffer", 0)]
2337
void draw(uint2 dispatchThreadId: SV_DispatchThreadID)
2438
{
2539
if (mousePosition.z >= 0)
2640
return;
2741

28-
let offset = float2(dispatchThreadId.xy) - float(MAX_BRUSH_SIZE) / 2;
42+
let offset = float2(dispatchThreadId.xy) - brush_size;
2943
if (length(offset) > brush_size / 2)
3044
return;
3145

src/components/Help.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ defineExpose({
8686
<h4 class="doc-header"><code>[playground::CALL(512, 512, 1)]</code></h4>
8787
Dispatch a compute pass with the given grid of threads.
8888
The number of work-groups will be determined by dividing by the number of threads per work-group and rounding up.
89+
<h4 class="doc-header"><code>[playground::CALL_INDIRECT("BUFFER-NAME", 0)]</code></h4>
90+
Dispatch a compute pass with an indirect command buffer and an offset in bytes.
8991
<h4 class="doc-header"><code>[playground::CALL::ONCE]</code></h4>
9092
Only dispatch the compute pass once at the start of rendering.
9193
<h4>Playground functions</h4>

0 commit comments

Comments
 (0)