Skip to content
View source

Step-by-step tutorial: grayscale

examples/grayscale is the first proof from the architecture plan:

gradient -> grayscale -> pixelate -> output.ppm

Run everything from the repository root; every output below is real.

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);
}
}
// gradient -> grayscale -> pixelate
use "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;
终端窗口
entelechy run examples/grayscale/scene.esn --out output.png
scene loaded
published image.gradient@1 g1 from examples/grayscale/kernels/gradient.ent
published image.grayscale@1 g2 from examples/grayscale/kernels/grayscale.ent
published image.pixelate@1 g3 from examples/grayscale/kernels/pixelate.ent
scene loaded
evaluated in 1.241085ms: executed [gray+source, blocks], reused 0 -> output.png

Each Kernel is published at a generation (g1, g2…), which every replacement advances.

The grayscale output, 3× enlarged

终端窗口
entelechy replay examples/grayscale/tests/grayscale.replay
examples/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.0
expect 0 0 = 0.036 0.036 0.036 0.002
expect 7 7 = 0.036 0.036 0.036 0.002
expect 8 0 = 0.049 0.049 0.049 0.002
expect 64 64 = 0.504 0.504 0.504 0.002
expect 127 127 = 0.913 0.913 0.913 0.002
终端窗口
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 row

gray 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.

终端窗口
entelechy trace examples/grayscale/scene.esn --node gray --at 64 64
gray (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)
终端窗口
entelechy run examples/grayscale/scene.esn --out output.png --watch

While it runs, try in turn:

  1. Change amount = 1.0; in scene.esn to 0.5 and save — the picture recomputes, half its color back.
  2. Break grayscale.ent (say, a weight written nope) — you see rejected …:8:59: `nope` is not defined and the output stays.
  3. Change pixelate.ent’s default size to 16 — the scene says size = 8;, and a node’s value wins over the Kernel’s default.
终端窗口
entelechy run examples/grayscale/scene.esn --save gray.result=gray.png --out output.png
saved 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.

The gray node’s output

终端窗口
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.

终端窗口
entelechy serve examples/grayscale/scene.esn

Then, 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.

  • 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.

Next, browse the examples or start the Manual.