Author SHA1 Message Date
daniel 5ab7263c9c display-demo: stop drawing a cursor and stop blocking on the mouse
Two real bugs visible in a normal `zig build run-x86-64` boot (but not in the
display-demo test, which spawns no input service):

  - Two cursors. The demo drew its own cursor layer while the display service
    now draws one too (its mouse-listener thread). The demo's went through the
    client IPC protocol and lagged; the service's is in-process and tracks
    tightly — "one responds better than the other."

  - Animation frozen until the mouse moves. The demo called a *blocking*
    `mouse.next()` (ipc_reply_wait) inside its animation loop, so the sliding
    box advanced only one frame per mouse event. In the display-demo test
    there is no input service, so subscribeMouse() returned null and the loop
    ran free on its 30ms timer — which is exactly why the test passed while
    the real boot was broken.

The compositor now owns the cursor (docs/display.md), so the demo should draw
none and read no input: it becomes a pure client-animation proof whose loop is
independent of the mouse. Removes its cursor layer, mouse subscription, and the
blocking read.

Also harden displayDemoTest to spawn the `input` service alongside the demo
(matching real boot): a client that blocks its animation loop on a mouse read
would now stall before `display-demo: ok` and fail the test, instead of
passing because no input service happened to be present.

Verified: display / display-service / display-demo / display-cursor all green.
2026-07-21 02:37:47 +01:00
daniel 7f415e724f display: track the mouse with a listener thread (Shape A) + cursor
The display's first use of threads (docs/threading.md, docs/display.md). The
compositor stays the single owner of the framebuffer — only the main
service.run loop touches the backend and layer stack — and a dedicated
mouse-listener thread runs beside it:

  - Listener: blocks on input.subscribeMouse(), accumulates relative dx/dy
    into an absolute cursor position clamped to the screen, and hands it to
    the compositor. It never touches the compositor, so no lock guards the
    framebuffer; a parked next() lets the core halt.
  - CursorChannel: a single-slot latest-value cell under a Thread.Mutex (the
    renderer wants where the cursor is now, not a replay of deltas), with a
    coalesced self-ipc.send poke that wakes the main loop — parked in
    replyWait — as a message-notification. At most one poke is queued while
    the last is undrained, so a fast mouse can't flood the endpoint.
  - Render: the cursor is a top-z compositor layer; on the poke the main loop
    moves it via configure + present (which damages old + new footprints).

The display binary opts into threads (addThreadedUserBinary), and
input-source gains a "mouse" mode that publishes pure motion to drive it.

