From c59ecd3b3b69e0cb284e74fb33877f532ccb6fad Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 20:36:53 +0200 Subject: [PATCH 1/8] Design: a display scale factor and live mode changes The design round's empty commit. The design and its roast are the pull request's body; the build drops this commit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy From 9d8a3811a591f52c4319602a9806a51492c33785 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 22:42:40 +0200 Subject: [PATCH 2/8] M1: the loader reports each GOP handle's EDID protocols, for one T14 and AMD bench reading PR #895's S2 sets the GOP machines' scale from the EDID the loader reads through UEFI's EFI_EDID_ACTIVE_PROTOCOL before ExitBootServices, and no boot of either laptop has asked whether its firmware publishes it. The loader now asks on every handle that carries a GOP: whether EDID Active and EDID Discovered are there, their SizeOfEdid, and each base block in hex. The metal row m1_edid_reading prints those lines from loader.log and is red only where the loader wrote none. Both are a reading, not S2's design: S2 replaces them with the copy into the handoff and its gop_edid_scale row. Under QEMU 11.1.1's OVMF, metal-sim's two GOP handles publish neither protocol, as edk2's QemuVideoDxe leaves it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- loader/src/efi/mod.rs | 2 +- loader/src/efi/proto.rs | 40 ++++++++++++++++++++++++++++++++++++++++ loader/src/loaderlog.rs | 3 +++ loader/src/main.rs | 41 ++++++++++++++++++++++++++++++++++++++++- tests/toyos.rs | 18 ++++++++++++++++++ 5 files changed, 102 insertions(+), 2 deletions(-) diff --git a/loader/src/efi/mod.rs b/loader/src/efi/mod.rs index b0525001e80..d71137a59a1 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, EdidDiscovered, File, Gop, 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..8674663e04e 100644 --- a/loader/src/efi/proto.rs +++ b/loader/src/efi/proto.rs @@ -595,6 +595,46 @@ impl Gop { } } +/// UEFI 2.10's `EFI_EDID_ACTIVE_PROTOCOL` and `EFI_EDID_DISCOVERED_PROTOCOL`: +/// one layout under two GUIDs, on the child handle of the video output they +/// describe. +#[repr(C)] +pub struct EdidOf { + size_of_edid: u32, + edid: *const u8, +} + +pub type EdidActive = EdidOf; +pub type EdidDiscovered = EdidOf; + +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]); +} + +// SAFETY: the specification's layout, under its GUID. +unsafe impl Protocol for EdidDiscovered { + const GUID: Guid = Guid::new(0x1c0c34f6, 0xd380, 0x41fa, [0xa0, 0x49, 0x8a, 0xd0, 0x6c, 0x1a, 0x66, 0xaa]); +} + +impl EdidOf { + /// 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/loaderlog.rs b/loader/src/loaderlog.rs index 68a225c7478..f0605e8b02b 100644 --- a/loader/src/loaderlog.rs +++ b/loader/src/loaderlog.rs @@ -23,6 +23,9 @@ pub const BEGINS_AT: &str = "ToyOS Bootloader 1.0"; /// tells this line from the kernel's own `GOP:` line on a console with both. pub const GOP_AT: &str = "GOP: mode"; +/// What `report_edid` begins each line with. +pub const EDID_AT: &str = "EDID:"; + /// The file's last line: [`close`] runs before the memory map is sized, and a /// write after that could grow the map the kernel is about to be handed. const ENDS_AT: &str = "Loader log: the kernel handoff begins, so this file ends here"; diff --git a/loader/src/main.rs b/loader/src/main.rs index 84551569d01..4ab3f435533 100644 --- a/loader/src/main.rs +++ b/loader/src/main.rs @@ -10,7 +10,7 @@ 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, EdidDiscovered, 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}; @@ -360,6 +360,7 @@ struct GopInfo { /// §12.9.2.2's `SetMode` is never called. fn query_gop(system_table: &SystemTable) -> Option { let bs = system_table.boot_services(); + report_edid(bs); let gop_handle = bs.handle_for::().ok()?; let gop = bs.get::(gop_handle).ok()?; @@ -397,6 +398,44 @@ fn query_gop(system_table: &SystemTable) -> Option { }) } +/// M1 (PR #895): what each handle carrying a GOP publishes of its output's +/// EDID, the first being the one [`query_gop`] takes, and the base block of +/// each one present. A reading, replaced by S2's copy into the handoff. +fn report_edid(bs: &efi::BootServices) { + let Ok(handles) = bs.handles::() else { + println!("{} no handle carries a GOP", loaderlog::EDID_AT); + return; + }; + for (index, handle) in handles.iter().enumerate() { + let active = bs.get::(*handle); + let discovered = bs.get::(*handle); + let size = |bytes: Option<&[u8]>| match bytes { + Some(bytes) => alloc::format!("{}", bytes.len()), + None => alloc::string::String::from("absent"), + }; + println!( + "{} GOP handle {index} of {} active={} discovered={}", + loaderlog::EDID_AT, + handles.len(), + size(active.as_deref().ok().map(|p| p.bytes())), + size(discovered.as_deref().ok().map(|p| p.bytes())), + ); + for (which, bytes) in [ + ("active", active.as_deref().ok().map(|p| p.bytes())), + ("discovered", discovered.as_deref().ok().map(|p| p.bytes())), + ] { + if let Some(base) = bytes.and_then(|b| b.get(..128)) { + let mut hex = alloc::string::String::with_capacity(256); + for byte in base { + use core::fmt::Write; + let _ = write!(hex, "{byte:02x}"); + } + println!("{} GOP handle {index} {which} base {hex}", loaderlog::EDID_AT); + } + } + } +} + /// 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. diff --git a/tests/toyos.rs b/tests/toyos.rs index 327e2b970fe..3fb8b33b161 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -304,6 +304,17 @@ const SCREEN_TESTS: &[(&str, qemu::Profile)] = &[ ("virt_low_ecam", qemu::Profile::VirtLowEcam), ]; +/// M1 (PR #895): every `EDID:` line the loader wrote, printed for the reading; +/// red only where it wrote none, which is a loader that did not ask. +fn m1_edid_reading(loader: &str) -> Result<(), String> { + let lines: Vec<&str> = loader.lines().filter(|l| l.contains("EDID: ")).collect(); + if lines.is_empty() { + return Err(format!("loader.log carries no EDID: line, so the loader never asked\n{loader}")); + } + lines.iter().for_each(|line| eprintln!(" [m1] {line}")); + 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] = &[ @@ -826,6 +837,13 @@ const METAL: &[(&str, metal::Metal)] = &[ "metal_sim_scanout_wc", metal::Metal { arms: METALCASE, judge: |b| scanout_wc(b[0].kernel().text()) }, ), + ( + // M1 (PR #895): what the firmware publishes of the panel's EDID, read + // from `loader.log`. A reading for S2's design, replaced by its + // `gop_edid_scale` row. + "m1_edid_reading", + metal::Metal { arms: METALCASE, judge: |b| m1_edid_reading(b[0].loader().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 From f355b6f13c90ef0461cf25b09cbbca6c7e0a47c2 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 01:11:32 +0200 Subject: [PATCH 3/8] S2's GOP half: the panel's EDID from the firmware to the compositor's scale The loader takes the panel's EDID base block from the one GOP handle that shows the scanout it hands over, by frame buffer base, size, format and stride, and that carries EFI_EDID_ACTIVE_PROTOCOL. M1 read the protocol on the first of two GOP handles on the T14 and on the second on the AMD bench, so the handle that shows the display is matched, not assumed; two matching handles that both carry one cannot be told apart, and neither is taken. KernelArgs and FramebufferInfo gain the block, unparsed, and its length, 0 or 128; the kernel refuses any other length the loader wrote. The compositor decodes it with the new toyos-edid crate (E-EDID 1.4: the first detailed timing's millimetres, falling back to bytes 0x15/0x16 in cm, an aspect ratio or unknown size giving none) and chooses a scale with toyos-desktop's Scale::auto, ToyOS's own rule as PR #895 states it: CSS Values 3's reference pixel at an assumed 24 inches, 112 dpi, the nearest quarter step from 1 to 4 with ties going down, stepped down until the logical screen holds 642x446. It logs the mode, the size and the scale, and `inspect display` reads the scale. Nothing draws at it yet. lib/edid's fixtures are both benches' panels as M1 read them, QEMU 11.1.1's generator, and its virtio-gpu's GET_EDID answer. M1's report and its row go; gop_edid_scale rides metalcase's boot and holds each bench to the handle M1 found the protocol on and to its fixture's size and scale. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- Cargo.lock | 5 + Cargo.toml | 1 + kernel/src/drivers/gop.rs | 3 + kernel/src/drivers/virtio_gpu.rs | 2 + kernel/src/gpu.rs | 8 + kernel/src/main.rs | 11 ++ kernel/src/object/device.rs | 2 + kernel/src/syscall/device.rs | 4 +- lib/edid/Cargo.toml | 13 ++ lib/edid/fixtures/SOURCE | 15 ++ lib/edid/fixtures/amd-bench-b140han06.8.bin | Bin 0 -> 128 bytes lib/edid/fixtures/qemu-default.bin | Bin 0 -> 128 bytes lib/edid/fixtures/qemu-dpi157-1920x1080.bin | Bin 0 -> 128 bytes lib/edid/fixtures/qemu-dpi254-2560x1600.bin | Bin 0 -> 128 bytes lib/edid/fixtures/qemu-dpi254-3456x2234.bin | Bin 0 -> 128 bytes lib/edid/fixtures/t14-n140hca-eae.bin | Bin 0 -> 128 bytes .../fixtures/virtio-gpu-get-edid-1024x768.bin | Bin 0 -> 128 bytes lib/edid/src/lib.rs | 99 ++++++++++ lib/edid/src/tests.rs | 98 ++++++++++ loader/src/efi/mod.rs | 2 +- loader/src/efi/proto.rs | 17 +- loader/src/loaderlog.rs | 2 +- loader/src/main.rs | 107 +++++++---- sdk/abi/src/boot.rs | 16 +- sdk/abi/src/lib.rs | 8 +- system/compositor/Cargo.toml | 1 + system/compositor/desktop/src/lib.rs | 2 + system/compositor/desktop/src/scale.rs | 175 ++++++++++++++++++ system/compositor/src/session.rs | 34 +++- tests/toyos.rs | 47 +++-- 30 files changed, 602 insertions(+), 70 deletions(-) create mode 100644 lib/edid/Cargo.toml create mode 100644 lib/edid/fixtures/SOURCE create mode 100644 lib/edid/fixtures/amd-bench-b140han06.8.bin create mode 100644 lib/edid/fixtures/qemu-default.bin create mode 100644 lib/edid/fixtures/qemu-dpi157-1920x1080.bin create mode 100644 lib/edid/fixtures/qemu-dpi254-2560x1600.bin create mode 100644 lib/edid/fixtures/qemu-dpi254-3456x2234.bin create mode 100644 lib/edid/fixtures/t14-n140hca-eae.bin create mode 100644 lib/edid/fixtures/virtio-gpu-get-edid-1024x768.bin create mode 100644 lib/edid/src/lib.rs create mode 100644 lib/edid/src/tests.rs create mode 100644 system/compositor/desktop/src/scale.rs diff --git a/Cargo.lock b/Cargo.lock index a66297b3989..51be6680c32 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", @@ -4179,6 +4180,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 d1205df4082..54fa8b521a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ members = [ "lib/device-memory", "lib/disk", "lib/elf", + "lib/edid", "lib/elide", "lib/fat32", "lib/fat32/check", 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 fa8ade0c680..1fbb62d889d 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 64aebc035d2..ab13c964a96 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -385,7 +385,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 { @@ -396,6 +396,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 0000000000000000000000000000000000000000..89f78ffea5cc381fdc2f1f96b245eb608d131c01 GIT binary patch literal 128 zcmZSh4+abd>t=N@fPgF`%Tjs43g#VB%R;#Gu8M5LKHa|9v--O;3C6Hhs_LA85$cJ z9bRcN90*j9F>p|Gad15?z`!5@)cT!)i@~9ZK|=v#4jBApFshYX@Sl-O0Stg*e;5K? ReM1#|^Yb!G@{70_tpJHtB>?~c literal 0 HcmV?d00001 diff --git a/lib/edid/fixtures/qemu-dpi157-1920x1080.bin b/lib/edid/fixtures/qemu-dpi157-1920x1080.bin new file mode 100644 index 0000000000000000000000000000000000000000..625eaa72266dc2ba3c3afabee3de8700e49f84a5 GIT binary patch literal 128 zcmZSh4+adLA|^r%K%gbT$g)&ku!8N~#Kj>#Gu8M5LKHa|9v--O;3C6Hhs_LA85$cJ z9WI45EGV*2b$DPI>R`4`fPp~*sP#Jo7lT6+gN6dg95DFHU{ou&;6Edm0vG_r{xAf( S`i3g_=I3RWfpeF$U literal 0 HcmV?d00001 diff --git a/lib/edid/fixtures/qemu-dpi254-2560x1600.bin b/lib/edid/fixtures/qemu-dpi254-2560x1600.bin new file mode 100644 index 0000000000000000000000000000000000000000..836f0ab061d1ff774a459ba7953fe6c6551124ff GIT binary patch literal 128 zcmZSh4+adLA|^r%K%gbT$g)&Ypn~n)#Kj>#Gu8M5LKHa|9v--O;3C6Hhs_LA85$cJ z9ab-AXjtrEk#Gu8M5LKHa|9v--O;3C6Hhs_LA85$cJ z9e@hIGjK6DG%;u>0ND`mm%*r3Zoz*>E(I_E3jSdTboC8Y@XgQ5EXgn80*VMg)j}zz F>i{2`AprmY literal 0 HcmV?d00001 diff --git a/lib/edid/fixtures/t14-n140hca-eae.bin b/lib/edid/fixtures/t14-n140hca-eae.bin new file mode 100644 index 0000000000000000000000000000000000000000..19ce893d23332eace186422678cbcba0213956b3 GIT binary patch literal 128 zcmZSh4+adp>$pT1fIw50k!7j8UA>h#EvK6SZ(5TGa{8Za|!SWsZ0;9#JzjKOrR l00V;rP|-gIKSL7(4`)YRS4USa1+a*-uOF8J3}8{i1OT&S8vy_S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f9962d80020ffa242ed648bf94105ad7c228b8a1 GIT binary patch literal 128 zcmZSh4+adLA|^r%K%gbT$g)&QxPtB7#Kj>#Gu8M5LKHa|9v--O;3C6Hhs_LA85$cJ z9aI$<(i|D23>f5$9as(vFfd2}wSH&dVsL0;&`) -> 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 d71137a59a1..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, EdidActive, EdidDiscovered, 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 8674663e04e..3d22054bd05 100644 --- a/loader/src/efi/proto.rs +++ b/loader/src/efi/proto.rs @@ -595,18 +595,14 @@ impl Gop { } } -/// UEFI 2.10's `EFI_EDID_ACTIVE_PROTOCOL` and `EFI_EDID_DISCOVERED_PROTOCOL`: -/// one layout under two GUIDs, on the child handle of the video output they -/// describe. +/// UEFI 2.10's `EFI_EDID_ACTIVE_PROTOCOL`, on the child handle of the video +/// output it describes. #[repr(C)] -pub struct EdidOf { +pub struct EdidActive { size_of_edid: u32, edid: *const u8, } -pub type EdidActive = EdidOf; -pub type EdidDiscovered = EdidOf; - const _: () = { assert!(size_of::() == 16); assert!(offset_of!(EdidActive, edid) == 8); @@ -617,12 +613,7 @@ unsafe impl Protocol for EdidActive { const GUID: Guid = Guid::new(0xbd8c1056, 0x9f36, 0x44ec, [0x92, 0xa8, 0xa6, 0x33, 0x7f, 0x81, 0x79, 0x86]); } -// SAFETY: the specification's layout, under its GUID. -unsafe impl Protocol for EdidDiscovered { - const GUID: Guid = Guid::new(0x1c0c34f6, 0xd380, 0x41fa, [0xa0, 0x49, 0x8a, 0xd0, 0x6c, 0x1a, 0x66, 0xaa]); -} - -impl EdidOf { +impl EdidActive { /// The EDID's bytes, empty where the output gave none: `SizeOfEdid` is 0 /// and `Edid` null then. pub fn bytes(&self) -> &[u8] { diff --git a/loader/src/loaderlog.rs b/loader/src/loaderlog.rs index f0605e8b02b..07378282a10 100644 --- a/loader/src/loaderlog.rs +++ b/loader/src/loaderlog.rs @@ -23,7 +23,7 @@ pub const BEGINS_AT: &str = "ToyOS Bootloader 1.0"; /// tells this line from the kernel's own `GOP:` line on a console with both. pub const GOP_AT: &str = "GOP: mode"; -/// What `report_edid` begins each line with. +/// What `scanout_edid` begins its line with. pub const EDID_AT: &str = "EDID:"; /// The file's last line: [`close`] runs before the memory map is sized, and a diff --git a/loader/src/main.rs b/loader/src/main.rs index 4ab3f435533..81bdc3b84fa 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, EdidActive, EdidDiscovered, 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 @@ -360,7 +361,6 @@ struct GopInfo { /// §12.9.2.2's `SetMode` is never called. fn query_gop(system_table: &SystemTable) -> Option { let bs = system_table.boot_services(); - report_edid(bs); let gop_handle = bs.handle_for::().ok()?; let gop = bs.get::(gop_handle).ok()?; @@ -384,6 +384,7 @@ fn query_gop(system_table: &SystemTable) -> Option { }; let (framebuffer, framebuffer_size) = gop.frame_buffer(); + let edid = scanout_edid(bs, (framebuffer, GopModeKey::of(mode))); println!("{} {}x{} stride={} format={} fb={:#x} size={}", loaderlog::GOP_AT, width, height, stride, pixel_format, framebuffer, framebuffer_size); @@ -395,43 +396,69 @@ fn query_gop(system_table: &SystemTable) -> Option { height, stride, pixel_format, + edid, }) } -/// M1 (PR #895): what each handle carrying a GOP publishes of its output's -/// EDID, the first being the one [`query_gop`] takes, and the base block of -/// each one present. A reading, replaced by S2's copy into the handoff. -fn report_edid(bs: &efi::BootServices) { - let Ok(handles) = bs.handles::() else { - println!("{} no handle carries a GOP", loaderlog::EDID_AT); - return; - }; - for (index, handle) in handles.iter().enumerate() { - let active = bs.get::(*handle); - let discovered = bs.get::(*handle); - let size = |bytes: Option<&[u8]>| match bytes { - Some(bytes) => alloc::format!("{}", bytes.len()), - None => alloc::string::String::from("absent"), - }; - println!( - "{} GOP handle {index} of {} active={} discovered={}", - loaderlog::EDID_AT, - handles.len(), - size(active.as_deref().ok().map(|p| p.bytes())), - size(discovered.as_deref().ok().map(|p| p.bytes())), - ); - for (which, bytes) in [ - ("active", active.as_deref().ok().map(|p| p.bytes())), - ("discovered", discovered.as_deref().ok().map(|p| p.bytes())), - ] { - if let Some(base) = bytes.and_then(|b| b.get(..128)) { - let mut hex = alloc::string::String::with_capacity(256); - for byte in base { - use core::fmt::Write; - let _ = write!(hex, "{byte:02x}"); - } - println!("{} GOP handle {index} {which} base {hex}", loaderlog::EDID_AT); - } +/// 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 handles = bs.handles::().ok()?; + let mut carrying = handles.iter().enumerate().filter_map(|(index, handle)| { + let gop = bs.get::(*handle).ok()?; + 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()); + let at = loaderlog::EDID_AT; + let of = handles.len(); + 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, } } } @@ -610,7 +637,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 db548978ea2..e20c209f5cb 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 window; @@ -44,6 +45,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..6b5ef17d2e4 --- /dev/null +++ b/system/compositor/desktop/src/scale.rs @@ -0,0 +1,175 @@ +//! 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 as 120ths, the representation the wire carries. +#[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. +pub 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); + + /// The 120ths, as the wire carries them. + pub const fn n(self) -> u16 { + self.0 + } + + 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 [`LEAST`]. A mode below [`LEAST`] 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. + pub 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 e0e2c6657c5..11d52fe362e 100644 --- a/system/compositor/src/session.rs +++ b/system/compositor/src/session.rs @@ -28,7 +28,7 @@ use toyos_input::{Merge, Pointer, Slot, State}; use toyos_desktop::clipboard::{Landing, Watch}; 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; @@ -105,6 +105,33 @@ 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[..(info.edid_len as usize).min(info.edid.len())]; + if edid.is_empty() { + eprintln!("{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)); + eprintln!("{said}, panel {width}x{height} mm, scale {scale}"); + scale + } + Ok(size) => { + eprintln!("{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 @@ -141,6 +168,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, @@ -206,6 +235,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 @@ -342,6 +372,7 @@ impl Session { apps, total_mem, max_windows, + scale, dead: Vec::new(), ready: Vec::new(), totals: FrameTotals::default(), @@ -1443,6 +1474,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 3fb8b33b161..9c459282a4d 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -304,14 +304,32 @@ const SCREEN_TESTS: &[(&str, qemu::Profile)] = &[ ("virt_low_ecam", qemu::Profile::VirtLowEcam), ]; -/// M1 (PR #895): every `EDID:` line the loader wrote, printed for the reading; -/// red only where it wrote none, which is a loader that did not ask. -fn m1_edid_reading(loader: &str) -> Result<(), String> { - let lines: Vec<&str> = loader.lines().filter(|l| l.contains("EDID: ")).collect(); - if lines.is_empty() { - return Err(format!("loader.log carries no EDID: line, so the loader never asked\n{loader}")); - } - lines.iter().for_each(|line| eprintln!(" [m1] {line}")); +/// 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(()) } @@ -838,11 +856,14 @@ const METAL: &[(&str, metal::Metal)] = &[ metal::Metal { arms: METALCASE, judge: |b| scanout_wc(b[0].kernel().text()) }, ), ( - // M1 (PR #895): what the firmware publishes of the panel's EDID, read - // from `loader.log`. A reading for S2's design, replaced by its - // `gop_edid_scale` row. - "m1_edid_reading", - metal::Metal { arms: METALCASE, judge: |b| m1_edid_reading(b[0].loader().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 From 2355f7b84ec8f7401e2163dc5a0bd5866c0278f8 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 01:52:06 +0200 Subject: [PATCH 4/8] S2: declare the EDID fixtures as the device and firmware output they are Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- build/licence.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/build/licence.rs b/build/licence.rs index be62f8b1429..6414080e022 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 From 6fd69e2b4a76082b7b82596d442f52c0eec7687e Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 02:04:18 +0200 Subject: [PATCH 5/8] S2: close the loader's GOP before asking every handle again A second GET_PROTOCOL open by the same image shares the first's entry, so closing both answered NOT_FOUND and the loader panicked on metal-sim. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- loader/src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/loader/src/main.rs b/loader/src/main.rs index 81bdc3b84fa..91fc80f2f98 100644 --- a/loader/src/main.rs +++ b/loader/src/main.rs @@ -384,7 +384,11 @@ fn query_gop(system_table: &SystemTable) -> Option { }; let (framebuffer, framebuffer_size) = gop.frame_buffer(); - let edid = scanout_edid(bs, (framebuffer, GopModeKey::of(mode))); + 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); From 56a8bfbb673be9cc360be43bad05c6b71d765253 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 02:22:37 +0200 Subject: [PATCH 6/8] S2: the compositor's panel reading goes to standard output Only an EDID block refused as malformed is an error; a size, or the lack of one, is a reading, as the compositor's other non-failure lines are. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/compositor/src/session.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/compositor/src/session.rs b/system/compositor/src/session.rs index 7201e4121ca..b65109e1ee3 100644 --- a/system/compositor/src/session.rs +++ b/system/compositor/src/session.rs @@ -119,17 +119,17 @@ fn panel_scale(info: &toyos_abi::FramebufferInfo) -> Scale { let said = format!("compositor: display {}x{}", mode.0, mode.1); let edid = &info.edid[..(info.edid_len as usize).min(info.edid.len())]; if edid.is_empty() { - eprintln!("{said}, no panel size, scale 1"); + 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)); - eprintln!("{said}, panel {width}x{height} mm, scale {scale}"); + println!("{said}, panel {width}x{height} mm, scale {scale}"); scale } Ok(size) => { - eprintln!("{said}, the panel's EDID gives no size ({size:?}), scale 1"); + println!("{said}, the panel's EDID gives no size ({size:?}), scale 1"); Scale::ONE } Err(why) => { From 229d3689269182e6726abe77b2a5e418bd424e79 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 02:37:38 +0200 Subject: [PATCH 7/8] The rest of the HiDPI work is a track of its own issues/the-desktop-draws-at-the-panels-scale.md carries what PR #895 does not land: the display port and its override, drawing at the scale, the window protocol and the winit fork, and virtio-gpu leaving the kernel after the slot issue. It links the accepted design and its reviews on #895, and the desktop track's stage 5 points at it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- .../the-desktop-draws-at-the-panels-scale.md | 57 +++++++++++++++++++ issues/toyos-has-a-desktop.md | 3 +- 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 issues/the-desktop-draws-at-the-panels-scale.md 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..bcd19c9d943 --- /dev/null +++ b/issues/the-desktop-draws-at-the-panels-scale.md @@ -0,0 +1,57 @@ +--- +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.** The `display` port, served by the compositor and + received by `toybox` alone (`build/build.rs`'s `HELD_ALONE`), carrying + `screen scale |auto`, kept in `/state/compositor`. The window + protocol's `WindowInfo` and `ResizeInfo` carry the scale as `n/120`, a + create request says whether its size is logical or physical, and + `toyos_window::Window` answers `scale_factor()`. The compositor draws its + chrome, glyphs (from the TTF, in `toyos-font`), icons and cursor (one + rasterizer, `sdk/icon`, replacing `sprite` and `files`' copy) at the scale, + relays windows out on a change, and holds two buffers per window across a + resize, both counted by `budget::window_bytes`. terminal, filepicker and + `files` render at the scale; `editor` and `paint` stay at 1 until they read + it in their own repositories. One commit on the winit fork's + `toyos-0.30.13` answers `scale_factor()` and sends `ScaleFactorChanged`. +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 From cb9d2a4ea207f9d93e7f04a8e5c530e1a5fe65e3 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 03:02:14 +0200 Subject: [PATCH 8/8] Review: Scale has no wire accessor yet, and the EDID path says why at every turn Scale::n() goes with its false comment: no wire carries a scale until the track's first stage, which adds it with its first caller. LEAST and holds_least are private. The loader names a GOP list or open it could not make, where it dropped them silently, and spells its prefix in place. The compositor takes the EDID length the kernel guarantees with an expect naming that contract, where it clamped. The track's first item lists deliverables and leaves the design to the comment it links. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- .../the-desktop-draws-at-the-panels-scale.md | 18 +++++------------ loader/src/loaderlog.rs | 3 --- loader/src/main.rs | 20 +++++++++++++++---- system/compositor/desktop/src/scale.rs | 13 ++++-------- system/compositor/src/session.rs | 5 ++++- 5 files changed, 29 insertions(+), 30 deletions(-) diff --git a/issues/the-desktop-draws-at-the-panels-scale.md b/issues/the-desktop-draws-at-the-panels-scale.md index bcd19c9d943..fbdc51ecace 100644 --- a/issues/the-desktop-draws-at-the-panels-scale.md +++ b/issues/the-desktop-draws-at-the-panels-scale.md @@ -22,19 +22,11 @@ chooses the scale, and the compositor logs it and answers it in What is left: -1. **The rest of the scale.** The `display` port, served by the compositor and - received by `toybox` alone (`build/build.rs`'s `HELD_ALONE`), carrying - `screen scale |auto`, kept in `/state/compositor`. The window - protocol's `WindowInfo` and `ResizeInfo` carry the scale as `n/120`, a - create request says whether its size is logical or physical, and - `toyos_window::Window` answers `scale_factor()`. The compositor draws its - chrome, glyphs (from the TTF, in `toyos-font`), icons and cursor (one - rasterizer, `sdk/icon`, replacing `sprite` and `files`' copy) at the scale, - relays windows out on a change, and holds two buffers per window across a - resize, both counted by `budget::window_bytes`. terminal, filepicker and - `files` render at the scale; `editor` and `paint` stay at 1 until they read - it in their own repositories. One commit on the winit fork's - `toyos-0.30.13` answers `scale_factor()` and sends `ScaleFactorChanged`. +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 diff --git a/loader/src/loaderlog.rs b/loader/src/loaderlog.rs index 07378282a10..68a225c7478 100644 --- a/loader/src/loaderlog.rs +++ b/loader/src/loaderlog.rs @@ -23,9 +23,6 @@ pub const BEGINS_AT: &str = "ToyOS Bootloader 1.0"; /// tells this line from the kernel's own `GOP:` line on a console with both. pub const GOP_AT: &str = "GOP: mode"; -/// What `scanout_edid` begins its line with. -pub const EDID_AT: &str = "EDID:"; - /// The file's last line: [`close`] runs before the memory map is sized, and a /// write after that could grow the map the kernel is about to be handed. const ENDS_AT: &str = "Loader log: the kernel handoff begins, so this file ends here"; diff --git a/loader/src/main.rs b/loader/src/main.rs index 91fc80f2f98..29547d45dc1 100644 --- a/loader/src/main.rs +++ b/loader/src/main.rs @@ -414,17 +414,29 @@ fn query_gop(system_table: &SystemTable) -> Option { /// 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 handles = bs.handles::().ok()?; + 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 = bs.get::(*handle).ok()?; + 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()); - let at = loaderlog::EDID_AT; - let of = handles.len(); match (first, second) { (None, _) => { println!("{at} no GOP handle showing the scanout publishes EDID Active: none handed over"); diff --git a/system/compositor/desktop/src/scale.rs b/system/compositor/desktop/src/scale.rs index 6b5ef17d2e4..f239efee2e2 100644 --- a/system/compositor/desktop/src/scale.rs +++ b/system/compositor/desktop/src/scale.rs @@ -11,7 +11,7 @@ use core::fmt; use crate::layout::Chrome; -/// A scale as 120ths, the representation the wire carries. +/// A scale in 120ths: a quarter step from 1 to 4. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Scale(u16); @@ -31,7 +31,7 @@ const CELL: (i32, i32) = (8, 16); /// The least logical screen: an 80x24 terminal of 8x16 cells in a window /// frame, above the taskbar. -pub const LEAST: (u32, u32) = { +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; @@ -41,11 +41,6 @@ pub const LEAST: (u32, u32) = { impl Scale { pub const ONE: Self = Self(120); - /// The 120ths, as the wire carries them. - pub const fn n(self) -> u16 { - self.0 - } - fn quarters(q: u32) -> Self { Self(q as u16 * PER_QUARTER) } @@ -53,7 +48,7 @@ impl Scale { /// 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 [`LEAST`]. A mode below [`LEAST`] at 1 gets 1. + /// 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. @@ -74,7 +69,7 @@ impl Scale { } /// Whether `mode` at this scale is at least [`LEAST`] in logical pixels. - pub fn holds_least(self, mode: (u32, u32)) -> bool { + 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 diff --git a/system/compositor/src/session.rs b/system/compositor/src/session.rs index b65109e1ee3..141ba8189ae 100644 --- a/system/compositor/src/session.rs +++ b/system/compositor/src/session.rs @@ -117,7 +117,10 @@ impl Wallpaper { 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[..(info.edid_len as usize).min(info.edid.len())]; + 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;