Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ab7263c9c | ||
|
|
7f415e724f | ||
|
|
cf140eb772 |
@@ -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
|
||||
// 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 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 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");
|
||||
|
||||
+46
-5
@@ -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
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
- **`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`.
|
||||
- **`display-demo`** — the full pipeline from a separate process: the hardware-free
|
||||
[`display-demo`](../system/services/display-demo/) client (the
|
||||
[`input-source`](../system/services/input-source/) analog) drives layers — a wallpaper, a
|
||||
sliding rectangle, a cursor — through the layer client API and heartbeats
|
||||
[`input-source`](../system/services/input-source/) analog) drives layers — a wallpaper and
|
||||
a sliding rectangle — through the layer client API and heartbeats
|
||||
`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
|
||||
visible motion itself is a screenshot away via `zig build run-x86-64`.
|
||||
the [input test](input.md) proves an event travels source → service → subscriber. It draws
|
||||
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
|
||||
packing are additionally covered by pure host unit tests under `zig build test`.
|
||||
|
||||
@@ -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
|
||||
process (shared fate). The supervisor restarts the **process**, which respawns its
|
||||
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)
|
||||
|
||||
|
||||
@@ -256,6 +256,13 @@ fn failErr(state: *architecture.CpuState, errno: i64) void {
|
||||
/// create_ipc_endpoint() -> handle: allocate an endpoint and install it in the
|
||||
/// caller's handle table.
|
||||
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 h = ipc.installHandle(scheduler.current(), endpoint);
|
||||
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
|
||||
/// well-known id so other processes can find it.
|
||||
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 endpoint = ipc.resolveHandle(scheduler.current(), architecture.systemCallArg(state, 1)) orelse return failErr(state, ipc.EBADF);
|
||||
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
|
||||
/// handle to it in the caller.
|
||||
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 endpoint = ipc.lookup(id) orelse return failErr(state, ipc.ENOENT);
|
||||
const h = ipc.installHandle(scheduler.current(), endpoint);
|
||||
|
||||
@@ -101,6 +101,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
displayServiceTest(boot_information);
|
||||
} else if (eql(case, "display-demo")) {
|
||||
displayDemoTest(boot_information);
|
||||
} else if (eql(case, "display-cursor")) {
|
||||
displayCursorTest(boot_information);
|
||||
} else if (eql(case, "shm")) {
|
||||
shmTest(boot_information);
|
||||
} else if (eql(case, "virtio-gpu")) {
|
||||
@@ -2782,6 +2784,46 @@ fn displayServiceTest(boot_information: *const BootInformation) void {
|
||||
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
|
||||
/// 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
|
||||
@@ -2808,6 +2850,11 @@ fn displayDemoTest(boot_information: *const BootInformation) void {
|
||||
result();
|
||||
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");
|
||||
scheduler.setPriority(1); // below the service + demo, so they run
|
||||
while (true) scheduler.yield();
|
||||
@@ -3029,6 +3076,19 @@ fn spawnNamed(rd: initial_ramdisk.Reader, name: []const u8) bool {
|
||||
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.
|
||||
fn hpetGsi() ?u32 {
|
||||
var buffer: [16]device_abi.DeviceDescriptor = undefined;
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
//! 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
|
||||
//! each frame, and a small cursor, then drives the compositor in a present loop — proof
|
||||
//! that a *separate process* can compose a moving scene through the display service over
|
||||
//! IPC, exercising the layer client API and damage-driven present end to end
|
||||
//! `input-source` analog for the compositor. It creates a wallpaper and a rectangle it
|
||||
//! slides each frame, then drives the compositor in a present loop — proof that a
|
||||
//! *separate process* can compose a moving scene through the display service over IPC,
|
||||
//! 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.
|
||||
//!
|
||||
//! 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 display = runtime.display;
|
||||
const system = runtime.system;
|
||||
const time = runtime.time;
|
||||
const input = runtime.input;
|
||||
|
||||
pub fn main() void {
|
||||
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();
|
||||
_ = 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();
|
||||
_ = system.write("display-demo: scene up; animating\n");
|
||||
|
||||
@@ -45,18 +40,7 @@ pub fn main() void {
|
||||
var dx: i32 = 8;
|
||||
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) {
|
||||
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;
|
||||
if (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 {
|
||||
_ = system.write("display-demo: create failed\n");
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ const backend_mod = @import("backend.zig");
|
||||
const protocol = runtime.display_protocol;
|
||||
const ipc = runtime.ipc;
|
||||
const system = runtime.system;
|
||||
const input = runtime.input;
|
||||
const Thread = runtime.Thread;
|
||||
const Rect = compositor.Rect;
|
||||
const Surface = compositor.Surface;
|
||||
|
||||
@@ -328,6 +330,157 @@ fn fail_check(_: []const u8) void {
|
||||
_ = 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 ----------------------------------------------------------------
|
||||
|
||||
fn initialise(endpoint: ipc.Handle) bool {
|
||||
@@ -350,6 +503,9 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
_ = system.write("display: presented frame 0\n");
|
||||
|
||||
selfCheck();
|
||||
|
||||
// Bring up the cursor and the mouse-listener thread now that the backend is live.
|
||||
startCursorTracking();
|
||||
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
|
||||
/// screen into the freshly attached native surface, verify the frame landed, then run the
|
||||
/// one-shot mode-set self-check (V5).
|
||||
/// Two notification sources reach the compositor. A **message-notification** is a poke
|
||||
/// from the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`):
|
||||
/// 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 {
|
||||
_ = badge;
|
||||
if (badge & ipc.notify_message_bit != 0) {
|
||||
renderCursor();
|
||||
return;
|
||||
}
|
||||
present(); // native present + verify (first timer fire after the upgrade)
|
||||
if (pending_modeset_check) {
|
||||
pending_modeset_check = false;
|
||||
|
||||
@@ -10,17 +10,38 @@
|
||||
//! keyboard and mouse drivers publish their own synthetic streams today; swapping in
|
||||
//! decoded hardware is a follow-up (see docs/input.md).
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const input = runtime.input;
|
||||
const system = runtime.system;
|
||||
|
||||
pub fn main() void {
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
var source = input.connectSource() orelse {
|
||||
_ = system.write("input-source: input service unavailable\n");
|
||||
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;
|
||||
while (true) : (step +%= 1) {
|
||||
// Rotate across the device classes so every publish path (and the service's
|
||||
|
||||
@@ -185,6 +185,16 @@ CASES = [
|
||||
{"name": "display-demo",
|
||||
"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"},
|
||||
# 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
|
||||
# capability to shm-server, which maps it and confirms the same bytes — proving
|
||||
# cross-process shared pages over the extended capability passing.
|
||||
|
||||
Reference in New Issue
Block a user