Two kernel-level findings this surfaced, both fixed:

  1. IPC handles do not cross threads. The handle table lives on the Task, so
     the listener can't reuse the main loop's endpoint handle — it
     ipc.lookup(.display)s its own handle to the same endpoint to poke through.

  2. Concurrent IPC from two threads raced unlocked kernel state. The display
     is the first process issuing IPC syscalls from two threads at once, which
     exposed a data race (flaky #GP in installEntry): create_ipc_endpoint /
     ipc_register / ipc_lookup allocate from the kernel heap and mutate the
     global registry, endpoint refcounts, and handle tables without the big
     kernel lock. They were safe only while a process couldn't race itself.
     They now sync.enter() like call/reply_wait/send already did (the kernel
     heap has no lock of its own yet — heap.zig: "a lock comes with
     threads/SMP" — so the big lock keeps its callers serialized).

Test: -Dtest-case=display-cursor (smp:4) spawns the input service, the
threaded display, and input-source in mouse mode; asserts the display's
"cursor tracking mouse ok" marker once the cursor has tracked a run of motion
end to end. Verified green 6/6 under stress (the race hit ~1-in-4 before the
lock fix) and in the full 29-case QEMU guardrail suite; zig build test clean.
2026-07-21 02:31:29 +01:00
daniel cf140eb772 Merge threading-phase2: threading Phase 2 (M7-M11) + naming cleanups
Completes the std.Thread-shaped runtime.Thread. Phase 1 (M1-M6: address-space
refcount, thread_spawn/exit, join/detach, futex + Mutex/Condition/Semaphore,
getCurrentId) was already on main; this brings the Phase 2 hardening and the
coding-standards/arch-neutrality passes on top:

  M7  thread-safe allocation (per-address-space mmap arena + locked heap)
  M8  the task reaper — reclaim dead tasks' kernel stacks
  M9  thread_join syscall — retire the per-thread endpoint
  M10 per-thread thread pointer — the TLS mechanism (x86_64 IA32_FS_BASE)
  M11 RwLock, WaitGroup, and host-testable sync

Plus: the TLS thread pointer named arch-neutrally (not fs.base) so the kernel
stays architecture-agnostic; aspace/vaddr/paddr spelled out per
docs/coding-standards.md across kernel, runtime, ABI, tests, and docs; and the
misleading fs.base dot-notation dropped in favour of "thread pointer"
(arch-neutral) / "FS base" (x86-specific).

Deferred by design (no consumer yet): the Zig threadlocal *compiler* layer
(M10) and detached-thread user-stack reclaim (M9) — both noted in place.

Verified: zig build, zig build test, and the full 25-case QEMU guardrail suite
all green.
2026-07-21 01:55:23 +01:00
9 changed files with 347 additions and 44 deletions
+3 -1
View File
@@ -535,7 +535,9 @@ pub fn build(b: *std.Build) void {
// The FAT filesystem server: mounts the block device and serves it into the VFS // The FAT filesystem server: mounts the block device and serves it into the VFS
// at /mnt/usb. Its engine (engine.zig / on-disk.zig) is imported relatively. // at /mnt/usb. Its engine (engine.zig / on-disk.zig) is imported relatively.
const fat_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig"); const fat_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig");
const display_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display", "system/services/display/display.zig"); // Threaded: the display runs a mouse-listener thread alongside its compositor loop
// (docs/threading.md, docs/display.md), so it opts into real atomics/TLS.
const display_exe = addThreadedUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display", "system/services/display/display.zig");
const display_demo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display-demo", "system/services/display-demo/display-demo.zig"); const display_demo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "display-demo", "system/services/display-demo/display-demo.zig");
const virtio_gpu_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "virtio-gpu", "system/drivers/virtio-gpu/virtio-gpu.zig"); const virtio_gpu_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "virtio-gpu", "system/drivers/virtio-gpu/virtio-gpu.zig");
const shm_server_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "shm-server", "system/services/shm-server/shm-server.zig"); const shm_server_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "shm-server", "system/services/shm-server/shm-server.zig");
+46 -5
View File
@@ -211,6 +211,38 @@ with a boot-race retry): `display.info()`, a `Layer` handle with `fill` / `blitT
`damage`, and `present()`. Application code never issues the raw syscalls — it calls the `damage`, and `present()`. Application code never issues the raw syscalls — it calls the
runtime, as with every other danos service. runtime, as with every other danos service.
## The cursor: a mouse-listener thread feeding the compositor
The compositor is the single owner of the framebuffer — only the main `service.run` loop
touches the backend and the layer stack. Tracking the mouse without breaking that
ownership is the display's first use of [threads](threading.md): the service is built
multi-threaded (`addThreadedUserBinary`) and, at startup, spawns a **mouse-listener
thread** beside the compositor loop.
- **Listener thread.** Blocks on the input service's mouse stream
(`input.subscribeMouse()`), accumulates the relative `dx`/`dy` motion into an absolute
cursor position clamped to the screen, and hands it to the compositor. It never touches
the compositor — so no lock guards the framebuffer. A parked `next()` leaves its core
free to halt ([halting.md](halting.md)).
- **The channel.** A single-slot *latest-value* cell (`CursorChannel`) guarded by a
`runtime.Thread.Mutex`: the renderer wants where the cursor *is now*, not a replay of
every delta, so a new position overwrites the old. The listener also **pokes** the
compositor awake — the main loop is parked in `replyWait`, so the listener posts a
zero-payload `ipc.send` to the compositor's endpoint, which arrives as a
message-notification ([ipc.md](ipc.md)). The poke is *coalesced*: at most one is queued
while the main loop has not drained the last, so a fast mouse cannot flood the endpoint.
- **Render.** On the poke, the main loop takes the latest position and moves the cursor —
which is just a top-z compositor layer — with the existing `configure` + `present` path
(it damages the old and new footprints, so only those two rectangles repaint).
Two threading facts shape this (both in [threading.md](threading.md)). IPC **handles do
not cross threads**, so the listener can't reuse the main loop's endpoint handle — it
`ipc.lookup(.display)`s its *own* handle to the same endpoint to poke through. And a
multi-threaded service doing concurrent IPC is why the kernel's endpoint-create / register
/ lookup syscalls now serialize under the big kernel lock. Shared fate applies: a fault in
the listener takes the whole display down, and the supervisor restarts the process
([resilience.md](resilience.md)).
## What v1 does not do (and why that's fine) ## What v1 does not do (and why that's fine)
Two capabilities are deliberately out of the first cut. Neither reshapes anything above; Two capabilities are deliberately out of the first cut. Neither reshapes anything above;
@@ -232,7 +264,7 @@ both are clean additions behind the interfaces v1 establishes.
## Verifying it ## Verifying it
Three QEMU test cases ([tests.zig](../system/kernel/tests.zig), `python3 Four QEMU test cases ([tests.zig](../system/kernel/tests.zig), `python3
test/qemu_test.py <case>`), each layering on the last: test/qemu_test.py <case>`), each layering on the last:
- **`display`** — the kernel handoff: the seeded `display` device is shaped correctly and - **`display`** — the kernel handoff: the seeded `display` device is shaped correctly and
@@ -245,11 +277,20 @@ test/qemu_test.py <case>`), each layering on the last:
layer — logging `display: compositor self-check ok`. layer — logging `display: compositor self-check ok`.
- **`display-demo`** — the full pipeline from a separate process: the hardware-free - **`display-demo`** — the full pipeline from a separate process: the hardware-free
[`display-demo`](../system/services/display-demo/) client (the [`display-demo`](../system/services/display-demo/) client (the
[`input-source`](../system/services/input-source/) analog) drives layers — a wallpaper, a [`input-source`](../system/services/input-source/) analog) drives layers — a wallpaper and
sliding rectangle, a cursor — through the layer client API and heartbeats a sliding rectangle — through the layer client API and heartbeats
`display-demo: ok`, proving a frame travelled client → compositor → screen, exactly as `display-demo: ok`, proving a frame travelled client → compositor → screen, exactly as
the [input test](input.md) proves an event travels source → service → subscriber. The the [input test](input.md) proves an event travels source → service → subscriber. It draws
visible motion itself is a screenshot away via `zig build run-x86-64`. no cursor and reads no input — the cursor is the service's own (below), and the demo
animates on its own frame timer, independent of the mouse (the test spawns `input`
alongside it to keep that independence honest). The visible motion itself is a screenshot
away via `zig build run-x86-64`.
- **`display-cursor`** — the mouse-listener thread end to end: with the `input` service up,
`input-source mouse` publishes pure motion, and the display's listener thread accumulates
it into a cursor position handed to the render loop over the `CursorChannel`. Once the
cursor has tracked a run of that motion, the service logs
`display: cursor tracking mouse ok`. Runs `smp: 4` — the compositor and listener threads
execute on different cores, which is what surfaced the IPC-under-lock requirement above.
The compositor's pixel math (rectangle clipping, fill, composite, tile blit) and colour The compositor's pixel math (rectangle clipping, fill, composite, tile blit) and colour
packing are additionally covered by pure host unit tests under `zig build test`. packing are additionally covered by pure host unit tests under `zig build test`.
+15
View File
@@ -249,6 +249,21 @@ it may call `runtime.Thread.spawn`. Everyone else stays single-threaded and lean
- **Resilience** ([resilience.md](resilience.md)): a faulting thread kills its whole - **Resilience** ([resilience.md](resilience.md)): a faulting thread kills its whole
process (shared fate). The supervisor restarts the **process**, which respawns its process (shared fate). The supervisor restarts the **process**, which respawns its
threads from a known-good state — restart granularity stays the process. threads from a known-good state — restart granularity stays the process.
- **IPC — two consequences threads forced ([ipc.md](ipc.md)):**
- *Handles do not cross threads.* The handle table lives on the `Task`
([scheduler.zig](../system/kernel/scheduler.zig)), so a handle number is meaningful
only to the thread that created it — thread A's endpoint handle `3` is not thread B's.
A thread that needs to reach an endpoint another thread owns looks it up
(`ipc.lookup(service)`) to install its **own** handle to the same underlying endpoint.
This is how the display's mouse-listener thread reaches the compositor loop's endpoint
to poke it awake (docs/display.md).
- *IPC syscalls that touch shared kernel state now serialize under the big kernel lock.*
`create_ipc_endpoint`/`ipc_register`/`ipc_lookup` allocate from the kernel heap and
mutate the global service registry, endpoint refcounts, and handle tables. Those paths
were unlocked because a single-threaded process could not race itself; a multi-threaded
one can, from two cores at once. They now take `sync.enter()` like `call`/`reply_wait`/
`send` already did — the kernel heap has no lock of its own yet (heap.zig: "a lock comes
with threads/SMP"), so the big lock is what keeps its callers serialized.
## Build-out plan (staged, each gate serial-checkable) ## Build-out plan (staged, each gate serial-checkable)
+16
View File
@@ -256,6 +256,13 @@ fn failErr(state: *architecture.CpuState, errno: i64) void {
/// create_ipc_endpoint() -> handle: allocate an endpoint and install it in the /// create_ipc_endpoint() -> handle: allocate an endpoint and install it in the
/// caller's handle table. /// caller's handle table.
fn systemCreateIpcEndpoint(state: *architecture.CpuState) void { fn systemCreateIpcEndpoint(state: *architecture.CpuState) void {
// Under the big kernel lock: this allocates from the kernel heap and mutates the
// caller's handle table. A multi-threaded process (e.g. the display's compositor +
// mouse-listener threads) can drive this concurrently from two cores, so the endpoint
// allocation and every other lock holder must serialize (heap.zig: "a lock comes with
// threads/SMP").
const flags = sync.enter();
defer sync.leave(flags);
const endpoint = ipc.createIpcEndpoint() orelse return failErr(state, ipc.ENOMEM); const endpoint = ipc.createIpcEndpoint() orelse return failErr(state, ipc.ENOMEM);
const h = ipc.installHandle(scheduler.current(), endpoint); const h = ipc.installHandle(scheduler.current(), endpoint);
if (h < 0) { if (h < 0) {
@@ -268,6 +275,10 @@ fn systemCreateIpcEndpoint(state: *architecture.CpuState) void {
/// ipc_register(service_id, handle): publish the caller's endpoint under a /// ipc_register(service_id, handle): publish the caller's endpoint under a
/// well-known id so other processes can find it. /// well-known id so other processes can find it.
fn systemIpcRegister(state: *architecture.CpuState) void { fn systemIpcRegister(state: *architecture.CpuState) void {
// Under the big kernel lock: mutates the global service registry and endpoint
// refcounts, which threads of the same (or another) process can race.
const flags = sync.enter();
defer sync.leave(flags);
const id: u32 = @truncate(architecture.systemCallArg(state, 0)); const id: u32 = @truncate(architecture.systemCallArg(state, 0));
const endpoint = ipc.resolveHandle(scheduler.current(), architecture.systemCallArg(state, 1)) orelse return failErr(state, ipc.EBADF); const endpoint = ipc.resolveHandle(scheduler.current(), architecture.systemCallArg(state, 1)) orelse return failErr(state, ipc.EBADF);
architecture.setSystemCallResult(state, @bitCast(ipc.register(id, endpoint))); architecture.setSystemCallResult(state, @bitCast(ipc.register(id, endpoint)));
@@ -276,6 +287,11 @@ fn systemIpcRegister(state: *architecture.CpuState) void {
/// ipc_lookup(service_id) -> handle: find a published endpoint and install a /// ipc_lookup(service_id) -> handle: find a published endpoint and install a
/// handle to it in the caller. /// handle to it in the caller.
fn systemIpcLookup(state: *architecture.CpuState) void { fn systemIpcLookup(state: *architecture.CpuState) void {
// Under the big kernel lock: reads the global registry, takes an endpoint reference,
// and installs a handle — all racy against concurrent threads (this is the path the
// display's mouse-listener thread takes to reach the compositor endpoint).
const flags = sync.enter();
defer sync.leave(flags);
const id: u32 = @truncate(architecture.systemCallArg(state, 0)); const id: u32 = @truncate(architecture.systemCallArg(state, 0));
const endpoint = ipc.lookup(id) orelse return failErr(state, ipc.ENOENT); const endpoint = ipc.lookup(id) orelse return failErr(state, ipc.ENOENT);
const h = ipc.installHandle(scheduler.current(), endpoint); const h = ipc.installHandle(scheduler.current(), endpoint);
+60
View File
@@ -101,6 +101,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
displayServiceTest(boot_information); displayServiceTest(boot_information);
} else if (eql(case, "display-demo")) { } else if (eql(case, "display-demo")) {
displayDemoTest(boot_information); displayDemoTest(boot_information);
} else if (eql(case, "display-cursor")) {
displayCursorTest(boot_information);
} else if (eql(case, "shm")) { } else if (eql(case, "shm")) {
shmTest(boot_information); shmTest(boot_information);
} else if (eql(case, "virtio-gpu")) { } else if (eql(case, "virtio-gpu")) {
@@ -2782,6 +2784,46 @@ fn displayServiceTest(boot_information: *const BootInformation) void {
while (true) scheduler.yield(); while (true) scheduler.yield();
} }
/// The threaded compositor tracks a mouse (docs/threading.md, docs/display.md). Spawn the
/// `input` fan-out service, the display (which runs a mouse-listener thread alongside its
/// compositor loop and draws a top-z cursor), and `input-source` in `mouse` mode — a
/// synthetic source publishing pure motion. The display's own marker,
/// `display: cursor tracking mouse ok`, is printed once the cursor has tracked a run of
/// motion end to end (source -> input service -> listener thread -> channel -> render), so
/// like the other display cases we match on serial rather than poll in-kernel.
fn displayCursorTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: display-cursor\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
if (!spawnNamed(rd, "input")) {
log("display-cursor: could not spawn the input service\n", .{});
result();
return;
}
if (!spawnNamed(rd, "display")) {
log("display-cursor: could not spawn the display service\n", .{});
result();
return;
}
if (!spawnNamedWithArg(rd, "input-source", "mouse")) {
log("display-cursor: could not spawn the mouse source\n", .{});
result();
return;
}
scheduler.setPriority(1); // below the services, so they run
while (true) scheduler.yield();
}
/// D4 — a separate process drives the compositor. Spawn the display service and the /// D4 — a separate process drives the compositor. Spawn the display service and the
/// hardware-free `display-demo` client, which creates a wallpaper, a moving rectangle, /// hardware-free `display-demo` client, which creates a wallpaper, a moving rectangle,
/// and a cursor and presents a run of frames. Its `display-demo: ok` heartbeat — printed /// and a cursor and presents a run of frames. Its `display-demo: ok` heartbeat — printed
@@ -2808,6 +2850,11 @@ fn displayDemoTest(boot_information: *const BootInformation) void {
result(); result();
return; return;
} }
// Spawn the input service too — real boot has it, and it guards the demo's
// independence from input: the demo must animate to `display-demo: ok` on its own
// frame timer even with the input service available (a client that blocks its
// animation loop on a mouse read would stall here, never reaching the marker).
_ = spawnNamed(rd, "input");
_ = spawnNamed(rd, "display-demo"); _ = spawnNamed(rd, "display-demo");
scheduler.setPriority(1); // below the service + demo, so they run scheduler.setPriority(1); // below the service + demo, so they run
while (true) scheduler.yield(); while (true) scheduler.yield();
@@ -3029,6 +3076,19 @@ fn spawnNamed(rd: initial_ramdisk.Reader, name: []const u8) bool {
return false; return false;
} }
/// As `spawnNamed`, but passes one extra argv entry (argv[1]) — e.g. a mode selector like
/// `input-source mouse`.
fn spawnNamedWithArg(rd: initial_ramdisk.Reader, name: []const u8, arg: []const u8) bool {
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (eql(item.name, name)) {
return if (process.spawnProcess(item.blob, 4, &.{ item.name, arg })) true else |_| false;
}
}
return false;
}
/// The GSI discovery recorded for the HPET, from the same device table drivers see. /// The GSI discovery recorded for the HPET, from the same device table drivers see.
fn hpetGsi() ?u32 { fn hpetGsi() ?u32 {
var buffer: [16]device_abi.DeviceDescriptor = undefined; var buffer: [16]device_abi.DeviceDescriptor = undefined;
+9 -32
View File
@@ -1,15 +1,19 @@
//! system/services/display-demo — a hardware-free client of the display service, the //! system/services/display-demo — a hardware-free client of the display service, the
//! `input-source` analog for the compositor. It creates a wallpaper, a rectangle it moves //! `input-source` analog for the compositor. It creates a wallpaper and a rectangle it
//! each frame, and a small cursor, then drives the compositor in a present loop — proof //! slides each frame, then drives the compositor in a present loop — proof that a
//! that a *separate process* can compose a moving scene through the display service over //! *separate process* can compose a moving scene through the display service over IPC,
//! IPC, exercising the layer client API and damage-driven present end to end //! exercising the layer client API and damage-driven present end to end
//! (docs/display.md). It logs `display-demo: ok` once it has driven a run of frames. //! (docs/display.md). It logs `display-demo: ok` once it has driven a run of frames.
//!
//! It draws no cursor and reads no input: the on-screen cursor is the display service's
//! own, tracked by the service's mouse-listener thread (docs/display.md). The demo's job
//! is only to prove client-driven animation, so its loop runs on its own frame timer and
//! is deliberately independent of the mouse.
const runtime = @import("runtime"); const runtime = @import("runtime");
const display = runtime.display; const display = runtime.display;
const system = runtime.system; const system = runtime.system;
const time = runtime.time; const time = runtime.time;
const input = runtime.input;
pub fn main() void { pub fn main() void {
const mode = display.info() orelse { const mode = display.info() orelse {
@@ -28,15 +32,6 @@ pub fn main() void {
const box = display.createLayer(0, box_y, box_w, box_h, 1) orelse return createFailed(); const box = display.createLayer(0, box_y, box_w, box_h, 1) orelse return createFailed();
_ = box.fill(0, 0, box_w, box_h, display.color(0xE0, 0x60, 0x40)); _ = box.fill(0, 0, box_w, box_h, display.color(0xE0, 0x60, 0x40));
// A little cursor on top. Its position is signed (the layer API is i32) and clamped to
// the screen; mouse motion arrives as relative deltas we accumulate below.
var cursor_x: i32 = @intCast(mode.width / 2);
var cursor_y: i32 = @intCast(mode.height / 2);
const cursor_max_x: i32 = @as(i32, @intCast(mode.width)) - 12;
const cursor_max_y: i32 = @as(i32, @intCast(mode.height)) - 12;
const cursor = display.createLayer(cursor_x, cursor_y, 12, 12, 2) orelse return createFailed();
_ = cursor.fill(0, 0, 12, 12, display.color(0xF0, 0xF0, 0xF0));
_ = display.present(); _ = display.present();
_ = system.write("display-demo: scene up; animating\n"); _ = system.write("display-demo: scene up; animating\n");
@@ -45,18 +40,7 @@ pub fn main() void {
var dx: i32 = 8; var dx: i32 = 8;
var frame: u32 = 0; var frame: u32 = 0;
var mouse = input.subscribeMouse(); // type: ?input.MouseSubscriber
if (mouse == null) _ = system.write("display-demo: no mouse; animating without it\n");
while (true) : (frame += 1) { while (true) : (frame += 1) {
if (mouse) |*ms| {
if (ms.next()) |event| {
cursor_x = clamp(cursor_x + event.dx, 0, cursor_max_x);
cursor_y = clamp(cursor_y + event.dy, 0, cursor_max_y);
_ = cursor.configure(cursor_x, cursor_y, 2, true);
}
}
x += dx; x += dx;
if (x <= 0) { if (x <= 0) {
x = 0; x = 0;
@@ -74,13 +58,6 @@ pub fn main() void {
} }
} }
/// Clamp `v` to the inclusive range [lo, hi].
fn clamp(v: i32, lo: i32, hi: i32) i32 {
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}
fn createFailed() void { fn createFailed() void {
_ = system.write("display-demo: create failed\n"); _ = system.write("display-demo: create failed\n");
} }
+165 -4
View File
@@ -21,6 +21,8 @@ const backend_mod = @import("backend.zig");
const protocol = runtime.display_protocol; const protocol = runtime.display_protocol;
const ipc = runtime.ipc; const ipc = runtime.ipc;
const system = runtime.system; const system = runtime.system;
const input = runtime.input;
const Thread = runtime.Thread;
const Rect = compositor.Rect; const Rect = compositor.Rect;
const Surface = compositor.Surface; const Surface = compositor.Surface;
@@ -328,6 +330,157 @@ fn fail_check(_: []const u8) void {
_ = system.write("display: compositor self-check FAILED (setup)\n"); _ = system.write("display: compositor self-check FAILED (setup)\n");
} }
// --- cursor + mouse-input thread --------------------------------------------
//
// The compositor is the single owner of the framebuffer: only the main service
// loop touches `backend` and the layer stack. A dedicated listener thread (spawned
// in `initialise`) blocks on the input service's mouse stream, accumulates relative
// motion into an absolute cursor position, and hands that position to the main loop
// through `cursor_channel` — a single-slot latest-value cell (the renderer wants
// where the cursor *is*, not a replay of every delta). The listener never touches
// the compositor; it only writes the channel and pokes the main loop awake with a
// self-directed `ipc.send`, which arrives as a message-notification in the service
// loop (docs/threading.md, docs/display.md). Shared fate: a fault in the listener
// takes the whole display down and the supervisor restarts it (docs/resilience.md).
const cursor_size = 10; // a small square sprite — enough to prove tracking
const cursor_z = 0xFFFF_FFFF; // always above client layers
const cursor_report_threshold = 5; // px of travel before the tracking marker latches
var cursor_layer: ?u32 = null;
var cursor_origin_x: i32 = 0;
var cursor_origin_y: i32 = 0;
/// Latched once the cursor has demonstrably tracked a run of motion end to end
/// (source -> input service -> listener -> channel -> render): the `display-cursor`
/// test's success marker.
var cursor_tracking_reported: bool = false;
const poke_byte = [_]u8{0}; // the poke carries no payload; the value lives in the channel
/// Shared between the listener thread (producer) and the main loop (consumer).
/// Latest-value semantics with a coalesced wake: at most one poke is queued while
/// the main loop has not drained the last one, so a fast mouse cannot flood the
/// service endpoint.
const CursorChannel = struct {
lock: Thread.Mutex = .{},
poke_endpoint: ipc.Handle = 0,
x: i32 = 0,
y: i32 = 0,
buttons: u32 = 0,
dirty: bool = false,
poke_pending: bool = false,
const Snapshot = struct { x: i32, y: i32, buttons: u32 };
/// Producer (listener thread): record the newest position and, unless a wake is
/// already queued, poke the main loop awake.
fn publish(self: *CursorChannel, x: i32, y: i32, buttons: u32) void {
self.lock.lock();
self.x = x;
self.y = y;
self.buttons = buttons;
self.dirty = true;
const need_poke = !self.poke_pending;
if (need_poke) self.poke_pending = true;
self.lock.unlock();
if (need_poke) _ = ipc.send(self.poke_endpoint, &poke_byte);
}
/// Consumer (main loop): take the latest position, or null if nothing changed
/// since the last take. Clears the wake latch so the next publish pokes again.
fn take(self: *CursorChannel) ?Snapshot {
self.lock.lock();
defer self.lock.unlock();
self.poke_pending = false;
if (!self.dirty) return null;
self.dirty = false;
return .{ .x = self.x, .y = self.y, .buttons = self.buttons };
}
};
var cursor_channel: CursorChannel = .{};
fn clampAxis(value: i32, max: i32) i32 {
if (value < 0) return 0;
if (value > max) return max;
return value;
}
/// The mouse-listener thread. Blocks on the input service's mouse stream, accumulates
/// relative motion into an absolute position clamped to the screen, and publishes each
/// update. Runs for the life of the process; a parked `next()` leaves the core free to
/// halt (docs/halting.md). It reads only its own state and the channel — never the
/// compositor — so no lock guards the framebuffer.
fn mouseListener(width: u32, height: u32) void {
var mouse = input.subscribeMouse() orelse {
_ = system.write("display: mouse subscribe failed\n");
return;
};
// Our own handle to the compositor's endpoint. IPC handles are per-thread, so we
// cannot reuse the main thread's service handle — we look the service up to install a
// handle in this thread's table. A poke posted here wakes the compositor loop parked
// in replyWait (docs/threading.md: handles do not cross threads).
cursor_channel.poke_endpoint = ipc.lookup(.display) orelse {
_ = system.write("display: mouse listener could not reach the compositor endpoint\n");
return;
};
const max_x: i32 = @as(i32, @intCast(width)) - 1;
const max_y: i32 = @as(i32, @intCast(height)) - 1;
var x: i32 = @divTrunc(max_x, 2);
var y: i32 = @divTrunc(max_y, 2);
var buttons: u32 = 0;
while (true) {
const event = mouse.next() orelse continue;
// Switch on the raw kind (not @enumFromInt, which would panic on a scroll or
// future kind): motion moves the cursor, anything else just updates buttons.
if (event.kind == @intFromEnum(input.MouseEventKind.motion)) {
x = clampAxis(x + event.dx, max_x);
y = clampAxis(y + event.dy, max_y);
} else {
buttons = event.buttons;
}
cursor_channel.publish(x, y, buttons);
}
}
/// Consume the latest cursor position from the channel and repaint the cursor layer at
/// it. Runs on the main loop (the compositor owner) in response to a listener poke.
/// `configureLayer` damages both the old and new footprints, so a plain `present`
/// repaints exactly the two rectangles that changed.
fn renderCursor() void {
const snapshot = cursor_channel.take() orelse return;
const id = cursor_layer orelse return;
_ = configureLayer(id, snapshot.x, snapshot.y, cursor_z, true);
present();
if (!cursor_tracking_reported and
@abs(snapshot.x - cursor_origin_x) >= cursor_report_threshold and
@abs(snapshot.y - cursor_origin_y) >= cursor_report_threshold)
{
cursor_tracking_reported = true;
_ = system.write("display: cursor tracking mouse ok\n");
}
}
/// Create the cursor sprite (a top-z square) at screen centre and spawn the listener
/// thread. Called from `initialise` once the backend is up. If either step fails the
/// display still serves drawing clients — it just has no cursor.
fn startCursorTracking() void {
const mode = backend.info();
cursor_origin_x = @divTrunc(@as(i32, @intCast(mode.width)), 2);
cursor_origin_y = @divTrunc(@as(i32, @intCast(mode.height)), 2);
const id = createLayer(cursor_origin_x, cursor_origin_y, cursor_size, cursor_size, cursor_z, true) orelse {
_ = system.write("display: could not create cursor layer\n");
return;
};
cursor_layer = id;
_ = fillLayer(id, Rect.init(0, 0, cursor_size, cursor_size), protocol.pack(mode.format, 0xF0, 0xF0, 0xF0));
present(); // show the cursor at its start position
_ = Thread.spawn(.{}, mouseListener, .{ mode.width, mode.height }) catch {
_ = system.write("display: could not spawn mouse listener\n");
};
}
// --- service ---------------------------------------------------------------- // --- service ----------------------------------------------------------------
fn initialise(endpoint: ipc.Handle) bool { fn initialise(endpoint: ipc.Handle) bool {
@@ -350,6 +503,9 @@ fn initialise(endpoint: ipc.Handle) bool {
_ = system.write("display: presented frame 0\n"); _ = system.write("display: presented frame 0\n");
selfCheck(); selfCheck();
// Bring up the cursor and the mouse-listener thread now that the backend is live.
startCursorTracking();
return true; return true;
} }
@@ -435,11 +591,16 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Han
} }
} }
/// The only notification the compositor arms is the post-attach present timer: repaint the /// Two notification sources reach the compositor. A **message-notification** is a poke
/// screen into the freshly attached native surface, verify the frame landed, then run the /// from the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`):
/// one-shot mode-set self-check (V5). /// repaint the cursor at its latest channel position. Anything else is the post-attach
/// present **timer**: repaint into the freshly attached native surface, verify the frame
/// landed, then run the one-shot mode-set self-check (V5).
fn onNotification(badge: u64) void { fn onNotification(badge: u64) void {
_ = badge; if (badge & ipc.notify_message_bit != 0) {
renderCursor();
return;
}
present(); // native present + verify (first timer fire after the upgrade) present(); // native present + verify (first timer fire after the upgrade)
if (pending_modeset_check) { if (pending_modeset_check) {
pending_modeset_check = false; pending_modeset_check = false;
+23 -2
View File
@@ -10,17 +10,38 @@
//! keyboard and mouse drivers publish their own synthetic streams today; swapping in //! keyboard and mouse drivers publish their own synthetic streams today; swapping in
//! decoded hardware is a follow-up (see docs/input.md). //! decoded hardware is a follow-up (see docs/input.md).
const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const input = runtime.input; const input = runtime.input;
const system = runtime.system; const system = runtime.system;
pub fn main() void { pub fn main(init: runtime.process.Init) void {
var source = input.connectSource() orelse { var source = input.connectSource() orelse {
_ = system.write("input-source: input service unavailable\n"); _ = system.write("input-source: input service unavailable\n");
return; return;
}; };
_ = system.write("input-source: publishing synthetic input events\n");
// "mouse" mode publishes a steady stream of pure motion (dx=dy=+1), for driving a
// cursor (the `display-cursor` test). The default "rotate" mode cycles all device
// classes to exercise the service's per-device routing (the `input` test).
const mode = init.arguments.get(1) orelse "rotate";
if (std.mem.eql(u8, mode, "mouse")) {
_ = system.write("input-source: publishing synthetic mouse motion\n");
while (true) {
_ = source.publishMouseEvent(.{
.kind = @intFromEnum(input.MouseEventKind.motion),
.button = 0,
.dx = 1,
.dy = 1,
.scroll_x = 0,
.scroll_y = 0,
.buttons = 0,
});
system.sleep(20); // ~50 events/sec: moves the cursor briskly
}
}
_ = system.write("input-source: publishing synthetic input events\n");
var step: usize = 0; var step: usize = 0;
while (true) : (step +%= 1) { while (true) : (step +%= 1) {
// Rotate across the device classes so every publish path (and the service's // Rotate across the device classes so every publish path (and the service's
+10
View File
@@ -185,6 +185,16 @@ CASES = [
{"name": "display-demo", {"name": "display-demo",
"expect": r"display-demo: scene up[\s\S]*display-demo: ok", "expect": r"display-demo: scene up[\s\S]*display-demo: ok",
"fail": r"display-demo: (no display|create failed)|display: could not|CPU EXCEPTION|KERNEL PANIC"}, "fail": r"display-demo: (no display|create failed)|display: could not|CPU EXCEPTION|KERNEL PANIC"},
# Threaded compositor tracks a mouse (docs/threading.md, docs/display.md): the display
# runs a mouse-listener thread alongside its compositor loop. `input-source mouse`
# publishes pure motion -> the input service fans it to the display's listener -> the
# listener accumulates it into a cursor position handed to the render loop over a
# single-slot channel. `display: cursor tracking mouse ok` latches once the cursor has
# tracked a run of that motion end to end.
{"name": "display-cursor",
"smp": 4,
"expect": r"display: online \d+x\d+[\s\S]*display: cursor tracking mouse ok",
"fail": r"display: (could not|mouse subscribe failed)|CPU EXCEPTION|KERNEL PANIC"},
# Shared memory (v2 V2): shm-client creates a region, writes a pattern, and passes its # Shared memory (v2 V2): shm-client creates a region, writes a pattern, and passes its
# capability to shm-server, which maps it and confirms the same bytes — proving # capability to shm-server, which maps it and confirms the same bytes — proving
# cross-process shared pages over the extended capability passing. # cross-process shared pages over the extended capability passing.