Field note 01116 Aug 2026M11 complete

The kernel didn’t draw this.

A Lux process encoded every GPU command, owned the framebuffer, and bounced the white band across the screen. Yggdrasil moved opaque buffers—and stayed out of the way.

virtio-gpu / scanout 0 captured
The final Lux animation frame: a white horizontal band on a dark blue 320 by 200 framebuffer y 118–157
320 × 200frame 60QEMU P6 screendumpactual output
100ktail-recursive iterations
129Lux tests green
4 minheadless animation soak
02screendumps asserted
00 / Plan → proof

The plan said “could.”
The commits say “does.”

Two repositories moved as one vertical slice: runtime, language, device surface, driver, and acceptance harness.

Yggdrasild627786

Complete M11 GC, tail calls, and Lux GPU driver

Fifteen files reshape the bytecode, verifier, interpreter, JIT, process heap, ports, virtio transport, self-tests, and QEMU harness.

+1,747−15615 files
Lux4c818c9

Finish M11 support and short-circuit booleans

Tail-aware codegen, lazy boolean lowering, kernel intrinsics, three executable proofs, and all 129 Lux tests passing.

+549−697 files
Read before / afterRevisit the original M11 implementation plan
01 / Runtime

First, make the loop immortal.

The GPU demo is interesting because it draws. It is convincing because it keeps drawing. That demanded two runtime properties Yggdrasil did not have the morning M11 began: a constant native stack and memory that comes back.

JIT / interpreterrun
tag 7sentinel
stashed argsexact roots
pressure?Cheney GC

A tail call is now an engine handoff.

TAIL_CALL and TAIL_CALL_EXT do not recurse in native code. The running engine stashes the next target and arguments, then returns a reserved tag-7 sentinel. modload::invoke catches it, resolves the current module version, and dispatches again from a loop. Hot-code migration still happens at the same boundary; the C stack simply stops growing.

Between hops

Segmented, non-moving growth

When a bump span fills, the owning process adds another—doubling toward its quota. Existing pointers stay valid for interpreter and JIT code.

At outer hops

Moving, sharing-safe compaction

Stashed arguments are the complete live set. Cheney evacuation follows tuples, lists, maps, and binaries; forwarding words preserve shared structure.

Acceptance [ok] lux loop: 100k tail-recursive iterations in bounded memory

The loop allocates a list and a map every turn with a heap cap small enough to kill the old runtime in roughly 200 iterations.

02 / Devices

The port became the whole driver surface.

M11 does not add a friendly GPU API. It adds the small set of primitives from which a driver can be written without asking the kernel to understand its protocol.

PORT_SUBMIT2all-register SQE submissionport · op · arg0 · arg1 · tag
BUF_NEW / WRITE / READmutable fixed-size kernel blobsoff-heap · pinned · GC-immune
receive after Ntimer-wheel sleepframe pacing without a busy loop
{port_reply, …}completion as a messageordinary Lux receive semantics
Luxencode command bytes
Portmove opaque buffer
VirtQueuenotify device
Mailboxreturn response

The single protocol-shaped assist is OP_CTRL_ATTACH. Lux creates the RESOURCE_ATTACH_BACKING prefix; the kernel appends one physical-address record for the pinned buffer because verified bytecode must never see guest physical addresses. Everything else remains opaque.

AcceptanceLUX-PORT-OK

A Lux process opens the serial port and emits the marker byte by byte through PORT_SUBMIT2. No new serial driver code was needed.

03 / Graphic output

Then the process painted the screen.

gpu_demo.lux is 212 lines of language-level driver: little-endian encoding, response parsing, resource setup, framebuffer construction, presentation, and a paced animation loop.

build/gpu.ppmcaptured after lux_gpu_anim_ok
The verified final frame from the Lux virtio-gpu animation
0118157199
The final animation frame, losslessly converted from QEMU’s P6 PPM.

The harness expects white at (160, 138) and dark blue at (160, 60) and (160, 185). This image is that artifact—not a reconstruction.

Canvas320 × 200B8G8R8X8 resource
Band40 rowswhite, written in place
Background32 · 32 · 96RGB after screendump
Pacing16 msreceive after

The protocol lives entirely above the kernel.

GET_DISPLAY_INFORESOURCE_CREATE_2DATTACH_BACKINGSET_SCANOUTTRANSFERFLUSH

Lux encodes each struct as a byte list, converts it to a binary once, submits it through the raw GPU port, and parses the response. The render loop mutates only the changed 40 rows with buf_write, transfers the resource, flushes, sleeps, and tail-calls the next frame.

let _draw = ygg::buf_write(backing, y * 1280, band)
let _shown = present(port, 2)
receive {
    after 16 => anim_step(port, backing, band, erase, y, dy, n)
}
See it movecargo xtask watch

Boots the Lux GPU driver under QEMU with a visible GTK display.

04 / Compiler detail

The tiny fix that kept the big demo alive.

The last Lux commit is not named after graphics. It is named after short-circuit booleans—and that is not incidental.

Strictok && anim_go(…)

Evaluate both sides like ordinary calls; guarded recursion can stay nested and accumulate frame garbage.

Lazycase ok { true → anim_go(…) }

Lower &&/|| to andalso/orelse semantics so the recursive branch remains in tail position.

A collector with exact roots is only as good as the compiler’s ability to reach those root boundaries.

With lazy lowering, guarded render loops return to the outer trampoline. That gives the collector the tiny root set it was designed around instead of leaving per-frame garbage trapped inside a strict caller.

05 / Verification

The pixels are one line in a larger proof.

Runtime100k

Tail-recursive Lux iterations complete inside a deliberately bounded heap.

constant stack + compaction
Language129

Every Lux test passes, including trampoline-aware backend coverage.

cargo test
Static scene5 px

Red, green, white rectangle, green margin, and blue are probed after screendump.

headless assertion
Animation3 px

Band center, background above, and background below are checked at frame 60.

headless assertion
Endurance4 min

The headless animation runs without a quota death or kernel panic.

soak
System4 CPU

The complete two-boot suite still covers TCP echo, pcap, disk persistence, JIT, and SMP.

cargo xtask test
acceptance transcript
[ok] lux loop: 100k tail-recursive iterations in bounded memory
LUX-PORT-OK
[ok] lux port: serial written via PORT_SUBMIT2
[ok] lux gpu: scene rendered via virtio-gpu
[ok] lux gpu: animation played via buf_write
[selftest] all passed
06 / Boundaries

What M11 deliberately does not pretend.

01

GC is outer-hop only.

Nested calls and native drivers can retain invisible terms. Anywhere-GC needs stack maps; until then those paths grow within quota instead of moving unsafely.

02

GPU completion is synchronous.

The port pump waits on the control queue. Fence- and interrupt-driven completion is the next performance step, not a new architecture.

03

3D still needs a GL device.

Virgl commands already fit the same opaque OP_CTRL surface, but QEMU needs virtio-gpu-gl and a GL-capable display to accept contexts.

Next unlock

The kernel API for 3D is already finished.

CTX_CREATE, capset queries, resource creation, and SUBMIT_3D are just more command buffers a Lux process can encode and send through the port it already owns.