//! The compositor's **scanout backend** — how a finished frame reaches the panel //! (docs/display-v2.md). The compositor composes its layer stack into the backend's //! cacheable `surface()` and calls `present(damage)`; everything device-specific lives //! here. Today there is one backend, `Gop` — the firmware framebuffer: a cacheable back //! buffer streamed write-combining to the linear framebuffer. A native virtio-gpu backend //! slots in beside it later (V4); the compositor never learns which is active. const std = @import("std"); const runtime = @import("runtime"); const compositor = @import("compositor.zig"); const system = runtime.system; const device = runtime.device; const ipc = runtime.ipc; const scanout_protocol = runtime.scanout_protocol; const Rect = compositor.Rect; const Surface = compositor.Surface; /// The current display mode, as a backend reports it. pub const Info = struct { width: u32, height: u32, pitch: u32, format: u32 }; /// Enumeration scratch — a `DeviceDescriptor` is large, and only one scan is ever needed. var device_table: [64]device.DeviceDescriptor = undefined; /// The GOP framebuffer backend: claims the kernel-seeded `display` device, maps the linear /// framebuffer write-combining as the front buffer, and keeps a cacheable back buffer of /// the same geometry as the compose target. `present` streams the damaged rectangle from /// the back buffer to the LFB (sequential WC writes; the LFB is never read). No mode-set, /// no vsync — the portable floor (docs/display-v2.md). pub const Gop = struct { device_id: u64, front: [*]volatile u8, // the LFB (write-combining) back: [*]u8, // cacheable compose target, same geometry width: u32, height: u32, pitch: u32, format: u32, /// The framebuffer's id and geometry, captured together. `findDisplay` reads these out of /// the enumeration table and returns them by value, so the caller never re-reads the table /// across later syscalls (`device_enumerate` writes the whole table straight into this /// process's memory; reading a descriptor's tail again after other syscalls have run is a /// window we simply avoid by copying the few fields we need up front). const Found = struct { id: u64, width: u32, height: u32, pitch: u32, format: u32 }; /// The first `display`-class device with a *valid* (non-zero) geometry, or null. A zero /// geometry is treated as "not ready yet" so the caller retries — a real framebuffer always /// has a non-zero width, height, and pitch. fn findDisplay() ?Found { 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)) continue; if (d.display.width == 0 or d.display.height == 0 or d.display.pitch == 0) continue; return .{ .id = d.id, .width = d.display.width, .height = d.display.height, .pitch = d.display.pitch, .format = d.display.format }; } return null; } /// Claim the framebuffer (retrying while discovery catches up), map the LFB, and /// allocate the back buffer. Null if there is no framebuffer or a mapping fails. pub fn init() ?Gop { var tries: u32 = 0; const found = while (tries < 100) : (tries += 1) { if (findDisplay()) |f| break f; system.sleep(50); } else { _ = system.write("display: no framebuffer device (headless?)\n"); return null; }; if (!device.claim(found.id)) { _ = system.write("display: could not claim the framebuffer\n"); return null; } // 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 null; }; const size = @as(usize, found.height) * found.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 null; } return .{ .device_id = found.id, .front = @ptrFromInt(front_base), .back = @ptrFromInt(back_base), .width = found.width, .height = found.height, .pitch = found.pitch, .format = found.format, }; } pub fn info(self: *const Gop) Info { return .{ .width = self.width, .height = self.height, .pitch = self.pitch, .format = self.format }; } /// The cacheable compose target (the back buffer). pub fn surface(self: *const Gop) Surface { return .{ .pixels = @ptrCast(@alignCast(self.back)), .stride = self.pitch / 4, // pitch is bytes; a 32-bpp row is pitch/4 pixels .width = self.width, .height = self.height, }; } /// Stream the damaged rectangle from the back buffer to the write-combining LFB, row by /// row (sequential writes — what WC memory wants; the LFB is never read). pub fn present(self: *const Gop, damage: Rect) void { const c = damage.intersect(.{ .x = 0, .y = 0, .w = @intCast(self.width), .h = @intCast(self.height) }); if (c.isEmpty()) return; var y: i32 = c.y; while (y < c.bottom()) : (y += 1) { const off = @as(usize, @intCast(y)) * self.pitch; const src: [*]const u32 = @ptrCast(@alignCast(self.back + off)); const dst: [*]volatile u32 = @ptrCast(@alignCast(self.front + off)); var x: i32 = c.x; while (x < c.right()) : (x += 1) dst[@intCast(x)] = src[@intCast(x)]; } } }; /// A display mode the native backend can switch to. pub const Mode = scanout_protocol.Mode; /// The native virtio-gpu backend: the compositor composes into a **shared** scanout surface /// (an `shm` region the driver created and handed over) and `present` asks the driver to put /// a frame on the panel over its `.scanout` endpoint. Unlike GOP there is no local copy — the /// surface *is* the device's resource backing, so compositing writes land straight where the /// driver transfers-and-flushes from (x86 DMA is cache-coherent, so the cacheable shared pages /// need no explicit flush). Built by the display service when a driver announces (V4). The /// surface is sized to the driver's largest mode, so `stride` (its row width) is fixed while /// `width`/`height` — the active mode — change under `setMode` (V5). pub const VirtioGpu = struct { pixels: [*]u32, // the shared scanout surface, mapped into the compositor stride: u32, // the surface's row stride in pixels (the driver's max mode width) — fixed width: u32, // the active mode height: u32, format: u32, scanout: ipc.Handle, // the driver's present + mode channel (looked up on `.scanout`) pub fn info(self: *const VirtioGpu) Info { return .{ .width = self.width, .height = self.height, .pitch = self.stride * 4, .format = self.format }; } pub fn surface(self: *const VirtioGpu) Surface { return .{ .pixels = self.pixels, .stride = self.stride, .width = self.width, .height = self.height }; } /// Ask the driver to present. The composited pixels are already in the shared surface, so /// this is a single request over `.scanout`; the driver transfers + fenced-flushes. pub fn present(self: *const VirtioGpu, damage: Rect) void { _ = damage; var request = scanout_protocol.Request{ .operation = @intFromEnum(scanout_protocol.Operation.present), .width = self.width, .height = self.height, }; var reply: [scanout_protocol.reply_size]u8 = undefined; _ = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch {}; } /// Fill `out` with the driver's offered modes; returns how many were written. pub fn modes(self: *const VirtioGpu, out: []Mode) usize { var request = scanout_protocol.Request{ .operation = @intFromEnum(scanout_protocol.Operation.get_modes) }; var reply: [scanout_protocol.modes_reply_size]u8 = undefined; const n = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch return 0; if (n < scanout_protocol.modes_reply_size) return 0; const answer = std.mem.bytesToValue(scanout_protocol.ModesReply, reply[0..scanout_protocol.modes_reply_size]); if (answer.status != 0) return 0; const count = @min(@min(answer.count, scanout_protocol.max_modes), out.len); for (0..count) |i| out[i] = answer.modes[i]; return count; } /// Change the scanout resolution. On success the active `width`/`height` update (the shared /// surface — sized to the max mode — is unchanged, so `stride` stays put). pub fn setMode(self: *VirtioGpu, w: u32, h: u32) bool { if (w == 0 or h == 0 or w > self.stride) return false; var request = scanout_protocol.Request{ .operation = @intFromEnum(scanout_protocol.Operation.set_mode), .width = w, .height = h, }; var reply: [scanout_protocol.reply_size]u8 = undefined; const n = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch return false; if (n < scanout_protocol.reply_size) return false; if (std.mem.bytesToValue(scanout_protocol.Reply, reply[0..scanout_protocol.reply_size]).status != 0) return false; self.width = w; self.height = h; return true; } }; /// The pluggable scanout backend. A tagged union so the compositor holds one value and /// dispatches without caring which is active; the `virtio` native backend joins `gop` at V4. pub const Backend = union(enum) { gop: Gop, virtio: VirtioGpu, pub fn info(self: *const Backend) Info { return switch (self.*) { inline else => |*b| b.info(), }; } pub fn surface(self: *const Backend) Surface { return switch (self.*) { inline else => |*b| b.surface(), }; } pub fn present(self: *const Backend, damage: Rect) void { switch (self.*) { inline else => |*b| b.present(damage), } } /// The modes this backend can switch to (none for GOP); returns how many were written. pub fn modes(self: *const Backend, out: []Mode) usize { return switch (self.*) { .virtio => |*v| v.modes(out), .gop => 0, }; } /// Change the resolution; false if this backend can't mode-set or the mode was refused. pub fn setMode(self: *Backend, w: u32, h: u32) bool { return switch (self.*) { .virtio => |*v| v.setMode(w, h), .gop => false, }; } /// Whether this backend supports runtime mode-setting (GOP: no; virtio-gpu: yes, V5). pub fn canModeSet(self: *const Backend) bool { return switch (self.*) { .gop => false, .virtio => true, }; } /// Whether this backend has a vblank/fence for tear-free present (virtio-gpu: yes, V5 — every /// flush is fenced, so the device signals completion when the frame is actually on screen). pub fn hasVsync(self: *const Backend) bool { return switch (self.*) { .gop => false, .virtio => true, }; } }; /// Which backend to use. The pure selection *decision* is `chooseKind`; `select` below /// binds it to the (syscall-bound) bring-up. pub const Kind = enum { gop, virtio }; /// The selection decision, factored out of bring-up so it stays pure and host-testable: /// prefer a native driver when one has announced itself (docs/display-v2.md V4), else the /// GOP floor. Trivial today; it grows real inputs when native detection lands. pub fn chooseKind(native_available: bool) Kind { return if (native_available) .virtio else .gop; } /// Pick and bring up the best available backend. Today the GOP framebuffer is the only one /// (`chooseKind(false)` → `.gop`), so this is `Gop.init()`. V4 adds the native-if-present /// branch, with GOP as the floor. pub fn select() ?Backend { return switch (chooseKind(false)) { .gop => .{ .gop = Gop.init() orelse return null }, .virtio => unreachable, // no native detection yet (V4) }; } test "selection prefers native when present, else the gop floor" { try std.testing.expectEqual(Kind.gop, chooseKind(false)); try std.testing.expectEqual(Kind.virtio, chooseKind(true)); }