diff --git a/Cargo.lock b/Cargo.lock index b4ee63ff397..511f095b954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -553,6 +553,7 @@ dependencies = [ "toyos", "toyos-abi", "toyos-desktop", + "toyos-edid", "toyos-font", "toyos-input", "toyos-inspect", @@ -4188,6 +4189,10 @@ version = "0.1.0" name = "toyos-dns" version = "0.1.0" +[[package]] +name = "toyos-edid" +version = "0.1.0" + [[package]] name = "toyos-elf" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 79f6d90e898..1da5a159e1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ members = [ "lib/device-memory", "lib/disk", "lib/elf", + "lib/edid", "lib/elide", "lib/fat32", "lib/fat32/check", diff --git a/build/licence.rs b/build/licence.rs index 2e1d9bc24ad..8866f07a663 100644 --- a/build/licence.rs +++ b/build/licence.rs @@ -253,6 +253,50 @@ pub const EXCEPTIONS: &[Exception] = &[ /// `issues/the-third-party-corpus-is-in-no-machine-read-ledger.md` is /// what is left of that gap. pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ + // EDID base blocks: device and firmware output, not third-party source. + // `lib/edid/fixtures/SOURCE` says how each was read. + ( + "lib/edid/fixtures/amd-bench-b140han06.8.bin", + "dad4ed7740aab2a5f11f10bf807e300128d8bf7acfee25f8f57e5845d781b2d6", + "ours: the AMD bench panel's EDID base block, as its firmware publishes it, read by PR #895's M1 boot", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "lib/edid/fixtures/qemu-default.bin", + "edfffbb5aa7dbf17fa43c73e5ad1df719411a5c948576f397c2528767d0e402a", + "ours: QEMU's own EDID generator, `qemu-edid` with no option", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "lib/edid/fixtures/qemu-dpi157-1920x1080.bin", + "a43c85fc61efc09595a67407c865e53de94b6ddb657ab9da843c2bd8b7b6eecc", + "ours: QEMU's own EDID generator, `qemu-edid -x 1920 -y 1080 -d 157`", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "lib/edid/fixtures/qemu-dpi254-2560x1600.bin", + "d4436bf2d72005aa03f09e0c4660decf85f4e5738c223269cd1a1a3b397fd1ed", + "ours: QEMU's own EDID generator, `qemu-edid -x 2560 -y 1600 -d 254`", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "lib/edid/fixtures/qemu-dpi254-3456x2234.bin", + "c2eb70f0f0877e745d10856b676ddfbff7015f7ae9231c1b6262f73cb9771cb8", + "ours: QEMU's own EDID generator's base block, `qemu-edid -x 3456 -y 2234 -d 254`", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "lib/edid/fixtures/t14-n140hca-eae.bin", + "057bdacb8b19658fb43dcc583ab265bda8c9d7636846ef7bbcf97d009073894b", + "ours: the T14 panel's EDID base block, as its firmware publishes it, read by PR #895's M1 boot", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "lib/edid/fixtures/virtio-gpu-get-edid-1024x768.bin", + "9e4773b40dba7143b625474ebe83588562f7ec95cb02941df249ba50063b32f9", + "ours: QEMU's virtio-gpu answering GET_EDID at 1024x768", + Terms::Spdx("MIT OR Apache-2.0"), + ), // The ACPI tables QEMU 11.1.1 published to a `Profile::Headless` guest, // read out of guest physical memory over the monitor. Firmware output, not // third-party source: `lib/acpi/tests/fixtures.rs` decodes them against diff --git a/issues/the-desktop-draws-at-the-panels-scale.md b/issues/the-desktop-draws-at-the-panels-scale.md new file mode 100644 index 00000000000..fbdc51ecace --- /dev/null +++ b/issues/the-desktop-draws-at-the-panels-scale.md @@ -0,0 +1,49 @@ +--- +status: assigned +kind: track +opened: 2026-10-14 +--- + +# The desktop draws at the panel's scale, and follows a mode change + +Held by the orchestrator. Stage 5 of `issues/toyos-has-a-desktop.md`, and +the owner's go for a display scale factor and live resize on QEMU and on metal +alike. + +The accepted design, its three review rounds and the orchestrator's rulings +are on pull request #895: the design as accepted +(https://github.com/ToyOSOrg/ToyOS/pull/895#issuecomment-6158220132), the +reviews (#issuecomment-6131030591, #issuecomment-6154531219, +#issuecomment-6154671069) and the acceptance (#issuecomment-6154686668). #895 +landed the first part: the loader hands the panel's EDID over from the GOP +handle that shows the scanout, `lib/edid` decodes it, `toyos_desktop::Scale::auto` +chooses the scale, and the compositor logs it and answers it in +`inspect display`. Nothing draws at it. + +What is left: + +1. **The rest of the scale**, as the accepted design says it: the `display` + port and `screen scale`, the window protocol's scale, the compositor + drawing its chrome, text, icons and cursor at it and relaying windows out + on a change, terminal, filepicker and `files` at the scale, and the winit + fork's `scale_factor()`. +2. **virtio-gpu leaves the kernel** (stage 1 of + `issues/every-driver-is-still-in-the-kernel.md`): the compositor drives it + through a `pci:1af4:1050` claim and follows the host window's display + events. Blocked on `issues/a-cargo-run-boot-claims-every-pcidev-slot.md`: + the Virtio profile's fifth claim is refused until a slot is free. + +Constraints already paid for: + +- **The panel's size comes only from the EDID**, read before + `ExitBootServices`. On the T14 the protocol is on the first of two GOP + handles, on the AMD bench on the second; the panels are 309x173 mm and + 309x174 mm at 1920x1080, and the rule gives both 1.5 (`lib/edid/fixtures/`). +- **QEMU's OVMF publishes no EDID protocol**, so every GOP guest runs at 1. +- **QEMU writes no cursor-queue answer** (used `len` 0) and its `GET_EDID` + `size` field is the buffer's 1024, measured on #895's evidence branch + `pr-895` at `7d47ad2b`. + +Exit: the desktop and its own clients draw at the scale on both benches and +under QEMU's virtio-gpu, a host window's resize reaches the guest's mode, and +no `SYS_GPU_*` number is left in `sdk/abi/src/syscall.rs`. diff --git a/issues/toyos-has-a-desktop.md b/issues/toyos-has-a-desktop.md index 8634bab212a..86ad6691a5a 100644 --- a/issues/toyos-has-a-desktop.md +++ b/issues/toyos-has-a-desktop.md @@ -105,8 +105,7 @@ until the Rust target is upstream. screenshots. Clipboard reads and screenshots are privileges granted under the package track's request ∩ grant ∩ ceiling rule (`issues/a-package-is-a-directory-under-apps-and-the-installer-is-a-program.md`). -5. **Fractional scaling.** The T14 panel is 14" at 1080p, which wants about - 1.25×. +5. **Fractional scaling**: `issues/the-desktop-draws-at-the-panels-scale.md`. 6. **Accessibility**: accesskit, keyboard-only navigation, high contrast. 7. **Input**: the T14 touchpad (`issues/the-t14-touchpad-is-i2c-hid-and-unbuilt.md`), scrolling diff --git a/kernel/src/drivers/gop.rs b/kernel/src/drivers/gop.rs index bdfe4f1018b..c01ce97ec31 100644 --- a/kernel/src/drivers/gop.rs +++ b/kernel/src/drivers/gop.rs @@ -34,6 +34,7 @@ pub fn init( height: u32, stride: u32, pixel_format: u32, + edid: ([u8; toyos_abi::boot::EDID_BLOCK], u32), ) -> Option<(Box, GpuInfo)> { if let Err(why) = crate::mm::paging::scanout_whole_pages(addr, size) { log!("GOP: the scanout at {addr:#x}+{size:#x} is refused: {why}"); @@ -97,6 +98,8 @@ pub fn init( stride, pixel_format, flags: 0, + edid: edid.0, + edid_len: edid.1, }; Some((Box::new(GopGpu), info)) diff --git a/kernel/src/drivers/virtio_gpu.rs b/kernel/src/drivers/virtio_gpu.rs index 7ae304658d7..52fe40b0ddb 100644 --- a/kernel/src/drivers/virtio_gpu.rs +++ b/kernel/src/drivers/virtio_gpu.rs @@ -452,6 +452,8 @@ impl GpuController { stride: self.width, pixel_format: 1, // BGR (B8G8R8X8_UNORM) flags: FLAG_HARDWARE_CURSOR, + edid: [0; toyos_abi::boot::EDID_BLOCK], + edid_len: 0, } } } diff --git a/kernel/src/gpu.rs b/kernel/src/gpu.rs index e8e908d82bf..e0197b70861 100644 --- a/kernel/src/gpu.rs +++ b/kernel/src/gpu.rs @@ -14,6 +14,10 @@ pub struct GpuInfo { pub stride: u32, pub pixel_format: u32, pub flags: u32, + /// The panel's EDID base block the loader handed over, and its length: + /// [`toyos_abi::boot::EDID_BLOCK`] or zero. + pub edid: [u8; toyos_abi::boot::EDID_BLOCK], + pub edid_len: u32, } /// Hardware-agnostic GPU interface; register an implementation with `gpu::register()`. @@ -40,6 +44,8 @@ pub fn screen(info: &GpuInfo) -> crate::device::Screen { stride: info.stride, pixel_format: info.pixel_format, flags: info.flags, + edid: info.edid, + edid_len: info.edid_len, }, scanout: info.scanout.clone(), cursor: info.cursor.clone(), @@ -95,6 +101,8 @@ pub fn set_resolution(width: u32, height: u32) -> Result stride: new_info.stride, pixel_format: new_info.pixel_format, flags: new_info.flags, + edid: new_info.edid, + edid_len: new_info.edid_len, }); Ok(new_info) } diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 215cdc76cc6..a2a7abcf66b 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -169,6 +169,16 @@ fn panic(info: &core::panic::PanicInfo) -> ! { panic::halt_all_cpus(); } +/// The EDID block the loader handed over and its length, which the loader +/// writes as a whole block or nothing; the bytes are not read here. +fn gop_edid(args: &KernelArgs) -> ([u8; toyos_abi::boot::EDID_BLOCK], u32) { + match args.gop_edid_len { + 0 => ([0; toyos_abi::boot::EDID_BLOCK], 0), + len if len == toyos_abi::boot::EDID_BLOCK as u64 => (args.gop_edid, len as u32), + other => panic!("the loader wrote {other} as gop_edid_len, which is 0 or {}", toyos_abi::boot::EDID_BLOCK), + } +} + fn register_gpu(driver: Box, info: gpu::GpuInfo) { gpu::register(driver, info); } @@ -536,6 +546,7 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { kernel_args.gop_height, kernel_args.gop_stride, kernel_args.gop_pixel_format, + gop_edid(kernel_args), ) { Some((gpu_driver, gpu_info)) => register_gpu(gpu_driver, gpu_info), None => log!("GPU: none this boot, running headless"), diff --git a/kernel/src/object/device.rs b/kernel/src/object/device.rs index d6b1a3279bc..e9833b293d0 100644 --- a/kernel/src/object/device.rs +++ b/kernel/src/object/device.rs @@ -227,6 +227,8 @@ pub(crate) fn mint_rollback_selftest() { stride: 0, pixel_format: 0, flags: 0, + edid: [0; toyos_abi::boot::EDID_BLOCK], + edid_len: 0, }, buffers, ); diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index 6a6e4b76836..fa5ed5a203c 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -407,7 +407,7 @@ pub(super) fn sys_gpu_reset_scanout( gpu_info: crate::gpu::GpuInfo, info_out: UserAddr, ) -> u64 { - let crate::gpu::GpuInfo { scanout, cursor, width, height, stride, pixel_format, flags } = + let crate::gpu::GpuInfo { scanout, cursor, width, height, stride, pixel_format, flags, edid, edid_len } = gpu_info; let screen = device::Screen { info: toyos_abi::FramebufferInfo { @@ -418,6 +418,8 @@ pub(super) fn sys_gpu_reset_scanout( stride, pixel_format, flags, + edid, + edid_len, }, scanout, cursor, diff --git a/lib/edid/Cargo.toml b/lib/edid/Cargo.toml new file mode 100644 index 00000000000..dd79d78e121 --- /dev/null +++ b/lib/edid/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "toyos-edid" +description = "The panel's image size out of a display's E-EDID base block, decoded as an input from outside: refused by name when malformed." +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +publish = false + +[lib] +doctest = false + +[lints.rust] +unsafe_code = "forbid" diff --git a/lib/edid/fixtures/SOURCE b/lib/edid/fixtures/SOURCE new file mode 100644 index 00000000000..8dd0b424455 --- /dev/null +++ b/lib/edid/fixtures/SOURCE @@ -0,0 +1,15 @@ +Each file is one 128-byte EDID base block. + +t14-n140hca-eae.bin, amd-bench-b140han06.8.bin: each laptop's panel, as its +firmware publishes it through EFI_EDID_ACTIVE_PROTOCOL, read by the loader on a +metalcase boot (PR #895's M1 reading; EDID Discovered gave the same bytes). +Both had their ID serial (bytes 0x0C-0x0F) zero already and carry no 0xFF +serial descriptor, so nothing was zeroed and each checksum is the panel's own. +Their first descriptors give 309x173 mm (T14) and 309x174 mm (AMD bench). + +qemu-*.bin: the base block QEMU 11.1.1's generator writes, by `qemu-edid -o` +with no option (default), and with `-x 2560 -y 1600 -d 254`, +`-x 3456 -y 2234 -d 254` and `-x 1920 -y 1080 -d 157`. + +virtio-gpu-get-edid-1024x768.bin: QEMU 11.1.1's virtio-gpu-pci, xres=1024, +yres=768, answering VIRTIO_GPU_CMD_GET_EDID for scanout 0. diff --git a/lib/edid/fixtures/amd-bench-b140han06.8.bin b/lib/edid/fixtures/amd-bench-b140han06.8.bin new file mode 100644 index 00000000000..89f78ffea5c Binary files /dev/null and b/lib/edid/fixtures/amd-bench-b140han06.8.bin differ diff --git a/lib/edid/fixtures/qemu-default.bin b/lib/edid/fixtures/qemu-default.bin new file mode 100644 index 00000000000..5ac0def41df Binary files /dev/null and b/lib/edid/fixtures/qemu-default.bin differ diff --git a/lib/edid/fixtures/qemu-dpi157-1920x1080.bin b/lib/edid/fixtures/qemu-dpi157-1920x1080.bin new file mode 100644 index 00000000000..625eaa72266 Binary files /dev/null and b/lib/edid/fixtures/qemu-dpi157-1920x1080.bin differ diff --git a/lib/edid/fixtures/qemu-dpi254-2560x1600.bin b/lib/edid/fixtures/qemu-dpi254-2560x1600.bin new file mode 100644 index 00000000000..836f0ab061d Binary files /dev/null and b/lib/edid/fixtures/qemu-dpi254-2560x1600.bin differ diff --git a/lib/edid/fixtures/qemu-dpi254-3456x2234.bin b/lib/edid/fixtures/qemu-dpi254-3456x2234.bin new file mode 100644 index 00000000000..ea97baa3e71 Binary files /dev/null and b/lib/edid/fixtures/qemu-dpi254-3456x2234.bin differ diff --git a/lib/edid/fixtures/t14-n140hca-eae.bin b/lib/edid/fixtures/t14-n140hca-eae.bin new file mode 100644 index 00000000000..19ce893d233 Binary files /dev/null and b/lib/edid/fixtures/t14-n140hca-eae.bin differ diff --git a/lib/edid/fixtures/virtio-gpu-get-edid-1024x768.bin b/lib/edid/fixtures/virtio-gpu-get-edid-1024x768.bin new file mode 100644 index 00000000000..f9962d80020 Binary files /dev/null and b/lib/edid/fixtures/virtio-gpu-get-edid-1024x768.bin differ diff --git a/lib/edid/src/lib.rs b/lib/edid/src/lib.rs new file mode 100644 index 00000000000..21397fd8282 --- /dev/null +++ b/lib/edid/src/lib.rs @@ -0,0 +1,99 @@ +//! The panel's image size out of a display's E-EDID base block. +//! +//! Every `§` and table here is VESA's *Enhanced EDID Standard*, Release A +//! Rev. 2 (EDID 1.4). The bytes come from outside the trust of whoever reads +//! them, firmware or a device, so a block that is short, has no header or does +//! not sum to zero is refused by name. Nothing but the size is read: a mode +//! comes from the device or the firmware, never from here. + +#![no_std] + +/// One EDID block: the base block, or one extension. +pub const BLOCK: usize = 128; + +const HEADER: [u8; 8] = [0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00]; +const SCREEN_W_CM: usize = 0x15; +const SCREEN_H_CM: usize = 0x16; +/// The first of §3.10's four 18-byte descriptors, which carries the +/// preferred timing. +const FIRST_DESCRIPTOR: usize = 0x36; + +/// Why a block was not read. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refusal { + /// Fewer bytes than one block. + Short { len: usize }, + /// The first eight bytes are not §3.3's header. + Header, + /// The block's bytes do not sum to zero modulo 256 (§3.11). + Checksum { sum: u8 }, +} + +impl core::fmt::Display for Refusal { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Short { len } => write!(f, "{len} byte(s) are less than one {BLOCK}-byte block"), + Self::Header => write!(f, "the block does not open with the EDID header"), + Self::Checksum { sum } => write!(f, "the block sums to {sum:#04x}, not zero"), + } + } +} + +/// What a block says of the panel's size. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ImageSize { + Millimetres { width: u32, height: u32 }, + /// Table 3.12: one of bytes 0x15/0x16 zero makes the other an aspect + /// ratio, which says nothing of size. + AspectRatio, + /// Table 3.12: both zero, "unknown or undefined". + Unknown, +} + +/// A base block that has its header and sums to zero. +#[derive(Clone, Copy)] +pub struct BaseBlock<'a>(&'a [u8; BLOCK]); + +impl<'a> BaseBlock<'a> { + /// The first [`BLOCK`] bytes of `bytes`; whatever follows is extensions, + /// which nothing here reads. + pub fn parse(bytes: &'a [u8]) -> Result { + let block: &[u8; BLOCK] = bytes + .get(..BLOCK) + .and_then(|b| b.try_into().ok()) + .ok_or(Refusal::Short { len: bytes.len() })?; + if block[..HEADER.len()] != HEADER { + return Err(Refusal::Header); + } + let sum = block.iter().fold(0u8, |sum, b| sum.wrapping_add(*b)); + if sum != 0 { + return Err(Refusal::Checksum { sum }); + } + Ok(Self(block)) + } + + /// The panel's size: the first descriptor's, where it is a detailed + /// timing that states one (Table 3.21 notes 11 and 12); otherwise bytes + /// 0x15/0x16 in centimetres (Table 3.12). A timing's 0 mm is "undefined" + /// (note 17), so it falls through to the base block. + pub fn image_size(&self) -> ImageSize { + let d = &self.0[FIRST_DESCRIPTOR..FIRST_DESCRIPTOR + 18]; + // §3.10: a detailed timing (§3.10.2) has a nonzero pixel clock; a + // display descriptor (§3.10.3) opens with two zero bytes. + if d[0] != 0 || d[1] != 0 { + let width = u32::from(d[12]) | u32::from(d[14] >> 4) << 8; + let height = u32::from(d[13]) | u32::from(d[14] & 0x0f) << 8; + if width != 0 && height != 0 { + return ImageSize::Millimetres { width, height }; + } + } + match (self.0[SCREEN_W_CM], self.0[SCREEN_H_CM]) { + (0, 0) => ImageSize::Unknown, + (0, _) | (_, 0) => ImageSize::AspectRatio, + (w, h) => ImageSize::Millimetres { width: u32::from(w) * 10, height: u32::from(h) * 10 }, + } + } +} + +#[cfg(test)] +mod tests; diff --git a/lib/edid/src/tests.rs b/lib/edid/src/tests.rs new file mode 100644 index 00000000000..e4214f2c1c3 --- /dev/null +++ b/lib/edid/src/tests.rs @@ -0,0 +1,98 @@ +use super::*; + +fn fixture(name: &str) -> &'static [u8] { + match name { + "t14" => include_bytes!("../fixtures/t14-n140hca-eae.bin"), + "amd" => include_bytes!("../fixtures/amd-bench-b140han06.8.bin"), + "qemu-default" => include_bytes!("../fixtures/qemu-default.bin"), + "qemu-1920" => include_bytes!("../fixtures/qemu-dpi157-1920x1080.bin"), + "qemu-2560" => include_bytes!("../fixtures/qemu-dpi254-2560x1600.bin"), + "qemu-3456" => include_bytes!("../fixtures/qemu-dpi254-3456x2234.bin"), + "virtio-gpu" => include_bytes!("../fixtures/virtio-gpu-get-edid-1024x768.bin"), + other => panic!("no fixture {other}"), + } +} + +fn size(bytes: &[u8]) -> ImageSize { + BaseBlock::parse(bytes).expect("a fixture is a valid block").image_size() +} + +fn mm(width: u32, height: u32) -> ImageSize { + ImageSize::Millimetres { width, height } +} + +/// A copy of `bytes` with `at` set to `value` and the checksum made good. +fn edited(bytes: &[u8], edits: &[(usize, u8)]) -> [u8; BLOCK] { + let mut block: [u8; BLOCK] = bytes[..BLOCK].try_into().unwrap(); + for &(at, value) in edits { + block[at] = value; + } + let rest = block[..BLOCK - 1].iter().fold(0u8, |s, b| s.wrapping_add(*b)); + block[BLOCK - 1] = 0u8.wrapping_sub(rest); + block +} + +#[test] +fn a_panels_size_is_its_first_detailed_timings() { + assert_eq!(size(fixture("t14")), mm(309, 173)); + assert_eq!(size(fixture("amd")), mm(309, 174)); + assert_eq!(size(fixture("qemu-default")), mm(325, 203)); + assert_eq!(size(fixture("qemu-1920")), mm(310, 174)); + assert_eq!(size(fixture("qemu-2560")), mm(256, 160)); + assert_eq!(size(fixture("virtio-gpu")), mm(260, 195)); +} + +/// QEMU's large-screen block carries a display descriptor first, so the size +/// is the base block's centimetres. +#[test] +fn with_no_timing_first_the_size_is_the_base_blocks_centimetres() { + assert_eq!(size(fixture("qemu-3456")), mm(340, 220)); +} + +/// Table 3.21 note 17: a timing's 0 mm is undefined, not a size. +#[test] +fn a_timing_of_zero_millimetres_falls_to_the_base_block() { + let block = edited(fixture("t14"), &[(0x36 + 12, 0), (0x36 + 13, 0), (0x36 + 14, 0)]); + assert_eq!(size(&block), mm(310, 170)); +} + +#[test] +fn one_zero_byte_of_the_screen_size_is_an_aspect_ratio() { + let landscape = edited(fixture("qemu-3456"), &[(SCREEN_H_CM, 0)]); + assert_eq!(size(&landscape), ImageSize::AspectRatio); + let portrait = edited(fixture("qemu-3456"), &[(SCREEN_W_CM, 0)]); + assert_eq!(size(&portrait), ImageSize::AspectRatio); +} + +#[test] +fn both_zero_is_unknown() { + let block = edited(fixture("qemu-3456"), &[(SCREEN_W_CM, 0), (SCREEN_H_CM, 0)]); + assert_eq!(size(&block), ImageSize::Unknown); +} + +#[test] +fn a_short_block_is_refused() { + assert_eq!(BaseBlock::parse(&fixture("t14")[..127]).err(), Some(Refusal::Short { len: 127 })); + assert_eq!(BaseBlock::parse(&[]).err(), Some(Refusal::Short { len: 0 })); +} + +#[test] +fn a_block_without_the_header_is_refused() { + let block = edited(fixture("t14"), &[(0, 0x01)]); + assert_eq!(BaseBlock::parse(&block).err(), Some(Refusal::Header)); +} + +#[test] +fn a_block_that_does_not_sum_to_zero_is_refused() { + let mut block: [u8; BLOCK] = fixture("t14").try_into().unwrap(); + block[0x15] = block[0x15].wrapping_add(1); + assert_eq!(BaseBlock::parse(&block).err(), Some(Refusal::Checksum { sum: 1 })); +} + +/// What follows the base block is an extension, read by nothing here. +#[test] +fn bytes_past_the_base_block_are_not_read() { + let mut longer = [0xa5u8; 2 * BLOCK]; + longer[..BLOCK].copy_from_slice(fixture("amd")); + assert_eq!(size(&longer), mm(309, 174)); +} diff --git a/loader/src/efi/mod.rs b/loader/src/efi/mod.rs index b0525001e80..071393bf230 100644 --- a/loader/src/efi/mod.rs +++ b/loader/src/efi/mod.rs @@ -26,7 +26,7 @@ use alloc::vec::Vec; pub use boot::{AllocateType, BootServices, MemoryDescriptor, MemoryMap, Scoped, PAGE_SIZE}; pub use proto::{ - BlockIo, DevicePath, File, Gop, HardDrive, LoadedImage, Mode, PartitionInfo, PciRootBridgeIo, + BlockIo, DevicePath, EdidActive, File, Gop, GopModeInfo, HardDrive, LoadedImage, Mode, PartitionInfo, PciRootBridgeIo, PixelFormat, Rng, SimpleFileSystem, }; pub use runtime::{ResetType, RuntimeServices, Time, VariableAttributes, GLOBAL_VARIABLE}; diff --git a/loader/src/efi/proto.rs b/loader/src/efi/proto.rs index c33d49b2f85..3d22054bd05 100644 --- a/loader/src/efi/proto.rs +++ b/loader/src/efi/proto.rs @@ -595,6 +595,37 @@ impl Gop { } } +/// UEFI 2.10's `EFI_EDID_ACTIVE_PROTOCOL`, on the child handle of the video +/// output it describes. +#[repr(C)] +pub struct EdidActive { + size_of_edid: u32, + edid: *const u8, +} + +const _: () = { + assert!(size_of::() == 16); + assert!(offset_of!(EdidActive, edid) == 8); +}; + +// SAFETY: the specification's layout, under its GUID. +unsafe impl Protocol for EdidActive { + const GUID: Guid = Guid::new(0xbd8c1056, 0x9f36, 0x44ec, [0x92, 0xa8, 0xa6, 0x33, 0x7f, 0x81, 0x79, 0x86]); +} + +impl EdidActive { + /// The EDID's bytes, empty where the output gave none: `SizeOfEdid` is 0 + /// and `Edid` null then. + pub fn bytes(&self) -> &[u8] { + if self.edid.is_null() { + return &[]; + } + // SAFETY: firmware's read-only buffer of `SizeOfEdid` bytes, live while + // the protocol is open. + unsafe { core::slice::from_raw_parts(self.edid, self.size_of_edid as usize) } + } +} + /// `EFI_RNG_PROTOCOL` (UEFI 2.10 §37.5.1). #[repr(C)] pub struct Rng { diff --git a/loader/src/main.rs b/loader/src/main.rs index 84551569d01..29547d45dc1 100644 --- a/loader/src/main.rs +++ b/loader/src/main.rs @@ -10,10 +10,10 @@ use alloc::alloc::Layout; use toyos_elf::section::SectionTable; use toyos_elf::{rela, ImageOffset, Op, RelaTable, SymTab}; use efi::{ - cstr16, CStr16, DevicePath, Gop, Handle, HardDrive, LoadedImage, MemoryDescriptor, Mode, PixelFormat, ResetType, + cstr16, CStr16, DevicePath, EdidActive, Gop, Handle, HardDrive, LoadedImage, MemoryDescriptor, Mode, PixelFormat, ResetType, SimpleFileSystem, Status, SystemTable, ACPI2_GUID, PAGE_SIZE, }; -use toyos_abi::boot::{KernelArgs, MemoryMapEntry, RootBridgeWindow, MAX_ROOT_BRIDGE_WINDOWS}; +use toyos_abi::boot::{KernelArgs, MemoryMapEntry, RootBridgeWindow, EDID_BLOCK, MAX_ROOT_BRIDGE_WINDOWS}; use toyos_bootmap::{Plan, Table, BOOT_MAP_BYTES, MAX_PAGES}; use toyos_update::policy; use toyos_update::record::{self, Booted, Ended, Record}; @@ -353,6 +353,7 @@ struct GopInfo { height: u32, stride: u32, pixel_format: u32, + edid: Option<[u8; EDID_BLOCK]>, } /// The mode is the firmware's: `Mode->Info` is the mode it already set for the @@ -383,6 +384,11 @@ fn query_gop(system_table: &SystemTable) -> Option { }; let (framebuffer, framebuffer_size) = gop.frame_buffer(); + let shown = (framebuffer, GopModeKey::of(mode)); + // Closed before the handles are asked again: a second GET_PROTOCOL open by + // this image shares the first's entry, so two closes would be one too many. + drop(gop); + let edid = scanout_edid(bs, shown); println!("{} {}x{} stride={} format={} fb={:#x} size={}", loaderlog::GOP_AT, width, height, stride, pixel_format, framebuffer, framebuffer_size); @@ -394,9 +400,85 @@ fn query_gop(system_table: &SystemTable) -> Option { height, stride, pixel_format, + edid, }) } +/// The panel's EDID base block, from the one GOP handle that shows the +/// scanout `shown` describes and carries `EFI_EDID_ACTIVE_PROTOCOL`. +/// +/// A machine's firmware puts the protocol on the child handle of the output +/// showing it, and the GOP this loader takes can be another handle over the +/// same output: on the T14 the first handle carries it, on the AMD bench the +/// second. A handle whose frame buffer and mode are not the scanout's shows +/// another output, so it is not asked; two that are and both carry one cannot +/// be told apart, and neither is taken. +fn scanout_edid(bs: &efi::BootServices, shown: (u64, GopModeKey)) -> Option<[u8; EDID_BLOCK]> { + let at = "EDID:"; + let handles = match bs.handles::() { + Ok(handles) => handles, + Err(status) => { + println!("{at} the GOP handles could not be listed ({status}): none handed over"); + return None; + } + }; + let of = handles.len(); + let mut carrying = handles.iter().enumerate().filter_map(|(index, handle)| { + let gop = match bs.get::(*handle) { + Ok(gop) => gop, + Err(status) => { + println!("{at} GOP handle {index} of {of} would not open its GOP ({status}), so it is not asked"); + return None; + } + }; + if (gop.frame_buffer().0, GopModeKey::of(gop.current_mode_info())) != shown { + return None; + } + Some((index, bs.get::(*handle).ok()?)) + }); + let (first, second) = (carrying.next(), carrying.next()); + match (first, second) { + (None, _) => { + println!("{at} no GOP handle showing the scanout publishes EDID Active: none handed over"); + None + } + (Some(_), Some(_)) => { + println!("{at} more than one GOP handle showing the scanout publishes EDID Active: none handed over"); + None + } + (Some((index, edid)), None) => { + let len = edid.bytes().len(); + let Some(base) = edid.bytes().first_chunk::() else { + println!("{at} GOP handle {index} of {of} shows the scanout and publishes {len} byte(s), less than a block: none handed over"); + return None; + }; + println!("{at} GOP handle {index} of {of} shows the scanout and publishes {len} byte(s), the base block handed over"); + Some(*base) + } + } +} + +/// What makes two GOP handles show one scanout: the mode's size, format and +/// stride, beside the frame buffer's base. +#[derive(Clone, Copy, PartialEq, Eq)] +struct GopModeKey { + width: u32, + height: u32, + format: PixelFormat, + stride: u32, +} + +impl GopModeKey { + fn of(mode: &efi::GopModeInfo) -> Self { + Self { + width: mode.horizontal_resolution, + height: mode.vertical_resolution, + format: mode.pixel_format, + stride: mode.pixels_per_scan_line, + } + } +} + /// Every range firmware's map says is write-back memory, `(base, length)`: /// a descriptor carrying `EFI_MEMORY_WB` and not one of the two I/O types, /// which a firmware may give the attribute without meaning memory. @@ -571,7 +653,13 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v wall_clock_secs: 0, wall_clock_counter: 0, wall_clock_known: 0, + gop_edid: [0; EDID_BLOCK], + gop_edid_len: 0, }; + if let Some(edid) = gop.as_ref().and_then(|g| g.edid) { + kernel_args.gop_edid = edid; + kernel_args.gop_edid_len = edid.len() as u64; + } kernel_args.loader_seed_len = seed::read(&system_table, &mut kernel_args.loader_seed); if let Some((civil, counter)) = wall_clock { kernel_args.wall_clock_secs = civil.to_unix_secs(); diff --git a/sdk/abi/src/boot.rs b/sdk/abi/src/boot.rs index 6d9071f7545..6840a8d728d 100644 --- a/sdk/abi/src/boot.rs +++ b/sdk/abi/src/boot.rs @@ -127,8 +127,18 @@ pub struct KernelArgs { /// failed or answered one that does not, which is a machine and not an /// error: the loader's line says which, and the kernel has no wall clock. pub wall_clock_known: u64, + /// The base block of the panel's EDID, unparsed, as firmware published it + /// through `EFI_EDID_ACTIVE_PROTOCOL` on the one GOP handle that shows + /// [`Self::gop_framebuffer`] in its mode, and how many of these bytes it + /// is: [`EDID_BLOCK`], or zero where no such handle carries one. The bytes + /// are firmware's, decoded by whoever reads them. + pub gop_edid: [u8; EDID_BLOCK], + pub gop_edid_len: u64, } +/// The bytes of [`KernelArgs::gop_edid`]: one EDID base block. +pub const EDID_BLOCK: usize = 128; + /// The bytes of [`KernelArgs::loader_seed`]. pub const SEED_LEN: usize = 32; @@ -252,7 +262,9 @@ const _: () = { assert!(offset_of!(KernelArgs, wall_clock_secs) == 1312); assert!(offset_of!(KernelArgs, wall_clock_counter) == 1320); assert!(offset_of!(KernelArgs, wall_clock_known) == 1328); - assert!(size_of::() == 1336); + assert!(offset_of!(KernelArgs, gop_edid) == 1336); + assert!(offset_of!(KernelArgs, gop_edid_len) == 1464); + assert!(size_of::() == 1472); assert!(LAYOUT as i32 > 1440 || (LAYOUT as i32) < -1440); assert!(align_of::() == 8); assert!(size_of::() == 16); @@ -352,6 +364,8 @@ mod tests { wall_clock_secs: 0, wall_clock_counter: 0, wall_clock_known: 0, + gop_edid: [0; EDID_BLOCK], + gop_edid_len: 0, }; #[test] diff --git a/sdk/abi/src/lib.rs b/sdk/abi/src/lib.rs index 51f7fd8cb6b..4a49aaf5b69 100644 --- a/sdk/abi/src/lib.rs +++ b/sdk/abi/src/lib.rs @@ -91,11 +91,15 @@ crate::user_safe! { pub stride: u32, pub pixel_format: u32, pub flags: u32, + /// The panel's EDID base block as the loader handed it over, unparsed, + /// and how many of these bytes it is: [`boot::EDID_BLOCK`] or zero. + pub edid: [u8; boot::EDID_BLOCK], + pub edid_len: u32, } } -// SAFETY: FramebufferInfo is #[repr(C)] and every field is a u32 or a -// `repr(transparent)` wrapper over one — no padding, no pointers. +// SAFETY: FramebufferInfo is #[repr(C)] and every field is a u32, bytes, or a +// `repr(transparent)` wrapper over a u32 — no padding, no pointers. unsafe impl Sync for FramebufferInfo {} // SAFETY: see the `Sync` impl immediately above — the same reasoning (no // pointers, no interior mutability) covers `Send`. diff --git a/system/compositor/Cargo.toml b/system/compositor/Cargo.toml index 9000141de58..d5749ada6d5 100644 --- a/system/compositor/Cargo.toml +++ b/system/compositor/Cargo.toml @@ -10,6 +10,7 @@ sprite = { path = "sprite" } toyos = { path = "../../sdk/toyos" } toyos-abi = { path = "../../sdk/abi" } toyos-desktop = { path = "desktop" } +toyos-edid = { path = "../../lib/edid" } toyos-input = { path = "../../lib/input" } toyos-inspect = { path = "../../lib/inspect" } toyos-font = { path = "../../sdk/font" } diff --git a/system/compositor/desktop/src/lib.rs b/system/compositor/desktop/src/lib.rs index 92c941e043c..a33652c5706 100644 --- a/system/compositor/desktop/src/lib.rs +++ b/system/compositor/desktop/src/lib.rs @@ -30,6 +30,7 @@ pub mod input; pub mod layout; pub mod plan; pub mod rect; +pub mod scale; pub mod stack; pub mod taskbar; pub mod volume; @@ -45,6 +46,7 @@ pub use input::{ pub use layout::{set_mode, Chrome, Desk}; pub use plan::{compose, content_blit, Blit, Layer}; pub use rect::{Point, Rect}; +pub use scale::Scale; pub use stack::Stack; pub use taskbar::{Taskbar, MAX_STATUS_CHARS, STATUS_MARGIN}; pub use window::{Level, Window, WindowId, WindowMode}; diff --git a/system/compositor/desktop/src/scale.rs b/system/compositor/desktop/src/scale.rs new file mode 100644 index 00000000000..f239efee2e2 --- /dev/null +++ b/system/compositor/desktop/src/scale.rs @@ -0,0 +1,170 @@ +//! How many panel pixels the desktop's pixel is, chosen from the panel's +//! density. +//! +//! ToyOS's own rule. The desktop's pixel is CSS Values 3 §5.2's reference +//! pixel, the visual angle of 1/96 inch at 28 inches, and every panel is +//! assumed viewed from 24 inches, so it is held to 96 x 28 / 24 = 112 dots +//! per inch. One distance for every panel makes the scale a function of +//! density alone; 24 inches is ToyOS's choice, not a published figure. + +use core::fmt; + +use crate::layout::Chrome; + +/// A scale in 120ths: a quarter step from 1 to 4. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Scale(u16); + +/// Every scale is a quarter step: 4/4 to 16/4. +const QUARTERS: core::ops::RangeInclusive = 4..=16; +const PER_QUARTER: u16 = 30; + +/// The reference pixel's density at the assumed distance, as the ratio +/// `96 * 28 / 24`. +const TARGET_DPI: (u64, u64) = (96 * 28, 24); +/// Millimetres per inch, as a ratio. +const MM_PER_INCH: (u64, u64) = (254, 10); + +/// A terminal's grid and its cell, which the least screen must hold. +const TERMINAL: (i32, i32) = (80, 24); +const CELL: (i32, i32) = (8, 16); + +/// The least logical screen: an 80x24 terminal of 8x16 cells in a window +/// frame, above the taskbar. +const LEAST: (u32, u32) = { + let c = Chrome::DEFAULT; + let w = TERMINAL.0 * CELL.0 + c.chrome_w(); + let h = TERMINAL.1 * CELL.1 + c.chrome_h() + c.taskbar; + (w as u32, h as u32) +}; + +impl Scale { + pub const ONE: Self = Self(120); + + fn quarters(q: u32) -> Self { + Self(q as u16 * PER_QUARTER) + } + + /// The scale for a `mode` in pixels on a panel of `mm`: the quarter step + /// from 1 to 4 nearest its density over [`TARGET_DPI`], a tie going to the + /// smaller, then the largest step at or below it that leaves the logical + /// screen at least 642x446. A mode below that at 1 gets 1. + pub fn auto(mode: (u32, u32), mm: (u32, u32)) -> Self { + // density / target = (px_diag / mm_diag) * (254/10) / (2688/24), compared + // squared so no root is taken: ideal^2 = A / B. + let px2 = u64::from(mode.0).pow(2) + u64::from(mode.1).pow(2); + let mm2 = u64::from(mm.0).pow(2) + u64::from(mm.1).pow(2); + let a = u128::from(px2) * u128::from(MM_PER_INCH.0 * TARGET_DPI.1).pow(2); + let b = u128::from(mm2) * u128::from(MM_PER_INCH.1 * TARGET_DPI.0).pow(2); + // Quarter q is nearest while ideal <= (2q + 1) / 8, i.e. 64 A <= (2q+1)^2 B. + let nearest = QUARTERS + .clone() + .find(|&q| 64 * a <= u128::from(2 * q + 1).pow(2) * b) + .unwrap_or(*QUARTERS.end()); + (*QUARTERS.start()..=nearest) + .rev() + .map(Self::quarters) + .find(|s| s.holds_least(mode)) + .unwrap_or(Self::ONE) + } + + /// Whether `mode` at this scale is at least [`LEAST`] in logical pixels. + fn holds_least(self, mode: (u32, u32)) -> bool { + let n = u64::from(self.0); + u64::from(mode.0) * 120 >= u64::from(LEAST.0) * n + && u64::from(mode.1) * 120 >= u64::from(LEAST.1) * n + } +} + +/// As a decimal: `1`, `1.25`, `1.5`, `1.75`. +impl fmt::Display for Scale { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (whole, frac) = (self.0 / 120, self.0 % 120); + match frac { + 0 => write!(f, "{whole}"), + 30 => write!(f, "{whole}.25"), + 60 => write!(f, "{whole}.5"), + 90 => write!(f, "{whole}.75"), + _ => unreachable!("a scale is a quarter step"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + + fn auto(w: u32, h: u32, wmm: u32, hmm: u32) -> alloc::string::String { + Scale::auto((w, h), (wmm, hmm)).to_string() + } + + #[test] + fn the_least_screen_is_a_terminal_over_the_taskbar() { + assert_eq!(LEAST, (642, 446)); + } + + /// The panels M1 read: 309x173 mm and 309x174 mm at 1920x1080. + #[test] + fn the_benches_panels_are_one_and_a_half() { + assert_eq!(auto(1920, 1080, 309, 173), "1.5"); + assert_eq!(auto(1920, 1080, 309, 174), "1.5"); + } + + /// PR #895's `scale-rule-r3.log` rows. + #[test] + fn each_row_of_the_rule_gives_its_step() { + let rows = [ + ((1280, 800, 325, 203), "1"), + ((1280, 720, 325, 183), "1"), + ((1024, 768, 260, 195), "1"), + ((1920, 1080, 310, 174), "1.5"), + ((2560, 1600, 256, 160), "2.25"), + ((3456, 2234, 340, 220), "2.25"), + ((1280, 720, 128, 72), "1.5"), + ((2560, 1440, 531, 299), "1"), + ((3840, 2160, 598, 336), "1.5"), + ((5120, 2880, 598, 336), "2"), + ((3840, 2160, 509, 286), "1.75"), + ((1920, 1080, 432, 243), "1"), + ((1920, 1080, 443, 249), "1"), + ]; + for ((w, h, wmm, hmm), want) in rows { + assert_eq!(auto(w, h, wmm, hmm), want, "{w}x{h} on {wmm}x{hmm} mm"); + } + } + + /// 2.25 is 254 dpi's nearest step and leaves 569x320 at 1280x720, so the + /// step falls until 642x446 fits. + #[test] + fn a_small_mode_steps_down_to_the_least_screen() { + assert_eq!(auto(1280, 720, 128, 72), "1.5"); + assert!(!Scale::quarters(7).holds_least((1280, 720))); + assert!(Scale::quarters(6).holds_least((1280, 720))); + } + + #[test] + fn a_mode_below_the_least_screen_is_one() { + assert_eq!(auto(640, 360, 163, 91), "1"); + } + + /// 1890x2520 on 381x508 mm is 126 dpi, 1.125 times the target: exactly + /// between 1 and 1.25. + #[test] + fn a_tie_goes_to_the_smaller_step() { + assert_eq!(auto(1890, 2520, 381, 508), "1"); + assert_eq!(auto(1890, 2520, 381, 507), "1.25"); + } + + #[test] + fn the_largest_step_is_four() { + assert_eq!(auto(7680, 4320, 100, 56), "4"); + } + + #[test] + fn a_scale_reads_as_a_decimal() { + let shown: alloc::vec::Vec<_> = QUARTERS.map(|q| Scale::quarters(q).to_string()).collect(); + assert_eq!(shown[..5], ["1", "1.25", "1.5", "1.75", "2"]); + assert_eq!(shown[12], "4"); + } +} diff --git a/system/compositor/src/session.rs b/system/compositor/src/session.rs index 49af3da6f2d..141ba8189ae 100644 --- a/system/compositor/src/session.rs +++ b/system/compositor/src/session.rs @@ -30,7 +30,7 @@ use toyos_desktop::volume::{overlay_rect, Overlay, Request, Requests, Shown}; use toyos::audio::{Volume, VolumeStep, MSG_VOLUME, MSG_VOLUME_MUTE, MSG_VOLUME_STEP, MSG_VOLUME_UNAVAILABLE}; use toyos_desktop::{ cursor_from_abs, cursor_style, fold_mouse, hit_test, key_action, set_mode, tab_action, Chrome, - CursorStyle, Damage, Desk, Grab, Held, Hit, KeyAction, Level, Point, Port, PromptGate, Rect, + CursorStyle, Damage, Desk, Scale, Grab, Held, Hit, KeyAction, Level, Point, Port, PromptGate, Rect, Released, Stack, TabAction, Verdict, Window, WindowId, WindowMode, }; use window::Screen; @@ -112,6 +112,36 @@ impl Wallpaper { } } +/// The scale for the mode `info` describes, from the panel's size in the EDID +/// block the claim carries; 1 where it carries none or no size, each said. +fn panel_scale(info: &toyos_abi::FramebufferInfo) -> Scale { + let mode = (info.width, info.height); + let said = format!("compositor: display {}x{}", mode.0, mode.1); + let edid = info + .edid + .get(..info.edid_len as usize) + .expect("the kernel hands over an EDID length of 0 or one block"); + if edid.is_empty() { + println!("{said}, no panel size, scale 1"); + return Scale::ONE; + } + match toyos_edid::BaseBlock::parse(edid).map(|block| block.image_size()) { + Ok(toyos_edid::ImageSize::Millimetres { width, height }) => { + let scale = Scale::auto(mode, (width, height)); + println!("{said}, panel {width}x{height} mm, scale {scale}"); + scale + } + Ok(size) => { + println!("{said}, the panel's EDID gives no size ({size:?}), scale 1"); + Scale::ONE + } + Err(why) => { + eprintln!("{said}, the panel's EDID is refused: {why}; scale 1"); + Scale::ONE + } + } +} + pub struct Session { acceptor: Acceptor, /// The prompt layer's port ([`toyos_manifest::consent::PROMPT`]), which @@ -148,6 +178,8 @@ pub struct Session { /// present with. fb_dev: FramebufferDev, fb_info: toyos_abi::FramebufferInfo, + /// The scale the panel's EDID gives the mode the claim was handed. + scale: Scale, /// Held because [`Session::screen`] points into it. _fb_shm: SharedMemory, screen: Screen, @@ -221,6 +253,7 @@ impl Session { .expect("the manifest gives this program the framebuffer"); let fb_info = fb_dev.info().expect("failed to read framebuffer info"); + let scale = panel_scale(&fb_info); let fb_size = fb_info.stride as usize * fb_info.height as usize * 4; // The scanout and cursor buffers are handles the claim's own read // installed, so a refusal here is the kernel contradicting itself and @@ -355,6 +388,7 @@ impl Session { apps, total_mem, max_windows, + scale, dead: Vec::new(), ready: Vec::new(), totals: FrameTotals::default(), @@ -1548,6 +1582,7 @@ impl Session { let mut snap = toyos_inspect::Snapshot::new(toyos_inspect::DISPLAY); snap.put("width", self.fb_info.width); snap.put("height", self.fb_info.height); + snap.put("scale", self.scale.to_string()); snap.put("cursor", if self.hw_cursor { "hardware" } else { "software" }); snap.put("windows.open", self.stack.len()); snap.put("windows.max", self.max_windows); diff --git a/tests/toyos.rs b/tests/toyos.rs index f5a8d6c34ae..d56ca6f4c50 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -312,6 +312,35 @@ const SCREEN_TESTS: &[(&str, qemu::Profile)] = &[ ("virt_low_ecam", qemu::Profile::VirtLowEcam), ]; +/// Which handle the loader took the panel's EDID from, and what the compositor +/// made of it, on each bench: the handle M1 found the protocol on, and the +/// size and scale `lib/edid`'s fixture of that bench's panel gives. +fn gop_edid_scale(loader: &str, log: &str, kernel: &str) -> Result<(), String> { + const T14: u32 = 0x0008_06c1; + const AMD_BENCH: u32 = 0x0086_0f01; + let signature = microcode_lines(kernel)? + .first() + .ok_or("the kernel wrote no microcode line to tell the bench by")? + .signature; + let (handle, panel) = match signature { + T14 => (0, "panel 309x173 mm, scale 1.5"), + AMD_BENCH => (1, "panel 309x174 mm, scale 1.5"), + other => return Err(format!("no bench of signature {other:#x} says which panel it has")), + }; + let handed = format!("EDID: GOP handle {handle} of 2 shows the scanout and publishes 128 byte(s), the base block handed over"); + if !loader.contains(&handed) { + let said: Vec<&str> = loader.lines().filter(|l| l.contains("EDID: ")).collect(); + return Err(format!("loader.log does not say {handed:?}; its EDID lines: {said:?}")); + } + let chosen = format!("compositor: display 1920x1080, {panel}"); + if !log.contains(&chosen) { + let said: Vec<&str> = log.lines().filter(|l| l.contains("compositor: display ")).collect(); + return Err(format!("the log does not say {chosen:?}; the compositor said {said:?}")); + } + eprintln!(" [gop_edid_scale] signature {signature:#x}: GOP handle {handle}, {panel}"); + Ok(()) +} + /// The tests whose machine shape *is* the test, each on a boot of its own. /// `run_machine_test` dispatches them. const MACHINE_TESTS: &[&str] = &[ @@ -865,6 +894,16 @@ const METAL: &[(&str, metal::Metal)] = &[ "metal_sim_scanout_wc", metal::Metal { arms: METALCASE, judge: |b| scanout_wc(b[0].kernel().text()) }, ), + ( + // The panel's EDID from the firmware's GOP to the compositor's scale: + // the loader's read before `ExitBootServices` is no host test's, and + // QEMU's OVMF publishes no EDID protocol. + "gop_edid_scale", + metal::Metal { + arms: METALCASE, + judge: |b| gop_edid_scale(b[0].loader().text(), b[0].log().text(), b[0].kernel().text()), + }, + ), ( // The machine's own i8042 and EC, driven from userland: the keyboard // reset and armed set 2 translated, its `0xAA`'s time named, the aux