diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..3ba13e0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/report.yml b/.github/ISSUE_TEMPLATE/report.yml new file mode 100644 index 00000000..22de804c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/report.yml @@ -0,0 +1,117 @@ +name: Bug report or feature request +description: Report a WhateverXe issue or request a change. +body: + - type: checkboxes + id: pre-submission-check + attributes: + label: Pre-submission check + options: + - label: I have successfully installed and booted this version of macOS with neither WhateverXe nor WhateverXe Graphics installed. + required: true + - type: input + id: cpu + attributes: + label: CPU model + placeholder: e.g. Intel Core i7-1165G7 + validations: + required: true + - type: input + id: macos-version + attributes: + label: macOS version + description: Include the build number if available. + placeholder: e.g. macOS 26.7.2 (25G332) + validations: + required: true + - type: input + id: wex-version + attributes: + label: WhateverXe version + description: For a custom build, provide its commit hash. + placeholder: e.g. 0.7.0 + validations: + required: true + - type: input + id: graphics-version + attributes: + label: WhateverXe Graphics version + description: For a custom build, provide its version and source commit hash. + placeholder: e.g. 0.4.0 + validations: + required: true + - type: input + id: hardware-model + attributes: + label: Computer model or motherboard + placeholder: e.g. Lenovo ThinkPad L13 Gen 2 + validations: + required: false + - type: input + id: igpu-device-id + attributes: + label: iGPU device ID + placeholder: e.g. 0x9A49 + validations: + required: false + - type: dropdown + id: installation-method + attributes: + label: Graphics installation method + options: + - Standard pkg + - AutoPkg + validations: + required: false + - type: textarea + id: display-configuration + attributes: + label: Display configuration + description: For display issues, include the displays, connections and any adapters or docks involved. + placeholder: | + Internal display: 1920x1080 at 60 Hz + External display: 1920x1080 at 60 Hz, USB-C to HDMI adapter + Connected displays when the issue occurs: internal + external + validations: + required: false + - type: textarea + id: request-or-symptoms + attributes: + label: Request or symptoms + placeholder: | + Expected behavior: + Observed behavior or requested change: + validations: + required: true + - type: textarea + id: reproduction-steps + attributes: + label: Steps to reproduce + description: Leave blank if not applicable to your request. + placeholder: | + 1. Boot macOS with WEX enabled. + 2. Connect an external display. + 3. The external display remains black. + Frequency: every time / intermittent + validations: + required: false + - type: textarea + id: regression + attributes: + label: Regression / last working version + description: If this previously worked, give the last working versions and what changed. + placeholder: | + Last working: WEX version / Graphics version / macOS version + First failing: WEX version / Graphics version / macOS version + Other changes: hardware, display connection or configuration changes + validations: + required: false + - type: textarea + id: logs-and-attachments + attributes: + label: Logs and attachments + description: Remove personal information before uploading. + placeholder: | + Paste relevant log excerpts or attach log files, crash reports, screenshots or videos. + Include when each log or capture was taken relative to the issue. + validations: + required: false diff --git a/AGENTS.md b/AGENTS.md index 0e0187f6..cf5b0668 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,8 +14,14 @@ Keep README.md and README_ja.md synchronized in the same change, including requirements, compatibility, links and examples. Do not add personal names or private identifying information to user documentation without explicit consent. -Document required and optional boot arguments in both READMEs. Do not add -migration instructions for unreleased internal revisions. +Keep setup essentials prominent in both READMEs. Do not append hardware internals, +diagnostic boot arguments or per-change validation details. MIPI DSI needs only +its user-facing scope and limitations in README. Consolidate common regression +coverage and describe only configuration-specific differences in the table. +Preserve concise GPU-family lists grouped as hardware-tested, expected but untested, +further implementation required, and no support planned. Reference a distant table +by its section link rather than saying "below". +Do not add migration instructions for unreleased internal revisions. Public docs describe current settings and requirements. Keep investigation notes, implementation diaries, internal diagnostic layouts, experimental procedures and live machine evidence in the maintainer's private diff --git a/AppleIntelTGLCompat/AppleIntelTGLCompat.cpp b/AppleIntelTGLCompat/AppleIntelTGLCompat.cpp index 0c1d5afa..56a2e258 100644 --- a/AppleIntelTGLCompat/AppleIntelTGLCompat.cpp +++ b/AppleIntelTGLCompat/AppleIntelTGLCompat.cpp @@ -52,7 +52,7 @@ static uint32_t read32(void *access, unsigned long offset) { // it does not read plugin boot arguments or depend on framebuffer globals. // The provider pins the immutable binding until reboot, like the donor owner. extern "C" uint32_t xelpProbeCapabilities(uint32_t schema,uint32_t configABI) { - return schema==XELP::ProbeCapabilityABI && configABI==XELP::ProbeABI ? XELP::ProbeDisplay13ComboDP|XELP::ProbeDisplay13FixedTC|XELP::ProbeDisplay13FixedDP|XELP::ProbeDisplay13Panel : 0; + return schema==XELP::ProbeCapabilityABI && configABI==XELP::ProbeABI ? XELP::ProbeDisplay13ComboDP|XELP::ProbeDisplay13FixedTC|XELP::ProbeDisplay13FixedDP|XELP::ProbeDisplay13Panel|XELP::ProbeDisplay13DPAlt : 0; } extern "C" bool xelpConfigureDisplay(void *controller,const XELP::ProbeConfig *config) { if(!controller || !config || !config->valid())return false; diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e3d37d..10544d89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.7.0 + +Use the latest [WhateverXe Graphics](https://github.com/b00t0x/WhateverXe-Graphics/releases) package with this update. + +### Added + +- Add USB-C DisplayPort Alt Mode output on Alder Lake-class GPUs, including multi-display startup and reconnection. +- Add experimental MIPI DSI display support with graphics acceleration, brightness control, display off/on, rotation, HiDPI and external-display coexistence. Tested on GPD MicroPC 2 and CHUWI MiniBook X N100 only; do not expect it to work on other devices. System sleep/wake remains unsupported. + +### Fixed + +- Fix graphics initialization and acceleration on additional supported Tiger Lake and Alder/Raptor Lake GPU variants, including single-channel configurations. +- Fix graphics initialization on compatible Sequoia and Tahoe updates without requiring an exact OS build match. +- Fix display freezes during cursor resizing and improve multi-display startup and display off/on with MIPI DSI panels. +- Fix system power-off hangs with a USB-C display connected on Alder Lake-class GPUs. + ## 0.6.1 Use the latest [WhateverXe Graphics](https://github.com/b00t0x/WhateverXe-Graphics/releases) package. diff --git a/README.md b/README.md index 48f9838e..bd1de692 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,11 @@ [日本語](README_ja.md) -WhateverXe is a Lilu plugin that adapts Apple graphics drivers for selected -Intel Xe-LP integrated GPUs. It is derived from WhateverGreen. - -This project is experimental and requires the signed graphics package from -[WhateverXe Graphics](https://github.com/b00t0x/WhateverXe-Graphics). It installs -the graphics stack under `/Library` and updates the Auxiliary Kernel Collection -(AuxKC), without patching System files or snapshots. WEX and the graphics package -have independent release versions. Use the latest releases of both WEX and -WhateverXe Graphics. +WhateverXe is a Lilu plugin, derived from WhateverGreen, that adapts Apple graphics drivers for selected Intel Xe-LP integrated GPUs. +Use the latest releases of [WhateverXe](https://github.com/b00t0x/WhateverXe/releases) and [WhateverXe Graphics](https://github.com/b00t0x/WhateverXe-Graphics/releases) together. See [CHANGELOG](CHANGELOG.md) for release notes. ## Compatibility -One WEX binary and one graphics pkg cover the following macOS versions: - | macOS | Required version | | --- | --- | | Big Sur | 11.7.11 | @@ -25,241 +16,113 @@ One WEX binary and one graphics pkg cover the following macOS versions: | Sequoia | 15.8.x | | Tahoe | 26.7 or newer | -Big Sur through Sonoma require the qualified kernel identities. Sequoia and -Tahoe accept servicing updates within their OS family while retaining runtime -instruction and ABI checks. Future updates are not prevalidated. - -WEX selects the OS ABI and GPU backend independently. OS-specific graphics -payloads and the common AppleIntelTGLCompat provider are installed by WhateverXe -Graphics; GPU generation and connector routing are handled by WEX. +Sequoia and Tahoe accept compatible updates within their OS family. Future Apple changes may require an update to WEX or Graphics. ### Tested on hardware -| GPU family | Tested configurations | -| --- | --- | -| Tiger Lake-U | 48- and 80-EU graphics; desktop-style and laptop display layouts | -| Alder Lake-N | 16- and 24-EU graphics; two HDMI displays | -| Alder Lake-H | 96-EU graphics (`46A6`); fixed TC HDMI and DisplayPort through a DP-to-HDMI adapter | -| Alder Lake-S | UHD Graphics 770 (`4690`); HDMI, DisplayPort and a USB-C port wired as fixed DisplayPort | +- Tiger Lake-U: 48- and 80-EU configurations. +- Alder Lake-N: 16- and 24-EU configurations. +- Twin Lake: 32-EU configurations. +- Alder Lake-H: 96-EU configurations, including single-channel operation. +- Alder Lake-S: UHD Graphics 770. -These are tested configurations within each family, not a guarantee for every -model or connector. See [tested features and known issues](#tested-features-and-known-issues) -for OS coverage and remaining problems. +See [tested features and known issues](#tested-features-and-known-issues) for OS coverage and display configurations. PCI IDs are listed in [device selection](docs/device-selection.md). ### Expected to work, not yet tested on hardware -- Other supported Tiger Lake-U/H35 configurations, including 96-EU graphics. -- Tiger Lake-H45 (GT1): `9A60`, `9A68`, `9A70`. -- Other supported Alder Lake-N configurations and Twin Lake. -- Other Alder Lake-H/P/U and Raptor Lake-H/P/U configurations with supported fixed HDMI or fixed TC DisplayPort wiring. -- Experimental internal eDP on Alder Lake-N / Twin Lake and Alder Lake-H/P/U / Raptor Lake-H/P/U: a single firmware-described Combo A/B panel with PPS0 and PCH PWM brightness. This path has local CPU and donor replay coverage only; use the corresponding WhateverXe Graphics release with this plugin. See [display limits](docs/device-selection.md#display-limits). -- Other supported Alder Lake-S configurations, Alder Lake-HX, Raptor Lake-S and Raptor Lake-HX. - -These devices use implemented support shared with the tested families. Their -IDs are accepted automatically; no CPU-model or machine-specific setting is -needed. Hardware validation is still outstanding. All accepted IDs and resource -limits are listed in [device selection](docs/device-selection.md). +- Other supported Tiger Lake-U/H35 configurations, including 96 EU, and Tiger Lake-H45. +- Other supported Alder Lake-N / Twin Lake configurations. +- Other supported Alder Lake-H/P/U and Raptor Lake-H/P configurations. +- Other supported Alder Lake-S/HX and Raptor Lake-S/HX configurations. +- Internal eDP on Alder Lake-N / Twin Lake and Alder Lake-H/P/U / Raptor Lake-H/P is implemented but has not been tested on hardware. ### Further implementation required -- On Alder Lake-N / Twin Lake and Alder Lake-H/P/U / Raptor Lake-H/P/U, - external Combo-PHY DisplayPort and Type-C-managed output need - further implementation. eDP panels using other power/backlight methods also need additional support. Accepting the GPU ID does not enable these outputs. -- Rocket Lake-S: its display power and clock handling differ from Tiger Lake. - It is not supported by this release, and no hardware validation is scheduled. +- Raptor Lake-U: dedicated display-clock handling is not implemented. +- Rocket Lake-S: display control requires additional support. ### No support planned - Xe-LPG, including Meteor Lake integrated graphics. - Xe2 and later graphics architectures. -## Required components - -Download the **RELEASE** ZIP from [Releases](https://github.com/b00t0x/WhateverXe/releases). -The DEBUG ZIP is for troubleshooting. Each ZIP contains only `WhateverXe.kext` -and its matching dSYM. `SHA256SUMS` contains checksums for both archives. -See [CHANGELOG](CHANGELOG.md) for release notes. - -| Component | Role | -| --- | --- | -| Lilu | Runtime required by WhateverXe | -| WhateverXe.kext | Load through OpenCore after Lilu | -| [WhateverXe Graphics pkg](https://github.com/b00t0x/WhateverXe-Graphics/releases) | Install the signed graphics stack into `/Library` and rebuild AuxKC | -| [AMFIPass v1.4.1 Mod](https://github.com/b00t0x/WhateverXe-Graphics/blob/main/extras/AMFIPass-v1.4.1-Mod-RELEASE.zip) | Load through OpenCore for the modified graphics stack | - -Configure SIP with `csr-active-config=03080000` (value `0x803`). Full SIP is not -supported. With the signed graphics package and linked Mod AMFIPass, -`amfi=0x80` and `ipc_control_port_options=0` are not required. +## Installation -Tahoe requires `CoreMedia / AllowOnePassMetalScaling` to be disabled; the -WhateverXe Graphics installer applies this setting. +1. Add Lilu, `WhateverXe.kext` and [AMFIPass 1.4.1 Mod](https://github.com/b00t0x/WhateverXe-Graphics/blob/main/extras/AMFIPass-v1.4.1-Mod-RELEASE.zip) to OpenCore, with WEX after Lilu. Use the WEX **RELEASE** ZIP; DEBUG is for troubleshooting. +2. Set `csr-active-config=03080000` (`0x803`) and apply the [configuration](#configuration). +3. Boot the target macOS, install the latest [WhateverXe Graphics pkg](https://github.com/b00t0x/WhateverXe-Graphics/releases), then restart. [Package installation and removal](https://github.com/b00t0x/WhateverXe-Graphics#install). -Boot the target OS with the latest WEX before installing the latest graphics -pkg. If an older Xe System patch is installed, restore it using its original -recovery procedure first. The pkg does not change EFI, System files, snapshots -or SIP settings. See [installation and removal](https://github.com/b00t0x/WhateverXe-Graphics#install) -for the package instructions and the standalone uninstall script. +Use the linked **Mod** AMFIPass instead of upstream AMFIPass; do not load both. WhateverGreen and WhateverXe must not be enabled together. `amfi=0x80` and `ipc_control_port_options=0` are unnecessary. -Extract `AMFIPass.kext` from the linked **Mod** archive, place it in -`EFI/OC/Kexts`, and enable it in `Kernel > Add`. Replace an existing AMFIPass -with this Mod build; do not load both copies. Upstream AMFIPass does not support -the Mod signing chain. - -If you use OCLP for other root patches, use the -[custom OCLP build](https://github.com/b00t0x/OpenCore-Legacy-Patcher), which uses -the same Mod signing chain. Xe graphics installation itself uses WhateverXe -Graphics independently of OCLP. - -WhateverGreen and WhateverXe must not be enabled together, including in -mixed-GPU configurations. +Graphics installs under `/Library` and rebuilds AuxKC without patching System files or snapshots. Restore an old Xe System patch before installing. If you need OCLP for other root patches, use the [custom OCLP build](https://github.com/b00t0x/OpenCore-Legacy-Patcher) with the matching Mod signing chain. ## Configuration -GPU selection is automatic from the PCI device ID. Connector routing is read -from firmware VBT where supported; fixed HDMI and fixed DisplayPort layouts -normally need no manual wiring properties. Advanced `device-id` overrides are -described in [device selection](docs/device-selection.md). - -On supported GPUs, WhateverXe also normalizes the detected Intel Management -Engine device name to `IMEI`, as WhateverGreen does. A separate ACPI rename -from `HECI` or `MEI` to `IMEI` is unnecessary. - -Tiger Lake automatically selects VBT-described fixed DisplayPort routes on TC1–TC4 when AUX wiring matches the native controller. Fixed DP and DP Alt Mode use the detected FIA layout. TC3/TC4 fixed DP and non-modular FIA support are implemented but have not been verified on hardware. TC5/TC6 and cross-routed AUX require further backend support. - -Alder Lake-N / Twin Lake and Alder Lake-H/P/U / Raptor Lake-H/P/U use -VBT-described Combo A/B and fixed TC1–TC4 HDMI routes. Experimental fixed -TC1–TC4 SST DisplayPort requires matching VBT AUX wiring. The explicit -Alder Lake-N HDMI property applies only to Combo routes. - -ADL-H has been tested with fixed HDMI outputs and a DP-to-HDMI adapter, -including three connected displays at startup, firmware-scaled boot displays -and DP reconnection. Direct DP sinks and other GPUs in these families have -not been verified with the fixed TC DP path. MST, USB-C DP Alt Mode, -Thunderbolt and cross-routed AUX remain unsupported on these GPUs. - -ADL-P/RPL-P uses its physical ID and detected GPU resources automatically. -No device-ID override or machine-specific AGDP kext is required. -`-wex-noaccel` retains framebuffer-only operation. +### Required boot arguments -### Internal-panel brightness +Add to OpenCore's `boot-args`: -Add `SSDT-PNLF` to OpenCore to enable internal-display brightness control. +```text +-allow3d -disablegfxfirmware +``` ### DVMT -Set **DVMT Pre-Allocated to 96MB** in the BIOS/UEFI settings where possible. - -If it is fixed at **60MB**, add `-igfxdvmt` to `boot-args` and set both properties -below on the iGPU under OpenCore's `DeviceProperties > Add` -(usually `PciRoot(0x0)/Pci(0x2,0x0)`). +Set **DVMT Pre-Allocated to 96MB** in BIOS/UEFI where possible. +If it is fixed at **60MB**, also add `-igfxdvmt` and set these iGPU properties under `DeviceProperties > Add` (usually `PciRoot(0x0)/Pci(0x2,0x0)`): | Property | Type | Value (hex) | | --- | --- | --- | | `framebuffer-patch-enable` | Data | `01000000` | | `framebuffer-stolenmem` | Data | `0000C003` | -These Data values use little-endian byte order. `-igfxdvmt` corrects the capacity -calculation, while the properties limit the display memory pool to fit within -60MB. Neither setting increases the memory allocated by the firmware. -With 96MB, these DVMT-specific settings are unnecessary. - -### Required boot arguments - -Add the following to OpenCore's `boot-args`: - -```text --allow3d -disablegfxfirmware -``` - -| Argument | Purpose | -| --- | --- | -| `-allow3d` | Enable the Apple driver's 3D path. | -| `-disablegfxfirmware` | Disable the Apple driver's graphics-firmware loading path for the current backend. | - -The `-wex-nopanel` boot argument has been removed and is no longer needed. -WEX automatically handles systems with or without an internal display. - -No family-selection or per-feature enable arguments are required for -Alder Lake-N / Twin Lake. -GPU backend selection is independent of hardware test coverage: untested PCI -revisions use the same family implementation. Revision-specific behavior is -added when a documented difference or observed failure requires it. +These settings are unnecessary at 96MB and do not increase firmware-allocated memory. -Alder Lake-N / Twin Lake and Alder Lake-H/P/U / Raptor Lake-H/P/U select -Y-tiled scanout automatically; `AAPL,GfxYTile` is not required in OpenCore. -Existing values are preserved. Do not use the Apple driver's `-yTileDisable` -argument on these GPUs: their X-tiled scanout path is unsupported. +### Other settings -For framebuffer bring-up, the optional `-wex-noaccel` argument disables accelerator -startup. On Alder Lake-N / Twin Lake and Alder Lake-H/P/U / Raptor Lake-H/P/U, -it also skips GPU resource probing while preserving the -framebuffer's independent boot-memory and MMIO checks. It does not enable an -otherwise unsupported display backend. - -Temporary development diagnostic: `-wex-adlp-readout` captures ADL-P boot resources while keeping GPU driver admission disabled. It is not required for supported hardware. -`-wex-adlp-load-only` blocks both native framebuffer and accelerator probes/starts on a selected ADL-P/RPL-P device, for load-only diagnostics without GPU initialization. -`-wex-display13-offline` keeps native display controller initialization enabled on Alder Lake-N / Twin Lake and Alder Lake-H/P/U / Raptor Lake-H/P/U but reports selected outputs as disconnected, isolating HPD/EDID and mode programming for framebuffer diagnostics. It does not establish display operation. +- Add `SSDT-PNLF` for internal-display brightness control. +- GPU selection is automatic; a `device-id` override is normally unnecessary. +- Alder Lake-N / Twin Lake and Alder Lake-H/P/U / Raptor Lake-H/P select tiling automatically. Do not use `-yTileDisable`; `AAPL,GfxYTile` is unnecessary. ## Tested features and known issues -| Hardware | Verified features | +The common regression checks cover Metal, OpenGL, OpenCL, hardware H.264 encode/decode and HEVC decode. The table summarizes OS coverage and configuration-specific display features. + +| Configuration | Verified coverage | | --- | --- | -| Tiger Lake / Alder Lake-N | Big Sur–Tahoe: dual 1920×1080 at 60 Hz output, Metal/MPS, OpenGL, OpenCL, hardware H.264 encode/decode, HEVC decode, display sleep/wake, Safari video decoding and Widevine playback | -| Tiger Lake laptops | Big Sur–Tahoe: internal 1366×768 eDP display, brightness control, HDMI and USB-C DisplayPort Alt Mode, up to three simultaneous displays; USB-C reconnection remains unreliable | -| Alder Lake-N | Big Sur/Tahoe: automatic Y tiling without an EFI `AAPL,GfxYTile` setting | -| Alder Lake-H | Big Sur–Tahoe: Metal, OpenGL, hardware H.264 encode/decode, HEVC decode and three displays using fixed HDMI and a DP-to-HDMI adapter | -| Alder Lake-S | Big Sur–Tahoe: Metal, OpenGL, OpenCL, hardware H.264 encode/decode, HEVC decode and three displays (two 1080p60 and one 720p60); Safari video playback; Widevine checked on Big Sur and Tahoe | -| Tiger Lake / Alder Lake-S | Tahoe: Chrome hardware-accelerated Canvas, WebGL and WebGPU | +| Tiger Lake mini PCs | Big Sur–Tahoe: 80 EU, two 1080p60 displays, display off/on, Safari video and Widevine | +| Tiger Lake laptops | Big Sur–Tahoe: 48 EU, internal eDP and brightness, HDMI/USB-C, up to three displays and display off/on | +| Alder Lake-N mini PCs | Big Sur–Tahoe: 16/24 EU, two 1080p60 HDMI displays and display off/on | +| Alder Lake-N / Twin Lake laptops | MIPI DSI on MicroPC 2 (N250) and MiniBook X N100. Big Sur: brightness, rotation, display off/on and external HDMI/USB-C coexistence; MicroPC 2 also has HiDPI and up to three displays. Tahoe: MicroPC 2 internal/external displays, brightness, rotation and HiDPI | +| Alder Lake-H | Big Sur–Tahoe: 96 EU, three displays using HDMI and a DP-to-HDMI adapter. Single-channel `4626` graphics acceleration checked on Big Sur | +| Alder Lake-S | Big Sur–Tahoe: UHD 770, HDMI/DP/USB-C and up to three displays; Safari video, with Widevine checked on Big Sur/Tahoe | -These results cover tested systems, not every GPU, display, adapter or dock. -The listed resolutions describe test coverage; they are not a fixed resolution -limit imposed by WEX. +MIPI DSI support is **experimental and tested only on GPD MicroPC 2 and CHUWI MiniBook X N100**. Do not expect it to work on other devices. ### Known issues and limits -- Some image-conversion operations can crash the application. -- App Store video playback can freeze the GPU on Sequoia. -- Reconnecting a monitor to a dedicated DisplayPort connector can leave the - screen black or freeze the computer. USB-C DisplayPort Alt Mode reconnection - can also freeze the computer; this has been reproduced on both Big Sur and - Tahoe. -- Safari video playback may occasionally drop frames. -- Up to three displays can be used simultaneously; a fourth display will not - receive an image. -- DisplayPort output or reconnection can be slow on Alder Lake-S. -- Some connector layouts, Thunderbolt-managed outputs and panel backlight - methods are not supported. See [display limits](docs/device-selection.md#display-limits). -- FairPlay-protected playback is not supported. -- Full system sleep/wake and resolutions above those listed above are unverified. - USB-C display output currently prevents part of the display hardware from - entering a low-power state. - -## Roadmap - -- Complete DisplayPort, USB-C and internal-display support on Alder Lake-N / Twin Lake and Alder Lake-H/P/U / Raptor Lake-H/P/U. +- DisplayPort reconnection can cause black screens or freezes. Tiger Lake USB-C reconnection is also unreliable; Alder Lake-S DP output/reconnection can be slow. +- Some image-conversion operations can crash applications; App Store video can freeze the GPU on Sequoia. +- Safari video may occasionally drop frames. FairPlay-protected playback is unsupported. +- Up to three displays are supported. Resolutions beyond the tested configurations and full system sleep/wake remain unverified. +- Some connector layouts and backlight methods, MST and Thunderbolt-managed outputs are unsupported. + +### Roadmap + +- Implement Raptor Lake-U display-clock support. +- Complete DisplayPort, USB-C and internal-display support on Alder Lake-N / Twin Lake and Alder Lake-H/P/U / Raptor Lake-H/P. - Improve display reconnection reliability and investigate full system sleep/wake. ## Build and test -Use macOS with Xcode command-line tools, Python 3.9 or newer, MacKernelSDK and -Lilu's development headers/library. CI uses the Lilu -[1.7.2 release tag](https://github.com/acidanthera/Lilu/tree/1.7.2), -which includes the Tahoe kernel definition. Place `MacKernelSDK/` and `Lilu.kext/` in the -repository root, or pass their paths with `--sdk` and `--lilu`. +Requires macOS, Xcode command-line tools, Python 3.9+, MacKernelSDK and Lilu 1.7.2 development headers/library. Place `MacKernelSDK/` and `Lilu.kext/` in the repository root, or pass `--sdk` and `--lilu`. ```sh python3 tools/build.py --release python3 tools/test.py ``` -WhateverXe and AppleIntelTGLCompat kexts and a source manifest are written to -`build/`. TGLCompat source and shared headers are maintained here; its signed -binary is distributed through WhateverXe Graphics. The WEX release ZIP contains -only the OpenCore plugin, not the graphics package. Optional donor replay tests -accept `--donor /path/to/AppleIntelTGLGraphicsFramebuffer` and -`--accelerator /path/to/AppleIntelTGLGraphics`, using the exact binaries accepted -by each test. CPU tests do not establish physical GPU compatibility. -WhateverGreen source is not a build dependency. +Output is under `build/`. AppleIntelTGLCompat is built here and distributed through Graphics. Optional exact-donor tests accept `--donor` and `--accelerator` paths. ## License and credits diff --git a/README_ja.md b/README_ja.md index 2aa82e20..dd365876 100644 --- a/README_ja.md +++ b/README_ja.md @@ -2,16 +2,11 @@ [English](README.md) -WhateverXeは、一部のIntel Xe-LP内蔵GPU向けにAppleのグラフィックスドライバを適応させるLiluプラグインです。WhateverGreenを基に開発しています。 - -本プロジェクトは実験段階であり、[WhateverXe Graphics](https://github.com/b00t0x/WhateverXe-Graphics)の署名済みグラフィックスpkgが必要です。資材を`/Library`へ配置してAuxiliary Kernel Collection(AuxKC)を更新し、Systemファイルやsnapshotにはパッチを適用しません。WEXとグラフィックスpkgは、それぞれ独立したバージョンでリリースします。 - -WEXとWhateverXe Graphicsは、どちらも最新版を使用してください。 +WhateverXeは、WhateverGreenを基に開発した、一部のIntel Xe-LP内蔵GPU向けにAppleのグラフィックスドライバーを適応させるLiluプラグインです。 +[WhateverXe](https://github.com/b00t0x/WhateverXe/releases)と[WhateverXe Graphics](https://github.com/b00t0x/WhateverXe-Graphics/releases)は両方とも最新版を使用してください。変更点は[CHANGELOG](CHANGELOG.md)を参照してください。 ## 対応範囲 -単一のWEXバイナリと1つのグラフィックスpkgで、以下のmacOSに対応します。 - | macOS | 必要バージョン | | --- | --- | | Big Sur | 11.7.11 | @@ -21,178 +16,113 @@ WEXとWhateverXe Graphicsは、どちらも最新版を使用してください | Sequoia | 15.8.x | | Tahoe | 26.7以上 | -Big Sur〜Sonomaは検証済みカーネルの識別を必要とします。SequoiaとTahoeでは、実行時の命令列・ABI検査を維持した上で、同じOS系列内の更新を受け付けます。将来の更新が検証済みという意味ではありません。 - -WEXはOSのABIとGPUバックエンドを独立して選択します。OS別のグラフィックス資材と共通のAppleIntelTGLCompatはWhateverXe Graphicsが導入し、GPU世代・コネクタ配線への対応はWEXが担います。 +SequoiaとTahoeは同じOS系列内の互換性のある更新に対応します。将来のApple側の変更によりWEXやGraphicsの更新が必要になる場合があります。 ### 実機で動作確認済み -| GPUファミリー | 確認した構成 | -| --- | --- | -| Tiger Lake-U | 48 EU・80 EU。外部画面を使う構成とノートPCの画面構成 | -| Alder Lake-N | 16 EU・24 EU。HDMI 2画面 | -| Alder Lake-H | 96 EU(`46A6`)。固定TC HDMIとDP→HDMI変換器経由のDisplayPort | -| Alder Lake-S | UHD Graphics 770(`4690`)。HDMI、DisplayPort、固定DisplayPortとして配線されたUSB-C端子 | +- Tiger Lake-U:48 EU・80 EU構成。 +- Alder Lake-N:16 EU・24 EU構成。 +- Twin Lake:32 EU構成。 +- Alder Lake-H:シングルチャネルを含む96 EU構成。 +- Alder Lake-S:UHD Graphics 770。 -各ファミリーの一部構成で確認した結果であり、全モデル・全端子の動作保証ではありません。 -OS別の検証範囲と残る問題は[動作確認済みの機能と既知の問題](#動作確認済みの機能と既知の問題)を参照してください。 +OS別の範囲と表示構成は[動作確認済みの機能と既知の問題](#動作確認済みの機能と既知の問題)、PCI IDは[デバイス選択](docs/device-selection.md)を参照してください。 ### 動作を期待できるが実機未検証 -- 96 EUを含む、対応IDの他のTiger Lake-U/H35構成。 -- Tiger Lake-H45(GT1):`9A60`、`9A68`、`9A70`。 -- 対応IDの他のAlder Lake-N構成とTwin Lake。 -- 対応する固定HDMIまたは固定TC DisplayPort配線の、他のAlder Lake-H/P/U・Raptor Lake-H/P/U構成。 -- 対応IDの他のAlder Lake-S構成、Alder Lake-HX、Raptor Lake-S、Raptor Lake-HX。 - -確認済みのファミリーと処理を共有する実装があり、IDは自動で受け付けます。 -CPUモデル名や機種固有の設定は不要ですが、実機での動作確認は残っています。 -受付IDの一覧と対応する資源構成は[デバイス選択](docs/device-selection.md)を参照してください。 - -実験的に、Alder Lake-N / Twin LakeとAlder Lake-H/P/U / Raptor Lake-H/P/Uの内蔵eDPを実装しています。対象はfirmwareで一意に記述されたCombo A/Bの単一パネルとPPS0・PCH PWMによる輝度制御です。CPU試験とdonor再生試験のみで、実機未検証です。対応するWhateverXe Graphicsリリースと組み合わせて使用してください。条件は[表示の制限](docs/device-selection.md#display-limits)を参照してください。 +- 96 EUを含む対応IDの他のTiger Lake-U/H35構成と、Tiger Lake-H45。 +- 対応IDの他のAlder Lake-N / Twin Lake構成。 +- 対応IDの他のAlder Lake-H/P/U・Raptor Lake-H/P構成。 +- 対応IDの他のAlder Lake-S/HX・Raptor Lake-S/HX構成。 +- Alder Lake-N / Twin LakeとAlder Lake-H/P/U / Raptor Lake-H/Pの内蔵eDPは実装済み・実機未検証です。 ### 対応に追加改修が必要 -- Alder Lake-N / Twin LakeとAlder Lake-H/P/U / Raptor Lake-H/P/Uの - Combo PHY経由の外部DisplayPort・Type-C管理下の出力には追加改修が必要です。 - その他の電源・輝度制御方式を使うeDPパネルも追加対応が必要です。 - GPUのIDを受け付けるだけで、これらの出力が有効になるわけではありません。 -- Rocket Lake-S:Tiger Lakeとは表示の電源・クロック制御が異なります。 - このリリースでは未対応で、実機検証の予定もありません。 +- Raptor Lake-U:専用の表示クロック制御が未実装です。 +- Rocket Lake-S:表示制御に追加対応が必要です。 ### 対応予定なし - Meteor Lakeの内蔵GPUなど、Xe-LPG。 - Xe2以降のグラフィックス世代。 -## 必須構成 - -[Releases](https://github.com/b00t0x/WhateverXe/releases)から **RELEASE** ZIPをダウンロードしてください。DEBUG ZIPは問題調査用です。各ZIPには`WhateverXe.kext`と対応するdSYMのみを収録し、両ZIPのチェックサムは`SHA256SUMS`で配布します。各版の変更点は[CHANGELOG](CHANGELOG.md)を参照してください。 - -| コンポーネント | 役割 | -| --- | --- | -| Lilu | WhateverXeの動作に必要なランタイム | -| WhateverXe.kext | OpenCoreでLiluより後に読み込む | -| [WhateverXe Graphics pkg](https://github.com/b00t0x/WhateverXe-Graphics/releases) | 署名済み資材を`/Library`へ導入しAuxKCを更新する | -| [AMFIPass v1.4.1 Mod](https://github.com/b00t0x/WhateverXe-Graphics/blob/main/extras/AMFIPass-v1.4.1-Mod-RELEASE.zip) | 改変されたグラフィックス構成に必要。OpenCoreで導入する | +## インストール -SIPは`csr-active-config=03080000`(値`0x803`)に設定してください。SIPの全面有効化には対応していません。署名済みグラフィックスpkgとリンク先のMod AMFIPassを使用する場合、`amfi=0x80`と`ipc_control_port_options=0`は不要です。 +1. Lilu・`WhateverXe.kext`・[AMFIPass 1.4.1 Mod](https://github.com/b00t0x/WhateverXe-Graphics/blob/main/extras/AMFIPass-v1.4.1-Mod-RELEASE.zip)をOpenCoreに追加し、WEXはLiluの後に設定します。WEXは**RELEASE** ZIPを使用してください。DEBUGは障害調査用です。 +2. `csr-active-config=03080000`(`0x803`)と、[設定](#設定)を適用します。 +3. 対象macOSを起動して最新版の[WhateverXe Graphics pkg](https://github.com/b00t0x/WhateverXe-Graphics/releases)を導入し、再起動します。[pkgの導入・削除手順](https://github.com/b00t0x/WhateverXe-Graphics/blob/main/README_ja.md#インストール)。 -Tahoeでは`CoreMedia / AllowOnePassMetalScaling`の無効化が必要です。WhateverXe Graphicsのインストーラーが設定します。 +AMFIPassは本家版に代えてリンク先の**Mod版**を使用し、両方を同時に読み込まないでください。WhateverGreenとWhateverXeも併用できません。`amfi=0x80`と`ipc_control_port_options=0`は不要です。 -最新版のWEXを読み込んだ対象OSから、最新版のグラフィックスpkgを実行してください。旧Xe System patchがある場合は、先に元の復元手順で戻してください。pkgはEFI・Systemファイル・snapshot・SIP設定を変更しません。pkgの使用方法と単独のアンインストールスクリプトは[導入と削除](https://github.com/b00t0x/WhateverXe-Graphics/blob/main/README_ja.md#インストール)を参照してください。 - -リンク先の **Mod版** アーカイブから`AMFIPass.kext`を展開し、`EFI/OC/Kexts`へ配置して、`Kernel > Add`で有効にしてください。既存のAMFIPassがある場合はこのMod版へ置き換え、両方を同時に読み込まないでください。upstreamのAMFIPassはMod版の署名チェーンに対応していません。 - -他のroot patchにOCLPを併用する場合は、同じMod版の署名チェーンを使用する[独自ビルド版OCLP](https://github.com/b00t0x/OpenCore-Legacy-Patcher)を使用してください。Xeグラフィックスの導入自体はOCLPに依存せず、WhateverXe Graphicsを使用します。 - -複数GPUの構成を含め、WhateverGreenとWhateverXeは同時に有効にしないでください。 +Graphicsは`/Library`への配置とAuxKC生成を行い、Systemファイルやsnapshotにはパッチしません。旧Xe System patchは先に復元してください。他のroot patchのためにOCLPを使う場合は、同じMod署名に対応する[独自ビルド版OCLP](https://github.com/b00t0x/OpenCore-Legacy-Patcher)を使用します。 ## 設定 -GPUはPCI device IDから自動選択します。対応範囲内ではファームウェアのVBTから配線を読み取るため、固定HDMI/固定DisplayPort構成は通常、配線propertyの手動指定が不要です。高度な設定としての`device-id`上書きは[デバイス選択](docs/device-selection.md)で説明しています。 - -対応GPUでは、WhateverGreenと同様に、検出されたIntel Management Engineのデバイス名も`IMEI`へ統一します。`HECI`や`MEI`から`IMEI`へのACPI renameを別途追加する必要はありません。 - -Tiger Lakeはnative controllerとAUX配線が一致するVBTの固定DisplayPortをTC1〜TC4で自動選択します。固定DPとDP Alt Modeは検出したFIA構成を使用します。TC3/TC4の固定DPと非modular FIAは実装済み・実機未検証です。TC5/TC6と交差AUX配線には追加のbackend対応が必要です。 - -Alder Lake-N / Twin LakeとAlder Lake-H/P/U / Raptor Lake-H/P/Uでは、Combo A/B・固定TC1〜TC4 HDMI配線をVBTから選択します。試験的な固定TC1〜TC4 SST DisplayPortには、VBTのAUX配線の一致が必要です。Alder Lake-NのHDMI明示propertyはCombo配線にのみ適用します。 - -ADL-Hでは、固定HDMIとDP→HDMI変換器を使い、3画面接続での起動、ファームウェアによる拡大表示からの起動、DP再接続を確認しています。固定TC DP経路での直接DP接続の表示先と、これらのファミリーの他のGPUは未検証です。これらのGPUではMST、USB-C DP Alt Mode、Thunderbolt、交差AUX配線は未対応です。 - -ADL-P/RPL-Pは物理IDと検出したGPU資源を自動で使用します。device-ID上書きや機種別AGDP kextは不要です。`-wex-noaccel`でFramebufferのみの動作を維持します。 - -### 内蔵パネルの輝度 - -内蔵ディスプレイの輝度調整には、OpenCoreへ`SSDT-PNLF`を追加してください。 - -### DVMT - -BIOS/UEFIで変更できる場合は、**DVMT Pre-Allocatedを96MBに設定することを推奨します**。 - -**60MBから変更できない場合**は、`boot-args`に`-igfxdvmt`を追加し、OpenCoreの`DeviceProperties > Add`にあるiGPUの項目(通常は`PciRoot(0x0)/Pci(0x2,0x0)`)へ以下の2つを設定してください。 - -| Property | 型 | 値(16進数) | -| --- | --- | --- | -| `framebuffer-patch-enable` | Data | `01000000` | -| `framebuffer-stolenmem` | Data | `0000C003` | - -Data値はリトルエンディアンです。`-igfxdvmt`は容量計算を補正し、DevicePropertiesは表示用メモリ領域を60MB内に収めます。ファームウェアが確保したメモリを増やす設定ではありません。96MBの場合、これらのDVMT用設定は不要です。 - ### 必須ブート引数 -OpenCoreの`boot-args`に以下を追加してください。 +OpenCoreの`boot-args`に追加します。 ```text -allow3d -disablegfxfirmware ``` -| 引数 | 用途 | -| --- | --- | -| `-allow3d` | Appleドライバの3D処理を有効にする。 | -| `-disablegfxfirmware` | 現在のバックエンドで、Appleドライバのグラフィックスファームウェア読み込みを無効にする。 | +### DVMT -`-wex-nopanel`ブート引数は廃止され、不要になりました。内蔵ディスプレイの有無は自動で判定します。 +BIOS/UEFIで設定できる場合は、**DVMT Pre-Allocatedを96MB**にします。 +**60MB固定**の場合は、`-igfxdvmt`を追加し、`DeviceProperties > Add`のiGPU(通常は`PciRoot(0x0)/Pci(0x2,0x0)`)に以下を設定します。 -Alder Lake-N / Twin Lakeで世代選択や個別機能の有効化引数を追加する必要はありません。 -GPU backendの選択と実機検証範囲は独立しています。未検証のPCI revisionも同じfamily実装を使い、 -文書化された差分や実際の不具合で必要になった場合にrevision別の処理を追加します。 +| Property | 型 | 値(hex) | +| --- | --- | --- | +| `framebuffer-patch-enable` | Data | `01000000` | +| `framebuffer-stolenmem` | Data | `0000C003` | -Alder Lake-N / Twin LakeとAlder Lake-H/P/U / Raptor Lake-H/P/UではYタイル出力を自動選択するため、OpenCoreの`AAPL,GfxYTile`設定は不要です。既存の値は保持します。 -これらのGPUはXタイル出力に未対応なので、Appleドライバの`-yTileDisable`引数は使用しないでください。 +96MBの場合はこれらの設定は不要です。firmwareが確保するメモリを増やす設定ではありません。 -FBの立上げ時は、任意の`-wex-noaccel`引数でAcceleratorの起動を止められます。 -Alder Lake-N / Twin LakeとAlder Lake-H/P/U / Raptor Lake-H/P/UではGPU資源のprobeも省略し、FB独自の起動メモリ・MMIOの確認は維持します。 -未対応のDisplayバックエンドを有効にする引数ではありません。 +### その他の設定 -開発時の一時診断用に `-wex-adlp-readout` を指定すると、GPUドライバの受付を閉じたままADL-Pの起動資源を採取します。対応済みハードウェアでは不要です。 -`-wex-adlp-load-only` は選択済みADL-P/RPL-Pで、FramebufferとAccelerator両方のnative probe/startを止め、GPUを初期化せずにロード段階を診断します。 -`-wex-display13-offline` はAlder Lake-N / Twin LakeとAlder Lake-H/P/U / Raptor Lake-H/P/Uのnative display controller初期化を有効に保ち、選択した出力を未接続として扱います。FB診断でHPD/EDIDとmode設定を切り離すための設定で、表示成立を意味しません。 +- 内蔵画面の輝度調整には`SSDT-PNLF`を追加します。 +- GPUは自動選択するため、通常は`device-id`上書きは不要です。 +- Alder Lake-N / Twin LakeとAlder Lake-H/P/U / Raptor Lake-H/Pではタイル形式を自動選択します。`-yTileDisable`は使わないでください。`AAPL,GfxYTile`は不要です。 ## 動作確認済みの機能と既知の問題 -| ハードウェア | 確認済みの機能 | +共通の回帰項目はMetal・OpenGL・OpenCL・H.264ハードウェアエンコード/デコード・HEVCデコードです。以下にOS別の範囲と、各構成に固有の表示機能をまとめます。 + +| 構成 | 確認した範囲 | | --- | --- | -| Tiger Lake / Alder Lake-N | Big Sur〜Tahoe:外部2画面の1920×1080・60Hz出力、Metal/MPS、OpenGL、OpenCL、ハードウェアH.264エンコード/デコード、HEVCデコード、画面の消灯/復帰、Safariの動画デコード、Widevine再生 | -| Tiger Lakeラップトップ | Big Sur〜Tahoe:内蔵1366×768 eDP、輝度調整、HDMI、USB-C DisplayPort Alt Mode、最大3画面同時出力。USB-Cの再接続には未解決の問題あり | -| Alder Lake-N | Big Sur/Tahoe:EFIの`AAPL,GfxYTile`設定なしでのYタイル自動選択 | -| Alder Lake-H | Big Sur〜Tahoe:Metal、OpenGL、ハードウェアH.264エンコード/デコード、HEVCデコード、固定HDMIとDP→HDMI変換器を使った3画面 | -| Alder Lake-S | Big Sur〜Tahoe:Metal、OpenGL、OpenCL、ハードウェアH.264エンコード/デコード、HEVCデコード、3画面(1080p60×2+720p60×1)、Safari動画再生。WidevineはBig SurとTahoeで確認 | -| Tiger Lake / Alder Lake-S | Tahoe:ChromeのCanvas、WebGL、WebGPUによるハードウェア描画 | +| Tiger LakeミニPC | Big Sur〜Tahoe:80 EU、1080p60の2画面、消灯復帰、Safari動画・Widevine | +| Tiger Lakeラップトップ | Big Sur〜Tahoe:48 EU、内蔵eDPと輝度調整、HDMI/USB-C、最大3画面・消灯復帰 | +| Alder Lake-NミニPC | Big Sur〜Tahoe:16/24 EU、1080p60のHDMI 2画面・消灯復帰 | +| Alder Lake-N / Twin Lakeラップトップ | MicroPC 2(N250)とMiniBook X N100のMIPI DSI。Big Sur:輝度・回転・消灯復帰・外部HDMI/USB-C共存。MicroPC 2はHiDPI・最大3画面も確認。Tahoe:MicroPC 2の内蔵/外部表示・輝度・回転・HiDPI | +| Alder Lake-H | Big Sur〜Tahoe:96 EU、HDMIとDP→HDMI変換器による3画面。シングルチャネル構成の`4626`でもBig Surの加速を確認 | +| Alder Lake-S | Big Sur〜Tahoe:UHD 770、HDMI/DP/USB-C、最大3画面、Safari動画。WidevineはBig Sur/Tahoeで確認 | -検証した機器での結果であり、すべてのGPU・ディスプレイ・アダプター・ドックでの動作を保証するものではありません。 -記載した解像度は検証範囲であり、WEXが設けた固定の解像度制限ではありません。 +MIPI DSIは**GPD MicroPC 2とCHUWI MiniBook X N100のみで検証した試験的対応**です。それ以外の機器での動作は期待しないでください。 ### 既知の問題と制限 -- 一部の画像変換処理でアプリがクラッシュすることがあります。 -- SequoiaではApp Storeの動画再生でGPUが停止することがあります。 -- 専用DisplayPort端子の抜き差しで黒画面やシステム停止が起きることがあります。 - USB-C DisplayPort Alt Modeでも再接続時のシステム停止がBig SurとTahoeの両方で再現しています。 -- Safariの動画再生で時折コマ落ちすることがあります。 -- 同時表示は最大3画面です。4画面目には映像が出ません。 -- Alder Lake-SではDisplayPortの表示開始や再接続後の復帰に時間がかかる場合があります。 -- 一部の端子配線、Thunderbolt管理の出力、内蔵画面の輝度制御方式は未対応です。 - 詳細は[表示の制限](docs/device-selection.md#display-limits)を参照してください。 -- FairPlayで保護されたコンテンツの再生には未対応です。 -- システム全体のスリープ/復帰と、上記を超える解像度は未検証です。 - USB-C映像出力中は表示ハードウェアの一部が低消費電力状態へ移行しません。 - -## ロードマップ - -- Alder Lake-N / Twin LakeとAlder Lake-H/P/U / Raptor Lake-H/P/UでのDisplayPort・USB-C・内蔵画面対応の拡充。 +- DisplayPortの再接続で黒画面・停止が起きる場合があります。Tiger LakeのUSB-C再接続にも未解決の問題があり、Alder Lake-SのDP表示開始・再接続は時間がかかる場合があります。 +- 一部の画像変換でアプリがクラッシュし、SequoiaのApp Store動画ではGPUが停止することがあります。 +- Safari動画で時折コマ落ちします。FairPlay保護コンテンツは未対応です。 +- 同時表示は最大3画面です。検証構成を超える解像度とシステム全体のスリープ/復帰は未検証です。 +- 一部の端子配線・輝度制御方式、MST、Thunderbolt管理の出力は未対応です。 + +### ロードマップ + +- Raptor Lake-Uの表示クロック対応。 +- Alder Lake-N / Twin LakeとAlder Lake-H/P/U / Raptor Lake-H/PでのDisplayPort・USB-C・内蔵画面対応の拡充。 - 画面再接続の安定性改善とシステム全体のスリープ/復帰の調査。 ## ビルドとテスト -macOS、Xcode command-line tools、Python 3.9以降、MacKernelSDK、Liluの開発用ヘッダ/ライブラリを使用します。CIではTahoeのkernel定義を含むLiluの[1.7.2リリースタグ](https://github.com/acidanthera/Lilu/tree/1.7.2)を使用します。`MacKernelSDK/`と`Lilu.kext/`をリポジトリ直下に配置するか、`--sdk`と`--lilu`でパスを指定してください。 +macOS、Xcode Command Line Tools、Python 3.9以上、MacKernelSDK、Lilu 1.7.2の開発用ヘッダー/ライブラリが必要です。`MacKernelSDK/`と`Lilu.kext/`をリポジトリ直下に置くか、`--sdk`・`--lilu`で指定します。 ```sh python3 tools/build.py --release python3 tools/test.py ``` -WhateverXeとAppleIntelTGLCompatのkext、およびソースのmanifestは`build/`に出力されます。TGLCompatのソースと共有ヘッダーはこのリポジトリで管理し、署名済みバイナリはWhateverXe Graphicsで配布します。WEXのRelease ZIPに含まれるkextはOpenCore用プラグインのみで、グラフィックスpkgは含みません。任意のdonor再生試験には`--donor /path/to/AppleIntelTGLGraphicsFramebuffer`と`--accelerator /path/to/AppleIntelTGLGraphics`を指定します。各試験が受け付ける正確なバイナリが必要です。CPUテストだけで実GPUの互換性が確認できるわけではありません。ビルドにWhateverGreenのソースは不要です。 +出力先は`build/`です。AppleIntelTGLCompatもここでビルドし、Graphicsで配布します。任意の実ドライバー再生試験には`--donor`・`--accelerator`で対応する原本を指定します。 ## ライセンスとクレジット diff --git a/WhateverXe/kern_display13_boot.hpp b/WhateverXe/kern_display13_boot.hpp index d12027a1..612bba77 100644 --- a/WhateverXe/kern_display13_boot.hpp +++ b/WhateverXe/kern_display13_boot.hpp @@ -3,6 +3,7 @@ #ifndef KERN_DISPLAY13_BOOT_HPP #define KERN_DISPLAY13_BOOT_HPP #include "kern_alderlake_n.hpp" +#include "kern_dsi_readout.hpp" #include "kern_xelp_gop_stop.hpp" #include "kern_display13_fb_irq.hpp" #include "kern_display13_fb_topology.hpp" @@ -47,29 +48,32 @@ struct Result { uint32_t version {1};Status status {Status::Unavailable}; uint32_t stage {},writes {},before {},after {},lastReg {},expected {},observed {},framesBefore {},framesAfter {}; }; -// Firmware can leave ownership on a selected fixed TC port that has no GOP. +// Firmware can leave ownership on a selected direct TC port that has no GOP. // Linux intel_tc_port_sanitize_mode disconnects unused PHYs. Here admit only -// the already-cold case: disabled/idle buffer, stopped/gated PLL and clocks, +// the already-cold case: disabled/idle buffer, stopped PLL and clock source, // no transcoder route and no AUX command. Do not stop an unclassified link. struct UnusedTCResult { uint32_t version {1};Status status {Status::Unavailable}; uint32_t cleared {},writes {},ddi {},buffer {},ready {},clock {},pll {},gate {}; }; template UnusedTCResult releaseUnusedTC(IO &io,const AlderLakeN::DisplayReadout &boot, - bool admitted,const Display13HDMIWiring &w) { + bool admitted,const Display13HDMIWiring &w,const XELP::DSI::Readout *dsi=nullptr) { UnusedTCResult r; if(!admitted)return r; r.status=Status::Topology; - if(boot.status!=AlderLakeN::ReadoutStatus::Complete || boot.activePipes!=1 || + if((w.dsi && !dsi) || boot.status!=AlderLakeN::ReadoutStatus::Complete || boot.activePipes!=(dsi?0U:1U) || + (dsi && (w.dsi!=1 || dsi->identity.pipe!=0 || !XELP::DSI::sourceUnchanged(io,*dsi))) || !(boot.poweredPipes&1) || !Display13FBTopology::multiOutputWiring(w))return r; uint32_t power=0,auxPower=0; if(!AlderLakeN::value(boot,0x45404,power) || (power&5)!=5 || !AlderLakeN::value(boot,0x45444,auxPower))return r; + uint32_t gated=0; const auto stable=[&] { + if(dsi && !XELP::DSI::sourceUnchanged(io,*dsi))return false; for(unsigned i=0;i UnusedTCResult releaseUnusedTC(IO &io,const AlderLakeN::Displ r.ready=io.read(port.ready);r.clock=io.read(port.clock);r.pll=io.read(port.pll); r.gate=io.read(Display13TC::Clock::Gate); const auto aux=0x64310+0x100*(d-3); - if((r.buffer&0x800000C0U)!=0xC0 || r.ready==UINT32_MAX || (r.ready&7)!=4 || + if((r.buffer&0x800000C0U)!=0xC0 || !(r.ready&4) || + !Display13TC::Port::modeMatches(r.ready,(w.typeC&(1U< Result stop(IO &io,unsigned pipeIndex=0) { +template Result stop(IO &io,unsigned pipeIndex=0,const Display13Transcoder::Bindings &transcoders={}) { Result r; // This is a display pipe index, independent of HDMI A/B DDI identity. // The caller proves the path/FB association and limits its writer to this bank. if(pipeIndex>=3) {r.status=Status::Conflict;return r;} const uint32_t bank=pipeIndex*0x1000; if(!io.readable(0x700AC+bank) || !Display13FBIRQ::powered(io.read(0x45404),pipeIndex)) {r.status=Status::Power;return r;} - const auto pipe=io.read(0x70008+bank);r.before=io.read(0x70080+bank); + const auto pipe=Display13Transcoder::control(io,pipeIndex,transcoders);r.before=io.read(0x70080+bank); if(pipe==UINT32_MAX || r.before==UINT32_MAX) {r.status=Status::Readback;return r;} for(unsigned d=0;d<3;++d) { const auto dsb=io.read(Display13FBIRQ::DSBControl(pipeIndex,d)); diff --git a/WhateverXe/kern_display13_dbuf.hpp b/WhateverXe/kern_display13_dbuf.hpp index 9ad9f8dd..5441d9b9 100644 --- a/WhateverXe/kern_display13_dbuf.hpp +++ b/WhateverXe/kern_display13_dbuf.hpp @@ -32,6 +32,7 @@ struct PipeAllocation { bool pendingCursor {}; }; struct State { + Display13Transcoder::Bindings transcoders; bool begun {},faulted {},joined {}; uint32_t initial[4] {},raw[4] {},initialRequests {},requested {},powered {},owned {},consumers {},mbus {}; PipeAllocation pipe[3]; @@ -129,7 +130,7 @@ template Layout singlePipeLayout(IO &io,const State &s) { if(s.joined || io.read(0x4438C)!=s.mbus || (wells&(3U<<16))) {r.status=Status::Topology;return r;} for(unsigned pipe=0;pipe<3;++pipe) { if(!(wells&(1U<<(2*(5+pipe)))))continue; - const auto conf=io.read(0x70008+pipe*0x1000); + const auto conf=Display13Transcoder::control(io,pipe,s.transcoders); if(conf==UINT32_MAX || (pipe && (conf&(Request|On)))) {r.status=Status::Topology;return r;} if(!pipe) { // Transitional enable/disable must settle before changing DDB. @@ -159,9 +160,10 @@ template Layout pipeLayout(IO &io,const State &s,unsigned index) { if(s.updating&(1U< Layout pipeLayout(IO &io,const State &s,unsigned index) { } // Read the measured boot pipe A allocation, not a Linux allocation copied into // the hardware. All four physical slices exist; a single unjoined A uses S0/1. -template Status begin(IO &io,State &s,uint32_t capacity=0) { +template Status begin(IO &io,State &s,uint32_t capacity=0,const Display13Transcoder::Bindings &transcoders={}) { if(s.faulted)return Status::Faulted; - if(s.begun)return capacity==s.capacity ? Status::Complete : Status::Topology; + if(s.begun)return capacity==s.capacity && transcoders.dsi0Pipe==s.transcoders.dsi0Pipe ? Status::Complete : Status::Topology; + if(!transcoders.valid() || (transcoders.dsi0Pipe!=UINT32_MAX && transcoders.dsi0Pipe!=0))return Status::Topology; + s.transcoders=transcoders; if(capacity && capacity!=7)return Status::Topology; s.capacity=capacity; if(!io.readable(0x4438C) || !io.readable(0x7027C) || !io.readable(0x7017C))return Status::Unavailable; const auto wells=io.read(0x45404); if(wells==UINT32_MAX || (wells&0x403)!=0x403)return fail(s,Status::Power); if(wells&(3U<<16))return fail(s,Status::Topology); // Pipe D is outside this native owner. - if((io.read(0x70008)&0xC0000000)!=0xC0000000 || !(io.read(0x70180)&Request))return fail(s,Status::Topology); + if((Display13Transcoder::control(io,0,s.transcoders)&0xC0000000)!=0xC0000000 || !(io.read(0x70180)&Request))return fail(s,Status::Topology); s.mbus=io.read(0x4438C);s.joined=s.mbus&Request; if(s.mbus==UINT32_MAX || (s.joined && (((s.mbus>>26)&7)!=0 || !(s.mbus&On))))return fail(s,Status::Topology); const auto primary=io.read(0x7027C),cursor=io.read(0x7017C); @@ -210,7 +214,7 @@ template Status begin(IO &io,State &s,uint32_t capacity=0) { // the allocation or slice lifetime calculation. for(unsigned pipe=0;pipe<3;++pipe) { if(!(wells&(1U<<(2*(5+pipe)))))continue; - const auto conf=io.read(0x70008+pipe*0x1000); + const auto conf=Display13Transcoder::control(io,pipe,s.transcoders); if(conf==UINT32_MAX || (pipe && (conf&Request)))return fail(s,Status::Topology); for(unsigned plane=pipe ? 0 : 1;plane<5;++plane) { const auto ctl=io.read(0x70180+pipe*0x1000+plane*0x100); @@ -302,7 +306,7 @@ template Status bindOffPipe(IO &io,State &s,unsigned index) { const auto bit=1U< Status resumeEngine(IO &io,State &s) { if(a.primary.start || a.primary.end || a.cursor.start || a.cursor.end || a.heldPrimary.start || a.heldPrimary.end || a.heldCursor.start || a.heldCursor.end || a.pendingRaw || a.pendingCursor)return fail(s,Status::Conflict); - if(!Display13FBIRQ::consumersStopped(io,7))return fail(s,Status::Conflict); + if(!Display13FBIRQ::consumersStopped(io,7,s.transcoders))return fail(s,Status::Conflict); return power(io,s,0,0); } // STOP_ACK, disabled planes/cursor and idle DSB prove only this pipe stopped. @@ -397,7 +401,7 @@ template Status stopPipe(IO &io,State &s,unsigned index) { const auto bit=1U< Status detach(IO &io,State &s,unsigned index) { if(!s.begun || index>=3 || !(s.selected&(1U< Status stop(IO &io,State &s) { if(s.updating)return fail(s,Status::Conflict); const auto wells=io.read(0x45404); if(wells==UINT32_MAX || (wells&3)!=3 || (wells&(3U<<16)))return fail(s,Status::Power); + if(s.transcoders.dsi0Pipe<3 && !Display13FBIRQ::consumersStopped(io,1U< Result transfer(IO &io,bool requested,const AlderLakeN::DisplayReadout &boot, + const XELPMemory::Layout &memory,unsigned hdmiMask) { + Result r; + if(!requested)return r; + r.status=Status::Source; + if(hdmiMask!=2 || boot.status!=AlderLakeN::ReadoutStatus::Complete || boot.activePipes || + !(boot.poweredPipes&1) || !memory.valid)return r; + const auto read=[&](uint32_t reg){return io.readable(reg)?io.read(reg):UINT32_MAX;}; + const auto get=[&](uint32_t reg){uint32_t v=UINT32_MAX;AlderLakeN::value(boot,reg,v);return v;}; + const auto conf=read(0x7B008),func=read(0x6B400),plane=get(0x70180),surface=get(0x7019C); + const auto geometry=XELPGeometry::linear(plane,get(0x70188),get(0x70190),get(0x7018C),get(0x701A4)); + const auto power=get(0x45404),ddiPower=get(0x45454); + if(conf!=0xC0000000 || !(func&0x80000000U) || (func&0x7000) || read(0x6B404) || + read(0x7B808)&0xC0000000U || read(0x6B094)!=1 || read(0x6B894) || + (read(0x6B030)&0x303F0000U)!=0x20130000U || read(0x6B034) || + get(0x64000)!=0x80000000U || (get(0x64100)&0x80000080U)!=0x80 || + (power&0x405)!=0x405 || (ddiPower&3)!=3 || + !geometry.valid || geometry.width!=1080 || geometry.height!=1920 || + surface!=get(0x701AC) || (surface&4095) || !memory.range(surface,geometry.bytes,true))return r; + for(unsigned p=0;p<3;++p)if(boot.poweredPipes&(1U<=2 || (mux&(1U<<10)))return r; + r.pll=r.sourcePLL^1; + XELPComboClock::PLL target,source; + if(!XELPComboClock::PLL::resolve(r.pll,target) || !XELPComboClock::PLL::resolve(r.sourcePLL,source) || + read(target.enable)&0xCC000000U || (read(source.enable)&0xCC000000U)!=0xCC000000U)return r; + r.status=Status::PHY; + if(!(read(0x6C100)&0x80000000U))return r; // firmware initialized Combo-B compensation + const uint32_t phyRegs[]={0x6C804,0x6C014,0x6C894,0x6C028}; + for(auto reg:phyRegs)if(read(reg)==UINT32_MAX)return r; + const uint32_t laneWords[]={0x888,0x890,0x89C}; + for(unsigned n=0;n<4;++n)for(auto off:laneWords)if(read(0x6C000+0x100*n+off)==UINT32_MAX)return r; + // Early bootstrap is before WEX's/native PCODE clients exist. Refuse an + // already-busy mailbox; use the shared actual-latency calculation. + r.status=Status::Latency; + const auto latency=Display13WM::queryLatency(io); + const auto wm=Display13WM::plan(1920,2200,148500000,2048,latency,false,false,1080); + if(wm.status!=Display13WM::Status::Complete || !wm.sagv.enable)return r; + if(read(0x7B008)!=conf || read(0x6B400)!=func || read(0x70180)!=plane || + read(0x7019C)!=surface || read(0x45404)!=power || read(0x164280)!=mux)return r; + const auto write=[&](uint32_t reg,uint32_t value){io.write(reg,value);++r.writes;r.reg=reg;r.value=read(reg);}; + const auto wait=[&](uint32_t reg,uint32_t mask,uint32_t expected,unsigned us) { + for(unsigned elapsed=0;elapsed<=us;elapsed+=10) { + r.reg=reg;r.value=read(reg); + if(r.value!=UINT32_MAX && (r.value&mask)==expected)return true; + io.delayUs(10); + } + r.status=Status::Timeout;return false; + }; + const auto frames=[&]() { + const auto first=read(0x70040); + for(unsigned elapsed=0;elapsed<100000;elapsed+=100) { + r.frames=read(0x70040)-first; + if(r.frames>=2 && r.frames<0x10000000)return true; + io.delayUs(100); + } + r.status=Status::Timeout;r.reg=0x70040;return false; + }; + // 148.5 MHz * 5 * 12 = 8.91 GHz. P=2,Q=3,K=2. 38.4MHz + // reference uses 19.2MHz arithmetic and WA_22010492432 halves fraction. + // Firmware may leave B's mux ungated even though its buffer is idle and + // no ordinary transcoder is active. Gate that qualified unused consumer + // before powering/programming the free PLL; A's DSI clock stays intact. + r.stage=1;write(0x164280,mux|(1U<<11)); + if(read(0x164280)!=(mux|(1U<<11))){r.status=Status::Readback;return r;} + write(0x45454,ddiPower|8); + if(!wait(0x45454,12,12,1000))return r; + write(target.enable,0x08000000); + if(!wait(target.enable,0x0C000000,0x0C000000,1000))return r; + write(target.cfg0,0x001001D0);write(target.cfg1,0x00000E84); + write(target.enable,read(target.enable)|0x80000000U); + if(!wait(target.enable,0xCC000000,0xCC000000,1000))return r; + r.stage=2; + write(0x164280,(mux&~(3U<<2))|(r.pll<<2)); // B stays gated while changing source + write(0x164280,read(0x164280)&~(1U<<11)); + write(0x6C604,read(0x6C804)&~(1U<<26)); // HDMI common keeper off + for(unsigned n=0;n<4;++n)write(0x6C890+0x100*n,(read(0x6C890+0x100*n)&~0x80000000U)|(n?0x80000000U:0)); + write(0x6C014,read(0x6C014)|3); + auto tx=read(0x6C894)&~0x80000000U;write(0x6C694,tx); + tx=(tx&~0x601C0038U)|0x20080030U;write(0x6C694,tx); + // Linux default HDMI table entry: swing6, scalar127, cursor53, post1=0, post2=10. + for(unsigned n=0;n<4;++n) { + const auto b=0x6C880+0x100*n; + write(b+8,(read(b+8)&~0xB8FFU)|0x3098); + write(b+16,(read(b+16)&~0x3FFFFU)|(10U<<6)|53U); + write(b+28,(read(b+28)&~0x7F000000U)|0x7F000000U); + } + write(0x6C694,tx|0x80000000U);write(0x6C028,read(0x6C028)&~0xF0U); + // Last non-destructive check before retiring the firmware surface. + if(read(0x7B008)!=conf || read(0x6B400)!=func || !frames())return r; + r.stage=3; + write(0xC8250,read(0xC8250)&~0x80000000U); + write(0x70180,plane&~0x80000000U);write(0x7019C,surface); + if(!frames())return r; + write(0x7B008,conf&~0x80000000U); + if(!wait(0x7B008,0xC0000000,0,50000))return r; + write(0x6B0D8,(read(0x6B0D8)&~(1U<<8))|1); + if(!wait(0x6B0D8,1U<<16,1U<<16,1000))return r; + write(0x6B400,func&~0x80000000U); + write(0x64000,0); + if(!wait(0x64000,0x80000080,0x80,1000))return r; + write(0x164280,read(0x164280)|(1U<<10)); + write(source.enable,read(source.enable)&~0x80000000U); + if(!wait(source.enable,1U<<30,0,1000))return r; + write(source.enable,read(source.enable)&~(1U<<27)); + if(!wait(source.enable,1U<<26,0,1000))return r; + r.stage=4; + write(0x46140,0x20000000); // pipe A clock from actual Combo B + const uint32_t timing[][2]={{0x60000,0x0897077F},{0x60004,0x0897077F},{0x60008,0x080307D7}, + {0x6000C,0x04640437},{0x60010,0x04640437},{0x60014,0x0440043B},{0x60028,0}, + {0x6001C,0x077F0437}}; + for(auto &v:timing)write(v[0],v[1]); + write(0x42084,read(0x42084)|(1U<<30));write(0x70028,read(0x70028)|(1U<<13)); + write(0x70188,120);write(0x7018C,0);write(0x70190,0x0437077F);write(0x701A4,0); + write(0x45270,Display13WM::pipeLineTime(2200,148500000)); + const auto programmed=Display13WM::program(io,wm,false,false,0); + r.writes+=programmed.writes; + if(programmed.status!=Display13WM::Status::Complete){r.status=Status::Readback;return r;} + r.stage=5; + write(0x60404,0);write(0x60400,0x90030000); // B HDMI, 8bpc, +H/+V + write(0x70008,0x80000000); + if(!wait(0x70008,0xC0000000,0xC0000000,50000))return r; + write(0x64100,0x80000000); + if(!wait(0x64100,0x80000080,0x80000000,3000))return r; + write(0x70180,(plane&~(7U<<28))|(1U<<28));write(0x7019C,surface); + if(!frames())return r; + r.stage=6;r.status=Status::Complete;return r; +} +} +#endif diff --git a/WhateverXe/kern_display13_fb_irq.hpp b/WhateverXe/kern_display13_fb_irq.hpp index 198a408a..48be1ac3 100644 --- a/WhateverXe/kern_display13_fb_irq.hpp +++ b/WhateverXe/kern_display13_fb_irq.hpp @@ -4,6 +4,7 @@ #define KERN_DISPLAY13_FB_IRQ_HPP #include #include "kern_display13_power_bridge.hpp" +#include "kern_display13_transcoder.hpp" namespace Display13FBIRQ { // The IRQ simple lock protects this list. Each node lives on its real native // handler's stack until end(). A later master restore cannot grant DE access @@ -46,10 +47,11 @@ constexpr uint32_t DSBInterrupt(unsigned pipe,unsigned dsb) {return 0x70B28+0x10 constexpr uint32_t DSBControl(unsigned pipe,unsigned dsb) {return DSBInterrupt(pipe,dsb)-0x20;} enum class Status : uint32_t {Unavailable,Complete,Power,ForeignIRQ,Topology,Readback,Faulted}; struct State { + Display13Transcoder::Bindings transcoders; bool begun {},enabled {},faulted {}; Status firstFailure {Status::Unavailable}; uint32_t failureRegister {},failureBits {}; - uint32_t excludedPCH {},ports {3}; + uint32_t excludedPCH {},ports {3},typeC {}; uint32_t active {},configured {},priorIER[3] {},priorIMR[3] {},priorPCHIER {},priorPCHIMR {}; uint32_t priorTCEnable {},priorDDIEnable {},priorDSB[3][3] {},lastMaster {},raw[3] {},lastPCH {},lastDDI {},lastTC {},foreign {}; uint64_t interrupts {},vblanks[3] {},flips[3] {},underruns[3] {},dsbErrors[3] {}; @@ -60,13 +62,15 @@ inline bool powered(uint32_t wells,unsigned pipe) { if(wells==UINT32_MAX || pipe>=3 || (wells&3)!=3)return false; return (wells&(1U<<(10+2*pipe))) && (!pipe || (wells&(1U<<2))); } -template ActivePipes readActivePipes(IO &io,uint32_t allowed) { +template ActivePipes readActivePipes(IO &io,uint32_t allowed,const Display13Transcoder::Bindings &transcoders={}) { ActivePipes r;r.allowed=allowed; - if(!allowed || (allowed&~7U)) {r.status=Status::Topology;return r;} + if(!allowed || (allowed&~7U) || !transcoders.valid()) {r.status=Status::Topology;return r;} const auto wells=io.read(Driver);if(wells==UINT32_MAX) {r.status=Status::Readback;return r;} + if(transcoders.dsi0Pipe<3 && !powered(wells,transcoders.dsi0Pipe)) {r.status=Status::Power;return r;} for(unsigned p=0;p<3;++p)if(powered(wells,p)) { - const auto conf=io.read(0x70008+0x1000*p); + const auto conf=Display13Transcoder::control(io,p,transcoders); if(conf==UINT32_MAX) {r.status=Status::Readback;return r;} + if(transcoders.dsi(p) && (conf&0xC0000000U) && (conf&0xC0000000U)!=0xC0000000U) {r.status=Status::Topology;return r;} if(conf&Enable)r.active|=1U<>3)<<24);} +inline uint32_t pchOwned(const State &s) {return ((s.ports&7)<<16)|(((s.ports&~s.typeC)>>3)<<24);} +inline uint32_t northOwned(const State &s) {return (s.typeC>>3)<<16;} +constexpr uint32_t NorthControl=0x44038; +inline uint32_t northControl(const State &s,uint32_t value) { + uint32_t r=0;for(unsigned tc=0;tc<4;++tc)if(s.typeC&(8U< Status enable(IO &io,State &s,uint32_t excludedPCH=0,uint32_t sharedGT=0,uint32_t expectedPipes=1,uint32_t ports=3) { +template Status enable(IO &io,State &s,uint32_t excludedPCH=0,uint32_t sharedGT=0,uint32_t expectedPipes=1,uint32_t ports=3,uint32_t typeC=0) { if(s.faulted)return Status::Faulted; - if(!ports || (ports&~0x7FU) || (s.begun && ports!=s.ports))return fail(io,s,Status::Topology); - s.ports=ports; + const bool dsi=s.transcoders.dsi0Pipe!=UINT32_MAX; + if(!s.transcoders.valid() || (!ports && !dsi) || (dsi && (ports&1)) || (ports&~0x7FU) || (typeC&~(ports&0x78)) || (s.begun && (ports!=s.ports || typeC!=s.typeC)))return fail(io,s,Status::Topology); + if(dsi)excludedPCH|=1U<<16; // HPD-A is the panel reset GPIO, not hotplug. + s.ports=ports;s.typeC=typeC;excludedPCH|=(typeC>>3)<<24; if((excludedPCH&pchOwned(s)) || (excludedPCH&~(PCHTypeC|PCHCombo)) || (sharedGT && sharedGT!=NativeGT) || !expectedPipes || (expectedPipes&~7U))return fail(io,s,Status::Topology); - if(!available(io) || ((ports&0x78) && !io.readable(TCControl)))return Status::Unavailable; + if(!available(io) || (typeC && !io.readable(NorthControl)) || ((ports&0x78) && !io.readable(TCControl)))return Status::Unavailable; // Native repeated enable is idempotent. Do not drop a pending flip/DSB // completion or undo hwSetInterruptForTransaction's VBL mask. if(s.enabled && excludedPCH!=s.excludedPCH)return fail(io,s,Status::Topology); @@ -137,11 +156,13 @@ template Status enable(IO &io,State &s,uint32_t excludedPCH=0,uint32_t if(s.enabled && s.active)return (io.read(Master)&Enable) && (io.read(GfxMaster)&Enable) ? Status::Complete : fail(io,s,Status::Readback); auto wells=io.read(Driver); + if(dsi && !powered(wells,s.transcoders.dsi0Pipe))return fail(io,s,Status::Power); for(unsigned p=0;p<3;++p)if((expectedPipes&(1U< Status enable(IO &io,State &s,uint32_t excludedPCH=0,uint32_t configured|=1U< Status enable(IO &io,State &s,uint32_t excludedPCH=0,uint32_t if((io.read(HPDBank+4)&excludedHPD) || (~io.read(HPDBank-4)&excludedHPD)) return fail(io,s,Status::Readback); } + if(typeC) { + const auto ctl=io.read(NorthControl); + if(ctl==UINT32_MAX)return fail(io,s,Status::Readback); + io.write(NorthControl,(ctl&~0x333333U)|northControl(s,8)|(ctl&northControl(s,3))); + io.write(HPDBank-4,io.read(HPDBank-4)&~northOwned(s)); + io.write(HPDBank+4,io.read(HPDBank+4)|northOwned(s)); + const auto stale=io.read(HPDBank)&northOwned(s);if(stale)io.write(HPDBank,stale); + if((io.read(NorthControl)&northControl(s,8))!=northControl(s,8) || + (io.read(HPDBank+4)&northOwned(s))!=northOwned(s) || (io.read(HPDBank-4)&northOwned(s))) + return fail(io,s,Status::Readback); + } s.excludedPCH=excludedPCH; for(unsigned p=0;p<3;++p)if(configured&(1U< Status enable(IO &io,State &s,uint32_t excludedPCH=0,uint32_t } const auto ddi=io.read(DDIControl); if(ddi==UINT32_MAX)return fail(io,s,Status::Readback); - io.write(DDIControl,(ddi&~0x3333U)|ddiMask(s,8)|(ddi&ddiMask(s,3))); // Clear only owned stale HPD status. + // DSI's HPD-A is an output GPIO: disable its HPD detector, preserve + // OUTPUT_DATA bit2 and leave its W1C status untouched under this IRQ lock. + io.write(DDIControl,(ddi&~(0x3333U|(dsi?8U:0U)))|ddiMask(s,8)|(ddi&ddiMask(s,3))); + if(dsi && (io.read(DDIControl)&12)!=(ddi&4))return fail(io,s,Status::Readback); if(ports&0x78) { const auto tc=io.read(TCControl);s.lastTC=tc;if(tc==UINT32_MAX)return fail(io,s,Status::Readback); io.write(TCControl,(tc&~0x333333U)|tcMask(s,8)|(tc&tcMask(s,3))); @@ -240,9 +273,12 @@ template Status disable(IO &io,State &s) { // stale-status ACK is part of this native activation boundary. template Status extendActivePipes(IO &io,State &s,uint32_t allowed) { if(!s.begun || !s.enabled || s.faulted)return Status::Unavailable; - const auto actual=readActivePipes(io,allowed); + const auto actual=readActivePipes(io,allowed,s.transcoders); if(actual.status!=Status::Complete)return actual.status; - if(s.active&~actual.active)return Status::Topology; + // s.active tracks armed IRQ banks, not currently running transcoders. + // Native display-off may stop a peer while retaining its powered bank. + // Preserve that bank and pending completions until explicit retirement; + // it must not prevent another pipe from being rearmed during wake. const auto master=io.read(Master),gfx=io.read(GfxMaster); if(master==UINT32_MAX || gfx==UINT32_MAX || !(master&Enable) || !(gfx&Enable))return Status::Readback; const auto added=actual.active&~s.active; @@ -293,14 +329,20 @@ inline uint32_t portPowerDomains() { } // PLL bypass transitions retain pending interrupt status. Unlike physical // well release, they only need stopped consumers while the master is masked. -template bool consumersStopped(IO &io,uint32_t pipes) { +template bool consumersStopped(IO &io,uint32_t pipes,const Display13Transcoder::Bindings &transcoders={}) { + if(!transcoders.valid())return false; const auto wells=io.read(Driver); if(wells==UINT32_MAX || (wells&3)!=3 || (wells&(3U<<16)))return false; + if(transcoders.dsi0Pipe<3 && (pipes&(1U< Status parkStoppedPipe(IO &io,State &s,unsigned pipe,unsigned if(master==UINT32_MAX)return Status::Readback; if((master&Enable) || handlers || softwareHandlers || (queued&pipeEvents(pipe)))return Status::Unavailable; if(!powered(io.read(Driver),pipe))return Status::Power; - if(!consumersStopped(io,1U< Status parkStoppedPipe(IO &io,State &s,unsigned pipe,unsigned const auto stale=io.read(iir)&PipeOwned;if(stale)io.write(iir,stale); if((io.read(iir+4)&PipeOwned) || (~io.read(iir-4)&PipeOwned))return fail(io,s,Status::Readback); for(unsigned d=0;d<3;++d)if(io.read(DSBInterrupt(pipe,d))&0xF0000)return fail(io,s,Status::Readback); - if(!consumersStopped(io,1U< Status hotplugOnly(IO &io,State &s,bool preservePipeStatus=fa if(already && (!preservePipeStatus || !s.enabled || s.active))return Status::Unavailable; const auto wells=io.read(Driver); if(wells==UINT32_MAX || (wells&3)!=3)return Status::Power; - if(!consumersStopped(io,7))return Status::Topology; + if(!consumersStopped(io,7,s.transcoders))return Status::Topology; if((io.read(DDIControl)&ddiMask(s,8))!=ddiMask(s,8) || (io.read(0xC400C)&pchOwned(s))!=pchOwned(s) || (io.read(0xC4004)&pchOwned(s)))return Status::Readback; uint32_t ier[3] {},imr[3] {}; @@ -397,7 +439,7 @@ template bool quiescentPipes(IO &io,const State &s,uint32_t domains,un const auto ddi=io.read(0x64000+port*0x100); if(ddi==UINT32_MAX || (ddi&Enable))return false; } - if(!consumersStopped(io,pipes))return false; + if(!consumersStopped(io,pipes,s.transcoders))return false; for(unsigned p=0;p<3;++p)if((pipes&(1U< Status beginPowerRelease(IO &io,State &s,PowerReleaseScope &g if(displayHandlers || softwareHandlers || (queued&events) || auxiliary || (hotplug&hpd))return Status::Unavailable; const auto master=io.read(Master),wells=io.read(Driver); if(master==UINT32_MAX || wells==UINT32_MAX || bool(master&Enable)!=s.enabled)return Status::Readback; - if(!consumersStopped(io,pipes))return Status::Topology; + if(!consumersStopped(io,pipes,s.transcoders))return Status::Topology; // Preflight every bank before parking any. A never-configured bank must // already have all notifications masked; it is not ours to clear. for(unsigned p=0;p<3;++p)if(pipes&(1U< Events collect(IO &io,State &s) { if(status)io.write(DDIControl,(ddi&~0x3333U)|status); const auto owned=sde&pchOwned(s);if(owned)io.write(0xC4008,owned); } - constexpr uint32_t supported=(7U<<16)|(1U<<23)|Enable; + if(master&(1U<<21)) { + const auto status=io.read(HPDBank); + const auto pending=status&io.read(HPDBank+4)&~io.read(HPDBank-4); + if(status==UINT32_MAX || (pending&~northOwned(s))) {e.status=fail(io,s,Status::ForeignIRQ,HPDBank,pending);return e;} + const auto ctl=io.read(NorthControl); + if(ctl==UINT32_MAX){e.status=fail(io,s,Status::Readback);return e;} + e.aux=nativeDPAlt(pending,ctl); + uint32_t ack=0;for(unsigned tc=0;tc<4;++tc)if(pending&(1U<<(16+tc)))ack|=ctl&(3U<<(4*tc)); + if(ack)io.write(NorthControl,(ctl&~0x333333U)|ack); + const auto owned=status&northOwned(s);if(owned)io.write(HPDBank,owned); + } + const uint32_t supported=(7U<<16)|(1U<<23)|(s.typeC?(1U<<21):0U)|Enable; if(master&~supported) {s.foreign|=master&~supported;e.status=fail(io,s,Status::ForeignIRQ);return e;} io.write(Master,Enable);e.status=Status::Complete;return e; } diff --git a/WhateverXe/kern_display13_fb_memory.hpp b/WhateverXe/kern_display13_fb_memory.hpp index c4b9ca44..52c2a0d5 100644 --- a/WhateverXe/kern_display13_fb_memory.hpp +++ b/WhateverXe/kern_display13_fb_memory.hpp @@ -3,6 +3,7 @@ // GGTT entries and is not an accelerator memory manager. #ifndef KERN_DISPLAY13_FB_MEMORY_HPP #define KERN_DISPLAY13_FB_MEMORY_HPP +#include "kern_dsi_readout.hpp" #include #include "kern_xelp_memory_layout.hpp" @@ -100,11 +101,11 @@ template inline T singlePath(const void *framebuffer) { // private object. Pool zero is the donor's pre-existing stolen allocation. inline bool associations(const void *controller, size_t controllerBytes, const void *framebuffer, size_t framebufferBytes, uintptr_t expectedOwner=0, - bool nativeSurface=false) { + bool nativeSurface=false,uintptr_t logicalSurface=0) { if (!controller || !framebuffer || controllerBytes<0x1C20 || framebufferBytes<0x4B90 || field(framebuffer,0x1D0)!=reinterpret_cast(controller) || field(controller,0xC0)!=expectedOwner || field(controller,0xE49) || - field(framebuffer,0x4340) || + field(framebuffer,0x4340)!=logicalSurface || (logicalSurface && !expectedOwner) || (field(framebuffer,0x4348) && (!nativeSurface || !expectedOwner))) return false; const auto index=field(framebuffer,0x1DC),count=field(controller,0xD5C); return count && count<=3 && index SurfaceReadout inspectSurface(IO &io,const void *controller,size_t controllerBytes, - const void *fb,size_t fbBytes,uintptr_t owner,uint64_t reserve,uint64_t gpuLimit,const XELPMemory::Layout &memory) { +// Caller pins the record and native memory/map through the PTE walk. A pending +// record need not be published into FB+4348; its existing pointer is checked +// separately so publication during the inspection is detected. +template SurfaceReadout inspectSurfaceRecord(IO &io,const void *controller,size_t controllerBytes, + const void *fb,size_t fbBytes,uintptr_t owner,uint64_t reserve,uint64_t gpuLimit, + const XELPMemory::Layout &memory,const void *surface,const void *attached,uintptr_t logicalSurface=0) { SurfaceReadout r;r.reserve=reserve; - if(!owner || !associations(controller,controllerBytes,fb,fbBytes,owner,true)) { + if(!owner || !associations(controller,controllerBytes,fb,fbBytes,owner,true,logicalSurface)) { r.status=SurfaceStatus::Association;return r; } - const auto surface=field(fb,0x4348);if(!surface)return r; + if(field(fb,0x4348)!=attached) {r.status=SurfaceStatus::Association;return r;} + if(!surface)return r; uint8_t snapshot[64];memcpy(snapshot,surface,sizeof(snapshot)); r.record=reinterpret_cast(surface);r.map=field(snapshot,0x10); r.memory=field(snapshot,0x18);r.gpu=field(snapshot,0x20); @@ -319,21 +325,55 @@ template SurfaceReadout inspectSurface(IO &io,const void *controller,s } ++r.verifiedPages; } - if(!associations(controller,controllerBytes,fb,fbBytes,owner,true) || - field(fb,0x4348)!=surface || memcmp(snapshot,surface,sizeof(snapshot)) || !io.memoryReady(r.map,r.memory,owner,r.bytes)) { + if(!associations(controller,controllerBytes,fb,fbBytes,owner,true,logicalSurface) || + field(fb,0x4348)!=attached || memcmp(snapshot,surface,sizeof(snapshot)) || !io.memoryReady(r.map,r.memory,owner,r.bytes)) { r.status=SurfaceStatus::StateChanged;return r; } r.status=SurfaceStatus::Complete;return r; } +template SurfaceReadout inspectSurface(IO &io,const void *controller,size_t controllerBytes, + const void *fb,size_t fbBytes,uintptr_t owner,uint64_t reserve,uint64_t gpuLimit,const XELPMemory::Layout &memory, + uintptr_t logicalSurface=0) { + const auto surface=fb && fbBytes>=0x4B90 ? field(fb,0x4348) : nullptr; + return inspectSurfaceRecord(io,controller,controllerBytes,fb,fbBytes,owner,reserve,gpuLimit,memory,surface,surface,logicalSurface); +} +// First DSI handoff: the real FB still owns stolen slot zero while the GPU +// allocator owns a pending record beyond the reserved prefix. This proves +// backing, not display-pipe ownership, serialization, or permission to arm. +template SurfaceReadout inspectPendingSurface(IO &io,const void *controller,size_t controllerBytes, + const void *fb,size_t fbBytes,uintptr_t owner,uint64_t reserve,uint64_t gpuLimit, + const XELPMemory::Layout &memory,const void *pending) { + SurfaceReadout failure;failure.status=SurfaceStatus::Association; + if(!pending || !owner || !associations(controller,controllerBytes,fb,fbBytes,owner) || + allocatedSlot(controller,fb)!=0)return failure; + const auto before=pool(controller); + const auto mask=field(controller,0xDF0); + const auto used=field(controller,0xDF4),count=field(controller,0xDF5); + if(before.state!=1 || before.descriptor || before.base || !before.bytes || (before.bytes&4095) || + before.firstPage || before.pages!=before.bytes/4096 || !memory.valid || + !memory.range(before.base,before.bytes,true) || before.physical!=memory.base+before.base || + uint64_t(before.base)+before.bytes>reserve)return failure; + auto r=inspectSurfaceRecord(io,controller,controllerBytes,fb,fbBytes,owner,reserve,gpuLimit,memory,pending,nullptr); + if(r.status==SurfaceStatus::Complete && (allocatedSlot(controller,fb)!=0 || + field(controller,0xDF0)!=mask || field(controller,0xDF4)!=used || + field(controller,0xDF5)!=count || !same(before,pool(controller))))r.status=SurfaceStatus::StateChanged; + return r; +} template Readout inspect(IO &io,const void *controller,size_t controllerBytes, const void *framebuffer,size_t framebufferBytes, uint64_t apertureBytes,const XELPMemory::Layout &memory,bool bootScanout=true,uintptr_t expectedOwner=0, unsigned expectedDDI=1,const Display13TC::Boot *tc=nullptr, - const AlderLakeN::DisplayReadout *dp=nullptr,bool panel=false) { + const AlderLakeN::DisplayReadout *dp=nullptr,bool panel=false, + const XELP::DSI::Readout *dsi=nullptr) { Readout r; if (!associations(controller,controllerBytes,framebuffer,framebufferBytes,expectedOwner)) { r.status=Status::Association;return r; } + // The first DSI GOP handoff is pipe A/PHY A. A validated description is + // not backing ownership: revalidate the leased source on both sides of + // the PTE walk below. Never combine it with DP/AUX/TC source semantics. + if(dsi && (!bootScanout || expectedDDI || dp || tc || panel || dsi->identity.pipe!=0 || + !XELP::DSI::sourceUnchanged(io,*dsi))) {r.status=Status::DisplayState;return r;} if(bootScanout && (expectedDDI>=7 || (expectedDDI>=3 && (!tc || !tc->valid() || tc->port.ddi!=expectedDDI || tc->pipe!=0 || !(tc->buffer&64) || (!dp && tc->ownershipOptional) || !tc->unchanged(io))))) { @@ -358,8 +398,10 @@ template Readout inspect(IO &io,const void *controller,size_t controll !io.readable(0x800000+8*(p.firstPage+p.pages)-1)) { r.status=Status::PoolRange;return r; } - // Read only the powered boot pipe. Linear XRGB8888 FHD uses byte GGTT - // offsets, with Display 13's DPT disabled. No fence register is required. + // Read only the powered boot pipe. Linear RGB uses byte GGTT offsets. + // Linux intel_fb_modifier_uses_dpt excludes LINEAR even when the global + // DPT-disable bit is clear, as in the DSI GOP. Keep the established ordinary + // output contract; do not toggle the global bit to inspect DSI backing. constexpr uint32_t regs[]={0x45404,0x42084,0x60400,0x70008,0x70180,0x70188, 0x7018C,0x70190,0x701A4,0x7019C,0x701AC}; if(bootScanout)for(auto reg:regs)if(!io.readable(reg))return r; @@ -367,13 +409,16 @@ template Readout inspect(IO &io,const void *controller,size_t controll const auto wells=io.read(0x45404); if(wells==UINT32_MAX || (wells&(bootScanout ? 0x403U : 3U))!=(bootScanout ? 0x403U : 3U)) {r.status=Status::DisplayState;return r;} uint32_t dpt=0,trans=0,conf=0,position=0,offset=0; + const uint32_t transReg=dsi?0x6B400U:0x60400U,confReg=dsi?0x7B008U:0x70008U; if(bootScanout) { - dpt=io.read(0x42084);trans=io.read(0x60400);conf=io.read(0x70008); + dpt=io.read(0x42084);trans=io.read(transReg);conf=io.read(confReg); position=io.read(0x7018C);offset=io.read(0x701A4); r.surface=io.read(0x7019C);r.control=io.read(0x70180);r.stride=io.read(0x70188);r.size=io.read(0x70190); const auto geometry=XELPGeometry::linear(r.control,r.stride,r.size,position,offset); - if (!(dpt&(1U<<30)) || !(trans&(1U<<31)) || ((trans>>27)&15)!=expectedDDI+1 || - (dp ? ((trans>>24)&7)!=2 : ((trans>>24)&7)>1) || ((trans>>20)&7) || (conf&0xC0000000)!=0xC0000000 || + const bool source=dsi ? !(io.read(0x60400)&(1U<<31)) && XELPGeometry::timing(geometry,dsi->htotal,dsi->vtotal) : + (dpt&(1U<<30)) && ((trans>>27)&15)==expectedDDI+1 && + (dp ? ((trans>>24)&7)==2 : ((trans>>24)&7)<=1) && !((trans>>20)&7); + if (dpt==UINT32_MAX || !source || !(trans&(1U<<31)) || (conf&0xC0000000)!=0xC0000000 || !geometry.valid || position || offset || (r.surface&4095) || r.surface!=io.read(0x701AC) || r.surface Readout inspect(IO &io,const void *controller,size_t controll if(finalWells==UINT32_MAX || (finalWells&(bootScanout ? 0x403U : 3U))!=(bootScanout ? 0x403U : 3U) || !same(p,pool(controller)) || !associations(controller,controllerBytes,framebuffer,framebufferBytes,expectedOwner) || field(framebuffer,0x4288)!=0 || !(field(controller,0xDF0)&1) || - (bootScanout && (io.read(0x42084)!=dpt || io.read(0x60400)!=trans || io.read(0x70008)!=conf || + (bootScanout && (io.read(0x42084)!=dpt || io.read(transReg)!=trans || io.read(confReg)!=conf || io.read(0x7018C)!=position || io.read(0x701A4)!=offset || io.read(0x7019C)!=r.surface || io.read(0x701AC)!=r.surface || io.read(0x70180)!=r.control || io.read(0x70188)!=r.stride || io.read(0x70190)!=r.size || (expectedDDI>=3 && !tc->unchanged(io)) || - (dp && !AlderLakeN::dpSourceUnchanged(io,*dp,0,expectedDDI,0,tc,panel))))) { + (dp && !AlderLakeN::dpSourceUnchanged(io,*dp,0,expectedDDI,0,tc,panel)) || + (dsi && (!XELP::DSI::sourceUnchanged(io,*dsi) || (io.read(0x60400)&(1U<<31))))))) { r.status=Status::StateChanged;return r; } r.status=Status::Complete;return r; diff --git a/WhateverXe/kern_display13_fb_sysmem.hpp b/WhateverXe/kern_display13_fb_sysmem.hpp index 8553be79..f7514f08 100644 --- a/WhateverXe/kern_display13_fb_sysmem.hpp +++ b/WhateverXe/kern_display13_fb_sysmem.hpp @@ -39,14 +39,14 @@ template Result install(IO &io,const Display13FBMemory::PoolLayout &la const auto &p=layout.slots[slot]; if(p.state || p.descriptor) {r.status=Status::Layout;return r;} if(!io.ownsWindow(slot,p) || !io.consumersStopped(slot,p)) {r.status=Status::Ownership;return r;} - // Cache the complete physical page list, checking even a late invalid page - // and every prior PTE before exposing any RAM to the display/GPU. + // An unallocated window has no defined PTE contents: firmware may leave + // arbitrary bits here. Ownership comes from the reserved allocator range + // and the empty native slot, not from zero/scratch entries. Preflight all + // physical pages, then initialize only that owned, stopped window. for(unsigned i=0;i Result install(IO &io,const Display13FBMemory::PoolLayout &la if(!io.ownsWindow(slot,p) || !io.consumersStopped(slot,p)) {r.status=Status::Ownership;return r;} uint64_t span=0;const auto physical=io.physical(uint64_t(i)*4096,span); if(physical!=pages[i] || span<4096) {r.status=Status::BackingChanged;return r;} - const auto address=reg(p,i);const auto old=io.readPTE(address);r.observed=old; - if(old && old!=scratchPTE) {r.status=Status::Occupied;return r;} + const auto address=reg(p,i); r.expected=Display13FBGTT::pte(pages[i],0); // A possibly delivered write requires backing retention even when // the posting read fails. The caller owns that reference through error. diff --git a/WhateverXe/kern_display13_fb_topology.hpp b/WhateverXe/kern_display13_fb_topology.hpp index 0ca75370..d005c9b1 100644 --- a/WhateverXe/kern_display13_fb_topology.hpp +++ b/WhateverXe/kern_display13_fb_topology.hpp @@ -15,15 +15,17 @@ inline bool selectedWiring(const Display13OutputWiring &w) { inline bool adlnWiring(const Display13OutputWiring &w) {return selectedWiring(w) && !(w.selected()&~3U);} // DP topology is composable, but runtime admission stays closed until its // complete signal/training/bootstrap operation contract is installed. -inline bool supportedWiring(const Display13OutputWiring &w,bool fixedTC=false) { +inline bool supportedWiring(const Display13OutputWiring &w,bool fixedTC=false,bool dsiAdoption=false) { // Fixed HDMI is composable across the physical Combo A/B and TC1..4. // TC provider capability, legacy mode and clock hooks are checked separately. - return !(w.dp&~0x78U) && w.described() && !(w.selected()&~(fixedTC ? Display13OutputWiring::Physical : 3U)); + // DSI requires explicit GOP adoption selection. Native start separately + // checks its mode, writer, probe, cache and disposal hooks. + return (!w.dsi || dsiAdoption) && !(w.dp&~0x78U) && w.described() && !(w.selected()&~(fixedTC ? Display13OutputWiring::Physical : 3U)); } // A TC route uses the same validated handoff regardless of the GT family. // Keep the established Combo-only ADL-N startup ordering unchanged. inline bool deferredBootPrepare(const Display13OutputWiring &w,bool mobile) { - return w.panel || mobile || (w.selected()&0x78); + return w.internal() || mobile || (w.selected()&0x78); } inline bool multiOutputWiring(const Display13OutputWiring &w) { return selectedWiring(w) && (w.selected()&(w.selected()-1)); @@ -113,8 +115,8 @@ inline uint32_t excludedHotplug(const void *platform,uint32_t bytes,const Displa const auto config=reinterpret_cast(p+0x20+24*d); if(config[0]!=d || (!(w.selected()&(1U<>3)<<24)) | - ((7U & ~w.selected())<<16); + return (0x3F000000 & ~((w.hotplug()>>3)<<24)) | + ((7U & ~w.hotplug())<<16); } } #endif diff --git a/WhateverXe/kern_display13_firmware.hpp b/WhateverXe/kern_display13_firmware.hpp index 260dfd45..db6f5b1a 100644 --- a/WhateverXe/kern_display13_firmware.hpp +++ b/WhateverXe/kern_display13_firmware.hpp @@ -20,8 +20,8 @@ namespace XELP { namespace VBT { } return w; } - // Display13 VBT AUX F..I maps directly to TC1..4. Capabilities do not - // select live DP-alt: only the provider's legacy HPD/mode proof admits it. + // Display13 VBT AUX F..I maps directly to TC1..4. DP-only USB-C records + // describe a possible DP-alt route, not a live connection or ownership. inline Display13OutputWiring display13Fixed(const Ports &ports,uint32_t backend,Family family=Family::AlderLakeN) { auto w=display13HDMI(ports,backend); if(!w.valid || !ports.valid())return w; @@ -29,12 +29,13 @@ namespace XELP { namespace VBT { w.panel=1U<=Ports)return Transport::None; + const auto bit=1U<=Ports)return false; if(hdmi&(1U<(platform);if(p[0x12]!=Ports)return false; - for(unsigned d=0;d(p+0x20+24*d); const uint32_t panel[]={0,0,0,0,2,0x18}; if(!d && !memcmp(c,panel,24))continue; @@ -69,7 +81,7 @@ struct Display13OutputWiring { c[1]=ddc[d];c[2]=2;c[4]=0x800;c[5]=panel?0x202:(c[5]&~9U)|2U; } if(!TGLComboDP::apply(dp&3,platform,bytes))return false; - for(unsigned d=0;d<2;++d)if(panel&(1U<>12)&15U)<>b)&1;return n;} + bool valid() const { + return (mask==1 || mask==2 || mask==4 || mask==8 || mask==3 || mask==12 || mask==15) && pin>=1 && pin<=6; + } + template static Lanes capture(IO &io,const Port &p) { + if(!io.readable(p.laneAssignment()) || !io.readable(p.pinAssignment()) || + !io.readable(0xc4000) || !io.readable(0x44470) || !io.readable(p.ready))return {}; + const auto lanes=io.read(p.laneAssignment()),pins=io.read(p.pinAssignment()); + if(lanes==UINT32_MAX || pins==UINT32_MAX || !Port::modeMatches(io.read(p.ready),3) || + !p.altHPD(io.read(0xc4000),io.read(0x44470)))return {}; + Lanes r{(lanes>>(8*(p.tc&1)))&15,(pins>>(4*(p.tc&1)))&15}; + return r.valid()?r:Lanes{}; + } + template bool unchanged(IO &io,const Port &p) const { + const auto now=capture(io,p);return valid() && now.mask==mask && now.pin==pin; + } +}; // Read-only physical certificate for an inherited fixed link. This never takes // ownership or programs the PLL. Callers must select the physical Display13 // backend before reading TCSS; TGL uses a different ownership mechanism. @@ -77,14 +107,14 @@ struct Boot { struct Ownership { uintptr_t owner {},identity {};Port port;bool active {}; Ownership()=default;Ownership(const Ownership&)=delete;Ownership& operator=(const Ownership&)=delete; - template Result acquire(IO &io,uintptr_t who,uintptr_t object,unsigned ddi,bool adopt=false) { + template Result acquire(IO &io,uintptr_t who,uintptr_t object,unsigned ddi,bool adopt=false,unsigned mode=4) { Result r;Port p;if(!who || !object || !Port::resolve(ddi,p))return r; if(owner && (owner!=who || identity!=object || port.ddi!=ddi)){r.status=Status::InUse;return r;} if(!io.powerHeld(who,object,ddi)){r.status=Status::NoPower;return r;} if(!io.readable(p.ready) || !io.readable(p.buffer)){r.status=Status::Unavailable;return r;} const auto initialReady=io.read(p.ready); if(initialReady==UINT32_MAX){r.status=Status::Unavailable;return r;} - if(initialReady&3){r.status=Status::UnsupportedMode;return r;} + if(!Port::modeMatches(initialReady,mode)){r.status=Status::UnsupportedMode;return r;} r.before=io.read(p.buffer);if(r.before==UINT32_MAX){r.status=Status::Unavailable;return r;} if(owner && active && !(r.before&64)) {r.status=Status::Readback;return r;} if(!owner && ((r.before&0x80000040U)!=0) && (!adopt || !io.inheritedVerified(who,object,ddi))) { @@ -102,7 +132,7 @@ struct Ownership { for(;;) { const auto ready=io.read(p.ready); if(ready==UINT32_MAX){r.status=Status::Unavailable;return r;} - if(ready&3){r.status=Status::UnsupportedMode;return r;} + if(!Port::modeMatches(ready,mode)){r.status=Status::UnsupportedMode;return r;} if(ready&4)break; if(r.elapsedUs==500000){r.status=Status::NotReady;return r;} io.delayUs(100);r.elapsedUs+=100; diff --git a/WhateverXe/kern_display13_tc_clock.hpp b/WhateverXe/kern_display13_tc_clock.hpp index d97c8e04..278decc3 100644 --- a/WhateverXe/kern_display13_tc_clock.hpp +++ b/WhateverXe/kern_display13_tc_clock.hpp @@ -114,14 +114,24 @@ struct Clock { static uint32_t gateBit(unsigned tc) {return tc<3?1U<<(12+tc):tc==3?1U<<21:0;} // Runtime IO validates the immutable owner/config/path bindings. A ready // register alone never authorizes touching an active/foreign clock. - template static bool preflight(IO &io,const Port &p) { + template static bool accessible(IO &io,const Port &p) { Port checked; if(!Port::resolve(p.ddi,checked) || p.pll!=checked.pll || p.clock!=checked.clock || p.ready!=checked.ready || p.buffer!=checked.buffer || p.tc!=checked.tc || p.phy!=checked.phy || p.hip!=checked.hip || p.shift!=checked.shift || - !io.powerHeld() || !io.owned(p.ddi) || !io.quiescent(p.ddi))return false; - const uint32_t registers[]={p.pll,p.clock,p.ready,p.buffer,Gate}; + !io.powerHeld() || !io.quiescent(p.ddi))return false; + const uint32_t registers[]={p.pll,p.clock,p.buffer,Gate}; for(auto reg:registers)if(!io.readable(reg) || io.read(reg)==UINT32_MAX)return false; - return (io.read(p.ready)&7)==4 && (io.read(p.buffer)&0x80000040)==64; + return true; + } + template static bool preflight(IO &io,const Port &p) { + return accessible(io,p) && io.owned(p.ddi) && io.readable(p.ready) && io.read(p.ready)!=UINT32_MAX && + (io.read(p.ready)&6)==4 && (io.read(p.buffer)&0x80000040)==64; + } + // HPD, pin assignment and TCSS mode may disappear before native teardown. + // Only stop our retained, powered hardware after buffer/scanout quiescence. + // Never use this certificate to start a PLL or program the PHY. + template static bool stopPreflight(IO &io,const Port &p) { + return accessible(io,p) && io.retainedOwned(p.ddi) && (io.read(p.buffer)&0x800000c0U)==0xc0; } template static bool rmw(IO &io,uint32_t reg,uint32_t mask,uint32_t value) { const auto irq=io.lock();const auto old=io.read(reg); @@ -136,12 +146,16 @@ struct Clock { if(elapsed==1000)return false;io.delayUs(10); } } - template static bool disable(IO &io,const Port &p) { - if(!preflight(io,p))return false; +private: + template static bool disableRegisters(IO &io,const Port &p) { return rmw(io,Gate,gateBit(p.tc),gateBit(p.tc)) && rmw(io,p.clock,0xf0000000,0) && rmw(io,p.pll,Enable,0) && wait(io,p.pll,Locked,0) && rmw(io,p.pll,Power,0) && wait(io,p.pll,Powered,0); } +public: + template static bool disable(IO &io,const Port &p) { + return preflight(io,p) && disableRegisters(io,p); + } template static bool enable(IO &io,const Port &p,const TMDSClock &plan) { if(!plan.valid() || !preflight(io,p))return false; // No reprogramming of running PLLs. Preserve an enabled GOP until @@ -178,7 +192,8 @@ struct Clock { step(LinkStage::Complete);return true; } template static bool stopLink(IO &io,const Port &p) { - return io.unstarted() || (preflight(io,p) && io.linkPower(false) && disable(io,p)); + return io.unstarted() || (stopPreflight(io,p) && io.linkPower(false) && + stopPreflight(io,p) && disableRegisters(io,p)); } }; diff --git a/WhateverXe/kern_display13_tc_dp.hpp b/WhateverXe/kern_display13_tc_dp.hpp index 228fecad..57194981 100644 --- a/WhateverXe/kern_display13_tc_dp.hpp +++ b/WhateverXe/kern_display13_tc_dp.hpp @@ -3,6 +3,7 @@ #ifndef KERN_DISPLAY13_TC_DP_HPP #define KERN_DISPLAY13_TC_DP_HPP #include "kern_display13_tc_clock.hpp" +#include "kern_tgl_port_map.hpp" namespace Display13TC { struct DPClock : TMDSClock { static DPClock calculate(uint32_t linkRate,uint32_t ref) { @@ -50,6 +51,34 @@ struct DPRetainedLink { } }; struct DPSignal { + // ADL-P/N DDI_BUF_PHY_LINK_RATE, Linux ddi_buf_phy_link_rate(). + // The TGL donor's bits15:8 do not select this Display13 PHY rate. + static bool bufferRate(uint32_t rate,uint32_t &buffer) { + unsigned code; + switch(rate) {case 6:code=0;break;case 10:code=1;break;case 20:code=2;break;case 30:code=3;break;default:return false;} + buffer=(buffer&~0x700000U)|(code<<20);return true; + } + // ADL-P/N, Linux adlp_tbt_to_dp_alt_switch_wa: release both PCS cores + // after selecting DP training and before enabling the DDI buffer. + // This is independent of the current USB-C orientation and pin mask. + template static bool prepareBuffer(IO &io,const Port &p,uint32_t before[2],uint32_t after[2]) { + if(!io.powerHeld() || !io.linkHeld() || !io.owned(p.ddi) || !io.readable(p.buffer))return false; + const auto buffer=io.read(p.buffer);if(buffer==UINT32_MAX)return false; + for(unsigned group=0;group<2;++group) { + if(!PHY::access(io,p,group*0x1000+0x14,UINT32_MAX,before[group],false))return false; + after[group]=before[group]; + } + if(!((before[0]|before[1])&0x800))return true; + // A retained live link must already have released reset. Do not + // alter PCS underneath an enabled or not-yet-idle transmitter. + if((buffer&0x80000080U)!=0x80)return false; + for(unsigned group=0;group<2;++group)if(before[group]&0x800) { + uint32_t value=0; + if(!PHY::access(io,p,group*0x1000+0x14,0x800,value,true))return false; + after[group]=value; + } + return true; + } static bool coefficients(uint32_t rate,uint32_t swing,uint32_t emphasis,uint32_t &value) { if((rate!=6 && rate!=10 && rate!=20 && rate!=30) || swing>3 || emphasis>3 || swing+emphasis>3)return false; constexpr uint8_t starts[]={0,4,7,9},voltage[]={7,5,2,0,5,2,0,2,0,0}; @@ -57,41 +86,48 @@ struct DPSignal { const auto index=starts[swing]+emphasis; value=voltage[index]|(uint32_t(rate<=10?low[index]:high[index])<<8);return true; } - template static bool mode(IO &io,const Port &p,unsigned lanes) { + template static bool mode(IO &io,const Port &p,unsigned lanes,unsigned pin=0) { if(!io.powerHeld() || !io.owned(p.ddi) || !io.quiescent(p.ddi) || (lanes!=1 && lanes!=2 && lanes!=4))return false; + // The pin-to-X1/X2 calculation is shared with Display12. Only the + // powered register access and ownership adapter differ on Display13. + uint32_t modes[2];if(!TGLDklDPMode::select(pin,lanes,modes))return false; for(unsigned group=0;group<2;++group) { - uint32_t value=lanes==1?(group?0x40U:0U):0x80U; + uint32_t value=modes[group]; if(!PHY::access(io,p,0x1000*group+0xa0,0xc0,value,true))return false; } return true; } // The donor negotiates a common swing/emphasis for the active lanes. // Training can change the electrical level while the DDI buffer is on. - template static bool program(IO &io,const Port &p,unsigned rate,unsigned swing,unsigned emphasis) { + template static bool program(IO &io,const Port &p,unsigned rate,unsigned swing,unsigned emphasis,unsigned physical=15) { uint32_t selected; - if(!io.powerHeld() || !io.owned(p.ddi) || !coefficients(rate,swing,emphasis,selected))return false; + if(!physical || physical>15 || !io.powerHeld() || !io.owned(p.ddi) || !coefficients(rate,swing,emphasis,selected))return false; for(unsigned group=0;group<2;++group) { + if(!(physical&(3U<<(2*group))))continue; const auto bank=0x1000*group;uint32_t zero=0; if(!PHY::access(io,p,bank+0xd00,UINT32_MAX,zero,true))return false; for(unsigned tx=0;tx<2;++tx) { + if(!(physical&(1U<<(2*group+tx))))continue; auto value=selected; if(!PHY::access(io,p,bank+0x2c0+4*tx,0x3ff07,value,true))return false; } - zero=0;if(!PHY::access(io,p,bank+0x2c8,0x7c,zero,true))return false; + const auto loadMask=4U|((physical&(1U<<(2*group)))?0x18U:0U)|((physical&(2U<<(2*group)))?0x60U:0U); + zero=0;if(!PHY::access(io,p,bank+0x2c8,loadMask,zero,true))return false; } return true; } - template static bool startLink(IO &io,const Port &p,const DPClock &plan,unsigned lanes,Clock::LinkStage *stage=nullptr) { + template static bool startLink(IO &io,const Port &p,const DPClock &plan,unsigned lanes,Clock::LinkStage *stage=nullptr,unsigned pin=0,unsigned physical=15) { auto step=[&](Clock::LinkStage s){if(stage)*stage=s;}; step(Clock::LinkStage::Preflight); - if(!plan.valid() || (lanes!=1 && lanes!=2 && lanes!=4) || !Clock::preflight(io,p))return false; + unsigned available=0;for(unsigned bit=0;bit<4;++bit)available+=(physical>>bit)&1; + if(!physical || physical>15 || available6 || !plan.valid() || (lanes!=1 && lanes!=2 && lanes!=4) || !Clock::preflight(io,p))return false; step(Clock::LinkStage::ReleaseDDI);if(!io.linkPower(false))return false; step(Clock::LinkStage::StopPLL);if(!Clock::disable(io,p))return false; step(Clock::LinkStage::Lanes);if(!Clock::lanes(io,p,lanes))return false; step(Clock::LinkStage::PLL);if(!Clock::enable(io,p,plan))return false; step(Clock::LinkStage::DDI);if(!io.linkPower(true))return false; step(Clock::LinkStage::Signal); - if(!mode(io,p,lanes) || !program(io,p,plan.rate/27000,0,0))return false; + if(!mode(io,p,lanes,pin) || !program(io,p,plan.rate/27000,0,0,physical))return false; step(Clock::LinkStage::Complete);return true; } }; diff --git a/WhateverXe/kern_display13_tc_mode.hpp b/WhateverXe/kern_display13_tc_mode.hpp index 1b181f88..3b7b1fa0 100644 --- a/WhateverXe/kern_display13_tc_mode.hpp +++ b/WhateverXe/kern_display13_tc_mode.hpp @@ -1,4 +1,4 @@ -// Fixed legacy TC lifetime. The host serializes native clients and proves +// Legacy/DP-alt TC lifetime. The host serializes native clients and proves // firmware adoption/quiescence; this core never executes the TGL FIA setter. #ifndef KERN_DISPLAY13_TC_MODE_HPP #define KERN_DISPLAY13_TC_MODE_HPP @@ -14,6 +14,7 @@ struct Mode { uintptr_t controller {},object {};unsigned ddi {}; Phase phase {Phase::Idle};bool disconnectPending {}; Ownership ownership; + unsigned selectedMode {};Lanes lanes; // ADLP: register/lane access -> ownership -> AUX cold block. // DDI IO belongs to the clock lifecycle, after the PLL is running. // An already-powered firmware link is not evidence that these scopes can @@ -25,31 +26,36 @@ struct Mode { Port p;return Port::resolve(d,p) ? Display13Power::ddiDomain(d)| Display13Power::auxDomain(d)|Display13Power::bit(Display13Power::Domain::PW2) : 0; } - template Change connect(IO &io,uintptr_t owner,uintptr_t identity,unsigned d) { + template Change connect(IO &io,uintptr_t owner,uintptr_t identity,unsigned d,unsigned wanted=4) { Change r; - if(!owner || !identity || !domains(d))return r; + if(!owner || !identity || !domains(d) || (wanted!=3 && wanted!=4))return r; if(controller && (owner!=controller || identity!=object || d!=ddi)){r.status=Status::InUse;return r;} if(!io.admissionReady()){r.status=Status::NotReady;return r;} if(phase==Phase::Cleanup){r.status=Status::InUse;return r;} if(phase==Phase::Active) { - if(io.nativeMode()!=4){r.status=Status::Readback;return r;} - r.phy=ownership.acquire(io,owner,identity,d); + if(wanted!=selectedMode || io.nativeMode()!=selectedMode || + (wanted==3 && !lanes.unchanged(io,ownership.port))){r.status=Status::Readback;return r;} + r.phy=ownership.acquire(io,owner,identity,d,false,wanted); r.status=r.phy.status;if(r.status==Status::Complete)disconnectPending=false; return r; } if(io.nativeMode()!=0){r.status=Status::NeedsAdoption;return r;} // Keep failed acquisitions bound, including their exact power/PHY // cleanup obligations. Never replace them with another native object. - controller=owner;object=identity;ddi=d;phase=Phase::Cleanup; + controller=owner;object=identity;ddi=d;selectedMode=wanted;phase=Phase::Cleanup; r.power=io.acquireDomains(accessLease,accessDomains()); if(r.power.status!=Display13Power::Status::Complete){r.status=Status::NoPower;return r;} const bool firmware=io.inheritedVerified(owner,identity,d); - r.phy=ownership.acquire(io,owner,identity,d,firmware); + r.phy=ownership.acquire(io,owner,identity,d,firmware,wanted); if(r.phy.status!=Status::Complete){r.status=r.phy.status;return r;} r.power=io.acquireDomains(auxLease,Display13Power::auxDomain(d)); if(r.power.status!=Display13Power::Status::Complete){r.status=Status::NoPower;return r;} - r.native=io.nativeSetMode(4); - if(r.native || io.nativeMode()!=4){r.status=Status::Readback;return r;} + if(wanted==3) { + lanes=Lanes::capture(io,ownership.port); + if(!lanes.valid()){r.status=Status::NotReady;return r;} + } + r.native=io.nativeSetMode(wanted); + if(r.native || io.nativeMode()!=wanted){r.status=Status::Readback;return r;} phase=Phase::Active;disconnectPending=false;r.status=Status::Complete;return r; } template Change disconnect(IO &io,uintptr_t owner,uintptr_t identity,unsigned d) { @@ -76,7 +82,7 @@ struct Mode { r.power=io.releaseDomains(accessLease); if(r.power.status!=Display13Power::Status::Complete){r.status=Status::NoPower;return r;} } - controller=object=0;ddi=0;phase=Phase::Idle;disconnectPending=false;r.status=Status::Complete;return r; + controller=object=0;ddi=selectedMode=0;lanes={};phase=Phase::Idle;disconnectPending=false;r.status=Status::Complete;return r; } }; } diff --git a/WhateverXe/kern_display13_tc_native.hpp b/WhateverXe/kern_display13_tc_native.hpp index 2fad5123..fcf28889 100644 --- a/WhateverXe/kern_display13_tc_native.hpp +++ b/WhateverXe/kern_display13_tc_native.hpp @@ -26,6 +26,12 @@ static constexpr Entry entries[]={ {"__ZN14AppleIntelPort15writeRegister32Ejj",0x1fea8,{0x55,0x48,0x89,0xe5,0x48,0xff,0x5,0x9d,0x73,0xb,0x0,0x48,0x8b,0xbf,0x78,0x5,0x0,0x0,0x89,0xf6}}, }; +// Nonzero base Mode skips the reset callback and records only software mode. +// DSI uses mode1 through this exact branch, never the hardware HAL setter. +static constexpr uint32_t baseModeBranchOffset=0x1b4db; +static constexpr uint8_t baseModeBranch[]={0x85,0xf6,0x75,0x2e}; +static constexpr uint32_t baseModeStoreOffset=0x1b50d; +static constexpr uint8_t baseModeStore[]={0x41,0x89,0x9e,0x50,0x05,0x00,0x00,0x31,0xc0,0x5b,0x41,0x5e,0x5d,0xc3}; // Native clock replacements retain these DDI reference boundaries. // Boot Mode is acquired in PowerWell::init: native ports must already exist. // Full modesets stop the inherited link before LightUpDP programs its PLL. diff --git a/WhateverXe/kern_display13_transcoder.hpp b/WhateverXe/kern_display13_transcoder.hpp new file mode 100644 index 00000000..ba8980e5 --- /dev/null +++ b/WhateverXe/kern_display13_transcoder.hpp @@ -0,0 +1,39 @@ +// Display pipe and timing transcoder are independent identities. The owner +// installs this mapping only after DSI readout/resource qualification, and +// changes it only while affected callers/IRQs are quiesced. Default is ordinary +// A/B/C timing, with no DSI MMIO reads. This is not an MMIO address translator. +#ifndef KERN_DISPLAY13_TRANSCODER_HPP +#define KERN_DISPLAY13_TRANSCODER_HPP +#include +namespace Display13Transcoder { +struct Bindings { + uint32_t dsi0Pipe {UINT32_MAX}; + bool valid() const {return dsi0Pipe==UINT32_MAX || dsi0Pipe<3;} + bool dsi(unsigned pipe) const {return pipe<3 && pipe==dsi0Pipe;} + uint32_t control(unsigned pipe) const {return dsi(pipe)?0x7B008U:0x70008U+0x1000*pipe;} + uint32_t function(unsigned pipe) const {return dsi(pipe)?0x6B400U:0x60400U+0x1000*pipe;} + uint32_t timing(unsigned pipe) const {return dsi(pipe)?0x6B000U:0x60000U+0x1000*pipe;} +}; +// Caller proves the pipe and DSI bank's power lifetime. Normal PIPECONF must +// remain disabled for an owned DSI route. Check the live input selector as +// well as the binding: a stale CPU mapping must not qualify another pipe. +// A partial enable/active pair is returned intact for stop/wait consumers; +// callers requiring running scanout must require both bits. +template uint32_t control(IO &io,unsigned pipe,const Bindings &bindings={}) { + if(pipe>=3 || !bindings.valid())return UINT32_MAX; + const auto normal=io.read(0x70008+0x1000*pipe); + if(!bindings.dsi(pipe) || normal==UINT32_MAX)return normal; + if(normal&0xC0000000U)return UINT32_MAX; + const auto conf=io.read(0x7B008),function=io.read(0x6B400); + const uint32_t inputs[]={0,5,6}; + if(conf==UINT32_MAX || function==UINT32_MAX || ((function>>12)&7)!=inputs[pipe] || + ((conf&0xC0000000U) && !(function&0x80000000U)))return UINT32_MAX; + return conf; +} +template bool running(IO &io,unsigned pipe,const Bindings &bindings={}) { + const auto conf=control(io,pipe,bindings); + const auto mask=bindings.dsi(pipe)?0xC0000000U:0x80000000U; + return conf!=UINT32_MAX && (conf&mask)==mask; +} +} +#endif diff --git a/WhateverXe/kern_display13_wm.hpp b/WhateverXe/kern_display13_wm.hpp index abbe3772..b4cc4584 100644 --- a/WhateverXe/kern_display13_wm.hpp +++ b/WhateverXe/kern_display13_wm.hpp @@ -155,8 +155,11 @@ inline uint32_t cursorWidth(uint32_t control) { default:return 0; } } +inline bool cursorWidthSupported(uint32_t width) { + return width==64 || width==128 || width==256; +} inline bool cursorMatches(uint32_t control,uint32_t width,bool allowDisabled=false) { - if(control==UINT32_MAX || (width!=64 && width!=128 && width!=256))return false; + if(control==UINT32_MAX || !cursorWidthSupported(width))return false; return (!(control&0x27) && allowDisabled) || cursorWidth(control)==width; } inline Level yTiledLevel(uint32_t width,uint32_t htotal,uint64_t pixelHz,uint32_t latencyUs,uint32_t allocation) { @@ -240,6 +243,7 @@ struct Snapshot { uint32_t pipeLineTime {}; uint64_t pipePixelHz {}; unsigned pipeIndex {}; // Display pipe, not physical HDMI DDI or framebuffer index. + Display13Transcoder::Bindings transcoders; }; // FlushTriggerPackets writes these native packets in append order. A flip // queued after WM enable may supersede the surface sampled at enable time. @@ -289,7 +293,7 @@ inline bool clockFits(const Snapshot &s,uint64_t nativeHz) { (!s.cursorActive || (s.cursor && s.cursor->status==Status::Complete && s.cursor->pixelHz && s.cursor->pixelHz<=nativeHz/2)); } -enum class LatchCheck : uint32_t {None,Controls,OtherPlane,DSB,Clock,Registers,Live,Frame,AwaitingScanout}; +enum class LatchCheck : uint32_t {None,Controls,OtherPlane,DSB,Clock,Registers,Live,Frame,AwaitingScanout,AwaitingFlip}; struct Latch { Status status {Status::Unavailable}; uint32_t frameBefore {},frameAfter {},waitUs {},budgetUs {},lastReg {},expected {},observed {}; @@ -321,17 +325,18 @@ template bool verifyWM(IO &io,Latch &r,const Plan &p,bool cursor,bool // nor the workloop, so hardware can advance while delayUs sleeps. template Latch latch(IO &io,const Snapshot &s) { Latch r; - if(s.pipeIndex>=3) {r.status=Status::Input;return r;} + if(s.pipeIndex>=3 || !s.transcoders.valid()) {r.status=Status::Input;return r;} const uint32_t bank=s.pipeIndex*0x1000; + const auto timing=s.transcoders.timing(s.pipeIndex); if(!s.primary || s.primary->status!=Status::Complete || !s.primary->normal[0].enable || !s.primary->sagv.enable || !clockFits(s,s.nativeClockHz) || !s.pipeLineTime || s.pipeLineTime>511 || (s.cursorActive && (!s.cursor || s.cursor->status!=Status::Complete || !s.cursor->sagv.enable)))return r; - const uint32_t required[] {0x45404,0x70008+bank,0x70180+bank,0x70080+bank,0x70190+bank,0x60000+bank,0x6000C+bank,0x70040+bank}; + const uint32_t required[] {0x45404,0x70008+bank,0x70180+bank,0x70080+bank,0x70190+bank,timing,timing+0xC,0x70040+bank}; for(auto reg:required) if(!io.readable(reg))return r; const auto wells=io.read(0x45404); if(!Display13FBIRQ::powered(wells,s.pipeIndex)) {r.status=Status::Power;return r;} - const auto ht=io.read(0x60000+bank),vt=io.read(0x6000C+bank); + const auto ht=io.read(timing),vt=io.read(timing+0xC); const auto htotal=((ht>>16)&0x3FFF)+1,vtotal=((vt>>16)&0x3FFF)+1; if(ht==UINT32_MAX || vt==UINT32_MAX || htotal!=s.primary->htotal || !s.primary->height || s.primary->height>8192 || vtotalheight || !s.primary->pixelHz) {r.status=Status::Input;return r;} const auto period=ceilDiv(uint64_t(htotal)*vtotal*1000000,s.pipePixelHz); @@ -343,14 +348,15 @@ template Latch latch(IO &io,const Snapshot &s) { for(;;) { const auto power=io.read(0x45404); if(!Display13FBIRQ::powered(power,s.pipeIndex)) {r.status=Status::Power;return r;} - const auto pipe=io.read(0x70008+bank),plane=io.read(0x70180+bank),cursor=io.read(0x70080+bank); + const auto pipe=Display13Transcoder::control(io,s.pipeIndex,s.transcoders),plane=io.read(0x70180+bank),cursor=io.read(0x70080+bank); r.check=LatchCheck::Controls;r.pipeControl=pipe;r.planeControl=plane;r.cursorControl=cursor; - // Native enablePlane may return before any scanout is attached, while - // programPipeScaler still flushes a zero SURF trigger via armUpdate. - // A disabled primary with no intended surface is not a live latch to - // prove yet. Keep WM/DDB pending; never retire storage or certify the - // shared clock until a later real plane/surface update passes below. - const bool preparing=plane!=UINT32_MAX && !(plane&Enabled) && !s.primarySurface && !cursor && !s.cursorActive; + // Native enablePlane/scaler can flush WM before enabling scanout. + // Reconnect may retain a nonzero SURF from the previous path, so a + // surface address alone cannot distinguish this from initial setup. + // A disabled primary has no live latch to prove, even if the cursor + // is already active. Keep WM/DDB pending and the shared clock + // unqualified until a later enabled plane passes the full proof. + const bool preparing=plane!=UINT32_MAX && !(plane&Enabled); if(pipe==UINT32_MAX || plane==UINT32_MAX || cursor==UINT32_MAX || (pipe&0xC0000000)!=0xC0000000 || (!preparing && (!(plane&Enabled) || !supportedRGBControl(plane) || (((plane>>10)&7)==4)!=s.primary->yTiled)) || bool(cursor&0x27)!=s.cursorActive || (s.cursorActive && !cursorMatches(cursor,s.cursor->width))) {r.status=Status::Conflict;return r;} @@ -373,7 +379,7 @@ template Latch latch(IO &io,const Snapshot &s) { if(r.clockHz!=s.nativeClockHz) {r.status=Status::Conflict;return r;} if(preparing) {r.check=LatchCheck::AwaitingScanout;return r;} r.check=LatchCheck::Registers; - if(!compare(io,r,0x60000+bank,ht) || !compare(io,r,0x6000C+bank,vt) || !compare(io,r,0x45270+4*s.pipeIndex,s.pipeLineTime) || + if(!compare(io,r,timing,ht) || !compare(io,r,timing+0xC,vt) || !compare(io,r,0x45270+4*s.pipeIndex,s.pipeLineTime) || !compare(io,r,0x70190+bank,((s.primary->height-1)<<16)|(s.primary->width-1)) || !compare(io,r,0x7027C+bank,s.primaryDDB) || !compare(io,r,0x7017C+bank,s.cursorDDB) || !compare(io,r,0x45004,s.ipc) || !compare(io,r,0x7019C+bank,s.primarySurface) || @@ -389,12 +395,20 @@ template Latch latch(IO &io,const Snapshot &s) { // readback catches up (observed for a native direct cursor swap). // Keep every programmed/control/clock check on each poll; qualify // retirement only after both actual live surfaces match. A stable - // mismatch at the existing deadline remains a Readback failure. + // mismatch at the existing deadline leaves retirement pending. + // A later native flush may prove it; do not fault the owner or + // infer success from the programmed surface. WM/DDB resources + // and clock qualification remain held until a complete proof. r.check=LatchCheck::Live; const bool live=compare(io,r,0x701AC+bank,s.primarySurface) && (!s.cursorActive || compare(io,r,0x700AC+bank,s.cursorSurface)); if(live) {r.status=Status::Complete;return r;} - if(r.waitUs>=r.budgetUs)return r; + if(r.waitUs>=r.budgetUs) { + if(r.status==Status::Readback && r.observed!=UINT32_MAX) { + r.status=Status::Unavailable;r.check=LatchCheck::AwaitingFlip; + } + return r; + } } if(r.waitUs>=r.budgetUs) {r.status=Status::CommitTimeout;return r;} io.delayUs(1000); diff --git a/WhateverXe/kern_dsi_adapter_backing.inc b/WhateverXe/kern_dsi_adapter_backing.inc new file mode 100644 index 00000000..c0817c53 --- /dev/null +++ b/WhateverXe/kern_dsi_adapter_backing.inc @@ -0,0 +1,563 @@ +// Native scanout allocation, ownership, publication and surface handoff. +// Included only by kern_xe.cpp; retains its private owners and lock ordering. + +bool XE::processDSIBackingHooks(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { + const char *symbols[]={"__ZN18IGAccelDisplayPipe21allocateScanoutMemoryEP19IntelScaledModeDataj", + "__ZN18IGAccelDisplayPipe23deallocateScanoutMemoryEP19IntelScaledModeDataj"}; + const uintptr_t offsets[]={0x7E5B2,0x7E772}; + const uint8_t entries[][16]={ + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC}, + {0x55,0x48,0x89,0xE5,0x41,0x56,0x53,0x8B,0x46,0x38,0x85,0xC0,0x74,0x62,0x49,0x89}}; + bool known=size>0x7F80A; + for(unsigned i=0;i<2;++i) { + const auto fn=patcher.solveSymbol(index,symbols[i],address,size);patcher.clearError(); + known=known && fn==address+offsets[i] && !memcmp(reinterpret_cast(fn),entries[i],16); + } + KernelPatcher::RouteRequest routes[]={ + {symbols[0],wrapDSIBackingAllocate,orgDSIBacking[0]}, + {symbols[1],wrapDSIBackingFree,orgDSIBacking[1]}}; + // Short routes copy only the six-byte push/mov/push prologue. The free + // entry has a relative branch at byte 12; never copy it into a trampoline. + const bool ready=known && patcher.routeMultipleShort(index,routes,2,address,size);patcher.clearError(); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIBackingHooksReady",ready);return ready; +} + +uint32_t XE::wrapDSIBackingAllocate(OSObject *pipe,void *record,uint32_t flags) { + auto &o=dsiPendingBacking; + if(!__atomic_load_n(&o.framebuffer,__ATOMIC_ACQUIRE)) + return FunctionCast(wrapDSIBackingAllocate,callbackXE->orgDSIBacking[0])(pipe,record,flags); + if(__atomic_load_n(&o.thread,__ATOMIC_ACQUIRE)!=reinterpret_cast(current_thread()) || record!=o.record) { + if(pipe && pipe==__atomic_load_n(&o.pipe,__ATOMIC_ACQUIRE))return kIOReturnNotReady; + return FunctionCast(wrapDSIBackingAllocate,callbackXE->orgDSIBacking[0])(pipe,record,flags); + } + if(!o.allocating || flags || o.pipe || !pipe || !pipe->metaCast("IGAccelDisplayPipe") || + pipe->getMetaClass()->getClassSize()<0x12B0 || Display13FBMemory::field(pipe,0x88)!=o.accelerator || + Display13FBMemory::field(pipe,0x1298) || Display13FBMemory::field(pipe,0x12A0) || + Display13FBMemory::field(pipe,0x12A8))return kIOReturnNotReady; + pipe->retain();__atomic_store_n(&o.pipe,pipe,__ATOMIC_RELEASE); + const auto result=FunctionCast(wrapDSIBackingAllocate,callbackXE->orgDSIBacking[0])(pipe,record,flags); + o.nativeResult=result;const auto &r=*o.record; + o.allocated=!result && r.refs==1 && r.memory && r.map && + Display13FBMemory::field(pipe,0x1298)==r.memory && + Display13FBMemory::field(pipe,0x12A0)==r.map && !Display13FBMemory::field(pipe,0x12A8); + if(o.allocated) { + // Native init/destroyScanoutResource can replace these pipe fields. + // Pin the objects while still inside the native GPU lock, before + // returning to the PTE inspector. Pins alone do not preserve GGTT. + o.pinnedMap=reinterpret_cast(r.map);o.pinnedMemory=reinterpret_cast(r.memory); + o.pinnedMap->retain();o.pinnedMemory->retain(); + } + return o.allocated && callbackXE->validateDSINativeBacking() && + callbackXE->initializeDSINativeBacking() && callbackXE->switchDSINativeSurface() && + callbackXE->publishDSINativeBacking() ? result : kIOReturnNotReady; +} + +uint32_t XE::wrapDSIBackingFree(OSObject *pipe,void *record,uint32_t flags) { + auto &o=dsiPendingBacking; + if(!__atomic_load_n(&o.framebuffer,__ATOMIC_ACQUIRE)) + return FunctionCast(wrapDSIBackingFree,callbackXE->orgDSIBacking[1])(pipe,record,flags); + if(__atomic_load_n(&o.thread,__ATOMIC_ACQUIRE)!=reinterpret_cast(current_thread()) || record!=o.record) { + if(pipe && pipe==__atomic_load_n(&o.pipe,__ATOMIC_ACQUIRE))return kIOReturnNotReady; + return FunctionCast(wrapDSIBackingFree,callbackXE->orgDSIBacking[1])(pipe,record,flags); + } + if(!o.freeing || flags || o.armed || !pipe || pipe!=o.pipe || + Display13FBMemory::field(pipe,0x88)!=o.accelerator)return kIOReturnNotReady; + const auto &r=*o.record; + if(r.refs>1 || Display13FBMemory::field(pipe,0x1298)!=r.memory || + Display13FBMemory::field(pipe,0x12A0)!=r.map || Display13FBMemory::field(pipe,0x12A8) || + (!r.refs && (r.map || r.memory || r.gpu || r.pitch)))return kIOReturnNotReady; + const auto result=FunctionCast(wrapDSIBackingFree,callbackXE->orgDSIBacking[1])(pipe,record,flags); + o.nativeResult=result;o.freed=!result && !r.refs && !r.map && !r.memory && !r.gpu && !r.pitch && + !Display13FBMemory::field(pipe,0x1298) && !Display13FBMemory::field(pipe,0x12A0); + return o.freed ? result : kIOReturnNotReady; +} + +bool XE::prepareDSINativeBacking(IOService *fb) { + dsiBackingStage=0; + if(adlnOutputWiring.dsi!=1 || !fb || fb->getMetaClass()->getClassSize()<0x4B90 || + !dsiModeTarget(fb) || !dsiBackingHooksReady || !dsiProfile.valid())return false; + dsiBackingStage=1; + DSIModeScope scope(fb);if(!scope.lock || !dsiNativeUpdateAllowed(fb))return false; + auto &o=dsiPendingBacking;auto &c=display13FBPower; + if(__atomic_load_n(&o.framebuffer,__ATOMIC_ACQUIRE))return false; + dsiBackingStage=2; + IOLockLock(c.lock); + auto controller=static_cast(c.controller); + auto owner=reinterpret_cast(display13FBExpectedOwner(controller)); + const auto reserve=__atomic_load_n(&display13FBGTT.sharedPoolPrefix,__ATOMIC_ACQUIRE); + auto machine=owner && owner->getMetaClass()->getClassSize()>=0x1500 ? Display13FBMemory::field(owner,0x378) : nullptr; + XELP::DSI::NativeBacking::Record request; + const bool ready=adlnOutputWiring.dsi==1 && !c.dsiNativeWriters && owner && machine && + machine->metaCast("IGAccelDisplayMachine") && machine->getMetaClass()->getClassSize()>=0x130 && + Display13FBMemory::field(machine,0x128)==owner && adlnSharedBinding(controller,owner) && + reserve && reserve==Display13FBMemory::field(owner,0x13C0) && + adlnSysMemOwner(controller,Display13FBMemory::pool(controller)) && + Display13FBMemory::allocatedSlot(controller,fb)==0 && + XELP::DSI::NativeBacking::request(dsiProfile,Display13FBMemory::field(owner,0x1470),request) && + display13FBInspectMemory(fb,true,0,false).status==Display13FBMemory::Status::Complete; + if(ready){fb->retain();owner->retain();} + IOLockUnlock(c.lock);if(!ready)return false; + // Prepare the real native dimensions before handing this allocation to + // its later public mode owner. No mode-success or backing is published. + if(!probeDSIModeGeometry(fb)) {owner->release();fb->release();return false;} + dsiBackingStage=3; + auto record=static_cast(IOMalloc(sizeof(request))); + if(!record){owner->release();fb->release();return false;} + memcpy(record,&request,sizeof(request));o.accelerator=owner;o.record=record; + __atomic_store_n(&o.thread,reinterpret_cast(current_thread()),__ATOMIC_RELEASE);o.allocating=true; + __atomic_store_n(&o.framebuffer,fb,__ATOMIC_RELEASE); + dsiBackingStage=4; + uint32_t index=Display13FBMemory::field(fb,0x1DC),flags=0; + // Native callPlatformFunction takes the GPU lock. Never hold c.lock here. + const auto result=owner->callPlatformFunction("allocscanmem",false,record,&index,&flags,nullptr); + o.allocating=false; + dsiBackingStage=5; + if(result || !o.allocated)return false; // Retain partial state for discard. + IOLockLock(c.lock); + const bool verified=c.controller==controller && adlnSharedBinding(controller,owner) && + o.pipe && o.pinnedMap && o.pinnedMemory && + Display13FBMemory::field(o.pipe,0x88)==owner && + Display13FBMemory::field(o.pipe,0x1298)==record->memory && + Display13FBMemory::field(o.pipe,0x12A0)==record->map && + !Display13FBMemory::field(o.pipe,0x12A8) && + reserve==__atomic_load_n(&display13FBGTT.sharedPoolPrefix,__ATOMIC_ACQUIRE) && + reserve==Display13FBMemory::field(owner,0x13C0) && + !c.memoryFaulted && !c.clockFaulted && + XELP::DSI::NativeBacking::allocated(*record,dsiProfile,o.proof); + IOLockUnlock(c.lock);if(verified)dsiBackingStage=6;return verified; +} + +// The first P3 step uses the donor's CPU geometry helper. The selected Y +// layout is the same native tile-mode-3 allocation validated by the caller. +// Retain a bounded receipt of the remaining native transaction prerequisites. +bool XE::probeDSIModeGeometry(IOService *fb) { + if(!dsiComputeGeometry || !dsiModeTarget(fb) || !dsiModeCoherent(fb))return false; + auto path=Display13FBMemory::singlePath(fb); + auto &c=display13FBPower;IOLockLock(c.lock); + auto controller=static_cast(c.controller); + uint64_t receipt[16] {1,0,Display13FBMemory::field(fb,0x4A18), + Display13FBMemory::field(fb,0x4350),Display13FBMemory::field(fb,0x420C)}; + const bool bound=path && path->getMetaClass()->getClassSize()>=0x3730 && controller && + Display13FBMemory::field(fb,0x1D0)==controller && + Display13FBMemory::field(path,0x32B0)==fb && !c.dsiNativeWriters && + !c.memoryFaulted && !c.clockFaulted && !dsiStopAttempted; + uint8_t savedFB[0x30],savedPath[0x20]; + memcpy(savedFB,reinterpret_cast(fb)+0x430C,sizeof(savedFB)); + if(bound)memcpy(savedPath,reinterpret_cast(path)+0xF0,sizeof(savedPath)); + const bool prepared=bound && XELP::DSI::AppleAdapter::prepareGeometry(fb,path,dsiProfile,dsiFixedMode.timing, + reinterpret_cast(dsiComputeGeometry)); + receipt[1]=prepared;receipt[5]=Display13FBMemory::field(fb,0x430C); + receipt[6]=Display13FBMemory::field(fb,0x4310);receipt[7]=Display13FBMemory::field(fb,0x4314); + receipt[8]=Display13FBMemory::field(fb,0x4334);receipt[9]=Display13FBMemory::field(fb,0x4338); + if(controller) { + receipt[10]=Display13FBMemory::field(controller,0xC10)!=nullptr; + receipt[11]=Display13FBMemory::field(controller,0x1BB4); + auto panel=Display13FBMemory::field(controller,0xD0); + receipt[12]=panel!=nullptr; + if(panel && panel->getMetaClass()->getClassSize()>=0x70) { + receipt[13]=Display13FBMemory::field(panel,0x68); + receipt[14]=Display13FBMemory::field(panel,0x50)==fb; + } + receipt[15]=Display13FBMemory::field(controller,0xE53); + } + // Until FB+4348 publishes the native backing, leave public aperture + // dimensions untouched. Otherwise IOGraphics could map the old GOP while + // the identification plane reads the new tiled allocation. + if(bound) { + memcpy(reinterpret_cast(fb)+0x430C,savedFB,sizeof(savedFB)); + memcpy(reinterpret_cast(path)+0xF0,savedPath,sizeof(savedPath)); + const uint32_t bpp=static_cast(receipt[3]); + memcpy(reinterpret_cast(fb)+0x4350,&bpp,sizeof(bpp)); + *(reinterpret_cast(fb)+0x4A18)=static_cast(receipt[2]); + } + IOLockUnlock(c.lock); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIGeometry",receipt,sizeof(receipt)); + return prepared; +} + +// Called by allocateScanoutMemory's routed entry while the native display +// machine still holds the GPU lock. Keep object/PTE validation in that scope; +// retained map/memory references alone do not serialize GGTT replacement. +bool XE::validateDSINativeBacking() { + auto &o=dsiPendingBacking;auto &c=display13FBPower; + auto fb=o.framebuffer;auto owner=o.accelerator;auto record=o.record; + if(!fb || !owner || !record || !o.allocating || !o.allocated || !o.pinnedMap || !o.pinnedMemory || + o.thread!=reinterpret_cast(current_thread()) || !c.lock)return false; + auto controller=static_cast(c.controller); + const auto reserve=__atomic_load_n(&display13FBGTT.sharedPoolPrefix,__ATOMIC_ACQUIRE); + IOLockLock(c.lock); + bool verified=false; + const auto pipeBound=[&]() { + return o.pipe && Display13FBMemory::field(o.pipe,0x88)==owner && + Display13FBMemory::field(o.pipe,0x1298)==record->memory && + Display13FBMemory::field(o.pipe,0x12A0)==record->map && + !Display13FBMemory::field(o.pipe,0x12A8); + }; + if(pipeBound() && c.controller==controller && c.map && c.map->getVirtualAddress() && c.map->getLength()==0x1000000 && + adlnSharedBinding(controller,owner) && !c.memoryFaulted && !c.clockFaulted && + reserve && reserve==Display13FBMemory::field(owner,0x13C0)) { + Display13SurfaceIO io {c.map,0}; + o.proof=Display13FBMemory::inspectPendingSurface(io,controller,controller->getMetaClass()->getClassSize(), + fb,fb->getMetaClass()->getClassSize(),reinterpret_cast(owner),reserve, + (c.map->getLength()-0x800000)/8*4096,xelpMemory,record); + verified=pipeBound() && XELP::DSI::NativeBacking::allocated(*record,dsiProfile,o.proof); + } + IOLockUnlock(c.lock);return verified; +} + +// Mapping and writes remain inside the same native GPU lock as the allocator +// and full PTE proof. The GPU map is IGAccelMemoryMap, not IOMemoryMap: create +// a separate CPU mapping from its validated SysMemory descriptor. +bool XE::initializeDSINativeBacking() { + auto &o=dsiPendingBacking; + if(!o.allocating || !o.allocated || o.armed || !o.record || !o.pinnedMemory || + o.thread!=reinterpret_cast(current_thread()) || + !XELP::DSI::NativeBacking::allocated(*o.record,dsiProfile,o.proof))return false; + o.initialized=false; + auto descriptor=OSDynamicCast(IOMemoryDescriptor,Display13FBMemory::field(o.pinnedMemory,0xD0)); + if(!descriptor || descriptor->getLength()createMappingInTask(kernel_task,0,kIOMapAnywhere,0,o.proof.bytes); + if(!map)return false; + const auto address=map->getVirtualAddress(); + if(address && map->getLength()==o.proof.bytes && !(address&4095)) { + // Clear the entire allocation, including tile padding, before it can + // become visible. Black is independent of the Y-tile pixel swizzle. + memset(reinterpret_cast(address),0,o.proof.bytes); + for(uint64_t offset=0;offsetrelease(); + return o.initialized; +} + +// The allocator's native GPU lock and FB mutex span this transaction. The +// owner lock protects shared clock/DDB/IRQ association; no IRQ spinlock spans +// the bounded frame wait. Publish the backing only after the plane has latched. +bool XE::switchDSINativeSurface() { + using namespace XELP::DSI; + auto &o=dsiPendingBacking;auto &c=display13FBPower;auto &irq=display13FBIRQ; + auto fb=o.framebuffer; + if(!fb || !o.initialized || !o.allocating || !o.allocated || o.armed || !o.record || !c.lock || + o.thread!=reinterpret_cast(current_thread()) || dsiSession.state!=SessionState::Unbound)return false; + uint64_t receipt[18] {2,1}; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSISurface",receipt,sizeof(receipt)); + auto path=Display13FBMemory::singlePath(fb); + IOLockLock(c.lock); + const auto owned=[&]() { + return dsiResourcesReady && dsiNativeWriterHooksReady && dsiCacheHooksReady && dsiBackingHooksReady && + !dsiStopAttempted && !c.dsiNativeWriters && !c.memoryFaulted && !c.clockFaulted && + !c.bridge.state.faulted && !c.bridge.state.suspended && !c.dbuf.faulted && !c.dbuf.updating && + !c.pipe[0].destroyingPath && !c.pipe[0].wmProgramming && !c.pipe[0].wmCommitting && + c.pipe[0].memoryFramebuffer==fb && c.transcoders.dsi0Pipe==0 && path && + path->getMetaClass()->getClassSize()>=0x3730 && + Display13FBMemory::field(path,0x32B0)==fb && + Display13FBMemory::field(path,0x3640)==0 && + Display13FBMemory::field(path,0x3644)==2 && + Display13FBMemory::field(path,0x3648)==c.dsiPort && + c.bridge.bootAdopted && c.bridge.bootPath==reinterpret_cast(path) && + adlnSharedBinding(c.controller,o.accelerator) && adlnWriterMappingBound() && + o.pipe && Display13FBMemory::field(o.pipe,0x1298)==o.record->memory && + Display13FBMemory::field(o.pipe,0x12A0)==o.record->map && + XELP::DSI::NativeBacking::allocated(*o.record,dsiProfile,o.proof); + }; + struct IO : Display13FBBootIO { + decltype(owned) &owns; + IO(IOMemoryMap *m,decltype(owned) &f):Display13FBBootIO{m},owns(f){} + bool ready(){return owns();} + uint64_t nativeClockHz(){Display13FBClockIO clock{map};return Display13Clock::read(clock).nativeHz();} + uint64_t nowUs(){uint64_t t=0,ns=0;clock_get_uptime(&t);absolutetime_to_nanoseconds(t,&ns);return ns/1000;} + void delayUs(unsigned us){if(us>=1000)IOSleep((us+999)/1000);else IODelay(us);} + void write(uint32_t reg,uint32_t value) { + const bool wm=(reg>=0x70240 && reg<=0x7025C && !(reg&3)) || reg==0x70268; + bool plane=false;for(auto r:Surface::Registers)plane|=r==reg; + if(readable(reg) && (wm || plane || reg==0x45270)) + *reinterpret_cast(map->getVirtualAddress()+reg)=value; + } + } io{c.map,owned}; + bool irqReady=false; + if(owned() && irq.lock && adlnIRQOwner(static_cast(c.controller))) { + const auto saved=IOSimpleLockLockDisableInterrupt(irq.lock); + irqReady=!irq.quiescing && !irq.state.faulted && irq.state.enabled && (irq.admittedPipes&1) && + irq.state.transcoders.dsi0Pipe==0; + IOSimpleLockUnlockEnableInterrupt(irq.lock,saved); + } + receipt[2]=irqReady; + Surface::Result result; + if(irqReady && dsiBoot.complete() && c.bootDSI.sameSource(dsiBoot.source) && + c.bridge.outputHeld(io,0,0) && c.latency.status==Display13WM::Status::Complete) { + const auto ddb=Display13DBUF::hardwareRaw(c.dbuf,0,c.dbuf.pipe[0].primary); + const auto cursorDDB=Display13DBUF::hardwareRaw(c.dbuf,0,c.dbuf.pipe[0].cursor); + const auto range=Display13DBUF::decode(ddb);const auto ipc=io.read(0x45004); + auto wm=Display13WM::plan(dsiProfile.mode.width,dsiProfile.mode.htotal(),uint64_t(dsiProfile.mode.clockKHz)*1000, + Display13DBUF::valid(range)?range.end-range.start:0,c.latency, + (ipc&8) || Display13FBMemory::field(c.controller,0x1B4C),true,dsiProfile.mode.height); + const auto hz=io.nativeClockHz();const auto pixel=uint64_t(dsiProfile.mode.clockKHz)*1000; + const Display13WM::Snapshot snapshot{&wm,nullptr,ddb,cursorDDB,static_cast(o.proof.gpu),0,ipc,hz,false,false, + Display13WM::pipeLineTime(dsiProfile.mode.htotal(),pixel),pixel,0,c.transcoders}; + Surface::Plane current{dsiBoot.control,io.read(0x701CC),dsiBoot.stride,dsiBoot.size,dsiBoot.surface,0,0}; + auto next=current;next.control=Display13WM::arbitrationControl((current.control&~(7U<<10))|(4U<<10)); + next.color|=1U<<13;next.stride=static_cast(o.proof.pitch/128);next.surface=static_cast(o.proof.gpu); + Surface::Backing backing{reinterpret_cast(o.record),o.proof.gpu,o.proof.bytes,o.proof.pitch}; + Capabilities caps;caps.installed=Capabilities::Adoption; + if(ddb!=UINT32_MAX && cursorDDB!=UINT32_MAX && Display13DBUF::valid(range) && + dsiSession.bind(caps,c.bootDSI,dsiProfile,1,reinterpret_cast(path),reinterpret_cast(&dsiBoot))) { + result=Surface::change(io,dsiSession,c.bootDSI,dsiProfile,current,next,backing,snapshot); + o.armed=result.writes!=0 || dsiSession.state==SessionState::Switching || dsiSession.state==SessionState::Faulted || + dsiSession.state==SessionState::Streaming; + if(!o.armed)dsiSession={}; + } + receipt[3]=ddb;receipt[4]=cursorDDB;receipt[5]=hz;receipt[6]=wm.allocation; + receipt[7]=next.control;receipt[8]=next.stride;receipt[9]=next.surface; + } + receipt[10]=static_cast(result.status);receipt[11]=result.writes; + receipt[12]=static_cast(result.wm.status);receipt[13]=static_cast(result.latch.status); + receipt[14]=static_cast(dsiSession.state);receipt[15]=o.armed; + receipt[16]=result.latch.frameBefore;receipt[17]=result.latch.frameAfter; + // Surface::change already proves frame progress, the live surface and + // source/owner stability before completing the session transaction. + const bool switched=result.status==Surface::Status::Complete; + IOLockUnlock(c.lock); + receipt[1]=2; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSISurface",receipt,sizeof(receipt)); + return switched; +} + +// Still inside allocateScanoutMemory's GPU lock and the FB mutex. Transfer +// the already latched allocation to the native FB without reallocating it. +// Returning stolen slot zero changes bookkeeping only; its GGTT reservation +// and the firmware retired-backing record remain held through Session stop. +bool XE::publishDSINativeBacking() { + auto &o=dsiPendingBacking;auto &c=display13FBPower;auto fb=o.framebuffer; + if(!fb || !o.allocating || o.thread!=reinterpret_cast(current_thread()) || + !o.armed || !o.allocated || o.published || dsiSession.state!=XELP::DSI::SessionState::Streaming || + !dsiComputeGeometry || !orgAlderLakeNScanoutFree)return false; + IOLockLock(c.lock); + auto controller=static_cast(c.controller);auto path=Display13FBMemory::singlePath(fb); + const auto pool=Display13FBMemory::pool(controller); + const auto count=Display13FBMemory::field(controller,0xDF5); + const auto used=Display13FBMemory::field(controller,0xDF4); + const auto mask=Display13FBMemory::field(controller,0xDF0); + uint64_t receipt[12] {1,1,count,used,mask}; + uint8_t savedFB[0x30],savedPath[0x20]; + memcpy(savedFB,reinterpret_cast(fb)+0x430C,sizeof(savedFB)); + if(!path) {IOLockUnlock(c.lock);return false;} + memcpy(savedPath,reinterpret_cast(path)+0xF0,sizeof(savedPath)); + const auto oldBpp=Display13FBMemory::field(fb,0x4350); + const auto oldTiled=Display13FBMemory::field(fb,0x4A18); + Display13FBBootIO io{c.map}; + bool ready=dsiResourcesReady && !dsiStopAttempted && !c.memoryFaulted && !c.clockFaulted && + !c.dsiNativeWriters && !c.bridge.state.faulted && dsiModeCoherent(fb) && + Display13FBMemory::allocatedSlot(controller,fb)==0 && pool.state==1 && !pool.descriptor && + adlnSharedBinding(controller,o.accelerator) && + XELP::DSI::NativeBacking::allocated(*o.record,dsiProfile,o.proof) && + XELP::DSI::Surface::source(io,c.bootDSI) && + XELP::DSI::Surface::word(io,0x701AC,UINT32_MAX,static_cast(o.proof.gpu)) && + XELP::DSI::AppleAdapter::prepareGeometry(fb,path,dsiProfile,dsiFixedMode.timing, + reinterpret_cast(dsiComputeGeometry)); + IOLockUnlock(c.lock); + if(ready) { + // FB+4348 is still null, so the exact donor takes only the slot-zero + // FreeSysMem/freeFBMemory branch; no GPU free is reachable here. + FunctionCast(wrapAlderLakeNScanoutFree,orgAlderLakeNScanoutFree)(fb); + IOLockLock(c.lock); + receipt[5]=Display13FBMemory::field(controller,0xDF4); + receipt[6]=Display13FBMemory::field(controller,0xDF0); + receipt[7]=Display13FBMemory::field(fb,0x4288); + ready=Display13FBMemory::slotReturned(count,0,used,mask,receipt[5],receipt[6],receipt[7]) && + Display13FBMemory::poolReturned(pool,Display13FBMemory::pool(controller),0) && + !Display13FBMemory::field(fb,0x4348) && !c.memoryFaulted && + Display13FBMemory::field(fb,0x4334)==o.proof.pitch; + if(ready) { + *reinterpret_cast(reinterpret_cast(fb)+0x4348)=o.record; + *reinterpret_cast(reinterpret_cast(fb)+0x4330)=static_cast(o.proof.gpu); + c.pipe[0].scanout=o.proof; + __atomic_store_n(&c.pipe[0].publishedScanout,reinterpret_cast(o.record),__ATOMIC_RELEASE); + o.published=true;receipt[1]=2; + } + IOLockUnlock(c.lock); + if(ready)ready=updateDSIBootCache(controller,fb,path); + } + if(!o.published) { + IOLockLock(c.lock); + memcpy(reinterpret_cast(fb)+0x430C,savedFB,sizeof(savedFB)); + memcpy(reinterpret_cast(path)+0xF0,savedPath,sizeof(savedPath)); + memcpy(reinterpret_cast(fb)+0x4350,&oldBpp,sizeof(oldBpp)); + *(reinterpret_cast(fb)+0x4A18)=oldTiled; + IOLockUnlock(c.lock); + } + receipt[8]=ready;receipt[9]=o.published;receipt[10]=o.proof.gpu;receipt[11]=o.proof.pitch; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIPublicBacking",receipt,sizeof(receipt)); + return ready; +} + +// The FB mutex spans native mode completion. These donor CPU leaves preserve +// transaction/notification semantics; the ordinary DDI modesetter is bypassed +// because the DSI link and the new primary plane are already running. +bool XE::publishDSINativeMode(IOService *fb) { + auto &o=dsiPendingBacking;auto &c=display13FBPower; + if(!dsiPublishedTarget(fb) || !dsiModeCoherent(fb) || !dsiModeCPU[0] || !dsiModeCPU[1] || !dsiModeCPU[2])return false; + if(o.modeReady)return dsiNativeUpdateAllowed(fb); + uint64_t receipt[12] {1,1}; + IOLockLock(c.lock);auto controller=static_cast(c.controller); + auto path=Display13FBMemory::singlePath(fb); + const bool ready=!dsiStopAttempted && dsiResourcesReady && !c.memoryFaulted && !c.clockFaulted && + !c.bridge.state.faulted && !c.dbuf.faulted && !c.watermarkFaulted && !c.dsiNativeWriters && dsiArmedBackingOwned(fb,path) && + dsiSession.state==XELP::DSI::SessionState::Streaming && + !Display13FBMemory::field(controller,0xD0) && + !Display13FBMemory::field(fb,0x4340) && Display13FBMemory::field(fb,0x4338)==1; + IOLockUnlock(c.lock); + receipt[2]=ready;receipt[3]=ready && updateDSIBootCache(controller,fb,path); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIPublicMode",receipt,sizeof(receipt)); + if(!receipt[2] || !receipt[3])return false; + uint32_t tile=2; + receipt[2]=o.accelerator->callPlatformFunction("EFITilingMode",true,&tile,nullptr,nullptr,nullptr); + if(!receipt[2]) { + *reinterpret_cast(reinterpret_cast(controller)+0xE53)=0; + *reinterpret_cast(reinterpret_cast(fb)+0x4250)=XELP::DSI::AppleAdapter::FixedModeID; + *reinterpret_cast(reinterpret_cast(fb)+0x4254)=0; + receipt[3]=reinterpret_cast(dsiModeCPU[0])(fb); + if(!receipt[3] && !Display13FBMemory::field(fb,0x4340)) { + reinterpret_cast(dsiModeCPU[1])(fb,0); + reinterpret_cast(dsiModeCPU[2])(fb); + IOLockLock(c.lock); + receipt[4]=Display13FBMemory::field(fb,0x4000); + receipt[5]=Display13FBMemory::field(fb,0x4008); + receipt[6]=Display13FBMemory::field(fb,0x400C); + receipt[7]=Display13FBMemory::field(fb,0x4200); + o.modeReady=!receipt[4] && receipt[5]==UINT32_MAX && receipt[6]==UINT32_MAX && receipt[7]==1 && + !c.memoryFaulted && !c.clockFaulted && !c.bridge.state.faulted && !dsiStopAttempted && + dsiArmedBackingOwned(fb,path) && dsiPublishedTarget(fb); + receipt[8]=o.modeReady;receipt[9]=Display13FBMemory::field(fb,0x402C); + receipt[10]=Display13FBMemory::field(fb,0x4334);receipt[11]=Display13FBMemory::field(fb,0x4314); + IOLockUnlock(c.lock); + } + } + if(o.modeReady) { + // setupClocks normally copies the detailed timing's Hz into this CPU + // path field before WM calculation. DSI retains its live PLL, so supply + // that fixed-mode cache without entering the native DDI clock setup. + memcpy(reinterpret_cast(path)+0x118,&dsiFixedMode.timing.pixelClock,sizeof(uint64_t)); + // Still under the native FB mutex: admit the existing DDB transaction, + // then require its latch before returning mode success to any client. + // GOP adoption may have no cursor reservation and a full primary DDB. + adlnSetupDBUFPipe(controller,fb,0,false); + IOLockLock(c.lock); + const auto &allocation=c.dbuf.pipe[0];const auto &pipe=c.pipe[0]; + o.modeReady=!c.dbuf.faulted && !c.watermarkFaulted && !c.memoryFaulted && !c.clockFaulted && + !c.bridge.state.faulted && !dsiStopAttempted && !pipe.wmPending && + allocation.primary.start==40 && allocation.primary.end==2048 && + allocation.cursor.start==0 && allocation.cursor.end==40 && + pipe.wmLatch.status==Display13WM::Status::Complete && dsiPublishedTarget(fb); + uint64_t layout[] {1,allocation.primary.start,allocation.primary.end,allocation.cursor.start,allocation.cursor.end, + pipe.wmPending,static_cast(pipe.wmLatch.status),static_cast(pipe.wmLatch.check), + c.watermarkFaulted,c.dbuf.faulted,pipe.watermarkClockHz,o.modeReady}; + IOLockUnlock(c.lock); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIModeDBUF",layout,sizeof(layout)); + } + receipt[8]=o.modeReady; + receipt[1]=2;IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIPublicMode",receipt,sizeof(receipt)); + return o.modeReady; +} + +// A stopped stream may retain a native backing after partial cleanup. Its +// ownership is distinct from the GOP pool, which stays reserved until later. +bool XE::dsiArmedBackingOwned(IOService *fb,OSObject *path) { + auto &o=dsiPendingBacking; + if(!dsiPendingTarget(fb) || !o.armed || o.allocating || o.freeing || !o.record || !o.allocated || + !o.pipe || !o.pinnedMap || !o.pinnedMemory || !path)return false; + const auto handle=reinterpret_cast(o.record),lease=reinterpret_cast(path); + const bool session=dsiSession.lease==lease && + (dsiSession.currentBacking==handle || dsiSession.pendingBacking==handle) && + (dsiSession.state==XELP::DSI::SessionState::Streaming || dsiSession.state==XELP::DSI::SessionState::Faulted); + const bool cleanup=dsiSession.state==XELP::DSI::SessionState::Released && dsiCleanup.lease==lease && + (dsiCleanup.currentBacking==handle || dsiCleanup.pendingBacking==handle); + return (session || cleanup) && dsiSession.identity.same(dsiBoot.source.identity) && + adlnSharedBinding(display13FBPower.controller,o.accelerator) && + Display13FBMemory::field(o.pipe,0x88)==o.accelerator && + Display13FBMemory::field(o.pipe,0x1298)==o.record->memory && + Display13FBMemory::field(o.pipe,0x12A0)==o.record->map; +} + +// Caller has completed host/IRQ/clock stop and dropped owner/clock locks, but +// keeps FB serialization and the provider gate across native GPU cleanup. +bool XE::retireDSINativeBacking(IOService *fb,OSObject *path) { + if(!dsiPendingTarget(fb))return true; + if(!dsiStop.complete() || !dsiIRQParked || !dsiClockStop.complete() || !dsiDDIReleased || + !dsiArmedBackingOwned(fb,path) || !releaseDSILogical(fb))return false; + if(dsiSession.state!=XELP::DSI::SessionState::Released) { + const XELP::DSI::StopProof proof{dsiSession.ticket(),dsiBoot.source.identity,true,true,true,true}; + if(!dsiSession.stopped(proof))return false; + dsiCleanup=dsiSession.takeCleanup(); + } + auto &o=dsiPendingBacking;auto &c=display13FBPower; + const bool published=o.published;auto record=o.record; + IOLockLock(c.lock); + if(published && !dsiPublishedTarget(fb)) {IOLockUnlock(c.lock);return false;} + o.modeReady=false; + if(published) { + *reinterpret_cast(reinterpret_cast(fb)+0x4348)=nullptr; + __atomic_store_n(&c.pipe[0].publishedScanout,uintptr_t(0),__ATOMIC_RELEASE); + c.pipe[0].scanout={};o.published=false; + } + IOLockUnlock(c.lock);o.armed=false; + if(discardDSINativeBacking(fb))return true; + IOLockLock(c.lock); + if(published) {*reinterpret_cast(reinterpret_cast(fb)+0x4348)=record;o.published=true;} + o.armed=true;IOLockUnlock(c.lock);return false; +} + +// The first real fixed-mode request establishes an identification surface +// while the native FB mutex is held. Retain any armed backing until stop. +// Backing publication shares the native allocation serializer. Do this once after allocation starts; +// an unavailable accelerator may be retried by a later mode request. Never +// report a WindowServer mode before its subsequent CPU completion succeeds. +void XE::probeDSINativeBacking(IOService *fb) { + if(dsiBackingAttempted)return; + dsiBackingAttempted=true; + uint64_t receipt[16] {1,1}; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIBackingTrial",receipt,sizeof(receipt)); + const bool verified=prepareDSINativeBacking(fb); + auto &o=dsiPendingBacking; + const bool pending=dsiPendingTarget(fb); + receipt[1]=2;receipt[2]=dsiBackingStage;receipt[3]=verified;receipt[4]=pending; + if(pending) { + receipt[5]=o.allocated;receipt[6]=o.nativeResult;receipt[7]=static_cast(o.proof.status); + receipt[8]=o.proof.bytes;receipt[9]=o.proof.pitch;receipt[10]=o.proof.gpu;receipt[11]=o.proof.verifiedPages; + receipt[12]=o.pinnedMap && o.pinnedMemory; + } + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIBackingTrial",receipt,sizeof(receipt)); + if(pending && !o.armed)receipt[13]=discardDSINativeBacking(fb); + receipt[14]=dsiPendingTarget(fb);receipt[15]=pending && !o.armed && !receipt[13];receipt[1]=3; + if(!pending)dsiBackingAttempted=false; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIBackingTrial",receipt,sizeof(receipt)); +} + +// Only a never-armed pending surface may take this path. After SURF is armed, +// retain it through Session's stop proof; freeing an unlatched target is unsafe. +bool XE::discardDSINativeBacking(IOService *fb) { + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return false; + IOService *owner=nullptr;OSObject *pipe=nullptr,*mapping=nullptr,*memory=nullptr;void *record=nullptr; + { + DSIModeScope scope(fb);if(!scope.lock)return false; + auto &o=dsiPendingBacking; + if(!dsiPendingTarget(fb) || o.armed || o.allocating || o.freeing || !o.record)return false; + if(!adlnSharedBinding(display13FBPower.controller,o.accelerator))return false; + if(o.pipe) { + __atomic_store_n(&o.thread,reinterpret_cast(current_thread()),__ATOMIC_RELEASE);o.freeing=true;o.freed=false; + uint32_t index=Display13FBMemory::field(fb,0x1DC),flags=0; + o.accelerator->callPlatformFunction("freescanmem",false,o.record,&index,&flags,nullptr); + o.freeing=false; + // DisplayMachine's free discards the pipe method's return value. + // Only our observed native record AND pipe cleanup prove success. + if(!o.freed)return false; + } else if(o.record->refs || o.record->map || o.record->memory || o.record->gpu || o.record->pitch)return false; + owner=o.accelerator;pipe=o.pipe;record=o.record;mapping=o.pinnedMap;memory=o.pinnedMemory; + __atomic_store_n(&o.framebuffer,static_cast(nullptr),__ATOMIC_RELEASE); + __atomic_store_n(&o.pipe,static_cast(nullptr),__ATOMIC_RELEASE); + o.accelerator=nullptr;o.record=nullptr;o.pinnedMap=o.pinnedMemory=nullptr;__atomic_store_n(&o.thread,uintptr_t(0),__ATOMIC_RELEASE);o.allocated=o.freed=o.published=o.modeReady=false;o.nativeResult=0;o.proof={};o.initialized=false; + } + IOFree(record,sizeof(XELP::DSI::NativeBacking::Record)); + if(mapping)mapping->release();if(memory)memory->release();if(pipe)pipe->release();owner->release();fb->release();return true; +} diff --git a/WhateverXe/kern_dsi_adapter_boot.inc b/WhateverXe/kern_dsi_adapter_boot.inc new file mode 100644 index 00000000..080ff502 --- /dev/null +++ b/WhateverXe/kern_dsi_adapter_boot.inc @@ -0,0 +1,314 @@ +// Firmware profile, native boot/cache binding and resource qualification. +// Included only by kern_xe.cpp; retains its private owners and lock ordering. + +bool XE::prepareDSIFirmware(IOService *provider) { + auto source=provider ? OSDynamicCast(OSData,provider->getProperty("TGLVBT")) : nullptr; + if(!source || !source->getLength() || source->getLength()>65536)return false; + if(dsiFirmware)return dsiProfile.valid() && dsiFirmware->getLength()==source->getLength() && + !memcmp(dsiFirmware->getBytesNoCopy(),source->getBytesNoCopy(),source->getLength()); + auto copy=OSData::withBytes(source->getBytesNoCopy(),source->getLength()); + if(!copy)return false; + auto parsed=XELP::DSI::parseProfile({static_cast(copy->getBytesNoCopy()),copy->getLength()},true); + if(!parsed.valid()){copy->release();return false;} + dsiProfile=parsed;dsiFirmware=copy;return true; +} + +// Keep profile parsing temporaries out of the already large bootstrap frame. +__attribute__((noinline)) void XE::auditDSIBoot(IOPCIDevice *pci,volatile const uint8_t *base,uint64_t length, + const AlderLakeN::DisplayReadout &normal,bool admitted) { + if(!checkKernelArgument("-wex-dsi-audit"))return; + struct Scratch {XELP::DSI::Profile profile;XELP::DSI::Boot boot;XELP::DSI::BootAudit audit;}; + auto scratch=static_cast(IOMalloc(sizeof(Scratch)));if(!scratch)return; + // Pre-native, before the optional external-output handoff. Read only; + // the scratch certificate must never become the live dsiBoot owner. + auto vbt=OSDynamicCast(OSData,pci->getProperty("TGLVBT"));scratch->profile={}; + if(vbt && vbt->getLength() && vbt->getLength()<=65536) + scratch->profile=XELP::DSI::parseProfile({static_cast(vbt->getBytesNoCopy()),vbt->getLength()},true); + struct ReadIO { + volatile const uint8_t *base;uint64_t length; + bool readable(uint32_t reg) const {return base && length>=4 && reg<=length-4;} + uint32_t read(uint32_t reg) {return *reinterpret_cast(base+reg);} + uint64_t readPTE(uint32_t reg) {return *reinterpret_cast(base+reg);} + } io {base,length}; + XELP::DSI::auditBoot(io,normal,scratch->profile,xelpMemory,admitted,scratch->boot,scratch->audit); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIBootAudit",&scratch->audit,sizeof(scratch->audit)); + IOFree(scratch,sizeof(Scratch)); +} + +bool XE::processDisplay13DSICache(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { + const char *symbols[]={ + "__ZN31AppleIntelFramebufferController16hwUpdateRegCacheEP21AppleIntelFramebufferP21AppleIntelDisplayPath", + "__ZN31AppleIntelFramebufferController9hwGetCRTCEP21AppleIntelFramebufferP21AppleIntelDisplayPath"}; + const uint32_t offsets[]={0x4C4E2,0x4C79A}; + const uint8_t entries[][16]={ + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x54,0x53,0x48,0xFF,0x05,0x94,0x2A}, + {0x55,0x48,0x89,0xE5,0x41,0x56,0x53,0x48,0x89,0xF3,0x49,0x89,0xFE,0x48,0xFF,0x05}}; + if(!display13FramebufferRequested || size<=0x4C866)return false; + for(unsigned i=0;i<2;++i) { + const auto fn=patcher.solveSymbol(index,symbols[i],address,size);patcher.clearError(); + if(fn!=address+offsets[i] || memcmp(reinterpret_cast(fn),entries[i],16))return false; + } + KernelPatcher::RouteRequest routes[]={ + {symbols[0],wrapDSIUpdateCache,orgDSICache[0]}, {symbols[1],wrapDSIGetCRTC,orgDSICache[1]}}; + const bool ready=patcher.routeMultiple(index,routes,2,address,size);patcher.clearError();return ready; +} + +bool XE::updateDSIBootCache(IOService *controller,IOService *fb,OSObject *path) { + auto &c=display13FBPower; + if(!c.lock || !controller || !fb || !path || fb->getMetaClass()->getClassSize()<0x4B90 || + controller->getMetaClass()->getClassSize()<0x1C20 || path->getMetaClass()->getClassSize()<0x3730)return false; + DSIModeScope scope(fb);if(!scope.lock)return false; + IOLockLock(c.lock);Display13FBBootIO io{c.map}; + const auto binding=adlnInspectPathBinding(fb,path,adlnPlatform,adlnOutputWiring,false); + const bool bound=dsiCacheHooksReady && !display13Offline && dsiResourcesReady && c.map && + adlnOutputWiring.dsi==1 && controller==c.controller && controller==adlnTopologyController && + binding.status==TGLFBBinding::Status::Complete && binding.pipe==0 && binding.ddi==0 && + c.transcoders.dsi0Pipe==0 && !c.memoryFaulted && !c.clockFaulted && + !c.bridge.state.faulted && !c.bridge.state.suspended && + Display13FBMemory::field(path,0x3648)==c.dsiPort && c.dsiPort && + Display13FBMemory::field(c.dsiPort,0x550)==1 && + Display13FBMemory::field(path,0x34)==0x4A480 && + c.bootDSI.sameSource(dsiBoot.source) && c.bridge.outputHeld(io,0,0); + bool ready=false; + if(bound) { + auto plane=Display13FBMemory::field(path,0x32C8); + auto scaler=Display13FBMemory::field(path,0x32C0); + bool addresses=true; + for(unsigned i=0;i<6;++i)addresses=addresses && + Display13FBMemory::field(plane,XELP::DSI::NativeCache::PlaneAddressFields[i])==XELP::DSI::NativeCache::PlaneRegisters[i]; + uint32_t scale=0; + if(scaler) { + addresses=addresses && scaler->getMetaClass()->getClassSize()>=0x50; + if(addresses) { + scale=Display13FBMemory::field(scaler,0x30); + addresses=Display13FBMemory::field(scaler,0x28)==c.access && + Display13FBMemory::field(scaler,0x18)==0 && (scale==0x68180 || scale==0x68280) && + Display13FBMemory::field(scaler,0x34)==scale-0x10 && + Display13FBMemory::field(scaler,0x38)==scale-0xC; + } + } + XELP::DSI::NativeCache::View view; + ready=addresses && (dsiPublishedTarget(fb) ? XELP::DSI::NativeCache::readActive(io,dsiBoot,scale,view) : + XELP::DSI::NativeCache::read(io,dsiBoot,xelpMemory,scale,view)); + if(ready)XELP::DSI::NativeCache::publish(view,path,plane,scaler); + } + IOLockUnlock(c.lock);return ready; +} + +void XE::wrapDSIUpdateCache(IOService *controller,IOService *fb,OSObject *path) { + if(callbackXE->adlnOutputWiring.dsi && dsiCachePath(path)) { + if(!callbackXE->updateDSIBootCache(controller,fb,path)) { + auto &c=display13FBPower; + if(c.lock){IOLockLock(c.lock);adlnFault(c.memoryFaulted,__LINE__);IOLockUnlock(c.lock);} + } + return; + } + FunctionCast(wrapDSIUpdateCache,callbackXE->orgDSICache[0])(controller,fb,path); +} + +void XE::wrapDSIGetCRTC(IOService *controller,IOService *fb,OSObject *path) { + if(callbackXE->adlnOutputWiring.dsi && dsiCachePath(path)) { + // DSI brightness belongs to its panel backend. The donor's internal + // FB branch divides by an ordinary PWM period that may be zero here. + wrapDSIUpdateCache(controller,fb,path);return; + } + FunctionCast(wrapDSIGetCRTC,callbackXE->orgDSICache[1])(controller,fb,path); +} + +bool XE::processDisplay13DSIBoot(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { + const char *symbol="__ZN31AppleIntelFramebufferController13probeBootPipeEPbPN17AppleIntelPortHAL3DDIE"; + const uint8_t entry[]={0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x50,0x49,0x89}; + // setupBootDisplay consumes the returned pipe, builtin bool and DDI before + // allocating the real native path. Do not replace that allocation with a + // fabricated path. Its DSI cache/stop adapters remain separate prerequisites. + const uint8_t call[]={0xE8,0xA7,0x1D,0x00,0x00}; + if(!display13FramebufferRequested || size<=0x455E8)return false; + const auto fn=patcher.solveSymbol(index,symbol,address,size);patcher.clearError(); + if(fn!=address+0x453D6 || memcmp(reinterpret_cast(fn),entry,sizeof(entry)) || + memcmp(reinterpret_cast(address+0x4362A),call,sizeof(call)))return false; + KernelPatcher::RouteRequest route(symbol,wrapDSIBootProbe,orgDSIBootProbe); + const bool ready=patcher.routeMultiple(index,&route,1,address,size);patcher.clearError();return ready; +} + +uint32_t XE::wrapDSIBootProbe(IOService *controller,bool *builtin,uint32_t *ddi) { + auto self=callbackXE; + if(!self->adlnOutputWiring.dsi) + return FunctionCast(wrapDSIBootProbe,self->orgDSIBootProbe)(controller,builtin,ddi); + // Failure must not leak an old builtin/DDI result to setupBootDisplay. + if(builtin)*builtin=false;if(ddi)*ddi=UINT32_MAX; + auto &c=display13FBPower; + if(!self->dsiBootProbeReady || display13Offline || !c.lock || !controller || + controller->getMetaClass()->getClassSize()<0x1C20 || + !__atomic_load_n(&self->dsiResourcesReady,__ATOMIC_ACQUIRE))return 0xFFFF; + IOLockLock(c.lock);Display13FBBootIO io{c.map}; + auto port=c.dsiPort; + const auto config=port && port->getMetaClass()->getClassSize()>=0x6B0 ? + Display13FBMemory::field(port,0x548) : nullptr; + const bool valid=c.map && self->adlnOutputWiring.dsi==1 && controller==c.controller && + controller==self->adlnTopologyController && c.transcoders.dsi0Pipe==0 && + !c.memoryFaulted && !c.clockFaulted && !c.bridge.state.faulted && !c.bridge.state.suspended && + c.access && Display13FBTopology::bound(self->adlnPlatform,config,0) && + self->adlnOutputWiring.matches(config) && config[0]==0 && + Display13FBMemory::field(port,0x120)==0 && + Display13FBMemory::field(port,0x550)==1 && + Display13FBMemory::field(port,0x578)==c.access && + self->dsiBoot.complete() && c.bootDSI.sameSource(self->dsiBoot.source) && + c.bridge.outputHeld(io,0,0) && XELP::DSI::bootUnchanged(io,self->dsiBoot,xelpMemory); + if(valid){if(builtin)*builtin=true;if(ddi)*ddi=0;} + IOLockUnlock(c.lock);return valid?0U:0xFFFFU; +} + +// The Display13 native Mode hook has one owner for TC, Combo and DSI. +// Caller must have installed the source/mapping and native power references at +// the adoption boundary. A board record or firmware-on bits alone never admit. +uint32_t XE::setDSIPortMode(OSObject *port,uint32_t wanted) { + auto self=callbackXE;auto &c=display13FBPower; + if(!self->display13FBTCHooksReady || !self->display13TCBaseMode || !c.lock || + !port || port->getMetaClass()->getClassSize()<0x6B0)return kIOReturnNotReady; + // Mode zero must be connected to an actual DSI stop/cleanup transaction. + // Reject it for now, including when the native caller asks only for reset. + if(wanted!=1)return kIOReturnUnsupported; + IOLockLock(c.lock); + auto config=Display13FBMemory::field(port,0x548); + const auto previous=Display13FBMemory::field(port,0x550); + const bool bound=self->adlnOutputWiring.dsi==1 && self->adlnOutputWiring.matches(config) && + config[0]==0 && Display13FBTopology::bound(self->adlnPlatform,config,0) && + c.controller && c.controller==self->adlnTopologyController && c.access && + Display13FBMemory::field(port,0x578)==c.access && + Display13FBMemory::field(port,0x120)==0 && + c.transcoders.dsi(0) && c.bootDSI.complete() && c.bootDSI.identity.pipe==0 && + c.bootDSI.identity.phy==0 && c.bootDSI.identity.transcoder==0 && + (!c.dsiPort || c.dsiPort==port) && previous<=1 && + !c.memoryFaulted && !c.clockFaulted && !c.bridge.state.faulted; + Display13FBBootIO io{c.map}; + const bool ready=bound && c.bridge.outputHeld(io,0,0) && XELP::DSI::sourceUnchanged(io,c.bootDSI); + uint32_t result=kIOReturnNotReady; + if(ready) { + // Exact donor base setPortMode(1) only records port+0x550. The HAL + // setter and mode0's reset callback must never run on this path. + result=reinterpret_cast(self->display13TCBaseMode)(port,1); + if(result==kIOReturnSuccess && Display13FBMemory::field(port,0x550)!=1)result=kIOReturnNotReady; + if(result==kIOReturnSuccess && !c.dsiPort){port->retain();c.dsiPort=port;} + } + IOLockUnlock(c.lock);return result; +} + +// Called only after the real PCI/workloop IRQ source and native memory have +// been qualified. Keep the FB mutex across mode publication; never call the +// native timing setter or registry code with the display-owner lock held. +bool XE::adoptDSIBootPath(IOService *fb,OSObject *path) { + auto &c=display13FBPower;auto &irq=display13FBIRQ; + if(!c.lock || !fb || !path || fb->getMetaClass()->getClassSize()<0x4B90 || + path->getMetaClass()->getClassSize()<0x3730)return false; + DSIModeScope scope(fb);if(!scope.lock)return false; + const auto qualified=[&]() { + if(adlnOutputWiring.dsi!=1 || !dsiResourcesReady || !dsiCacheHooksReady || !dsiBootProbeReady || + display13Offline || !c.map || !c.power || !c.controller || c.controller!=adlnTopologyController || + c.pipe[0].memoryFramebuffer!=fb || c.memoryFaulted || c.clockFaulted || + c.bridge.state.faulted || c.bridge.state.suspended || c.transcoders.dsi0Pipe!=0 || !irq.lock || + !Display13Power::objectAssociations(c.power,0x80,c.controller,0x1C20))return false; + const auto binding=adlnInspectPathBinding(fb,path,adlnPlatform,adlnOutputWiring); + if(binding.status!=TGLFBBinding::Status::Complete || binding.pipe!=0 || binding.ddi!=0 || + Display13FBMemory::field(path,0x3648)!=c.dsiPort || !c.dsiPort || + Display13FBMemory::field(c.dsiPort,0x550)!=1 || + Display13FBMemory::field(path,0x3644)!=2 || + Display13FBMemory::field(fb,0x4348) || + !Display13FBMemory::field(fb,0x44DE) || !Display13FBMemory::field(fb,0x1E7))return false; + Display13FBBootIO io{c.map}; + if(!c.bridge.outputHeld(io,0,0) || !dsiBoot.complete() || + !c.bootDSI.sameSource(dsiBoot.source) || !XELP::DSI::bootUnchanged(io,dsiBoot,xelpMemory))return false; + const auto memory=display13FBInspectMemory(fb,true,0,false); + if(memory.status!=Display13FBMemory::Status::Complete || memory.control!=dsiBoot.control || + memory.stride!=dsiBoot.stride || memory.size!=dsiBoot.size || memory.surface!=dsiBoot.surface)return false; + const auto saved=IOSimpleLockLockDisableInterrupt(irq.lock); + const bool enabled=irq.controller==c.controller && (irq.admittedPipes&1) && !irq.quiescing && + irq.state.begun && irq.state.enabled && !irq.state.faulted && irq.state.transcoders.dsi0Pipe==0; + IOSimpleLockUnlockEnableInterrupt(irq.lock,saved);return enabled; + }; + IOLockLock(c.lock);const bool before=qualified();IOLockUnlock(c.lock); + bool ready=before && bindDSIFixedMode(fb,dsiProfile); + IOLockLock(c.lock); + ready=ready && qualified(); + if(ready) { + const auto key=reinterpret_cast(path); + ready=c.bridge.bootAdopted ? c.bridge.bootPath==key : + c.bridge.adoptBootPath(key,0,0).status==Display13Power::Status::Complete; + ready=ready && Display13Power::publishObject(c.bridge,c.power,0x80,c.controller,0x1C20); + } + if(!ready) { + // The setter may already have published its raw array pointer. Retain + // all backing/objects until stop; only withdraw mode readiness here. + if(dsiModeTarget(fb))dsiFixedMode.ready=false; + adlnFault(c.memoryFaulted,__LINE__); + } + IOLockUnlock(c.lock); + if(!ready && dsiModeTarget(fb))fb->setProperty("WEXDSIFixedModeReady",false); + return ready; +} + +// Called outside the owner lock, before native engine/probe/IRQ users start. +// The lock order matches clock clients: Combo clock -> display owner -> IRQ. +// Failure retains any already acquired boot power references for cleanup. +bool XE::bindDSIResources(IOService *controller) { + auto &c=display13FBPower;auto &clock=comboClockTransaction;auto &irq=display13FBIRQ; + if(!controller || controller->getMetaClass()->getClassSize()<0x1C20 || + !display13FBIRQReady || !dsiNativeWriterHooksReady || !clock.lock || !c.lock)return false; + IORecursiveLockLock(clock.lock);IOLockLock(c.lock); + struct IO : Display13FBBootIO { + explicit IO(IOMemoryMap *map):Display13FBBootIO{map} {} + void write(uint32_t reg,uint32_t value) { + if(reg==0x42084 && readable(reg)) + *reinterpret_cast(map->getVirtualAddress()+reg)=value; + } + } io{c.map}; + const bool owned=adlnOutputWiring.dsi==1 && dsiProfile.valid() && dsiBoot.complete() && + c.controller==controller && controller==adlnTopologyController && !clock.thread && + !dsiResourcesReady && c.transcoders.dsi0Pipe==UINT32_MAX && + !c.engineEnabled && !c.dbuf.begun && !c.memoryFaulted && !c.clockFaulted && + c.bridge.outputHeld(io,0,0) && XELP::DSI::bootUnchanged(io,dsiBoot,xelpMemory); + auto irqLock=Display13FBMemory::field(controller,0x90); + bool ready=false; + if(owned && irqLock) { + XELPComboClock::ClockSnapshot snapshot;snapshot.layout=XELPComboClock::Layout::Display13; + snapshot.mux[0]=io.read(0x164280); + for(unsigned p=0;p(controller),snapshot)) { + // Native X/Y tiled scanout and every external output use GGTT. + // The live DSI GOP is linear, so this shared mode can be prepared + // without disabling its plane. Retain PLL/power on any failure. + if(XELP::DSI::prepareGGTT(io,dsiBoot,xelpMemory,dsiGGTT)) { + c.bootDSI=dsiBoot.source;c.transcoders.dsi0Pipe=0; + irq.state.transcoders=c.transcoders;irq.base=c.map->getVirtualAddress();irq.lock=irqLock; + __atomic_store_n(&irq.admittedPipes,1U,__ATOMIC_RELEASE); + __atomic_store_n(&irq.controller,controller,__ATOMIC_RELEASE); + __atomic_store_n(&dsiResourcesReady,true,__ATOMIC_RELEASE);ready=true; + } else adlnFault(c.memoryFaulted,__LINE__); + } + IOSimpleLockUnlockEnableInterrupt(irqLock,saved); + } + // External hotplug may request a different CDCLK divider. Prepare it + // before native display users start; this is not an opt-in host trial. + bool prepareExternalClock=false; + if(ready && !dsiPanelOnly && Display13FBTopology::multiOutputWiring(adlnOutputWiring)) { + const auto current=Display13Clock::read(io); + ready=current.status==Display13Clock::Status::Complete; + prepareExternalClock=ready && current.nativeHz()!=current.nativeMaxHz(); + } + IOLockUnlock(c.lock);IORecursiveLockUnlock(clock.lock); + if(ready)dsiPublishIRQLive(); + if(ready && dsiCommandTest)ready=probeDSICommands(controller); + if(ready && (dsiPanelTest || prepareExternalClock))ready=probeDSIPanel(controller,prepareExternalClock); + if(ready) { + IOLockLock(c.lock); + using Light=XELP::DSI::Backlight; + if(dsiBacklight.capture(dsiProfile,io.read(Light::Control),io.read(Light::Period),io.read(Light::Duty))) { + *reinterpret_cast(reinterpret_cast(controller)+0xE58)=dsiBacklight.requested; + *reinterpret_cast(reinterpret_cast(controller)+0xE60)=dsiBacklight.pwm.period; + } + IOLockUnlock(c.lock);publishDSIBacklight(); + } + return ready; +} diff --git a/WhateverXe/kern_dsi_adapter_diagnostics.inc b/WhateverXe/kern_dsi_adapter_diagnostics.inc new file mode 100644 index 00000000..af8e1ed5 --- /dev/null +++ b/WhateverXe/kern_dsi_adapter_diagnostics.inc @@ -0,0 +1,213 @@ +// Opt-in command/panel experiments and read-only IRQ diagnostics. +// Included only by kern_xe.cpp; retains its private owners and lock ordering. + +// One optional bootstrap NOP/drain trial before native engine and IRQ users. +// This exercises the same packet engine used by panel sequences, without +// changing panel power/reset, timing, brightness, or any framebuffer state. +bool XE::probeDSICommands(IOService *controller) { + auto &c=display13FBPower;auto &clock=comboClockTransaction; + if(!controller || !c.lock || !clock.lock)return false; + IORecursiveLockLock(clock.lock);IOLockLock(c.lock); + const auto owned=[&]() { + return controller==c.controller && controller==adlnTopologyController && + dsiResourcesReady && !dsiStopAttempted && adlnOutputWiring.dsi==1 && + !c.engineEnabled && !c.memoryFaulted && !c.clockFaulted && + !c.bridge.state.faulted && !c.bridge.state.suspended && !clock.thread && + clock.allocation.external[0]==reinterpret_cast(controller) && + clock.allocation.binding[0]==dsiBoot.source.identity.pll+1; + }; + struct IO : Display13FBBootIO { + decltype(owned) &owns; + IO(IOMemoryMap *m,decltype(owned) &q):Display13FBBootIO{m},owns(q) {} + bool ready() { + if(!owns() || !readable(0x45404))return false; + const auto wells=read(0x45404);return wells!=UINT32_MAX && (wells&0x403)==0x403; + } + void write(uint32_t reg,uint32_t value) { + if(reg==0x6B100 || reg==0x6B104) + *reinterpret_cast(map->getVirtualAddress()+reg)=value; + } + void delayUs(unsigned us){IODelay(us);} + } io{c.map,owned}; + XELP::DSI::CommandPort port{io};uint64_t record[20] {2}; + // Active video can defer an LP packet until its blanking interval. The + // cold panel-sequence engine keeps its Linux 100 us default. Bound this + // running-link trial to three frames per credit wait, rounded to 10 us. + const auto &mode=dsiProfile.mode; + const uint64_t period=mode.clockKHz ? (uint64_t(mode.htotal())*mode.vtotal()*1000+mode.clockKHz-1)/mode.clockKHz : 0; + record[16]=period ? ((3*period+9)/10)*10 : 0; + uint32_t credits=0,lp=0; + record[1]=io.ready() && !dsiCommandHost.enabled && record[16]>=100 && record[16]<=100000 && dsiProfile.valid() && + c.bridge.outputHeld(io,0,0) && XELP::DSI::bootUnchanged(io,dsiBoot,xelpMemory); + if(record[1]) { + const bool empty=port.credits(credits) && port.read(0x6B0D8,lp) && + (credits&0x1FFF)==0x1040 && !(lp&((1U<<17)|(1U<<16)|1U)); + record[2]=credits;record[3]=lp;record[4]=empty; + if(empty) { + record[17]=io.read(0x70040); + dsiCommandHost.enabled=true;const uint8_t nop=0; + record[5]=dsiCommandHost.send(port,0x05,{&nop,1}); + record[6]=record[5] && dsiCommandHost.drain(port,static_cast(record[16])); + record[18]=io.read(0x70040); + } + record[7]=port.credits(credits)?credits:UINT32_MAX; + record[8]=port.read(0x6B0D8,lp)?lp:UINT32_MAX; + record[9]=io.ready() && XELP::DSI::bootUnchanged(io,dsiBoot,xelpMemory); + } + record[10]=static_cast(dsiCommandHost.error);record[11]=dsiCommandHost.pending; + record[12]=dsiCommandHost.headers;record[13]=dsiCommandHost.payloadWords; + record[14]=dsiCommandHost.waitedUs;record[15]=port.writes;record[19]=dsiBoot.source.conf; + const bool complete=record[1] && record[4] && record[6] && record[9] && + !record[10] && !record[11] && !port.failed && (record[7]&0x1FFF)==0x1040 && + record[8]!=UINT32_MAX && !(record[8]&((1U<<17)|(1U<<16)|1U)); + IOLockUnlock(c.lock);IORecursiveLockUnlock(clock.lock); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSICommandTrial",record,sizeof(record)); + return complete; +} + +// Bootstrap-only panel sequence experiment. By default retain host/PHY/PLL; +// the optional host trial rebuilds them before panel INIT_OTP. Neither path +// releases shared resources or enables runtime power transitions. +bool XE::probeDSIPanel(IOService *controller,bool prepareExternalClock) { + const bool restart=dsiHostTest || prepareExternalClock; + auto &c=display13FBPower;auto &clock=comboClockTransaction;auto &irq=display13FBIRQ; + if(!controller || !c.lock || !clock.lock || !irq.lock)return false; + IORecursiveLockLock(clock.lock);IOLockLock(c.lock); + const auto owned=[&]() { + return controller==c.controller && controller==adlnTopologyController && + dsiResourcesReady && !dsiStopAttempted && adlnOutputWiring.dsi==1 && + !c.engineEnabled && !c.memoryFaulted && !c.clockFaulted && + !c.bridge.state.faulted && !c.bridge.state.suspended && !clock.thread && + clock.allocation.external[0]==reinterpret_cast(controller) && + clock.allocation.binding[0]==dsiBoot.source.identity.pll+1 && + irq.controller==controller && !irq.state.begun && !irq.state.enabled && + !irq.quiescing && !irq.handlers && !irq.softwareHandlers; + }; + struct IO : Display13FBBootIO { + decltype(owned) &owns;IOSimpleLock *irqLock; + bool restart {};unsigned pll {};uint32_t writes {},gpios {};XELP::DSI::Panel::GPIOResult lastGPIO; + IO(IOMemoryMap *m,decltype(owned) &q,IOSimpleLock *l,bool cold,unsigned id):Display13FBBootIO{m},owns(q),irqLock(l),restart(cold),pll(id) {} + bool ready() { + if(!owns() || !readable(0x45404))return false; + const auto wells=read(0x45404);return wells!=UINT32_MAX && (wells&0x403)==0x403; + } + void write(uint32_t reg,uint32_t value) { + if(restart && XELP::DSI::Restart::writable(reg,pll)) { + *reinterpret_cast(map->getVirtualAddress()+reg)=value;++writes;return; + } + switch(reg) { + case 0x6B100:case 0x6B104:case 0xC4030:case 0xC7204:case 0xC7304: + case 0x70180:case 0x7019C:case 0x7B008: + *reinterpret_cast(map->getVirtualAddress()+reg)=value;++writes;break; + } + } + bool gpio(unsigned role,unsigned value) { + // Only this short RMW is under the IRQ lock. All VBT delays and + // FIFO/frame polling execute with interrupts enabled. + const auto saved=IOSimpleLockLockDisableInterrupt(irqLock); + lastGPIO=XELP::DSI::Panel::gpio(*this,role,value); + IOSimpleLockUnlockEnableInterrupt(irqLock,saved); + if(lastGPIO.complete)++gpios; + return lastGPIO.complete; + } + void delayUs(unsigned us){IODelay(us);} + } io{c.map,owned,irq.lock,restart,dsiBoot.source.identity.pll}; + Display13Clock::Change bootCDClock; + struct Lifecycle { + bool enabled,prepareExternal;XELPComboClock::ClockAllocation &allocation;uintptr_t owner; + XELP::DSI::Restart::State &state;Display13Clock::Change &cdclk; + bool stop(IO &io,const XELP::DSI::Readout &source,const XELP::DSI::Profile &profile) { + return !enabled || XELP::DSI::Restart::stop(io,source,profile,allocation,owner,state); + } + bool start(IO &io,XELP::DSI::Readout &source) { + if(!enabled)return true; + // Native external modes request the maximum CDCLK. Prepare that + // shared clock while the bootstrap cycle has stopped DSI and all + // other consumers, before native mode setting/IRQ users start. + // The DSI link PLL remains a separate retained allocation. + if(prepareExternal) { + auto &c=display13FBPower; + Display13FBClockIO clockIO {c.map}; + const auto current=Display13Clock::read(clockIO); + cdclk.before=current;cdclk.requestedHz=current.nativeMaxHz(); + const bool stopped=io.ready() && state.stopped && !state.started && + !c.engineEnabled && Display13FBIRQ::consumersStopped(io,7,c.transcoders); + if(!stopped || current.status!=Display13Clock::Status::Complete)return false; + cdclk=Display13Clock::set(clockIO,current.nativeMaxHz(),{true,true,0}); + if(cdclk.status!=Display13Clock::ChangeStatus::Complete) { + adlnFault(c.clockFaulted,__LINE__);return false; + } + c.clock=cdclk.after;c.clockStopped=false;c.resumeClockHz=cdclk.after.nativeHz(); + auto controller=reinterpret_cast(owner); + *reinterpret_cast(reinterpret_cast(controller)+0xE98)=cdclk.after.nativeHz(); + *reinterpret_cast(reinterpret_cast(controller)+0xEA0)=cdclk.after.nativeHz(); + } + if(!XELP::DSI::Restart::start(io,allocation,owner,state))return false; + source=XELP::DSI::Restart::expected(source,state);return source.complete(); + } + } lifecycle{restart,!dsiPanelOnly,clock.allocation,reinterpret_cast(controller),dsiHostRestart,bootCDClock}; + uint64_t record[32] {1}; + record[1]=io.ready() && !dsiPanelTrial.attempted && dsiProfile.valid() && + !dsiCommandHost.faulted && !dsiCommandHost.pending && + c.bridge.outputHeld(io,0,0) && XELP::DSI::bootUnchanged(io,dsiBoot,xelpMemory); + if(record[1]) { + record[20]=io.read(0xC7204);record[22]=io.read(0xC4030); + record[24]=io.read(0xC7200);record[28]=io.read(0x70040); + XELP::DSI::Panel::cycle(io,dsiBoot.source,dsiProfile,dsiBoot.control,dsiBoot.surface,dsiPanelTrial,lifecycle); + record[21]=io.ready()?io.read(0xC7204):UINT32_MAX; + record[23]=io.ready()?io.read(0xC4030):UINT32_MAX; + record[25]=io.ready()?io.read(0xC7200):UINT32_MAX; + record[26]=io.ready()?io.read(0x6B0D0):UINT32_MAX; + record[27]=io.ready()?io.read(0x6B0D8):UINT32_MAX; + record[29]=io.ready()?io.read(0x70040):UINT32_MAX; + // Publish the new clock/transport witness only after the complete + // cycle and old backing/geometry have been revalidated together. + auto nextBoot=dsiBoot; + if(dsiPanelTrial.complete())nextBoot.source=dsiPanelTrial.source; + record[30]=io.ready() && dsiPanelTrial.complete() && XELP::DSI::bootUnchanged(io,nextBoot,xelpMemory); + if(record[30]){dsiBoot=nextBoot;c.bootDSI=nextBoot.source;} + } + const auto &r=dsiPanelTrial;const auto &h=r.host; + record[2]=static_cast(r.stage);record[3]=static_cast(r.error); + record[4]=r.reg;record[5]=r.value;record[6]=io.writes;record[7]=r.waitUs; + record[8]=static_cast(h.error);record[9]=h.pending;record[10]=h.sequenceId; + record[11]=h.operationOffset;record[12]=h.headers;record[13]=h.payloadWords; + record[14]=h.waitedUs;record[15]=h.completedOperations;record[16]=io.gpios; + record[17]=io.lastGPIO.role;record[18]=io.lastGPIO.value;record[19]=io.lastGPIO.complete; + const bool complete=record[1] && r.complete() && record[30];record[31]=complete; + const auto &host=dsiHostRestart;const auto &p=host.config; + uint64_t hostRecord[20] {1,restart,static_cast(host.stage),static_cast(host.error), + host.reg,host.value,host.writes,host.waitUs,host.stopped,host.started,host.pll, + p.pll.cfg0(),p.pll.cfg1(),p.link.clockTiming,p.link.dataTiming,p.hsTimeout,p.lpTimeout,p.taTimeout, + record[30],complete}; + IOLockUnlock(c.lock);IORecursiveLockUnlock(clock.lock); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIPanelTrial",record,sizeof(record)); + if(restart)IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIHostRestart",hostRecord,sizeof(hostRecord)); + if(restart && !dsiPanelOnly) + IORegistryEntry::getRegistryRoot()->setProperty("N97FBDSIBootCDClock",&bootCDClock,sizeof(bootCDClock)); + return complete; +} + +namespace { +// PowerInit retains the native controller (including its IRQ lock) until reboot. +// Read only the counters already collected by the IRQ handler. Never refresh +// the register cache, power a well, or call a native driver from this serializer. +static bool dsiIRQLiveSerialize(void *,void *,OSSerialize *serializer) { + auto &c=display13FBIRQ; + if(!__atomic_load_n(&c.controller,__ATOMIC_ACQUIRE) || !c.lock)return false; + uint64_t record[32] {}; + const auto saved=IOSimpleLockLockDisableInterrupt(c.lock); + adlnIRQRecord(record,3); + IOSimpleLockUnlockEnableInterrupt(c.lock,saved); + auto data=OSData::withBytes(record,sizeof(record));if(!data)return false; + const bool ok=data->serialize(serializer);data->release();return ok; +} + +static void dsiPublishIRQLive() { + auto property=OSSerializer::forTarget(nullptr,dsiIRQLiveSerialize); + if(property) { + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIIRQLive",property); + property->release(); + } +} +} diff --git a/WhateverXe/kern_dsi_adapter_lifecycle.inc b/WhateverXe/kern_dsi_adapter_lifecycle.inc new file mode 100644 index 00000000..f258b1f4 --- /dev/null +++ b/WhateverXe/kern_dsi_adapter_lifecycle.inc @@ -0,0 +1,881 @@ +// Native writer admission, brightness, display power and path retirement. +// Included only by kern_xe.cpp; retains its private owners and lock ordering. + +// Caller holds the native FB mutex through the admitted writer. quiesce uses +// the same mutex, so a stop cannot interleave after this check. This is one +// update boundary, not a certificate for other native writers or Session Stop. +bool XE::dsiNativeUpdateAllowed(IOService *fb) { + if(!callbackXE->adlnOutputWiring.dsi)return true; + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return false; + const auto path=Display13FBMemory::singlePath(fb); + if(!dsiLifecycleTarget(fb,path))return true; + auto &c=display13FBPower;if(!c.lock || dsiFixedMode.changingThread || dsiFixedMode.presentationFault)return false; + IOLockLock(c.lock); + const bool ready=(!dsiPendingTarget(fb) || (dsiPublishedTarget(fb) && dsiPendingBacking.modeReady)) && !callbackXE->dsiStopAttempted && callbackXE->dsiResourcesReady && + !c.memoryFaulted && !c.clockFaulted && !c.bridge.state.faulted && !c.bridge.state.suspended && + !c.dbuf.faulted && !c.watermarkFaulted && c.pipe[0].memoryFramebuffer==fb && !c.pipe[0].destroyingPath && + c.transcoders.dsi0Pipe==0 && path && path->getMetaClass()->getClassSize()>=0x3730 && + Display13FBMemory::field(fb,0x1D0)==c.controller && + Display13FBMemory::field(path,0x32B0)==fb && Display13FBMemory::field(path,0x3640)==0 && + Display13FBMemory::field(path,0x3644)==2 && c.dsiPort && c.dsiPort->getMetaClass()->getClassSize()>=0x580 && + Display13FBMemory::field(path,0x3648)==c.dsiPort && Display13FBMemory::field(c.dsiPort,0x550)==1; + IOLockUnlock(c.lock);return ready; +} + +// Owner lock held. DSI reserves pipe A even after its mapping is +// withdrawn. Late shared callbacks must not revive it or fault peer outputs. +// An unbound/ambiguous callback cannot establish a different output owner. +bool XE::dsiSharedWriterStopped(IOService *fb,unsigned pipe) { + return callbackXE->adlnOutputWiring.dsi && ((dsiPendingTarget(fb) && !dsiPendingBacking.modeReady) || + (callbackXE->dsiStopAttempted && (pipe==0 || pipe>=3 || dsiModeTarget(fb)))); +} + +// Cursor image/state bodies can wait for a frame, so do not extend their +// native FB-lock intervals. Count admitted DSI calls instead. Stop takes the +// same FB mutex before testing the count and cannot start until it is zero. +int XE::beginDSINativeWriter(IOService *controller,IOService *fb) { + if(!callbackXE->adlnOutputWiring.dsi)return 0; + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return -1; + DSIModeScope scope(fb);if(!scope.lock)return -1; + if(!dsiLifecycleTarget(fb,Display13FBMemory::singlePath(fb)))return 0; + if(!callbackXE->dsiNativeWriterHooksReady || !dsiNativeUpdateAllowed(fb))return -1; + auto &c=display13FBPower;IOLockLock(c.lock); + const bool admitted=controller==c.controller && c.dsiNativeWriters!=UINT32_MAX; + if(admitted)++c.dsiNativeWriters; + IOLockUnlock(c.lock);return admitted ? 1 : -1; +} + +void XE::endDSINativeWriter(int token) { + if(token!=1)return; + auto &c=display13FBPower;IOLockLock(c.lock); + if(c.dsiNativeWriters)--c.dsiNativeWriters; + else adlnFault(c.memoryFaulted,__LINE__); + IOLockUnlock(c.lock); +} + +uint32_t XE::wrapDSICursorImage(IOService *controller,IOService *fb,void *info) { + const int token=beginDSINativeWriter(controller,fb);if(token<0)return kIOReturnNotReady; + const auto result=FunctionCast(wrapDSICursorImage,callbackXE->orgDSICursor[0])(controller,fb,info); + endDSINativeWriter(token);return result; +} + +uint32_t XE::wrapDSICursorState(IOService *controller,IOService *fb,int32_t x,int32_t y,bool visible,bool commit) { + const int token=beginDSINativeWriter(controller,fb);if(token<0)return kIOReturnNotReady; + const auto result=FunctionCast(wrapDSICursorState,callbackXE->orgDSICursor[1])(controller,fb,x,y,visible,commit); + endDSINativeWriter(token);return result; +} + +// Transaction bodies may wait and may call armUpdate, cursor or color hooks. +// Count the complete body without changing its native lock/wait intervals. +int XE::beginDSIFramebufferWriter(IOService *fb) { + if(!callbackXE->adlnOutputWiring.dsi)return 0; + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return -1; + return beginDSINativeWriter(Display13FBMemory::field(fb,0x1D0),fb); +} + +uint32_t XE::wrapDSIFlipTransaction(IOService *fb,void *transaction,uint64_t options,void *params) { + const int token=beginDSIFramebufferWriter(fb);if(token<0)return kIOReturnNotReady; + const auto result=FunctionCast(wrapDSIFlipTransaction,callbackXE->orgDSITransaction[0])(fb,transaction,options,params); + endDSINativeWriter(token);return result; +} + +uint32_t XE::wrapDSIGammaTransaction(IOService *fb,void *transaction,void *params) { + const int token=beginDSIFramebufferWriter(fb);if(token<0)return kIOReturnNotReady; + const auto result=FunctionCast(wrapDSIGammaTransaction,callbackXE->orgDSITransaction[1])(fb,transaction,params); + endDSINativeWriter(token);return result; +} + +uint32_t XE::wrapDSICscTransaction(IOService *fb,void *transaction,void *matrix,uint64_t options,bool final) { + const int token=beginDSIFramebufferWriter(fb);if(token<0)return kIOReturnNotReady; + const auto result=FunctionCast(wrapDSICscTransaction,callbackXE->orgDSITransaction[2])(fb,transaction,matrix,options,final); + endDSINativeWriter(token);return result; +} + +uint32_t XE::wrapDSIPlaneGammaTransaction(IOService *fb,void *transaction,bool post) { + const int token=beginDSIFramebufferWriter(fb);if(token<0)return kIOReturnNotReady; + const auto result=FunctionCast(wrapDSIPlaneGammaTransaction,callbackXE->orgDSITransaction[3])(fb,transaction,post); + endDSINativeWriter(token);return result; +} + +uint32_t XE::wrapDSISetGammaTable(IOService *fb,uint32_t channels,uint32_t count,uint32_t bits,void *data,bool sync) { + const int token=beginDSIFramebufferWriter(fb);if(token<0)return kIOReturnNotReady; + const auto result=FunctionCast(wrapDSISetGammaTable,callbackXE->orgDSITransaction[4])(fb,channels,count,bits,data,sync); + endDSINativeWriter(token);return result; +} + +// Power/backlight use DSI owners. Block only the donor branches that enter +// unsupported native transport controls; let ordinary OS attributes reach +// their native implementations under the existing writer lifetime. +uint32_t XE::wrapDSISetAttribute(IOService *fb,uint32_t attribute,uintptr_t value) { + recordDSIClient(fb,7,attribute,UINT32_MAX); + const auto result=[&]() -> uint32_t { + int token=0; + if(callbackXE->adlnOutputWiring.dsi) { + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return kIOReturnNotReady; + DSIModeScope scope(fb);if(!scope.lock)return kIOReturnNotReady; + if(dsiLifecycleTarget(fb,Display13FBMemory::singlePath(fb))) { + if(attribute==0x706F7772 || attribute==0x70777273) { // powr / IOGraphics pwrs relay. + if(value>UINT32_MAX)return kIOReturnUnsupported; + return wrapDSISetPowerState(fb,static_cast(value)); + } + if(attribute==0x626B7376)return callbackXE->saveDSIBrightness(fb,value); + if(XELP::DSI::AppleAdapter::transportAttribute(attribute))return kIOReturnUnsupported; + token=beginDSIFramebufferWriter(fb);if(token<0)return kIOReturnNotReady; + } + } + const auto result=FunctionCast(wrapDSISetAttribute,callbackXE->orgDSIAttributes[0])(fb,attribute,value); + endDSINativeWriter(token);return result; + }(); + recordDSIClient(fb,7,attribute,result); + recordDSIAttribute(fb,7,attribute,value,result); + return result; +} + +// Bounded CPU-only history: the last attribute alone hides earlier rejected UI +// requests behind a successful gamma flush. No MMIO or object pointers are exposed. +void XE::recordDSIAttribute(IOService *fb,unsigned event,uint32_t attribute,uintptr_t value,uint32_t result) { + auto s=callbackXE;auto &c=display13FBPower; + if(!s->adlnOutputWiring.dsi || !dsiModeTarget(fb) || !c.lock)return; + DSIModeScope scope(fb);if(!scope.lock)return; + static uint64_t history[2+16*8] {1,0}; + IOLockLock(c.lock); + auto controller=Display13FBMemory::field(fb,0x1D0); + if(controller && controller==c.controller && controller==s->adlnTopologyController) { + const auto sequence=++history[1];auto row=history+2+((sequence-1)%16)*8; + row[0]=sequence;row[1]=event;row[2]=attribute;row[3]=value<=UINT32_MAX?value:UINT64_MAX;row[4]=result; + row[5]=Display13FBMemory::field(controller,0xE58); + row[6]=Display13FBMemory::field(controller,0xE5C); + row[7]=Display13FBMemory::field(fb,0x44E0); + } + IOLockUnlock(c.lock); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIAttributeHistory",history,sizeof(history)); +} + +void XE::publishDSIBacklight() { + auto &c=display13FBPower;if(!c.lock)return; + IOLockLock(c.lock);const auto &b=dsiBacklight; + uint64_t receipt[]={1,b.initialized,b.faulted,b.requested,b.applied,b.pwm.period,b.minimum, + b.control,b.requests,b.writes,dsiRuntimePower.sleeping}; + IOLockUnlock(c.lock); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIBacklight",receipt,sizeof(receipt)); +} + +// Exact donor setAttribute('bksv') stores its 32-bit value at controller+E5C. +// This is user preference bookkeeping, not a hardware brightness command. +uint32_t XE::saveDSIBrightness(IOService *fb,uintptr_t value) { + auto &c=display13FBPower; + if(value>UINT32_MAX)return kIOReturnBadArgument; + if(!c.lock || !dsiModeTarget(fb))return kIOReturnNotReady; + IOLockLock(c.lock); + const bool owned=dsiResourcesReady && c.controller && c.controller==adlnTopologyController && + Display13FBMemory::field(fb,0x1D0)==c.controller && c.pipe[0].memoryFramebuffer==fb; + if(owned)*reinterpret_cast(static_cast(c.controller)+0xE5C)=static_cast(value); + IOLockUnlock(c.lock);return owned?kIOReturnSuccess:kIOReturnNotReady; +} + +// The caller holds the same framebuffer mutex as stop/resume. Deferred requests +// update only software; the panel-on sequence applies PWM before its enable GPIO. +uint32_t XE::setDSIBrightness(IOService *fb,uintptr_t value) { + auto &c=display13FBPower; + if(value>XELP::DSI::Backlight::Maximum)return kIOReturnBadArgument; + if(!c.lock || !dsiModeTarget(fb))return kIOReturnNotReady; + const bool awake=dsiNativeUpdateAllowed(fb); + IOLockLock(c.lock); + const bool owned=dsiResourcesReady && dsiBacklight.initialized && !dsiRuntimePower.failed && + !dsiBacklight.faulted && !c.memoryFaulted && !c.clockFaulted && !c.watermarkFaulted && + !c.bridge.state.faulted && !c.bridge.state.suspended && !c.dbuf.faulted && + c.pipe[0].memoryFramebuffer==fb && !c.pipe[0].destroyingPath && + Display13FBMemory::field(fb,0x1D0)==c.controller && + c.controller==adlnTopologyController && adlnWriterMappingBound(); + const bool deferred=owned && dsiPowerTest && dsiRuntimePower.sleeping && dsiStopAttempted && + dsiStop.complete() && dsiPanelStop.complete && dsiIRQParked && !dsiMemoryReleased && !dsiDDIReleased; + struct IO:Display13FBBootIO { + bool owned;IO(IOMemoryMap *m,bool o):Display13FBBootIO{m},owned(o){} + bool ready(){return owned;} + void write(uint32_t reg,uint32_t value) { + if(owned && reg==XELP::DSI::Backlight::Duty && readable(reg)) + *reinterpret_cast(map->getVirtualAddress()+reg)=value; + } + } io{c.map,owned && awake && !dsiStopAttempted}; + const bool accepted=owned && (deferred || (io.ready() && c.bridge.outputHeld(io,0,0))) && dsiBacklight.request(value); + const bool complete=accepted && (deferred || dsiBacklight.apply(io)); + // The donor getter returns this 16-bit requested value with range 0..65535. + if(complete)*reinterpret_cast(static_cast(c.controller)+0xE58)=dsiBacklight.requested; + IOLockUnlock(c.lock);publishDSIBacklight(); + return complete?kIOReturnSuccess:kIOReturnNotReady; +} + +uint32_t XE::wrapDSISetConnectionAttribute(IOService *fb,int32_t connection,uint32_t attribute,uintptr_t value) { + recordDSIClient(fb,8,attribute,UINT32_MAX); + const auto result=[&]() -> uint32_t { + int token=0; + if(callbackXE->adlnOutputWiring.dsi) { + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return kIOReturnNotReady; + DSIModeScope scope(fb);if(!scope.lock)return kIOReturnNotReady; + if(dsiLifecycleTarget(fb,Display13FBMemory::singlePath(fb))) { + if(connection!=0)return kIOReturnUnsupported; + if(attribute==0x626B6C74)return callbackXE->setDSIBrightness(fb,value); + if(XELP::DSI::AppleAdapter::transportConnectionAttribute(attribute))return kIOReturnUnsupported; + token=beginDSIFramebufferWriter(fb);if(token<0)return kIOReturnNotReady; + } + } + const auto result=FunctionCast(wrapDSISetConnectionAttribute,callbackXE->orgDSIAttributes[1])(fb,connection,attribute,value); + endDSINativeWriter(token);return result; + }(); + recordDSIClient(fb,8,attribute,result); + recordDSIAttribute(fb,8,attribute,value,result); + return result; +} + +uint32_t XE::wrapDSISetPowerState(IOService *fb,uint32_t state) { + recordDSIClient(fb,9,state,UINT32_MAX); + const auto result=[&]() -> uint32_t { + if(callbackXE->adlnOutputWiring.dsi) { + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return kIOReturnNotReady; + DSIModeScope scope(fb);if(!scope.lock)return kIOReturnNotReady; + if(dsiLifecycleTarget(fb,Display13FBMemory::singlePath(fb))) { + if(callbackXE->dsiPowerTest && state<=2 && dsiModeTarget(fb)) { + auto controller=Display13FBMemory::field(fb,0x1D0); + auto path=Display13FBMemory::singlePath(fb); + auto &power=callbackXE->dsiRuntimePower; + const bool ready=!power.failed && (state==2 ? (power.sleeping ? callbackXE->resumeDSIPath(controller,fb,path) : dsiNativeUpdateAllowed(fb)) : + (power.sleeping ? true : (dsiNativeUpdateAllowed(fb) && callbackXE->quiesceDSIPath(controller,fb,path,true)))); + if(!ready)return kIOReturnNotReady; + *reinterpret_cast(reinterpret_cast(fb)+0x4284)=state; + return kIOReturnSuccess; + } + // One-way runtime stop experiment through the real power callback. + // Recovery is a reboot; state 2 never replays the donor's DDI path. + if(callbackXE->dsiStopTest && state<2 && dsiModeTarget(fb)) { + const bool stopped=callbackXE->dsiMemoryReleased && callbackXE->dsiDDIReleased && + callbackXE->dsiClockStop.complete() && callbackXE->dsiPanelStop.complete; + if(stopped)return kIOReturnSuccess; + if(!callbackXE->dsiNativeWriterHooksReady || !dsiNativeUpdateAllowed(fb))return kIOReturnNotReady; + auto controller=Display13FBMemory::field(fb,0x1D0); + auto path=Display13FBMemory::singlePath(fb); + if(!callbackXE->quiesceDSIPath(controller,fb,path))return kIOReturnNotReady; + *reinterpret_cast(reinterpret_cast(fb)+0x4284)=state; + return kIOReturnSuccess; + } + // Match only the donor's already-Wake no-op. Do not publish a new + // native power state without executing a DSI power transaction. + if(state!=2 || Display13FBMemory::field(fb,0x4284)!=2)return kIOReturnUnsupported; + return callbackXE->dsiNativeWriterHooksReady && dsiNativeUpdateAllowed(fb) ? kIOReturnSuccess : kIOReturnNotReady; + } + } + return FunctionCast(wrapDSISetPowerState,callbackXE->orgDSIAttributes[2])(fb,state); + }(); + recordDSIClient(fb,9,state,result); + return result; +} + +uint32_t XE::wrapDSISetStream(IOService *controller,IOService *fb,OSObject *path,const void *timing) { + if(callbackXE->adlnOutputWiring.dsi) { + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return kIOReturnNotReady; + DSIModeScope scope(fb);if(!scope.lock)return kIOReturnNotReady; + if(dsiLifecycleTarget(fb,path))return kIOReturnNotReady; + } + return FunctionCast(wrapDSISetStream,callbackXE->orgDSIAttributes[3])(controller,fb,path,timing); +} + +bool XE::processDSINativeAttributes(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { + const char *symbols[]={ + "__ZN21AppleIntelFramebuffer12setAttributeEjm", + "__ZN21AppleIntelFramebuffer25setAttributeForConnectionEijm", + "__ZN21AppleIntelFramebuffer24setFramebufferPowerStateEj", + "__ZN31AppleIntelFramebufferController11hwSetStreamEP21AppleIntelFramebufferP21AppleIntelDisplayPathPK29IODetailedTimingInformationV2"}; + const uint32_t offsets[]={0x3331E,0x352D4,0x24DE8,0x50DB0}; + const uint8_t entries[][20]={ + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC,0x18,0x48,0xFF,0x05}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC,0x18,0x48,0xFF,0x05}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x53,0x50,0x41,0x89,0xF6,0x49,0x89,0xFF,0x48,0xFF,0x05,0xF1}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x81,0xEC,0x68,0x01,0x00,0x00}}; + if(!display13FramebufferRequested || size<0x50DC4)return false; + for(unsigned i=0;i<4;++i) { + const auto fn=patcher.solveSymbol(index,symbols[i],address,size);patcher.clearError(); + if(fn!=address+offsets[i] || memcmp(reinterpret_cast(fn),entries[i],20))return false; + } + // Standard routes copy 17/17/16/20-byte prologues, before RIP counters. + KernelPatcher::RouteRequest routes[]={ + {symbols[0],wrapDSISetAttribute,orgDSIAttributes[0]}, + {symbols[1],wrapDSISetConnectionAttribute,orgDSIAttributes[1]}, + {symbols[2],wrapDSISetPowerState,orgDSIAttributes[2]}, + {symbols[3],wrapDSISetStream,orgDSIAttributes[3]}}; + const bool ready=patcher.routeMultiple(index,routes,arrsize(routes),address,size);patcher.clearError();return ready; +} + +bool XE::processDSINativeTransactions(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { + const char *symbols[]={ + "__ZN21AppleIntelFramebuffer22PerformFlipTransactionEP30IOAccelDisplayPipeTransaction2yP21FlipTransactionParams", + "__ZN21AppleIntelFramebuffer23PerformGammaTransactionEP30IOAccelDisplayPipeTransaction2P20GammaTransactionArgs", + "__ZN21AppleIntelFramebuffer21PerformCscTransactionEP30IOAccelDisplayPipeTransaction2P29IOAccelDisplayPipeColorMatrixyb", + "__ZN21AppleIntelFramebuffer28PerformPlaneGammaTransactionEP30IOAccelDisplayPipeTransaction2b", + "__ZN21AppleIntelFramebuffer13setGammaTableEjjjPvb"}; + const uint32_t offsets[]={0x2B602,0x2C800,0x2D03C,0x2D838,0x36E72}; + // Whole prologues; no relocated RIP-relative counter in these 17 bytes. + const uint8_t entries[][17]={ + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC,0x48}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC,0x38}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC,0x38}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC,0x68}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC,0x58}}; + if(!display13FramebufferRequested || size<0x36E83)return false; + for(unsigned i=0;i<5;++i) { + const auto fn=patcher.solveSymbol(index,symbols[i],address,size);patcher.clearError(); + if(fn!=address+offsets[i] || memcmp(reinterpret_cast(fn),entries[i],17))return false; + } + KernelPatcher::RouteRequest routes[]={ + {symbols[0],wrapDSIFlipTransaction,orgDSITransaction[0]}, + {symbols[1],wrapDSIGammaTransaction,orgDSITransaction[1]}, + {symbols[2],wrapDSICscTransaction,orgDSITransaction[2]}, + {symbols[3],wrapDSIPlaneGammaTransaction,orgDSITransaction[3]}, + {symbols[4],wrapDSISetGammaTable,orgDSITransaction[4]}}; + const bool ready=patcher.routeMultiple(index,routes,arrsize(routes),address,size);patcher.clearError();return ready; +} + +bool XE::processDSINativeWriters(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { + const char *symbols[]={ + "__ZN31AppleIntelFramebufferController16hwSetCursorImageEP21AppleIntelFramebufferP20IOHardwareCursorInfo", + "__ZN31AppleIntelFramebufferController16hwSetCursorStateEP21AppleIntelFramebufferiibb"}; + const uint32_t offsets[]={0x558F6,0x55DA8}; + const uint8_t entries[][20]={ + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC,0x28,0x49,0x89,0xD4}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC,0x38,0x45,0x89,0xCE}}; + if(!display13FramebufferRequested || size<=0x55DBC)return false; + for(unsigned i=0;i<2;++i) { + const auto fn=patcher.solveSymbol(index,symbols[i],address,size);patcher.clearError(); + if(fn!=address+offsets[i] || memcmp(reinterpret_cast(fn),entries[i],20))return false; + } + KernelPatcher::RouteRequest routes[]={ + {symbols[0],wrapDSICursorImage,orgDSICursor[0]}, + {symbols[1],wrapDSICursorState,orgDSICursor[1]}}; + const bool ready=patcher.routeMultiple(index,routes,arrsize(routes),address,size);patcher.clearError();return ready; +} + +// The whole freeDisplayPathResources body also touches global audio and can +// reset a normal DDI port. Keep only its audited CPU leaves for the DSI path. +bool XE::processDSIPathDisposal(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { + const char *symbols[]={ + "__ZN31AppleIntelFramebufferController9freePlaneEP15AppleIntelPlane", + "__ZN31AppleIntelFramebufferController10freeScalerEP16AppleIntelScaler", + "__ZN21AppleIntelFramebuffer17detachDisplayPathEP21AppleIntelDisplayPath", + "__ZN21AppleIntelFramebuffer17removeDisplayPathEP21AppleIntelDisplayPath"}; + const uint32_t offsets[]={0x5751C,0x5739A,0x67FB4,0x68EA4}; + const uint8_t entries[][16]={ + {0x55,0x48,0x89,0xE5,0x48,0xFF,0x05,0xC9,0x96,0x08,0x00,0x48,0x85,0xF6,0x74,0x50}, + {0x55,0x48,0x89,0xE5,0x48,0xFF,0x05,0xEB,0x97,0x08,0x00,0x48,0x85,0xF6,0x74,0x20}, + {0x55,0x48,0x89,0xE5,0x48,0xFF,0x05,0x19,0xC5,0x07,0x00,0x48,0x8B,0x05,0x0A,0xC5}, + {0x55,0x48,0x89,0xE5,0x41,0x56,0x53,0x48,0xFF,0x05,0xEE,0xB8,0x07,0x00,0x48,0x8B}}; + if(size<=0x138BB8)return false; + mach_vm_address_t functions[4] {}; + for(unsigned i=0;i<4;++i) { + functions[i]=patcher.solveSymbol(index,symbols[i],address,size);patcher.clearError(); + if(functions[i]!=address+offsets[i] || memcmp(reinterpret_cast(functions[i]),entries[i],16))return false; + } + const auto map=patcher.solveSymbol(index,"__ZN31AppleIntelFramebufferController12fPipeToFBMapE",address,size); + patcher.clearError();if(map!=address+0x138BB0)return false; + dsiFreePlane=functions[0];dsiFreeScaler=functions[1];dsiDetachPath=functions[2];dsiRemovePath=functions[3];dsiPipeMap=map; + return true; +} + +// Caller retains FB/path, owns its recursive mutex, and has detached its DDB +// reservation. Gate ownership excludes queued native work across PG put and +// CPU disposal. A failed put leaves the native path arrays and objects intact. +bool XE::disposeDSIPath(IOService *fb,OSObject *path) { + auto &c=display13FBPower; + if(!c.lock || !fb || !path || fb->getMetaClass()->getClassSize()<0x4B90 || + path->getMetaClass()->getClassSize()<0x3730 || !dsiFreePlane || !dsiFreeScaler || + !dsiDetachPath || !dsiRemovePath || !dsiPipeMap || + !adlnWorkLoopInGate || !adlnWorkLoopTryClose || !adlnWorkLoopOpen)return false; + IOLockLock(c.lock);auto loop=c.workLoop;if(loop)loop->retain();IOLockUnlock(c.lock); + if(!loop)return false; + const bool inGate=reinterpret_cast(adlnWorkLoopInGate)(loop); + const bool acquired=!inGate && reinterpret_cast(adlnWorkLoopTryClose)(loop); + if(!inGate && !acquired){loop->release();return false;} + uint64_t record[14] {1};IOLockLock(c.lock); + auto controller=static_cast(c.controller); + auto port=c.dsiPort; + auto plane=Display13FBMemory::field(path,0x32C8); + auto scaler=Display13FBMemory::field(path,0x32C0); + auto planeScaler=plane && plane->getMetaClass()->getClassSize()>=0x1C8 ? Display13FBMemory::field(plane,0x88) : nullptr; + auto pipeMap=*reinterpret_cast(dsiPipeMap); + const auto scalerOwned=[&](OSObject *o) { + return !o || (o->getMetaClass()->getClassSize()>=0x58 && + Display13FBMemory::field(o,0x10)==fb && Display13FBMemory::field(o,0x20)==1 && + Display13FBMemory::field(o,0x18)==0); + }; + bool planeTailEmpty=true;for(unsigned i=1;i<5;++i)if(Display13FBMemory::field(path,0x32C8+8*i))planeTailEmpty=false; + const auto attached=Display13FBMemory::field(fb,0x4A00); + const auto irqIdle=[&]() { + auto &irq=display13FBIRQ; + if(!controller || controller->getMetaClass()->getClassSize()<0x1C20 || !adlnIRQOwner(controller) || !irq.lock)return false; + const auto saved=IOSimpleLockLockDisableInterrupt(irq.lock); + const bool idle=!irq.state.faulted && !irq.quiescing && !irq.blockedPipes && + !(irq.state.active&1) && !(irq.admittedPipes&1) && irq.scopes.total==irq.handlers && + !irq.scopes.display && !irq.softwareHandlers && + !(__atomic_load_n(reinterpret_cast(reinterpret_cast(controller)+0xE08),__ATOMIC_ACQUIRE)&Display13FBIRQ::pipeEvents(0)) && + !__atomic_load_n(reinterpret_cast(reinterpret_cast(controller)+0xE0C),__ATOMIC_ACQUIRE) && + !(__atomic_load_n(reinterpret_cast(reinterpret_cast(controller)+0xE10),__ATOMIC_ACQUIRE)&3U); + IOSimpleLockUnlockEnableInterrupt(irq.lock,saved);return idle; + }; + const bool irqBefore=irqIdle();record[12]=irqBefore; + const bool ready=irqBefore && !c.dsiNativeWriters && adlnOutputWiring.dsi==1 && controller && controller==adlnTopologyController && + controller->getMetaClass()->getClassSize()>=0x1C20 && c.workLoop==loop && + Display13FBMemory::field(controller,0xE00)==loop && + Display13FBMemory::field(fb,0x1D0)==controller && + Display13FBMemory::field(path,0x32B0)==fb && + Display13FBMemory::field(path,0x3640)==0 && Display13FBMemory::field(path,0x3644)==1 && + c.pipe[0].memoryFramebuffer==fb && c.pipe[0].destroyingPath && !c.pipe[0].publishedScanout && !c.pipe[0].scanout.record && + !c.memoryFaulted && !c.clockFaulted && !c.bridge.state.faulted && !c.bridge.state.suspended && !c.dbuf.faulted && + !(c.dbuf.selected&1) && !c.dbuf.pipe[0].consumers && !c.dbuf.pipe[0].unretired && + dsiStop.complete() && dsiClockStop.complete() && dsiIRQParked && dsiDDIReleased && dsiMemoryReleased && !dsiPGReleased && + c.bridge.bootAdopted && c.bridge.bootPath==reinterpret_cast(path) && c.bridge.bootPort==0 && + !c.bridge.bootPGProtected && c.bridge.pgRefs[0]==1 && !c.bridge.ddiRefs[0] && + Display13FBMemory::field(fb,0x4288)==0xFF && !Display13FBMemory::field(fb,0x4348) && + port && port->getMetaClass()->getClassSize()>=0x580 && Display13FBMemory::field(path,0x3648)==port && + Display13FBMemory::field(port,0x550)==1 && Display13FBMemory::field(port,0x554)<=3 && + !Display13FBMemory::field(path,0x32B8) && planeTailEmpty && + plane && plane->getMetaClass()->getClassSize()>=0x1C8 && Display13FBMemory::field(plane,0x68)==fb && + Display13FBMemory::field(plane,0x70)==path && Display13FBMemory::field(plane,0x78)==0 && + scalerOwned(scaler) && scalerOwned(planeScaler) && + Display13FBMemory::field(fb,0x49E8)==1 && Display13FBMemory::field(fb,0x49F0)==path && + attached<=1 && Display13FBMemory::field(fb,0x4A08)==(attached ? path : nullptr) && pipeMap && pipeMap[0]==fb; + record[1]=ready;record[2]=c.bridge.pgRefs[0];record[3]=c.dbuf.selected;record[4]=attached; + auto power=c.power;IOLockUnlock(c.lock); + const bool put=ready && power && adlnPowerReference(power,2,1); + IOLockLock(c.lock); + if(put)dsiPGReleased=c.power==power && c.controller==controller && !c.bridge.state.faulted && + !c.bridge.pgRefs[0] && !c.bridge.pgLease[0].owner; + record[5]=put;record[6]=dsiPGReleased;record[7]=c.bridge.pgRefs[0]; + bool removed=false;const bool irqAfter=put && irqIdle();record[13]=irqAfter; + if(irqAfter && dsiPGReleased && !c.memoryFaulted && !c.dbuf.faulted && !c.clockFaulted && + c.pipe[0].memoryFramebuffer==fb && *reinterpret_cast(dsiPipeMap)==pipeMap && pipeMap[0]==fb) { + // These leaves only update CPU objects; no power or MMIO callback. + reinterpret_cast(dsiFreePlane)(controller,plane); + reinterpret_cast(dsiFreeScaler)(controller,scaler); + if(Display13FBMemory::field(port,0x554)==3) + *reinterpret_cast(reinterpret_cast(port)+0x554)=2; + // The normal setPortMode(0) could reset DDI. Hardware is already stopped. + if(!Display13FBMemory::field(port,0x554)) + *reinterpret_cast(reinterpret_cast(port)+0x550)=0; + *reinterpret_cast(reinterpret_cast(path)+0x3648)=nullptr; + reinterpret_cast(dsiDetachPath)(fb,path); + reinterpret_cast(dsiRemovePath)(fb,path); + const auto freedScaler=[](OSObject *o) {return !o || (!Display13FBMemory::field(o,0x10) && !Display13FBMemory::field(o,0x20));}; + removed=!Display13FBMemory::field(fb,0x49E8) && !Display13FBMemory::field(fb,0x49F0) && + !Display13FBMemory::field(fb,0x4A00) && !Display13FBMemory::field(fb,0x4A08) && !pipeMap[0] && + !Display13FBMemory::field(path,0x3644) && !Display13FBMemory::field(path,0x32B0) && + !Display13FBMemory::field(path,0x3648) && !Display13FBMemory::field(plane,0x68) && + freedScaler(scaler) && freedScaler(planeScaler); + } + record[8]=removed;record[9]=Display13FBMemory::field(fb,0x49E8); + record[10]=Display13FBMemory::field(fb,0x4A00);record[11]=Display13FBMemory::field(path,0x3644); + IOLockUnlock(c.lock); + if(removed) { + removed=releaseDSIFixedMode(fb); + if(removed) { + IOLockLock(c.lock); + removed=c.dsiPort==port; + if(removed)c.dsiPort=nullptr; + IOLockUnlock(c.lock); + if(removed)port->release(); + } + } + record[8]=removed; + if(acquired)reinterpret_cast(adlnWorkLoopOpen)(loop); + loop->release();adlnPublishMemory("N97FBDSIPathDispose",0,record,sizeof(record));return removed; +} + +// Host/IRQ/PLL retirement; the native DDI reference follows the stopped clock. +// Keep the real stopped source separate from Session cleanup: it is not a full +// Session StopProof. The one-shot journal retains a partial stop for diagnosis. +bool XE::quiesceDSIPath(IOService *controller,IOService *fb,OSObject *path,bool retain) { + auto &c=display13FBPower;auto &clock=comboClockTransaction;auto &irq=display13FBIRQ; + if(!c.lock || !clock.lock || !controller || controller->getMetaClass()->getClassSize()<0x1C20 || + !fb || fb->getMetaClass()->getClassSize()<0x4B90 || !path || path->getMetaClass()->getClassSize()<0x3730 || + !adlnWorkLoopInGate || !adlnWorkLoopTryClose || !adlnWorkLoopOpen)return false; + auto mutex=Display13FBMemory::field(fb,0x4220);if(!mutex)return false; + IORecursiveLockLock(mutex); + IOLockLock(c.lock); + auto loop=controller==c.controller ? c.workLoop : nullptr; + if(loop)loop->retain(); + IOLockUnlock(c.lock); + const bool boundLoop=loop && Display13FBMemory::field(controller,0xE00)==loop; + const bool inGate=boundLoop && reinterpret_cast(adlnWorkLoopInGate)(loop); + const bool acquired=boundLoop && !inGate && reinterpret_cast(adlnWorkLoopTryClose)(loop); + if(!inGate && !acquired) {if(loop)loop->release();IORecursiveLockUnlock(mutex);return false;} + IORecursiveLockLock(clock.lock);IOLockLock(c.lock); + uint64_t record[51] {6};record[38]=c.dsiNativeWriters;bool masked=false,maskOK=false,parked=false,restored=false,enabled=false,modeWithdrawn=false;uint32_t master=0; + const auto qualified=[&]() { + if(adlnOutputWiring.dsi!=1 || !dsiResourcesReady || !dsiNativeWriterHooksReady || c.dsiNativeWriters || (dsiPendingTarget(fb) && !dsiArmedBackingOwned(fb,path)) || !dsiProfile.valid() || + controller!=c.controller || controller!=adlnTopologyController || c.workLoop!=loop || + Display13FBMemory::field(controller,0xE00)!=loop || + c.pipe[0].memoryFramebuffer!=fb || Display13FBMemory::singlePath(fb)!=path || + c.transcoders.dsi0Pipe!=0 || c.bridge.state.faulted || c.bridge.state.suspended || c.clockFaulted || + c.pipe[0].destroyingPath || c.dbuf.updating || c.pipe[0].wmProgramming || c.pipe[0].wmCommitting || + Display13FBMemory::field(path,0x35F0) || !adlnWriterMappingBound() || + !c.dsiPort || Display13FBMemory::field(path,0x3648)!=c.dsiPort || + Display13FBMemory::field(c.dsiPort,0x550)!=1 || + Display13FBMemory::field(path,0x3644)!=2 || + clock.thread || clock.allocation.external[0]!=reinterpret_cast(controller) || + clock.allocation.binding[0]!=dsiBoot.source.identity.pll+1 || + !c.bootDSI.sameSource(dsiBoot.source))return false; + const auto binding=adlnInspectPathBinding(fb,path,adlnPlatform,adlnOutputWiring); + Display13FBBootIO io{c.map}; + return binding.status==TGLFBBinding::Status::Complete && binding.pipe==0 && binding.ddi==0 && + c.bridge.outputHeld(io,0,0); + }; + struct IO : Display13FBBootIO { + decltype(qualified) &owns;IOSimpleLock *irqLock;bool panel,retain; + IO(IOMemoryMap *m,decltype(qualified) &q,IOSimpleLock *l,bool p,bool r):Display13FBBootIO{m},owns(q),irqLock(l),panel(p),retain(r) {} + bool ready(){return owns();} + void write(uint32_t reg,uint32_t value) { + if(readable(reg) && (reg==0x70080 || reg==0x70084 || reg==0x70088 || reg==0x70180 || reg==0x7019C || + reg==0x7B008 || reg==0x6B0D8 || reg==0x6B400 || reg==0x164280 || reg==0x64000 || + reg==0x46010+4*display13FBPower.bootDSI.identity.pll || + (retain && XELP::DSI::Restart::writable(reg,display13FBPower.bootDSI.identity.pll)) || + (panel && (reg==0x6B100 || reg==0x6B104 || reg==0xC4030 || reg==0xC7204 || reg==0xC7304)))) + *reinterpret_cast(map->getVirtualAddress()+reg)=value; + } + bool gpio(unsigned role,unsigned value) { + if(!panel || !irqLock)return false; + const auto saved=IOSimpleLockLockDisableInterrupt(irqLock); + const auto r=XELP::DSI::Panel::gpio(*this,role,value); + IOSimpleLockUnlockEnableInterrupt(irqLock,saved);return r.complete; + } + void delayUs(unsigned us){if(us>=1000)IOSleep((us+999)/1000);else IODelay(us);} + } io{c.map,qualified,irq.lock,dsiStopTest || dsiPowerTest || retain,retain}; + auto &power=dsiRuntimePower; + record[1]=qualified(); + bool snapshot=!retain; + if(retain && record[1] && !dsiStopAttempted && !power.sleeping && !c.memoryFaulted && !c.dbuf.faulted && !c.watermarkFaulted && + dsiPublishedTarget(fb) && dsiPendingBacking.modeReady && dsiArmedBackingOwned(fb,path)) { + auto &p=power.plane; + p.control=io.read(0x70180);p.surface=io.read(0x7019C);p.stride=io.read(0x70188);p.size=io.read(0x70190); + p.position=io.read(0x7018C);p.offset=io.read(0x701A4);p.color=io.read(0x701CC); + power.cursorControl=io.read(0x70080);power.cursorPosition=io.read(0x70088);power.cursorBase=io.read(0x70084); + // The published aperture is not the current native flip surface. + // As in NativeCache::readActive, retain the native mode/display pipe + // and its transaction ownership; do not free/rebind it or substitute + // the initial backing. Writers are excluded through wake. Restore only + // the latched native plane image, with the supported fixed geometry. + snapshot=XELP::DSI::Surface::shape(p,dsiProfile) && XELP::DSI::Surface::matches(io,p,true) && + power.cursorControl!=UINT32_MAX && power.cursorBase!=UINT32_MAX && power.cursorPosition!=UINT32_MAX && + Display13Cursor::controlWrite(0x70080,power.cursorControl)==power.cursorControl; + if(snapshot){power.host=XELP::DSI::Restart::State{};power.panel=XELP::DSI::Panel::Result{};dsiPanelStop={};dsiStop={};} + } + record[1]=record[1] && snapshot; + if(record[1] && !dsiStopAttempted && adlnIRQOwner(controller) && irq.lock && + irq.lock==Display13FBMemory::field(controller,0x90) && irq.base==c.map->getVirtualAddress()) { + Display13FBIRQIO irqIO{irq.base}; + auto saved=IOSimpleLockLockDisableInterrupt(irq.lock); + const auto queued=[&](unsigned offset) { + return __atomic_load_n(reinterpret_cast(reinterpret_cast(controller)+offset),__ATOMIC_ACQUIRE); + }; + master=irqIO.read(Display13FBIRQ::Master);enabled=irq.state.enabled; + record[2]=irq.handlers;record[3]=irq.scopes.display;record[4]=irq.softwareHandlers; + record[5]=queued(0xE08);record[6]=queued(0xE0C);record[7]=queued(0xE10); + const bool quiet=irq.scopes.total==irq.handlers && !irq.scopes.display && !irq.softwareHandlers && + !(record[5]&Display13FBIRQ::pipeEvents(0)) && !record[6] && !(record[7]&3); + const bool ready=quiet && !irq.quiescing && !irq.blockedPipes && !irq.state.faulted && + irq.state.begun && (irq.state.configured&1) && (irq.admittedPipes&1) && irq.state.transcoders.dsi0Pipe==0 && + master!=UINT32_MAX && bool(master&Display13FBIRQ::Enable)==enabled; + if(ready) { + // DE permission is assigned on actual HW-handler entry. New shared + // GT-only handlers can still run but cannot consume display banks. + irq.quiescing=true;irq.blockedPipes=1;irq.state.enabled=false;masked=true; + irqIO.write(Display13FBIRQ::Master,0); + const auto after=irqIO.read(Display13FBIRQ::Master); + maskOK=after!=UINT32_MAX && !(after&Display13FBIRQ::Enable); + if(!maskOK)irq.state.faulted=true; + } + IOSimpleLockUnlockEnableInterrupt(irq.lock,saved); + if(maskOK) { + dsiStopAttempted=true; + if(!retain)dsiPanelStop={}; + if(dsiStopTest || dsiPowerTest || retain)dsiStop=XELP::DSI::Quiesce::run(io,c.bootDSI,dsiProfile,dsiPanelStop); + else dsiStop=XELP::DSI::Quiesce::run(io,c.bootDSI,dsiProfile); + } + if(masked) { + saved=IOSimpleLockLockDisableInterrupt(irq.lock); + record[8]=irq.handlers;record[9]=irq.scopes.display;record[10]=irq.softwareHandlers; + record[11]=queued(0xE08);record[12]=queued(0xE0C);record[13]=queued(0xE10); + if(dsiStop.complete() && irq.scopes.total==irq.handlers && !irq.scopes.display && + !irq.softwareHandlers && !record[12] && !(record[13]&3)) { + // Pass the real DE-permitted scope count. A GT-only stack does + // not own a display consumer, even when it spans this stop. + const auto status=Display13FBIRQ::parkStoppedPipe(irqIO,irq.state,0,irq.scopes.display, + irq.softwareHandlers,static_cast(record[11])); + record[14]=static_cast(status);parked=status==Display13FBIRQ::Status::Complete; + } + IOSimpleLockUnlockEnableInterrupt(irq.lock,saved); + if(parked) { + if(retain)XELP::DSI::Restart::stopQuiesced(io,c.bootDSI,dsiProfile,dsiStop,clock.allocation, + reinterpret_cast(controller),power.host); + else dsiClockStop=XELP::DSI::Clock::retire(io,c.bootDSI,dsiStop,true,clock.allocation, + reinterpret_cast(controller)); + } + saved=IOSimpleLockLockDisableInterrupt(irq.lock); + if(!retain && dsiClockStop.complete()) { + // The host and its PLL are now stopped. Withdraw both maps at + // the same masked IRQ boundary, so an external clock client + // no longer tries to re-adopt the obsolete running DSI source. + c.transcoders.dsi0Pipe=UINT32_MAX;irq.state.transcoders=c.transcoders;c.dbuf.transcoders=c.transcoders; + __atomic_fetch_and(&irq.admittedPipes,~1U,__ATOMIC_RELEASE); + __atomic_store_n(&dsiResourcesReady,false,__ATOMIC_RELEASE); + } + irq.quiescing=false;irq.blockedPipes=0; + const auto wells=irqIO.read(Display13FBIRQ::Driver); + if(wells==UINT32_MAX || (wells&3)!=3)irq.state.faulted=true; + if(!irq.state.faulted) { + irqIO.write(Display13FBIRQ::Master,master&Display13FBIRQ::Enable); + const auto after=irqIO.read(Display13FBIRQ::Master); + restored=after!=UINT32_MAX && bool(after&Display13FBIRQ::Enable)==enabled; + irq.state.enabled=restored && enabled; + if(!restored) {irq.state.faulted=true;irqIO.write(Display13FBIRQ::Master,0);} + } + adlnIRQCache(controller,irqIO);IOSimpleLockUnlockEnableInterrupt(irq.lock,saved); + } + } + if(masked)dsiIRQParked=parked && restored; + record[15]=masked;record[16]=parked;record[17]=restored;record[18]=dsiStopAttempted; + record[19]=static_cast(dsiStop.stage);record[20]=static_cast(dsiStop.error); + record[21]=dsiStop.writes;record[22]=dsiStop.waitUs;record[23]=dsiIRQParked; + record[24]=static_cast(dsiClockStop.status);record[25]=dsiClockStop.reg;record[26]=dsiClockStop.after; + record[27]=dsiClockStop.writes;record[28]=dsiClockStop.waitUs;record[29]=dsiClockStop.complete(); + // The donor's disableClocks(port,peer) puts this DDI reference at 6db04; + // PG1 belongs to freeDisplayPathResources and must remain until memory / + // DDB / path destruction. Match that ownership without running DDI/DP code. + const bool stopped=!retain && parked && restored && dsiClockStop.complete(); + record[30]=c.bridge.ddiRefs[0]; + if(stopped && c.bridge.bootAdopted && c.bridge.bootPath==reinterpret_cast(path) && + c.bridge.bootPort==0 && !c.bridge.bootDDIProtected && c.bridge.ddiRefs[0]==1 && c.power) { + auto power=c.power; + IOLockUnlock(c.lock);const bool put=adlnPowerReference(power,4,0);IOLockLock(c.lock); + dsiDDIReleased=put && c.power==power && !c.bridge.ddiRefs[0] && !c.bridge.ddiLease[0].owner; + } + record[31]=c.bridge.ddiRefs[0];record[32]=dsiDDIReleased; + const bool stoppedPath=stopped && dsiDDIReleased; + if(stoppedPath && !c.memoryFaulted && !c.dbuf.faulted) { + // Keep the real FB mutex and workloop gate while the shared native + // retirement path acquires its own allocator serializer. Neither the + // Combo mutex nor the owner lock may span native allocator callbacks. + // PG1 remains held for PTE/DDB validation; the stopped DSI PLL is free. + IOLockUnlock(c.lock);IORecursiveLockUnlock(clock.lock); + const bool released=retireDSINativeBacking(fb,path) && adlnFreeStoppedPipe(fb); + IORecursiveLockLock(clock.lock);IOLockLock(c.lock); + Display13FBDBUFIO dbufIO{c.map}; + // A native no-resource return can precede path destruction. Retire + // that path's DDB consumers too, even if no allocator call was needed. + const auto dbufStatus=released && !c.memoryFaulted ? Display13DBUF::stopPipe(dbufIO,c.dbuf,0) : Display13DBUF::Status::Unavailable; + record[35]=released;record[36]=static_cast(dbufStatus); + const bool dbufStopped=dbufStatus==Display13DBUF::Status::Complete; + bool irqRestored=false; + if(released && irq.controller==controller && irq.lock && irq.base) { + Display13FBIRQIO irqIO{irq.base};const auto saved=IOSimpleLockLockDisableInterrupt(irq.lock); + const auto wells=irqIO.read(Display13FBIRQ::Driver); + const bool powered=wells!=UINT32_MAX && (wells&3)==3; + const auto after=powered ? irqIO.read(Display13FBIRQ::Master) : UINT32_MAX; + irqRestored=!irq.state.faulted && !irq.quiescing && !irq.blockedPipes && !(irq.state.active&1) && + after!=UINT32_MAX && bool(after&Display13FBIRQ::Enable)==irq.state.enabled; + if(!irqRestored){irq.state.faulted=true;irq.state.enabled=false;if(powered)irqIO.write(Display13FBIRQ::Master,0);} + adlnIRQCache(controller,irqIO);IOSimpleLockUnlockEnableInterrupt(irq.lock,saved); + } + record[37]=irqRestored; + dsiMemoryReleased=dbufStopped && irqRestored && c.controller==controller && c.pipe[0].memoryFramebuffer==fb && + Display13FBMemory::singlePath(fb)==path && !c.memoryFaulted && + Display13FBMemory::field(fb,0x4288)==0xFF && !Display13FBMemory::field(fb,0x4348) && + !c.pipe[0].scanout.record && !__atomic_load_n(&c.pipe[0].publishedScanout,__ATOMIC_ACQUIRE) && + !c.dbuf.pipe[0].consumers && !c.dbuf.pipe[0].unretired; + } + record[39]=dsiStop.reg;record[40]=dsiStop.value; + record[41]=dsiPanelStop.stage;record[42]=dsiPanelStop.complete; + record[43]=static_cast(dsiPanelStop.host.error);record[44]=dsiPanelStop.host.sequenceId; + record[45]=dsiPanelStop.host.operationOffset;record[46]=dsiPanelStop.host.headers; + record[47]=dsiPanelStop.host.payloadWords;record[48]=dsiPanelStop.host.completedOperations; + record[49]=dsiPanelStop.host.waitedUs;record[50]=dsiPanelStop.host.pending; + record[33]=dsiMemoryReleased;record[34]=c.memoryFaulted; + const bool complete=retain ? (parked && restored && power.host.stopped && !power.host.started && + power.host.error==XELP::DSI::Restart::Error::None && dsiPanelStop.complete && qualified() && + dsiArmedBackingOwned(fb,path) && !c.memoryFaulted && !c.dbuf.faulted) : (stoppedPath && dsiMemoryReleased); + if(retain && complete)power.sleeping=true; + if(complete && !retain) { + // Exact donor disableDisplay publishes path state 1 only after its + // clock-disable call (4dbba -> 4dbc6). Mirror that CPU completion now, + // never on a partial host/IRQ/PLL stop or by replaying the donor body. + *reinterpret_cast(reinterpret_cast(path)+0x3644)=1; + *reinterpret_cast(reinterpret_cast(path)+0x170)=0; + *reinterpret_cast(reinterpret_cast(fb)+0x4270)=0; + if(dsiModeTarget(fb)){dsiFixedMode.ready=false;modeWithdrawn=true;} + } + // Native backing and the GOP pool have been retired before a full stop + // leaves PG/path disposal to releaseDisplay13Path; only partial stops + // need the global retention guard. Never clear a pre-existing real fault. + if(masked && !complete){if(retain)power.failed=true;adlnFault(c.memoryFaulted,__LINE__);} + IOLockUnlock(c.lock);IORecursiveLockUnlock(clock.lock); + if(modeWithdrawn)fb->setProperty("WEXDSIFixedModeReady",false); + if(acquired)reinterpret_cast(adlnWorkLoopOpen)(loop); + loop->release();IORecursiveLockUnlock(mutex); + adlnPublishMemory("N97FBDSIHostStop",0,record,sizeof(record)); + if(retain) { + const auto &h=power.host; + uint64_t receipt[] {1,0,complete,power.cycles,power.sleeping,snapshot, + power.plane.control,power.plane.surface,power.cursorControl,power.cursorBase, + static_cast(h.stage),static_cast(h.error),h.reg,h.value,h.writes,h.waitUs, + parked,restored,dsiPanelStop.complete,dsiMemoryReleased,dsiDDIReleased}; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIRuntimePower",receipt,sizeof(receipt)); + } + return complete; +} + +// Runtime display wake keeps the native scanout allocation and every display +// lease from the matching off transaction. System power loss is not covered. +bool XE::resumeDSIPath(IOService *controller,IOService *fb,OSObject *path) { + auto &c=display13FBPower;auto &clock=comboClockTransaction;auto &irq=display13FBIRQ;auto &power=dsiRuntimePower; + if(!c.lock || !clock.lock || !controller || controller->getMetaClass()->getClassSize()<0x1C20 || + !fb || fb->getMetaClass()->getClassSize()<0x4B90 || !path || path->getMetaClass()->getClassSize()<0x3730 || + !adlnWorkLoopInGate || !adlnWorkLoopTryClose || !adlnWorkLoopOpen)return false; + auto mutex=Display13FBMemory::field(fb,0x4220);if(!mutex)return false; + IORecursiveLockLock(mutex);IOLockLock(c.lock); + auto loop=controller==c.controller ? c.workLoop : nullptr;if(loop)loop->retain();IOLockUnlock(c.lock); + const bool boundLoop=loop && Display13FBMemory::field(controller,0xE00)==loop; + const bool inGate=boundLoop && reinterpret_cast(adlnWorkLoopInGate)(loop); + const bool acquired=boundLoop && !inGate && reinterpret_cast(adlnWorkLoopTryClose)(loop); + if(!inGate && !acquired){if(loop)loop->release();IORecursiveLockUnlock(mutex);return false;} + IORecursiveLockLock(clock.lock);IOLockLock(c.lock); + const auto owned=[&]() { + if(!power.sleeping || power.failed || !dsiStopAttempted || !dsiIRQParked || !dsiResourcesReady || + !dsiStop.complete() || !dsiPanelStop.complete || !dsiNativeWriterHooksReady || !dsiProfile.valid() || + controller!=c.controller || controller!=adlnTopologyController || c.workLoop!=loop || + Display13FBMemory::field(controller,0xE00)!=loop || adlnOutputWiring.dsi!=1 || + c.pipe[0].memoryFramebuffer!=fb || Display13FBMemory::singlePath(fb)!=path || + c.transcoders.dsi0Pipe!=0 || c.dsiNativeWriters || c.memoryFaulted || c.clockFaulted || c.watermarkFaulted || + c.bridge.state.faulted || c.bridge.state.suspended || c.dbuf.faulted || c.dbuf.updating || + c.pipe[0].destroyingPath || c.pipe[0].wmProgramming || c.pipe[0].wmCommitting || + !c.dsiPort || Display13FBMemory::field(path,0x3648)!=c.dsiPort || + Display13FBMemory::field(c.dsiPort,0x550)!=1 || Display13FBMemory::field(path,0x3644)!=2 || + Display13FBMemory::field(path,0x35F0) || !adlnWriterMappingBound() || + clock.thread || clock.allocation.external[0]!=reinterpret_cast(controller) || + clock.allocation.binding[0]!=dsiBoot.source.identity.pll+1 || !c.bootDSI.sameSource(dsiBoot.source) || + !dsiPublishedTarget(fb) || !dsiPendingBacking.modeReady || !dsiArmedBackingOwned(fb,path))return false; + Display13FBBootIO io{c.map};return c.bridge.outputHeld(io,0,0); + }; + struct IO : Display13FBBootIO { + decltype(owned) &owns;IOSimpleLock *lock;unsigned pll; + IO(IOMemoryMap *m,decltype(owned) &o,IOSimpleLock *l,unsigned p):Display13FBBootIO{m},owns(o),lock(l),pll(p) {} + bool ready(){return owns();} + void write(uint32_t r,uint32_t v) { + if(readable(r) && (XELP::DSI::Restart::writable(r,pll) || r==0x6B100 || r==0x6B104 || + r==0xC4030 || r==0xC7204 || r==0xC7304 || r==0x70180 || r==0x7019C || r==0x7B008 || + r==0x70080 || r==0x70088 || r==0x70084 || r==XELP::DSI::Backlight::Duty)) + *reinterpret_cast(map->getVirtualAddress()+r)=v; + } + bool gpio(unsigned role,unsigned value) { + const auto saved=IOSimpleLockLockDisableInterrupt(lock);const auto r=XELP::DSI::Panel::gpio(*this,role,value); + IOSimpleLockUnlockEnableInterrupt(lock,saved);return r.complete; + } + void delayUs(unsigned us){if(us>=1000)IOSleep((us+999)/1000);else IODelay(us);} + } io{c.map,owned,irq.lock,dsiBoot.source.identity.pll}; + uint64_t receipt[24] {1,1,0,power.cycles,power.sleeping},irqReceipt[24] {1}; + bool masked=false,restored=false,rearmed=false,cursor=false,complete=false;uint32_t master=0; + auto stoppedPlane=power.plane;stoppedPlane.control&=~(1U<<31); + receipt[5]=owned() && power.host.stopped && !power.host.started && !power.panel.attempted && + power.host.error==XELP::DSI::Restart::Error::None && XELP::DSI::Surface::matches(io,stoppedPlane,false); + if(receipt[5] && adlnIRQOwner(controller) && irq.lock && irq.lock==Display13FBMemory::field(controller,0x90) && + irq.base==c.map->getVirtualAddress()) { + Display13FBIRQIO irqIO{irq.base};auto saved=IOSimpleLockLockDisableInterrupt(irq.lock); + const auto queued=[&](unsigned offset) { + return __atomic_load_n(reinterpret_cast(reinterpret_cast(controller)+offset),__ATOMIC_ACQUIRE); + }; + master=irqIO.read(Display13FBIRQ::Master); + const bool quiet=irq.scopes.total==irq.handlers && !irq.scopes.display && !irq.softwareHandlers && + !(queued(0xE08)&Display13FBIRQ::pipeEvents(0)) && !queued(0xE0C) && !(queued(0xE10)&3); + if(quiet && !irq.quiescing && !irq.blockedPipes && !irq.state.faulted && irq.state.begun && irq.state.enabled && + !(irq.state.active&1) && (irq.state.configured&1) && (irq.admittedPipes&1) && irq.state.transcoders.dsi0Pipe==0 && + master!=UINT32_MAX && (master&Display13FBIRQ::Enable)) { + irq.quiescing=true;irq.blockedPipes=1;irq.state.enabled=false;masked=true; + irqIO.write(Display13FBIRQ::Master,0); + const auto after=irqIO.read(Display13FBIRQ::Master); + if(after==UINT32_MAX || (after&Display13FBIRQ::Enable))irq.state.faulted=true; + } + const bool canStart=masked && !irq.state.faulted; + IOSimpleLockUnlockEnableInterrupt(irq.lock,saved); + if(canStart) { + struct Lifecycle { + XELPComboClock::ClockAllocation &allocation;uintptr_t owner;XELP::DSI::Restart::State &state; + bool stop(IO &,const XELP::DSI::Readout &,const XELP::DSI::Profile &){return false;} + bool start(IO &io,XELP::DSI::Readout &source) { + if(!XELP::DSI::Restart::start(io,allocation,owner,state))return false; + source=XELP::DSI::Restart::expected(source,state);return source.complete(); + } + } lifecycle{clock.allocation,reinterpret_cast(controller),power.host}; + XELP::DSI::Panel::cycle(io,c.bootDSI,dsiProfile,power.plane.control,power.plane.surface,power.panel,lifecycle,&dsiPanelStop, + dsiBacklight.initialized?&dsiBacklight:nullptr); + if(power.panel.complete() && owned() && XELP::DSI::Surface::matches(io,power.plane,true)) { + io.write(0x70080,power.cursorControl);io.write(0x70088,power.cursorPosition);io.write(0x70084,power.cursorBase); + const auto before=io.read(0x70040); + for(unsigned us=0;us<=50000;us+=1000) { + const auto after=io.read(0x70040); + if(before!=UINT32_MAX && after!=UINT32_MAX && after!=before) { + cursor=XELP::DSI::Surface::word(io,0x70080,UINT32_MAX,power.cursorControl) && + XELP::DSI::Surface::word(io,0x70088,UINT32_MAX,power.cursorPosition) && + XELP::DSI::Surface::word(io,0x70084,UINT32_MAX,power.cursorBase);break; + } + if(us<50000)io.delayUs(1000); + } + if(cursor){dsiBoot.source=power.panel.source;c.bootDSI=power.panel.source;} + } + } + if(masked) { + saved=IOSimpleLockLockDisableInterrupt(irq.lock); + const auto wells=irqIO.read(Display13FBIRQ::Driver); + if(wells==UINT32_MAX || (wells&3)!=3)irq.state.faulted=true; + if(!irq.state.faulted) { + irqIO.write(Display13FBIRQ::Master,master&Display13FBIRQ::Enable); + const auto after=irqIO.read(Display13FBIRQ::Master); + restored=after!=UINT32_MAX && (after&Display13FBIRQ::Enable);irq.state.enabled=restored; + if(!restored)irq.state.faulted=true; + if(restored && cursor && owned()) { + const auto actual=Display13FBIRQ::readActivePipes(irqIO,irq.admittedPipes,irq.state.transcoders); + irqReceipt[1]=irq.admittedPipes;irqReceipt[2]=irq.state.active;irqReceipt[3]=actual.active; + irqReceipt[4]=static_cast(actual.status);irqReceipt[5]=wells; + for(unsigned p=0;p<3;++p)if(Display13FBIRQ::powered(wells,p)) { + irqReceipt[6+3*p]=Display13Transcoder::control(irqIO,p,irq.state.transcoders); + irqReceipt[7+3*p]=irqIO.read(Display13FBIRQ::PipeIIR(p)+4); + irqReceipt[8+3*p]=irqIO.read(Display13FBIRQ::PipeIIR(p)-4); + } + const auto status=Display13FBIRQ::extendActivePipes(irqIO,irq.state,irq.admittedPipes); + irqReceipt[15]=static_cast(status);irqReceipt[16]=irq.state.active; + irqReceipt[17]=static_cast(irq.state.firstFailure); + irqReceipt[18]=irq.state.failureRegister;irqReceipt[19]=irq.state.failureBits; + irqReceipt[20]=irq.state.enabled;irqReceipt[21]=irq.state.faulted; + rearmed=status==Display13FBIRQ::Status::Complete && (irq.state.active&1); + } + } + irq.quiescing=false;irq.blockedPipes=0; + adlnIRQCache(controller,irqIO);IOSimpleLockUnlockEnableInterrupt(irq.lock,saved); + complete=restored && rearmed && cursor && power.panel.complete() && owned(); + if(complete){power.sleeping=false;++power.cycles;dsiStopAttempted=false;dsiIRQParked=false;} + else{power.failed=true;adlnFault(c.memoryFaulted,__LINE__);} + } + } + const auto &h=power.host;const auto &p=power.panel; + receipt[2]=complete;receipt[3]=power.cycles;receipt[4]=power.sleeping; + receipt[6]=power.plane.control;receipt[7]=power.plane.surface;receipt[8]=power.cursorControl;receipt[9]=power.cursorBase; + receipt[10]=static_cast(h.stage);receipt[11]=static_cast(h.error); + receipt[12]=h.reg;receipt[13]=h.value;receipt[14]=h.writes;receipt[15]=h.waitUs; + receipt[16]=masked;receipt[17]=restored;receipt[18]=rearmed;receipt[19]=cursor; + receipt[20]=static_cast(p.stage);receipt[21]=static_cast(p.error);receipt[22]=p.reg;receipt[23]=p.value; + const auto &commands=p.host; + uint64_t commandReceipt[] {1,static_cast(commands.error),commands.pending,commands.faulted, + commands.sequenceId,commands.operationOffset,commands.headers,commands.payloadWords, + commands.completedOperations,commands.waitedUs}; + IOLockUnlock(c.lock);IORecursiveLockUnlock(clock.lock); + if(acquired)reinterpret_cast(adlnWorkLoopOpen)(loop); + loop->release();IORecursiveLockUnlock(mutex); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIRuntimePower",receipt,sizeof(receipt)); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIResumeIRQ",irqReceipt,sizeof(irqReceipt)); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIResumeCommands",commandReceipt,sizeof(commandReceipt)); + publishDSIBacklight(); + return complete; +} diff --git a/WhateverXe/kern_dsi_adapter_modes.inc b/WhateverXe/kern_dsi_adapter_modes.inc new file mode 100644 index 00000000..6d600ab7 --- /dev/null +++ b/WhateverXe/kern_dsi_adapter_modes.inc @@ -0,0 +1,553 @@ +// Apple fixed-link mode adapter and native GPU presentation transactions. +// Rotation/scaling remain Apple operations; this is their DSI attachment. +// Included only by kern_xe.cpp; retains its private owners and lock ordering. + +// Pipe A belongs to DSI for this adoption phase. Malformed/ambiguous objects +// must not fall into a normal-transcoder read or the donor PWM calculation. +static bool dsiCachePath(OSObject *path) { + if(!path || path->getMetaClass()->getClassSize()<0x3730)return true; + const auto pipe=Display13FBMemory::field(path,0x3640); + if(pipe==0 || pipe>=3)return true; + const auto port=Display13FBMemory::field(path,0x3648); + return !port || port->getMetaClass()->getClassSize()<0x580 || Display13FBMemory::field(port,0x120)==0; +} + +// Called at the existing mode/disable owners, before any donor notification or +// cursor/MMIO write. A retained panel FB remains ours even if its path fields +// are damaged. Ambiguous objects must not escape into the normal DDI writer. +static bool dsiLifecycleTarget(IOService *fb,OSObject *path) { + if(dsiModeTarget(fb) || !fb || fb->getMetaClass()->getClassSize()<0x4B90)return true; + const auto attached=Display13FBMemory::singlePath(fb); + return attached!=path || dsiCachePath(attached); +} + +bool XE::processDisplay13DSIModes(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { + const char *symbols[]={ + "__ZN21AppleIntelFramebuffer18setDetailedTimingsEP7OSArray", + "__ZN21AppleIntelFramebuffer19validateDisplayModeEiPPKNS_15ModeDescriptionEPPK29IODetailedTimingInformationV2", + "__ZN21AppleIntelFramebuffer15getDisplayModesEPi", + "__ZN21AppleIntelFramebuffer19getDisplayModeCountEv", + "__ZN21AppleIntelFramebuffer28getInformationForDisplayModeEiP24IODisplayModeInformation", + "__ZN21AppleIntelFramebuffer14setDisplayModeEii", + "__ZN21AppleIntelFramebuffer21getCurrentDisplayModeEPiS0_", + "__ZN21AppleIntelFramebuffer13getOnlineInfoEP21AppleIntelDisplayPathPhS2_", + "__ZN21AppleIntelFramebuffer13fbGetDDCBlockEjPhPyjP21AppleIntelDisplayPath"}; + const uint32_t offsets[]={0x30D0E,0x30DE2,0x31FE6,0x32D04,0x32010,0x30F10,0x32A88,0x2E232,0x2F1B4}; + const uint8_t entries[][16]={ + {0x55,0x48,0x89,0xE5,0x41,0x56,0x53,0x49,0x89,0xF6,0x48,0x89,0xFB,0x48,0xFF,0x05}, + {0x55,0x48,0x89,0xE5,0x41,0x56,0x53,0x48,0xFF,0x05,0x98,0x93,0x0A,0x00,0x83,0xFE}, + {0x55,0x48,0x89,0xE5,0x48,0xFF,0x05,0x8F,0x84,0x0A,0x00,0x80,0xBF,0xE0,0x01,0x00}, + {0x55,0x48,0x89,0xE5,0x48,0xFF,0x05,0xE1,0x79,0x0A,0x00,0x80,0xBF,0xE0,0x01,0x00}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x53,0x48,0x83,0xEC,0x18,0x49,0x89,0xD6}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x81,0xEC}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x81,0xEC}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x81,0xEC}, + {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x50,0x44,0x89}}; + if(!display13FramebufferRequested || size<=0x32D14)return false; + for(unsigned i=0;i(index,symbols[i],address,size);patcher.clearError(); + if(fn!=address+offsets[i] || memcmp(reinterpret_cast(fn),entries[i],16))return false; + } + const char *geometry="__ZN21AppleIntelFramebuffer32computeTransformAndSetDimensionsEPK29IODetailedTimingInformationV2"; + const auto compute=patcher.solveSymbol(index,geometry,address,size);patcher.clearError(); + const uint8_t entry[]={0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC}; + if(compute!=address+0x31484 || memcmp(reinterpret_cast(compute),entry,sizeof(entry)))return false; + dsiComputeGeometry=compute; + const char *modeSymbols[]={"__ZN21AppleIntelFramebuffer21allocateLogicalBufferEv", + "__ZN21AppleIntelFramebuffer28setupInitialTransactionStateEj", + "__ZN21AppleIntelFramebuffer18setTransactionCapsEv", + "__ZN21AppleIntelFramebuffer20releaseLogicalBufferEv"}; + const uint32_t modeOffsets[]={0x31726,0x318E8,0x319D2,0x33062}; + for(unsigned i=0;i(index,modeSymbols[i],address,size);patcher.clearError(); + if(fn!=address+modeOffsets[i] || memcmp(reinterpret_cast(fn),entry,4))return false; + dsiModeCPU[i]=fn; + } + const char *scaler="__ZN16AppleIntelScaler19isSupportedByScalerEjjjjyjb"; + const auto scalerFn=patcher.solveSymbol(index,scaler,address,size);patcher.clearError(); + const uint8_t scalerEntry[]={0x55,0x48,0x89,0xE5,0x48,0xFF,0x05,0xD7,0xD7,0x07,0x00,0x48,0x8B,0x47,0x10}; + if(size<=0x6686D || scalerFn!=address+0x6685E || + memcmp(reinterpret_cast(scalerFn),scalerEntry,sizeof(scalerEntry)))return false; + const char *detailed="__ZN21AppleIntelFramebuffer22validateDetailedTimingEPvy"; + const char *source="__ZN21AppleIntelFramebuffer18ValidateSourceSizeEP29IODetailedTimingInformationV2"; + const auto detailedFn=patcher.solveSymbol(index,detailed,address,size);patcher.clearError(); + const auto sourceFn=patcher.solveSymbol(index,source,address,size);patcher.clearError(); + const uint8_t detailedEntry[]={0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x81,0xEC}; + const uint8_t sourceEntry[]={0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x54,0x53,0x48,0x83,0xEC,0x20}; + if(detailedFn!=address+0x2FFCA || sourceFn!=address+0x30AD8 || + memcmp(reinterpret_cast(detailedFn),detailedEntry,sizeof(detailedEntry)) || + memcmp(reinterpret_cast(sourceFn),sourceEntry,sizeof(sourceEntry)))return false; + const char *pixel="__ZN21AppleIntelFramebuffer19getPixelInformationEiiiP18IOPixelInformation"; + const auto pixelFn=patcher.solveSymbol(index,pixel,address,size);patcher.clearError(); + if(pixelFn!=address+0x324A4 || memcmp(reinterpret_cast(pixelFn),entry,sizeof(entry)))return false; + KernelPatcher::RouteRequest routes[]={ + {pixel,wrapDSIPixelInfo,orgDSIPixelInfo}, + {detailed,wrapDSIValidateDetailed,orgDSIDetailedValidation}, + {source,wrapDSIValidateSource,orgDSISourceValidation}, + {scaler,wrapDSIScalerSupport,orgDSIScalerSupport}, + {symbols[0],wrapDSISetDetailedTimings,orgDSIMode[0]}, + {symbols[1],wrapDSIValidateMode,orgDSIMode[1]}, + {symbols[2],wrapDSIGetModes,orgDSIMode[2]}, + {symbols[3],wrapDSIGetModeCount,orgDSIMode[3]}, + {symbols[4],wrapDSIGetModeInformation,orgDSIMode[4]}, + {symbols[5],wrapDSISetDisplayMode,orgDSIMode[5]}, + {symbols[6],wrapDSIGetCurrentMode,orgDSIMode[6]}, + {symbols[7],wrapDSIGetOnlineInfo,orgDSIMode[7]}, + {symbols[8],wrapDSIDDC,orgDSIMode[8]}}; + const bool ready=patcher.routeMultiple(index,routes,arrsize(routes),address,size); + patcher.clearError();return ready; +} + +// Bounded, copied CPU evidence from the real FB0 client callbacks. Also +// observe calls before the fixed-mode owner is bound; do not treat the trace +// as admission or invoke any driver/hardware operation while collecting it. +void XE::recordDSIClient(IOService *fb,unsigned event,uint64_t argument,uint32_t result) { + if(!callbackXE->adlnOutputWiring.dsi || event>=13 || !fb || + fb->getMetaClass()->getClassSize()<0x4B90 || Display13FBMemory::field(fb,0x1DC))return; + DSIModeScope scope(fb);if(!scope.lock)return; + static uint64_t counts[13] {}; + const char *keys[] {"WEXDSIClientBind","WEXDSIClientTimings","WEXDSIClientValidate", + "WEXDSIClientModes","WEXDSIClientCount","WEXDSIClientInfo","WEXDSIClientSetMode", + "WEXDSIClientAttribute","WEXDSIClientConnection","WEXDSIClientPower", + "WEXDSIClientCurrent","WEXDSIClientOnline","WEXDSIClientDDC"}; + const auto count=__atomic_add_fetch(&counts[event],1ULL,__ATOMIC_RELAXED); + uint64_t words[] {1,event,count,argument,result, + Display13FBMemory::field(fb,0x1E0),Display13FBMemory::field(fb,0x44DE), + Display13FBMemory::field(fb,0x1E7),Display13FBMemory::field(fb,0x4250), + Display13FBMemory::field(fb,0x4254),Display13FBMemory::field(fb,0x4284), + Display13FBMemory::field(fb,0x420C),dsiModeTarget(fb), + dsiModeTarget(fb) && dsiFixedMode.ready,Display13FBMemory::field(fb,0x4260)!=nullptr, + Display13FBMemory::field(fb,0x4348)!=nullptr}; + IORegistryEntry::getRegistryRoot()->setProperty(keys[event],words,sizeof(words)); +} + +bool XE::bindDSIFixedMode(IOService *fb,const XELP::DSI::Profile &profile) { + auto &c=display13FBPower;auto &o=dsiFixedMode; + if(!display13DSIModeHooksReady || adlnOutputWiring.dsi!=1 || !c.lock || !fb || + fb->getMetaClass()->getClassSize()<0x4B90 || !profile.valid())return false; + DSIModeScope scope(fb);if(!scope.lock)return false; + XELP::DSI::AppleAdapter::Timing timing; + if(!XELP::DSI::AppleAdapter::makeTiming(profile,timing))return false; + if(__atomic_load_n(&o.framebuffer,__ATOMIC_ACQUIRE))return dsiModeTarget(fb) && dsiModeCoherent(fb) && + !memcmp(&timing,&o.timing,sizeof(timing)); + auto data=OSData::withBytes(&timing,sizeof(timing));auto array=OSArray::withCapacity(1); + if(!data || !array || !array->setObject(data)) {OSSafeReleaseNULL(data);OSSafeReleaseNULL(array);return false;} + IOLockLock(c.lock); + const auto path=Display13FBMemory::singlePath(fb); + const auto binding=adlnInspectPathBinding(fb,path,adlnPlatform,adlnOutputWiring); + const bool bound=!__atomic_load_n(&o.framebuffer,__ATOMIC_ACQUIRE) && + c.controller==adlnTopologyController && binding.status==TGLFBBinding::Status::Complete && + binding.ddi==0 && c.transcoders.dsi(binding.pipe) && c.bootDSI.matches(profile) && + c.bootDSI.identity.pipe==binding.pipe && !c.memoryFaulted && !c.bridge.state.faulted; + if(bound) { + o.array=array;o.data=data;o.timing=timing;o.mode=profile.mode;o.ready=false; + fb->retain();__atomic_store_n(&o.framebuffer,fb,__ATOMIC_RELEASE); + } + IOLockUnlock(c.lock); + if(!bound) {data->release();array->release();return false;} + // The donor ignores setProperty's bool result. Retain our own array/data + // before calling it, then verify both the raw pointer and registry owner. + const auto result=FunctionCast(wrapDSISetDetailedTimings,orgDSIMode[0])(fb,array); + o.ready=result==kIOReturnSuccess; + o.ready=dsiModeCoherent(fb); + fb->setProperty("WEXDSIFixedModeReady",o.ready); + recordDSIClient(fb,0,XELP::DSI::AppleAdapter::FixedModeID,o.ready); + return o.ready; +} + +// Called with the FB mutex and provider gate held, after stopped path/PG +// disposal. The donor's qualified null setter removes only the timing property +// and its raw +4260 pointer; it performs no display or allocator operation. +bool XE::releaseDSIFixedMode(IOService *fb) { + auto &o=dsiFixedMode; + if(!o.framebuffer)return true; + if(!dsiModeTarget(fb) || o.ready || o.logical || !orgDSIMode[0])return false; + auto raw=Display13FBMemory::field(fb,0x4260); + auto property=fb->getProperty("IOFBDetailedTimings"); + if((raw && raw!=o.array) || (property && property!=o.array))return false; + const auto result=FunctionCast(wrapDSISetDetailedTimings,orgDSIMode[0])(fb,nullptr); + if(result!=kIOReturnSuccess || Display13FBMemory::field(fb,0x4260) || + fb->getProperty("IOFBDetailedTimings"))return false; + auto array=o.array;auto data=o.data;auto offered=o.offeredData; + __atomic_store_n(&o.framebuffer,static_cast(nullptr),__ATOMIC_RELEASE); + o.array=nullptr;o.data=nullptr;o.offeredData=nullptr;o.offered=nullptr;o.mode={};o.timing={};o.clientMode=XELP::DSI::AppleAdapter::FixedModeID; + o.presentation={};o.requested={};o.offerCount=0;o.activeMode=o.clientMode;o.presentationReady=false;o.presentationFault=false;o.changingThread=0; + OSSafeReleaseNULL(array);OSSafeReleaseNULL(data);OSSafeReleaseNULL(offered);fb->release();return true; +} + +// IOGraphics also requests EDID independently of online detection. Stop at +// the shared internal DDC entry, before it acquires AUX power on behalf of +// the built-in panel. Supply one synthetic profile descriptor from CPU memory. +uint32_t XE::wrapDSIDDC(IOService *fb,uint32_t block,uint8_t *data,uint64_t *length,uint32_t flags,OSObject *path) { + if(!callbackXE->adlnOutputWiring.dsi)return FunctionCast(wrapDSIDDC,callbackXE->orgDSIMode[8])(fb,block,data,length,flags,path); + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return kIOReturnNotReady; + if(Display13FBMemory::field(fb,0x1DC))return FunctionCast(wrapDSIDDC,callbackXE->orgDSIMode[8])(fb,block,data,length,flags,path); + const auto result=[&]() -> uint32_t { + if(!length || !data)return kIOReturnBadArgument; + const auto capacity=*length;*length=0; + if(block<1 || block>2 || flags)return kIOReturnUnsupported; + if(capacity<128)return kIOReturnBadArgument; + DSIModeScope scope(fb); + if(!scope.lock || !dsiModeTarget(fb) || !dsiModeCoherent(fb) || + (path && path!=Display13FBMemory::singlePath(fb)))return kIOReturnNotReady; + if(block==2 && !XELP::DSI::AppleAdapter::panelNeedsDisplayID(dsiFixedMode.mode))return kIOReturnUnsupported; + uint8_t edid[128]; + if(!XELP::DSI::AppleAdapter::makePanelEDID(dsiFixedMode.mode,edid,block))return kIOReturnNotReady; + memcpy(data,edid,sizeof(edid));*length=sizeof(edid);return kIOReturnSuccess; + }(); + recordDSIClient(fb,12,block,result);return result; +} + +// Native online detection assumes eDP AUX/EDID. A DSI panel has no such +// sink. Answer only for the real adopted path and its immutable fixed mode; +// its synthetic profile descriptor is supplied without touching AUX power. +uint32_t XE::wrapDSIGetOnlineInfo(IOService *fb,OSObject *path,uint8_t *online,uint8_t *edid) { + if(!callbackXE->adlnOutputWiring.dsi)return FunctionCast(wrapDSIGetOnlineInfo,callbackXE->orgDSIMode[7])(fb,path,online,edid); + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return kIOReturnNotReady; + if(Display13FBMemory::field(fb,0x1DC)) { + // Explicit development isolation; leave physical wiring and the + // ordinary external-output backend intact for coexistence testing. + if(callbackXE->dsiPanelOnly) { + if(!online || !edid)return kIOReturnBadArgument; + *online=0;bzero(edid,128);return kIOReturnSuccess; + } + return FunctionCast(wrapDSIGetOnlineInfo,callbackXE->orgDSIMode[7])(fb,path,online,edid); + } + if(!online || !edid)return kIOReturnBadArgument; + *online=0;bzero(edid,128); + DSIModeScope scope(fb); + const bool ready=scope.lock && dsiModeTarget(fb) && dsiModeCoherent(fb) && + path && path==Display13FBMemory::singlePath(fb) && dsiNativeUpdateAllowed(fb); + uint8_t descriptor[128]; + const bool described=ready && XELP::DSI::AppleAdapter::makePanelEDID(dsiFixedMode.mode,descriptor); + if(described){*online=1;memcpy(edid,descriptor,sizeof(descriptor));dsiAdvertisePresentation(fb);} + const auto result=described ? kIOReturnSuccess : kIOReturnNotReady; + recordDSIClient(fb,11,*online,result);return result; +} + +// The donor initializes current mode to -5, then tries to recover it through +// normal-transcoder timing conversion. DSI already has a qualified GOP and +// fixed timing. Publish that same current mode under the native FB mutex. +uint32_t XE::wrapDSIGetCurrentMode(IOService *fb,int32_t *mode,int32_t *depth) { + if(!callbackXE->adlnOutputWiring.dsi)return FunctionCast(wrapDSIGetCurrentMode,callbackXE->orgDSIMode[6])(fb,mode,depth); + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return kIOReturnNotReady; + if(Display13FBMemory::field(fb,0x1DC))return FunctionCast(wrapDSIGetCurrentMode,callbackXE->orgDSIMode[6])(fb,mode,depth); + DSIModeScope scope(fb); + const bool ready=scope.lock && dsiModeTarget(fb) && dsiModeCoherent(fb) && + Display13FBMemory::field(fb,0x1E0) && + ((dsiFixedMode.changingThread && dsiFixedMode.changingThread==reinterpret_cast(current_thread())) || dsiNativeUpdateAllowed(fb)); + if(ready) { + const uint32_t fixed=dsiFixedMode.presentationReady ? dsiFixedMode.activeMode : dsiFixedMode.clientMode; + memcpy(reinterpret_cast(fb)+0x4250,&fixed,sizeof(fixed)); + if(mode)*mode=fixed; + if(depth)*depth=Display13FBMemory::field(fb,0x4254); + } + const auto result=ready ? kIOReturnSuccess : kIOReturnNotReady; + recordDSIClient(fb,10,ready ? dsiFixedMode.clientMode : 0,result);return result; +} + +uint32_t XE::wrapDSISetDetailedTimings(IOService *fb,OSArray *array) { + recordDSIClient(fb,1,array!=nullptr,UINT32_MAX); + const auto result=[&]() -> uint32_t { + if(!dsiModeTarget(fb))return FunctionCast(wrapDSISetDetailedTimings,callbackXE->orgDSIMode[0])(fb,array); + DSIModeScope scope(fb);if(!scope.lock || !dsiModeCoherent(fb))return kIOReturnNotReady; + uint32_t client=0;unsigned count=0; + uint64_t offer[16] {3,array ? array->getCount() : 0,UINT32_MAX}; + if(array)for(unsigned i=0;igetCount();++i) { + auto bytes=OSDynamicCast(OSData,array->getObject(i)); + if(!bytes || bytes->getLength()!=sizeof(dsiFixedMode.timing))continue; + const auto &t=*static_cast(bytes->getBytesNoCopy()); + const bool native=XELP::DSI::AppleAdapter::clientTiming(dsiFixedMode.timing,t,i); + if(!native && !XELP::DSI::AppleAdapter::presentationTiming(dsiFixedMode.timing,t,i)) { + // Keep the last rejected OS request across later successful + // enumeration calls, so clock/geometry admission is observable. + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSITimingRejected",bytes); + continue; + } + ++count;if(native)client=t.reservedA[0];offer[2]=i;offer[3]=client;offer[4]=t.pixelClock; + offer[5]=t.horizontalActive;offer[6]=t.horizontalBlanking;offer[7]=t.horizontalSyncOffset;offer[8]=t.horizontalSyncWidth; + offer[9]=t.verticalActive;offer[10]=t.verticalBlanking;offer[11]=t.verticalSyncOffset;offer[12]=t.verticalSyncWidth; + offer[13]=t.scalerFlags;offer[14]=t.horizontalScaled;offer[15]=t.verticalScaled; + } + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSITimingOffer",offer,sizeof(offer)); + auto data=array && array->getCount()==1 ? OSDynamicCast(OSData,array->getObject(0)) : nullptr; + const bool canonical=data && data->getLength()==sizeof(dsiFixedMode.timing) && + !memcmp(data->getBytesNoCopy(),&dsiFixedMode.timing,sizeof(dsiFixedMode.timing)); + if(!count && !canonical)return kIOReturnUnsupported; + // Size storage from the accepted OS list, not a fixed number of modes. + // Prepare the complete replacement before touching native state so an + // allocation failure preserves the previous list and its timing pointers. + OSData *replacement=nullptr; + if(count) { + if(count>UINT32_MAX/sizeof(dsiFixedMode.timing))return kIOReturnNoResources; + replacement=OSData::withCapacity(count*sizeof(dsiFixedMode.timing)); + if(!replacement)return kIOReturnNoResources; + for(unsigned i=0;igetCount();++i) { + auto bytes=OSDynamicCast(OSData,array->getObject(i)); + if(!bytes || bytes->getLength()!=sizeof(dsiFixedMode.timing))continue; + const auto &t=*static_cast(bytes->getBytesNoCopy()); + if(XELP::DSI::AppleAdapter::clientTiming(dsiFixedMode.timing,t,i) || + XELP::DSI::AppleAdapter::presentationTiming(dsiFixedMode.timing,t,i)) { + if(!replacement->appendBytes(&t,sizeof(t))) { + replacement->release();return kIOReturnNoResources; + } + } + } + } + // Native storage retains the immutable wire mode. Qualified presentation + // timings are returned by validation, without changing the DSI link. + const auto result=FunctionCast(wrapDSISetDetailedTimings,callbackXE->orgDSIMode[0])(fb,dsiFixedMode.array); + if(result==kIOReturnSuccess && dsiModeCoherent(fb)) { + auto previous=dsiFixedMode.offeredData; + dsiFixedMode.offeredData=replacement; + dsiFixedMode.offered=replacement ? static_cast(replacement->getBytesNoCopy()) : nullptr; + dsiFixedMode.offerCount=count; + if(replacement)IORegistryEntry::getRegistryRoot()->setProperty("WEXDSITimingList",replacement); + else IORegistryEntry::getRegistryRoot()->removeProperty("WEXDSITimingList"); + if(client)dsiFixedMode.clientMode=client; + OSSafeReleaseNULL(previous); + } else { + OSSafeReleaseNULL(replacement); + if(result==kIOReturnSuccess)return kIOReturnNotReady; + } + return result; + }(); + recordDSIClient(fb,1,array!=nullptr,result); + return result; +} + +uint32_t XE::wrapDSIValidateMode(IOService *fb,int32_t mode,const void **description,const void **timing) { + recordDSIClient(fb,2,static_cast(mode),UINT32_MAX); + const auto result=[&]() -> uint32_t { + if(!dsiModeTarget(fb))return FunctionCast(wrapDSIValidateMode,callbackXE->orgDSIMode[1])(fb,mode,description,timing); + if(description)*description=nullptr;if(timing)*timing=nullptr; + DSIModeScope scope(fb);if(!scope.lock || !dsiModeCoherent(fb))return kIOReturnNotReady; + const auto selected=dsiPresentation(mode);if(!selected)return kIOReturnBadArgument; + const auto result=FunctionCast(wrapDSIValidateMode,callbackXE->orgDSIMode[1])( + fb,XELP::DSI::AppleAdapter::FixedModeID,description,timing); + if(!result && timing) { + if(*timing!=dsiFixedMode.data->getBytesNoCopy())return kIOReturnNotReady; + if(selected->scalerFlags || selected->horizontalScaled || selected->verticalScaled)*timing=selected; + } + return result; + }(); + recordDSIClient(fb,2,static_cast(mode),result); + return result; +} + +uint32_t XE::wrapDSIGetModes(IOService *fb,int32_t *modes) { + recordDSIClient(fb,3,modes!=nullptr,UINT32_MAX); + const auto result=[&]() -> uint32_t { + if(!dsiModeTarget(fb))return FunctionCast(wrapDSIGetModes,callbackXE->orgDSIMode[2])(fb,modes); + DSIModeScope scope(fb);if(!scope.lock || !dsiModeCoherent(fb))return kIOReturnNotReady; + if(!modes)return kIOReturnBadArgument; + if(dsiFixedMode.offerCount)for(unsigned i=0;i uint32_t { + if(!dsiModeTarget(fb))return FunctionCast(wrapDSIGetModeCount,callbackXE->orgDSIMode[3])(fb); + DSIModeScope scope(fb);return scope.lock && dsiModeCoherent(fb) ? (dsiFixedMode.offerCount ? dsiFixedMode.offerCount : 1) : 0; + }(); + recordDSIClient(fb,4,0,result); + return result; +} + +uint32_t XE::wrapDSIPixelInfo(IOService *fb,int32_t mode,int32_t depth,int32_t aperture,void *info) { + const auto result=FunctionCast(wrapDSIPixelInfo,callbackXE->orgDSIPixelInfo)(fb,mode,depth,aperture,info); + if(dsiModeTarget(fb)) { + static uint32_t sequence=0;uint64_t record[28] {1,__sync_add_and_fetch(&sequence,1), + static_cast(mode),static_cast(depth),static_cast(aperture),result}; + if(info && !result)memcpy(record+6,info,172); + char key[40];snprintf(key,sizeof(key),"WEXDSIPixelCall%u",static_cast(record[1])%32); + IORegistryEntry::getRegistryRoot()->setProperty(key,record,sizeof(record)); + } + return result; +} + +// Observe native mode admission separately from the later published mode list. +uint32_t XE::wrapDSIValidateDetailed(IOService *fb,void *raw,uint64_t size) { + uint64_t record[60] {};const bool observe=dsiModeTarget(fb) && raw && size==204; + if(observe)memcpy(record+8,raw,204); + const auto result=FunctionCast(wrapDSIValidateDetailed,callbackXE->orgDSIDetailedValidation)(fb,raw,size); + // Match getInformationForDisplayMode: the retained scanout is RGB8. + // CoreDisplay consumes this metadata before it asks for a mode's info. + if(observe && !result)static_cast(raw)->depth=0; + if(observe) { + static uint32_t sequence=0;record[0]=1;record[1]=__sync_add_and_fetch(&sequence,1); + record[2]=result;record[3]=Display13FBMemory::field(fb,0x4318); + record[4]=Display13FBMemory::field(fb,0x431C); + memcpy(reinterpret_cast(record)+268,raw,204); + char key[40];snprintf(key,sizeof(key),"WEXDSIDetailedValidation%u",static_cast(record[1])%32); + IORegistryEntry::getRegistryRoot()->setProperty(key,record,sizeof(record)); + } + return result; +} + +bool XE::wrapDSIValidateSource(IOService *fb,void *raw) { + const auto result=FunctionCast(wrapDSIValidateSource,callbackXE->orgDSISourceValidation)(fb,raw); + if(dsiModeTarget(fb) && raw) { + static uint32_t sequence=0;uint64_t record[25] {1,__sync_add_and_fetch(&sequence,1),result, + Display13FBMemory::field(fb,0x4318),Display13FBMemory::field(fb,0x431C)}; + memcpy(record+5,raw,160); + char key[40];snprintf(key,sizeof(key),"WEXDSISourceValidation%u",static_cast(record[1])%32); + IORegistryEntry::getRegistryRoot()->setProperty(key,record,sizeof(record)); + } + return result; +} + +// The adopted DSI scanout is fixed-size. It has no panel-fitter resource +// transaction, so let the donor select its existing GPU scaling path. Preserve +// the real OS timing and transform; other framebuffer owners keep native policy. +bool XE::wrapDSIScalerSupport(void *scaler,uint32_t outputW,uint32_t outputH, + uint32_t sourceW,uint32_t sourceH,uint64_t clock,uint32_t flags,bool format) { + auto fb=scaler ? Display13FBMemory::field(scaler,0x10) : nullptr; + if(dsiModeTarget(fb))return false; + return FunctionCast(wrapDSIScalerSupport,callbackXE->orgDSIScalerSupport)( + scaler,outputW,outputH,sourceW,sourceH,clock,flags,format); +} + +uint32_t XE::wrapDSIGetModeInformation(IOService *fb,int32_t mode,void *raw) { + recordDSIClient(fb,5,static_cast(mode),UINT32_MAX); + const auto result=[&]() -> uint32_t { + if(!dsiModeTarget(fb))return FunctionCast(wrapDSIGetModeInformation,callbackXE->orgDSIMode[4])(fb,mode,raw); + DSIModeScope scope(fb);if(!scope.lock || !dsiModeCoherent(fb))return kIOReturnNotReady; + const auto selected=dsiPresentation(mode);if(!raw || !selected)return kIOReturnBadArgument; + const bool rotated=(selected->scalerFlags&0x10)!=0; + const auto result=FunctionCast(wrapDSIGetModeInformation,callbackXE->orgDSIMode[4])(fb,mode,raw); + if(!result && !XELP::DSI::AppleAdapter::annotateMode(dsiFixedMode.mode, + *static_cast(raw),rotated,selected->horizontalScaled,selected->verticalScaled))return kIOReturnNotReady; + return result; + }(); + if(dsiModeTarget(fb)) { + DSIModeScope scope(fb); + for(unsigned i=0;scope.lock && i(mode)) { + uint32_t receipt[12] {1,static_cast(mode),result}; + if(raw && !result)memcpy(receipt+3,raw,36); + char key[40];snprintf(key,sizeof(key),"WEXDSIModeInfo%u",i); + IORegistryEntry::getRegistryRoot()->setProperty(key,receipt,sizeof(receipt));break; + } + } + recordDSIClient(fb,5,static_cast(mode),result); + return result; +} + +// Native logical storage belongs to the same GPU pipe as the retained scanout. +// Never free the scanout or modify the DSI host while changing presentation. +bool XE::releaseDSILogical(IOService *fb) { + auto &o=dsiFixedMode;auto pipe=dsiPendingBacking.pipe; + if(!dsiModeTarget(fb) || !pipe || !dsiModeCPU[3] || + Display13FBMemory::field(fb,0x4340)!=o.logical)return false; + if(!o.logical)return true; + reinterpret_cast(dsiModeCPU[3])(fb); + const bool released=!Display13FBMemory::field(fb,0x4340) && + !Display13FBMemory::field(pipe,0x12B8) && !Display13FBMemory::field(pipe,0x12C0); + if(released)__atomic_store_n(&o.logical,uintptr_t(0),__ATOMIC_RELEASE); + else o.presentationFault=true; + return released; +} + +uint32_t XE::setDSIPresentation(IOService *fb,uint32_t mode,const XELP::DSI::AppleAdapter::Timing &requested) { + using Display13FBMemory::field; + auto &o=dsiFixedMode;auto &c=display13FBPower; + if(!dsiModeTarget(fb) || !dsiModeCoherent(fb) || !dsiNativeUpdateAllowed(fb) || o.changingThread)return kIOReturnNotReady; + auto path=Display13FBMemory::singlePath(fb);auto pipe=dsiPendingBacking.pipe; + if(!path || !pipe || !dsiArmedBackingOwned(fb,path))return kIOReturnNotReady; + const auto old=o.presentationReady ? o.presentation : o.timing; + const uint32_t oldMode=o.presentationReady ? o.activeMode : o.clientMode; + if(requested.scalerFlags==old.scalerFlags && requested.horizontalScaled==old.horizontalScaled && + requested.verticalScaled==old.verticalScaled && requested.horizontalInset==old.horizontalInset && + requested.verticalInset==old.verticalInset) { + o.presentation=requested;o.activeMode=mode;o.presentationReady=true; + *reinterpret_cast(reinterpret_cast(fb)+0x4250)=mode; + return kIOReturnSuccess; + } + IOLockLock(c.lock); + const bool idle=!c.dsiNativeWriters && !c.pipe[0].wmPending && !c.pipe[0].wmCommitting && + !field(path,0x35F0) && !c.dbuf.updating; + IOLockUnlock(c.lock);if(!idle)return kIOReturnNotReady; + o.changingThread=reinterpret_cast(current_thread()); + uint64_t receipt[16] {1,mode,requested.scalerFlags,oldMode,old.scalerFlags}; + uint64_t gpuState[13] {1}; + const auto captureGPU=[&](unsigned base) { + const unsigned offsets[]={0x1264,0x1268,0x128C,0x1290,0x1294,0x12D8}; + if(pipe->getMetaClass()->getClassSize()>=0x12DC) + for(unsigned i=0;i(pipe,offsets[i]); + }; + captureGPU(1); + const auto apply=[&](uint32_t selected,const XELP::DSI::AppleAdapter::Timing &timing) -> bool { + if(!releaseDSILogical(fb))return false; + auto cpu=o.timing;cpu.scalerFlags=timing.scalerFlags; + cpu.horizontalScaled=timing.horizontalScaled;cpu.verticalScaled=timing.verticalScaled; + cpu.horizontalInset=timing.horizontalInset;cpu.verticalInset=timing.verticalInset; + if(!XELP::DSI::AppleAdapter::prepareGeometry(fb,path,dsiProfile,cpu, + reinterpret_cast(dsiComputeGeometry),true))return false; + // The native allocator asks getCurrentDisplayMode/getPixelInformation + // through its GPU callback. Publish the selected CPU contract first, + // while other display writers remain excluded by changingThread. + o.presentation=timing;o.activeMode=selected;o.presentationReady=true; + *reinterpret_cast(reinterpret_cast(fb)+0x4250)=selected; + const auto result=reinterpret_cast(dsiModeCPU[0])(fb); + __atomic_store_n(&o.logical,field(fb,0x4340),__ATOMIC_RELEASE); + receipt[5]=result;receipt[6]=o.logical;receipt[7]=field(pipe,0x12B8);receipt[8]=field(pipe,0x12C0); + if(result)return false; // native failure can still leave a record to release + if(field(fb,0x4338)&0x30) { + auto record=reinterpret_cast(o.logical); + if(!record || record->kind!=2 || record->columns!=(field(fb,0x430C)*4+127)/128 || record->rows!=(field(fb,0x4310)+31)/32 || + record->pitch!=field(fb,0x4334) || record->bytes!=uint64_t(record->pitch)*field(fb,0x4310) || !record->memory || !record->map || + field(pipe,0x12B8)!=record->memory || field(pipe,0x12C0)!=record->map)return false; + } else if(o.logical || field(pipe,0x12B8) || field(pipe,0x12C0))return false; + reinterpret_cast(dsiModeCPU[1])(fb,0); + reinterpret_cast(dsiModeCPU[2])(fb); + return dsiArmedBackingOwned(fb,path) && dsiPublishedTarget(fb); + }; + const bool changed=apply(mode,requested); + receipt[9]=changed; + if(!changed) { + const bool restored=!o.presentationFault && apply(oldMode,old); + receipt[10]=restored;if(!restored)o.presentationFault=true; + } + receipt[11]=field(fb,0x4338);receipt[12]=field(fb,0x4348); + receipt[13]=o.presentationFault;receipt[14]=field(path,0xF8);receipt[15]=field(path,0xFC); + captureGPU(7); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIPresentationGPU",gpuState,sizeof(gpuState)); + o.changingThread=0; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIPresentation",receipt,sizeof(receipt)); + return changed ? kIOReturnSuccess : kIOReturnNotReady; +} + +// Native timing/resource setup can disable or reallocate before hwSetMode. +// DSI uses its own fixed-mode transaction to retain the adopted host link. +uint32_t XE::wrapDSISetDisplayMode(IOService *fb,int32_t mode,int32_t depth) { + recordDSIClient(fb,6,(uint64_t(static_cast(mode))<<32)|static_cast(depth),UINT32_MAX); + const auto result=[&]() -> uint32_t { + if(callbackXE->adlnOutputWiring.dsi) { + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return kIOReturnNotReady; + if(callbackXE->dsiPanelOnly && Display13FBMemory::field(fb,0x1DC))return kIOReturnNotReady; + DSIModeScope scope(fb);if(!scope.lock)return kIOReturnNotReady; + if(dsiLifecycleTarget(fb,Display13FBMemory::singlePath(fb))) { + // A bounded receipt of the real request. Keep the donor's mode + // reallocation path closed until the DSI surface transaction owns it. + uint64_t record[] {1,Display13FBMemory::field(fb,0x1DC), + Display13FBMemory::field(fb,0x1E0),static_cast(mode), + static_cast(depth),dsiModeTarget(fb),callbackXE->dsiResourcesReady, + Display13FBMemory::field(fb,0x420C)}; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIModeRequest",record,sizeof(record)); + const auto selected=dsiPresentation(mode); + if(!selected || depth || !dsiModeTarget(fb))return kIOReturnNotReady; + const auto presentation=*selected; + if(dsiNativeUpdateAllowed(fb) && !dsiPendingTarget(fb))callbackXE->probeDSINativeBacking(fb); + if(!callbackXE->publishDSINativeMode(fb))return kIOReturnNotReady; + return callbackXE->setDSIPresentation(fb,mode,presentation); + } + } + return FunctionCast(wrapDSISetDisplayMode,callbackXE->orgDSIMode[5])(fb,mode,depth); + }(); + recordDSIClient(fb,6,(uint64_t(static_cast(mode))<<32)|static_cast(depth),result); + return result; +} diff --git a/WhateverXe/kern_dsi_adapter_state.inc b/WhateverXe/kern_dsi_adapter_state.inc new file mode 100644 index 00000000..5c6667be --- /dev/null +++ b/WhateverXe/kern_dsi_adapter_state.inc @@ -0,0 +1,91 @@ +// DSI attachment owners and identity predicates. Included inside the +// existing anonymous namespace; no new owner, lock or initialization phase. + +static void dsiPublishIRQLive(); + +// The FB mutex serializes prepare/discard/publication. Native allocation also +// publishes GPU-pipe references before this record becomes the FB scanout. +// Keep both owners retained, including on partial failure, until proven free. +struct DSIPendingBackingOwner { + IOService *framebuffer {},*accelerator {}; + OSObject *pipe {},*pinnedMap {},*pinnedMemory {}; + XELP::DSI::NativeBacking::Record *record {}; + uintptr_t thread {}; + bool allocating {},freeing {},allocated {},freed {},armed {},published {},modeReady {}; + uint32_t nativeResult {}; + Display13FBMemory::SurfaceReadout proof; + bool initialized {}; +}; +static DSIPendingBackingOwner dsiPendingBacking; +static bool dsiPendingTarget(IOService *fb) { + return fb && __atomic_load_n(&dsiPendingBacking.framebuffer,__ATOMIC_ACQUIRE)==fb; +} + +static bool dsiPublishedTarget(IOService *fb) { + return dsiPendingTarget(fb) && dsiPendingBacking.published && dsiPendingBacking.record && + Display13FBMemory::field(fb,0x4348)==dsiPendingBacking.record; +} + + +// One DSI0 panel. Publish only after native path/pipe association; the FB and +// immutable timing array remain retained until a quiesced DSI stop can detach +// them. A donor property failure must never leave its raw array pointer dangling. +struct DSIFixedModeOwner { + IOService *framebuffer {}; + OSArray *array {}; + OSData *data {}; + XELP::DSI::Mode mode; + XELP::DSI::AppleAdapter::Timing timing; + XELP::DSI::AppleAdapter::Timing presentation,requested; + OSData *offeredData {}; + const XELP::DSI::AppleAdapter::Timing *offered {}; + unsigned offerCount {}; + uint32_t activeMode {XELP::DSI::AppleAdapter::FixedModeID}; + uintptr_t changingThread {},logical {}; + bool presentationReady {},presentationFault {}; + uint32_t clientMode {XELP::DSI::AppleAdapter::FixedModeID}; + bool ready {}; +}; +static DSIFixedModeOwner dsiFixedMode; +static bool dsiModeTarget(IOService *fb) { + return fb && __atomic_load_n(&dsiFixedMode.framebuffer,__ATOMIC_ACQUIRE)==fb; +} +static uintptr_t dsiLogicalSurface(const void *fb) { + const auto &o=dsiFixedMode; + return fb && __atomic_load_n(&o.framebuffer,__ATOMIC_ACQUIRE)==fb ? + __atomic_load_n(&o.logical,__ATOMIC_ACQUIRE) : 0; +} +static const XELP::DSI::AppleAdapter::Timing *dsiPresentation(uint32_t mode) { + auto &o=dsiFixedMode; + if(o.changingThread && o.presentationReady && mode==o.activeMode)return &o.presentation; + const XELP::DSI::AppleAdapter::Timing *selected=nullptr; + for(unsigned i=0;igetProperty("IOFBScalerInfo")); + if(!data || data->getLength()!=48)return; + uint32_t scaler[12];memcpy(scaler,data->getBytesNoCopy(),sizeof(scaler)); + if(!(scaler[4]&0x20)){scaler[4]|=0x20;fb->setProperty("IOFBScalerInfo",scaler,sizeof(scaler));} +} +struct DSIModeScope { + IORecursiveLock *lock; + explicit DSIModeScope(IOService *fb):lock(Display13FBMemory::field(fb,0x4220)) { + if(lock)IORecursiveLockLock(lock); + } + ~DSIModeScope(){if(lock)IORecursiveLockUnlock(lock);} +}; +static bool dsiModeCoherent(IOService *fb) { + const auto &o=dsiFixedMode; + return o.ready && o.array && o.data && o.array->getCount()==1 && o.array->getObject(0)==o.data && + o.data->getLength()==sizeof(o.timing) && !memcmp(o.data->getBytesNoCopy(),&o.timing,sizeof(o.timing)) && + Display13FBMemory::field(fb,0x4260)==o.array && fb->getProperty("IOFBDetailedTimings")==o.array; +} diff --git a/WhateverXe/kern_dsi_apple_timing.hpp b/WhateverXe/kern_dsi_apple_timing.hpp new file mode 100644 index 00000000..55c23b0e --- /dev/null +++ b/WhateverXe/kern_dsi_apple_timing.hpp @@ -0,0 +1,148 @@ +// Exact TGL donor's IODetailedTimingInformationV2 data contract. Apple-specific +// layout stays outside the DSI host; no native object is owned by this value. +#ifndef KERN_DSI_APPLE_TIMING_HPP +#define KERN_DSI_APPLE_TIMING_HPP +#include "kern_dsi_profile.hpp" +namespace XELP { namespace DSI { namespace AppleAdapter { +struct Timing { + uint32_t reservedA[3] {},horizontalInset {},verticalInset {},scalerFlags {},horizontalScaled {},verticalScaled {}; + uint32_t signalConfig {},signalLevels {}; + uint64_t pixelClock {},minPixelClock {},maxPixelClock {}; + uint32_t horizontalActive {},horizontalBlanking {},horizontalSyncOffset {},horizontalSyncWidth {}; + uint32_t verticalActive {},verticalBlanking {},verticalSyncOffset {},verticalSyncWidth {}; + uint32_t borders[4] {},horizontalSyncConfig {},horizontalSyncLevel {},verticalSyncConfig {},verticalSyncLevel {}; + uint32_t numLinks {},verticalBlankingExtension {}; + uint16_t pixelEncoding {},bitsPerColorComponent {},colorimetry {},dynamicRange {},dsc[3] {},reservedB[5] {}; +}; +static_assert(sizeof(Timing)==160 && offsetof(Timing,pixelClock)==0x28 && + offsetof(Timing,horizontalActive)==0x40 && offsetof(Timing,pixelEncoding)==0x88,"Apple timing ABI"); +constexpr uint32_t FixedModeID=0x10000; +struct ModeInformation { + uint32_t width {},height {},refresh {},depth {},flags {}; + uint16_t widthMM {},heightMM {}; + uint32_t reserved[3] {}; +}; +static_assert(sizeof(ModeInformation)==36,"IODisplayModeInformation ABI"); +inline bool annotateMode(const Mode &m,ModeInformation &info,bool rotated=false,uint32_t drawWidth=0,uint32_t drawHeight=0) { + const auto width=drawWidth?drawWidth:(rotated?m.height:m.width); + const auto height=drawHeight?drawHeight:(rotated?m.width:m.height); + if(!m.valid() || info.width!=width || info.height!=height)return false; + const auto refresh=((uint64_t(m.clockKHz)*1000*65536+m.htotal()*uint64_t(m.vtotal())/2)/ + (m.htotal()*uint64_t(m.vtotal()))); + if(refresh>UINT32_MAX)return false; + info.refresh=uint32_t(refresh); + info.depth=0; // the adopted DSI surface is RGB8 only + info.widthMM=static_cast(rotated?m.heightMM:m.widthMM);info.heightMM=static_cast(rotated?m.widthMM:m.heightMM); + // IOGraphics: valid/safe/default + built-in + native. Acceleration is a + // separate runtime qualification, never inferred from a fixed mode. + info.flags|=0x403; + if(width==(rotated?m.height:m.width) && height==(rotated?m.width:m.height))info.flags|=0x02000004; + else info.flags&=~0x02000004U; // A scaled source is not the native/default mode. + return true; +} +// CoreDisplay can replace the reserved client IDs when rebuilding its mode +// list. Treat reserved IDs as opaque: the complete +// timing, not an assumed generation/index bit layout, qualifies this alias. +inline bool clientTiming(const Timing &fixed,const Timing &offered,unsigned /*index*/) { + if(offered.reservedA[0]<0x80000000U || offered.reservedA[0]==UINT32_MAX || offered.signalLevels>1)return false; + const uint64_t rounded=((fixed.pixelClock+5000)/10000)*10000; + if((offered.pixelClock!=rounded && offered.pixelClock!=fixed.pixelClock) || + offered.minPixelClock!=offered.pixelClock || offered.maxPixelClock!=offered.pixelClock || + offered.pixelClockfixed.maxPixelClock)return false; + auto normalized=offered; + normalized.reservedA[0]=fixed.reservedA[0];normalized.signalLevels=fixed.signalLevels; + normalized.pixelClock=fixed.pixelClock;normalized.minPixelClock=fixed.minPixelClock;normalized.maxPixelClock=fixed.maxPixelClock; + return !memcmp(&normalized,&fixed,sizeof(fixed)); +} +inline bool acceptsMode(uint32_t mode,uint32_t clientMode) { + return mode==FixedModeID || mode==clientMode; +} +inline bool presentationTiming(const Timing &fixed,const Timing &offered,unsigned index) { + // Presentation is supplied by CoreDisplay. Do not whitelist rotation, + // scaling sizes or mode-ID generations here; qualify only the wire timing. + auto native=offered;native.scalerFlags=0;native.horizontalScaled=0;native.verticalScaled=0; + native.horizontalInset=0;native.verticalInset=0; + return clientTiming(fixed,native,index); +} +// Synthetic Apple enumeration descriptor with timing from the panel profile. +// EDID wire layout reference: Linux v6.12 include/drm/drm_edid.h. This is not +// an EDID read from the panel and does not advertise DP/HDMI, HDR or DPMS. +inline bool panelNeedsDisplayID(const Mode &m) { + return (uint64_t(m.clockKHz)+5)/10>65535 || m.width>4095 || m.height>4095 || m.hblank>4095 || m.vblank>4095 || + m.hfront>1023 || m.hsync>1023 || m.vfront>63 || m.vsync>63; +} +inline bool makePanelEDID(const Mode &m,uint8_t (&edid)[128],unsigned block=1) { + memset(edid,0,sizeof(edid)); + const uint32_t clock=uint32_t((uint64_t(m.clockKHz)+5)/10); + if(!m.valid() || !clock || clock>0x1000000 || + m.widthMM>2550 || m.heightMM>2550)return false; + const bool extended=panelNeedsDisplayID(m); + // DisplayID Type I has 15-bit sync offsets, stored minus one. Preserve + // the real panel timing instead of truncating the EDID DTD's six-bit VFP. + if(extended && (!m.hfront || !m.vfront || m.hfront>32768 || m.vfront>32768))return false; + if(block<1 || block>2 || (block==2 && !extended))return false; + const uint32_t rounded=clock*10; + const uint32_t error=rounded>m.clockKHz ? rounded-m.clockKHz : m.clockKHz-rounded; + if(uint64_t(error)*10000>m.clockKHz)return false; // retained link: at most 100 ppm + if(block==2) { + // VESA DisplayID 1.2, Type I timing; Linux drm_displayid_internal.h + // and Big Sur CoreDisplay's InstallDisplayIDEXT use this wire layout. + edid[0]=0x70;edid[1]=0x12;edid[2]=23; // one 3+20 byte data block + edid[3]=0;edid[4]=0; // EDID extension, no further DisplayID sections + edid[5]=3;edid[6]=0;edid[7]=20; + auto d=edid+8;const uint32_t encodedClock=clock-1; + d[0]=encodedClock;d[1]=encodedClock>>8;d[2]=encodedClock>>16;d[3]=0x80; + const uint16_t fields[]={m.width,m.hblank,m.hfront,m.hsync,m.height,m.vblank,m.vfront,m.vsync}; + for(unsigned i=0;i<8;++i) { + const auto value=uint16_t(fields[i]-1);d[4+2*i]=value;d[5+2*i]=value>>8; + } + uint8_t section=0;for(unsigned i=1;i<28;++i)section+=edid[i];edid[28]=uint8_t(0-section); + uint8_t sum=0;for(unsigned i=0;i<127;++i)sum+=edid[i];edid[127]=uint8_t(0-sum); + return true; + } + const uint8_t header[]={0,255,255,255,255,255,255,0};memcpy(edid,header,8); + edid[8]=0x5C;edid[9]=0xB8; // synthetic WEX identity, no panel serial + edid[10]=0x10;edid[11]=0xD5; + // EDID 1.4 model year of this synthetic descriptor, not panel manufacture. + // A zero year selects Apple's pre-2007 non-linear-only backlight path. + edid[16]=0xFF;edid[17]=2026-1990; + edid[18]=1;edid[19]=4;edid[20]=0xA0; // digital RGB8, transport unspecified + edid[21]=static_cast((m.widthMM+5)/10); + edid[22]=static_cast((m.heightMM+5)/10); + edid[23]=0xFF;edid[24]=extended?0:2; // preferred timing lives in DTD or DisplayID + for(unsigned i=38;i<54;++i)edid[i]=1; // no standard/established timings + auto d=edid+54; + if(extended) { + d[3]=0x10; // unused base descriptor; exact timing is in extension 1 + edid[126]=1; + } else { + d[0]=clock;d[1]=clock>>8; + d[2]=m.width;d[3]=m.hblank;d[4]=((m.width>>8)<<4)|(m.hblank>>8); + d[5]=m.height;d[6]=m.vblank;d[7]=((m.height>>8)<<4)|(m.vblank>>8); + d[8]=m.hfront;d[9]=m.hsync;d[10]=((m.vfront&15)<<4)|(m.vsync&15); + d[11]=((m.hfront>>8)<<6)|((m.hsync>>8)<<4)|((m.vfront>>4)<<2)|(m.vsync>>4); + d[12]=m.widthMM;d[13]=m.heightMM;d[14]=((m.widthMM>>8)<<4)|(m.heightMM>>8); + d[17]=0x18; // separate negative sync, matching the fixed Apple timing + } + edid[75]=0xFC;memcpy(edid+77,"MIPI DSI\n ",13); + edid[93]=0x10;edid[111]=0x10; // unused descriptor slots + uint8_t sum=0;for(unsigned i=0;i<127;++i)sum+=edid[i];edid[127]=static_cast(0-sum); + return true; +} +inline bool makeTiming(const Profile &p,Timing &result) { + result={};const auto &m=p.mode; + if(!p.valid() || !m.valid())return false; + result.pixelClock=uint64_t(m.clockKHz)*1000; + const auto error=(result.pixelClock+9999)/10000; // 100 ppm, also used by DSI readout + result.minPixelClock=result.pixelClock-error;result.maxPixelClock=result.pixelClock+error; + result.horizontalActive=m.width;result.horizontalBlanking=m.hblank; + result.horizontalSyncOffset=m.hfront;result.horizontalSyncWidth=m.hsync; + result.verticalActive=m.height;result.verticalBlanking=m.vblank; + result.verticalSyncOffset=m.vfront;result.verticalSyncWidth=m.vsync; + // IOGraphics timing feature fields are bit sets, not literal bit counts. + result.numLinks=1;result.pixelEncoding=1;result.bitsPerColorComponent=2; + result.colorimetry=1;result.dynamicRange=1; + return true; +} +}}} +#endif diff --git a/WhateverXe/kern_dsi_attributes.hpp b/WhateverXe/kern_dsi_attributes.hpp new file mode 100644 index 00000000..cee316ef --- /dev/null +++ b/WhateverXe/kern_dsi_attributes.hpp @@ -0,0 +1,35 @@ +// Attribute routing for the exact TGL framebuffer donor. Keep transport-only +// operations away from the adopted DSI path; leave OS bookkeeping and native +// Unsupported results to their original implementations. +#ifndef KERN_DSI_ATTRIBUTES_HPP +#define KERN_DSI_ATTRIBUTES_HPP +#include +namespace XELP { namespace DSI { namespace AppleAdapter { +inline bool transportAttribute(uint32_t attribute) { + switch(attribute) { + // AGPM selectors belong to the shared GPU power implementation and + // are not DSI transport requests. Do not filter them here. + case 0x68646370U: // hdcp: DisplayPath::initHDCP (DDI/AUX) + case 0x73707772U: // spwr: controller hwShutdown, outside DSI stop + return true; + default:return false; + } +} +inline bool transportConnectionAttribute(uint32_t attribute) { + switch(attribute) { + case 0x00696772U: // igr: direct native hwSetBacklight + case 0x706E6C74U: // pnlt: native panel power configuration + case 0x7073723FU: // psr?: eDP panel self-refresh + case 0x70726F62U: // prob: native physical connection probe + case 0x64707461U: // dpta: Port::setLinkConfig (DP) + case 0x7073636EU: // pscn: hardware panel-fitter update, not GPU scaling + // Native setAttributeForUpstream reaches PAVP/HDCP link operations. + case 0x00617572U: case 0x00617573U: case 0x00617577U: + case 0x61757063U: case 0x61757068U: case 0x61757070U: case 0x61757072U: + case 0x61756D73U: case 0x61756363U: + return true; + default:return false; + } +} +}}} +#endif diff --git a/WhateverXe/kern_dsi_backlight.hpp b/WhateverXe/kern_dsi_backlight.hpp new file mode 100644 index 00000000..96142972 --- /dev/null +++ b/WhateverXe/kern_dsi_backlight.hpp @@ -0,0 +1,43 @@ +// Native PWM backlight for the supported DSI panel profile. The Apple adapter +// owns serialization/power and keeps requested brightness across panel-off. +// PWM register layout and VBT minimum scaling follow Linux intel_backlight.c. +#ifndef KERN_DSI_BACKLIGHT_HPP +#define KERN_DSI_BACKLIGHT_HPP +#include "kern_dsi_profile.hpp" +#include "kern_xelp_panel_pwm.hpp" +namespace XELP { namespace DSI { +struct Backlight { + static constexpr uint32_t Control=0xC8250,Period=0xC8254,Duty=0xC8258,Maximum=65535; + PanelPWM pwm; + uint32_t control {},minimum {},requested {},applied {},requests {},writes {}; + bool initialized {},faulted {}; + bool capture(const Profile &p,uint32_t ctl,uint32_t period,uint32_t duty) { + if(initialized || !p.valid() || p.controller || p.precision!=8 || + ctl==UINT32_MAX || !(ctl&(1U<<31)) || !period || period==UINT32_MAX || duty>period)return false; + pwm={period,ctl&(1U<<29)};control=ctl; + const auto vbtMin=p.minimum>64?64:p.minimum; + minimum=uint32_t((uint64_t(period)*vbtMin+127)/255); + applied=duty;requested=uint32_t((uint64_t(duty)*Maximum+period/2)/period); + initialized=true;return true; + } + bool request(uintptr_t value) { + if(!initialized || faulted || value>Maximum)return false; + requested=static_cast(value);++requests;return true; + } + uint32_t target() const { + uint32_t duty=0;pwm.duty(requested,duty); + // Apple brightness zero is dark. Positive requests respect the panel's + // VBT minimum; turning the panel off is a separate lifecycle operation. + return requested && duty bool apply(IO &io) { + if(!initialized || faulted || !io.ready() || !io.readable(Control) || + !io.readable(Period) || !io.readable(Duty))return false; + if(io.read(Control)!=control || io.read(Period)!=pwm.period){faulted=true;return false;} + const auto duty=target();io.write(Duty,duty);++writes; + if(!io.ready() || io.read(Duty)!=duty){faulted=true;return false;} + applied=duty;return true; + } +}; +}} +#endif diff --git a/WhateverXe/kern_dsi_boot.hpp b/WhateverXe/kern_dsi_boot.hpp new file mode 100644 index 00000000..a22c54d9 --- /dev/null +++ b/WhateverXe/kern_dsi_boot.hpp @@ -0,0 +1,132 @@ +// Read-only certificate for a single DSI0/pipe-A linear firmware scanout. +// Caller serializes pre-native bootstrap (before any competing GPU writer), +// or holds the runtime source/pipe leases. No link, plane or PTE is modified. +#ifndef KERN_DSI_BOOT_HPP +#define KERN_DSI_BOOT_HPP +#include "kern_dsi_readout.hpp" +#include "kern_alderlake_n.hpp" +#include "kern_xelp_memory_layout.hpp" +#include "kern_xelp_geometry.hpp" +namespace XELP { namespace DSI { +enum class BootStatus : uint32_t {Unavailable,Candidate,Complete,Source,Peers,Geometry,Memory,PTE,Changed}; +struct Boot { + uint32_t version {1};BootStatus status {BootStatus::Unavailable}; + Readout source;XELPMemory::Layout memory;XELPGeometry::Linear geometry; + uint32_t poweredPipes {},control {},stride {},size {},surface {},dpt {},verifiedPages {}; + bool usable() const {return status==BootStatus::Candidate || status==BootStatus::Complete;} + bool complete() const {return status==BootStatus::Complete;} +}; +// No active normal transcoder, secondary DSI, cursor, overlay, scaler, DSC or +// DSB is adopted implicitly. Recheck power before reading any pipe-local bank. +template bool bootPeers(IO &io,uint32_t poweredPipes) { + if(!(poweredPipes&1) || (poweredPipes&~7U))return false; + const auto value=[&](uint32_t reg,uint32_t mask,uint32_t expected) { + if(!io.readable(reg))return false;const auto v=io.read(reg); + return v!=UINT32_MAX && (v&mask)==expected; + }; + if(!io.readable(0x45404))return false; + const auto power=io.read(0x45404); + if(power==UINT32_MAX || (power&3)!=3 || + (((power>>10)&1)|((power>>11)&2)|((power>>12)&4))!=poweredPipes || + ((poweredPipes&6) && (power&5)!=5) || !value(0x7B808,0xC0000000U,0))return false; + for(unsigned p=0;p<3;++p)if(poweredPipes&(1U< bool bootUnchanged(IO &io,const Boot &boot,const XELPMemory::Layout &memory) { + if(!boot.usable() || !XELPMemory::same(boot.memory,memory) || + !sourceUnchanged(io,boot.source) || !bootPeers(io,boot.poweredPipes))return false; + const uint32_t regs[]={0x42084,0x70180,0x70188,0x70190,0x7019C,0x701AC,0x7018C,0x701A4}; + const uint32_t expected[]={boot.dpt,boot.control,boot.stride,boot.size,boot.surface,boot.surface,0,0}; + for(unsigned i=0;i void captureBoot(IO &io,const AlderLakeN::DisplayReadout &normal, + const Profile &profile,const XELPMemory::Layout &memory,bool admitted,Boot &r) { + r={};if(!admitted || !profile.valid())return; + r.status=BootStatus::Memory; + if(!memory.valid || !memory.reservationKnown || !memory.bytes || memory.bytes%4096 || + memory.bytes>XELPMemory::Aperture || memory.bytes>memory.apertureBytes || !io.readable(0x800000+8*(memory.bytes/4096)-1))return; + r.memory=memory;r.poweredPipes=normal.poweredPipes;r.status=BootStatus::Peers; + if(normal.status!=AlderLakeN::ReadoutStatus::Complete || normal.activePipes || + !bootPeers(io,r.poweredPipes))return; + r.status=BootStatus::Source; + if(!io.readable(0x51004))return; + const auto ref=(io.read(0x51004)>>29)&7;const uint32_t references[]={24000,19200,38400}; + if(ref>=3)return; + r.source=decodeReadout([&](uint32_t reg,uint32_t &v) { + if(!io.readable(reg))return false;v=io.read(reg);return true; + },references[ref]); + if(r.source.identity.pipe!=0 || !r.source.matches(profile))return; + r.status=BootStatus::Geometry; + const uint32_t regs[]={0x42084,0x70180,0x70188,0x70190,0x7019C,0x701AC,0x7018C,0x701A4}; + uint32_t words[8]{}; + for(unsigned i=0;i<8;++i){if(!io.readable(regs[i]))return;words[i]=io.read(regs[i]);if(words[i]==UINT32_MAX)return;} + r.dpt=words[0];r.control=words[1];r.stride=words[2];r.size=words[3];r.surface=words[4]; + r.geometry=XELPGeometry::linear(r.control,r.stride,r.size,words[6],words[7]); + // LINEAR uses byte GGTT offsets even with global DPT-disable clear. + if(!XELPGeometry::timing(r.geometry,r.source.htotal,r.source.vtotal) || r.surface!=words[5] || + !memory.range(r.surface,r.geometry.bytes,true))return; + r.status=BootStatus::Candidate; + if(!bootUnchanged(io,r,memory))r.status=BootStatus::Changed; + return; +} +template Boot captureBoot(IO &io,const AlderLakeN::DisplayReadout &normal, + const Profile &profile,const XELPMemory::Layout &memory,bool admitted) { + Boot r;captureBoot(io,normal,profile,memory,admitted,r);return r; +} +// Prove the full stolen prefix that the native allocator will protect, not +// merely the visible pixels. This does not grant an allocator reservation. +template bool verifyBootPrefix(IO &io,Boot &boot,const XELPMemory::Layout &memory) { + if(!bootUnchanged(io,boot,memory)){boot.status=BootStatus::Changed;return false;} + boot.verifiedPages=0; + for(uint32_t i=0;i void auditBoot(IO &io,const AlderLakeN::DisplayReadout &normal, + const Profile &profile,const XELPMemory::Layout &memory, + bool admitted,Boot &scratch,BootAudit &audit) { + audit={};auto q=audit.words;q[0]=1;q[2]=admitted;q[3]=uint32_t(profile.status); + q[4]=uint32_t(normal.status);q[5]=normal.activePipes;q[6]=normal.poweredPipes; + q[7]=q[9]=UINT32_MAX;q[13]=memory.bytes/4096;q[14]=memory.base; + q[15]=memory.bytes;q[16]=memory.allocationEnd; + // Only read operations are exposed, including when the caller's IO can + // write. Preserve the last attempted access for a bounded failure report. + struct ReadOnly { + IO &io;uint64_t *q; + bool readable(uint32_t reg) { + if(io.readable(reg))return true;q[7]=reg;q[8]=UINT32_MAX;return false; + } + uint32_t read(uint32_t reg) {q[7]=reg;q[8]=io.read(reg);return uint32_t(q[8]);} + uint64_t readPTE(uint32_t reg) {q[9]=reg;q[10]=io.readPTE(reg);return q[10];} + } ro {io,q}; + captureBoot(ro,normal,profile,memory,admitted,scratch); + if(scratch.usable())verifyBootPrefix(ro,scratch,memory); + q[1]=uint32_t(scratch.status);q[12]=scratch.verifiedPages; + if(scratch.status==BootStatus::PTE)q[11]=memory.pte(uint64_t(scratch.verifiedPages)*4096); + q[17]=scratch.control;q[18]=scratch.stride;q[19]=scratch.size;q[20]=scratch.surface;q[21]=scratch.dpt; + q[22]=scratch.geometry.width;q[23]=scratch.geometry.height;q[24]=scratch.geometry.pitch;q[25]=scratch.geometry.bytes; + q[26]=scratch.source.identity.pipe;q[27]=scratch.source.identity.transcoder; + q[28]=scratch.source.identity.phy;q[29]=scratch.source.identity.pll; + q[30]=scratch.source.afeKHz;q[31]=uint32_t(scratch.source.status); +} +}} +#endif diff --git a/WhateverXe/kern_dsi_clock.hpp b/WhateverXe/kern_dsi_clock.hpp new file mode 100644 index 00000000..991e4991 --- /dev/null +++ b/WhateverXe/kern_dsi_clock.hpp @@ -0,0 +1,81 @@ +// Retire an exclusively retained Display13 DSI Combo PLL after host/IRQ stop. +// Sequence: Linux v6.12 icl_pll_disable; register bits from i915_reg.h. +#ifndef KERN_DSI_CLOCK_HPP +#define KERN_DSI_CLOCK_HPP +#include "kern_dsi_quiesce.hpp" +#include "kern_xelp_combo_clock.hpp" +namespace XELP { namespace DSI { namespace Clock { +enum class Status : uint32_t {Input,Ownership,Consumer,Peer,Source,Disable,Power,Complete}; +struct Result { + Status status {Status::Input};uint32_t reg {},before {},after {},writes {},waitUs {}; + bool complete() const {return status==Status::Complete;} +}; +// Caller holds shared Combo/FB/owner serialization and has parked the actual +// native IRQ bank under its workloop gate. No waits under the IRQ spinlock. +// IO::ready checks retained ownership, not the original running source image. +// Keep the external allocation on every failure, including a half-disabled PLL. +// Never commandeer a peer PLL or clear a firmware power-well request here. +template Result retire(IO &io,const Readout &link,const Quiesce::Result &host,bool irqParked, + XELPComboClock::ClockAllocation &allocation,uintptr_t owner) { + Result r; + if(!owner || !link.complete() || !link.identity.valid() || link.identity.pipe!=0 || !host.complete() || !irqParked)return r; + XELPComboClock::PLL pll;if(!XELPComboClock::PLL::resolve(link.identity.pll,pll))return r; + r.reg=pll.enable; + const auto value=[&](uint32_t reg,uint32_t &out) { + if(!io.readable(reg))return false; + out=io.read(reg);return out!=UINT32_MAX; + }; + const auto owned=[&]() { + uint32_t wells=0; + return io.ready() && allocation.external[0]==owner && allocation.binding[0]==link.identity.pll+1 && + value(0x45404,wells) && (wells&0x403)==0x403; + }; + const auto stopped=[&]() { + Display13Transcoder::Bindings bindings;bindings.dsi0Pipe=0; + uint32_t buffer=0,secondary=0,mux=0; + // host.complete() witnessed ULPS before disabling the function. The + // request self-clears and host disable may reset its status. + return Display13FBIRQ::consumersStopped(io,1,bindings) && value(0x64000,buffer) && + (buffer&0x80000080U)==0x80 && value(0x7B808,secondary) && !(secondary&0xC0000000U) && + value(0x6BC00,secondary) && !(secondary&(1U<<31)) && + value(0x164280,mux) && (mux&0x403U)==(0x400U|link.identity.pll); + }; + const auto exclusive=[&]() { + XELPComboClock::ClockSnapshot s;s.layout=XELPComboClock::Layout::Display13; + if(!value(0x164280,s.mux[0]))return false; + if(s.peers(XELPComboClock::Port(0))&(1U<=1000)return false; + io.delayUs(10);r.waitUs+=10; + } + }; + // Require both request and state bits clear. A lost write must not be + // accepted merely because its status bit happened to read unlocked. + r.status=Status::Disable;io.write(pll.enable,r.before&~(1U<<31));++r.writes; + if(!wait(0xC0000000U))return r; + r.status=Status::Power;io.write(pll.enable,r.after&~(1U<<27));++r.writes; + if(!wait(0xCC000000U) || !held())return r; + if(!allocation.release(0,owner)){r.status=Status::Ownership;return r;} + r.status=Status::Complete;return r; +} +}}} +#endif diff --git a/WhateverXe/kern_dsi_commands.hpp b/WhateverXe/kern_dsi_commands.hpp new file mode 100644 index 00000000..e2a0199f --- /dev/null +++ b/WhateverXe/kern_dsi_commands.hpp @@ -0,0 +1,29 @@ +// Intel icl_dsi.c / icl_dsi_regs.h v7.0 command transport. The owning adapter +// serializes the sole producer and holds the host's power/clock throughout. +#ifndef KERN_DSI_COMMANDS_HPP +#define KERN_DSI_COMMANDS_HPP +#include "kern_dsi_host.hpp" +namespace XELP { namespace DSI { +template struct CommandPort { + IO &io; + bool failed {};uint32_t writes {}; + bool cancelled(){return failed || !io.ready();} + bool read(uint32_t reg,uint32_t &value) { + if(cancelled() || !io.readable(reg))return false; + value=io.read(reg);return value!=UINT32_MAX; + } + void write(uint32_t reg,uint32_t value) { + if(cancelled() || !io.readable(reg)){failed=true;return;} + io.write(reg,value);++writes; + } + bool credits(uint32_t &value){return read(0x6B0D0,value);} + bool lpBusy(bool &busy) { + uint32_t value=0;if(!read(0x6B0D8,value) || (value&((1U<<16)|1U)))return false; + busy=value&(1U<<17);return true; + } + void payload(uint32_t value){write(0x6B104,value);} + void header(uint32_t value){write(0x6B100,value);} + void delayUs(unsigned us){io.delayUs(us);} +}; +}} +#endif diff --git a/WhateverXe/kern_dsi_ggtt.hpp b/WhateverXe/kern_dsi_ggtt.hpp new file mode 100644 index 00000000..7554287c --- /dev/null +++ b/WhateverXe/kern_dsi_ggtt.hpp @@ -0,0 +1,37 @@ +// Configure shared Display13 scanout addressing before native display users. +// Linux v6.12 intel_fb_modifier_uses_dpt excludes LINEAR; tiled native surfaces +// use GGTT addresses and need CHICKEN_MISC_DISABLE_DPT (i915_reg.h). +#ifndef KERN_DSI_GGTT_HPP +#define KERN_DSI_GGTT_HPP +#include "kern_dsi_boot.hpp" +namespace XELP { namespace DSI { +struct GGTTMode { + enum class Status : uint32_t {Unprepared,Source,Readback,Changed,Complete}; + Status status {Status::Unprepared};uint32_t before {},after {},writes {}; + bool ready() const {return status==Status::Complete;} +}; +// Caller owns startup serialization, source power/PLL, and the global display +// mode. No external output or native engine may have started. This changes no +// PTE, plane, timing, or DSI register. Preserve every unrelated chicken bit. +// Do not restore the global mode when only DSI stops: external tiled consumers +// share it. A failed write remains recorded for controller-level cleanup. +template bool prepareGGTT(IO &io,Boot &boot,const XELPMemory::Layout &memory,GGTTMode &mode) { + if(mode.status!=GGTTMode::Status::Unprepared)return false; + mode.status=GGTTMode::Status::Source; + if(!boot.complete() || !bootUnchanged(io,boot,memory))return false; + mode.before=boot.dpt;mode.after=mode.before|(1U<<30); + if(mode.before!=mode.after) { + io.write(0x42084,mode.after);++mode.writes; + mode.status=GGTTMode::Status::Readback; + if(!io.readable(0x42084) || io.read(0x42084)!=mode.after) { + boot.status=BootStatus::Changed;return false; + } + } + // Extend the certificate only after exact posting readback. No caller may + // keep using the earlier dpt word as proof once this write has happened. + boot.dpt=mode.after;mode.status=GGTTMode::Status::Changed; + if(!bootUnchanged(io,boot,memory)){boot.status=BootStatus::Changed;return false;} + mode.status=GGTTMode::Status::Complete;return true; +} +}} +#endif diff --git a/WhateverXe/kern_dsi_host.hpp b/WhateverXe/kern_dsi_host.hpp new file mode 100644 index 00000000..6c2e84ae --- /dev/null +++ b/WhateverXe/kern_dsi_host.hpp @@ -0,0 +1,117 @@ +// Pure packet/sequence engine; IO is supplied by an owning, serialized adapter. +// Intel icl_dsi.c/icl_dsi_regs.h (Linux v7.0) define credit and LP drain rules. +// No Apple objects, shared-resource allocation, or hardware MMIO lives here. +#ifndef KERN_DSI_HOST_HPP +#define KERN_DSI_HOST_HPP +#include "kern_dsi_sequence.hpp" +namespace XELP { namespace DSI { +enum class HostError : uint32_t {None, Disabled, Invalid, Cancelled, Unavailable, CreditTimeout, DrainTimeout, GPIO}; +struct Packet { + bool valid {};uint32_t header {};Span payload; +}; +inline Packet packet(uint8_t type,Span data) { + Packet p; + if(!data.data || !data.size || data.size>256)return p; + uint32_t parameters=0; + if(type==0x05 && data.size==1)parameters=data.data[0]; + else if(type==0x15 && data.size==2)parameters=data.data[0]|uint32_t(data.data[1])<<8; + else if(type==0x39){parameters=uint32_t(data.size);p.payload=data;} + else return p; + // Fresh header: VC0, LP, no stale fence/flush or short-packet payload bit. + p.header=(p.payload.data?1U<<31:0)|(1U<<30)|(parameters<<8)|type; + p.valid=true;return p; +} +struct Host { + HostError error {HostError::None}; + bool enabled {},pending {},faulted {}; + uint32_t payloadWords {},headers {},completedOperations {},sequenceId {},operationOffset {}; + uint64_t waitedUs {}; + explicit Host(bool writable=false):enabled(writable) {} + bool fail(HostError e) {if(error==HostError::None)error=e;faulted=true;return false;} + // No implicit recovery/release after failure. Adapter must stop hardware, + // retain uncertain leases, and construct a new Host only after recovery. + template bool ready(IO &io) { + if(faulted)return false; + if(!enabled)return fail(HostError::Disabled); + if(io.cancelled())return fail(HostError::Cancelled); + return true; + } + template bool credits(IO &io,unsigned h,unsigned p,unsigned budgetUs=100) { + if(budgetUs>200000 || budgetUs%10)return fail(HostError::Invalid); + for(unsigned us=0;;us+=10) { + if(!ready(io))return false; + uint32_t status=0; + if(!io.credits(status) || status==UINT32_MAX)return fail(HostError::Unavailable); + const auto headers=(status>>8)&31,payload=status&255; + if(headers>16 || payload>64)return fail(HostError::Unavailable); + if(headers>=h && payload>=p)return true; + if(us==budgetUs)return fail(HostError::CreditTimeout); + io.delayUs(10);waitedUs+=10; + } + } + template bool send(IO &io,uint8_t type,Span data,unsigned creditBudgetUs=100) { + const auto p=packet(type,data); + if(!p.valid)return fail(HostError::Invalid); + if(!ready(io))return false; + // Reserve sufficient credit before the first payload write. The caller + // serializes the only producer; hardware can only return more credit. + if(!credits(io,1,unsigned((p.payload.size+3)/4),creditBudgetUs))return false; + for(size_t at=0;at bool drain(IO &io,unsigned creditBudgetUs=100) { + if(!ready(io))return false; + if(!pending)return true; + if(!credits(io,16,64,creditBudgetUs))return false; + const uint8_t nop=0; + if(!send(io,0x05,{&nop,1},creditBudgetUs) || !credits(io,16,0,creditBudgetUs))return false; + for(unsigned us=0;;us+=2) { + if(!ready(io))return false; + bool busy=false; + if(!io.lpBusy(busy))return fail(HostError::Unavailable); + if(!busy){pending=false;return true;} + if(us==20)return fail(HostError::DrainTimeout); + io.delayUs(2);waitedUs+=2; + } + } + template bool delay(IO &io,uint32_t us) { + while(us) { + if(!ready(io))return false; + const auto chunk=us>1000?1000:us; + io.delayUs(chunk);waitedUs+=chunk;us-=chunk; + } + return ready(io); + } + template bool run(IO &io,const Sequences &sequences,unsigned id,unsigned creditBudgetUs=100) { + if(faulted)return false; + if(!sequences.valid() || !id || id>=12 || creditBudgetUs>200000 || creditBudgetUs%10)return fail(HostError::Invalid); + sequenceId=id;operationOffset=0; + if(absentSequence(sequences.sequence[id]))return drain(io,creditBudgetUs); + const auto bytes=sequences.sequence[id];Sequences check; + if(validateSequence(bytes,check)!=SequenceStatus::Valid)return fail(HostError::Invalid); + if(!ready(io))return false; + for(size_t at=0;atUINT32_MAX)return r; + r.afeKHz=uint32_t(afe);r.portKHz=r.afeKHz/5; + // ADL-P/N escape word clock divider must be odd, >=3, <=20 MHz. + auto word=(afe+159999)/160000;word|=1;if(word<3)word=3; + // Both escape-divider registers have nine bits. PLL feasibility and + // scanout bandwidth are checked by their respective consumers. + if(word*8>0x1FF)return {}; + r.escapeDivider=uint32_t(word*8);r.phyEscapeDivider=uint32_t((word-1)/2); + constexpr uint32_t tlpx=50; // byteClock=0: 20 MHz escape timing reference + r.clockTiming=(1U<<31)|(cappedCeil(p.clockPrepare*4,tlpx,7)<<28)|(1U<<27)| + (cappedCeil(p.clockPrepareZero-p.clockPrepare,tlpx,15)<<20)|(1U<<19)|(cappedCeil(p.clockPre,tlpx,3)<<16); + r.dataTiming=(1U<<31)|(cappedCeil(p.hsPrepare*4,tlpx,7)<<24)|(1U<<23)| + (cappedCeil(p.hsPrepareZero-p.hsPrepare,tlpx,15)<<16)|(1U<<7)|cappedCeil(p.hsExit,tlpx,7); + r.htotal=((m.htotal()-1)<<16)|(m.width-1); + r.vtotal=((m.vtotal()-1)<<16)|(m.height-1); + r.hsync=((uint32_t(m.width)+m.hfront+m.hsync-1)<<16)|(uint32_t(m.width)+m.hfront-1); + r.vsync=((uint32_t(m.height)+m.vfront+m.vsync-1)<<16)|(uint32_t(m.height)+m.vfront-1); + r.valid=true;return r; +} +struct PLLPlan { + bool valid {};uint32_t referenceKHz {},dcoKHz {},divider {},p {},q {},k {},integer {},fraction {}; + uint32_t cfg0() const {return valid?(fraction<<10)|integer:0;} + uint32_t cfg1() const { + const auto pe=p==2?1U:p==3?2U:p==5?4U:p==7?8U:0; + const auto ke=k==1?1U:k==2?2U:k==3?4U:0; + return valid?(q<<10)|(q!=1?1U<<9:0)|(ke<<6)|(pe<<2):0; + } +}; +// Decode the TGL/ADL Combo format (fraction starts at bit10, not the older +// SKL WRPLL's bit9). Return AFE kHz before the DSI lane/bpp conversion. +inline uint32_t pllAFE(uint32_t cfg0,uint32_t cfg1,uint32_t referenceKHz) { + if((referenceKHz!=19200 && referenceKHz!=24000 && referenceKHz!=38400) || + (cfg0&~0x1FFFFFFU) || (cfg1&~0x3FFFCU))return 0; + const auto pe=(cfg1>>2)&15,ke=(cfg1>>6)&7; + const auto p=pe==1?2U:pe==2?3U:pe==4?5U:pe==8?7U:0; + const auto k=ke==1?1U:ke==2?2U:ke==4?3U:0; + const auto q=(cfg1&(1U<<9))?((cfg1>>10)&255):1; + if(!p || !k || !q || (k!=2 && q!=1))return 0; + const auto integer=cfg0&1023;auto fraction=(cfg0>>10)&32767; + if(referenceKHz==38400){referenceKHz=19200;fraction*=2;} + const auto dco=uint64_t(integer)*referenceKHz+uint64_t(fraction)*referenceKHz/32768; + if(dco<7998000 || dco>10000000)return 0; + return uint32_t(dco/(p*q*k)); +} +inline PLLPlan pllPlan(uint32_t portKHz,uint32_t referenceKHz) { + PLLPlan r; + if(!portKHz || (referenceKHz!=19200 && referenceKHz!=24000 && referenceKHz!=38400))return r; + // Hardware divider choices, in the documented tie-breaking order. + static const uint8_t dividers[]={2,4,6,8,10,12,14,16,18,20,24,28,30,32,36,40,42,44,48,50,52,54, + 56,60,64,66,68,70,72,76,78,80,84,88,90,92,96,98,100,102,3,5,7,9,15,21}; + uint32_t distance=UINT32_MAX; + for(auto d:dividers) { + const uint64_t dco=uint64_t(portKHz)*5*d; + if(dco<7998000 || dco>10000000)continue; + const auto diff=uint32_t(dco>8999000?dco-8999000:8999000-dco); + if(diff>15);r.fraction=uint32_t(fixed&0x7FFF); + // Display WA 22010492432: 38.4 MHz uses half the nominal fraction. + if(referenceKHz==38400)r.fraction=(r.fraction+1)/2; + r.valid=true;return r; +} +}} +#endif diff --git a/WhateverXe/kern_dsi_native_backing.hpp b/WhateverXe/kern_dsi_native_backing.hpp new file mode 100644 index 00000000..8d6dc612 --- /dev/null +++ b/WhateverXe/kern_dsi_native_backing.hpp @@ -0,0 +1,37 @@ +// Exact TGL IntelScaledModeData wire record for a pending DSI scanout. +// Allocation/free use the native allocscanmem/freescanmem platform functions. +// The adapter must retain the record and serialize the native display pipe; +// allocating also changes its memory/map references, before FB publication. +#ifndef KERN_DSI_NATIVE_BACKING_HPP +#define KERN_DSI_NATIVE_BACKING_HPP +#include "kern_dsi_profile.hpp" +#include "kern_display13_fb_memory.hpp" +#include +namespace XELP { namespace DSI { namespace NativeBacking { +struct Record { + uint32_t reserved {},columns {},rows {},flags {}; + uint64_t map {},memory {},gpu {}; + uint32_t bytes {},pitch {},reserved30 {},reserved34 {},refs {},kind {}; +}; +static_assert(sizeof(Record)==64 && offsetof(Record,map)==0x10 && offsetof(Record,refs)==0x38,"native scanout ABI"); +// Native tile mode 3 gives a 128-byte pitch unit and 32-row tiles. This is +// uncompressed Y, the format admitted by the shared Display13 WM backend. +// Flags zero avoids the optional fence allocation/free path. +inline bool request(const Profile &p,uint32_t nativeTileMode,Record &out) { + if(!p.valid() || !p.mode.valid() || p.mode.width>8192 || p.mode.height>8192 || nativeTileMode!=3)return false; + Record r;r.columns=(uint32_t(p.mode.width)*4+127)/128;r.rows=(uint32_t(p.mode.height)+31)/32;r.kind=1; + // Leave bytes zero as the native FB does: allocator computes tiles*4096. + out=r;return true; +} +inline bool allocated(const Record &r,const Profile &p,const Display13FBMemory::SurfaceReadout &proof) { + Record expected;if(!request(p,3,expected))return false; + return r.columns==expected.columns && r.rows==expected.rows && !r.flags && r.kind==1 && r.refs==1 && + !r.reserved && !r.reserved30 && !r.reserved34 && !r.bytes && + proof.status==Display13FBMemory::SurfaceStatus::Complete && + proof.record==reinterpret_cast(&r) && proof.refs==r.refs && proof.kind==r.kind && proof.flags==r.flags && + proof.map==r.map && proof.memory==r.memory && + proof.gpu==r.gpu && proof.bytes==uint64_t(r.columns)*r.rows*4096 && + proof.pitch==r.pitch && r.pitch==r.columns*128 && proof.verifiedPages==uint64_t(r.columns)*r.rows; +} +}}} +#endif diff --git a/WhateverXe/kern_dsi_native_cache.hpp b/WhateverXe/kern_dsi_native_cache.hpp new file mode 100644 index 00000000..56b4c4e8 --- /dev/null +++ b/WhateverXe/kern_dsi_native_cache.hpp @@ -0,0 +1,80 @@ +// Exact donor boot/active cache view. This is not a register-address translator or a +// restore/programming image: DSI mode/stop must use their own adapter. DP M/N, +// MSA, normal DDI function and unsupported HBLANK have no DSI counterpart here. +#ifndef KERN_DSI_NATIVE_CACHE_HPP +#define KERN_DSI_NATIVE_CACHE_HPP +#include "kern_dsi_boot.hpp" +namespace XELP { namespace DSI { namespace NativeCache { +struct View { + uint32_t timing[6] {},source {},conf {},misc {},shift {},csc {},cursor {}; + uint32_t plane[6] {},scaler[3] {}; +}; +constexpr uint32_t PlaneRegisters[]={0x701A4,0x70180,0x701CC,0x70190,0x70188,0x7019C}; +constexpr uint32_t PlaneAddressFields[]={0xA8,0x98,0x9C,0xB4,0xB0,0xB8}; +constexpr uint32_t PlaneCacheFields[]={0x110,0x100,0x104,0x11C,0x118,0x120}; +// Caller owns the FB and shared power locks. Take two bounded snapshots and +// publish no CPU cache field until both agree and the GOP certificate holds. +template bool read(IO &io,const Boot &boot,const XELPMemory::Layout &memory,uint32_t scaler,View &out) { + if(!boot.complete() || boot.source.identity.pipe!=0 || (scaler && scaler!=0x68180 && scaler!=0x68280))return false; + const auto capture=[&](View &v) { + if(!bootUnchanged(io,boot,memory))return false; + const auto word=[&](uint32_t reg,uint32_t &value) { + if(!io.readable(reg))return false;value=io.read(reg);return value!=UINT32_MAX; + }; + // HBLANK is absent in video DSI. Fixed-mode geometry comes from the + // retained IODetailedTiming array, not this native restore cache. + const uint32_t timing[]={0x6B000,0,0x6B008,0x6B00C,0x6B010,0x6B014}; + for(unsigned i=0;i<6;++i)if(timing[i] && !word(timing[i],v.timing[i]))return false; + if(!word(0x6001C,v.source) || !word(0x7B008,v.conf) || !word(0x70030,v.misc) || + !word(0x6B028,v.shift) || !word(0x4A480,v.csc) || !word(0x70080,v.cursor))return false; + for(unsigned i=0;i<6;++i)if(!word(PlaneRegisters[i],v.plane[i]))return false; + if(scaler && (!word(scaler,v.scaler[0]) || !word(scaler-0x10,v.scaler[1]) || !word(scaler-0xC,v.scaler[2])))return false; + return v.timing[0]==boot.source.htotal && v.timing[2]==boot.source.hsync && + v.timing[3]==boot.source.vtotal && v.timing[4]==v.timing[3] && v.timing[5]==boot.source.vsync && + v.source==((boot.source.htotal&65535)<<16 | (boot.source.vtotal&65535)) && + v.conf==boot.source.conf && !(v.cursor&63) && + v.plane[0]==0 && v.plane[1]==boot.control && v.plane[3]==boot.size && + v.plane[4]==boot.stride && v.plane[5]==boot.surface && !(v.scaler[0]&0x80000000U) && + bootUnchanged(io,boot,memory); + }; + View a,b;if(!capture(a) || !capture(b) || memcmp(&a,&b,sizeof(a)))return false; + out=a;return true; +} +// Once native backing is public, plane surfaces and cursor/color caches +// belong to ordinary native transactions. Check the retained DSI link and +// fixed source geometry, but do not require the obsolete GOP plane image. +template bool readActive(IO &io,const Boot &boot,uint32_t scaler,View &out) { + if(!boot.complete() || boot.source.identity.pipe!=0 || (scaler && scaler!=0x68180 && scaler!=0x68280))return false; + const auto capture=[&](View &v) { + if(!sourceUnchanged(io,boot.source))return false; + const auto word=[&](uint32_t r,uint32_t &value) { + if(!io.readable(r))return false;value=io.read(r);return value!=UINT32_MAX; + }; + const uint32_t timing[]={0x6B000,0,0x6B008,0x6B00C,0x6B010,0x6B014}; + for(unsigned i=0;i<6;++i)if(timing[i] && !word(timing[i],v.timing[i]))return false; + if(!word(0x6001C,v.source) || !word(0x7B008,v.conf) || !word(0x70030,v.misc) || + !word(0x6B028,v.shift) || !word(0x4A480,v.csc) || !word(0x70080,v.cursor))return false; + for(unsigned i=0;i<6;++i)if(!word(PlaneRegisters[i],v.plane[i]))return false; + if(scaler && (!word(scaler,v.scaler[0]) || !word(scaler-0x10,v.scaler[1]) || !word(scaler-0xC,v.scaler[2])))return false; + return v.timing[0]==boot.source.htotal && v.timing[2]==boot.source.hsync && + v.timing[3]==boot.source.vtotal && v.timing[4]==v.timing[3] && v.timing[5]==boot.source.vsync && + v.source==((boot.source.htotal&65535)<<16 | (boot.source.vtotal&65535)) && + v.conf==boot.source.conf && sourceUnchanged(io,boot.source); + }; + View a,b;if(!capture(a) || !capture(b) || memcmp(&a,&b,sizeof(a)))return false; + out=a;return true; +} +// Class sizes and actual path/plane/scaler associations are checked by the +// kernel adapter. Only cache words change; register addresses/ownership don't. +inline void publish(const View &v,void *path,void *plane,void *scaler) { + const auto put=[](void *object,uint32_t offset,uint32_t value){memcpy(static_cast(object)+offset,&value,4);}; + for(unsigned i=0;i<6;++i)put(path,0x138+4*i,v.timing[i]); + const uint32_t absent[]={0x12C,0x130,0x134,0x160,0x164,0x168,0x16C}; + for(auto offset:absent)put(path,offset,0); + put(path,0x150,v.source);put(path,0x154,v.conf);put(path,0x158,v.misc);put(path,0x15C,v.shift); + put(path,0x18C,v.csc);put(path,0x170,v.cursor&~0x8000U); + for(unsigned i=0;i<6;++i)put(plane,PlaneCacheFields[i],v.plane[i]); + if(scaler)for(unsigned i=0;i<3;++i)put(scaler,0x44+4*i,v.scaler[i]); +} +}}} +#endif diff --git a/WhateverXe/kern_dsi_native_mode.hpp b/WhateverXe/kern_dsi_native_mode.hpp new file mode 100644 index 00000000..33d8e9e7 --- /dev/null +++ b/WhateverXe/kern_dsi_native_mode.hpp @@ -0,0 +1,57 @@ +// CPU-only fixed-mode preparation for the exact TGL framebuffer donor. +// Host programming, backing publication and transaction admission are separate. +#ifndef KERN_DSI_NATIVE_MODE_HPP +#define KERN_DSI_NATIVE_MODE_HPP +#include "kern_dsi_apple_timing.hpp" +#include "kern_display13_fb_memory.hpp" +namespace XELP { namespace DSI { namespace AppleAdapter { +// The caller holds the FB mutex and validates object classes/ownership. No +// wire timing changes are admitted. GPU presentation preparation retains the physical +// scanout; its separate logical buffer must be retired by the caller first. +template bool prepareGeometry(void *fb,void *path,const Profile &profile, + const Timing &timing,Compute compute,bool presentation=false) { + using Display13FBMemory::field; + Timing expected; + if(!makeTiming(profile,expected))return false; + const bool scaled=presentation && (timing.horizontalScaled || timing.verticalScaled); + if(presentation) { + expected.scalerFlags=timing.scalerFlags; + expected.horizontalScaled=timing.horizontalScaled;expected.verticalScaled=timing.verticalScaled; + expected.horizontalInset=timing.horizontalInset;expected.verticalInset=timing.verticalInset; + } + if(!fb || !path || memcmp(&expected,&timing,sizeof(timing)) || + Display13FBMemory::singlePath(fb)!=path || field(fb,0x4340) || + (!presentation && field(fb,0x4348)))return false; + // Preserve the native CPU state if the donor produces an unsupported + // transform. The helper writes only these geometry fields for this mode. + uint8_t savedFB[0x30],savedPath[0x20]; + memcpy(savedFB,static_cast(fb)+0x430C,sizeof(savedFB)); + memcpy(savedPath,static_cast(path)+0xF0,sizeof(savedPath)); + const auto bpp=field(fb,0x4350);const auto tiled=field(fb,0x4A18); + const auto put=[](void *o,unsigned offset,uint32_t v){memcpy(static_cast(o)+offset,&v,4);}; + put(fb,0x4350,4);*(static_cast(fb)+0x4A18)=1; + compute(fb,&timing); + const auto &m=profile.mode;const uint32_t width=scaled?timing.horizontalScaled:m.width,height=scaled?timing.verticalScaled:m.height; + // Check the allocated geometry, not an OS rotation/mode whitelist. This + // transaction retains the scanout and can own a separate GPU source only. + const uint32_t pitch=((width*4+127)/128)*128; + const auto transform=field(fb,0x4338); + const bool gpuTransform=(transform&0x30) && !(transform&~0x30U); + const bool valid=width && height && uint64_t(width)*4+127<=UINT32_MAX && + uint64_t(pitch)*height<=UINT32_MAX && field(fb,0x430C)==width && field(fb,0x4310)==height && + field(fb,0x4314)==pitch*m.height && field(fb,0x4320)==m.width && + field(fb,0x4324)==m.height && field(fb,0x4328)==m.width && + field(fb,0x432C)==m.height && field(fb,0x4334)==pitch && + (gpuTransform || (transform==1 && width==m.width && height==m.height)) && !field(path,0xF0) && + field(path,0xF8)==m.width && field(path,0xFC)==m.height && + !field(path,0x100) && field(path,0x108)==m.width && + field(path,0x10C)==m.height; + if(!valid) { + memcpy(static_cast(fb)+0x430C,savedFB,sizeof(savedFB)); + memcpy(static_cast(path)+0xF0,savedPath,sizeof(savedPath)); + put(fb,0x4350,bpp);*(static_cast(fb)+0x4A18)=tiled; + } + return valid; +} +}}} +#endif diff --git a/WhateverXe/kern_dsi_panel.hpp b/WhateverXe/kern_dsi_panel.hpp new file mode 100644 index 00000000..42624d40 --- /dev/null +++ b/WhateverXe/kern_dsi_panel.hpp @@ -0,0 +1,203 @@ +// Native display GPIOs used by Intel VBT DSI sequences (intel_dsi_vbt.c v7.0). +// GPIO role is a display function, not an OS gpiochip number. The adapter must +// hold the display IRQ lock across RESET's HPD control read/modify/write only. +#ifndef KERN_DSI_PANEL_HPP +#define KERN_DSI_PANEL_HPP +#include "kern_dsi_commands.hpp" +#include "kern_dsi_surface.hpp" +#include "kern_dsi_backlight.hpp" +namespace XELP { namespace DSI { namespace Panel { +struct GPIOResult {uint32_t role {},value {},reg {},before {},after {},writes {};bool complete {};}; +template GPIOResult gpio(IO &io,unsigned role,unsigned value) { + GPIOResult r;r.role=role;r.value=value; + if(!nativePanelGPIO(role) || value>1 || !io.ready())return r; + const unsigned bank=role/5,function=role%5; + r.reg=function==0?0xC4030:0xC7204+0x100*bank; + if(!io.readable(r.reg))return r; + r.before=io.read(r.reg);if(r.before==UINT32_MAX)return r; + const uint32_t mask=function==0?(12U<<(4*bank)):function==1?1U:4U; + const uint32_t bits=value?(function==0?(4U<<(4*bank)):function==1?1U:4U):0; + // HPD status is W1C. Reset must not acknowledge another port's event. + const auto next=(r.before&~(mask|(function==0?0x3333U:0U)))|bits; + if(!io.ready())return r; + io.write(r.reg,next);++r.writes; + r.after=io.read(r.reg); + r.complete=r.after!=UINT32_MAX && (r.after&mask)==bits && io.ready();return r; +} +enum class Stage : uint32_t { + Input,BacklightOff,PlaneOff,TranscoderOff,DisplayOff,AssertReset,PowerOff, + PowerCycle,PowerOn,DeassertReset,Init,TranscoderOn,PlaneOn,BacklightOn,Complete,HostOff,HostOn +}; +enum class Error : uint32_t {None,Input,Ownership,Readback,Timeout,Sequence,Host}; +struct Result { + Stage stage {Stage::Input};Error error {Error::Input}; + uint32_t reg {},value {},writes {},waitUs {},frameBefore {},frameAfter {}; + Host host {};Readout source;bool attempted {}; + bool complete() const {return stage==Stage::Complete && error==Error::None;} +}; +// LP commands submitted during video can wait for a blanking opportunity. +// Bound each credit wait by two actual frame periods, not the stopped host's +// 100 us allowance. Keep completion/drain checks before delays and GPIOs. +inline unsigned videoCreditBudgetUs(const Mode &m) { + if(!m.valid())return 0; + const uint64_t period=(uint64_t(m.htotal())*m.vtotal()*1000+m.clockKHz-1)/m.clockKHz; + if(period<1000 || period>100000)return 0; + return unsigned((2*period+9)/10*10); +} +// Panel half of a serialized runtime shutdown. Host/IRQ/clock/memory remain +// owned by the caller. Reset/power-off must contain no packets after ULPS. +struct Shutdown { + Host host {true};unsigned stage {};bool complete {}; + bool preflight(const Profile &p) { + if(!p.valid() || !videoCreditBudgetUs(p.mode))return false; + constexpr unsigned ids[]={7,4,5,11}; + for(auto id:ids) { + if(absentSequence(p.sequences.sequence[id]))continue; + Sequences n; + if(validateSequence(p.sequences.sequence[id],n)!=SequenceStatus::Valid || + n.delayUs>5000000 || ((id==5 || id==11) && n.packets))return false; + } + return p.ppsUs[2]<=5000000 && p.ppsUs[3]<=5000000; + } + template bool step(IO &io,const Profile &p,unsigned next) { + if(next!=stage+1 || complete)return false; + stage=next; + struct Port:CommandPort { + explicit Port(IO &q):CommandPort{q} {} + bool gpio(unsigned role,unsigned value){return this->io.gpio(role,value);} + } port{io}; + if(next==1)return host.run(port,p.sequences,7,videoCreditBudgetUs(p.mode)) && host.delay(port,p.ppsUs[2]); + if(next==2)return host.run(port,p.sequences,4); + if(next==3) { + complete=host.run(port,p.sequences,5) && host.delay(port,p.ppsUs[3]) && + host.run(port,p.sequences,11) && !host.pending && !port.failed; + return complete; + } + return false; + } +}; +// Bounded bootstrap panel experiment. Keep existing scanout backing pinned. +// Stop pixel transfer, execute the complete panel off/reset/on sequences, and +// restart the same scanout. Lifecycle may stop/rebuild the host between panel +// operations; RetainedHost leaves its settings intact. Neither variant is a +// complete OS DPMS implementation or releases allocation/shared power ownership. +// A verified Shutdown permits the same power-on/init tail on runtime resume; +// native backing, cursor and IRQ lifetime still belong to the owning adapter. +// IO::gpio serializes HPD writes; no delays may run under that IRQ lock. +// A failed transaction is never retried and must retain all resources. +template void cycle(IO &io,const Readout &link,const Profile &profile, + uint32_t planeControl,uint32_t surface,Result &r,Lifecycle &lifecycle,const Shutdown *shutdown=nullptr,Backlight *backlight=nullptr) { + Readout current=link; + if(r.attempted)return; + r.attempted=true; + if((shutdown && (!shutdown->complete || shutdown->host.faulted || shutdown->host.pending)) || + !profile.valid() || !link.matches(profile) || link.identity.pipe!=0 || + !(planeControl&(1U<<31)) || (surface&4095) || !profile.sequences.valid())return; + constexpr unsigned lifecycleIds[]={1,2,3,4,5,6,7,10,11}; + for(auto id:lifecycleIds) { + if(absentSequence(profile.sequences.sequence[id]))continue; + Sequences count; + if(validateSequence(profile.sequences.sequence[id],count)!=SequenceStatus::Valid || count.delayUs>5000000)return; + } + for(auto us:profile.ppsUs)if(us>5000000)return; + const uint64_t period=(uint64_t(profile.mode.htotal())*profile.mode.vtotal()*1000+profile.mode.clockKHz-1)/profile.mode.clockKHz; + if(period<1000 || period>100000)return; + const auto held=[&]() {if(!io.ready()){r.error=Error::Ownership;return false;}return true;}; + const auto read=[&](uint32_t reg,uint32_t &v) { + r.reg=reg;if(!held())return false; + if(!io.readable(reg)){r.error=Error::Readback;return false;} + v=io.read(reg);r.value=v;if(v==UINT32_MAX){r.error=Error::Readback;return false;}return true; + }; + const auto write=[&](uint32_t reg,uint32_t v) { + if(!held() || !io.readable(reg)){r.error=Error::Ownership;return false;} + io.write(reg,v);++r.writes;return true; + }; + const auto wait=[&](uint32_t reg,uint32_t mask,uint32_t value,uint32_t budget) { + for(uint32_t us=0;;us+=1000) { + uint32_t observed=0;if(!read(reg,observed))return false; + if((observed&mask)==value)return true; + if(us>=budget){r.error=Error::Timeout;return false;} + io.delayUs(1000);r.waitUs+=1000; + } + }; + const auto frame=[&]() { + if(!read(0x70040,r.frameBefore))return false; + for(uint32_t us=0;;us+=1000) { + if(!read(0x70040,r.frameAfter))return false; + const auto n=uint32_t(r.frameAfter-r.frameBefore); + if(n && n<0x80000000U)return true; + if(n || us>=3*period+1000){r.error=Error::Timeout;return false;} + io.delayUs(1000);r.waitUs+=1000; + } + }; + struct Port : CommandPort { + explicit Port(IO &q):CommandPort{q} {} + bool gpio(unsigned role,unsigned value){return this->io.gpio(role,value);} + } port{io}; + const auto sequence=[&](unsigned id) { + const bool video=r.stage==Stage::BacklightOff || r.stage==Stage::TranscoderOn || r.stage==Stage::BacklightOn; + const auto budget=video?videoCreditBudgetUs(profile.mode):100; + if(!r.host.run(port,profile.sequences,id,budget)){r.error=Error::Sequence;return false;}return true; + }; + const auto delay=[&](unsigned index) { + if(!r.host.delay(port,profile.ppsUs[index])){r.error=Error::Sequence;return false;}return true; + }; + r.error=Error::Readback; + uint32_t credits=0,lp=0; + if(shutdown) { + // The adapter retains the native backing and all display leases. + // Restart::start separately proves the stopped PLL/host and its owner. + if(!held() || !wait(0x7B008,0xC0000000U,0,0) || !wait(0x70180,1U<<31,0,0) || + !wait(0x70080,63,0,0))return; + r.host=shutdown->host; + } else { + if(!held() || !Surface::source(io,link) || + !Surface::word(io,0x70180,UINT32_MAX,planeControl) || + !Surface::word(io,0x7019C,UINT32_MAX,surface) || + !read(0x6B0D0,credits) || !read(0x6B0D8,lp) || + (credits&0x1FFF)!=0x1040 || (lp&0x30001))return; + r.host.enabled=true; + r.stage=Stage::BacklightOff;if(!sequence(7) || !delay(2))return; + r.stage=Stage::PlaneOff; + if(!write(0x70180,planeControl&~(1U<<31)) || !write(0x7019C,surface) || + !wait(0x70180,1U<<31,0,0) || !frame())return; + r.stage=Stage::TranscoderOff; + if(!write(0x7B008,link.conf&~(1U<<31)) || !wait(0x7B008,0xC0000000U,0,50000))return; + r.stage=Stage::DisplayOff;if(!sequence(4))return; + r.stage=Stage::HostOff;if(!lifecycle.stop(io,link,profile)){r.error=Error::Host;return;} + r.stage=Stage::AssertReset;if(!sequence(5) || !delay(3))return; + r.stage=Stage::PowerOff;if(!sequence(11))return; + } + r.stage=Stage::PowerCycle;if(!delay(4))return; + r.stage=Stage::PowerOn;if(!sequence(10) || !delay(0))return; + r.stage=Stage::DeassertReset;if(!sequence(1))return; + r.stage=Stage::HostOn;if(!lifecycle.start(io,current)){r.error=Error::Host;return;} + r.stage=Stage::Init;if(!sequence(2))return; + // Sequences drain before returning; never enable video over queued data. + if(port.failed || r.host.pending || !held())return; + r.stage=Stage::TranscoderOn; + if(!write(0x7B008,link.conf) || !wait(0x7B008,0xC0000000U,0xC0000000U,10000))return; + if(!sequence(3))return; + r.stage=Stage::PlaneOn; + if(!write(0x70180,planeControl) || !write(0x7019C,surface) || + !wait(0x70180,UINT32_MAX,planeControl,0) || !frame() || + !wait(0x701AC,UINT32_MAX,surface,0))return; + r.stage=Stage::BacklightOn; + // Restore a deferred brightness before enabling the panel's backlight GPIO. + if(backlight && !backlight->apply(io)){r.error=Error::Readback;return;} + if(!delay(1) || !sequence(6))return; + if(!held() || !Surface::source(io,current) || port.failed || r.host.pending || + !read(0x6B0D0,credits) || !read(0x6B0D8,lp) || + (credits&0x1FFF)!=0x1040 || (lp&0x30001))return; + r.source=current;r.stage=Stage::Complete;r.error=Error::None; +} +struct RetainedHost { + template bool stop(IO &,const Readout &,const Profile &){return true;} + template bool start(IO &,Readout &){return true;} +}; +template void cycle(IO &io,const Readout &link,const Profile &profile, + uint32_t planeControl,uint32_t surface,Result &r) { + RetainedHost retained;cycle(io,link,profile,planeControl,surface,r,retained); +} +}}} +#endif diff --git a/WhateverXe/kern_dsi_profile.hpp b/WhateverXe/kern_dsi_profile.hpp new file mode 100644 index 00000000..e2aa04d1 --- /dev/null +++ b/WhateverXe/kern_dsi_profile.hpp @@ -0,0 +1,148 @@ +// Independently implemented VBT wire decoder. Layout reference: Intel's +// intel_vbt_defs.h/intel_bios.c (Linux v6.12), intel_dsi_vbt.c (Linux v7.0). +// This is a description, not permission to program a panel. Borrowed spans +// require immutable backing storage; the Apple adapter owns that storage. +#ifndef KERN_DSI_PROFILE_HPP +#define KERN_DSI_PROFILE_HPP +#include "kern_dsi_sequence.hpp" +#include +namespace XELP { namespace DSI { +inline uint16_t profileU16(const uint8_t *p) {return uint16_t(uint16_t(p[0]) | uint16_t(p[1])<<8);} +enum class ProfileStatus : uint32_t {Bounds, Version, Duplicate, Missing, Child, Config, Timing, Backlight, Sequence, Valid}; +struct Mode { + uint32_t clockKHz {}; + uint16_t width {},hblank {},hfront {},hsync {},height {},vblank {},vfront {},vsync {},widthMM {},heightMM {}; + bool hsyncPositive {},vsyncPositive {}; + uint32_t htotal() const {return uint32_t(width)+hblank;} + uint32_t vtotal() const {return uint32_t(height)+vblank;} + bool valid() const { + return clockKHz && width && height && hsync && vsync && + uint32_t(hfront)+hsyncinput.size || bovs || vs-bo<22)return failure; + const auto b=p+bo;const size_t bh=profileU16(b+18),bs=profileU16(b+20); + if(memcmp(b,"BIOS_DATA_BLOCK ",16) || bh<22 || bsvs-bo)return failure; + r.version=profileU16(b+16); + // The consumed generic DTD, MIPI and 8-bit backlight fields share + // this layout from BDB 236 onward. Validate known prefixes and declared + // record strides rather than rejecting later BIOS versions by number. + if(r.version<236){failure.status=ProfileStatus::Version;return failure;} + for(size_t i=0;iavailable) { + // Observed BDB256 tail excludes exactly the final panel END. + // Require the caller's policy, final block 53, and that byte to + // be inside the *declared VBT*, not merely the mapped page. + if(!allowFinalEndQuirk || id!=53 || n!=available+1 || + vs-bo!=bs+1 || b[bs]!=0)return failure; + r.bdbTailQuirk=true; + } + Span *slot=nullptr; + switch(id) { + case 2:slot=&children;break;case 40:slot=&options;break; + case 43:slot=&backlight;break;case 52:slot=&config;break; + case 53:slot=&seq;break;case 58:slot=&dtd;break; + } + if(slot) { + if(slot->data){failure.status=ProfileStatus::Duplicate;return failure;} + *slot={b+at+3,n}; + } + at+=3+n; + } + failure.status=ProfileStatus::Missing; + if(!children.data || !options.data || !config.data || !dtd.data || !backlight.data)return failure; + failure.status=ProfileStatus::Child; + // Intel's legacy child prefix is 33 bytes; later tails are optional here. + if(children.size<5 || children.data[4]<33 || + (children.size-5)%children.data[4])return failure; + unsigned mipi=0; + for(size_t at=5;at=6 || config.size<792)return failure; + r.panel=options.data[0];const auto c=config.data+122*r.panel; + const auto general=sequenceU32(c+2);const auto port=profileU16(c+6); + // Bridge, command mode, dual link, reversal and alternate DCS + // destinations are deliberately outside the first implementation. + if((general&4) || ((general>>3)&3)!=2 || (general&32) || + ((general>>6)&3)!=2 || ((general>>10)&15)!=4 || + (port&0xFFF3) || (profileU16(c+24)&7) || c[23]!=0)return failure; + // VBT rotation describes panel mounting, not the DSI wire protocol. + // Keep the metadata without synthesizing or restricting OS transforms. + r.orientationQuarterTurns=(general>>14)&3; + r.lanes=((port>>2)&3)+1;if(r.lanes!=4)return failure; + r.byteClock=c[23];r.btaDisabled=(general>>16)&1; + r.hsTxTimeout=sequenceU32(c+26);r.lpRxTimeout=sequenceU32(c+30); + r.turnaroundTimeout=sequenceU32(c+34);r.initTimer=sequenceU32(c+42); + r.clockPre=c[93];r.clockPrepare=c[94];r.clockPrepareZero=profileU16(c+98); + r.hsExit=c[103];r.hsPrepare=c[104];r.hsPrepareZero=profileU16(c+105); + if(r.clockPrepareZero255 || r.minimum>r.brightness)return failure; + failure.status=ProfileStatus::Sequence; + // An absent block specifies no panel commands; a present malformed block + // must not be treated as absence. Host/PLL/PPS sequencing still executes. + if(seq.data)r.sequences=parseSequences(seq,r.panel); + else r.sequences.status=SequenceStatus::Valid; + if(!r.sequences.valid())return failure; + r.status=ProfileStatus::Valid;return r; +} +}} +#endif diff --git a/WhateverXe/kern_dsi_quiesce.hpp b/WhateverXe/kern_dsi_quiesce.hpp new file mode 100644 index 00000000..f1c4a9bd --- /dev/null +++ b/WhateverXe/kern_dsi_quiesce.hpp @@ -0,0 +1,119 @@ +// Host shutdown, following Linux v7.0 gen11_dsi_post_disable: +// stop plane/cursor, transcoder, ULPS, function, then ungate/stop/gate buffer. +// Optional panel lifecycle supplies backlight, DISPLAY_OFF and reset/power-off. +#ifndef KERN_DSI_QUIESCE_HPP +#define KERN_DSI_QUIESCE_HPP +#include "kern_dsi_surface.hpp" +#include "kern_display13_cursor.hpp" +namespace XELP { namespace DSI { namespace Quiesce { +enum class Stage : uint32_t {Source,Cursor,Plane,Transcoder,Commands,ULPS,Function,PortClock,Buffer,Gate,Complete,Panel}; +enum class Error : uint32_t {None,Input,Ownership,Readback,Timeout,Commands,Cursor,Panel}; +struct Result { + Stage stage {Stage::Source};Error error {Error::Input};uint32_t writes {},waitUs {},reg {},value {}; + uint32_t frameBefore {},frameAfter {};Display13Cursor::Result cursor; + bool complete() const {return stage==Stage::Complete && error==Error::None;} +}; +// Adapter holds FB/mode and shared clock serialization, keeps power/PLL/backing +// pinned and prevents new IRQ/flip producers. Never hold an IRQ spinlock here. +// IO::ready is an ownership check that remains meaningful after TRANSCONF is +// stopped, not sourceUnchanged(). No IRQ queue or allocator handle is invented +// by this core. Completion permits real IRQ drain/clock detach; it is not a +// Session StopProof and does not release anything. +// P2 has no command producer. Require empty hardware command FIFOs and LP idle +// both before shutdown and before ULPS; a pending panel sequence needs the +// later Host/panel drain backend. Never silently discard such a command. +struct RetainedPanel { + bool preflight(const Profile &){return true;} + template bool step(IO &,const Profile &,unsigned){return true;} +}; +template Result run(IO &io,const Readout &link,const Profile &profile,Panel &panel) { + Result r; + if(!link.matches(profile) || link.identity.pipe!=0 || !profile.mode.valid() || !panel.preflight(profile))return r; + const auto period=(uint64_t(profile.mode.htotal())*profile.mode.vtotal()*1000+profile.mode.clockKHz-1)/profile.mode.clockKHz; + if(period<1000 || period>100000)return r; + const auto held=[&]() { + if(!io.ready() || !io.readable(0x45404))return false; + const auto wells=io.read(0x45404); + return wells!=UINT32_MAX && (wells&0x403)==0x403; + }; + const auto read=[&](uint32_t reg,uint32_t &v) { + r.reg=reg;if(!held()){r.error=Error::Ownership;return false;} + if(!io.readable(reg)){r.error=Error::Readback;return false;} + v=io.read(reg);r.value=v;if(v==UINT32_MAX){r.error=Error::Readback;return false;}return true; + }; + const auto write=[&](uint32_t reg,uint32_t v) { + if(!held() || !io.readable(reg)){r.error=Error::Ownership;return false;} + io.write(reg,v);++r.writes;return true; + }; + const auto wait=[&](uint32_t reg,uint32_t mask,uint32_t value,uint32_t budget,uint32_t step) { + for(uint32_t us=0;;us+=step) { + uint32_t observed=0;if(!read(reg,observed))return false; + if((observed&mask)==value)return true; + if(us>=budget){r.error=Error::Timeout;return false;} + io.delayUs(step);r.waitUs+=step; + } + }; + const auto empty=[&]() { + uint32_t credits=0,lp=0; + if(!read(0x6B0D0,credits) || !read(0x6B0D8,lp))return false; + if((credits&0x1FFF)!=0x1040 || (lp&((1U<<17)|(1U<<16)|1U))) {r.error=Error::Commands;return false;} + return true; + }; + if(!held()){r.error=Error::Ownership;return r;} + if(!Surface::source(io,link,true) || !empty())return r; + const uint32_t registers[]={0x70080,0x70084,0x70088,0x700AC,0x70040,0x70180,0x7019C, + 0x7B008,0x6B0D8,0x6B400,0x164280,0x64000}; + for(auto reg:registers)if(!io.readable(reg))return r; + r.stage=Stage::Panel;if(!panel.step(io,profile,1)){r.error=Error::Panel;return r;} + r.stage=Stage::Cursor; + Display13Transcoder::Bindings bindings;bindings.dsi0Pipe=0; + r.cursor=Display13Cursor::stop(io,0,bindings);r.writes+=r.cursor.writes;r.waitUs+=r.cursor.waitUs; + if(r.cursor.status!=Display13Cursor::Status::Complete){r.error=Error::Cursor;return r;} + r.stage=Stage::Plane; + uint32_t control=0,surface=0; + if(!read(0x70180,control) || !read(0x7019C,surface) || (surface&4095))return r; + if(!write(0x70180,control&~(1U<<31)) || !write(0x7019C,surface) || + !wait(0x70180,1U<<31,0,0,1) || !read(0x70040,r.frameBefore))return r; + // Arm even an already-disabled shadow image and prove a subsequent VBlank + // while DSI timing is still running. SURF/live equality alone cannot prove + // that the plane stopped fetching its old allocation. + for(uint32_t us=0;;us+=1000) { + if(!read(0x70040,r.frameAfter))return r; + const auto frames=uint32_t(r.frameAfter-r.frameBefore); + if(frames && frames<0x80000000U)break; + if(frames || us>=3*period+1000){r.error=Error::Timeout;return r;} + if(!held()){r.error=Error::Ownership;return r;} + io.delayUs(1000);r.waitUs+=1000; + } + if(!held() || !Surface::source(io,link) || !wait(0x70180,1U<<31,0,0,1))return r; + r.stage=Stage::Transcoder; + if(!write(0x7B008,link.conf&~(1U<<31)) || !wait(0x7B008,0xC0000000U,0,50000,1000))return r; + r.stage=Stage::Panel;if(!panel.step(io,profile,2)){r.error=Error::Panel;return r;} + r.stage=Stage::Commands;if(!empty())return r; + r.stage=Stage::ULPS;uint32_t lp=0; + if(!read(0x6B0D8,lp) || !write(0x6B0D8,(lp&~(1U<<8))|1) || !wait(0x6B0D8,1U<<16,1U<<16,10,1))return r; + r.stage=Stage::Function; + if(!write(0x6B400,link.function&~(1U<<31)) || !wait(0x6B400,1U<<31,0,0,1))return r; + r.stage=Stage::PortClock;uint32_t mux=0; + if(!read(0x164280,mux) || (mux&3)!=link.identity.pll || + !write(0x164280,mux&~(1U<<10)) || !wait(0x164280,UINT32_MAX,mux&~(1U<<10),0,1))return r; + r.stage=Stage::Buffer;uint32_t buffer=0; + if(!read(0x64000,buffer) || !write(0x64000,buffer&~(1U<<31)) || !wait(0x64000,0x80000080U,0x80,8,1))return r; + r.stage=Stage::Gate; + if(!read(0x164280,mux) || (mux&3)!=link.identity.pll || + !write(0x164280,mux|(1U<<10)) || !wait(0x164280,UINT32_MAX,mux|(1U<<10),0,1))return r; + r.stage=Stage::Panel;if(!panel.step(io,profile,3)){r.error=Error::Panel;return r;} + // ULPS was witnessed before function disable; its status may reset when + // the host is disabled. Do not require a disabled host to retain it. + // Hardware-only proof. The caller must separately drain actual native IRQ + // handlers/events and detach the shared clock owner before releasing memory. + if(!wait(0x70180,1U<<31,0,0,1) || !wait(0x70080,63,0,0,1) || + !wait(0x7B008,0xC0000000U,0,0,1) || !wait(0x6B400,1U<<31,0,0,1) || + !wait(0x64000,0x80000080U,0x80,0,1))return r; + r.stage=Stage::Complete;r.error=Error::None;return r; +} +template Result run(IO &io,const Readout &link,const Profile &profile) { + RetainedPanel panel;return run(io,link,profile,panel); +} +}}} +#endif diff --git a/WhateverXe/kern_dsi_readout.hpp b/WhateverXe/kern_dsi_readout.hpp new file mode 100644 index 00000000..8a077b57 --- /dev/null +++ b/WhateverXe/kern_dsi_readout.hpp @@ -0,0 +1,107 @@ +// Decode a caller-owned, cached Display13 DSI0 snapshot. This does not acquire +// power, read MMIO, prove snapshot stability, or grant resource ownership. +// Registers: Intel i915_reg.h v6.12 and icl_dsi_regs.h/icl_dsi.c v7.0. +#ifndef KERN_DSI_READOUT_HPP +#define KERN_DSI_READOUT_HPP +#include "kern_dsi_math.hpp" +namespace XELP { namespace DSI { +struct Identity { + uint32_t pipe {UINT32_MAX},transcoder {UINT32_MAX},phy {UINT32_MAX},pll {UINT32_MAX}; + bool valid() const {return pipe<3 && transcoder==0 && phy==0 && pll<2;} + bool same(const Identity &b) const {return pipe==b.pipe && transcoder==b.transcoder && phy==b.phy && pll==b.pll;} +}; +enum class ReadoutStatus : uint32_t {Missing, Disabled, Transition, Routing, Power, Transport, Clock, Timing, Complete}; +struct Readout { + uint32_t version {1};ReadoutStatus status {ReadoutStatus::Missing}; + Identity identity; + uint32_t conf {},function {},transport {},link {},mux {},pllEnable {},cfg0 {},cfg1 {},referenceKHz {},afeKHz {}; + uint32_t normalConf {},wells {},buffer {},htotal {},hsync {},vtotal {},vsync {}; + bool complete() const {return status==ReadoutStatus::Complete;} + uint32_t activePipes() const {return complete()?1U<p.afeKHz?afeKHz-p.afeKHz:p.afeKHz-afeKHz; + return uint64_t(difference)*1000000<=uint64_t(p.afeKHz)*tolerancePpm; + } +}; +// get(reg,value) must inspect immutable cached words only. DSI timing belongs +// to transcoder0; frame/IRQ/planes belong to the separately decoded pipe. +template Readout decodeReadout(Get get,uint32_t referenceKHz) { + Readout r;r.referenceKHz=referenceKHz; + const auto value=[&](uint32_t reg,uint32_t &out){return get(reg,out) && out!=UINT32_MAX;}; + if(!value(0x7B008,r.conf))return r; + if(!(r.conf&0xC0000000U)){r.status=ReadoutStatus::Disabled;return r;} + if((r.conf&0xC0000000U)!=0xC0000000U){r.status=ReadoutStatus::Transition;return r;} + if(!value(0x6B400,r.function))return r; + r.status=ReadoutStatus::Routing; + const auto input=(r.function>>12)&7; + const auto pipe=input==0?0U:input==5?1U:input==6?2U:UINT32_MAX; + if(pipe==UINT32_MAX || !(r.function&0x80000000U))return r; + r.identity.pipe=pipe;r.identity.transcoder=0;r.identity.phy=0; + r.status=ReadoutStatus::Missing; + if(!value(0x45404,r.wells) || !value(0x70008+0x1000*pipe,r.normalConf))return r; + r.status=ReadoutStatus::Power; + if(!(r.wells&1) || !(r.wells&(1U<<(10+2*pipe))) || (pipe && !(r.wells&4)))return r; + r.status=ReadoutStatus::Routing; + if(r.normalConf&0xC0000000U)return r; // two timing consumers for one pipe + r.status=ReadoutStatus::Missing;uint32_t secondary=0,command=0; + if(!value(0x6B030,r.transport) || !value(0x6B094,r.link) || + !value(0x6B404,secondary) || !value(0x6B034,command) || !value(0x64000,r.buffer))return r; + r.status=ReadoutStatus::Transport; + if((r.function&0x70000E)!=6 || secondary || command || + (r.transport&0x303F0000U)!=0x20130000U || !(r.link&1) || (r.buffer&0x80000080U)!=0x80000000U)return r; + r.status=ReadoutStatus::Missing; + if(!value(0x164280,r.mux))return r; + r.identity.pll=r.mux&3;r.status=ReadoutStatus::Clock; + // DSI deliberately gates the DDI clock after enabling its PHY/buffer + // (gen11_dsi_enable_port_and_phy). This bit does NOT make its PLL free. + if(r.identity.pll>=2)return r; + r.status=ReadoutStatus::Missing; + const auto pll=r.identity.pll; + if(!value(0x46010+4*pll,r.pllEnable) || !value(0x164284+8*pll,r.cfg0) || + !value(0x164288+8*pll,r.cfg1))return r; + r.status=ReadoutStatus::Clock; + if((r.pllEnable&0xCC000000U)!=0xCC000000U)return r; + r.afeKHz=pllAFE(r.cfg0,r.cfg1,referenceKHz);if(!r.afeKHz)return r; + r.status=ReadoutStatus::Missing; + if(!value(0x6B000,r.htotal) || !value(0x6B008,r.hsync) || + !value(0x6B00C,r.vtotal) || !value(0x6B014,r.vsync))return r; + r.status=ReadoutStatus::Timing; + const auto ordered=[](uint32_t total,uint32_t sync) { + return (total&65535)<(sync&65535) && (sync&65535)<(sync>>16) && (sync>>16)<(total>>16); + }; + if(!ordered(r.htotal,r.hsync) || !ordered(r.vtotal,r.vsync))return r; + r.status=ReadoutStatus::Complete;return r; +} +// Revalidate an already owned source before/after a slow backing proof. The +// caller holds the DSI/pipe power leases and serializes mode changes. Checking +// the wells first avoids a DSI read after a visible power loss; this does not +// itself acquire leases or make unsynchronized firmware MMIO atomic. +template bool sourceUnchanged(IO &io,const Readout &expected) { + if(!expected.complete() || !expected.identity.valid() || !io.readable(0x45404) || !io.readable(0x51004))return false; + const auto powered=[&]() { + const auto wells=io.read(0x45404);const auto pipe=expected.identity.pipe; + return wells!=UINT32_MAX && (wells&1) && (wells&(1U<<(10+2*pipe))) && (!pipe || (wells&4)); + }; + if(!powered())return false; + const auto dssm=io.read(0x51004);const auto index=(dssm>>29)&7; + const uint32_t reference[]={24000,19200,38400}; + if(index>=3 || reference[index]!=expected.referenceKHz)return false; + const auto current=decodeReadout([&](uint32_t reg,uint32_t &v) { + if(!io.readable(reg))return false;v=io.read(reg);return true; + },reference[index]); + return expected.sameSource(current) && powered(); +} +}} +#endif diff --git a/WhateverXe/kern_dsi_restart.hpp b/WhateverXe/kern_dsi_restart.hpp new file mode 100644 index 00000000..b232ef2f --- /dev/null +++ b/WhateverXe/kern_dsi_restart.hpp @@ -0,0 +1,255 @@ +// Display13 DSI0/Combo-A host restart. Register ordering: Intel icl_dsi.c +// (Linux v7.0), intel_combo_phy.c / intel_dpll_mgr.c (v6.12). Panel sequences, +// framebuffer lifetime and OS power references belong to the calling adapter. +#ifndef KERN_DSI_RESTART_HPP +#define KERN_DSI_RESTART_HPP +#include "kern_dsi_readout.hpp" +#include "kern_dsi_quiesce.hpp" +#include "kern_xelp_combo_clock.hpp" +namespace XELP { namespace DSI { namespace Restart { +enum class Stage : uint32_t { + Input,ULPS,FunctionOff,BufferOff,ModeOff,PLLDisable,PLLPowerOff,Stopped, + ModeOn,Escape,PLLPowerOn,PLLConfig,PLLEnable,Map,PHY,BufferOn,Timing,Protocol,Link,Ready +}; +enum class Error : uint32_t {None,Input,Ownership,Consumer,Peer,Readback,Timeout}; +struct Plan { + bool valid {};LinkPlan link;PLLPlan pll; + uint32_t init {},hsTimeout {},lpTimeout {},taTimeout {},function {},transport {}; +}; +inline Plan plan(const Profile &p,uint32_t referenceKHz) { + Plan r;r.link=linkPlan(p);if(!r.link.valid || p.initTimer>65535)return r; + r.pll=pllPlan(r.link.portKHz,referenceKHz);if(!r.pll.valid)return r; + r.init=p.initTimer; + // Linux's byte-count -> escape-count expression, evaluated in 64 bits. + // Its u32 intermediates overflow for this profile. Do not silently rely + // on that overflow (or copy previously programmed timeout registers). + const uint64_t divisor=uint64_t(50)*r.link.afeKHz*1000; + const auto timeout=[&](uint32_t bytes){return (uint64_t(bytes)*8000000+divisor-1)/divisor;}; + const auto hs=timeout(p.hsTxTimeout),lp=timeout(p.lpRxTimeout),ta=timeout(p.turnaroundTimeout); + if(hs>65535 || lp>65535 || ta>65535)return r; + r.hsTimeout=uint32_t(hs);r.lpTimeout=uint32_t(lp);r.taTimeout=uint32_t(ta); + r.function=0x80000006U|(p.mode.hsyncPositive?1U<<16:0)|(p.mode.vsyncPositive?1U<<17:0); + // RGB888, VC0, sync events, continuous HS clock, blanking packets, EOTP. + r.transport=0x20030304;r.valid=true;return r; +} +struct State { + Stage stage {Stage::Input};Error error {Error::None};Plan config; + uint32_t pll {UINT32_MAX},reg {},value {},writes {},waitUs {}; + bool attempted {},stopped {},started {}; + bool fail(Error e){if(error==Error::None)error=e;return false;} +}; +// Registers writable by this backend; used by the owning MMIO adapter. +inline bool writable(uint32_t reg,unsigned pllId) { + XELPComboClock::PLL pll;if(pllId>=2 || !XELPComboClock::PLL::resolve(pllId,pll))return false; + if(reg==pll.enable || reg==pll.cfg0 || reg==pll.cfg1)return true; + switch(reg) { + case 0x6B0D8:case 0x6B400:case 0x164280:case 0x64000:case 0x6B094: + case 0x6B090:case 0x162190:case 0x1621A0:case 0x162028:case 0x162390: + case 0x162388:case 0x162688:case 0x162304:case 0x162604:case 0x162014: + case 0x162694:case 0x162394:case 0x162180:case 0x162184:case 0x6B088: + case 0x6B080:case 0x6B084:case 0x6B000:case 0x6B008:case 0x6B00C: + case 0x6B014:case 0x6B010:case 0x6B028:case 0x6B044:case 0x6B048: + case 0x6B04C:case 0x6B030:case 0x6B034:case 0x6B404:case 0x6B0C0:return true; + } + for(unsigned lane=0;lane<4;++lane)if(reg==0x162890+0x100*lane)return true; + return false; +} +inline Readout expected(const Readout &previous,const State &s) { + if(!s.started || s.stage!=Stage::Ready || s.error!=Error::None || !s.config.valid || + !previous.complete() || previous.identity.pll!=s.pll)return {}; + auto r=previous;const auto &p=s.config; + r.cfg0=p.pll.cfg0();r.cfg1=p.pll.cfg1();r.afeKHz=pllAFE(r.cfg0,r.cfg1,r.referenceKHz); + r.function=p.function;r.transport=p.transport|(1U<<20);r.link|=1;r.mux|=1U<<10; + r.buffer=(r.buffer&~0x80U)|(1U<<31); + r.htotal=p.link.htotal;r.hsync=p.link.hsync;r.vtotal=p.link.vtotal;r.vsync=p.link.vsync; + return r; +} +// Caller keeps Combo-clock -> display-owner serialization and IRQ producers +// parked for the entire operation. io.ready() verifies its real leases and +// object identity, including DDI IO power; this does not acquire any lease. +// Retain the explicit external clock owner through off/on, including failure. +// Do not clear shared power-well requests or release allocator state here. +template struct Access { + IO &io;XELPComboClock::ClockAllocation &allocation;uintptr_t owner;State &s; + bool raw(uint32_t reg,uint32_t &v) { + s.reg=reg; + if(!io.readable(reg))return s.fail(Error::Readback); + v=io.read(reg);s.value=v;return v!=UINT32_MAX || s.fail(Error::Readback); + } + bool bits(uint32_t reg,uint32_t mask,uint32_t expected) { + uint32_t v=0;return raw(reg,v) && (v&mask)==expected; + } + bool held() { + if(s.error!=Error::None)return false; + if(!io.ready() || !owner || s.pll>=2 || allocation.external[0]!=owner || + allocation.binding[0]!=s.pll+1 || !bits(0x45404,0x403,0x403))return s.fail(Error::Ownership); + // Pixel/DSB consumers must remain stopped while clocks/PHY change. + if(!bits(0x7B008,0xC0000000U,0) || !bits(0x70008,0xC0000000U,0) || + !bits(0x60400,1U<<31,0) || !bits(0x70080,63,0) || + !bits(0x7B808,0xC0000000U,0) || !bits(0x6BC00,1U<<31,0))return s.fail(Error::Consumer); + if(!bits(0x78000,0xF0300000U,0) || !bits(0x78004,0x80008000U,0) || + !bits(0x68180,1U<<31,0) || !bits(0x68280,1U<<31,0))return s.fail(Error::Consumer); + for(unsigned n=0;n<7;++n)if(!bits(0x70180+0x100*n,1U<<31,0))return s.fail(Error::Consumer); + for(unsigned n=0;n<3;++n)if(!bits(0x70B08+0x100*n,0x80000001U,0))return s.fail(Error::Consumer); + XELPComboClock::ClockSnapshot snapshot;snapshot.layout=XELPComboClock::Layout::Display13; + if(!raw(0x164280,snapshot.mux[0]))return false; + if((snapshot.mux[0]&3)!=s.pll || (snapshot.peers(XELPComboClock::Port(0))&(1U<=budget)return s.fail(Error::Timeout); + io.delayUs(step);s.waitUs+=step; + } + } + bool set(uint32_t reg,uint32_t value,uint32_t mask=UINT32_MAX) { + return write(reg,value) && wait(reg,mask,value&mask,0); + } + bool rmw(uint32_t reg,uint32_t clear,uint32_t value) { + uint32_t v=0;return read(reg,v) && set(reg,(v&~clear)|value,clear|value); + } + bool group(uint32_t lane0,uint32_t broadcast,uint32_t clear,uint32_t value) { + uint32_t v=0;if(!read(lane0,v) || !write(broadcast,(v&~clear)|value))return false; + // Broadcast registers are write aliases; inspect the four real lanes. + for(unsigned lane=0;lane<4;++lane)if(!wait(lane0+0x100*lane,clear|value,value,0))return false; + return true; + } +}; +// Shared tail for bootstrap stop and a runtime Quiesce proof. The latter +// retains backing, DDI power and external PLL ownership for display wake. +template bool powerOff(Access &a,const XELPComboClock::PLL &pll) { + auto &s=a.s; + s.stage=Stage::ModeOff;if(!a.rmw(0x6B094,1,0))return false; + s.stage=Stage::PLLDisable; + if(!a.rmw(pll.enable,1U<<31,0) || !a.wait(pll.enable,0xC0000000U,0,1000,10))return false; + s.stage=Stage::PLLPowerOff; + if(!a.rmw(pll.enable,1U<<27,0) || !a.wait(pll.enable,0xCC000000U,0,1000,10))return false; + s.stage=Stage::Stopped;s.stopped=true;return true; +} +template bool stopQuiesced(IO &io,const Readout &source,const Profile &profile, + const Quiesce::Result &host,XELPComboClock::ClockAllocation &allocation, + uintptr_t owner,State &s) { + if(s.attempted)return false;s.attempted=true; + if(!host.complete() || !source.matches(profile) || source.identity.pipe!=0)return s.fail(Error::Input); + s.config=plan(profile,source.referenceKHz);s.pll=source.identity.pll; + if(!s.config.valid)return s.fail(Error::Input); + Access a{io,allocation,owner,s};XELPComboClock::PLL pll; + if(!XELPComboClock::PLL::resolve(s.pll,pll) || !a.held())return false; + // Re-read consumers and ownership: a completed software receipt alone + // cannot authorize stopping a currently active or reassigned PLL. + if(!a.wait(pll.enable,0xCC000000U,0xCC000000U,0) || + !a.wait(pll.cfg0,UINT32_MAX,source.cfg0,0) || !a.wait(pll.cfg1,UINT32_MAX,source.cfg1,0) || + !a.wait(0x6B400,1U<<31,0,0) || !a.wait(0x64000,0x80000080U,0x80,0) || + !a.wait(0x164280,0x403,0x400|s.pll,0))return false; + return powerOff(a,pll); +} +// Invoke only after plane/cursor/transcoder stop and DISPLAY_OFF/drain. +// Hardware readbacks recheck those preconditions; a caller assertion alone +// must not permit clock writes. This stop deliberately retains the owner. +template bool stop(IO &io,const Readout &source,const Profile &profile, + XELPComboClock::ClockAllocation &allocation,uintptr_t owner,State &s) { + if(s.attempted)return false;s.attempted=true; + if(!source.matches(profile) || source.identity.pipe!=0)return s.fail(Error::Input); + s.config=plan(profile,source.referenceKHz);s.pll=source.identity.pll; + if(!s.config.valid)return s.fail(Error::Input); + Access a{io,allocation,owner,s};XELPComboClock::PLL pll; + if(!XELPComboClock::PLL::resolve(s.pll,pll) || !a.held())return false; + if(!a.wait(pll.enable,0xCC000000U,0xCC000000U,0) || + !a.wait(pll.cfg0,UINT32_MAX,source.cfg0,0) || !a.wait(pll.cfg1,UINT32_MAX,source.cfg1,0) || + !a.wait(0x6B400,UINT32_MAX,source.function,0) || + !a.wait(0x6B0D0,0x1FFF,0x1040,0) || !a.wait(0x6B0D8,0x30001,0,0))return false; + s.stage=Stage::ULPS; + // LINK_ENTER_ULPS is a self-clearing request on the live Display13 host. + // Completion is LINK_IN_ULPS, not an echoed request bit. Do not reject a + // transition which already completed before the first posting read. + uint32_t lp=0; + if(!a.read(0x6B0D8,lp) || !a.write(0x6B0D8,(lp&~(1U<<8))|1U) || + !a.wait(0x6B0D8,1U<<16,1U<<16,10))return false; + s.stage=Stage::FunctionOff;if(!a.rmw(0x6B400,1U<<31,0))return false; + s.stage=Stage::BufferOff; + if(!a.rmw(0x164280,1U<<10,0) || !a.rmw(0x64000,1U<<31,0) || + !a.wait(0x64000,0x80000080U,0x80,8) || !a.rmw(0x164280,0,1U<<10))return false; + return powerOff(a,pll); +} +// A peer clock transaction may run while this host is parked. Revalidate the +// retained allocation and stopped hardware, without demanding the old live +// source or reacquiring an allocation whose PLL is intentionally powered off. +// Keep the stop receipt immutable; failed observation cannot advance it. +template bool retainedStopped(IO &io,const Readout &source, + XELPComboClock::ClockAllocation &allocation,uintptr_t owner,const State &state) { + if(!source.complete() || source.identity.pipe!=0 || source.identity.phy!=0 || + source.identity.pll!=state.pll || !state.stopped || state.started || + state.stage!=Stage::Stopped || state.error!=Error::None || !state.config.valid)return false; + State observed=state;Access a{io,allocation,owner,observed};XELPComboClock::PLL pll; + return XELPComboClock::PLL::resolve(state.pll,pll) && a.held() && + a.bits(pll.enable,0xCC000000U,0) && a.bits(0x6B400,1U<<31,0) && + a.bits(0x64000,0x80000080U,0x80) && a.bits(0x6B094,1,0) && + a.bits(0x164280,0x403,0x400|state.pll); +} +// Invoke after panel POWER_ON/PPS/DEASSERT_RESET, before INIT_OTP. Timing and +// transport derive from the profile; no firmware PLL/PHY register dump replay. +// Leaves pixel transfer off, ready for command sequence initialization. The +// caller must enable video/plane, validate and publish the new source identity. +template bool start(IO &io,XELPComboClock::ClockAllocation &allocation,uintptr_t owner,State &s) { + if(!s.stopped || s.started || s.stage!=Stage::Stopped || s.error!=Error::None || !s.config.valid)return false; + Access a{io,allocation,owner,s};XELPComboClock::PLL pll; + if(!XELPComboClock::PLL::resolve(s.pll,pll) || !a.held())return false; + if(!a.wait(pll.enable,0xCC000000U,0,0) || !a.wait(0x6B400,1U<<31,0,0) || + !a.wait(0x64000,0x80000080U,0x80,0) || !a.wait(0x164280,0x403,0x400|s.pll,0))return false; + const auto &p=s.config; + s.stage=Stage::ModeOn;if(!a.rmw(0x6B094,0,1))return false; + s.stage=Stage::Escape; + if(!a.set(0x6B090,p.link.escapeDivider,0x1FF) || !a.set(0x162190,p.link.escapeDivider,0x1FF) || + // Linux ADL_MIPIO_DW8 writes zero (unshifted divider & shifted mask), + // matching the saved Linux state. Do not enable an unverified override. + !a.set(0x1621A0,0,0xFF0000))return false; + s.stage=Stage::PLLPowerOn; + if(!a.rmw(pll.enable,0,1U<<27) || !a.wait(pll.enable,1U<<26,1U<<26,1000,10))return false; + s.stage=Stage::PLLConfig;if(!a.set(pll.cfg0,p.pll.cfg0()) || !a.set(pll.cfg1,p.pll.cfg1()))return false; + s.stage=Stage::PLLEnable; + if(!a.rmw(pll.enable,0,1U<<31) || !a.wait(pll.enable,0xCC000000U,0xCC000000U,1000,10))return false; + s.stage=Stage::Map;if(!a.rmw(0x164280,0x403,s.pll))return false; + s.stage=Stage::PHY; + if(!a.rmw(0x162028,0xF0,0) || !a.rmw(0x162390,1U<<31,0))return false; + for(unsigned lane=0;lane<4;++lane)if(!a.rmw(0x162890+0x100*lane,1U<<31,lane==2?0:1U<<31))return false; + if(!a.rmw(0x162388,0x700,0x500) || !a.group(0x162888,0x162688,0x700,0x500) || + !a.rmw(0x162304,12,0) || !a.group(0x162804,0x162604,12,4) || + !a.group(0x162804,0x162604,1U<<26,0) || !a.rmw(0x162304,1U<<26,0) || + !a.rmw(0x162014,0,3) || + !a.group(0x162894,0x162694,1U<<31,0) || !a.rmw(0x162394,1U<<31,0) || + !a.group(0x162894,0x162694,0x1C0038,0x60080030) || !a.rmw(0x162394,0x1C0038,0x60080030) || + !a.group(0x162888,0x162688,0xB8FF,0x1098) || !a.rmw(0x162388,0xB8FF,0x1098) || + !a.rmw(0x162390,0x3FFFF,0x3F))return false; + for(unsigned lane=0;lane<4;++lane)if(!a.rmw(0x162890+0x100*lane,0x3FFFF,0x3F))return false; + if(!a.group(0x162894,0x162694,0,1U<<31) || !a.rmw(0x162394,0,1U<<31) || + !a.set(0x162180,p.link.clockTiming) || !a.set(0x162184,p.link.dataTiming))return false; + s.stage=Stage::BufferOn; + if(!a.rmw(0x64000,0,1U<<31) || !a.wait(0x64000,0x80000080U,0x80000000U,500) || + !a.rmw(0x164280,0,1U<<10))return false; + s.stage=Stage::Timing; + if(!a.rmw(0x6B088,65535,p.init) || !a.set(0x6B080,p.link.clockTiming) || !a.set(0x6B084,p.link.dataTiming) || + !a.set(0x6B000,p.link.htotal) || !a.set(0x6B008,p.link.hsync) || + !a.set(0x6B00C,p.link.vtotal) || !a.set(0x6B014,p.link.vsync) || + !a.set(0x6B010,p.link.vtotal) || !a.set(0x6B028,0))return false; + // VSYNCSHIFT is relevant to interlace; this supported profile is progressive. + // Linux's unconditional interlace calculation is explicitly marked FIXME. + s.stage=Stage::Protocol; + // Timeout status bits are W1C; write only the new timeout field. + if(!a.set(0x6B044,p.hsTimeout<<16,0xFFFF0000U) || !a.set(0x6B048,p.lpTimeout,65535) || + !a.set(0x6B04C,p.taTimeout,65535) || !a.set(0x6B034,0) || !a.set(0x6B404,0) || + !a.rmw(0x6B0C0,0xF000,0x4000) || !a.set(0x6B030,p.transport,~(1U<<20)) || + !a.set(0x6B400,p.function))return false; + s.stage=Stage::Link; + if(!a.wait(0x6B030,1U<<20,1U<<20,2500,10) || !a.wait(0x6B0D8,0x30001,0,2500,10) || + !a.wait(0x6B0D0,0x1FFF,0x1040,100,10))return false; + s.stage=Stage::Ready;s.started=true;return true; +} +}}} +#endif diff --git a/WhateverXe/kern_dsi_sequence.hpp b/WhateverXe/kern_dsi_sequence.hpp new file mode 100644 index 00000000..e450b31f --- /dev/null +++ b/WhateverXe/kern_dsi_sequence.hpp @@ -0,0 +1,102 @@ +// Bounded VBT v3 sequence description. No hardware operations or allocations. +// Layout reference: Intel intel_bios.c/intel_dsi_vbt.c, Linux v7.0. +// Spans borrow immutable VBT storage: its owner must outlive this description. +#ifndef KERN_DSI_SEQUENCE_HPP +#define KERN_DSI_SEQUENCE_HPP +#include +#include +namespace XELP { namespace DSI { +struct Span { const uint8_t *data {}; size_t size {}; }; +inline bool nativePanelGPIO(unsigned role) {return role<=2 || (role>=5 && role<=7);} +// Firmware may combine operations in other lifecycle sequences or omit them. +// Only the fully empty span means absent; malformed spans still reject. +inline bool absentSequence(Span bytes) {return !bytes.data && !bytes.size;} +enum class SequenceStatus : uint32_t { + Valid, Bounds, Version, Panel, Duplicate, Sequence, Operation, Packet, GPIO +}; +struct Sequences { + SequenceStatus status {SequenceStatus::Bounds}; + Span sequence[12] {}; + uint32_t packets {}, delays {}, gpios {}; + uint64_t delayUs {}; + bool valid() const {return status==SequenceStatus::Valid;} +}; +inline uint32_t sequenceU32(const uint8_t *p) { + return uint32_t(p[0]) | uint32_t(p[1])<<8 | uint32_t(p[2])<<16 | uint32_t(p[3])<<24; +} +inline SequenceStatus validateSequence(Span bytes, Sequences &counts) { + if(!bytes.data || !bytes.size || bytes.size>65536)return SequenceStatus::Bounds; + for(size_t at=0;atbytes.size-at)return SequenceStatus::Bounds; + const auto p=bytes.data+at;at+=n; + switch(op) { + case 1: { + if(n<4)return SequenceStatus::Packet; + const size_t length=uint32_t(p[2]) | uint32_t(p[3])<<8; + // Initial profile: LP commands, VC0, port A. Other transport + // flags require a separate host contract, never silent coercion. + if(p[0] || length!=n-4)return SequenceStatus::Packet; + if(!((p[1]==0x05 && length==1) || (p[1]==0x15 && length==2) || + (p[1]==0x39 && length>=1)))return SequenceStatus::Packet; + ++counts.packets;break; + } + case 2: + if(n!=4)return SequenceStatus::Operation; + ++counts.delays;counts.delayUs+=sequenceU32(p);break; + case 3: + // v3 native display GPIO: index, GPIO role, value. The index + // is retained in the bytes; it is not a gpiochip identifier. + if(n!=3 || !nativePanelGPIO(p[1]) || p[2]>1)return SequenceStatus::GPIO; + ++counts.gpios;break; + default:return SequenceStatus::Operation; + } + } + return SequenceStatus::Bounds; // required END byte absent +} +inline Sequences parseSequences(Span block, uint8_t selectedPanel) { + Sequences r; + if(!block.data || block.size<5 || block.size>65536)return r; + if(block.data[0]!=3){r.status=SequenceStatus::Version;return r;} + // BDB v3 size excludes the version and size header (five bytes). + if(sequenceU32(block.data+1)!=block.size-5)return r; + if(selectedPanel>=6){r.status=SequenceStatus::Panel;return r;} + uint32_t panels=0;Span selected; + for(size_t at=5;at=6){r.status=SequenceStatus::Panel;return r;} + if(panels&(1U<block.size-at)return r; + if(panel==selectedPanel)selected={block.data+at,n}; + at+=n; + } + if(!selected.data){r.status=SequenceStatus::Panel;return r;} + // Parse into a temporary: a failed preflight never publishes executable + // spans or partial counts to a later interpreter. + Sequences candidate; + for(size_t at=0;at=12){r.status=SequenceStatus::Sequence;return r;} + if(candidate.sequence[id].data){r.status=SequenceStatus::Duplicate;return r;} + if(selected.size-at<4)return r; + const size_t n=sequenceU32(selected.data+at);at+=4; + if(!n || n>selected.size-at)return r; + Span bytes {selected.data+at,n}; + r.status=validateSequence(bytes,candidate); + if(r.status!=SequenceStatus::Valid)return r; + r.status=SequenceStatus::Bounds; + candidate.sequence[id]=bytes;at+=n; + } + return r; +} +}} +#endif diff --git a/WhateverXe/kern_dsi_session.hpp b/WhateverXe/kern_dsi_session.hpp new file mode 100644 index 00000000..d44f92bb --- /dev/null +++ b/WhateverXe/kern_dsi_session.hpp @@ -0,0 +1,83 @@ +// P2 adoption journal, separate from Apple objects and MMIO. The adapter owns +// serialization, immutable profile storage, and opaque resource handles. +// This is an internal contract, not the future separately loaded kext ABI. +#ifndef KERN_DSI_SESSION_HPP +#define KERN_DSI_SESSION_HPP +#include "kern_dsi_readout.hpp" +namespace XELP { namespace DSI { +struct Capabilities { + enum Bit : uint32_t { + Profile=1U<<0,Readout=1U<<1,FixedMode=1U<<2,InternalDisplay=1U<<3, + BindResources=1U<<4,SwitchSurface=1U<<5,Stop=1U<<6,IRQ=1U<<7, + Commands=1U<<8,PanelPower=1U<<9,Backlight=1U<<10 + }; + // Independent namespace: never borrow bits from DisplayContract::Mask. + uint32_t version {1},bytes {sizeof(Capabilities)},installed {}; + static constexpr uint32_t Adoption=Profile|Readout|FixedMode|InternalDisplay|BindResources|SwitchSurface|Stop|IRQ; + bool admits(uint32_t required) const {return version==1 && bytes==sizeof(*this) && (installed&required)==required;} +}; +enum class SessionState : uint32_t {Unbound,FirmwareActive,Switching,Streaming,Quiesced,Faulted,Released}; +enum class SessionError : uint32_t {None,Contract,Binding,Transition,Switch,Stop}; +struct Ticket {uint64_t epoch {},serial {};}; +struct SurfaceProof { + Ticket ticket;Identity identity; + uint64_t backing {};bool liveSurfaceMatches {},frameAdvanced {},timingMatches {}; +}; +struct StopProof { + Ticket ticket;Identity identity; + bool planeStopped {},irqDrained {},dsiStopped {},clockDetached {}; +}; +struct Session { + SessionState state {SessionState::Unbound};SessionError firstError {SessionError::None}; + Identity identity; + uint64_t epoch {},serial {},lease {},currentBacking {},pendingBacking {},retiredBacking {}; + Ticket ticket() const {return {epoch,serial};} + bool accepts(Ticket t) const {return t.epoch==epoch && t.serial==serial && epoch && serial;} + bool fail(SessionError error) {if(firstError==SessionError::None)firstError=error;state=SessionState::Faulted;return false;} + bool bind(const Capabilities &caps,const Readout &snapshot,const Profile &profile, + uint64_t generation,uint64_t sharedLease,uint64_t firmwareBacking) { + // A rejected bind must not consume caller-owned handles. + if(state!=SessionState::Unbound || !caps.admits(Capabilities::Adoption) || + !generation || !sharedLease || !firmwareBacking || !snapshot.identity.valid() || !snapshot.matches(profile))return false; + epoch=generation;serial=1;lease=sharedLease;currentBacking=firmwareBacking; + identity=snapshot.identity;state=SessionState::FirmwareActive;return true; + } + bool beginSurface(uint64_t backing) { + if((state!=SessionState::FirmwareActive && state!=SessionState::Streaming) || + !backing || backing==currentBacking || retiredBacking || serial==UINT64_MAX)return false; + pendingBacking=backing;++serial;state=SessionState::Switching;return true; + } + bool completeSurface(const SurfaceProof &proof) { + // Stale callbacks do not mutate the current transaction or its error. + if(state!=SessionState::Switching || !accepts(proof.ticket))return false; + if(!proof.identity.same(identity) || proof.backing!=pendingBacking || + !proof.liveSurfaceMatches || !proof.frameAdvanced || !proof.timingMatches)return fail(SessionError::Switch); + retiredBacking=currentBacking;currentBacking=pendingBacking;pendingBacking=0; + state=SessionState::Streaming;return true; + } + bool abort(Ticket t,SessionError error) { + if(!accepts(t) || error==SessionError::None || state==SessionState::Released || state==SessionState::Quiesced)return false; + return fail(error); // both backings remain held if a switch may have reached hardware + } + // Returns one cleanup obligation to the adapter, at most once. The adapter + // frees backing outside native/owner lock inversions and records completion. + uint64_t takeRetiredBacking() { + if(state!=SessionState::Streaming)return 0; + const auto result=retiredBacking;retiredBacking=0;return result; + } + bool stopped(const StopProof &proof) { + if(state==SessionState::Unbound || state==SessionState::Released || state==SessionState::Quiesced || + !accepts(proof.ticket))return false; + if(!proof.identity.same(identity) || !proof.planeStopped || !proof.irqDrained || + !proof.dsiStopped || !proof.clockDetached)return fail(SessionError::Stop); + state=SessionState::Quiesced;return true; + } + struct Cleanup {uint64_t lease {},currentBacking {},pendingBacking {},retiredBacking {};}; + Cleanup takeCleanup() { + if(state!=SessionState::Quiesced)return {}; + Cleanup result {lease,currentBacking,pendingBacking,retiredBacking}; + lease=currentBacking=pendingBacking=retiredBacking=0;state=SessionState::Released;return result; + } +}; +}} +#endif diff --git a/WhateverXe/kern_dsi_surface.hpp b/WhateverXe/kern_dsi_surface.hpp new file mode 100644 index 00000000..d69d8bc0 --- /dev/null +++ b/WhateverXe/kern_dsi_surface.hpp @@ -0,0 +1,113 @@ +// GOP-to-native scanout transaction for the fixed DSI0/pipe-A mode. Register +// ordering follows Linux v6.12 icl_plane_update_noarm/arm. This owns no Apple +// objects: the adapter proves/pins actual GGTT backing, leases, IRQ and DDB, +// serializes native writers, and publishes the resulting native caches. +#ifndef KERN_DSI_SURFACE_HPP +#define KERN_DSI_SURFACE_HPP +#include "kern_dsi_session.hpp" +#include "kern_display13_wm.hpp" +#include "kern_display13_dbuf.hpp" +namespace XELP { namespace DSI { namespace Surface { +struct Plane { + uint32_t control {},color {},stride {},size {},surface {},position {},offset {}; +}; +struct Backing {uint64_t handle {},gpu {},bytes {},pitch {};}; +enum class Status : uint32_t {Input,Source,Watermark,Plane,Completion,Complete}; +struct Result { + Status status {Status::Input};uint32_t writes {};Ticket ticket; + Display13WM::Program wm;Display13WM::Latch latch; +}; +constexpr uint32_t Registers[]={0x70188,0x7018C,0x70190,0x701A4,0x701CC,0x70180,0x7019C}; +inline void values(const Plane &p,uint32_t (&v)[7]) { + v[0]=p.stride;v[1]=p.position;v[2]=p.size;v[3]=p.offset;v[4]=p.color;v[5]=p.control;v[6]=p.surface; +} +inline bool shape(const Plane &p,const Profile &profile) { + // The existing Display13 WM backend admits linear and uncompressed Y. + // Keep the P2 fixed RGB8888 geometry; rotation/scaling/color modes are not + // implicitly implemented by copying the donor's register cache. + return profile.valid() && profile.mode.valid() && profile.mode.width<=8192 && profile.mode.height<=8192 && + Display13WM::supportedRGBControl(p.control) && (p.control&(1U<<31)) && + ((p.control>>23)&31)==8 && !(p.control&((3U<<21)|(1U<<9)|(1U<<8))) && + !p.position && !p.offset && p.size==((uint32_t(profile.mode.height)-1)<<16 | (uint32_t(profile.mode.width)-1)) && + p.stride && p.stride<=2047 && !(p.surface&4095); +} +inline bool target(const Plane &p,const Profile &profile,const Backing &b) { + if(!shape(p,profile) || !b.handle || !b.bytes || (b.bytes&4095) || b.gpu!=p.surface || + b.gpu>=0x100000000ULL || b.bytes>0x100000000ULL-b.gpu || + Display13WM::arbitrationControl(p.control)!=p.control)return false; + const bool tiled=((p.control>>10)&7)==4; + const auto pitch=uint64_t(p.stride)*(tiled?128:64); + const auto rows=tiled?((profile.mode.height+31)&~31U):profile.mode.height; + return b.pitch==pitch && pitch>=uint64_t(profile.mode.width)*4 && pitch*rows<=b.bytes; +} +template bool word(IO &io,uint32_t reg,uint32_t mask,uint32_t expected) { + if(!io.readable(reg))return false;const auto v=io.read(reg); + return v!=UINT32_MAX && (v&mask)==expected; +} +template bool matches(IO &io,const Plane &p,bool live) { + uint32_t v[7];values(p,v); + for(unsigned i=0;i<7;++i)if(!word(io,Registers[i],UINT32_MAX,v[i]))return false; + return !live || word(io,0x701AC,UINT32_MAX,p.surface); +} +template bool source(IO &io,const Readout &link,bool stoppingCursor=false) { + if(link.identity.pipe!=0 || !sourceUnchanged(io,link) || !word(io,0x42084,1U<<30,1U<<30) || + !word(io,0x6001C,UINT32_MAX,((link.htotal&65535)<<16)|(link.vtotal&65535)) || + !word(io,0x6B010,UINT32_MAX,link.vtotal) || !word(io,0x6B028,UINT32_MAX,0) || + !word(io,0x60400,1U<<31,0) || !word(io,0x7B808,0xC0000000U,0) || + (!stoppingCursor && !word(io,0x70080,63,0)) || + !word(io,0x78000,0xF0300000U,0) || !word(io,0x78004,0x80008000U,0))return false; + for(unsigned p=1;p<7;++p)if(!word(io,0x70180+0x100*p,1U<<31,0))return false; + for(unsigned s=0;s<2;++s)if(!word(io,0x68180+0x100*s,1U<<31,0))return false; + for(unsigned d=0;d<3;++d)if(!word(io,0x70B08+0x100*d,0x80000001U,0))return false; + return true; +} +// IO::ready revalidates the adapter's actual object/backing/resource binding. +// Hold its mode serialization and power through the final latch; do not hold +// an IRQ spinlock while waiting. Failure after begin retains both backings and +// the lease in Session. This never calls cleanup or reports a successful stop. +template Result change(IO &io,Session &session,const Readout &link,const Profile &profile, + const Plane ¤t,const Plane &next,const Backing &backing, + const Display13WM::Snapshot &wm) { + Result r; + if(!io.ready() || !link.matches(profile) || !session.identity.same(link.identity) || + !shape(current,profile) || !target(next,profile,backing) || next.surface==current.surface || + ((next.control^current.control)&~((7U<<28)|(7U<<10))) || + (next.color!=current.color && next.color!=(current.color|(1U<<13))) || + wm.pipeIndex!=0 || wm.transcoders.dsi0Pipe!=0 || wm.cursorActive || !wm.primary || + wm.primary->width!=profile.mode.width || wm.primary->height!=profile.mode.height || + wm.primary->htotal!=profile.mode.width+profile.mode.hblank || + wm.primary->pixelHz!=uint64_t(profile.mode.clockKHz)*1000 || wm.pipePixelHz!=wm.primary->pixelHz || + wm.primary->yTiled!=(((next.control>>10)&7)==4) || wm.primarySurface!=next.surface || + wm.nativeClockHz!=io.nativeClockHz() || !Display13WM::clockFits(wm,wm.nativeClockHz))return r; + const auto ddb=Display13DBUF::decode(wm.primaryDDB); + if(!Display13DBUF::valid(ddb) || Display13DBUF::encoded(ddb)!=wm.primaryDDB || + ddb.end-ddb.start!=wm.primary->allocation)return r; + r.status=Status::Source; + if(!source(io,link) || !matches(io,current,true) || + !word(io,0x7027C,UINT32_MAX,wm.primaryDDB) || !word(io,0x7017C,UINT32_MAX,wm.cursorDDB) || + !word(io,0x45004,UINT32_MAX,wm.ipc))return r; + for(auto reg:Registers)if(!io.readable(reg))return r; + if(!io.readable(0x45270) || !io.ready() || !session.beginSurface(backing.handle))return r; + r.ticket=session.ticket(); + const auto fail=[&]() {session.abort(r.ticket,SessionError::Switch);return r;}; + r.status=Status::Watermark; + r.wm=Display13WM::program(io,*wm.primary,false,wm.wm0Only,0);r.writes+=r.wm.writes; + if(r.wm.status!=Display13WM::Status::Complete)return fail(); + io.write(0x45270,wm.pipeLineTime);++r.writes; + if(!word(io,0x45270,UINT32_MAX,wm.pipeLineTime) || !source(io,link) || !matches(io,current,true) || !io.ready())return fail(); + r.status=Status::Plane; + uint32_t v[7];values(next,v); + // All non-arming words precede control/surface. The existing enabled + // plane remains enabled; SURF is the final write that arms the change. + for(unsigned i=0;i<7;++i) { + io.write(Registers[i],v[i]);++r.writes; + if(!word(io,Registers[i],UINT32_MAX,v[i]))return fail(); + } + r.status=Status::Completion;r.latch=Display13WM::latch(io,wm); + if(r.latch.status!=Display13WM::Status::Complete || !io.ready() || !source(io,link) || !matches(io,next,true))return fail(); + SurfaceProof proof {r.ticket,link.identity,backing.handle,true,true,true}; + if(!session.completeSurface(proof))return fail(); + r.status=Status::Complete;return r; +} +}}} +#endif diff --git a/WhateverXe/kern_firmware.cpp b/WhateverXe/kern_firmware.cpp index 751eb82f..691e4962 100644 --- a/WhateverXe/kern_firmware.cpp +++ b/WhateverXe/kern_firmware.cpp @@ -1,4 +1,5 @@ #include "kern_xe.hpp" +#include "kern_dsi_math.hpp" #include #include @@ -57,4 +58,22 @@ void XE::xelpReadFirmware(IOPCIDevice *pci) { const auto &panel=firmwarePorts.panel; uint32_t panelFacts[]={panel.valid,panel.type,panel.ddi,panel.lowSwing,panel.hobl}; pci->setProperty("WhateverXeFirmwarePanel",panelFacts,sizeof(panelFacts)); + if(checkKernelArgument("-wex-dsi-audit")) { + // Synchronous audit of the existing copied firmware property. Never + // retain borrowed sequence pointers, acquire power, or authorize IO. + const auto vbt=OSDynamicCast(OSData,pci->getProperty("TGLVBT")); + const auto profile=XELP::DSI::parseProfile({vbt?static_cast(vbt->getBytesNoCopy()):nullptr, + vbt?vbt->getLength():0U},true); + const auto link=XELP::DSI::linkPlan(profile); + uint32_t audit[]={1,static_cast(profile.status),profile.bdbTailQuirk, + profile.version,profile.checksum,profile.panel,profile.lanes, + profile.mode.width,profile.mode.height,profile.mode.clockKHz,profile.mode.htotal(),profile.mode.vtotal(), + profile.pwmHz,profile.minimum,profile.brightness,profile.precision, + profile.sequences.packets,profile.sequences.delays,profile.sequences.gpios, + uint32_t(profile.sequences.delayUs),uint32_t(profile.sequences.delayUs>>32), + profile.ppsUs[0],profile.ppsUs[1],profile.ppsUs[2],profile.ppsUs[3],profile.ppsUs[4], + link.valid,link.afeKHz,link.portKHz,link.escapeDivider,link.phyEscapeDivider, + link.clockTiming,link.dataTiming}; + publishBytes(pci,"WEXDSIFirmwareAudit",audit,sizeof(audit)); + } } diff --git a/WhateverXe/kern_tgl_fb_binding.hpp b/WhateverXe/kern_tgl_fb_binding.hpp index 63432ef9..894658ac 100644 --- a/WhateverXe/kern_tgl_fb_binding.hpp +++ b/WhateverXe/kern_tgl_fb_binding.hpp @@ -6,7 +6,7 @@ #include "kern_display13_fb_topology.hpp" namespace TGLFBBinding { struct Object {const void *pointer {};size_t bytes {};}; -struct Objects {Object controller,framebuffer,path,plane,port;uintptr_t accelerator {};}; +struct Objects {Object controller,framebuffer,path,plane,port;uintptr_t accelerator {},logicalSurface {};}; enum class Status : uint32_t {Unavailable,Association,Path,Port,Plane,Complete}; struct Readout { Status status {Status::Unavailable}; @@ -58,7 +58,7 @@ inline Readout inspect(const Objects &o,const XELP::NativePorts &ports,bool atta plane=o.plane.pointer,port=o.port.pointer; if(!c || !fb || !path || !plane || !port || o.controller.bytes<0x1C20 || o.framebuffer.bytes<0x4B90 || o.path.bytes<0x3730 || o.plane.bytes<0x1C8 || o.port.bytes<0x580)return r; - if(!Display13FBMemory::associations(c,o.controller.bytes,fb,o.framebuffer.bytes,o.accelerator,true)) { + if(!Display13FBMemory::associations(c,o.controller.bytes,fb,o.framebuffer.bytes,o.accelerator,true,o.logicalSurface)) { r.status=Status::Association;return r; } r.framebuffer=field(fb,0x1DC);r.pipe=field(path,0x3640); diff --git a/WhateverXe/kern_xe.cpp b/WhateverXe/kern_xe.cpp index daf40539..7b8316bf 100644 --- a/WhateverXe/kern_xe.cpp +++ b/WhateverXe/kern_xe.cpp @@ -7,14 +7,24 @@ // Copyright © 2018 vit9696. All rights reserved. // +#include "kern_dsi_native_cache.hpp" +#include "kern_dsi_native_backing.hpp" +#include "kern_dsi_native_mode.hpp" #include "kern_xe.hpp" #include "kern_framebuffer_donor.hpp" #include "kern_xelp_os.hpp" +#include "kern_xelp_call_target.hpp" +#include "kern_xelp_native_placement.hpp" +#include #include "kern_alderlake_n.hpp" #include "kern_xelp_device.hpp" #include "kern_xelp_memory_layout.hpp" #include "kern_xelp_forcewake_irq.hpp" #include "kern_display13_boot.hpp" +#include "kern_display13_dsi_trial.hpp" +#include "kern_dsi_readout.hpp" +#include "kern_dsi_attributes.hpp" +#include "kern_dsi_apple_timing.hpp" #include "kern_display13_firmware.hpp" #include "kern_display13_tc_aux.hpp" #include "kern_display13_tc_mode.hpp" @@ -242,9 +252,10 @@ static XELPMemory::Layout xelpPhysicalMemory(IOPCIDevice *pci) { pci->configRead32(0xC4),aperture->getLength()) : XELPMemory::Layout {}; } static bool xelpMemoryMatches(IOPCIDevice *pci) {return xelpMemory.reservationKnown && XELPMemory::samePhysical(xelpMemory,xelpPhysicalMemory(pci));} -// Normal dual-output profile uses native accelerator surfaces. Fixed FBMemMgr -// slot1/2 scatter backing is unqualified and must not reserve task aperture. -static constexpr bool adlnFixedSysMemEnabled=false; +// Native server-state transitions can request aperture backing even after +// accelerator binding. Protect slot1/2 through the existing registration +// contract before admitting their owned scatter mappings. +static bool adlnFixedSysMemEnabled=true; // Latched from the native layout before accelerator registration. static bool tglPcodeGateSerialize(void *,void *,OSSerialize *); static bool tglPcodeEDRAMSerialize(void *,void *,OSSerialize *); static bool tglTCBootstrapSerialize(void *,void *,OSSerialize *); @@ -1999,6 +2010,20 @@ static void tglPrepareVenturaMMIOUC(KernelPatcher &patcher,IOPCIDevice *pci) { auto evidence=OSSerializer::forTarget(nullptr,tglMMIOEvidenceSerialize); if(evidence) {root->setProperty("TGLMMIOMappingEvidence",evidence);evidence->release();} } +static size_t tglNativeInstructionSize(const uint8_t *bytes,size_t length) { + // HDE may read ahead by 32 bytes. Decode a padded local copy, then require + // the complete instruction to lie in the nofault-read native span. + uint8_t code[32] {};memcpy(code,bytes,length(code),&ins); + return !(ins.flags&F_ERROR) && size<=length ? size : 0; +} +static bool tglNativeProtectCall(uintptr_t protect,uintptr_t pmap,bool tahoe) { + uint8_t code[512] {}; + return tglVM.nofault && protect && pmap && + tglVM.nofault(protect,reinterpret_cast(code),sizeof(code))==sizeof(code) && + XELPNative::protectCall(code,sizeof(code),protect,pmap,tahoe,tglNativeInstructionSize); +} static void tglPrepareTahoeMMIOUC(KernelPatcher &patcher,IOPCIDevice *pci) { tglMMIOUCReady=false; auto bar=pci->getDeviceMemoryWithRegister(0x10); @@ -2013,7 +2038,7 @@ static void tglPrepareTahoeMMIOUC(KernelPatcher &patcher,IOPCIDevice *pci) { const uint8_t create[] {0x8B,0x75,0xB4,0x89,0xF1,0x83,0xE1,0x01,0x48,0x09,0xD1,0x40,0xF6,0xC6,0x02}; const uint8_t update[] {0x8B,0x55,0xB4,0x89,0xD0,0x83,0xE0,0x01,0x48,0x09,0xC8,0xF6,0xC2,0x02}; const uint8_t protectFrame[] {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC,0x28}; - const uint8_t protectCall[] {0xE8,0x2F,0xF3,0x02,0x00}; + const bool protectCallReady=tglNativeProtectCall(protect,fn,true); uint32_t lo,hi;asm volatile("rdmsr" : "=a"(lo),"=d"(hi) : "c"(0x277)); const uint64_t pat=static_cast(hi)<<32|lo; bool ok=tglVM.enabled && tglPrepareMMIOBAR(bar) && @@ -2026,14 +2051,15 @@ static void tglPrepareTahoeMMIOUC(KernelPatcher &patcher,IOPCIDevice *pci) { !memcmp(reinterpret_cast(fn+0x10C1),create,sizeof(create)) && !memcmp(reinterpret_cast(fn+0x1197),update,sizeof(update)) && !memcmp(reinterpret_cast(protect),protectFrame,sizeof(protectFrame)) && - !memcmp(reinterpret_cast(protect+0xCC),protectCall,sizeof(protectCall)) && + protectCallReady && tglVMRead(kernelMap,tglKernelMap) && (tglKernelMap>>47)==0x1FFFF; uint64_t guard[20] {1,tglVM.enabled,tglVM.pciBAR,bar ? bar->getLength() : 0,pat,bool(fn),bool(protect),bool(kernelMap)}; if(fn && protect) { - const uintptr_t sites[] {fn,fn+0x15A,fn+0x44,fn+0x61,fn+0xAA,fn+0x10C1,fn+0x1197,protect,protect+0xCC}; - const uint8_t *bytes[] {frame,flags,args,protection,nonEPT,create,update,protectFrame,protectCall}; - const size_t lengths[] {sizeof(frame),sizeof(flags),sizeof(args),sizeof(protection),sizeof(nonEPT),sizeof(create),sizeof(update),sizeof(protectFrame),sizeof(protectCall)}; - for(unsigned i=0;i<9;++i)guard[8+i]=!memcmp(reinterpret_cast(sites[i]),bytes[i],lengths[i]); + const uintptr_t sites[] {fn,fn+0x15A,fn+0x44,fn+0x61,fn+0xAA,fn+0x10C1,fn+0x1197,protect}; + const uint8_t *bytes[] {frame,flags,args,protection,nonEPT,create,update,protectFrame}; + const size_t lengths[] {sizeof(frame),sizeof(flags),sizeof(args),sizeof(protection),sizeof(nonEPT),sizeof(create),sizeof(update),sizeof(protectFrame)}; + for(unsigned i=0;i<8;++i)guard[8+i]=!memcmp(reinterpret_cast(sites[i]),bytes[i],lengths[i]); + guard[16]=protectCallReady; } guard[17]=kernelMap && tglVMRead(kernelMap,tglKernelMap);guard[18]=tglKernelMap;guard[19]=ok; IORegistryEntry::getRegistryRoot()->setProperty("TGLTahoeMMIOGuard",guard,sizeof(guard)); @@ -2069,7 +2095,7 @@ static void tglPrepareSequoiaMMIOUC(KernelPatcher &patcher,IOPCIDevice *pci) { const uint8_t create[] {0x8B,0x75,0xB4,0x89,0xF1,0x83,0xE1,0x01,0x48,0x09,0xD1,0x40,0xF6,0xC6,0x02}; const uint8_t update[] {0x8B,0x55,0xB4,0x89,0xD0,0x83,0xE0,0x01,0x48,0x09,0xC8,0xF6,0xC2,0x02}; const uint8_t protectFrame[] {0x55,0x48,0x89,0xE5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xEC,0x28}; - const uint8_t protectCall[] {0xE8,0x71,0xCB,0x02,0x00}; + const bool protectCallReady=tglNativeProtectCall(protect,fn,false); uint32_t lo,hi;asm volatile("rdmsr" : "=a"(lo),"=d"(hi) : "c"(0x277)); const uint64_t pat=static_cast(hi)<<32|lo; bool ok=tglVM.enabled && tglPrepareMMIOBAR(bar) && @@ -2082,14 +2108,15 @@ static void tglPrepareSequoiaMMIOUC(KernelPatcher &patcher,IOPCIDevice *pci) { !memcmp(reinterpret_cast(fn+0x1390),create,sizeof(create)) && !memcmp(reinterpret_cast(fn+0x1030),update,sizeof(update)) && !memcmp(reinterpret_cast(protect),protectFrame,sizeof(protectFrame)) && - !memcmp(reinterpret_cast(protect+0xCA),protectCall,sizeof(protectCall)) && + protectCallReady && tglVMRead(kernelMap,tglKernelMap) && (tglKernelMap>>47)==0x1FFFF; uint64_t guard[20] {1,tglVM.enabled,tglVM.pciBAR,bar ? bar->getLength() : 0,pat,bool(fn),bool(protect),bool(kernelMap)}; if(fn && protect) { - const uintptr_t sites[] {fn,fn+0x159,fn+0x35,fn+0x52,fn+0xB8,fn+0x1390,fn+0x1030,protect,protect+0xCA}; - const uint8_t *bytes[] {frame,flags,args,protection,nonEPT,create,update,protectFrame,protectCall}; - const size_t lengths[] {sizeof(frame),sizeof(flags),sizeof(args),sizeof(protection),sizeof(nonEPT),sizeof(create),sizeof(update),sizeof(protectFrame),sizeof(protectCall)}; - for(unsigned i=0;i<9;++i)guard[8+i]=!memcmp(reinterpret_cast(sites[i]),bytes[i],lengths[i]); + const uintptr_t sites[] {fn,fn+0x159,fn+0x35,fn+0x52,fn+0xB8,fn+0x1390,fn+0x1030,protect}; + const uint8_t *bytes[] {frame,flags,args,protection,nonEPT,create,update,protectFrame}; + const size_t lengths[] {sizeof(frame),sizeof(flags),sizeof(args),sizeof(protection),sizeof(nonEPT),sizeof(create),sizeof(update),sizeof(protectFrame)}; + for(unsigned i=0;i<8;++i)guard[8+i]=!memcmp(reinterpret_cast(sites[i]),bytes[i],lengths[i]); + guard[16]=protectCallReady; } guard[17]=kernelMap && tglVMRead(kernelMap,tglKernelMap);guard[18]=tglKernelMap;guard[19]=ok; IORegistryEntry::getRegistryRoot()->setProperty("TGLSequoiaMMIOGuard",guard,sizeof(guard)); @@ -2228,7 +2255,7 @@ static void tglPrepareMMIOUC(KernelPatcher &patcher,IOPCIDevice *pci) { } void tglVMPrepare(KernelPatcher &patcher,IOPCIDevice *pci) { // The only private pmap field used is root-table VA at+50. Verify the - // matching native accessor at each exact admitted kernel, without calling + // native accessor contract, without calling // pmap_verify_noncacheable (it deliberately panics on a cacheable mapping). auto verify=patcher.solveSymbol(KernelPatcher::KernelID,"_pmap_verify_noncacheable"); auto kp=patcher.solveSymbol(KernelPatcher::KernelID,"_kernel_pmap"); @@ -2237,19 +2264,30 @@ void tglVMPrepare(KernelPatcher &patcher,IOPCIDevice *pci) { auto nf=patcher.solveSymbol(KernelPatcher::KernelID,"_ml_nofault_copy"); patcher.clearError(); if (!verify || !kp || !pb || !pe || !nf) return; - const uint8_t callPrefix[] {0x48,0x89,0xDE,0xE8}; - if (memcmp(reinterpret_cast(verify+0x10),callPrefix,sizeof(callPrefix))) return; - int32_t displacement;memcpy(&displacement,reinterpret_cast(verify+0x14),4); - const auto target=verify+0x18+displacement; const bool dp1=(XELP::kernelOS(version)==XELP::OS::Donor); const bool monterey=(XELP::kernelOS(version)==XELP::OS::Monterey); const bool ventura=(XELP::kernelOS(version)==XELP::OS::Ventura); const bool sonoma=(XELP::kernelOS(version)==XELP::OS::Sonoma); const bool sequoia=(XELP::kernelOS(version)==XELP::OS::Sequoia); const bool tahoe=(XELP::kernelOS(version)==XELP::OS::Tahoe); - if (target!=verify-(dp1 ? 0x3930 : monterey ? 0x4190 : ventura ? 0x3D70 : sonoma ? 0x3EF0 : sequoia ? 0x3FE0 : tahoe ? 0x3D40 : 0x3AB0)) return; - const uint8_t rootLoad[] {0x48,0x8B,0x47,0x50}; - if (memcmp(reinterpret_cast(target+0x20),rootLoad,sizeof(rootLoad))) return; + if(sequoia || tahoe) { + const auto store=patcher.solveSymbol(KernelPatcher::KernelID,"_kernel_pmap_store"); + patcher.clearError(); + uint8_t caller[96] {},accessor[64] {}; + const auto copy=reinterpret_cast(nf); + if(!store || copy(verify,reinterpret_cast(caller),sizeof(caller))!=sizeof(caller))return; + const auto target=XELPNative::rootCall(caller,sizeof(caller),verify,store); + if((target>>47)!=0x1FFFF || copy(target,reinterpret_cast(accessor),sizeof(accessor))!=sizeof(accessor) || + !XELPNative::rootAccessor(accessor,sizeof(accessor)))return; + } else { + const uint8_t callPrefix[] {0x48,0x89,0xDE,0xE8}; + if (memcmp(reinterpret_cast(verify+0x10),callPrefix,sizeof(callPrefix))) return; + int32_t displacement;memcpy(&displacement,reinterpret_cast(verify+0x14),4); + const auto target=verify+0x18+displacement; + if (target!=verify-(dp1 ? 0x3930 : monterey ? 0x4190 : ventura ? 0x3D70 : sonoma ? 0x3EF0 : 0x3AB0)) return; + const uint8_t rootLoad[] {0x48,0x8B,0x47,0x50}; + if (memcmp(reinterpret_cast(target+0x20),rootLoad,sizeof(rootLoad))) return; + } tglVM.nofault=reinterpret_cast(nf); if (!tglVMRead(kp,tglVM.kernelPmap) || !tglVMRead(pb,tglVM.directBase) || !tglVMRead(pe,tglVM.directEnd)) return; @@ -2972,7 +3010,7 @@ void tglHostPublish() { // No added forcewake, sleeps, allocation, or registry work in the write bridge. void tglGuCDMABoundary(uint32_t stage, volatile uint8_t *base) { auto &o=tglGuCObservation; - if (stage>1 || o.calls!=1 || o.thread!=reinterpret_cast(current_thread()) || + if (stage>1 || o.calls!=1 || __atomic_load_n(&o.thread,__ATOMIC_ACQUIRE)!=reinterpret_cast(current_thread()) || !o.accelerator || !base || o.barSize<0x1000000 || o.boundary[stage][0]) return; auto a=static_cast(o.accelerator); if (base!=*reinterpret_cast(a+0x1240)) return; @@ -2991,7 +3029,7 @@ void tglGuCDMABoundary(uint32_t stage, volatile uint8_t *base) { // r15,r14,r13,r12,r11,r10,r9,r8,rdi,rsi,rdx,rcx,rax,RFLAGS (14 QWords). void tglGuCDMAPreStart(volatile uint8_t *base, const uint64_t *saved) { auto &o=tglGuCObservation; - if (o.calls!=1 || o.thread!=reinterpret_cast(current_thread()) || + if (o.calls!=1 || __atomic_load_n(&o.thread,__ATOMIC_ACQUIRE)!=reinterpret_cast(current_thread()) || !o.accelerator || !base || !saved || o.barSize<0x1000000 || o.preStart[0]) return; if (base!=*reinterpret_cast(static_cast(o.accelerator)+0x1240)) return; auto r=o.preStart; uint64_t ticks; @@ -3796,6 +3834,7 @@ void XE::init() { void XE::selectProfile(const XELP::Profile &profile) { xeProfile=profile; + tglGT2Detected=profile.admitted() && !profile.overridden() && XELP::tigerLakeGT2Device(profile.identity); display12SmallGTDetected=profile.admitted() && XELP::display12SmallGT(profile.gt); adlsDisplaySelected=profile.admitted() && XELP::displayAdapter(profile.family)==XELP::DisplayAdapter::ADLS; adlnNativeRCSProfileRequested=false; @@ -3849,14 +3888,35 @@ Display13OutputWiring XE::display13BoardWiring(IOService *provider) { if(!xeProfile.displayAdmitted() || xeProfile.display!=XELP::Display::XeLPD)return {}; // Display13 physical capacity is shared by ADL-N and mobile ADL/RPL. // VBT chooses board routing; a family name must not exclude TC PHYs. - if(!property)return XELP::VBT::display13Fixed(firmwarePorts,Display13OutputWiring::Physical,xeProfile.family); - // Preserve the older ADL-N Combo property contract. It cannot select TC. - if(adlpDisplaySelected)return {}; - auto wiring=OSDynamicCast(OSData,property); - return XELP::VBT::selectHDMI(true,wiring?wiring->getBytesNoCopy():nullptr, - wiring?wiring->getLength():0,firmwarePorts,3); + Display13OutputWiring board; + if(!property)board=XELP::VBT::display13Fixed(firmwarePorts,Display13OutputWiring::Physical,xeProfile.family); + else { + // Preserve the older ADL-N Combo property contract. It cannot select TC. + if(adlpDisplaySelected)return {}; + auto wiring=OSDynamicCast(OSData,property); + board=XELP::VBT::selectHDMI(true,wiring?wiring->getBytesNoCopy():nullptr, + wiring?wiring->getLength():0,firmwarePorts,3); + } + if(checkKernelArgument("-wex-dsi-adopt")) { + // The old HDMI experiment tears down the firmware DSI link. Never + // combine that handoff with adoption, or replace an existing A route. + if(checkKernelArgument("-wex-dsi-hdmi-test") || !board.valid || board.panel || + (board.selected()&1) || !prepareDSIFirmware(provider))return {}; + board.dsi=1; + dsiPanelOnly=checkKernelArgument("-wex-dsi-panel-only"); + dsiCommandTest=checkKernelArgument("-wex-dsi-command-test"); + dsiHostTest=checkKernelArgument("-wex-dsi-host-test"); + dsiStopTest=checkKernelArgument("-wex-dsi-stop-test"); + dsiPowerTest=checkKernelArgument("-wex-dsi-power-test"); + dsiPanelTest=checkKernelArgument("-wex-dsi-panel-test") || dsiHostTest; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIPanelOnly",dsiPanelOnly); + if(!board.described())return {}; + } + return board; } + + void XE::adlnBootstrap(IOPCIDevice *pci) { pci->setProperty("N97BringupVersion", 1ULL, 32); pci->setProperty("N97NativeStartAllowed", false); @@ -3875,9 +3935,10 @@ void XE::adlnBootstrap(IOPCIDevice *pci) { // Bootstrap runs before framebuffer start loads its board policy. Native // GOP qualification must already use that physical port, not assume B. const auto board=display13BoardWiring(pci);adlnOutputWiring=board; - if(displayRequested && !Display13FBTopology::supportedWiring(board,xeProfile.display==XELP::Display::XeLPD)) { + if(displayRequested && !Display13FBTopology::supportedWiring(board,xeProfile.display==XELP::Display::XeLPD,checkKernelArgument("-wex-dsi-adopt"))) { pci->setProperty("N97NativeRoutingUnavailable",true);return; } + if(board.dsi && !prepareDSIFirmware(pci))return; auto bootDDI=displayRequested ? UINT32_MAX : 1U; uint64_t connector[] {1,displayRequested,pci->getProperty("xelp-hdmi-wiring")!=nullptr,board.valid,board.hdmi,bootDDI}; pci->setProperty("N97NativeBootConnector",connector,sizeof(connector)); @@ -3934,6 +3995,45 @@ void XE::adlnBootstrap(IOPCIDevice *pci) { pci->setProperty("N97StolenReservation",reservation,sizeof(reservation)); AlderLakeN::readDisplay(io,barReady,result); pci->setProperty("N97Display13Boot", &result, sizeof(result)); + // Observe the separate DSI transcoder without treating it as a normal + // DDI GOP. Cached words only; no command FIFO or clear-on-read registers. + if(barReady && !result.activePipes && (result.poweredPipes&1)) { + static const uint32_t regs[] {0x7B008,0x7B808,0x6B000,0x6B004,0x6B008,0x6B00C,0x6B010,0x6B014, + 0x6B030,0x6B034,0x6B090,0x6B094,0x6B0D8,0x6B400,0x6B404,0x6B894,0x6BC00, + 0x46010,0x46014,0x164280,0x164284,0x164288,0x16428C,0x164290, + 0xC7200,0xC7204,0xC8250,0xC8254,0xC8258,0x48400,0x51004,0x46070,0x46008, + 0x44300,0x45008,0x4500C,0x45500,0x45504,0x7017C,0x7027C,0x7037C, + 0x162028,0x6C028,0x6C100,0x6C104,0x6C10C,0x6C120,0x6C124,0x6C128,0x64C04, + 0x6C804,0x6C014,0x6C894,0x6C888,0x6C890,0x6C89C,0x6C898}; + auto words=static_cast(IOMalloc(sizeof(regs)*2)); + if(words) { + for(unsigned i=0;isetProperty("WEXDisplay13DSIBoot",words,sizeof(regs)*2); + if(checkKernelArgument("-wex-dsi-audit")) { + // Decode only already collected words. Overlapping sources + // must agree; this summary is not a stable adoption certificate. + const auto cached=[&](uint32_t reg,uint32_t &value) { + uint32_t bootValue=0;const bool found=AlderLakeN::value(result,reg,bootValue); + for(unsigned i=0;i>29)&7; + auto decoded=XELP::DSI::decodeReadout(cached,index<3?references[index]:0); + pci->setProperty("WEXDSIReadout",&decoded,sizeof(decoded)); + } + IOFree(words,sizeof(regs)*2); + } + } + auditDSIBoot(pci,io.base,io.length,result,barReady && reservationReady); + if(checkKernelArgument("-wex-dsi-hdmi-test")) { + auto handoff=Display13DSITrial::transfer(io,barReady && xelpMemoryMatches(pci),result,xelpMemory,board.hdmi); + pci->setProperty("WEXDisplay13DSIHandoff",&handoff,sizeof(handoff)); + AlderLakeN::readDisplay(io,barReady,result); + pci->setProperty("WEXDisplay13DSIAfter",&result,sizeof(result)); + } // Bounded pre-native evidence for selected fixed TC ports, including ports // which firmware owns without routing a transcoder to them. uint32_t bootTC[1+4*8] {1},bootPower=0; @@ -3948,8 +4048,10 @@ void XE::adlnBootstrap(IOPCIDevice *pci) { pci->setProperty("WEXDisplay13TCBootPorts",bootTC,sizeof(bootTC)); } - bool bootstrapReady=true; - if(displayRequested && linearTest && (result.activePipes&(result.activePipes-1))) { + if(displayRequested && board.dsi) + XELP::DSI::captureBoot(io,result,dsiProfile,xelpMemory,barReady && reservationReady,dsiBoot); + bool bootstrapReady=!board.dsi || dsiBoot.usable(); + if(!board.dsi && displayRequested && linearTest && (result.activePipes&(result.activePipes-1))) { auto parked=Display13Boot::normalize(io,result,barReady && Display13FBTopology::multiOutputWiring(board) && xelpMemoryMatches(pci),board,xelpMemory,snapshot->normalize); pci->setProperty("N97DualGOPNormalize",&parked,sizeof(parked)); AlderLakeN::readDisplay(io,barReady,result); @@ -3957,13 +4059,15 @@ void XE::adlnBootstrap(IOPCIDevice *pci) { bootstrapReady=parked.status==Display13Boot::Status::Complete; } if(displayRequested && linearTest && bootstrapReady && Display13FBTopology::multiOutputWiring(board)) { - auto unused=Display13Boot::releaseUnusedTC(io,result,barReady && xelpMemoryMatches(pci),board); + auto unused=Display13Boot::releaseUnusedTC(io,result,barReady && xelpMemoryMatches(pci),board, + board.dsi ? &dsiBoot.source : nullptr); pci->setProperty("WEXDisplay13UnusedTC",&unused,sizeof(unused)); bootstrapReady=unused.status==Display13Boot::Status::Complete; } if(displayRequested && bootstrapReady) { uint32_t trans=UINT32_MAX; - if(AlderLakeN::value(result,0x60400,trans))bootDDI=Display13FBTopology::bootstrapDDI(board, + if(board.dsi)bootDDI=0; + else if(AlderLakeN::value(result,0x60400,trans))bootDDI=Display13FBTopology::bootstrapDDI(board, result.status==AlderLakeN::ReadoutStatus::Complete,result.poweredPipes,result.activePipes,trans,true); connector[5]=bootDDI;pci->setProperty("N97NativeBootConnector",connector,sizeof(connector)); } @@ -4109,9 +4213,11 @@ void XE::adlnBootstrap(IOPCIDevice *pci) { AlderLakeN::LinearPlan bootPlan; uint32_t sourceProof[]={1,bootDDI,uint32_t(protocol),uint32_t(AlderLakeN::linearPlan( result,bootPlan,bootDDI,protocol,bootDDI>=3?&tc:nullptr)),tc.valid()}; - pci->setProperty("N97NativeBootSource",sourceProof,sizeof(sourceProof)); - const bool prefix=Display13Boot::reservedPrefix(io,result,bootstrapReady,bootDDI,xelpMemory, - bootDDI>=3 ? &tc : nullptr,protocol); + if(!board.dsi)pci->setProperty("N97NativeBootSource",sourceProof,sizeof(sourceProof)); + const bool prefix=board.dsi ? bootstrapReady && XELP::DSI::verifyBootPrefix(io,dsiBoot,xelpMemory) : + Display13Boot::reservedPrefix(io,result,bootstrapReady,bootDDI,xelpMemory, + bootDDI>=3 ? &tc : nullptr,protocol); + if(board.dsi)pci->setProperty("WEXDSIBootPrefix",&dsiBoot,sizeof(dsiBoot)); auto aperture=pci->getDeviceMemoryWithRegister(0x18); const bool memoryReady= aperture && aperture->getLength()==0x10000000 && page_size==4096 && @@ -4190,13 +4296,18 @@ bool XE::xelpBindProbe(void *controller) { config.legacy=tglTCProbeMask();config.typeC=tglTCWiring.typeC;config.excluded=tglTCWiring.excluded; config.legacyBackend=tglAUXParentMask; } else { + auto external=adlnOutputWiring;external.dsi=0; + const bool dsi=adlnOutputWiring.dsi!=0; if(!display13FBTopologyHooksReady || controller!=adlnTopologyController || - !Display13FBTopology::supportedWiring(adlnOutputWiring,xeProfile.display==XELP::Display::XeLPD) || + (dsi && (!adlnOutputWiring.described() || !__atomic_load_n(&dsiResourcesReady,__ATOMIC_ACQUIRE))) || + !Display13FBTopology::supportedWiring(dsi?external:adlnOutputWiring,xeProfile.display==XELP::Display::XeLPD) || ((adlnOutputWiring.selected()&0x78) && (!display13TCProbeReady || !display13FBTCHooksReady)) || (adlnOutputWiring.dp && (!display13TCDPProbeReady || !display13FBSSTHooksReady || !display13FBAUXHooksReady)) || + (adlnOutputWiring.typeC && !display13DPAltProbeReady) || (adlnOutputWiring.panel && (!display13PanelProbeReady || !display13PanelPowerReady || !display13FBSSTHooksReady || !display13FBAUXHooksReady)))return false; - config.combo=adlnOutputWiring.selected()&3;config.comboBackend=3; - config.legacy=adlnOutputWiring.selected()&0x78;config.legacyBackend=config.legacy?0x78:0; + config.combo=(adlnOutputWiring.selected()&~adlnOutputWiring.dsi)&3;config.comboBackend=3; + config.typeC=adlnOutputWiring.typeC; + config.legacy=(adlnOutputWiring.selected()&0x78)&~config.typeC;config.legacyBackend=(config.legacy|config.typeC)?0x78:0; } const bool bound=config.valid() && reinterpret_cast(xelpProviderConfigureProbe)(controller,&config); IORegistryEntry::getRegistryRoot()->setProperty("XELPSharedProbeBound",bound); @@ -4283,11 +4394,11 @@ void XE::processKernel(KernelPatcher &patcher, DeviceInfo *info) { if(map)map->release(); return; } - if(display12SmallGTDetected && XELP::display12SmallGTDevice(xeProfile.identity) && + if(((display12SmallGTDetected && XELP::display12SmallGTDevice(xeProfile.identity)) || tglGT2Detected) && !xeProfile.overridden() && physicalGPU && - XELP::hostOS(XELP::kernelOS(version)) && (physicalGPU->configRead16(4)&2)) { + (XELP::hostOS(XELP::kernelOS(version)) || (tglGT2Detected && XELP::kernelOS(version)==XELP::OS::Donor)) && (physicalGPU->configRead16(4)&2)) { // Gen12 forcewake/fuse coordinates are shared with the existing reader. - // Its ADL-N profile verdict is not used to admit Display12 GT1. No engine, + // Its ADL-N profile verdict is not used to admit Display12 GT1/GT2. No engine, // GGTT, display, PLL or reset programming is performed here. auto map=physicalGPU->mapDeviceMemoryWithRegister(0x10,kIOMapInhibitCache); if(map && map->getVirtualAddress() && map->getLength()==0x1000000) { @@ -4300,12 +4411,17 @@ void XE::processKernel(KernelPatcher &patcher, DeviceInfo *info) { } io {reinterpret_cast(map->getVirtualAddress())}; uint32_t edram=UINT32_MAX; auto gt=AlderLakeN::readGT(io,true,&edram); - display12SmallGTReady=XELP::decodeSmallGT(gt.fuses,display12SmallGT) && - (gt.status==AlderLakeN::GTStatus::Complete || gt.status==AlderLakeN::GTStatus::ProfileMismatch) && + const bool readoutReady=(gt.status==AlderLakeN::GTStatus::Complete || gt.status==AlderLakeN::GTStatus::ProfileMismatch) && gt.requestedMask==3 && gt.releasedMask==3 && !gt.releaseFailedMask && edram==0; - physicalGPU->setProperty(adlsDisplaySelected?"ADLSGTQualified":"XELPSmallGTQualified",display12SmallGTReady); - physicalGPU->setProperty(adlsDisplaySelected?"ADLSGTBoot":"XELPSmallGTBoot",>,sizeof(gt)); - physicalGPU->setProperty(adlsDisplaySelected?"ADLSEDRAM":"XELPSmallGTEDRAM",edram,32); + display12SmallGTReady=display12SmallGTDetected && readoutReady && XELP::decodeSmallGT(gt.fuses,display12SmallGT); + tglGT2Ready=tglGT2Detected && readoutReady && XELP::decodeTGLGT2(gt.fuses,tglGT2Resources); + if(tglGT2Detected) { + physicalGPU->setProperty("TGLGT2Qualified",tglGT2Ready); + physicalGPU->setProperty("TGLGT2Boot",>,sizeof(gt)); + } + if(display12SmallGTDetected)physicalGPU->setProperty(adlsDisplaySelected?"ADLSGTQualified":"XELPSmallGTQualified",display12SmallGTReady); + if(display12SmallGTDetected)physicalGPU->setProperty(adlsDisplaySelected?"ADLSGTBoot":"XELPSmallGTBoot",>,sizeof(gt)); + if(display12SmallGTDetected)physicalGPU->setProperty(adlsDisplaySelected?"ADLSEDRAM":"XELPSmallGTEDRAM",edram,32); if(adlsDisplaySelected) { uint32_t registers[][2] { {0x42000,0},{0x45404,0},{0x45450,0},{0x45454,0}, @@ -4321,7 +4437,7 @@ void XE::processKernel(KernelPatcher &patcher, DeviceInfo *info) { for(auto ®:registers)reg[1]=io.read(reg[0]); physicalGPU->setProperty("ADLSDisplayBoot",registers,sizeof(registers)); } - SYSLOG("igfx","Display12 small-GT boot readout status=%u fuses=%x/%x/%x/%x/%x release=%x/%x", + SYSLOG("igfx","Display12 GT boot readout status=%u fuses=%x/%x/%x/%x/%x release=%x/%x", static_cast(gt.status),gt.fuses[0],gt.fuses[1],gt.fuses[2],gt.fuses[3],gt.fuses[4], gt.releasedMask,gt.releaseFailedMask); } @@ -4416,6 +4532,8 @@ bool XE::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t add XELP::display13PanelProbeCapable(reinterpret_cast(query)); display13TCProbeReady=query>=address && query-address(query)); + display13DPAltProbeReady=query>=address && query-address(query)); display13TCDPProbeReady=query>=address && query-address(query)); } @@ -4450,6 +4568,8 @@ bool XE::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t add return false; } +static bool dsiCachePath(OSObject *); +static bool dsiLifecycleTarget(IOService *,OSObject *); static bool adlnWriterMappingBound(); namespace { struct Display13GMBusScope { @@ -4490,6 +4610,12 @@ struct Display13FBPipeContext { uint64_t watermarkClockHz {}; }; struct Display13FBPowerContext { + // Publish with IRQ state only under a quiesced adoption/retirement boundary. + // No DSI binding is installed until the complete adoption backend exists. + Display13Transcoder::Bindings transcoders; + XELP::DSI::Readout bootDSI; // Captured before handoff; not an implicit admission flag. + OSObject *dsiPort {}; // Retained after source-qualified native mode binding. + uint32_t dsiNativeWriters {}; // Owner-lock count; cursor bodies may wait without the FB mutex. IOSimpleLock *tcBankLock {}; bool tcSupported {},tcBankFaulted {}; Display13TC::Mode tcMode[4]; @@ -4515,6 +4641,8 @@ struct Display13FBPowerContext { // Borrow only while lock is held, and finish consuming the snapshot // before releasing it or calling a native operation that can reenter. AlderLakeN::DisplayReadout displayScratch; + // Failure-only receipt, filled and copied to IORegistry while lock is held. + uint64_t wmGeometryScratch[56] {}; void *power {}, *controller {}, *access {}; IOMemoryMap *map {}; // Owned mapping remains valid even on native start failure. uint32_t sequence {}, errors {}, writes {}, firstFaultLine {}; @@ -4526,6 +4654,8 @@ struct Display13FBPowerContext { uint64_t powerContext[24] {1}; // Bounded CPU-only execution-context evidence. }; static Display13FBPowerContext display13FBPower; +#include "kern_dsi_adapter_state.inc" + // AlderLakeN start bypasses the generic TGL framebuffer path and its power sampler. // Use the retained, uncached BAR owner. These GT mirrors require no forcewake, // mailbox transaction, MMIO write or native policy intervention. @@ -4623,7 +4753,7 @@ static uint32_t adlnSharedGT(const void *controller) { // validation; they never follow native map objects or read GGTT entries. static bool display13FBAssociations(const void *controller,size_t controllerBytes, const void *fb,size_t fbBytes,uintptr_t owner) { - if(!Display13FBMemory::associations(controller,controllerBytes,fb,fbBytes,owner,true))return false; + if(!Display13FBMemory::associations(controller,controllerBytes,fb,fbBytes,owner,true,dsiLogicalSurface(fb)))return false; const auto surface=Display13FBMemory::field(fb,0x4348); return Display13FBMemory::publishedSurface(display13FBPower.pipe,fb,surface); } @@ -4641,7 +4771,7 @@ static TGLFBBinding::Readout adlnInspectPathBinding(IOService *fb,OSObject *path const TGLFBBinding::Objects objects { {controller,controller->getMetaClass()->getClassSize()}, {fb,fb->getMetaClass()->getClassSize()}, {path,path->getMetaClass()->getClassSize()}, {plane,plane ? plane->getMetaClass()->getClassSize() : 0}, - {port,port ? port->getMetaClass()->getClassSize() : 0}, display13FBExpectedOwner(controller) + {port,port ? port->getMetaClass()->getClassSize() : 0}, display13FBExpectedOwner(controller),dsiLogicalSurface(fb) }; return TGLFBBinding::inspect(objects,platform,wiring,attached); } @@ -4762,6 +4892,7 @@ static void adlnIRQRecord(uint64_t (&r)[32],unsigned event) { r[29]=reinterpret_cast(c.lock);r[30]=c.handlers;r[31]=c.quiescing|(c.blockedPipes<<8)|(uint64_t(c.softwareHandlers)<<32); } + // These operations run under the display owner lock. AUX power qualification // needs the TC bank lock but never enters the native port or power callbacks. struct Display13FBTCAuxIO { @@ -4779,7 +4910,8 @@ struct Display13FBTCAuxIO { if(!admitted(tc))return false; const auto pw=read(Display13Power::Driver),buffer=read(0x64300+0x100*tc),mode=read(0x161500+4*tc); return pw!=UINT32_MAX && buffer!=UINT32_MAX && mode!=UINT32_MAX && (pw&5)==5 && - (buffer&64) && (mode&7)==4; + (buffer&64) && (mode&4) && Display13TC::Port::modeMatches(mode, + display13FBPower.tcMode[tc].selectedMode==3?3:4); } bool auxPowered(unsigned candidate) const { if(!admitted(candidate))return false;const auto value=read(Display13Power::AUX); @@ -4880,11 +5012,27 @@ struct Display13FBTCIO : Display13FBTCAuxIO { const auto pll=read(p.pll); return pll!=UINT32_MAX && !(pll&0xcc000000U) && !c.tcSignalReady[tc]; } + bool retainedOwned(unsigned ddi) { + if(ddi!=tc+3 || !powerHeld())return false; + const auto &m=display13FBPower.tcMode[tc]; + const auto native=Display13FBMemory::field(object,0x550); + return (m.phase==Display13TC::Mode::Phase::Active || m.phase==Display13TC::Mode::Phase::Cleanup) && + (m.selectedMode==3 || m.selectedMode==4) && + (native==m.selectedMode || (m.phase==Display13TC::Mode::Phase::Cleanup && !native)) && + m.ownership.active && m.ownership.owner==m.controller && + m.ownership.identity==reinterpret_cast(object) && m.ownership.port.ddi==ddi && + retainedHardwareOwner(m.ownership.port.buffer); + } + bool retainedHardwareOwner(uint32_t reg) { + const auto buffer=read(reg);return buffer!=UINT32_MAX && (buffer&64); + } bool owned(unsigned ddi) { + if(!retainedOwned(ddi))return false; const auto &m=display13FBPower.tcMode[tc]; - return ddi==tc+3 && powerHeld() && m.phase==Display13TC::Mode::Phase::Active && - m.ownership.active && m.ownership.identity==reinterpret_cast(object) && - Display13FBMemory::field(object,0x550)==4; + return m.phase==Display13TC::Mode::Phase::Active && + Display13FBMemory::field(object,0x550)==m.selectedMode && + Display13TC::Port::modeMatches(read(m.ownership.port.ready),m.selectedMode) && + (m.selectedMode!=3 || m.lanes.unchanged(*this,m.ownership.port)); } bool quiescent(unsigned ddi) { if(ddi!=tc+3 || !accessHeld())return false; @@ -4898,6 +5046,17 @@ struct Display13FBTCIO : Display13FBTCAuxIO { } return true; } + bool modeQuiescent(uintptr_t controller,uintptr_t port,unsigned ddi) { + auto &c=display13FBPower; + if(controller!=reinterpret_cast(c.controller) || port!=reinterpret_cast(object) || + ddi!=tc+3 || c.tcBusy[tc] || c.bridge.nativeDDIRefs(ddi) || c.bridge.nativeAUXRefs(ddi) || + !quiescent(ddi))return false; + // 0x570 belongs to cached AGDCDPLinkConfig, cleared by the native + // resetSoftwareState callback. It is not an outstanding-use count. + Display13TC::Port p;Display13TC::Port::resolve(ddi,p); + const auto pll=read(p.pll),aux=read(0x64310+0x100*tc); + return pll!=UINT32_MAX && aux!=UINT32_MAX && !(pll&0xcc000000) && !(aux&0x80000000); + } void delayUs(unsigned us){IODelay(us);} }; // Only used after native object/BAR binding and the power lease proof. The @@ -4978,7 +5137,7 @@ static bool adlnBeginClockGuard(Display13FBClockGuard &g) { if(quiet)break; if(ms<20)IOSleep(1); } - quiet=quiet && !(io.read(Display13FBIRQ::Master)&Display13FBIRQ::Enable) && Display13FBIRQ::consumersStopped(io,7); + quiet=quiet && !(io.read(Display13FBIRQ::Master)&Display13FBIRQ::Enable) && Display13FBIRQ::consumersStopped(io,7,c.transcoders); if(!quiet) {failure(3);adlnEndClockGuard(g,true);} return quiet; } @@ -5023,6 +5182,34 @@ static void adlnPublishMemory(const char *name,unsigned pipe,void *data,unsigned if(!first || !root->getProperty(key))root->setProperty(key,data,bytes); } +struct Display13SurfaceIO { + IOMemoryMap *map;unsigned pipe;IOMemoryDescriptor *descriptor {};uint32_t options {}; + bool memoryReady(uintptr_t mapping,uintptr_t memory,uintptr_t owner,uint64_t bytes) { + auto m=reinterpret_cast(mapping),s=reinterpret_cast(memory); + uint64_t q[16] {1,0,m->getMetaClass()->getClassSize(),s->getMetaClass()->getClassSize()}; + auto publish=[&](uint64_t reason) {q[1]=reason;adlnPublishMemory("N97FBScanoutMemoryOwner",pipe,q,sizeof(q));return !reason;}; + if(!m->metaCast("IGAccelMemoryMap") || q[2]!=0x128 || + !s->metaCast("IGAccelSysMemory") || q[3]!=0x198)return publish(1); + q[4]=Display13FBMemory::field(m,0x88);q[5]=Display13FBMemory::field(s,0x28); + q[6]=Display13FBMemory::field(m,0x18);q[7]=Display13FBMemory::field(m,0x90); + q[8]=Display13FBMemory::field(reinterpret_cast(owner),0x150);q[9]=Display13FBMemory::field(s,0x40); + if(q[4]!=owner || q[5]!=owner || q[6]!=memory || q[7]!=q[8] || q[9](s,0xD0)); + auto manager=Display13FBMemory::field(reinterpret_cast(owner),0x1260); + q[10]=descriptor ? descriptor->getLength() : 0;q[11]=descriptor!=nullptr; + q[12]=manager ? manager->getMetaClass()->getClassSize() : 0;q[14]=reinterpret_cast(descriptor); + if(!descriptor || q[10]metaCast("IGMemoryManager") || q[12]<0x8C)return publish(3); + // Match native getPhysicalSegment's mapper policy. + options=Display13FBMemory::field(manager,0x88);q[13]=options;return publish(0); + } + + uint64_t physical(uintptr_t,uint64_t off,uint64_t &span) { + IOByteCount length=0;const auto p=descriptor->getPhysicalSegment(off,&length,options);span=length;return p; + } + uint64_t readPTE(uint32_t reg) {return *reinterpret_cast(map->getVirtualAddress()+reg);} +}; + // Caller holds the display-owner lock. Mapping is owned by PowerInit, not a // register-access callback with an unknown lifetime or an on-read serializer. static Display13FBMemory::Readout display13FBInspectMemory(IOService *fb,bool bootScanout=true,unsigned pipe=0,bool publish=true) { @@ -5042,36 +5229,10 @@ static Display13FBMemory::Readout display13FBInspectMemory(IOService *fb,bool bo if(Display13FBMemory::field(fb,0x4288)!=0xFF || !adlnSharedBinding(controller,reinterpret_cast(owner)) || !Display13FBMemory::identityAvailable(c.pipe,fb,pipe))return r; - struct SurfaceIO { - IOMemoryMap *map;unsigned pipe;IOMemoryDescriptor *descriptor {};uint32_t options {}; - bool memoryReady(uintptr_t mapping,uintptr_t memory,uintptr_t owner,uint64_t bytes) { - auto m=reinterpret_cast(mapping),s=reinterpret_cast(memory); - uint64_t q[16] {1,0,m->getMetaClass()->getClassSize(),s->getMetaClass()->getClassSize()}; - auto publish=[&](uint64_t reason) {q[1]=reason;adlnPublishMemory("N97FBScanoutMemoryOwner",pipe,q,sizeof(q));return !reason;}; - if(!m->metaCast("IGAccelMemoryMap") || q[2]!=0x128 || - !s->metaCast("IGAccelSysMemory") || q[3]!=0x198)return publish(1); - q[4]=Display13FBMemory::field(m,0x88);q[5]=Display13FBMemory::field(s,0x28); - q[6]=Display13FBMemory::field(m,0x18);q[7]=Display13FBMemory::field(m,0x90); - q[8]=Display13FBMemory::field(reinterpret_cast(owner),0x150);q[9]=Display13FBMemory::field(s,0x40); - if(q[4]!=owner || q[5]!=owner || q[6]!=memory || q[7]!=q[8] || q[9](s,0xD0)); - auto manager=Display13FBMemory::field(reinterpret_cast(owner),0x1260); - q[10]=descriptor ? descriptor->getLength() : 0;q[11]=descriptor!=nullptr; - q[12]=manager ? manager->getMetaClass()->getClassSize() : 0;q[14]=reinterpret_cast(descriptor); - if(!descriptor || q[10]metaCast("IGMemoryManager") || q[12]<0x8C)return publish(3); - // Match native getPhysicalSegment's mapper policy. - options=Display13FBMemory::field(manager,0x88);q[13]=options;return publish(0); - } - - uint64_t physical(uintptr_t,uint64_t off,uint64_t &span) { - IOByteCount length=0;const auto p=descriptor->getPhysicalSegment(off,&length,options);span=length;return p; - } - uint64_t readPTE(uint32_t reg) {return *reinterpret_cast(map->getVirtualAddress()+reg);} - } io {c.map,pipe}; + Display13SurfaceIO io {c.map,pipe}; const auto reserve=owner ? Display13FBMemory::field(reinterpret_cast(owner),0x13C0) : 0; c.pipe[pipe].scanout=Display13FBMemory::inspectSurface(io,controller,controller->getMetaClass()->getClassSize(), - fb,fb->getMetaClass()->getClassSize(),owner,reserve,(c.map->getLength()-0x800000)/8*4096,xelpMemory); + fb,fb->getMetaClass()->getClassSize(),owner,reserve,(c.map->getLength()-0x800000)/8*4096,xelpMemory,dsiLogicalSurface(fb)); adlnPublishMemory("N97FBAcceleratorBacking",pipe,&c.pipe[pipe].scanout,sizeof(c.pipe[pipe].scanout)); const auto prefix=__atomic_load_n(&display13FBGTT.sharedPoolPrefix,__ATOMIC_ACQUIRE); const bool dualReservation=prefix && prefix==reserve && adlnSysMemOwner(controller,Display13FBMemory::pool(controller)); @@ -5117,10 +5278,22 @@ static Display13FBMemory::Readout display13FBInspectMemory(IOService *fb,bool bo return Display13FBMemory::inspect(io,controller,controller->getMetaClass()->getClassSize(), fb,fb->getMetaClass()->getClassSize(),aperture->getLength(),xelpMemory,bootScanout,display13FBExpectedOwner(controller),c.bridge.bootPort, c.bridge.bootPort>=3 ? &tc : nullptr,dp?&source:nullptr, - c.bootPanel); + c.bootPanel,bootScanout && c.transcoders.dsi(0) ? &c.bootDSI : nullptr); } } + + + + + + + + + + + + bool XE::processDisplay13Framebuffer(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { // All offsets and entry bytes are from the exact c004fc1a donor. Do not // fall through into the large TGL patcher or admit Display12 operations. @@ -5331,6 +5504,11 @@ bool XE::processDisplay13Framebuffer(KernelPatcher &patcher,size_t index,mach_vm display13FBGTTHooksReady=processDisplay13FramebufferGTT(patcher,index,address,size); IORegistryEntry::getRegistryRoot()->setProperty("N97FBGTTHooksReady",display13FBGTTHooksReady); display13FBTopologyHooksReady=processDisplay13FramebufferTopology(patcher,index,address,size); + display13DSIModeHooksReady=processDisplay13DSIModes(patcher,index,address,size); + dsiNativeWriterHooksReady=!adlnOutputWiring.dsi || (processDSINativeWriters(patcher,index,address,size) && + processDSINativeTransactions(patcher,index,address,size) && processDSINativeAttributes(patcher,index,address,size)); + dsiBootProbeReady=processDisplay13DSIBoot(patcher,index,address,size); + dsiCacheHooksReady=processDisplay13DSICache(patcher,index,address,size); IORegistryEntry::getRegistryRoot()->setProperty("N97FBTopologyHooksReady",display13FBTopologyHooksReady); using C=XELP::DisplayContract;const auto installed=display13Available(); display13FBPowerBackendReady=C{C::Power13,installed}.ready(); @@ -5407,6 +5585,7 @@ bool XE::processDisplay13FramebufferLifecycle(KernelPatcher &patcher,size_t inde const uint8_t removeCall[] {0xE8,0x67,0x02,0x00,0x00}; const bool pathKnown=pathFn==address+0x68AAE && !memcmp(reinterpret_cast(pathFn),pathEntry,sizeof(pathEntry)) && !memcmp(reinterpret_cast(address+0x68C38),removeCall,sizeof(removeCall)); + if(adlnOutputWiring.dsi && !processDSIPathDisposal(patcher,index,address,size))return false; KernelPatcher::RouteRequest pathRoute {pathSymbol,wrapAlderLakeNFreePath,orgAlderLakeNFreePath}; const bool pathRouted=disableRouted && pathKnown && patcher.routeMultipleShort(index,&pathRoute,1,address,size); patcher.clearError(); @@ -5858,11 +6037,11 @@ IOReturn XE::wrapDisplay13FBEnableIRQ(IOService *controller) { for(unsigned i=0;i<4;++i)check[19+i]=io.read(otherBanks[i]+4)&~io.read(otherBanks[i]-4); if(Display13FBTopology::multiOutputWiring(callbackXE->adlnOutputWiring)) { const auto admitted=__atomic_load_n(&c.admittedPipes,__ATOMIC_ACQUIRE); - const auto actual=Display13FBIRQ::readActivePipes(io,admitted); + const auto actual=Display13FBIRQ::readActivePipes(io,admitted,c.state.transcoders); c.status=c.quiescing ? Display13FBIRQ::Status::Power : actual.status; if(c.status==Display13FBIRQ::Status::Complete)c.status=c.state.begun && c.state.enabled ? - Display13FBIRQ::extendActivePipes(io,c.state,admitted) : Display13FBIRQ::enable(io,c.state,excluded,adlnSharedGT(controller),actual.active,callbackXE->adlnOutputWiring.selected()); - } else c.status=c.quiescing ? Display13FBIRQ::Status::Power : Display13FBIRQ::enable(io,c.state,excluded,adlnSharedGT(controller),1,callbackXE->adlnOutputWiring.selected()); + Display13FBIRQ::extendActivePipes(io,c.state,admitted) : Display13FBIRQ::enable(io,c.state,excluded,adlnSharedGT(controller),actual.active,callbackXE->adlnOutputWiring.hotplug(),callbackXE->adlnOutputWiring.typeC); + } else c.status=c.quiescing ? Display13FBIRQ::Status::Power : Display13FBIRQ::enable(io,c.state,excluded,adlnSharedGT(controller),1,callbackXE->adlnOutputWiring.hotplug(),callbackXE->adlnOutputWiring.typeC); adlnIRQCache(controller,io);adlnIRQRecord(record,1); check[23]=static_cast(c.status);check[24]=c.state.begun; check[25]=c.state.enabled;check[26]=c.state.faulted;check[27]=c.state.active;check[28]=c.state.configured; @@ -5911,10 +6090,10 @@ void XE::wrapDisplay13FBConfigureVBL(IOService *controller,IOService *fb,OSObjec // Native enablePipe enables PIPECONF before this VBL callback. Re-arm // the full pipe bank only at that real activation boundary after idle HPD. if(!Display13FBTopology::multiOutputWiring(callbackXE->adlnOutputWiring) && enabled && c.state.begun && c.state.enabled && !c.state.active && !c.state.faulted && - Display13FBIRQ::powered(io.read(Display13FBIRQ::Driver),pipe) && (io.read(0x70008+0x1000*pipe)&Display13FBIRQ::Enable)) { + Display13FBIRQ::powered(io.read(Display13FBIRQ::Driver),pipe) && Display13Transcoder::running(io,pipe,c.state.transcoders)) { const auto excluded=controller==callbackXE->adlnTopologyController ? Display13FBTopology::excludedHotplug(callbackXE->adlnPlatform,sizeof(callbackXE->adlnPlatform),callbackXE->adlnOutputWiring) : 0; - c.status=Display13FBIRQ::enable(io,c.state,excluded,adlnSharedGT(controller),1,callbackXE->adlnOutputWiring.selected()); + c.status=Display13FBIRQ::enable(io,c.state,excluded,adlnSharedGT(controller),1,callbackXE->adlnOutputWiring.hotplug(),callbackXE->adlnOutputWiring.typeC); } if(!c.state.faulted)c.status=Display13FBIRQ::vblank(io,c.state,pipe,enabled); } @@ -6183,7 +6362,7 @@ void XE::adlnSysMemOperation(IOService *controller,uint32_t slot,bool release) { bool consumersStopped(unsigned,const Display13FBMemory::Pool &) { if(pipe>=3 || display13FBPower.pipe[pipe].wmProgramming || display13FBPower.pipe[pipe].wmCommitting)return false; Display13FBIRQIO io {display13FBGTT.map->getVirtualAddress()}; - return Display13FBIRQ::consumersStopped(io,1U<getPhysicalSegment(offset,&length,kIOMemoryMapperNone);span=length;return value; @@ -6344,6 +6523,13 @@ void XE::wrapDisplay13FBMemoryInit(IOService *controller) { for(uint32_t i=0;valid && igetMetaClass()->getClassSize(),UINT32_MAX); + adlnFixedSysMemEnabled=valid && Display13FBTopology::multiOutputWiring(s->adlnOutputWiring) && + Display13FBMemory::sharedReservation(layout,Display13FBGTT::Aperture,true).valid; + IORegistryEntry::getRegistryRoot()->setProperty("N97FixedSysMemEnabled",adlnFixedSysMemEnabled); } else OSSafeReleaseNULL(map); if(!valid) {g.faulted=true;adlnFault(display13FBPower.memoryFaulted,__LINE__);} uint64_t record[] {1,valid,g.reads,g.faulted}; @@ -6395,6 +6581,17 @@ IOReturn XE::adlnSetupAuxiliaryMemory(IOService *controller,bool dsb) { IOReturn XE::wrapDisplay13CursorSetup(IOService *controller) {return adlnSetupAuxiliaryMemory(controller,false);} IOReturn XE::wrapAlderLakeNDSBSetup(IOService *controller) {return adlnSetupAuxiliaryMemory(controller,true);} + + + + + + + + + + + bool XE::processDisplay13FramebufferTopology(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { const char *os="__ZN31AppleIntelFramebufferController16getOSInformationEv"; const char *state="__ZN14AppleIntelPort14probePortStateEv"; @@ -6424,7 +6621,7 @@ bool XE::processDisplay13FramebufferTopology(KernelPatcher &patcher,size_t index bool XE::wrapAlderLakeNGetOSInformation(IOService *controller) { auto s=callbackXE; if(!s->display13FBTopologyHooksReady || !controller || controller->getMetaClass()->getClassSize()<0x1C20 || - !Display13FBTopology::supportedWiring(s->adlnOutputWiring,s->xeProfile.display==XELP::Display::XeLPD) || !checkKernelArgument("-disablegfxfirmware"))return false; + !Display13FBTopology::supportedWiring(s->adlnOutputWiring,s->xeProfile.display==XELP::Display::XeLPD,checkKernelArgument("-wex-dsi-adopt")) || !checkKernelArgument("-disablegfxfirmware"))return false; if(!FunctionCast(wrapAlderLakeNGetOSInformation,s->orgAlderLakeNGetOSInformation)(controller))return false; auto platform=Display13FBMemory::field(controller,0xD40); if(!Display13FBTopology::apply(s->adlnPlatform,sizeof(s->adlnPlatform),platform,s->adlnOutputWiring))return false; @@ -6452,6 +6649,30 @@ bool XE::wrapAlderLakeNGetOSInformation(IOService *controller) { return true; } + + + + + + + + + + + + + + + +uint32_t XE::wrapDisplay13ProbeMode(OSObject *port) { + auto self=callbackXE; + if(!port || port->getMetaClass()->getClassSize()<0x6B0)return 0; + auto config=Display13FBMemory::field(port,0x548); + if(self->adlnOutputWiring.dsi && config && config[0]==0) + return !display13Offline && __atomic_load_n(&self->dsiResourcesReady,__ATOMIC_ACQUIRE) && setDSIPortMode(port,1)==kIOReturnSuccess ? 1U : 0U; + return self->xelpProviderProbe ? reinterpret_cast(self->xelpProviderProbe)(port) : 0; +} + uint32_t XE::wrapAlderLakeNPortState(OSObject *port) { auto s=callbackXE; if(!s->display13FBTopologyHooksReady || !s->adlnTopologyController || !port || port->getMetaClass()->getClassSize()<0x580)return 0; @@ -6464,6 +6685,12 @@ uint32_t XE::wrapAlderLakeNPortState(OSObject *port) { IORegistryEntry::getRegistryRoot()->setProperty("WEXDisplay13OfflineProbe",true); return 0; } + if(s->adlnOutputWiring.dsi && config[0]==0) { + // DSI has neither HPD nor a DPCD sink. Only an adopted live source + // may report connected; never enter the donor's eDP probe branch. + const auto state=setDSIPortMode(port,1)==kIOReturnSuccess ? 2U : 0U; + *reinterpret_cast(reinterpret_cast(port)+0x554)=state;return state; + } // CPU binding only. TC bits are added when mode/AUX/PLL/GOP become // qualified together; a board description alone is insufficient. if(!s->xelpBindProbe(s->adlnTopologyController)) { @@ -6501,13 +6728,16 @@ unsigned XE::display13TCTarget(OSObject *port,bool cleanup) { return config[0]; } + uint32_t XE::wrapDisplay13TCMode(OSObject *port,uint32_t wanted) { auto self=callbackXE;auto &c=display13FBPower; auto config=port && port->getMetaClass()->getClassSize()>=0x580 ? Display13FBMemory::field(port,0x548) : nullptr; + if(self->adlnOutputWiring.dsi && config && config[0]==0)return setDSIPortMode(port,wanted); if(config && config[0]<3)return FunctionCast(wrapDisplay13TCMode,self->orgDisplay13TCMode)(port,wanted); if(!c.lock)return kIOReturnNotReady; IOLockLock(c.lock);const auto ddi=display13TCTarget(port,wanted==0); - if(ddi>=7 || (wanted!=0 && wanted!=4) || c.tcBusy[ddi-3] || + const auto selected=ddi<7 && (self->adlnOutputWiring.typeC&(1U<=7 || (wanted!=0 && wanted!=selected) || c.tcBusy[ddi-3] || (c.tcObject[ddi-3] && c.tcObject[ddi-3]!=port)) {IOLockUnlock(c.lock);return kIOReturnNotReady;} const auto tc=ddi-3;auto &mode=c.tcMode[tc]; if(!c.tcObject[tc]){c.tcObject[tc]=port;port->retain();} @@ -6539,13 +6769,7 @@ uint32_t XE::wrapDisplay13TCMode(OSObject *port,uint32_t wanted) { port==reinterpret_cast(object) && d==tc+3; } bool quiescent(uintptr_t controller,uintptr_t port,unsigned d) { - auto &c=display13FBPower; - if(controller!=reinterpret_cast(c.controller) || port!=reinterpret_cast(object) || - d!=tc+3 || c.tcBusy[tc] || c.bridge.nativeDDIRefs(d) || c.bridge.nativeAUXRefs(d) || - Display13FBMemory::field(object,0x570) || !Display13FBTCIO::quiescent(d))return false; - Display13TC::Port p;Display13TC::Port::resolve(d,p); - const auto pll=read(p.pll),aux=read(0x64310+0x100*tc); - return pll!=UINT32_MAX && aux!=UINT32_MAX && !(pll&0xc0000000) && !(aux&0x80000000); + return modeQuiescent(controller,port,d); } }; // Adopt only the validated, protected firmware boot route. Merely finding @@ -6560,15 +6784,16 @@ uint32_t XE::wrapDisplay13TCMode(OSObject *port,uint32_t wanted) { auto &source=c.displayScratch;AlderLakeN::readDisplay(bootIO,true,source); inherited=source.activePipes==1 && AlderLakeN::dpSourceUnchanged(bootIO,source,0,ddi,0,&boot); } - const auto preparation=framebufferLinkRequirements(port,4).preparation(inherited); + const auto preparation=framebufferLinkRequirements(port,selected).preparation(inherited); IO io{port,tc,reinterpret_cast(self->display13TCBaseMode),preparation}; const auto owner=reinterpret_cast(c.controller),object=reinterpret_cast(port); - const auto result=wanted?mode.connect(io,owner,object,ddi):mode.disconnect(io,owner,object,ddi); + const auto result=wanted?mode.connect(io,owner,object,ddi,wanted):mode.disconnect(io,owner,object,ddi); const bool done=result.status==Display13TC::Status::Complete; if(!wanted && done){c.tcSignalReady[tc]=false;c.tcDPLink[tc]={};OSSafeReleaseNULL(c.tcObject[tc]);} - uint32_t record[]={1,ddi,wanted,uint32_t(result.status),uint32_t(result.phy.status),uint32_t(result.power.status),uint32_t(mode.phase),mode.disconnectPending}; + uint32_t record[]={2,ddi,wanted,uint32_t(result.status),uint32_t(result.phy.status),uint32_t(result.power.status),uint32_t(mode.phase),mode.disconnectPending,mode.lanes.mask,mode.lanes.pin}; IOLockUnlock(c.lock);auto root=IORegistryEntry::getRegistryRoot(); root->setProperty("WEXDisplay13TCMode",record,sizeof(record)); + char modeKey[40];snprintf(modeKey,sizeof(modeKey),"WEXDisplay13TCMode%u",ddi);root->setProperty(modeKey,record,sizeof(record)); // Keep the initiating power-domain result; subsequent native getPG errors // can only observe the shared fault and cannot reconstruct this boundary. if(result.status==Display13TC::Status::NoPower) { @@ -6584,6 +6809,27 @@ uint32_t XE::wrapDisplay13TCMode(OSObject *port,uint32_t wanted) { return done ? kIOReturnSuccess : kIOReturnNotReady; } +void XE::drainDisplay13TCMode(unsigned ddi) { + auto &c=display13FBPower;auto self=callbackXE; + if(!c.lock || ddi<3 || ddi>6)return; + // This continuation runs after native path removal, under its workloop + // gate. Never race a new connect while dropping the owner lock for reset. + IOLockLock(c.lock);auto port=c.tcObject[ddi-3]; + const bool gated=c.workLoop && self->adlnWorkLoopInGate && + Display13FBMemory::field(c.controller,0xE00)==c.workLoop && + reinterpret_cast(self->adlnWorkLoopInGate)(c.workLoop); + const bool pending=port && c.tcMode[ddi-3].disconnectPending && !c.tcBusy[ddi-3]; + uint32_t record[]={1,ddi,pending,gated,c.bridge.nativeDDIRefs(ddi),c.bridge.nativeAUXRefs(ddi), + port?Display13FBMemory::field(port,0x570):0,0,0}; + if(pending && gated)port->retain(); + IOLockUnlock(c.lock); + if(pending && gated) { + record[7]=wrapDisplay13TCMode(port,0); + record[8]=Display13FBMemory::field(port,0x550);port->release(); + } + if(pending)IORegistryEntry::getRegistryRoot()->setProperty("WEXDisplay13TCDrain",record,sizeof(record)); +} + void XE::wrapDisplay13TCEnableClocks(void *controller,void *raw,void *other,uint32_t *params) { auto self=callbackXE;auto port=static_cast(raw);auto &c=display13FBPower; auto config=port && port->getMetaClass()->getClassSize()>=0x580 ? Display13FBMemory::field(port,0x548):nullptr; @@ -6607,7 +6853,9 @@ void XE::wrapDisplay13TCEnableClocks(void *controller,void *raw,void *other,uint ok=c.tcSignalReady[tc] && c.tcDPLink[tc].matches(io,p,plan,lanes); if(ok)stage=Display13TC::Clock::LinkStage::Complete; else { - ok=Display13TC::DPSignal::startLink(io,p,plan,lanes,&stage); + ok=Display13TC::DPSignal::startLink(io,p,plan,lanes,&stage, + c.tcMode[tc].selectedMode==3?c.tcMode[tc].lanes.pin:0, + c.tcMode[tc].selectedMode==3?c.tcMode[tc].lanes.mask:15); if(ok)c.tcDPLink[tc]={plan.rate,plan.reference,lanes}; } } else { @@ -6661,7 +6909,8 @@ uint32_t XE::wrapDisplay13TCSignal(void *raw,uint32_t swing,uint32_t emphasis,vo Display13FBTCIO io{port,ddi-3};ok=io.owned(ddi) && io.linkHeld() && c.tcSignalReady[ddi-3]; if(ok && (self->adlnOutputWiring.dp&(1U<(port,0x128),swing,emphasis); + ok=!drive && Display13TC::DPSignal::program(io,p,Display13FBMemory::field(port,0x128),swing,emphasis, + c.tcMode[ddi-3].selectedMode==3?c.tcMode[ddi-3].lanes.mask:15); } } if(!ok)adlnFault(c.clockFaulted,__LINE__);IOLockUnlock(c.lock); @@ -6683,8 +6932,10 @@ uint32_t XE::wrapDisplay13DPLaneMask(void *port) { return FunctionCast(wrapDisplay13DPLaneMask,s->orgDisplay13DPLaneMask)(port); auto &c=display13FBPower;if(!c.lock)return 0;IOLockLock(c.lock); const auto ddi=display13TCTarget(object);bool ok=false; - if(ddi>=3 && ddi<=6){Display13FBTCIO io{object,ddi-3};ok=io.owned(ddi);} - IOLockUnlock(c.lock);return ok?15U:0; + uint32_t mask=0; + if(ddi>=3 && ddi<=6){Display13FBTCIO io{object,ddi-3};ok=io.owned(ddi); + mask=c.tcMode[ddi-3].selectedMode==3?c.tcMode[ddi-3].lanes.mask:15U;} + IOLockUnlock(c.lock);return ok?mask:0; } void XE::wrapDisplay13DPPhyGating(void *port,bool gate) { auto s=callbackXE;auto object=static_cast(port); @@ -6696,10 +6947,12 @@ void XE::wrapDisplay13DPPhyGating(void *port,bool gate) { const auto ddi=display13TCTarget(object);bool ok=false; if(ddi>=3 && ddi<=6) { Display13FBTCIO io{object,ddi-3};Display13TC::Port p;Display13TC::Port::resolve(ddi,p); - ok=io.owned(ddi) && io.linkHeld(); + ok=(io.owned(ddi) || (gate && io.retainedOwned(ddi) && io.quiescent(ddi))) && io.linkHeld(); // Same fixed-DP contract as TGL: Linux leaves DP_MODE bits5:1 // intact during training. Do not run the donor's clear operation. if(ok && gate)for(unsigned group=0;group<2;++group) { + const auto &mode=c.tcMode[ddi-3]; + if(mode.selectedMode==3 && !(mode.lanes.mask&(3U<<(2*group))))continue; uint32_t value=0x3e;ok=Display13TC::PHY::access(io,p,0x1000*group+0xa0,0x3e,value,true) && ok; } } @@ -6713,8 +6966,10 @@ void XE::wrapDisplay13DPPortWrite(void *port,uint32_t reg,uint32_t value) { IOLockLock(c.lock);const auto ddi=display13TCTarget(object); if(ddi>=3 && ddi<=6 && (s->adlnOutputWiring.dp&(1U<(port,0x128); - if(io.owned(ddi) && io.linkHeld() && (rate==6 || rate==10 || rate==20 || rate==30)) - value=(value&~0xff00U)|(((rate*27+9)/10)<<8); + Display13TC::Port p;uint32_t before[2]{},after[2]{}; + const bool ready=io.owned(ddi) && io.linkHeld() && Display13TC::DPSignal::bufferRate(rate,value) && + Display13TC::Port::resolve(ddi,p) && Display13TC::DPSignal::prepareBuffer(io,p,before,after); + if(ready)value=(value&~0xff00U)|(((rate*27+9)/10)<<8); else {adlnFault(c.clockFaulted,__LINE__);IOLockUnlock(c.lock);return;} } IOLockUnlock(c.lock); @@ -6784,6 +7039,15 @@ uint32_t XE::wrapDisplay13TCLightUp(void *controller,void *rawFB,void *rawPath,c uint32_t XE::wrapDisplay13TCSetMode(void *controller,void *fb,const void *timing) { auto self=callbackXE; + // The GOP-preserving surface adapter must replace this refusal before + // startup admission can open. The original sends a begin notification and + // writes ordinary clock/timing state before reaching LightUpEDP. + if(self->adlnOutputWiring.dsi) { + auto service=static_cast(fb); + const auto path=service && service->getMetaClass()->getClassSize()>=0x4B90 ? + Display13FBMemory::singlePath(service) : nullptr; + if(dsiLifecycleTarget(service,path))return kIOReturnNotReady; + } return Display13TC::finishMode( [&](){return FunctionCast(wrapDisplay13TCSetMode,self->orgDisplay13TCSetMode)(controller,fb,timing);}, [&](){reinterpret_cast(self->display13TCNotify)(controller,fb,1);}); @@ -6800,6 +7064,8 @@ bool XE::processDisplay13FramebufferTC(KernelPatcher &patcher,size_t index,mach_ } struct Span {uint32_t offset;const uint8_t *bytes;size_t size;}; const Span spans[]={ + {baseModeBranchOffset,baseModeBranch,sizeof(baseModeBranch)}, + {baseModeStoreOffset,baseModeStore,sizeof(baseModeStore)}, {bootPortInitOffset,bootPortInit,sizeof(bootPortInit)}, {bootPowerCallOffset,bootPowerCall,sizeof(bootPowerCall)}, {bootSetupCallOffset,bootSetupCall,sizeof(bootSetupCall)}, @@ -6984,7 +7250,7 @@ bool XE::processDisplay13FramebufferAUX(KernelPatcher &patcher,size_t index,mach // Caller holds the display owner lock. Probe may run before setPortMode, // hence mode0 is allowed; a DDI/main-link lease is deliberately not required. -bool XE::display13AUXReady(void *port,bool command) { +bool XE::display13AUXReady(void *port,bool command,bool retained) { auto s=callbackXE;auto &c=display13FBPower;auto object=static_cast(port); if(!s->display13FramebufferRequested || s->xeProfile.display!=XELP::Display::XeLPD || !s->display13FBAUXHooksReady || !object || object->getMetaClass()->getClassSize()<0x5AC || !c.controller || @@ -6998,6 +7264,7 @@ bool XE::display13AUXReady(void *port,bool command) { !Display13FBMemory::field(c.controller,0xC30))return false; auto p=static_cast(port);auto config=Display13FBMemory::field(p,0x548); if(!Display13FBTopology::selected(s->adlnPlatform,config,s->adlnOutputWiring) || config[0]>=7 || config[0]==2 || + (s->adlnOutputWiring.dsi & (1U<(p,0x120)!=config[0] || (config[0]<3 && Display13FBMemory::field(p,0x550)>1) || Display13FBMemory::field(p,0x578)!=c.access || @@ -7005,8 +7272,11 @@ bool XE::display13AUXReady(void *port,bool command) { Display13FBMemory::field(c.power,0x78)!=c.access)return false; if(config[0]>=3) { Display13FBTCIO tc{object,config[0]-3}; - if(!(s->adlnOutputWiring.dp&(1U<adlnOutputWiring.dp&(1U<(p,0x594+4*n)!=ctl+4*n)return false; @@ -7045,8 +7315,11 @@ uint32_t XE::wrapDisplay13RunAUX(void *port,uint32_t command,uint32_t *packet,ui IOLockUnlock(c.lock);if(!ready)return kIOReturnNotReady; const auto r=FunctionCast(wrapDisplay13RunAUX,callbackXE->orgDisplay13RunAUX)(port,command,packet,length,flag); IOLockLock(c.lock);Display13FBPowerIO io {c.map};const bool held=c.bridge.endAUXCommand(io,ddi); - const bool complete=display13AUXReady(port,false) && held; - if(!complete)adlnFault(c.bridge.state.faulted,__LINE__); + // HPD/assignment can disappear while the donor waits for the AUX reply. + // Reject that reply, but only lost identity/power/leases poison the owner. + const bool protectedCompletion=held && display13AUXReady(port,false,true); + const bool complete=protectedCompletion && display13AUXReady(port,false); + if(!protectedCompletion)adlnFault(c.bridge.state.faulted,__LINE__); IOLockUnlock(c.lock);return r ? r : complete ? 0 : kIOReturnNotReady; } @@ -7280,7 +7553,7 @@ uint8_t *XE::display13SSTPort(void *port) { auto p=static_cast(port);auto config=Display13FBMemory::field(p,0x548); if(!Display13FBTopology::selected(s->adlnPlatform,config,s->adlnOutputWiring) || !(s->adlnOutputWiring.links()&(1U<(p,0x120)!=config[0] || Display13FBMemory::field(p,0x550)!=(config[0]>=3?4U:1U) || + Display13FBMemory::field(p,0x120)!=config[0] || Display13FBMemory::field(p,0x550)!=(config[0]>=3?((s->adlnOutputWiring.typeC&(1U<(p,0x578)!=c.access)return nullptr; return p; } @@ -7292,7 +7565,7 @@ bool XE::display13HDMIPort(void *port) { return Display13FBTopology::selected(s->adlnPlatform,config,s->adlnOutputWiring) && s->adlnOutputWiring.matchesHDMI(config); } -bool XE::display13MainLinkAUX(void *port,bool acquire) { +bool XE::display13MainLinkAUX(void *port,bool acquire,bool stopping) { auto &c=display13FBPower;if(!c.lock)return false; OSObject *retired=nullptr;bool ok=false;IOLockLock(c.lock); auto p=display13SSTPort(port); @@ -7303,7 +7576,8 @@ bool XE::display13MainLinkAUX(void *port,bool acquire) { if(ddi>=3 && ddi<=6) { // Mode owns AUX from before first DPCD through buffer/PLL stop. Do not // add the Combo main-link lease or release mode power at buffer-disable. - Display13FBTCIO tc{static_cast(port),ddi-3};ok=tc.owned(ddi) && tc.linkHeld(); + Display13FBTCIO tc{static_cast(port),ddi-3}; + ok=(acquire && !stopping ? tc.owned(ddi) : tc.retainedOwned(ddi)) && tc.linkHeld(); } if(ddi<2 && Display13FBMemory::field(p,0x590)==0x64000+0x100*ddi) { auto &link=c.mainLink[ddi];const auto previous=link.owner;Display13FBPowerIO io{c.map}; @@ -7333,10 +7607,10 @@ uint32_t XE::wrapDisplay13DisablePort(void *port) { auto s=callbackXE; if(!s->adlnOutputWiring.links() || display13HDMIPort(port)) return FunctionCast(wrapDisplay13DisablePort,s->orgDisplay13DisablePort)(port); - if(!display13MainLinkAUX(port,true))return kIOReturnNotReady; + if(!display13MainLinkAUX(port,true,true))return kIOReturnNotReady; const auto result=FunctionCast(wrapDisplay13DisablePort,s->orgDisplay13DisablePort)(port); if(!result && s->display13ClockOrderReady && !display13StopTranscoder(port))return kIOReturnNotReady; - const bool released=display13MainLinkAUX(port,false); + const bool released=display13MainLinkAUX(port,false,true); return result ? result : released ? 0 : kIOReturnNotReady; } @@ -7834,11 +8108,65 @@ bool XE::processFramebufferAGDC(KernelPatcher &patcher,size_t index,mach_vm_addr {client,wrapFramebufferUserClient,orgFramebufferUserClient}, {callback,wrapFramebufferAGDCCallback,orgFramebufferAGDCCallback} }; - const bool ready=patcher.routeMultipleShort(index,routes,arrsize(routes),address,size);patcher.clearError(); + bool ready=patcher.routeMultipleShort(index,routes,arrsize(routes),address,size);patcher.clearError(); + if(ready)ready=processDisplay13TrialAGDC(patcher,index,address,size); IORegistryEntry::getRegistryRoot()->setProperty("WEXDisplay13AGDCGuardReady",ready); return ready; } +// This belongs to the opt-in DSI bootstrap experiment, not the USB-C backend. +// A disconnected temporary boot port needs an explicit native retirement when +// policy first replaces it. Real DSI support can remove this experiment whole. +bool XE::processDisplay13TrialAGDC(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { + if(!display13FramebufferRequested || !checkKernelArgument("-wex-dsi-hdmi-test"))return true; + const char *symbol="__ZN31AppleIntelFramebufferController30commitResourceConfigurationSetEP30AGDCResourceConfigurationSet_tS1_"; + const auto fn=patcher.solveSymbol(index,symbol,address,size);patcher.clearError(); + const uint8_t entry[]={0x55,0x48,0x89,0xe5,0x53,0x50}; + if(size<=0x670B9 || fn!=address+0x66F6E || memcmp(reinterpret_cast(fn),entry,sizeof(entry)))return false; + KernelPatcher::RouteRequest route(symbol,wrapDisplay13TrialCommit,orgDisplay13TrialCommit); + const bool ready=patcher.routeMultipleShort(index,&route,1,address,size);patcher.clearError();return ready; +} + +uint32_t XE::wrapDisplay13TrialCommit(void *controller,void *request,void *output) { + auto req=static_cast(request); + const auto count=req ? req[0]:0; + const auto index=req && output && count && count<=16 ? req[4]:UINT32_MAX; + auto fb=tglResourceFB(controller,index); + // Opt-in DSI handoff trial only: an unplugged temporary boot connector + // never becomes online, so policy does not retire its boot allocation. + // Use the normal stop/free path before committing a different live port. + if(callbackXE->display13FramebufferRequested && checkKernelArgument("-wex-dsi-hdmi-test") && + fb && count==1 && display13RetirementHealthy() && + !Display13FBMemory::field(fb,0x1E0) && + Display13FBMemory::field(fb,0x44DE) && + Display13FBMemory::field(fb,0x1E7) && + Display13FBMemory::field(fb,0x49E8)==1 && + Display13FBMemory::field(fb,0x4A00)==1) { + auto path=Display13FBMemory::singlePath(fb); + const auto binding=adlnInspectPathBinding(static_cast(fb),path, + callbackXE->adlnPlatform,callbackXE->adlnOutputWiring); + const auto target=req[0x48/4]; + auto port=path ? Display13FBMemory::field(path,0x3648) : nullptr; + auto next=target && target<=9 && callbackXE->tglPortByDDI ? + reinterpret_cast(callbackXE->tglPortByDDI)(target-1) : nullptr; + if(binding.status==TGLFBBinding::Status::Complete && port && next && + next!=port && next->getMetaClass()->getClassSize()>=0x580 && + Display13FBTopology::selected(callbackXE->adlnPlatform, + Display13FBMemory::field(next,0x548),callbackXE->adlnOutputWiring) && + Display13FBMemory::field(next,0x120)==target-1 && + Display13FBMemory::field(path,0x3644)>=1 && + Display13FBMemory::field(path,0x3644)<=2 && + Display13FBMemory::field(port,0x554)<2 && + Display13FBMemory::field(next,0x554)>1) { + const auto retired=wrapDisplay13FreeResources(fb); + uint32_t record[]={1,binding.ddi,target-1,retired}; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDisplay13TrialBootRetire",record,sizeof(record)); + if(retired)return retired; + } + } + return FunctionCast(wrapDisplay13TrialCommit,callbackXE->orgDisplay13TrialCommit)(controller,request,output); +} + uint32_t XE::wrapFramebufferUserClient(IOService *fb,void *task,void *security,uint32_t type,void **client) { FramebufferDonor::Call admission(callbackXE->framebufferProtocolCalls); if(!admission){if(client)*client=nullptr;return kIOReturnNotReady;} @@ -7935,6 +8263,13 @@ bool XE::prepareDisplay13BootScanout(IOPCIDevice *pci) { void delay(){IODelay(100);} } io{reinterpret_cast(map->getVirtualAddress()),map->getLength()}; const bool mapped=io.base && io.bytes==0x1000000; + if(adlnOutputWiring.dsi) { + // Preserve the DSI GOP exactly. Native power/path adoption follows; + // normal-plane reinitialization would destroy the inherited source. + const bool ready=mapped && dsiBoot.complete() && XELP::DSI::verifyBootPrefix(io,dsiBoot,xelpMemory); + pci->setProperty("WEXDSIBootHandoff",&dsiBoot,sizeof(dsiBoot)); + map->release();return ready; + } const auto boot=AlderLakeN::readDisplay(io,mapped); uint32_t trans=UINT32_MAX;AlderLakeN::value(boot,0x60400,trans); const auto ddi=Display13FBTopology::bootstrapDDI(adlnOutputWiring, @@ -7957,8 +8292,17 @@ bool XE::wrapDisplay13FramebufferStart(IOService *that,IOService *provider) { if(s->tglObservationController==that)s->tglObservationController=nullptr; if(!pci)return false; const auto board=s->display13BoardWiring(provider); - if(!Display13FBTopology::supportedWiring(board,s->xeProfile.display==XELP::Display::XeLPD) || + if(!Display13FBTopology::supportedWiring(board,s->xeProfile.display==XELP::Display::XeLPD,checkKernelArgument("-wex-dsi-adopt")) || board.selected()!=s->adlnOutputWiring.selected())return false; + if(board.dsi) { + const bool disposal=s->dsiFreePlane && s->dsiFreeScaler && s->dsiDetachPath && s->dsiRemovePath && s->dsiPipeMap; + const bool ready=s->display13DSIModeHooksReady && s->dsiNativeWriterHooksReady && + s->dsiBootProbeReady && s->dsiCacheHooksReady && disposal; + uint64_t record[] {1,ready,s->display13DSIModeHooksReady,s->dsiNativeWriterHooksReady, + s->dsiBootProbeReady,s->dsiCacheHooksReady,disposal}; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIStartupHooks",record,sizeof(record)); + if(!ready)return false; + } if((board.selected()&0x78) && (!s->display13FBTCHooksReady || !s->display13TCProbeReady)) { IORegistryEntry::getRegistryRoot()->setProperty("ADLPFBTCUnavailable",true);return false; } @@ -8025,6 +8369,7 @@ bool XE::wrapDisplay13FramebufferStart(IOService *that,IOService *provider) { return accepted; } + void XE::wrapAlderLakeNSetupWorkLoop(IOService *controller,IOWorkLoop *workLoop) { FunctionCast(wrapAlderLakeNSetupWorkLoop,callbackXE->orgAlderLakeNSetupWorkLoop)(controller,workLoop); auto &c=display13FBPower;if(!c.lock)return; @@ -8126,7 +8471,12 @@ void XE::wrapAlderLakeNSetupWorkLoop(IOService *controller,IOWorkLoop *workLoop) valid=enabled && !c.bridge.state.faulted && !c.memoryFaulted && !c.clockFaulted; } check[37]=valid; - if(valid) { + if(valid && callbackXE->adlnOutputWiring.dsi) { + IOLockUnlock(c.lock); + const bool adopted=callbackXE->adoptDSIBootPath(fb,path); + IOLockLock(c.lock); + result.status=adopted ? Display13Power::Status::Complete : Display13Power::Status::NotReady; + } else if(valid) { if(!c.bridge.bootAdopted) { const auto ddi=c.bridge.bootPort; const bool dp=ddi<7 && (callbackXE->adlnOutputWiring.links()&(1U<(fb),callbackXE->adlnPlatform,callbackXE->adlnOutputWiring); pipe=binding.pipe; } + if(dsiPendingTarget(fb) || dsiSharedWriterStopped(fb,pipe)) {IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex);return false;} record[12]=pipe;record[13]=static_cast(binding.status);record[14]=dual; if(pipe<3 && !c.pipe[pipe].destroyingPath && (!dual || binding.status==TGLFBBinding::Status::Complete) && (c.bridge.bootAdopted || !pipe) && callbackXE->display13FBMemoryHooksReady && controller && c.bridge.begun && @@ -8241,6 +8592,7 @@ void XE::wrapAlderLakeNScanoutSetup(IOService *fb) { IOWorkLoop *acquiredSwapGate=nullptr;OSObject *stopPath=nullptr;bool stopSwap=false; IOLockLock(c.lock); pipe=Display13FBMemory::boundPipe(c.pipe,fb);record[18]=pipe; + if(dsiPendingTarget(fb) || dsiSharedWriterStopped(fb,pipe)) {IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex);return;} const auto binding=adlnInspectPathBinding(fb,Display13FBMemory::singlePath(fb),callbackXE->adlnPlatform,callbackXE->adlnOutputWiring); record[19]=static_cast(binding.status); if(callbackXE->display13FBMemoryHooksReady && pipe<3 && !c.pipe[pipe].destroyingPath && !c.memoryFaulted && @@ -8264,7 +8616,7 @@ void XE::wrapAlderLakeNScanoutSetup(IOService *fb) { swap[5]=transition.aperture;swap[6]=transition.replace;swap[7]=stolen; if(transition.replace && !stolen) { Display13FBIRQIO io {c.map->getVirtualAddress()}; - const bool stopped=Display13FBIRQ::consumersStopped(io,1U<(fb); const bool bound=c.workLoop && Display13FBMemory::field(controller,0xE00)==c.workLoop; @@ -8300,7 +8652,7 @@ void XE::wrapAlderLakeNScanoutSetup(IOService *fb) { IOLockLock(c.lock); const auto current=adlnInspectPathBinding(fb,Display13FBMemory::singlePath(fb),callbackXE->adlnPlatform,callbackXE->adlnOutputWiring); swap[15]=static_cast(current.status); - Display13FBIRQIO io {c.map->getVirtualAddress()};swap[16]=Display13FBIRQ::consumersStopped(io,1U<getVirtualAddress()};swap[16]=Display13FBIRQ::consumersStopped(io,1U<(fb)==stopPath && swap[16] && !c.bridge.state.faulted && !c.clockFaulted && !c.memoryFaulted; @@ -8346,6 +8698,7 @@ void XE::wrapAlderLakeNScanoutFree(IOService *fb) { auto &c=display13FBPower;if(!c.lock || !fb || fb->getMetaClass()->getClassSize()<0x4B90)return; auto mutex=Display13FBMemory::field(fb,0x4220);if(!mutex)return; IORecursiveLockLock(mutex); + if(dsiPendingTarget(fb)){IORecursiveLockUnlock(mutex);return;} uint64_t record[30] {},handoff[8] {1};bool freed=false;IOLockLock(c.lock); const auto pool=c.controller ? Display13FBMemory::pool(c.controller) : Display13FBMemory::Pool {}; const bool associated=fb==c.pipe[0].memoryFramebuffer && display13FBAssociations(c.controller,0x1C20,fb,fb->getMetaClass()->getClassSize(),display13FBExpectedOwner(c.controller)); @@ -8422,7 +8775,9 @@ bool XE::adlnFreeStoppedPipe(IOService *fb) { auto &c=display13FBPower;auto &irq=display13FBIRQ; if(!c.lock || !display13FBSysMem.retireLock || !fb || fb->getMetaClass()->getClassSize()<0x4B90)return false; auto mutex=Display13FBMemory::field(fb,0x4220);if(!mutex)return false; - IORecursiveLockLock(mutex);IORecursiveLockLock(display13FBSysMem.retireLock);IOLockLock(c.lock); + IORecursiveLockLock(mutex); + if(dsiPendingTarget(fb)){IORecursiveLockUnlock(mutex);return false;} + IORecursiveLockLock(display13FBSysMem.retireLock);IOLockLock(c.lock); const auto pipe=Display13FBMemory::boundPipe(c.pipe,fb); uint64_t record[24] {1,pipe};bool freed=false,held=false;uint32_t master=0,blocked=0;bool enabled=false; auto controller=static_cast(c.controller); @@ -8522,8 +8877,10 @@ bool XE::adlnFreeStoppedPipe(IOService *fb) { return freed; } + + uint32_t XE::releaseDisplay13Path(IOService *fb,OSObject *path) { - if(!Display13FBTopology::multiOutputWiring(callbackXE->adlnOutputWiring)) { + if(!Display13FBTopology::multiOutputWiring(callbackXE->adlnOutputWiring) && !callbackXE->adlnOutputWiring.dsi) { FunctionCast(wrapAlderLakeNFreePath,callbackXE->orgAlderLakeNFreePath)(fb,path);return 0; } auto &c=display13FBPower; @@ -8562,6 +8919,15 @@ uint32_t XE::releaseDisplay13Path(IOService *fb,OSObject *path) { adlnFault(c.memoryFaulted,__LINE__);IOLockUnlock(c.lock); IORecursiveLockUnlock(mutex);path->release();fb->release();return kIOReturnNotReady; } + // DSI has no audio link or normal-DDI port reset. Dispose only its proven + // stopped native CPU associations and PG lease, never the full donor body. + if(callbackXE->adlnOutputWiring.dsi && dsiLifecycleTarget(fb,path)) { + if(!selected || !callbackXE->disposeDSIPath(fb,path)) { + IOLockLock(c.lock);if(pipe<3)c.pipe[pipe].destroyingPath=false; + adlnFault(c.memoryFaulted,__LINE__);IOLockUnlock(c.lock); + IORecursiveLockUnlock(mutex);path->release();fb->release();return kIOReturnNotReady; + } + } else FunctionCast(wrapAlderLakeNFreePath,callbackXE->orgAlderLakeNFreePath)(fb,path); IOLockLock(c.lock); record[6]=countOffset ? Display13FBMemory::field(fb,countOffset) : UINT32_MAX; @@ -8580,6 +8946,7 @@ uint32_t XE::releaseDisplay13Path(IOService *fb,OSObject *path) { record[7]=released;record[8]=detached;record[9]=removed;record[10]=withdrawn!=nullptr; record[11]=c.memoryFaulted;record[12]=c.dbuf.selected;record[13]=c.dbuf.consumers;record[14]=c.dbuf.unretired;record[15]=countOffset; IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex); + if(removed)drainDisplay13TCMode(binding.ddi); OSSafeReleaseNULL(withdrawn);path->release();fb->release(); adlnPublishMemory("N97FBPathFree",pipe,record,sizeof(record)); return selected && !removed ? kIOReturnNotReady : 0; @@ -8610,6 +8977,15 @@ IOMemoryDescriptor *XE::wrapAlderLakeNAperture(IOService *fb,int apertureIndex) const auto layout=Display13FBMemory::poolLayout(c.controller,0x1C20,0x10000000); backing=adlnInspectSysMem(static_cast(c.controller),fb,slot,layout).status==Display13FBSysMem::Status::Complete; } + // IOGraphics may ask before the first DSI mode transaction has set + // dimensions, including while P2's private identification backing is + // armed. The validated GOP pool is still owned, but no client aperture + // was published. Return unavailable without poisoning that shared owner. + // Nonzero invalid ranges and lost backing retain the fault path below. + if(!base && !bytes && parent && backing && !slot && callbackXE->adlnOutputWiring.dsi==1 && + dsiModeTarget(fb) && !callbackXE->dsiStopAttempted) { + IOLockUnlock(c.lock);return nullptr; + } if(!parent || !backing || !Display13FBMemory::apertureRange(c.pipe[pipe].memory.pool,base,bytes,parent->getLength()))parent=nullptr; if(parent)parent->retain();else { @@ -8958,12 +9334,13 @@ void XE::adlnWMSetup(OSObject *plane,OSObject *path,bool cursor) { {fb,fb ? fb->getMetaClass()->getClassSize() : 0}, {path,path ? path->getMetaClass()->getClassSize() : 0}, {plane,plane ? plane->getMetaClass()->getClassSize() : 0}, - {port,port ? port->getMetaClass()->getClassSize() : 0},display13FBExpectedOwner(controller) + {port,port ? port->getMetaClass()->getClassSize() : 0},display13FBExpectedOwner(controller),dsiLogicalSurface(fb) }; const auto binding=TGLFBBinding::inspect(objects,callbackXE->adlnPlatform,callbackXE->adlnOutputWiring); uint64_t bindingRecord[] {1,static_cast(binding.status),binding.framebuffer,binding.pipe,binding.ddi,binding.plane, binding.status==TGLFBBinding::Status::Complete ? Display13FBMemory::allocatedSlot(controller,fb) : UINT32_MAX}; const auto pipe=binding.status==TGLFBBinding::Status::Complete ? binding.pipe : UINT32_MAX; + if(dsiSharedWriterStopped(fb,pipe)) {IOLockUnlock(c.lock);return;} const auto bank=pipe<3 ? 0x1000*pipe : 0U; // Use the real path's pipe and a unique published memory owner. Neither // framebuffer index, physical DDI nor aperture slot selects a WM bank. @@ -8995,8 +9372,12 @@ void XE::adlnWMSetup(OSObject *plane,OSObject *path,bool cursor) { inputs[14]=wells;inputs[15]=rangeRaw;inputs[16]=ctl;inputs[17]=ipc;inputs[18]=cursorMode; inputs[19]=width;inputs[20]=htotal;inputs[21]=rgbSupported;inputs[22]=powered; inputs[23]=Display13FBMemory::field(plane,0x10C); + // Native cursor changes prepare watermarks for FB +0x4278 before + // writing the new CURCNTR. The live register can still describe the + // previous size here. Validate the requested size now; latch() checks + // the applied control against the prepared plan after the update. if(powered && rangeRaw!=UINT32_MAX && Display13DBUF::valid(range) && rgbSupported && ipc!=UINT32_MAX && - (!cursor || Display13WM::cursorMatches(cursorMode,width,true)) && + (!cursor || Display13WM::cursorWidthSupported(width)) && !(Display13DBUF::slices(range)&~Display13DBUF::allowed(c.dbuf,pipe)) && (cursor || !Display13DBUF::overlap(range,c.dbuf.pipe[pipe].cursor))) { // Audited native pixel/scaling calculation is CPU-only. Preserve its @@ -9055,6 +9436,7 @@ void XE::wrapDisplay13CursorWMSetup(OSObject *plane,OSObject *path) {adlnWMSetup void XE::wrapAlderLakeNPipeWMSetup(IOService *controller,IOService *fb,OSObject *path,void *params) { auto &c=display13FBPower;if(!c.lock)return; uint64_t record[8] {};IOLockLock(c.lock); + if(dsiSharedWriterStopped(fb,Display13FBMemory::boundPipe(c.pipe,fb))) {IOLockUnlock(c.lock);return;} const bool associated=callbackXE->display13FBWatermarkHooksReady && controller==c.controller && fb && path && controller->getMetaClass()->getClassSize()>=0x1C20 && fb->getMetaClass()->getClassSize()>=0x4B90 && path->getMetaClass()->getClassSize()>=0x3730 && Display13FBMemory::field(fb,0x1D0)==controller && @@ -9099,7 +9481,17 @@ static bool adlnWriterMappingBound() { native->getPhysicalAddress(),native->getLength(),c.map->getPhysicalAddress(),c.map->getLength()); } + + void XE::wrapAlderLakeNDisableDisplay(IOService *controller,IOService *fb,OSObject *path,uint8_t mode) { + if(callbackXE->adlnOutputWiring.dsi && dsiLifecycleTarget(fb,path)) { + const bool stopped=callbackXE->quiesceDSIPath(controller,fb,path); + // Failed stops retain resources. Successful GOP-only stops can now + // reach the qualified PG/path/timing disposal at the native free entry. + auto &c=display13FBPower; + if(!stopped && c.lock){IOLockLock(c.lock);adlnFault(c.memoryFaulted,__LINE__);IOLockUnlock(c.lock);} + return; + } auto &c=display13FBPower;Display13Cursor::Result result;bool selected=false;unsigned pipe=UINT32_MAX; if(c.lock && controller==c.controller && fb && fb->getMetaClass()->getClassSize()>=0x4B90 && path && path->getMetaClass()->getClassSize()>=0x3730) { IOLockLock(c.lock); @@ -9117,7 +9509,7 @@ void XE::wrapAlderLakeNDisableDisplay(IOService *controller,IOService *fb,OSObje void write(uint32_t r,uint32_t v) {if(r==0x70080+bank || r==0x70088+bank || r==0x70084+bank)*reinterpret_cast(map->getVirtualAddress()+r)=v;} void delayUs(unsigned us) {IOSleep(us/1000);} } io {c.map,pipe*0x1000}; - result=Display13Cursor::stop(io,pipe); + result=Display13Cursor::stop(io,pipe,c.transcoders); if(result.status==Display13Cursor::Status::Complete) { // Keep the donor's visible/control cache consistent with the stopped cursor. *reinterpret_cast(reinterpret_cast(path)+0x170)=0; @@ -9147,6 +9539,7 @@ void XE::adlnWMEnable(OSObject *plane,bool arm,bool cursor) { uint64_t record[16] {},inputs[24] {};Display13WM::Program r;uint32_t surface=0,queueIndex=0;bool queued=false; IOLockLock(c.lock); const auto pipe=Display13FBMemory::boundPipe(c.pipe,fb); + if(dsiSharedWriterStopped(fb,pipe)) {IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex);return;} if(pipe>=3) {IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex);return;} const auto bank=0x1000*pipe; const auto &p=c.pipe[pipe].wm[cursor ? 1 : 0]; @@ -9258,7 +9651,7 @@ Display13WM::Latch XE::adlnVerifyWMLatch(IOService *fb,uint64_t nativeHz,uint64_ result.status=Display13WM::Status::Conflict;return result; } const bool cursor=readout.read(0x70080+bank)&63; - const auto conf=readout.read(0x70008+bank); + const auto conf=Display13Transcoder::control(readout,pipe,c.transcoders); if(conf==UINT32_MAX) {result.status=Display13WM::Status::Readback;return result;} // Cursor setup may follow primary arm. Keep old slices and pending bits // until every active consumer has actually been programmed. @@ -9280,7 +9673,7 @@ Display13WM::Latch XE::adlnVerifyWMLatch(IOService *fb,uint64_t nativeHz,uint64_ const Display13WM::Snapshot expected { &c.pipe[pipe].wm[0],&c.pipe[pipe].wm[1],primaryRaw,cursorRaw, c.pipe[pipe].wmSurface[0],c.pipe[pipe].wmSurface[1],readout.read(0x45004),nativeHz,c.pipe[pipe].wm0Only[0],cursor, - Display13WM::pipeLineTime(c.pipe[pipe].wm[0].htotal,pipePixel),pipePixel,pipe + Display13WM::pipeLineTime(c.pipe[pipe].wm[0].htotal,pipePixel),pipePixel,pipe,c.transcoders }; if(!Display13WM::clockFits(expected,targetHz)) {result.status=Display13WM::Status::Input;return result;} return Display13WM::latch(io,expected); @@ -9337,6 +9730,22 @@ static bool adlnObserveScanoutPixels(unsigned pipe,uint64_t (&q)[88]) { } void XE::wrapDisplay13WMArmUpdate(IOService *fb) { + if(!callbackXE->adlnOutputWiring.dsi){display13WMArmUpdate(fb);return;} + if(!fb || fb->getMetaClass()->getClassSize()<0x4B90)return; + { + DSIModeScope scope(fb);if(!scope.lock)return; + if(dsiLifecycleTarget(fb,Display13FBMemory::singlePath(fb))) { + // armUpdate flushes native packets even without pending WM work. + // Keep FB serialization from admission through flush/sendPush. + if(dsiNativeUpdateAllowed(fb))display13WMArmUpdate(fb); + return; + } + } + // Preserve the original external body's locking and callback boundaries. + display13WMArmUpdate(fb); +} + +void XE::display13WMArmUpdate(IOService *fb) { auto &c=display13FBPower;bool owner=false; const auto pipe=Display13FBMemory::boundPipe(c.pipe,fb); Display13WM::TriggerSurfaces intent;OSObject *path=nullptr;uint32_t packetCount=0; @@ -9412,7 +9821,7 @@ void XE::wrapDisplay13WMArmUpdate(IOService *fb) { if(observations[pipe]<32 && c.engineEnabled && c.map && adlnWriterMappingBound()) { Display13FBDBUFIO io {c.map};const auto wells=io.read(0x45404); if(Display13FBIRQ::powered(wells,pipe)) { - const uint32_t regs[] {0x70180,0x701CC,0x70188,0x70190,0x7019C,0x701AC, + static constexpr uint32_t regs[] {0x70180,0x701CC,0x70188,0x70190,0x7019C,0x701AC, 0x701A4,0x70008,0x70040,0x4A480,0x49028,0x49010,0x49014, 0x49018,0x4901C,0x49020,0x49024,0x49050,0x49054,0x49058, 0x4905C,0x49060,0x49064,0x70080,0x70084,0x700AC, @@ -9473,6 +9882,43 @@ void XE::wrapDisplay13WMArmUpdate(IOService *fb) { result.pipeControl,result.planeControl,result.cursorControl,result.checkRegister,result.checkValue, result.clockHz,actualHz,result.frameBefore,result.frameAfter,result.lastReg,result.expected,result.observed}; adlnPublishMemory("N97FBWMLatchFailure",pipe,proof,sizeof(proof),true); + // Retain the software source/destination geometry with the first + // failed latch. OS orientation is independent of DSI wire timing; + // distinguish a native scaling request from a link/stride mismatch. + auto &geometry=c.wmGeometryScratch;bzero(geometry,sizeof(geometry)); + geometry[0]=3;geometry[1]=pipe;geometry[2]=c.pipe[pipe].wm[0].width;geometry[3]=c.pipe[pipe].wm[0].height; + geometry[4]=c.pipe[pipe].wm[0].htotal;geometry[5]=c.pipe[pipe].wm[0].pixelHz;geometry[6]=actualHz; + static constexpr unsigned fbOffsets[]={0x430C,0x4310,0x4314,0x4320,0x4324,0x4328,0x432C,0x4334,0x4338}; + for(unsigned i=0;i(fb,fbOffsets[i]); + if(path && path->getMetaClass()->getClassSize()>=0x3730) { + static constexpr unsigned pathOffsets[]={0xF8,0xFC,0x108,0x10C,0x138,0x144}; + for(unsigned i=0;i(path,pathOffsets[i]); + auto plane=Display13FBMemory::field(path,0x32C8); + auto scaler=Display13FBMemory::field(path,0x32C0); + if(plane && plane->getMetaClass()->getClassSize()>=0x1C8) { + static constexpr unsigned planeOffsets[]={0x100,0x104,0x108,0x10C,0x110,0x114,0x118,0x11C,0x120,0x124}; + for(unsigned i=0;i(plane,planeOffsets[i]); + auto planeScaler=Display13FBMemory::field(plane,0x88); + if(planeScaler && planeScaler->getMetaClass()->getClassSize()>=0x50) { + geometry[46]=Display13FBMemory::field(planeScaler,0x44); + geometry[47]=Display13FBMemory::field(planeScaler,0x4C); + } + } + if(scaler && scaler->getMetaClass()->getClassSize()>=0x58) + for(unsigned i=0;i<6;++i)geometry[32+i]=Display13FBMemory::field(scaler,0x40+4*i); + } + Display13FBDBUFIO geometryIO {c.map}; + if(Display13FBIRQ::powered(geometryIO.read(0x45404),pipe)) { + static constexpr unsigned registers[]={0x70180,0x70188,0x7018C,0x70190,0x7019C,0x701A4,0x701AC,0x6001C}; + for(unsigned i=0;igetMetaClass()->getClassSize()>=0x12DC) { + static constexpr unsigned offsets[]={0x1264,0x1268,0x128C,0x1290,0x1294,0x12D8}; + for(unsigned i=0;i(dsiPendingBacking.pipe,offsets[i]); + } + adlnPublishMemory("N97FBWMGeometryFailure",pipe,geometry,sizeof(geometry),true); uint64_t cursor[8] {1,pipe};Display13FBDBUFIO readout {c.map}; if(Display13FBIRQ::powered(readout.read(0x45404),pipe)) { cursor[2]=readout.read(0x70080+0x1000*pipe); @@ -9530,7 +9976,7 @@ void XE::wrapAlderLakeNEngineEnable(void *power) { !c.bridge.state.suspended && acquired.status==Display13Power::Status::Complete && !c.clockFaulted && clock.status==Display13Clock::Status::Complete && Display13Power::objectAssociations(power,0x80,c.controller,0x1C20)) { Display13FBDBUFIO io {c.map}; - status=Display13DBUF::begin(io,c.dbuf,__builtin_popcount(callbackXE->adlnOutputWiring.selected())>=3 ? 7U : 0U); + status=Display13DBUF::begin(io,c.dbuf,__builtin_popcount(callbackXE->adlnOutputWiring.selected())>=3 ? 7U : 0U,c.transcoders); if(status==Display13DBUF::Status::Complete) { if(acquired.status==Display13Power::Status::Complete)status=Display13DBUF::resumeEngine(io,c.dbuf); else status=Display13DBUF::Status::Power; @@ -9674,6 +10120,12 @@ void XE::wrapAlderLakeNEngineDisable(void *power) { void XE::wrapAlderLakeNPowerOverride(void *power,bool enable) { auto &c=display13FBPower;if(!c.lock || power!=c.power || !c.controller)return; + // The last hwShutdown can suspend the power banks before returning. + // Return idle TC PHY ownership while its access lease is still usable. + // Ordinary display/system sleep override calls keep their existing policy. + if(!enable && callbackXE->display13ShutdownPowerCaller && + reinterpret_cast(__builtin_return_address(0))==callbackXE->display13ShutdownPowerCaller) + finishDisplay13TCShutdown(c.controller); // override(true) is only reached by the donor's simulator backend; our // physical object has simulator=0. Do not force unrelated DDI/AUX domains. if(!enable)wrapAlderLakeNEngineDisable(power); @@ -9721,6 +10173,7 @@ void XE::wrapAlderLakeNPrimaryDBUF(OSObject *plane,uint32_t start,uint32_t end,b IORecursiveLockLock(mutex); uint64_t record[20] {};IOLockLock(c.lock); const auto pipe=Display13FBMemory::boundPipe(c.pipe,fb); + if(dsiSharedWriterStopped(fb,pipe)) {IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex);return;} if(pipe>=3 || c.pipe[pipe].destroyingPath) {IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex);return;} Display13DBUF::Status status=Display13DBUF::Status::Unavailable; auto path=fb ? Display13FBMemory::field(plane,0x70) : nullptr; @@ -9770,6 +10223,7 @@ void XE::wrapAlderLakeNPlanarDBUFWrite(void *access,uint64_t reg,uint32_t value) if(fb->getMetaClass()->getClassSize()<0x4B90) {fb->release();return;} auto mutex=Display13FBMemory::field(fb,0x4220);if(!mutex) {fb->release();return;} IORecursiveLockLock(mutex);IOLockLock(c.lock); + if(dsiSharedWriterStopped(fb,pipe)) {IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex);fb->release();return;} if(c.pipe[pipe].destroyingPath) {IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex);fb->release();return;} uint64_t record[14] {2,reg,value};auto status=Display13DBUF::Status::Unavailable; auto path=Display13FBMemory::singlePath(fb); @@ -9808,6 +10262,7 @@ void XE::wrapDisplay13CursorDBUF(OSObject *plane) { IORecursiveLockLock(mutex);IOLockLock(c.lock); Display13DBUF::Status status=Display13DBUF::Status::Unavailable;uint64_t record[16] {}; const auto pipe=Display13FBMemory::boundPipe(c.pipe,fb); + if(dsiSharedWriterStopped(fb,pipe)) {IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex);return;} if(pipe>=3 || c.pipe[pipe].destroyingPath) {IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex);return;} auto path=Display13FBMemory::field(plane,0x70); const auto binding=adlnInspectPathBinding(fb,path,callbackXE->adlnPlatform,callbackXE->adlnOutputWiring); @@ -9869,7 +10324,8 @@ void XE::adlnSetupDBUFPipe(IOService *controller,IOService *fb,unsigned pipe,boo if(pipe>=3 || !fb || fb->getMetaClass()->getClassSize()<0x4B90)return; auto mutex=Display13FBMemory::field(fb,0x4220);if(!mutex)return; IORecursiveLockLock(mutex);IOLockLock(c.lock); - Display13DBUF::Layout layout;Display13DBUF::Range relativePrimary;OSObject *plane=nullptr;uint64_t record[18] {},resume[6] {}; + if(dsiSharedWriterStopped(fb,pipe)) {IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex);return;} + Display13DBUF::Layout layout;Display13DBUF::Range relativePrimary;OSObject *plane=nullptr;uint64_t record[20] {},resume[6] {}; auto path=Display13FBMemory::singlePath(fb); const auto binding=adlnInspectPathBinding(fb,path,callbackXE->adlnPlatform,callbackXE->adlnOutputWiring); const bool associated=Display13FBMemory::boundPipe(c.pipe,fb)==pipe && @@ -9924,7 +10380,18 @@ void XE::adlnSetupDBUFPipe(IOService *controller,IOService *fb,unsigned pipe,boo // arm it after this allocation; keep its old slices pending until then. IOLockLock(c.lock); if(!allocated || c.watermarkFaulted)layout.status=Display13DBUF::Status::Faulted; - else if(layout.active && (c.pipe[pipe].wmPending || c.pipe[pipe].wmLatch.status!=Display13WM::Status::Complete))layout.status=Display13DBUF::Status::Conflict; + else if(layout.active && (c.pipe[pipe].wmPending || c.pipe[pipe].wmLatch.status!=Display13WM::Status::Complete)) { + // An enabled timing pipe can still be preparing its primary, or + // waiting for the live surface to catch up. The WM proof explicitly + // leaves those cases pending. Allocation success is not retirement: + // retain the old slices and clock guard until a later native arm + // proves the actual scanout. Other unavailable/failed proofs are not + // permission to continue an active allocation. + const auto &latch=c.pipe[pipe].wmLatch; + const bool awaiting=latch.status==Display13WM::Status::Unavailable && + (latch.check==Display13WM::LatchCheck::AwaitingScanout || latch.check==Display13WM::LatchCheck::AwaitingFlip); + if(!awaiting)layout.status=Display13DBUF::Status::Conflict; + } if(layout.status==Display13DBUF::Status::Complete && !optimized) reinterpret_cast(controller)[0x1AFA]=0; } @@ -9933,13 +10400,18 @@ void XE::adlnSetupDBUFPipe(IOService *controller,IOService *fb,unsigned pipe,boo record[5]=c.dbuf.pipe[pipe].primary.start;record[6]=c.dbuf.pipe[pipe].primary.end;record[7]=c.dbuf.pipe[pipe].cursor.start;record[8]=c.dbuf.pipe[pipe].cursor.end; record[9]=c.dbuf.consumers;record[10]=c.dbuf.unretired;record[11]=c.dbuf.requested;record[12]=c.dbuf.powered; record[13]=c.pipe[pipe].wmPending;record[14]=c.pipe[pipe].watermarkClockHz;record[15]=static_cast(c.pipe[pipe].wmLatch.status);record[16]=pipe;record[17]=binding.ddi; + record[18]=static_cast(c.pipe[pipe].wmLatch.check);record[19]=c.dbuf.faulted; IOLockUnlock(c.lock);IORecursiveLockUnlock(mutex); adlnPublishMemory("N97FBDBUFLayout",pipe,record,sizeof(record)); + if(layout.status!=Display13DBUF::Status::Complete)adlnPublishMemory("N97FBDBUFLayoutFailure",pipe,record,sizeof(record),true); if(resume[0])adlnPublishMemory("N97FBDBUFColdResume",pipe,resume,sizeof(resume)); } void XE::wrapAlderLakeNDefaultDBUF(IOService *controller) {adlnSetupDBUF(controller,false);} void XE::wrapAlderLakeNOptimizedDBUF(IOService *controller) {adlnSetupDBUF(controller,true);} + + + void XE::wrapAlderLakeNPowerInit(void *power,void *controller) { auto &c=display13FBPower; if(!c.lock)return; uint64_t record[32] {}; Display13Power::Result result; @@ -9962,16 +10434,23 @@ void XE::wrapAlderLakeNPowerInit(void *power,void *controller) { // Reference failures retain mappings/objects for diagnosis until reboot. service->retain();static_cast(power)->retain();static_cast(c.access)->retain(); Display13FBPowerIO io {map}; - const auto wells=io.read(Display13Power::Driver),a=io.read(0x60400),pipeA=io.read(0x70008); - const auto bootPort=Display13FBTopology::bootDDI(callbackXE->adlnOutputWiring,a,true); + const auto wells=io.read(Display13Power::Driver); + const bool pipePowered=Display13FBIRQ::powered(wells,0); + const auto a=pipePowered ? io.read(0x60400) : UINT32_MAX; + const auto pipeA=pipePowered ? io.read(0x70008) : UINT32_MAX; + const bool dsi=callbackXE->adlnOutputWiring.dsi==1; + const auto bootPort=dsi ? 0U : Display13FBTopology::bootDDI(callbackXE->adlnOutputWiring,a,true); const bool dpBoot=bootPort<7 && (callbackXE->adlnOutputWiring.links()&(1U<dsiBoot.complete() && + XELP::DSI::bootUnchanged(sourceIO,callbackXE->dsiBoot,xelpMemory) : + bootPort<7 && wells!=UINT32_MAX && (wells&(1U<<10)) && pipeA!=UINT32_MAX && ddi!=UINT32_MAX && (pipeA&(1U<<31)) && (ddi&(1U<<31)); - for(unsigned pipe=1;pipe<3;++pipe) if(wells&(1U<<(2*(5+pipe)))) { + for(unsigned pipe=1;boot && pipe<3;++pipe) if(Display13FBIRQ::powered(wells,pipe)) { const auto trans=io.read(0x60400+pipe*0x1000); boot=boot && trans!=UINT32_MAX && !(trans&(1U<<31)); } @@ -9997,6 +10476,12 @@ void XE::wrapAlderLakeNPowerInit(void *power,void *controller) { if(result.status==Display13Power::Status::Complete && !Display13Power::publishObject(c.bridge,power,0x80,controller,0xC60)) { result.status=Display13Power::Status::NotReady;adlnFault(c.bridge.state.faulted,__LINE__); } + if(result.status==Display13Power::Status::Complete && dsi) { + IOLockUnlock(c.lock); + const bool bound=callbackXE->bindDSIResources(service); + IOLockLock(c.lock); + if(!bound){result.status=Display13Power::Status::NotReady;adlnFault(c.bridge.state.faulted,__LINE__);} + } if(result.status==Display13Power::Status::Complete) { // Initial discovery can call the virtual probe directly without // probePortState. Bind at the proven manager/power-owner boundary @@ -10005,7 +10490,7 @@ void XE::wrapAlderLakeNPowerInit(void *power,void *controller) { IORegistryEntry::getRegistryRoot()->setProperty("N97FBSharedProbeBound",bound); if(!bound){result.status=Display13Power::Status::NotReady;adlnFault(c.bridge.state.faulted,__LINE__);} } - if(result.status==Display13Power::Status::Complete && callbackXE->display13FBIRQReady) { + if(result.status==Display13Power::Status::Complete && !dsi && callbackXE->display13FBIRQReady) { auto irqLock=*reinterpret_cast(static_cast(controller)+0x90); if(irqLock) { display13FBIRQ.base=map->getVirtualAddress();display13FBIRQ.lock=irqLock; @@ -10018,6 +10503,16 @@ void XE::wrapAlderLakeNPowerInit(void *power,void *controller) { if(result.status==Display13Power::Status::Complete && dpBoot && bootPort>=3)tcBootDDI=bootPort; } else OSSafeReleaseNULL(map); } + if(result.status==Display13Power::Status::Complete && callbackXE->adlnOutputWiring.dsi) { + auto getPort=reinterpret_cast(callbackXE->tglPortByDDI); + auto port=getPort?getPort(0):nullptr; + bool adopted=false; + if(port) { + port->retain();IOLockUnlock(c.lock);adopted=setDSIPortMode(port,1)==kIOReturnSuccess; + IOLockLock(c.lock);port->release(); + } + if(!adopted){result.status=Display13Power::Status::NotReady;adlnFault(c.bridge.state.faulted,__LINE__);} + } if(tcBootDDI<7) { // Exact donor start initializes every port before PowerWell::init. // Establish the same durable Mode used by probing/hotplug before any @@ -10209,7 +10704,27 @@ void XE::wrapAlderLakeNPowerGetPG(void *p,uint32_t d) { adlnPowerReference(p,1,d void XE::wrapAlderLakeNPowerPutPG(void *p,uint32_t d) { adlnPowerReference(p,2,d); } void XE::wrapAlderLakeNPowerGetDDI(void *p,uint32_t d) { adlnPowerReference(p,3,d); } void XE::wrapAlderLakeNPowerPutDDI(void *p,uint32_t d) { adlnPowerReference(p,4,d); } -void XE::wrapAlderLakeNPowerGetAUX(void *p,uint32_t d) { adlnPowerReference(p,5,d); } +void XE::wrapAlderLakeNPowerGetAUX(void *p,uint32_t d) { + // Capture only the first DSI AUX-A caller. Port's forwarding entry tail + // calls this method, so frame zero also identifies its native caller. + static bool captured=false; + const bool capture=callbackXE && callbackXE->adlnOutputWiring.dsi==1 && d==0 && + __sync_bool_compare_and_swap(&captured,false,true); + uint64_t record[6] {1,d,0,UINT64_MAX,0,0}; + if(capture) { + const auto caller=reinterpret_cast(__builtin_return_address(0)); + const auto base=callbackXE->display13DPImage; + record[2]=base && caller>=base && caller-base<0xC8000; + if(record[2])record[3]=caller-base; + record[4]=p==display13FBPower.power; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIFirstAUXCaller",record,sizeof(record)); + } + const bool complete=adlnPowerReference(p,5,d); + if(capture) { + record[5]=complete ? 1 : 2; + IORegistryEntry::getRegistryRoot()->setProperty("WEXDSIFirstAUXCaller",record,sizeof(record)); + } +} void XE::wrapAlderLakeNPowerPutAUX(void *p,uint32_t d) { adlnPowerReference(p,6,d); } IOService *XE::wrapAlderLakeNRejectedAcceleratorProbe(IOService *,IOService *,SInt32 *) { @@ -10498,6 +11013,7 @@ bool XE::processAlderLakeNTaskBoundary(KernelPatcher &patcher, size_t index, mac } bool XE::processTGLAccelerator(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { + if(adlnDetected && tglAcceleratorRequested)dsiBackingHooksReady=processDSIBackingHooks(patcher,index,address,size); // Framebuffer-only mode must never enter the donor accelerator probe. // The bring-up payload omits this bundle; also reject it if already present. if (!tglAcceleratorRequested) { @@ -10651,7 +11167,8 @@ bool XE::processTGLAccelerator(KernelPatcher &patcher, size_t index, mach_vm_add uint8_t urbReplaceAlderLakeN[] {0x83,0xFE,0x01,0x75,0x59,0x83,0xFA,0x04,0x75,0x54,0x41,0xC7,0x87,0x64,0x11,0x00,0x00,0x04,0x00,0x00,0x00}; if (adlnDetected) { urbReplaceAlderLakeN[7]=adlnNativeSubslices;urbReplaceAlderLakeN[17]=adlnL3Banks; } if(display12SmallGTDetected) {urbReplaceAlderLakeN[7]=display12SmallGT.nativeSubslices;urbReplaceAlderLakeN[17]=display12SmallGT.l3Banks;} - const auto urbReplacement = (adlnDetected || XELP::display12SmallGT(xeProfile.gt)) ? urbReplaceAlderLakeN : + if(tglGT2Detected) {urbReplaceAlderLakeN[7]=tglGT2Resources.nativeSubslices;urbReplaceAlderLakeN[17]=tglGT2Resources.l3Banks;} + const auto urbReplacement = (adlnDetected || XELP::display12SmallGT(xeProfile.gt) || tglGT2Detected) ? urbReplaceAlderLakeN : (xeProfile.gt==XELP::GT::TGLG4 ? urbReplaceG4 : urbReplace); auto gpuInfo=patcher.solveSymbol(index, "__ZN16IntelAccelerator10getGPUInfoEv",address,size); @@ -11417,6 +11934,61 @@ bool XE::processTGLAccelerator(KernelPatcher &patcher, size_t index, mach_vm_add return true; } +bool XE::processDisplay13Shutdown(KernelPatcher &p,size_t index,mach_vm_address_t address,size_t size) { + const char *symbol="__ZN31AppleIntelFramebufferController10hwShutdownEP21AppleIntelFramebuffer"; + const uint8_t entry[]={0x55,0x48,0x89,0xe5,0x41,0x57,0x41,0x56,0x41,0x55,0x41,0x54,0x53,0x48,0x83,0xec,0x18}; + const uint8_t powerCall[]={0xe8,0x3b,0x97,0,0}; + const auto target=p.solveSymbol(index,symbol,address,size);p.clearError(); + const auto delta=static_cast(reinterpret_cast(wrapDisplay13Shutdown)-(target+5)); + bool known=size>0x51d4b && target==address+0x5173a && + !memcmp(reinterpret_cast(target),entry,sizeof(entry)) && + !memcmp(reinterpret_cast(address+0x51d24),powerCall,sizeof(powerCall)) && delta==static_cast(delta); + if(known) { + KernelPatcher::RouteRequest route(symbol,wrapDisplay13Shutdown,orgDisplay13Shutdown); + known=p.routeMultipleShort(index,&route,1,address,size);p.clearError(); + if(known)display13ShutdownPowerCaller=address+0x51d29; + } + IORegistryEntry::getRegistryRoot()->setProperty("WEXDisplay13ShutdownReady",known); + return known; +} + +void XE::finishDisplay13TCShutdown(void *controller) { + auto self=callbackXE;auto &c=display13FBPower; + if(!c.lock || !self->display13FBTCHooksReady)return; + IOLockLock(c.lock); + if(controller!=c.controller || !c.tcSupported || !c.bridge.begun || c.bridge.state.suspended) {IOLockUnlock(c.lock);return;} + // Native mode0 resets the port outside the owner lock. Serialize against + // hotplug on the same workloop, using the existing nonblocking gate API. + auto gate=c.workLoop;bool acquired=false; + bool gated=gate && Display13FBMemory::field(controller,0xE00)==gate && + self->adlnWorkLoopInGate && self->adlnWorkLoopTryClose && self->adlnWorkLoopOpen; + if(gated && !reinterpret_cast(self->adlnWorkLoopInGate)(gate)) + gated=acquired=reinterpret_cast(self->adlnWorkLoopTryClose)(gate); + uint32_t record[8] {1,static_cast(gated),0,0,0,0,0,0}; + if(gated)for(unsigned tc=0;tc<4;++tc) { + auto port=c.tcObject[tc]; + // Other framebuffers may not have shut down yet. Their live references + // remain intact; a later hwShutdown handles them after link/PLL stop. + if(!port || c.tcBusy[tc] || c.tcSignalReady[tc] || + c.bridge.nativeDDIRefs(tc+3) || c.bridge.nativeAUXRefs(tc+3))continue; + port->retain();record[2]|=1U<release();IOLockLock(c.lock); + record[4+tc]=result;if(!result)record[3]|=1U<(self->adlnWorkLoopOpen)(gate); + IORegistryEntry::getRegistryRoot()->setProperty("WEXDisplay13TCShutdown",record,sizeof(record)); +} + +uint32_t XE::wrapDisplay13Shutdown(void *controller,void *fb) { + const auto result=FunctionCast(wrapDisplay13Shutdown,callbackXE->orgDisplay13Shutdown)(controller,fb); + // hwShutdown is called by the donor only for system power-off/restart. + // A disconnected sink's hotplug path already does this; a connected sink + // still needs mode0 after its scanout/link stop, even while another FB lives. + if(!result)finishDisplay13TCShutdown(controller); + return result; +} + // One entry owns donor ABI/lifecycle repairs for every hardware backend. // Backend installers cannot independently opt into or replace these hooks. bool XE::processFramebufferKext(KernelPatcher &patcher,size_t index,mach_vm_address_t address,size_t size) { @@ -11430,6 +12002,9 @@ bool XE::processFramebufferKext(KernelPatcher &patcher,size_t index,mach_vm_addr KernelPatcher::RouteRequest gate(symbol,wrapFramebufferStart,orgFramebufferStart); const bool routed=patcher.routeMultiple(index,&gate,1,address,size);patcher.clearError(); if(!routed)return false; + if(display13FramebufferRequested) + processDisplay13Shutdown(patcher,index,address,size); + framebufferDonorCommonReady=processFramebufferCommon(patcher,index,address,size); // Install hardware routes even after a common failure so their diagnostics // remain available. The common start gate never enters native startup then. @@ -11452,7 +12027,7 @@ bool XE::processFramebufferCommon(KernelPatcher &patcher,size_t index,mach_vm_ad patcher.clearError(); if(probe) { // Slot repair is common; the target retains the existing hardware adapter. - auto replacement=display13FramebufferRequested ? xelpProviderProbe : reinterpret_cast(wrapTGLProbePort); + auto replacement=display13FramebufferRequested ? reinterpret_cast(wrapDisplay13ProbeMode) : reinterpret_cast(wrapTGLProbePort); patcher.routeBlock(table+0x188,reinterpret_cast(&replacement),sizeof(replacement)); probe=patcher.getError()==KernelPatcher::Error::NoError && *reinterpret_cast(table+0x188)==replacement; patcher.clearError(); @@ -11506,8 +12081,13 @@ bool XE::wrapFramebufferStart(IOService *that,IOService *provider) { IORecursiveLockUnlock(s->tglPathPoolLock); if(comboClockTransaction.lock) { IORecursiveLockLock(comboClockTransaction.lock); - comboClockTransaction.allocation={}; + const bool reset=comboClockTransaction.allocation.reset(); IORecursiveLockUnlock(comboClockTransaction.lock); + if(!reset) { + IORecursiveLockLock(s->tglPathPoolLock);s->framebufferStartInProgress=false;IORecursiveLockUnlock(s->tglPathPoolLock); + if(that)that->setProperty("WEXFramebufferRetainedClockOwner",true); + return false; + } } const bool native=s->display13FramebufferRequested ? wrapDisplay13FramebufferStart(that,provider) : wrapTGLFramebufferStart(that,provider); IORecursiveLockLock(s->tglPathPoolLock); @@ -11564,7 +12144,7 @@ XELP::LinkRequirements XE::framebufferLinkRequirements(void *port,uint32_t mode) using R=XELP::LinkRequirements; if(self->display13FramebufferRequested) { if(!Display13FBTopology::selected(self->adlnPlatform,config,self->adlnOutputWiring) || - mode!=(ddi>=3?4U:1U))return req; + mode!=(ddi>=3?((self->adlnOutputWiring.typeC&(1U<xeProfile.display,ddi>=3?R::PHY::TC:R::PHY::Combo, (self->adlnOutputWiring.links()&(1U<display12SmallGTDetected && !callbackXE->display12SmallGTReady) || + if((callbackXE->tglGT2Detected && !callbackXE->tglGT2Ready) || + (callbackXE->display12SmallGTDetected && !callbackXE->display12SmallGTReady) || (callbackXE->adlsDisplaySelected && (!callbackXE->adlsDisplayHooksReady || __atomic_load_n(&callbackXE->adlsBootConsumersFailed,__ATOMIC_ACQUIRE))))return false; if (callbackXE->adlnDetected && adlnNativeRCSProfileRequested && !adlnNativeRCSProfileReady) { @@ -13785,7 +14366,8 @@ void XE::tglObserveAccelerator(IOService *that, uint32_t stage, bool done, uint6 } IOService *XE::wrapTGLAcceleratorProbe(IOService *that, IOService *provider, SInt32 *score) { - if((callbackXE->display12SmallGTDetected && !callbackXE->display12SmallGTReady) || + if((callbackXE->tglGT2Detected && !callbackXE->tglGT2Ready) || + (callbackXE->display12SmallGTDetected && !callbackXE->display12SmallGTReady) || (callbackXE->adlsDisplaySelected && !callbackXE->adlsDisplayHooksReady))return nullptr; if (!callbackXE->tglAcceleratorRequested) return nullptr; auto pci = OSDynamicCast(IOPCIDevice, provider); @@ -18033,10 +18615,33 @@ bool XE::comboBeginClock(void *controller,void *port,bool enable,bool &keepPLL,v snapshot.mux[0]=comboClockRead(access,0x164280); if(s->adlsDisplaySelected)snapshot.mux[1]=comboClockRead(access,0x1642BC); for(unsigned i=0;idisplay13FramebufferRequested && display13FBPower.transcoders.dsi0Pipe<3) { + // The adoption boundary installs the qualified source/power binding + // before any native clock client. Revalidate here under the shared + // clock mutex, then pin its PLL even when DDI_CLK_OFF_A is set. + const auto &source=display13FBPower.bootDSI; + struct IO { + void *access;bool parked; + bool ready(){return parked;} + bool readable(uint32_t r) {return r(access,0x60);} + uint32_t read(uint32_t r) {return XE::comboClockRead(access,r);} + } io {access,s->dsiRuntimePower.sleeping && !s->dsiRuntimePower.failed && + s->dsiStopAttempted && s->dsiIRQParked && s->dsiResourcesReady && + s->dsiStop.complete() && s->dsiPanelStop.complete}; + const auto owner=reinterpret_cast(controller); + const bool retained=io.parked ? XELP::DSI::Restart::retainedStopped( + io,source,t.allocation,owner,s->dsiRuntimePower.host) : + (XELP::DSI::sourceUnchanged(io,source) && + t.allocation.retain(source.identity.phy,source.identity.pll,owner,snapshot)); + if(source.identity.pipe!=display13FBPower.transcoders.dsi0Pipe || + !retained) { + comboClockPower(power,false);IORecursiveLockUnlock(t.lock);return false; + } + } const auto otherConfig=other && other!=port ? *reinterpret_cast(static_cast(other)+0x548):nullptr; const auto choice=enable && otherConfig ? t.allocation.share(XELPComboClock::Port(otherConfig[0]),snapshot): t.allocation.choose(route,snapshot,enable); - if(!choice.valid) { + if(!choice.valid || route.phy>=snapshot.slots() || t.allocation.external[route.phy]) { comboClockPower(power,false);IORecursiveLockUnlock(t.lock);return false; } if(enable)t.allocation.binding[route.phy]=choice.pll+1; @@ -18582,6 +19187,13 @@ uint32_t XE::wrapTGLPixelInfo(void *fb, int mode, int depth, int aperture, void if (!result && info) memcpy(r + 6, info, 172); char key[48]; snprintf(key, sizeof(key), "TGLPixelFB%uDepth%d", *reinterpret_cast(static_cast(fb) + 0x1DC), depth); IORegistryEntry::getRegistryRoot()->setProperty(key, r, sizeof(r)); + if(dsiModeTarget(static_cast(fb))) { + DSIModeScope scope(static_cast(fb)); + for(unsigned i=0;scope.lock && i(mode)) { + snprintf(key,sizeof(key),"WEXDSIPixelInfo%u",i); + IORegistryEntry::getRegistryRoot()->setProperty(key,r,sizeof(r));break; + } + } return result; } IOMemoryDescriptor *XE::wrapTGLAperture(void *fb, int aperture) { @@ -19460,3 +20072,12 @@ void XE::wrapTGLWriteDekel(void *port, uint32_t index, uint32_t value) { IORegistryEntry::getRegistryRoot()->setProperty("TGLDekelRejectedWrite", index, 32); } } + +// DSI attachment implementations share this translation unit intentionally. +// Common eDP/DP/HDMI dispatch, private resource owners and hook call sites stay +// above; these fragments do not introduce another driver or a shared ABI. +#include "kern_dsi_adapter_boot.inc" +#include "kern_dsi_adapter_backing.inc" +#include "kern_dsi_adapter_modes.inc" +#include "kern_dsi_adapter_lifecycle.inc" +#include "kern_dsi_adapter_diagnostics.inc" diff --git a/WhateverXe/kern_xe.hpp b/WhateverXe/kern_xe.hpp index b65a2fea..b35a3d45 100644 --- a/WhateverXe/kern_xe.hpp +++ b/WhateverXe/kern_xe.hpp @@ -9,6 +9,16 @@ #define kern_xe_hpp #include "kern_tgl_tc_boot.hpp" +#include "kern_dsi_boot.hpp" +#include "kern_dsi_apple_timing.hpp" +#include "kern_dsi_ggtt.hpp" +#include "kern_dsi_surface.hpp" +#include "kern_dsi_quiesce.hpp" +#include "kern_dsi_clock.hpp" +#include "kern_dsi_commands.hpp" +#include "kern_dsi_panel.hpp" +#include "kern_dsi_backlight.hpp" +#include "kern_dsi_restart.hpp" #include "kern_tgl_tc_native.hpp" #include "kern_tgl_native_fia.hpp" #include "kern_tgl_hdmi_wiring.hpp" @@ -35,6 +45,7 @@ class IOPCIDevice; class IOInterruptEventSource; namespace Display13Clock { struct Readout; } namespace Display13WM { struct Latch; } +namespace XELP { namespace DSI { struct Profile; } } class XE { public: @@ -91,6 +102,8 @@ class XE { // Render and Display selections are independent; shared PHY/port code // must not depend on which GT implementation the product uses. bool display12SmallGTDetected {},display12SmallGTReady {}; + bool tglGT2Detected {},tglGT2Ready {}; + XELP::GTResources tglGT2Resources; bool adlsDisplaySelected {},adlsDisplayHooksReady {}; bool adlsPortInitReady {},tglHPDHookReady {}; bool adlsPLLTableReady {},adlsClockReady {},adlsPowerReady {},adlsPanelPWMReady {}; @@ -130,6 +143,39 @@ class XE { // Display handoff needs GOP/GGTT, mapper and aperture qualification, // independently of GT fuses or an accelerator being allowed to start. bool display13BootReady {false}; + OSData *dsiFirmware {}; // Private immutable copy backing profile/sequence spans. + XELP::DSI::Profile dsiProfile; + XELP::DSI::Boot dsiBoot; + XELP::DSI::GGTTMode dsiGGTT; + bool prepareDSIFirmware(IOService *); + bool dsiResourcesReady {}; + bool bindDSIResources(IOService *); + XELP::DSI::Quiesce::Result dsiStop; + XELP::DSI::Clock::Result dsiClockStop; + bool dsiStopAttempted {},dsiIRQParked {},dsiDDIReleased {},dsiMemoryReleased {}; + bool quiesceDSIPath(IOService *,IOService *,OSObject *,bool retain=false); + bool resumeDSIPath(IOService *,IOService *,OSObject *); + XELP::DSI::Backlight dsiBacklight; + uint32_t setDSIBrightness(IOService *,uintptr_t); + uint32_t saveDSIBrightness(IOService *,uintptr_t); + void publishDSIBacklight(); + static void recordDSIAttribute(IOService *,unsigned,uint32_t,uintptr_t,uint32_t); + bool processDSIPathDisposal(KernelPatcher &,size_t,mach_vm_address_t,size_t); + bool disposeDSIPath(IOService *,OSObject *); + mach_vm_address_t dsiFreePlane {},dsiFreeScaler {},dsiDetachPath {},dsiRemovePath {},dsiPipeMap {}; + bool dsiPGReleased {}; + static uint32_t wrapDisplay13ProbeMode(OSObject *); + bool dsiCacheHooksReady {}; + mach_vm_address_t orgDSICache[2] {}; + bool processDisplay13DSICache(KernelPatcher &,size_t,mach_vm_address_t,size_t); + bool updateDSIBootCache(IOService *,IOService *,OSObject *); + bool adoptDSIBootPath(IOService *,OSObject *); + static void wrapDSIUpdateCache(IOService *,IOService *,OSObject *); + static void wrapDSIGetCRTC(IOService *,IOService *,OSObject *); + bool dsiBootProbeReady {}; + mach_vm_address_t orgDSIBootProbe {}; + bool processDisplay13DSIBoot(KernelPatcher &,size_t,mach_vm_address_t,size_t); + static uint32_t wrapDSIBootProbe(IOService *,bool *,uint32_t *); // Render additionally requires the native GT resource certificate. bool adlnNativeReady {false}; bool display13FramebufferRequested {false}; @@ -147,14 +193,14 @@ class XE { mach_vm_address_t display13AUXControllerSlot {}; mach_vm_address_t orgDisplay13ReadAUX {},orgDisplay13WriteAUX {},orgDisplay13I2CAUX {},orgDisplay13RunAUX {}; bool processDisplay13FramebufferAUX(KernelPatcher &,size_t,mach_vm_address_t,size_t); - static bool display13AUXReady(void *port,bool command); + static bool display13AUXReady(void *port,bool command,bool retained=false); static uint32_t wrapDisplay13ReadAUX(void *port,uint32_t address,void *data,uint32_t length); static uint32_t wrapDisplay13WriteAUX(void *port,uint32_t address,void *data,uint32_t length); static uint32_t wrapDisplay13I2CAUX(void *port,uint32_t command,uint32_t *packet,uint32_t length,uint8_t flag); static uint32_t wrapDisplay13RunAUX(void *port,uint32_t command,uint32_t *packet,uint32_t length,bool flag); mach_vm_address_t orgDisplay13TrainingPattern {},orgDisplay13SignalLevels {}; mach_vm_address_t orgDisplay13EnablePort {},orgDisplay13DisablePort {}; - static bool display13MainLinkAUX(void *port,bool acquire); + static bool display13MainLinkAUX(void *port,bool acquire,bool stopping=false); static uint32_t wrapDisplay13EnablePort(void *port); static uint32_t wrapDisplay13DisablePort(void *port); bool processDisplay13FramebufferSST(KernelPatcher &,size_t,mach_vm_address_t,size_t); @@ -174,13 +220,99 @@ class XE { static uint32_t wrapDisplay13TrainingPattern(void *path,uint32_t pattern); static uint32_t wrapDisplay13SignalLevels(void *port,uint32_t swing,uint32_t emphasis,void *overrideConfig); mach_vm_address_t xelpProviderProbe {},xelpProviderConfigureProbe {}; - bool display13DPProbeReady {},display13TCProbeReady {},display13TCDPProbeReady {},display13FBDPHooksReady {}; + bool display13DPAltProbeReady {},display13DPProbeReady {},display13TCProbeReady {},display13TCDPProbeReady {},display13FBDPHooksReady {}; bool adlpDisplaySelected {}; bool adlpRenderRequested {}; Display13OutputWiring display13BoardWiring(IOService *); bool prepareDisplay13BootScanout(IOPCIDevice *); bool xelpBindProbe(void *controller); bool display13FBTopologyHooksReady {false}; + bool display13DSIModeHooksReady {}; + mach_vm_address_t orgDSIMode[9] {},orgDSIScalerSupport {},orgDSIDetailedValidation {},orgDSISourceValidation {},orgDSIPixelInfo {}; + bool processDisplay13DSIModes(KernelPatcher &,size_t,mach_vm_address_t,size_t); + static void recordDSIClient(IOService *,unsigned,uint64_t,uint32_t); + bool releaseDSIFixedMode(IOService *); + bool bindDSIFixedMode(IOService *,const XELP::DSI::Profile &); + static uint32_t setDSIPortMode(OSObject *,uint32_t); + static uint32_t wrapDSIDDC(IOService *,uint32_t,uint8_t *,uint64_t *,uint32_t,OSObject *); + static uint32_t wrapDSIGetCurrentMode(IOService *,int32_t *,int32_t *); + static uint32_t wrapDSIGetOnlineInfo(IOService *,OSObject *,uint8_t *,uint8_t *); + static uint32_t wrapDSISetDetailedTimings(IOService *,OSArray *); + static uint32_t wrapDSISetDisplayMode(IOService *,int32_t,int32_t); + static bool dsiNativeUpdateAllowed(IOService *); + static bool dsiSharedWriterStopped(IOService *,unsigned); + bool processDSINativeWriters(KernelPatcher &,size_t,mach_vm_address_t,size_t); + bool processDSINativeTransactions(KernelPatcher &,size_t,mach_vm_address_t,size_t); + bool processDSINativeAttributes(KernelPatcher &,size_t,mach_vm_address_t,size_t); + mach_vm_address_t orgDSIAttributes[4] {}; + static uint32_t wrapDSISetStream(IOService *,IOService *,OSObject *,const void *); + static uint32_t wrapDSISetAttribute(IOService *,uint32_t,uintptr_t); + static uint32_t wrapDSISetConnectionAttribute(IOService *,int32_t,uint32_t,uintptr_t); + static uint32_t wrapDSISetPowerState(IOService *,uint32_t); + mach_vm_address_t orgDSITransaction[5] {}; + static int beginDSIFramebufferWriter(IOService *); + static uint32_t wrapDSIFlipTransaction(IOService *,void *,uint64_t,void *); + static uint32_t wrapDSIGammaTransaction(IOService *,void *,void *); + static uint32_t wrapDSICscTransaction(IOService *,void *,void *,uint64_t,bool); + static uint32_t wrapDSIPlaneGammaTransaction(IOService *,void *,bool); + static uint32_t wrapDSISetGammaTable(IOService *,uint32_t,uint32_t,uint32_t,void *,bool); + bool dsiNativeWriterHooksReady {}; + bool dsiBackingHooksReady {}; + bool dsiPanelOnly {}; + bool dsiCommandTest {}; + bool dsiPanelTest {}; + bool dsiHostTest {}; + bool dsiStopTest {}; + bool dsiPowerTest {}; + struct { + bool sleeping {},failed {};uint32_t cycles {}; + XELP::DSI::Surface::Plane plane; + uint32_t cursorControl {},cursorPosition {},cursorBase {}; + XELP::DSI::Restart::State host; + XELP::DSI::Panel::Result panel; + } dsiRuntimePower; + XELP::DSI::Panel::Shutdown dsiPanelStop; + XELP::DSI::Restart::State dsiHostRestart; + XELP::DSI::Panel::Result dsiPanelTrial; + bool probeDSIPanel(IOService *,bool prepareExternalClock=false); + XELP::DSI::Host dsiCommandHost; + bool probeDSICommands(IOService *); + mach_vm_address_t orgDSIBacking[2] {}; + bool processDSIBackingHooks(KernelPatcher &,size_t,mach_vm_address_t,size_t); + mach_vm_address_t dsiComputeGeometry {},dsiModeCPU[4] {}; + bool publishDSINativeBacking(); + bool publishDSINativeMode(IOService *); + uint32_t setDSIPresentation(IOService *,uint32_t,const XELP::DSI::AppleAdapter::Timing &); + bool releaseDSILogical(IOService *); + bool probeDSIModeGeometry(IOService *); + bool prepareDSINativeBacking(IOService *); + bool validateDSINativeBacking(); + bool initializeDSINativeBacking(); + bool switchDSINativeSurface(); + bool dsiArmedBackingOwned(IOService *,OSObject *); + bool retireDSINativeBacking(IOService *,OSObject *); + XELP::DSI::Session dsiSession; + XELP::DSI::Session::Cleanup dsiCleanup; + bool discardDSINativeBacking(IOService *); + void probeDSINativeBacking(IOService *); + bool dsiBackingAttempted {}; + uint32_t dsiBackingStage {}; + static uint32_t wrapDSIBackingAllocate(OSObject *,void *,uint32_t); + static uint32_t wrapDSIBackingFree(OSObject *,void *,uint32_t); + + mach_vm_address_t orgDSICursor[2] {}; + static int beginDSINativeWriter(IOService *,IOService *); + static void endDSINativeWriter(int); + static uint32_t wrapDSICursorImage(IOService *,IOService *,void *); + static uint32_t wrapDSICursorState(IOService *,IOService *,int32_t,int32_t,bool,bool); + static uint32_t wrapDSIValidateMode(IOService *,int32_t,const void **,const void **); + static uint32_t wrapDSIGetModes(IOService *,int32_t *); + static uint32_t wrapDSIGetModeCount(IOService *); + static uint32_t wrapDSIGetModeInformation(IOService *,int32_t,void *); + static uint32_t wrapDSIPixelInfo(IOService *,int32_t,int32_t,int32_t,void *); + static uint32_t wrapDSIValidateDetailed(IOService *,void *,uint64_t); + static bool wrapDSIValidateSource(IOService *,void *); + static bool wrapDSIScalerSupport(void *,uint32_t,uint32_t,uint32_t,uint32_t,uint64_t,uint32_t,bool); Display13OutputWiring adlnOutputWiring; mach_vm_address_t adlnComboPLLFractionCaller {}; uint64_t adlnPlatform[0x1C0/8] {}; @@ -232,6 +364,7 @@ class XE { bool processDisplay13FramebufferTC(KernelPatcher &,size_t,mach_vm_address_t,size_t); static unsigned display13TCTarget(OSObject *,bool cleanup=false); static uint32_t wrapDisplay13TCMode(OSObject *,uint32_t); + static void drainDisplay13TCMode(unsigned); static void wrapDisplay13TCEnableClocks(void *,void *,void *,uint32_t *); static void wrapDisplay13TCDisableClocks(void *,void *,void *); static uint32_t wrapDisplay13TCSignal(void *,uint32_t,uint32_t,void *); @@ -242,6 +375,9 @@ class XE { mach_vm_address_t orgDisplay13FBEnable {},orgAlderLakeNConnectionAttr {},orgAlderLakeNPortDDC {},orgDisplay13FBDDC {}; static uint32_t wrapDisplay13FBEnable(IOService *fb); bool processFramebufferAGDC(KernelPatcher &,size_t,mach_vm_address_t,size_t); + bool processDisplay13TrialAGDC(KernelPatcher &,size_t,mach_vm_address_t,size_t); + static uint32_t wrapDisplay13TrialCommit(void *,void *,void *); + mach_vm_address_t orgDisplay13TrialCommit {}; mach_vm_address_t orgFramebufferUserClient {},orgFramebufferAGDCCallback {}; static uint32_t wrapFramebufferUserClient(IOService *,void *,void *,uint32_t,void **); static uint32_t wrapFramebufferAGDCCallback(IOService *,uint32_t,unsigned long,uint32_t); @@ -285,6 +421,7 @@ class XE { static void wrapAlderLakeNPlaneWMEnable(OSObject *plane,bool arm); static void wrapDisplay13CursorWMEnable(OSObject *plane,bool arm); static void wrapDisplay13WMArmUpdate(IOService *framebuffer); + static void display13WMArmUpdate(IOService *framebuffer); static Display13WM::Latch adlnVerifyWMLatch(IOService *framebuffer,uint64_t nativeHz,uint64_t targetHz); mach_vm_address_t orgDisplay13WMArmUpdate {}; static void wrapAlderLakeNPrimaryDBUF(OSObject *plane,uint32_t start,uint32_t end,bool wait); @@ -338,6 +475,7 @@ class XE { static void wrapDisplay13FBProcessDE(IOService *controller,uint32_t *pipe,uint32_t *aux,uint32_t *hotplug); static bool wrapDisplay13FBProcessIRQ(IOService *controller); static void wrapDisplay13FBSWIRQ(IOService *controller,IOInterruptEventSource *source,int count); + void auditDSIBoot(IOPCIDevice *,volatile const uint8_t *,uint64_t,const AlderLakeN::DisplayReadout &,bool); void adlnBootstrap(IOPCIDevice *pci); uint16_t tglDeviceId {0}; // Physical PCI ID, not a DeviceProperties override. mach_vm_address_t orgTGLEnableCombo {}, orgTGLEngineEnable {}, orgTGLEngineDisable {}; @@ -624,6 +762,10 @@ class XE { bool framebufferDonorAttempted {},framebufferDonorCommonReady {},framebufferBackendInstalled {}; mach_vm_address_t orgFramebufferStart {},orgFramebufferInit {},orgFramebufferComputeLanes {},framebufferLaneCaller {}; __attribute__((noinline)) bool processFramebufferKext(KernelPatcher &,size_t,mach_vm_address_t,size_t); + bool processDisplay13Shutdown(KernelPatcher &,size_t,mach_vm_address_t,size_t); + static uint32_t wrapDisplay13Shutdown(void *,void *); + static void finishDisplay13TCShutdown(void *); + mach_vm_address_t orgDisplay13Shutdown {},display13ShutdownPowerCaller {}; __attribute__((noinline)) bool processFramebufferCommon(KernelPatcher &,size_t,mach_vm_address_t,size_t); bool framebufferTrainingHooksReady {}; bool framebufferStartInProgress {}; diff --git a/WhateverXe/kern_xelp_call_target.hpp b/WhateverXe/kern_xelp_call_target.hpp new file mode 100644 index 00000000..ca168842 --- /dev/null +++ b/WhateverXe/kern_xelp_call_target.hpp @@ -0,0 +1,23 @@ +// Validate a direct x86 call by its resolved target, retaining opcode checks. +#ifndef KERN_XELP_CALL_TARGET_HPP +#define KERN_XELP_CALL_TARGET_HPP +#include +#include +#include +namespace XELPCall { +inline uintptr_t relativeTarget(const uint8_t *relative,uintptr_t next) { + const uint32_t displacement=static_cast(relative[0]) | + static_cast(relative[1])<<8 | static_cast(relative[2])<<16 | + static_cast(relative[3])<<24; + if(displacement&0x80000000U) { + const uint64_t distance=0x100000000ULL-displacement; + return next>=distance ? next-distance : 0; + } + return next<=UINTPTR_MAX-displacement ? next+displacement : 0; +} +inline bool targets(const uint8_t *bytes,size_t length,uintptr_t site,uintptr_t target) { + if(!bytes || length<5 || !site || !target || bytes[0]!=0xE8 || site>UINTPTR_MAX-5)return false; + return relativeTarget(bytes+1,site+5)==target; +} +} +#endif diff --git a/WhateverXe/kern_xelp_combo_clock.hpp b/WhateverXe/kern_xelp_combo_clock.hpp index 0ee42e43..05042673 100644 --- a/WhateverXe/kern_xelp_combo_clock.hpp +++ b/WhateverXe/kern_xelp_combo_clock.hpp @@ -53,16 +53,39 @@ struct ClockChoice { }; struct ClockAllocation { template ClockChoice share(const P &source,const ClockSnapshot &s) const { - if(source.phy>=s.slots() || !s.valid() || !s.active(source))return {}; + if(source.phy>=s.slots() || external[source.phy] || !s.valid() || !s.active(source))return {}; const auto pll=s.selected(source); return pll=s.slots() || pll>=s.pllCount() || !s.valid() || + s.selected(Port(phy))!=pll || (s.enable[pll]&0xCC000000U)!=0xCC000000U)return false; + if(external[phy])return external[phy]==owner && binding[phy]==pll+1; + if(binding[phy] || (s.peers(Port(phy))&(1U<(pll+1);return true; + } + // Caller must first stop/detach this link and drain its consumers under + // the same serialized ownership scope. A closed DDI gate is insufficient. + bool release(unsigned phy,uintptr_t owner) { + if(phy>=3 || !owner || external[phy]!=owner)return false; + external[phy]=0;binding[phy]=0;return true; + } template ClockChoice choose(const P &p,const ClockSnapshot &s,bool on) const { - if(p.phy>=s.slots() || !s.valid())return {}; + if(p.phy>=s.slots() || external[p.phy] || !s.valid())return {}; auto peers=s.peers(p); for(unsigned phy=0;phy<3;++phy) if(phy!=p.phy && binding[phy])peers|=1U<<(binding[phy]-1); diff --git a/WhateverXe/kern_xelp_devices.hpp b/WhateverXe/kern_xelp_devices.hpp index f902a6e4..b696dc51 100644 --- a/WhateverXe/kern_xelp_devices.hpp +++ b/WhateverXe/kern_xelp_devices.hpp @@ -24,6 +24,18 @@ constexpr DeviceEntry DeviceTable[] { {0x4690, Family::AlderLakeS, Display::XeD, GT::ADLSSmall, "Intel UHD Graphics 770"}, {0x4692, Family::AlderLakeS, Display::XeD, GT::ADLSSmall, "Intel UHD Graphics 730"}, {0x4693, Family::AlderLakeS, Display::XeD, GT::ADLSSmall, "Intel UHD Graphics 710"}, + {0x4626, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0x4628, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0x462A, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0x46A0, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0x46A1, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0x46A2, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0x46B0, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0x46B1, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0x46B2, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0x46C0, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0x46C1, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0x46C2, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, {0x46A3, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, {0x46A6, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel Iris Xe Graphics"}, {0x46A8, Family::AlderLakeP, Display::XeLPD, GT::ADLPMobile, "Intel Iris Xe Graphics"}, @@ -39,6 +51,11 @@ constexpr DeviceEntry DeviceTable[] { {0x4C8B, Family::RocketLake, Display::XeD, GT::Unknown, "Intel UHD Graphics 730"}, {0x9A40, Family::TigerLake, Display::XeD, GT::TGLGT2, "Intel Iris Xe Graphics"}, {0x9A49, Family::TigerLake, Display::XeD, GT::TGLGT2, "Intel Iris Xe Graphics"}, + {0x9A59, Family::TigerLake, Display::XeD, GT::TGLGT2, "Intel UHD Graphics"}, + {0x9AC0, Family::TigerLake, Display::XeD, GT::TGLGT2, "Intel UHD Graphics"}, + {0x9AC9, Family::TigerLake, Display::XeD, GT::TGLGT2, "Intel UHD Graphics"}, + {0x9AD9, Family::TigerLake, Display::XeD, GT::TGLGT2, "Intel UHD Graphics"}, + {0x9AF8, Family::TigerLake, Display::XeD, GT::TGLGT2, "Intel UHD Graphics"}, {0x9A60, Family::TigerLake, Display::XeD, GT::TGLGT1, "Intel UHD Graphics"}, {0x9A68, Family::TigerLake, Display::XeD, GT::TGLGT1, "Intel UHD Graphics"}, {0x9A70, Family::TigerLake, Display::XeD, GT::TGLGT1, "Intel UHD Graphics"}, @@ -51,6 +68,8 @@ constexpr DeviceEntry DeviceTable[] { {0xA789, Family::RaptorLakeS, Display::XeD, GT::ADLSSmall, "Intel UHD Graphics"}, {0xA78A, Family::RaptorLakeS, Display::XeD, GT::ADLSSmall, "Intel UHD Graphics"}, {0xA78B, Family::RaptorLakeS, Display::XeD, GT::ADLSSmall, "Intel UHD Graphics"}, + {0xA720, Family::RaptorLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, + {0xA721, Family::RaptorLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, {0xA7A0, Family::RaptorLakeP, Display::XeLPD, GT::ADLPMobile, "Intel Iris Xe Graphics"}, {0xA7A1, Family::RaptorLakeP, Display::XeLPD, GT::ADLPMobile, "Intel Iris Xe Graphics"}, {0xA7A8, Family::RaptorLakeP, Display::XeLPD, GT::ADLPMobile, "Intel UHD Graphics"}, @@ -65,6 +84,10 @@ inline const DeviceEntry *deviceEntry(uint32_t pci) { for(const auto &entry:DeviceTable)if(entry.device==(pci>>16))return &entry; return nullptr; } +inline bool tigerLakeGT2Device(uint32_t pci) { + const auto e=deviceEntry(pci); + return e && e->family==Family::TigerLake && (e->backend==GT::TGLGT2 || e->backend==GT::TGLG4); +} inline bool mobileDisplay13Device(uint32_t pci) { const auto e=deviceEntry(pci); return e && e->backend==GT::ADLPMobile; diff --git a/WhateverXe/kern_xelp_gt_resources.hpp b/WhateverXe/kern_xelp_gt_resources.hpp index 156748b1..545028a3 100644 --- a/WhateverXe/kern_xelp_gt_resources.hpp +++ b/WhateverXe/kern_xelp_gt_resources.hpp @@ -54,5 +54,18 @@ inline bool decodeADLPGT(const uint32_t *fuses, GTResources &out) { return true; } +// TGL U/Y shares Gen12 SSEU and L3-disable fuses with the existing reader. +// Reuse only the established 48-EU/6-bank and 80/96-EU/8-bank URB shapes. +// A PCI ID (including Linux's GT2 classification) does not determine capacity. +inline bool decodeTGLGT2(const uint32_t *fuses, GTResources &out) { + GTResources decoded; + out={}; + if(!decodeADLPGT(fuses,decoded))return false; + if(!((decoded.dss==3 && decoded.eus==48 && decoded.l3Banks==6) || + ((decoded.dss==5 || decoded.dss==6) && decoded.eus==16*decoded.dss && decoded.l3Banks==8)))return false; + out=decoded; + return true; +} + } #endif diff --git a/WhateverXe/kern_xelp_native_placement.hpp b/WhateverXe/kern_xelp_native_placement.hpp new file mode 100644 index 00000000..152af629 --- /dev/null +++ b/WhateverXe/kern_xelp_native_placement.hpp @@ -0,0 +1,73 @@ +// Native instruction contracts: instruction-boundary matching, no build/VAs. +#ifndef KERN_XELP_NATIVE_PLACEMENT_HPP +#define KERN_XELP_NATIVE_PLACEMENT_HPP +#include "kern_xelp_call_target.hpp" +namespace XELPNative { +// Only canonical, side-effect-free alignment NOPs may separate the expected +// instructions. Never scan past an unrecognised instruction to find a suffix. +struct Cursor { + const uint8_t *bytes;size_t length,pos; + bool take(const char *value,size_t count) { + while(pos=2 && bytes[pos]==0x66 && bytes[pos+1]==0x90) {pos+=2;continue;} + break; + } + if(count>length-pos || memcmp(bytes+pos,value,count))return false; + pos+=count;return true; + } + bool skip(size_t count) {if(count>length-pos)return false;pos+=count;return true;} +}; +// Decode the verifier's first call only after proving both argument sources: +// original VA in RSI and symbol-resolved kernel_pmap_store in RDI. +inline uintptr_t rootCall(const uint8_t *bytes,size_t length,uintptr_t base,uintptr_t store) { + if(!bytes || !base || !store || length>UINTPTR_MAX-base)return 0; + Cursor c {bytes,length,0}; + if(!c.take("\x55",1) || !c.take("\x48\x89\xe5",3) || !c.take("\x53",1) || + !c.take("\x50",1) || !c.take("\x48\x89\xfb",3) || !c.take("\x48\x8d\x3d",3) || + !c.skip(4) || XELPCall::relativeTarget(bytes+c.pos-4,base+c.pos)!=store || + !c.take("\x48\x89\xde",3) || !c.take("\xe8",1) || !c.skip(4))return 0; + const auto target=XELPCall::relativeTarget(bytes+c.pos-4,base+c.pos); + // The caller consumes the returned PTE pointer; an unrelated call is rejected. + return c.take("\x48\x85\xc0",3) ? target : 0; +} +inline bool rootAccessor(const uint8_t *bytes,size_t length) { + if(!bytes)return false; + Cursor c {bytes,length,0}; + return c.take("\x55",1) && c.take("\x48\x89\xe5",3) && + c.take("\x48\xb8\0\0\0\0\0\x80\xff\xff",10) && c.take("\x48\x01\xf0",3) && + c.take("\x48\xc1\xe8\x30",4) && c.take("\x3d\xff\xff\0\0",5) && + c.take("\x0f\x82",2) && c.skip(4) && c.take("\x48\x8b\x47\x50",4); +} +// Scan a bounded native function on decoded instruction boundaries. Require +// exactly one call to pmap, its complete register/stack argument setup, stack +// cleanup, and the function's terminating RET. Embedded bytes are not calls. +template bool protectCall(const uint8_t *bytes,size_t length,uintptr_t base, + uintptr_t pmap,bool tahoe,Decode decode) { + if(!bytes || !base || !pmap || length>UINTPTR_MAX-base)return false; + unsigned found=0; + for(size_t pos=0;pos15 || size>length-pos)return false; + if(size==1 && bytes[pos]==0xc3)return found==1; + Cursor c {bytes,length,pos}; + bool args=(!tahoe || c.take("\x48\x83\xec\x08",4)) && + c.take("\x4c\x89\xff",3) && c.take("\x4c\x89\xee",3) && c.take("\x89\xc2",2) && + c.take("\x8b\x4d\xd4",3) && c.take("\x44\x8b\x4d\xd0",4) && + (tahoe || c.take("\x6a\0",2)) && c.take("\x48\x8d\x45\xb8",4) && + c.take("\x50",1) && c.take("\x6a\x20",2) && c.take("\x6a\x01",2) && c.take("\xe8",1); + if(args && c.skip(4) && XELPCall::relativeTarget(bytes+c.pos-4,base+c.pos)==pmap && + c.take("\x48\x83\xc4\x20",4)) { + if(++found!=1)return false; + // Skip the proven call sequence, including inter-instruction NOPs, to + // avoid counting the same match twice when it begins with alignment. + pos=c.pos;continue; + } + // A pmap call with a different argument contract is never admitted. + if(size==5 && XELPCall::targets(bytes+pos,size,base+pos,pmap))return false; + pos+=size; + } + return false; +} +} +#endif diff --git a/WhateverXe/kern_xelp_platform.hpp b/WhateverXe/kern_xelp_platform.hpp index 9a857b5b..c98f6c2b 100644 --- a/WhateverXe/kern_xelp_platform.hpp +++ b/WhateverXe/kern_xelp_platform.hpp @@ -82,8 +82,9 @@ struct NativePorts { // ABI3 includes physical/effective identity and binds a profile and immutable probe policy, not mutable connection state. // legacy permits probing for mode4; it does not force mode4 or classify a // USB-C-capable connector as physically fixed DP. The provider checks generation-specific live state. -// Type-C intent is represented but not admitted by either current backend. -constexpr uint32_t ProbeABI=3,ProbeCapabilityABI=1,ProbeDisplay13ComboDP=1U,ProbeDisplay13FixedTC=2U,ProbeDisplay13FixedDP=4U,ProbeDisplay13Panel=8U; +// DP-alt uses the existing typeC mask and an optional capability; ABI3 layout +// stays unchanged. An older provider must not bind the new route. +constexpr uint32_t ProbeABI=3,ProbeCapabilityABI=1,ProbeDisplay13ComboDP=1U,ProbeDisplay13FixedTC=2U,ProbeDisplay13FixedDP=4U,ProbeDisplay13Panel=8U,ProbeDisplay13DPAlt=16U; using QueryProbeCapabilities=uint32_t (*)(uint32_t,uint32_t); // Optional export: old ABI3 providers remain valid for their existing routes, // but their lack of a capability reply cannot authorize a new DP route. @@ -99,6 +100,9 @@ inline bool display13TCProbeCapable(QueryProbeCapabilities query) { inline bool display13TCDPProbeCapable(QueryProbeCapabilities query) { return query && (query(ProbeCapabilityABI,ProbeABI)&ProbeDisplay13FixedDP); } +inline bool display13DPAltProbeCapable(QueryProbeCapabilities query) { + return query && (query(ProbeCapabilityABI,ProbeABI)&ProbeDisplay13DPAlt); +} struct ProbeConfig { uint32_t abi {ProbeABI},bytes {sizeof(ProbeConfig)}; @@ -117,9 +121,9 @@ struct ProbeConfig { if((combo&~7U) || ((legacy|typeC|excluded|legacyBackend)&~0x1F8U) || (comboBackend&~7U) || (legacy&typeC) || (legacy&excluded) || (typeC&excluded))return false; if(profile.display==Display::XeD)return comboBackend==7; - return profile.display==Display::XeLPD && (combo|legacy) && !(combo&~3U) && - comboBackend && !(comboBackend&~3U) && !typeC && - !(legacy&~0x78U) && !(legacyBackend&~0x78U) && !(legacy&~legacyBackend); + return profile.display==Display::XeLPD && (combo|legacy|typeC) && !(combo&~3U) && + comboBackend && !(comboBackend&~3U) && !(typeC&~0x78U) && + !(legacy&~0x78U) && !(legacyBackend&~0x78U) && !((legacy|typeC)&~legacyBackend); } }; inline bool same(const ProbeConfig &a,const ProbeConfig &b) { diff --git a/WhateverXe/kern_xelp_probe.hpp b/WhateverXe/kern_xelp_probe.hpp index 022c0c15..fdc5b104 100644 --- a/WhateverXe/kern_xelp_probe.hpp +++ b/WhateverXe/kern_xelp_probe.hpp @@ -43,6 +43,10 @@ template uint32_t probeMode(IO &io,const ProbeBinding &b,uintptr_t man Display13TC::Port p; const bool hdmi=c[1]>=9 && c[1]<=12 && c[2]==2 && c[3]==1 && c[4]==0x800 && (c[5]&0xB)==2; const bool dp=c[1]==c[0]+6 && c[2]==1 && c[3]==1 && c[4]==0x400 && c[5]==0x201; + if(dp && (b.config.typeC&b.config.legacyBackend&(1U< Add`. +Use **Data containing four little-endian bytes**, with the 16-bit ID followed by two zero bytes. For example, `D2460000` selects `46D2`. -Do not use an override merely to change the displayed GPU name. +An override cannot make unsupported hardware compatible or change its physical resources. Some backends require the original physical ID. Invalid or unsupported values are rejected; remove obsolete ICL or other GPU spoofs before using automatic selection. Restart after changing the selection. ## Display name -A user-supplied `model` property is preserved. Otherwise, the name associated -with the physical ID is used; when that ID is unknown, the selected ID's name is -used instead. Names are descriptive labels and do not control functionality or -EU counts. SKU and system configuration can affect Intel's marketing name. -Metal may still report the Apple driver's internal name. - -PCI ID/family groupings were checked against Linux v6.12's i915 tables; individual -marketing names are not a claim of independently verified SKU branding. - -## Display limits - -Connector labels alone do not identify the electrical path. Firmware VBT routing -must match the implemented path; ambiguous or cross-routed AUX layouts are excluded. - -- TGL: fixed Combo A/B DP and existing HDMI, eDP and USB-C paths share display - operations. Additional fixed-DP layouts have CPU coverage, not hardware coverage. -- ADL-S/HX and RPL-S/HX: Combo PHY A–C are implemented; PHY D/E and Type-C/TBT-managed - routes are not. A USB-C socket wired as fixed DP is different from a Type-C-managed - path. Firmware-derived DDI A eDP is implemented but not hardware-tested. -- TGL, ADL-S and experimental ADL-P/N panel handling supports the qualified PPS0/PWM0 path. Other PPS - controllers and AUX-controlled backlights need additional work. Panel timing - comes from VBT, firmware state and explicit properties. High-rate link selection - has CPU coverage; not every panel or link rate has been tested on hardware. -- TGL panels requesting HOBL use standard signal settings; the HOBL power-saving - optimization is disabled until its retry behavior is supported. -- Display 13: fixed HDMI uses firmware Combo A/B and TC1–4 routes; TC requires - verified legacy mode. Experimental fixed TC1–4 SST DP requires matching VBT - AUX wiring. Boot handoff supports validated linear RGB scanout, including - supported firmware scaling, and retires secondary fixed TC firmware outputs - before native startup. ADL-H has been tested with three outputs and a - DP-to-HDMI adapter. Direct DP sinks remain unverified. External Combo DP, MST, - Type-C-managed output and cross-routed AUX still need further implementation. - -The Display12 small-GT implementation accepts one slice, up to two DSS, -16/24/32 EUs, four L3 banks and the supported media engines. Different resource -layouts need additional implementation. These checks are independent of whether -an individual PCI revision has been tested. +A user-supplied `model` property is preserved. Otherwise, WEX uses the physical GPU's name. Names do not control compatibility or resource detection, and Metal may still show the Apple driver's internal name. Do not use a device-ID override merely to change the displayed name. diff --git a/tests/adlp_fb_candidate.py b/tests/adlp_fb_candidate.py index 67562083..97a3120d 100644 --- a/tests/adlp_fb_candidate.py +++ b/tests/adlp_fb_candidate.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 """Replay production board selection and WEX/provider binding for firmware-selected fixed HDMI.""" from pathlib import Path +from production_source import read_source import re,subprocess,tempfile -r=Path(__file__).resolve().parents[1];s=(r/'WhateverXe/kern_xe.cpp').read_text() +r=Path(__file__).resolve().parents[1];s=read_source(r/'WhateverXe/kern_xe.cpp') f='\n'.join(re.search(re.escape(sig)+r'.*?\n\}',s,re.S).group() for sig in ['Display13OutputWiring XE::display13BoardWiring(', 'bool XE::xelpBindProbe(']) code=r''' #include @@ -12,6 +13,8 @@ #include "kern_display13_firmware.hpp" #include "kern_display13_fb_topology.hpp" #include "kern_alderlake_s.hpp" +bool adopt=false,hdmiTrial=false; +bool checkKernelArgument(const char *s){return !strcmp(s,"-wex-dsi-adopt")?adopt:!strcmp(s,"-wex-dsi-hdmi-test")?hdmiTrial:false;} struct OSData {void *data=nullptr;unsigned bytes=0;void *getBytesNoCopy(){return data;}unsigned getLength(){return bytes;}}; #define OSDynamicCast(T,p) static_cast(p) struct IOService {OSData *property=nullptr;OSData *getProperty(const char *){return property;}}; @@ -19,12 +22,15 @@ XELP::ProbeConfig seen;unsigned calls=0; bool configure(void *c,const XELP::ProbeConfig *p){assert(c==(void*)1);seen=*p;++calls;return p->valid();} struct XE { + bool dsiPanelOnly=false,dsiCommandTest=false,dsiPanelTest=false,dsiHostTest=false,dsiStopTest=false,dsiPowerTest=false; XELP::Profile xeProfile=XELP::identify(0x46A68086,12,0x9A498086); XELP::VBT::Ports firmwarePorts; bool adlpDisplaySelected=true,adlsDisplaySelected=false,tglHDMIWiringApplied=false,tglTCWiringLoaded=false; bool display13PanelProbeReady=true,display13PanelPowerReady=true; + bool dsiResourcesReady=false,profileReady=true; + bool prepareDSIFirmware(IOService *){return profileReady;} bool display13FBTopologyHooksReady=true,display13TCProbeReady=true,display13FBTCHooksReady=true; - bool display13TCDPProbeReady=true,display13DPProbeReady=false,display13FBSSTHooksReady=true,display13FBAUXHooksReady=true; + bool display13DPAltProbeReady=true,display13TCDPProbeReady=true,display13DPProbeReady=false,display13FBSSTHooksReady=true,display13FBAUXHooksReady=true; uintptr_t xelpProviderProbe=1,xelpProviderConfigureProbe=reinterpret_cast(configure); void *adlnTopologyController=(void*)1;Display13OutputWiring adlnOutputWiring,tglHDMIWiring; struct {bool valid=false;unsigned excluded=0;unsigned selected(){return 0;}}adlsWiring; @@ -35,10 +41,22 @@ }; FUNCTIONS int main(){ + XE dsi;dsi.adlnOutputWiring.valid=true;dsi.adlnOutputWiring.dsi=1; + dsi.adlnOutputWiring.hdmi=2;dsi.adlnOutputWiring.ddc[1]=2; + assert(!dsi.xelpBindProbe((void*)1));dsi.dsiResourcesReady=true; + assert(dsi.xelpBindProbe((void*)1) && seen.combo==2); XE x;IOService pci;x.firmwarePorts.status=XELP::VBT::Status::Valid;x.firmwarePorts.present=0x38; for(unsigned d=3;d<=5;++d){auto &p=x.firmwarePorts.port[d];p.type=d==5?4:0x12;p.ddc=d;p.capabilities=3;p.hdmiPreset=7;} auto w=x.display13BoardWiring(&pci);assert(w.described() && w.hdmi==0x18 && w.ddc[3]==9 && w.ddc[4]==10 && !w.dp); assert(!Display13FBTopology::supportedWiring(w) && Display13FBTopology::supportedWiring(w,true)); + adopt=true; + auto dw=x.display13BoardWiring(&pci);assert(dw.described() && dw.dsi==1 && dw.hdmi==w.hdmi); + assert(!Display13FBTopology::supportedWiring(dw,true) && Display13FBTopology::supportedWiring(dw,true,true)); + hdmiTrial=true;assert(!x.display13BoardWiring(&pci).described());hdmiTrial=false; + x.profileReady=false;assert(!x.display13BoardWiring(&pci).described());x.profileReady=true; + x.firmwarePorts.present|=1;x.firmwarePorts.port[0].type=0x12;x.firmwarePorts.port[0].ddc=1; + assert(!x.display13BoardWiring(&pci).described());x.firmwarePorts.present&=~1U; + adopt=false;assert(!x.display13BoardWiring(&pci).dsi); x.adlnOutputWiring=w;assert(x.xelpBindProbe((void*)1)); assert(seen.valid() && !seen.combo && seen.comboBackend==3 && seen.legacy==0x18 && seen.legacyBackend==0x78); assert(seen.profile.family==XELP::Family::AlderLakeP && seen.profile.admitted()); diff --git a/tests/adlp_load_only.py b/tests/adlp_load_only.py index b335aa37..17262154 100644 --- a/tests/adlp_load_only.py +++ b/tests/adlp_load_only.py @@ -5,7 +5,7 @@ r=Path(__file__).resolve().parents[1];s=(r/'WhateverXe/kern_xe.cpp').read_text() a=s.index('// ADLP_LOAD_ONLY_HOOKS:');b=s.index('// ADLP_LOAD_ONLY_HOOKS_END',a);body=s[a:b] a=s.index(' // ADLP_LOAD_ONLY_KERNEL:');b=s.index(' // ADLP_LOAD_ONLY_KERNEL_END',a);selection=s[a:b] -a=s.index(' if(xeProfile.mobileDisplay13() && physicalGPU && checkKernelArgument("-wex-adlp-readout")');b=s.index(' if(display12SmallGTDetected',a);diagnostic=s[a:b] +a=s.index(' if(xeProfile.mobileDisplay13() && physicalGPU && checkKernelArgument("-wex-adlp-readout")');b=s.index(' if(((display12SmallGTDetected',a);diagnostic=s[a:b] dispatch=s[s.index('bool XE::processKext('):s.index(' if((tglRequested',s.index('bool XE::processKext('))] assert dispatch.index('if(adlpLoadOnly)') struct IOService {}; struct XE { + bool tglGT2Detected=false,tglGT2Ready=false; bool display12SmallGTDetected=false,display12SmallGTReady=true; bool adlsDisplaySelected=true,adlsDisplayHooksReady=true,adlsBootConsumersFailed=false; static bool wrapTGLAcceleratorStart(IOService*,IOService*); diff --git a/tests/adls_clock_runtime.py b/tests/adls_clock_runtime.py index 72ea1b08..a9dac3da 100644 --- a/tests/adls_clock_runtime.py +++ b/tests/adls_clock_runtime.py @@ -8,6 +8,10 @@ functions='\n'.join(re.search(r'(?:bool|void) XE::'+n+r'\(.*?\n\}',s,re.S).group() for n in ['comboBeginClock','comboEndClock']) code=r''' #include "kern_adls_clock.hpp" +#include "kern_dsi_restart.hpp" +#include "kern_dsi_panel.hpp" +#include "kern_display13_fb_memory.hpp" +#include "kern_display13_transcoder.hpp" #include #include #include @@ -18,6 +22,7 @@ void *current_thread(){return reinterpret_cast(thread);} unsigned refs=0,getCount=0,putCount=0,reads=0; std::map mmio; +struct {Display13Transcoder::Bindings transcoders;XELP::DSI::Readout bootDSI;} display13FBPower; alignas(8) uint8_t ctl[0xf00]{},port[0x700]{},access[0x80]{},power[0x100]{}; uint32_t config[6]{}; templateT &at(void *p,unsigned off){return *reinterpret_cast(static_cast(p)+off);} @@ -30,6 +35,9 @@ void putPG(void *p,uint32_t pg){assert(p==power && pg==1 && refs);--refs;++putCount;} struct XE { bool adlsDisplaySelected=true,display13FramebufferRequested=false; + bool dsiStopAttempted=false,dsiIRQParked=false,dsiResourcesReady=false; + XELP::DSI::Quiesce::Result dsiStop;XELP::DSI::Panel::Shutdown dsiPanelStop; + struct {bool sleeping=false,failed=false;XELP::DSI::Restart::State host;} dsiRuntimePower; struct Native { bool ready=true;bool complete(){return ready;} void (*getPG)(void*,uint32_t)=::getPG,(*putPG)(void*,uint32_t)=::putPG; @@ -83,7 +91,27 @@ XE::comboEndClock(); assert(!refs && !lock.depth); } - puts("PASS production common Combo clock scope: core power, nested rejection, thread/access isolation, failed-enable reservation and balanced teardown"); + // Production transaction discovers/pins DSI's PLL1 before selecting B's + // clock, even with both DDI gates closed. No donor call may own DSI PHY A. + comboClockTransaction.allocation={};config[0]=1;at(access,0x60)=0x1000000; + display13FBPower.transcoders.dsi0Pipe=0; + mmio={{0x45404,0xFC0F},{0x51004,0x40000000},{0x164280,0x1E07C01},{0x46010,0},{0x46014,0xCC000000}, + {0x7B008,0xC0000000},{0x6B400,0x80030006},{0x70008,0},{0x6B030,0x20130300},{0x6B094,1}, + {0x6B404,0},{0x6B034,0},{0x64000,0x80000000},{0x16428C,0x3001FB},{0x164290,0xE84}, + {0x6B000,0x04650437},{0x6B008,0x044F044D},{0x6B00C,0x07D1077F},{0x6B014,0x07A907A7}}; + display13FBPower.bootDSI=XELP::DSI::decodeReadout([&](uint32_t r,uint32_t &v){assert(mmio.count(r));v=mmio.at(r);return true;},38400); + assert(XE::comboBeginClock(ctl,port,true,keep) && comboClockTransaction.translation.pll==0); + XE::comboEndClock();assert(!refs && !lock.depth); + assert(comboClockTransaction.allocation.external[0]==reinterpret_cast(ctl)); + assert(!comboClockTransaction.allocation.reset()); + config[0]=0;assert(!XE::comboBeginClock(ctl,port,true,keep) && !XE::comboBeginClock(ctl,port,false,keep)); + alignas(8) uint8_t otherPort[0x700]{};uint32_t otherConfig[6]{1};at(otherPort,0x548)=otherConfig; + mmio[0x164280]&=~0x80CU;mmio[0x46010]=0xCC000000; + assert(!XE::comboBeginClock(ctl,port,true,keep,otherPort)); // sharing also cannot overwrite DSI's target + assert(comboClockTransaction.allocation.binding[0]==2); + config[0]=1;mmio[0x16428C]^=0x400;assert(!XE::comboBeginClock(ctl,port,true,keep)); + assert(!refs && !lock.depth && getCount==putCount); + puts("PASS production common Combo clock scope: balanced power, failed-enable retention, DSI source revalidation and external PLL ownership"); } '''.replace('STATE',state).replace('FUNCTIONS',functions) with tempfile.TemporaryDirectory(prefix='adls-clock-runtime-') as d: diff --git a/tests/boot_arguments.py b/tests/boot_arguments.py index b5fe488d..36702c6a 100644 --- a/tests/boot_arguments.py +++ b/tests/boot_arguments.py @@ -15,7 +15,8 @@ external = {'-igfxdvmt', 'gfxrst', '-allow3d', '-disablegfxfirmware'} used = set() for directory in ['WhateverXe', 'AppleIntelTGLCompat']: - for path in (root / directory).glob('*.cpp'): + for path in (root / directory).iterdir(): + if path.suffix not in {'.cpp', '.inc'}: continue used.update(re.findall(r'(?:checkKernelArgument|PE_parse_boot_argn)\("([^"]+)"', path.read_text())) for array, expected in [('Off', '-wexoff'), ('Debug', '-wexdbg'), ('Beta', '-wexbeta')]: body = re.search(r'bootarg' + array + r'\[\] \{(.*?)\}', start, re.S).group(1) @@ -64,6 +65,7 @@ static struct { void onKextLoadForce(int *) {} } lilu; struct XE { XELP::Profile xeProfile; + bool tglGT2Detected=false,tglGT2Ready=false; bool adlnRequested=false, adlnDetected=false, adlnKernelReady=false, adlnNativeRequested=false, adlsDisplaySelected=false, display12SmallGTDetected=false; bool display13FramebufferRequested=false, display13TraceEnabled=false, adlpDisplaySelected=false, adlpRenderRequested=false; bool display12SmallGTReady=false, adlsDisplayHooksReady=false; @@ -99,6 +101,12 @@ assert(wrapTGLAcceleratorStartGate()==ready); assert(wrapTGLAcceleratorProbeGate()==ready); } + for(unsigned bits=0;bits<4;++bits) { + XE selected;selected.tglGT2Detected=bits&1;selected.tglGT2Ready=bits&2;callbackXE=&selected; + assert(wrapTGLFramebufferStartGate()); + assert(wrapTGLAcceleratorStartGate()==(!(bits&1) || (bits&2))); + assert(wrapTGLAcceleratorProbeGate()==(!(bits&1) || (bits&2))); + } // Every implemented ADL-N/Twin ID enables the qualified normal path without args. XE xe; for(uint32_t pci:{0x46D08086U,0x46D18086U,0x46D28086U,0x46D38086U,0x46D48086U}) { @@ -145,7 +153,7 @@ } // Exercise the complete production selection, including the shared Display // adapter on registered desktop products and the still-unimplemented ADL-P. - for(auto kernel:allKernels)for(auto id:{0x9A49U,0x9A78U,0x4690U,0x4680U,0xA780U,0x4688U,0x468BU,0xA788U,0xA78BU,0x9A60U,0x9A68U,0x9A70U,0x46A6U,0x46D2U}) + for(auto kernel:allKernels)for(auto id:{0x9A49U,0x9A78U,0x9A59U,0x9AC0U,0x9AC9U,0x9AD9U,0x9AF8U,0x4690U,0x4680U,0xA780U,0x4688U,0x468BU,0xA788U,0xA78BU,0x9A60U,0x9A68U,0x9A70U,0x46A6U,0x46D2U}) for(auto rev:{0U,4U,12U,255U})for(unsigned option=0;option<4;++option) { version=kernel;args={}; if(option&1)args.insert("-wex-tgl-off"); @@ -153,7 +161,8 @@ XE selected;selected.init();selected.selectProfile(XELP::identify((id<<16)|0x8086,rev)); const bool desktop=id==0x4690 || id==0x4680 || id==0xA780 || id==0x4688 || id==0x468B || id==0xA788 || id==0xA78B; const bool gt1=id==0x9A60 || id==0x9A68 || id==0x9A70; - const bool mobile=id==0x9A49 || id==0x9A78 || gt1; + const bool mobile=XELP::tigerLakeGT2Device((id<<16)|0x8086) || gt1; + assert(selected.tglGT2Detected==(mobile && !gt1)); const bool mobile13=id==0x46A6 || id==0xA7A0; const bool small13=id==0x46D2; const bool render13=small13 || (mobile13 && !(option&2)); diff --git a/tests/boot_stack_limits.py b/tests/boot_stack_limits.py index 168de087..bf5fd772 100644 --- a/tests/boot_stack_limits.py +++ b/tests/boot_stack_limits.py @@ -24,6 +24,7 @@ def rejects(paths): '_ZN2XE27wrapAlderLakeNSetupWorkLoopEP9IOServiceP10IOWorkLoop':2440, '_ZN12_GLOBAL__N_124display13FBInspectMemoryEP9IOServicebjb':2408, '_ZN2XE19wrapDisplay13TCModeEP8OSObjectj':1816, + '_ZN2XE20display13WMArmUpdateEP9IOService':3432, '_ZL15tglHostCSBFatalPKcPKjPv':6504, '_ZN2XE21wrapTGLGuCHeaderPanicEPv':3992, '_ZN13Display13Boot9normalizeI_fixture':2328, @@ -48,4 +49,4 @@ def rejects(paths): try:module.stack_files([obj]) except ValueError:pass else:raise AssertionError('missing report for emitted code accepted') -print('PASS all-TU frame limits: unknown/dynamic frames, 7 old large frames, duplicate bodies, missing records') +print('PASS all-TU frame limits: unknown/dynamic frames, 8 old large frames, duplicate bodies, missing records') diff --git a/tests/device_selection.cpp b/tests/device_selection.cpp index f31fd319..ced500d3 100644 --- a/tests/device_selection.cpp +++ b/tests/device_selection.cpp @@ -25,7 +25,7 @@ int main() { assert(!strcmp(model(f),entry.model)); auto nonIntel=select(0x123410DE,0,true,&value,4);assert(!nonIntel.admitted()); } - assert(implemented==41); // Recognition of other table rows adds no native backend. + assert(implemented==60); // Recognition of other table rows adds no native backend. uint32_t id=0x46D2; assert(select(0x12348086,0,true,&id,4).admitted()); assert(select(0x12348086,1,true,&id,4).admitted()); diff --git a/tests/display13_agdc.py b/tests/display13_agdc.py index f6d2c71a..347bb3cc 100644 --- a/tests/display13_agdc.py +++ b/tests/display13_agdc.py @@ -47,6 +47,7 @@ uint32_t framebufferProtocolCalls=0;void *framebufferStartedController=nullptr; uintptr_t orgFramebufferUserClient=0,orgFramebufferAGDCCallback=0; bool processFramebufferAGDC(KernelPatcher &,size_t,uintptr_t,size_t); + bool processDisplay13TrialAGDC(KernelPatcher &,size_t,uintptr_t,size_t){return true;} static uint32_t wrapFramebufferUserClient(IOService *,void *,void *,uint32_t,void **); static uint32_t wrapFramebufferAGDCCallback(IOService *,uint32_t,unsigned long,uint32_t); static void observeFramebufferUserClient(IOService*,uint32_t,void**,uint32_t){}; diff --git a/tests/display13_aux_fixture.cpp b/tests/display13_aux_fixture.cpp index 8852799c..fba40ad3 100644 --- a/tests/display13_aux_fixture.cpp +++ b/tests/display13_aux_fixture.cpp @@ -2,6 +2,7 @@ // completion and kernel imports are mocked; optional native callers execute // their original get/put, retry, buffer-copy and result paths. bool failGet=false,failPut=false,failCommand=false;unsigned auxCalls=0,auxResult=0; +bool disconnectDuringCommand=false,loseTCIdentity=false; void getAux(void *power,unsigned ddi){ assert(!locked&&power==display13FBPower.power);auto &c=display13FBPower;Display13FBPowerIO io{c.map}; if(failGet)c.map->drop=0x45444; @@ -26,6 +27,8 @@ unsigned auxTransport(void *port,unsigned,uint32_t *packet,unsigned,bool){ assert(c.bridge.putAUX(io,ddi).status==Display13Power::Status::Complete&&c.bridge.auxBusy()); packet[0]=0;packet[1]=0x12345678; if(failCommand)c.map->regs[0x45444]=0; + if(disconnectDuringCommand)tcConnected=false; + if(loseTCIdentity)tcOwned=false; return auxResult; } unsigned syntheticFront(void *port,unsigned,void *data,unsigned){ @@ -48,6 +51,7 @@ void auxSetup(Fixture &f,Map &map,unsigned ddi,bool hdmi,unsigned mode){ state.orgDisplay13ReadAUX=state.orgDisplay13WriteAUX=reinterpret_cast(syntheticFront); state.orgDisplay13I2CAUX=reinterpret_cast(syntheticI2C);state.orgDisplay13RunAUX=reinterpret_cast(auxTransport); failGet=failPut=failCommand=false;auxCalls=auxResult=0; + disconnectDuringCommand=loseTCIdentity=false; // Probe must work after all DDI/pipe owners are gone. Display13FBPowerIO io{&map};assert(c.bridge.putDDI(io,ddi).status==Display13Power::Status::Complete); assert(c.bridge.putPG(io,1).status==Display13Power::Status::Complete); @@ -66,6 +70,37 @@ int main(int argc,char **argv){ for(unsigned at:{0x1CEABU,0x1D19CU})redirect(image,at,0x300040,reinterpret_cast(stackFail)); } unsigned cases=0; + // A selected DSI path must never reach native AUX get/put or HAL, even if + // its port object has the donor's AUX-A register coordinates initialized. + {Fixture f(0,0,0);Map map;auxSetup(f,map,0,false,1); + state.adlnOutputWiring.dp&=~1U;state.adlnOutputWiring.panel=0;state.adlnOutputWiring.dsi=1; + assert(Display13FBTopology::apply(f.platform,sizeof(f.platform),f.donor,state.adlnOutputWiring)); + const auto reads=map.reads.size(),writes=map.writes.size();uint32_t data[5]{0xabcdef01}; + assert(XE::wrapDisplay13ReadAUX(f.port,0x100,data,4)==kIOReturnNotReady); + assert(XE::wrapDisplay13WriteAUX(f.port,0x100,data,4)==kIOReturnNotReady); + assert(XE::wrapDisplay13I2CAUX(f.port,0,data,4,0)==kIOReturnNotReady); + assert(XE::wrapDisplay13RunAUX(f.port,0,data,4,false)==kIOReturnNotReady); + assert(!auxCalls && data[0]==0xabcdef01 && map.reads.size()==reads && map.writes.size()==writes); + assert(!display13FBPower.bridge.state.faulted && !display13FBPower.bridge.nativeAUXRefs(0)); + } + // Exact production completion checks: a disconnect during AUX is a failed + // transaction, not a permanently faulted bridge. Lost ownership still is. + for(unsigned ddi=3;ddi<=6;++ddi)for(bool lost:{false,true})for(unsigned nativeError:{0U,0xE00002BCU}) { + Fixture f(0,0,ddi);Map map;auxSetup(f,map,ddi,false,3);auto &c=display13FBPower; + tcOwned=true;disconnectDuringCommand=true;loseTCIdentity=lost;auxResult=nativeError; + uint32_t data[5]{}; + assert(XE::wrapDisplay13ReadAUX(f.port,0x202,data,4)==(nativeError?nativeError:kIOReturnNotReady)); + assert(auxCalls==1 && !c.bridge.auxBusy() && c.bridge.state.faulted==lost); + assert(!data[0]); + if(!lost) { + assert(!c.bridge.nativeAUXRefs(ddi)); + const auto calls=auxCalls; + assert(XE::wrapDisplay13ReadAUX(f.port,0x202,data,4)==kIOReturnNotReady && auxCalls==calls); + tcConnected=true;disconnectDuringCommand=false;auxResult=0; + assert(!XE::wrapDisplay13ReadAUX(f.port,0x202,data,4) && data[0]==0x12345678); + assert(!c.bridge.state.faulted && !c.bridge.nativeAUXRefs(ddi)); + } + } for(unsigned ddi=0;ddi<2;++ddi)for(bool hdmi:{false,true})for(unsigned mode:{0U,1U}) for(unsigned op=0;op<3;++op)for(unsigned fault=0;fault<5;++fault){ Fixture f(0,0,ddi);Map map;auxSetup(f,map,ddi,hdmi,mode);auto &c=display13FBPower; diff --git a/tests/display13_boot.cpp b/tests/display13_boot.cpp index 8580df4f..3a70ffdb 100644 --- a/tests/display13_boot.cpp +++ b/tests/display13_boot.cpp @@ -160,6 +160,7 @@ struct Runtime : IO { } bool inheritedVerified(uintptr_t,uintptr_t,unsigned){return false;} bool powerHeld(){return mode.accessLease.owner && mode.auxLease.owner && Display13Power::held(*this,power,mode.auxLease.domains);} + bool retainedOwned(unsigned d){return owned(d);} bool owned(unsigned d){return d==port.ddi && mode.phase==Display13TC::Mode::Phase::Active && mode.ownership.active && native==4;} bool quiescent(unsigned d){return d==port.ddi && !(read(port.buffer)&0x80000000U);} bool quiescent(uintptr_t,uintptr_t,unsigned d){return quiescent(d) && !(read(port.pll)&0xcc000000U) && !ddi.owner;} @@ -297,6 +298,29 @@ static void unusedTCBoot() { assert(io.writes.size()==1 && io.writes[0].offset==0x64300 && io.writes[0].value==0x80); auto again=Display13Boot::releaseUnusedTC(io,AlderLakeN::readDisplay(io,true),true,w); assert(again.status==Display13Boot::Status::Complete && again.writes==0); + // DSI-only GOP firmware can leave DP-alt ownership with no TC output. + {IO t;auto wiring=setup(t);wiring.hdmi&=~8U;wiring.ddc[3]=0;wiring.dp=wiring.typeC=8; + t.regs[0x161500]=0x1d;const auto before=t.regs; + const auto released=Display13Boot::releaseUnusedTC(t,AlderLakeN::readDisplay(t,true),true,wiring); + assert(released.status==Display13Boot::Status::Complete && released.cleared==8 && released.writes==1); + for(auto x:before)if(x.first!=0x64300)assert(t.regs[x.first]==x.second); + t.regs[0x64300]=0xc0;t.regs[0x161500]=6;t.writes.clear(); + assert(Display13Boot::releaseUnusedTC(t,AlderLakeN::readDisplay(t,true),true,wiring).status!=Display13Boot::Status::Complete && t.writes.empty());} + // Firmware may leave the idle port mux ungated after the last OS used it. + for(unsigned alt=0;alt<2;++alt) { + IO t;auto wiring=setup(t); + if(alt){wiring.hdmi&=~8U;wiring.ddc[3]=0;wiring.dp=wiring.typeC=8;t.regs[0x161500]=0x1d;} + t.regs[Display13TC::Clock::Gate]&=~(1U<<12);const auto before=t.regs; + const auto released=Display13Boot::releaseUnusedTC(t,AlderLakeN::readDisplay(t,true),true,wiring); + assert(released.status==Display13Boot::Status::Complete && released.cleared==8 && released.writes==2); + assert(t.writes[0].offset==Display13TC::Clock::Gate && t.writes[0].value==(before.at(Display13TC::Clock::Gate)|(1U<<12))); + assert(t.writes[1].offset==0x64300 && t.writes[1].value==0x80); + for(auto x:before)if(x.first!=0x64300 && x.first!=Display13TC::Clock::Gate)assert(t.regs[x.first]==x.second); + IO failed;auto fw=setup(failed);failed.regs[Display13TC::Clock::Gate]&=~(1U<<12); + failed.failWrite=Display13TC::Clock::Gate; + const auto rejected=Display13Boot::releaseUnusedTC(failed,AlderLakeN::readDisplay(failed,true),true,fw); + assert(rejected.status==Display13Boot::Status::Readback && failed.regs[0x64300]==0xc0 && failed.writes.size()==1); + } // Any live/ambiguous resource prevents release of this unclassified port. for(unsigned fault=0;fault<9;++fault) { IO t;auto wiring=setup(t); @@ -305,7 +329,7 @@ static void unusedTCBoot() { if(fault==2)t.regs[0x161500]|=1; if(fault==3)t.regs[0x4610C]=0x80000000; if(fault==4)t.regs[0x46038]=0xCC000000; - if(fault==5)t.regs[Display13TC::Clock::Gate]&=~(1U<<12); + if(fault==5)t.regs[Display13TC::Clock::Gate]=UINT32_MAX; if(fault==6)t.regs[0x64310]=0x80000000; if(fault==7)t.regs[0x64300]=UINT32_MAX; if(fault==8)t.regs[0x161500]=UINT32_MAX; diff --git a/tests/display13_clock_set.cpp b/tests/display13_clock_set.cpp index 67719263..e5c8b7a5 100644 --- a/tests/display13_clock_set.cpp +++ b/tests/display13_clock_set.cpp @@ -43,6 +43,15 @@ int main() { assert(select(38400,0,true)==1305600000 && select(24000,0,true)==1296000000); assert(select(38400,345600000,false)==358400000 && select(38400,358400000,false)==384000000); assert(!select(38400,1305600000,false) && !select(0,0,true)); + // DSI firmware clock (38.4 MHz reference, /3) to the external-output + // maximum (/2): prepare only in the stopped bootstrap interval. + for (unsigned active : {0U,1U}) { + IO io;io.regs[DSSM]=0x40000000;io.regs[PLL]=0xC000000E;io.regs[Control]=0x780164; + auto r=set(io,1305600000,{true,true,active}); + assert(r.before.nativeHz()==358400000); + if(active)assert(r.status==ChangeStatus::ActiveDividerChange && io.writes.empty()); + else assert(r.status==ChangeStatus::Complete && r.after.nativeHz()==1305600000 && r.after.divider==2); + } {IO io;auto r=stop(io,{false,false,0});assert(r.status==ChangeStatus::OwnershipRequired && io.writes.empty());} {IO io;auto r=stop(io,ready);assert(r.status==ChangeStatus::ActiveDividerChange && io.writes.empty());} {IO io;auto r=stop(io,{true,false,0});assert(r.status==ChangeStatus::Complete && r.after.status==Status::Disabled && diff --git a/tests/display13_cursor.cpp b/tests/display13_cursor.cpp index eb337245..d18b6bfc 100644 --- a/tests/display13_cursor.cpp +++ b/tests/display13_cursor.cpp @@ -35,6 +35,14 @@ int main() { IO a;auto r=stop(a);assert(r.status==Status::Complete && r.waitUs==17000 && r.frameAfter==11); assert((a.writes==std::vector{0x70080,0x70088,0x70084}));assert(!a.regs[0x70084]); IO off;off.regs[0x70008]=0;r=stop(off);assert(r.status==Status::Complete && !r.waitUs && r.writes==3); + // PIPECONF=0 on DSI is not evidence of stopped scanout. Cursor retirement + // still needs the separately bound pipe's frame to advance. + {IO io;io.regs[0x70008]=0;io.regs[0x7B008]=0xC0000000;io.regs[0x6B400]=0x80000006; + r=stop(io,0,{0});assert(r.status==Status::Complete && r.waitUs==17000); + IO frozen;frozen.regs=io.regs;frozen.regs[0x70080]=0x27;frozen.frozen=true; + assert(stop(frozen,0,{0}).status==Status::Timeout); + IO wrong;wrong.regs[0x70008]=0;wrong.regs[0x7B008]=0xC0000000;wrong.regs[0x6B400]=0x80005006; + assert(stop(wrong,0,{0}).status==Status::Readback && wrong.writes.empty());} IO empty;empty.regs[0x70080]=0;r=stop(empty);assert(r.status==Status::Complete && empty.writes.empty()); IO busy;busy.regs[0x70C08]=1;r=stop(busy);assert(r.status==Status::Conflict && busy.writes.empty()); IO power;power.regs[0x45404]=3;r=stop(power);assert(r.status==Status::Power && power.writes.empty()); diff --git a/tests/display13_cursor_resize.py b/tests/display13_cursor_resize.py new file mode 100644 index 00000000..0af6901a --- /dev/null +++ b/tests/display13_cursor_resize.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +"""Replay production WM preparation while CURCNTR still holds the old size.""" +from pathlib import Path +import re, subprocess, tempfile +r=Path(__file__).resolve().parents[1] +s=(r/'WhateverXe/kern_xe.cpp').read_text() +body=re.search(r'void XE::adlnWMSetup\(.*?\n\}',s,re.S).group() +code=(r/'tests/display13_cursor_resize_fixture.cpp').read_text().replace('// PRODUCTION_SETUP',body) +with tempfile.TemporaryDirectory(prefix='cursor-resize-') as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-g','-fsanitize=address,undefined','-I'+str(r/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/display13_cursor_resize_fixture.cpp b/tests/display13_cursor_resize_fixture.cpp new file mode 100644 index 00000000..3bb1f58f --- /dev/null +++ b/tests/display13_cursor_resize_fixture.cpp @@ -0,0 +1,88 @@ +#include "kern_display13_fb_memory.hpp" +#include "kern_display13_dbuf.hpp" +#include "kern_display13_wm.hpp" +#include +#include +#include +#include +#include +using IOLock=int; +static unsigned locks; +void IOLockLock(int*){assert(!locks);++locks;} +void IOLockUnlock(int*){assert(locks);--locks;} +struct OSObject {unsigned size;OSObject *getMetaClass(){return this;}unsigned getClassSize(){return size;}}; +using IOService=OSObject; +struct Map {std::map r;}; +struct Display13FBDBUFIO {Map *map;uint32_t read(uint32_t reg){assert(map->r.count(reg));return map->r[reg];}}; +static unsigned selectedPipe; +namespace TGLFBBinding { + enum class Status {Unavailable,Complete}; + struct Object {void *p;unsigned size;}; + struct Objects {Object controller,fb,path,plane,port;unsigned owner;bool logical;}; + struct Result {Status status;unsigned framebuffer,pipe,ddi,plane;}; + Result inspect(const Objects&,void*,unsigned){return {Status::Complete,selectedPipe,selectedPipe,selectedPipe,0};} +} +struct Pipe {IOService *memoryFramebuffer{};uint64_t watermarkClockHz{};Display13WM::Plan wm[2];}; +struct Power { + int *lock{};IOService *controller{};Map *map{}; + bool engineEnabled{},clockFaulted{},memoryFaulted{},watermarkFaulted{}; + struct {struct {bool faulted{};}state;}bridge; + Display13DBUF::State dbuf;Display13WM::Latency latency;Pipe pipe[3]; +}display13FBPower; +struct IORegistryEntry { + std::map> records; + static IORegistryEntry *getRegistryRoot(){static IORegistryEntry root;return &root;} + void setProperty(const char *name,const void *p,unsigned size){records[name]=std::vector((const uint8_t*)p,(const uint8_t*)p+size);} + void *getProperty(const char *name){return records.count(name)?this:nullptr;} +}; +unsigned display13FBExpectedOwner(void*){return 0;} +bool dsiLogicalSurface(void*){return true;} +void adlnFault(bool &f,unsigned){f=true;} +struct XE { + bool display13FBWatermarkHooksReady=true;void *adlnPlatform{};unsigned adlnOutputWiring=0; + uintptr_t adlnAdjustPixelClock{}; + static bool dsiSharedWriterStopped(void*,unsigned){return false;} + static void adlnWMSetup(OSObject*,OSObject*,bool); +}state,*callbackXE=&state; +uint64_t adjust(void*,void*,void*,bool cursor,void*){assert(cursor);return 135255000;} +templatevoid put(void *p,unsigned off,T v){memcpy((uint8_t*)p+off,&v,sizeof(v));} +// PRODUCTION_SETUP +alignas(16) static uint8_t ctl[0x1c20],fb[0x4b90],path[0x3730],plane[0x1c8]; +static int mutex; +void setup(Map &map,unsigned pipe,uint32_t oldControl,uint32_t width){ + memset(ctl,0,sizeof(ctl));memset(fb,0,sizeof(fb));memset(path,0,sizeof(path));memset(plane,0,sizeof(plane)); + ((OSObject*)ctl)->size=sizeof(ctl);((OSObject*)fb)->size=sizeof(fb); + ((OSObject*)path)->size=sizeof(path);((OSObject*)plane)->size=sizeof(plane); + selectedPipe=pipe;display13FBPower={};auto &c=display13FBPower; + c.lock=&mutex;c.controller=(IOService*)ctl;c.map=↦c.engineEnabled=true;c.dbuf.begun=true; + c.pipe[pipe].memoryFramebuffer=(IOService*)fb; + c.dbuf.pipe[pipe].cursor=pipe?Display13DBUF::Range{2048,2088}:Display13DBUF::Range{0,40}; + c.latency=Display13WM::decodeLatency(0x63503300,0x00909090,30); + assert(c.latency.status==Display13WM::Status::Complete); + put(plane,0x68,(IOService*)fb);put(plane,0x100,0x94001000U); + put(fb,0x4278,width);put(path,0x13A,uint16_t(1125)); + map.r={{0x45404,0xFC0F},{0x45004,8},{0x70080+0x1000*pipe,oldControl}}; + state.adlnAdjustPixelClock=(uintptr_t)adjust;IORegistryEntry::getRegistryRoot()->records.clear(); +} +int main(){ + const unsigned widths[]={64,128,256},controls[]={0x10000027,0x10000022,0x10000023}; + for(unsigned p=0;p<3;++p)for(unsigned old=0;old<3;++old)for(unsigned next=0;next<3;++next){ + Map map;setup(map,p,controls[old],widths[next]);auto before=map.r; + XE::adlnWMSetup((OSObject*)plane,(OSObject*)path,true); + auto &c=display13FBPower;const auto &plan=c.pipe[p].wm[1]; + assert(!c.watermarkFaulted && plan.status==Display13WM::Status::Complete && plan.width==widths[next]); + assert(map.r==before); // Preparation does not change CURCNTR to make the check pass. + const auto expected=Display13WM::plan(widths[next],1126,135255000,40,c.latency,true,false,widths[next]); + assert(plan.normal[0].encoded()==expected.normal[0].encoded()); + // After application the old size must still fail the strict match. + assert(Display13WM::cursorMatches(controls[old],plan.width)==(old==next)); + assert(Display13WM::cursorMatches(controls[next],plan.width)); + } + for(unsigned invalid:{0U,32U,512U}){ + Map map;setup(map,0,0x10000027,invalid);XE::adlnWMSetup((OSObject*)plane,(OSObject*)path,true); + assert(display13FBPower.watermarkFaulted); + } + Map map;setup(map,0,0x10000027,256);display13FBPower.dbuf.pipe[0].cursor={0,0}; + XE::adlnWMSetup((OSObject*)plane,(OSObject*)path,true);assert(display13FBPower.watermarkFaulted); + assert(!locks);puts("production cursor WM: 27 size transitions, post-apply mismatch and invalid input checks passed"); +} diff --git a/tests/display13_dbuf.cpp b/tests/display13_dbuf.cpp index 7439e7a0..6551e7eb 100644 --- a/tests/display13_dbuf.cpp +++ b/tests/display13_dbuf.cpp @@ -31,6 +31,19 @@ struct IO { void delayUs(unsigned us) {delays+=us;} }; int main() { + for(unsigned failure=0;failure<5;++failure) { + IO io;State state;Display13Transcoder::Bindings dsi;dsi.dsi0Pipe=0; + io.r[0x70008]=0;io.r[0x7B008]=0xC0000000;io.r[0x6B400]=0x80030006; + assert(begin(io,state,0,dsi)==Status::Complete && !io.writes); + assert(begin(io,state)==Status::Topology); // cannot erase the active mapping + assert(singlePipeLayout(io,state).active && pipeLayout(io,state,0).active); + if(failure==0)assert(stopPipe(io,state,0)==Status::Conflict && state.pipe[0].consumers); + if(failure==1)assert(stop(io,state)==Status::Conflict && state.pipe[0].consumers); + if(failure==2){io.r[0x6B400]|=5U<<12;assert(singlePipeLayout(io,state).status==Status::Topology);} + if(failure==3){io.r[0x7B008]=0x80000000;assert(pipeLayout(io,state,0).status==Status::Conflict);} + if(failure==4){io.r[0x7B008]=0;io.r[0x6B400]=0;io.r[0x70180]=0; + assert(stopPipe(io,state,0)==Status::Complete && !state.pipe[0].consumers);} + } // Independent Linux/MBUS examples: absolute ownership and relative plane // registers intentionally differ on B, while A remains byte-identical. State address; diff --git a/tests/display13_dbuf_setup.py b/tests/display13_dbuf_setup.py new file mode 100644 index 00000000..e6b554c6 --- /dev/null +++ b/tests/display13_dbuf_setup.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +"""Exercise the production allocation adapter with real DBUF ownership.""" +from pathlib import Path +from production_source import read_source +import re, subprocess, tempfile +r = Path(__file__).resolve().parents[1] +s = read_source(r/'WhateverXe/kern_xe.cpp') +guard = re.search(r'bool XE::dsiSharedWriterStopped\(.*?\n\}', s, re.S).group() +body = re.search(r'void XE::adlnSetupDBUFPipe\(.*?\n\}', s, re.S).group() +pipewm = re.search(r'void XE::wrapAlderLakeNPipeWMSetup\(.*?\n\}', s, re.S).group() +code = (r / 'tests/display13_dbuf_setup_fixture.cpp').read_text().replace('// PRODUCTION_SETUP', guard+'\n'+body+'\n'+pipewm) +with tempfile.TemporaryDirectory(prefix='dbuf-setup-') as d: + p = Path(d) + (p / 'test.cpp').write_text(code) + subprocess.run(['clang++', '-std=c++17', '-O1', '-g', '-fsanitize=address,undefined', + '-I' + str(r / 'WhateverXe'), str(p / 'test.cpp'), '-o', str(p / 'test')], check=True) + subprocess.run([str(p / 'test')], check=True) diff --git a/tests/display13_dbuf_setup_fixture.cpp b/tests/display13_dbuf_setup_fixture.cpp new file mode 100644 index 00000000..2304114f --- /dev/null +++ b/tests/display13_dbuf_setup_fixture.cpp @@ -0,0 +1,170 @@ +#include "kern_display13_fb_memory.hpp" +#include "kern_display13_dbuf.hpp" +#include "kern_display13_wm.hpp" +#include +#include +#include +using IOLock=int;using IORecursiveLock=int; +static unsigned ownerDepth,fbDepth; +void IOLockLock(int*){assert(fbDepth && !ownerDepth);++ownerDepth;} +void IOLockUnlock(int*){assert(ownerDepth);--ownerDepth;} +void IORecursiveLockLock(int*){assert(!ownerDepth);++fbDepth;} +void IORecursiveLockUnlock(int*){assert(fbDepth && !ownerDepth);--fbDepth;} +struct OSObject {unsigned size;OSObject *getMetaClass(){return this;}unsigned getClassSize(){return size;}}; +using IOService=OSObject; +struct Map { + unsigned reads=0;std::map r; + Map() { + r[0x45404]=0xFC0F;r[0x4438C]=0x1C000700; + for(unsigned p=0;p<3;++p){const auto b=p*0x1000; + r[0x70008+b]=p?0:0xC0000000;r[0x70080+b]=r[0x60400+b]=0; + r[0x7027C+b]=p?0:0x07FF0000;r[0x7017C+b]=0; + for(unsigned n=0;n<5;++n)r[0x70180+b+0x100*n]=!p&&!n?0x84000000:0; + for(unsigned n=0;n<3;++n)r[0x70B08+b+0x100*n]=0; + } + for(unsigned i=0;i<4;++i)r[Display13DBUF::regs[i]]=i<2?0xC040C000:0x40C000; + } +}; +struct Display13FBDBUFIO { + Map *map;bool readable(uint32_t reg){return map->r.count(reg);} + uint32_t read(uint32_t reg){assert(readable(reg));++map->reads;return map->r[reg];} + void write(uint32_t reg,uint32_t value){assert(readable(reg));map->r[reg]=(value&~(1U<<30))|((value&(1U<<31))>>1);} + void delayUs(unsigned){} +}; +namespace TGLFBBinding {enum class Status {Unavailable,Complete};struct Readout {Status status{};unsigned pipe{},ddi{};};} +namespace Display13FBTopology {bool multiOutputWiring(unsigned w){return w!=0;}bool selected(void*,uint32_t*,unsigned){return true;}} +struct Pipe { + IOService *memoryFramebuffer{};bool destroyingPath{},wmProgramming{},wmCommitting{}; + unsigned wmPending{};uint64_t watermarkClockHz{};Display13WM::Plan wmProgram[2];Display13WM::Latch wmLatch; +}; +struct Power { + int *lock{};IOService *controller{};Map *map{};void *power{}; + bool engineEnabled{},clockStopped{},memoryFaulted{},clockFaulted{},watermarkFaulted{}; + struct Bridge {bool bootAdopted{},begun{};struct {bool suspended{},faulted{};}state;}bridge; + struct {unsigned domains{};}engine; + Pipe pipe[3];Display13DBUF::State dbuf; +}display13FBPower; +static bool badBinding,failPrimary,failWM;static unsigned armCalls;static Display13WM::Latch nextLatch; +static std::map> records; +struct IORegistryEntry {static IORegistryEntry *getRegistryRoot(){static IORegistryEntry root;return &root;}void setProperty(const char*,void*,unsigned){}}; +void adlnFault(bool &f,unsigned,unsigned=0){f=true;} +void adlnPublishMemory(const char *name,unsigned p,void *data,unsigned bytes,bool=false){ + if(!strcmp(name,"N97FBDBUFLayout"))records[p]=std::vector((uint64_t*)data,(uint64_t*)data+bytes/8); +} +TGLFBBinding::Readout adlnInspectPathBinding(IOService *fb,OSObject *path,void*,unsigned){ + const auto p=Display13FBMemory::boundPipe(display13FBPower.pipe,fb); + return badBinding||!path?TGLFBBinding::Readout{}:TGLFBBinding::Readout{TGLFBBinding::Status::Complete,p,3+p}; +} +static IOService *dsiFB=nullptr; +bool dsiModeTarget(IOService *fb){return fb && fb==dsiFB;} +struct XE { + bool display13FBDBUFHooksReady=true,display13FBWatermarkHooksReady=true;struct {unsigned dsi=0;operator unsigned()const{return 0x78;}}adlnOutputWiring;void *adlnPlatform{}; + bool dsiStopAttempted=false; + uintptr_t adlnAdjustPixelClock=0; + static bool dsiSharedWriterStopped(IOService*,unsigned); + static void wrapAlderLakeNPipeWMSetup(IOService*,IOService*,OSObject*,void*); + static void adlnSetupDBUFPipe(IOService*,IOService*,unsigned,bool); + static void wrapAlderLakeNEngineEnable(void*){assert(false);} + static void wrapAlderLakeNPrimaryDBUF(OSObject*,unsigned,unsigned,bool); + static void wrapDisplay13CursorDBUF(OSObject*); + static void adlnWMEnable(OSObject*,bool,bool){if(failWM)display13FBPower.watermarkFaulted=true;} + static void wrapDisplay13WMArmUpdate(IOService*); +}state,*callbackXE=&state; +templatevoid put(void *p,unsigned off,T v){memcpy(static_cast(p)+off,&v,sizeof(v));} +unsigned planePipe(OSObject *plane){return Display13FBMemory::boundPipe(display13FBPower.pipe,Display13FBMemory::field(plane,0x68));} +void XE::wrapAlderLakeNPrimaryDBUF(OSObject *plane,unsigned start,unsigned end,bool){ + assert(fbDepth&&!ownerDepth);auto &c=display13FBPower;const auto p=planePipe(plane);Display13FBDBUFIO io{c.map}; + if(failPrimary){c.dbuf.faulted=true;return;} + const auto next=Display13DBUF::toAbsolute(c.dbuf,p,{start,end+1}); + assert(Display13DBUF::preparePrimary(io,c.dbuf,next.start,next.end-1,p)==Display13DBUF::Status::Complete); + c.map->r[0x7027C+0x1000*p]=Display13DBUF::pendingHardwareRaw(c.dbuf,p); + assert(Display13DBUF::finishPrimary(io,c.dbuf,p)==Display13DBUF::Status::Complete); +} +void XE::wrapDisplay13CursorDBUF(OSObject *plane){ + assert(fbDepth&&!ownerDepth);auto &c=display13FBPower;const auto p=planePipe(plane);Display13FBDBUFIO io{c.map}; + assert(Display13DBUF::prepareCursor(io,c.dbuf,p)==Display13DBUF::Status::Complete); + c.map->r[0x7017C+0x1000*p]=Display13DBUF::pendingHardwareRaw(c.dbuf,p); + assert(Display13DBUF::finishCursor(io,c.dbuf,p)==Display13DBUF::Status::Complete); + c.pipe[p].wmPending|=2; +} +void XE::wrapDisplay13WMArmUpdate(IOService *fb){ + assert(fbDepth&&!ownerDepth);++armCalls;auto &c=display13FBPower;const auto p=Display13FBMemory::boundPipe(c.pipe,fb); + c.pipe[p].wmLatch=nextLatch; + if(nextLatch.status==Display13WM::Status::Complete){ + Display13FBDBUFIO io{c.map}; + assert(Display13DBUF::retire(io,c.dbuf,io.read(0x7027C+0x1000*p),io.read(0x7017C+0x1000*p),10,11,p)==Display13DBUF::Status::Complete); + c.pipe[p].wmPending=0;c.pipe[p].watermarkClockHz=614400000; + } +} +IOService *pendingBackingFB=nullptr; +struct {bool modeReady=false;} dsiPendingBacking; +bool dsiPendingTarget(IOService *fb){return fb && fb==pendingBackingFB;} +// PRODUCTION_SETUP +alignas(16) static uint8_t ctl[0x1c20],fb[3][0x4b90],paths[3][0x3730],planes[3][0x1c8]; +static int ownerMutex,frameMutex; +void setup(Map &map){ + memset(ctl,0,sizeof(ctl));memset(fb,0,sizeof(fb));memset(paths,0,sizeof(paths));memset(planes,0,sizeof(planes)); + state={};dsiFB=nullptr;display13FBPower={};auto &c=display13FBPower;c.lock=&ownerMutex;c.controller=(IOService*)ctl;c.map=↦c.engineEnabled=true; + for(unsigned p=0;p<3;++p){ + ((OSObject*)fb[p])->size=sizeof(fb[p]);((OSObject*)planes[p])->size=sizeof(planes[p]); + c.pipe[p].memoryFramebuffer=(IOService*)fb[p];put(fb[p],0x4220,&frameMutex);put(fb[p],0x4A00,1U);put(fb[p],0x4A08,(OSObject*)paths[p]); + put(paths[p],0x32C8,(OSObject*)planes[p]);put(planes[p],0x68,(IOService*)fb[p]);put(planes[p],0x70,(OSObject*)paths[p]); + } + Display13FBDBUFIO io{&map};assert(Display13DBUF::begin(io,c.dbuf,7)==Display13DBUF::Status::Complete); + badBinding=failPrimary=failWM=false;armCalls=0;records.clear();nextLatch={}; +} +int main(){ + using S=Display13WM::Status;using C=Display13WM::LatchCheck; + // The caller really programs reservations, but a prepared primary or a + // delayed live surface must retain them until a later successful arm. + for(auto check:{C::AwaitingScanout,C::AwaitingFlip})for(bool optimized:{false,true})for(unsigned p=0;p<3;++p){ + Map map;setup(map);auto &c=display13FBPower;Display13FBDBUFIO io{&map}; + if(p)assert(Display13DBUF::bindOffPipe(io,c.dbuf,p)==Display13DBUF::Status::Complete); + map.r[0x70008+p*0x1000]=0xC0000000;map.r[0x70180+p*0x1000]=0; + nextLatch.check=check;XE::adlnSetupDBUFPipe(c.controller,(IOService*)fb[p],p,optimized); + assert(!c.dbuf.faulted&&armCalls==1&&c.pipe[p].wmPending&&c.dbuf.pipe[p].unretired&&!c.pipe[p].watermarkClockHz); + assert(records[p][2]==unsigned(Display13DBUF::Status::Complete)); + // Another pipe can allocate without retiring this pipe's reservation. + const auto held=c.dbuf.pipe[p].unretired;unsigned peer=p?0:1; + if(!p)XE::adlnSetupDBUFPipe(c.controller,(IOService*)fb[peer],peer,false); + assert(!c.dbuf.faulted&&c.dbuf.pipe[p].unretired==held); + nextLatch.status=S::Complete;IORecursiveLockLock(&frameMutex);XE::wrapDisplay13WMArmUpdate((IOService*)fb[p]);IORecursiveLockUnlock(&frameMutex); + assert(!c.pipe[p].wmPending&&!c.dbuf.pipe[p].unretired&&c.pipe[p].watermarkClockHz); + } + // A late callback after DSI stop does not touch MMIO, revive the engine, + // allocate a DDB range, or turn an ordinary stop into a shared fault. + for(bool pending:{false,true})for(bool cold:{false,true}) { + Map map;setup(map);auto &c=display13FBPower;state.adlnOutputWiring.dsi=1;state.dsiStopAttempted=!pending; + pendingBackingFB=pending?(IOService*)fb[0]:nullptr; + dsiFB=(IOService*)fb[0]; + if(cold){c.engineEnabled=false;c.clockStopped=true;c.bridge.bootAdopted=c.bridge.begun=true;} + const auto registers=map.r;const auto reads=map.reads;const auto dbuf=c.dbuf; + XE::adlnSetupDBUFPipe(c.controller,(IOService*)fb[0],0,false); + IORecursiveLockLock(&frameMutex);XE::wrapAlderLakeNPipeWMSetup(c.controller,(IOService*)fb[0],(OSObject*)paths[0],nullptr);IORecursiveLockUnlock(&frameMutex); + assert(map.r==registers && map.reads==reads && !armCalls && records.empty()); + assert(!memcmp(&dbuf,&c.dbuf,sizeof(dbuf)) && !c.memoryFaulted && !c.dbuf.faulted && !c.watermarkFaulted); + assert(!ownerDepth && !fbDepth); + if(!cold){ + Display13FBDBUFIO io{&map};assert(Display13DBUF::bindOffPipe(io,c.dbuf,1)==Display13DBUF::Status::Complete); + map.r[0x71008]=0xC0000000;map.r[0x71180]=0; + nextLatch.check=C::AwaitingScanout; + XE::adlnSetupDBUFPipe(c.controller,(IOService*)fb[1],1,false); + assert(armCalls==1 && records.count(1) && !c.dbuf.faulted); + IORecursiveLockLock(&frameMutex);XE::wrapAlderLakeNPipeWMSetup(c.controller,(IOService*)fb[1],(OSObject*)paths[1],nullptr);IORecursiveLockUnlock(&frameMutex); + assert(c.watermarkFaulted); // Peer malformed association still reports its real fault. + } + pendingBackingFB=nullptr; + } + // Generic unavailable results, mismatches, timeouts and allocation failures + // must not be turned into deferred success by the new branch. + for(unsigned fault=0;fault<6;++fault){ + Map map;setup(map);auto &c=display13FBPower; + if(fault==1){nextLatch.status=S::Conflict;nextLatch.check=C::AwaitingScanout;} + if(fault==2){nextLatch.status=S::CommitTimeout;nextLatch.check=C::Frame;} + if(fault==3)failPrimary=true; + if(fault==4)failWM=true; + if(fault==5)badBinding=true; + XE::adlnSetupDBUFPipe(c.controller,(IOService*)fb[0],0,false);assert(c.dbuf.faulted); + } + assert(!ownerDepth&&!fbDepth);puts("production DBUF setup: deferred retirement and failure guards passed"); +} diff --git a/tests/display13_dp_alt_probe.cpp b/tests/display13_dp_alt_probe.cpp new file mode 100644 index 00000000..9398d210 --- /dev/null +++ b/tests/display13_dp_alt_probe.cpp @@ -0,0 +1,36 @@ +#include "kern_display13_firmware.hpp" +#include "kern_xelp_probe.hpp" +#include +#include +#include +struct IO { + std::map regs; + bool readable(uint32_t r){return regs.count(r);} + uint32_t read(uint32_t r){assert(readable(r));return regs[r];} +}; +int main() { + for(unsigned d=3;d<=6;++d) { + XELP::VBT::Ports ports;ports.status=XELP::VBT::Status::Valid;ports.present=2|(1U< +#include +#include +#include +struct IO { + std::map regs; + std::vector> writes; + XELPMemory::Layout memory=XELPMemory::decode(0x3c1,0x7a800000,0); + unsigned time=0,fail=0;bool badPTE=false; + IO():regs{ + {0x45404,0xfc0f}, + {0x45400,0x5405}, + {0x45408,0x40000000}, + {0x4540c,0x5405}, + {0x45450,0x1}, + {0x45454,0x3}, + {0x4545c,0x1}, + {0x45420,0xa5294a1}, + {0x45424,0xe3}, + {0x45444,0x0}, + {0x42000,0x8e380000}, + {0x42084,0x0}, + {0x44300,0xc040c000}, + {0x4438c,0x1c000700}, + {0x45008,0xc040c000}, + {0x4500c,0xffff}, + {0x45010,0x0}, + {0x45014,0x0}, + {0x46000,0x780164}, + {0x46004,0x0}, + {0x164280,0x1e07801}, + {0x164284,0x18001d4}, + {0x164288,0x2644}, + {0x16428c,0x3001fb}, + {0x64000,0x80000000}, + {0x64100,0x80}, + {0x64200,0x80}, + {0x60400,0x30000}, + {0x60000,0x0}, + {0x60004,0x0}, + {0x60008,0x0}, + {0x6000c,0x0}, + {0x60010,0x0}, + {0x60014,0x0}, + {0x6001c,0x437077f}, + {0x70008,0x0}, + {0x70028,0x2a0f}, + {0x70040,0x321}, + {0x70080,0x0}, + {0x70084,0x0}, + {0x70180,0x94000008}, + {0x70188,0x44}, + {0x7018c,0x0}, + {0x70190,0x77f0437}, + {0x7019c,0x0}, + {0x701a4,0x0}, + {0x701ac,0x0}, + {0x7027c,0x7ff0000}, + {0x7037c,0x0}, + {0x70280,0x8}, + {0x70380,0x8}, + {0x70480,0x8}, + {0x70580,0x8}, + {0x70680,0x0}, + {0x70780,0x0}, + {0x60404,0x0}, + {0x60540,0x0}, + {0x60030,0x0}, + {0x60034,0x0}, + {0x60040,0x0}, + {0x60044,0x0}, + {0x46140,0x0}, + {0x68170,0x0}, + {0x68174,0x0}, + {0x68180,0x0}, + {0x68270,0x0}, + {0x68274,0x0}, + {0x68280,0x200}, + {0x78000,0x0}, + {0x78004,0x0}, + {0x61400,0x30000}, + {0x61000,0x0}, + {0x61004,0x0}, + {0x61008,0x0}, + {0x6100c,0x0}, + {0x61010,0x0}, + {0x61014,0x0}, + {0x6101c,0x0}, + {0x71008,0x0}, + {0x71028,0xa0f}, + {0x71040,0x0}, + {0x71080,0x0}, + {0x71084,0x0}, + {0x71180,0x8}, + {0x71188,0x0}, + {0x7118c,0x0}, + {0x71190,0x0}, + {0x7119c,0x0}, + {0x711a4,0x0}, + {0x711ac,0x0}, + {0x7127c,0x0}, + {0x7137c,0x0}, + {0x71280,0x8}, + {0x71380,0x8}, + {0x71480,0x8}, + {0x71580,0x8}, + {0x71680,0x0}, + {0x71780,0x0}, + {0x61404,0x0}, + {0x61540,0x0}, + {0x61030,0x0}, + {0x61034,0x0}, + {0x61040,0x0}, + {0x61044,0x0}, + {0x46144,0x0}, + {0x68970,0x0}, + {0x68974,0x0}, + {0x68980,0x200}, + {0x68a70,0x0}, + {0x68a74,0x0}, + {0x68a80,0x200}, + {0x78200,0x0}, + {0x78204,0x0}, + {0x62400,0x30000}, + {0x62000,0x0}, + {0x62004,0x0}, + {0x62008,0x0}, + {0x6200c,0x0}, + {0x62010,0x0}, + {0x62014,0x0}, + {0x6201c,0x0}, + {0x72008,0x0}, + {0x72028,0xa0f}, + {0x72040,0x0}, + {0x72080,0x0}, + {0x72084,0x0}, + {0x72180,0x8}, + {0x72188,0x0}, + {0x7218c,0x0}, + {0x72190,0x0}, + {0x7219c,0x0}, + {0x721a4,0x0}, + {0x721ac,0x0}, + {0x7227c,0x0}, + {0x7237c,0x0}, + {0x72280,0x8}, + {0x72380,0x8}, + {0x72480,0x8}, + {0x72580,0x8}, + {0x72680,0x0}, + {0x72780,0x0}, + {0x62404,0x0}, + {0x62540,0x0}, + {0x62030,0x0}, + {0x62034,0x0}, + {0x62040,0x0}, + {0x62044,0x0}, + {0x46148,0x0}, + {0x69170,0x0}, + {0x69174,0x0}, + {0x69180,0x200}, + {0x69270,0x0}, + {0x69274,0x0}, + {0x69280,0x200}, + {0x78400,0x0}, + {0x78404,0x0}, + {0x7b008,0xc0000000}, + {0x7b808,0x0}, + {0x6b000,0x4650437}, + {0x6b004,0x0}, + {0x6b008,0x44f044d}, + {0x6b00c,0x7d1077f}, + {0x6b010,0x7d1077f}, + {0x6b014,0x7a907a7}, + {0x6b030,0x20130300}, + {0x6b034,0x0}, + {0x6b090,0x60038}, + {0x6b094,0x1}, + {0x6b0d8,0x0}, + {0x6b400,0x80030006}, + {0x6b404,0x0}, + {0x6b894,0x0}, + {0x6bc00,0x30000}, + {0x46010,0x0}, + {0x46014,0xcc000000}, + {0x164290,0xe84}, + {0xc7200,0x80000008}, + {0xc7204,0xf}, + {0xc8250,0x80000000}, + {0xc8254,0x1e0}, + {0xc8258,0xf0}, + {0x48400,0x0}, + {0x51004,0x40000000}, + {0x46070,0xc000000e}, + {0x46008,0xf00ffff}, + {0x45500,0x0}, + {0x45504,0x0}, + {0x7017c,0x0}, + {0x162028,0x3000a}, + {0x6c028,0x3000a}, + {0x6c100,0x80005f25}, + {0x6c104,0x81000400}, + {0x6c10c,0xc0606f21}, + {0x6c120,0xd0280}, + {0x6c124,0x62ab67bb}, + {0x6c128,0x51914f96}, + {0x64c04,0x23000000}, + {0x6c804,0x1c000004}, + {0x6c014,0x12040458}, + {0x6c894,0x290000}, + {0x6c888,0x1098}, + {0x6c890,0x280018}, + {0x6c89c,0x7f000000}, + {0x6c898,0x0} + } {} + bool readable(unsigned) {return true;} + unsigned read(unsigned r) {if(r==0x70040)return time/1000;return regs[r];} + void write(unsigned r,unsigned v) { + writes.push_back({r,v}); + if(r==0x7b008)assert((regs[0x46010]&0xcc000000)==0xcc000000); + if(r==0x60400 && (v&0x80000000))assert(!(regs[0x7b008]&0xc0000000)); + if(r==0x138124) {regs[r]=0;regs[0x138128]=(v&255)==0x23?35:0x09090909;return;} + regs[r]=v;if(r==fail)return; + if(r==0x45454)regs[r]=v|((v>>1)&0x15); + if(r==0x46010 || r==0x46014)regs[r]=(v&~0x44000000U)|((v&0x80000000U)?0x40000000U:0)|((v&0x08000000U)?0x04000000U:0); + if(r==0x70008 || r==0x7b008)regs[r]=(v&0x80000000U)?0xc0000000U:0; + if(r==0x64000 || r==0x64100)regs[r]=(v&0x80000000U)?v&~0x80U:0x80; + if(r==0x6b0d8)regs[r]=v|0x10000; + if(r==0x7019c)regs[0x701ac]=v; + } + void delayUs(unsigned us) {time+=us;} + unsigned long long readPTE(unsigned r) {return memory.pte((r-0x800000ULL)/8*4096)^(badPTE?4096:0);} +}; +int main() { + using namespace Display13DSITrial; + IO io;auto boot=AlderLakeN::readDisplay(io,true); + auto result=transfer(io,true,boot,io.memory,2); + printf("status=%u stage=%u reg=%x value=%x writes=%u\n",unsigned(result.status),result.stage,result.reg,result.value,result.writes); + assert(result.status==Status::Complete); + auto after=AlderLakeN::readDisplay(io,true);AlderLakeN::LinearPlan plan; + assert(AlderLakeN::linearPlan(after,plan,1)==AlderLakeN::LinearStatus::Ready); + assert(plan.geometry.width==1920 && plan.geometry.height==1080 && plan.geometry.pitch==7680); + // Warm firmware can leave the idle B mux ungated. Its first mutation must + // close only B before the free PLL is powered; preserve the active DSI mux. + IO ungated;ungated.regs[0x164280]&=~(1U<<11); + auto ub=AlderLakeN::readDisplay(ungated,true); + auto ur=transfer(ungated,true,ub,ungated.memory,2); + assert(ur.status==Status::Complete); + bool gated=false;for(const auto &w:ungated.writes) { + if(w.first==0x164280){assert(w.second==0x01e07801);gated=true;break;} + assert(w.first!=0x46010); + }assert(gated); + for(unsigned mode=0;mode<5;++mode) { + IO x;if(mode==1)x.regs[0x46010]=0xcc000000; + if(mode==2)x.badPTE=true; + if(mode==3)x.regs[0x70180]|=1; + if(mode==4)x.regs[0x7b808]=0xc0000000; + auto b=AlderLakeN::readDisplay(x,true);auto r=transfer(x,mode!=0,b,x.memory,2); + assert(r.status!=Status::Complete && x.writes.empty()); + } + IO stuck;stuck.fail=0x46010; + auto b=AlderLakeN::readDisplay(stuck,true);auto r=transfer(stuck,true,b,stuck.memory,2); + assert(r.status==Status::Timeout && r.stage==1 && stuck.regs[0x7b008]==0xc0000000); + puts("DSI transfer source/PLL refusal, bounded timeout, ordering and real GOP postcondition passed"); +} diff --git a/tests/display13_fb_irq.cpp b/tests/display13_fb_irq.cpp index c3a2e184..9cfa073d 100644 --- a/tests/display13_fb_irq.cpp +++ b/tests/display13_fb_irq.cpp @@ -13,7 +13,7 @@ struct IO { uint32_t rejectRegister {UINT32_MAX}; IO() { r[Master]=r[GfxMaster]=0;r[DDIControl]=0x3300;r[0x64100]=r[0x64000]=0; - r[TCControl]=0; + r[TCControl]=r[NorthControl]=0; r[0xC4004]=~0U;r[0xC4008]=r[0xC400C]=0; for(unsigned p=0;p<3;++p) { r[0x70008+p*0x1000]=p ? 0 : 0xC0000000; @@ -36,7 +36,7 @@ struct IO { writes.push_back(reg);check(reg);assert(r.count(reg)); if(reg==rejectRegister)return; if(reg==Master || reg==GfxMaster) {r[reg]=(r[reg]&~Enable)|(rejectEnable ? 0 : value&Enable);return;} - if(reg==TCControl) {r[reg]=(value&~0x333333U)|((r[reg]&0x333333U)&~value);return;} + if(reg==TCControl || reg==NorthControl) {r[reg]=(value&~0x333333U)|((r[reg]&0x333333U)&~value);return;} if(reg==DDIControl) {r[reg]=(value&~0x3333U)|((r[reg]&0x3333U)&~value);return;} for(unsigned p=0;p<3;++p) { if(reg==PipeIIR(p)) {r[reg]&=~value;return;} @@ -44,10 +44,55 @@ struct IO { r[reg]=(value&~0xFU)|((r[reg]&0xFU)&~value);return; } } - if(reg==0xC4008)r[reg]&=~value;else r[reg]=value; + if(reg==0xC4008 || reg==HPDBank)r[reg]&=~value;else r[reg]=value; } }; int main() { + // DSI0 can feed A/B/C. IRQ/frame/plane banks follow the pipe, while + // control/function come from DSI0. Keep external A active for B/C cases. + for(unsigned pipe=0;pipe<3;++pipe) { + IO io;State s;s.transcoders.dsi0Pipe=pipe; + io.r[0x70008+0x1000*pipe]=0;io.r[0x7B008]=0xC0000000; + const uint32_t inputs[]={0,5,6};io.r[0x6B400]=0x80000006|(inputs[pipe]<<12); + const auto expected=(1U< source { + {0x7B008,0xC0000000},{0x6B400,0x80030006},{0x70008,0},{0x60400,0}, + {0x6B030,0x20130300},{0x6B094,1},{0x6B404,0},{0x6B034,0},{0x64000,0x80000000}, + {0x164280,0x1E07801},{0x46014,0xCC000000},{0x16428C,0x3001FB},{0x164290,0xE84}, + {0x6B000,0x04650437},{0x6B008,0x044F044D},{0x6B00C,0x07D1077F},{0x6B014,0x07A907A7}, + {0x51004,0x40000000},{0x42084,0},{0x70188,68},{0x70190,0x077F0437}}; + for(auto entry:source)f.registers[entry.first]=entry.second; + return XELP::DSI::decodeReadout([&](uint32_t reg,uint32_t &v) { + auto i=f.registers.find(reg);if(i==f.registers.end())return false;v=i->second;return true; + },38400); + }; + const auto inspect=[](Fixture &f,const XELP::DSI::Readout &source) { + return Display13FBMemory::inspect(f,f.controller,sizeof(f.controller),f.framebuffer,sizeof(f.framebuffer), + 0x10000000,memory,true,0,0,nullptr,nullptr,false,&source); + }; + Fixture good;const auto source=setup(good);assert(source.complete()); + auto result=inspect(good,source);assert(result.status==Status::Complete && result.verifiedPages==good.ptes.size()); + // The GOP has global DPT enabled, but its linear plane still addresses GGTT. + // No acceptance of tiled/rotated geometry or a shortened pool follows. + for(auto pair:std::map{{0x70180,0x94000408},{0x70188,67}, + {0x70190,0x0437077F},{0x42084,UINT32_MAX},{0x60400,0x80000000}}) { + Fixture bad;setup(bad);bad.registers[pair.first]=pair.second; + assert(inspect(bad,source).status==Status::DisplayState && !bad.reads); + } + for(auto reg:{0x7B008U,0x6B400U,0x6B000U,0x16428CU,0x51004U}) { + Fixture changed;setup(changed);changed.changeRegister=reg; + assert(inspect(changed,source).status==Status::StateChanged); + } + Fixture pte;setup(pte);pte.ptes[7]^=2;assert(inspect(pte,source).status==Status::PTE); + Fixture lost;setup(lost);lost.deadPower=true;assert(inspect(lost,source).status==Status::StateChanged); + auto wrong=source;wrong.identity.pipe=1;assert(inspect(good,wrong).status==Status::DisplayState); +} static void testRelocatedPools() { for(auto ggc : {0xFEC1U,0x02C1U,0x03C1U,0x04C1U}) { Fixture f;const auto moved=XELPMemory::decode(ggc,0x91000001,1);assert(moved.valid); @@ -75,6 +109,7 @@ static void testSlotClaims() { } int main() { + testDSIBacking(); for(unsigned ddi=3;ddi<=6;++ddi) { Fixture f;const auto tc=ddi-3; f.registers[0x45454]=3U<<(2*ddi);f.registers[0x161500+4*tc]=0xC; @@ -268,6 +303,11 @@ int main() { assert(!associations(shared.controller,sizeof(shared.controller),shared.framebuffer,sizeof(shared.framebuffer),owner+8)); put(shared.framebuffer,0x4348,uintptr_t(1)); assert(!associations(shared.controller,sizeof(shared.controller),shared.framebuffer,sizeof(shared.framebuffer),owner)); + put(shared.framebuffer,0x4340,uintptr_t(0x543200)); + assert(!associations(shared.controller,sizeof(shared.controller),shared.framebuffer,sizeof(shared.framebuffer),owner,true)); + assert(!associations(shared.controller,sizeof(shared.controller),shared.framebuffer,sizeof(shared.framebuffer),owner,true,0x543208)); + assert(associations(shared.controller,sizeof(shared.controller),shared.framebuffer,sizeof(shared.framebuffer),owner,true,0x543200)); + assert(!associations(shared.controller,sizeof(shared.controller),shared.framebuffer,sizeof(shared.framebuffer),0,true,0x543200)); Fixture changedOwner;put(changedOwner.controller,0xC0,owner);changedOwner.changeOwner=true; assert(Display13FBMemory::inspect(changedOwner,changedOwner.controller,sizeof(changedOwner.controller),changedOwner.framebuffer,sizeof(changedOwner.framebuffer),0x10000000,memory,true,owner).status==Status::StateChanged); Fixture sysmem;put(sysmem.controller,0xE49,uint8_t(1));assert(sysmem.inspect().status==Status::Association); diff --git a/tests/display13_fb_sysmem.cpp b/tests/display13_fb_sysmem.cpp index 14a86a67..f5501ee3 100644 --- a/tests/display13_fb_sysmem.cpp +++ b/tests/display13_fb_sysmem.cpp @@ -56,7 +56,15 @@ int main() { } {IO io;io.badPhysical=0x3FFF;const auto r=io.map();assert(r.status==Status::Physical && !r.written && !r.mustRetainBacking);} {IO io;io.shortPhysical=0x3FFF;assert(io.map().status==Status::Physical && !io.writes);} - {IO io;io.ptes.back()=0x300000001ULL;assert(io.map().status==Status::Occupied && !io.writes);} + // Empty native slots can contain undefined firmware GGTT words. Include + // the observed nonpresent value and present-looking residual entries. + {IO io;for(unsigned i=0;i #include #include #include -struct OSObject { unsigned char bytes[0x580]{}; OSObject *getMetaClass(){return this;}unsigned getClassSize(){return sizeof(bytes);} }; +constexpr uint32_t kIOReturnSuccess=0; +struct OSObject { alignas(8) unsigned char bytes[0x580]{}; OSObject *getMetaClass(){return this;}unsigned getClassSize(){return sizeof(bytes);} }; struct IORegistryEntry {static IORegistryEntry *getRegistryRoot(){static IORegistryEntry r;return &r;}void setProperty(const char *,bool){}void setProperty(const char *,const void *,unsigned){}}; -namespace Display13FBMemory {templateT field(void *p,unsigned off){T v;memcpy(&v,(char*)p+off,sizeof(v));return v;}} -struct Wiring {unsigned selected(){return 8;}}; +namespace Display13FBMemory {templateT &field(void *p,unsigned off){return *reinterpret_cast((char*)p+off);}} +struct Wiring {unsigned dsi=0;unsigned selected(){return 8;}}; namespace Display13FBTopology {constexpr unsigned Ports=9;bool selected(void *p,uint32_t *c,Wiring){return c==(uint32_t*)p;}bool bound(void *,uint32_t *,unsigned){return false;}} static bool display13Offline;static unsigned calls,binds; uint32_t native(OSObject *){++calls;return 1;} #define FunctionCast(type,fn) fn -struct XE {bool display13FBTopologyHooksReady=true;void *adlnTopologyController=(void*)1;void *adlnPlatform;Wiring adlnOutputWiring;uint32_t (*orgAlderLakeNPortState)(OSObject*)=native;bool xelpBindProbe(void*){++binds;return true;}static uint32_t wrapAlderLakeNPortState(OSObject*);}; +struct XE {static uint32_t setDSIPortMode(OSObject*,uint32_t){assert(false);return 1;}bool display13FBTopologyHooksReady=true;void *adlnTopologyController=(void*)1;void *adlnPlatform;Wiring adlnOutputWiring;uint32_t (*orgAlderLakeNPortState)(OSObject*)=native;bool xelpBindProbe(void*){++binds;return true;}static uint32_t wrapAlderLakeNPortState(OSObject*);}; static XE *callbackXE; BODY int main(){XE x;callbackXE=&x;uint32_t config[3]={3,9,1};x.adlnPlatform=config;OSObject port;auto cp=config;memcpy(port.bytes+0x548,&cp,sizeof(cp));uint32_t connected=4;memcpy(port.bytes+0x554,&connected,4); diff --git a/tests/display13_retirement_fixture.cpp b/tests/display13_retirement_fixture.cpp index 52613ec9..d030b4d3 100644 --- a/tests/display13_retirement_fixture.cpp +++ b/tests/display13_retirement_fixture.cpp @@ -38,10 +38,15 @@ struct Display13FBDBUFIO {void *map;}; void adlnFault(bool &fault,unsigned){fault=true;} void adlnPublishMemory(const char*,unsigned,void*,size_t){} struct XE { - bool display13FramebufferRequested=true;unsigned adlnOutputWiring=0x78;void *adlnPlatform{}; + bool display13FramebufferRequested=true;struct Wiring{unsigned value=0x78,dsi=0;operator unsigned()const{return value;}Wiring &operator=(unsigned v){value=v;return *this;}}adlnOutputWiring;void *adlnPlatform{}; uintptr_t orgAlderLakeNFreePath{},orgDisplay13FreeResources{},orgDisplay13AllocateResources{}; + bool disposeDSIPath(IOService*,OSObject*); static bool adlnFreeStoppedPipe(IOService*); static void wrapAlderLakeNFreePath(IOService*,OSObject*); + static void drainDisplay13TCMode(unsigned ddi) { + assert(ddi==3 && ctlDepth && !ownerDepth); + assert(!display13FBPower.pipe[0].memoryFramebuffer && !display13FBPower.pipe[0].publishedScanout); + } static uint32_t releaseDisplay13Path(IOService*,OSObject*),retireDisplay13Paths(void*),wrapDisplay13FreeResources(void*); static bool display13RetirementHealthy(),wrapDisplay13AllocateResources(void*),retireDisplay13Controller(void*); }state,*callbackXE=&state; @@ -51,6 +56,7 @@ TGLFBBinding::Readout adlnInspectPathBinding(IOService*f,OSObject*p,void*,unsign if(!p||Display13FBMemory::field(p,0x32b0)!=f)return {}; return {TGLFBBinding::Status::Complete,pipe,3}; } +bool dsiLifecycleTarget(IOService*,OSObject*){return true;} // FUNCTIONS alignas(16) static uint8_t ctl[0x1c20],fb[0x4b90],peer[0x4b90],path[0x3730]; static unsigned failure,memoryCalls,pathCalls,postCalls,transactionStops,allocCalls; @@ -72,6 +78,13 @@ void nativePath(IOService*f,OSObject*p){ put(f,0x49e8,0U);put(f,0x49f0,static_cast(nullptr)); put(p,0x3644,0U);put(p,0x32b0,static_cast(nullptr));put(p,0x3648,static_cast(nullptr)); } +unsigned dsiDisposals=0;bool dsiRefuse=false; +bool XE::disposeDSIPath(IOService *f,OSObject *p){ + assert(ctlDepth && fbDepth && !ownerDepth);++dsiDisposals; + if(dsiRefuse)return false; + put(f,0x49e8,0U);put(f,0x49f0,uintptr_t(0));put(p,0x3644,0U);put(p,0x32b0,uintptr_t(0));put(p,0x3648,uintptr_t(0)); + return true; +} void transactions(void*){assert(ctlDepth&&!fbDepth);++transactionStops;} void disable(void*f,bool){assert(ctlDepth&&!fbDepth);put(path,0x3644,failure==5?2U:1U);} void post(void*){assert(ctlDepth&&!fbDepth);++postCalls;} @@ -139,6 +152,14 @@ int main(int argc,char**argv){ // completes. Clear only that synthetic peer for the empty-controller case. assert(!XE::retireDisplay13Controller(ctl));assert(pathCalls==1); display13FBPower.pipe[1]={};assert(XE::retireDisplay13Controller(ctl));assert(pathCalls==1); + // DSI selected at the real release boundary must never fall through to the + // normal audio/DDI cleanup body, including when its dedicated disposal fails. + setup(native);state.adlnOutputWiring.dsi=1;dsiRefuse=true;dsiDisposals=0; + assert(XE::wrapDisplay13FreeResources(fb)==kIOReturnNotReady && dsiDisposals==1 && !pathCalls && !postCalls); + assert(Display13FBMemory::field(path,0x32b0)==fb && display13FBPower.pipe[0].memoryFramebuffer==reinterpret_cast(fb)); + setup(native);state.adlnOutputWiring.dsi=1;dsiRefuse=false;dsiDisposals=0; + assert(!XE::wrapDisplay13FreeResources(fb) && dsiDisposals==1 && !pathCalls && postCalls==4); + assert(!display13FBPower.pipe[0].memoryFramebuffer); setup(native);state.display13FramebufferRequested=false;display13FBPower.memoryFaulted=true; assert(XE::display13RetirementHealthy()&&XE::retireDisplay13Controller(nullptr)); // other backends unchanged if(image)munmap(image,bytes); diff --git a/tests/display13_shutdown.py b/tests/display13_shutdown.py new file mode 100644 index 00000000..a52b9ccf --- /dev/null +++ b/tests/display13_shutdown.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Exercise the production shutdown adapter without powering hardware.""" +from pathlib import Path +import re,subprocess,tempfile,sys +root=Path(__file__).resolve().parents[1] +s=(root/'WhateverXe/kern_xe.cpp').read_text() +body='\n'.join(re.search(r'(?:void|uint32_t) XE::'+n+r'\(.*?\n\}',s,re.S)[0] for n in ['finishDisplay13TCShutdown','wrapDisplay13Shutdown']) +code=r''' +#include +#include +#include +#include +struct Lock{bool held=false;}; +void IOLockLock(Lock*l){assert(!l->held);l->held=true;}void IOLockUnlock(Lock*l){assert(l->held);l->held=false;} +struct IOWorkLoop{bool held=false,available=true;}; +bool inGate(const IOWorkLoop*g){return g->held;} +bool tryGate(IOWorkLoop*g){if(!g->available)return false;g->held=true;return true;} +void openGate(IOWorkLoop*g){assert(g->held);g->held=false;} +struct Port{unsigned id,refs=1;void retain(){++refs;}void release(){assert(refs>1);--refs;}}; +struct Bridge{bool begun=true;struct{bool suspended=false;}state;unsigned ddi[7]{},aux[7]{}; + unsigned nativeDDIRefs(unsigned d){return ddi[d];}unsigned nativeAUXRefs(unsigned d){return aux[d];}}; +struct {Lock*lock;void*controller;IOWorkLoop*workLoop;bool tcSupported=true;Bridge bridge;Port*tcObject[4]{};unsigned tcBusy[4]{};bool tcSignalReady[4]{};}display13FBPower; +namespace Display13FBMemory{templateT field(void*p,unsigned off){T v;memcpy(&v,(char*)p+off,sizeof(v));return v;}} +uint32_t published[8]{};struct IORegistryEntry{static IORegistryEntry*getRegistryRoot(){static IORegistryEntry r;return &r;}void setProperty(const char*,const void*p,size_t n){assert(n==sizeof(published));memcpy(published,p,n);}}; +std::vector events;unsigned modeCalls=0,nativeCalls=0;uint32_t nativeResult=0,modeResult=0;bool shutdownSuspends=false; +#define FunctionCast(fn,p) reinterpret_cast(p) +struct XE{bool display13FBTCHooksReady=true;uintptr_t orgDisplay13Shutdown; + uintptr_t adlnWorkLoopInGate=(uintptr_t)inGate,adlnWorkLoopTryClose=(uintptr_t)tryGate,adlnWorkLoopOpen=(uintptr_t)openGate; + static void finishDisplay13TCShutdown(void*);static uint32_t wrapDisplay13Shutdown(void*,void*); + static uint32_t wrapDisplay13TCMode(Port*p,uint32_t mode){auto &c=display13FBPower;assert(!c.lock->held && c.workLoop->held && mode==0 && p->refs==2 && !c.bridge.state.suspended);++modeCalls;events.push_back(3);if(!modeResult)c.tcObject[p->id]=nullptr;return modeResult;} +};XE*callbackXE; +FUNCTIONS +uint32_t nativeShutdown(void*c,void*){++nativeCalls;events.push_back(2); + if(shutdownSuspends){XE::finishDisplay13TCShutdown(c);display13FBPower.bridge.state.suspended=true;} + return nativeResult;} +int main(){ + for(unsigned test=0;test<12;++test){Lock lock;IOWorkLoop gate;char controller[0xe10]{};auto ptr=&gate;memcpy(controller+0xe00,&ptr,sizeof(ptr)); + display13FBPower={};auto &c=display13FBPower;c.lock=&lock;c.controller=controller;c.workLoop=&gate;c.tcSupported=true;c.bridge.begun=true; + Port idle{1},active{0};c.tcObject[0]=&active;c.tcObject[1]=&idle;c.tcSignalReady[0]=true;c.bridge.ddi[3]=1; + XE x;callbackXE=&x;x.orgDisplay13Shutdown=(uintptr_t)nativeShutdown; + modeCalls=nativeCalls=0;modeResult=nativeResult=0;events.clear();shutdownSuspends=false; + if(test==1)gate.available=false; + if(test==2)c.bridge.state.suspended=true; + if(test==3)c.tcBusy[1]=1; + if(test==4)c.bridge.ddi[4]=1; + if(test==5)c.bridge.aux[4]=1; + if(test==6)c.tcSignalReady[1]=true; + if(test==7)nativeResult=0x1234; + if(test==8)modeResult=0x4567; + if(test==9)gate.held=true; + if(test==10)shutdownSuspends=true; + assert(XE::wrapDisplay13Shutdown(test==11?nullptr:controller,nullptr)==nativeResult); + assert(nativeCalls==1 && events.front()==2 && events.size()==1+modeCalls); + const bool attempted=test==0||test==8||test==9||test==10; + assert(modeCalls==unsigned(attempted));assert(!lock.held && gate.held==(test==9));assert(idle.refs==1&&active.refs==1); + assert(c.tcObject[0]==&active && c.bridge.ddi[3]==1); + assert((c.tcObject[1]==nullptr)==(attempted&&test!=8)); + if(test==0){XE::wrapDisplay13Shutdown(controller,nullptr);assert(modeCalls==1);c.tcSignalReady[0]=false;c.bridge.ddi[3]=0;XE::wrapDisplay13Shutdown(controller,nullptr);assert(modeCalls==2&&!c.tcObject[0]);} + } +} +'''.replace('FUNCTIONS',body) +with tempfile.TemporaryDirectory(prefix='wex-shutdown-') as tmp: + p=Path(tmp);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined',str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) +override=re.search(r'void XE::wrapAlderLakeNPowerOverride\(.*?\n\}',s,re.S)[0] +assert override.index('display13ShutdownPowerCaller')1: + b=Path(sys.argv[1]).read_bytes() + install=re.search(r'bool XE::processDisplay13Shutdown\(.*?\n\}',s,re.S)[0] + for name,offset in [('entry',0x5173a),('powerCall',0x51d24)]: + raw=bytes(int(v,0) for v in re.search(r'const uint8_t '+name+r'\[\]=\{([^}]+)',install)[1].split(',')) + assert b[offset:offset+len(raw)]==raw + assert b[0x339b8:0x339d3]==bytes.fromhex('48ff05a96f0a00b8100300e04939c6740ab8500200e04939c67570') + assert b[0x339e4]==0xe8 and 0x339e9+int.from_bytes(b[0x339e5:0x339e9],'little',signed=True)==0x5173a + assert 0x51d29+int.from_bytes(b[0x51d25:0x51d29],'little',signed=True)==0x5b464 +print('PASS shutdown mode0: live-port exclusion, gate contention, native errors, retry/idempotence, cleanup before power suspend, donor power-off/restart callsites') diff --git a/tests/display13_sst_fixture.cpp b/tests/display13_sst_fixture.cpp index a9329914..3beb1ee7 100644 --- a/tests/display13_sst_fixture.cpp +++ b/tests/display13_sst_fixture.cpp @@ -44,11 +44,12 @@ struct Display13FBPowerIO { // TC lifecycle itself is exercised by display13_tc_mode. This fixture // supplies the ownership witness and verifies the frontend does not borrow // Combo leases; false is the default for all existing Combo regressions. -bool tcOwned=false,tcLinked=false; +bool tcOwned=false,tcLinked=false,tcConnected=true; struct Display13FBTCIO { OSObject *object;unsigned tc; - bool owned(unsigned d){return tcOwned && tc<4 && d==tc+3 && Display13FBMemory::field(object,0x550)==4;} - bool linkHeld(){return tcLinked && owned(tc+3);} + bool retainedOwned(unsigned d){return tcOwned && tc<4 && d==tc+3 && (Display13FBMemory::field(object,0x550)==4 || Display13FBMemory::field(object,0x550)==3);} + bool owned(unsigned d){return tcConnected && retainedOwned(d);} + bool linkHeld(){return tcLinked && retainedOwned(tc+3);} }; struct Display13FBSSTIO { Map *map;uint32_t read(uint32_t r){assert(locked);return map->read(r);} @@ -65,13 +66,14 @@ struct XE { Display13OutputWiring adlnOutputWiring;void *adlnPlatform=nullptr,*adlnTopologyController=nullptr; uintptr_t orgDisplay13TrainingPattern=0,orgDisplay13SignalLevels=0; uintptr_t orgDisplay13EnablePort=0,orgDisplay13DisablePort=0; - static bool display13MainLinkAUX(void*,bool); + static bool display13MainLinkAUX(void*,bool,bool=false); static uint32_t wrapDisplay13EnablePort(void*);static uint32_t wrapDisplay13DisablePort(void*); static uint8_t *display13SSTPort(void*);static bool display13HDMIPort(void*); static uint32_t wrapDisplay13TCSignal(void*,uint32_t,uint32_t,void*){assert(false);return 1;} static uint32_t wrapDisplay13SignalLevels(void*,uint32_t,uint32_t,void*); static uint32_t wrapDisplay13TrainingPattern(void*,uint32_t); }state,*callbackXE=&state; +uintptr_t dsiLogicalSurface(const void*){return 0;} // BINDING // FUNCTIONS unsigned nativeSignal(void*,unsigned,unsigned,void*){assert(!locked);++calls;return nativeResult;} @@ -119,7 +121,7 @@ unsigned nativePattern(void *path,unsigned pattern){ } void setup(Fixture &f,Map &map,unsigned ddi,unsigned pipe,unsigned lanes,unsigned rate,bool panel=false){ display13FBPower.~Context();new(&display13FBPower)Context;state={};calls=nativeResult=0;bound=true;loseAfterNative=false; - tcOwned=tcLinked=false;assert(objectRefs.empty());linkCalls=linkResult=0;changeBuffer=bufferIdle=true;loseLinkPower=false; + tcOwned=tcLinked=false;tcConnected=true;assert(objectRefs.empty());linkCalls=linkResult=0;changeBuffer=bufferIdle=true;loseLinkPower=false; auto &c=display13FBPower;c.controller=f.ctl;c.access=f.access;c.map=↦ currentPath=f.path; put(f.ctl,0xC8,c.power);put(c.power,0x10,static_cast(f.ctl));put(c.power,0x78,static_cast(f.access)); @@ -233,8 +235,9 @@ int main(int argc,char **argv){ // TC SST frontend: real graph/bridge and native pattern replay. The // separate lifecycle suite proves the ownership witness supplied here. for(unsigned ddi=3;ddi<=6;++ddi)for(unsigned pipe=0;pipe<3;++pipe) - for(unsigned lanes:{1U,2U,4U})for(unsigned rate:{6U,10U,20U,30U}){ + for(unsigned lanes:{1U,2U,4U})for(unsigned rate:{6U,10U,20U,30U})for(bool alt:{false,true}){ Fixture f(2-pipe,pipe,ddi);Map map;setup(f,map,ddi,pipe,lanes,rate); + if(alt){state.adlnOutputWiring.typeC=1U< regs,bank; std::vector> writes; + bool connected {true}; bool held {},power {true},owner {true},idle {true},stuckPower {},stuckLock {},fault {}; unsigned time {},locks {},unlocks {};uint32_t reject {},absent {}; unsigned expectedLanes {4}; @@ -25,7 +26,8 @@ struct IO { } bool linkHeld(){return power&&link;} bool powerHeld(){return power;} - bool owned(unsigned d){return owner && d==p.ddi;} + bool retainedOwned(unsigned d){return owner && d==p.ddi;} + bool owned(unsigned d){return retainedOwned(d) && connected;} bool quiescent(unsigned d){return idle && d==p.ddi;} bool readable(uint32_t r){return r!=absent && (regs.count(r) || (r>=p.phy && r>p.shift)&15)<<12)|(r-p.phy);} @@ -45,6 +47,69 @@ struct IO { void bankFault(){fault=true;} }; int main() { + // Independent Linux DP DDI rate encodings; retain ownership, lane width, + // reversal, enable and unrelated bits; replace the previous rate code. + for(const auto &pair:std::map{{6,0},{10,1},{20,2},{30,3}}) { + uint32_t value=0xfff1abcd; + assert(DPSignal::bufferRate(pair.first,value)); + assert(value==((0xfff1abcdU&~0x700000U)|(pair.second<<20))); + } + for(unsigned invalid:{0U,1U,12U,UINT32_MAX}) { + uint32_t value=0xa5a5a5a5;assert(!DPSignal::bufferRate(invalid,value) && value==0xa5a5a5a5); + } + for(unsigned ddi=3;ddi<=6;++ddi)for(unsigned reset=0;reset<4;++reset) { + IO io(ddi);io.link=true; + io.bank[0x14]=0xa5a50001|((reset&1)?0x800:0); + io.bank[0x1014]=0x5a5a0002|((reset&2)?0x800:0); + const auto hip=io.regs[io.p.hip];uint32_t before[2]{},after[2]{}; + assert(DPSignal::prepareBuffer(io,io.p,before,after)); + assert(after[0]==0xa5a50001 && after[1]==0x5a5a0002); + assert(io.bank[0x14]==after[0] && io.bank[0x1014]==after[1] && io.regs[io.p.hip]==hip); + io.regs[io.p.buffer]|=0x80000000; + assert(DPSignal::prepareBuffer(io,io.p,before,after)); // retained, already released + io.bank[0x14]|=0x800;const auto prior=io.bank; + assert(!DPSignal::prepareBuffer(io,io.p,before,after) && io.bank==prior); + } + for(unsigned failure=0;failure<6;++failure) { + IO io(3);io.link=true;io.bank[0x14]=io.bank[0x1014]=0x800; + if(failure==0)io.power=false;if(failure==1)io.owner=false;if(failure==2)io.link=false; + if(failure==3)io.regs[io.p.buffer]=UINT32_MAX;if(failure==4)io.regs[io.p.buffer]&=~128U; + if(failure==5)io.reject=io.p.phyRegister(0x14); + uint32_t before[2]{},after[2]{};assert(!DPSignal::prepareBuffer(io,io.p,before,after)); + assert(io.bank[0x14]==0x800 && io.bank[0x1014]==0x800); + } + // Pin D, both physical orientations: do not change the USB pair's + // signal coefficients or wake register. Logical main-link lanes stay0/1. + for(unsigned ddi=3;ddi<=6;++ddi)for(unsigned physical:{3U,12U}) { + IO io(ddi);io.expectedLanes=2;io.regs[io.p.ready]=0x1d; + for(unsigned group=0;group<2;++group)for(unsigned reg:{0xa0U,0xd00U,0x2c0U,0x2c4U,0x2c8U})io.bank[group*0x1000+reg]=0xa5a5a5a5; + Clock::LinkStage stage;assert(DPSignal::startLink(io,io.p,DPClock::calculate(10,38400),2,&stage,4,physical)); + const unsigned usb=physical==3?0x1000:0; + for(unsigned reg:{0xd00U,0x2c0U,0x2c4U,0x2c8U})assert(io.bank[usb+reg]==0xa5a5a5a5); + const auto count=io.writes.size(); + assert(!DPSignal::startLink(io,io.p,DPClock::calculate(10,38400),4,&stage,4,physical) && io.writes.size()==count); + } + // Stop retains ownership and power even when the connector state is gone. + for(unsigned ddi=3;ddi<=6;++ddi)for(unsigned gone:{0U,4U,0x1dU,UINT32_MAX}) { + IO io(ddi);io.expectedLanes=2;io.regs[io.p.ready]=0x1d; + for(unsigned group=0;group<2;++group)for(unsigned reg:{0xa0U,0xd00U,0x2c0U,0x2c4U,0x2c8U})io.bank[group*0x1000+reg]=0; + assert(DPSignal::startLink(io,io.p,DPClock::calculate(10,38400),2,nullptr,4,12)); + io.connected=false;io.regs[io.p.ready]=gone; + const auto bank=io.bank;const auto peer=io.regs[Clock::Gate]&~Clock::gateBit(io.p.tc); + const auto before=io.writes.size(); + assert(!Clock::enable(io,io.p,TMDSClock::calculate(148500,38400)) && io.writes.size()==before); + assert(Clock::stopLink(io,io.p) && !io.link && io.linkPuts==1); + assert(io.bank==bank && !(io.regs[io.p.pll]&0xcc000000U)); + assert((io.regs[Clock::Gate]&~Clock::gateBit(io.p.tc))==peer); + assert(Clock::stopLink(io,io.p) && io.linkPuts==1); // no double put + } + for(unsigned fault=0;fault<7;++fault) { + IO io(4);io.connected=false;io.regs[io.p.ready]=0; + if(fault==0)io.owner=false;if(fault==1)io.power=false;if(fault==2)io.idle=false; + if(fault==3)io.regs[io.p.buffer]|=1U<<31;if(fault==4)io.regs[io.p.buffer]&=~64U; + if(fault==5)io.regs[io.p.buffer]&=~128U;if(fault==6)io.regs[io.p.pll]=UINT32_MAX; + assert(!Clock::stopLink(io,io.p) && io.writes.empty() && !io.linkPuts); + } // Independent DP divisor oracle: 8.1 GHz / (5 * link symbol clock). const unsigned rates[]={6,10,20,30},high[]={5,3,3,2},div[]={2,2,1,1}; // Existing independent Linux capture of TC3 at RBR x4. @@ -138,7 +203,7 @@ int main() { for(unsigned failure=0;failure<8;++failure) { IO io(3); if(failure==0)io.power=false;if(failure==1)io.owner=false;if(failure==2)io.idle=false; - if(failure==3)io.regs[io.p.ready]=5;if(failure==4)io.regs[io.p.buffer]|=1U<<31; + if(failure==3)io.regs[io.p.ready]=6;if(failure==4)io.regs[io.p.buffer]|=1U<<31; if(failure==5)io.regs[io.p.buffer]&=~64U;if(failure==6)io.absent=io.p.pll;if(failure==7)io.regs[io.p.pll]=~0U; assert(!Clock::enable(io,io.p,full) && io.writes.empty()); assert(!Clock::disable(io,io.p) && io.writes.empty()); diff --git a/tests/display13_tc_disconnect.py b/tests/display13_tc_disconnect.py new file mode 100644 index 00000000..7abcc20f --- /dev/null +++ b/tests/display13_tc_disconnect.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +"""Compose the actual TC IO owner checks with clock stop and mode release.""" +from pathlib import Path +import re,subprocess,tempfile +r=Path(__file__).resolve().parents[1];s=(r/'WhateverXe/kern_xe.cpp').read_text() +a=s.index('struct Display13FBTCIO :');b=s.index('// Only used after native object/BAR binding',a) +code=(r/'tests/display13_tc_disconnect_fixture.cpp').read_text().replace('// PRODUCTION_IO',s[a:b]).replace('// PRODUCTION_GATE',re.search(r'void XE::wrapDisplay13DPPhyGating\(.*?\n\}',s,re.S).group()) +code=code.replace('// PRODUCTION_DRAIN',re.search(r'void XE::drainDisplay13TCMode\(.*?\n\}',s,re.S).group()) +release=s[s.index('uint32_t XE::releaseDisplay13Path('):s.index('void XE::wrapAlderLakeNFreePath(')] +assert release.index('orgAlderLakeNFreePath)(fb,path)') < release.index('if(removed)drainDisplay13TCMode(binding.ddi);') +with tempfile.TemporaryDirectory(prefix='tc-disconnect-') as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-g','-fsanitize=address,undefined','-I'+str(r/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/display13_tc_disconnect_fixture.cpp b/tests/display13_tc_disconnect_fixture.cpp new file mode 100644 index 00000000..391e4e84 --- /dev/null +++ b/tests/display13_tc_disconnect_fixture.cpp @@ -0,0 +1,133 @@ +#include "kern_display13_tc_dp.hpp" +#include "kern_display13_tc_mode.hpp" +#include "kern_display13_fb_memory.hpp" +#include "kern_display13_output_wiring.hpp" +#include +#include +#include +#include +#include +#include +using namespace Display13TC; +struct OSObject {alignas(8) uint8_t bytes[0x700]{};int refs=1;void retain(){++refs;}void release(){assert(refs>0);--refs;}const OSObject*getMetaClass(){return this;}unsigned getClassSize()const{return sizeof(bytes);}}; +struct IOWorkLoop {} workLoop; +bool inGate=true;bool workLoopInGate(const IOWorkLoop*){return inGate;} +struct IORegistryEntry {static IORegistryEntry*getRegistryRoot(){static IORegistryEntry r;return &r;}void setProperty(const char*,const void*,unsigned){}}; +struct MMIO { + std::map regs;std::vector writes,reads;bool noConnectorReads=false; + bool readable(unsigned r){return regs.count(r);} + unsigned read(unsigned r){assert(readable(r));if(noConnectorReads)assert(!(r>=0x161500&&r<=0x16150c)&&r!=0xc4000&&r!=0x44470&&r!=0x1638a0&&r!=0x163880&&r!=0x16e8a0&&r!=0x16e880);reads.push_back(r);return regs[r];} + void write(unsigned r,unsigned v){assert(readable(r));writes.push_back(r); + if(r==0x45404||r==0x45444||r==0x45454){unsigned mask=r==0x45404?0xa80aU:0x2aaaU;v=(v&~(mask>>1))|((v&mask)>>1);} + if(r>=0x46038&&r<=0x46050){v=(v&~(Clock::Locked|Clock::Powered))|((v&Clock::Enable)?Clock::Locked:0)|((v&Clock::Power)?Clock::Powered:0);} + regs[r]=v; + } +}; +struct Context { + Display13Power::NativeBridge bridge;Mode tcMode[4];OSObject *tcObject[4]{};MMIO *map=nullptr; + alignas(8) uint8_t controllerBytes[0xe08]{},powerBytes[0x80]{}; + unsigned tcBusy[4]{};IOWorkLoop *workLoop=&::workLoop; + void *controller=controllerBytes,*power=powerBytes,*lock=(void*)1;bool tcSignalReady[4]{},clockFaulted=false; +} display13FBPower; +struct Display13FBPowerIO { + MMIO *map; + bool readable(unsigned r){return map->readable(r);}unsigned read(unsigned r){return map->read(r);}void write(unsigned r,unsigned v){map->write(r,v);}void delayUs(unsigned){} + bool tcAuxSupported(){return true;}bool prepareTCAux(unsigned t){return (read(0x64300+256*t)&64)!=0;}bool tcAuxHealthy(unsigned){return true;} +}; +struct Display13FBTCAuxIO : Display13FBPowerIO { + unsigned tc;Display13FBTCAuxIO(MMIO*m,unsigned t):Display13FBPowerIO{m},tc(t){} + bool admitted(unsigned t){return t==tc&&tc<4&&map==display13FBPower.map;} + unsigned lock(){return 0;}void unlock(unsigned){}void bankFault(){assert(false);} +}; +void IODelay(unsigned){} +// PRODUCTION_IO +bool locked=false;void IOLockLock(void*){assert(!locked);locked=true;}void IOLockUnlock(void*){assert(locked);locked=false;} +void adlnFault(bool&fault,unsigned){fault=true;} +#define FunctionCast(f,p) reinterpret_cast(p) +struct XE { + Display13OutputWiring adlnOutputWiring;uintptr_t orgDisplay13DPPhyGating=0; + uintptr_t adlnWorkLoopInGate=reinterpret_cast(workLoopInGate); + static unsigned display13TCTarget(OSObject*p){return Display13FBMemory::field(p,0x120);} + static void wrapDisplay13DPPhyGating(void*,bool); + static unsigned wrapDisplay13TCMode(OSObject*,unsigned); + static void drainDisplay13TCMode(unsigned); +} instance,*callbackXE=&instance; +// PRODUCTION_GATE +struct Transport : Display13FBTCIO { + using Display13FBTCIO::Display13FBTCIO; + using Display13FBTCIO::powerHeld;using Display13FBTCIO::quiescent; + bool admissionReady(){return true;}bool inheritedVerified(uintptr_t,uintptr_t,unsigned){return false;} + unsigned nativeMode(){return Display13FBMemory::field(object,0x550);} + unsigned nativeSetMode(unsigned mode){memcpy(object->bytes+0x550,&mode,4);if(!mode)memset(object->bytes+0x558,0,0x1c);return 0;} + Display13Power::Result acquireDomains(Display13Power::Lease&l,unsigned d){Display13FBPowerIO io{map};return Display13Power::acquire(io,display13FBPower.bridge.state,l,d);} + Display13Power::Result releaseDomains(Display13Power::Lease&l){Display13FBPowerIO io{map};return Display13Power::release(io,display13FBPower.bridge.state,l);} + bool powerHeld(uintptr_t c,uintptr_t o,unsigned d){return c==reinterpret_cast(display13FBPower.controller)&&o==reinterpret_cast(object)&&d==tc+3&&accessHeld();} + bool quiescent(uintptr_t c,uintptr_t o,unsigned d){return modeQuiescent(c,o,d);} +}; +unsigned XE::wrapDisplay13TCMode(OSObject*p,unsigned wanted){ + assert(!wanted);auto &c=display13FBPower;unsigned d=Display13FBMemory::field(p,0x120); + Transport io{p,d-3};auto result=c.tcMode[d-3].disconnect(io,reinterpret_cast(c.controller),reinterpret_cast(p),d); + if(result.status==Status::Complete){c.tcObject[d-3]=nullptr;p->release();return 0;}return 1; +} +// PRODUCTION_DRAIN +templatevoid put(void*p,unsigned at,T value){memcpy(static_cast(p)+at,&value,sizeof(value));} +int main(){ + for(unsigned d=3;d<=6;++d)for(unsigned gone:{0U,4U,0x2dU,UINT32_MAX})for(bool cleanup:{false,true}){ + display13FBPower.~Context();new(&display13FBPower)Context;auto &c=display13FBPower;MMIO map;c.map=↦ + Port p;assert(Port::resolve(d,p));OSObject port;c.tcObject[p.tc]=&port; + unsigned cfg[6]={d};put(port.bytes,0x548,cfg);put(port.bytes,0x120,d); + instance.adlnOutputWiring.dp=instance.adlnOutputWiring.typeC=1U<(&map)); + put(c.controller,0xe00,c.workLoop); + map.regs={{0x45404,0},{0x45444,0},{0x45454,0},{0x42000,0x8e380000},{p.ready,0x1d},{p.buffer,0x80},{p.pll,0},{p.clock,0},{Clock::Gate,0xa5000000|Clock::gateBit(p.tc)}, + {p.laneAssignment(),12U<<(8*(p.tc&1))},{p.pinAssignment(),4U<<(4*(p.tc&1))},{0xc4000,0},{0x44470,1U<<(16+p.tc)}, + {0x60400,0},{0x70008,0},{0x46140,0},{p.hip,0x01010101},{p.phyRegister(0xa0),0},{0x64310+256*p.tc,0}}; + Display13FBPowerIO power{&map};assert(Display13Power::initialize(power,c.bridge.state,Display13Power::nativeDomains(0x7b)));c.bridge.begun=true;c.bridge.selectedPorts=0x78; + Transport io{&port,p.tc};auto &mode=c.tcMode[p.tc];const auto who=reinterpret_cast(c.controller),obj=reinterpret_cast(&port); + assert(mode.connect(io,who,obj,d,3).status==Status::Complete&&io.owned(d)); + assert(c.bridge.getDDI(power,d).status==Display13Power::Status::Complete); + map.regs[p.pll]=0xcc000000;map.regs[p.clock]=0x80001234;map.regs[Clock::Gate]&=~Clock::gateBit(p.tc);c.tcSignalReady[p.tc]=true; + put(port.bytes,0x570,1U); // Cached link config survives until native reset. + assert(mode.disconnect(io,who,obj,d).status==Status::InUse&&mode.disconnectPending); + map.regs[0x44470]=0;map.regs[p.ready]=gone;map.regs[p.laneAssignment()]=map.regs[p.pinAssignment()]=0; + assert(!io.owned(d)&&io.retainedOwned(d)); + // The power-down PHY gate keeps the captured assignment after HPD falls. + // Pin D mask C selects only the second physical pair. + map.writes.clear();XE::wrapDisplay13DPPhyGating(&port,true); + assert(!c.clockFaulted&&map.regs[p.phyRegister(0xa0)]==0x3e&&map.writes.size()==1); + if(cleanup){mode.phase=Mode::Phase::Cleanup;io.nativeSetMode(0);assert(io.retainedOwned(d));} + const auto buffer=map.regs[p.buffer]; + map.regs[p.buffer]&=~64U;assert(!io.retainedOwned(d)); + map.regs[p.buffer]=UINT32_MAX;assert(!io.retainedOwned(d));map.regs[p.buffer]=buffer; + OSObject stranger;c.tcObject[p.tc]=&stranger;assert(!io.retainedOwned(d));c.tcObject[p.tc]=&port; + auto leaseOwner=mode.auxLease.owner;mode.auxLease.owner=nullptr;assert(!io.retainedOwned(d));mode.auxLease.owner=leaseOwner; + const auto owner=mode.ownership.owner;mode.ownership.owner=owner+1; + assert(!io.retainedOwned(d));mode.ownership.owner=owner; + const auto peer=map.regs[Clock::Gate]&~Clock::gateBit(p.tc);map.writes.clear();map.noConnectorReads=true; + map.regs[p.buffer]|=1U<<31;assert(!Clock::stopLink(io,p)&&map.writes.empty());map.regs[p.buffer]&=~(1U<<31); + const auto wells=map.regs[0x45404];map.regs[0x45404]|=1U<<10; + map.regs[0x60400]=0x80000000U|((d+1)<<27);map.regs[0x70008]=0xc0000000; + assert(!Clock::stopLink(io,p)&&map.writes.empty()); + map.regs[0x60400]=map.regs[0x70008]=0;map.regs[0x45404]=wells; + assert(Clock::stopLink(io,p)&&!c.bridge.nativeDDIRefs(d)&&!(map.regs[p.pll]&0xcc000000U)); + assert((map.regs[Clock::Gate]&~Clock::gateBit(p.tc))==peer); + c.tcSignalReady[p.tc]=false; + if(!cleanup) { + inGate=false;XE::drainDisplay13TCMode(d);assert(mode.disconnectPending&&io.nativeMode()==3); + inGate=true;c.tcBusy[p.tc]=1;XE::drainDisplay13TCMode(d);assert(mode.disconnectPending); + c.tcBusy[p.tc]=0;map.regs[0x64310+256*p.tc]=0x80000000; + XE::drainDisplay13TCMode(d);assert(mode.disconnectPending); + map.regs[0x64310+256*p.tc]=0; + assert(Display13FBMemory::field(&port,0x570)==1); + XE::drainDisplay13TCMode(d);assert(!mode.disconnectPending&&!io.nativeMode()&&!c.tcObject[p.tc]&&!port.refs); + port.refs=1;c.tcObject[p.tc]=&port; + } else assert(mode.disconnect(io,who,obj,d).status==Status::Complete&&!io.nativeMode()); + assert(!(map.regs[p.buffer]&64)&&mode.phase==Mode::Phase::Idle&&!mode.lanes.mask); + for(auto refs:c.bridge.state.refs)assert(!refs); + // Reconnect must capture the new assignment, not the previous orientation. + map.noConnectorReads=false;map.regs[p.ready]=0x1d;map.regs[0x44470]=1U<<(16+p.tc); + map.regs[p.laneAssignment()]=3U<<(8*(p.tc&1));map.regs[p.pinAssignment()]=4U<<(4*(p.tc&1)); + assert(mode.connect(io,who,obj,d,3).status==Status::Complete&&mode.lanes.mask==3&&io.owned(d)); + } + puts("PASS real TC owner IO: lost HPD/mode/assignment, retained identity and power, clock stop then mode release, new orientation on reconnect"); +} diff --git a/tests/display13_tc_mode.cpp b/tests/display13_tc_mode.cpp index dc2a5fe1..327f72db 100644 --- a/tests/display13_tc_mode.cpp +++ b/tests/display13_tc_mode.cpp @@ -59,6 +59,22 @@ int main() { assert(m.phase==Mode::Phase::Idle && !m.ownership.owner && !m.auxLease.owner && !io.native); assert(m.connect(io,1,2,d).status==Status::Complete);assert(m.disconnect(io,1,2,d).status==Status::Complete); } + // DP-alt cannot reuse legacy native mode or unprotected FIA state. + for(unsigned d=3;d<=6;++d)for(unsigned physical:{3U,12U,15U}) { + IO io(d);Mode m;auto &p=io.port; + io.regs[p.ready]=0x1d;io.regs[0xc4000]=0;io.regs[0x44470]=1U<<(16+p.tc); + io.regs[p.laneAssignment()]=physical<<(8*(p.tc&1)); + io.regs[p.pinAssignment()]=(physical==15?3U:4U)<<(4*(p.tc&1)); + assert(m.connect(io,1,2,d,3).status==Status::Complete && io.native==3); + assert(m.lanes.mask==physical && m.lanes.count()==(physical==15?4:2)); + assert(m.connect(io,1,2,d,4).status==Status::Readback); + auto writes=io.sequence.size();io.regs[0x44470]=0; + assert(m.connect(io,1,2,d,3).status==Status::Readback && io.sequence.size()==writes); + assert(m.disconnect(io,1,2,d).status==Status::Complete && !m.lanes.mask); + io.regs[0x44470]=1U<<(16+p.tc);io.regs[p.laneAssignment()]=0; + assert(m.connect(io,1,2,d,3).status==Status::NotReady && !io.native); + assert(m.disconnect(io,1,2,d).status==Status::Complete); + } // Firmware may leave a fixed legacy DP source enabled but unowned (Linux // intel_tc_port_init_mode). Transfer only ownership; preserve width/link. for(unsigned d=3;d<=6;++d)for(unsigned owned:{0U,64U}) { diff --git a/tests/display13_tc_preparation.py b/tests/display13_tc_preparation.py index e0006512..27e5ca05 100644 --- a/tests/display13_tc_preparation.py +++ b/tests/display13_tc_preparation.py @@ -10,6 +10,7 @@ # Admission never substitutes an installed prototype for the real mode route. assert 'display13FBTCHooksReady' in s[s.index('unsigned XE::display13TCTarget('):s.index('uint32_t XE::wrapDisplay13TCMode(')] code=(r/'tests/display13_tc_preparation_fixture.cpp').read_text().replace('// FUNCTIONS',f) +code=code.replace('// MODE_QUIESCENCE',re.search(r' bool modeQuiescent\(.*?\n \}',s,re.S).group()) with tempfile.TemporaryDirectory(prefix='tc-preparation-') as d: p=Path(d);(p/'test.cpp').write_text(code) subprocess.run(['clang++','-std=c++17','-O1','-g','-fsanitize=address,undefined','-I'+str(r/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) diff --git a/tests/display13_tc_preparation_fixture.cpp b/tests/display13_tc_preparation_fixture.cpp index 0fb279d8..2fa939b6 100644 --- a/tests/display13_tc_preparation_fixture.cpp +++ b/tests/display13_tc_preparation_fixture.cpp @@ -46,6 +46,7 @@ struct Display13FBTCIO:Display13FBPowerIO { bool tcAuxSupported(){return true;}bool prepareTCAux(unsigned t){return t==tc && accessHeld() && (read(0x64300+256*t)&64);} bool tcAuxHealthy(unsigned){return true;} bool quiescent(unsigned d){return !(read(0x64000+256*d)&0x80000000);} + // MODE_QUIESCENCE }; struct XE { bool display13FramebufferRequested=true,display13FBTCHooksReady=true;XELP::Profile xeProfile; @@ -55,6 +56,7 @@ struct XE { static XELP::LinkRequirements framebufferLinkRequirements(void*,uint32_t); static unsigned display13TCTarget(OSObject*,bool=false); static uint32_t wrapDisplay13TCMode(OSObject*,uint32_t); + static uint32_t setDSIPortMode(OSObject*,uint32_t){assert(false && "TC must not enter DSI");return kIOReturnNotReady;} } instance,*callbackXE=&instance; // FUNCTIONS alignas(8) uint8_t portBytes[0x700]; @@ -71,7 +73,8 @@ unsigned base(OSObject*p,unsigned mode){ } int main(){ unsigned cases=0; - for(unsigned ddi=3;ddi<=6;++ddi)for(bool dp:{false,true})for(unsigned fault=0;fault<6;++fault){ + for(unsigned ddi=3;ddi<=6;++ddi)for(unsigned protocol=0;protocol<3;++protocol)for(unsigned fault=0;fault<6;++fault){ + const bool dp=protocol!=0,alt=protocol==2;if(alt && fault==1)continue; display13FBPower.~Context();new(&display13FBPower)Context;auto &c=display13FBPower; instance=XE{};instance.xeProfile=XELP::identify(0x46a68086,3);instance.display13TCBaseMode=(uintptr_t)base; MMIO map;c.map=↦map.regs[0x42000]=0x8e380000; @@ -80,6 +83,10 @@ int main(){ Display13FBPowerIO power{&map};assert(Display13Power::initialize(power,c.bridge.state,Display13Power::nativeDomains(0x7b))); c.bridge.begun=true;c.bridge.selectedPorts=0x78;c.bridge.bootPort=ddi; auto &w=instance.adlnOutputWiring;w.valid=true;if(dp)w.dp=1u<(reinterpret_cast(instance.adlnPlatform)+0x20+24*ddi); unsigned values[]={ddi,ddi+6,dp?1u:2u,1u,dp?0x400u:0x800u,dp?0x201u:0x202u};memcpy(cfg,values,24); memset(portBytes,0,sizeof(portBytes));auto p=new(portBytes)OSObject; @@ -96,7 +103,7 @@ int main(){ if(fault==3)instance.display13FBTCHooksReady=false; if(fault==4)map.failOwner=true; if(fault==5)cfg[4]=1; - const auto before=map.regs;auto result=XE::wrapDisplay13TCMode(p,4); + const auto before=map.regs;auto result=XE::wrapDisplay13TCMode(p,alt?3:4); if((result==0)!=(fault<=1))fprintf(stderr,"case ddi=%u dp=%u fault=%u phase=%u buffer=%x power=%x aux=%x\n",ddi,dp,fault,unsigned(c.tcMode[ddi-3].phase),map.regs[0x64000+256*ddi],map.regs[0x45404],map.regs[0x45444]); assert((result==0)==(fault<=1));assert(nativeCalls==(fault<=1?1u:0u)); if(result==0){assert(c.tcMode[ddi-3].ownership.active && c.tcMode[ddi-3].auxLease.owner);if(fault==1)assert(map.regs[0x64000+256*ddi]==before.at(0x64000+256*ddi));} diff --git a/tests/display13_trial_retirement.py b/tests/display13_trial_retirement.py new file mode 100644 index 00000000..c776b87d --- /dev/null +++ b/tests/display13_trial_retirement.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Exercise the opt-in boot-retirement wrapper's native ordering and refusals.""" +from pathlib import Path +import re,subprocess,tempfile +root=Path(__file__).resolve().parents[1] +s=(root/'WhateverXe/kern_xe.cpp').read_text() +functions='\n'.join(re.search(r'(?:uint32_t|void \*) XE::'+n+r'\(.*?\n\}',s,re.S).group() for n in ['wrapDisplay13TrialCommit']) +code=r''' +#include "kern_display13_fb_memory.hpp" +#include +#include +#include +struct OSObject {alignas(8) unsigned char bytes[0x4b90]{};unsigned size=0x4b90; + OSObject *getMetaClass(){return this;} unsigned getClassSize(){return size;}}; +using IOService=OSObject; +namespace TGLFBBinding {enum class Status {Unavailable,Complete};struct Readout {Status status=Status::Complete;unsigned ddi=1;};} +struct Wiring{}; +bool trial=true,healthy=true,selected=true,bound=true; +TGLFBBinding::Readout adlnInspectPathBinding(IOService*,OSObject*,void*,Wiring){return {bound?TGLFBBinding::Status::Complete:TGLFBBinding::Status::Unavailable,1};} +namespace Display13FBTopology {bool selected(void*,uint32_t*,Wiring){return ::selected;}} +bool checkKernelArgument(const char*){return trial;} +struct IORegistryEntry {static IORegistryEntry*getRegistryRoot(){static IORegistryEntry r;return &r;}templatevoid setProperty(T...){} }; +#define FunctionCast(f,p) reinterpret_cast(p) +OSObject ctl,fb,path,oldPort,newPort;unsigned nativeCalls=0,freeCalls=0,freeError=0;bool fbPresent=true; +unsigned native(void *c,void*,void*){assert(c==&ctl);++nativeCalls;return 123;} +OSObject *lookup(unsigned index){return index==3?&newPort:nullptr;} +struct XE { + bool display13FramebufferRequested=true;void*adlnPlatform=nullptr;Wiring adlnOutputWiring; + uintptr_t tglPortByDDI=(uintptr_t)lookup,orgDisplay13TrialCommit=(uintptr_t)native; + static XE *callbackXE; + static void *tglResourceFB(void *c,unsigned i){return c==&ctl&&i==0&&fbPresent?&fb:nullptr;} + static bool display13RetirementHealthy(){return healthy;} + static unsigned wrapDisplay13FreeResources(void *f){assert(f==&fb&&!nativeCalls);++freeCalls;return freeError;} + static uint32_t wrapDisplay13TrialCommit(void*,void*,void*); +} state; +XE *XE::callbackXE=&state; +FUNCTIONS +templatevoid put(OSObject &o,unsigned at,T v){memcpy(o.bytes+at,&v,sizeof(v));} +int main(){ + for(unsigned bad=0;bad<24;++bad){ + state={};ctl={};fb={};path={};oldPort={};newPort={};trial=healthy=selected=bound=fbPresent=true; + nativeCalls=freeCalls=freeError=0;uint32_t req[32]={1},out[32]{};req[0x48/4]=4; + put(fb,0x44de,uint8_t(1));put(fb,0x1e7,uint8_t(1));put(fb,0x49e8,1u);put(fb,0x4a00,1u); + put(fb,0x4a08,&path);put(path,0x3648,&oldPort);put(path,0x3644,1u);put(newPort,0x554,2u);put(newPort,0x120,3u); + switch(bad){ + case 1:trial=false;break;case 2:state.display13FramebufferRequested=false;break; + case 3:fbPresent=false;break;case 4:req[0]=2;break;case 5:healthy=false;break; + case 6:put(fb,0x1e0,uint8_t(1));break;case 7:put(fb,0x44de,uint8_t(0));break; + case 8:put(fb,0x1e7,uint8_t(0));break;case 9:put(fb,0x49e8,2u);break; + case 10:put(fb,0x4a00,0u);break;case 11:bound=false;break;case 12:req[0x48/4]=0;break; + case 13:req[0x48/4]=10;break;case 14:state.tglPortByDDI=0;break; + case 15:put(path,0x3644,0u);break;case 16:put(path,0x3644,3u);break; + case 17:put(oldPort,0x554,2u);break;case 18:put(newPort,0x554,1u);break; + case 19:selected=false;break;case 20:put(newPort,0x120,4u);break; + case 21:freeError=77;break;case 22:put(path,0x3644,2u);break; + case 23:put(path,0x3648,&newPort);break; + } + const auto result=XE::wrapDisplay13TrialCommit(&ctl,req,out); + const bool retires=bad==0||bad==21||bad==22; + assert(freeCalls==unsigned(retires));assert(nativeCalls==unsigned(bad!=21)); + assert(result==(bad==21?77u:123u)); + } + assert(XE::wrapDisplay13TrialCommit(&ctl,nullptr,nullptr)==123); + puts("PASS trial retirement: native stop before replacement, failure propagation, opt-in and live-resource refusals"); +} +'''.replace('FUNCTIONS',functions) +with tempfile.TemporaryDirectory(prefix='wex-trial-retire-') as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/display13_wm.cpp b/tests/display13_wm.cpp index 85130b0d..a62f533e 100644 --- a/tests/display13_wm.cpp +++ b/tests/display13_wm.cpp @@ -261,6 +261,21 @@ int main() { assert(latch(io,io.snapshot(variable)).status==Status::Readback); variable.height=0;assert(latch(io,io.snapshot(variable)).status==Status::Input); } + // DSI timing follows transcoder0 while the surface/frame/WM proof stays + // on the actual pipe. Remove ordinary timings so accidental fallback fails. + for(unsigned pipe=0;pipe<3;++pipe) { + LatchIO io(p);io.moveToPipe(pipe);const auto bank=0x1000*pipe; + io.regs[0x6B000]=io.regs[0x60000+bank];io.regs[0x6B00C]=io.regs[0x6000C+bank]; + io.regs.erase(0x60000+bank);io.regs.erase(0x6000C+bank); + io.regs[0x70008+bank]=0;io.regs[0x7B008]=0xC0000000; + const uint32_t inputs[]={0,5,6};io.regs[0x6B400]=0x80000006|(inputs[pipe]<<12); + auto expected=io.snapshot(p);expected.transcoders.dsi0Pipe=pipe; + auto result=latch(io,expected); + assert(result.status==Status::Complete && result.waitUs==2000 && io.writes.empty()); + io.regs[0x6B400]^=1U<<12;assert(latch(io,expected).status==Status::Conflict); + io.regs[0x6B400]^=1U<<12;io.regs[0x70008+bank]=0xC0000000; + assert(latch(io,expected).status==Status::Conflict); + } LatchIO committed(p);const auto proof=latch(committed,committed.snapshot(p)); assert(proof.status==Status::Complete && proof.frameBefore==100 && proof.frameAfter==101 && proof.waitUs==2000); assert(proof.budgetUs==51001 && committed.writes.empty()); @@ -283,10 +298,19 @@ int main() { assert(delayedProof.status==Status::Complete && delayedProof.frameAfter!=delayedProof.frameBefore && delayedProof.waitUs==3000); DelayedCursor stuckCursor(p,cursor);stuckCursor.stuck=true; const auto stuckProof=latch(stuckCursor,stuckCursor.snapshot(p,&cursor)); - assert(stuckProof.status==Status::Readback && stuckProof.waitUs==52000 && stuckProof.lastReg==0x700AC); + assert(stuckProof.status==Status::Unavailable && stuckProof.check==LatchCheck::AwaitingFlip && stuckProof.waitUs==52000 && stuckProof.lastReg==0x700AC); LatchIO oldLive(p);oldLive.surface=finalFlip.primary;oldLive.regs[0x7019C]=oldLive.surface; oldLive.oldSurface=true; - assert(latch(oldLive,oldLive.snapshot(p)).status==Status::Readback); + const auto pendingFlip=latch(oldLive,oldLive.snapshot(p)); + assert(pendingFlip.status==Status::Unavailable && pendingFlip.check==LatchCheck::AwaitingFlip && oldLive.writes.empty()); + // Repeated attempts cannot manufacture completion or retire anything. + oldLive.elapsed=0; + assert(latch(oldLive,oldLive.snapshot(p)).status==Status::Unavailable); + oldLive.elapsed=0;oldLive.oldSurface=false; + assert(latch(oldLive,oldLive.snapshot(p)).status==Status::Complete); + // An invalid read remains a fault, even at the live-register boundary. + LatchIO invalidLive(p);invalidLive.oldSurface=true;invalidLive.regs[0x701AC]=UINT32_MAX; + assert(latch(invalidLive,invalidLive.snapshot(p)).status==Status::Readback); auto clockPlan=committed.snapshot(p); assert(clockFits(clockPlan,1305600000) && clockFits(clockPlan,297000000)); assert(!clockFits(clockPlan,296999999)); @@ -301,7 +325,7 @@ int main() { const auto timeoutProof=latch(sameFrame,sameFrame.snapshot(p)); assert(timeoutProof.status==Status::CommitTimeout && timeoutProof.waitUs==52000); LatchIO staleSurface(p);staleSurface.oldSurface=true; - assert(latch(staleSurface,staleSurface.snapshot(p)).status==Status::Readback); + assert(latch(staleSurface,staleSurface.snapshot(p)).status==Status::Unavailable); LatchIO poweredOff(p);poweredOff.losePower=true; assert(latch(poweredOff,poweredOff.snapshot(p)).status==Status::Power); LatchIO invalidWM(p);invalidWM.regs[0x70258]^=1; @@ -349,14 +373,23 @@ int main() { assert(latch(wrongWM,wrongWM.snapshot(p)).status==Status::Readback); LatchIO wrongLine(p);wrongLine.moveToPipe(pipe);wrongLine.regs[0x45270+4*pipe]=118; assert(latch(wrongLine,wrongLine.snapshot(p)).status==Status::Readback); - // The diagnostic must retain the rejecting branch and the values - // already consumed by it, including pipe C, without probing farther. + // Reconnect keeps the old SURF while native WM/scaler flushes precede + // plane enable. A nonzero address cannot certify an active scanout. LatchIO disabledPlane(p);disabledPlane.moveToPipe(pipe); disabledPlane.regs[0x70180+0x1000*pipe]&=~Enabled; const auto controls=latch(disabledPlane,disabledPlane.snapshot(p)); - assert(controls.status==Status::Conflict && controls.check==LatchCheck::Controls && + assert(controls.status==Status::Unavailable && controls.check==LatchCheck::AwaitingScanout && controls.pipeControl==0xC0000000 && controls.planeControl==0x04000000 && - !controls.cursorControl && disabledPlane.reads.back()==0x70080+0x1000*pipe && disabledPlane.writes.empty()); + !controls.cursorControl && disabledPlane.writes.empty() && !disabledPlane.elapsed && controls.frameAfter==0); + disabledPlane.regs[0x70280+0x1000*pipe]=Enabled; + assert(latch(disabledPlane,disabledPlane.snapshot(p)).status==Status::Conflict); + disabledPlane.regs[0x70280+0x1000*pipe]=0; + disabledPlane.regs[0x70180+0x1000*pipe]|=Enabled; + disabledPlane.frozen=true; + assert(latch(disabledPlane,disabledPlane.snapshot(p)).status==Status::CommitTimeout); + disabledPlane.frozen=false;disabledPlane.elapsed=0; + disabledPlane.regs[0x701AC+0x1000*pipe]=0; + assert(latch(disabledPlane,disabledPlane.snapshot(p)).status==Status::Complete); // Native scaler flush before the first attached scanout: timing and // WM are programmed, but the primary remains disabled and SURF is 0. // This is pending, not success; a later enabled surface must earn the @@ -385,6 +418,22 @@ int main() { LatchIO blankWithCursor(p);blankWithCursor.moveToPipe(pipe);blankWithCursor.surface=0; blankWithCursor.regs[0x70180+bank]=0;blankWithCursor.regs[0x70080+bank]=7; assert(latch(blankWithCursor,blankWithCursor.snapshot(p)).status==Status::Conflict); + // A qualified cursor may arrive before the first primary surface. + // It cannot certify retirement; the later primary still needs VBlank + // and live-surface evidence. An inconsistent cursor remains a fault. + LatchIO cursorFirst(p,&cursor);cursorFirst.moveToPipe(pipe); + cursorFirst.surface=0;cursorFirst.regs[0x70180+bank]=0;cursorFirst.regs[0x7019C+bank]=0; + const auto cursorPending=latch(cursorFirst,cursorFirst.snapshot(p,&cursor)); + assert(cursorPending.status==Status::Unavailable && cursorPending.check==LatchCheck::AwaitingScanout && + cursorFirst.writes.empty() && !cursorFirst.elapsed); + cursorFirst.regs[0x70080+bank]=0; + assert(latch(cursorFirst,cursorFirst.snapshot(p,&cursor)).status==Status::Conflict); + cursorFirst.regs[0x70080+bank]=0x22; + assert(latch(cursorFirst,cursorFirst.snapshot(p,&cursor)).status==Status::Conflict); + cursorFirst.regs[0x70080+bank]=7; + cursorFirst.regs[0x70180+bank]=0x84000000; + cursorFirst.regs[0x7019C+bank]=cursorFirst.surface=0x100000; + assert(latch(cursorFirst,cursorFirst.snapshot(p,&cursor)).status==Status::Complete && cursorFirst.elapsed==2000); LatchIO peerPlane(p);peerPlane.moveToPipe(pipe);peerPlane.regs[0x70280+0x1000*pipe]=Enabled; const auto peer=latch(peerPlane,peerPlane.snapshot(p)); assert(peer.status==Status::Conflict && peer.check==LatchCheck::OtherPlane && diff --git a/tests/display_backend_dispatch.py b/tests/display_backend_dispatch.py index 48659cb5..5a01f451 100644 --- a/tests/display_backend_dispatch.py +++ b/tests/display_backend_dispatch.py @@ -5,7 +5,7 @@ r=Path(__file__).resolve().parents[1] s=(r/'WhateverXe/kern_xe.cpp').read_text() a=s.index('\t// Display dispatch is independent') -b=s.index('\nstatic bool adlnWriterMappingBound();',a) +b=s.index('\n}',a)+2 body=s[a:b].rstrip()[:-1] kernel_start=s.index('\tif (adlnDetected || display13FramebufferRequested) {') kernel_end=s.index('\n\tif (tglRequested) {',kernel_start) diff --git a/tests/dsi_aperture_adapter.py b/tests/dsi_aperture_adapter.py new file mode 100644 index 00000000..aabf1301 --- /dev/null +++ b/tests/dsi_aperture_adapter.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""An unpublished DSI aperture stays unavailable while private scanout is armed.""" +from pathlib import Path +from production_source import read_source +import re,subprocess,tempfile +root=Path(__file__).resolve().parents[1] +s=read_source(root/'WhateverXe/kern_xe.cpp') +body=re.search(r'IOMemoryDescriptor \*XE::wrapAlderLakeNAperture\(.*?\n\}',s,re.S)[0] +code=r''' +#include "kern_display13_fb_memory.hpp" +#include +#include +#include +using namespace Display13FBMemory; +using IOByteCount=uint64_t; +unsigned depth=0;struct IOLock{};void IOLockLock(IOLock*){assert(!depth++);}void IOLockUnlock(IOLock*){assert(depth--==1);} +struct OSObject {alignas(8) uint8_t data[0x4B90]{};unsigned refs=1; + OSObject *getMetaClass(){return this;}unsigned getClassSize(){return sizeof(data);} + uint64_t getLength(){return 0x10000000;}uint64_t getPhysicalSegment(uint64_t off,uint64_t *span,unsigned){*span=getLength();return 0x80000000+off;} + void retain(){++refs;}void release(){assert(refs>1);--refs;} +}; +using IOService=OSObject;using IOMemoryDescriptor=OSObject; +#define OSDynamicCast(T,p) static_cast(p) +#define OSSafeReleaseNULL(p) do{if(p)(p)->release();p=nullptr;}while(0) +constexpr unsigned kIODirectionInOut=1,kIOMemoryThreadSafe=2,kIOMemoryMapperNone=0; +struct IOSubMemoryDescriptor {static IOMemoryDescriptor *withSubRange(IOMemoryDescriptor*,unsigned,unsigned,unsigned){assert(false);return nullptr;}}; +struct {IOLock *lock;void *controller;bool memoryFaulted=false; + struct {IOService *memoryFramebuffer=nullptr;struct {Pool pool;}memory;}pipe[3];}display13FBPower; +bool associationOK=true,target=true,pending=false; +bool display13FBAssociations(void*,unsigned,void*,unsigned,uintptr_t){return associationOK;} +uintptr_t display13FBExpectedOwner(void*){return 1;} +bool dsiModeTarget(IOService*){return target;}bool dsiPendingTarget(IOService*){return pending;} +namespace Display13FBSysMem {enum class Status{Complete};} +struct Inspection{Display13FBSysMem::Status status=Display13FBSysMem::Status::Complete;}; +Inspection adlnInspectSysMem(IOService*,IOService*,unsigned,const PoolLayout&){assert(false);return {};} +void adlnPublishMemory(const char*,unsigned,void*,unsigned,bool){} +void adlnFault(bool &v,unsigned){v=true;} +struct XE {bool display13FBMemoryHooksReady=true,dsiStopAttempted=false;struct {unsigned dsi=1;}adlnOutputWiring; + static IOMemoryDescriptor *wrapAlderLakeNAperture(IOService*,int); +}*callbackXE; +BODY +templatevoid put(void *p,unsigned off,T v){memcpy(static_cast(p)+off,&v,sizeof(v));} +int main(){ + for(unsigned problem=0;problem<7;++problem){IOService fb,controller,parent;IOLock lock;XE xe;callbackXE=&xe; + display13FBPower={};auto &c=display13FBPower;c.lock=&lock;c.controller=&controller;c.pipe[0].memoryFramebuffer=&fb; + put(&fb,0x1E0,uint8_t(1));put(&fb,0x4288,uint8_t(0));put(&controller,0xDF0,1U); + put(&controller,0xDF4,uint8_t(1));put(&controller,0xDF5,uint8_t(3));put(&controller,0x1A10,1U); + put(&controller,0x1A28,uint64_t(0x80000000));put(&controller,0x1A30,0x800000U);put(&controller,0x1A34,0x800U); + put(&controller,0xC80,&parent);c.pipe[0].memory.pool=pool(&controller); + pending=problem!=0;target=true; + switch(problem){case 2:put(&fb,0x4314,1U);break;case 3:put(&fb,0x4330,1U);break; + case 4:c.pipe[0].memory.pool.pages++;break;case 5:target=false;break;case 6:xe.dsiStopAttempted=true;break;} + assert(!xe.wrapAlderLakeNAperture(&fb,0) && !depth && parent.refs==1); + assert(c.memoryFaulted==(problem>=2)); + } + puts("PASS production DSI aperture: zero unpublished range before/after private surface stays unavailable; invalid nonzero/backing/target/stop still faults"); +} +'''.replace('BODY',body) +with tempfile.TemporaryDirectory(prefix='dsi-aperture-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_apple_adapter.py b/tests/dsi_apple_adapter.py new file mode 100644 index 00000000..4b7566d1 --- /dev/null +++ b/tests/dsi_apple_adapter.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python3 +"""Replay production DSI fixed-mode ownership/dispatch with IOKit leaves mocked.""" +from pathlib import Path +from production_source import read_source +import hashlib, re, subprocess, sys, tempfile + +root = Path(__file__).resolve().parents[1] +source = read_source(root/'WhateverXe/kern_xe.cpp') +owner = source[source.index('struct DSIFixedModeOwner {'):source.index('// AlderLakeN start bypasses')] +names = ['recordDSIClient', 'releaseDSIFixedMode', 'bindDSIFixedMode', 'wrapDSISetDetailedTimings', 'wrapDSIValidateMode', + 'wrapDSIGetModes', 'wrapDSIGetModeCount', 'wrapDSIGetModeInformation', 'wrapDSIGetCurrentMode', 'wrapDSIGetOnlineInfo', 'wrapDSIDDC'] +functions = '\n'.join(re.search(r'(?:void|bool|uint32_t) XE::'+name+r'\(.*?\n\}', source, re.S)[0] for name in names) +if len(sys.argv) == 2: + binary = Path(sys.argv[1]).read_bytes() + assert hashlib.sha256(binary).hexdigest() == 'c004fc1ac9ee4157e2e12736f40fa6639bde025a279326beeb9dd48c9b445731' + installer = re.search(r'bool XE::processDisplay13DSIModes\(.*?\n\}', source, re.S)[0] + offsets = [int(x, 0) for x in re.search(r'offsets\[\]=\{([^}]+)', installer)[1].split(',')] + entries = re.search(r'entries\[\]\[16\]=\{(.*?)\};', installer, re.S)[1] + entries = [bytes(int(x, 0) for x in row.split(',')) for row in re.findall(r'\{([^}]+)\}', entries)] + assert len(entries) == len(offsets) == 9 + for offset, entry in zip(offsets, entries): + assert binary[offset:offset+16] == entry, hex(offset) + +code = r''' +#include "kern_dsi_apple_timing.hpp" +#include "kern_dsi_readout.hpp" +#include "kern_display13_transcoder.hpp" +#include "kern_display13_output_wiring.hpp" +#include +#include +#include +#include +#include +constexpr uint32_t kIOReturnSuccess=0,kIOReturnNotReady=0xe00002d8U, + kIOReturnUnsupported=0xe00002c7U,kIOReturnBadArgument=0xe00002c2U,kIOReturnNoResources=0xe00002bdU; +void *current_thread(){return reinterpret_cast(1);} +#define arrsize(x) (sizeof(x)/sizeof(*(x))) +unsigned alive=0,calls[5]{};bool dataOK=true,arrayOK=true,publishOK=true,geometryOK=true,clearOK=true,capacityOK=true,appendOK=true; +struct Meta {unsigned getClassSize(){return 0x4b90;}} meta; +struct OSObject { + unsigned refs=1;OSObject(){++alive;}virtual ~OSObject(){--alive;} + void retain(){assert(refs);++refs;}void release(){assert(refs);if(!--refs)delete this;} + Meta *getMetaClass(){return &meta;} +}; +struct OSNumber:OSObject {uint64_t value=0;uint64_t unsigned64BitValue(){return value;}}; +struct OSData:OSObject { + std::vector bytes; + static OSData *withCapacity(unsigned n){if(!capacityOK)return nullptr;auto d=new OSData;d->bytes.reserve(n);return d;} + bool appendBytes(const void *p,unsigned n){if(!appendOK)return false;auto b=static_cast(p);bytes.insert(bytes.end(),b,b+n);return true;} + static OSData *withBytes(const void *p,unsigned n){if(!dataOK)return nullptr;auto d=new OSData; + d->bytes.assign(static_cast(p),static_cast(p)+n);return d;} + unsigned getLength(){return bytes.size();}const void *getBytesNoCopy(){return bytes.data();} +}; +struct OSArray:OSObject { + std::vector objects; + static OSArray *withCapacity(unsigned n){assert(n>=1 && n<=2);return arrayOK?new OSArray:nullptr;} + bool setObject(OSObject *p){p->retain();objects.push_back(p);return true;} + unsigned getCount(){return objects.size();}OSObject *getObject(unsigned i){return irelease();} +}; +struct IOService:OSObject { + alignas(8) uint8_t storage[0x4c00]{};std::map properties; + bool setProperty(const char *k,OSObject *p){if(!publishOK)return false;p->retain();removeProperty(k);properties[k]=p;return true;} + void setProperty(const char *,bool){} + bool setProperty(const char *k,const void *p,unsigned n){auto d=OSData::withBytes(p,n);bool ok=setProperty(k,d);d->release();return ok;} + OSObject *getProperty(const char *k){auto i=properties.find(k);return i==properties.end()?nullptr:i->second;} + void removeProperty(const char *k){auto i=properties.find(k);if(i!=properties.end()){i->second->release();properties.erase(i);}} + ~IOService(){for(auto i:properties)i.second->release();} +}; +struct IORegistryEntry {void setProperty(const char *key,OSData *d){assert((!strcmp(key,"WEXDSITimingRejected") && d->getLength()==160) || (!strcmp(key,"WEXDSITimingList") && !(d->getLength()%160)));} + void removeProperty(const char *key){assert(!strcmp(key,"WEXDSITimingList"));} + static IORegistryEntry *getRegistryRoot(){static IORegistryEntry r;return &r;} + void setProperty(const char *key,const void *p,unsigned n){if(!strncmp(key,"WEXDSIModeInfo",14)){assert(n==48);return;}if(!strcmp(key,"WEXDSITimingList")){assert(!(n%160));return;}if(!strcmp(key,"WEXDSITimingRejected")){assert(n==160);return;}assert(n==128);if(!strcmp(key,"WEXDSITimingOffer"))return;const auto w=static_cast(p);assert(w[0]==1 && w[1]<13 && w[2]);}}; +struct IORecursiveLock {unsigned depth=0;};struct IOLock {unsigned depth=0;}; +void IORecursiveLockLock(IORecursiveLock *l){++l->depth;}void IORecursiveLockUnlock(IORecursiveLock *l){assert(l->depth);--l->depth;} +void IOLockLock(IOLock *l){++l->depth;}void IOLockUnlock(IOLock *l){assert(l->depth);--l->depth;} +#define OSDynamicCast(T,p) dynamic_cast(p) +#define OSSafeReleaseNULL(p) do{if(p){p->release();p=nullptr;}}while(0) +#define FunctionCast(f,p) reinterpret_cast(p) +OSObject *testPath=nullptr; +namespace Display13FBMemory { + templateT &field(void *p,unsigned o){return *reinterpret_cast(static_cast(p)+o);} + templateT singlePath(void *){return static_cast(testPath);} +} +namespace TGLFBBinding {enum class Status {Bad,Complete};} +struct Binding {TGLFBBinding::Status status=TGLFBBinding::Status::Complete;uint32_t ddi=0,pipe=0;} binding; +Binding adlnInspectPathBinding(IOService *,OSObject *path,void *,const Display13OutputWiring &){assert(path);return binding;} +struct {IOLock *lock;void *controller;Display13Transcoder::Bindings transcoders;XELP::DSI::Readout bootDSI; + bool memoryFaulted=false;struct {struct {bool faulted=false;} state;} bridge;} display13FBPower; +struct XE { + bool dsiPanelOnly=false;bool display13DSIModeHooksReady=true;Display13OutputWiring adlnOutputWiring;void *adlnTopologyController=nullptr; + uint8_t adlnPlatform[0x1c0]{};uintptr_t orgDSIMode[9]{}; + static bool updateReady; + static bool dsiNativeUpdateAllowed(IOService *){return updateReady;} + static uint32_t wrapDSIDDC(IOService *,uint32_t,uint8_t *,uint64_t *,uint32_t,OSObject *); + static uint32_t wrapDSIGetCurrentMode(IOService *,int32_t *,int32_t *); + static uint32_t wrapDSIGetOnlineInfo(IOService *,OSObject *,uint8_t *,uint8_t *); + static void recordDSIClient(IOService *,unsigned,uint64_t,uint32_t); + bool releaseDSIFixedMode(IOService *); + bool bindDSIFixedMode(IOService *,const XELP::DSI::Profile &); + static uint32_t wrapDSISetDetailedTimings(IOService *,OSArray *); + static uint32_t wrapDSIValidateMode(IOService *,int32_t,const void **,const void **); + static uint32_t wrapDSIGetModes(IOService *,int32_t *); + static uint32_t wrapDSIGetModeCount(IOService *); + static uint32_t wrapDSIGetModeInformation(IOService *,int32_t,void *); +} xe,*callbackXE=&xe; +bool XE::updateReady=true; +OWNER +FUNCTIONS +uint32_t publish(IOService *fb,OSArray *a){++calls[0];if(a)fb->setProperty("IOFBDetailedTimings",a);else {if(!clearOK)return 0;fb->removeProperty("IOFBDetailedTimings");} + Display13FBMemory::field(fb,0x4260)=a;return 0;} +uint32_t validate(IOService *fb,int32_t mode,const void **desc,const void **timing){++calls[1]; + auto a=Display13FBMemory::field(fb,0x4260);assert(a && (mode&1023)==0); + if(desc)*desc=reinterpret_cast(1);if(timing)*timing=static_cast(a->getObject(0))->getBytesNoCopy();return 0;} +uint32_t modes(IOService *,int32_t *p){++calls[2];if(p)*p=42;return 0;} +uint32_t count(IOService *){++calls[3];return 3;} +uint32_t information(IOService *fb,int32_t mode,void *p){++calls[4];const void *t=nullptr; + auto r=XE::wrapDSIValidateMode(fb,mode,nullptr,&t);if(r)return r; + auto timing=static_cast(t); + auto info=static_cast(p);*info={}; + info->width=geometryOK?(timing->horizontalScaled?timing->horizontalScaled:timing->horizontalActive):1;info->height=timing->verticalScaled?timing->verticalScaled:timing->verticalActive;info->depth=2;return 0;} +unsigned nativeCurrentCalls=0,nativeOnlineCalls=0,nativeDDCCalls=0; +uint32_t nativeDDC(IOService *,uint32_t block,uint8_t *data,uint64_t *length,uint32_t flags,OSObject *path){ + assert(block==2 && flags==1 && path==testPath);++nativeDDCCalls;*length=128;data[0]=42;return 9;} + +uint32_t nativeCurrent(IOService *,int32_t *m,int32_t *d){++nativeCurrentCalls;if(m)*m=77;if(d)*d=3;return 7;} +uint32_t nativeOnline(IOService *,OSObject *,uint8_t *on,uint8_t *edid){++nativeOnlineCalls;if(on)*on=9;if(edid)edid[0]=42;return 8;} +int main(){ + using namespace XELP::DSI;using namespace AppleAdapter;IORecursiveLock fbLock;IOLock ownerLock; + auto fb=new IOService;auto peer=new IOService;testPath=peer; + Display13FBMemory::field(fb,0x4220)=&fbLock; + display13FBPower.lock=&ownerLock;display13FBPower.controller=xe.adlnTopologyController=peer; + display13FBPower.transcoders.dsi0Pipe=0;xe.adlnOutputWiring.valid=true;xe.adlnOutputWiring.dsi=1; + xe.orgDSIMode[0]=reinterpret_cast(publish);xe.orgDSIMode[1]=reinterpret_cast(validate); + xe.orgDSIMode[2]=reinterpret_cast(modes);xe.orgDSIMode[3]=reinterpret_cast(count); + xe.orgDSIMode[4]=reinterpret_cast(information);xe.orgDSIMode[8]=reinterpret_cast(nativeDDC); + xe.orgDSIMode[6]=reinterpret_cast(nativeCurrent);xe.orgDSIMode[7]=reinterpret_cast(nativeOnline); + Display13FBMemory::field(peer,0x1DC)=1; + Profile p;p.status=ProfileStatus::Valid;p.lanes=4;p.mode={135255,1080,46,22,2,1920,82,40,2,86,154}; + auto plan=linkPlan(p);auto &r=display13FBPower.bootDSI;r.status=ReadoutStatus::Complete;r.identity={0,0,0,1}; + r.afeKHz=811500;r.htotal=plan.htotal;r.hsync=plan.hsync;r.vtotal=plan.vtotal;r.vsync=plan.vsync; + for(unsigned failure=0;failure<4;++failure){const auto before=alive;dataOK=failure!=0;arrayOK=failure!=1; + binding.status=failure==2?TGLFBBinding::Status::Bad:TGLFBBinding::Status::Complete; + display13FBPower.memoryFaulted=failure==3; + assert(!xe.bindDSIFixedMode(fb,p) && !dsiModeTarget(fb) && alive==before && !calls[0]);} + dataOK=arrayOK=true;display13FBPower.memoryFaulted=false; + assert(xe.bindDSIFixedMode(fb,p) && fb->refs==2 && dsiFixedMode.array->refs==2 && dsiFixedMode.data->refs==2); + // Profile-derived enumeration never calls native EDID/AUX. + uint8_t online=0,edid[128];memset(edid,0xCC,sizeof(edid));uint64_t length=128; + assert(!XE::wrapDSIDDC(fb,1,edid,&length,0,nullptr) && length==128 && edid[0]==0 && edid[1]==255 && !nativeDDCCalls); + uint8_t expected[128];assert(makePanelEDID(p.mode,expected) && !memcmp(edid,expected,128)); + length=127;memset(edid,0xCC,128);assert(XE::wrapDSIDDC(fb,1,edid,&length,0,nullptr)==kIOReturnBadArgument && !length && edid[0]==0xCC); + length=128;assert(XE::wrapDSIDDC(fb,2,edid,&length,0,nullptr)==kIOReturnUnsupported && !length); + {const auto saved=dsiFixedMode.mode;dsiFixedMode.mode={136887,1200,68,40,12,1920,239,205,2,141,226}; + length=128;assert(!XE::wrapDSIDDC(fb,1,edid,&length,0,nullptr) && edid[126]==1); + length=128;assert(!XE::wrapDSIDDC(fb,2,edid,&length,0,nullptr) && length==128 && edid[0]==0x70); + uint8_t block[128];assert(makePanelEDID(dsiFixedMode.mode,block,2) && !memcmp(block,edid,128)); + dsiFixedMode.mode=saved;length=128;assert(!XE::wrapDSIDDC(fb,1,edid,&length,0,nullptr));} + length=128;assert(XE::wrapDSIDDC(fb,1,edid,&length,1,nullptr)==kIOReturnUnsupported && !length); + assert(XE::wrapDSIDDC(peer,2,edid,&length,1,testPath)==9 && length==128 && nativeDDCCalls==1); + xe.adlnOutputWiring.dsi=0;assert(XE::wrapDSIDDC(fb,2,edid,&length,1,testPath)==9 && nativeDDCCalls==2);xe.adlnOutputWiring.dsi=1; + assert(!XE::wrapDSIGetOnlineInfo(fb,peer,&online,edid) && online==1 && !nativeOnlineCalls); + assert(!memcmp(edid,expected,128)); + assert(XE::wrapDSIGetOnlineInfo(fb,fb,&online,edid)==kIOReturnNotReady && !online); + assert(XE::wrapDSIGetOnlineInfo(fb,peer,nullptr,edid)==kIOReturnBadArgument); + int32_t current=-9,depth=-9; + Display13FBMemory::field(fb,0x1E0)=1; + Display13FBMemory::field(fb,0x4250)=uint32_t(-5); + Display13FBMemory::field(fb,0x4254)=0; + assert(!XE::wrapDSIGetCurrentMode(fb,¤t,&depth) && current==FixedModeID && !depth && !nativeCurrentCalls); + assert(Display13FBMemory::field(fb,0x4250)==FixedModeID); + XE::updateReady=false;current=-9; + assert(XE::wrapDSIGetCurrentMode(fb,¤t,&depth)==kIOReturnNotReady && current==-9); + assert(XE::wrapDSIGetOnlineInfo(fb,peer,&online,edid)==kIOReturnNotReady && !online);XE::updateReady=true; + Display13FBMemory::field(fb,0x1E0)=0; + assert(XE::wrapDSIGetCurrentMode(fb,¤t,&depth)==kIOReturnNotReady); + Display13FBMemory::field(fb,0x1E0)=1; + assert(XE::wrapDSIGetCurrentMode(peer,¤t,&depth)==7 && current==77 && depth==3); + assert(XE::wrapDSIGetOnlineInfo(peer,peer,&online,edid)==8 && online==9 && edid[0]==42); + xe.dsiPanelOnly=true;auto beforeOnline=nativeOnlineCalls; + assert(!XE::wrapDSIGetOnlineInfo(peer,peer,&online,edid) && !online && !edid[0] && nativeOnlineCalls==beforeOnline); + assert(XE::wrapDSIGetOnlineInfo(peer,peer,nullptr,edid)==kIOReturnBadArgument);xe.dsiPanelOnly=false; + xe.adlnOutputWiring.dsi=0;assert(XE::wrapDSIGetCurrentMode(fb,¤t,&depth)==7); + assert(XE::wrapDSIGetOnlineInfo(fb,peer,&online,edid)==8);xe.adlnOutputWiring.dsi=1; + assert(nativeCurrentCalls==2 && nativeOnlineCalls==2); + auto callsBeforeRepeat=calls[0];assert(xe.bindDSIFixedMode(fb,p) && calls[0]==callsBeforeRepeat && fb->refs==2 && dsiFixedMode.array->refs==2); + auto other=p;++other.mode.clockKHz;assert(!xe.bindDSIFixedMode(fb,other)); + assert(XE::wrapDSIGetModeCount(fb)==1 && XE::wrapDSIGetModeCount(peer)==3); + int32_t peerMode=0;assert(!XE::wrapDSIGetModes(peer,&peerMode) && peerMode==42); + assert(XE::wrapDSIGetModes(fb,nullptr)==kIOReturnBadArgument); + int32_t mode=0;assert(!XE::wrapDSIGetModes(fb,&mode) && mode==FixedModeID); + const void *timing=nullptr,*description=nullptr; + assert(!XE::wrapDSIValidateMode(fb,mode,&description,&timing) && timing==dsiFixedMode.data->getBytesNoCopy()); + auto validations=calls[1];assert(XE::wrapDSIValidateMode(fb,mode+0x400,&description,&timing)==kIOReturnBadArgument); + assert(!description && !timing && calls[1]==validations); + ModeInformation info;assert(!XE::wrapDSIGetModeInformation(fb,mode,&info)); + assert(info.width==1080 && info.height==1920 && info.widthMM==86 && info.heightMM==154 && info.depth==0); + assert(!(info.flags&0x00400000)); + auto invalid=p.mode;invalid.width=0;assert(!annotateMode(invalid,info)); + assert(XE::wrapDSIGetModeInformation(fb,mode,nullptr)==kIOReturnBadArgument); + assert(info.refresh>59*65536 && info.refresh<61*65536 && (info.flags&0x02000407)==0x02000407); + geometryOK=false;assert(XE::wrapDSIGetModeInformation(fb,mode,&info)==kIOReturnNotReady);geometryOK=true; + auto replacement=OSArray::withCapacity(1);auto data=OSData::withBytes(&dsiFixedMode.timing,sizeof(Timing)); + replacement->setObject(data);assert(!XE::wrapDSISetDetailedTimings(fb,replacement)); + assert(Display13FBMemory::field(fb,0x4260)==dsiFixedMode.array && replacement->refs==1); + data->bytes.resize(1);assert(XE::wrapDSISetDetailedTimings(fb,replacement)==kIOReturnUnsupported); + assert(XE::wrapDSISetDetailedTimings(fb,nullptr)==kIOReturnUnsupported); + data->release();replacement->release(); + auto offered=dsiFixedMode.timing;offered.reservedA[0]=0x80001001;offered.signalLevels=1; + offered.pixelClock=offered.minPixelClock=offered.maxPixelClock=135260000; + auto list=OSArray::withCapacity(1);auto wrong=offered;wrong.horizontalActive=640; + auto wrongData=OSData::withBytes(&wrong,sizeof(wrong));auto offeredData=OSData::withBytes(&offered,sizeof(offered)); + list->setObject(wrongData);list->setObject(offeredData); + assert(!XE::wrapDSISetDetailedTimings(fb,list));assert(dsiFixedMode.clientMode==0x80001001 && list->refs==1); + assert(dsiModeCoherent(fb) && Display13FBMemory::field(fb,0x4260)==dsiFixedMode.array); + assert(!XE::wrapDSIValidateMode(fb,static_cast(0x80001001),nullptr,&timing)); + assert(timing==dsiFixedMode.data->getBytesNoCopy()); + assert(XE::wrapDSIValidateMode(fb,static_cast(0x80001000),nullptr,&timing)==kIOReturnBadArgument); + assert(!XE::wrapDSIGetModes(fb,&mode) && static_cast(mode)==0x80001001); + assert(!XE::wrapDSIGetCurrentMode(fb,¤t,&depth) && static_cast(current)==0x80001001); + assert(!XE::wrapDSIGetModeInformation(fb,mode,&info) && info.width==1080 && info.height==1920 && !info.depth); + offeredData->bytes[64]^=1; + assert(XE::wrapDSISetDetailedTimings(fb,list)==kIOReturnUnsupported && dsiFixedMode.clientMode==0x80001001); + offered.reservedA[0]=0x80003000; + auto renewed=OSArray::withCapacity(1);auto renewedData=OSData::withBytes(&offered,sizeof(offered));renewed->setObject(renewedData); + assert(!XE::wrapDSISetDetailedTimings(fb,renewed) && dsiFixedMode.clientMode==0x80003000); + assert(dsiModeCoherent(fb) && renewed->refs==1); + assert(XE::wrapDSIValidateMode(fb,static_cast(0x80001001),nullptr,&timing)==kIOReturnBadArgument); + assert(!XE::wrapDSIValidateMode(fb,static_cast(0x80003000),nullptr,&timing) && timing==dsiFixedMode.data->getBytesNoCopy()); + assert(!XE::wrapDSIGetModes(fb,&mode) && static_cast(mode)==0x80003000); + assert(!XE::wrapDSIGetCurrentMode(fb,¤t,&depth) && static_cast(current)==0x80003000); + // Rotation offers must survive validation without changing the retained wire mode. + auto rotated=offered;rotated.reservedA[0]=0x80005000;rotated.scalerFlags=0x30;rotated.horizontalScaled=1920;rotated.verticalScaled=1080; + auto rotationList=OSArray::withCapacity(2);auto rotationData=OSData::withBytes(&rotated,sizeof(rotated)); + rotationList->setObject(renewedData);rotationList->setObject(rotationData); + assert(!XE::wrapDSISetDetailedTimings(fb,rotationList)); + assert(XE::wrapDSIGetModeCount(fb)==2);int32_t pair[2];assert(!XE::wrapDSIGetModes(fb,pair)); + assert(static_cast(pair[1])==rotated.reservedA[0]); + assert(!XE::wrapDSIValidateMode(fb,pair[1],nullptr,&timing)); + assert(static_cast(timing)->scalerFlags==0x30 && !dsiFixedMode.timing.scalerFlags); + assert(!XE::wrapDSIGetModeInformation(fb,pair[1],&info) && info.width==1920 && info.height==1080); + // A large OS list with many distinct scaled dimensions must not be + // rejected by the former 32-mode capacity or 1024-index gates. + auto many=OSArray::withCapacity(1); + for(unsigned i=0;i<1100;++i){auto t=rotated;t.reservedA[0]=0x80006000+i; + t.horizontalScaled=1280+2*i;t.verticalScaled=720+i; + t.pixelClock=t.minPixelClock=t.maxPixelClock=(i&1)?135255000:135260000; + auto data=OSData::withBytes(&t,sizeof(t));many->setObject(data);data->release();} + auto previous=dsiFixedMode.offeredData;const auto beforePublish=calls[0]; + capacityOK=false; + assert(XE::wrapDSISetDetailedTimings(fb,many)==kIOReturnNoResources); + capacityOK=true;appendOK=false; + assert(XE::wrapDSISetDetailedTimings(fb,many)==kIOReturnNoResources); + appendOK=true; + assert(dsiFixedMode.offeredData==previous && XE::wrapDSIGetModeCount(fb)==2 && calls[0]==beforePublish); + assert(!XE::wrapDSISetDetailedTimings(fb,many) && XE::wrapDSIGetModeCount(fb)==1100); + int32_t modes[1100];assert(!XE::wrapDSIGetModes(fb,modes)); + for(unsigned i=0;i<1100;++i){assert(static_cast(modes[i])==0x80006000+i); + assert(!XE::wrapDSIValidateMode(fb,modes[i],nullptr,&timing)); + assert(static_cast(timing)->horizontalScaled==1280+2*i); + assert(!XE::wrapDSIGetModeInformation(fb,modes[i],&info) && info.width==1280+2*i && info.height==720+i);} + many->release();assert(!XE::wrapDSISetDetailedTimings(fb,rotationList)); + rotationData->release();rotationList->release(); + // Legacy UI attributes alone must never fabricate a presentation timing. + auto orientation=new OSNumber;fb->setProperty("IOFBTransform",orientation); + for(auto transform:{3ULL,5ULL,0ULL}) { + orientation->value=transform; + assert(!XE::wrapDSIValidateMode(fb,static_cast(offered.reservedA[0]),nullptr,&timing)); + assert(static_cast(timing)->scalerFlags==0); + assert(!static_cast(timing)->horizontalScaled); + assert(!static_cast(timing)->verticalScaled); + assert(!dsiFixedMode.timing.scalerFlags && static_cast(timing)->horizontalActive==1080); + } + orientation->value=7; + assert(!XE::wrapDSIValidateMode(fb,static_cast(offered.reservedA[0]),nullptr,&timing)); + assert(!static_cast(timing)->scalerFlags); + fb->removeProperty("IOFBTransform");orientation->release(); + renewedData->bytes[64]^=1; + assert(XE::wrapDSISetDetailedTimings(fb,renewed)==kIOReturnUnsupported && dsiFixedMode.clientMode==0x80003000); + renewedData->release();renewed->release(); + wrongData->release();offeredData->release();list->release(); + fb->removeProperty("IOFBDetailedTimings");assert(!XE::wrapDSIGetModeCount(fb) && dsiFixedMode.array->refs==1); + assert(XE::wrapDSIValidateMode(fb,mode,nullptr,nullptr)==kIOReturnNotReady); + const auto cleanup=[&]{ + assert(!xe.releaseDSIFixedMode(peer));dsiFixedMode.ready=true; + assert(!xe.releaseDSIFixedMode(fb));dsiFixedMode.ready=false; + const auto refs=dsiFixedMode.array->refs;clearOK=false; + assert(!xe.releaseDSIFixedMode(fb) && dsiModeTarget(fb) && dsiFixedMode.array->refs==refs && fb->refs==2); + clearOK=true; + assert(xe.releaseDSIFixedMode(fb) && !dsiModeTarget(fb) && fb->refs==1); + assert(!Display13FBMemory::field(fb,0x4260) && !fb->getProperty("IOFBDetailedTimings")); + assert(xe.releaseDSIFixedMode(fb));}; + cleanup();publishOK=false;assert(!xe.bindDSIFixedMode(fb,p)); + assert(dsiModeTarget(fb) && dsiFixedMode.array->refs==1 && Display13FBMemory::field(fb,0x4260)==dsiFixedMode.array); + assert(!XE::wrapDSIGetModeCount(fb));cleanup(); + assert(!fbLock.depth && !ownerLock.depth);fb->release();peer->release();assert(!alive); + puts("PASS production DSI fixed mode: retained native array, publication failure, dynamic enumeration, metadata and foreign FB dispatch"); +} +'''.replace('OWNER', owner).replace('FUNCTIONS', functions) +with tempfile.TemporaryDirectory(prefix='dsi-apple-adapter-') as directory: + path = Path(directory) + (path/'test.cpp').write_text(code) + subprocess.run(['clang++', '-std=c++17', '-O1', '-fsanitize=address,undefined', + '-I'+str(root/'WhateverXe'), str(path/'test.cpp'), '-o', str(path/'test')], check=True) + subprocess.run([str(path/'test')], check=True) diff --git a/tests/dsi_apple_timing.cpp b/tests/dsi_apple_timing.cpp new file mode 100644 index 00000000..0931d659 --- /dev/null +++ b/tests/dsi_apple_timing.cpp @@ -0,0 +1,93 @@ +#include "kern_dsi_apple_timing.hpp" +#include +#include +#include +using namespace XELP::DSI; +int main() { + Profile p;p.status=ProfileStatus::Valid;p.mode={135255,1080,46,22,2,1920,82,40,2,86,154}; + AppleAdapter::Timing t;assert(AppleAdapter::makeTiming(p,t)); + assert(t.pixelClock==135255000&&t.minPixelClock==135241474&&t.maxPixelClock==135268526); + assert(t.horizontalActive==1080&&t.horizontalBlanking==46&&t.horizontalSyncOffset==22&&t.horizontalSyncWidth==2); + assert(t.verticalActive==1920&&t.verticalBlanking==82&&t.verticalSyncOffset==40&&t.verticalSyncWidth==2); + assert(t.bitsPerColorComponent==2&&t.pixelEncoding==1&&t.numLinks==1); + assert(!t.scalerFlags&&!t.horizontalScaled&&!t.verticalScaled&&!t.verticalBlankingExtension); + auto offered=t;offered.reservedA[0]=0x80001000;offered.signalLevels=1; + offered.pixelClock=offered.minPixelClock=offered.maxPixelClock=135260000; + assert(AppleAdapter::clientTiming(t,offered,0)); + for(unsigned offset=4;offset(&corrupt)[offset]^=0x80; + assert(!AppleAdapter::clientTiming(t,corrupt,0)); + } + auto exact=offered;exact.pixelClock=exact.minPixelClock=exact.maxPixelClock=t.pixelClock; + assert(AppleAdapter::clientTiming(t,exact,1)); + exact.minPixelClock--;assert(!AppleAdapter::clientTiming(t,exact,1)); + assert(AppleAdapter::clientTiming(t,offered,1)); // IDs are opaque, not array positions. + assert(AppleAdapter::clientTiming(t,offered,1024)); + for(uint32_t id:{0x80002000U,0x80003000U,0x80005007U}) { + offered.reservedA[0]=id;assert(AppleAdapter::clientTiming(t,offered,0)); + } + for(uint32_t id:{0U,AppleAdapter::FixedModeID,0x7FFFFFFFU,UINT32_MAX}) { + offered.reservedA[0]=id;assert(!AppleAdapter::clientTiming(t,offered,0)); + } + offered.reservedA[0]=0x80001001;assert(AppleAdapter::clientTiming(t,offered,1)); + for(unsigned rotation:{0x30U,0x50U}) { + auto r=offered;r.scalerFlags=rotation; + assert(AppleAdapter::presentationTiming(t,r,1)); + r.horizontalScaled=t.verticalActive;r.verticalScaled=t.horizontalActive; + assert(AppleAdapter::presentationTiming(t,r,1)); + r.horizontalScaled=2880;r.verticalScaled=1620; + assert(AppleAdapter::presentationTiming(t,r,1)); + AppleAdapter::ModeInformation info;info.width=2880;info.height=1620;info.flags=0x02000004; + assert(AppleAdapter::annotateMode(p.mode,info,true,2880,1620)); + assert(!(info.flags&0x02000004) && info.widthMM==154 && info.heightMM==86); + r.horizontalScaled=8192;assert(AppleAdapter::presentationTiming(t,r,1)); + r.horizontalScaled=2880;r.verticalScaled=0;assert(AppleAdapter::presentationTiming(t,r,1)); + r=offered;r.scalerFlags=rotation;r.horizontalActive=1920;r.verticalActive=1080; + assert(!AppleAdapter::presentationTiming(t,r,1)); + } + auto portrait=offered;portrait.horizontalScaled=1620;portrait.verticalScaled=2880; + assert(AppleAdapter::presentationTiming(t,portrait,0)); + portrait.scalerFlags=1;assert(AppleAdapter::presentationTiming(t,portrait,0)); + portrait.scalerFlags=0x20;portrait.horizontalInset=4;assert(AppleAdapter::presentationTiming(t,portrait,0)); + assert(!AppleAdapter::acceptsMode(0x80001002,0x80001001)); + {auto mode=p.mode;mode.clockKHz=750000;uint8_t base[128],ext[128]; + assert(AppleAdapter::makePanelEDID(mode,base) && base[126]==1); + assert(AppleAdapter::makePanelEDID(mode,ext,2)); + assert((uint32_t(ext[8])|(uint32_t(ext[9])<<8)|(uint32_t(ext[10])<<16))==74999); + for(auto bytes:{base,ext}){unsigned sum=0;for(unsigned i=0;i<128;++i)sum+=bytes[i];assert(!(sum&255));} + mode.clockKHz=UINT32_MAX;assert(!AppleAdapter::makePanelEDID(mode,base));} + uint8_t edid[128];assert(AppleAdapter::makePanelEDID(p.mode,edid)); + unsigned sum=0;for(auto b:edid)sum+=b;assert(!(sum&255)); + assert(edid[16]==0xFF && edid[17]>=17); // Model year enables native linear brightness. + assert(edid[18]==1 && edid[19]==4 && edid[20]==0xA0 && edid[24]==2 && !edid[126]); + const auto d=edid+54; + assert((unsigned(d[0])+(unsigned(d[1])<<8))*10==135260); + assert((d[2]|((d[4]&0xF0)<<4))==1080 && (d[3]|((d[4]&15)<<8))==46); + assert((d[5]|((d[7]&0xF0)<<4))==1920 && (d[6]|((d[7]&15)<<8))==82); + assert((d[8]|((d[11]>>6)<<8))==22 && (d[9]|(((d[11]>>4)&3)<<8))==2); + assert(((d[10]>>4)|(((d[11]>>2)&3)<<4))==40 && ((d[10]&15)|((d[11]&3)<<4))==2); + assert(d[12]==86 && d[13]==154 && !d[14] && d[17]==0x18); + for(unsigned i=35;i<38;i++)assert(!edid[i]);for(unsigned i=38;i<54;i++)assert(edid[i]==1); + assert(edid[75]==0xFC && !memcmp(edid+77,"MIPI DSI\n ",13)); + auto wide=p.mode;wide.width=4096;assert(AppleAdapter::makePanelEDID(wide,edid) && edid[126]==1); + auto tall=p.mode;tall.vfront=64;assert(AppleAdapter::makePanelEDID(tall,edid) && edid[126]==1); + Mode extended{136887,1200,68,40,12,1920,239,205,2,141,226}; + assert(AppleAdapter::makePanelEDID(extended,edid) && edid[126]==1 && !edid[54] && edid[57]==0x10); + sum=0;for(auto b:edid)sum+=b;assert(!(sum&255)); + uint8_t ext[128];assert(AppleAdapter::makePanelEDID(extended,ext,2)); + sum=0;for(auto b:ext)sum+=b;assert(!(sum&255)); + sum=0;for(unsigned i=1;i<=28;++i)sum+=ext[i];assert(!(sum&255)); + assert(ext[0]==0x70 && ext[1]==0x12 && ext[2]==23 && ext[5]==3 && ext[7]==20); + auto u16=[](const uint8_t *b){return unsigned(b[0])|(unsigned(b[1])<<8);}; + const auto e=ext+8; + assert((1+e[0]+(unsigned(e[1])<<8)+(unsigned(e[2])<<16))*10==136890 && e[3]==0x80); + const unsigned expected[]={1200,68,40,12,1920,239,205,2}; + for(unsigned i=0;i<8;++i)assert(u16(e+4+2*i)+1==expected[i]); + assert(!AppleAdapter::makePanelEDID(p.mode,ext,2) && !AppleAdapter::makePanelEDID(extended,ext,3)); + auto bad=extended;bad.vfront=32769;bad.vblank=40000;assert(!AppleAdapter::makePanelEDID(bad,edid)); + bad=p.mode;bad.clockKHz=10001;assert(AppleAdapter::makePanelEDID(bad,edid)); + bad.clockKHz=10005;assert(!AppleAdapter::makePanelEDID(bad,edid)); + p.mode.hsync=46;assert(!AppleAdapter::makeTiming(p,t)&&!t.pixelClock); + p={};assert(!AppleAdapter::makeTiming(p,t)); + puts("DSI fixed Apple timing: Hz/range, portrait geometry, RGB8 bit fields, no scaling/DSC: PASS"); +} diff --git a/tests/dsi_apple_timing_native.py b/tests/dsi_apple_timing_native.py new file mode 100644 index 00000000..be71f549 --- /dev/null +++ b/tests/dsi_apple_timing_native.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Replay exact donor timing publication/validation with retained mock objects. + +No Apple bytes are stored in the fixture. This checks the native data contract, +not CoreDisplay mode enumeration, native lock behavior, or hardware modesetting. +""" +from pathlib import Path +import hashlib, subprocess, sys, tempfile + +root = Path(__file__).resolve().parents[1] +donor = Path(sys.argv[1]).resolve() +data = donor.read_bytes() +assert hashlib.sha256(data).hexdigest() == 'c004fc1ac9ee4157e2e12736f40fa6639bde025a279326beeb9dd48c9b445731' +assert data[0x30e6b] == 0xe8 +assert data[0x30e4c:0x30e52] == bytes.fromhex('81 e6 ff 03 00 00') +with tempfile.TemporaryDirectory(prefix='dsi-timing-') as directory: + executable = Path(directory) / 'test' + subprocess.run(['clang++', '-arch', 'x86_64', '-std=c++17', '-O1', + '-I'+str(root/'WhateverXe'), str(root/'tests/dsi_apple_timing_native_fixture.cpp'), + '-o', str(executable)], check=True) + subprocess.run([str(executable), str(donor)], check=True) diff --git a/tests/dsi_apple_timing_native_fixture.cpp b/tests/dsi_apple_timing_native_fixture.cpp new file mode 100644 index 00000000..aa037e39 --- /dev/null +++ b/tests/dsi_apple_timing_native_fixture.cpp @@ -0,0 +1,73 @@ +#include "kern_dsi_apple_timing.hpp" +#include +#include +#include +#include +#include +#include +using namespace XELP::DSI; +struct Object {void **vtable {};unsigned refs {1};bool data {};}; +struct Data : Object {std::vector bytes;}; +struct Array : Object {Data *entry {};}; +static Array *property=nullptr; +static bool publishOK=true; +void retain(Object *p){assert(p&&p->refs);++p->refs;} +void release(Object *p){assert(p&&p->refs);--p->refs;} +bool setProperty(void *,const char *key,Array *array) { + assert(!strcmp(key,"IOFBDetailedTimings"));if(!publishOK)return false; + retain(array);if(property)release(property);property=array;return true; +} +void removeProperty(void *,const char *key) { + assert(!strcmp(key,"IOFBDetailedTimings"));if(property)release(property);property=nullptr; +} +void *getObject(Array *a,uint32_t index){assert(a->refs);return index==0?a->entry:nullptr;} +void *getBytes(Data *d){assert(d->refs);return d->bytes.data();} +void *cast(Object *o,const void *){return o&&o->data?o:nullptr;} +templatevoid put(void *p,size_t offset,T value){memcpy(static_cast(p)+offset,&value,sizeof(value));} +void jump(uint8_t *p,uintptr_t target){p[0]=0x48;p[1]=0xb8;memcpy(p+2,&target,8);p[10]=0xff;p[11]=0xe0;} +int main(int argc,char **argv) { + assert(argc==2);std::ifstream f(argv[1],std::ios::binary);assert(f); + std::vector binary{std::istreambuf_iterator(f),{}}; + constexpr size_t size=4*1024*1024;assert(binary.size()(mmap(nullptr,size,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0)); + assert(image!=MAP_FAILED);memcpy(image,binary.data(),binary.size()); + // Redirect only the imported meta-cast leaf, retaining all donor branches. + jump(image+0x300000,reinterpret_cast(cast)); + int32_t relative=0x300000-0x30e70;put(image,0x30e6c,relative); + void *meta=nullptr;put(image,0xc8050,&meta); + auto publish=reinterpret_cast(image+0x30d0e); + auto validate=reinterpret_cast(image+0x30de2); + void *fbTable[0x2a8/8] {},*arrayTable[0x1f0/8] {},*dataTable[0x180/8] {}; + fbTable[0x268/8]=reinterpret_cast(setProperty);fbTable[0x2a0/8]=reinterpret_cast(removeProperty); + arrayTable[0x1e8/8]=reinterpret_cast(getObject);dataTable[0x178/8]=reinterpret_cast(getBytes); + alignas(8) uint8_t fb[0x4300] {};put(fb,0,fbTable+0);fb[0x1e0]=1; + Profile p;p.status=ProfileStatus::Valid;p.mode={135255,1080,46,22,2,1920,82,40,2,86,154}; + AppleAdapter::Timing timing;assert(AppleAdapter::makeTiming(p,timing)); + Data data;data.vtable=dataTable;data.data=true; + const auto bytes=reinterpret_cast(&timing);data.bytes.assign(bytes,bytes+sizeof(timing)); + Array array;array.vtable=arrayTable;array.entry=&data;retain(&data); // array retains OSData + assert(!publish(fb,&array)&&property==&array&&array.refs==2); + release(&array);release(&data); // only property/array now retain the data + const void *result=nullptr;void *description=nullptr; + assert(!validate(fb,AppleAdapter::FixedModeID,&description,&result)&&description&&result==data.bytes.data()); + assert(!memcmp(result,&timing,sizeof(timing))&&array.refs==1&&data.refs==1); + assert(validate(fb,0x10001,nullptr,&result)==0xe00002c2U); + assert(validate(fb,0xffff,nullptr,&result)==0xe00002c2U); + data.data=false;assert(validate(fb,0x10000,nullptr,&result)==0xe00002c2U);data.data=true; + // Native mode indexing aliases high bits; the DSI mode dispatcher must + // separately constrain the selected fixed mode rather than relying on it. + assert(!validate(fb,0x10400,nullptr,&result)); + // Native validation does not check OSData length. Publication must always + // carry the complete structure; a type-correct short blob is insufficient. + data.bytes.resize(1);assert(!validate(fb,0x10000,nullptr,&result)); + assert(!publish(fb,nullptr)&&!property&&!array.refs); + assert(validate(fb,0x10000,nullptr,&result)==0xe00002c2U);release(&data); + // Native publication ignores setProperty failure and still stores a raw + // array pointer. The eventual adapter must retain its own backing owner. + Array held;held.vtable=arrayTable;held.entry=nullptr;publishOK=false; + assert(!publish(fb,&held)&&!property&&held.refs==1); + Array *raw=nullptr;memcpy(&raw,fb+0x4260,sizeof(raw));assert(raw==&held); + assert(!publish(fb,nullptr));release(&held); + munmap(image,size); + puts("PASS exact donor timing: retained array/data, type rejection, index alias, short-data and property-failure limits"); +} diff --git a/tests/dsi_backing_adapter.py b/tests/dsi_backing_adapter.py new file mode 100644 index 00000000..9f8121c2 --- /dev/null +++ b/tests/dsi_backing_adapter.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +"""Exercise the production pending-backing adapter with bounded native leaves.""" +from pathlib import Path +from production_source import read_source +import hashlib,re,subprocess,sys,tempfile +root=Path(__file__).resolve().parents[1] +s=read_source(root/'WhateverXe/kern_xe.cpp') +functions='\n'.join(re.search(r'(?:bool|uint32_t|void) XE::'+name+r'\(.*?\n\}',s,re.S)[0] for name in + ['wrapDSIBackingAllocate','wrapDSIBackingFree','prepareDSINativeBacking','validateDSINativeBacking','initializeDSINativeBacking','discardDSINativeBacking','probeDSINativeBacking']) +state=re.search(r'struct DSIPendingBackingOwner \{.*?\n\};',s,re.S)[0] +if len(sys.argv)>1: + b=Path(sys.argv[1]).read_bytes();assert hashlib.sha256(b).hexdigest()=='bd1a21f5e622aadabdab6441f6b5c38ed73fa3053c783fdf0fb7e4b309f273d0' + installer=re.search(r'bool XE::processDSIBackingHooks\(.*?\n\}',s,re.S)[0] + offsets=[int(x,0) for x in re.search(r'offsets\[\]=\{([^}]+)',installer)[1].split(',')] + entries=re.search(r'entries\[\]\[16\]=\{(.*?)\};',installer,re.S)[1] + for offset,row in zip(offsets,re.findall(r'\{([^}]+)\}',entries)): + assert b[offset:offset+16]==bytes(int(x,0) for x in row.split(',')) + assert 'routeMultipleShort' in installer +code=r''' +#include "kern_dsi_native_backing.hpp" +#include "kern_dsi_quiesce.hpp" +#include "kern_dsi_clock.hpp" +#include +#include +#include +#include +using namespace XELP::DSI; +using namespace Display13FBMemory; +constexpr uint32_t kIOReturnNotReady=0xE00002D8; +templatevoid put(void *p,unsigned off,T v){memcpy(static_cast(p)+off,&v,sizeof(v));} +unsigned fbDepth=0,ownerDepth=0,gpuDepth=0,allocCalls=0,freeCalls=0,buffers=0,expectedFlags=0; +uintptr_t thread=1;void *current_thread(){return reinterpret_cast(thread);} +struct IOLock{};struct IORecursiveLock{}; +void IOLockLock(IOLock*){assert(fbDepth && !ownerDepth && gpuDepth<=1);++ownerDepth;} +void IOLockUnlock(IOLock*){assert(ownerDepth==1);--ownerDepth;} +void *IOMalloc(size_t n){assert(!ownerDepth && !gpuDepth);++buffers;return calloc(1,n);} +void IOFree(void *p,size_t n){assert(!ownerDepth && !gpuDepth && n==64 && buffers);--buffers;free(p);} +alignas(4096) uint32_t cpuImage[8355840/4]; +bool failCPUMap=false;unsigned cpuMaps=0; +constexpr unsigned kIOMapAnywhere=1;void *kernel_task=reinterpret_cast(1); +struct CPUMap { + uintptr_t getVirtualAddress(){return reinterpret_cast(cpuImage);} + uint64_t getLength(){return sizeof(cpuImage);} + void release(){assert(gpuDepth==1 && !ownerDepth && cpuMaps==1);--cpuMaps;} +}cpuMap; +struct OSObject { + alignas(8) uint8_t bytes[0x4B90]{};unsigned size=sizeof(bytes),refs=1;const char *kind="object"; + OSObject *getMetaClass(){return this;}unsigned getClassSize(){return size;} + bool metaCast(const char *name){return !strcmp(kind,name);}void retain(){++refs;}void release(){assert(refs>1);--refs;} + uint32_t callPlatformFunction(const char*,bool,void*,void*,void*,void*); + uint64_t getLength(){return sizeof(cpuImage);} + CPUMap *createMappingInTask(void *task,uintptr_t address,unsigned options,uint64_t offset,uint64_t bytes){ + assert(gpuDepth==1 && fbDepth && !ownerDepth && !cpuMaps && task==kernel_task && !address && options==kIOMapAnywhere && !offset && bytes==sizeof(cpuImage)); + if(failCPUMap)return nullptr;++cpuMaps;return &cpuMap;} + +}; +using IOService=OSObject;using IOMemoryDescriptor=OSObject; +#define OSDynamicCast(T,p) static_cast(p) +uint64_t receipt[16]{}; +struct IORegistryEntry {static IORegistryEntry *getRegistryRoot(){static IORegistryEntry root;return &root;} + void setProperty(const char *name,const void *p,unsigned n){assert(!strcmp(name,"WEXDSIBackingTrial") && n==sizeof(receipt));memcpy(receipt,p,n);}}; +OSObject nativeMapping,nativeMemory,nativeDescriptor; +IOService *modeFB=nullptr;bool dsiModeTarget(IOService *fb){return fb && modeFB==fb;} +struct DSIModeScope {IORecursiveLock *lock;explicit DSIModeScope(IOService *fb):lock(field(fb,0x4220)){if(lock)++fbDepth;} + ~DSIModeScope(){assert(!ownerDepth && !gpuDepth);if(lock){assert(fbDepth);--fbDepth;}}}; +STATE +DSIPendingBackingOwner dsiPendingBacking; +bool dsiPendingTarget(IOService *fb){return fb && dsiPendingBacking.framebuffer==fb;} +struct Map {uintptr_t getVirtualAddress(){return 1;}uint64_t getLength(){return 0x1000000;}}; +struct {IOLock *lock;IOService *controller;Map *map;unsigned dsiNativeWriters=0;bool memoryFaulted=false,clockFaulted=false;}display13FBPower; +struct {uint32_t sharedPoolPrefix=0x6192000;}display13FBGTT; +XELPMemory::Layout xelpMemory=XELPMemory::decode(0x03C1,0x7A800001,0); +bool binding=true,source=true,available=true,badPTE=false,failAllocate=false,failFree=false,dropMap=false,changePipe=false,detachAfterAllocate=false; +OSObject *nativePipe=nullptr,*alternatePipe=nullptr; +uintptr_t display13FBExpectedOwner(IOService *controller){return field(controller,0xC0);} +bool adlnSharedBinding(void *controller,void *owner){return binding && controller==display13FBPower.controller && field(controller,0xC0)==owner;} +bool adlnSysMemOwner(void*,const Pool&){assert(ownerDepth);return true;} +Display13FBMemory::Readout display13FBInspectMemory(IOService*,bool,unsigned,bool){assert(ownerDepth);Display13FBMemory::Readout r;r.status=source?Status::Complete:Status::PTE;return r;} +struct Display13SurfaceIO { + Map *map;unsigned pipe;bool memoryReady(uintptr_t m,uintptr_t mem,uintptr_t owner,uint64_t n){ + assert(ownerDepth && gpuDepth==1 && map);return m==reinterpret_cast(&nativeMapping) && mem==reinterpret_cast(&nativeMemory) && owner && n==8355840;} + uint64_t physical(uintptr_t,uint64_t off,uint64_t &span){span=4096;return 0x180000000ULL+off;} + uint64_t readPTE(uint32_t reg){const uint64_t off=(uint64_t(reg)-0x800000)*512-0x10000000; + return (0x180000000ULL+off+(badPTE?4096:0))|0x99;} +}; +uint32_t nativeAllocate(OSObject *pipe,void *p,uint32_t flags){ + assert(gpuDepth && !ownerDepth && flags==expectedFlags);++allocCalls;if(failAllocate)return 0xE00002BC; + auto &r=*static_cast(p);r.refs=1;r.map=reinterpret_cast(&nativeMapping);r.memory=reinterpret_cast(&nativeMemory);r.gpu=0x10000000;r.pitch=4352; + nativeMapping.retain();nativeMemory.retain(); + put(pipe,0x1298,r.memory);put(pipe,0x12A0,r.map);if(dropMap)display13FBPower.map=nullptr;return 0; +} +uint32_t nativeFree(OSObject *pipe,void *p,uint32_t flags){ + assert(gpuDepth && !ownerDepth && flags==expectedFlags);++freeCalls;if(failFree)return 0xE00002BC; + auto &r=*static_cast(p);if(r.refs){nativeMapping.release();nativeMemory.release();}r.refs=0;r.map=r.memory=r.gpu=0;r.pitch=0; + put(pipe,0x1298,uintptr_t(0));put(pipe,0x12A0,uintptr_t(0));return 0; +} +#define FunctionCast(f,p) reinterpret_cast(p) +struct XE { + bool dsiBackingHooksReady=true;Profile dsiProfile;struct{unsigned dsi=1;}adlnOutputWiring; + uintptr_t orgDSIBacking[2]{reinterpret_cast(nativeAllocate),reinterpret_cast(nativeFree)}; + static bool dsiNativeUpdateAllowed(IOService *fb){assert(fbDepth);return available && !dsiPendingTarget(fb);} + static uint32_t wrapDSIBackingAllocate(OSObject*,void*,uint32_t); + static uint32_t wrapDSIBackingFree(OSObject*,void*,uint32_t); + bool initializeDSINativeBacking();Session dsiSession; + bool publishDSINativeBacking(){assert(gpuDepth==1 && fbDepth && !ownerDepth);return true;} + bool switchDSINativeSurface(){assert(gpuDepth==1 && fbDepth && !ownerDepth);return true;} + bool probeDSIModeGeometry(IOService*){assert(fbDepth && !ownerDepth && !gpuDepth);return true;} + bool prepareDSINativeBacking(IOService*);bool validateDSINativeBacking();bool discardDSINativeBacking(IOService*); + void probeDSINativeBacking(IOService*);bool dsiBackingAttempted=false;uint32_t dsiBackingStage=0; +}*callbackXE; +uint32_t OSObject::callPlatformFunction(const char *name,bool wait,void *record,void *index,void *flags,void *extra){ + assert(fbDepth && !ownerDepth && !gpuDepth && !wait && !extra && *static_cast(index)==0); + ++gpuDepth;uint32_t r; + if(!strcmp(name,"allocscanmem"))r=XE::wrapDSIBackingAllocate(nativePipe,record,*static_cast(flags)); + else {assert(!strcmp(name,"freescanmem"));XE::wrapDSIBackingFree(changePipe?alternatePipe:nativePipe,record,*static_cast(flags));r=0;} + --gpuDepth;if(detachAfterAllocate && !strcmp(name,"allocscanmem"))put(nativePipe,0x1298,uintptr_t(0));return r; +} +FUNCTIONS +struct Case { + IOService fb,owner,controller,machine,pipe,other;IOLock lock;IORecursiveLock fbLock;Map map;XE xe; + Case(){ + assert(!dsiPendingBacking.framebuffer && !buffers && !cpuMaps);failCPUMap=false;put(&nativeMemory,0xD0,&nativeDescriptor);callbackXE=&xe;modeFB=&fb;nativePipe=&pipe;alternatePipe=&other; + binding=source=available=true;badPTE=failAllocate=failFree=dropMap=changePipe=detachAfterAllocate=false;thread=1; + display13FBPower={&lock,&controller,&map};display13FBGTT.sharedPoolPrefix=0x6192000; + xe.dsiProfile.status=ProfileStatus::Valid;xe.dsiProfile.mode={135255,1080,46,22,2,1920,82,40,2,86,154}; + owner.size=0x1600;machine.kind="IGAccelDisplayMachine";pipe.kind="IGAccelDisplayPipe"; + put(&owner,0x378,&machine);put(&machine,0x128,&owner);put(&owner,0x1470,3U);put(&owner,0x13C0,display13FBGTT.sharedPoolPrefix); + put(&controller,0xC0,&owner);put(&controller,0xD5C,1U);put(&controller,0xD60,&fb);put(&fb,0x1D0,&controller); + put(&controller,0xDF0,1U);put(&controller,0xDF4,uint8_t(1));put(&controller,0xDF5,uint8_t(3)); + put(&controller,0x1A10,1U);put(&controller,0x1A28,xelpMemory.base);put(&controller,0x1A30,0x800000U);put(&controller,0x1A34,0x800U); + put(&fb,0x4220,&fbLock);put(&pipe,0x88,&owner);put(&other,0x88,&owner); + } + ~Case(){assert(!dsiPendingBacking.framebuffer && !buffers && fb.refs==1 && owner.refs==1 && pipe.refs==1 && nativeMapping.refs==1 && nativeMemory.refs==1 && !fbDepth && !ownerDepth && !gpuDepth);} + void discard(){assert(xe.discardDSINativeBacking(&fb));} +}; +int main(){ + {Case c;failCPUMap=true;assert(!c.xe.prepareDSINativeBacking(&c.fb));assert(!dsiPendingBacking.initialized && !cpuMaps);c.discard();} + {Case c;memset(cpuImage,0xA5,sizeof(cpuImage));assert(c.xe.prepareDSINativeBacking(&c.fb)); + assert(dsiPendingBacking.initialized && !cpuMaps); + for(auto pixel:cpuImage)assert(pixel==0); + c.discard();assert(!dsiPendingBacking.initialized);} + + {Case c;const auto a=allocCalls,f=freeCalls;DSIModeScope scope(&c.fb); + available=false;c.xe.probeDSINativeBacking(&c.fb);assert(!c.xe.dsiBackingAttempted && allocCalls==a && receipt[2]==1); + available=true;c.xe.probeDSINativeBacking(&c.fb); + assert(c.xe.dsiBackingAttempted && allocCalls==a+1 && freeCalls==f+1 && !dsiPendingTarget(&c.fb)); + assert(receipt[1]==3 && receipt[2]==6 && receipt[3] && receipt[11]==2040 && receipt[13] && !receipt[14]); + c.xe.probeDSINativeBacking(&c.fb);assert(allocCalls==a+1 && freeCalls==f+1); + assert(!field(&c.fb,0x4348));} + {Case c;failFree=true;const auto a=allocCalls;{DSIModeScope scope(&c.fb);c.xe.probeDSINativeBacking(&c.fb); + assert(c.xe.dsiBackingAttempted && receipt[14] && receipt[15] && !receipt[13]); + c.xe.probeDSINativeBacking(&c.fb);assert(allocCalls==a+1);} + failFree=false;c.discard();} + + {Case c;NativeBacking::Record r;assert(NativeBacking::request(c.xe.dsiProfile,3,r)); + gpuDepth=1;expectedFlags=4;assert(!XE::wrapDSIBackingAllocate(&c.pipe,&r,4)); + expectedFlags=2;assert(!XE::wrapDSIBackingFree(&c.pipe,&r,2)); + expectedFlags=0;failAllocate=true;assert(XE::wrapDSIBackingAllocate(&c.pipe,&r,0)==0xE00002BC);gpuDepth=0;} + {Case c;assert(c.xe.prepareDSINativeBacking(&c.fb));assert(dsiPendingBacking.proof.verifiedPages==2040); + assert(nativeMapping.refs==3 && nativeMemory.refs==3); + assert(dsiPendingBacking.pipe==&c.pipe && c.fb.refs==2 && c.owner.refs==2 && c.pipe.refs==2); + assert(!field(&c.fb,0x4348) && allocatedSlot(&c.controller,&c.fb)==0); + assert(!c.xe.prepareDSINativeBacking(&c.fb));c.discard();assert(!c.xe.discardDSINativeBacking(&c.fb));} + for(unsigned failure=0;failure<11;++failure){Case c;const auto before=allocCalls; + switch(failure){case 0:c.xe.dsiBackingHooksReady=false;break;case 1:binding=false;break;case 2:source=false;break; + case 3:display13FBPower.dsiNativeWriters=1;break;case 4:put(&c.owner,0x1470,2U);break; + case 5:c.machine.kind="wrong";break;case 6:put(&c.machine,0x128,&c.other);break; + case 7:put(&c.fb,0x4220,static_cast(nullptr));break;case 8:put(&c.owner,0x13C0,0U);break;case 9:modeFB=&c.other;break;case 10:c.xe.adlnOutputWiring.dsi=0;break;} + assert(!c.xe.prepareDSINativeBacking(&c.fb) && allocCalls==before && !dsiPendingBacking.framebuffer);} + for(unsigned failure=0;failure<4;++failure){Case c; + switch(failure){case 0:failAllocate=true;break;case 1:badPTE=true;break;case 2:dropMap=true;break;case 3:put(&c.pipe,0x1298,uintptr_t(7));break;} + assert(!c.xe.prepareDSINativeBacking(&c.fb) && dsiPendingBacking.framebuffer==&c.fb);c.discard();} + {Case c;detachAfterAllocate=true;assert(!c.xe.prepareDSINativeBacking(&c.fb)); + assert(nativeMapping.refs==3 && nativeMemory.refs==3 && !c.xe.discardDSINativeBacking(&c.fb)); + put(&c.pipe,0x1298,reinterpret_cast(&nativeMemory));c.discard();} + {Case c;assert(c.xe.prepareDSINativeBacking(&c.fb));failFree=true; + assert(!c.xe.discardDSINativeBacking(&c.fb) && dsiPendingBacking.record && c.pipe.refs==2); + failFree=false;changePipe=true;const auto before=freeCalls;assert(!c.xe.discardDSINativeBacking(&c.fb) && freeCalls==before); + changePipe=false;dsiPendingBacking.armed=true;assert(!c.xe.discardDSINativeBacking(&c.fb));dsiPendingBacking.armed=false;c.discard();} + {Case c;assert(c.xe.prepareDSINativeBacking(&c.fb));NativeBacking::Record foreign;assert(NativeBacking::request(c.xe.dsiProfile,3,foreign)); + thread=2;fbDepth=gpuDepth=1;const auto before=allocCalls; + assert(XE::wrapDSIBackingAllocate(&c.pipe,&foreign,0)==kIOReturnNotReady && allocCalls==before); + assert(XE::wrapDSIBackingFree(&c.pipe,&foreign,0)==kIOReturnNotReady); + assert(!XE::wrapDSIBackingAllocate(&c.other,&foreign,0) && allocCalls==before+1); + assert(!XE::wrapDSIBackingFree(&c.other,&foreign,0));fbDepth=gpuDepth=0;thread=1;c.discard();} + puts("PASS production DSI backing adapter: actual native pipe capture, pending PTE proof, retained failures, discard, peer forwarding and lock boundaries"); +} +'''.replace('STATE',state).replace('FUNCTIONS',functions) +with tempfile.TemporaryDirectory(prefix='dsi-backing-adapter-') as directory: + d=Path(directory);(d/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(d/'test.cpp'),'-o',str(d/'test')],check=True) + subprocess.run([str(d/'test')],check=True) diff --git a/tests/dsi_backing_native.py b/tests/dsi_backing_native.py new file mode 100644 index 00000000..b923287d --- /dev/null +++ b/tests/dsi_backing_native.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Replay native pending scanout allocation/free; allocator leaves are mocked. + +Requires the exact accelerator; stores no Apple bytes. This establishes record +and display-pipe reference behavior, not GPU allocation or live PTE validity. +""" +from pathlib import Path +import hashlib, subprocess, sys, tempfile +root=Path(__file__).resolve().parents[1] +donor=Path(sys.argv[1]).resolve() +assert hashlib.sha256(donor.read_bytes()).hexdigest()=='bd1a21f5e622aadabdab6441f6b5c38ed73fa3053c783fdf0fb7e4b309f273d0' +code=r''' +#include "kern_dsi_native_backing.hpp" +#include +#include +#include +#include +#include +#include +using namespace XELP::DSI; +templatevoid put(void *p,unsigned off,T v){memcpy(static_cast(p)+off,&v,sizeof(v));} +templateT get(const void *p,unsigned off){return Display13FBMemory::field(p,off);} +alignas(8) uint8_t owner[0x1500]{},pipe[0x1300]{},memory[0x198]{},mapping[0x128]{}; +uintptr_t memVT[64]{},mapVT[64]{}; +unsigned allocations=0,releases=0,unmaps=0;bool allocationFails=false,mapFails=false; +void *allocate(void *o,void *,void *shared,void *resource,uint32_t options,uint64_t bytes){ + assert(o==owner && !shared && !resource && options==0x23 && bytes==8355840);++allocations; + return allocationFails?nullptr:memory; +} +void *createMap(void *m,void *addressSpace,uint32_t flags){ + assert(m==memory && addressSpace==reinterpret_cast(0x1234) && flags==0x800000B); + return mapFails?nullptr:mapping; +} +uint64_t address(void *p){assert(p==mapping);return 0x10000000;} +bool ready(void *p){assert(p==mapping);return true;} +void release(void *p){assert(p==mapping || p==memory);++releases;} +void unmap(void *p){assert(p==mapping);++unmaps;} +int main(int argc,char **argv){ + assert(argc==2);std::ifstream file(argv[1],std::ios::binary); + std::vector bytes((std::istreambuf_iterator(file)),{}); + constexpr size_t size=0x2000000;assert(bytes.size()(mmap(nullptr,size,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0)); + assert(image!=MAP_FAILED);memcpy(image,bytes.data(),bytes.size()); + // Replace the imported IOAccelSysMemory::withOptions call with a bounded + // allocation leaf; execute native allocation/map/publication/free bodies. + constexpr unsigned stub=0x1000000,call=0x7F674; + assert(image[call]==0xE8);image[stub]=0x48;image[stub+1]=0xB8; + put(image,stub+2,reinterpret_cast(allocate));image[stub+10]=0xFF;image[stub+11]=0xE0; + put(image,call+1,int32_t(stub-call-5)); + uint64_t pageSize=4096,kernelTask=0; + put(image,0xC8238,reinterpret_cast(&pageSize));put(image,0xC8228,reinterpret_cast(&kernelTask)); + put(pipe,0x88,owner);put(owner,0x1470,3U);put(owner,0x150,uintptr_t(0x1234)); + put(memory,0,memVT);put(mapping,0,mapVT);mapping[0x10]=1; + memVT[0x138/8]=reinterpret_cast(createMap);memVT[0x28/8]=reinterpret_cast(release); + mapVT[0x128/8]=reinterpret_cast(address);mapVT[0x138/8]=reinterpret_cast(ready); + mapVT[0x140/8]=reinterpret_cast(unmap);mapVT[0x28/8]=reinterpret_cast(release); + Profile p;p.status=ProfileStatus::Valid;p.mode={135255,1080,46,22,2,1920,82,40,2,86,154}; + auto alloc=reinterpret_cast(image+0x7E5B2); + auto free=reinterpret_cast(image+0x7E772); + NativeBacking::Record record;assert(NativeBacking::request(p,3,record)); + assert(!alloc(pipe,&record,0)); + assert(record.map==reinterpret_cast(mapping) && record.memory==reinterpret_cast(memory)); + assert(record.gpu==0x10000000 && record.pitch==4352 && record.refs==1 && !record.bytes); + assert(get(pipe,0x1298)==memory && get(pipe,0x12A0)==mapping && !get(pipe,0x12A8)); + assert(!free(pipe,&record,0) && releases==2 && unmaps==1 && !record.refs && !record.map && !record.memory && !record.gpu && !record.pitch); + assert(!get(pipe,0x1298) && !get(pipe,0x12A0)); + assert(!free(pipe,&record,0) && releases==2 && unmaps==1); + allocationFails=true;assert(NativeBacking::request(p,3,record));assert(alloc(pipe,&record,0)==0xE00002BC); + assert(!record.refs && !record.map && !get(pipe,0x1298)); + allocationFails=false;mapFails=true;assert(alloc(pipe,&record,0)==0xE00002BC && releases==3); + assert(!record.refs && !record.map && !record.memory && !get(pipe,0x1298)); + assert(allocations==3);assert(!munmap(image,size)); + puts("PASS exact native DSI Y allocation/free: request geometry, pipe references, failed allocation/map cleanup"); +} +''' +with tempfile.TemporaryDirectory(prefix='dsi-backing-native-') as directory: + d=Path(directory);(d/'test.cpp').write_text(code) + subprocess.run(['clang++','-arch','x86_64','-std=c++17','-O1','-I'+str(root/'WhateverXe'),str(d/'test.cpp'),'-o',str(d/'test')],check=True) + subprocess.run([str(d/'test'),str(donor)],check=True) diff --git a/tests/dsi_backlight.cpp b/tests/dsi_backlight.cpp new file mode 100644 index 00000000..95a6d223 --- /dev/null +++ b/tests/dsi_backlight.cpp @@ -0,0 +1,41 @@ +#include "kern_dsi_backlight.hpp" +#include +#include +#include +using namespace XELP::DSI; +struct IO { + bool owned=true,drop=false;unsigned writes=0; + std::map regs{{Backlight::Control,0x80000000},{Backlight::Period,480},{Backlight::Duty,154}}; + bool ready(){return owned;} + bool readable(uint32_t reg){return regs.count(reg);} + uint32_t read(uint32_t reg){return regs.at(reg);} + void write(uint32_t reg,uint32_t value){assert(reg==Backlight::Duty && owned);++writes;if(!drop)regs[reg]=value;} +}; +int main() { + Profile p;p.status=ProfileStatus::Valid;p.precision=8;p.minimum=6; + Backlight b;IO io; + assert(b.capture(p,0x80000000,480,154) && b.requested==21026 && b.minimum==11); + assert(!b.capture(p,0x80000000,480,154)); + for(auto value:{0U,1U,32768U,65535U}) { + assert(b.request(value));const auto before=io.writes; + // Deferred requests have no IO dependency and must not relight the panel. + assert(io.writes==before);assert(b.apply(io)); + assert(b.applied==(value==0?0U:value==1?11U:value==32768?240U:480U)); + } + const auto requested=b.requested; + assert(!b.request(65536) && !b.request(0x100000001ULL) && b.requested==requested); + io.owned=false;auto writes=io.writes;assert(!b.apply(io) && io.writes==writes && !b.faulted); + io.owned=true;io.regs[Backlight::Period]=481; + assert(!b.apply(io) && b.faulted && io.writes==writes && !b.request(123)); + Backlight dropped;IO bad;assert(dropped.capture(p,0x80000000,480,154)); + assert(dropped.request(0));bad.drop=true; + assert(!dropped.apply(bad) && dropped.faulted && dropped.applied==154); + for(unsigned which=0;which<5;++which) { + Backlight invalid;auto profile=p; + if(which==0)profile.controller=1;if(which==1)profile.precision=16; + assert(!invalid.capture(profile,which==2?0:0x80000000,which==3?0:480,which==4?481:154)); + } + Backlight wide;assert(wide.capture(p,0x80000000,1000000,500000)); + assert(wide.request(65535) && wide.target()==1000000); + puts("PASS DSI backlight: Apple range, VBT floor, zero duty, deferred request, 64-bit scaling, ownership/readback failure"); +} diff --git a/tests/dsi_backlight_adapter.py b/tests/dsi_backlight_adapter.py new file mode 100644 index 00000000..c3f51024 --- /dev/null +++ b/tests/dsi_backlight_adapter.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Execute the production brightness adapter across retained display-off.""" +from pathlib import Path +from production_source import read_source +import re, subprocess, tempfile +r=Path(__file__).resolve().parents[1] +s=read_source(r/'WhateverXe/kern_xe.cpp') +body='\n'.join(re.search(r'uint32_t XE::'+name+r'\(.*?\n\}',s,re.S)[0] for name in ['setDSIBrightness','saveDSIBrightness']) +code=r''' +#include "kern_dsi_backlight.hpp" +#include +#include +#include +#include +struct IOService {alignas(8) unsigned char bytes[0x4B90]{};}; +namespace Display13FBMemory {templateT field(void *p,unsigned offset){T v;memcpy(&v,(char*)p+offset,sizeof(v));return v;}} +struct IOMemoryMap {std::vector words=std::vector(0x1000000/4);uintptr_t getVirtualAddress(){return (uintptr_t)words.data();}}; +struct Display13FBBootIO {IOMemoryMap *map;bool readable(uint32_t r){return map && r<0x1000000;}uint32_t read(uint32_t r){return map->words[r/4];}}; +unsigned depth=0,heldQueries=0;using IOLock=int; +void IOLockLock(IOLock*){assert(!depth);++depth;}void IOLockUnlock(IOLock*){assert(depth==1);--depth;} +struct Context { + IOLock *lock=nullptr;IOMemoryMap *map=nullptr;void *controller=nullptr; + bool memoryFaulted=false,clockFaulted=false,watermarkFaulted=false; + struct {struct {bool faulted=false,suspended=false;}state;bool outputHeld(Display13FBBootIO&,int,int){assert(depth);++heldQueries;return true;}}bridge; + struct {bool faulted=false;}dbuf; + struct {IOService *memoryFramebuffer=nullptr;bool destroyingPath=false;}pipe[1]; +}display13FBPower; +bool mapping=true,awake=true;IOService *target=nullptr; +bool adlnWriterMappingBound(){return mapping;}bool dsiModeTarget(IOService *fb){return fb==target;} +constexpr uint32_t kIOReturnSuccess=0,kIOReturnBadArgument=1,kIOReturnNotReady=2; +struct XE { + XELP::DSI::Backlight dsiBacklight;bool dsiResourcesReady=true,dsiPowerTest=true,dsiStopAttempted=false; + bool dsiIRQParked=false,dsiMemoryReleased=false,dsiDDIReleased=false;void *adlnTopologyController=nullptr; + struct {bool sleeping=false,failed=false;}dsiRuntimePower; + struct {bool done=false;bool complete(){return done;}}dsiStop; + struct {bool complete=false;}dsiPanelStop; + unsigned published=0;void publishDSIBacklight(){assert(!depth);++published;} + bool dsiNativeUpdateAllowed(IOService*){assert(!depth);return awake;} + uint32_t setDSIBrightness(IOService*,uintptr_t); + uint32_t saveDSIBrightness(IOService*,uintptr_t); +}; +BODY +int main(){ + IOService fb,controller;IOLock lock;IOMemoryMap map;XE xe;auto &c=display13FBPower; + c.lock=&lock;c.map=↦c.controller=&controller;c.pipe[0].memoryFramebuffer=&fb; + xe.adlnTopologyController=&controller;target=&fb;memcpy(fb.bytes+0x1D0,&c.controller,sizeof(void*)); + XELP::DSI::Profile p;p.status=XELP::DSI::ProfileStatus::Valid;p.precision=8;p.minimum=6; + using Light=XELP::DSI::Backlight; + map.words[Light::Control/4]=0x80000000;map.words[Light::Period/4]=480;map.words[Light::Duty/4]=154; + assert(xe.dsiBacklight.capture(p,0x80000000,480,154)); + assert(xe.setDSIBrightness(&fb,32768)==0 && map.words[Light::Duty/4]==240); + assert(Display13FBMemory::field(&controller,0xE58)==32768); + awake=false;xe.dsiRuntimePower.sleeping=true;xe.dsiStopAttempted=xe.dsiIRQParked=xe.dsiStop.done=xe.dsiPanelStop.complete=true; + auto queries=heldQueries,writes=xe.dsiBacklight.writes; + assert(xe.setDSIBrightness(&fb,65535)==0 && map.words[Light::Duty/4]==240); + assert(heldQueries==queries && xe.dsiBacklight.writes==writes); + assert(Display13FBMemory::field(&controller,0xE58)==65535 && xe.dsiBacklight.requested==65535); + assert(xe.saveDSIBrightness(&fb,12345)==0); + assert(Display13FBMemory::field(&controller,0xE5C)==12345); + assert(xe.dsiBacklight.requested==65535 && xe.dsiBacklight.writes==writes); + assert(xe.saveDSIBrightness(&fb,0x100000001ULL)==kIOReturnBadArgument); + xe.dsiDDIReleased=true;assert(xe.setDSIBrightness(&fb,0)==kIOReturnNotReady); + xe.dsiDDIReleased=false;c.bridge.state.suspended=true;assert(xe.setDSIBrightness(&fb,0)==kIOReturnNotReady); + c.bridge.state.suspended=false;xe.dsiRuntimePower.failed=true;assert(xe.setDSIBrightness(&fb,0)==kIOReturnNotReady); + assert(xe.setDSIBrightness(&fb,0x100000001ULL)==kIOReturnBadArgument && !depth); + puts("PASS actual DSI brightness adapter: active PWM, native readback, deferred value without MMIO, retired/suspended/failed rejection"); +} +'''.replace('BODY',body) +with tempfile.TemporaryDirectory(prefix='dsi-backlight-adapter-') as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(r/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_boot.cpp b/tests/dsi_boot.cpp new file mode 100644 index 00000000..8f0ca090 --- /dev/null +++ b/tests/dsi_boot.cpp @@ -0,0 +1,112 @@ +#include "kern_dsi_boot.hpp" +#include "kern_display13_boot.hpp" +#include +#include +#include +#include +#include +using namespace XELP::DSI; +struct IO { + std::map regs; + std::vector reads,writes; + XELPMemory::Layout memory=XELPMemory::decode(0x03C1,0x7A800000,0); + uint32_t ptes=0,badPTE=UINT32_MAX,missing=UINT32_MAX,changeAt=UINT32_MAX,changeReg=0; + bool driftOnWrite=false,strict=false; + IO(){ + assert(XELPMemory::reservation(memory,memory.base+memory.bytes-0x100000)); + regs={{0x7B008,0xC0000000},{0x6B400,0x80030006},{0x45404,0xFC0F},{0x70008,0}, + {0x6B030,0x20130300},{0x6B094,1},{0x6B404,0},{0x6B034,0},{0x64000,0x80000000},{0x164280,0x1E07801}, + {0x46014,0xCC000000},{0x16428C,0x3001FB},{0x164290,0xE84}, + {0x6B000,0x04650437},{0x6B008,0x044F044D},{0x6B00C,0x07D1077F},{0x6B014,0x07A907A7}, + {0x51004,0x40000000},{0x70180,0x94000008},{0x70188,68},{0x70190,0x077F0437}}; + } + bool readable(uint32_t reg){return reg<0x1000000 && reg!=missing && (!strict || regs.count(reg) || reg>=0x800000);} + uint32_t read(uint32_t reg){assert(readable(reg));reads.push_back(reg);return regs[reg];} + uint64_t readPTE(uint32_t reg){ + assert(reg==0x800000+8*ptes);const auto index=ptes++; + if(index==changeAt)regs[changeReg]^=1; + return memory.pte(uint64_t(index)*4096)^(index==badPTE?4096:0); + } + void write(uint32_t reg,uint32_t v){writes.push_back(reg);regs[reg]=v;if(driftOnWrite)regs[0x6B000]^=1;} +}; +Profile profile(){Profile p;p.status=ProfileStatus::Valid;p.lanes=4;p.mode={135255,1080,46,22,2,1920,82,40,2,86,154};return p;} +Boot capture(IO &io){return captureBoot(io,AlderLakeN::readDisplay(io,true),profile(),io.memory,true);} +int main(int argc,char **argv){ + // The pre-handoff audit reports real qualification failures without writes + // or promoting its scratch certificate into an active owner. + { + IO observed;auto normal=AlderLakeN::readDisplay(observed,true);Boot scratch;BootAudit audit; + observed.reads.clear();auditBoot(observed,normal,profile(),observed.memory,false,scratch,audit); + assert(audit.words[0]==1 && audit.words[1]==uint32_t(BootStatus::Unavailable)); + assert(observed.reads.empty() && !observed.ptes && observed.writes.empty()); + Profile invalid; + auditBoot(observed,normal,invalid,observed.memory,true,scratch,audit); + assert(observed.reads.empty() && !observed.ptes); + auditBoot(observed,normal,profile(),observed.memory,true,scratch,audit); + assert(scratch.complete() && audit.words[12]==observed.memory.bytes/4096); + assert(audit.words[22]==1080 && audit.words[23]==1920 && audit.words[24]==4352); + assert(audit.words[29]==1 && audit.words[30]==811500 && observed.writes.empty()); + } + for(bool dsb:{false,true}) { + IO observed;auto normal=AlderLakeN::readDisplay(observed,true);Boot scratch;BootAudit audit; + if(dsb)observed.regs[0x70B08]=1;else observed.badPTE=17; + auditBoot(observed,normal,profile(),observed.memory,true,scratch,audit); + if(dsb)assert(audit.words[1]==uint32_t(BootStatus::Peers) && audit.words[7]==0x70B08 && audit.words[8]==1 && !observed.ptes); + else assert(audit.words[1]==uint32_t(BootStatus::PTE) && audit.words[9]==0x800000+8*17 && + audit.words[12]==17 && audit.words[10]!=audit.words[11] && audit.words[11]==observed.memory.pte(17*4096)); + assert(observed.writes.empty()); + } + { + IO observed;auto normal=AlderLakeN::readDisplay(observed,true);Boot scratch;BootAudit audit; + observed.changeAt=4;observed.changeReg=0x45404; + auditBoot(observed,normal,profile(),observed.memory,true,scratch,audit); + assert(audit.words[1]==uint32_t(BootStatus::Changed) && observed.reads.back()==0x45404 && observed.writes.empty()); + } + IO io;auto boot=capture(io);assert(boot.status==BootStatus::Candidate && !boot.complete()); + assert(boot.geometry.width==1080 && boot.geometry.height==1920 && boot.geometry.pitch==4352 && boot.dpt==0); + assert(verifyBootPrefix(io,boot,io.memory) && boot.complete() && boot.verifiedPages==io.memory.bytes/4096); + assert(io.writes.empty() && io.ptes==boot.verifiedPages); + // Candidate capture accepts no unsupported active peer or scaler/DSC path. + for(auto change:std::map{{0x7B808,0x80000000},{0x71008,0x80000000},{0x61400,0x80000000}, + {0x70080,1},{0x70280,0x80000000},{0x70780,0x80000000},{0x71180,0x80000000}, + {0x68180,0x80000000},{0x78000,0x80000000},{0x70B08,1},{0x71D08,0x80000000}, + {0x70180,0x94000408},{0x7018C,1},{0x701A4,1},{0x701AC,4096},{0x7019C,1},{0x70188,16}, + {0x6B000,0x04650438},{0x51004,0},{0x42084,UINT32_MAX}}){ + IO bad;bad.regs[change.first]=change.second;assert(!capture(bad).usable() && !bad.ptes && bad.writes.empty()); + } + for(unsigned index:{0U,8191U,24575U}){ + IO bad;auto b=capture(bad);bad.badPTE=index;assert(!verifyBootPrefix(bad,b,bad.memory)); + assert(b.status==BootStatus::PTE && b.verifiedPages==index && bad.ptes==index+1 && bad.writes.empty()); + } + for(auto reg:{0x70180U,0x70188U,0x70190U,0x7019CU,0x701ACU,0x42084U,0x6B000U,0x164280U,0x45404U}){ + IO bad;auto b=capture(bad);bad.changeAt=4;bad.changeReg=reg;bad.reads.clear(); + assert(!verifyBootPrefix(bad,b,bad.memory) && b.status==BootStatus::Changed && bad.writes.empty()); + if(reg==0x45404)assert(bad.reads.back()==0x45404); // no DSI/pipe read after power loss + } + for(auto reg:{0x70B08U,0x7B808U,0x701ACU,0x51004U}){IO bad;bad.missing=reg;assert(!capture(bad).usable());} + IO invalid;invalid.memory.reservationKnown=false;assert(capture(invalid).status==BootStatus::Memory); + IO changed;auto b=capture(changed);auto m=changed.memory;m.allocationEnd-=4096; + assert(!verifyBootPrefix(changed,b,m) && !changed.ptes); + // The existing unused-TC cleanup must also retain a DSI source witness. + for(bool drift:{false,true}){ + IO tc;Display13OutputWiring w;w.valid=true;w.dsi=1;w.hdmi=8;w.ddc[3]=9; + tc.regs[0x64300]=0xC0;tc.regs[0x161500]=0xC;tc.regs[0x46110]=0; + auto b=capture(tc);assert(b.usable());auto normal=AlderLakeN::readDisplay(tc,true); + tc.driftOnWrite=drift;auto result=Display13Boot::releaseUnusedTC(tc,normal,true,w,&b.source); + if(drift)assert(result.status==Display13Boot::Status::Readback && tc.writes.size()==1); + else assert(result.status==Display13Boot::Status::Complete && result.cleared==8 && tc.regs[0x64300]==0x80); + assert(tc.regs[0x70180]==b.control && tc.regs[0x7019C]==b.surface && tc.regs[0x164280]==b.source.mux); + } + if(argc==2){ + IO saved;saved.regs.clear();saved.strict=true;std::ifstream f(argv[1]);assert(f);uint32_t a,v; + while(f>>std::hex>>a>>v)assert(saved.regs.emplace(a,v).second);assert(f.eof()); + // The saved report omits DSB CTRL and real PTEs. Supply explicit synthetic + // idle DSBs/PTEs for this geometry replay; never call it a live certificate. + for(unsigned p=0;p<3;++p)for(unsigned d=0;d<3;++d)saved.regs.emplace(0x70B08+0x1000*p+0x100*d,0); + auto actual=capture(saved);assert(actual.usable()); + assert(actual.geometry.width==1080 && actual.geometry.height==1920 && actual.geometry.pitch==4352); + assert(verifyBootPrefix(saved,actual,saved.memory)); + puts("saved GOP register geometry accepted; DSB idle and stolen PTEs are synthetic, not collected evidence"); + } + puts("PASS DSI GOP certificate: native portrait pitch, full prefix, peer exclusion, source drift, unused TC isolation; no scanout/PTE writes"); +} diff --git a/tests/dsi_boot_adapter.py b/tests/dsi_boot_adapter.py new file mode 100644 index 00000000..1d511168 --- /dev/null +++ b/tests/dsi_boot_adapter.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Execute production firmware-copy and DSI pre-native handoff with mock IOKit.""" +from pathlib import Path +from production_source import read_source +import re, subprocess, tempfile +root=Path(__file__).resolve().parents[1] +source=read_source(root/'WhateverXe/kern_xe.cpp') +# Observation must precede the destructive external-output trial and must +# use scratch storage, not publish the certificate to the live DSI owner. +audit_source=re.search(r'void XE::auditDSIBoot\([^;{]*\{.*?\n\}',source,re.S)[0] +assert 'if(!checkKernelArgument("-wex-dsi-audit"))return;' in audit_source +assert 'scratch->boot,scratch->audit' in audit_source and '"WEXDSIBootAudit"' in audit_source +assert 'dsiBoot=' not in audit_source and 'write(' not in audit_source +bootstrap=re.search(r'void XE::adlnBootstrap\([^;{]*\{.*?\n\}',source,re.S)[0] +assert bootstrap.index('auditDSIBoot(pci,') < bootstrap.index('Display13DSITrial::transfer(') +functions=audit_source+'\n'+'\n'.join(re.search(r'bool XE::'+name+r'\(.*?\n\}',source,re.S)[0] + for name in ['prepareDSIFirmware','prepareDisplay13BootScanout']) +fixture=(root/'tests/dsi_profile.cpp').read_text() +fixture=fixture[fixture.index('using Bytes='):fixture.index('Profile parse(')] +code=r''' +#include "kern_dsi_boot.hpp" +#include "kern_display13_boot.hpp" +#include +#include +#include +using namespace XELP::DSI; +FIXTURE +bool copyOK=true,deviceOK=true,memoryOK=true,auditEnabled=false;unsigned alive=0,releases=0,audits=0,allocations=0; +BootAudit lastAudit; +bool checkKernelArgument(const char *key){assert(!strcmp(key,"-wex-dsi-audit"));return auditEnabled;} +void *IOMalloc(size_t bytes){++allocations;return calloc(1,bytes);} +void IOFree(void *p,size_t){--allocations;free(p);} +struct OSObject {OSObject(){++alive;}virtual ~OSObject(){--alive;}void release(){delete this;}}; +struct OSData:OSObject { + std::vector bytes; + unsigned getLength(){return bytes.size();}void *getBytesNoCopy(){return bytes.data();} + static OSData *withBytes(const void *p,unsigned n){if(!copyOK)return nullptr;auto d=new OSData; + d->bytes.assign(static_cast(p),static_cast(p)+n);return d;} +}; +struct IOService {OSObject *property=nullptr;OSObject *getProperty(const char *){return property;} + void setProperty(const char *key,const void *p,unsigned bytes){ + if(!strcmp(key,"WEXDSIBootAudit")){assert(bytes==sizeof(lastAudit));memcpy(&lastAudit,p,bytes);++audits;} + };}; +struct IOMemoryMap { + std::vector bytes=std::vector(0x1000000); + uintptr_t getVirtualAddress(){return reinterpret_cast(bytes.data());} + size_t getLength(){return bytes.size();}void release(){++releases;} +}; +struct IOPCIDevice:IOService {IOMemoryMap *map;bool mapped=true;unsigned command=6; + uint16_t configRead16(unsigned o){assert(o==4);return command;} + IOMemoryMap *mapDeviceMemoryWithRegister(unsigned reg,unsigned){assert(reg==0x10);return mapped?map:nullptr;} +}; +struct IORegistryEntry:IOService {static IORegistryEntry *getRegistryRoot(){static IORegistryEntry p;return &p;}}; +constexpr unsigned kIOMapInhibitCache=1; +void IODelay(unsigned){assert(false && "preserved DSI handoff must not poll or reinitialize");} +#define SYSLOG(...) +#define OSDynamicCast(T,p) dynamic_cast(p) +struct XE { + OSData *dsiFirmware=nullptr;Profile dsiProfile;Boot dsiBoot;bool display13BootReady=true; + XELPMemory::Layout xelpMemory;Display13OutputWiring adlnOutputWiring; + bool xelpDisplay13Device(IOPCIDevice *){return deviceOK;} + bool xelpMemoryMatches(IOPCIDevice *){return memoryOK;} + void auditDSIBoot(IOPCIDevice *,volatile const uint8_t *,uint64_t,const AlderLakeN::DisplayReadout &,bool); + bool prepareDSIFirmware(IOService *); + bool prepareDisplay13BootScanout(IOPCIDevice *); +}; +FUNCTIONS +struct IO { + IOMemoryMap ↦ + bool readable(uint32_t r){return r(xe.dsiFirmware->getBytesNoCopy()); + for(auto span:xe.dsiProfile.sequences.sequence)if(span.data)assert(span.data>=start && span.data+span.size<=start+bytes.size()); + auto source=static_cast(provider.property);source->bytes[0]^=1; + assert(!xe.prepareDSIFirmware(&provider) && xe.dsiProfile.valid() && start[0]=='$'); + source->release();provider.property=nullptr; + assert(xe.dsiProfile.mode.width==1080 && xe.dsiProfile.sequences.sequence[1].data[0]==0); + IOMemoryMap map;IOPCIDevice pci;pci.map=↦IO io{map}; + xe.xelpMemory=XELPMemory::decode(0x03C1,0x7A800000,0); + assert(XELPMemory::reservation(xe.xelpMemory,xe.xelpMemory.base+xe.xelpMemory.bytes-0x100000)); + const uint32_t regs[][2]={{0x7B008,0xC0000000},{0x6B400,0x80030006},{0x45404,0xFC0F}, + {0x6B030,0x20130300},{0x6B094,1},{0x64000,0x80000000},{0x164280,0x1E07801}, + {0x46014,0xCC000000},{0x16428C,0x3001FB},{0x164290,0xE84}, + {0x6B000,0x04650437},{0x6B008,0x044F044D},{0x6B00C,0x07D1077F},{0x6B014,0x07A907A7}, + {0x51004,0x40000000},{0x70180,0x94000008},{0x70188,68},{0x70190,0x077F0437}}; + for(auto &r:regs)io.word(r[0],r[1]); + for(unsigned i=0;irelease();pci.property=nullptr; + xe.auditDSIBoot(&pci,map.bytes.data(),map.bytes.size(),normal,true); + assert(audits==3 && !allocations && lastAudit.words[1]==uint32_t(BootStatus::Unavailable)); + xe.dsiBoot=captureBoot(io,normal,xe.dsiProfile,xe.xelpMemory,true); + xe.adlnOutputWiring.valid=true;xe.adlnOutputWiring.dsi=1; + auto before=map.bytes; + assert(!xe.prepareDisplay13BootScanout(&pci) && releases==1 && map.bytes==before); // prefix not certified yet + assert(verifyBootPrefix(io,xe.dsiBoot,xe.xelpMemory)); + assert(xe.prepareDisplay13BootScanout(&pci) && releases==2 && map.bytes==before); + memoryOK=false;assert(!xe.prepareDisplay13BootScanout(&pci) && releases==2);memoryOK=true; + pci.command=0;assert(!xe.prepareDisplay13BootScanout(&pci) && releases==2);pci.command=6; + io.word(0x7019C,4096);before=map.bytes; + assert(!xe.prepareDisplay13BootScanout(&pci) && releases==3 && map.bytes==before && xe.dsiBoot.status==BootStatus::Changed); + xe.dsiFirmware->release();assert(!alive); + puts("PASS production DSI bootstrap: immutable firmware spans, stale firmware rejection, full-prefix prerequisite, zero-write handoff and map cleanup"); +} +'''.replace('FIXTURE',fixture).replace('FUNCTIONS',functions) +with tempfile.TemporaryDirectory(prefix='dsi-boot-adapter-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_boot_adoption.py b/tests/dsi_boot_adoption.py new file mode 100644 index 00000000..3fc5b1fc --- /dev/null +++ b/tests/dsi_boot_adoption.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Exercise the production boot adoption transaction; mode setter is a fault-injectable leaf.""" +from pathlib import Path +from production_source import read_source +import re,subprocess,tempfile +root=Path(__file__).resolve().parents[1] +s=read_source(root/'WhateverXe/kern_xe.cpp') +body=re.search(r'bool XE::adoptDSIBootPath\(.*?\n\}',s,re.S)[0] +binding=re.search(r'static TGLFBBinding::Readout adlnInspectPathBinding\(.*?\n\}',s,re.S)[0] +boot=(root/'tests/dsi_boot.cpp').read_text();io=boot[boot.index('struct IO {'):boot.index('Boot capture(')] +# Reuse the exact native object graph and lease setup from the cache fixture. +base=(root/'tests/dsi_native_cache.py').read_text().split("code=r'''",1)[1].split("'''.replace('IO_FIXTURE'",1)[0] +base=base[:base.index(' const auto unchanged=')] +base=base.replace('#include "kern_dsi_native_cache.hpp"','#include "kern_dsi_native_cache.hpp"\n#include "kern_display13_fb_irq.hpp"') +base=base.replace('unsigned size=sizeof(bytes);','unsigned size=sizeof(bytes);bool readyProperty=true;void setProperty(const char*,bool b){readyProperty=b;}') +base=base.replace('struct IOLock{};struct IORecursiveLock{};','''struct IOLock{};struct IORecursiveLock{};struct IOSimpleLock{}; +unsigned IOSimpleLockLockDisableInterrupt(IOSimpleLock*){assert(level==3);level=4;return 1;} +void IOSimpleLockUnlockEnableInterrupt(IOSimpleLock*,unsigned v){assert(v==1 && level==4);level=3;}''') +base=base.replace('IOLock *lock;IOService *controller;OSObject *access,*dsiPort;IO *map;','IOLock *lock;IOService *controller;OSObject *access,*dsiPort,*power;IO *map;\n struct {IOService *memoryFramebuffer;} pipe[3];') +base=base.replace('BINDING','''struct {IOSimpleLock *lock;IOService *controller;uint32_t admittedPipes=1;bool quiescing=false;Display13FBIRQ::State state;} display13FBIRQ; +struct {IOService *framebuffer=nullptr;bool ready=false;} dsiFixedMode; +bool dsiModeTarget(IOService *fb){return dsiFixedMode.framebuffer==fb;} +Display13FBMemory::Readout measuredMemory; +Display13FBMemory::Readout display13FBInspectMemory(IOService*,bool boot,unsigned pipe,bool publish){assert(level==3 && boot && !pipe && !publish);return measuredMemory;} +BINDING''') +base=base.replace('bool dsiCacheHooksReady=true,dsiResourcesReady=true;','bool dsiCacheHooksReady=true,dsiResourcesReady=true,dsiBootProbeReady=true;Profile dsiProfile=profile();') +base=base.replace(' bool updateDSIBootCache(', ' bool adoptDSIBootPath(IOService *,OSObject *);\n bool bindDSIFixedMode(IOService *,const Profile &);\n bool updateDSIBootCache(') +base=base.replace('FUNCTIONS','''unsigned modeCalls=0;bool publishMode=true,changeSource=false; +bool XE::bindDSIFixedMode(IOService *fb,const Profile&){assert(level==1);++modeCalls; + dsiFixedMode.framebuffer=fb;dsiFixedMode.ready=publishMode;fb->setProperty("WEXDSIFixedModeReady",publishMode); + if(changeSource)display13FBPower.map->regs[0x6B000]^=1; + return publishMode; +} +FUNCTIONS''') +base+=r''' + IOSimpleLock irqLock;OSObject power; + c.power=&power;put(&controller,0xC8,&power);c.pipe[0].memoryFramebuffer=&fb; + put(&fb,0x4A00,1U);put(&fb,0x4A08,&path);put(&fb,0x44DE,uint8_t(1));put(&fb,0x1E7,uint8_t(1));put(&path,0x3644,2U); + b.bootPort=0;b.bootPGProtected=b.bootDDIProtected=true; + measuredMemory.status=Display13FBMemory::Status::Complete;measuredMemory.control=xe.dsiBoot.control; + measuredMemory.stride=xe.dsiBoot.stride;measuredMemory.size=xe.dsiBoot.size;measuredMemory.surface=xe.dsiBoot.surface; + auto &irq=display13FBIRQ;irq.lock=&irqLock;irq.controller=&controller;irq.state.begun=irq.state.enabled=true;irq.state.transcoders.dsi0Pipe=0; + const auto rejected=[&]{const auto calls=modeCalls;assert(!xe.adoptDSIBootPath(&fb,&path)); + assert(modeCalls==calls && c.memoryFaulted && !b.bootAdopted && !b.bootPath && b.bootPGProtected && b.bootDDIProtected && !level); + c.memoryFaulted=false;}; + irq.state.enabled=false;rejected();irq.state.enabled=true; + irq.quiescing=true;rejected();irq.quiescing=false; + measuredMemory.surface=4096;rejected();measuredMemory.surface=0; + put(&fb,0x4A08,&plane);rejected();put(&fb,0x4A08,&path); + b.ddiRefs[0]=0;rejected();b.ddiRefs[0]=1; + publishMode=false;assert(!xe.adoptDSIBootPath(&fb,&path) && modeCalls==1 && c.memoryFaulted); + assert(!b.bootAdopted && b.bootPGProtected && b.bootDDIProtected && dsiModeTarget(&fb) && !dsiFixedMode.ready && !fb.readyProperty); + c.memoryFaulted=false;publishMode=true;changeSource=true; + assert(!xe.adoptDSIBootPath(&fb,&path) && modeCalls==2 && !dsiFixedMode.ready && !b.bootAdopted); + assert(b.pgRefs[0]==1 && b.ddiRefs[0]==1 && b.bootPGProtected && b.bootDDIProtected); + c.memoryFaulted=false;changeSource=false;io.regs[0x6B000]^=1; + assert(xe.adoptDSIBootPath(&fb,&path) && dsiFixedMode.ready && b.bootAdopted && b.bootPath==reinterpret_cast(&path)); + assert(!b.bootPGProtected && !b.bootDDIProtected && b.pgRefs[0]==1 && b.ddiRefs[0]==1); + assert(Display13FBMemory::field(&power,0x10)==&controller && Display13FBMemory::field(&power,0x78)==&access); + assert(xe.adoptDSIBootPath(&fb,&path) && b.pgRefs[0]==1 && b.ddiRefs[0]==1); + b.bootPath=reinterpret_cast(&plane); + assert(!xe.adoptDSIBootPath(&fb,&path) && c.memoryFaulted && !dsiFixedMode.ready); + assert(b.bootPath==reinterpret_cast(&plane) && b.pgRefs[0]==1 && b.ddiRefs[0]==1 && !level && io.writes.empty()); + puts("PASS production DSI adoption: real path/leases/IRQ and memory prerequisites, mode publication outside owner lock, revalidation, failure retention and single boot transfer"); +} +''' +code=base.replace('IO_FIXTURE',io).replace('BINDING',binding).replace('FUNCTIONS',body) +with tempfile.TemporaryDirectory(prefix='dsi-boot-adoption-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_boot_probe.py b/tests/dsi_boot_probe.py new file mode 100644 index 00000000..b76f6cdb --- /dev/null +++ b/tests/dsi_boot_probe.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Qualify DSI boot recognition; optionally replay the exact native consumer.""" +from pathlib import Path +from production_source import read_source +import hashlib, re, subprocess, sys, tempfile +root=Path(__file__).resolve().parents[1] +source=read_source(root/'WhateverXe/kern_xe.cpp') +body=re.search(r'uint32_t XE::wrapDSIBootProbe\(.*?\n\}',source,re.S)[0] +boot=(root/'tests/dsi_boot.cpp').read_text() +io=boot[boot.index('struct IO {'):boot.index('Boot capture(')] +donor=Path(sys.argv[1]).resolve() if len(sys.argv)>1 else None +if donor: + b=donor.read_bytes() + assert hashlib.sha256(b).hexdigest()=='c004fc1ac9ee4157e2e12736f40fa6639bde025a279326beeb9dd48c9b445731' + installer=re.search(r'bool XE::processDisplay13DSIBoot\(.*?\n\}',source,re.S)[0] + for name,offset in [('entry',0x453d6),('call',0x4362a)]: + expected=bytes(int(x,0) for x in re.search(name+r'\[\]=\{([^}]+)',installer)[1].split(',')) + assert b[offset:offset+len(expected)]==expected + for at,target in [(0x4362a,0x453d6),(0x436bc,0x1b1ea),(0x436ea,0x66ea0)]: + assert b[at]==0xe8 and at+5+int.from_bytes(b[at+1:at+5],'little',signed=True)==target +code=r''' +#include "kern_dsi_boot.hpp" +#include "kern_display13_power_bridge.hpp" +#include "kern_display13_fb_topology.hpp" +#include "kern_display13_transcoder.hpp" +#include +#include +#include +#include +#include +#include +using namespace XELP::DSI; +IO_FIXTURE +struct OSObject {alignas(8) uint8_t bytes[0x4B90]{};unsigned size=sizeof(bytes); + OSObject *getMetaClass(){return this;}unsigned getClassSize(){return size;}}; +using IOService=OSObject; +struct IOLock {bool held=false;};void IOLockLock(IOLock *l){assert(!l->held);l->held=true;} +void IOLockUnlock(IOLock *l){assert(l->held);l->held=false;} +namespace Display13FBMemory {templateT &field(void *p,unsigned o){return *reinterpret_cast(static_cast(p)+o);}} +struct Display13FBBootIO {IO *map;bool readable(uint32_t r){return map && map->readable(r);}uint32_t read(uint32_t r){return map->read(r);}}; +struct {IOLock *lock;IOService *controller;OSObject *access,*dsiPort;IO *map; + Display13Transcoder::Bindings transcoders;Readout bootDSI;Display13Power::NativeBridge bridge; + bool memoryFaulted=false,clockFaulted=false;} display13FBPower; +XELPMemory::Layout xelpMemory;bool display13Offline=false;unsigned fallbacks=0,allocations=0; +IOService *nativeFB;OSObject *nativePort; +uint32_t nativeProbe(IOService*,bool *internal,uint32_t *ddi){++fallbacks;if(internal)*internal=false;if(ddi)*ddi=4;return 2;} +OSObject *getPort(uint32_t ddi){assert(ddi==0);return nativePort;} +bool allocate(IOService *fb,uint32_t port){assert(fb==nativeFB && port==0);++allocations;return true;} +#define FunctionCast(f,p) reinterpret_cast(p) +struct XE { + bool dsiBootProbeReady=true,dsiResourcesReady=true;IOService *adlnTopologyController; + Display13OutputWiring adlnOutputWiring;alignas(8) uint8_t adlnPlatform[0x1C0]{}; + uintptr_t orgDSIBootProbe=reinterpret_cast(nativeProbe);Boot dsiBoot; + static uint32_t wrapDSIBootProbe(IOService *,bool *,uint32_t *); +} xe,*callbackXE=&xe; +BODY +void jump(void *image,unsigned offset,uintptr_t fn){auto p=static_cast(image)+offset; + const uint8_t prefix[]={0xff,0x25,0,0,0,0};memcpy(p,prefix,6);memcpy(p+6,&fn,8);} +int main(int argc,char **argv){ + IOLock lock;IO io;IOService controller,other,fb;OSObject port,access; + auto &c=display13FBPower;c.lock=&lock;c.map=&io;c.controller=xe.adlnTopologyController=&controller; + c.access=&access;c.dsiPort=&port;c.transcoders.dsi0Pipe=0; + xe.adlnOutputWiring.valid=true;xe.adlnOutputWiring.dsi=1;xe.adlnOutputWiring.hdmi=2;xe.adlnOutputWiring.ddc[1]=2; + xe.adlnPlatform[0x12]=9;uint32_t panel[]={0,0,0,0,2,0x18};memcpy(xe.adlnPlatform+0x20,panel,sizeof(panel)); + auto config=reinterpret_cast(xe.adlnPlatform+0x20); + Display13FBMemory::field(&port,0x548)=config; + Display13FBMemory::field(&port,0x578)=&access;Display13FBMemory::field(&port,0x550)=1; + xelpMemory=io.memory;xe.dsiBoot=captureBoot(io,AlderLakeN::readDisplay(io,true),profile(),io.memory,true); + assert(verifyBootPrefix(io,xe.dsiBoot,io.memory));c.bootDSI=xe.dsiBoot.source; + io.regs[0x45454]=3;io.regs[0x42000]=0x8E380000; + auto &b=c.bridge;auto &s=b.state;b.begun=true;b.selectedPorts=3;s.ready=true;s.allowed=Display13Power::All; + const auto pg=Display13Power::dependencies(Display13Power::nativeScope(1)); + const auto ddi=Display13Power::dependencies(Display13Power::ddiDomain(0));s.borrowed=pg|ddi; + for(unsigned i=0;i(&port,0x578)=&other;reject();Display13FBMemory::field(&port,0x578)=&access; + uint32_t copy[6];memcpy(copy,config,sizeof(copy));Display13FBMemory::field(&port,0x548)=copy; + reject();Display13FBMemory::field(&port,0x548)=reinterpret_cast(1); + reject();Display13FBMemory::field(&port,0x548)=config; + io.regs[0x6B000]^=1;reject();io.regs[0x6B000]^=1; + io.regs[0x7019C]=4096;reject();io.regs[0x7019C]=0; + bool internal=false;uint32_t d=3; + assert(XE::wrapDSIBootProbe(&controller,&internal,&d)==0 && internal && d==0); + assert(XE::wrapDSIBootProbe(&controller,nullptr,nullptr)==0 && io.writes.empty()); + xe.adlnOutputWiring.dsi=0;assert(XE::wrapDSIBootProbe(&controller,&internal,&d)==2 && !internal && d==4 && fallbacks==1); + xe.adlnOutputWiring.dsi=1; + if(argc>1){ + std::ifstream f(argv[1],std::ios::binary);std::vector bytes((std::istreambuf_iterator(f)),{}); + void *image=mmap(nullptr,0x400000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0);assert(image!=MAP_FAILED); + memcpy(image,bytes.data(),bytes.size());jump(image,0x453d6,reinterpret_cast(XE::wrapDSIBootProbe)); + jump(image,0x1b1ea,reinterpret_cast(getPort));jump(image,0x66ea0,reinterpret_cast(allocate)); + nativeFB=&fb;nativePort=&port; + Display13FBMemory::field(&controller,0xD5C)=1; + Display13FBMemory::field(&controller,0xD60)=&fb; + Display13FBMemory::field(&fb,0x1D0)=&controller; + auto setup=reinterpret_cast(static_cast(image)+0x4360a); + for(unsigned builtin=0;builtin<2;++builtin){ + Display13FBMemory::field(&controller,0xE55)=builtin; + assert(!setup(&controller) && allocations==builtin+1); + assert(Display13FBMemory::field(&controller,0xD58)==0 && fb.bytes[0x44DE]==1 && fb.bytes[0x1E0]==1); + } + xe.dsiResourcesReady=false;assert(!setup(&controller) && allocations==2); + assert(Display13FBMemory::field(&controller,0xD58)==0xffff); + munmap(image,0x400000); + } + assert(!lock.held && io.writes.empty()); + puts("PASS DSI boot probe: qualified source/leases/port, cleared failure outputs, ordinary fallback and zero MMIO writes"); + if(argc>1)puts("PASS exact donor setupBootDisplay: pipe/DDI 0, internal FB selection, native allocation callback and rejected boot skips allocation"); +} +'''.replace('IO_FIXTURE',io).replace('BODY',body) +with tempfile.TemporaryDirectory(prefix='dsi-boot-probe-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')]+([str(donor)] if donor else []),check=True) diff --git a/tests/dsi_clock.py b/tests/dsi_clock.py new file mode 100644 index 00000000..e333a0f0 --- /dev/null +++ b/tests/dsi_clock.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Fault injection for retiring the retained DSI PLL after actual host stop.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[1] +s=(root/'tests/dsi_boot.cpp').read_text();fixture=s[s.index('struct IO {'):s.index('Boot capture(')] +code=r''' +#include "kern_dsi_clock.hpp" +#include "kern_dsi_boot.hpp" +#include +#include +#include +#include +using namespace XELP::DSI; +FIXTURE +struct Sim:IO { + unsigned dropped=0,pllWrites=0,ackFailure=0;bool held=true; + bool ready(){return held;} + void write(uint32_t reg,uint32_t value){writes.push_back(reg);if(reg==0x46014 && ++pllWrites==dropped)return;regs[reg]=value;if(reg==0x6B400 && !(value&(1U<<31)))regs[0x6B0D8]=0;} + void delayUs(unsigned us){ + if(us>=1000)++regs[0x70040]; + if(!(regs[0x7B008]&(1U<<31)))regs[0x7B008]&=~(1U<<30); + if(regs[0x6B0D8]&1)regs[0x6B0D8]=(regs[0x6B0D8]&~1U)|(1U<<16); + if(!(regs[0x64000]&(1U<<31)))regs[0x64000]|=0x80; + if(!(regs[0x46014]&(1U<<31)) && ackFailure!=1)regs[0x46014]&=~(1U<<30); + if(!(regs[0x46014]&(1U<<27)) && ackFailure!=2)regs[0x46014]&=~(1U<<26); + } +}; +struct Case { + Sim io;Readout link;Quiesce::Result host;XELPComboClock::ClockAllocation allocation; + Case(){ + io.regs[0x42084]=1U<<30;io.regs[0x6B0D0]=0x01001040;io.regs[0x6001C]=0x0437077F; + io.regs[0x6B010]=io.regs[0x6B00C];io.regs[0x70040]=100; + io.regs[0x164280]&=~(1U<<11);io.regs[0x46010]=0xCC000000; // another live PLL + link=decodeReadout([&](uint32_t r,uint32_t &v){v=io.read(r);return true;},38400);assert(link.complete()); + host=Quiesce::run(io,link,profile());assert(host.complete());io.writes.clear(); + allocation.external[0]=123;allocation.binding[0]=2; + } + Clock::Result run(bool irq=true){return Clock::retire(io,link,host,irq,allocation,123);} + void retained(){assert(allocation.external[0]==123 && allocation.binding[0]==2);} +}; +int main(){ + {Case c;const auto cfg0=c.io.regs[0x16428C],mux=c.io.regs[0x164280];const auto r=c.run(); + assert(r.complete() && r.writes==2 && r.after==0 && r.waitUs<=2000); + assert(c.io.writes==std::vector({0x46014,0x46014}) && !c.allocation.external[0] && !c.allocation.binding[0]); + assert(c.io.regs[0x46010]==0xCC000000 && c.io.regs[0x16428C]==cfg0 && c.io.regs[0x164280]==mux && c.io.regs[0x45404]==0xFC0F); + assert(!c.run().complete() && c.io.writes.size()==2); + } + for(unsigned drop:{1U,2U}){Case c;c.io.dropped=drop;const auto r=c.run();assert(!r.complete() && r.writes==drop && r.waitUs>=1000);c.retained();} + for(unsigned ack:{1U,2U}){Case c;c.io.ackFailure=ack;const auto r=c.run();assert(!r.complete() && r.writes==ack && r.waitUs>=1000);c.retained();} + for(auto reg:{0x45404U,0x7B008U,0x6B400U,0x70180U,0x70080U,0x64000U,0x7B808U,0x6BC00U,0x164280U,0x16428CU,0x164290U}){ + Case c;c.io.regs[reg]^=(reg==0x7B008 || reg==0x7B808 || reg==0x6B400 || reg==0x6BC00 || reg==0x64000 || reg==0x70180)?1U<<31:1; + assert(!c.run().complete() && c.io.writes.empty());c.retained(); + } + {Case c;assert(!c.run(false).complete() && c.io.writes.empty());c.retained();} + {Case c;c.host.error=Quiesce::Error::Timeout;assert(!c.run().complete() && c.io.writes.empty());c.retained();} + {Case c;c.io.held=false;assert(!c.run().complete() && c.io.writes.empty());c.retained();} + {Case c;c.io.missing=0x46014;assert(!c.run().complete() && c.io.writes.empty());c.retained();} + {Case c;c.io.regs[0x46014]=0x88000000;assert(!c.run().complete() && c.io.writes.empty());c.retained();} + for(bool software:{false,true}){Case c;if(software)c.allocation.binding[1]=2;else c.io.regs[0x164280]|=1U<<2; + assert(c.run().status==Clock::Status::Peer && c.io.writes.empty());c.retained();} + puts("PASS DSI PLL retirement: real host stop required, exclusive owner, peer exclusion, ordered unlock/power ACK, lost-write/timeouts retain allocation, no power-well/peer writes"); +} +'''.replace('FIXTURE',fixture) +with tempfile.TemporaryDirectory(prefix='dsi-clock-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_command_adapter.py b/tests/dsi_command_adapter.py new file mode 100644 index 00000000..c09711a8 --- /dev/null +++ b/tests/dsi_command_adapter.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Replay the bootstrap command trial's real ownership and MMIO boundary.""" +from pathlib import Path +from production_source import read_source +import re, subprocess, tempfile +root=Path(__file__).resolve().parents[1] +s=read_source(root/'WhateverXe/kern_xe.cpp') +body=re.search(r'bool XE::probeDSICommands\(.*?\n\}',s,re.S)[0] +code=r''' +#include "kern_dsi_commands.hpp" +#include +#include +#include +#include +unsigned level=0;struct IOService{};struct IOLock{};struct IORecursiveLock{}; +void IORecursiveLockLock(IORecursiveLock*){assert(!level);level=1;} +void IORecursiveLockUnlock(IORecursiveLock*){assert(level==1);level=0;} +void IOLockLock(IOLock*){assert(level==1);level=2;} +void IOLockUnlock(IOLock*){assert(level==2);level=1;} +void IODelay(unsigned){assert(level==2);} +struct IOMemoryMap {std::vector bar=std::vector(0x1000000/4); + uintptr_t getVirtualAddress(){return reinterpret_cast(bar.data());}}; +struct Display13FBBootIO {IOMemoryMap *map;bool readable(uint32_t r){return map && r<0x1000000 && !(r&3);} + uint32_t read(uint32_t r){assert(level==2 && readable(r));return map->bar[r/4];}}; +struct Profile {struct {unsigned clockKHz=135255;unsigned htotal() const {return 1126;}unsigned vtotal() const {return 2002;}}mode;bool valid(){return true;}}; +struct Boot {struct {struct {unsigned pll=1;}identity;unsigned conf=0xC0000000;}source;}; +bool unchanged=true;int xelpMemory; +namespace XELP {namespace DSI {templatebool bootUnchanged(IO&,const Boot&,int){return unchanged;}}} +struct {IORecursiveLock *lock;uintptr_t thread=0;struct {uintptr_t external[3]{};unsigned binding[3]{};}allocation;}comboClockTransaction; +struct {IOLock *lock;IOService *controller;IOMemoryMap *map; + bool engineEnabled=false,memoryFaulted=false,clockFaulted=false; + struct {struct {bool faulted=false,suspended=false;}state;bool held=true; + templatebool outputHeld(IO&,unsigned,unsigned){assert(level==2);return held;}}bridge;}display13FBPower; +uint64_t receipt[20]{};unsigned published=0; +struct IORegistryEntry {static IORegistryEntry *getRegistryRoot(){static IORegistryEntry r;return &r;} + void setProperty(const char *key,const void *p,unsigned size){assert(!level && !strcmp(key,"WEXDSICommandTrial") && size==sizeof(receipt));memcpy(receipt,p,size);++published;}}; +struct XE {IOService *adlnTopologyController;bool dsiResourcesReady=true,dsiStopAttempted=false; + struct {unsigned dsi=1;}adlnOutputWiring;Profile dsiProfile;Boot dsiBoot;XELP::DSI::Host dsiCommandHost; + bool probeDSICommands(IOService*);}; +BODY +int main(){ + IOService controller;IOLock owner;IORecursiveLock clock;IOMemoryMap map; + auto &c=display13FBPower;c.lock=&owner;c.controller=&controller;c.map=↦ + auto &k=comboClockTransaction;k.lock=&clock;k.allocation.external[0]=reinterpret_cast(&controller);k.allocation.binding[0]=2; + map.bar[0x45404/4]=0x403;map.bar[0x6B0D0/4]=0x1040; + XE xe;xe.adlnTopologyController=&controller; + c.engineEnabled=true;assert(!xe.probeDSICommands(&controller) && !map.bar[0x6B100/4]);c.engineEnabled=false; + unchanged=false;assert(!xe.probeDSICommands(&controller) && !map.bar[0x6B100/4]);unchanged=true; + map.bar[0x6B0D8/4]=1;assert(!xe.probeDSICommands(&controller) && !map.bar[0x6B100/4]);map.bar[0x6B0D8/4]=0; + assert(xe.probeDSICommands(&controller) && map.bar[0x6B100/4]==0x40000005 && !level); + assert(receipt[1] && receipt[4] && receipt[5] && receipt[6] && receipt[9]); + assert(receipt[7]==0x1040 && !receipt[8] && !receipt[10] && !receipt[11] && receipt[12]==2 && !receipt[13] && receipt[15]==2); + assert(!xe.probeDSICommands(&controller) && receipt[15]==0); // no replay + unsigned nonzero=0;for(auto word:map.bar)nonzero+=bool(word);assert(nonzero==3); + puts("PASS DSI command trial: owner/clock locks, no writes before admission, unchanged source, bootstrap-only and one-shot"); +} +'''.replace('BODY',body) +with tempfile.TemporaryDirectory(prefix='dsi-command-adapter-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_commands.cpp b/tests/dsi_commands.cpp new file mode 100644 index 00000000..6eea7e36 --- /dev/null +++ b/tests/dsi_commands.cpp @@ -0,0 +1,47 @@ +#include "kern_dsi_commands.hpp" +#include +#include +#include +using namespace XELP::DSI; +struct IO { + bool owned=true,missingHeader=false,ulps=false,stuckCredits=false,stuckLP=false; + unsigned delay=0,creditReads=0,lpReads=0,pending=0,deliveryUs=10; + std::vector regs,values; + bool ready(){return owned;} + bool readable(uint32_t r){return !(missingHeader && r==0x6B100);} + uint32_t read(uint32_t r){ + if(r==0x6B0D0){++creditReads;return stuckCredits || pending?0:0x1040;} + assert(r==0x6B0D8);++lpReads;return ulps?0x10001:stuckLP?0x20000:0; + } + void write(uint32_t r,uint32_t v){assert(owned);regs.push_back(r);values.push_back(v);if(r==0x6B100)pending=deliveryUs;} + void delayUs(unsigned us){delay+=us;pending=pending>us?pending-us:0;} +}; +int main(){ + IO io;CommandPort port{io};Host host(true);uint8_t nop=0; + assert(host.send(port,0x05,{&nop,1}) && host.drain(port)); + assert(io.regs==std::vector({0x6B100,0x6B100})); + assert(io.values==std::vector({0x40000005,0x40000005})); + assert(port.writes==2 && host.headers==2 && !host.pending && host.waitedUs==20); + IO longIO;CommandPort longPort{longIO};Host longHost(true);uint8_t bytes[]={0xB0,1,2,3,4}; + assert(longHost.send(longPort,0x39,{bytes,5}) && longHost.drain(longPort)); + assert(longIO.regs==std::vector({0x6B104,0x6B104,0x6B100,0x6B100})); + assert(longIO.values==std::vector({0x030201B0,4,0xC0000539,0x40000005})); + IO video;video.deliveryUs=16000;CommandPort vp{video};Host vh(true); + assert(vh.send(vp,0x05,{&nop,1}) && vh.drain(vp,50010) && vh.waitedUs==32000); + for(unsigned failure=0;failure<5;++failure){ + IO bad;CommandPort p{bad};Host h(true); + if(failure==0)bad.owned=false; + if(failure==1)bad.missingHeader=true; + if(failure==2)bad.stuckCredits=true; + if(failure==3)bad.ulps=true; + if(failure==4)bad.stuckLP=true; + assert(!(h.send(p,0x05,{&nop,1}) && h.drain(p))); + assert(h.faulted);const auto count=p.writes; + assert(!h.send(p,0x05,{&nop,1}) && p.writes==count); + if(failure<3)assert(!p.writes); + if(failure==2)assert(h.error==HostError::CreditTimeout && bad.delay==100); + if(failure==3)assert(h.error==HostError::Unavailable); + if(failure==4)assert(h.error==HostError::DrainTimeout && bad.delay==40); + } + puts("PASS command MMIO adapter: exact LP packet words, payload before header, credit/drain waits, ownership and sticky failures"); +} diff --git a/tests/dsi_ggtt.py b/tests/dsi_ggtt.py new file mode 100644 index 00000000..5e8507c4 --- /dev/null +++ b/tests/dsi_ggtt.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Exercise shared GGTT preparation against the actual GOP certificate.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[1] +s=(root/'tests/dsi_boot.cpp').read_text() +io=s[s.index('struct IO {'):s.index('Boot capture(')] +code=r'''#include "kern_dsi_ggtt.hpp" +#include +#include +#include +#include +using namespace XELP::DSI; +IO_FIXTURE +struct Probe:IO { + bool loseWrite=false,sourceDrift=false; + void write(uint32_t reg,uint32_t value){assert(reg==0x42084);writes.push_back(reg); + if(!loseWrite)regs[reg]=value;if(sourceDrift)regs[0x6B000]^=1;} +}; +Boot certify(Probe &io){auto boot=captureBoot(io,AlderLakeN::readDisplay(io,true),profile(),io.memory,true); + assert(verifyBootPrefix(io,boot,io.memory));return boot;} +int main(){ + for(bool already:{false,true}){ + Probe io;io.regs[0x42084]=0x20|(already?(1U<<30):0);auto b=certify(io);auto before=io.regs;GGTTMode mode; + assert(prepareGGTT(io,b,io.memory,mode) && mode.ready() && b.complete()); + assert(mode.before==before[0x42084] && mode.after==((1U<<30)|0x20) && mode.writes==!already); + assert(io.writes.size()==!already && bootUnchanged(io,b,io.memory)); + before[0x42084]|=1U<<30;assert(io.regs==before); + assert(!prepareGGTT(io,b,io.memory,mode) && mode.ready() && io.writes.size()==!already); + } + for(auto reg:{0x6B000U,0x70180U,0x7019CU,0x71008U,0x70B08U,0x42084U}){ + Probe io;auto b=certify(io);GGTTMode mode;io.regs[reg]^=reg==0x71008?0x80000000U:1; + assert(!prepareGGTT(io,b,io.memory,mode) && !mode.ready() && io.writes.empty()); + } + for(bool lost:{false,true}){ + Probe io;auto b=certify(io);GGTTMode mode;io.loseWrite=lost;io.sourceDrift=!lost; + assert(!prepareGGTT(io,b,io.memory,mode) && !mode.ready() && !b.usable()); + assert(mode.writes==1 && io.writes==std::vector{0x42084}); + assert(mode.status==(lost?GGTTMode::Status::Readback:GGTTMode::Status::Changed)); + assert(!prepareGGTT(io,b,io.memory,mode) && mode.writes==1); + } + puts("PASS shared DSI GGTT mode: linear GOP preserved, exact one-bit write, already configured, source/peer exclusion, readback/drift failure and no retry"); +} +'''.replace('IO_FIXTURE',io) +with tempfile.TemporaryDirectory(prefix='dsi-ggtt-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_host.cpp b/tests/dsi_host.cpp new file mode 100644 index 00000000..b160a47e --- /dev/null +++ b/tests/dsi_host.cpp @@ -0,0 +1,95 @@ +#include "kern_dsi_host.hpp" +#include "kern_dsi_profile.hpp" +#include +#include +#include +#include +#include +using namespace XELP::DSI; +struct Mock { + std::vector h,p; + uint32_t credit=0x1040,elapsed=0,gpios=0,checks=0,cancelAt=UINT32_MAX; + bool busy=false,available=true,gpioOK=true; + bool cancelled(){return ++checks>=cancelAt;} + bool credits(uint32_t &v){v=credit;return available;} + bool lpBusy(bool &v){v=busy;return available;} + void header(uint32_t v){h.push_back(v);} + void payload(uint32_t v){p.push_back(v);} + void delayUs(uint32_t us){elapsed+=us;} + bool gpio(uint8_t role,uint8_t value){assert(nativePanelGPIO(role)&&value<=1);++gpios;return gpioOK;} +}; +struct VideoMock:Mock { + uint32_t releaseAt=0; + bool credits(uint32_t &v){v=elapsed bytes{std::istreambuf_iterator(f),{}}; + const auto profile=parseProfile({bytes.data(),bytes.size()},true);assert(profile.valid()); + Mock saved;Host replay(true); + for(unsigned id:{10U,1U,2U,6U,7U,4U,5U,11U})assert(replay.run(saved,profile.sequences,id)); + assert(saved.gpios==8&&saved.elapsed==486000&&replay.completedOperations==378); + assert(!replay.pending&&!replay.faulted); + printf("saved sequences mock replay: operations=%u GPIO=%u delay=%u us headers=%zu (includes drain NOPs)\n", + replay.completedOperations,saved.gpios,saved.elapsed,saved.h.size()); + } + puts("DSI packet, credit/drain timeout, cancellation, preflight, first error: PASS"); +} diff --git a/tests/dsi_irq_live.py b/tests/dsi_irq_live.py new file mode 100644 index 00000000..d26c0aef --- /dev/null +++ b/tests/dsi_irq_live.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Exercise the production memory-only DSI IRQ snapshot and lock lifetime.""" +from pathlib import Path +from production_source import read_source +import re, subprocess, tempfile +root=Path(__file__).resolve().parents[1] +s=read_source(root/'WhateverXe/kern_xe.cpp') +parts=[re.search(pattern,s,re.S)[0] for pattern in [ + r'struct Display13FBIRQContext \{.*?\n\};', + r'static void adlnIRQRecord\(.*?\n\}', + r'static bool dsiIRQLiveSerialize\(.*?\n\}']] +code=r''' +#include "kern_display13_fb_irq.hpp" +#include +#include +#include +struct IOSimpleLock {};struct IOService {};struct OSSerialize {}; +bool held=false,failAllocation=false,failSerialize=false;unsigned reads=0,releases=0; +uint64_t output[32]{}; +unsigned IOSimpleLockLockDisableInterrupt(IOSimpleLock *p){assert(p && !held);held=true;++reads;return 7;} +void IOSimpleLockUnlockEnableInterrupt(IOSimpleLock *p,unsigned v){assert(p && held && v==7);held=false;} +struct OSData { + static OSData *withBytes(const void *p,unsigned n){assert(!held && n==sizeof(output));memcpy(output,p,n);static OSData data;return failAllocation?nullptr:&data;} + bool serialize(OSSerialize*){assert(!held);return !failSerialize;} + void release(){assert(!held);++releases;} +}; +CONTEXT +static Display13FBIRQContext display13FBIRQ; +RECORD +SERIALIZE +int main(){ + auto &c=display13FBIRQ;OSSerialize serializer; + assert(!dsiIRQLiveSerialize(nullptr,nullptr,&serializer) && !reads); + IOService controller;c.controller=&controller; + assert(!dsiIRQLiveSerialize(nullptr,nullptr,&serializer) && !reads); + IOSimpleLock lock;c.lock=&lock;c.state.begun=true;c.state.enabled=true; + c.status=Display13FBIRQ::Status::Complete;c.state.active=1;c.state.configured=1; + c.state.interrupts=100;c.state.vblanks[0]=70;c.state.flips[0]=20; + c.state.underruns[0]=2;c.state.dsbErrors[2]=4;c.handlers=1; + auto before=c; + assert(dsiIRQLiveSerialize(nullptr,nullptr,&serializer) && !held && reads==1 && releases==1); + assert(!memcmp(&before,&c,sizeof(c))); + assert(output[0]==1 && output[1]==3 && output[2]==1 && output[4]==1); + assert(output[10]==100 && output[11]==70 && output[14]==20 && output[17]==2 && output[22]==4 && output[30]==1); + c.state.interrupts+=10;c.state.vblanks[0]+=8;c.state.flips[0]+=2; + assert(dsiIRQLiveSerialize(nullptr,nullptr,&serializer)); + assert(output[10]==110 && output[11]==78 && output[14]==22 && output[17]==2); + c.quiescing=true;c.blockedPipes=1;c.softwareHandlers=2;c.state.enabled=false; + assert(dsiIRQLiveSerialize(nullptr,nullptr,&serializer)); + assert(output[4]==0 && output[31]==(1ULL|256|(2ULL<<32))); + failAllocation=true;assert(!dsiIRQLiveSerialize(nullptr,nullptr,&serializer) && !held && releases==3); + failAllocation=false;failSerialize=true;assert(!dsiIRQLiveSerialize(nullptr,nullptr,&serializer) && !held && releases==4); + puts("PASS DSI IRQ live: current counters, read-only state, unlock before allocation/serialization, failure cleanup"); +} +'''.replace('CONTEXT',parts[0]).replace('RECORD',parts[1]).replace('SERIALIZE',parts[2]) +with tempfile.TemporaryDirectory(prefix='dsi-irq-live-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_lifecycle_dispatch.py b/tests/dsi_lifecycle_dispatch.py new file mode 100644 index 00000000..e499b98e --- /dev/null +++ b/tests/dsi_lifecycle_dispatch.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Run production lifecycle entry points; no DSI request may reach donor writes.""" +from pathlib import Path +from production_source import read_source +import re,subprocess,tempfile +root=Path(__file__).resolve().parents[1] +s=read_source(root/'WhateverXe/kern_xe.cpp') +functions='\n'.join(re.search(r'(?:static bool|uint32_t|void) (?:XE::)?'+name+r'\([^;{]*\{.*?\n\}',s,re.S)[0] + for name in ['dsiCachePath','dsiLifecycleTarget','wrapDisplay13TCSetMode','wrapAlderLakeNDisableDisplay']) +code=r''' +#include "kern_display13_fb_memory.hpp" +#include "kern_display13_cursor.hpp" +#include "kern_display13_tc_lightup.hpp" +#include +#include +struct OSObject {alignas(8) uint8_t bytes[0x4B90]{};unsigned size=sizeof(bytes); + OSObject *getMetaClass(){return this;}unsigned getClassSize(){return size;}}; +using IOService=OSObject;using IOLock=int; +bool hostStopOK=false; +unsigned depth=0,bindingCalls=0,modeCalls=0,stopCalls=0,notifications=0,modeResult=0,hostStops=0; +void IOLockLock(IOLock*){assert(!depth);++depth;}void IOLockUnlock(IOLock*){assert(depth==1);--depth;} +void adlnFault(bool &v,unsigned){assert(depth==1);v=true;} +void IOSleep(unsigned){assert(false);} +struct IOMemoryMap {uintptr_t getVirtualAddress(){assert(false);return 0;}size_t getLength(){return 0;}}; +struct {IOLock *lock=nullptr;IOService *controller=nullptr;IOMemoryMap *map=nullptr;bool memoryFaulted=false,clockFaulted=false; + struct {struct {bool faulted=false;}state;}bridge;struct {unsigned updating=0;}dbuf; + struct {IOService *memoryFramebuffer=nullptr;bool wmProgramming=false,wmCommitting=false;}pipe[3]; + Display13Transcoder::Bindings transcoders;}display13FBPower; +namespace TGLFBBinding {enum class Status{Unavailable,Complete};struct Readout{Status status{};unsigned pipe=0;};} +TGLFBBinding::Readout adlnInspectPathBinding(IOService*,OSObject*,void*,unsigned){++bindingCalls;return {};} +bool adlnWriterMappingBound(){return false;} +void adlnPublishMemory(const char*,unsigned,void*,size_t,bool=false){} +IOService *panel=nullptr;bool dsiModeTarget(IOService *fb){return fb && fb==panel;} +constexpr uint32_t kIOReturnNotReady=0xe00002d8; +#define FunctionCast(f,p) reinterpret_cast(p) +uint32_t nativeMode(void*,void*,const void*){++modeCalls;return modeResult;} +void nativeStop(IOService*,IOService*,OSObject*,uint8_t){++stopCalls;} +void notify(void*,void*,uint32_t value){assert(value==1);++notifications;} +struct XE { + struct Wiring{unsigned dsi=0;operator unsigned()const{return dsi;}}adlnOutputWiring;void *adlnPlatform=nullptr; + uintptr_t orgDisplay13TCSetMode=reinterpret_cast(nativeMode),display13TCNotify=reinterpret_cast(notify); + uintptr_t orgAlderLakeNDisableDisplay=reinterpret_cast(nativeStop); + bool quiesceDSIPath(IOService*,IOService*,OSObject*){++hostStops;return hostStopOK;} + static uint32_t wrapDisplay13TCSetMode(void*,void*,const void*); + static void wrapAlderLakeNDisableDisplay(IOService*,IOService*,OSObject*,uint8_t); +}xe,*callbackXE=&xe; +FUNCTIONS +templatevoid put(void *p,unsigned o,T v){memcpy(static_cast(p)+o,&v,sizeof(v));} +int main(){ + IOService controller,fb;OSObject path,other,port;IOLock lock; + auto &c=display13FBPower;c.lock=&lock;c.controller=&controller; + put(&fb,0x4A00,1U);put(&fb,0x4A08,&path);put(&path,0x3648,&port); + const auto reject=[&](IOService *f,OSObject *p){ + const auto m=modeCalls,n=notifications,d=stopCalls,b=bindingCalls,h=hostStops; + assert(XE::wrapDisplay13TCSetMode(&controller,f,nullptr)==kIOReturnNotReady); + assert(modeCalls==m && notifications==n); + c.memoryFaulted=false;XE::wrapAlderLakeNDisableDisplay(&controller,f,p,0); + assert(c.memoryFaulted && stopCalls==d && bindingCalls==b && hostStops==h+1 && !depth); + }; + xe.adlnOutputWiring.dsi=1;reject(&fb,&path); // actual pipe A / port A + hostStopOK=true;c.memoryFaulted=false;XE::wrapAlderLakeNDisableDisplay(&controller,&fb,&path,0); + assert(!c.memoryFaulted && stopCalls==0);hostStopOK=false; + put(&path,0x3640,1U);put(&port,0x120,3U); + panel=&fb;reject(&fb,&path);panel=nullptr; // retained identity survives corrupted path + put(&path,0x3640,3U);reject(&fb,&path); + put(&path,0x3640,1U);put(&port,0x120,0U);reject(&fb,&path); + put(&port,0x120,3U);port.size=0x100;reject(&fb,&path);port.size=sizeof(port.bytes); + put(&path,0x3648,static_cast(nullptr));reject(&fb,&path);put(&path,0x3648,&port); + path.size=0x100;reject(&fb,&path);path.size=sizeof(path.bytes); + put(&fb,0x4A00,0U);reject(&fb,nullptr);put(&fb,0x4A00,2U);reject(&fb,&path); + put(&fb,0x4A00,1U);fb.size=0x100;reject(&fb,&path);fb.size=sizeof(fb.bytes);reject(nullptr,nullptr); + // An otherwise external FB with a mismatched supplied stop path is ambiguous. + c.memoryFaulted=false;const auto stops=stopCalls; + XE::wrapAlderLakeNDisableDisplay(&controller,&fb,&other,1);assert(c.memoryFaulted && stops==stopCalls); + // Qualified external path keeps native mode return/notification behavior. + c.memoryFaulted=false;auto m=modeCalls,n=notifications,d=stopCalls; + assert(!XE::wrapDisplay13TCSetMode(&controller,&fb,nullptr));assert(modeCalls==m+1 && notifications==n); + modeResult=7;assert(XE::wrapDisplay13TCSetMode(&controller,&fb,nullptr)==7);assert(notifications==n+1); + XE::wrapAlderLakeNDisableDisplay(&controller,&fb,&path,0);assert(stopCalls==d+1 && !c.memoryFaulted); + // With DSI disabled no additional native-object interpretation is introduced. + xe.adlnOutputWiring.dsi=0;panel=&fb;modeResult=0; + assert(!XE::wrapDisplay13TCSetMode(nullptr,nullptr,nullptr)); + XE::wrapAlderLakeNDisableDisplay(nullptr,nullptr,nullptr,0);assert(stopCalls==d+2 && !c.memoryFaulted); + puts("PASS production DSI mode/disable dispatch: no donor entry or notifications, fault retention, ambiguous object rejection, external return behavior"); +} +'''.replace('FUNCTIONS',functions) +with tempfile.TemporaryDirectory(prefix='dsi-lifecycle-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_native_backing.cpp b/tests/dsi_native_backing.cpp new file mode 100644 index 00000000..adbf5a5c --- /dev/null +++ b/tests/dsi_native_backing.cpp @@ -0,0 +1,64 @@ +#include "kern_dsi_native_backing.hpp" +#include +#include +#include +using namespace XELP::DSI; +using namespace Display13FBMemory; +template void put(void *p,unsigned off,T v){memcpy(static_cast(p)+off,&v,sizeof(v));} +struct Fixture { + alignas(8) uint8_t controller[0x1C20]{},fb[0x4B90]{}; + Profile profile;NativeBacking::Record record; + XELPMemory::Layout layout=XELPMemory::decode(0x03C1,0x7A800001,0); + uintptr_t owner=0x1000;uint64_t reserve=0x06192000; + unsigned drift=0,reads=0;bool badPage=false; + Fixture(){ + profile.status=ProfileStatus::Valid;profile.mode={135255,1080,46,22,2,1920,82,40,2,86,154}; + assert(NativeBacking::request(profile,3,record)); + record.map=0x2000;record.memory=0x3000;record.gpu=0x10000000;record.pitch=4352;record.refs=1; + put(controller,0xC0,owner);put(controller,0xD5C,1U);put(controller,0xD60,fb); + put(fb,0x1D0,controller);put(controller,0xDF0,1U);put(controller,0xDF4,uint8_t(1));put(controller,0xDF5,uint8_t(3)); + put(controller,0x1A10,1U);put(controller,0x1A28,layout.base); + put(controller,0x1A30,0x800000U);put(controller,0x1A34,0x800U); + } + bool memoryReady(uintptr_t map,uintptr_t memory,uintptr_t o,uint64_t n){ + return map==record.map && memory==record.memory && o==owner && n==8355840; + } + uint64_t physical(uintptr_t,uint64_t off,uint64_t &span){span=4096;return 0x180000000ULL+off;} + uint64_t readPTE(uint32_t reg){ + const uint64_t off=(uint64_t(reg)-0x800000)*512-record.gpu; + assert(off<8355840);++reads; + if(reads==2)switch(drift){ + case 1:put(fb,0x4348,&record);break; + case 2:put(controller,0xDF0,3U);put(controller,0xDF4,uint8_t(2));break; + case 3:put(controller,0x1A30,0x400000U);break; + case 4:record.pitch+=128;break; + case 5:put(controller,0xC0,uintptr_t(0));break; + case 6:put(fb,0x4288,uint8_t(0xFF));break; + } + return (0x180000000ULL+off+(badPage && reads==3?4096:0))|0x99; + } + SurfaceReadout inspect(){return inspectPendingSurface(*this,controller,sizeof(controller),fb,sizeof(fb),owner,reserve,0x100000000ULL,layout,&record);} +}; +int main(){ + Fixture f;const auto proof=f.inspect();assert(proof.status==SurfaceStatus::Complete); + assert(NativeBacking::allocated(f.record,f.profile,proof) && f.reads==2040); + assert(!field(f.fb,0x4348) && allocatedSlot(f.controller,f.fb)==0); + assert(inspectSurface(f,f.controller,sizeof(f.controller),f.fb,sizeof(f.fb),f.owner,f.reserve,0x100000000ULL,f.layout).status==SurfaceStatus::Unavailable); + for(unsigned drift=1;drift<=6;++drift){Fixture changed;changed.drift=drift;assert(changed.inspect().status==SurfaceStatus::StateChanged);} + for(unsigned fault=0;fault<6;++fault){Fixture bad; + switch(fault){case 0:put(bad.fb,0x4348,&bad.record);break;case 1:put(bad.fb,0x4288,uint8_t(1));break; + case 2:put(bad.controller,0x1A28,uint64_t(0));break;case 3:bad.record.gpu=0;break; + case 4:bad.record.gpu=0x100000000ULL-4096;break;case 5:bad.badPage=true;break;} + assert(bad.inspect().status!=SurfaceStatus::Complete); + } + // A validated record is transferred only in exactly the requested shape. + for(unsigned fault=0;fault<5;++fault){auto r=f.record; + switch(fault){case 0:r.refs=2;break;case 1:r.flags=4;break;case 2:r.kind=2;break;case 3:r.bytes=8355840;break;case 4:r.rows=61;break;} + auto p=proof;p.record=reinterpret_cast(&r);assert(!NativeBacking::allocated(r,f.profile,p)); + } + NativeBacking::Record request; + assert(NativeBacking::request(f.profile,3,request) && request.columns==34 && request.rows==60 && !request.bytes); + const auto previous=request;assert(!NativeBacking::request(f.profile,2,request) && !memcmp(&request,&previous,sizeof(request))); + f.profile.mode.height=0;assert(!NativeBacking::request(f.profile,3,request)); + puts("PASS pending native DSI backing: GOP slot preserved, exact Y request, PTE proof and publication/ownership drift"); +} diff --git a/tests/dsi_native_cache.py b/tests/dsi_native_cache.py new file mode 100644 index 00000000..48fe35f1 --- /dev/null +++ b/tests/dsi_native_cache.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Execute DSI cache hooks with real binding/boot helpers and bounded fake objects.""" +from pathlib import Path +from production_source import read_source +import hashlib,re,subprocess,sys,tempfile +root=Path(__file__).resolve().parents[1] +s=read_source(root/'WhateverXe/kern_xe.cpp') +names=['updateDSIBootCache','wrapDSIUpdateCache','wrapDSIGetCRTC'] +functions='\n'.join(re.search(r'(?:bool|void) XE::'+n+r'\([^;{]*\{.*?\n\}',s,re.S)[0] for n in names) +functions=re.search(r'static bool dsiCachePath\([^;{]*\{.*?\n\}',s,re.S)[0]+'\n'+functions +binding=re.search(r'static TGLFBBinding::Readout adlnInspectPathBinding\([^;{]*\{.*?\n\}',s,re.S)[0] +boot=(root/'tests/dsi_boot.cpp').read_text();io=boot[boot.index('struct IO {'):boot.index('Boot capture(')] +# Non-source cache-word drift must be caught by the two snapshots as well. +io=io.replace('bool driftOnWrite=false,strict=false;','bool driftOnWrite=false,strict=false,cacheDrift=false;unsigned cacheReads=0;') +io=io.replace('reads.push_back(reg);return regs[reg];','reads.push_back(reg);if(cacheDrift && reg==0x701CC && ++cacheReads==2)++regs[reg];return regs[reg];') +if len(sys.argv)>1: + b=Path(sys.argv[1]).read_bytes();assert hashlib.sha256(b).hexdigest()=='c004fc1ac9ee4157e2e12736f40fa6639bde025a279326beeb9dd48c9b445731' + install=re.search(r'bool XE::processDisplay13DSICache\([^;{]*\{.*?\n\}',s,re.S)[0] + offsets=[int(x,0) for x in re.search(r'offsets\[\]=\{([^}]+)',install)[1].split(',')] + entries=re.search(r'entries\[\]\[16\]=\{(.*?)\};',install,re.S)[1] + for offset,row in zip(offsets,re.findall(r'\{([^}]+)\}',entries)): + assert b[offset:offset+16]==bytes(int(x,0) for x in row.split(',')) + # Native path+34 is pipe CSC mode, not DDI_BUF_CTL. Scaler tables use -10/-C. + assert b[0x91315:0x9131e]==bytes.fromhex('8d9080a40400895734') + import struct + assert struct.unpack_from('<2I',b,0x99f30)==(0x68180,0x68280) + assert struct.unpack_from('<2I',b,0x99f48)==(0x68170,0x68270) + assert struct.unpack_from('<2I',b,0x99f60)==(0x68174,0x68274) +code=r''' +#include "kern_dsi_native_cache.hpp" +#include "kern_tgl_fb_binding.hpp" +#include "kern_display13_power_bridge.hpp" +#include "kern_display13_transcoder.hpp" +#include +#include +#include +#include +using namespace XELP::DSI; +IO_FIXTURE +struct OSObject {alignas(8) uint8_t bytes[0x4B90]{};unsigned size=sizeof(bytes); + OSObject *getMetaClass(){return this;}unsigned getClassSize(){return size;}}; +using IOService=OSObject; +unsigned level=0,faults=0,nativeCache=0,nativeCRTC=0; +struct IOLock{};struct IORecursiveLock{}; +void IOLockLock(IOLock*){assert(level==0 || level==1);level+=2;}void IOLockUnlock(IOLock*){assert(level>=2);level-=2;} +void adlnFault(bool &flag,unsigned){assert(level>=2);flag=true;++faults;} +struct DSIModeScope {IORecursiveLock *lock;explicit DSIModeScope(IOService *fb):lock(Display13FBMemory::field(fb,0x4220)){assert(level==0);if(lock)level=1;} + ~DSIModeScope(){assert(level==(lock?1U:0U));level=0;}}; +struct Display13FBBootIO {IO *map;bool readable(uint32_t r){return map && map->readable(r);}uint32_t read(uint32_t r){return map->read(r);}}; +struct {IOLock *lock;IOService *controller;OSObject *access,*dsiPort;IO *map; + Display13Transcoder::Bindings transcoders;Readout bootDSI;Display13Power::NativeBridge bridge; + bool memoryFaulted=false,clockFaulted=false;} display13FBPower; +uintptr_t display13FBExpectedOwner(IOService*){return 0;} +uintptr_t dsiLogicalSurface(const void*){return 0;} +BINDING +XELPMemory::Layout xelpMemory;bool display13Offline=false; +void originalCache(IOService*,IOService*,OSObject*){++nativeCache;} +void originalCRTC(IOService*,IOService*,OSObject*){++nativeCRTC;} +#define FunctionCast(f,p) reinterpret_cast(p) +bool publishedBacking=false;bool dsiPublishedTarget(IOService*){return publishedBacking;} +struct XE { + bool dsiCacheHooksReady=true,dsiResourcesReady=true;IOService *adlnTopologyController; + Display13OutputWiring adlnOutputWiring;alignas(8) uint8_t adlnPlatform[0x1C0]{}; + uintptr_t orgDSICache[2]{reinterpret_cast(originalCache),reinterpret_cast(originalCRTC)};Boot dsiBoot; + bool updateDSIBootCache(IOService *,IOService *,OSObject *); + static void wrapDSIUpdateCache(IOService *,IOService *,OSObject *); + static void wrapDSIGetCRTC(IOService *,IOService *,OSObject *); +} xe,*callbackXE=&xe; +FUNCTIONS +templatevoid put(void *p,unsigned off,T value){memcpy(static_cast(p)+off,&value,sizeof(value));} +int main(){ + IOLock lock;IORecursiveLock fbLock;IO io;IOService controller,fb;OSObject path,plane,port,scaler,access; + auto &c=display13FBPower;c.lock=&lock;c.map=&io;c.controller=xe.adlnTopologyController=&controller; + c.access=&access;c.dsiPort=&port;c.transcoders.dsi0Pipe=0; + xe.adlnOutputWiring.valid=true;xe.adlnOutputWiring.dsi=1;xe.adlnOutputWiring.hdmi=2;xe.adlnOutputWiring.ddc[1]=2; + xe.adlnPlatform[0x12]=9;uint32_t panel[]={0,0,0,0,2,0x18};memcpy(xe.adlnPlatform+0x20,panel,sizeof(panel)); + uint32_t hdmi[]={1,2,2,0,0x800,0x202};memcpy(xe.adlnPlatform+0x38,hdmi,sizeof(hdmi)); + put(&port,0x548,reinterpret_cast(xe.adlnPlatform+0x20));put(&port,0x550,1U); + put(&controller,0xD5C,1U);put(&controller,0xD60,&fb);put(&controller,0xC40,&access);put(&controller,0xF58,&plane); + put(&fb,0x1D0,&controller);put(&fb,0x4A40,&access);put(&fb,0x4220,&fbLock); + put(&path,0x32B0,&fb);put(&path,0x32C8,&plane);put(&path,0x3648,&port);put(&path,0x32C0,&scaler);put(&path,0x34,0x4A480U); + put(&plane,0x68,&fb);put(&plane,0x70,&path);put(&plane,0x90,&access);put(&plane,0xA0,0x7027CU);put(&plane,0xA4,0x7017CU); + for(unsigned i=0;i<6;++i)put(&plane,NativeCache::PlaneAddressFields[i],NativeCache::PlaneRegisters[i]); + put(&scaler,0x28,&access);put(&scaler,0x30,0x68180U);put(&scaler,0x34,0x68170U);put(&scaler,0x38,0x68174U); + xelpMemory=io.memory;xe.dsiBoot=captureBoot(io,AlderLakeN::readDisplay(io,true),profile(),io.memory,true); + assert(verifyBootPrefix(io,xe.dsiBoot,io.memory));c.bootDSI=xe.dsiBoot.source; + io.regs[0x45454]=3;io.regs[0x42000]=0x8E380000; + io.regs[0x6B010]=c.bootDSI.vtotal;io.regs[0x6001C]=0x0437077F;io.regs[0x70030]=0x20; + io.regs[0x4A480]=0x1234;io.regs[0x701CC]=0x5000;io.regs[0x68170]=0x11;io.regs[0x68174]=0x22; + auto &b=c.bridge;auto &s=b.state;b.begun=true;b.selectedPorts=3;s.ready=true;s.allowed=Display13Power::All; + const auto pg=Display13Power::dependencies(Display13Power::nativeScope(1)); + const auto ddi=Display13Power::dependencies(Display13Power::ddiDomain(0));s.borrowed=pg|ddi; + for(unsigned i=0;i(&path,0x138)==c.bootDSI.htotal && Display13FBMemory::field(&path,0x144)==c.bootDSI.vtotal); + assert(!Display13FBMemory::field(&path,0x13C) && !Display13FBMemory::field(&path,0x12C) && !Display13FBMemory::field(&path,0x168)); + assert(Display13FBMemory::field(&path,0x18C)==0x1234 && Display13FBMemory::field(&plane,0x104)==0x5001); + assert(Display13FBMemory::field(&scaler,0x48)==0x11 && Display13FBMemory::field(&scaler,0x4C)==0x22); + XE::wrapDSIGetCRTC(&controller,&fb,&path);assert(!nativeCache && !nativeCRTC && !faults); + auto originalRegs=io.regs;publishedBacking=true; + io.regs[0x7019C]=0x40400000;io.regs[0x70180]=0x94001008;io.regs[0x70188]=34;io.regs[0x70080]=0x27; + assert(xe.updateDSIBootCache(&controller,&fb,&path)); + assert(Display13FBMemory::field(&plane,0x120)==0x40400000 && Display13FBMemory::field(&path,0x170)==0x27); + io.regs[0x6B000]^=1;unchanged();io.regs=originalRegs;publishedBacking=false; + assert(xe.updateDSIBootCache(&controller,&fb,&path)); + b.ddiRefs[0]=0;XE::wrapDSIGetCRTC(&controller,&fb,&path);assert(faults==1 && c.memoryFaulted);b.ddiRefs[0]=1;c.memoryFaulted=false; + put(&path,0x3640,1U);put(&port,0x120,1U);XE::wrapDSIGetCRTC(&controller,&fb,&path);XE::wrapDSIUpdateCache(&controller,&fb,&path); + assert(nativeCRTC==1 && nativeCache==1); + put(&path,0x3640,3U);XE::wrapDSIGetCRTC(&controller,&fb,&path); + assert(nativeCRTC==1 && c.memoryFaulted && faults==2);c.memoryFaulted=false; + XE::wrapDSIUpdateCache(&controller,&fb,nullptr);assert(nativeCache==1 && c.memoryFaulted && faults==3); + xe.adlnOutputWiring.dsi=0; + XE::wrapDSIGetCRTC(&controller,&fb,nullptr);assert(nativeCRTC==2); + assert(io.writes.empty() && !level);for(auto reg:io.reads)assert(reg<0xC7200 || reg>0xC8258); + puts("PASS production DSI cache: native object binding, split timing/pipe fields, coherent snapshots, no partial publication, no PWM, failure latch and external fallback"); +} +'''.replace('IO_FIXTURE',io).replace('BINDING',binding).replace('FUNCTIONS',functions) +with tempfile.TemporaryDirectory(prefix='dsi-native-cache-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_native_mode.py b/tests/dsi_native_mode.py new file mode 100644 index 00000000..cae71166 --- /dev/null +++ b/tests/dsi_native_mode.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +"""Check geometry rollback and optionally replay the exact donor CPU helpers.""" +from pathlib import Path +import hashlib, subprocess, sys, tempfile +root=Path(__file__).resolve().parents[1] +args=[] +if len(sys.argv)>1: + donor=Path(sys.argv[1]).resolve() + assert hashlib.sha256(donor.read_bytes()).hexdigest()=='c004fc1ac9ee4157e2e12736f40fa6639bde025a279326beeb9dd48c9b445731' + args=[str(donor)] +with tempfile.TemporaryDirectory(prefix='dsi-mode-') as directory: + executable=Path(directory)/'test' + subprocess.run(['clang++','-arch','x86_64','-std=c++17','-O1','-I'+str(root/'WhateverXe'), + str(root/'tests/dsi_native_mode_fixture.cpp'),'-o',str(executable)],check=True) + subprocess.run([str(executable),*args],check=True) diff --git a/tests/dsi_native_mode_fixture.cpp b/tests/dsi_native_mode_fixture.cpp new file mode 100644 index 00000000..bb09afa9 --- /dev/null +++ b/tests/dsi_native_mode_fixture.cpp @@ -0,0 +1,172 @@ +// Execute the real donor CPU helper when its exact binary is supplied. +#include "kern_dsi_native_mode.hpp" +#include "kern_dsi_native_backing.hpp" +#include +#include +#include +#include +#include +#include +#include +using namespace XELP::DSI; +using Display13FBMemory::field; +templatevoid put(void *p,unsigned offset,T value){memcpy(static_cast(p)+offset,&value,sizeof(value));} +unsigned notifications=0; +bool noPanelFitter(void *,uint32_t,uint32_t,uint32_t,uint32_t,uint64_t,uint32_t,bool){return false;} +void notify(void *){++notifications;} +void zero(void *p,size_t n){memset(p,0,n);} +unsigned logicalAllocations=0,logicalFrees=0,recordAllocations=0,recordFrees=0; +uint32_t logicalResult=0; +bool failRecord=false; +void *allocateRecord(size_t bytes) { + assert(bytes==64);if(failRecord)return nullptr; + ++recordAllocations;return malloc(bytes); +} +void freeRecord(void *p,size_t bytes) {assert(p && bytes==64);++recordFrees;free(p);} +uint32_t logicalCallback(void *,const void *selector,void *,void *raw,void *,void *,void *) { + auto &r=*static_cast(raw); + if(selector==reinterpret_cast(1)) { + ++logicalAllocations;assert(r.kind==2 && r.columns && r.rows); + if(logicalResult)return logicalResult; + r.pitch=r.columns*128;r.gpu=0x100000;r.map=0x200000;r.memory=0x300000; + } else {assert(selector==reinterpret_cast(2));++logicalFrees;} + return 0; +} +void patchCall(uint8_t *image,unsigned call,uintptr_t function) { + static unsigned nextStub=0x200000; + const unsigned stub=nextStub;nextStub+=16;assert(nextStub<0x201000); + image[stub]=0x48;image[stub+1]=0xB8; + memcpy(image+stub+2,&function,8);image[stub+10]=0xFF;image[stub+11]=0xE0; + const int32_t delta=stub-call-5;assert(image[call]==0xE8);memcpy(image+call+1,&delta,4); +} +int main(int argc,char **argv) { + alignas(8) uint8_t fb[0x4B90]{},controller[0x1C20]{},path[0x3730]{}; + put(fb,0x1D0,controller);put(fb,0x4A00,1U);put(fb,0x4A08,path); + Profile p;p.status=ProfileStatus::Valid;p.mode={135255,1080,46,22,2,1920,82,40,2,86,154}; + AppleAdapter::Timing t;assert(AppleAdapter::makeTiming(p,t)); + unsigned calls=0; + auto unsupported=[&](void *f,const AppleAdapter::Timing*) { + ++calls;memset(static_cast(f)+0x430C,0x55,0x30);memset(path+0xF0,0x33,0x20); + }; + // The rejected helper must leave its complete CPU geometry unchanged. + put(fb,0x4350,8U);put(path,0xF8,640U); + const std::vector savedFB(fb,fb+sizeof(fb)),savedPath(path,path+sizeof(path)); + assert(!AppleAdapter::prepareGeometry(fb,path,p,t,unsupported));assert(calls==1); + assert(!memcmp(fb,savedFB.data(),sizeof(fb)) && !memcmp(path,savedPath.data(),sizeof(path))); + auto scaled=t;scaled.horizontalScaled=720; + assert(!AppleAdapter::prepareGeometry(fb,path,p,scaled,unsupported));assert(calls==1); + put(fb,0x4348,uintptr_t(1));assert(!AppleAdapter::prepareGeometry(fb,path,p,t,unsupported));assert(calls==1); + put(fb,0x4348,uintptr_t(0));put(fb,0x4A00,2U); + assert(!AppleAdapter::prepareGeometry(fb,path,p,t,unsupported));assert(calls==1);put(fb,0x4A00,1U); + if(argc==2) { + std::ifstream f(argv[1],std::ios::binary);std::vector bytes((std::istreambuf_iterator(f)),{}); + assert(bytes.size()<0x400000); + auto image=static_cast(mmap(nullptr,0x400000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0)); + assert(image!=MAP_FAILED);memcpy(image,bytes.data(),bytes.size()); + patchCall(image,0x3B841,reinterpret_cast(zero)); + uintptr_t vtable[0x220/8]{};vtable[0x218/8]=reinterpret_cast(notify); + auto object=vtable;put(controller,0xC10,&object);put(controller,0x1BB4,uint8_t(1)); + put(controller,0xD5C,1U);put(controller,0xD60,fb);put(fb,0x1E0,uint8_t(1)); + alignas(8) uint8_t port[0x580]{},config[0x30]{}; + put(path,0x3648,port);put(port,0x548,config); + const auto compute=reinterpret_cast(image+0x31484); + for(int joined:{-1,0})for(bool owner:{false,true}) { + put(fb,0x3FD8,joined);put(fb,0x49E8,1U);put(fb,0x49F0,path); + put(controller,0xC0,owner?uintptr_t(1):uintptr_t(0)); + assert(AppleAdapter::prepareGeometry(fb,path,p,t,compute)); + assert(field(fb,0x4334)==4352 && field(fb,0x4314)==8355840); + assert(!field(fb,0x4348) && !field(fb,0x4340)); + // Exact unscaled logical-buffer path must be a successful no-op. + assert(reinterpret_cast(image+0x31726)(fb)==0); + assert(!field(fb,0x4340)); + reinterpret_cast(image+0x318E8)(fb,0); + assert(!field(fb,0x4000) && field(fb,0x4008)==UINT32_MAX && + field(fb,0x400C)==UINT32_MAX && field(fb,0x4200)==1); + reinterpret_cast(image+0x319D2)(fb); + assert(field(fb,0x402C)>=1 && field(fb,0x402C)<=2); + + } + // setupClocks' CPU assignment at 0x6C719 reads Timing+0x28 and + // stores path+0x118. Replay both WM consumers without a DDI modeset. + const uint8_t cacheCopy[]={0x49,0x8B,0x77,0x28,0x48,0x89,0xB3,0x18,0x01,0x00,0x00}; + assert(!memcmp(image+0x6C719,cacheCopy,sizeof(cacheCopy))); + alignas(8) uint8_t scaler[0x80]{},plane[0x1C8]{}; + put(path,0x32C0,scaler);put(path,0x32C8,plane); + put(scaler,0x4C,uint32_t(p.mode.width<<16 | p.mode.height)); + put(plane,0x88,uintptr_t(0));put(path,0x118,t.pixelClock); + const auto adjust=reinterpret_cast(image+0x4ACE4); + assert(adjust(controller,fb,path,false,nullptr)==135255000); + assert(adjust(controller,fb,path,true,nullptr)==135255000); + put(scaler,0x4C,0U); // Disabled boot scaler also leaves Hz unchanged. + assert(adjust(controller,fb,path,false,nullptr)==135255000); + assert(notifications==4); + // Quarter-turns use a separate GPU logical buffer, not a rotated + // hardware scanout. Exercise the exact geometry/allocation/release + // bodies; only kernel heap and accelerator allocation are mocked. + patchCall(image,0x31790,reinterpret_cast(allocateRecord)); + patchCall(image,0x317A5,reinterpret_cast(zero)); + patchCall(image,0x330E3,reinterpret_cast(freeRecord)); + uintptr_t gpuVtable[0x6C0/8]{};gpuVtable[0x6B8/8]=reinterpret_cast(logicalCallback); + auto gpuObject=gpuVtable;put(controller,0xC0,&gpuObject); + put(fb,0x420C,2U);put(fb,0x4AD8,uintptr_t(1));put(fb,0x4AE0,uintptr_t(2)); + put(scaler,0x10,fb); + const auto allocateLogical=reinterpret_cast(image+0x31726); + const auto releaseLogical=reinterpret_cast(image+0x33062); + for(uint32_t rotation:{0x30U,0x50U})for(bool hidpi:{false,true}) { + auto presentation=t;presentation.scalerFlags=rotation; + presentation.horizontalScaled=1920;presentation.verticalScaled=1080; + if(hidpi){presentation.horizontalScaled=2880;presentation.verticalScaled=1620;} + put(fb,0x4348,uintptr_t(0x12345000)); + assert(AppleAdapter::prepareGeometry(fb,path,p,presentation,compute,true)); + assert(field(fb,0x430C)==(hidpi?2880U:1920U)); + assert(field(fb,0x4310)==(hidpi?1620U:1080U)); + for(unsigned offset:{0x4320U,0x4328U})assert(field(fb,offset)==1080); + for(unsigned offset:{0x4324U,0x432CU})assert(field(fb,offset)==1920); + assert(field(fb,0x4338)==0x30U); + assert(field(fb,0x4334)==(hidpi?11520U:7680U)); + assert(field(path,0xF8)==1080 && field(path,0xFC)==1920); + assert(allocateLogical(fb)==0); + auto record=field(fb,0x4340); + assert(record && record->pitch==(hidpi?11520U:7680U)); + assert(record->bytes==record->pitch*(hidpi?1620U:1080U)); + assert(field(fb,0x4348)==0x12345000); // Logical allocation preserves the scanout. + releaseLogical(fb);assert(!field(fb,0x4340)); + } + // Replay the native GPU fallback for a portrait HiDPI source. Only + // hardware-scaler admission is declined, as for the adopted DSI owner. + patchCall(image,0x32909,reinterpret_cast(noPanelFitter)); + auto portrait=t;portrait.horizontalScaled=1620;portrait.verticalScaled=2880; + assert(AppleAdapter::prepareGeometry(fb,path,p,portrait,compute,true)); + assert(field(fb,0x4338)==0x20 && field(fb,0x4334)==6528); + assert(field(path,0xF8)==1080 && field(path,0xFC)==1920); + assert(allocateLogical(fb)==0); + auto portraitRecord=field(fb,0x4340); + assert(portraitRecord && portraitRecord->bytes==6528U*2880); + releaseLogical(fb);assert(!field(fb,0x4340)); + // A failing accelerator callback still publishes the heap record. + // A future mode adapter must explicitly retire it before rollback. + logicalResult=0xE00002BD;assert(allocateLogical(fb)==logicalResult); + assert(field(fb,0x4340));releaseLogical(fb); + logicalResult=0;failRecord=true;assert(allocateLogical(fb)==0xE00002BD); + assert(!field(fb,0x4340)); + assert(logicalAllocations==6 && logicalFrees==6 && recordAllocations==6 && recordFrees==6); + // Different render sizes and all ordinary rotations use the same + // donor contract. No preferred HiDPI size is an admission condition. + failRecord=false; + for(uint32_t rotation:{0U,0x30U,0x40U,0x50U})for(unsigned width:{1280U,2560U,3200U}) { + auto other=t;other.scalerFlags=rotation; + const auto height=width*9/16; + other.horizontalScaled=(rotation&0x10)?width:height; + other.verticalScaled=(rotation&0x10)?height:width; + assert(AppleAdapter::prepareGeometry(fb,path,p,other,compute,true)); + assert(allocateLogical(fb)==0); + auto record=field(fb,0x4340); + assert(record && record->bytes==uint64_t(record->pitch)*other.verticalScaled); + assert(field(fb,0x4348)==0x12345000); + releaseLogical(fb);assert(!field(fb,0x4340)); + } + assert(logicalAllocations==18 && logicalFrees==18 && recordAllocations==18 && recordFrees==18); + munmap(image,0x400000);puts("DSI native geometry: fixed scanout, quarter-turn logical buffers, HiDPI and failure cleanup replay passed"); + } + puts("DSI native mode preparation rollback and admission passed"); +} diff --git a/tests/dsi_panel.py b/tests/dsi_panel.py new file mode 100644 index 00000000..e9a39b34 --- /dev/null +++ b/tests/dsi_panel.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Panel cycle ordering, GPIO side effects and bounded failure; optional saved VBT.""" +from pathlib import Path +import subprocess, tempfile, sys +root=Path(__file__).resolve().parents[1] +boot=(root/'tests/dsi_boot.cpp').read_text() +io=boot[boot.index('struct IO {'):boot.index('Boot capture(')] +profile=(root/'tests/dsi_profile.cpp').read_text() +fixture=profile[profile.index('using Bytes='):profile.index('int main(')] +code=r''' +#include "kern_dsi_panel.hpp" +#include "kern_dsi_clock.hpp" +#include "kern_xelp_memory_layout.hpp" +#include +#include +#include +#include +#include +using namespace XELP::DSI; +IO_FIXTURE +PROFILE_FIXTURE +struct Sim:IO { + uint32_t expectedDuty=UINT32_MAX; + bool owned=true,freezeFrame=false,stuckStop=false,stuckStart=false,stuckCommand=false; + uint32_t drop=UINT32_MAX,elapsed=0,headers=0,payload=0,videoCommandDelay=0,creditRelease=0;unsigned irqDepth=0; + std::vector packets,gpioTrace; + bool ready(){return owned && (regs[0x45404]&0x403)==0x403;} + uint32_t read(uint32_t reg){ + if(reg==0x6B0D0 && stuckCommand && headers)return 0; + if(reg==0x6B0D0 && elapsed=1000 && (conf&0xC0000000U)==0xC0000000U){++regs[0x70040];regs[0x701AC]=regs[0x7019C];} + } + bool gpio(unsigned role,unsigned value){ + if(role==2 && value && expectedDuty!=UINT32_MAX)assert(regs[Backlight::Duty]==expectedDuty); + ++irqDepth;const auto r=Panel::gpio(*this,role,value);--irqDepth; + if(r.complete)gpioTrace.push_back(role*2+value);return r.complete; + } + Sim(){regs[0x42084]=1U<<30;regs[0x6B0D0]=0x1040;regs[0x6001C]=0x0437077F; + regs[0x6B010]=regs[0x6B00C];regs[0xC4030]=0xA7;regs[0xC7204]=0xABCD0015; + regs[0x71180]=0x94000000;regs[0x64100]=0x80000000;} +}; +struct Fixture { + Sim io;Bytes vbt;Profile p;Readout link;Panel::Result result; + std::vector seq=std::vector(12); + Fixture(){vbt=fixture();p=parse(vbt); + seq[1]={3,3,0,0,1,0};seq[2]={1,9,0,0x39,5,0,0xB0,1,2,3,4,1,5,0,5,1,0,0x11,0}; + seq[4]={1,5,0,5,1,0,0x10,1,5,0,5,1,0,0x28,0};seq[5]={3,3,0,0,0,0}; + seq[6]={3,3,0,2,1,0};seq[7]={3,3,0,2,0,0};seq[10]={3,3,0,1,1,0};seq[11]={3,3,0,1,0,0}; + for(unsigned id:{1U,2U,4U,5U,6U,7U,10U,11U})p.sequences.sequence[id]={seq[id].data(),seq[id].size()}; + link=decodeReadout([&](uint32_t r,uint32_t &v){v=io.read(r);return true;},38400);assert(link.matches(p)); + } + void run(){Panel::cycle(io,link,p,0x94000008,0,result);} +}; +void stop(Fixture &f,bool success=true) { + Panel::Shutdown panel; + const auto host=Quiesce::run(f.io,f.link,f.p,panel); + if(!success){assert(!host.complete() && !panel.complete);return;} + assert(host.complete() && panel.complete && !panel.host.pending && !panel.host.faulted); + assert(f.io.gpioTrace==std::vector({4,0,2})); + assert(!(f.io.regs[0x70180]&(1U<<31)) && !f.io.regs[0x7B008] && !f.io.regs[0x6B0D8]); + XELPComboClock::ClockAllocation allocation;allocation.external[0]=123;allocation.binding[0]=2; + assert(Clock::retire(f.io,f.link,host,true,allocation,123).complete()); + assert(!allocation.external[0] && !allocation.binding[0] && !f.io.regs[0x46014]); + assert(f.io.regs[0x71180]==0x94000000 && f.io.regs[0x64100]==0x80000000); +} +int main(int argc,char **argv){ + for(bool stalled:{false,true}) { + Fixture f;f.io.videoCommandDelay=stalled?200000:1000; + f.seq[3]={1,5,0,5,1,0,0x11,2,4,0xA0,0x86,1,0,1,5,0,5,1,0,0x29,2,4,0xA0,0x86,1,0,0}; + f.p.sequences.sequence[3]={f.seq[3].data(),f.seq[3].size()}; + f.run();assert(f.result.complete()==!stalled); + if(stalled)assert(f.result.stage==Panel::Stage::TranscoderOn && + f.result.host.error==HostError::CreditTimeout && f.io.gpioTrace.back()!=5); + else assert(!f.result.host.pending && f.result.host.sequenceId==6); + } + for(unsigned role:{0U,1U,2U,5U,6U,7U}) { + Fixture f;f.io.regs[0xC7304]=0xABCD0015; + const auto before=f.io.regs;assert(f.io.gpio(role,0));assert(f.io.gpio(role,1)); + const auto reg=role%5==0?0xC4030U:0xC7204U+0x100*(role/5); + const auto mask=role%5==0?12U<<(4*(role/5)):role%5==1?1U:4U; + assert((f.io.regs[reg]&~mask)==(before.at(reg)&~mask)); + for(auto item:before)if(item.first!=reg)assert(f.io.regs[item.first]==item.second); + } + {Fixture f;f.p.sequences.sequence[1]={};f.p.sequences.sequence[5]={}; + f.seq[6][3]=7;f.seq[7][3]=7;f.io.regs[0xC7304]=4; + f.run();assert(f.result.complete()); + assert(f.io.gpioTrace==std::vector({14,2,3,15})); + assert(f.io.regs[0xC7304]==4 && f.io.regs[0xC4030]==0xA7); + } + // Every lifecycle slot can be absent, including a panel with all operations + // folded outside these slots. Hardware stop/start and frame checks still run. + for(unsigned mask=0;mask<512;++mask) { + Fixture f;const unsigned ids[]={1,2,3,4,5,6,7,10,11}; + for(unsigned i=0;i<9;++i)if(mask&(1U<({4}));} + {Fixture f;f.p.sequences.sequence[11]=f.p.sequences.sequence[4];stop(f,false);assert(f.io.writes.empty());} + + {Fixture f;f.run();assert(f.result.complete()); + assert(f.io.gpioTrace==std::vector({4,0,2,3,1,5})); + assert(f.io.packets==std::vector({0x40001005,0x40002805,0x40000005,0xC0000539,0x40001105,0x40000005})); + assert(f.io.payload==2 && f.io.regs[0xC4030]==0xA7 && f.io.regs[0xC7204]==0xABCD0015); + assert(f.io.regs[0x70180]==0x94000008 && f.io.regs[0x7B008]==0xC0000000); + assert(f.io.regs[0x71180]==0x94000000 && f.io.regs[0x64100]==0x80000000 && f.io.regs[0x46014]==0xCC000000); + const auto writes=f.io.writes.size();f.run();assert(f.io.writes.size()==writes); + for(auto reg:f.io.writes)assert(reg==0xC4030 || reg==0xC7204 || reg==0x70180 || reg==0x7019C || reg==0x7B008 || reg==0x6B100 || reg==0x6B104); + } + {Fixture f;f.p.sequences.sequence[2].size-=1;f.run();assert(!f.result.complete() && f.io.writes.empty());} + {Fixture f;f.io.owned=false;f.run();assert(!f.result.complete() && f.io.writes.empty());} + for(unsigned fault=0;fault<6;++fault){Fixture f; + if(fault==0)f.io.freezeFrame=true; + if(fault==1)f.io.stuckStop=true; + if(fault==2)f.io.stuckCommand=true; + if(fault==3)f.io.drop=0xC4030; + if(fault==4)f.io.drop=0xC7204; + if(fault==5)f.io.stuckStart=true; + f.run();assert(!f.result.complete()); + assert(f.io.elapsed<1000000); // bounded failure, no repeated power cycles + assert(f.io.regs[0x46014]==0xCC000000); // retain ownership/clock on failure + const auto writes=f.io.writes.size();f.run();assert(f.io.writes.size()==writes); + if(fault<3)assert(f.io.gpioTrace==std::vector{4}); // no reset after stop/drain failure + if(fault==5)assert(f.result.stage==Panel::Stage::TranscoderOn && !(f.io.regs[0x70180]&(1U<<31))); + } + if(argc==2){Fixture f;std::ifstream in(argv[1],std::ios::binary);assert(in); + f.vbt=Bytes{std::istreambuf_iterator(in),{}};f.p=parseProfile({f.vbt.data(),f.vbt.size()},true);assert(f.p.valid()); + {Fixture stopped;stopped.vbt=f.vbt;stopped.p=parseProfile({stopped.vbt.data(),stopped.vbt.size()},true);stop(stopped);} + f.run();assert(f.result.complete() && f.result.host.completedOperations==f.p.sequences.packets+f.p.sequences.delays+f.p.sequences.gpios); + assert(f.io.gpioTrace==std::vector({4,0,2,3,1,0,1,5})); + printf("saved VBT replay: packets=%u payloadWords=%u operations=%u gpio=%zu delayUs=%llu; simulation only\n",f.result.host.headers,f.result.host.payloadWords,f.result.host.completedOperations,f.io.gpioTrace.size(),static_cast(f.result.host.waitedUs)); + } + puts("PASS panel cycle: packet/GPIO order, retained host, restart after drain, HPD W1C preservation, bounded failures and no retry"); +} +'''.replace('IO_FIXTURE',io).replace('PROFILE_FIXTURE',fixture) +with tempfile.TemporaryDirectory(prefix='dsi-panel-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test'),*sys.argv[1:]],check=True) diff --git a/tests/dsi_path_disposal.py b/tests/dsi_path_disposal.py new file mode 100644 index 00000000..de481246 --- /dev/null +++ b/tests/dsi_path_disposal.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Exercise production DSI PG/path disposal, optionally replay exact CPU leaves.""" +from pathlib import Path +from production_source import read_source +import hashlib,re,subprocess,sys,tempfile +root=Path(__file__).resolve().parents[1] +s=read_source(root/'WhateverXe/kern_xe.cpp') +body=re.search(r'bool XE::disposeDSIPath\(.*?\n\}',s,re.S)[0] +code=r''' +#include "kern_dsi_quiesce.hpp" +#include "kern_dsi_clock.hpp" +#include "kern_display13_fb_memory.hpp" +#include "kern_display13_power_bridge.hpp" +#include +#include +#include +#include +#include +#include +#include +using namespace XELP::DSI; +struct OSObject {alignas(8) uint8_t bytes[0x4B90]{};unsigned size=sizeof(bytes),refs=1; + OSObject *getMetaClass(){return this;}unsigned getClassSize(){return size;} + void retain(){++refs;}void release(){assert(refs>1);--refs;}}; +using IOService=OSObject;using IOLock=int; +unsigned depth=0;bool gate=false,busy=false,queueOnPut=false;unsigned opens=0,closes=0,putCalls=0,leaves=0;bool failPut=false,liePut=false,failLeaf=false,failTiming=false; +void IOLockLock(IOLock*){assert(!depth);++depth;}void IOLockUnlock(IOLock*){assert(depth==1);--depth;} +struct IOWorkLoop:OSObject{}; +bool inGate(const IOWorkLoop*){assert(!depth);return gate;} +bool tryClose(IOWorkLoop*){assert(!depth);++closes;return !busy && (gate=true);} +void open(IOWorkLoop*){assert(!depth && gate);gate=false;++opens;} +struct {IOLock *lock=nullptr;IOService *controller=nullptr;IOWorkLoop *workLoop=nullptr;OSObject *dsiPort=nullptr;void *power=nullptr; + unsigned dsiNativeWriters=0;bool memoryFaulted=false,clockFaulted=false;Display13Power::NativeBridge bridge;Display13DBUF::State dbuf; + struct {IOService *memoryFramebuffer=nullptr;bool destroyingPath=true;uintptr_t publishedScanout=0;struct {uintptr_t record=0;}scanout;}pipe[3]; +}display13FBPower; +uint64_t receipt[14]{}; +struct {IOLock *lock=nullptr;Display13FBIRQ::State state;Display13FBIRQ::HandlerScopes scopes; + bool quiescing=false;unsigned blockedPipes=0,admittedPipes=2,handlers=0,softwareHandlers=0;}display13FBIRQ; +bool adlnIRQOwner(IOService *c){return c==display13FBPower.controller;} +bool IOSimpleLockLockDisableInterrupt(IOLock*){assert(depth==1);++depth;return true;} +void IOSimpleLockUnlockEnableInterrupt(IOLock*,bool){assert(depth==2);--depth;} +void adlnPublishMemory(const char*,unsigned,void *p,size_t n){assert(!depth && n==sizeof(receipt));memcpy(receipt,p,n);} +templatevoid put(void *p,unsigned o,T v){memcpy(static_cast(p)+o,&v,sizeof(v));} +IOService *mapEntries[3]{},**pipeMap=mapEntries; +void freeScaler(IOService*,OSObject *s){assert(depth && gate);if(s){put(s,0x10,uintptr_t(0));put(s,0x20,uint8_t(0));}} +void freePlane(IOService *c,OSObject *p){++leaves;assert(depth && gate);freeScaler(c,Display13FBMemory::field(p,0x88));put(p,0x68,uintptr_t(0));} +void detach(IOService *fb,OSObject*){assert(depth && gate);put(fb,0x4A00,0U);put(fb,0x4A08,uintptr_t(0));} +void removePath(IOService *fb,OSObject *p){assert(depth && gate);if(failLeaf)return;pipeMap[0]=nullptr; + put(fb,0x49E8,0U);put(fb,0x49F0,uintptr_t(0));put(p,0x32B0,uintptr_t(0));put(p,0x3644,0U);} +struct XE { + uintptr_t dsiFreePlane=reinterpret_cast(freePlane),dsiFreeScaler=reinterpret_cast(freeScaler), + dsiDetachPath=reinterpret_cast(detach),dsiRemovePath=reinterpret_cast(removePath),dsiPipeMap=reinterpret_cast(&pipeMap); + uintptr_t adlnWorkLoopInGate=reinterpret_cast(inGate),adlnWorkLoopTryClose=reinterpret_cast(tryClose),adlnWorkLoopOpen=reinterpret_cast(open); + struct {unsigned dsi=1;}adlnOutputWiring;IOService *adlnTopologyController=nullptr; + bool dsiIRQParked=true,dsiDDIReleased=true,dsiMemoryReleased=true,dsiPGReleased=false; + Quiesce::Result dsiStop;Clock::Result dsiClockStop; + static bool adlnPowerReference(void *p,unsigned op,unsigned domain){ + assert(!depth && gate && p==display13FBPower.power && op==2 && domain==1);++putCalls; + if(queueOnPut)*reinterpret_cast(display13FBPower.controller->bytes+0xE08)=1; + if(failPut)return false;if(liePut)return true; + // Real NativeBridge balances the borrowed firmware PG1, preserving PG3. + struct IO {uint32_t read(uint32_t r){return r==0x45404?0xC0000003U:0;}bool readable(uint32_t){return true;} + void write(uint32_t,uint32_t){assert(false);}void delayUs(unsigned){assert(false);}}io; + return display13FBPower.bridge.putPG(io,1).status==Display13Power::Status::Complete; + } + bool releaseDSIFixedMode(IOService*){assert(!depth && gate);return !failTiming;} + bool disposeDSIPath(IOService*,OSObject*); +}; +BODY +uint8_t *image=nullptr; +struct Case { + XE xe;IOService controller,fb,peer;OSObject path,port,plane,scaler,planeScaler;IOWorkLoop loop;IOLock lock; + Case(){ + using Power=decltype(display13FBPower);display13FBPower.~Power();new (&display13FBPower) Power;auto &c=display13FBPower;c.lock=&lock;c.controller=&controller;c.workLoop=&loop;c.dsiPort=&port;c.power=&controller; + port.retain();c.pipe[0].memoryFramebuffer=&fb;c.pipe[0].destroyingPath=true;c.dbuf.selected=2; + c.bridge.begun=true;c.bridge.bootAdopted=true;c.bridge.bootPath=reinterpret_cast(&path);c.bridge.bootPort=0; + c.bridge.pgRefs[0]=1;c.bridge.pgRefs[2]=1; + auto &state=c.bridge.state;state.ready=true;state.allowed=Display13Power::All; + const auto pg=Display13Power::dependencies(Display13Power::nativeScope(1)),peerPG=Display13Power::dependencies(Display13Power::nativeScope(3)); + state.borrowed=pg|peerPG; + for(unsigned i=0;i(image+0x5751C);xe.dsiFreeScaler=reinterpret_cast(image+0x5739A); + xe.dsiDetachPath=reinterpret_cast(image+0x67FB4);xe.dsiRemovePath=reinterpret_cast(image+0x68EA4); + put(image,0x138BB0,pipeMap);xe.dsiPipeMap=reinterpret_cast(image+0x138BB0);} + } + void retained(){assert(!xe.disposeDSIPath(&fb,&path));assert(!putCalls && !leaves && !xe.dsiPGReleased); + assert(Display13FBMemory::field(&fb,0x49F0)==&path && pipeMap[0]==&fb && loop.refs==1 && !depth && !gate);} + void success(){assert(xe.disposeDSIPath(&fb,&path));assert(xe.dsiPGReleased && putCalls==1 && loop.refs==1 && !depth); + assert(!pipeMap[0] && pipeMap[1]==&peer && !display13FBPower.bridge.pgRefs[0] && display13FBPower.bridge.pgRefs[2]==1); + assert(Display13FBMemory::field(&port,0x554)==2 && !Display13FBMemory::field(&plane,0x68)); + assert(!xe.disposeDSIPath(&fb,&path) && xe.dsiPGReleased && putCalls==1);} +}; +void run(){ + {Case c;c.success();assert(opens==closes && !gate);} + {Case c;gate=true;c.success();assert(!opens && !closes && gate);gate=false;} + {Case c;busy=true;c.retained();} + {Case c;c.xe.dsiMemoryReleased=false;c.retained();} + {Case c;failTiming=true;assert(!c.xe.disposeDSIPath(&c.fb,&c.path)); + assert(display13FBPower.dsiPort==&c.port && c.port.refs==2 && c.xe.dsiPGReleased);} + + {Case c;display13FBPower.memoryFaulted=true;c.retained();} + {Case c;display13FBPower.dsiNativeWriters=1;c.retained();} + {Case c;display13FBIRQ.state.active|=1;c.retained();} + {Case c;display13FBIRQ.softwareHandlers=1;c.retained();} + {Case c;put(&c.controller,0xE08,1U);c.retained();} + {Case c;put(&c.controller,0xE10,1U);c.retained();} + {Case c;put(&c.controller,0xE08,Display13FBIRQ::pipeEvents(1));put(&c.controller,0xE10,0xC0000U);c.success();} + {Case c;queueOnPut=true;assert(!c.xe.disposeDSIPath(&c.fb,&c.path));assert(c.xe.dsiPGReleased && pipeMap[0]==&c.fb && !leaves);} + {Case c;display13FBPower.dbuf.selected|=1;c.retained();} + {Case c;display13FBPower.bridge.pgRefs[0]=2;c.retained();} + {Case c;display13FBPower.bridge.bootPGProtected=true;c.retained();} + {Case c;put(&c.path,0x3644,2U);c.retained();} + {Case c;put(&c.path,0x32B8,&c.peer);c.retained();} + {Case c;put(&c.planeScaler,0x10,&c.peer);c.retained();} + {Case c;put(&c.fb,0x49E8,2U);c.retained();} + {Case c;put(&c.fb,0x4A08,&c.peer);c.retained();} + {Case c;put(&c.fb,0x4A00,0U);c.retained();} + {Case c;c.controller.size=0x100;c.retained();} + {Case c;put(&c.fb,0x4A00,0U);put(&c.fb,0x4A08,uintptr_t(0));c.success();} + {Case c;put(&c.path,0x32D0,&c.peer);c.retained();} + {Case c;failPut=true;assert(!c.xe.disposeDSIPath(&c.fb,&c.path));assert(putCalls==1 && !c.xe.dsiPGReleased && !leaves && pipeMap[0]==&c.fb);} + {Case c;liePut=true;assert(!c.xe.disposeDSIPath(&c.fb,&c.path));assert(putCalls==1 && !c.xe.dsiPGReleased && !leaves && pipeMap[0]==&c.fb);} + if(!image){Case c;failLeaf=true;assert(!c.xe.disposeDSIPath(&c.fb,&c.path));assert(c.xe.dsiPGReleased && pipeMap[0]==&c.fb);} +} +int main(int argc,char **argv){ + run(); + if(argc==2){std::ifstream f(argv[1],std::ios::binary);std::vector b{std::istreambuf_iterator(f),{}}; + image=static_cast(mmap(nullptr,4*1024*1024,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0));assert(image!=MAP_FAILED); + memcpy(image,b.data(),b.size()); + // freePath -> resetSoftwareState remains native; only imported bzero is supplied. + image[0x300000]=0x48;image[0x300001]=0xB8;put(image,0x300002,reinterpret_cast(bzero));image[0x30000A]=0xFF;image[0x30000B]=0xE0; + for(unsigned call:{0x9161FU,0x91630U,0x91641U,0x91652U,0x9166CU})put(image,call+1,int32_t(0x300000-call-5)); + run();munmap(image,4*1024*1024); + } + std::puts("PASS production DSI PG/path disposal: ownership, gate, failed put retention, CPU arrays/scalers and peer preservation"); +} +'''.replace('BODY',body) +args=[];arch=[] +if len(sys.argv)>1: + donor=Path(sys.argv[1]).resolve();data=donor.read_bytes() + assert hashlib.sha256(data).hexdigest()=='c004fc1ac9ee4157e2e12736f40fa6639bde025a279326beeb9dd48c9b445731' + args=[str(donor)];arch=['-arch','x86_64'] +with tempfile.TemporaryDirectory(prefix='dsi-dispose-') as d: + source=Path(d)/'test.cpp';source.write_text(code);binary=Path(d)/'test' + subprocess.run(['clang++',*arch,'-std=c++17','-O1','-I'+str(root/'WhateverXe'),str(source),'-o',str(binary)],check=True) + subprocess.run([str(binary),*args],check=True) diff --git a/tests/dsi_port_adapter.py b/tests/dsi_port_adapter.py new file mode 100644 index 00000000..e4529712 --- /dev/null +++ b/tests/dsi_port_adapter.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Replay production DSI port/probe with real source and power qualification. + +Optional exact donor executes its base mode setter; HAL/AUX/reset are forbidden. +The topology, power leases and register values are synthetic CPU fixtures. +""" +from pathlib import Path +from production_source import read_source +import hashlib, re, subprocess, sys, tempfile +root=Path(__file__).resolve().parents[1] +source=read_source(root/'WhateverXe/kern_xe.cpp') +functions='\n'.join(re.search(r'uint32_t XE::'+name+r'\(.*?\n\}',source,re.S)[0] + for name in ['setDSIPortMode','wrapAlderLakeNPortState','wrapDisplay13ProbeMode']) +dispatch=re.search(r'uint32_t XE::wrapDisplay13TCMode\(.*?\n\}',source,re.S)[0] +assert dispatch.index('return setDSIPortMode(port,wanted)')orgDisplay13TCMode') +donor=None +if len(sys.argv)>1: + donor=Path(sys.argv[1]).resolve() + assert hashlib.sha256(donor.read_bytes()).hexdigest()=='c004fc1ac9ee4157e2e12736f40fa6639bde025a279326beeb9dd48c9b445731' +code=r''' +#include "kern_dsi_readout.hpp" +#include "kern_display13_power_bridge.hpp" +#include "kern_display13_transcoder.hpp" +#include "kern_display13_fb_topology.hpp" +#include +#include +#include +#include +#include +#include +constexpr uint32_t kIOReturnSuccess=0,kIOReturnNotReady=0xe00002d8U,kIOReturnUnsupported=0xe00002c7U; +struct OSObject { + alignas(8) uint8_t bytes[0x6B0]{};unsigned refs=1;unsigned size=sizeof(bytes); + OSObject *getMetaClass(){return this;}unsigned getClassSize(){return size;}void retain(){++refs;} +}; +struct IOLock {bool held=false;};void IOLockLock(IOLock *p){assert(!p->held);p->held=true;} +void IOLockUnlock(IOLock *p){assert(p->held);p->held=false;} +namespace Display13FBMemory {templateT &field(void *p,unsigned off){return *reinterpret_cast(static_cast(p)+off);}} +struct IORegistryEntry {static IORegistryEntry *getRegistryRoot(){static IORegistryEntry r;return &r;} + void setProperty(const char *,bool){}void setProperty(const char *,const void *,unsigned){}}; +using Registers=std::map; +struct Display13FBBootIO { + Registers *map; + bool readable(uint32_t r){return map && map->count(r);} + uint32_t read(uint32_t r){assert(readable(r));return map->at(r);} +}; +struct {IOLock *lock;void *controller,*access;Registers *map;OSObject *dsiPort=nullptr; + Display13Transcoder::Bindings transcoders;XELP::DSI::Readout bootDSI;Display13Power::NativeBridge bridge; + bool memoryFaulted=false,clockFaulted=false;} display13FBPower; +unsigned nativeCalls=0,providerCalls=0,modeCalls=0; +uint32_t nativeProbe(OSObject *){++nativeCalls;return 1;} +uint32_t baseMode(OSObject *p,uint32_t m){assert(m==1);++modeCalls;Display13FBMemory::field(p,0x550)=m;return 0;} +struct XE { + bool dsiResourcesReady=false;uintptr_t xelpProviderProbe=reinterpret_cast(nativeProbe); + bool display13FBTCHooksReady=true,display13FBTopologyHooksReady=true; + uintptr_t display13TCBaseMode=reinterpret_cast(baseMode); + void *adlnTopologyController;alignas(8) uint8_t adlnPlatform[0x1C0]{};Display13OutputWiring adlnOutputWiring; + uint32_t (*orgAlderLakeNPortState)(OSObject*)=nativeProbe; + bool xelpBindProbe(void *){++providerCalls;return true;} + static uint32_t setDSIPortMode(OSObject *,uint32_t); + static uint32_t wrapDisplay13ProbeMode(OSObject *); + static uint32_t wrapAlderLakeNPortState(OSObject *); +} xe,*callbackXE=&xe; +bool display13Offline=false; +#define FunctionCast(f,p) reinterpret_cast(p) +FUNCTIONS +void noReset(void *){assert(false && "DSI must not call native reset");} +int main(int argc,char **argv){ + void *image=nullptr; + if(argc==2){std::ifstream f(argv[1],std::ios::binary);std::vector b((std::istreambuf_iterator(f)),{}); + image=mmap(nullptr,0x400000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0);assert(image!=MAP_FAILED); + assert(b.size()<0x400000);memcpy(image,b.data(),b.size());xe.display13TCBaseMode=reinterpret_cast(image)+0x1b4c8;} + Registers r={{0x7B008,0xC0000000},{0x6B400,0x80030006},{0x45404,0xFC0F},{0x70008,0}, + {0x6B030,0x20130300},{0x6B094,1},{0x6B404,0},{0x6B034,0},{0x64000,0x80000000},{0x164280,0x1E07801}, + {0x46014,0xCC000000},{0x16428C,0x3001FB},{0x164290,0xE84}, + {0x6B000,0x04650437},{0x6B008,0x044F044D},{0x6B00C,0x07D1077F},{0x6B014,0x07A907A7}, + {0x51004,0x40000000},{0x45454,3},{0x42000,0x0EFFFFFF}}; + auto &c=display13FBPower;IOLock lock;c.lock=&lock;c.map=&r; + OSObject controller,access,port,peer;uintptr_t vtable[64]{};vtable[0x120/8]=reinterpret_cast(noReset); + Display13FBMemory::field(port.bytes,0)=vtable; + c.controller=xe.adlnTopologyController=&controller;c.access=&access; + xe.adlnOutputWiring.valid=true;xe.adlnOutputWiring.dsi=1;xe.adlnOutputWiring.hdmi=2;xe.adlnOutputWiring.ddc[1]=2; + xe.adlnPlatform[0x12]=9; + uint32_t panel[]={0,0,0,0,2,0x18};memcpy(xe.adlnPlatform+0x20,panel,sizeof(panel)); + uint32_t hdmi[]={1,2,2,0,0x800,0x202};memcpy(xe.adlnPlatform+0x38,hdmi,sizeof(hdmi)); + auto config=reinterpret_cast(xe.adlnPlatform+0x20); + Display13FBMemory::field(port.bytes,0x548)=config; + Display13FBMemory::field(port.bytes,0x578)=&access; + Display13FBMemory::field(peer.bytes,0x548)=reinterpret_cast(xe.adlnPlatform+0x38); + c.transcoders.dsi0Pipe=0; + c.bootDSI=XELP::DSI::decodeReadout([&](uint32_t a,uint32_t &v){if(!r.count(a))return false;v=r.at(a);return true;},38400); + assert(c.bootDSI.complete()); + auto &bridge=c.bridge;bridge.begun=true;bridge.selectedPorts=3;bridge.bootPort=0; + auto &state=bridge.state;state.ready=true;state.allowed=Display13Power::All; + const auto pg=Display13Power::dependencies(Display13Power::nativeScope(1)); + const auto ddi=Display13Power::dependencies(Display13Power::ddiDomain(0)); + state.borrowed=pg|ddi; + for(unsigned i=0;i(&port,0x550) && port.refs==1 && !c.dsiPort && !lock.held);}; + bridge.ddiRefs[0]=0;reject();bridge.ddiRefs[0]=1; + bridge.pgRefs[0]=0;reject();bridge.pgRefs[0]=1; + c.memoryFaulted=true;reject();c.memoryFaulted=false; + c.clockFaulted=true;reject();c.clockFaulted=false; + c.transcoders.dsi0Pipe=1;reject();c.transcoders.dsi0Pipe=0; + r[0x6B000]^=1;reject();r[0x6B000]^=1; + r[0x164280]^=1;reject();r[0x164280]^=1; + Display13FBMemory::field(&port,0x578)=&controller;reject();Display13FBMemory::field(&port,0x578)=&access; + uint32_t copied[6];memcpy(copied,config,sizeof(copied));Display13FBMemory::field(&port,0x548)=copied; + reject();Display13FBMemory::field(&port,0x548)=config; + assert(XE::setDSIPortMode(&port,0)==kIOReturnUnsupported && !c.dsiPort); + display13Offline=true;assert(!XE::wrapAlderLakeNPortState(&port) && !c.dsiPort);display13Offline=false; + assert(!XE::wrapDisplay13ProbeMode(&port));xe.dsiResourcesReady=true; + assert(XE::wrapDisplay13ProbeMode(&port)==1 && !nativeCalls && !providerCalls); + assert(XE::wrapAlderLakeNPortState(&port)==2 && !nativeCalls && !providerCalls); + assert(Display13FBMemory::field(&port,0x550)==1 && Display13FBMemory::field(&port,0x554)==2); + assert(c.dsiPort==&port && port.refs==2); + assert(XE::wrapAlderLakeNPortState(&port)==2 && port.refs==2); + assert(XE::setDSIPortMode(&port,0)==kIOReturnUnsupported && Display13FBMemory::field(&port,0x550)==1); + state.suspended=true;assert(!XE::wrapAlderLakeNPortState(&port));state.suspended=false; + assert(port.refs==2 && c.dsiPort==&port && !nativeCalls && !providerCalls); + assert(XE::wrapAlderLakeNPortState(&peer)==1 && nativeCalls==1 && providerCalls==1); + assert(!lock.held);if(image)munmap(image,0x400000); + puts("PASS production DSI port/probe: source and lease qualification, no HPD/AUX/reset, retained identity, external fallback"); +} +'''.replace('FUNCTIONS',functions) +with tempfile.TemporaryDirectory(prefix='dsi-port-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + command=['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')] + if donor: command[1:1]=['-arch','x86_64'] + subprocess.run(command,check=True) + subprocess.run([str(p/'test')]+([str(donor)] if donor else []),check=True) diff --git a/tests/dsi_presentation.py b/tests/dsi_presentation.py new file mode 100644 index 00000000..6e31dd82 --- /dev/null +++ b/tests/dsi_presentation.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Exercise the production presentation transaction with native allocation leaves mocked.""" +from pathlib import Path +from production_source import read_source +import re, subprocess, tempfile +root=Path(__file__).resolve().parents[1] +source=read_source(root/'WhateverXe/kern_xe.cpp') +functions='\n'.join(re.search(r'(?:bool|uint32_t) XE::'+name+r'\(.*?\n\}',source,re.S)[0] for name in ['releaseDSILogical','setDSIPresentation']) +code=r''' +#include "kern_dsi_native_mode.hpp" +#include "kern_dsi_native_backing.hpp" +#include +#include +#include +using Display13FBMemory::field; +using XELP::DSI::AppleAdapter::Timing; +using XELP::DSI::NativeBacking::Record; +struct IOService {alignas(8) uint8_t bytes[0x4B90]{};IOService *getMetaClass(){return this;}unsigned getClassSize(){return sizeof(bytes);}}; +templateconstexpr unsigned arrsize(T(&)[N]){return N;} +using OSObject=IOService; +templatevoid put(void *p,unsigned offset,T value){memcpy(static_cast(p)+offset,&value,sizeof(value));} +struct IOLock {unsigned depth=0;}; +void IOLockLock(IOLock *l){++l->depth;}void IOLockUnlock(IOLock *l){assert(l->depth);--l->depth;} +void *current_thread(){return reinterpret_cast(1);} +constexpr uint32_t kIOReturnNotReady=0xE00002D8,kIOReturnSuccess=0; +uint64_t receiptCopy[16]{}; +struct IORegistryEntry {static IORegistryEntry *getRegistryRoot(){static IORegistryEntry r;return &r;} + void setProperty(const char *,const void *p,unsigned n){assert(n==sizeof(receiptCopy) || n==13*sizeof(uint64_t));if(n==sizeof(receiptCopy))memcpy(receiptCopy,p,n);}}; +struct {IOService *framebuffer=nullptr;Timing timing,presentation;XELP::DSI::Mode mode; + uint32_t clientMode=0x80001000,activeMode=0;uintptr_t logical=0,changingThread=0; + bool presentationReady=false,presentationFault=false;}dsiFixedMode; +struct {IOService *pipe=nullptr;}dsiPendingBacking; +struct {IOLock *lock=nullptr;unsigned dsiNativeWriters=0;struct {unsigned wmPending=0;bool wmCommitting=false;}pipe[1]; + struct {bool updating=false;}dbuf;}display13FBPower; +bool dsiModeTarget(IOService *f){return f==dsiFixedMode.framebuffer;} +bool dsiModeCoherent(IOService *){return true;} +bool dsiPublishedTarget(IOService *f){return field(f,0x4348)==0x100000;} +struct XE { + XELP::DSI::Profile dsiProfile;uintptr_t dsiModeCPU[4]{},dsiComputeGeometry=0; + bool dsiArmedBackingOwned(IOService *f,OSObject *){return dsiPublishedTarget(f);} + bool dsiNativeUpdateAllowed(IOService *){return !dsiFixedMode.changingThread && !dsiFixedMode.presentationFault;} + bool releaseDSILogical(IOService *); + uint32_t setDSIPresentation(IOService *,uint32_t,const Timing &); +}xe; +FUNCTIONS +unsigned allocations=0,frees=0,caps=0;bool failAllocation=false,failRelease=false; +void compute(void *f,const Timing *t){ + auto path=Display13FBMemory::singlePath(f); + const unsigned width=t->horizontalScaled?t->horizontalScaled:t->horizontalActive; + const unsigned height=t->verticalScaled?t->verticalScaled:t->verticalActive; + const unsigned pitch=(width*4+127)&~127U; + for(unsigned off:{0x430CU,0x4320U,0x4328U})put(f,off,t->horizontalActive); + for(unsigned off:{0x4310U,0x4324U,0x432CU})put(f,off,t->verticalActive); + put(f,0x430C,width);put(f,0x4310,height); + put(f,0x4314,pitch*t->verticalActive);put(f,0x4334,pitch);put(f,0x4338,t->scalerFlags?0x30U:((width!=t->horizontalActive || height!=t->verticalActive)?0x20U:1U)); + put(path,0xF0,uint64_t(0));put(path,0x100,uint64_t(0)); + for(unsigned off:{0xF8U,0x108U})put(path,off,t->horizontalActive); + for(unsigned off:{0xFCU,0x10CU})put(path,off,t->verticalActive); +} +uint32_t allocate(IOService *f){ + assert(dsiFixedMode.changingThread && !field(f,0x4340)); + assert(field(f,0x4250)==dsiFixedMode.activeMode); + if(!(field(f,0x4338)&0x30))return 0; + ++allocations;auto r=new Record;r->kind=2;r->columns=(field(f,0x430C)*4+127)/128;r->rows=(field(f,0x4310)+31)/32; + r->pitch=field(f,0x4334);r->bytes=r->pitch*field(f,0x4310); + r->memory=0x200000;r->map=0x300000; + put(f,0x4340,r);put(dsiPendingBacking.pipe,0x12B8,r->memory);put(dsiPendingBacking.pipe,0x12C0,r->map); + return failAllocation?0xE00002BD:0; +} +void release(IOService *f){auto r=field(f,0x4340);assert(r);++frees;delete r; + put(f,0x4340,uintptr_t(0));if(!failRelease){put(dsiPendingBacking.pipe,0x12B8,uintptr_t(0));put(dsiPendingBacking.pipe,0x12C0,uintptr_t(0));}} +void initial(IOService *,uint32_t){}void capabilities(IOService *){++caps;} +int main(){ + IOService fb,path,pipe,other;IOLock lock;display13FBPower.lock=&lock;dsiPendingBacking.pipe=&pipe; + auto &o=dsiFixedMode;o.framebuffer=&fb;xe.dsiProfile.status=XELP::DSI::ProfileStatus::Valid; + xe.dsiProfile.mode={135255,1080,46,22,2,1920,82,40,2,86,154};o.mode=xe.dsiProfile.mode; + assert(XELP::DSI::AppleAdapter::makeTiming(xe.dsiProfile,o.timing)); + put(&fb,0x3FD8,0U);put(&fb,0x4A00,1U);put(&fb,0x4A08,&path);put(&fb,0x4348,uintptr_t(0x100000)); + compute(&fb,&o.timing);xe.dsiComputeGeometry=reinterpret_cast(compute); + xe.dsiModeCPU[0]=reinterpret_cast(allocate);xe.dsiModeCPU[1]=reinterpret_cast(initial); + xe.dsiModeCPU[2]=reinterpret_cast(capabilities);xe.dsiModeCPU[3]=reinterpret_cast(release); + auto turn=o.timing;turn.scalerFlags=0x30;turn.horizontalScaled=1920;turn.verticalScaled=1080; + assert(xe.setDSIPresentation(&other,2,turn)==kIOReturnNotReady && !allocations); + display13FBPower.pipe[0].wmPending=1;assert(xe.setDSIPresentation(&fb,2,turn)==kIOReturnNotReady && !allocations); + display13FBPower.pipe[0].wmPending=0; + assert(!xe.setDSIPresentation(&fb,2,turn) && o.logical && o.activeMode==2 && !o.changingThread); + assert(field(&fb,0x4338)==0x30 && dsiPublishedTarget(&fb)); + turn.scalerFlags=0x50;assert(!xe.setDSIPresentation(&fb,3,turn) && allocations==2 && frees==1); + auto scaled=turn;scaled.horizontalScaled=2880;scaled.verticalScaled=1620; + assert(!xe.setDSIPresentation(&fb,8,scaled) && allocations==3 && frees==2); + assert(field(&fb,0x430C)==2880 && field(&fb,0x4340)->bytes==18662400); + assert(!xe.setDSIPresentation(&fb,9,scaled) && allocations==3 && frees==2); + assert(!xe.setDSIPresentation(&fb,3,turn) && allocations==4 && frees==3); + assert(!xe.setDSIPresentation(&fb,4,o.timing) && !o.logical && frees==4 && field(&fb,0x4338)==1); + failAllocation=true;assert(xe.setDSIPresentation(&fb,5,turn)==kIOReturnNotReady); + assert(o.activeMode==4 && !o.logical && !o.presentationFault && !o.changingThread && allocations==5 && frees==5); + assert(field(&fb,0x4338)==1 && dsiPublishedTarget(&fb) && receiptCopy[10]==1); + failAllocation=false; + auto portrait=o.timing;portrait.horizontalScaled=1620;portrait.verticalScaled=2880; + assert(!xe.setDSIPresentation(&fb,10,portrait) && o.logical); + assert(field(&fb,0x4338)==0x20 && field(&fb,0x4340)->bytes==18800640); + assert(!xe.setDSIPresentation(&fb,11,portrait) && allocations==6 && frees==5); + assert(!xe.setDSIPresentation(&fb,4,o.timing) && !o.logical && allocations==frees); + assert(!xe.setDSIPresentation(&fb,6,turn));failRelease=true; + assert(xe.setDSIPresentation(&fb,7,o.timing)==kIOReturnNotReady && o.presentationFault && !o.changingThread); + assert(allocations==frees && !lock.depth && dsiPublishedTarget(&fb)); + puts("PASS production presentation: quarter-turns, native restore, partial allocation cleanup and failed-release admission closure"); +} +'''.replace('FUNCTIONS',functions) +with tempfile.TemporaryDirectory(prefix='dsi-presentation-') as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_profile.cpp b/tests/dsi_profile.cpp new file mode 100644 index 00000000..1a366df6 --- /dev/null +++ b/tests/dsi_profile.cpp @@ -0,0 +1,143 @@ +#include "kern_dsi_restart.hpp" +#include +#include +#include +#include +#include +using namespace XELP::DSI; +using Bytes=std::vector; +void put(Bytes &b,size_t at,uint32_t value,unsigned n=2) {for(unsigned i=0;i>(8*i);} +void add(Bytes &b,uint8_t id,const Bytes &data) { + b.push_back(id);b.push_back(data.size());b.push_back(data.size()>>8); + b.insert(b.end(),data.begin(),data.end()); +} +Bytes fixture(unsigned version=256,bool inlineReset=false,unsigned panel=0,unsigned stride=28,unsigned childStride=0,unsigned mask=0xCFE) { + Bytes b(70);memcpy(b.data(),"$VBT",4);put(b,22,48);put(b,28,48,4); + memcpy(b.data()+48,"BIOS_DATA_BLOCK ",16);put(b,64,version);put(b,66,22); + Bytes child(5+(childStride?childStride:version<256?39:40));child[4]=child.size()-5;put(child,7,0x1400);child[21]=21;add(b,2,child); + add(b,40,Bytes{uint8_t(panel)}); + Bytes config(792);put(config,2,0x11190,4);put(config,6,12); + config[93]=8;config[94]=50;put(config,98,310);config[103]=110;config[104]=50;put(config,105,200); + for(unsigned i=0;i<5;++i)put(config,732+2*i,i==4?5000:500); + if(panel) { + for(unsigned i=0;i<122;++i)config[122*panel+i]=config[i]; + for(unsigned i=0;i<10;++i)config[732+10*panel+i]=config[732+i]; + } + add(b,52,config); + Bytes dtd(2+stride*(panel+1),0xA5);put(dtd,0,stride);const auto t=2+stride*panel;put(dtd,t,135255,4); + const unsigned fields[]={1080,46,22,2,1920,82,40,2,86,154}; + for(unsigned i=0;i<10;++i)put(dtd,t+4+2*i,fields[i]);dtd[t+24]=0xC0;add(b,58,dtd); + Bytes bl(273,0xA5);bl[0]=6;bl[1+6*panel]=2;put(bl,2+6*panel,40000); + bl[113+panel]=2;put(bl,129+4*panel,255);put(bl,193+4*panel,6);bl[257+panel]=8;add(b,43,bl); + Bytes seq(10);seq[0]=3;seq[5]=panel; + // Empty, terminated sequences prove structure without firmware commands. + for(unsigned id:{1U,2U,4U,5U,6U,7U,10U,11U}) { + if(!(mask&(1U<=7998000&&v.dcoKHz<=10000000); + const auto reconstructed=(uint64_t(v.integer)*32768+v.fraction)*24000/32768; + assert(reconstructed<=v.dcoKHz&&v.dcoKHz-reconstructed<2); + } + for(size_t i=0;i(f),{}}; + assert(!parse(real).valid());auto q=parse(real,true); + printf("saved status=%u\n",unsigned(q.status)); + assert(q.valid()&&q.bdbTailQuirk&&q.panel==0&&q.lanes==4); + assert(q.mode.clockKHz==135255&&q.mode.width==1080&&q.mode.height==1920); + assert(q.sequences.packets==359&&q.sequences.gpios==8&&q.sequences.delayUs==486000); + assert(q.clockPrepare==50&&q.hsPrepare==50&&q.clockPrepareZero==310&&q.hsPrepareZero==200); + assert(q.ppsUs[0]==50000&&q.ppsUs[4]==500000&&q.minimum==6&&q.pwmHz==40000); + const auto saved=linkPlan(q);assert(saved.valid&&saved.clockTiming==link.clockTiming&&saved.dataTiming==link.dataTiming); + printf("saved profile: %ux%u clock=%u kHz PWM=%u Hz\n",q.mode.width,q.mode.height,q.mode.clockKHz,q.pwmHz); + } + puts("DSI profile bounds, explicit tail quirk, supported transport: PASS"); +} diff --git a/tests/dsi_quiesce.py b/tests/dsi_quiesce.py new file mode 100644 index 00000000..13e3cf3a --- /dev/null +++ b/tests/dsi_quiesce.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Fault injection for the DSI host shutdown core; no simulated cleanup proof.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[1] +s=(root/'tests/dsi_boot.cpp').read_text();io=s[s.index('struct IO {'):s.index('Boot capture(')] +code=r''' +#include "kern_dsi_quiesce.hpp" +#include "kern_xelp_memory_layout.hpp" +#include +#include +#include +#include +using namespace XELP::DSI; +IO_FIXTURE +struct Sim:IO { + uint32_t drop=UINT32_MAX,elapsed=0,failStage=0;bool frozen=false,unbound=false,losePower=false,pendingAfterStop=false; + bool ready(){return !unbound;} + void write(uint32_t reg,uint32_t value){writes.push_back(reg);if(reg!=drop){regs[reg]=value;if(reg==0x6B400 && !(value&(1U<<31)))regs[0x6B0D8]=0;}} + void delayUs(unsigned us){elapsed+=us; + if(losePower){regs[0x45404]=0;return;} + if(!frozen && us>=1000)++regs[0x70040]; + if(!(regs[0x7B008]&(1U<<31)) && failStage!=1){regs[0x7B008]&=~(1U<<30);if(pendingAfterStop)regs[0x6B0D0]=0;} + if((regs[0x6B0D8]&1) && failStage!=2)regs[0x6B0D8]=(regs[0x6B0D8]&~1U)|(1U<<16); + if(!(regs[0x64000]&(1U<<31)) && failStage!=3)regs[0x64000]|=0x80; + } +}; +struct Fixture { + Sim io;Profile p=profile();Readout link; + Fixture(bool cursor=false){ + io.regs[0x42084]=1U<<30;io.regs[0x6B0D0]=0x01001040; + io.regs[0x6001C]=0x0437077F;io.regs[0x6B010]=io.regs[0x6B00C];io.regs[0x70040]=100; + io.regs[0x70080]=cursor?7:0;io.regs[0x70084]=cursor?0x200000:0; + // A live external pipe must survive the target-only stop. + io.regs[0x71008]=0xC0000000;io.regs[0x61400]=0x90000000;io.regs[0x71180]=0x94001000; + io.regs[0x7119C]=0x30000000;io.regs[0x64100]=0x80000000;io.regs[0x164280]|=1U<<10; + link=decodeReadout([&](uint32_t r,uint32_t &v){v=io.read(r);return true;},38400);assert(link.matches(p)); + } + Quiesce::Result run(){return Quiesce::run(io,link,p);} +}; +int main(){ + for(bool cursor:{false,true}){Fixture f(cursor);const auto mux=f.io.regs[0x164280];const auto r=f.run(); + assert(r.complete() && r.writes==f.io.writes.size() && r.writes==(cursor?11U:8U)); + assert(r.frameAfter>r.frameBefore && f.io.regs[0x7B008]==0 && !(f.io.regs[0x6B400]&(1U<<31))); + Display13Transcoder::Bindings map;map.dsi0Pipe=0;assert(Display13FBIRQ::consumersStopped(f.io,1,map)); + assert(f.io.regs[0x164280]==mux && f.io.regs[0x46014]==0xCC000000 && f.io.regs[0x45404]==0xFC0F); + assert(f.io.regs[0x71180]==0x94001000 && f.io.regs[0x7119C]==0x30000000 && f.io.regs[0x64100]==0x80000000); + for(auto reg:f.io.writes)assert(reg==0x70080 || reg==0x70084 || reg==0x70088 || reg==0x70180 || reg==0x7019C || + reg==0x7B008 || reg==0x6B0D8 || reg==0x6B400 || reg==0x164280 || reg==0x64000); + // Host stopped is not an IRQ/clock/panel teardown proof, and cannot be + // silently replayed using the now-stale active-link snapshot. + const auto count=f.io.writes.size();assert(!f.run().complete() && f.io.writes.size()==count); + } + for(auto reg:{0x6B0D0U,0x6B0D8U,0x6B000U,0x70780U,0x70B08U,0x6001CU}){Fixture f; + f.io.regs[reg]^=reg==0x70780?1U<<31:1; + assert(!f.run().complete() && f.io.writes.empty()); + } + {Fixture f;f.io.unbound=true;assert(!f.run().complete() && f.io.writes.empty());} + for(auto reg:{0x70180U,0x7B008U,0x6B0D8U,0x6B400U,0x164280U,0x64000U}){Fixture f;f.io.drop=reg; + const auto r=f.run();assert(!r.complete() && r.error!=Quiesce::Error::None && !f.io.writes.empty()); + assert(f.io.regs[0x46014]==0xCC000000 && f.io.regs[0x45404]==0xFC0F); // no PLL/power release on uncertainty + } + for(unsigned stage=1;stage<=3;++stage){Fixture f;f.io.failStage=stage;const auto r=f.run(); + assert(!r.complete() && r.error==Quiesce::Error::Timeout); + assert(r.stage==(stage==1?Quiesce::Stage::Transcoder:stage==2?Quiesce::Stage::ULPS:Quiesce::Stage::Buffer)); + } + {Fixture f;f.io.pendingAfterStop=true;const auto r=f.run();assert(!r.complete() && r.stage==Quiesce::Stage::Commands && r.error==Quiesce::Error::Commands);assert(!f.io.regs[0x6B0D8]);} + {Fixture f;f.io.frozen=true;const auto r=f.run();assert(!r.complete() && r.stage==Quiesce::Stage::Plane && r.error==Quiesce::Error::Timeout); + assert(f.io.regs[0x7B008]==f.link.conf);} + {Fixture f;f.io.losePower=true;const auto r=f.run();assert(!r.complete() && r.error==Quiesce::Error::Ownership); + assert(f.io.writes==std::vector({0x70180,0x7019C}));} + puts("PASS DSI host quiesce: cursor/plane/frame then transcoder/ULPS/port, peer preservation, FIFO precondition, bounded failures and no resource release"); +} +'''.replace('IO_FIXTURE',io) +with tempfile.TemporaryDirectory(prefix='dsi-quiesce-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_quiesce_adapter.py b/tests/dsi_quiesce_adapter.py new file mode 100644 index 00000000..6c3fff82 --- /dev/null +++ b/tests/dsi_quiesce_adapter.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +"""Exercise the production native DSI stop boundary with real host/IRQ cores.""" +from pathlib import Path +from production_source import read_source +import re,subprocess,tempfile +root=Path(__file__).resolve().parents[1] +s=read_source(root/'WhateverXe/kern_xe.cpp') +body=re.search(r'bool XE::quiesceDSIPath\(.*?\n\}',s,re.S)[0] +b=(root/'tests/dsi_boot.cpp').read_text();fixture=b[b.index('struct IO {'):b.index('Boot capture(')] +code=r''' +#include "kern_dsi_quiesce.hpp" +#include "kern_dsi_clock.hpp" +#include "kern_dsi_restart.hpp" +#include "kern_dsi_panel.hpp" +#include "kern_dsi_boot.hpp" +#include "kern_dsi_native_backing.hpp" +#include "kern_display13_fb_memory.hpp" +#include "kern_display13_power_bridge.hpp" +#include "kern_xelp_combo_clock.hpp" +#include +#include +#include +#include +#include +using namespace XELP::DSI; +FIXTURE +std::vector locks; +#ifndef DSI_SURFACE_REPLAY +struct IORegistryEntry {static IORegistryEntry *getRegistryRoot(){static IORegistryEntry r;return &r;} + void setProperty(const char*,const void*,unsigned){assert(locks.empty());}}; +#endif +struct IORecursiveLock{unsigned rank;};struct IOLock{};struct IOSimpleLock{}; +void IORecursiveLockLock(IORecursiveLock *p){assert(locks.empty() || locks.back()rank);locks.push_back(p->rank);} +void IORecursiveLockUnlock(IORecursiveLock *p){assert(locks.back()==p->rank);locks.pop_back();} +void IOLockLock(IOLock*){assert(!locks.empty() && locks.back()<3);locks.push_back(3);} +void IOLockUnlock(IOLock*){assert(locks.back()==3);locks.pop_back();} +bool IOSimpleLockLockDisableInterrupt(IOSimpleLock*){assert(locks.back()==3);locks.push_back(4);return true;} +void IOSimpleLockUnlockEnableInterrupt(IOSimpleLock*,bool v){assert(v && locks.back()==4);locks.pop_back();} +struct OSObject {alignas(8) uint8_t bytes[0x4B90]{};unsigned refs=1,size=sizeof(bytes); + OSObject *getMetaClass(){return this;}unsigned getClassSize(){return size;} + void setProperty(const char*,bool value){assert(locks==std::vector{1} && !value);} + void retain(){++refs;}void release(){assert(refs>1);--refs;}}; +using IOService=OSObject; +bool armedOwner=false,failNativeRetire=false;unsigned nativeRetires=0;IOService *pendingFB=nullptr;bool dsiPendingTarget(IOService *fb){return fb && pendingFB==fb;} +struct IOWorkLoop:OSObject {bool gate=false,busy=false;unsigned closes=0,opens=0;}; +bool inGate(const IOWorkLoop *p){assert(locks==std::vector{1});return p->gate;} +bool tryClose(IOWorkLoop *p){assert(locks==std::vector{1});++p->closes;if(p->busy)return false;return p->gate=true;} +void open(IOWorkLoop *p){assert(locks==std::vector{1} && p->gate);p->gate=false;++p->opens;} +struct IOMemoryMap { + std::vector bar=std::vector(0x1000000/4);bool freeze=false,pending=false,failMasterRestore=false,failMasterMask=false; + unsigned masterWrites=0,pllFail=0;bool gtOnly=false; + uintptr_t getVirtualAddress(){return reinterpret_cast(bar.data());}size_t getLength(){return 0x1000000;} + uint32_t ®(uint32_t r){return bar[r/4];} +}*activeMap; +struct Display13FBBootIO { + IOMemoryMap *map;bool readable(uint32_t r){return map && r<0x1000000 && !(r&3);} + uint32_t read(uint32_t r){assert(locks.back()==3);return map->reg(r);} +}; +struct Display13FBDBUFIO:Display13FBBootIO {void write(uint32_t,uint32_t){assert(false);}void delayUs(unsigned){assert(false);}}; +struct Display13FBIRQIO { + uintptr_t base;bool readable(uint32_t r){return base && r<0x1000000 && !(r&3);} + uint32_t read(uint32_t r){assert(locks.back()==4);if(r==Display13FBIRQ::Master)assert((activeMap->reg(0x45404)&3)==3);return *reinterpret_cast(base+r);} + void write(uint32_t r,uint32_t v){assert(locks.back()==4); + if(r==Display13FBIRQ::Master){assert((activeMap->reg(0x45404)&3)==3);++activeMap->masterWrites; + if((activeMap->masterWrites==1 && activeMap->failMasterMask) || (activeMap->masterWrites==2 && activeMap->failMasterRestore))return; + } + auto ®=*reinterpret_cast(base+r); + if(r==Display13FBIRQ::PipeIIR(0))reg&=~v;else reg=v; + } +}; +struct {IORecursiveLock *lock=nullptr;uintptr_t thread=0;XELPComboClock::ClockAllocation allocation;}comboClockTransaction; +struct {IOLock *lock=nullptr;IOService *controller=nullptr;void *power=nullptr;IOMemoryMap *map=nullptr;IOWorkLoop *workLoop=nullptr;OSObject *dsiPort=nullptr; + Display13Transcoder::Bindings transcoders;Readout bootDSI;Display13Power::NativeBridge bridge;bool clockFaulted=false,memoryFaulted=false,watermarkFaulted=false; + Display13DBUF::State dbuf;unsigned dsiNativeWriters=0; + struct {IOService *memoryFramebuffer=nullptr;uintptr_t publishedScanout=0;struct {uintptr_t record=0;}scanout;bool destroyingPath=false,wmProgramming=false,wmCommitting=false;}pipe[3]; +}display13FBPower; +struct {uintptr_t base=0;IOSimpleLock *lock=nullptr;IOService *controller=nullptr;Display13FBIRQ::State state; + unsigned handlers=0,softwareHandlers=0,admittedPipes=0,blockedPipes=0;bool quiescing=false; + Display13FBIRQ::HandlerScopes scopes;}display13FBIRQ; +void adlnFault(bool &fault,unsigned){assert(locks==std::vector({1,2,3}));fault=true;} +struct {bool ready=true;}dsiFixedMode; + #ifndef DSI_SURFACE_REPLAY +struct {NativeBacking::Record *record=nullptr;bool modeReady=false;} dsiPendingBacking; +bool dsiPublishedTarget(IOService *){return false;} // This fixture retires backing; it does not retain it for wake. +#endif +bool dsiModeTarget(IOService *fb){return fb==display13FBPower.pipe[0].memoryFramebuffer;} +bool mappingBound=true,failDDIPut=false,failMemoryFree=false,lieMemoryFree=false,loseDBUF=false,loseIRQRestore=false,losePowerFree=false;unsigned memoryFrees=0; +bool adlnWriterMappingBound(){return mappingBound;} +bool adlnIRQOwner(IOService *p){return p && p==display13FBIRQ.controller;} +namespace TGLFBBinding {enum class Status{Complete,Unavailable};struct Binding{Status status=Status::Complete;unsigned pipe=0,ddi=0;};} +TGLFBBinding::Binding pathBinding; +TGLFBBinding::Binding adlnInspectPathBinding(IOService*,OSObject*,void*,unsigned){return pathBinding;} +void adlnIRQCache(IOService*,Display13FBIRQIO&){assert(locks.back()==4);} +void adlnPublishMemory(const char*,unsigned,void*,size_t){assert(locks.empty());} +void tick(unsigned us){assert(locks==std::vector({1,2,3}));auto &m=*activeMap; + if(!m.freeze && us>=1000)++m.reg(0x70040); + if(!(m.reg(0x7B008)&(1U<<31)))m.reg(0x7B008)&=~(1U<<30); + if(m.reg(0x6B0D8)&1)m.reg(0x6B0D8)|=1U<<16; + if(!(m.reg(0x64000)&(1U<<31)))m.reg(0x64000)|=0x80; + if(!(m.reg(0x46014)&(1U<<31)) && m.pllFail!=1)m.reg(0x46014)&=~(1U<<30); + if(!(m.reg(0x46014)&(1U<<27)) && m.pllFail!=2)m.reg(0x46014)&=~(1U<<26); + if(us==10 && m.pllFail==3)m.reg(0x45404)=0; + if(us==10 && m.pllFail==4)m.reg(0x16428C)^=1; + if(m.pending){auto c=display13FBPower.controller;*reinterpret_cast(c->bytes+0xE08)|=1;} + if(m.gtOnly){display13FBIRQ.handlers=1;display13FBIRQ.scopes.total=1;} +} +void IOSleep(unsigned ms){tick(ms*1000);}void IODelay(unsigned us){tick(us);} +struct XE { + uintptr_t adlnWorkLoopInGate=reinterpret_cast(inGate),adlnWorkLoopTryClose=reinterpret_cast(tryClose),adlnWorkLoopOpen=reinterpret_cast(open); + struct {unsigned dsi=1;operator unsigned()const{return dsi;}}adlnOutputWiring;void *adlnPlatform=nullptr; + bool dsiNativeWriterHooksReady=true,dsiResourcesReady=true,dsiStopAttempted=false,dsiIRQParked=false,dsiDDIReleased=false,dsiMemoryReleased=false; + bool dsiStopTest=false,dsiPowerTest=false; + Panel::Shutdown dsiPanelStop; + struct {bool sleeping=false,failed=false;unsigned cycles=0;Surface::Plane plane; + uint32_t cursorControl=0,cursorPosition=0,cursorBase=0;Restart::State host;Panel::Result panel;}dsiRuntimePower; + IOService *adlnTopologyController=nullptr;Profile dsiProfile;Boot dsiBoot;Quiesce::Result dsiStop;Clock::Result dsiClockStop; + bool dsiArmedBackingOwned(IOService *fb,OSObject*){return armedOwner && pendingFB==fb;} + bool retireDSINativeBacking(IOService *fb,OSObject*){ + if(!dsiPendingTarget(fb))return true; + assert(armedOwner && dsiStop.complete() && dsiIRQParked && dsiClockStop.complete() && dsiDDIReleased); + assert(locks==std::vector{1} && display13FBPower.workLoop->gate);++nativeRetires; + if(failNativeRetire)return false;pendingFB=nullptr;return true; + } + static bool adlnFreeStoppedPipe(IOService *fb){ + assert(!pendingFB); + assert(locks==std::vector{1} && display13FBPower.workLoop->gate); + assert(!display13FBPower.memoryFaulted && display13FBPower.bridge.pgRefs[0]==1); + assert(!comboClockTransaction.allocation.external[0] && !display13FBPower.bridge.ddiRefs[0]); + ++memoryFrees;if(losePowerFree)activeMap->reg(0x45404)=0;if(loseIRQRestore)activeMap->reg(Display13FBIRQ::Master)=0;if(loseDBUF)activeMap->reg(0x4438C)^=1;if(failMemoryFree)return false;if(lieMemoryFree)return true; + *reinterpret_cast(fb->bytes+0x4288)=0xFF; + *reinterpret_cast(fb->bytes+0x4348)=0; + display13FBPower.pipe[0].publishedScanout=0;display13FBPower.pipe[0].scanout.record=0; + display13FBPower.dbuf.pipe[0].consumers=display13FBPower.dbuf.pipe[0].unretired=0; + Display13DBUF::syncMasks(display13FBPower.dbuf); + return true; + } + static bool adlnPowerReference(void *power,unsigned operation,unsigned domain){ + assert(locks==std::vector({1,2}) && operation==4 && domain==0 && power==display13FBPower.power); + if(failDDIPut)return false; + IOLockLock(display13FBPower.lock);Display13FBBootIO io{display13FBPower.map}; + // Initial firmware DDI power is borrowed; releasing its actual native + // reference cannot manufacture a power-well write in this fixture. + struct PowerIO:Display13FBBootIO {void write(uint32_t,uint32_t){assert(false);}void delayUs(unsigned){assert(false);}} pio{io}; + const auto result=display13FBPower.bridge.putDDI(pio,0); + IOLockUnlock(display13FBPower.lock);return result.status==Display13Power::Status::Complete; + } + bool quiesceDSIPath(IOService*,IOService*,OSObject*,bool retain=false); +}; +BODY +templatevoid put(void *p,unsigned o,T v){memcpy(static_cast(p)+o,&v,sizeof(v));} +struct Case { + XE xe;IOService controller,fb;OSObject path,port;IOWorkLoop loop;IOMemoryMap map; + IORecursiveLock fbLock{1},clockLock{2};IOLock ownerLock;IOSimpleLock irqLock; + Case(){ + using Power=decltype(display13FBPower);display13FBPower.~Power();new (&display13FBPower) Power; + display13FBIRQ={};comboClockTransaction={};mappingBound=true;pathBinding={};activeMap=↦dsiFixedMode.ready=true;failDDIPut=failMemoryFree=lieMemoryFree=loseDBUF=loseIRQRestore=losePowerFree=false;memoryFrees=0; + IO defaults;for(const auto &v:defaults.regs)map.reg(v.first)=v.second; + map.reg(0x42084)=1U<<30;map.reg(0x6B0D0)=0x01001040;map.reg(0x6001C)=0x0437077F;map.reg(0x6B010)=map.reg(0x6B00C); + map.reg(0x45454)=3;map.reg(0x42000)=0x8E380000;map.reg(0x70040)=100; + // Keep a real peer IRQ/plane image live and make preservation observable. + map.reg(0x164280)&=~(1U<<11);map.reg(0x46010)=0xCC000000; + map.reg(0x71008)=0xC0000000;map.reg(0x71180)=0x94001000;map.reg(0x61400)=0x90000000; + map.reg(Display13FBIRQ::Master)=Display13FBIRQ::Enable; + map.reg(Display13FBIRQ::PipeIIR(0)+4)=Display13FBIRQ::PipeOwned; + map.reg(Display13FBIRQ::PipeIIR(1)+4)=Display13FBIRQ::PipeOwned; + auto &c=display13FBPower;auto &irq=display13FBIRQ;auto &clock=comboClockTransaction; + c.lock=&ownerLock;c.controller=&controller;c.power=&controller;c.map=↦c.workLoop=&loop;c.dsiPort=&port; + c.pipe[0].memoryFramebuffer=&fb;c.pipe[0].scanout.record=0x1000;c.pipe[0].publishedScanout=0x1000;c.transcoders.dsi0Pipe=0;c.dbuf.transcoders=c.transcoders; + c.dbuf.begun=true;c.dbuf.selected=3;c.dbuf.initialRequests=c.dbuf.requested=c.dbuf.powered=5; + c.dbuf.pipe[0].consumers=c.dbuf.pipe[0].unretired=1;c.dbuf.pipe[1].consumers=4;Display13DBUF::syncMasks(c.dbuf); + map.reg(Display13DBUF::regs[0])=map.reg(Display13DBUF::regs[2])=0xC0000000; + xe.adlnTopologyController=&controller;xe.dsiProfile=profile(); + c.bootDSI=decodeReadout([&](uint32_t r,uint32_t &v){v=map.reg(r);return true;},38400);assert(c.bootDSI.matches(xe.dsiProfile));xe.dsiBoot.source=c.bootDSI; + clock.lock=&clockLock;clock.allocation.external[0]=reinterpret_cast(&controller);clock.allocation.binding[0]=2; + irq.controller=&controller;irq.lock=&irqLock;irq.base=map.getVirtualAddress();irq.admittedPipes=3; + irq.state.begun=true;irq.state.enabled=true;irq.state.active=3;irq.state.configured=3;irq.state.transcoders=c.transcoders; + auto &b=c.bridge;auto &s=b.state;b.begun=true;b.selectedPorts=3;b.bootPort=0;b.bootAdopted=true;b.bootPath=reinterpret_cast(&path);s.ready=true;s.allowed=Display13Power::All; + const auto pg=Display13Power::dependencies(Display13Power::nativeScope(1)),ddi=Display13Power::dependencies(Display13Power::ddiDomain(0));s.borrowed=pg|ddi; + for(unsigned i=0;i(&controller))); + assert(map.reg(0x46010)==0xCC000000 && map.reg(0x45404)==0xFC0F); + assert(Display13FBMemory::field(&path,0x3644)==(xe.dsiIRQParked && xe.dsiClockStop.complete() && xe.dsiDDIReleased && xe.dsiMemoryReleased?1U:2U)); + } +}; +int main(){ + {Case c;pendingFB=&c.fb;armedOwner=true;nativeRetires=0; + assert(c.run() && nativeRetires==1 && !pendingFB && memoryFrees==1);armedOwner=false;} + {Case c;pendingFB=&c.fb;armedOwner=true;failNativeRetire=true;nativeRetires=0; + assert(!c.run() && nativeRetires==1 && pendingFB==&c.fb && !memoryFrees); + pendingFB=nullptr;armedOwner=failNativeRetire=false;} + + for(bool own:{false,true}){Case c;c.loop.gate=own;c.map.gtOnly=true; + put(&c.controller,0xE08,2U);put(&c.controller,0xE10,1U<<6); + assert(c.run() && c.xe.dsiStop.complete() && c.xe.dsiIRQParked); + assert(!display13FBPower.memoryFaulted); + assert(display13FBPower.dbuf.consumers==4 && !display13FBPower.dbuf.pipe[0].unretired); + assert(c.xe.dsiStop.writes==8 && display13FBIRQ.state.active==2 && display13FBIRQ.state.enabled); + assert(!display13FBIRQ.quiescing && !display13FBIRQ.blockedPipes && c.loop.gate==own); + assert(c.loop.opens==unsigned(!own) && c.loop.closes==unsigned(!own)); + assert(c.map.reg(0x71180)==0x94001000 && c.map.reg(Display13FBIRQ::PipeIIR(1)+4)==Display13FBIRQ::PipeOwned); + assert(Display13FBMemory::field(&c.controller,0xE08)==2 && Display13FBMemory::field(&c.controller,0xE10)==(1U<<6)); + assert(!dsiFixedMode.ready && c.xe.dsiDDIReleased && c.xe.dsiMemoryReleased && memoryFrees==1); + assert(c.xe.dsiClockStop.complete() && c.xe.dsiClockStop.writes==2 && !c.map.reg(0x46014)); + assert(display13FBPower.transcoders.dsi0Pipe==UINT32_MAX && display13FBIRQ.state.transcoders.dsi0Pipe==UINT32_MAX); + assert(display13FBIRQ.admittedPipes==2 && display13FBPower.dbuf.transcoders.dsi0Pipe==UINT32_MAX && !c.xe.dsiResourcesReady); + XELPComboClock::ClockSnapshot snapshot;snapshot.layout=XELPComboClock::Layout::Display13; + snapshot.mux[0]=c.map.reg(0x164280);snapshot.enable[0]=c.map.reg(0x46010);snapshot.enable[1]=c.map.reg(0x46014); + const auto reused=comboClockTransaction.allocation.choose(XELPComboClock::Port(0),snapshot,true); + assert(reused.valid && reused.pll==1 && !reused.keepPLL); + c.retained(true);const auto before=c.map.bar;assert(!c.run() && c.map.bar==before && c.xe.dsiIRQParked); // one-shot journal + } + {Case c;pendingFB=&c.fb;const auto before=c.map.bar;assert(!c.run() && c.map.bar==before);pendingFB=nullptr;} + {Case c;display13FBPower.dsiNativeWriters=1;const auto before=c.map.bar; + assert(!c.run() && c.map.bar==before && !c.xe.dsiStopAttempted && !memoryFrees); + display13FBPower.dsiNativeWriters=0;assert(c.run() && c.xe.dsiMemoryReleased);} + {Case c;c.xe.dsiNativeWriterHooksReady=false;const auto before=c.map.bar; + assert(!c.run() && c.map.bar==before && !c.xe.dsiStopAttempted);} + for(unsigned bad=0;bad<12;++bad){Case c; + switch(bad){case 0:c.loop.busy=true;break;case 1:mappingBound=false;break; + case 2:display13FBIRQ.softwareHandlers=1;break;case 3:display13FBIRQ.handlers=1;display13FBIRQ.scopes.total=1;display13FBIRQ.scopes.display=1;break; + case 4:put(&c.controller,0xE08,1U);break;case 5:put(&c.controller,0xE0C,1U);break; + case 6:put(&c.path,0x35F0,1U);break;case 7:comboClockTransaction.allocation.external[0]=0;break; + case 8:display13FBPower.bridge.ddiRefs[0]=0;break;case 9:display13FBIRQ.scopes.total=1;break; + case 10:put(&c.controller,0xE10,1U);break;case 11:pathBinding.ddi=1;break;} + const auto before=c.map.bar;assert(!c.run() && c.map.bar==before && !c.xe.dsiStopAttempted && !c.loop.gate); + } + {Case c;c.map.failMasterMask=true;const auto before=c.map.bar;assert(!c.run() && c.map.bar==before && !c.xe.dsiStopAttempted); + assert(display13FBIRQ.state.faulted && !display13FBIRQ.state.enabled);c.retained();} + {Case c;c.map.freeze=true;assert(!c.run() && c.xe.dsiStopAttempted && !c.xe.dsiIRQParked); + assert(c.xe.dsiStop.stage==Quiesce::Stage::Plane && display13FBIRQ.state.enabled && display13FBIRQ.state.active==3);c.retained();} + {Case c;c.map.pending=true;assert(!c.run() && c.xe.dsiStop.complete() && !c.xe.dsiIRQParked); + assert(display13FBIRQ.state.active==3 && Display13FBMemory::field(&c.controller,0xE08)==1);c.retained();} + {Case c;c.map.failMasterRestore=true;assert(!c.run() && c.xe.dsiStop.complete() && !c.xe.dsiIRQParked); + assert(display13FBIRQ.state.faulted && !display13FBIRQ.state.enabled && !c.map.reg(Display13FBIRQ::Master));c.retained(true);} + {Case c;failDDIPut=true;assert(!c.run() && c.xe.dsiClockStop.complete() && !c.xe.dsiDDIReleased); + assert(dsiFixedMode.ready && display13FBPower.bridge.pgRefs[0]==1 && display13FBPower.bridge.ddiRefs[0]==1);c.retained(true);} + {Case c;display13FBPower.bridge.bootAdopted=false;assert(!c.run() && c.xe.dsiClockStop.complete() && !c.xe.dsiDDIReleased); + assert(display13FBPower.bridge.ddiRefs[0]==1);c.retained(true);} + for(unsigned failure=0;failure<3;++failure){Case c; + if(!failure)display13FBPower.memoryFaulted=true;else if(failure==1)failMemoryFree=true;else lieMemoryFree=true; + assert(!c.run() && c.xe.dsiDDIReleased && !c.xe.dsiMemoryReleased); + assert(display13FBPower.memoryFaulted && memoryFrees==unsigned(failure!=0)); + assert(display13FBPower.pipe[0].scanout.record==0x1000 && Display13FBMemory::field(&c.fb,0x4348)==0x1000); + c.retained(true); + } + {Case c;loseDBUF=true;assert(!c.run() && c.xe.dsiDDIReleased && !c.xe.dsiMemoryReleased); + assert(memoryFrees==1 && !Display13FBMemory::field(&c.fb,0x4348) && display13FBPower.dbuf.faulted); + assert(dsiFixedMode.ready && display13FBPower.bridge.pgRefs[0]==1);c.retained(true); + } + {Case c;loseIRQRestore=true;assert(!c.run() && !c.xe.dsiMemoryReleased && memoryFrees==1); + assert(display13FBIRQ.state.faulted && !display13FBIRQ.state.enabled && !Display13FBMemory::field(&c.fb,0x4348)); + assert(dsiFixedMode.ready && display13FBPower.bridge.pgRefs[0]==1);c.retained(true); + } + {Case c;losePowerFree=true;assert(!c.run() && !c.xe.dsiMemoryReleased && memoryFrees==1); + assert(display13FBIRQ.state.faulted && !display13FBIRQ.state.enabled && display13FBPower.bridge.pgRefs[0]==1); + } + for(unsigned failure=1;failure<=4;++failure){Case c;c.map.pllFail=failure; + assert(!c.run() && c.xe.dsiStop.complete() && c.xe.dsiIRQParked==(failure!=3) && !c.xe.dsiClockStop.complete()); + assert(dsiFixedMode.ready); + assert(c.xe.dsiClockStop.status==(failure==1?Clock::Status::Disable:failure==2?Clock::Status::Power:failure==3?Clock::Status::Ownership:Clock::Status::Source)); + assert(comboClockTransaction.allocation.external[0]==reinterpret_cast(&c.controller)); + assert(display13FBPower.transcoders.dsi0Pipe==0 && display13FBIRQ.state.transcoders.dsi0Pipe==0 && c.xe.dsiResourcesReady); + assert(c.map.reg(0x46010)==0xCC000000); + const auto before=c.map.bar;assert(!c.run() && c.map.bar==before); + } + for(bool software:{false,true}){Case c; + if(software)comboClockTransaction.allocation.binding[1]=2;else c.map.reg(0x164280)|=1U<<2; + assert(!c.run() && c.xe.dsiStop.complete() && c.xe.dsiClockStop.status==Clock::Status::Peer); + assert(!c.xe.dsiClockStop.writes && c.map.reg(0x46014)==0xCC000000);c.retained(); + } + puts("PASS production DSI quiesce: real host/IRQ cores, FB-gate-clock-owner ordering, no wait under IRQ lock, actual queues/scopes, PLL/DDI/backing retirement, allocator lock boundary, actual empty-slot/DDB proof, GT/peer preservation and retained failures"); +} +'''.replace('FIXTURE',fixture).replace('BODY',body) +with tempfile.TemporaryDirectory(prefix='dsi-quiesce-adapter-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_readout.cpp b/tests/dsi_readout.cpp new file mode 100644 index 00000000..3d7223e5 --- /dev/null +++ b/tests/dsi_readout.cpp @@ -0,0 +1,64 @@ +#include "kern_dsi_readout.hpp" +#include +#include +#include +#include +#include +using namespace XELP::DSI; +using Registers=std::map; +Readout decode(const Registers ®s,uint32_t ref=38400) { + return decodeReadout([&](uint32_t a,uint32_t &v){auto i=regs.find(a);if(i==regs.end())return false;v=i->second;return true;},ref); +} +Registers firmware() { + // Numeric register facts from a DSI0/pipeA/PLL1 GOP; no firmware code. + return {{0x7B008,0xC0000000},{0x6B400,0x80030006},{0x45404,0xFC0F},{0x70008,0}, + {0x6B030,0x20130300},{0x6B094,1},{0x6B404,0},{0x6B034,0},{0x64000,0x80000000},{0x164280,0x1E07801}, + {0x46014,0xCC000000},{0x16428C,0x3001FB},{0x164290,0xE84}, + {0x6B000,0x04650437},{0x6B008,0x044F044D},{0x6B00C,0x07D1077F},{0x6B014,0x07A907A7}}; +} +int main(int argc,char **argv) { + auto regs=firmware();auto r=decode(regs); + assert(r.complete()&&r.identity.valid()&&r.identity.pipe==0&&r.identity.transcoder==0&&r.identity.phy==0&&r.identity.pll==1); + assert(r.normalConf==0&&r.activePipes()==1&&r.afeKHz==811500); + struct IO { + Registers regs;std::vector reads; + bool readable(uint32_t reg){return regs.count(reg);} + uint32_t read(uint32_t reg){reads.push_back(reg);return regs.at(reg);} + } io{regs,{}}; + io.regs[0x51004]=0x40000000; + assert(sourceUnchanged(io,r)); + io.regs[0x164280]^=0x808;assert(sourceUnchanged(io,r)); // peer B's PLL/gate only + io.regs[0x164280]^=1;assert(!sourceUnchanged(io,r)); + io.regs[0x164280]=r.mux;io.regs[0x51004]=0;assert(!sourceUnchanged(io,r)); + io.regs[0x51004]=0x40000000;io.regs[0x45404]=0;io.reads.clear(); + assert(!sourceUnchanged(io,r) && io.reads==std::vector{0x45404}); + Profile profile;profile.status=ProfileStatus::Valid;profile.lanes=4; + profile.mode={135255,1080,46,22,2,1920,82,40,2,86,154}; + assert(r.matches(profile)&&!r.matches(profile,1)); + for(const auto &entry:regs) { + auto missing=regs;missing.erase(entry.first);assert(!decode(missing).complete()); + auto absent=regs;absent[entry.first]=UINT32_MAX;assert(!decode(absent).complete()); + } + auto linux=regs;linux[0x164280]=0x1E06C04;linux[0x46010]=0xCC000000; + linux[0x164284]=0x34CDFB;linux[0x164288]=0xE84;linux[0x6B030]=0x20130304; + auto l=decode(linux);assert(l.complete()&&l.identity.pll==0&&l.afeKHz==811529&&l.matches(profile)&&l.ddiClockGated()); + // DSI0 may consume pipe B; neither its register bank nor PHY follows pipe. + auto b=regs;b[0x6B400]|=5<<12;b.erase(0x70008);b[0x71008]=0; + auto pipeB=decode(b);assert(pipeB.complete()&&pipeB.activePipes()==2&&pipeB.identity.pipe==1&&pipeB.identity.pll==1); + b[0x71008]=0xC0000000;assert(decode(b).status==ReadoutStatus::Routing); + for(auto pair:Registers{{0x7B008,0},{0x6B400,0x80034006},{0x6B094,0},{0x6B404,1}, + {0x6B034,1},{0x45404,1},{0x164280,0x1E07803},{0x46014,0x80000000},{0x64000,0}, + {0x6B000,0x04370465},{0x6B400,0x80130006}}) { + auto bad=regs;bad[pair.first]=pair.second;assert(!decode(bad).complete()); + } + auto transition=regs;transition[0x7B008]=0x80000000;assert(decode(transition).status==ReadoutStatus::Transition); + assert(!decode(regs,27000).complete()); + if(argc==2) { + std::ifstream f(argv[1]);assert(f);Registers saved;uint32_t a,v; + while(f>>std::hex>>a>>v){assert(saved.emplace(a,v).second);}assert(f.eof()); + auto actual=decode(saved);assert(actual.complete()&&actual.matches(profile)); + printf("saved DSI snapshot: pipe=%u transcoder=%u PHY=%u PLL=%u AFE=%u kHz normalCONF=%x\n", + actual.identity.pipe,actual.identity.transcoder,actual.identity.phy,actual.identity.pll,actual.afeKHz,actual.normalConf); + } + puts("DSI snapshot: separate pipe/transcoder/PHY/PLL, bounded clocks, missing/conflicting state: PASS"); +} diff --git a/tests/dsi_resources.py b/tests/dsi_resources.py new file mode 100644 index 00000000..489b45a2 --- /dev/null +++ b/tests/dsi_resources.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Replay the production DSI resource publication boundary and lock order.""" +from pathlib import Path +from production_source import read_source +import re, subprocess, tempfile +root=Path(__file__).resolve().parents[1] +source=read_source(root/'WhateverXe/kern_xe.cpp') +body=re.search(r'bool XE::bindDSIResources\(.*?\n\}',source,re.S)[0] +boot=(root/'tests/dsi_boot.cpp').read_text() +io=boot[boot.index('struct IO {'):boot.index('Boot capture(')] +code=r''' +#include "kern_dsi_ggtt.hpp" +#include "kern_dsi_backlight.hpp" +#include "kern_display13_power_bridge.hpp" +#include "kern_display13_fb_irq.hpp" +#include "kern_display13_fb_topology.hpp" +#include "kern_display13_clock.hpp" +#include "kern_xelp_combo_clock.hpp" +#include +#include +#include +#include +using namespace XELP::DSI; +IO_FIXTURE +unsigned lockLevel=0; +struct IORecursiveLock{};struct IOLock{};struct IOSimpleLock{}; +void IORecursiveLockLock(IORecursiveLock*){assert(lockLevel==0);lockLevel=1;} +void IORecursiveLockUnlock(IORecursiveLock*){assert(lockLevel==1);lockLevel=0;} +unsigned ownerParent=0; +void IOLockLock(IOLock*){assert(lockLevel<=1);ownerParent=lockLevel;lockLevel=2;} +void IOLockUnlock(IOLock*){assert(lockLevel==2);lockLevel=ownerParent;} +bool IOSimpleLockLockDisableInterrupt(IOSimpleLock*){assert(lockLevel==2);lockLevel=3;return true;} +void IOSimpleLockUnlockEnableInterrupt(IOSimpleLock*,bool saved){assert(lockLevel==3 && saved);lockLevel=2;} +struct IOService {alignas(8) uint8_t bytes[0x1C20]{};IOService *getMetaClass(){return this;}unsigned getClassSize(){return sizeof(bytes);}}; +namespace Display13FBMemory {templateT field(const void *p,unsigned o){T v;memcpy(&v,static_cast(p)+o,sizeof(v));return v;}} +struct Map:IO { + uint32_t dpt=0;bool loseWrite=false,sourceDrift=false; + uintptr_t getVirtualAddress(){return reinterpret_cast(&dpt)-0x42084;} + uint32_t read(uint32_t reg){if(reg==0x42084 && regs[reg]!=dpt){writes.push_back(reg);if(loseWrite)dpt=regs[reg];else regs[reg]=dpt;if(sourceDrift)regs[0x6B000]^=1;}return IO::read(reg);} +}; +using IOMemoryMap=Map; +void adlnFault(bool &fault,unsigned){assert(lockLevel==3);fault=true;} +struct Display13FBBootIO { + Map *map;bool readable(uint32_t r){return map && map->readable(r);} + uint32_t read(uint32_t r){return map->read(r);} +}; +struct {IORecursiveLock *lock;uintptr_t thread=0;XELPComboClock::ClockAllocation allocation;} comboClockTransaction; +struct {IOLock *lock;IOService *controller;Map *map;Display13Transcoder::Bindings transcoders; + Readout bootDSI;Display13Power::NativeBridge bridge; + bool engineEnabled=false,memoryFaulted=false,clockFaulted=false;struct {bool begun=false;} dbuf;} display13FBPower; +struct {uintptr_t base=0;IOSimpleLock *lock=nullptr;IOService *controller=nullptr;Display13FBIRQ::State state; + unsigned handlers=0,softwareHandlers=0,admittedPipes=0;bool quiescing=false;} display13FBIRQ; +XELPMemory::Layout xelpMemory; +unsigned published=0; +void dsiPublishIRQLive(){assert(lockLevel==0 && display13FBIRQ.controller && display13FBIRQ.lock);++published;} +struct XE { + bool dsiPanelTest=false,dsiPanelOnly=false,cycleOK=true;unsigned cycles=0;bool externalPreparation=false; + bool probeDSIPanel(IOService*,bool prepare){assert(lockLevel==0);++cycles;externalPreparation=prepare;return cycleOK;} + bool dsiCommandTest=false;bool probeDSICommands(IOService*){assert(lockLevel==0);return true;} + bool display13FBIRQReady=true,dsiNativeWriterHooksReady=true,dsiResourcesReady=false;IOService *adlnTopologyController; + Display13OutputWiring adlnOutputWiring; + Profile dsiProfile;Boot dsiBoot;GGTTMode dsiGGTT;Backlight dsiBacklight; + void publishDSIBacklight(){assert(lockLevel==0);} + bool bindDSIResources(IOService *); +}; +BODY +int main(){ + IORecursiveLock clockLock;IOLock ownerLock;IOSimpleLock irqLock;IOService controller;Map map; + auto ptr=&irqLock;memcpy(controller.bytes+0x90,&ptr,sizeof(ptr)); + auto &c=display13FBPower;auto &irq=display13FBIRQ;auto &clock=comboClockTransaction; + c.lock=&ownerLock;c.controller=&controller;c.map=↦clock.lock=&clockLock; + XE xe;xe.adlnOutputWiring.dsi=1;xe.adlnOutputWiring.valid=true;xe.adlnTopologyController=&controller;xe.dsiProfile=profile();xelpMemory=map.memory; + xe.dsiBoot=captureBoot(map,AlderLakeN::readDisplay(map,true),xe.dsiProfile,map.memory,true); + assert(verifyBootPrefix(map,xe.dsiBoot,map.memory)); + xe.dsiProfile.precision=8;xe.dsiProfile.minimum=6; + map.regs[Backlight::Control]=1U<<31;map.regs[Backlight::Period]=480;map.regs[Backlight::Duty]=240; + map.regs[0x45454]=3;map.regs[0x42000]=0x8E380000; + auto &b=c.bridge;auto &s=b.state;b.begun=true;b.selectedPorts=3;s.ready=true;s.allowed=Display13Power::All; + const auto pg=Display13Power::dependencies(Display13Power::nativeScope(1)); + const auto ddi=Display13Power::dependencies(Display13Power::ddiDomain(0));s.borrowed=pg|ddi; + for(unsigned i=0;i(&controller) && clock.allocation.binding[0]==2); + assert(!xe.bindDSIResources(&controller) && !lockLevel && map.writes==std::vector{0x42084}); + assert(xe.dsiGGTT.ready() && xe.dsiGGTT.writes==1 && xe.dsiBoot.dpt==(1U<<30)); + assert(b.pgRefs[0]==1 && b.ddiRefs[0]==1); // publication did not acquire/release someone else's lease + // Actual production dispatch: future hotplug requires preparation even + // without a host-test argument; panel-only and an already-high clock do not. + for(unsigned scenario=0;scenario<6;++scenario) { + clock.allocation={};irq={};c.transcoders={};c.bootDSI={};c.memoryFaulted=false; + xe.dsiResourcesReady=false;xe.dsiGGTT={};xe.dsiBoot=initialBoot;xe.cycles=0;xe.externalPreparation=false; + xe.adlnOutputWiring.hdmi=scenario==0?2:0;xe.adlnOutputWiring.ddc[1]=scenario==0?2:0; + xe.adlnOutputWiring.dp=xe.adlnOutputWiring.typeC=scenario==0?0:8; + xe.dsiPanelOnly=scenario==2;xe.cycleOK=scenario!=4; + map=Map{};map.regs[0x45454]=3;map.regs[0x42000]=0x8E380000; + map.regs[Display13Clock::DSSM]=2U<<29;map.regs[Display13Clock::Squash]=0; + map.regs[Display13Clock::PLL]=scenario==3?0xC0000022:0xC000000E; + map.regs[Display13Clock::Control]=scenario==3?0x380518:0x780164; + if(scenario==5)map.regs[Display13Clock::PLL]=UINT32_MAX; + assert(xe.bindDSIResources(&controller)==(scenario!=4 && scenario!=5)); + const bool needsCycle=scenario==0 || scenario==1 || scenario==4; + assert(xe.cycles==unsigned(needsCycle) && xe.externalPreparation==needsCycle && !lockLevel); + } + xe.adlnOutputWiring.hdmi=xe.adlnOutputWiring.ddc[1]=xe.adlnOutputWiring.dp=xe.adlnOutputWiring.typeC=0; + xe.dsiPanelOnly=false;xe.cycleOK=true; + for(bool lost:{false,true}){ + clock.allocation={};irq={};c.transcoders={};c.bootDSI={};c.memoryFaulted=false; + xe.dsiResourcesReady=false;xe.dsiGGTT={};xe.dsiBoot=initialBoot; + map=Map{};map.regs[0x45454]=3;map.regs[0x42000]=0x8E380000; + map.loseWrite=lost;map.sourceDrift=!lost; + assert(!xe.bindDSIResources(&controller) && !lockLevel && c.memoryFaulted && !xe.dsiResourcesReady); + assert(!irq.controller && !irq.admittedPipes && c.transcoders.dsi0Pipe==UINT32_MAX && !xe.dsiBoot.usable()); + assert(clock.allocation.external[0]==reinterpret_cast(&controller) && b.pgRefs[0]==1 && b.ddiRefs[0]==1); + assert(map.writes==std::vector{0x42084} && xe.dsiGGTT.writes==1); + assert(!xe.bindDSIResources(&controller) && map.writes.size()==1); + } + puts("PASS production DSI resources: clock-owner-IRQ lock order, retained leases/source, PLL ownership, GGTT readback, atomic mapping publication and retained leases after write failure"); +} +'''.replace('IO_FIXTURE',io).replace('BODY',body) +# No kernel/Apple object contents are loaded; these leaves model publication only. +with tempfile.TemporaryDirectory(prefix='dsi-resources-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_restart.py b/tests/dsi_restart.py new file mode 100644 index 00000000..98283ca5 --- /dev/null +++ b/tests/dsi_restart.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +"""Stopped-host restart with register side effects and optional saved VBT input.""" +from pathlib import Path +import subprocess, tempfile, sys +root=Path(__file__).resolve().parents[1] +boot=(root/'tests/dsi_boot.cpp').read_text() +io=boot[boot.index('struct IO {'):boot.index('Boot capture(')] +profile=(root/'tests/dsi_profile.cpp').read_text() +fixture=profile[profile.index('using Bytes='):profile.index('int main(')] +code=r''' +#include "kern_dsi_restart.hpp" +#include "kern_dsi_panel.hpp" +#include "kern_xelp_memory_layout.hpp" +#include +#include +#include +#include +#include +using namespace XELP::DSI; +IO_FIXTURE +PROFILE_FIXTURE +struct Sim:IO { + bool owned=true,dropPLLDisable=false,dropPLLPower=false,dropPLLLock=false,dropBuffer=false,dropLink=false; + bool ulpsRequest=false; + uint32_t drop=UINT32_MAX,elapsed=0; + std::vector gpioTrace; + bool gpio(unsigned role,unsigned value){const auto r=Panel::gpio(*this,role,value);if(r.complete)gpioTrace.push_back(role*2+value);return r.complete;} + bool ready(){return owned;} + void write(uint32_t reg,uint32_t value){ + assert(owned);writes.push_back(reg);if(reg==drop)return; + if(reg==0x7B008){regs[reg]=(value&~(1U<<30))|(regs[reg]&(1U<<30));return;} + if(reg==0x1621A0){regs[reg]=value|0x400;return;} // non-field status survives a zero write + if(reg==0x46014){regs[reg]=(value&~0x44000000U)|(regs[reg]&0x44000000U);return;} + if(reg==0x64000){regs[reg]=(value&~0x80U)|(regs[reg]&0x80);return;} + if(reg==0x6B0D8){ulpsRequest=value&1;regs[reg]=(value&~0x30001U)|(regs[reg]&0x30000);return;} + if(reg==0x6B400 && !(value&(1U<<31))){ulpsRequest=false;regs[0x6B0D8]=0;regs[0x6B030]&=~(1U<<20);} + if(reg==0x6B030){regs[reg]=(value&~(1U<<20))|(regs[reg]&(1U<<20));return;} + if(reg==0x162604 || reg==0x162688 || reg==0x162694){ + for(unsigned lane=0;lane<4;++lane)regs[reg+0x200+0x100*lane]=value; + return; + } + regs[reg]=value; + } + void delayUs(unsigned us){ + elapsed+=us; + auto &conf=regs[0x7B008];if(conf&(1U<<31))conf|=1U<<30;else conf&=~(1U<<30); + if(us>=1000 && (conf&0xC0000000U)==0xC0000000U){++regs[0x70040];regs[0x701AC]=regs[0x7019C];} + auto &pll=regs[0x46014]; + if(pll&(1U<<27)){if(!dropPLLPower)pll|=1U<<26;}else if(!dropPLLPower)pll&=~(1U<<26); + if(pll&(1U<<31)){if(!dropPLLLock)pll|=1U<<30;}else if(!dropPLLDisable)pll&=~(1U<<30); + if(!dropBuffer){if(regs[0x64000]&(1U<<31))regs[0x64000]&=~0x80U;else regs[0x64000]|=0x80;} + if(ulpsRequest)regs[0x6B0D8]|=1U<<16; + if(!dropLink && (regs[0x6B400]&(1U<<31)))regs[0x6B030]|=1U<<20; + } +}; +struct Fixture { + Sim io;Bytes vbt;Profile p;Readout source;Restart::State state; + XELPComboClock::ClockAllocation allocation; + Fixture(){vbt=fixture();p=parse(vbt);source=decodeReadout([&](uint32_t r,uint32_t &v){v=io.read(r);return true;},38400); + assert(source.matches(p));allocation.external[0]=1;allocation.binding[0]=2; + io.regs[0x7B008]=0;io.regs[0x70180]&=~(1U<<31);io.regs[0x6B0D0]=0x1040; + io.regs[0x164280]|=1U<<10;io.regs[0x71180]=0x94000000;io.regs[0x64100]=0x80000000; + } + bool stop(){return Restart::stop(io,source,p,allocation,1,state);} + bool start(){return Restart::start(io,allocation,1,state);} +}; +struct Lifecycle { + XELPComboClock::ClockAllocation &allocation;Restart::State &state;bool breakStart=false; + bool stop(Sim &io,const Readout &source,const Profile &p){return Restart::stop(io,source,p,allocation,1,state);} + bool start(Sim &io,Readout &source){ + if(breakStart)io.dropPLLPower=true; + if(!Restart::start(io,allocation,1,state))return false; + source=Restart::expected(source,state);return source.complete(); + } +}; +void panelCycle(Fixture &f,bool fail=false){ + f.io.regs[0x7B008]=0xC0000000;f.io.regs[0x70180]=0x94000008; + f.io.regs[0x42084]=1U<<30;f.io.regs[0x6001C]=0x0437077F;f.io.regs[0x6B010]=f.io.regs[0x6B00C]; + f.io.regs[0xC7204]=15;f.io.regs[0xC4030]=0x84; + f.source.mux=f.io.regs[0x164280]; + Panel::Result panel;Lifecycle lifecycle{f.allocation,f.state,fail}; + Panel::cycle(f.io,f.source,f.p,0x94000008,0,panel,lifecycle); + if(fail){assert(!panel.complete() && panel.stage==Panel::Stage::HostOn && panel.error==Panel::Error::Host); + assert(!(f.io.regs[0x70180]&(1U<<31)) && !f.io.regs[0x7B008]);return;} + if(!panel.complete())fprintf(stderr,"panel stage=%u error=%u reg=%x value=%x host=%u seq=%u restartStage=%u error=%u reg=%x value=%x\n",unsigned(panel.stage),unsigned(panel.error),panel.reg,panel.value,unsigned(panel.host.error),panel.host.sequenceId,unsigned(f.state.stage),unsigned(f.state.error),f.state.reg,f.state.value); + assert(panel.complete() && panel.source.matches(f.p) && panel.source.cfg0!=f.source.cfg0); + assert(Surface::source(f.io,panel.source) && !Surface::source(f.io,f.source)); + assert(panel.host.completedOperations==f.p.sequences.packets+f.p.sequences.delays+f.p.sequences.gpios); + assert(f.io.regs[0x70180]==0x94000008 && f.io.regs[0x7B008]==0xC0000000); + assert(f.allocation.external[0]==1 && f.allocation.binding[0]==2); +} +void retainedCycle(Fixture &f,unsigned fault=0){ + f.io.regs[0x7B008]=0xC0000000;f.io.regs[0x70180]=0x94001000;f.io.regs[0x7019C]=f.io.regs[0x701AC]=0x44E00000; + f.io.regs[0x42084]=1U<<30;f.io.regs[0x6001C]=0x0437077F;f.io.regs[0x6B010]=f.io.regs[0x6B00C]; + f.io.regs[0xC7204]=15;f.io.regs[0xC4030]=0x84;f.source.mux=f.io.regs[0x164280]; + Panel::Shutdown shutdown; + const auto stopped=Quiesce::run(f.io,f.source,f.p,shutdown); + assert(stopped.complete() && shutdown.complete); + auto count=f.io.writes.size(); + if(fault==1)f.allocation.external[0]=2; + if(fault==2)f.io.regs[0x7B008]=0xC0000000; + if(fault==3)f.io.regs[0x16428C]^=1; + if(fault){assert(!Restart::stopQuiesced(f.io,f.source,f.p,stopped,f.allocation,1,f.state)); + assert(f.io.writes.size()==count && !f.state.stopped);return;} + assert(Restart::stopQuiesced(f.io,f.source,f.p,stopped,f.allocation,1,f.state)); + assert(f.state.stopped && !f.io.regs[0x46014] && f.allocation.external[0]==1 && f.allocation.binding[0]==2); + Panel::Result panel;Lifecycle lifecycle{f.allocation,f.state}; + Panel::cycle(f.io,f.source,f.p,0x94001000,0x44E00000,panel,lifecycle,&shutdown); + if(!panel.complete())fprintf(stderr,"resume stage=%u error=%u reg=%x host=%u restart=%u/%u\n",unsigned(panel.stage),unsigned(panel.error),panel.reg,unsigned(panel.host.error),unsigned(f.state.stage),unsigned(f.state.error)); + assert(panel.complete() && f.state.started && Surface::source(f.io,panel.source)); + assert(f.io.regs[0x70180]==0x94001000 && f.io.regs[0x7019C]==0x44E00000 && f.io.regs[0x701AC]==0x44E00000 && f.io.regs[0x46014]==0xCC000000); + assert(f.allocation.external[0]==1 && f.allocation.binding[0]==2); + assert(f.io.regs[0x71180]==0x94000000 && f.io.regs[0x64100]==0x80000000); +} +void successful(Fixture &f){ + assert(f.stop() && f.state.stopped && !f.state.started); + assert(f.io.regs[0x46014]==0 && f.io.regs[0x6B400]==0x30006 && f.io.regs[0x64000]==0x80 && f.io.regs[0x6B094]==0); + const auto oldCfg=f.source.cfg0;f.io.regs[0x16428C]=0x12345; // prove source PLL dump is not replayed + assert(f.start() && f.state.stage==Restart::Stage::Ready); + assert(f.io.regs[0x16428C]==f.state.config.pll.cfg0() && f.io.regs[0x16428C]!=oldCfg); + assert(f.io.regs[0x46014]==0xCC000000 && f.io.regs[0x64000]==0x80000000 && f.io.regs[0x6B094]==1); + assert(f.io.regs[0x6B030]==0x20130304 && f.io.regs[0x6B400]==f.state.config.function); + assert(f.io.regs[0x6B090]==56 && f.io.regs[0x162190]==56 && (f.io.regs[0x1621A0]&0xFF0000)==0 && (f.io.regs[0x1621A0]&0x400)); + assert(f.io.regs[0x162180]==0xC8690000 && f.io.regs[0x162184]==0x84830083); + assert(f.io.regs[0x162894]==0xE0080030 && f.io.regs[0x162888]==0x1598); + for(unsigned lane=0;lane<4;++lane){assert(f.io.regs[0x162804+0x100*lane]==4); + assert(f.io.regs[0x162890+0x100*lane]==((lane==2?0:1U<<31)|0x3F));} + assert(f.io.regs[0x70180]==0x14000008 && !f.io.regs[0x7B008]); // video remains off for INIT_OTP + assert(f.io.regs[0x71180]==0x94000000 && f.io.regs[0x64100]==0x80000000); + assert(f.allocation.external[0]==1 && f.allocation.binding[0]==2); + for(auto reg:f.io.writes){if(!Restart::writable(reg,1))fprintf(stderr,"unlisted write %x\n",reg);assert(Restart::writable(reg,1) && reg!=0x46010 && reg!=0x164284);} + auto count=f.io.writes.size();assert(!f.start() && !f.stop() && f.io.writes.size()==count); + // Once the separate panel/plane adapter starts video, the decoder must + // accept the new source with profile-derived PLL/timing/polarity. + f.io.regs[0x7B008]=0xC0000000; + auto rebuilt=decodeReadout([&](uint32_t r,uint32_t &v){v=f.io.read(r);return true;},38400); + assert(rebuilt.matches(f.p) && rebuilt.afeKHz==811529); +} +int main(int argc,char **argv){ + for(unsigned fault=0;fault<7;++fault){Fixture f; + assert(!Restart::retainedStopped(f.io,f.source,f.allocation,1,f.state)); + assert(f.stop());const auto count=f.io.writes.size();const auto state=f.state; + if(fault==1)f.allocation.external[0]=2; + if(fault==2)f.io.regs[0x46014]=0xCC000000; + if(fault==3)f.io.regs[0x6B400]|=1U<<31; + if(fault==4)f.io.regs[0x7B008]=0xC0000000; + if(fault==5)f.allocation.binding[1]=2; + if(fault==6)f.io.owned=false; + assert(Restart::retainedStopped(f.io,f.source,f.allocation,1,f.state)==!fault); + assert(f.io.writes.size()==count && !memcmp(&state,&f.state,sizeof(state))); + if(!fault){XELPComboClock::ClockSnapshot snapshot;snapshot.layout=XELPComboClock::Layout::Display13; + snapshot.mux[0]=f.io.regs[0x164280]|(1U<<11);snapshot.enable[1]=0; + const auto choice=f.allocation.choose(XELPComboClock::Port(1),snapshot,true); + assert(choice.valid && choice.pll==0); // powered-off DSI PLL1 remains reserved + assert(f.start()); + } + } + for(unsigned clock:{350000U,450000U,750000U}) { + Fixture f;assert(f.stop());f.p.mode.clockKHz=clock; + f.state.config=Restart::plan(f.p,38400);assert(f.state.config.valid && f.start()); + assert(f.io.regs[0x6B090]==f.state.config.link.escapeDivider); + f.io.regs[0x7B008]=0xC0000000; + auto link=decodeReadout([&](uint32_t r,uint32_t &v){v=f.io.read(r);return true;},38400); + assert(link.matches(f.p)); // program/read back, not just calculate a PLL + } + {Fixture f;successful(f);} + for(unsigned fault=0;fault<4;++fault){Fixture f;retainedCycle(f,fault);} + {Fixture f;panelCycle(f);} + {Fixture f;panelCycle(f,true);} + for(unsigned fault=0;fault<6;++fault){Fixture f; + if(fault==0)f.io.owned=false; + if(fault==1)f.allocation.binding[1]=2; + if(fault==2)f.io.regs[0x7B008]=0xC0000000; + if(fault==3)f.io.regs[0x6B0D0]=0; + if(fault==4)f.io.regs[0x68180]=1U<<31; + if(fault==5)f.io.regs[0x78000]=1U<<31; + assert(!f.stop() && f.io.writes.empty() && !f.start()); + } + for(unsigned fault=0;fault<4;++fault){Fixture f; + if(fault==0)f.io.dropPLLDisable=true; + if(fault==1)f.io.dropPLLPower=true; + if(fault==2)f.io.dropBuffer=true; + if(fault==3)f.io.drop=0x6B400; + assert(!f.stop() && f.state.error!=Restart::Error::None && !f.start()); + const auto count=f.io.writes.size();assert(!f.stop() && f.io.writes.size()==count); + assert(f.allocation.external[0]==1 && f.allocation.binding[0]==2 && f.io.elapsed<=1020); + } + for(unsigned fault=0;fault<6;++fault){Fixture f;assert(f.stop()); + if(fault==0)f.io.dropPLLPower=true; + if(fault==1)f.io.dropPLLLock=true; + if(fault==2)f.io.dropBuffer=true; + if(fault==3)f.io.dropLink=true; + if(fault==4)f.io.drop=0x162694; + if(fault==5)f.allocation.binding[1]=2; + assert(!f.start() && f.state.error!=Restart::Error::None); + const auto count=f.io.writes.size();assert(!f.start() && f.io.writes.size()==count); + assert(f.allocation.external[0]==1 && f.allocation.binding[0]==2 && !f.io.regs[0x7B008]); + assert(f.io.elapsed<5000); + } + if(argc==2){ + {Fixture f;std::ifstream in(argv[1],std::ios::binary);assert(in);f.vbt=Bytes{std::istreambuf_iterator(in),{}}; + f.p=parseProfile({f.vbt.data(),f.vbt.size()},true);retainedCycle(f);} + for(bool fail:{false,true}){Fixture f;std::ifstream in(argv[1],std::ios::binary);assert(in); + f.vbt=Bytes{std::istreambuf_iterator(in),{}};f.p=parseProfile({f.vbt.data(),f.vbt.size()},true); + panelCycle(f,fail); + assert(f.io.gpioTrace==std::vector(fail?std::vector{4,0,2,3,1,0,1}:std::vector{4,0,2,3,1,0,1,5})); + } + Fixture f;std::ifstream in(argv[1],std::ios::binary);assert(in);f.vbt=Bytes{std::istreambuf_iterator(in),{}}; + f.p=parseProfile({f.vbt.data(),f.vbt.size()},true);assert(f.p.valid());successful(f); + printf("saved VBT restart plan: init=%u timeout=%u/%u/%u function=%08x cfg0=%08x writes=%u pollUs=%u; simulation only\n", + f.state.config.init,f.state.config.hsTimeout,f.state.config.lpTimeout,f.state.config.taTimeout, + f.state.config.function,f.state.config.pll.cfg0(),f.state.writes,f.state.waitUs); + } + puts("PASS DSI host restart: exclusive ownership, off/on sequence, profile reconstruction, peer preservation, retained resources on bounded failure"); +} +'''.replace('IO_FIXTURE',io).replace('PROFILE_FIXTURE',fixture) +with tempfile.TemporaryDirectory(prefix='dsi-restart-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test'),*sys.argv[1:]],check=True) diff --git a/tests/dsi_sequence.cpp b/tests/dsi_sequence.cpp new file mode 100644 index 00000000..3e42d96d --- /dev/null +++ b/tests/dsi_sequence.cpp @@ -0,0 +1,51 @@ +#include "kern_dsi_sequence.hpp" +#include +#include +#include +#include +#include +using namespace XELP::DSI; +using Bytes=std::vector; +void word(Bytes &b,uint32_t n){for(unsigned i=0;i<4;++i)b.push_back(n>>(8*i));} +Bytes panel(const Bytes &ops,uint8_t id=3){Bytes b{id};word(b,ops.size());b.insert(b.end(),ops.begin(),ops.end());b.push_back(0);return b;} +Bytes block(const Bytes &data){Bytes b{3};word(b,data.size()+5);b.push_back(0);word(b,data.size());b.insert(b.end(),data.begin(),data.end());return b;} +Sequences parse(const Bytes &b){return parseSequences({b.data(),b.size()},0);} +void rejected(const Bytes &b){auto r=parse(b);assert(!r.valid());assert(!r.packets&&!r.delays&&!r.gpios);for(auto s:r.sequence)assert(!s.data);} +int main(int argc,char **argv) { + // Synthetic standard DCS command, delay and native GPIO; no vendor blob. + Bytes ops {1,5,0,5,1,0,0x29,2,4,0x10,0x27,0,0,3,3,0,2,1,0}; + auto b=block(panel(ops));auto r=parse(b); + assert(r.valid()&&r.packets==1&&r.delays==1&&r.gpios==1&&r.delayUs==10000); + for(size_t n=0;n>{{0,4},{1,255},{5,6},{10,12},{11,255}}) { + auto bad=b;bad[pair.first]=pair.second;rejected(bad); + } + for(auto op:Bytes{4,5,6,7,255}){auto bad=ops;bad[0]=op;rejected(block(panel(bad)));} + for(auto flags:Bytes{1,2,8,32,255}){auto bad=ops;bad[2]=flags;rejected(block(panel(bad)));} + auto bad=ops;bad[3]=6;rejected(block(panel(bad))); // read unsupported + bad=ops;bad[4]=2;rejected(block(panel(bad))); // wrong packet length + bad=ops;bad.pop_back();rejected(block(panel(bad))); // no operation END + bad=ops;bad.push_back(0);rejected(block(panel(bad))); // bytes after END + bad=ops;bad[16]=3;rejected(block(panel(bad))); // unimplemented GPIO role + for(unsigned role:{0U,1U,2U,5U,6U,7U}) { + auto gpio=ops;gpio[16]=role;assert(parse(block(panel(gpio))).valid()); + } + for(unsigned role:{3U,4U,8U,9U,255U}) { + auto gpio=ops;gpio[16]=role;rejected(block(panel(gpio))); + } + auto data=panel(ops);data.pop_back();auto second=panel(ops);data.insert(data.end(),second.begin(),second.end()); + rejected(block(data)); // duplicate sequence + auto duplicate=b;duplicate.insert(duplicate.end(),b.begin()+5,b.end()); + const auto size=duplicate.size()-5;for(unsigned i=0;i<4;++i)duplicate[1+i]=size>>(8*i); + rejected(duplicate); // duplicate panel + assert(parseSequences({b.data(),b.size()},1).status==SequenceStatus::Panel); + assert(!parseSequences({nullptr,5},0).valid()); + if(argc==2) { + std::ifstream f(argv[1],std::ios::binary);assert(f); + Bytes real{std::istreambuf_iterator(f),{}};auto q=parse(real); + assert(q.valid()&&q.packets==359&&q.gpios==8&&q.delays==11&&q.delayUs==486000); + assert(!q.sequence[3].data&&q.sequence[2].data&&q.sequence[10].data&&q.sequence[11].data); + printf("saved profile: packets=%u delays=%u GPIO=%u delayUs=%llu\n",q.packets,q.delays,q.gpios,(unsigned long long)q.delayUs); + } + puts("DSI sequence bounds/unsupported operations/transactional publication: PASS"); +} diff --git a/tests/dsi_session.cpp b/tests/dsi_session.cpp new file mode 100644 index 00000000..e3ed8d41 --- /dev/null +++ b/tests/dsi_session.cpp @@ -0,0 +1,49 @@ +#include "kern_dsi_session.hpp" +#include "kern_xelp_combo_clock.hpp" +#include +#include +using namespace XELP::DSI; +int main() { + Profile p;p.status=ProfileStatus::Valid;p.lanes=4; + p.mode={135255,1080,46,22,2,1920,82,40,2,86,154};const auto plan=linkPlan(p); + Readout r;r.status=ReadoutStatus::Complete;r.identity={0,0,0,1};r.afeKHz=811500; + r.htotal=plan.htotal;r.hsync=plan.hsync;r.vtotal=plan.vtotal;r.vsync=plan.vsync; + Capabilities caps;caps.installed=Capabilities::Adoption;Session s; + for(unsigned bit=0;bit<8;++bit){auto missing=caps;missing.installed&=~(1U< +#include +#include +#include +#include +#include +#include +using namespace XELP::DSI; +IO_FIXTURE +struct Sim:IO { + uint64_t elapsed=0,clock=614400000;uint32_t drop=UINT32_MAX,drift=0; + bool frozen=false,oldLive=false,backward=false,unbound=false,loseBinding=false,immediate=false; + bool ready(){return !unbound && !(loseBinding && !writes.empty());} + uint64_t nativeClockHz(){return clock;}uint64_t nowUs(){return elapsed;} + void write(uint32_t r,uint32_t v){writes.push_back(r);if(r!=drop)regs[r]=v;if(r==0x7019C && immediate){++regs[0x70040];regs[0x701AC]=v;}} + void delayUs(unsigned us){elapsed+=us;if(elapsed==2000){ + if(!frozen)regs[0x70040]=backward?99:regs[0x70040]+1; + if(!oldLive)regs[0x701AC]=regs[0x7019C]; + if(drift)regs[drift]^=drift==0x42084 ? 1U<<30 : 1; + }} +}; +struct Fixture { + Sim io;Profile p=profile();Readout link;Session session; + Surface::Plane current{0x94000008,0x5000,68,0x077F0437,0,0,0}; + Surface::Plane next{0x94001008,0x7000,34,0x077F0437,0x10000000,0,0}; + Surface::Backing backing{201,0x10000000,4352*1920,4352}; + Display13WM::Plan plan;Display13WM::Snapshot snapshot; + Fixture(){ + link=decodeReadout([&](uint32_t r,uint32_t &v){v=io.read(r);return true;},38400);assert(link.matches(p)); + Capabilities caps;caps.installed=Capabilities::Adoption;assert(session.bind(caps,link,p,1,100,200)); + plan=Display13WM::plan(1080,1126,135255000,2048,Display13WM::decodeLatency(0x63503300,0x00909090,30),true,true,1920); + assert(plan.status==Display13WM::Status::Complete); + snapshot.primary=&plan;snapshot.primaryDDB=0x07FF0000;snapshot.primarySurface=next.surface; + snapshot.nativeClockHz=io.clock;snapshot.pipeLineTime=Display13WM::pipeLineTime(1126,135255000); + snapshot.pipePixelHz=135255000;snapshot.transcoders.dsi0Pipe=0; + io.regs[0x42084]=1U<<30;io.regs[0x6001C]=0x0437077F;io.regs[0x6B010]=link.vtotal; + io.regs[0x7027C]=snapshot.primaryDDB;io.regs[0x70040]=100; + uint32_t values[7];Surface::values(current,values); + for(unsigned i=0;i<7;++i)io.regs[Surface::Registers[i]]=values[i]; + } + Surface::Result run(){return Surface::change(io,session,link,p,current,next,backing,snapshot);} +}; +templatevoid put(void *p,unsigned offset,T v){memcpy(static_cast(p)+offset,&v,sizeof(v));} +templateT get(const void *p,unsigned offset){T v;memcpy(&v,static_cast(p)+offset,sizeof(v));return v;} +void native(const char *path){ + std::ifstream file(path,std::ios::binary);std::vector bytes((std::istreambuf_iterator(file)),{});assert(!bytes.empty()); + constexpr size_t size=0x2000000;assert(bytes.size()(mmap(nullptr,size,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0)); + assert(image!=MAP_FAILED);memcpy(image,bytes.data(),bytes.size()); + for(bool y:{false,true}){ + Fixture f;alignas(8) uint8_t fb[0x4B90]{},controller[0x1C20]{},plane[0x1C8]{},display[0x3730]{}; + put(plane,0x68,fb);put(fb,0x1D0,controller);put(fb,0x4330,f.next.surface);put(fb,0x4334,uint32_t(f.backing.pitch)); + put(fb,0x4A18,uint8_t(y));put(plane,0x100,f.current.control);put(plane,0x104,f.current.color); + put(plane,0x118,f.current.stride);put(display,0xF8,uint32_t(f.p.mode.width));put(display,0xFC,uint32_t(f.p.mode.height)); + reinterpret_cast(image+0x1F50)(plane,display); + f.next={get(plane,0x100),get(plane,0x104),get(plane,0x118), + get(plane,0x11C),get(plane,0x120),0,get(plane,0x110)}; + assert(((f.next.control>>10)&7)==(y?4U:1U)); + if(y)assert(f.run().status==Surface::Status::Complete); + else assert(!Surface::target(f.next,f.p,f.backing)); // X is outside the shared WM contract + } + assert(!munmap(image,size));puts("PASS exact donor setupPlane cache feeds the DSI Y surface transaction; X rejected before MMIO"); +} +int main(int argc,char **argv){ + {Fixture f;const auto result=f.run();assert(result.status==Surface::Status::Complete && result.writes==17); + assert(result.latch.status==Display13WM::Status::Complete && result.latch.frameBefore==100 && result.latch.frameAfter==101); + assert(f.session.state==SessionState::Streaming && f.session.currentBacking==201 && f.session.retiredBacking==200 && f.session.lease==100); + assert(f.io.writes.back()==0x7019C && f.io.writes[f.io.writes.size()-2]==0x70180); + for(auto r:f.io.writes)assert((r>=0x70240 && r<=0x70268) || r==0x45270 || + r==0x70188 || r==0x7018C || r==0x70190 || r==0x701A4 || r==0x701CC || r==0x70180 || r==0x7019C); + assert(sourceUnchanged(f.io,f.link) && f.io.regs[0x70008]==0 && f.io.regs[0x42084]==(1U<<30)); + } + for(unsigned problem=0;problem<19;++problem){Fixture f; + switch(problem){ + case 0:f.backing.pitch+=128;break;case 1:f.backing.bytes-=4096;break;case 2:f.next.control|=1U<<15;break; + case 3:f.next.control^=0x1400;break;case 4:f.next.offset=1;break;case 5:f.next.surface+=4096;break; + case 6:f.plan.yTiled=false;break;case 7:f.snapshot.primarySurface=0;break;case 8:f.snapshot.primaryDDB=0;break; + case 9:f.io.regs[0x42084]=0;break;case 10:f.io.regs[0x70080]=7;break;case 11:f.io.regs[0x70780]=1U<<31;break; + case 12:f.io.regs[0x70B08]=1;break;case 13:f.io.regs[0x6001C]^=1;break;case 14:f.io.regs[0x701AC]=4096;break; + case 15:f.io.unbound=true;break;case 16:f.p.mode.height=0;break; + case 17:f.p.mode.height=40000;break;case 18:f.snapshot.nativeClockHz+=1;break; + } + assert(f.run().status!=Surface::Status::Complete && f.io.writes.empty() && f.session.state==SessionState::FirmwareActive); + } + for(auto drop:{0x70240U,0x45270U,0x70188U,0x701CCU,0x70180U,0x7019CU}){ + Fixture f;f.io.drop=drop;const auto r=f.run();assert(r.status!=Surface::Status::Complete && !f.io.writes.empty()); + assert(f.session.state==SessionState::Faulted && f.session.currentBacking==200 && f.session.pendingBacking==201 && !f.session.takeCleanup().lease); + } + for(unsigned failure=0;failure<7;++failure){Fixture f; + switch(failure){case 0:f.io.frozen=true;break;case 1:f.io.oldLive=true;break;case 2:f.io.backward=true;break; + case 3:f.io.drift=0x6B008;break;case 4:f.io.loseBinding=true;break;case 5:f.io.drift=0x42084;break;case 6:f.io.immediate=f.io.frozen=true;break;} + const auto r=f.run();assert(r.status!=Surface::Status::Complete && f.session.state==SessionState::Faulted); + assert(f.session.currentBacking==200 && f.session.pendingBacking==201 && !f.session.takeRetiredBacking()); + } + if(argc==2)native(argv[1]); + puts("PASS DSI surface: fixed native Y geometry, real WM programming/latch, link preservation, live/frame proof, failed writes/latch retain both backings"); +} +'''.replace('IO_FIXTURE',io) +donor=None +if len(sys.argv)==2: + donor=Path(sys.argv[1]).resolve() + assert hashlib.sha256(donor.read_bytes()).hexdigest()=='c004fc1ac9ee4157e2e12736f40fa6639bde025a279326beeb9dd48c9b445731' +with tempfile.TemporaryDirectory(prefix='dsi-surface-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1',*(['-arch','x86_64'] if donor else ['-fsanitize=address,undefined']),'-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test'),*([str(donor)] if donor else [])],check=True) diff --git a/tests/dsi_surface_adapter.py b/tests/dsi_surface_adapter.py new file mode 100644 index 00000000..5f30f69b --- /dev/null +++ b/tests/dsi_surface_adapter.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Exercise the production surface/retirement adapters on the stop fixture's BAR.""" +from pathlib import Path +from production_source import read_source +import re,subprocess,tempfile +root=Path(__file__).resolve().parents[1] +s=read_source(root/'WhateverXe/kern_xe.cpp') +functions='\n'.join(re.search(r'bool XE::'+name+r'\(.*?\n\}',s,re.S)[0] for name in + ('switchDSINativeSurface','dsiArmedBackingOwned','retireDSINativeBacking','publishDSINativeBacking','publishDSINativeMode')) +state=re.search(r'struct DSIPendingBackingOwner \{.*?\n\};',s,re.S)[0] +published=re.search(r'static bool dsiPublishedTarget\(.*?\n\}',s,re.S)[0] +# Share the established real power/IRQ/clock fixture; no implementation bodies +# are rewritten. Allocation/PTE proof and CPU mapping have their own adapter test. +q=(root/'tests/dsi_quiesce_adapter.py').read_text() +q=q.split("code=r'''",1)[1].split("'''.replace",1)[0] +prelude='#define DSI_SURFACE_REPLAY\n'+q[:q.index('struct XE {')] +prelude=prelude.replace('#include "kern_dsi_boot.hpp"','#include "kern_dsi_boot.hpp"\n#include "kern_dsi_surface.hpp"\n#include "kern_dsi_native_backing.hpp"\n#include "kern_display13_clock.hpp"') +b=(root/'tests/dsi_boot.cpp').read_text();prelude=prelude.replace('FIXTURE',b[b.index('struct IO {'):b.index('Boot capture(')]) +prelude=prelude.replace('bool armedOwner=false,failNativeRetire=false;unsigned nativeRetires=0;IOService *pendingFB=nullptr;bool dsiPendingTarget(IOService *fb){return fb && pendingFB==fb;}',state+'\nDSIPendingBackingOwner dsiPendingBacking;\nbool dsiPendingTarget(IOService *fb){return fb && dsiPendingBacking.framebuffer==fb;}') +prelude=prelude.replace('#include "kern_dsi_native_backing.hpp"','#include "kern_dsi_native_backing.hpp"\n#include "kern_dsi_native_mode.hpp"') +prelude=prelude.replace('bool clockFaulted=false,memoryFaulted=false;', 'bool clockFaulted=false,memoryFaulted=false,watermarkFaulted=false;') +prelude=prelude.replace('bool destroyingPath=false,wmProgramming=false,wmCommitting=false;', 'bool destroyingPath=false,wmProgramming=false,wmCommitting=false;unsigned wmPending=0;uint64_t watermarkClockHz=0;Display13WM::Latch wmLatch;') +prelude=prelude.replace('struct {uintptr_t record=0;}scanout;','Display13FBMemory::SurfaceReadout scanout;') +prelude=prelude.replace('struct {bool ready=true;}dsiFixedMode;','struct {bool ready=true;AppleAdapter::Timing timing;}dsiFixedMode;') +prelude=prelude.replace(' void retain(){',' uint32_t callPlatformFunction(const char*,bool,void*,void*,void*,void*);\n void retain(){') +prelude=prelude[:prelude.index('void tick(unsigned us)')]+published+r''' +uint64_t elapsed=0;unsigned gpuDepth=1;bool failDiscard=false;unsigned discards=0; +templatevoid put(void *p,unsigned o,T v){memcpy(static_cast(p)+o,&v,sizeof(v));} +void *current_thread(){return reinterpret_cast(1);} +void clock_get_uptime(uint64_t *v){*v=elapsed*1000;}void absolutetime_to_nanoseconds(uint64_t t,uint64_t *v){*v=t;} +void tick(unsigned us){assert(locks==std::vector({1,3}) && gpuDepth==1);elapsed+=us; + if(!activeMap->freeze && us>=1000)++activeMap->reg(0x70040); + activeMap->reg(0x701AC)=activeMap->reg(0x7019C); +} +void IOSleep(unsigned ms){tick(ms*1000);}void IODelay(unsigned us){tick(us);} +struct Display13FBClockIO:Display13FBBootIO {}; +bool adlnSharedBinding(void *controller,void *owner){return controller==display13FBPower.controller && owner && mappingBound;} +bool failSlotReturn=false,failCache=false,failLogical=false,failModeDBUF=false;unsigned slotReturns=0,cpuInitial=0,cpuCaps=0; +#define FunctionCast(f,p) reinterpret_cast(p) +bool dsiModeCoherent(IOService*){return dsiFixedMode.ready;} +uint32_t OSObject::callPlatformFunction(const char *name,bool wait,void *arg,void *b,void *c,void *d) { + assert(locks==std::vector{1} && !gpuDepth && !strcmp(name,"EFITilingMode") && wait && !b && !c && !d); + assert(*static_cast(arg)==2);return 0; +} +void compute(void *fb,const AppleAdapter::Timing*) { + assert(locks==std::vector({1,3}) && gpuDepth==1); + for(auto off:{0x430CU,0x4320U,0x4328U})put(fb,off,1080U); + for(auto off:{0x4310U,0x4324U,0x432CU})put(fb,off,1920U); + put(fb,0x4314,8355840U);put(fb,0x4334,4352U);put(fb,0x4338,1U); + auto path=Display13FBMemory::singlePath(fb); + put(path,0xF0,uint64_t(0));put(path,0x100,uint64_t(0)); + put(path,0xF8,1080U);put(path,0xFC,1920U);put(path,0x108,1080U);put(path,0x10C,1920U); +} +void freeSlot(IOService *fb) { + assert(locks==std::vector{1} && gpuDepth==1 && !Display13FBMemory::field(fb,0x4348)); + ++slotReturns;if(failSlotReturn)return; + auto controller=display13FBPower.controller;put(controller,0xDF0,0U);put(controller,0xDF4,uint8_t(0));put(fb,0x4288,uint8_t(0xFF)); +} +uint32_t logical(IOService*){assert(!gpuDepth && locks==std::vector{1});return failLogical?1:0;} +void initial(IOService *fb,uint32_t arg){assert(!gpuDepth && locks==std::vector{1} && !arg);++cpuInitial; + put(fb,0x4000,0U);put(fb,0x4008,UINT32_MAX);put(fb,0x400C,UINT32_MAX);put(fb,0x4200,1U);} +void caps(IOService *fb){assert(!gpuDepth && locks==std::vector{1});++cpuCaps;put(fb,0x402C,1U);} +uint64_t receipt[18]{}; +struct IORegistryEntry {static IORegistryEntry *getRegistryRoot(){static IORegistryEntry r;return &r;} + void setProperty(const char *name,const void *p,unsigned n){if(!strcmp(name,"WEXDSISurface")){assert(n==sizeof(receipt));memcpy(receipt,p,n);}else assert(n==96);}}; +struct XE { + bool dsiNativeWriterHooksReady=true,dsiResourcesReady=true,dsiCacheHooksReady=true,dsiBackingHooksReady=true; + bool dsiStopAttempted=false,dsiIRQParked=false,dsiDDIReleased=false; + Profile dsiProfile;Boot dsiBoot;Quiesce::Result dsiStop;Clock::Result dsiClockStop;Session dsiSession;Session::Cleanup dsiCleanup; + bool discardDSINativeBacking(IOService *fb){assert(locks==std::vector{1} && !gpuDepth && !dsiPendingBacking.armed && dsiPendingTarget(fb)); + ++discards;if(failDiscard)return false;dsiPendingBacking={};return true;} + uintptr_t dsiComputeGeometry=reinterpret_cast(compute),orgAlderLakeNScanoutFree=reinterpret_cast(freeSlot); + uintptr_t dsiModeCPU[3]{reinterpret_cast(logical),reinterpret_cast(initial),reinterpret_cast(caps)}; + static void wrapAlderLakeNScanoutFree(IOService*){} + bool updateDSIBootCache(IOService*,IOService*,OSObject*){assert(locks==std::vector{1});return !failCache;} + bool dsiNativeUpdateAllowed(IOService*){return dsiPendingBacking.modeReady && !display13FBPower.memoryFaulted;} + static void adlnSetupDBUFPipe(IOService *controller,IOService *fb,unsigned pipe,bool optimized) { + assert(!gpuDepth && locks==std::vector{1} && controller==display13FBPower.controller && dsiPublishedTarget(fb) && !pipe && !optimized); + assert(dsiPendingBacking.modeReady); + auto path=Display13FBMemory::singlePath(fb); + assert(Display13FBMemory::field(path,0x118)==dsiFixedMode.timing.pixelClock && dsiFixedMode.timing.pixelClock==135255000); + auto &c=display13FBPower; + if(failModeDBUF){c.dbuf.faulted=true;return;} + c.dbuf.pipe[0].primary={40,2048};c.dbuf.pipe[0].cursor={0,40};c.pipe[0].wmPending=0; + c.pipe[0].wmLatch.status=Display13WM::Status::Complete;c.pipe[0].watermarkClockHz=358400000; + } + bool publishDSINativeBacking();bool publishDSINativeMode(IOService*); + bool releaseDSILogical(IOService *fb){return !Display13FBMemory::field(fb,0x4340);} + bool switchDSINativeSurface();bool dsiArmedBackingOwned(IOService*,OSObject*);bool retireDSINativeBacking(IOService*,OSObject*); +}; +''' +prelude=prelude.replace('Display13DBUF::State dbuf;','Display13DBUF::State dbuf;Display13WM::Latency latency;') +# Reuse just initialization, leaving stop tests and stop adapter out of this TU. +case=q[q.index('struct Case {'):q.index(' bool run(){')] +case=case.replace('IOWorkLoop loop;','IOWorkLoop loop;IOService accelerator,pipe;OSObject mapping,memory;NativeBacking::Record record;') +case=case.replace('xe.adlnTopologyController=&controller;','').replace('put(&controller,0xE00,&loop);put(&controller,0x90,&irqLock);',r''' + put(&controller,0xE00,&loop);put(&controller,0x90,&irqLock); + put(&path,0x32B0,&fb);put(&path,0x3640,0U);put(&pipe,0x88,&accelerator); + map.reg(0x46070)=0xC0000010;map.reg(0x46000)=612;map.reg(0x46008)=0; + map.reg(0x701CC)=0x5000;map.reg(0x701AC)=map.reg(0x7019C)=0; + c.latency=Display13WM::decodeLatency(0x63503300,0x00909090,30); + c.dbuf.pipe[0].primary={0,2048};map.reg(0x7027C)=0x07FF0000; + xe.dsiBoot.status=BootStatus::Complete;xe.dsiBoot.control=map.reg(0x70180); + xe.dsiBoot.stride=map.reg(0x70188);xe.dsiBoot.size=map.reg(0x70190);xe.dsiBoot.surface=0; + NativeBacking::request(xe.dsiProfile,3,record);record.refs=1;record.map=reinterpret_cast(&mapping); + record.memory=reinterpret_cast(&memory);record.gpu=0x10000000;record.pitch=4352; + put(&pipe,0x1298,record.memory);put(&pipe,0x12A0,record.map); + auto &o=dsiPendingBacking;o={};o.framebuffer=&fb;o.accelerator=&accelerator;o.pipe=&pipe; + o.pinnedMap=&mapping;o.pinnedMemory=&memory;o.record=&record;o.thread=1;o.allocating=o.allocated=true; + auto &p=o.proof;p.status=Display13FBMemory::SurfaceStatus::Complete;p.record=reinterpret_cast(&record); + p.refs=p.kind=1;p.map=record.map;p.memory=record.memory;p.gpu=record.gpu;p.bytes=8355840;p.pitch=4352;p.verifiedPages=2040; + assert(AppleAdapter::makeTiming(xe.dsiProfile,dsiFixedMode.timing)); + put(&fb,0x4A00,1U);put(&fb,0x4A08,&path);put(&controller,0xDF5,uint8_t(3));put(&controller,0xDF4,uint8_t(1));put(&controller,0xDF0,1U); + put(&controller,0x1A10,1U);put(&fb,0x4288,uint8_t(0));failSlotReturn=failCache=failLogical=failModeDBUF=false;slotReturns=cpuInitial=cpuCaps=0; + o.initialized=true;elapsed=0;gpuDepth=1;failDiscard=false;discards=0;memset(receipt,0,sizeof(receipt)); +''') +code=prelude+functions+'\n'+case+r''' + bool run(){locks={1};bool result=xe.switchDSINativeSurface();assert(locks==std::vector{1});locks.clear();dsiPendingBacking.allocating=false;gpuDepth=0;return result;} + bool retire(){locks={1};bool r=xe.retireDSINativeBacking(&fb,&path);assert(locks==std::vector{1});locks.clear();return r;} + void stopped(){xe.dsiStop.stage=Quiesce::Stage::Complete;xe.dsiStop.error=Quiesce::Error::None;xe.dsiIRQParked=xe.dsiDDIReleased=true;xe.dsiClockStop.status=Clock::Status::Complete;} + ~Case(){assert(locks.empty());dsiPendingBacking={};} +}; +int main(){ + {Case c;dsiPendingBacking.initialized=false;const auto before=c.map.bar;assert(!c.run()); + assert(c.map.bar==before && !dsiPendingBacking.armed);} + {Case c;assert(c.run());assert(receipt[0]==2 && receipt[10]==unsigned(Surface::Status::Complete) && receipt[11]==17); + assert(receipt[17]>receipt[16]); + assert(c.map.reg(0x7019C)==c.record.gpu && c.map.reg(0x701AC)==c.record.gpu && c.map.reg(0x70188)==34); + assert(c.xe.dsiSession.state==SessionState::Streaming && dsiPendingBacking.armed && c.xe.dsiArmedBackingOwned(&c.fb,&c.path)); + assert(!c.retire() && !discards);c.stopped();assert(c.retire() && discards==1 && !dsiPendingTarget(&c.fb)); + assert(c.xe.dsiSession.state==SessionState::Released && c.xe.dsiCleanup.retiredBacking==reinterpret_cast(&c.xe.dsiBoot));} + for(unsigned fail=0;fail<2;++fail){Case c; + if(fail==0)c.map.freeze=true;else display13FBIRQ.state.enabled=false; + const auto before=c.map.bar;assert(!c.run()); + if(fail==1){assert(!dsiPendingBacking.armed && c.map.bar==before && c.xe.dsiSession.state==SessionState::Unbound);continue;} + assert(dsiPendingBacking.armed && c.xe.dsiSession.state==SessionState::Faulted && !c.retire() && !discards); + c.stopped();failDiscard=true;assert(!c.retire() && dsiPendingBacking.armed && c.xe.dsiSession.state==SessionState::Released); + assert(c.xe.dsiArmedBackingOwned(&c.fb,&c.path));failDiscard=false;assert(c.retire() && discards==2);} + {Case c;assert(c.run());auto &o=dsiPendingBacking;o.published=o.modeReady=true; + put(&c.fb,0x4348,&c.record);display13FBPower.pipe[0].publishedScanout=reinterpret_cast(&c.record); + assert(!c.retire() && dsiPublishedTarget(&c.fb));c.stopped();failDiscard=true; + assert(!c.retire() && dsiPublishedTarget(&c.fb) && !o.modeReady && o.armed); + failDiscard=false;assert(c.retire() && !Display13FBMemory::field(&c.fb,0x4348) && !display13FBPower.pipe[0].publishedScanout);} + for(unsigned failure=0;failure<5;++failure) {Case c;assert(c.run()); + put(&c.fb,0x4348,uintptr_t(0));put(&c.fb,0x4288,uint8_t(0)); + failSlotReturn=failure==1;failCache=failure==2;failLogical=failure==3;failModeDBUF=failure==4; + locks={1};gpuDepth=1;dsiPendingBacking.allocating=true; + const bool published=c.xe.publishDSINativeBacking();dsiPendingBacking.allocating=false;gpuDepth=0; + assert(published==(failure==0 || failure>=3) && slotReturns==1); + const bool ready=c.xe.publishDSINativeMode(&c.fb); + assert(ready==(failure==0));assert(cpuInitial==(failure==0 || failure==4) && cpuCaps==(failure==0 || failure==4)); + if(!failure)assert(dsiPublishedTarget(&c.fb) && dsiPendingBacking.modeReady && Display13FBMemory::field(&c.fb,0x4330)==c.record.gpu); + locks.clear(); + } + puts("PASS production DSI surface: actual MMIO allowlist/WM/latch, GPU/owner/IRQ locking, retained partial switches and stopped cleanup retry"); +} +''' +with tempfile.TemporaryDirectory(prefix='dsi-surface-adapter-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/dsi_update_dispatch.py b/tests/dsi_update_dispatch.py new file mode 100644 index 00000000..3450874a --- /dev/null +++ b/tests/dsi_update_dispatch.py @@ -0,0 +1,394 @@ +#!/usr/bin/env python3 +"""Execute production mode/update/cursor admission and stop serialization.""" +from pathlib import Path +from production_source import read_source +import hashlib,re,subprocess,sys,tempfile +root=Path(__file__).resolve().parents[1];s=read_source(root/'WhateverXe/kern_xe.cpp') +functions='\n'.join(re.search(r'(?:static bool|bool|uint32_t|void|int) (?:XE::)?'+name+r'\([^;{]*\{.*?\n\}',s,re.S)[0] + for name in ['dsiCachePath','dsiLifecycleTarget','wrapDSISetDisplayMode','dsiNativeUpdateAllowed','dsiSharedWriterStopped','wrapDisplay13WMArmUpdate','beginDSINativeWriter','endDSINativeWriter','wrapDSICursorImage','wrapDSICursorState','beginDSIFramebufferWriter','wrapDSIFlipTransaction','wrapDSIGammaTransaction','wrapDSICscTransaction','wrapDSIPlaneGammaTransaction','wrapDSISetGammaTable','wrapDSISetAttribute','wrapDSISetConnectionAttribute','wrapDSISetPowerState','wrapDSISetStream']) +functions=functions.replace('void XE::endDSINativeWriter(int token) {', 'void XE::endDSINativeWriter(int token) {\n struct EndScope {EndScope(){endingWriter=true;}~EndScope(){endingWriter=false;}} endScope;') +scope=re.search(r'struct DSIModeScope \{.*?\n\};',s,re.S)[0] +if len(sys.argv)==2: + binary=Path(sys.argv[1]).read_bytes() + assert hashlib.sha256(binary).hexdigest()=='c004fc1ac9ee4157e2e12736f40fa6639bde025a279326beeb9dd48c9b445731' + for name,width,count in [('processDSINativeWriters',20,2),('processDSINativeTransactions',17,5),('processDSINativeAttributes',20,4)]: + installer=re.search(r'bool XE::'+name+r'\([^;{]*\{.*?\n\}',s,re.S)[0] + offsets=[int(x,0) for x in re.search(r'offsets\[\]=\{([^}]+)',installer)[1].split(',')] + entries=re.search(r'entries\[\]\['+str(width)+r'\]=\{(.*?)\};',installer,re.S)[1] + entries=[bytes(int(x,0) for x in row.split(',')) for row in re.findall(r'\{([^}]+)\}',entries)] + assert len(offsets)==len(entries)==count + for offset,entry in zip(offsets,entries):assert binary[offset:offset+width]==entry,hex(offset) +code=r''' +#include "kern_display13_fb_memory.hpp" +#include "kern_dsi_apple_timing.hpp" +#include "kern_dsi_attributes.hpp" +#include "kern_display13_transcoder.hpp" +#include +#include +#include +struct OSObject {alignas(8) uint8_t bytes[0x4B90]{};unsigned size=sizeof(bytes); + OSObject *getMetaClass(){return this;}unsigned getClassSize(){return size;}}; +using IOService=OSObject; +struct IORegistryEntry {static IORegistryEntry *getRegistryRoot(){static IORegistryEntry r;return &r;} + void setProperty(const char *name,const void *data,unsigned bytes){ + assert(!strcmp(name,"WEXDSIModeRequest") && bytes==64); + const auto words=static_cast(data);assert(words[0]==1); + }}; +IOService *pendingFB=nullptr;bool dsiPendingTarget(IOService *fb){return fb && pendingFB==fb;}using IOLock=int;struct {bool modeReady=false;} dsiPendingBacking; +bool dsiPublishedTarget(IOService *fb){return dsiPendingTarget(fb) && dsiPendingBacking.modeReady;} +struct IORecursiveLock {unsigned depth=0;}; +unsigned owner=0,modeCalls=0,armCalls=0,backingCalls=0;IORecursiveLock fbLock;bool expectDSI=false,endingWriter=false; +void IOLockLock(IOLock*){assert((fbLock.depth || endingWriter) && !owner);++owner;}void IOLockUnlock(IOLock*){assert(owner==1);--owner;} +void IORecursiveLockLock(IORecursiveLock *p){assert(!owner);++p->depth;} +void IORecursiveLockUnlock(IORecursiveLock *p){assert(!owner && p->depth);--p->depth;} +struct {IOLock *lock=nullptr;IOService *controller=nullptr;OSObject *dsiPort=nullptr; + unsigned dsiNativeWriters=0;bool memoryFaulted=false,clockFaulted=false,watermarkFaulted=false; + struct {struct {bool faulted=false,suspended=false;}state;}bridge;struct {bool faulted=false;}dbuf; + struct {IOService *memoryFramebuffer=nullptr;bool destroyingPath=false;}pipe[3]; + Display13Transcoder::Bindings transcoders;}display13FBPower; +struct {uint32_t clientMode=0x10000;uintptr_t changingThread=0;bool presentationFault=false;}dsiFixedMode; +const XELP::DSI::AppleAdapter::Timing *dsiPresentation(uint32_t mode){ + static XELP::DSI::AppleAdapter::Timing t; + return XELP::DSI::AppleAdapter::acceptsMode(mode,dsiFixedMode.clientMode)?&t:nullptr; +} +IOService *panel=nullptr,*nativeFB=nullptr;bool dsiModeTarget(IOService *f){return f && f==panel;} +constexpr uint32_t kIOReturnNotReady=0xe00002d8U,kIOReturnUnsupported=0xe00002c7U,kIOReturnSuccess=0; +#define FunctionCast(f,p) reinterpret_cast(p) +uint32_t nativeMode(IOService*,int32_t m,int32_t d){assert(!fbLock.depth && !owner);assert(m==0x10000 && d==2);++modeCalls;return 7;} +void adlnFault(bool &f,unsigned){f=true;} +unsigned imageCalls=0,stateCalls=0,expectedWriters=1;bool nestedCursor=false; +uint32_t nativeImage(IOService*,IOService*,void*); +uint32_t nativeState(IOService*,IOService*,int32_t,int32_t,bool,bool); +unsigned txCalls[5]{},txAnchor=0,paramAnchor=0;bool nestedTx=false; +constexpr uint64_t txOptions=0xFEDCBA9876543210ULL; +uint32_t nativeFlip(IOService*,void*,uint64_t,void*); +uint32_t nativeGamma(IOService*,void*,void*); +uint32_t nativeCsc(IOService*,void*,void*,uint64_t,bool); +uint32_t nativePlaneGamma(IOService*,void*,bool); +uint32_t nativeGammaTable(IOService*,uint32_t,uint32_t,uint32_t,void*,bool); +unsigned attrCalls[3]{},expectedAttribute=0,expectedPower=0;int32_t expectedConnection=0; +uintptr_t expectedValue=0;bool nestedAttribute=false; +uint32_t nativeAttribute(IOService*,uint32_t,uintptr_t); +uint32_t nativeConnectionAttribute(IOService*,int32_t,uint32_t,uintptr_t); +uint32_t nativePower(IOService*,uint32_t); +unsigned streamCalls=0;OSObject *expectedPath=nullptr; +uint32_t nativeStream(IOService*,IOService*,OSObject*,const void*); +struct XE { + struct {unsigned dsi=1;}adlnOutputWiring;bool dsiStopAttempted=false,dsiResourcesReady=true; + bool dsiPowerTest=false;struct {bool sleeping=false,failed=false;}dsiRuntimePower;unsigned wakeCalls=0;bool wakeOK=false; + bool dsiStopTest=false,dsiMemoryReleased=false,dsiDDIReleased=false,stopOK=false;unsigned stopCalls=0; + struct {bool done=false;bool complete(){return done;}}dsiClockStop; + struct {bool complete=false;}dsiPanelStop; + bool quiesceDSIPath(IOService *ctl,IOService *fb,OSObject *path,bool retain=false){ + assert(fbLock.depth && !owner && ctl==display13FBPower.controller && fb==panel); + assert(path==Display13FBMemory::singlePath(fb));++stopCalls; + if(stopOK && retain){dsiStopAttempted=true;dsiRuntimePower.sleeping=true;return true;} + if(stopOK){dsiStopAttempted=true;dsiMemoryReleased=dsiDDIReleased=dsiPanelStop.complete=dsiClockStop.done=true;} + return stopOK; + } + bool resumeDSIPath(IOService *ctl,IOService *fb,OSObject *path){ + assert(fbLock.depth && !owner && ctl==display13FBPower.controller && fb==panel && dsiRuntimePower.sleeping); + assert(path==Display13FBMemory::singlePath(fb));++wakeCalls; + if(wakeOK){dsiRuntimePower.sleeping=false;dsiStopAttempted=false;} + return wakeOK; + } + bool dsiPanelOnly=false,publicationReady=false;bool publishDSINativeMode(IOService*){return publicationReady;} + uint32_t setDSIPresentation(IOService *fb,uint32_t mode,const XELP::DSI::AppleAdapter::Timing &){ + assert(fbLock.depth && !owner);memcpy(reinterpret_cast(fb)+0x4250,&mode,sizeof(mode));return 0;} + bool dsiNativeWriterHooksReady=true;uintptr_t orgDSICursor[2]{}; + uintptr_t orgDSIMode[6]{}; + static void recordDSIClient(IOService*,unsigned,uint64_t,uint32_t){}; + static void recordDSIAttribute(IOService*,unsigned,uint32_t,uintptr_t,uint32_t){}; + uintptr_t orgDSIAttributes[4]{}; + static uint32_t wrapDSISetStream(IOService*,IOService*,OSObject*,const void*); + static uint32_t wrapDSISetAttribute(IOService*,uint32_t,uintptr_t); + uint32_t saveDSIBrightness(IOService*,uintptr_t value){assert(fbLock.depth && !owner);return value<=UINT32_MAX?0U:kIOReturnUnsupported;} + uint32_t setDSIBrightness(IOService*,uintptr_t value){assert(fbLock.depth && !owner);return value<=65535?0U:kIOReturnUnsupported;} + static uint32_t wrapDSISetConnectionAttribute(IOService*,int32_t,uint32_t,uintptr_t); + static uint32_t wrapDSISetPowerState(IOService*,uint32_t); + uintptr_t orgDSITransaction[5]{}; + static int beginDSIFramebufferWriter(IOService*); + static uint32_t wrapDSIFlipTransaction(IOService*,void*,uint64_t,void*); + static uint32_t wrapDSIGammaTransaction(IOService*,void*,void*); + static uint32_t wrapDSICscTransaction(IOService*,void*,void*,uint64_t,bool); + static uint32_t wrapDSIPlaneGammaTransaction(IOService*,void*,bool); + static uint32_t wrapDSISetGammaTable(IOService*,uint32_t,uint32_t,uint32_t,void*,bool); + static int beginDSINativeWriter(IOService*,IOService*); + static void endDSINativeWriter(int); + static uint32_t wrapDSICursorImage(IOService*,IOService*,void*); + static uint32_t wrapDSICursorState(IOService*,IOService*,int32_t,int32_t,bool,bool); + static uint32_t wrapDSISetDisplayMode(IOService*,int32_t,int32_t); + void probeDSINativeBacking(IOService*){assert(fbLock.depth && !owner);++backingCalls;} + static bool dsiNativeUpdateAllowed(IOService*); + static bool dsiSharedWriterStopped(IOService*,unsigned); + static void wrapDisplay13WMArmUpdate(IOService*); + static void display13WMArmUpdate(IOService*){assert(!owner);assert(bool(fbLock.depth)==expectDSI);++armCalls;} +}xe,*callbackXE=&xe; +SCOPE +FUNCTIONS +uint32_t nativeState(IOService *ctl,IOService *fb,int32_t x,int32_t y,bool visible,bool commit){ + assert(!fbLock.depth && !owner);assert(ctl==display13FBPower.controller && fb==nativeFB); + assert(x==-17 && y==42 && visible && !commit);assert(display13FBPower.dsiNativeWriters==expectedWriters); + ++stateCalls;return 0xe0000001U; +} +uint32_t nativeImage(IOService *ctl,IOService *fb,void *info){ + assert(!fbLock.depth && !owner);assert(ctl==display13FBPower.controller && fb==nativeFB && info==&imageCalls); + assert(display13FBPower.dsiNativeWriters==expectedWriters);++imageCalls; + if(nestedCursor){++expectedWriters;assert(XE::wrapDSICursorState(ctl,fb,-17,42,true,false)==0xe0000001U); + --expectedWriters;assert(display13FBPower.dsiNativeWriters==expectedWriters);} + return 0xe0000002U; +} +uint32_t txResult(unsigned i){return i==3?0:0xe0000110U+i;} +uint32_t txLeaf(unsigned i,IOService *fb,void *tx){ + assert(fb==nativeFB && tx==&txAnchor && !owner && !fbLock.depth); + assert(display13FBPower.dsiNativeWriters==expectedWriters);++txCalls[i];return txResult(i); +} +uint32_t nativeFlip(IOService *fb,void *tx,uint64_t options,void *params){ + assert(options==txOptions && params==¶mAnchor);const auto result=txLeaf(0,fb,tx); + if(nestedTx){++expectedWriters;assert(XE::wrapDSICscTransaction(fb,tx,params,options,true)==txResult(2));--expectedWriters; + assert(display13FBPower.dsiNativeWriters==expectedWriters);} + return result; +} +uint32_t nativeGamma(IOService *fb,void *tx,void *params){assert(params==¶mAnchor);return txLeaf(1,fb,tx);} +uint32_t nativeCsc(IOService *fb,void *tx,void *matrix,uint64_t options,bool final){ + assert(matrix==¶mAnchor && options==txOptions && final);return txLeaf(2,fb,tx);} +uint32_t nativePlaneGamma(IOService *fb,void *tx,bool post){assert(!post);return txLeaf(3,fb,tx);} +uint32_t nativeGammaTable(IOService *fb,uint32_t channels,uint32_t count,uint32_t bits,void *data,bool sync){ + assert(channels==3 && count==1024 && bits==16 && data==¶mAnchor && sync);return txLeaf(4,fb,&txAnchor);} +uint32_t callTx(unsigned i,IOService *fb){ + switch(i){ + case 0:return XE::wrapDSIFlipTransaction(fb,&txAnchor,txOptions,¶mAnchor); + case 1:return XE::wrapDSIGammaTransaction(fb,&txAnchor,¶mAnchor); + case 2:return XE::wrapDSICscTransaction(fb,&txAnchor,¶mAnchor,txOptions,true); + case 3:return XE::wrapDSIPlaneGammaTransaction(fb,&txAnchor,false); + case 4:return XE::wrapDSISetGammaTable(fb,3,1024,16,¶mAnchor,true); + }assert(false);return 0; +} +uint32_t nativeAttribute(IOService *fb,uint32_t attribute,uintptr_t value){ + assert(fb==nativeFB && attribute==expectedAttribute && value==expectedValue && !owner && !fbLock.depth); + assert(display13FBPower.dsiNativeWriters==expectedWriters);++attrCalls[0]; + if(nestedAttribute){++expectedWriters;assert(callTx(0,fb)==txResult(0));--expectedWriters; + assert(display13FBPower.dsiNativeWriters==expectedWriters);} + return 0xe0000121U; +} +uint32_t nativeConnectionAttribute(IOService *fb,int32_t connection,uint32_t attribute,uintptr_t value){ + assert(fb==nativeFB && connection==expectedConnection && attribute==expectedAttribute && value==expectedValue && !owner && !fbLock.depth); + assert(display13FBPower.dsiNativeWriters==expectedWriters);++attrCalls[1];return 0xe0000122U; +} +uint32_t nativePower(IOService *fb,uint32_t state){ + assert(fb==nativeFB && state==expectedPower && !owner && !fbLock.depth);++attrCalls[2];return 0xe0000123U; +} +uint32_t nativeStream(IOService *ctl,IOService *fb,OSObject *path,const void *timing){ + assert(fb==nativeFB && ctl==display13FBPower.controller && path==expectedPath && timing==¶mAnchor && !fbLock.depth && !owner); + ++streamCalls;return 0xe0000124U; +} +templatevoid put(void *p,unsigned o,T v){memcpy(static_cast(p)+o,&v,sizeof(v));} +struct Case { + IOService fb,ctl,peer;OSObject path,port;IOLock lock; + Case(){assert(!fbLock.depth && !owner);expectedWriters=1;nestedCursor=nestedTx=nestedAttribute=false;xe={};dsiFixedMode={}; + xe.orgDSIAttributes[0]=reinterpret_cast(nativeAttribute); + xe.orgDSIAttributes[1]=reinterpret_cast(nativeConnectionAttribute); + xe.orgDSIAttributes[2]=reinterpret_cast(nativePower); + xe.orgDSIAttributes[3]=reinterpret_cast(nativeStream); + xe.orgDSITransaction[0]=reinterpret_cast(nativeFlip); + xe.orgDSITransaction[1]=reinterpret_cast(nativeGamma); + xe.orgDSITransaction[2]=reinterpret_cast(nativeCsc); + xe.orgDSITransaction[3]=reinterpret_cast(nativePlaneGamma); + xe.orgDSITransaction[4]=reinterpret_cast(nativeGammaTable); + xe.orgDSICursor[0]=reinterpret_cast(nativeImage);xe.orgDSICursor[1]=reinterpret_cast(nativeState);xe.orgDSIMode[5]=reinterpret_cast(nativeMode);display13FBPower={};auto &c=display13FBPower; + c.lock=&lock;c.controller=&ctl;c.dsiPort=&port;c.pipe[0].memoryFramebuffer=&fb;c.transcoders.dsi0Pipe=0;panel=nativeFB=&fb; + put(&fb,0x4220,&fbLock);put(&fb,0x1D0,&ctl);put(&fb,0x4A00,1U);put(&fb,0x4A08,&path); + put(&path,0x3640,0U);put(&path,0x3644,2U);put(&path,0x32B0,&fb);put(&path,0x3648,&port);put(&port,0x550,1U); + expectDSI=true; + } + void refuseCursor(){auto i=imageCalls,s=stateCalls; + assert(XE::wrapDSICursorImage(&ctl,&fb,&imageCalls)==kIOReturnNotReady); + assert(XE::wrapDSICursorState(&ctl,&fb,-17,42,true,false)==kIOReturnNotReady); + assert(i==imageCalls && s==stateCalls && !fbLock.depth && !owner); + } + void refuse(){const auto calls=armCalls;XE::wrapDisplay13WMArmUpdate(&fb);assert(calls==armCalls && !fbLock.depth && !owner);} +}; +int main(){ + + for(unsigned attr:{0x706F7772U,0x70777273U}){Case c;xe.dsiPowerTest=true;put(&c.fb,0x4284,2U); + assert(XE::wrapDSISetAttribute(&c.fb,attr,1)==kIOReturnNotReady && xe.stopCalls==1); + assert(Display13FBMemory::field(&c.fb,0x4284)==2); + xe.stopOK=true; + for(unsigned cycle=0;cycle<3;++cycle){ + assert(XE::wrapDSISetAttribute(&c.fb,attr,1)==kIOReturnSuccess && xe.dsiRuntimePower.sleeping); + auto stops=xe.stopCalls; + assert(XE::wrapDSISetAttribute(&c.fb,attr,0)==kIOReturnSuccess && xe.stopCalls==stops); + assert(Display13FBMemory::field(&c.fb,0x4284)==0 && !xe.dsiMemoryReleased && !xe.dsiDDIReleased); + c.refuse();c.refuseCursor();xe.wakeOK=false; + assert(XE::wrapDSISetAttribute(&c.fb,attr,2)==kIOReturnNotReady); + assert(Display13FBMemory::field(&c.fb,0x4284)==0); + xe.wakeOK=true;assert(XE::wrapDSISetAttribute(&c.fb,attr,2)==kIOReturnSuccess); + assert(!xe.dsiRuntimePower.sleeping && !xe.dsiStopAttempted && Display13FBMemory::field(&c.fb,0x4284)==2); + auto wakes=xe.wakeCalls;assert(XE::wrapDSISetAttribute(&c.fb,attr,2)==kIOReturnSuccess && xe.wakeCalls==wakes); + } + xe.dsiRuntimePower.failed=true;auto calls=xe.stopCalls; + assert(XE::wrapDSISetAttribute(&c.fb,attr,1)==kIOReturnNotReady && xe.stopCalls==calls); + } + + for(unsigned attr:{0x706F7772U,0x70777273U})for(unsigned state:{0U,1U}){Case c;xe.dsiStopTest=true;put(&c.fb,0x4284,2U); + assert(XE::wrapDSISetAttribute(&c.fb,attr,state)==kIOReturnNotReady && xe.stopCalls==1); + assert(Display13FBMemory::field(&c.fb,0x4284)==2); + xe.stopOK=true;assert(XE::wrapDSISetAttribute(&c.fb,attr,state)==kIOReturnSuccess && xe.stopCalls==2); + assert(Display13FBMemory::field(&c.fb,0x4284)==state); + assert(XE::wrapDSISetAttribute(&c.fb,attr,state)==kIOReturnSuccess && xe.stopCalls==2); + assert(XE::wrapDSISetAttribute(&c.fb,attr,2)==kIOReturnUnsupported && xe.stopCalls==2); + } + + {Case c;auto before=backingCalls; + assert(XE::wrapDSISetDisplayMode(&c.fb,0x10000,0)==kIOReturnNotReady && backingCalls==before+1); + assert(XE::wrapDSISetDisplayMode(&c.fb,0x10000,2)==kIOReturnNotReady && backingCalls==before+1); + assert(XE::wrapDSISetDisplayMode(&c.fb,0x10001,0)==kIOReturnNotReady && backingCalls==before+1); + xe.dsiStopAttempted=true; + assert(XE::wrapDSISetDisplayMode(&c.fb,0x10000,0)==kIOReturnNotReady && backingCalls==before+1);} + + {Case c;dsiFixedMode.clientMode=0x80001000;xe.publicationReady=true; + assert(!XE::wrapDSISetDisplayMode(&c.fb,static_cast(0x80001000),0)); + assert(Display13FBMemory::field(&c.fb,0x4250)==0x80001000); + assert(XE::wrapDSISetDisplayMode(&c.fb,static_cast(0x80001001),0)==kIOReturnNotReady);} + // Unsupported depth never reaches ordinary mode writes. + {Case c;auto before=modeCalls;assert(XE::wrapDSISetDisplayMode(&c.fb,0x10000,2)==kIOReturnNotReady); + assert(modeCalls==before && !fbLock.depth && !owner);} + {Case c;auto before=armCalls;XE::wrapDisplay13WMArmUpdate(&c.fb);assert(armCalls==before+1 && !fbLock.depth);} + {Case c;xe.dsiStopAttempted=true;c.refuse();} + {Case c;xe.dsiResourcesReady=false;c.refuse();} + {Case c;display13FBPower.watermarkFaulted=true;c.refuse();} + {Case c;display13FBPower.memoryFaulted=true;c.refuse();} + {Case c;display13FBPower.bridge.state.suspended=true;c.refuse();} + {Case c;display13FBPower.pipe[0].destroyingPath=true;c.refuse();} + {Case c;display13FBPower.transcoders={};c.refuse();} + {Case c;display13FBPower.pipe[0].memoryFramebuffer=nullptr;c.refuse();} + {Case c;put(&c.path,0x3644,1U);c.refuse();} + {Case c;put(&c.path,0x3640,1U);c.refuse();} + {Case c;put(&c.path,0x32B0,&c.peer);c.refuse();} + {Case c;put(&c.port,0x550,2U);c.refuse();} + {Case c;put(&c.fb,0x4A00,0U);c.refuse();} + {Case c;c.path.size=0x100;c.refuse();} + {Case c;put(&c.fb,0x4220,static_cast(nullptr));c.refuse();} + // Real external identity stays admitted during DSI stop/fault. The existing + // body gets its original return/arguments; DSI does not poison peer updates. + {Case c;panel=nullptr;put(&c.path,0x3640,1U);put(&c.port,0x120,3U);xe.dsiStopAttempted=true;display13FBPower.memoryFaulted=true; + auto a=armCalls,m=modeCalls;expectDSI=false;XE::wrapDisplay13WMArmUpdate(&c.fb); + assert(armCalls==a+1 && XE::wrapDSISetDisplayMode(&c.fb,0x10000,2)==7 && modeCalls==m+1 && !fbLock.depth);} + // Without DSI, forward unchanged even when this adapter cannot interpret FB. + {Case c;xe.adlnOutputWiring.dsi=0;auto a=armCalls,m=modeCalls;expectDSI=false; + XE::wrapDisplay13WMArmUpdate(nullptr);assert(armCalls==a+1); + assert(XE::wrapDSISetDisplayMode(nullptr,0x10000,2)==7 && modeCalls==m+1);} + // Shared callback policy is read under the owner lock. A retired DSI + // cannot revive through pipe A, retained FB identity, or an unknown pipe. + {Case c;IORecursiveLockLock(&fbLock);IOLockLock(display13FBPower.lock); + assert(!XE::dsiSharedWriterStopped(&c.fb,0));xe.dsiStopAttempted=true; + assert(XE::dsiSharedWriterStopped(&c.fb,0) && XE::dsiSharedWriterStopped(&c.fb,1)); + assert(XE::dsiSharedWriterStopped(&c.peer,UINT32_MAX)); + assert(!XE::dsiSharedWriterStopped(&c.peer,1) && !XE::dsiSharedWriterStopped(&c.peer,2)); + xe.adlnOutputWiring.dsi=0;assert(!XE::dsiSharedWriterStopped(&c.fb,0)); + assert(!display13FBPower.memoryFaulted);IOLockUnlock(display13FBPower.lock);IORecursiveLockUnlock(&fbLock);} + // Actual writer wrappers count nesting and release counts on native errors. + {Case c;nestedCursor=true;auto i=imageCalls,s=stateCalls; + assert(XE::wrapDSICursorImage(&c.ctl,&c.fb,&imageCalls)==0xe0000002U); + assert(imageCalls==i+1 && stateCalls==s+1 && !display13FBPower.dsiNativeWriters);} + {Case c;assert(XE::wrapDSICursorState(&c.ctl,&c.fb,-17,42,true,false)==0xe0000001U); + assert(!display13FBPower.dsiNativeWriters);} + {Case c;xe.dsiStopAttempted=true;c.refuseCursor();} + {Case c;xe.dsiResourcesReady=false;c.refuseCursor();} + {Case c;xe.dsiNativeWriterHooksReady=false;c.refuseCursor();} + {Case c;display13FBPower.memoryFaulted=true;c.refuseCursor();} + {Case c;pendingFB=&c.fb;c.refuseCursor();pendingFB=nullptr;} + {Case c;display13FBPower.dsiNativeWriters=UINT32_MAX;c.refuseCursor(); + assert(display13FBPower.dsiNativeWriters==UINT32_MAX);} + {Case c;auto i=imageCalls;assert(XE::wrapDSICursorImage(&c.peer,&c.fb,&imageCalls)==kIOReturnNotReady); + assert(i==imageCalls && !display13FBPower.dsiNativeWriters);} + {Case c;put(&c.fb,0x4220,static_cast(nullptr));c.refuseCursor();} + {Case c;panel=nullptr;put(&c.path,0x3640,1U);put(&c.port,0x120,3U); + xe.dsiStopAttempted=true;expectedWriters=0; + assert(XE::wrapDSICursorState(&c.ctl,nullptr,-17,42,true,false)==kIOReturnNotReady); + assert(XE::wrapDSICursorState(&c.ctl,&c.fb,-17,42,true,false)==0xe0000001U); + assert(!display13FBPower.dsiNativeWriters); + panel=&c.fb; // Identity predicate is irrelevant for disabled DSI. + xe.adlnOutputWiring.dsi=0; + assert(XE::wrapDSICursorImage(&c.ctl,&c.fb,&imageCalls)==0xe0000002U);} + for(unsigned i=0;i<5;++i){ + {Case c;auto before=txCalls[i];assert(callTx(i,&c.fb)==txResult(i)); + assert(txCalls[i]==before+1 && !display13FBPower.dsiNativeWriters && !owner && !fbLock.depth);} + for(unsigned bad=0;bad<5;++bad){Case c;auto before=txCalls[i]; + if(bad==0)xe.dsiStopAttempted=true; + if(bad==1)xe.dsiResourcesReady=false; + if(bad==2)xe.dsiNativeWriterHooksReady=false; + if(bad==3)display13FBPower.memoryFaulted=true; + if(bad==4)put(&c.fb,0x1D0,&c.peer); + assert(callTx(i,&c.fb)==kIOReturnNotReady && txCalls[i]==before && !display13FBPower.dsiNativeWriters);} + {Case c;panel=nullptr;put(&c.path,0x3640,1U);put(&c.port,0x120,3U);xe.dsiStopAttempted=true; + expectedWriters=0;assert(callTx(i,&c.fb)==txResult(i) && !display13FBPower.dsiNativeWriters);} + {Case c;xe.adlnOutputWiring.dsi=0;nativeFB=nullptr;expectedWriters=0; + assert(callTx(i,nullptr)==txResult(i) && !display13FBPower.dsiNativeWriters);} + } + {Case c;nestedTx=true;auto before=txCalls[2];assert(callTx(0,&c.fb)==txResult(0)); + assert(txCalls[2]==before+1 && !display13FBPower.dsiNativeWriters && !fbLock.depth && !owner);} + // Ordinary OS attributes reach native; lifetime accounting spans + // their nested flip/color work and preserves full pointer-sized values. + for(unsigned attr:{0x77737276U,0x63617064U,0x63727363U,0x706E6C70U,0x73484443U,0x2070656EU,0x20637463U,0xDEADBEEFU}){Case c;expectedAttribute=attr;expectedValue=txOptions;nestedAttribute=true; + const auto before=attrCalls[0];assert(XE::wrapDSISetAttribute(&c.fb,attr,expectedValue)==0xe0000121U); + assert(attrCalls[0]==before+1 && !display13FBPower.dsiNativeWriters && !owner && !fbLock.depth);} + for(unsigned attr:{0x00647064U,0x72677363U,0x67677363U,0x62677363U,0x67736320U,0x666C7573U,0x76626C6DU,0x64666C67U,0x00677264U,0x20627063U,0x706F7772U,0xDEADBEEFU}){ + Case c;expectedAttribute=attr;expectedValue=txOptions;expectedConnection=0; + assert(XE::wrapDSISetConnectionAttribute(&c.fb,0,attr,expectedValue)==0xe0000122U && !display13FBPower.dsiNativeWriters); + assert(XE::wrapDSISetConnectionAttribute(&c.fb,1,attr,expectedValue)==kIOReturnUnsupported);} + // Transport-only routes remain blocked, independently of the selector + // namespace. Native/IOGraphics handles all other attributes and return values. + for(unsigned attr:{0x68646370U,0x73707772U}) { + Case c;const auto before=attrCalls[0];const auto original=c.fb; + assert(XE::wrapDSISetAttribute(&c.fb,attr,0)==kIOReturnUnsupported); + assert(attrCalls[0]==before && !memcmp(&c.fb,&original,sizeof(c.fb)));} + for(unsigned attr:{0x706E6C74U,0x00696772U,0x7073723FU,0x70726F62U,0x64707461U,0x7073636EU, + 0x00617572U,0x61756D73U,0x61757063U}) { + Case c;const auto before=attrCalls[1];const auto original=c.fb; + assert(XE::wrapDSISetConnectionAttribute(&c.fb,0,attr,0)==kIOReturnUnsupported); + assert(attrCalls[1]==before && !memcmp(&c.fb,&original,sizeof(c.fb)) && !display13FBPower.memoryFaulted);} + {Case c;xe.dsiStopAttempted=true;const auto before=attrCalls[0]; + assert(XE::wrapDSISetAttribute(&c.fb,0x626B7376,123)==0); + assert(XE::wrapDSISetAttribute(&c.fb,0x626B7376,0x100000001ULL)==kIOReturnUnsupported); + assert(attrCalls[0]==before && !display13FBPower.dsiNativeWriters);} + {Case c;const auto before=attrCalls[1]; + assert(XE::wrapDSISetConnectionAttribute(&c.fb,0,0x626B6C74,12345)==0); + assert(XE::wrapDSISetConnectionAttribute(&c.fb,0,0x626B6C74,0x100000001ULL)==kIOReturnUnsupported); + assert(XE::wrapDSISetConnectionAttribute(&c.fb,1,0x626B6C74,0)==kIOReturnUnsupported); + assert(attrCalls[1]==before && !display13FBPower.dsiNativeWriters);} + for(unsigned old=0;old<3;++old)for(unsigned state=0;state<4;++state){Case c;put(&c.fb,0x4284,old);const auto before=attrCalls[2]; + const auto expected=old==2 && state==2?kIOReturnSuccess:kIOReturnUnsupported; + assert(XE::wrapDSISetPowerState(&c.fb,state)==expected && attrCalls[2]==before); + assert(Display13FBMemory::field(&c.fb,0x4284)==old); + assert(XE::wrapDSISetAttribute(&c.fb,0x706F7772,state)==expected);} + {Case c;put(&c.fb,0x4284,2U);assert(XE::wrapDSISetAttribute(&c.fb,0x706F7772,0x100000002ULL)==kIOReturnUnsupported);} + for(unsigned bad=0;bad<4;++bad){Case c;put(&c.fb,0x4284,2U);const auto before=attrCalls[0]+attrCalls[1]+attrCalls[2]; + if(bad==0)xe.dsiStopAttempted=true;if(bad==1)xe.dsiResourcesReady=false; + if(bad==2)xe.dsiNativeWriterHooksReady=false;if(bad==3)display13FBPower.memoryFaulted=true; + assert(XE::wrapDSISetAttribute(&c.fb,0x77737276,1)==kIOReturnNotReady); + assert(XE::wrapDSISetConnectionAttribute(&c.fb,0,0x72677363,1)==kIOReturnNotReady); + assert(XE::wrapDSISetPowerState(&c.fb,2)==kIOReturnNotReady); + assert(attrCalls[0]+attrCalls[1]+attrCalls[2]==before && !display13FBPower.dsiNativeWriters);} + for(bool disabled:{false,true}){Case c;expectedWriters=0;expectedValue=txOptions;expectedAttribute=0x706F7772;expectedConnection=4;expectedPower=0; + IOService *fb=&c.fb; + if(disabled){xe.adlnOutputWiring.dsi=0;fb=nativeFB=nullptr;} + else{panel=nullptr;put(&c.path,0x3640,1U);put(&c.port,0x120,3U);xe.dsiStopAttempted=true;} + assert(XE::wrapDSISetAttribute(fb,expectedAttribute,expectedValue)==0xe0000121U); + assert(XE::wrapDSISetConnectionAttribute(fb,expectedConnection,expectedAttribute,expectedValue)==0xe0000122U); + assert(XE::wrapDSISetPowerState(fb,expectedPower)==0xe0000123U);} + {Case c;auto before=streamCalls;assert(XE::wrapDSISetStream(&c.ctl,&c.fb,&c.path,¶mAnchor)==kIOReturnNotReady); + assert(streamCalls==before && !fbLock.depth && !owner);} + {Case c;panel=nullptr;put(&c.path,0x3640,1U);put(&c.port,0x120,3U);expectedPath=&c.path; + assert(XE::wrapDSISetStream(&c.ctl,&c.fb,&c.path,¶mAnchor)==0xe0000124U); + const auto before=streamCalls;assert(XE::wrapDSISetStream(&c.ctl,&c.fb,&c.port,¶mAnchor)==kIOReturnNotReady); + assert(streamCalls==before);} + {Case c;xe.adlnOutputWiring.dsi=0;nativeFB=nullptr;expectedPath=nullptr; + assert(XE::wrapDSISetStream(&c.ctl,nullptr,nullptr,¶mAnchor)==0xe0000124U);} + puts("PASS DSI public mode preflight and native update dispatch: stopped/faulted rejection, held FB serialization, nested cursor/transaction counts, transport-specific attribute/power admission, full-width arguments, native wait lock boundaries, peer and disabled-DSI forwarding"); +} +'''.replace('SCOPE',scope).replace('FUNCTIONS',functions) +with tempfile.TemporaryDirectory(prefix='dsi-update-') as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(root/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) diff --git a/tests/firmware_readout.py b/tests/firmware_readout.py index 7f55bea1..448558db 100644 --- a/tests/firmware_readout.py +++ b/tests/firmware_readout.py @@ -1,37 +1,64 @@ #!/usr/bin/env python3 """Execute production firmware acquisition with independent Display/Render profiles.""" from pathlib import Path -import subprocess,tempfile +import subprocess,tempfile,sys r=Path(__file__).resolve().parents[1] s=(r/'WhateverXe/kern_firmware.cpp').read_text();body=s[s.index('static void publishBytes'):] code=r''' #include #include +#include +#include +#include +#include +#include #include "kern_xelp_platform.hpp" #include "kern_vbt.hpp" +#include "kern_dsi_math.hpp" +bool auditEnabled=false; +bool checkKernelArgument(const char *arg){assert(!strcmp(arg,"-wex-dsi-audit"));return auditEnabled;} +#define OSDynamicCast(type,ptr) (ptr) constexpr unsigned kIODirectionIn=1,kIOMapReadOnly=2; -struct OSData {static OSData *withBytes(const void*,unsigned){static OSData d;return &d;}void release(){}}; +struct OSData { + std::vector bytes; + static OSData *withBytes(const void *p,unsigned n){auto d=new OSData;d->bytes.assign((const uint8_t*)p,(const uint8_t*)p+n);return d;} + void release(){delete this;}const void *getBytesNoCopy(){return bytes.data();}unsigned getLength(){return bytes.size();} +}; struct IOMemoryDescriptor { struct Map {uintptr_t getVirtualAddress(){return 0;}unsigned getLength(){return 0;}void release(){}}; static IOMemoryDescriptor *withPhysicalAddress(uint64_t,unsigned,unsigned){return nullptr;} Map *map(unsigned){return nullptr;}void release(){} }; struct IOPCIDevice {unsigned reads=0,publications=0;uint32_t configRead32(unsigned off){assert(off==0xfc);++reads;return 0;} - void setProperty(const char*,uint32_t,unsigned){++publications;}void setProperty(const char*,const void*,unsigned){++publications;}void setProperty(const char*,OSData*){++publications;} + std::map properties; + OSData *getProperty(const char *key){auto i=properties.find(key);return i==properties.end()?nullptr:&i->second;} + void setProperty(const char*,uint32_t,unsigned){++publications;}void setProperty(const char*,const void*,unsigned){++publications;} + void setProperty(const char *key,OSData *d){++publications;properties[key]=*d;} }; struct XE {XELP::Profile xeProfile;XELP::VBT::Ports firmwarePorts;void xelpReadFirmware(IOPCIDevice*);}; BODY -int main(){ +int main(int argc,char **argv){ for(unsigned kind=0;kind<5;++kind){XE x;IOPCIDevice pci; x.xeProfile=kind==0?XELP::identify(0x46a68086,12,0x9a498086):kind==1?XELP::identify(0x46a68086,12):kind==2?XELP::identify(0x9a498086,1):kind==3?XELP::identify(0x46d18086,0):XELP::identify(0x4c8a8086,0); const bool expected=kind!=4; if(!kind)assert(x.xeProfile.admitted() && x.xeProfile.displayAdmitted()); x.xelpReadFirmware(&pci);assert(pci.reads==unsigned(expected));assert(bool(pci.publications)==expected); + assert(!pci.getProperty("WEXDSIFirmwareAudit")); x.xelpReadFirmware(nullptr); } + auditEnabled=true;XE x;IOPCIDevice pci;x.xeProfile=XELP::identify(0x46d48086,0); + x.xelpReadFirmware(&pci);auto absent=pci.getProperty("WEXDSIFirmwareAudit");assert(absent&&absent->bytes.size()==33*4); + assert(XELP::VBT::u32(absent->bytes.data()+4)==unsigned(XELP::DSI::ProfileStatus::Bounds)); + if(argc==2){ + std::ifstream f(argv[1],std::ios::binary);assert(f); + pci.properties["TGLVBT"].bytes.assign(std::istreambuf_iterator(f),{}); + x.xelpReadFirmware(&pci);auto data=pci.getProperty("WEXDSIFirmwareAudit");assert(data); + assert(XELP::VBT::u32(data->bytes.data()+4)==unsigned(XELP::DSI::ProfileStatus::Valid)); + assert(XELP::VBT::u32(data->bytes.data()+16*4)==359); + } puts("PASS production VBT acquisition: physical and legacy-selector ADL-P plus TGL/ADL-N read firmware; unsupported RKL remains closed");} '''.replace('BODY',body) with tempfile.TemporaryDirectory(prefix='firmware-readout-') as d: p=Path(d);(p/'test.cpp').write_text(code) subprocess.run(['clang++','-std=c++17','-O1','-fsanitize=address,undefined','-I'+str(r/'WhateverXe'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) - subprocess.run([str(p/'test')],check=True) + subprocess.run([str(p/'test'),*sys.argv[1:]],check=True) diff --git a/tests/fixtures/boot_argument_names.json b/tests/fixtures/boot_argument_names.json index afbfff46..9bef982c 100644 --- a/tests/fixtures/boot_argument_names.json +++ b/tests/fixtures/boot_argument_names.json @@ -1,4 +1,11 @@ { + "-wex-dsi-audit": [], + "-wex-dsi-adopt": [], + "-wex-dsi-panel-only": [], + "-wex-dsi-command-test": [], + "-wex-dsi-panel-test": [], + "-wex-dsi-host-test": [], + "-wex-dsi-stop-test": [], "-wex-adln-config-stage": [ "-adlnconfigstage", "-n97configstage" @@ -138,5 +145,7 @@ "-wex-display-trace": [], "-wex-adlp-readout": [], "-wex-adlp-load-only": [], - "-wex-display13-offline": [] + "-wex-display13-offline": [], + "-wex-dsi-hdmi-test": [], + "-wex-dsi-power-test": [] } diff --git a/tests/framebuffer_common.py b/tests/framebuffer_common.py index 38c9a3b3..c30a1bed 100644 --- a/tests/framebuffer_common.py +++ b/tests/framebuffer_common.py @@ -60,6 +60,7 @@ }; bool tglInstallSonomaSurfaceABI(KernelPatcher&,size_t,uintptr_t,size_t,bool){return surfaceOK;} bool xelpInstallNVRAMABI(KernelPatcher&,size_t,uintptr_t,size_t){return nvramOK;} +unsigned shutdownCalls=0; struct XE { bool framebufferDonorAttempted=false,framebufferDonorCommonReady=false,framebufferBackendInstalled=false; bool display13FramebufferRequested=false,tglPlatformNVRAMReady=false,tglProbeReady=false,tglFramebufferInitReady=false; @@ -69,6 +70,7 @@ uintptr_t xelpProviderProbe=0x2000,xelpProviderConfigureProbe=0x3000; unsigned commonRuntimeCalls=0,backendCalls=0,startCalls=0;bool stopDuringStart=false,nativeOK=true; bool processFramebufferKext(KernelPatcher&,size_t,uintptr_t,size_t); + bool processDisplay13Shutdown(KernelPatcher&,size_t,uintptr_t,size_t){++shutdownCalls;return true;} bool processFramebufferCommon(KernelPatcher&,size_t,uintptr_t,size_t); bool processFramebufferDonorRuntime(KernelPatcher&,size_t,uintptr_t,size_t){++commonRuntimeCalls;return runtimeOK;} bool processFramebufferAGDC(KernelPatcher&,size_t,uintptr_t,size_t){return agdcOK;} @@ -81,7 +83,7 @@ static bool wrapFramebufferStart(IOService*,IOService*); static bool wrapDisplay13FramebufferStart(IOService*,IOService*); static bool wrapTGLFramebufferStart(IOService*,IOService*); - static void wrapTGLProbePort(){}static void wrapFramebufferInit(){}static void wrapFramebufferComputeLanes(){} + static void wrapDisplay13ProbeMode(){}static void wrapTGLProbePort(){}static void wrapFramebufferInit(){}static void wrapFramebufferComputeLanes(){} }; XE *callbackXE; bool XE::wrapDisplay13FramebufferStart(IOService*,IOService*){assert(callbackXE->display13FramebufferRequested);assert(!callbackXE->framebufferStartedController);if(callbackXE->stopDuringStart)callbackXE->tglLeasesStopping=true;++callbackXE->startCalls;return callbackXE->nativeOK;} @@ -97,19 +99,29 @@ if(argc>1)assert(!memcmp(image.data()+span.first,span.second.data(),span.second.size())); else memcpy(image.data()+span.first,span.second.data(),span.second.size()); } - for(bool display13:{false,true})for(unsigned failure=0;failure<13;++failure){ + for(bool display13:{false,true})for(unsigned failure=0;failure<14;++failure){ + shutdownCalls=0; auto b=image;uintptr_t pure=0x1234;memcpy(b.data()+0xccad0+0x188,&pure,sizeof(pure)); XE x;callbackXE=&x;x.display13FramebufferRequested=display13;KernelPatcher p; surfaceOK=failure!=5;nvramOK=failure!=6;x.runtimeOK=failure!=7;x.agdcOK=failure!=8;x.backendOK=failure!=9;x.trainingOK=failure!=10;x.stopDuringStart=failure==11;x.nativeOK=failure!=12; if(failure>=1&&failure<=4)p.failAt=failure; bool handled=x.processFramebufferKext(p,1,(uintptr_t)b.data(),b.size()); + assert(shutdownCalls==(display13 && failure!=1?1U:0U)); assert(handled==(failure!=1)); - IOService c;assert(XE::wrapFramebufferStart(&c,&c)==(failure==0)); + IOService c; + if(failure==13){XELPComboClock::ClockSnapshot clocks;clocks.layout=XELPComboClock::Layout::Display13; + clocks.mux[0]=0xC01;clocks.enable[1]=0xCC000000; + assert(comboClockTransaction.allocation.retain(0,1,reinterpret_cast(&c),clocks));} + assert(XE::wrapFramebufferStart(&c,&c)==(failure==0)); assert(x.startCalls==((failure==0||failure==11||failure==12)?1U:0U)); assert(x.framebufferStartedController==(failure==0?static_cast(&c):nullptr)); if(failure==0)assert(!XE::wrapFramebufferStart(&c,&c)&&x.startCalls==1); + if(failure==13){assert(!x.framebufferStartInProgress && comboClockTransaction.allocation.binding[0]==2); + assert(comboClockTransaction.allocation.release(0,reinterpret_cast(&c))); + assert(XE::wrapFramebufferStart(&c,&c) && x.startCalls==1);} const auto calls=p.attempts,backends=x.backendCalls; x.processFramebufferKext(p,1,(uintptr_t)b.data(),b.size()); + assert(shutdownCalls==(display13 && failure!=1?1U:0U)); assert(p.attempts==calls&&x.backendCalls==backends); // no second trampoline } puts("PASS common-before-backend order, single ownership, partial failures and mandatory native-start gate"); diff --git a/tests/host_publication.py b/tests/host_publication.py index a80017bd..a86433aa 100644 --- a/tests/host_publication.py +++ b/tests/host_publication.py @@ -43,6 +43,7 @@ bool routeMultiple(size_t,RouteRequest*,unsigned,mach_vm_address_t,size_t){return true;} void clearError(){} }; +// The optional observer is inactive in this host-publication fixture. namespace XeDataAccess {bool processKext(KernelPatcher&,size_t,mach_vm_address_t,size_t){return false;}void processKernel(KernelPatcher &){assert(published);events.push_back(7);}} struct Graphics {size_t loadIndex=4;bool off=false;void switchOff(){off=true;}} graphics; struct {size_t loadIndex=5;}backlight; diff --git a/tests/kernel_os_updates.cpp b/tests/kernel_os_updates.cpp index faf7d820..de642ecf 100644 --- a/tests/kernel_os_updates.cpp +++ b/tests/kernel_os_updates.cpp @@ -1,5 +1,6 @@ #include "kern_xelp_os.hpp" #include +#include int main() { using namespace XELP; @@ -12,6 +13,14 @@ int main() { assert(kernelOS("Darwin Kernel Version 24.6.1: update/RELEASE_X86_64")==OS::Sequoia); assert(kernelOS("Darwin Kernel Version 25.6.1: update/RELEASE_X86_64")==OS::Tahoe); assert(kernelOS("Darwin Kernel Version 25.7.0: update/RELEASE_X86_64")==OS::Tahoe); + // Metamorphic family acceptance: vary servicing minor/patch and arbitrary + // build metadata, without adding known Apple builds to an allowlist. + for(unsigned major=24;major<=25;++major) for(unsigned minor=6;minor<(major==24 ? 7U : 9U);++minor) + for(unsigned patch=0;patch<100;patch+=7) { + char version[160]; + snprintf(version,sizeof(version),"Darwin Kernel Version %u.%u.%u: synthetic-%u/RELEASE_X86_64",major,minor,patch,minor*109+patch); + assert(kernelOS(version)==(major==24 ? OS::Sequoia : OS::Tahoe)); + } const char *rejected[] { "Darwin Kernel Version 23.6.0: update/RELEASE_X86_64", "Darwin Kernel Version 24.5.0: update/RELEASE_X86_64", diff --git a/tests/native_placement.cpp b/tests/native_placement.cpp new file mode 100644 index 00000000..e24768ab --- /dev/null +++ b/tests/native_placement.cpp @@ -0,0 +1,80 @@ +#include "kern_xelp_native_placement.hpp" +#include +#include +#include +#include +#include +using Bytes=std::vector; +struct Program { + Bytes bytes;std::map boundaries; + size_t add(std::initializer_list instruction) { + auto start=bytes.size();boundaries[start]=instruction.size(); + bytes.insert(bytes.end(),instruction);return start; + } + void nop(unsigned n) {while(n--)add({0x90});} + void relative(size_t at,uintptr_t base,uintptr_t target) { + int64_t delta=static_cast(target)-static_cast(base+at+4); + assert(delta>=INT32_MIN && delta<=INT32_MAX); + int32_t d=static_cast(delta);memcpy(bytes.data()+at,&d,4); + } + // Independent instruction-boundary oracle for the deliberately assembled + // fixtures. Native replay separately exercises Lilu's actual HDE decoder. + size_t decode(const uint8_t *p,size_t) const { + auto entry=boundaries.find(p-bytes.data());return entry==boundaries.end()?0:entry->second; + } + bool protect(uintptr_t base,uintptr_t target,bool tahoe) const { + return XELPNative::protectCall(bytes.data(),bytes.size(),base,target,tahoe, + [this](const uint8_t *p,size_t n){return decode(p,n);}); + } + void arguments(uintptr_t base,uintptr_t target,bool tahoe,unsigned padding=0) { + if(tahoe)add({0x48,0x83,0xec,8}); + add({0x4c,0x89,0xff});nop(padding);add({0x4c,0x89,0xee});add({0x89,0xc2}); + add({0x8b,0x4d,0xd4});add({0x44,0x8b,0x4d,0xd0}); + if(!tahoe)add({0x6a,0}); + add({0x48,0x8d,0x45,0xb8});add({0x50});add({0x6a,0x20});nop(padding);add({0x6a,1}); + auto call=add({0xe8,0,0,0,0});relative(call+1,base,target); + add({0x48,0x83,0xc4,0x20}); + } +}; +int main() { + unsigned cases=0; + for(unsigned n=0;n<256;++n) { + uintptr_t base=0xffffff8000000000ULL+n*0x10000,store=base+0x678900-n*13,target=base-0x456780+n*17; + Program p;p.nop(n%7);p.add({0x55});p.nop(n%5);p.add({0x48,0x89,0xe5});p.add({0x53});p.add({0x50}); + p.add({0x48,0x89,0xfb});p.nop(n%3);auto lea=p.add({0x48,0x8d,0x3d,0,0,0,0});p.relative(lea+3,base,store); + p.nop(n%4);p.add({0x48,0x89,0xde});auto call=p.add({0xe8,0,0,0,0});p.relative(call+1,base,target); + p.add({0x48,0x85,0xc0}); + assert(XELPNative::rootCall(p.bytes.data(),p.bytes.size(),base,store)==target); + assert(!XELPNative::rootCall(p.bytes.data(),p.bytes.size(),base,store+1)); + assert(!XELPNative::rootCall(p.bytes.data(),call+4,base,store)); + p.bytes[lea+2]=0x35;assert(!XELPNative::rootCall(p.bytes.data(),p.bytes.size(),base,store)); + for(bool tahoe:{false,true}) { + Program q;q.nop(n);q.arguments(base,target,tahoe,n%3);q.add({0xc3}); + assert(q.protect(base,target,tahoe));assert(!q.protect(base,target+1,tahoe)); + assert(!q.protect(base,target,!tahoe)); + // A second valid call before RET is ambiguous, not a second opportunity. + Program duplicate;duplicate.arguments(base,target,tahoe);duplicate.arguments(base,target,tahoe);duplicate.add({0xc3}); + assert(!duplicate.protect(base,target,tahoe)); + q.bytes.back()=0x90;assert(!q.protect(base,target,tahoe)); + ++cases; + } + } + // Instruction-looking data in an opaque instruction is never a call site. + for(bool tahoe:{false,true}) { + Program p;p.arguments(0x10000,0x20000,tahoe);p.boundaries.clear();p.boundaries[0]=p.bytes.size();p.add({0xc3}); + assert(!p.protect(0x10000,0x20000,tahoe)); + Program immediate;immediate.add({0x48,0xb8,0xe8,0,0,0,0,0x4c,0x89,0xff});immediate.add({0xc3}); + assert(!immediate.protect(0x10000,0x10007,tahoe)); + Program early;early.add({0xc3});early.arguments(0x10000,0x20000,tahoe);early.add({0xc3}); + assert(!early.protect(0x10000,0x20000,tahoe)); + } + Program field;field.add({0x55});field.nop(3);field.add({0x48,0x89,0xe5}); + field.add({0x48,0xb8,0,0,0,0,0,0x80,0xff,0xff});field.nop(2);field.add({0x48,0x01,0xf0}); + field.add({0x48,0xc1,0xe8,0x30});field.add({0x3d,0xff,0xff,0,0}); + field.add({0x0f,0x82,0x12,0x34,0x56,0x78});field.nop(4);field.add({0x48,0x8b,0x47,0x50}); + assert(XELPNative::rootAccessor(field.bytes.data(),field.bytes.size())); + field.bytes.back()=0x58;assert(!XELPNative::rootAccessor(field.bytes.data(),field.bytes.size())); + assert(!XELPNative::rootCall(field.bytes.data(),field.bytes.size(),UINTPTR_MAX-8,1)); + puts("PASS: moved verifier arguments/calls/accessor field; wrong source, ABI, bounds and overflow rejected"); + printf("PASS: %u independently relocated protect call sites; duplicate, wrong-family/target and embedded bytes rejected\n",cases); +} diff --git a/tests/pmap_kernel_contract.py b/tests/pmap_kernel_contract.py new file mode 100644 index 00000000..fa5f763e --- /dev/null +++ b/tests/pmap_kernel_contract.py @@ -0,0 +1,75 @@ +"""Check runtime guards in any supplied x86_64 kernel/KC, without a build list.""" +import re +import struct +import subprocess +import tempfile +from pathlib import Path + + +def check(kernel, source, family, hde_source): + assert hde_source and hde_source.is_file(), '--kernel requires --hde-source (Lilu hde/hde64.c)' + data = kernel.read_bytes() + assert data[:4] == bytes.fromhex('cffaedfe'), 'expected x86_64 Mach-O' + symbols = {} + for line in subprocess.check_output(['nm', str(kernel)], text=True).splitlines(): + parts = line.split() + if len(parts) == 3: + try: + symbols[parts[2]] = int(parts[0], 16) + except ValueError: + pass + segments, pos = [], 32 + for _ in range(struct.unpack_from('\((fn|protect)(\+0x[0-9A-Fa-f]+)?\),([A-Za-z]+),sizeof', block) + assert len(checks) == 8, checks + for base, offset, name in checks: + address = (fn if base == 'fn' else protect) + (int(offset[1:], 16) if offset else 0) + values = re.search(r'const uint8_t ' + name + r'\[\] \{([^}]+)\}', block)[1] + expected = bytes(int(v.strip(), 0) for v in values.split(',')) + assert read(address, len(expected)) == expected, name + assert 'tglNativeProtectCall(protect,fn,' + ('true' if family == 'Tahoe' else 'false') + ')' in block + protect_code = read(protect, 512) + verify = symbols['_pmap_verify_noncacheable'] + caller = read(verify, 96) + store = symbols['_kernel_pmap_store'] + # Resolve the call with the production matcher below before fetching its + # callee. No verifier/call/field offsets or particular relative distance. + include = Path(__file__).resolve().parents[1] / 'WhateverXe' + with tempfile.TemporaryDirectory(prefix='xe-root-target-') as directory: + path = Path(directory) + (path / 'target.cpp').write_text('#include "kern_xelp_native_placement.hpp"\n#include \nint main(){unsigned char b[]={' + ','.join(map(str,caller)) + '};printf("%llx",(unsigned long long)XELPNative::rootCall(b,sizeof(b),' + str(verify) + 'ULL,' + str(store) + 'ULL));}') + subprocess.run(['clang++','-std=c++17','-I'+str(include),str(path/'target.cpp'),'-o',str(path/'target')],check=True) + target = int(subprocess.check_output([str(path/'target')],text=True),16) + assert target + accessor = read(target, 64) + # Execute the production decoder/field contract on the real native bytes. + def literal(value): + return ','.join(str(x) for x in value) + # Compile the exact production decoder wrapper against Lilu's HDE source. + decoder = re.search(r'static size_t tglNativeInstructionSize\(.*?\n\}',source,re.S)[0] + decoder = decoder.replace('Disassembler::hde_t','hde64s').replace('Disassembler::hdeDisasm(reinterpret_cast(code),','hde64_disasm(code,') + cpp = '#include "kern_xelp_native_placement.hpp"\n#include "hde64.h"\n#include \n' + decoder + '\nint main(){\n' + cpp += 'unsigned char protect[]={' + literal(protect_code) + '}, accessor[]={' + literal(accessor) + '};\n' + cpp += f'assert(XELPNative::protectCall(protect,sizeof(protect),{protect}ULL,{fn}ULL,' + ('true' if family=='Tahoe' else 'false') + ',tglNativeInstructionSize));\n' + cpp += 'assert(XELPNative::rootAccessor(accessor,sizeof(accessor)));}\n' + with tempfile.TemporaryDirectory(prefix='xe-kernel-contract-') as directory: + path = Path(directory) + (path / 'test.cpp').write_text(cpp) + headers = include.parent / 'Lilu.kext/Contents/Resources/Headers' + subprocess.run(['clang','-I'+str(headers),'-c',str(hde_source),'-o',str(path/'hde.o')],check=True) + subprocess.run(['clang++', '-std=c++17', '-I' + str(include), '-I'+str(headers), str(path / 'test.cpp'), str(path/'hde.o'), '-o', str(path / 'test')], check=True) + subprocess.run([str(path / 'test')], check=True) + print('PASS: 8 instruction/stack guards, decoded native protect/verifier calls and private pmap accessor; no build/hash/call-offset admission') diff --git a/tests/production_source.py b/tests/production_source.py new file mode 100644 index 00000000..996d2f03 --- /dev/null +++ b/tests/production_source.py @@ -0,0 +1,14 @@ +"""Read production source, expanding same-translation-unit implementation parts. + +Replay tests must extract the compiled implementation rather than a stale copy. +Ordinary headers stay as includes and are resolved by the C++ fixture compiler. +""" +import re +from pathlib import Path + + +def read_source(path): + path = Path(path) + return re.sub(r'^#include "([^"\n]+\.inc)"[ \t]*$', + lambda m: read_source(path.parent / m[1]), + path.read_text(), flags=re.M) diff --git a/tests/provider_binding.py b/tests/provider_binding.py index 88f54895..5ded7e6e 100644 --- a/tests/provider_binding.py +++ b/tests/provider_binding.py @@ -24,3 +24,9 @@ str(cpp), "-o", str(exe)], check=True) for mode in ["adlp-fb", "adlp-auto", "rplp-auto", "tgl", "tgl-9a40", "tgl-g4", "adln", "adln-16eu", "adln-abi1", "unknown", "adln-revision", "adln-fake", "tgl-fake", "adln-abi1-fake", "adls", "adls-revision", "rpls", "rpls-revision"]: subprocess.run([str(exe), mode], check=True) + # Every Linux ADL-P/RPL-P/U ID formerly missing from the shared table must + # pass the actual Compat binding, physical-owner and native probe paths. + for pci in ["4626", "4628", "462a", "46a0", "46a1", "46a2", "46b0", "46b1", "46b2", "46c0", "46c1", "46c2", "a720", "a721"]: + subprocess.run([str(exe), "adlp-auto", pci], check=True) + for pci in ["9a59", "9ac0", "9ac9", "9ad9", "9af8"]: + subprocess.run([str(exe), "tgl", pci], check=True) diff --git a/tests/provider_binding_fixture.cpp b/tests/provider_binding_fixture.cpp index 3fd677a7..54ff4102 100644 --- a/tests/provider_binding_fixture.cpp +++ b/tests/provider_binding_fixture.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -34,10 +35,10 @@ int main(int argc,char **argv) { assert(XELP::display13TCDPProbeCapable(xelpProbeCapabilities)); assert(!XELP::display13TCDPProbeCapable(+[](uint32_t,uint32_t)->uint32_t{return 3;})); for(unsigned schema=0;schema<5;++schema)for(unsigned abi=0;abi<6;++abi) - assert(xelpProbeCapabilities(schema,abi)==(schema==1&&abi==3?(XELP::ProbeDisplay13ComboDP|XELP::ProbeDisplay13FixedTC|XELP::ProbeDisplay13FixedDP|XELP::ProbeDisplay13Panel):0)); + assert(xelpProbeCapabilities(schema,abi)==(schema==1&&abi==3?(XELP::ProbeDisplay13ComboDP|XELP::ProbeDisplay13FixedTC|XELP::ProbeDisplay13FixedDP|XELP::ProbeDisplay13Panel|XELP::ProbeDisplay13DPAlt):0)); assert(!XELP::display13ProbeCapable(+[](uint32_t,uint32_t)->uint32_t{return 0;})); assert(!XELP::display13ProbeCapable(+[](uint32_t,uint32_t)->uint32_t{return 2;})); - assert(argc==2);const bool adln=!strncmp(argv[1],"adln",4); + assert(argc==2 || argc==3);const bool adln=!strncmp(argv[1],"adln",4); const bool rpls=!strncmp(argv[1],"rpls",4); const bool adls=!strncmp(argv[1],"adls",4) || rpls; const bool adlp=!strncmp(argv[1],"adlp",4) || !strncmp(argv[1],"rplp",4); @@ -59,6 +60,7 @@ int main(int argc,char **argv) { if(fake){pci.identity=0x12348086;fakeID.value=adln?0x46D2:0x9A49;pci.property=&fakeID;} if(adlp){pci.identity=!strncmp(argv[1],"rplp",4)?0xA7A88086:0x46A68086;pci.revision=12; if(!strcmp(argv[1],"adlp-fb")){fakeID.value=0x9A49;pci.property=&fakeID;}} + if(argc==3)pci.identity=(uint32_t(strtoul(argv[2],nullptr,16))<<16)|0x8086; controller.provider=&pci;platform[0x12]=9; map.address=reinterpret_cast(mmio.data());map.bytes=mmio.size(); field(&controller,0xC40)=&access;field(&controller,0xD40)=platform; diff --git a/tests/sequoia_pmap.py b/tests/sequoia_pmap.py index 3ff620f5..39212f97 100644 --- a/tests/sequoia_pmap.py +++ b/tests/sequoia_pmap.py @@ -1,39 +1,18 @@ #!/usr/bin/env python3 -"""Execute actual 24H32 template bridges against a guarded synthetic pmap frame. +"""Execute actual Sequoia template bridges against a guarded synthetic pmap frame. This checks instruction/register/flag semantics only, not native locking, TLBs, or GPU coherency. Optional --kernel checks all source guards against exact bytes. """ from pathlib import Path -import argparse,hashlib,re,struct,subprocess,tempfile +import argparse,re,subprocess,tempfile R=Path(__file__).resolve().parents[1] s=(R/'WhateverXe/kern_xe.cpp').read_text() -a=argparse.ArgumentParser();a.add_argument('--kernel',type=Path);a=a.parse_args() +a=argparse.ArgumentParser();a.add_argument('--kernel',type=Path);a.add_argument('--hde-source',type=Path);a=a.parse_args() bridges='\n'.join(re.search(r'extern "C" __attribute__\(\(naked,used\)\) void tglSequoiaPmap'+kind+r'Bridge\(\) \{.*?\n\}',s,re.S)[0] for kind in ['New','Update']) if a.kernel: - d=a.kernel.read_bytes() - assert hashlib.sha256(d).hexdigest()=='fb8da6196bcc6828d0ea9c750239a9115854429cb24c1bb88437c66546019faa' - segments=[];pos=32 - for _ in range(struct.unpack_from('