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.
Forms that compile (6)
Section titled “Forms that compile (6)”| Syntax | Description | Note |
|---|---|---|
Gpu.Run(nameof(Step), next, current); await Async.Frame(); | One step per frame | |
for (int i = 0; i < stepsPerFrame; i++) { Gpu.Run(...); } | Calls per frame | If 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 steps | The 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 time | The 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 buffer | A created buffer remains usable across a suspension. |
Pacing
Section titled “Pacing”One step per frame
Section titled “One step per frame”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(); }}Calls per frame
Section titled “Calls per frame”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(); }}Splitting into steps
Section titled “Splitting into steps”Splitting a reduction into steps
Section titled “Splitting a reduction into steps”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(); }}Separating load and run
Section titled “Separating load and run”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(); }}Display
Section titled “Display”Varying the passed values over time
Section titled “Varying the passed values over time”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
Section titled “Creating a buffer”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(); }}