//! 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 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, 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; } /// 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()) |d| break d; 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 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 null; } return .{ .device_id = found.id, .front = @ptrFromInt(front_base), .back = @ptrFromInt(back_base), .width = geometry.width, .height = geometry.height, .pitch = geometry.pitch, .format = geometry.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)]; } } }; /// The pluggable scanout backend. A tagged union so the compositor holds one value and /// dispatches without caring which is active; a `virtio` variant joins `gop` at V4. pub const Backend = union(enum) { gop: Gop, 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), } } /// Whether this backend supports runtime mode-setting (GOP: no; a native driver: yes). pub fn canModeSet(self: *const Backend) bool { return switch (self.*) { .gop => false, }; } /// Whether this backend has a vblank/fence for tear-free present (GOP: no). pub fn hasVsync(self: *const Backend) bool { return switch (self.*) { .gop => false, }; } }; /// 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)); }