display: the compositor service — claim, double-buffer, present (D2)
Stand up /system/services/display: a ring-3 process that claims the framebuffer D1 seeded, maps it write-combining as the front buffer, allocates a cacheable back buffer, and presents composed frames. The GUI track's compositor, reached by name over ServiceId.display (= 9). - protocol.zig: the display wire protocol (info/create_layer/configure_layer/ destroy_layer/fill_rect/blit_tile/damage/present). `info` and a whole-screen `present` are live; the layer ops fail-stub until D3. - display.zig: enumerate -> claim -> mmio_map(WC) the LFB, mmap a cacheable back buffer, clear it and present it (proving the double-buffer path), then serve. - runtime.display + barrel exports (display, display_protocol): a cached `.display` client with info()/present(), the runtime.block shape. - init spawns "display" in boot_services; build.zig wires the protocol onto the runtime, builds the exe, packs it into the initial-ramdisk, installs it. mmap fix the back buffer forced: systemMmap was capped at 256 pages (1 MiB) by a fixed kernel-stack scratch array. Rewrote it to map page-by-page with rollback (no array) and raised the cap to 8192 pages (32 MiB) — enough for a 4K back buffer. A real limitation met. Gate: `python3 test/qemu_test.py display-service` matches the service's own serial heartbeats (display: online WxH / presented frame 0), printed only after the full claim -> WC-map -> back-buffer -> present chain. Regression-checked usermem, heap, init, and D1's display.
This commit is contained in:
parent
cd812cc00e
commit
69b018cc32
11
build.zig
11
build.zig
|
|
@ -348,6 +348,13 @@ pub fn build(b: *std.Build) void {
|
|||
// The block protocol, so runtime.block (the block-device client) can speak it.
|
||||
runtime_module.addImport("block-protocol", block_protocol_module);
|
||||
|
||||
// The display protocol, so runtime.display (the compositor client) and the display
|
||||
// service both speak it through the runtime, like the other protocol modules.
|
||||
const display_protocol_module = b.addModule("display-protocol", .{
|
||||
.root_source_file = b.path("system/services/display/protocol.zig"),
|
||||
});
|
||||
runtime_module.addImport("display-protocol", display_protocol_module);
|
||||
|
||||
// The power protocol: system power's domain-named surface (docs/power.md).
|
||||
const power_protocol_module = b.addModule("power-protocol", .{
|
||||
.root_source_file = b.path("system/services/power/protocol.zig"),
|
||||
|
|
@ -459,6 +466,7 @@ 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");
|
||||
const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig");
|
||||
const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig");
|
||||
// The PCI bus driver decodes each function's class triple to human names in its
|
||||
|
|
@ -526,6 +534,8 @@ pub fn build(b: *std.Build) void {
|
|||
mk_run.addFileArg(fat_exe.getEmittedBin());
|
||||
mk_run.addArg("fat-test");
|
||||
mk_run.addFileArg(fat_test_exe.getEmittedBin());
|
||||
mk_run.addArg("display");
|
||||
mk_run.addFileArg(display_exe.getEmittedBin());
|
||||
mk_run.addArg("pci-bus");
|
||||
mk_run.addFileArg(pci_bus_exe.getEmittedBin());
|
||||
mk_run.addArg("crash-test");
|
||||
|
|
@ -563,6 +573,7 @@ pub fn build(b: *std.Build) void {
|
|||
.{ usb_hid_mouse_exe, "system/drivers" },
|
||||
.{ usb_storage_exe, "system/drivers" },
|
||||
.{ fat_exe, "system/services" },
|
||||
.{ display_exe, "system/services" },
|
||||
.{ log_flush_exe, "system/services" },
|
||||
}) |entry| {
|
||||
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
|
||||
|
|
|
|||
|
|
@ -62,30 +62,37 @@ visible fill folds into D2's gate (the service clears the screen through the bac
|
|||
Regression-checked: `discovery`, `ioport`, `claim-release`, `supervision`, `device-list`,
|
||||
`device-manager` all still pass with the +1 device in the table.
|
||||
|
||||
## D2 — Service skeleton, protocol, runtime module
|
||||
## D2 — Service skeleton, protocol, runtime module ✅
|
||||
|
||||
Stand up the named service and the double-buffer, no layers yet.
|
||||
|
||||
- [ ] `system/services/display/protocol.zig`: `Operation{ info, create_layer,
|
||||
- [x] `system/services/display/protocol.zig`: `Operation{ info, create_layer,
|
||||
configure_layer, destroy_layer, fill_rect, blit_tile, damage, present }`; `extern`
|
||||
`Request`/`Reply`; `message_maximum`/`request_size`/`reply_size` consts. (Model:
|
||||
[block/protocol.zig](../system/services/block/protocol.zig).)
|
||||
- [ ] [abi.zig](../system/abi.zig): `ServiceId.display = 9`.
|
||||
- [ ] `system/services/display/display.zig`: `main` → enumerate + claim + WC-map the LFB
|
||||
(front) → `mmap` a cacheable back buffer of `height*pitch` → `runtime.service.run(
|
||||
.{ .service = .display, .init, .on_message })`. Implement `info` and a whole-screen
|
||||
`present` (back → front) first.
|
||||
- [ ] [library/runtime/display.zig](../library/runtime/runtime.zig) (+ export in
|
||||
`runtime.zig`): `info()`, and a stub `present()`. Cached `.display` lookup with
|
||||
retry (model: [block.zig](../library/runtime/block.zig)).
|
||||
- [ ] [init.zig](../system/services/init/init.zig): add `"display"` to `boot_services`.
|
||||
- [ ] [build.zig](../build.zig): `display-protocol` module; `display` exe via
|
||||
`addUserBinary`; import the protocol into `runtime` and into the exe; pack into the
|
||||
initial-ramdisk; install to `/system/services/display`.
|
||||
`Request`/`Reply`; size + `maximum_payload` consts. (Model: block/protocol.zig.)
|
||||
- [x] [abi.zig](../system/abi.zig): `ServiceId.display = 9`.
|
||||
- [x] `system/services/display/display.zig`: `main` → enumerate + claim + WC-map the LFB
|
||||
(front) → `mmap` a cacheable back buffer of `height*pitch` → `runtime.service.run`.
|
||||
`info` and a whole-screen `present` (back → front) are live; layer ops fail-stub
|
||||
until D3. Init clears the back buffer and presents it — the double-buffer path.
|
||||
- [x] [library/runtime/display.zig](../library/runtime/runtime.zig) (+ barrel export of
|
||||
`display` and `display_protocol`): `info()` and `present()`, cached `.display`
|
||||
lookup with retry (model: block.zig).
|
||||
- [x] [init.zig](../system/services/init/init.zig): `"display"` added to `boot_services`.
|
||||
- [x] [build.zig](../build.zig): `display-protocol` module on the runtime; `display` exe
|
||||
via `addUserBinary`; packed into the initial-ramdisk; installed to
|
||||
`/system/services/display`.
|
||||
- [x] **Kernel fix the back buffer surfaced:** `mmap` was capped at 256 pages (1 MiB) by
|
||||
a fixed kernel-stack `frames` array. Rewrote `systemMmap` to map page-by-page with
|
||||
rollback (no scratch array) and raised the cap to 8192 pages (32 MiB) — enough for a
|
||||
4K back buffer. A real limitation met, exactly the kind this project chases.
|
||||
|
||||
**Gate:** boot; `init` spawns `display`; it logs `display: online {w}x{h}` and clears the
|
||||
screen to a colour **through the back buffer → present path** (double buffering proven —
|
||||
no direct-to-LFB drawing). Screenshot confirms.
|
||||
**Gate (met, automated):** `python3 test/qemu_test.py display-service` spawns the
|
||||
compositor and matches its own serial heartbeats — `display: online {w}x{h} pitch …`
|
||||
followed by `display: presented frame 0` — which it prints only after the whole
|
||||
claim → WC-map → back-buffer → clear → present chain succeeds (matched on serial like the
|
||||
fault cases, since a lone blocking service can't reschedule the in-kernel test context to
|
||||
poll). Regression-checked: `usermem`, `heap` (the `mmap` rewrite), `init` (the boot-list
|
||||
addition), and D1's `display` all still pass.
|
||||
|
||||
## D3 — Layer stack + compositor + damage present
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
//! User-space display client: talk to the display service (query the mode, and — from D3
|
||||
//! — create layers, draw, and present) without hand-rolling the IPC. The `runtime.block`
|
||||
//! shape: a cached `.display` lookup with a boot-race retry, then extern-struct request/
|
||||
//! reply marshalling. See system/services/display/ and docs/display.md.
|
||||
|
||||
const std = @import("std");
|
||||
const ipc = @import("ipc.zig");
|
||||
const system = @import("system.zig");
|
||||
const protocol = @import("display-protocol");
|
||||
|
||||
/// The display's current mode, as `info()` reports it.
|
||||
pub const Info = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
pitch: u32, // bytes per row (may exceed width*4; see docs/framebuffer.md)
|
||||
format: u32, // a device-abi DisplayFormat value (0 = rgbx, 1 = bgrx)
|
||||
};
|
||||
|
||||
/// The service endpoint, looked up once and cached.
|
||||
var handle: ?ipc.Handle = null;
|
||||
|
||||
/// Look up the display service, retrying while it comes up (a client races its
|
||||
/// registration at boot). Returns the endpoint, or null if it never appears.
|
||||
fn service() ?ipc.Handle {
|
||||
if (handle) |h| return h;
|
||||
var attempts: usize = 0;
|
||||
while (attempts < 100) : (attempts += 1) {
|
||||
if (ipc.lookup(.display)) |h| {
|
||||
handle = h;
|
||||
return h;
|
||||
}
|
||||
system.sleep(50);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Send one request, receive its reply; true on a zero status. `out` receives the reply
|
||||
/// so callers can read `info`/`layer` fields on success.
|
||||
fn transact(request: protocol.Request, out: *protocol.Reply) bool {
|
||||
const h = service() orelse return false;
|
||||
var req = request;
|
||||
var reply: [protocol.reply_size]u8 = undefined;
|
||||
const len = ipc.call(h, std.mem.asBytes(&req), &reply) catch return false;
|
||||
if (len < protocol.reply_size) return false;
|
||||
out.* = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
|
||||
return out.status == 0;
|
||||
}
|
||||
|
||||
/// The display's current mode, or null if the service never came up.
|
||||
pub fn info() ?Info {
|
||||
var reply: protocol.Reply = undefined;
|
||||
if (!transact(.{ .operation = @intFromEnum(protocol.Operation.info) }, &reply)) return null;
|
||||
return .{ .width = reply.width, .height = reply.height, .pitch = reply.pitch, .format = reply.format };
|
||||
}
|
||||
|
||||
/// Composite the dirty layers and flush the frame to the screen.
|
||||
pub fn present() bool {
|
||||
var reply: protocol.Reply = undefined;
|
||||
return transact(.{ .operation = @intFromEnum(protocol.Operation.present) }, &reply);
|
||||
}
|
||||
|
|
@ -46,6 +46,12 @@ pub const usb = @import("usb.zig");
|
|||
/// usb-storage). See library/runtime/block.zig.
|
||||
pub const block = @import("block.zig");
|
||||
|
||||
/// Display-service client: query the mode, and (from D3) create layers, draw, and
|
||||
/// present frames. See library/runtime/display.zig and system/services/display/.
|
||||
pub const display = @import("display.zig");
|
||||
/// The display wire protocol (shared with the display service and its clients).
|
||||
pub const display_protocol = @import("display-protocol");
|
||||
|
||||
/// The danos-native file API (open/read/write/list over the user-space VFS) — the
|
||||
/// layer danos programs use directly, and where the operations that later become
|
||||
/// `std.os.danos` are staged. See docs/zig-self-hosting.md.
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ pub const ServiceId = enum(u32) {
|
|||
usb_bus = 6, // the xHCI host-controller driver's transfer endpoint; USB class drivers look it up and `callCap`-open their device to get a private per-device transfer channel (docs/driver-model.md)
|
||||
block = 7, // a block-device driver (USB mass storage today): read/write of fixed-size blocks, the storage a filesystem sits on
|
||||
fat = 8, // the FAT filesystem server; the VFS mounts it and forwards paths under its mount point (/mnt/usb) to it
|
||||
display = 9, // the display service: owns the framebuffer, composites a layer stack, presents frames (docs/display.md)
|
||||
_,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -81,9 +81,11 @@ pub const device_arena_end: u64 = device_arena_base + (4 << 30);
|
|||
pub const dma_arena_base: u64 = 0x0000_7200_0000_0000;
|
||||
pub const dma_arena_end: u64 = dma_arena_base + (256 << 20); // 256 MiB per process
|
||||
|
||||
/// Largest single `mmap` grant, in pages (1 MiB). The user heap grows in small
|
||||
/// chunks, so this bound is generous; it also caps the frame scratch array below.
|
||||
const maximum_mmap_pages = 256;
|
||||
/// Largest single `mmap` grant, in pages (32 MiB). Big enough for a display service's
|
||||
/// back buffer at up to 4K (3840x2160x4 ≈ 8100 pages); the user heap otherwise grows in
|
||||
/// small chunks. `systemMmap` maps page by page with rollback, so this is only a sanity
|
||||
/// bound (and an overflow guard on the page count), not the size of any scratch array.
|
||||
const maximum_mmap_pages = 8192;
|
||||
|
||||
/// Ceiling on a process's argv entries, including argv[0]. Arguments are spawn
|
||||
/// parameters ("you are the driver for device 12"), not bulk data — IPC carries
|
||||
|
|
@ -1038,21 +1040,26 @@ fn systemMmap(state: *architecture.CpuState) void {
|
|||
const base = t.heap_next;
|
||||
if (base + pages * page_size > heap_arena_end) return fail(state); // arena exhausted
|
||||
|
||||
// Reserve all frames up front so a mid-way exhaustion rolls back cleanly
|
||||
// (no partially-mapped grant leaks into the address space).
|
||||
var frames: [maximum_mmap_pages]u64 = undefined;
|
||||
var got: usize = 0;
|
||||
while (got < pages) : (got += 1) {
|
||||
frames[got] = pmm.alloc() orelse {
|
||||
for (frames[0..got]) |f| pmm.free(f);
|
||||
// Map page by page. On mid-way frame exhaustion, roll back the pages already mapped
|
||||
// (unmap + free) so no partial grant leaks into the address space — the same
|
||||
// all-or-nothing guarantee as before, but without a fixed scratch array, so the
|
||||
// per-call size can be a multi-MiB framebuffer.
|
||||
var mapped: usize = 0;
|
||||
while (mapped < pages) : (mapped += 1) {
|
||||
const frame = pmm.alloc() orelse {
|
||||
var i: usize = 0;
|
||||
while (i < mapped) : (i += 1) {
|
||||
const va = base + i * page_size;
|
||||
if (architecture.translate(t.aspace, va)) |physical| {
|
||||
architecture.unmapUserPageInto(t.aspace, va);
|
||||
pmm.free(physical);
|
||||
}
|
||||
}
|
||||
return fail(state);
|
||||
};
|
||||
}
|
||||
|
||||
for (frames[0..pages], 0..) |frame, i| {
|
||||
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(frame));
|
||||
@memset(destination[0..page_size], 0); // hand out zeroed memory
|
||||
architecture.mapUserPageInto(t.aspace, base + i * page_size, frame, true, false); // RW + NX
|
||||
architecture.mapUserPageInto(t.aspace, base + mapped * page_size, frame, true, false); // RW + NX
|
||||
}
|
||||
t.heap_next = base + pages * page_size;
|
||||
architecture.setSystemCallResult(state, base);
|
||||
|
|
|
|||
|
|
@ -97,6 +97,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||
ioPortTest();
|
||||
} else if (eql(case, "display")) {
|
||||
displayTest(boot_information);
|
||||
} else if (eql(case, "display-service")) {
|
||||
displayServiceTest(boot_information);
|
||||
} else if (eql(case, "clock")) {
|
||||
clockTest();
|
||||
} else if (eql(case, "smp")) {
|
||||
|
|
@ -2307,6 +2309,41 @@ fn inputTest(boot_information: *const BootInformation) void {
|
|||
result();
|
||||
}
|
||||
|
||||
/// D2 — the display service comes up. Spawn it from the initial_ramdisk; it claims the
|
||||
/// framebuffer the kernel seeded (D1), maps it write-combining, allocates a cacheable
|
||||
/// back buffer, and proves the double-buffer path by clearing that buffer and presenting
|
||||
/// it. Its `display: online WxH` + `display: presented frame 0` heartbeats are the
|
||||
/// markers — seeing them proves a user-space compositor took the framebuffer and pushed
|
||||
/// a whole composed frame to the screen, without ever drawing straight to the LFB.
|
||||
fn displayServiceTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: display-service\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;
|
||||
};
|
||||
|
||||
// Spawn the compositor and hand it the core. Its own serial heartbeats — `display:
|
||||
// online WxH` and `display: presented frame 0` — are what the harness matches (it
|
||||
// reads serial directly, like the fault cases). We don't poll for them in-kernel: a
|
||||
// single service that comes up and blocks doesn't reschedule this bring-up context
|
||||
// (there is no other runnable task to bounce control back through), so the honest
|
||||
// observation point is the service's output itself, not a check() proxy here.
|
||||
if (!spawnNamed(rd, "display")) {
|
||||
log("display-service: could not spawn the display service\n", .{});
|
||||
result();
|
||||
return;
|
||||
}
|
||||
scheduler.setPriority(1); // below the service, so it runs and comes up first
|
||||
while (true) scheduler.yield();
|
||||
}
|
||||
|
||||
/// Process arguments, end to end: spawn args-echo bare (its argv[0] is the
|
||||
/// initial-ramdisk name). Instance 1 sees argc == 1 and respawns itself through
|
||||
/// `system_spawn` with the extra arguments "alpha beta-42" — the syscall argument
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
//! /system/services/display — the display service (docs/display.md). A ring-3 process
|
||||
//! that claims the framebuffer the kernel seeded (docs/display-plan.md D1), owns it as a
|
||||
//! **write-combining front buffer**, composites into a **cacheable back buffer**, and
|
||||
//! presents finished frames — the GUI track's compositor, the sibling of the input
|
||||
//! service. It is reached by name over `ServiceId.display`.
|
||||
//!
|
||||
//! This is the D2 skeleton: it comes up, claims + maps the framebuffer, allocates the
|
||||
//! back buffer, and proves the double-buffer path by clearing the back buffer and
|
||||
//! presenting it. The layer stack, damage tracking, and per-layer drawing arrive in D3;
|
||||
//! for now `info` and a whole-screen `present` are the live operations.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
|
||||
const protocol = runtime.display_protocol;
|
||||
const ipc = runtime.ipc;
|
||||
const system = runtime.system;
|
||||
const device = runtime.device;
|
||||
|
||||
/// The claimed framebuffer and its off-screen twin. The front buffer is the LFB —
|
||||
/// write-combining, so it is **only ever written**, never read; all compositing happens
|
||||
/// in the cacheable back buffer, which is then streamed to the front (docs/display.md).
|
||||
const Display = struct {
|
||||
device_id: u64,
|
||||
front: [*]volatile u8, // the LFB (write-combining)
|
||||
back: [*]u8, // cacheable, same geometry
|
||||
width: u32,
|
||||
height: u32,
|
||||
pitch: u32, // bytes per row (shared by both buffers)
|
||||
format: u32, // a device-abi DisplayFormat value
|
||||
frames: u64 = 0,
|
||||
};
|
||||
|
||||
var display: Display = undefined;
|
||||
|
||||
/// Enumeration buffer kept off the stack — a `DeviceDescriptor` is large, and this
|
||||
/// service only ever needs one scan.
|
||||
var device_table: [64]device.DeviceDescriptor = undefined;
|
||||
|
||||
/// The framebuffer node the kernel seeded (`DeviceClass.display`), or null if none.
|
||||
fn findDisplay() ?device.DeviceDescriptor {
|
||||
const total = device.enumerate(&device_table);
|
||||
const n = @min(total, device_table.len);
|
||||
for (device_table[0..n]) |d| {
|
||||
if (d.class == @intFromEnum(device.DeviceClass.display)) return d;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn initialise(endpoint: ipc.Handle) bool {
|
||||
_ = endpoint;
|
||||
|
||||
// Find the framebuffer, retrying while device discovery catches up with our spawn.
|
||||
var tries: u32 = 0;
|
||||
const found = while (tries < 100) : (tries += 1) {
|
||||
if (findDisplay()) |d| break d;
|
||||
system.sleep(50);
|
||||
} else {
|
||||
_ = system.write("display: no framebuffer device (headless?)\n");
|
||||
return false; // clean exit: nothing to drive
|
||||
};
|
||||
|
||||
if (!device.claim(found.id)) {
|
||||
_ = system.write("display: could not claim the framebuffer\n");
|
||||
return false;
|
||||
}
|
||||
// Resource 0 is the framebuffer memory window; the kernel maps it write-combining
|
||||
// because the resource carries that flag (docs/display-plan.md D1).
|
||||
const front_base = device.mmioMap(found.id, 0) orelse {
|
||||
_ = system.write("display: could not map the framebuffer\n");
|
||||
return false;
|
||||
};
|
||||
|
||||
const geometry = found.display;
|
||||
const size = @as(usize, geometry.height) * geometry.pitch;
|
||||
const back_base = system.mmap(size, system.PROT_READ | system.PROT_WRITE);
|
||||
if (system.mmapFailed(back_base)) {
|
||||
_ = system.write("display: could not allocate the back buffer\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
display = .{
|
||||
.device_id = found.id,
|
||||
.front = @ptrFromInt(front_base),
|
||||
.back = @ptrFromInt(back_base),
|
||||
.width = geometry.width,
|
||||
.height = geometry.height,
|
||||
.pitch = geometry.pitch,
|
||||
.format = geometry.format,
|
||||
};
|
||||
|
||||
// Prove the pipeline end to end: compose a cleared frame in the back buffer, then
|
||||
// present it to the screen. Nothing is drawn directly to the LFB.
|
||||
clear(0x0020_3048); // a dark slate; exact channel order is a D3 concern
|
||||
present();
|
||||
|
||||
var line: [96]u8 = undefined;
|
||||
_ = system.write(std.fmt.bufPrint(&line, "display: online {d}x{d} pitch {d} format {d}\n", .{
|
||||
display.width, display.height, display.pitch, display.format,
|
||||
}) catch "display: online\n");
|
||||
_ = system.write("display: presented frame 0\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Fill the whole back buffer with `colour`. Cacheable memory, so this is fast; touch
|
||||
/// only the visible width, stepping rows by `pitch` (which may exceed width*4).
|
||||
fn clear(colour: u32) void {
|
||||
var y: u32 = 0;
|
||||
while (y < display.height) : (y += 1) {
|
||||
const row: [*]u32 = @ptrCast(@alignCast(display.back + @as(usize, y) * display.pitch));
|
||||
var x: u32 = 0;
|
||||
while (x < display.width) : (x += 1) row[x] = colour;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whole-screen present: stream the back buffer to the write-combining front buffer, row
|
||||
/// by row. Sequential writes are what WC memory wants; we never read the front buffer.
|
||||
/// (D3 replaces this with a damage-driven present that copies only changed rectangles.)
|
||||
fn present() void {
|
||||
var y: u32 = 0;
|
||||
while (y < display.height) : (y += 1) {
|
||||
const src: [*]const u32 = @ptrCast(@alignCast(display.back + @as(usize, y) * display.pitch));
|
||||
const dst: [*]volatile u32 = @ptrCast(@alignCast(display.front + @as(usize, y) * display.pitch));
|
||||
var x: u32 = 0;
|
||||
while (x < display.width) : (x += 1) dst[x] = src[x];
|
||||
}
|
||||
display.frames += 1;
|
||||
}
|
||||
|
||||
fn writeReply(reply: []u8, value: protocol.Reply) usize {
|
||||
const bytes = std.mem.asBytes(&value);
|
||||
@memcpy(reply[0..bytes.len], bytes);
|
||||
return bytes.len;
|
||||
}
|
||||
|
||||
fn ok(reply: []u8) usize {
|
||||
return writeReply(reply, .{ .status = 0 });
|
||||
}
|
||||
|
||||
fn fail(reply: []u8) usize {
|
||||
return writeReply(reply, .{ .status = -1 });
|
||||
}
|
||||
|
||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize {
|
||||
_ = sender;
|
||||
_ = capability;
|
||||
if (message.len < protocol.request_size) return fail(reply);
|
||||
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
||||
// Switch on the raw operation value — an out-of-range one must fail cleanly, not
|
||||
// panic an `@enumFromInt`.
|
||||
switch (request.operation) {
|
||||
@intFromEnum(protocol.Operation.info) => return writeReply(reply, .{
|
||||
.status = 0,
|
||||
.width = display.width,
|
||||
.height = display.height,
|
||||
.pitch = display.pitch,
|
||||
.format = display.format,
|
||||
}),
|
||||
@intFromEnum(protocol.Operation.present) => {
|
||||
present();
|
||||
return ok(reply);
|
||||
},
|
||||
// The layer stack and per-layer drawing land in D3; until then those operations
|
||||
// are unimplemented rather than silently accepted.
|
||||
else => return fail(reply),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
runtime.service.run(protocol.message_maximum, .{
|
||||
.service = .display,
|
||||
.init = initialise,
|
||||
.on_message = onMessage,
|
||||
});
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
//! The display wire protocol — what a client says to the display service over its
|
||||
//! well-known `.display` endpoint. extern-struct messages with an `Operation` tag, the
|
||||
//! same shape as block/vfs/input protocols. The compositor owns the framebuffer and an
|
||||
//! ordered stack of **layers**; a client creates layers, draws into them with these
|
||||
//! operations, marks damage, and asks for a `present`. v1 surfaces are server-owned (a
|
||||
//! client draws by command); shared-memory surfaces are a later milestone (docs/display.md).
|
||||
|
||||
pub const Operation = enum(u32) {
|
||||
/// info() -> { width, height, pitch, format }: the display's current mode.
|
||||
info = 0,
|
||||
/// create_layer(x, y, width, height, z) -> { layer }: a new server-owned surface.
|
||||
create_layer = 1,
|
||||
/// configure_layer(layer, x, y, z, visible): move, restack, show, or hide a layer.
|
||||
configure_layer = 2,
|
||||
/// destroy_layer(layer): release a layer.
|
||||
destroy_layer = 3,
|
||||
/// fill_rect(layer, x, y, width, height, colour): fill a rectangle of a layer.
|
||||
fill_rect = 4,
|
||||
/// blit_tile(layer, x, y, width, height, <inline pixels>): copy a small pixel tile in.
|
||||
blit_tile = 5,
|
||||
/// damage(layer, x, y, width, height): mark a region dirty for the next present.
|
||||
damage = 6,
|
||||
/// present(): composite the dirty layers and flush to the screen.
|
||||
present = 7,
|
||||
};
|
||||
|
||||
/// The fixed request header. A `blit_tile`'s pixel payload (width*height 32-bit pixels)
|
||||
/// follows this header inline in the same message, up to `maximum_payload`.
|
||||
pub const Request = extern struct {
|
||||
operation: u32,
|
||||
layer: u32 = 0, // create/configure/destroy/fill/blit/damage: the target layer
|
||||
x: u32 = 0,
|
||||
y: u32 = 0,
|
||||
width: u32 = 0,
|
||||
height: u32 = 0,
|
||||
z: u32 = 0, // create_layer / configure_layer: stacking order (higher = in front)
|
||||
colour: u32 = 0, // fill_rect: the fill colour (native pixel value)
|
||||
visible: u32 = 1, // configure_layer: 0 hides the layer
|
||||
reserved: u32 = 0,
|
||||
};
|
||||
|
||||
pub const Reply = extern struct {
|
||||
status: i32, // 0 on success, negative on failure
|
||||
reserved: u32 = 0,
|
||||
// info():
|
||||
width: u32 = 0,
|
||||
height: u32 = 0,
|
||||
pitch: u32 = 0,
|
||||
format: u32 = 0, // a device-abi DisplayFormat value (0 = rgbx, 1 = bgrx)
|
||||
// create_layer():
|
||||
layer: u32 = 0,
|
||||
reserved2: u32 = 0,
|
||||
};
|
||||
|
||||
/// Sized to hold a modest `blit_tile` payload (a cursor / glyph-cell tile) inline on top
|
||||
/// of the header, not just the fixed messages — the compositor's receive/reply buffers
|
||||
/// (`runtime.service.run`) are this big.
|
||||
pub const message_maximum: usize = 4096;
|
||||
pub const request_size: usize = @sizeOf(Request);
|
||||
pub const reply_size: usize = @sizeOf(Reply);
|
||||
pub const maximum_payload: usize = message_maximum - request_size;
|
||||
|
|
@ -30,7 +30,7 @@ const log_path = "/mnt/usb/DANOS.LOG";
|
|||
/// microkernel keeps such choices in user space, not the kernel. Drivers are absent
|
||||
/// on purpose: the device manager owns those. (A future init reads this from a
|
||||
/// manifest under /system/services instead of a hardcoded list.)
|
||||
const boot_services = [_][]const u8{ "vfs", "input", "device-manager", "fat" };
|
||||
const boot_services = [_][]const u8{ "vfs", "input", "device-manager", "fat", "display" };
|
||||
|
||||
var children: [boot_services.len]u32 = .{0} ** boot_services.len;
|
||||
var child_count: usize = 0;
|
||||
|
|
|
|||
|
|
@ -171,6 +171,13 @@ CASES = [
|
|||
{"name": "display",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# Display service (D2): the user-space compositor claims the framebuffer, allocates a
|
||||
# cacheable back buffer, clears it, and presents that composed frame — proving the
|
||||
# double-buffer path. Matched on the service's own heartbeats (it prints them only
|
||||
# after the whole claim -> map(WC) -> back-buffer -> present chain succeeds).
|
||||
{"name": "display-service",
|
||||
"expect": r"display: online \d+x\d+ pitch \d+[\s\S]*display: presented frame 0",
|
||||
"fail": r"display: could not|CPU EXCEPTION|KERNEL PANIC"},
|
||||
# Monotonic clock (clock() syscall source): calibrated, advancing, never backwards.
|
||||
{"name": "clock",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
|
|
|
|||
Loading…
Reference in New Issue