Machine
- Lenovo ThinkBook 14+ (2024), Core Ultra 7 155H, iGPU
8086:7D55 rev 8, built-in 3072×1920 panel on pipe A
- macOS 15.8.1 (24H32), OpenCore 1.0.8, SIP 0x0A43
- XeGfx 0.0.47 from the release zip (binary unmodified), loaded through OpenCore
Kernel/Add (prelinked). It stays idle until kmutil load -b org.watchthelight.XeGfx, and was confirmed not loaded before that.
- boot-args:
-v keepsyms=1 debug=0x100 ps2kbdonly=1 itlwm_he=1 itlwm_mtl_internal_aux=1 amfi_get_out_of_my_way=0x1 amfi=0x80 ipc_control_port_options=0 xegfx.nvram_trace=0
Result
| Stage |
Result |
| 1 |
pass |
| 2 |
pass (XeGfxProbe result 1) |
| 3 |
pass |
| 4 |
fail: debug.xegfx_stage stays at 3 |
| 5, 6 |
not reached |
Stage 4 gets a long way before it fails:
drmn0: [drm] xegfx: display left alone (compat.linuxkpi.i915_xegfx_no_display)
drmn0: [drm] Skipping GT1 (Standalone Media GT) - MEI driver not available on FreeBSD
drmn0: [drm] xegfx: stolen allocator starts at 0x1680000, after the boot framebuffer
drmn0: successfully loaded firmware image 'i915/mtl_guc_70.bin'
drmn0: [drm] GT0: GuC firmware i915/mtl_guc_70.bin version 70.53.0
drmn0: [drm] GT0: GUC: submission enabled
drmn0: [drm] GT0: GUC: SLPC enabled
drmn0: [drm] GT0: GUC: RC enabled
<6>[drm] Initialized i915 1.6.0 for drmn0 on minor 0
After that first stage 4 run:
XeGfxGt = {"result"=1, "wedged"=0, "guc-fw-status"=10, "guc-submission"=1, "guc-version"=0x463500, "engines"=3,
"render-minor"=128, "gts"=1, "huc-fw-status"=-1, "gsc-fw-status"=-1, "gtawake-gts"=0}
XeGfxPost = {"result"=1, "frmcount-delta"=6, "ggtt-pte-surf"=0x800003, "plane-surf-1-a"=0xFE800000}
The display check, xg_drm_attached() and xg_gt_check() all pass, and yet the stage counter doesn't move. The only failure path left in xg_run_stage(4) is the boot surface reservation: xg_i915_reserve_bootfb(0xFE800000, 0x1680000) returns -ENOSPC. Because gBootSurf (0xFE800000) is below 0xFEE00000, rc = -28. I haven't seen the XeGfx: boot surface GGTT ... reserved: IOLog line itself, because my log capture missed it. This is deduced from the code path and the values above.
Why it overlaps
From stage 2 (XeGfxProbe):
PLANE_SURF_1_A = 0xFE800000
boot-fb-size = 0x1680000 (22.5 MiB, 3072×1920×4)
- stride 12288 bytes
dsm-base-lo = 0x7A800000
ggc = 0x2C0
So the GOP left the scanout at GGTT [0xFE800000, 0xFFE80000). That range crosses GUC_GGTT_TOP (0xFEE00000).
i915's ggtt_reserve_guc_top() reserves [0xFEE00000, 4 GiB) as ggtt->uc_fw. intel_uc_fw_init() → uc_fw_bind_ggtt() maps the GuC image at the start of that node, type 0, with INTEL_UC_RSVD_GGTT_PER_FW = 2 MiB per slot. The image stays mapped until intel_uc_fw_fini(). So once i915 initialises, the panel's PTEs from 0xFEE00000 point at the GuC binary. That point is 6 MiB into the framebuffer: rows 512 to about 538 on this panel, for the 320 KiB image. XeGfxPost only checks the PTE at the surface start (0x800003, unchanged), so the overlap doesn't show up there. (References are Linux v6.10. I assume the drm-kmod copy in XeGfx matches.)
The panel confirms it: after the stage runs, a thin strip of coloured noise runs across the full width, starting about 27–28% of the way down. That's measured from a photo, so perspective skews it a little. Row 512 of 1920 is 26.7%, and the noise looks like firmware bytes being scanned out as pixels. The strip looks a bit taller than the predicted ~27 rows, but the photo is too blurry to measure that, and four i915 instances were running by then (see below). Everything above and below the strip is intact. The same panel showed a band whenever another i915 port (VedCloud's NMXGPU, also display-disabled) initialised GuC, which fits this explanation.
The -28 guard is right to fail here. The N-256 exception (surface at or above GUC_GGTT_TOP) doesn't cover a surface that starts below GUC_GGTT_TOP and ends above it.
Possible fixes (your call)
- Move the scanout before i915 initialises the GGTT. Copy the framebuffer's PTEs to a free range below
GUC_GGTT_TOP that the stolen/GGTT allocators keep clear, write PLANE_SURF_1_A to the new offset (it latches at vblank), then reserve that range. It's a single display register write, after which all of the framebuffer is outside the GuC region.
- Keep the uC firmware away from the panel. Make the
uc_fw slot avoid the live surface. This is probably harder, because the GuC can't address above GUC_GGTT_TOP.
Secondary: a failed stage 4 can be run again
My stage runner wrote debug.xegfx_stage again after the failure. That was my mistake: I read debug.xegfx_results without -b. Each write re-ran stage 4 and attached another i915 instance on top of the first:
xegfx: devclass vgapci: drmn drmn drmn drmn
xegfx: make_dev drm/0: error 17
WARNING !xa_empty(&drm_minors_xa) failed at .../drm/drm_drv.c:1122
It might be worth making the stage sysctl refuse to run a stage that already failed this boot, or at least refuse stage 4.
Observation, low confidence
Without xegfx.nvram_trace=0, the desktop hard-froze right after the login password on two boots:
- once with XeGfx in the AuxKC
- once with it prelinked through OpenCore
XeGfx was not loaded in either case: no xegfx lines in the log, and no panic. With xegfx.nvram_trace=0 (README advice), login was normal and stages 1–4 ran. That's one boot of each, and I can't explain how a boot-arg affects a kext that isn't loaded, so this may be a coincidence.
Reports (ioreg dicts, debug.xegfx_log after every stage) are available if useful.