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:
@@ -348,6 +348,13 @@ pub fn build(b: *std.Build) void {
|
|||||||
// The block protocol, so runtime.block (the block-device client) can speak it.
|
// The block protocol, so runtime.block (the block-device client) can speak it.
|
||||||
runtime_module.addImport("block-protocol", block_protocol_module);
|
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).
|
// The power protocol: system power's domain-named surface (docs/power.md).
|
||||||
const power_protocol_module = b.addModule("power-protocol", .{
|
const power_protocol_module = b.addModule("power-protocol", .{
|
||||||
.root_source_file = b.path("system/services/power/protocol.zig"),
|
.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
|
// 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");
|
||||||
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 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");
|
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
|
// 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.addFileArg(fat_exe.getEmittedBin());
|
||||||
mk_run.addArg("fat-test");
|
mk_run.addArg("fat-test");
|
||||||
mk_run.addFileArg(fat_test_exe.getEmittedBin());
|
mk_run.addFileArg(fat_test_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("display");
|
||||||
|
mk_run.addFileArg(display_exe.getEmittedBin());
|
||||||
mk_run.addArg("pci-bus");
|
mk_run.addArg("pci-bus");
|
||||||
mk_run.addFileArg(pci_bus_exe.getEmittedBin());
|
mk_run.addFileArg(pci_bus_exe.getEmittedBin());
|
||||||
mk_run.addArg("crash-test");
|
mk_run.addArg("crash-test");
|
||||||
@@ -563,6 +573,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
.{ usb_hid_mouse_exe, "system/drivers" },
|
.{ usb_hid_mouse_exe, "system/drivers" },
|
||||||
.{ usb_storage_exe, "system/drivers" },
|
.{ usb_storage_exe, "system/drivers" },
|
||||||
.{ fat_exe, "system/services" },
|
.{ fat_exe, "system/services" },
|
||||||
|
.{ display_exe, "system/services" },
|
||||||
.{ log_flush_exe, "system/services" },
|
.{ log_flush_exe, "system/services" },
|
||||||
}) |entry| {
|
}) |entry| {
|
||||||
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
|
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
|
||||||
|
|||||||
+26
-19
@@ -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`,
|
Regression-checked: `discovery`, `ioport`, `claim-release`, `supervision`, `device-list`,
|
||||||
`device-manager` all still pass with the +1 device in the table.
|
`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.
|
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`
|
configure_layer, destroy_layer, fill_rect, blit_tile, damage, present }`; `extern`
|
||||||
`Request`/`Reply`; `message_maximum`/`request_size`/`reply_size` consts. (Model:
|
`Request`/`Reply`; size + `maximum_payload` consts. (Model: block/protocol.zig.)
|
||||||
[block/protocol.zig](../system/services/block/protocol.zig).)
|
- [x] [abi.zig](../system/abi.zig): `ServiceId.display = 9`.
|
||||||
- [ ] [abi.zig](../system/abi.zig): `ServiceId.display = 9`.
|
- [x] `system/services/display/display.zig`: `main` → enumerate + claim + WC-map the LFB
|
||||||
- [ ] `system/services/display/display.zig`: `main` → enumerate + claim + WC-map the LFB
|
(front) → `mmap` a cacheable back buffer of `height*pitch` → `runtime.service.run`.
|
||||||
(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
|
||||||
.{ .service = .display, .init, .on_message })`. Implement `info` and a whole-screen
|
until D3. Init clears the back buffer and presents it — the double-buffer path.
|
||||||
`present` (back → front) first.
|
- [x] [library/runtime/display.zig](../library/runtime/runtime.zig) (+ barrel export of
|
||||||
- [ ] [library/runtime/display.zig](../library/runtime/runtime.zig) (+ export in
|
`display` and `display_protocol`): `info()` and `present()`, cached `.display`
|
||||||
`runtime.zig`): `info()`, and a stub `present()`. Cached `.display` lookup with
|
lookup with retry (model: block.zig).
|
||||||
retry (model: [block.zig](../library/runtime/block.zig)).
|
- [x] [init.zig](../system/services/init/init.zig): `"display"` added to `boot_services`.
|
||||||
- [ ] [init.zig](../system/services/init/init.zig): add `"display"` to `boot_services`.
|
- [x] [build.zig](../build.zig): `display-protocol` module on the runtime; `display` exe
|
||||||
- [ ] [build.zig](../build.zig): `display-protocol` module; `display` exe via
|
via `addUserBinary`; packed into the initial-ramdisk; installed to
|
||||||
`addUserBinary`; import the protocol into `runtime` and into the exe; pack into the
|
`/system/services/display`.
|
||||||
initial-ramdisk; install 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
|
**Gate (met, automated):** `python3 test/qemu_test.py display-service` spawns the
|
||||||
screen to a colour **through the back buffer → present path** (double buffering proven —
|
compositor and matches its own serial heartbeats — `display: online {w}x{h} pitch …`
|
||||||
no direct-to-LFB drawing). Screenshot confirms.
|
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
|
## 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.
|
/// usb-storage). See library/runtime/block.zig.
|
||||||
pub const block = @import("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
|
/// 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
|
/// layer danos programs use directly, and where the operations that later become
|
||||||
/// `std.os.danos` are staged. See docs/zig-self-hosting.md.
|
/// `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)
|
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
|
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
|
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)
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+21
-14
@@ -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_base: u64 = 0x0000_7200_0000_0000;
|
||||||
pub const dma_arena_end: u64 = dma_arena_base + (256 << 20); // 256 MiB per process
|
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
|
/// Largest single `mmap` grant, in pages (32 MiB). Big enough for a display service's
|
||||||
/// chunks, so this bound is generous; it also caps the frame scratch array below.
|
/// back buffer at up to 4K (3840x2160x4 ≈ 8100 pages); the user heap otherwise grows in
|
||||||
const maximum_mmap_pages = 256;
|
/// 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
|
/// 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
|
/// 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;
|
const base = t.heap_next;
|
||||||
if (base + pages * page_size > heap_arena_end) return fail(state); // arena exhausted
|
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
|
// Map page by page. On mid-way frame exhaustion, roll back the pages already mapped
|
||||||
// (no partially-mapped grant leaks into the address space).
|
// (unmap + free) so no partial grant leaks into the address space — the same
|
||||||
var frames: [maximum_mmap_pages]u64 = undefined;
|
// all-or-nothing guarantee as before, but without a fixed scratch array, so the
|
||||||
var got: usize = 0;
|
// per-call size can be a multi-MiB framebuffer.
|
||||||
while (got < pages) : (got += 1) {
|
var mapped: usize = 0;
|
||||||
frames[got] = pmm.alloc() orelse {
|
while (mapped < pages) : (mapped += 1) {
|
||||||
for (frames[0..got]) |f| pmm.free(f);
|
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);
|
return fail(state);
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
for (frames[0..pages], 0..) |frame, i| {
|
|
||||||
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(frame));
|
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(frame));
|
||||||
@memset(destination[0..page_size], 0); // hand out zeroed memory
|
@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;
|
t.heap_next = base + pages * page_size;
|
||||||
architecture.setSystemCallResult(state, base);
|
architecture.setSystemCallResult(state, base);
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||||||
ioPortTest();
|
ioPortTest();
|
||||||
} else if (eql(case, "display")) {
|
} else if (eql(case, "display")) {
|
||||||
displayTest(boot_information);
|
displayTest(boot_information);
|
||||||
|
} else if (eql(case, "display-service")) {
|
||||||
|
displayServiceTest(boot_information);
|
||||||
} else if (eql(case, "clock")) {
|
} else if (eql(case, "clock")) {
|
||||||
clockTest();
|
clockTest();
|
||||||
} else if (eql(case, "smp")) {
|
} else if (eql(case, "smp")) {
|
||||||
@@ -2307,6 +2309,41 @@ fn inputTest(boot_information: *const BootInformation) void {
|
|||||||
result();
|
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
|
/// 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
|
/// initial-ramdisk name). Instance 1 sees argc == 1 and respawns itself through
|
||||||
/// `system_spawn` with the extra arguments "alpha beta-42" — the syscall argument
|
/// `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
|
/// 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
|
/// on purpose: the device manager owns those. (A future init reads this from a
|
||||||
/// manifest under /system/services instead of a hardcoded list.)
|
/// 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 children: [boot_services.len]u32 = .{0} ** boot_services.len;
|
||||||
var child_count: usize = 0;
|
var child_count: usize = 0;
|
||||||
|
|||||||
@@ -171,6 +171,13 @@ CASES = [
|
|||||||
{"name": "display",
|
{"name": "display",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"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.
|
# Monotonic clock (clock() syscall source): calibrated, advancing, never backwards.
|
||||||
{"name": "clock",
|
{"name": "clock",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
|
|||||||
Reference in New Issue
Block a user