danos/system/services/display/backend.zig

175 lines
7.0 KiB
Zig

//! 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));
}