selecting the displays native resolution
This commit is contained in:
+90
-8
@@ -3,6 +3,8 @@ const uefi = std.os.uefi;
|
||||
const elf = std.elf;
|
||||
const danos = @import("danos");
|
||||
const BootInfo = danos.BootInfo;
|
||||
const GraphicsOutput = uefi.protocol.GraphicsOutput;
|
||||
const EdidActive = uefi.protocol.edid.Active;
|
||||
|
||||
/// Name of the kernel ELF on the boot volume (installed to the ESP root by
|
||||
/// build.zig). UEFI wants a UTF-16, null-terminated path.
|
||||
@@ -46,10 +48,30 @@ fn boot() !noreturn {
|
||||
kernel(&boot_info);
|
||||
}
|
||||
|
||||
/// Read the current graphics mode into our own framebuffer description.
|
||||
/// A display resolution in pixels.
|
||||
const Resolution = struct { width: u32, height: u32 };
|
||||
|
||||
/// Switch the GPU to the monitor's native resolution (when we can determine it)
|
||||
/// and read the resulting graphics mode into our own framebuffer description.
|
||||
fn queryFramebuffer(bs: *uefi.tables.BootServices) !danos.Framebuffer {
|
||||
const gop = (try bs.locateProtocol(uefi.protocol.GraphicsOutput, null)) orelse
|
||||
// Enumerate the handles carrying the Graphics Output Protocol. We go through
|
||||
// handles (rather than locateProtocol) so we can also ask them for their EDID,
|
||||
// which is what tells us the panel's native resolution.
|
||||
const handles = (try bs.locateHandleBuffer(.{ .by_protocol = &GraphicsOutput.guid })) orelse
|
||||
return error.NoGraphicsOutput;
|
||||
defer _ = bs.freePool(@ptrCast(handles.ptr)) catch {};
|
||||
|
||||
const gop = (try bs.handleProtocol(GraphicsOutput, handles[0])) orelse
|
||||
return error.NoGraphicsOutput;
|
||||
|
||||
// Best effort: the monitor's preferred (native) timing from its EDID.
|
||||
const native = nativeResolution(bs, handles);
|
||||
|
||||
// Select the mode and switch to it if it isn't already current. setMode
|
||||
// updates gop.mode (info and frame_buffer_base) to describe the new mode.
|
||||
const target = pickMode(gop, native);
|
||||
if (target != gop.mode.mode) try gop.setMode(target);
|
||||
|
||||
const info = gop.mode.info;
|
||||
return .{
|
||||
.base = @intCast(gop.mode.frame_buffer_base),
|
||||
@@ -57,15 +79,75 @@ fn queryFramebuffer(bs: *uefi.tables.BootServices) !danos.Framebuffer {
|
||||
.height = info.vertical_resolution,
|
||||
// Each pixel is 32 bits, so the byte pitch is 4 * pixels-per-row.
|
||||
.pitch = info.pixels_per_scan_line * 4,
|
||||
.format = switch (info.pixel_format) {
|
||||
.red_green_blue_reserved_8_bit_per_color => .rgbx,
|
||||
.blue_green_red_reserved_8_bit_per_color => .bgrx,
|
||||
// bit_mask / blt_only have no linear 32bpp layout we can paint into.
|
||||
else => return error.UnsupportedPixelFormat,
|
||||
},
|
||||
.format = try pixelFormat(info.pixel_format),
|
||||
};
|
||||
}
|
||||
|
||||
/// Map a GOP pixel format to ours. bit_mask / blt_only have no linear 32bpp
|
||||
/// layout we can paint into, so they're rejected.
|
||||
fn pixelFormat(fmt: GraphicsOutput.PixelFormat) !danos.PixelFormat {
|
||||
return switch (fmt) {
|
||||
.red_green_blue_reserved_8_bit_per_color => .rgbx,
|
||||
.blue_green_red_reserved_8_bit_per_color => .bgrx,
|
||||
else => error.UnsupportedPixelFormat,
|
||||
};
|
||||
}
|
||||
|
||||
/// Choose the graphics mode to boot with. If we learned the monitor's native
|
||||
/// resolution from EDID and a mode offers it (with a layout we can paint into),
|
||||
/// use that. Otherwise keep whatever mode the firmware already selected: with a
|
||||
/// valid EDID present the firmware normally defaults to the native mode itself,
|
||||
/// so its default is a far safer bet than second-guessing it with, say, the
|
||||
/// largest advertised mode (which is often a huge non-native surface).
|
||||
fn pickMode(gop: *GraphicsOutput, native: ?Resolution) u32 {
|
||||
const n = native orelse return gop.mode.mode;
|
||||
var id: u32 = 0;
|
||||
while (id < gop.mode.max_mode) : (id += 1) {
|
||||
const info = gop.queryMode(id) catch continue;
|
||||
_ = pixelFormat(info.pixel_format) catch continue; // must be paintable
|
||||
if (info.horizontal_resolution == n.width and
|
||||
info.vertical_resolution == n.height) return id;
|
||||
}
|
||||
return gop.mode.mode; // native not on offer; trust the firmware's default
|
||||
}
|
||||
|
||||
/// The monitor's native resolution, read from an EDID's preferred timing. We try
|
||||
/// every GOP handle and both EDID protocols (Active first, then Discovered),
|
||||
/// since firmware installs them inconsistently — and many, including OVMF with
|
||||
/// QEMU's stdvga, don't expose them at all. Returns null when none is found, in
|
||||
/// which case pickMode keeps the firmware's default mode.
|
||||
fn nativeResolution(bs: *uefi.tables.BootServices, handles: []uefi.Handle) ?Resolution {
|
||||
for (handles) |h| {
|
||||
if (bs.handleProtocol(EdidActive, h) catch null) |e| {
|
||||
if (e.edid) |p| if (edidNative(p[0..e.size_of_edid])) |r| return r;
|
||||
}
|
||||
if (bs.handleProtocol(uefi.protocol.edid.Discovered, h) catch null) |e| {
|
||||
if (e.edid) |p| if (edidNative(p[0..e.size_of_edid])) |r| return r;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Parse the native resolution from a raw EDID block. The first Detailed Timing
|
||||
/// Descriptor (at byte 54) is the preferred — i.e. native — mode by convention;
|
||||
/// its active pixel counts are split across low bytes and the high nibbles of
|
||||
/// later bytes.
|
||||
fn edidNative(edid: []const u8) ?Resolution {
|
||||
if (edid.len < 128) return null;
|
||||
// Every EDID begins with this fixed 8-byte header.
|
||||
const header = [_]u8{ 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00 };
|
||||
if (!std.mem.eql(u8, edid[0..8], &header)) return null;
|
||||
|
||||
const dtd = edid[54..][0..18];
|
||||
// A zero pixel clock marks a display (not timing) descriptor: no resolution.
|
||||
if (dtd[0] == 0 and dtd[1] == 0) return null;
|
||||
|
||||
const w = @as(u32, dtd[2]) | (@as(u32, dtd[4] & 0xf0) << 4);
|
||||
const h = @as(u32, dtd[5]) | (@as(u32, dtd[7] & 0xf0) << 4);
|
||||
if (w == 0 or h == 0) return null;
|
||||
return .{ .width = w, .height = h };
|
||||
}
|
||||
|
||||
/// Open the kernel on the volume we booted from, read it into a pool buffer,
|
||||
/// load its segments, and return the physical entry-point address.
|
||||
fn loadKernel(bs: *uefi.tables.BootServices) !usize {
|
||||
|
||||
Reference in New Issue
Block a user