From b0c58dc887a244c5998765695e18a8637c93cee0 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 1 Nov 2026 11:30:47 +0100 Subject: [PATCH 1/3] tgl-display: the DMC's program loaded, PG3-PG5 down and DC6 allowed (stage 2) The display engine can enter DC5 and DC6 only with its context save and restore program loaded and every power well above PG2 off (Vol 12-12.21 "Sequences for Display C5 and C6"). The T14's GOP leaves PG1-PG4 requested and DC_STATE_EN at 0. `toyos-tgl-display` now embeds Intel's Tiger Lake DMC package, `i915/tgl_dmc_ver2_12.bin` from linux-firmware tag 20261019, unmodified and pinned by sha256 in NOTICE and the licence gate. LICENSE.i915 permits binary redistribution without modification, so an image that builds tgl-display carries its text under /system/share/licenses/. `dmc::Package::parse` reads the CSS header, the package header and every program the entries name, by the layouts Vol 12 "DMC Firmware Package" gives. It refuses a file that does not hold together, and a main program whose storage or header writes fall outside 80000h-8FFFFh. The main program is chosen for the display stepping that Vol 4 "Steppings and Device IDs" gives the function's PCI revision (9A49: 1h is C0, 3h is D0), with exceptions listed before defaults. `allow_display_c6` runs after the watermarks, on the one-plane scan-out `scanout` found: - it refuses a transcoder A on a DDI outside PG1, and VGA display on, since both need PG3; - it disallows DC states already allowed, since a DC5/DC6 switch goes through Disable and a reload; - it stores the payload, reads it back and makes the header's writes; - it withdraws the PG5, PG4 and PG3 requests and awaits each state (20 us, Vol 12 "Power Well"); - it sets 45520h[1:0] and allows up to DC6. Every refusal is by name, and DC6 is never allowed after one. Loading is MMIO in the iGPU's own BAR, with no DMA. The CSS header's key, modulus and exponent fields are "Not used" and 0 in this file, so nothing on record shows the engine verifies the program (issues/nothing-shows-the-tiger-lake-display-engine-verifies-its-dmc-program.md). With the pipe lit, DC5 is entered only while PSR is active, so this stage alone saves at most what PG3 and PG4 cost. The track now orders the compositor's present/damage piece of `display-output` before PSR, by the orchestrator's ruling, rather than an interim signal. Co-Authored-By: Claude Opus 5.5 --- NOTICE | 29 +++ build/build.rs | 13 + build/licence.rs | 8 +- ...ves-its-displays-from-a-userland-driver.md | 20 +- ...display-engine-verifies-its-dmc-program.md | 33 +++ licenses/Intel-i915-firmware-license.txt | 39 +++ system/tgl-display/driver/src/dc_tests.rs | 223 +++++++++++++++++ system/tgl-display/driver/src/dmc.rs | 232 ++++++++++++++++++ system/tgl-display/driver/src/lib.rs | 112 ++++++++- system/tgl-display/driver/src/model.rs | 47 +++- system/tgl-display/driver/src/regs.rs | 32 +++ .../tgl-display/firmware/tgl_dmc_ver2_12.bin | Bin 0 -> 19760 bytes system/tgl-display/src/main.rs | 28 ++- 13 files changed, 797 insertions(+), 19 deletions(-) create mode 100644 issues/nothing-shows-the-tiger-lake-display-engine-verifies-its-dmc-program.md create mode 100644 licenses/Intel-i915-firmware-license.txt create mode 100644 system/tgl-display/driver/src/dc_tests.rs create mode 100644 system/tgl-display/driver/src/dmc.rs create mode 100644 system/tgl-display/firmware/tgl_dmc_ver2_12.bin diff --git a/NOTICE b/NOTICE index 921836ac7f8..c74e136e7a3 100644 --- a/NOTICE +++ b/NOTICE @@ -748,6 +748,35 @@ parses is read. Whether the cards verify these files is unconfirmed: issues/nothing-shows-the-ax200-or-ax210-verifies-its-firmware.md. +system/tgl-display/firmware/* — Intel display firmware, redistributable unmodified +----------------------------------------------------------------------------------- + + tgl_dmc_ver2_12.bin 19,760 bytes, Tiger Lake's Display Micro-Controller + package, version 2.12 + sha256 3c013ef0ad96ba73aee8e5bd04a8e27cc9b1c6e9183b1a83ce124485f325afca + Upstream: https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git, + `i915/` at tag 20261019 (commit a2dcb924f0897368b0b4f305cbf9808cdda90ce0), + byte for byte + Copyright: Intel Corporation + Licence text: licenses/Intel-i915-firmware-license.txt (upstream's + `LICENSES/LICENSE.i915` at that commit, which WHENCE names + LicenseRef-LICENSE.i915, + sha256 8542aeabf2761935122d693561e16766ce1bcc2b0d003204f9040b7d6d929f2e) + SPDX-License-Identifier: LicenseRef-Intel-i915-Firmware + +The newest Tiger Lake build at that tag. `toyos-tgl-display` embeds it, and +the `tgl-display` driver loads its main program into the display engine's +program storage through its own registers. An image building `tgl-display` +carries the licence as /system/share/licenses/Intel-i915-firmware-license.txt. +The licence permits redistribution in binary form, without modification, on +three conditions: the copyright notice and the disclaimer are reproduced with +the distribution; Intel's name endorses nothing derived from it; and no +reverse engineering, decompilation or disassembly, so of the file only the +headers Vol 12 "DMC Firmware Package" lays out are read. Its patent licence +covers use alone or with an operating system under an OSI-approved licence. +Whether the display engine verifies the program is unknown: +issues/nothing-shows-the-tiger-lake-display-engine-verifies-its-dmc-program.md. + tests/cavp/SHA*.rsp — NIST's SHA-2 test vectors, CAVP SHAVS ------------------------------------------------------------ diff --git a/build/build.rs b/build/build.rs index 1ce2b51f27c..9636401357c 100644 --- a/build/build.rs +++ b/build/build.rs @@ -943,6 +943,15 @@ pub const MICROCODE_LICENCES: [(&str, &[u8]); 2] = [ ("share/licenses/Intel-microcode-license.txt", include_bytes!("../licenses/Intel-microcode-license.txt")), ]; +/// The program that embeds Intel's display firmware, where on ROOT that +/// firmware's licence is, and its text: the licence asks that a +/// redistribution carry it. +pub const DISPLAY_FIRMWARE_LICENCE: (&str, &str, &[u8]) = ( + "tgl-display", + "share/licenses/Intel-i915-firmware-license.txt", + include_bytes!("../licenses/Intel-i915-firmware-license.txt"), +); + /// The Mozilla root program's authorities as `webpki-root-certs` publishes /// them, each certificate a PEM block in the published order. pub fn trust_roots() -> Vec { @@ -982,6 +991,10 @@ fn build_and_assemble( } root_files.push((TRUST_ROOTS.to_string(), trust_roots())); + let (embedder, path, text) = DISPLAY_FIRMWARE_LICENCE; + if programs.contains(embedder) { + root_files.push((path.to_string(), text.to_vec())); + } root_files.push((TRUST_ROOTS_LICENCE.0.to_string(), TRUST_ROOTS_LICENCE.1.to_vec())); match arch { Arch::X86_64 => { diff --git a/build/licence.rs b/build/licence.rs index 08217a5ab07..a0cd00b97a3 100644 --- a/build/licence.rs +++ b/build/licence.rs @@ -90,7 +90,7 @@ const DATA_ONLY: &[&str] = &["CDLA-Permissive-2.0"]; /// Allowed for a [`Terms::Firmware`] row, and nowhere else: vendor firmware /// the owner ruled may ship, pinned and unmodified. const FIRMWARE_ONLY: &[&str] = - &["LicenseRef-AMD-Microcode", "LicenseRef-Intel-Microcode", "LicenseRef-Intel-iwlwifi-Firmware"]; + &["LicenseRef-AMD-Microcode", "LicenseRef-Intel-Microcode", "LicenseRef-Intel-iwlwifi-Firmware", "LicenseRef-Intel-i915-Firmware"]; /// What a licence is judged as: [`FONTS_ONLY`] passes only for a font, /// [`DATA_ONLY`] only for the trust roots and [`FIRMWARE_ONLY`] only for @@ -1136,6 +1136,12 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "NOTICE", Terms::Firmware("LicenseRef-Intel-iwlwifi-Firmware"), ), + ( + "system/tgl-display/firmware/tgl_dmc_ver2_12.bin", + "3c013ef0ad96ba73aee8e5bd04a8e27cc9b1c6e9183b1a83ce124485f325afca", + "NOTICE", + Terms::Firmware("LicenseRef-Intel-i915-Firmware"), + ), ]; /// `NOTICE` sections that name no files, and why. diff --git a/issues/each-gpu-drives-its-displays-from-a-userland-driver.md b/issues/each-gpu-drives-its-displays-from-a-userland-driver.md index 6ecdb11323a..bf6d1a8955e 100644 --- a/issues/each-gpu-drives-its-displays-from-a-userland-driver.md +++ b/issues/each-gpu-drives-its-displays-from-a-userland-driver.md @@ -79,14 +79,18 @@ The kernel keeps only generic mechanisms: - **Exit:** on the T14 with the panel lit, `osbench energy`'s idle row reads PC2 at most 400 ‰ and PC3 at least 600 ‰. - That is the residency stopping the plane's fetches altogether gave (pr-1100 r7: 338 ‰ and 656 ‰). PC3 is the most this stage can show until NVMe APST lands, because the NVMe caps the package there. - The power figure against Linux's 1622 mW idle is judged once APST lands. -2. **Intel DMC and DC5/DC6** (Intel's signed payload, pinned by version and hash), and -3. **PSR on the eDP panel**. The display task measures these two together. DC5 with a pipe running needs PSR active (Vol 12, "Display C5 and C6"), so stage 2 alone saves nothing with the panel lit. PSR's first-frame exit latency goes to the owner in absolute numbers before PSR lands. -4. **`display-output`.** The compositor moves onto it, with virtio-gpu and the framebuffer as drivers. The panic surface lands here. -5. **Intel modesetting**: combo-PHY DDIs, DPLLs and CDCLK, DP link training with DSC, HDMI 2.0 with SCDC, EDID over AUX and GMBUS, several outputs. -6. **Intel Type-C DP alt mode**: FIA/IOM state, TCCOLD, the Dekel PHY, hotplug. -7. **Intel high refresh and VRR.** -8. **AMD**: the Display Core adapter, to the same protocol. -9. **Display audio.** +2. **Intel DMC and DC5/DC6.** Intel's package, pinned by version and hash (`NOTICE`), with its main program loaded through the iGPU's own registers. + - PG3-PG5 are powered down, since the GOP leaves PG1-PG4 requested. PG1 holds pipe A, transcoder A with its PSR, and DDI A-C. Then DC6 is allowed. + - With the pipe lit, the engine enters DC5 only while PSR is active (Vol 12, "Display C5 and C6"), so this stage alone saves little. + - What it can show alone is the program held in storage, PG3-PG5 off, and the scan-out and `hda` unharmed. `hda`'s display codec is in PG3. +3. **Present and damage.** The `display-output` piece PSR needs is the compositor telling the driver which frame changed, and where. Otherwise PSR1 sees a CPU write only through FBC host tracking of a graphics-aperture fence (Vol 12 "CPU Host Front Buffer Tracking", "on projects that support that"). Whether that reaches the GOP's surface is unverified. This is ordered before PSR, rather than an interim signal. Its shape goes to the orchestrator before it is built, because it crosses the compositor. +4. **PSR on the eDP panel**, measured together with stage 2. PSR's first-frame exit latency goes to the owner in absolute numbers before PSR lands. +5. **The rest of `display-output`.** virtio-gpu and the framebuffer become drivers. The panic surface lands here. +6. **Intel modesetting**: combo-PHY DDIs, DPLLs and CDCLK, DP link training with DSC, HDMI 2.0 with SCDC, EDID over AUX and GMBUS, several outputs. +7. **Intel Type-C DP alt mode**: FIA/IOM state, TCCOLD, the Dekel PHY, hotplug. +8. **Intel high refresh and VRR.** +9. **AMD**: the Display Core adapter, to the same protocol. +10. **Display audio.** ## Tests diff --git a/issues/nothing-shows-the-tiger-lake-display-engine-verifies-its-dmc-program.md b/issues/nothing-shows-the-tiger-lake-display-engine-verifies-its-dmc-program.md new file mode 100644 index 00000000000..5b5f8912e09 --- /dev/null +++ b/issues/nothing-shows-the-tiger-lake-display-engine-verifies-its-dmc-program.md @@ -0,0 +1,33 @@ +--- +status: open +kind: defect +opened: 2026-11-01 +--- + +# Nothing shows the Tiger Lake display engine verifies its DMC program + +The tree carries Intel's Tiger Lake DMC package unmodified, pinned by hash +(`system/tgl-display/firmware/`, `NOTICE`). `tgl-display` loads its main +program into the display engine's CSR program storage through the iGPU's own +registers. Root `CLAUDE.md`'s firmware rule lets it ship: it is device +firmware, and what the display engine masters is behind the iGPU's VT-d +domain. + +Nothing on record shows that the engine verifies the program before running +it: + +- Vol 12-12.21 "DMC Firmware Package" marks the CSS header's `keySize`, + `modulusSize` and `exponentSize` "Not used". +- The pinned file's CSS header carries 0 in all three. Its CSS `size` covers + the whole 19,760-byte file as header, package header and programs, which + leaves no room for a key or a signature. +- No Vol 12 or Vol 2c text states a check. + +So, as far as the record shows, the program is unsigned. The hash pin keeps +an altered file out of a signed image, but it decides nothing about what the +engine accepts from a driver that hands it one. + +**Owner**: the orchestrator. + +**Exit**: an Intel statement, cited in the driver, that the engine verifies +the program before running it, and this file deleted. diff --git a/licenses/Intel-i915-firmware-license.txt b/licenses/Intel-i915-firmware-license.txt new file mode 100644 index 00000000000..2492b8d83d8 --- /dev/null +++ b/licenses/Intel-i915-firmware-license.txt @@ -0,0 +1,39 @@ +Copyright (c) 2015, Intel Corporation. +All rights reserved. + +Redistribution. Redistribution and use in binary form, without +modification, are permitted provided that the following conditions are +met: + +* Redistributions must reproduce the above copyright notice and the + following disclaimer in the documentation and/or other materials + provided with the distribution. +* Neither the name of Intel Corporation nor the names of its suppliers + may be used to endorse or promote products derived from this software + without specific prior written permission. +* No reverse engineering, decompilation, or disassembly of this software + is permitted. + +Limited patent license. Intel Corporation grants a world-wide, +royalty-free, non-exclusive license under patents it now or hereafter +owns or controls to make, have made, use, import, offer to sell and +sell ("Utilize") this software, but solely to the extent that any +such patent is necessary to Utilize the software alone, or in +combination with an operating system licensed under an approved Open +Source license as listed by the Open Source Initiative at +http://opensource.org/licenses. The patent license shall not apply to +any other combinations which include this software. No hardware per +se is licensed hereunder. + +DISCLAIMER. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/system/tgl-display/driver/src/dc_tests.rs b/system/tgl-display/driver/src/dc_tests.rs new file mode 100644 index 00000000000..93eaae0b94a --- /dev/null +++ b/system/tgl-display/driver/src/dc_tests.rs @@ -0,0 +1,223 @@ +//! The DMC package and the DC6 sequence, against the pinned package and +//! packages built here from Vol 12's layouts. + +extern crate std; + +use std::vec; +use std::vec::Vec; + +use crate::dmc::{self, Malformed, Package, Stepping, TGL_PACKAGE}; +use crate::model::Engine; +use crate::{allow_display_c6, regs, scanout, Refusal}; + +const C0: Stepping = Stepping { letter: b'C', digit: b'0' }; +const D0: Stepping = Stepping { letter: b'D', digit: b'0' }; + +#[test] +fn the_pinned_package_is_version_2_12_with_one_main_program_inside_the_window() { + let package = Package::parse(TGL_PACKAGE).expect("the pinned package parses"); + assert_eq!(package.version, 0x0002_000C); + let main = package.main_program(C0).expect("a main program for C0"); + assert_eq!((main.version, main.start, main.words().count()), (0x0001_0002, 0x8_0000, 4460)); + // The first two words are what the T14's storage held before any driver + // ran (the scout, 2026-10-31). + assert_eq!(main.words().take(2).collect::>(), [0x0c00_4040, 0x0001_0002]); + assert_eq!(main.writes().count(), 9); + assert!(main.writes().all(|(at, _)| dmc::WINDOW.contains(&at))); + assert_eq!(package.main_program(D0).map(|p| p.start), Some(0x8_0000)); +} + +#[test] +fn a_9a49s_revision_names_its_display_stepping_from_vol_4() { + assert_eq!(Stepping::of_9a49(0x1), Some(C0)); + assert_eq!(Stepping::of_9a49(0x3), Some(D0)); + assert_eq!(Stepping::of_9a49(0x2), None); +} + +/// One program: where it is stored, its payload and its header's writes. +struct Built { + start: u32, + words: Vec, + writes: Vec<(u32, u32)>, +} + +fn program(start: u32, words: &[u32], writes: &[(u32, u32)]) -> Built { + Built { start, words: words.to_vec(), writes: writes.to_vec() } +} + +/// A package in Vol 12's layout: `entries` as (DMC ID, stepping, +/// substepping, the program's index or `None` for 0xFFFFFFFF). +fn package(entries: &[(u8, u8, u8, Option)], programs: &[Built]) -> Vec { + let mut bodies: Vec = vec![]; + let mut offsets = vec![]; + for p in programs { + offsets.push(bodies.len() as u32 / 4); + let mut header = vec![0u8; 256]; + header[0..4].copy_from_slice(&0x4040_3E3Eu32.to_le_bytes()); + header[4] = 64; + header[5] = 3; + header[0x0C..0x10].copy_from_slice(&(p.words.len() as u32).to_le_bytes()); + header[0x10..0x14].copy_from_slice(&0x0001_0000u32.to_le_bytes()); + header[0x14..0x18].copy_from_slice(&p.start.to_le_bytes()); + header[0x5C..0x60].copy_from_slice(&(p.writes.len() as u32).to_le_bytes()); + for (n, (at, data)) in p.writes.iter().enumerate().take(20) { + header[0x60 + 4 * n..0x64 + 4 * n].copy_from_slice(&at.to_le_bytes()); + header[0xB0 + 4 * n..0xB4 + 4 * n].copy_from_slice(&data.to_le_bytes()); + } + bodies.extend(header); + bodies.extend(p.words.iter().flat_map(|w| w.to_le_bytes())); + } + let mut package_header = vec![0u8; 400]; + package_header[0] = 100; + package_header[1] = 2; + package_header[12..16].copy_from_slice(&(entries.len() as u32).to_le_bytes()); + for (n, (dmc, stepping, substepping, which)) in entries.iter().enumerate() { + let at = 16 + 12 * n; + package_header[at + 1] = *dmc; + package_header[at + 2] = *stepping; + package_header[at + 3] = *substepping; + let offset = which.map_or(0xFFFF_FFFF, |i| offsets[i]); + package_header[at + 4..at + 8].copy_from_slice(&offset.to_le_bytes()); + } + let mut file = vec![0u8; 128]; + let total = (128 + package_header.len() + bodies.len()) as u32 / 4; + for (at, value) in [(0x00, 0x09), (0x04, 32), (0x08, 0x1_0000), (0x18, total), (0x58, 0x0002_000C)] { + file[at..at + 4].copy_from_slice(&u32::to_le_bytes(value)); + } + file.extend(package_header); + file.extend(bodies); + file +} + +#[test] +fn an_exception_listed_first_wins_over_the_default_and_none_means_none() { + let file = package( + &[(0, b'C', b'0', Some(0)), (0, b'D', b'*', None), (0, b'*', b'*', Some(1))], + &[program(0x8_0000, &[1], &[]), program(0x8_1000, &[2], &[])], + ); + let package = Package::parse(&file).expect("parses"); + assert_eq!(package.main_program(C0).map(|p| p.start), Some(0x8_0000)); + assert_eq!(package.main_program(Stepping { letter: b'C', digit: b'1' }).map(|p| p.start), Some(0x8_1000)); + assert!(package.main_program(D0).is_none()); +} + +#[test] +fn a_pipe_dmc_entry_is_never_the_main_program() { + let file = package(&[(1, b'*', b'*', Some(0))], &[program(0x9_0000, &[1], &[(0x9_2074, 1)])]); + let package = Package::parse(&file).expect("a pipe DMC past the main window parses"); + assert!(package.main_program(C0).is_none()); +} + +#[test] +fn a_main_program_outside_the_window_is_refused() { + let write = package(&[(0, b'*', b'*', Some(0))], &[program(0x8_0000, &[1], &[(0x4_5504, 2)])]); + assert_eq!(Package::parse(&write).err(), Some(Malformed::OutsideWindow { entry: 0, at: 0x4_5504 })); + let storage = package(&[(0, b'*', b'*', Some(0))], &[program(0x8_FFFC, &[1, 2], &[])]); + assert_eq!(Package::parse(&storage).err(), Some(Malformed::OutsideWindow { entry: 0, at: 0x8_FFFC })); + let unaligned = package(&[(0, b'*', b'*', Some(0))], &[program(0x8_0000, &[1], &[(0x8_F002, 2)])]); + assert_eq!(Package::parse(&unaligned).err(), Some(Malformed::OutsideWindow { entry: 0, at: 0x8_F002 })); +} + +#[test] +fn a_package_that_does_not_hold_together_is_refused() { + let good = package(&[(0, b'*', b'*', Some(0))], &[program(0x8_0000, &[1, 2], &[(0x8_F000, 1)])]); + assert!(Package::parse(&good).is_ok()); + let truncated = &good[..good.len() - 4]; + assert_eq!(Package::parse(truncated).err(), Some(Malformed::Css)); + let mut signature = good.clone(); + signature[128 + 400] ^= 1; + assert_eq!(Package::parse(&signature).err(), Some(Malformed::Program { entry: 0 })); + let mut writes = good.clone(); + writes[128 + 400 + 0x5C] = 21; + assert_eq!(Package::parse(&writes).err(), Some(Malformed::Program { entry: 0 })); + let mut payload = good.clone(); + payload[128 + 400 + 0x0C] = 3; + assert_eq!(Package::parse(&payload).err(), Some(Malformed::Program { entry: 0 })); + let mut header = good.clone(); + header[128 + 1] = 3; + assert_eq!(Package::parse(&header).err(), Some(Malformed::PackageHeader)); + let mut module = good; + module[0] = 0x0A; + assert_eq!(Package::parse(&module).err(), Some(Malformed::Css)); +} + +fn t14_program() -> dmc::Program<'static> { + Package::parse(TGL_PACKAGE).expect("parses").main_program(C0).expect("a main program") +} + +#[test] +fn the_t14_loads_the_program_powers_down_pg3_to_pg5_and_allows_dc6_last() { + let engine = Engine::t14(); + let one = scanout(&engine).expect("one plane"); + let main = t14_program(); + let allowed = allow_display_c6(&engine, &engine, &one, &main).expect("allowed"); + assert_eq!(allowed.before, [0x55, 0xff, 0]); + let written = engine.written(); + let words: Vec<_> = main.words().enumerate().map(|(n, w)| (0x8_0000 + 4 * n, w)).collect(); + let header: Vec<_> = main.writes().collect(); + assert_eq!(written[..words.len()], words[..], "the payload is stored first, in order"); + assert_eq!(written[words.len()..words.len() + header.len()], header[..], "then the header's writes"); + let rest = &written[words.len() + header.len()..]; + // PG4 then PG3 withdrawn from PWR_WELL_CTL2, PG5 never requested. + assert_eq!( + rest, + [ + (regs::PWR_WELL_CTL2, 0x7f), + (regs::PWR_WELL_CTL2, 0x1f), + (regs::DC_PREREQUISITE, 0b11), + (regs::DC_STATE_EN, 0b10), + ] + ); + assert_eq!(engine.get(regs::PWR_WELL_CTL2), 0x0f, "PG1 and PG2 stay requested and on"); +} + +#[test] +fn dc_states_already_allowed_are_disallowed_before_the_load() { + let engine = Engine::t14(); + engine.set(regs::DC_STATE_EN, 0b01); + let one = scanout(&engine).expect("one plane"); + allow_display_c6(&engine, &engine, &one, &t14_program()).expect("allowed"); + let written = engine.written(); + assert_eq!(written.first(), Some(&(regs::DC_STATE_EN, 0))); + assert_eq!(written.last(), Some(&(regs::DC_STATE_EN, 0b10))); +} + +fn refused(engine: &Engine, why: Refusal) -> Vec<(usize, u32)> { + let one = scanout(engine).expect("one plane"); + assert_eq!(allow_display_c6(engine, engine, &one, &t14_program()).map(|_| ()), Err(why)); + assert_ne!(engine.get(regs::DC_STATE_EN) & regs::DYNAMIC_DC_STATE, regs::UP_TO_DC6, "DC6 is not allowed"); + engine.written() +} + +#[test] +fn a_type_c_port_is_refused_untouched() { + let engine = Engine::t14(); + engine.set(regs::TRANS_DDI_FUNC_CTL_A, 1 << 31 | 0b0100 << regs::DDI_SELECT_SHIFT); + assert_eq!(refused(&engine, Refusal::Port(0b0100)), []); +} + +#[test] +fn vga_display_on_is_refused_untouched() { + let engine = Engine::t14(); + engine.set(regs::VGA_CONTROL, 0); + assert_eq!(refused(&engine, Refusal::Vga), []); +} + +#[test] +fn storage_that_does_not_hold_the_program_stops_before_any_well() { + let engine = Engine::t14(); + engine.storage_drops.set(true); + // Dropped writes leave the two words the storage already held, and zeros: + // the first payload word past those that is not zero is the one refused. + let (n, wrote) = t14_program().words().enumerate().skip(2).find(|(_, w)| *w != 0).expect("a non-zero word"); + let written = refused(&engine, Refusal::NotStored { at: 0x8_0000 + 4 * n, wrote, read: 0 }); + assert!(written.iter().all(|(at, _)| dmc::WINDOW.contains(at)), "nothing past the storage is written"); +} + +#[test] +fn a_well_held_by_another_requester_is_refused_by_name() { + let engine = Engine::t14(); + engine.held_wells.set(regs::power_well_state(3)); + let written = refused(&engine, Refusal::PowerWellHeld(3)); + assert!(!written.iter().any(|(at, _)| *at == regs::DC_STATE_EN)); +} diff --git a/system/tgl-display/driver/src/dmc.rs b/system/tgl-display/driver/src/dmc.rs new file mode 100644 index 00000000000..b33a18f8c26 --- /dev/null +++ b/system/tgl-display/driver/src/dmc.rs @@ -0,0 +1,232 @@ +//! The Display Micro-Controller's firmware package, and loading its main +//! program, from Vol 12 "DMC Firmware Package" and "Sequences for Display C5 +//! and C6". +//! +//! A package is a CSS header, a package header mapping steppings to programs, +//! and the programs, each a DMC header and its payload. Loading is MMIO in the +//! display engine's own registers: the payload is copied into the CSR program +//! storage at the header's start address, then the header's writes are made. +//! The package is pinned and parsed whole on the host, so [`Package::parse`] +//! failing is a bug in this code or a wrong file, not a machine's state. + +use toyos_device_memory::Registers; + +/// Intel's Tiger Lake DMC package, `i915/tgl_dmc_ver2_12.bin` from +/// linux-firmware, unmodified and pinned in `NOTICE`. +pub const TGL_PACKAGE: &[u8] = include_bytes!("../../firmware/tgl_dmc_ver2_12.bin"); + +/// The main DMC's MMIO window, 80000h-8FFFFh: Vol 12 bounds a program's +/// header writes to it, and the program storage holding its payload is held +/// to it too. +pub const WINDOW: core::ops::Range = 0x8_0000..0x9_0000; + +/// CSS header 1.0, package header 2 and DMC header 3: major version 2 of the +/// package, which Vol 12 says changes whenever any header layout does. +const CSS_MODULE_DMC: u32 = 0x09; +const CSS_HEADER_VERSION: u32 = 0x1_0000; +const CSS_VERSION_AT: usize = 0x58; +const PACKAGE_HEADER_VERSION: u8 = 0x02; +const PACKAGE_ENTRIES: usize = 32; +const PACKAGE_ENTRY_BYTES: usize = 12; +const NO_PROGRAM: u32 = 0xFFFF_FFFF; +const DMC_SIGNATURE: u32 = 0x4040_3E3E; +const DMC_HEADER_VERSION: u8 = 0x03; +/// `_DMCHeader` version 3 through `mmiodata[20]`. +const DMC_HEADER_BYTES: usize = 256; +const MMIO_COUNT_AT: usize = 0x5C; +const MMIO_WRITES: usize = 20; +/// The main DMC's ID in a package entry; pipe DMCs are 1-4. +const MAIN: u8 = 0; + +/// A display stepping as a package entry names it: a letter and a digit. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Stepping { + pub letter: u8, + pub digit: u8, +} + +impl Stepping { + /// The display stepping of a `8086:9a49` function at PCI revision + /// `revision`: Vol 4 "Steppings and Device IDs" gives RevID 1h as display + /// C0 and 3h as D0. + pub fn of_9a49(revision: u8) -> Option { + match revision { + 0x1 => Some(Self { letter: b'C', digit: b'0' }), + 0x3 => Some(Self { letter: b'D', digit: b'0' }), + _ => None, + } + } +} + +/// Why a package is not one this code reads. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Malformed { + /// The CSS header is short, not a DMC module's, not version 1.0, or does + /// not size the file. + Css, + /// The package header is not version 2, or its entries run off it. + PackageHeader, + /// An entry's program runs off the file, has no DMC header 3 signature, + /// or names more writes than the header has. + Program { entry: usize }, + /// A program's storage or one of its writes is outside [`WINDOW`], or not + /// dword aligned. + OutsideWindow { entry: usize, at: u32 }, +} + +/// A parsed package. +pub struct Package<'a> { + file: &'a [u8], + /// The package's version, major in 31:16 and minor in 15:0. + pub version: u32, + entries: [Option; PACKAGE_ENTRIES], + programs_at: usize, +} + +#[derive(Clone, Copy)] +struct Entry { + dmc: u8, + stepping: u8, + substepping: u8, + /// Dwords past the package header; [`NO_PROGRAM`] for none. + offset: u32, +} + +/// One DMC program, whole and inside [`WINDOW`]. +#[derive(Clone, Copy, Debug)] +pub struct Program<'a> { + /// The program's version, major in 31:16 and minor in 15:0. + pub version: u32, + /// Where its payload is stored. + pub start: usize, + payload: &'a [u8], + header: &'a [u8], + writes: usize, +} + +fn u32_at(bytes: &[u8], at: usize) -> Option { + Some(u32::from_le_bytes(bytes.get(at..at.checked_add(4)?)?.try_into().ok()?)) +} + +impl<'a> Package<'a> { + /// The package's headers, and every program its entries name, checked. + pub fn parse(file: &'a [u8]) -> Result { + let css = |at| u32_at(file, at).ok_or(Malformed::Css); + let css_bytes = css(0x04)? as usize * 4; + if css(0x00)? != CSS_MODULE_DMC || css(0x08)? != CSS_HEADER_VERSION || css(0x18)? as usize * 4 != file.len() { + return Err(Malformed::Css); + } + let version = css(CSS_VERSION_AT)?; + if css_bytes <= CSS_VERSION_AT { + return Err(Malformed::Css); + } + let header = file.get(css_bytes..).ok_or(Malformed::Css)?; + let (Some(&length), Some(&header_version)) = (header.first(), header.get(1)) else { + return Err(Malformed::PackageHeader); + }; + let header_bytes = usize::from(length) * 4; + let count = u32_at(header, 12).ok_or(Malformed::PackageHeader)? as usize; + if header_version != PACKAGE_HEADER_VERSION + || count > PACKAGE_ENTRIES + || header_bytes < 16 + PACKAGE_ENTRIES * PACKAGE_ENTRY_BYTES + || header.len() < header_bytes + { + return Err(Malformed::PackageHeader); + } + let mut entries = [None; PACKAGE_ENTRIES]; + for (n, slot) in entries.iter_mut().enumerate().take(count) { + let at = 16 + n * PACKAGE_ENTRY_BYTES; + let offset = u32_at(header, at + 4).ok_or(Malformed::PackageHeader)?; + *slot = Some(Entry { dmc: header[at + 1], stepping: header[at + 2], substepping: header[at + 3], offset }); + } + let package = Self { file, version, entries, programs_at: css_bytes + header_bytes }; + for (entry, found) in entries.iter().enumerate() { + if let Some(found) = found { + package.program(entry, found)?; + } + } + Ok(package) + } + + fn program(&self, entry: usize, found: &Entry) -> Result>, Malformed> { + if found.offset == NO_PROGRAM { + return Ok(None); + } + let malformed = Malformed::Program { entry }; + let start = (found.offset as usize).checked_mul(4).and_then(|at| at.checked_add(self.programs_at)).ok_or(malformed)?; + let header = self.file.get(start..).ok_or(malformed)?; + let word = |at| u32_at(header, at).ok_or(malformed); + let header_bytes = usize::from(*header.get(4).ok_or(malformed)?) * 4; + if word(0)? != DMC_SIGNATURE || header.get(5) != Some(&DMC_HEADER_VERSION) || header_bytes < DMC_HEADER_BYTES { + return Err(malformed); + } + let payload_bytes = (word(0x0C)? as usize).checked_mul(4).ok_or(malformed)?; + let payload = header.get(header_bytes..header_bytes.checked_add(payload_bytes).ok_or(malformed)?).ok_or(malformed)?; + let writes = word(MMIO_COUNT_AT)? as usize; + if writes > MMIO_WRITES { + return Err(malformed); + } + let storage = word(0x14)?; + let program = Program { version: word(0x10)?, start: storage as usize, payload, header: &header[..header_bytes], writes }; + // Only the main DMC's window is Vol 12's; a pipe DMC's is past it. + if found.dmc == MAIN { + let outside = |at: usize, bytes: usize| !at.is_multiple_of(4) || at < WINDOW.start || at + bytes > WINDOW.end; + if outside(program.start, payload_bytes) { + return Err(Malformed::OutsideWindow { entry, at: storage }); + } + if let Some((at, _)) = program.writes().find(|(at, _)| outside(*at, 4)) { + return Err(Malformed::OutsideWindow { entry, at: at as u32 }); + } + } + Ok(Some(program)) + } + + /// The main DMC's program for `stepping`: the first entry that names it, + /// exactly or by the `*` wildcard, since Vol 12 lists the exceptions + /// before the defaults. `None` where that entry says there is none, or no + /// entry names it. + pub fn main_program(&self, stepping: Stepping) -> Option> { + let names = |field: u8, value: u8| field == value || field == b'*'; + let (entry, found) = self + .entries + .iter() + .enumerate() + .filter_map(|(n, e)| Some((n, e.as_ref()?))) + .find(|(_, e)| e.dmc == MAIN && names(e.stepping, stepping.letter) && names(e.substepping, stepping.digit))?; + self.program(entry, found).expect("parse checked every entry's program") + } +} + +impl Program<'_> { + /// The payload's dwords, in storage order. + pub fn words(&self) -> impl Iterator + '_ { + self.payload.as_chunks::<4>().0.iter().map(|w| u32::from_le_bytes(*w)) + } + + /// The header's writes, address then data, in order. + pub fn writes(&self) -> impl Iterator + '_ { + let word = |at| u32_at(self.header, at).expect("parse checked the header's length"); + (0..self.writes).map(move |n| (word(MMIO_COUNT_AT + 4 + 4 * n) as usize, word(MMIO_COUNT_AT + 84 + 4 * n))) + } +} + +/// The program stored and its header's writes made (Vol 12 "Sequence to Allow +/// DC5 or DC6" steps 1b-1d). The storage is read back: the first word that +/// does not hold is returned, as its address, what was written and what reads. +pub fn load(regs: &R, program: &Program<'_>) -> Result<(), (usize, u32, u32)> { + for (n, word) in program.words().enumerate() { + regs.write32(program.start + 4 * n, word); + } + if let Some(differs) = program + .words() + .enumerate() + .map(|(n, word)| (program.start + 4 * n, word, regs.read32(program.start + 4 * n))) + .find(|(_, wrote, read)| wrote != read) + { + return Err(differs); + } + for (at, data) in program.writes() { + regs.write32(at, data); + } + Ok(()) +} diff --git a/system/tgl-display/driver/src/lib.rs b/system/tgl-display/driver/src/lib.rs index 3a3eaf11f0a..f1ff163aff6 100644 --- a/system/tgl-display/driver/src/lib.rs +++ b/system/tgl-display/driver/src/lib.rs @@ -3,13 +3,15 @@ //! IHD-OS-TGL-Vol 12-12.21 (Display Engine) and IHD-OS-TGL-Vol 2c-12.21 //! (registers). Each register and sequence is cited where it is used. //! -//! **What it does today is program watermarks on the firmware's surface** -//! ([`keep_memory_low_power`]): the mode, the plane and its buffer are the -//! GOP's and stay so. It reads the one plane the firmware scans out, pcode's -//! memory latencies and SAGV block time, computes watermark levels 0-7 by -//! Vol 12's algorithm ([`watermark`]) and programs them, so the display lets -//! memory reach the latencies the levels name. Anything it does not -//! understand of the firmware's state is refused by name and left as found. +//! **What it does today is program power on the firmware's surface**: the +//! mode, the plane and its buffer are the GOP's and stay so. +//! [`keep_memory_low_power`] reads the one plane the firmware scans out, +//! pcode's memory latencies and SAGV block time, computes watermark levels 0-7 +//! by Vol 12's algorithm ([`watermark`]) and programs them, so the display lets +//! memory reach the latencies the levels name. [`allow_display_c6`] loads the +//! DMC's program ([`dmc`]), powers down the wells the scan-out does not use and +//! lets the engine enter DC5 and DC6. Anything it does not understand of the +//! firmware's state is refused by name and left as found. #![no_std] #![forbid(unsafe_code)] @@ -18,9 +20,12 @@ use core::fmt; use toyos_device_memory::Registers; +pub mod dmc; pub mod regs; pub mod watermark; +#[cfg(test)] +mod dc_tests; #[cfg(test)] mod model; #[cfg(test)] @@ -44,6 +49,14 @@ const FRAME_POLL_NANOS: u64 = 50_000; /// Vol 12 "Retrieve Memory Latency Data" and "SAGV Block Time": "Timeout after /// 100 us and do not enable display planes." const MAILBOX_NANOS: u64 = 100_000; +/// Vol 12 "Power Well" disable sequence: "timeout after 20 us". +const POWER_WELL_NANOS: u64 = 20_000; +/// The wells the eDP scan-out on pipe A does not use, highest first, the +/// order Vol 12 disables them in: PG5 holds pipe D, PG4 pipe C, PG3 pipe B, +/// external displays, VGA and audio. Pipe A, transcoder A with its PSR and DDI +/// A-C are PG1's (Vol 12 "Power Well", "PG1 contains the functions for pipe A +/// display on eDP"). +const UNUSED_WELLS: [u32; 3] = [5, 4, 3]; /// Why the firmware's state was left as found. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -70,6 +83,20 @@ pub enum Refusal { /// The plane cannot tolerate SAGV, and masking QGV points takes the /// bandwidth calculation this driver does not yet make. SagvIntolerant, + /// The PCI revision is not one Vol 4 gives a display stepping for. + Revision(u8), + /// The DMC package has no main program for the display stepping. + NoProgram(dmc::Stepping), + /// Transcoder A drives a DDI outside PG1 (TRANS_DDI_FUNC_CTL_A's DDI + /// Select), so powering down PG3 would end the scan-out. + Port(u32), + /// VGA display is enabled, which needs PG3. + Vga, + /// The CSR program storage did not hold a word the load wrote. + NotStored { at: usize, wrote: u32, read: u32 }, + /// A power well stayed on after every request this driver can see was + /// withdrawn. + PowerWellHeld(u32), } impl fmt::Display for Refusal { @@ -88,6 +115,16 @@ impl fmt::Display for Refusal { } Self::LevelZero => write!(f, "watermark level 0 does not fit the plane's allocation"), Self::SagvIntolerant => write!(f, "the plane cannot tolerate SAGV's block time"), + Self::Revision(revision) => write!(f, "PCI revision {revision:#04x} is no display stepping Vol 4 names"), + Self::NoProgram(s) => { + write!(f, "the DMC package has no main program for display stepping {}{}", s.letter as char, s.digit as char) + } + Self::Port(ddi) => write!(f, "transcoder A drives DDI select {ddi:#x}, outside PG1"), + Self::Vga => write!(f, "VGA display is enabled"), + Self::NotStored { at, wrote, read } => { + write!(f, "the CSR storage at {at:#x} reads {read:#010x}, not the {wrote:#010x} written") + } + Self::PowerWellHeld(well) => write!(f, "power well {well} stayed on with its requests withdrawn"), } } } @@ -274,3 +311,64 @@ pub fn keep_memory_low_power(regs: &R, clock: &C) -> Res regs.write32(surface, regs.read32(surface)); Ok(Programmed { scanout, pixel_rate_khz, latencies, sagv_block_us, watermarks, before }) } + +/// What [`allow_display_c6`] found and did. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Allowed { + /// PWR_WELL_CTL1, PWR_WELL_CTL2 and DC_STATE_EN as the firmware left them. + pub before: [u32; 3], +} + +/// Vol 12 "Sequence to Allow DC5 or DC6", up to DC6, under the firmware's +/// scan-out on pipe A, which [`scanout`] found alone. +/// +/// DC states are disallowed first, since a switch between DC5 and DC6 goes +/// through "Disable" and a reload. Then `program` is loaded, the wells the +/// scan-out does not use are powered down and DC6 is allowed. With the pipe +/// lit, Vol 12 lets the engine enter DC5 only while PSR is active. +pub fn allow_display_c6( + regs: &R, + clock: &C, + _alone: &Scanout, + program: &dmc::Program<'_>, +) -> Result { + let before = [regs::PWR_WELL_CTL1, regs::PWR_WELL_CTL2, regs::DC_STATE_EN].map(|at| regs.read32(at)); + let ddi = (regs.read32(regs::TRANS_DDI_FUNC_CTL_A) >> regs::DDI_SELECT_SHIFT) & regs::DDI_SELECT; + if !(1..=3).contains(&ddi) { + return Err(Refusal::Port(ddi)); + } + if regs.read32(regs::VGA_CONTROL) & regs::VGA_DISPLAY_DISABLE == 0 { + return Err(Refusal::Vga); + } + let dc = regs.read32(regs::DC_STATE_EN); + if dc & regs::DYNAMIC_DC_STATE != 0 { + regs.write32(regs::DC_STATE_EN, dc & !regs::DYNAMIC_DC_STATE); + } + dmc::load(regs, program).map_err(|(at, wrote, read)| Refusal::NotStored { at, wrote, read })?; + for well in UNUSED_WELLS { + power_down(regs, clock, well)?; + } + regs.write32(regs::DC_PREREQUISITE, regs.read32(regs::DC_PREREQUISITE) | regs::DC_PREREQUISITE_BITS); + let dc = regs.read32(regs::DC_STATE_EN); + regs.write32(regs::DC_STATE_EN, dc & !regs::DYNAMIC_DC_STATE | regs::UP_TO_DC6); + Ok(Allowed { before }) +} + +/// Power well `well`'s requests withdrawn from both instances Vol 2c names, +/// and its state awaited (Vol 12 "Power Well" disable sequence). +fn power_down(regs: &R, clock: &C, well: u32) -> Result<(), Refusal> { + let request = regs::power_well_request(well); + for ctl in [regs::PWR_WELL_CTL1, regs::PWR_WELL_CTL2] { + let value = regs.read32(ctl); + if value & request != 0 { + regs.write32(ctl, value & !request); + } + } + let deadline = clock.nanos() + POWER_WELL_NANOS; + while regs.read32(regs::PWR_WELL_CTL2) & regs::power_well_state(well) != 0 { + if clock.nanos() > deadline { + return Err(Refusal::PowerWellHeld(well)); + } + } + Ok(()) +} diff --git a/system/tgl-display/driver/src/model.rs b/system/tgl-display/driver/src/model.rs index aba5f2ad9a2..a994ae3dc5b 100644 --- a/system/tgl-display/driver/src/model.rs +++ b/system/tgl-display/driver/src/model.rs @@ -1,6 +1,7 @@ //! A Tiger Lake display engine as far as this driver reaches it, from //! IHD-OS-TGL-Vol 2c-12.21 and Vol 12-12.21: a register file, a frame counter -//! advancing at the mode's rate, and pcode's GT driver mailbox. +//! advancing at the mode's rate, pcode's GT driver mailbox, power wells whose +//! state follows the ORed requests, and the DMC's program storage. //! //! The mailbox may stay busy for some reads after a command, as polling //! "for Run/Busy indication=0b" permits; every write is traced so a test can @@ -37,6 +38,10 @@ pub struct Engine { frame_nanos: Option, pcode: Pcode, busy: Cell, + /// Wells a requester the driver cannot see holds on, as state bits. + pub held_wells: Cell, + /// The program storage drops every write, as a window that misses it. + pub storage_drops: Cell, } impl Engine { @@ -55,13 +60,30 @@ impl Engine { r.insert(regs::plane_wm(0, 1, level), 0x0000_4007); } r.insert(regs::plane_buf_cfg(0, 1), 0x02a9_0000); + r.insert(regs::PWR_WELL_CTL1, 0x55); + r.insert(regs::PWR_WELL_CTL2, 0xff); + // VGA_CONTROL's default; the scout read neither it nor the DDI. DDI A + // in DP SST is assumed, and read on the bench. + r.insert(regs::VGA_CONTROL, regs::VGA_DISPLAY_DISABLE); + r.insert(regs::TRANS_DDI_FUNC_CTL_A, 1 << 31 | 1 << regs::DDI_SELECT_SHIFT | 0b010 << 24); + r.insert(crate::dmc::WINDOW.start, 0x0c00_4040); + r.insert(crate::dmc::WINDOW.start + 4, 0x0001_0002); let pcode = Pcode { latency_first: 0x3333_3300, latency_second: 0x706b_4633, sagv_block_us: 11, busy_reads: 3, error: 0 }; // 2080 × 1112 pixels at 138.78 MHz (LINK M/N × HBR's 270 MHz). Self::new(r, Some(16_666_667), pcode) } pub fn new(regs: BTreeMap, frame_nanos: Option, pcode: Pcode) -> Self { - Self { regs: RefCell::new(regs), writes: RefCell::new(Vec::new()), now: Cell::new(1_000_000), frame_nanos, pcode, busy: Cell::new(0) } + Self { + regs: RefCell::new(regs), + writes: RefCell::new(Vec::new()), + now: Cell::new(1_000_000), + frame_nanos, + pcode, + busy: Cell::new(0), + held_wells: Cell::new(0), + storage_drops: Cell::new(false), + } } pub fn set(&self, at: usize, value: u32) { @@ -90,6 +112,20 @@ impl Engine { } } +impl Engine { + /// A request written: every instance shows the state of the ORed requests, + /// and a well a hidden requester holds stays on. + fn request(&self, at: usize, value: u32) { + const REQUESTS: u32 = 0x2AA; + self.set(at, value & REQUESTS); + let requested = self.get(regs::PWR_WELL_CTL1) | self.get(regs::PWR_WELL_CTL2); + let state = (requested & REQUESTS) >> 1 | self.held_wells.get(); + for ctl in [regs::PWR_WELL_CTL1, regs::PWR_WELL_CTL2] { + self.set(ctl, self.get(ctl) & REQUESTS | state); + } + } +} + impl Registers for Engine { fn bytes(&self) -> usize { regs::SPAN @@ -130,6 +166,13 @@ impl Registers for Engine { self.command(value); return; } + if at == regs::PWR_WELL_CTL1 || at == regs::PWR_WELL_CTL2 { + self.request(at, value); + return; + } + if self.storage_drops.get() && crate::dmc::WINDOW.contains(&at) { + return; + } self.set(at, value); } } diff --git a/system/tgl-display/driver/src/regs.rs b/system/tgl-display/driver/src/regs.rs index 2cdd7dfb3a9..9ab90ce9172 100644 --- a/system/tgl-display/driver/src/regs.rs +++ b/system/tgl-display/driver/src/regs.rs @@ -73,6 +73,38 @@ pub fn plane_buf_cfg(pipe: usize, n: usize) -> usize { /// PLANE_WM: bit 31 Enable, 18:14 Lines, 10:0 Blocks. pub const WM_ENABLE: u32 = 1 << 31; +/// TRANS_DDI_FUNC_CTL_A, 60400h: 30:27 DDI Select, 1h-3h for DDI A-C. +pub const TRANS_DDI_FUNC_CTL_A: usize = 0x6_0400; +pub const DDI_SELECT_SHIFT: u32 = 27; +pub const DDI_SELECT: u32 = 0xF; + +/// VGA_CONTROL, 41000h: bit 31 VGA Display Disable. +pub const VGA_CONTROL: usize = 0x4_1000; +pub const VGA_DISPLAY_DISABLE: u32 = 1 << 31; + +/// PWR_WELL_CTL1, 45400h, generally the BIOS's, and PWR_WELL_CTL2, 45404h, +/// generally the driver's: requests from every instance are ORed. Power well +/// `n` (1-5) has its request at bit `2n - 1` and its state at bit `2n - 2`. +pub const PWR_WELL_CTL1: usize = 0x4_5400; +pub const PWR_WELL_CTL2: usize = 0x4_5404; +pub fn power_well_request(well: u32) -> u32 { + 1 << (2 * well - 1) +} +pub fn power_well_state(well: u32) -> u32 { + 1 << (2 * well - 2) +} + +/// DC_STATE_EN, 45504h: 1:0 Dynamic DC State Enable, 10b "Enable up to DC6". +/// Bits 9, 8 and 4 are hardware's and must not be changed. +pub const DC_STATE_EN: usize = 0x4_5504; +pub const DYNAMIC_DC_STATE: u32 = 0b11; +pub const UP_TO_DC6: u32 = 0b10; + +/// 45520h, which Vol 12 "Sequence to Allow DC5 or DC6" step 3 names by address +/// alone: bits 1:0 set before DC5 or DC6 is allowed, and never cleared. +pub const DC_PREREQUISITE: usize = 0x4_5520; +pub const DC_PREREQUISITE_BITS: u32 = 0b11; + /// The GT driver mailbox (Vol 2c GTDRIVER_MAILBOX_*; fields from Vol 12 /// "Mailbox Commands" and "Sequence Before Frequency Change"): Run/Busy bit /// 31, PARAM1 15:8, the command and its answered error code 7:0. diff --git a/system/tgl-display/firmware/tgl_dmc_ver2_12.bin b/system/tgl-display/firmware/tgl_dmc_ver2_12.bin new file mode 100644 index 0000000000000000000000000000000000000000..2995a5aee779aec7c5bef68eb1a459fe16ea96bc GIT binary patch literal 19760 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 k46)?P { say!( @@ -69,6 +76,25 @@ fn main() { .map(|level| format!("{:08x}", regs.read32(toyos_tgl_display::regs::plane_wm(0, done.scanout.plane, level)))) .collect(); say!("tgl-display: WM levels now {}", now.join(" ")); + let allowed = Stepping::of_9a49(revision) + .ok_or(Refusal::Revision(revision)) + .and_then(|stepping| package.main_program(stepping).ok_or(Refusal::NoProgram(stepping))) + .and_then(|program| { + toyos_tgl_display::allow_display_c6(®s, &Monotonic, &done.scanout, &program).map(|allowed| (program, allowed)) + }); + match allowed { + Ok((program, allowed)) => say!( + "tgl-display: DMC package {:08x}, main program {:08x} at {:#x} for revision {revision:#04x}; PWR_WELL_CTL1/2 and DC_STATE_EN were {:08x?}, now {:08x} {:08x} {:08x}", + package.version, + program.version, + program.start, + allowed.before, + regs.read32(toyos_tgl_display::regs::PWR_WELL_CTL1), + regs.read32(toyos_tgl_display::regs::PWR_WELL_CTL2), + regs.read32(toyos_tgl_display::regs::DC_STATE_EN), + ), + Err(why) => say!("tgl-display: DC5 and DC6 stay disallowed — {why}"), + } } Err(why) => say!("tgl-display: the firmware's display is left as found — {why}"), } From aa618bbb7e6904f23ce93531d736a0c23ba3d11e Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 1 Nov 2026 12:56:39 +0100 Subject: [PATCH 2/3] review r1 of #1132: DC6 only on DDI A, a fresh verdict past the deadline, and the audio buffer off after PG3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DC6 is allowed only with transcoder A on DDI A. Vol 12 "Sequence to Allow DC5 or DC6": "Ports A IO/PHY programming is preserved when DC6 is enabled. The programming for other combo PHY ports may not be." A panel on DDI B or C is refused by name. The DDI select read is logged with the allow, beside pipe A's frame counter, so a later reading shows the timing generator ran through it. - A power well's wait takes its verdict from a read made after the deadline. Before, a wait preempted past 20 µs while the well was still turning off was called held, and DC6 stayed disallowed under a false name. - AUDIO_PIN_BUF_CTL's enable is cleared after PG3 goes down, the step Vol 12 "Audio Bios Programming Sequence" pairs with it. The link-wakeup sequence also has the audio driver stop the iDisp link first. That needs `hda` and this driver to coordinate, so the track records it under display audio, with PG3's sharing. - The comments claimed every refusal leaves the firmware's state as found. What holds: a refusal before the load writes nothing, and none leaves DC6 allowed. - `allow_display_c6` drops its `Scanout` argument, which proved nothing. Its doc names the AUX and mode-set contract PSR will need. The DMC package doc no longer claims a version check it does not make, and a duplicate comment in main goes. Co-Authored-By: Claude Opus 5.5 --- ...ves-its-displays-from-a-userland-driver.md | 5 +- system/tgl-display/driver/src/dc_tests.rs | 33 ++++++++--- system/tgl-display/driver/src/dmc.rs | 4 +- system/tgl-display/driver/src/lib.rs | 56 ++++++++++++------- system/tgl-display/driver/src/model.rs | 22 +++++++- system/tgl-display/driver/src/regs.rs | 6 ++ system/tgl-display/src/main.rs | 9 +-- 7 files changed, 100 insertions(+), 35 deletions(-) diff --git a/issues/each-gpu-drives-its-displays-from-a-userland-driver.md b/issues/each-gpu-drives-its-displays-from-a-userland-driver.md index bf6d1a8955e..5ad6d47f89a 100644 --- a/issues/each-gpu-drives-its-displays-from-a-userland-driver.md +++ b/issues/each-gpu-drives-its-displays-from-a-userland-driver.md @@ -80,7 +80,8 @@ The kernel keeps only generic mechanisms: - That is the residency stopping the plane's fetches altogether gave (pr-1100 r7: 338 ‰ and 656 ‰). PC3 is the most this stage can show until NVMe APST lands, because the NVMe caps the package there. - The power figure against Linux's 1622 mW idle is judged once APST lands. 2. **Intel DMC and DC5/DC6.** Intel's package, pinned by version and hash (`NOTICE`), with its main program loaded through the iGPU's own registers. - - PG3-PG5 are powered down, since the GOP leaves PG1-PG4 requested. PG1 holds pipe A, transcoder A with its PSR, and DDI A-C. Then DC6 is allowed. + - PG3-PG5 are powered down, since the GOP leaves PG1-PG4 requested. PG1 holds pipe A, transcoder A with its PSR, and DDI A-C. AUDIO_PIN_BUF_CTL's enable is cleared after PG3 (Vol 12 "Audio Bios Programming Sequence"). Then DC6 is allowed. + - DC6 is allowed only with the panel on DDI A, since Vol 12 says only port A's PHY programming survives it. - With the pipe lit, the engine enters DC5 only while PSR is active (Vol 12, "Display C5 and C6"), so this stage alone saves little. - What it can show alone is the program held in storage, PG3-PG5 off, and the scan-out and `hda` unharmed. `hda`'s display codec is in PG3. 3. **Present and damage.** The `display-output` piece PSR needs is the compositor telling the driver which frame changed, and where. Otherwise PSR1 sees a CPU write only through FBC host tracking of a graphics-aperture fence (Vol 12 "CPU Host Front Buffer Tracking", "on projects that support that"). Whether that reaches the GOP's surface is unverified. This is ordered before PSR, rather than an interim signal. Its shape goes to the orchestrator before it is built, because it crosses the compositor. @@ -91,6 +92,8 @@ The kernel keeps only generic mechanisms: 8. **Intel high refresh and VRR.** 9. **AMD**: the Display Core adapter, to the same protocol. 10. **Display audio.** + - PG3 holds `hda`'s display codec, and since stage 2 `tgl-display` powers it down on its own. Display audio needs `hda` to ask the display driver for PG3. + - Vol 12 "Audio Programming Sequence for Link Wakeup" orders the iDisp link: the audio driver stops it (LCTL1.SPA = 0, then CPA = 0) before AUDIO_PIN_BUF_CTL is cleared. Today only the buffer is cleared, after PG3, so the two drivers must coordinate here. ## Tests diff --git a/system/tgl-display/driver/src/dc_tests.rs b/system/tgl-display/driver/src/dc_tests.rs index 93eaae0b94a..a0c1d9743e4 100644 --- a/system/tgl-display/driver/src/dc_tests.rs +++ b/system/tgl-display/driver/src/dc_tests.rs @@ -8,7 +8,7 @@ use std::vec::Vec; use crate::dmc::{self, Malformed, Package, Stepping, TGL_PACKAGE}; use crate::model::Engine; -use crate::{allow_display_c6, regs, scanout, Refusal}; +use crate::{allow_display_c6, regs, Refusal}; const C0: Stepping = Stepping { letter: b'C', digit: b'0' }; const D0: Stepping = Stepping { letter: b'D', digit: b'0' }; @@ -148,10 +148,9 @@ fn t14_program() -> dmc::Program<'static> { #[test] fn the_t14_loads_the_program_powers_down_pg3_to_pg5_and_allows_dc6_last() { let engine = Engine::t14(); - let one = scanout(&engine).expect("one plane"); let main = t14_program(); - let allowed = allow_display_c6(&engine, &engine, &one, &main).expect("allowed"); - assert_eq!(allowed.before, [0x55, 0xff, 0]); + let allowed = allow_display_c6(&engine, &engine, &main).expect("allowed"); + assert_eq!((allowed.before, allowed.ddi), ([0x55, 0xff, 0], 1)); let written = engine.written(); let words: Vec<_> = main.words().enumerate().map(|(n, w)| (0x8_0000 + 4 * n, w)).collect(); let header: Vec<_> = main.writes().collect(); @@ -164,6 +163,7 @@ fn the_t14_loads_the_program_powers_down_pg3_to_pg5_and_allows_dc6_last() { [ (regs::PWR_WELL_CTL2, 0x7f), (regs::PWR_WELL_CTL2, 0x1f), + (regs::AUDIO_PIN_BUF_CTL, 0), (regs::DC_PREREQUISITE, 0b11), (regs::DC_STATE_EN, 0b10), ] @@ -175,16 +175,14 @@ fn the_t14_loads_the_program_powers_down_pg3_to_pg5_and_allows_dc6_last() { fn dc_states_already_allowed_are_disallowed_before_the_load() { let engine = Engine::t14(); engine.set(regs::DC_STATE_EN, 0b01); - let one = scanout(&engine).expect("one plane"); - allow_display_c6(&engine, &engine, &one, &t14_program()).expect("allowed"); + allow_display_c6(&engine, &engine, &t14_program()).expect("allowed"); let written = engine.written(); assert_eq!(written.first(), Some(&(regs::DC_STATE_EN, 0))); assert_eq!(written.last(), Some(&(regs::DC_STATE_EN, 0b10))); } fn refused(engine: &Engine, why: Refusal) -> Vec<(usize, u32)> { - let one = scanout(engine).expect("one plane"); - assert_eq!(allow_display_c6(engine, engine, &one, &t14_program()).map(|_| ()), Err(why)); + assert_eq!(allow_display_c6(engine, engine, &t14_program()).map(|_| ()), Err(why)); assert_ne!(engine.get(regs::DC_STATE_EN) & regs::DYNAMIC_DC_STATE, regs::UP_TO_DC6, "DC6 is not allowed"); engine.written() } @@ -196,6 +194,25 @@ fn a_type_c_port_is_refused_untouched() { assert_eq!(refused(&engine, Refusal::Port(0b0100)), []); } +/// DDI B is in PG1 too, but DC6 may lose a combo PHY's programming other +/// than port A's. +#[test] +fn a_panel_on_ddi_b_is_refused_untouched() { + let engine = Engine::t14(); + engine.set(regs::TRANS_DDI_FUNC_CTL_A, 1 << 31 | 0b0010 << regs::DDI_SELECT_SHIFT); + assert_eq!(refused(&engine, Refusal::Port(0b0010)), []); +} + +/// A wait preempted past its deadline while the well was still turning off +/// reads it once more, off, rather than calling it held. +#[test] +fn a_wait_preempted_past_its_deadline_reads_the_well_again() { + let engine = Engine::t14(); + engine.preempted.set(true); + allow_display_c6(&engine, &engine, &t14_program()).expect("allowed"); + assert_eq!(engine.get(regs::DC_STATE_EN), 0b10); +} + #[test] fn vga_display_on_is_refused_untouched() { let engine = Engine::t14(); diff --git a/system/tgl-display/driver/src/dmc.rs b/system/tgl-display/driver/src/dmc.rs index b33a18f8c26..664b08207f1 100644 --- a/system/tgl-display/driver/src/dmc.rs +++ b/system/tgl-display/driver/src/dmc.rs @@ -20,8 +20,8 @@ pub const TGL_PACKAGE: &[u8] = include_bytes!("../../firmware/tgl_dmc_ver2_12.bi /// to it too. pub const WINDOW: core::ops::Range = 0x8_0000..0x9_0000; -/// CSS header 1.0, package header 2 and DMC header 3: major version 2 of the -/// package, which Vol 12 says changes whenever any header layout does. +/// CSS header 1.0, package header 2 and DMC header 3, each checked at its +/// header. const CSS_MODULE_DMC: u32 = 0x09; const CSS_HEADER_VERSION: u32 = 0x1_0000; const CSS_VERSION_AT: usize = 0x58; diff --git a/system/tgl-display/driver/src/lib.rs b/system/tgl-display/driver/src/lib.rs index f1ff163aff6..314941fc1a9 100644 --- a/system/tgl-display/driver/src/lib.rs +++ b/system/tgl-display/driver/src/lib.rs @@ -11,7 +11,8 @@ //! memory reach the latencies the levels name. [`allow_display_c6`] loads the //! DMC's program ([`dmc`]), powers down the wells the scan-out does not use and //! lets the engine enter DC5 and DC6. Anything it does not understand of the -//! firmware's state is refused by name and left as found. +//! firmware's state is refused by name. A refusal before a write leaves the +//! state as found, and no refusal leaves DC6 allowed. #![no_std] #![forbid(unsafe_code)] @@ -58,7 +59,8 @@ const POWER_WELL_NANOS: u64 = 20_000; /// display on eDP"). const UNUSED_WELLS: [u32; 3] = [5, 4, 3]; -/// Why the firmware's state was left as found. +/// Why this driver stopped short; [`keep_memory_low_power`] and +/// [`allow_display_c6`] say how far each got. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Refusal { /// The firmware's state is not one pipe A plane scanning out alone. @@ -87,8 +89,10 @@ pub enum Refusal { Revision(u8), /// The DMC package has no main program for the display stepping. NoProgram(dmc::Stepping), - /// Transcoder A drives a DDI outside PG1 (TRANS_DDI_FUNC_CTL_A's DDI - /// Select), so powering down PG3 would end the scan-out. + /// Transcoder A drives another DDI than A (TRANS_DDI_FUNC_CTL_A's DDI + /// Select, 1h for A). Vol 12 "Sequence to Allow DC5 or DC6": "Ports A + /// IO/PHY programming is preserved when DC6 is enabled. The programming + /// for other combo PHY ports may not be." Port(u32), /// VGA display is enabled, which needs PG3. Vga, @@ -119,7 +123,7 @@ impl fmt::Display for Refusal { Self::NoProgram(s) => { write!(f, "the DMC package has no main program for display stepping {}{}", s.letter as char, s.digit as char) } - Self::Port(ddi) => write!(f, "transcoder A drives DDI select {ddi:#x}, outside PG1"), + Self::Port(ddi) => write!(f, "transcoder A drives DDI select {ddi:#x}, not DDI A, whose PHY alone DC6 preserves"), Self::Vga => write!(f, "VGA display is enabled"), Self::NotStored { at, wrote, read } => { write!(f, "the CSR storage at {at:#x} reads {read:#010x}, not the {wrote:#010x} written") @@ -317,24 +321,26 @@ pub fn keep_memory_low_power(regs: &R, clock: &C) -> Res pub struct Allowed { /// PWR_WELL_CTL1, PWR_WELL_CTL2 and DC_STATE_EN as the firmware left them. pub before: [u32; 3], + /// TRANS_DDI_FUNC_CTL_A's DDI Select. + pub ddi: u32, } /// Vol 12 "Sequence to Allow DC5 or DC6", up to DC6, under the firmware's -/// scan-out on pipe A, which [`scanout`] found alone. +/// eDP scan-out on pipe A and DDI A. /// /// DC states are disallowed first, since a switch between DC5 and DC6 goes /// through "Disable" and a reload. Then `program` is loaded, the wells the /// scan-out does not use are powered down and DC6 is allowed. With the pipe -/// lit, Vol 12 lets the engine enter DC5 only while PSR is active. -pub fn allow_display_c6( - regs: &R, - clock: &C, - _alone: &Scanout, - program: &dmc::Program<'_>, -) -> Result { +/// lit, Vol 12 lets the engine enter DC5 only while PSR is active. A DDI AUX +/// transaction or a mode set must disallow DC5 and DC6 around it (the same +/// section). +/// +/// The refusals before the load write nothing. One after it leaves DC states +/// disallowed, and whatever wells were already withdrawn stay so. +pub fn allow_display_c6(regs: &R, clock: &C, program: &dmc::Program<'_>) -> Result { let before = [regs::PWR_WELL_CTL1, regs::PWR_WELL_CTL2, regs::DC_STATE_EN].map(|at| regs.read32(at)); let ddi = (regs.read32(regs::TRANS_DDI_FUNC_CTL_A) >> regs::DDI_SELECT_SHIFT) & regs::DDI_SELECT; - if !(1..=3).contains(&ddi) { + if ddi != regs::DDI_A { return Err(Refusal::Port(ddi)); } if regs.read32(regs::VGA_CONTROL) & regs::VGA_DISPLAY_DISABLE == 0 { @@ -348,14 +354,23 @@ pub fn allow_display_c6( for well in UNUSED_WELLS { power_down(regs, clock, well)?; } + // Vol 12 "Audio Bios Programming Sequence": "GFX driver disables + // AUDIO_PIN_BUF_CTL bit 31 after turning off the power well for audio + // (PG3)". + let pins = regs.read32(regs::AUDIO_PIN_BUF_CTL); + if pins & regs::AUDIO_BUFFER_ENABLE != 0 { + regs.write32(regs::AUDIO_PIN_BUF_CTL, pins & !regs::AUDIO_BUFFER_ENABLE); + } regs.write32(regs::DC_PREREQUISITE, regs.read32(regs::DC_PREREQUISITE) | regs::DC_PREREQUISITE_BITS); let dc = regs.read32(regs::DC_STATE_EN); regs.write32(regs::DC_STATE_EN, dc & !regs::DYNAMIC_DC_STATE | regs::UP_TO_DC6); - Ok(Allowed { before }) + Ok(Allowed { before, ddi }) } /// Power well `well`'s requests withdrawn from both instances Vol 2c names, -/// and its state awaited (Vol 12 "Power Well" disable sequence). +/// and its state awaited (Vol 12 "Power Well" disable sequence). The verdict +/// past the deadline is a read made after it, so a wait preempted past the +/// deadline is not called a held well. fn power_down(regs: &R, clock: &C, well: u32) -> Result<(), Refusal> { let request = regs::power_well_request(well); for ctl in [regs::PWR_WELL_CTL1, regs::PWR_WELL_CTL2] { @@ -365,10 +380,13 @@ fn power_down(regs: &R, clock: &C, well: u32) -> Result< } } let deadline = clock.nanos() + POWER_WELL_NANOS; - while regs.read32(regs::PWR_WELL_CTL2) & regs::power_well_state(well) != 0 { - if clock.nanos() > deadline { + loop { + let late = clock.nanos() > deadline; + if regs.read32(regs::PWR_WELL_CTL2) & regs::power_well_state(well) == 0 { + return Ok(()); + } + if late { return Err(Refusal::PowerWellHeld(well)); } } - Ok(()) } diff --git a/system/tgl-display/driver/src/model.rs b/system/tgl-display/driver/src/model.rs index a994ae3dc5b..0bf4ccd6ce9 100644 --- a/system/tgl-display/driver/src/model.rs +++ b/system/tgl-display/driver/src/model.rs @@ -42,6 +42,12 @@ pub struct Engine { pub held_wells: Cell, /// The program storage drops every write, as a window that misses it. pub storage_drops: Cell, + /// The driver is preempted once after its first read of a well's state + /// following a withdrawal: that read still shows the well on, and the + /// clock reading after it is 30 µs later. The well is off by then. + pub preempted: Cell, + stale: Cell>, + jump: Cell, } impl Engine { @@ -66,6 +72,8 @@ impl Engine { // in DP SST is assumed, and read on the bench. r.insert(regs::VGA_CONTROL, regs::VGA_DISPLAY_DISABLE); r.insert(regs::TRANS_DDI_FUNC_CTL_A, 1 << 31 | 1 << regs::DDI_SELECT_SHIFT | 0b010 << 24); + // Enabled, which the driver clears after PG3; the scout did not read it. + r.insert(regs::AUDIO_PIN_BUF_CTL, regs::AUDIO_BUFFER_ENABLE); r.insert(crate::dmc::WINDOW.start, 0x0c00_4040); r.insert(crate::dmc::WINDOW.start + 4, 0x0001_0002); let pcode = Pcode { latency_first: 0x3333_3300, latency_second: 0x706b_4633, sagv_block_us: 11, busy_reads: 3, error: 0 }; @@ -83,6 +91,9 @@ impl Engine { busy: Cell::new(0), held_wells: Cell::new(0), storage_drops: Cell::new(false), + preempted: Cell::new(false), + stale: Cell::new(None), + jump: Cell::new(0), } } @@ -117,6 +128,9 @@ impl Engine { /// and a well a hidden requester holds stays on. fn request(&self, at: usize, value: u32) { const REQUESTS: u32 = 0x2AA; + if self.preempted.replace(false) { + self.stale.set(Some(self.get(regs::PWR_WELL_CTL2))); + } self.set(at, value & REQUESTS); let requested = self.get(regs::PWR_WELL_CTL1) | self.get(regs::PWR_WELL_CTL2); let state = (requested & REQUESTS) >> 1 | self.held_wells.get(); @@ -138,6 +152,12 @@ impl Registers for Engine { } fn read32(&self, at: usize) -> u32 { assert!(at.is_multiple_of(4) && at < regs::SPAN, "an access at {at:#x}"); + if at == regs::PWR_WELL_CTL2 { + if let Some(stale) = self.stale.take() { + self.jump.set(30_000); + return stale; + } + } if at == regs::PIPE_FRMCNT_A { return self.frame_nanos.map_or(0x276, |period| (self.now.get() / period) as u32); } @@ -180,7 +200,7 @@ impl Registers for Engine { /// Every reading of the clock is a microsecond later, so a busy wait ends. impl Clock for Engine { fn nanos(&self) -> u64 { - self.now.set(self.now.get() + 1_000); + self.now.set(self.now.get() + 1_000 + self.jump.take()); self.now.get() } fn pause(&self, nanos: u64) { diff --git a/system/tgl-display/driver/src/regs.rs b/system/tgl-display/driver/src/regs.rs index 9ab90ce9172..a0eb91050ea 100644 --- a/system/tgl-display/driver/src/regs.rs +++ b/system/tgl-display/driver/src/regs.rs @@ -77,6 +77,12 @@ pub const WM_ENABLE: u32 = 1 << 31; pub const TRANS_DDI_FUNC_CTL_A: usize = 0x6_0400; pub const DDI_SELECT_SHIFT: u32 = 27; pub const DDI_SELECT: u32 = 0xF; +pub const DDI_A: u32 = 1; + +/// AUDIO_PIN_BUF_CTL, 48414h: bit 31 Enable, the display audio pins' I/O +/// buffers. +pub const AUDIO_PIN_BUF_CTL: usize = 0x4_8414; +pub const AUDIO_BUFFER_ENABLE: u32 = 1 << 31; /// VGA_CONTROL, 41000h: bit 31 VGA Display Disable. pub const VGA_CONTROL: usize = 0x4_1000; diff --git a/system/tgl-display/src/main.rs b/system/tgl-display/src/main.rs index aba33561949..b204f74c012 100644 --- a/system/tgl-display/src/main.rs +++ b/system/tgl-display/src/main.rs @@ -52,8 +52,6 @@ fn main() { toyos_pci_claim::start(&dev).unwrap_or_else(|why| panic!("tgl-display: {why}")); say!("tgl-display: PCI {:02x}:{:02x}.{} registers in BAR {BAR}, {span:#x} of its {bytes:#x} bytes", info.bus, info.dev, info.func); - // The pinned package parses on the host (`toyos-tgl-display`'s tests), so - // a failure here is this build's, not the machine's. let package = Package::parse(TGL_PACKAGE).unwrap_or_else(|why| panic!("tgl-display: the DMC package is malformed: {why:?}")); let revision = dev.config_read(0x08, RegWidth::U32).unwrap_or_else(|why| panic!("tgl-display: config space: {why:?}")) as u8; @@ -80,18 +78,21 @@ fn main() { .ok_or(Refusal::Revision(revision)) .and_then(|stepping| package.main_program(stepping).ok_or(Refusal::NoProgram(stepping))) .and_then(|program| { - toyos_tgl_display::allow_display_c6(®s, &Monotonic, &done.scanout, &program).map(|allowed| (program, allowed)) + toyos_tgl_display::allow_display_c6(®s, &Monotonic, &program).map(|allowed| (program, allowed)) }); match allowed { Ok((program, allowed)) => say!( - "tgl-display: DMC package {:08x}, main program {:08x} at {:#x} for revision {revision:#04x}; PWR_WELL_CTL1/2 and DC_STATE_EN were {:08x?}, now {:08x} {:08x} {:08x}", + "tgl-display: DMC package {:08x}, main program {:08x} at {:#x} for revision {revision:#04x}, DDI select {:#x}; PWR_WELL_CTL1/2 and DC_STATE_EN were {:08x?}, now {:08x} {:08x} {:08x}, AUDIO_PIN_BUF_CTL {:08x}; pipe A frame {}", package.version, program.version, program.start, + allowed.ddi, allowed.before, regs.read32(toyos_tgl_display::regs::PWR_WELL_CTL1), regs.read32(toyos_tgl_display::regs::PWR_WELL_CTL2), regs.read32(toyos_tgl_display::regs::DC_STATE_EN), + regs.read32(toyos_tgl_display::regs::AUDIO_PIN_BUF_CTL), + regs.read32(toyos_tgl_display::regs::PIPE_FRMCNT_A), ), Err(why) => say!("tgl-display: DC5 and DC6 stay disallowed — {why}"), } From 166ea4dcaddfa9efa89b874a1c38db509f96198a Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 1 Nov 2026 16:23:14 +0100 Subject: [PATCH 3/3] review r2 of #1132: no refusal allows DC6, rather than leaving it allowed A refusal before any write (a panel off DDI A, VGA on) leaves DC6 allowed where the firmware allowed it, so "no refusal leaves DC6 allowed" contradicted the sentence before it. What holds is that no refusal allows DC6. Co-Authored-By: Claude Opus 5.5 --- system/tgl-display/driver/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/tgl-display/driver/src/lib.rs b/system/tgl-display/driver/src/lib.rs index 314941fc1a9..f8233b2daef 100644 --- a/system/tgl-display/driver/src/lib.rs +++ b/system/tgl-display/driver/src/lib.rs @@ -12,7 +12,7 @@ //! DMC's program ([`dmc`]), powers down the wells the scan-out does not use and //! lets the engine enter DC5 and DC6. Anything it does not understand of the //! firmware's state is refused by name. A refusal before a write leaves the -//! state as found, and no refusal leaves DC6 allowed. +//! state as found, and no refusal allows DC6. #![no_std] #![forbid(unsafe_code)]