175 lines
7.0 KiB
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));
|
|
}
|