Wayland F01 — first pixel & init cost
Findings from ./examples/f01-first-pixel/app.d, the F01 spec's Wayland demo. It formalizes the measurement the scaffold made possible: one step name=… instrumentation event per init API call (round-trips marked rt=1), a concept n=… name=… event at the first touch of each protocol object type, a compile-time-computed LOC figure, and a summary line + stdout checklist. The demo presents exactly one confirmed frame, holds 10 more frame callbacks to prove the loop is live, and exits 0. Verified Tier A: weston --backend=headless (weston 15.0, libwayland 1.24), exit 0.
Last reviewed: June 10, 2026
| Measurement | Value |
|---|---|
| Concepts to first pixel | 11 protocol object types (see the scaffold's first-touch list) |
| LOC | 558 total / 413 code (computed at compile time from import("app.d"); instrument.d + c.c excluded per F01 §5) |
| Init steps logged | 21 step events from init_start to first_pixel_presented |
Round-trips (rt=1) | 2: wl_display_roundtrip (registry) + the wl_display_dispatch that waits for the first configure |
init_start → first_commit | ≈ 2.2 ms, of which ≈ 1.3 ms is painting the first 640×480 gradient |
init_start → first_pixel_presented | 11.1–18.6 ms across 7 runs — dominated by waiting for the compositor's next 60 Hz frame tick |
What the spec asked, and what Wayland can actually confirm
F01 requires a demo to log every init step, count round-trips, and stop the clock at a confirmed first pixel. On Wayland the confirmation is well-defined — better than on X11, which has nothing comparable without the Present extension: the contract is the first done event on a wl_surface.frame callback that was requested alongside the first buffer commit after xdg_surface.ack_configure. Per the core protocol, the callback fires "when it is a good time to start drawing a new frame" — i.e. the committed frame has been taken up by the compositor's repaint cycle. There is no per-pixel scanout proof (that would be the separate wp_presentation protocol, out of scope here), but it is an explicit, ordered, server-driven signal that the commit was consumed.
The init step sequence
The full instrument stream of a Tier-A run (microseconds since init_start; format per F01 § Instrumentation). concept lines interleave at first touch; rt=1 marks the two compositor waits:
0 f01-wayland init_start
45 f01-wayland concept n=1 name=wl_display
68 f01-wayland step name=wl_display_connect
94 f01-wayland concept n=2 name=wl_registry
110 f01-wayland step name=wl_display_get_registry
125 f01-wayland concept n=3 name=wl_callback
204 f01-wayland concept n=4 name=wl_compositor
220 f01-wayland step name=wl_registry_bind iface=wl_compositor version=5
307 f01-wayland concept n=5 name=wl_shm
323 f01-wayland step name=wl_registry_bind iface=wl_shm version=2
369 f01-wayland concept n=6 name=xdg_wm_base
385 f01-wayland step name=wl_registry_bind iface=xdg_wm_base version=5
410 f01-wayland step name=wl_display_roundtrip rt=1
430 f01-wayland concept n=7 name=wl_surface
446 f01-wayland step name=wl_compositor_create_surface
467 f01-wayland concept n=8 name=xdg_surface
482 f01-wayland step name=xdg_wm_base_get_xdg_surface
503 f01-wayland concept n=9 name=xdg_toplevel
518 f01-wayland step name=xdg_surface_get_toplevel
538 f01-wayland step name=xdg_toplevel_set_title
559 f01-wayland step name=xdg_toplevel_set_app_id
575 f01-wayland window_created
587 f01-wayland step name=wl_surface_commit(no-buffer)
655 f01-wayland xdg_toplevel_configure size=0x0
676 f01-wayland configure serial=20 size=640x480
696 f01-wayland step name=xdg_surface_ack_configure
711 f01-wayland first_configure
732 f01-wayland step name=memfd_create+mmap
754 f01-wayland concept n=10 name=wl_shm_pool
787 f01-wayland step name=wl_shm_create_pool
808 f01-wayland concept n=11 name=wl_buffer
827 f01-wayland step name=wl_shm_pool_create_buffer
848 f01-wayland buffer_alloc size=640x480 bytes=1228800
2170 f01-wayland step name=wl_surface_attach
2192 f01-wayland step name=wl_surface_damage_buffer
2213 f01-wayland step name=wl_surface_frame
2234 f01-wayland step name=wl_surface_commit(buffer)
2249 f01-wayland first_commit size=640x480
2265 f01-wayland step name=wl_display_dispatch(first_configure) rt=1
18535 f01-wayland frame_callback t=477151625
18561 f01-wayland first_pixel_presented
18573 f01-wayland summary concepts=11 loc=558 loc_code=413 steps=21 roundtrips=2 first_pixel_us=18561…and the stdout checklist the demo prints after clean teardown:
F01 checklist (wayland):
[x] one frame presented, confirmed by wl_surface.frame callback
[x] init steps logged: 21 (round-trips rt=1: 2)
[x] concepts to first pixel: 11
[x] loc: 558 total, 413 code (instrument.d and c.c excluded)
[x] init_start -> first_pixel_presented: 18561 us
[x] held 10 extra frames, clean teardown, exit 0
ok: presented 11 frames in 11 commits, final size 640x480Reading the deltas
- Connect →
window_createdis ~0.6 ms total. Every request in that span is a local buffered write; onlywl_display_roundtripactually waits (~300 µs on a local socket, including dispatching the registry's global burst — thewl_registry_bindsteps at 204–385 all run inside it). - The configure answer to the no-buffer commit costs ~90 µs (
wl_surface_commitat 587 →configure serial=20at 676). It is the second and last blocking wait before pixels. - Painting dominates client-side init:
buffer_allocat 848 →wl_surface_attachat 2170 is ~1.3 ms of filling 307 200 gradient pixels — protocol work up to that point is ~0.9 ms in total. - The frame clock dominates everything.
first_commitat 2.25 ms;first_pixel_presentedat 18.6 ms. The remaining ~16 ms is purely waiting for headless weston's next 60 Hz repaint tick. Across 7 runs the total ranged 11 149 µs to 18 561 µs while the pre-commit portion stayed ≈ 2.2 ms — the variance is exactly the phase of the compositor's frame clock at connect time, nothing the client does.
Round-trips: 2, by construction
The demo marks rt=1 on the only two places it blocks on the compositor:
wl_display_roundtripafterwl_registrycreation — global discovery needs the server's answer (and internally costs the demo its firstwl_callback, viawl_display.sync).- The
wl_display_dispatchloop that waits for the firstxdg_surface.configureafter the mandatory no-buffer commit — the no-buffer-no-window handshake makes mapping a window inherently one round-trip: the client may not attach a buffer until the compositor has spoken.
Everything else — surface/role creation, title, buffer creation, attach/damage/commit — is fire-and-forget on the client's send buffer. A Wayland window costs two waits, then one frame-clock tick.
Concepts and LOC
The 11 first-touch protocol object types match the scaffold's enumeration exactly (wl_display, wl_registry, wl_callback, wl_compositor, wl_shm, xdg_wm_base, wl_surface, xdg_surface, xdg_toplevel, wl_shm_pool, wl_buffer) — F01 re-derives the count mechanically (concept n=… events frozen at first_pixel_presented) instead of by inspection. Headless weston advertises no wl_seat, so the seat-binding code is unexercised and the Tier-A count stays 11. The two non-protocol OS objects (memfd_create(2) fd, mmap(2) mapping) ride along as before.
LOC is self-measuring: the demo string-imports its own source (enum demoLoc = countLoc(import("app.d")), enabled by stringImportPaths "." in the demo's dub.sdl) and CTFE-counts total and non-blank-non-comment lines, so the number in the summary line can never drift from the code. 558/413 is noticeably above the scaffold's 463/346 — the F01 instrumentation (concept tracking, step counters, the checklist) costs ~70 code lines on top of the windowing itself.
What surprised us
- "First pixel" is a frame-clock lottery. The honest number is not a constant: the 11–19 ms spread is the compositor's vsync phase, and no init optimization can move it. The actionable metric for a library is the ≈ 2.2 ms of client-side work (≈ 0.9 ms excluding the gradient paint), not the wall-clock-to-photon figure.
- The configure serial was 20 on a freshly mapped window. Weston's configure serial counter is per-compositor, not per-surface — earlier demo runs against the same weston instance had consumed serials 1–19. A client must treat the serial as an opaque token to echo back, never as a sequence number to interpret (see F02's serial logging).
- Headless weston keeps a strict 60 Hz frame clock with no display attached — the hold-phase
frame_callbackevents tick at 16.6–16.7 ms intervals exactly. "Presented" on a headless backend therefore means "consumed by a repaint that renders to nothing": the confirmation semantics are identical to a real output; only the photons are missing.
Open questions
wp_presentation(presentation-time protocol) would upgrade "the compositor consumed the commit" to "the frame hit scanout at time T". Worth adding when F04 (frame pacing) lands; headless weston advertises it, so it stays Tier A.- The first-touch order of
wl_callbackis an artifact of usingwl_display_roundtrip; a client drivingwl_display.syncmanually or polling the fd (F05 territory) could map a window with 10 concepts. Is the bootstrap round-trip ever avoidable in practice (registry events race with binds)?wl_display.syncsemantics say no — discovery requires one server echo.
Sources
- Protocol — the core Wayland protocol (
wl_surface.frame,wl_display.sync) andxdg-shell(xdg_surface.configure,ack_configure); glue generated fromwayland-protocols1.47 (stable/xdg-shell/xdg-shell.xml). - Spec implemented — F01 first pixel; conventions in the features index.
- Baseline — the scaffold findings (concept list, build gotchas); cross-platform grid in the feature matrix.
- Code —
./examples/f01-first-pixel/app.d,./examples/f01-first-pixel/instrument.d,./examples/f01-first-pixel/c.c.