examples.gpu-shaders · shader ·
Connect two GPU passes
Paint a checker, then soften it with a small neighborhood filter.
An image effect often reads neighboring pixels. Keep input and output in separate buffers, and finish the painting pass before the blur reads it.
[gpu] compute Paint(buffer of int pixels, int width, int height, float time)Status: native-metal-example
04 / Connect two GPU passes
Paint a checker, then soften it with a small neighborhood filter.
gpu · shader · image · type checked
[gpu]
compute Paint(buffer of int pixels, int width, int height, float time)
{
int index = compute.index
if index >= pixels.count { return }
int row = index / width
int column = index - row * width
float u = Math.IntToFloat(column) / Math.IntToFloat(width - 1)
float v = Math.IntToFloat(row) / Math.IntToFloat(height - 1)
int cellX = column / 16
int cellY = row / 16
int sum = cellX + cellY
float r = 0.02
float g = 0.04
float b = 0.08
if sum / 2 * 2 == sum
{
r = 0.08
g = 0.9
b = 0.7
}
int red = Math.RoundToInt(Math.ClampFloat(r, 0.0, 1.0) * 255.0)
int green = Math.RoundToInt(Math.ClampFloat(g, 0.0, 1.0) * 255.0)
int blue = Math.RoundToInt(Math.ClampFloat(b, 0.0, 1.0) * 255.0)
pixels[index] = red * 65536 + green * 256 + blue
}
[gpu]
compute Blur(buffer of int input, buffer of int output, int width, int height)
{
int index = compute.index
if index >= input.count { return }
if index >= output.count { return }
int row = index / width
int column = index - row * width
if row == 0 or row + 1 >= height or column == 0 or column + 1 >= width
{
output[index] = input[index]
return
}
int red = 0
int green = 0
int blue = 0
for yOffset from 0 < 3
{
for xOffset from 0 < 3
{
int pixel = input[(row + yOffset - 1) * width + column + xOffset - 1]
int r = pixel / 65536
int g = (pixel - r * 65536) / 256
int b = pixel - r * 65536 - g * 256
red = red + r
green = green + g
blue = blue + b
}
}
output[index] = (red / 9) * 65536 + (green / 9) * 256 + blue / 9
}
// CPU: allocate an image and submit one GPU invocation per pixel.
int width = 128
int height = 128
buffer of int pixels
pixels.Resize(width * height)
computeTask job = dispatch Paint(pixels, width, height, 0.0)
wait job
if job.failed { Console.Error(job.error.message) Process.Exit(1) }
buffer of int filtered
filtered.Resize(width * height)
computeTask filter = dispatch Blur(pixels, filtered, width, height)
wait filter
if filter.failed { Console.Error(filter.error.message) Process.Exit(1) }
// CPU: save the completed RGB pixels as a portable PPM image.
text image = "P3\n" + Text.From(width) + " " + Text.From(height) + "\n255\n"
for row from 0 < height
{
text line = ""
for column from 0 < width
{
int pixel = filtered[row * width + column]
int red = pixel / 65536
int green = (pixel - red * 65536) / 256
int blue = pixel - red * 65536 - green * 256
line = line + Text.From(red) + " " + Text.From(green) + " " + Text.From(blue) + " "
}
image = image + line + "\n"
}
File.WriteText("shader.ppm", image)
Console.Log("Wrote shader.ppm")
project.mech
manifestVersion = "2"
package = "learning.shaders.gpu_blur"
version = "0.1.0"
languageEdition = "yeho-core-2026.1"
[app]
kind = "headless"
Expected output
Wrote shader.ppm
Notes and source
Replace the Paint checker’s cell size with 8. Compare the sharp and blurred outputs. Keep input and output as distinct storage.
The dedicated shader verifier executes this program on Metal and compares every output channel with a separate CPU reference. Evidence: https://demonhunterlabs.com/learning/gpu-shaders/verification.json
The program writes shader.ppm in its current directory. Native image generation is separate from presenting frames in a game window.
demonhunterlabs/app/lib/yeho-gpu-shaders.ts