Step-by-step tutorial: grayscale
examples/grayscale is the first proof from the architecture plan:
gradient -> grayscale -> pixelate -> output.ppmRun everything from the repository root; every output below is real.
Step 1: the three Kernels
Section titled “Step 1: the three Kernels”kernels/gradient.ent — a generator: no inputs, so the scene must give it a domain:
/// A generator: no inputs, so the scene must give the node a domain.kernel image.gradient@1 { requires { field.write; } param depth: Float = 0.5; output result: Image; compute { let uv = coord() / (domain() - 1.0); return vec4(uv, depth, 1.0); }}kernels/grayscale.ent — luma weights to gray, amount the mix, fn luma a function of the Kernel’s own:
/// Edit the weights or `amount` while `entelechy run --watch` is running.kernel image.grayscale@1 { requires { field.read; field.write; } input source: Image; param amount: Float = 1.0; output result: Image; fn luma(c: Vec4) -> Float { return dot(c.rgb, vec3(0.2126, 0.7152, 0.0722)); } compute { let c = sample(source); let g = luma(c); return mix(c, vec4(g, g, g, c.a), amount); }}kernels/pixelate.ent — fetch reads any address (the block’s origin), not only this element:
kernel image.pixelate@1 { requires { field.read; field.write; } input source: Image; param size: Int = 8; output result: Image; compute { let block = float(max(size, 1)); let origin = floor(coord() / block) * block; return fetch(source, origin); }}Step 2: the scene
Section titled “Step 2: the scene”// gradient -> grayscale -> pixelateuse "kernels/";
node source: image.gradient@1 { domain = vec2(128, 128);}
node gray: image.grayscale@1 { source <- source.result; amount = 1.0;}
node blocks: image.pixelate@1 { source <- gray.result; size = 8;}
output result <- blocks.result;Step 3: run it
Section titled “Step 3: run it”entelechy run examples/grayscale/scene.esn --out output.pngscene loadedpublished image.gradient@1 g1 from examples/grayscale/kernels/gradient.entpublished image.grayscale@1 g2 from examples/grayscale/kernels/grayscale.entpublished image.pixelate@1 g3 from examples/grayscale/kernels/pixelate.entscene loadedevaluated in 1.241085ms: executed [gray+source, blocks], reused 0 -> output.pngEach Kernel is published at a generation (g1, g2…), which every replacement advances.

Step 4: test it
Section titled “Step 4: test it”entelechy replay examples/grayscale/tests/grayscale.replayexamples/grayscale/tests/grayscale.replay: ok (1 frames, 5 checks)The replay checks a few pixels: one gray within a block, lighter toward the far corner.
scene ../scene.esn
# The gradient in gray, in blocks of eight: a block's pixels are one# gray, the next block's another, lighter toward the far corner.frame 0.0expect 0 0 = 0.036 0.036 0.036 0.002expect 7 7 = 0.036 0.036 0.036 0.002expect 8 0 = 0.049 0.049 0.049 0.002expect 64 64 = 0.504 0.504 0.504 0.002expect 127 127 = 0.913 0.913 0.913 0.002Step 5: the plan
Section titled “Step 5: the plan”entelechy graph examples/grayscale/scene.esn// plan: 2 group(s)// gray + source: fused.gray@1 vec2(128, 128)// blocks: image.pixelate@1// gray runs apart: `blocks` reads it neither at its own element (`sample`) nor at one offset of a rowgray samples source only at its own element, so the two fuse into one Kernel; blocks reads other pixels with fetch, so gray must finish first. graph says why each producer runs apart.
Step 6: trace one pixel
Section titled “Step 6: trace one pixel”entelechy trace examples/grayscale/scene.esn --node gray --at 64 64gray (image.grayscale@1), element (64, 64), as the evaluation at time 0 runs it c: Vec4 = vec4(0.503937, 0.503937, 0.5, 1) g: Float = 0.50365275 call luma #1 -> result: Float = 0.50365275 -> result: Vec4 = vec4(0.50365275, 0.50365275, 0.50365275, 1)Step 7: change it while it runs
Section titled “Step 7: change it while it runs”entelechy run examples/grayscale/scene.esn --out output.png --watchWhile it runs, try in turn:
- Change
amount = 1.0;inscene.esnto0.5and save — the picture recomputes, half its color back. - Break
grayscale.ent(say, a weight writtennope) — you seerejected …:8:59: `nope` is not definedand the output stays. - Change
pixelate.ent’s defaultsizeto16— the scene sayssize = 8;, and a node’s value wins over the Kernel’s default.
Step 8: save an intermediate
Section titled “Step 8: save an intermediate”entelechy run examples/grayscale/scene.esn --save gray.result=gray.png --out output.pngsaved gray.result -> gray.png--save <node.port>=<file> writes any node’s output in the format its extension names (.png, .ppm, .field, .wav, .csv, .json…): how an asset is made.

Step 9: another backend, timed
Section titled “Step 9: another backend, timed”entelechy run examples/grayscale/scene.esn --backend cpu --out output.png --profile gray 0.952 ms cpu blocks 0.798 ms cpu--backend takes cpu (the reference interpreter), jit (the default) or gpu. With no adapter, --backend gpu fails at once with gpu unavailable: No suitable graphics adapter found…; the Player’s default best falls back to the JIT. See Backends.
Step 10: let an agent change it
Section titled “Step 10: let an agent change it”entelechy serve examples/grayscale/scene.esnThen, a line at a time on stdin (→ a request, ← the answer; on a copy in gp/):
→ {"id":2,"op":"publish","source":"kernel image.pixelate@1 { requires { field.read; field.write; } input source: Image; param size: Int = 16; output result: Image; compute { let b = float(max(size, 1)); return fetch(source, floor(coord() / b) * b); } }","expect":3}← {"id":2,"ok":{"generation":4,"id":"image.pixelate@1"}}→ {"id":3,"op":"compose","edits":[{"node":"blocks","member":"size","text":"size = 4;"}]}← {"id":3,"ok":{"events":[{"kind":"scene_loaded"}],"path":"gp/scene.esn","version":"bdced0a164ed8778","written":true}}publish with expect: 3 replaces only while the generation is still 3; compose with edits changed node blocks’s size in the scene file, on disk. More in Agent protocol.
What you learned
Section titled “What you learned”- Kernels bind by id; the scene only wires them;
- fusion, the plan and a trace are all observable;
- a wrong change is refused and the world runs on;
- a replay is a test;
- one program runs on three backends, and an agent can change it through the protocol.

