Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ members = [
"lib/device-memory",
"lib/disk",
"lib/elf",
"lib/edid",
"lib/elide",
"lib/fat32",
"lib/fat32/check",
Expand Down
44 changes: 44 additions & 0 deletions build/licence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions issues/the-desktop-draws-at-the-panels-scale.md
Original file line number Diff line number Diff line change
@@ -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`.
3 changes: 1 addition & 2 deletions issues/toyos-has-a-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions kernel/src/drivers/gop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub fn init(
height: u32,
stride: u32,
pixel_format: u32,
edid: ([u8; toyos_abi::boot::EDID_BLOCK], u32),
) -> Option<(Box<dyn Gpu>, 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}");
Expand Down Expand Up @@ -97,6 +98,8 @@ pub fn init(
stride,
pixel_format,
flags: 0,
edid: edid.0,
edid_len: edid.1,
};

Some((Box::new(GopGpu), info))
Expand Down
2 changes: 2 additions & 0 deletions kernel/src/drivers/virtio_gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions kernel/src/gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
Expand All @@ -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(),
Expand Down Expand Up @@ -95,6 +101,8 @@ pub fn set_resolution(width: u32, height: u32) -> Result<GpuInfo, SyscallError>
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)
}
Expand Down
11 changes: 11 additions & 0 deletions kernel/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn gpu::Gpu>, info: gpu::GpuInfo) {
gpu::register(driver, info);
}
Expand Down Expand Up @@ -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"),
Expand Down
2 changes: 2 additions & 0 deletions kernel/src/object/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down
4 changes: 3 additions & 1 deletion kernel/src/syscall/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -418,6 +418,8 @@ pub(super) fn sys_gpu_reset_scanout(
stride,
pixel_format,
flags,
edid,
edid_len,
},
scanout,
cursor,
Expand Down
13 changes: 13 additions & 0 deletions lib/edid/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
15 changes: 15 additions & 0 deletions lib/edid/fixtures/SOURCE
Original file line number Diff line number Diff line change
@@ -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.
Binary file added lib/edid/fixtures/amd-bench-b140han06.8.bin
Binary file not shown.
Binary file added lib/edid/fixtures/qemu-default.bin
Binary file not shown.
Binary file added lib/edid/fixtures/qemu-dpi157-1920x1080.bin
Binary file not shown.
Binary file added lib/edid/fixtures/qemu-dpi254-2560x1600.bin
Binary file not shown.
Binary file added lib/edid/fixtures/qemu-dpi254-3456x2234.bin
Binary file not shown.
Binary file added lib/edid/fixtures/t14-n140hca-eae.bin
Binary file not shown.
Binary file added lib/edid/fixtures/virtio-gpu-get-edid-1024x768.bin
Binary file not shown.
99 changes: 99 additions & 0 deletions lib/edid/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Self, Refusal> {
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;
Loading