display: pace the frame clock by the panel's EDID refresh rate
Both EDID moments the system has are now captured and carried to the compositor's frame clock: - EFI: the loader derives refresh from the preferred detailed timing (pixel clock / total pixels) while GOP is still alive — the only moment it is readable — and hands it through the boot handoff into the display0 node's DisplayInfo (new refresh_hz field, 0 = unknown). - GPU: the virtio-gpu driver derives the same figure from its own EDID read and carries it in the attach_scanout announce (request.y). updateFrameClock() re-derives the interval from the active backend's info at bring-up and again on every backend change — the boot framebuffer's clock dies with the GOP floor at upgrade, replaced by the GPU's rate. Unknown rate defaults to 60 Hz; the result is clamped to [30, 120] Hz so a mis-parsed EDID can neither starve nor flood the compositor. Rate only, never phase: without vblank, presents still free-run (docs/display-v2.md, 'Fenced is not vsync'). Observed in QEMU: OVMF exposes no EDID for the VGA adapter, so the GOP floor logs 'frame clock 62 Hz (default)' (real firmware does expose it); the virtio-gpu EDID advertises 75 Hz and the upgrade logs 'frame clock 76 Hz (panel EDID)'. The display kernel test asserts refresh_hz rides the seeded node; all 7 display QEMU cases pass.
This commit is contained in:
@@ -16,8 +16,10 @@ 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 };
|
||||
/// The current display mode, as a backend reports it. `refresh_hz` is the panel's
|
||||
/// refresh rate from EDID (0 = unknown) — the frame clock's pacing seed; without vblank
|
||||
/// it fixes the rate, never the phase (docs/display-v2.md, "Fenced is not vsync").
|
||||
pub const Info = struct { width: u32, height: u32, pitch: u32, format: u32, refresh_hz: u32 };
|
||||
|
||||
/// Enumeration scratch — a `DeviceDescriptor` is large, and only one scan is ever needed.
|
||||
var device_table: [64]device.DeviceDescriptor = undefined;
|
||||
@@ -35,13 +37,14 @@ pub const Gop = struct {
|
||||
height: u32,
|
||||
pitch: u32,
|
||||
format: u32,
|
||||
refresh_hz: u32, // from the boot EDID via the display0 node (0 = unknown)
|
||||
|
||||
/// 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 };
|
||||
const Found = struct { id: u64, width: u32, height: u32, pitch: u32, format: u32, refresh_hz: 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
|
||||
@@ -52,7 +55,7 @@ pub const Gop = struct {
|
||||
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 .{ .id = d.id, .width = d.display.width, .height = d.display.height, .pitch = d.display.pitch, .format = d.display.format, .refresh_hz = d.display.refresh_hz };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -93,11 +96,12 @@ pub const Gop = struct {
|
||||
.height = found.height,
|
||||
.pitch = found.pitch,
|
||||
.format = found.format,
|
||||
.refresh_hz = found.refresh_hz,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn info(self: *const Gop) Info {
|
||||
return .{ .width = self.width, .height = self.height, .pitch = self.pitch, .format = self.format };
|
||||
return .{ .width = self.width, .height = self.height, .pitch = self.pitch, .format = self.format, .refresh_hz = self.refresh_hz };
|
||||
}
|
||||
|
||||
/// The cacheable compose target (the back buffer).
|
||||
@@ -169,10 +173,11 @@ pub const VirtioGpu = struct {
|
||||
width: u32, // the active mode
|
||||
height: u32,
|
||||
format: u32,
|
||||
refresh_hz: u32, // from the driver's EDID read, carried in the announce (0 = unknown)
|
||||
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 };
|
||||
return .{ .width = self.width, .height = self.height, .pitch = self.stride * 4, .format = self.format, .refresh_hz = self.refresh_hz };
|
||||
}
|
||||
pub fn surface(self: *const VirtioGpu) Surface {
|
||||
return .{ .pixels = self.pixels, .stride = self.stride, .width = self.width, .height = self.height };
|
||||
|
||||
Reference in New Issue
Block a user