Skip to content

GPU kernels and await

Calls from Running a GPU kernel can be written inside work that spans frames. Where await Async.Frame() is placed determines how many calls are made per frame.

SyntaxDescriptionNote
Gpu.Run(nameof(Step), next, current); await Async.Frame();One step per frame
for (int i = 0; i < stepsPerFrame; i++) { Gpu.Run(...); }Calls per frameIf the count is held in a field, a value changed during the wait takes effect on the next frame.
Gpu.Reduce(nameof(Brighter), middle, full); await Async.Frame();Splitting a reduction into stepsThe result of the first step stays in middle, and the second step on the next frame reads it.
Gpu.Load(current, source); await Async.Frame();Separating load and run
Gpu.Show(nameof(Glow), surface, heat, gain);Varying the passed values over timeThe display uses the values as they were when passed to Gpu.Show, so call it again each time a value changes.
board = Gpu.Buffer(64, 64); await Async.Frame();Creating a bufferA created buffer remains usable across a suspension.

Calling Gpu.Run once per loop iteration and then suspending advances one step per frame.

using UnityEngine;
using Tsukimi;
public class ParGpuStepPerFrame : TsukimiBehaviour
{
public Renderer display;
private GpuBuffer2D current;
private GpuBuffer2D next;
[Kernel]
static Color4 Step(KernelId id, GpuBuffer2D prev)
{
Color4 c = prev[id];
return new Color4(c.R * 0.99f, c.G, c.B, 1f);
}
private async FrameTask Simulate()
{
for (int i = 0; i < 600; i++)
{
Gpu.Run(nameof(Step), next, current);
Gpu.Swap(ref current, ref next);
Gpu.Show(current, display);
await Async.Frame();
}
}
void Start()
{
current = Gpu.Buffer(128, 128);
next = Gpu.Buffer(128, 128);
Simulate();
}
}

Calling Gpu.Run for each iteration of the inner loop and waiting one frame in the outer loop sets the number of calls per frame.

using UnityEngine;
using Tsukimi;
public class ParGpuBudget : TsukimiBehaviour
{
public int stepsPerFrame;
private GpuBuffer2D current;
private GpuBuffer2D next;
[Kernel]
static Color4 Relax(KernelId id, GpuBuffer2D prev)
{
Color4 a = prev[id.Offset(-1, 0)];
Color4 b = prev[id.Offset(1, 0)];
return (a + b) * 0.5f;
}
private async FrameTask Settle()
{
int done = 0;
while (done < 2000)
{
for (int i = 0; i < stepsPerFrame; i++)
{
Gpu.Run(nameof(Relax), next, current);
Gpu.Swap(ref current, ref next);
done = done + 1;
}
await Async.Frame();
}
}
void Start()
{
stepsPerFrame = 4;
current = Gpu.Buffer(64, 64);
next = Gpu.Buffer(64, 64);
Settle();
}
}

Placing await between the steps of a fold splits the two steps of 64 → 8 → 1 across two frames.

using UnityEngine;
using Tsukimi;
public class ParGpuReduceStages : TsukimiBehaviour
{
private GpuBuffer2D full;
private GpuBuffer2D middle;
private GpuBuffer2D one;
[Reduce]
static Color4 Brighter(Color4 a, Color4 b)
{
if (a.R != b.R) return a.R > b.R ? a : b;
if (a.G != b.G) return a.G > b.G ? a : b;
if (a.B != b.B) return a.B > b.B ? a : b;
return a.A > b.A ? a : b;
}
private async FrameTask Fold()
{
Gpu.Reduce(nameof(Brighter), middle, full);
await Async.Frame();
Gpu.Reduce(nameof(Brighter), one, middle);
}
void Start()
{
full = Gpu.Buffer(64, 64);
middle = Gpu.Buffer(8, 8);
one = Gpu.Buffer(1, 1);
Fold();
}
}

The frame that loads an image with Gpu.Load can be separated from the frame in which Gpu.Run reads it.

using UnityEngine;
using Tsukimi;
public class ParGpuLoadThenRun : TsukimiBehaviour
{
public Texture source;
private GpuBuffer2D current;
private GpuBuffer2D next;
[Kernel]
static Color4 Blur(KernelId id, GpuBuffer2D prev)
{
Color4 a = prev[id.Offset(-1, 0)];
Color4 b = prev[id.Offset(1, 0)];
return (a + b) * 0.5f;
}
private async FrameTask Prepare()
{
Gpu.Load(current, source);
await Async.Frame();
Gpu.Run(nameof(Blur), next, current);
Gpu.Swap(ref current, ref next);
}
void Start()
{
current = Gpu.Buffer(256, 256);
next = Gpu.Buffer(256, 256);
Prepare();
}
}

Changing the value passed to a surface shader (experimental) each round and calling Gpu.Show again makes the appearance change over time.

using UnityEngine;
using Tsukimi;
public class ParGpuSurfaceOverTime : TsukimiBehaviour
{
public Renderer surface;
public float gain;
private GpuBuffer2D heat;
[Surface]
static Color4 Glow(SurfaceId id, GpuBuffer2D buf, float gain)
{
float t = buf.Sample(id.UV).R * gain;
return new Color4(t, t * 0.5f, 0f, 1f);
}
private async FrameTask FadeIn()
{
for (int i = 0; i < 120; i++)
{
gain = i / 120f;
Gpu.Show(nameof(Glow), surface, heat, gain);
await Async.Frame();
}
}
void Start()
{
heat = Gpu.Buffer(64, 64);
FadeIn();
}
}

Creating a buffer with Gpu.Buffer can also be written inside a frame-spanning method.

using UnityEngine;
using Tsukimi;
public class ParGpuBufferInAsync : TsukimiBehaviour
{
public Renderer display;
private GpuBuffer2D board;
[Kernel]
static Color4 Seed(KernelId id)
{
return new Color4(id.X * 0.01f, id.Y * 0.01f, 0f, 1f);
}
private async FrameTask Boot()
{
board = Gpu.Buffer(64, 64);
await Async.Frame();
Gpu.Run(nameof(Seed), board);
Gpu.Show(board, display);
}
void Start()
{
Boot();
}
}