Yeho / GPU shader lab
A little code.
A lot of color.
Write Yeho. Let the GPU paint. See exactly what each line does.
GPU compute shaders
Yes—these are compute shaders. A [gpu] compute function processes buffers on the GPU. Yeho generates Metal shader code on the Mac route. You author Yeho, dispatch the work, wait, and use the result.
Dolphin surface functions
Surface(Vector3) → dolphinSurface describes color, roughness, metallic, and emission. Atlas and Dolphin’s current sphere preview evaluate that contract on the CPU. GPU material export and arbitrary Studio binding are separate work.
[gpu] compute / native Metal
Paint your first pixels
Give each GPU invocation one pixel. Turn its coordinates into color.
A compute shader is a small program that runs across data on the GPU. Here the data is an image. Yeho translates the [gpu] compute body to the selected provider; on Mac, that provider is Metal.
Link to this example ↗
Find the pixel
compute.index is a one-dimensional index. Integer division finds its row; subtraction finds its column. Always guard the buffer length.
Make coordinates useful
u and v run from 0 to 1. We use them as blend weights for red and green. Blue fades in the opposite horizontal direction.
Finish on the CPU
Wait for the GPU, check failure, then write shader.ppm. The complete program saves a real image; the website shows a PNG converted from those exact Metal pixels.
[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)
float r = 0.08 + u * 0.8
float g = 0.08 + v * 0.8
float b = 0.3 + (1.0 - u) * 0.6
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
}
// 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) }
// 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 = pixels[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")
Download all four complete projects ↓API entry ↗Execution evidence ↗
Before you run it
Extract gpu-shaders beside your yeho source checkout. Build a current Mac developer compiler using the source setup lesson. Save both project files. The program writes shader.ppm in its current directory; use a PPM-capable image viewer to inspect it.
[gpu] requires a GPU provider. For a separate CPU comparison, replace it with [cpu] and compile with --backend cpu-oracle. The kernel body is otherwise unchanged. Windows Vulkan is a separate qualification route.
What can go inside the GPU kernel?
The current provider accepts int32 and float32 buffers, int32/float32/bool scalar inputs, arithmetic, conditions, buffer access, bounded literal loops, and its admitted typed Math operations. Keep console messages, file I/O, allocations, and arbitrary helper calls in the host code. Guard the first buffer before using its index, and keep independently written buffers distinct.
Your turn
Change one thing. See what happens.
Swap u and v in the red and green expressions. Then set b to 0.0. Rebuild and compare shader.ppm.
Take the same idea further.
Pixels, particles, and game simulation all use the same cycle: prepare data, dispatch independent work, wait, then consume the result.
