watchthelight labs

Intel built-in panel (NMIntelFB) on Arrow Lake-S 7D67: tracking #12

openarea:gpuarea:display
watchthelight 2026-10-21

Tracking issue for the Intel built-in panel framebuffer (NMIntelFB) on an Arrow Lake-S laptop, branch intel-xe. Each iteration gets a comment: what changed, what the boot showed, what is next.

Machine: Lenovo Legion 5 15IAX10, Core Ultra 9 275HX, iGPU 8086:7D67 (GMD_ID graphics 12.70, display 14.00), RTX 5070 Laptop. BIOS mux: dGPU mode puts the panel on NVIDIA (works, accelerated), Hybrid mode puts it on the iGPU. macOS Tahoe 26.7.1.

Goal: a desktop on the built-in panel in Hybrid mode, first unaccelerated on the firmware's mode, then composited by the NVIDIA accelerator (head 4 copy path), then Intel acceleration.

Measured so far (Linux probe, firmware state in Hybrid): plane A linear XRGB8888 2560x1600 at 165 Hz, PLANE_SURF 0xFF000000 (GGTT offset beyond the 256 MiB BAR2), GOP framebuffer 0x70800000 = DSMBASE (stolen memory), GSMBASE 0x70000000, PCode stolen-access bit (0x138914) = 1, PSR1 panel, backlight not on the PCH PWM.

Boots so far:

  • Apple's IONDRVFramebuffer on the iGPU: WindowServer builds a virtual framebuffer only (no GUI).
  • NMIntelFB 0.1/0.2 matched at boot: passes the register reads, the boot-console check and IOFramebuffer::start, then the machine hangs in the write-combined kernel map of the 0x70800000 framebuffer (NVRAM step breadcrumbs, two boots).
  • NMIntelFB 0.3 (no map in start()): first boot inconclusive.

Next: adversarial review of the whole display path (other components acting on the iGPU), then the next build.

watchthelight 2026-10-21

Workstreams are now sub-issues: probe data (#13, done), IONDRV no desktop (#14), NMIntelFB start hang (#15), NVIDIA compositing onto the Intel panel (#16), backlight (#17), Intel kernel driver and Metal (#18). Current step: an adversarial review of everything on the machine that can act on the iGPU or the panel (kexts, Lilu plugins, injected properties, daemons, OpenCore), plus a code review of the framebuffer, the accelerator hookup and the userland. Results and the next build will be posted here.

watchthelight 2026-10-21

Update 2026-10-20 ~21:00. Five parallel reviews finished (framebuffer, kernel port attach, Metal path, everything else on the machine that touches the iGPU, and an assumption breaker).

  • #14 has a source-level root cause: Apple's IONDRV only accepts a boot console that lies inside a PCI BAR; on Meteor/Arrow Lake the GOP framebuffer is in stolen memory. New kext IGPUConsoleRange (branch intel-xe, 4e5bedb) adds that range so Apple's own driver can use it. Next Hybrid boot tests it alone.
  • #15: the stage-8 hang is narrowed to IOFramebuffer's open/mapping window; NMIntelFB parked until #14's result.
  • #18: five userland/glue blockers listed for the Intel Metal path.
    Waiting on: one Hybrid boot.
watchthelight 2026-10-21

Red-team review of everything else on the machine (kexts, Lilu plugins, injected properties, daemons, OpenCore) finished:

  • WhateverGreen is an accomplice. It hooks IOFramebuffer::initFB and, unless boot-arg gfxrst=4 is set, copies the saved boot console into each new framebuffer on first open. NullMoth's own NVRMFB notes that this needs gfxrst=4 with WhateverGreen. Neither this machine nor the Tiger Lake test laptop had it. With the console in stolen memory that hook maps exactly the range the NMIntelFB freezes were in. Added gfxrst=4 to N-201.
  • In Hybrid mode the auxiliary kext collection loads ~5 s after userland, not ~60 s; every failed Hybrid boot died 4-15 s after NMIntelFB loaded.
  • Ruled out with the N-192 control boot (Hybrid, NVIDIA off, no NMIntelFB, alive): every background daemon, display audio (no Intel HDMI codec probed), AppleGraphicsDevicePolicy/mux kexts (not loaded), the injected iGPU properties (WhateverGreen's invalid-platform pattern), OpenCore settings.
    N-201 = IGPUConsoleRange + gfxrst=4 + NVIDIA off, Hybrid.
watchthelight 2026-10-21

N-201: first desktop on the Intel iGPU. Apple's own IONDRVFramebuffer now drives the built-in panel in Hybrid mode after IGPUConsoleRange (OpenCore-injected) adds the stolen-memory console range to the iGPU and gfxrst=4 keeps WhateverGreen's initFB hook out of the way. Unaccelerated (software compositor). #14 closed. Next: acceleration through the NVIDIA card rendering and NVAccel copying frames onto this framebuffer (#16), which needs NVAccel to work with a foreign framebuffer and no NVIDIA head.

watchthelight 2026-10-21

Next boot N-202 aims at an accelerated desktop on the Intel panel: SSDT powers the RTX 5070 on in Hybrid mode (macOS never ran the dGPU's ACPI power-on, so the GPU was absent from PCI), and a proxy head in NVRMFB lets the NVIDIA accelerator composite with Metal and copy each frame into the iGPU's framebuffer. Details on #16.

watchthelight 2026-10-21

Update 2026-10-20 ~21:35. N-202 booted to the software desktop: the dGPU was still absent, held off by an EC flag. N-203 attached it at run time and then cut power on all four boots, 40 to 80 s in, with no panic recorded. Lenovo's ACPI shows the EC write we suspected first is normal power-on behaviour, so the next boot (N-204) separates the attach from the NVIDIA driver start: attach on, NvGfx off. Details and the follow-ups for each outcome are on #16.

watchthelight 2026-10-21

N-204: power lost again with NvGfx off. The trigger is an iGPU reset (FLR) when IGPUConsoleRange hands the panel back after waiting 24 s for a dGPU that never appeared; power goes 7-8 s later. Safe config for the next boot; the fix plan is on #16.

watchthelight 2026-10-21

N-203 to N-205 (2026-10-20 21:12 to 21:47 EDT), Hybrid mode

  • N-203 / N-204: the laptop lost power 40-80 s into each boot. Cause found (I-093): IGPUConsoleRange 0.3 claimed the iGPU's framebuffer slot and, when no NVIDIA GPU had appeared after 24 s, terminated itself so IONDRV could take over. IOPCIFamily marks a nub whose client was terminated as needing a reset (IOPCIDevice::detachFromChild), and the next match runs an FLR on it (getResources, "Performing FLR on nub IGPU"). Resetting the iGPU while it scans out the panel cut power 7-8 s later. N-204 had -nvoff, so the NVIDIA driver is not involved.
  • N-205: safe config (no slot claim, SSDT off). IONDRV desktop, stable, no FLR.
  • The DGON attach result was lost: this kext's IOLog lines reach neither the unified log nor dmesg here.

N-206 (staged, NvGfx intel-xe eec12da): IGPUConsoleRange 0.4

  • The slot is decided once, in probe, and never handed back: with nvfbigpu=1 it attaches the dGPU from probe and claims only if the RTX 5070 is there.
  • sysctl debug.wtlacpi evaluates ACPI methods live (EC REJF, DGON, RP01 rescan), so the dGPU attach in Hybrid can be iterated on one boot instead of one reboot per try. The trace lives in debug.wtlacpi_log.
  • N-206 boots with IONDRV on the panel as in N-205; the attach is then tried live.
watchthelight 2026-10-21

N-206 (2026-10-20 21:59 EDT): live dGPU attach in Hybrid, result: power cut

IGPUConsoleRange 0.4 booted cleanly (IONDRV desktop, no FLR), and debug.wtlacpi read the Lenovo state live:

  • EC: IGPS 0, HPFF 0, CUMA 0, GSTA 0. The RTX 5070 is not detached in Hybrid, only powered off (PXP status 0). macOS never runs _PR0.
  • No GC6 (TDGC 0). Root port RP01 present (VDID ae4d8086), PXSX NVID 0xffff, RP01 LDIS 1.

DGON (Lenovo's PXP._ON power branch with a bounded wait) timed out: power was on (PSTA 1) but the link never trained (NVID still 0xffff). The laptop lost power about 12 s later, with no FLR this time. So I-093 is revised: the cut follows powering the dGPU while RP01's link stays down. N-203/N-204 fit the same timing. The FLR fix in 0.4 stays, but the FLR was probably not what cut power.

Live power-on experiments are paused: each one is a hard power loss. Next is a read-only study of Lenovo's Hybrid path (PXP._OFF is \STMC(4,0), so the real on/off goes through STMC), and of how Linux/Windows bring the link up, before any new attempt.

watchthelight 2026-10-21

N-207 (2026-10-20 22:08 EDT): DGON v3, power cut again. Hybrid dGPU attach parked.

  • RP01's Link Disable is set before any attach (LDIS 1 at 45 s; IOPCIFamily logged it clear at 1.8 s, so it gets set after boot).
  • DGON v3 cleared it after power-on and waited 1 s: the link still did not train (NVID 0xffff, LASX 0). Lenovo's POFF then did not drop power (PSTA stayed 1), and the laptop lost power seconds later.
  • Every live power-on of the RTX 5070 in Hybrid has now ended in a hard power cut: N-206, N-207, and the probe-time attach in N-203/N-204. That is too costly for the laptop and its disks, so the attach is stopped and SSDT-DGPU-ON is off again.
  • Consequence: the "NVIDIA renders, Intel panel scans out" proxy path needs the dGPU on PCI in Hybrid, so it is parked with the attach.
  • State now: Hybrid gives a stable IONDRV desktop on the iGPU (unaccelerated, N-201/N-205); Discrete gives the accelerated NVIDIA desktop. Accelerated Intel graphics needs a real Intel GPU driver (i915/xe port), which is the M2+ part of this plan.
watchthelight 2026-10-21

New direction (2026-10-20 22:20 EDT): a generic Intel Xe driver for macOS ("XeGfx")

Goal: any laptop with Intel Xe graphics (Tiger Lake to Arrow Lake first, Lunar Lake and later via xe in phase 2) boots an accelerated macOS desktop, Lenovo or not. The Legion-specific Hybrid dGPU route is closed (three power cuts, see above) and never generalised anyway.

Design:

  • Kernel: FreeBSD's drm-kmod (Linux i915 at the 6.14 level, MIT) on a LinuxKPI-for-XNU layer, GT only at first (display code disabled).
  • Userspace: Mesa ANV for Vulkan, plus a Metal-on-Vulkan layer for WindowServer.
  • Display without mode setting: the firmware lights the panel, and a flip is one PLANE_SURF write to a GGTT address. That works across display versions 12 to 14 without i915's 177k-line display code.

First measurements (sources: linux 9e73b18, freebsd d2145e9, drm-kmod c16de0f):

  • Compile harness: all 376 drm, dma-buf, ttm and i915 files (with the XNU Kconfig: no debugfs, procfs, fbdev, PXP) get through the front end for the macOS kernel target. That needed Mach-O linker sets, module metadata without ELF sections, and FreeBSD's generated interface headers. LinuxKPI's own sources: 30 of 38.
  • The objects need 458 functions nothing in them defines: macOS already exports 60, and 398 are the FreeBSD kernel API to write on XNU. By area: locks/sleepqueues, malloc/uma, taskqueue/callout/kthread, vm_page/pmap (GEM buffers and their user mappings, the hardest part), PCI resources/MSI (to IOPCIDevice), char devices/fds (for the DRM node), sysctl, firmware, and stubs for debugfs/vt.

Milestones: X1 compiles, X2 links, X3 loads GT-only on 7D67 next to IONDRV, X4 GuC + first request, X5 ANV offscreen, X6 accelerated desktop, X7 a non-Lenovo laptop.

watchthelight 2026-10-21

XeGfx now has its own repo: https://github.com/watchthelight-labs/XeGfx (BSD-2-Clause, public)

State at 2d060c5:

  • All 415 objects (drm, dma-buf, ttm, i915, LinuxKPI, our shim) build for the macOS kernel target.
  • Done on XNU: curthread shadows, per-CPU data, critical sections, clock/ticks/jiffies, malloc/contigmalloc/UMA, mtx/sx/rw/cv/sleepqueues, taskqueues, callouts.
  • Kext skeleton: idle unless xegfx=1, and it never claims the iGPU yet. Link script with generated stubs that panic with the name of whatever is still missing.

Now running: six parallel agents, one per area, on the remaining 305 functions:

  • pci: newbus, PCI and MSI on IOPCIDevice
  • dma: DMA, GTT, stolen memory, ACPI, backlight
  • vm: vm_page/pmap/cdev pager for GEM
  • fd: cdevsw bridge for the DRM node
  • sched: kthreads, RCU, timers, CPU features
  • misc: sysctl, firmware, the rest

Each one pushes to main as soon as it finishes, so progress shows up in the commit log as it lands. Next after that: a clean link, then the first load on the Legion in Hybrid with the GT side only.

watchthelight 2026-10-21

XeGfx X2 done: the whole stack links, using only public KPIs (XeGfx a36e0b4)

  • 423 objects: drm, dma-buf, ttm, i915, LinuxKPI, FreeBSD's eventhandler, and the shim. 4.0 MB, 0 stubs.
  • Every symbol resolves from a library the kext declares; nothing from kpi.private and nothing the kernel exports only to itself. tools/kpi_check.py enforces this on every link.
  • Consequences for the design: no direct map of RAM (PHYS_TO_DMAP is a lookup over the shim's IOKit mappings), critical sections and sched_pin run on public lck_spin locks, and there is no CPU binding (documented where it matters).

Next boot (N-208): the kext is in the auxiliary collection, idle; it does nothing at load and never claims the iGPU. After login, sysctl debug.xegfx_start=1 runs the shim and every SYSINIT (drm, ttm, i915 module init) without handing the Linux PCI driver a device yet. If that panics, the Mac reboots into the idle kext, with no config change.

watchthelight 2026-10-21

N-208 (XeGfx 0.0.1 idle in the aux collection): both Hybrid boots stopped within seconds of userland, with no boot dump, no flushed unified log and no panic. Likely cause: XeGfx's IOPCIDevice personality matches the live iGPU when the aux collection loads, and late matching on IGPU is what triggered the iGPU FLR and power loss in N-203/N-204. The iGPU is on PCI in Discrete mode too, so N-209 is a safe boot (-x skips the aux collection) to uninstall it. For the driver: start from IOResources and find IGPU by path instead of a PCI personality.

watchthelight 2026-10-21

N-209: safe boot (-x) cannot boot this machine (boot.efi kernel memory allocation fails, STOP 0x16), so XeGfx is fenced off another way for N-210: OpenCore gives the iGPU vendor-id 0x1234, XeGfx's vendor-0x8086 personality no longer matches it, and IGPUConsoleRange matches 0x1234 too. The Mac session uninstalls XeGfx on that boot. Lesson for XeGfx: no IOPCIDevice personality on a live iGPU.

watchthelight 2026-10-21

N-208 to N-210 (2026-10-21): the first XeGfx install stopped the Legion, now fixed in the design

  • N-208: with XeGfx in the auxiliary collection, two boots stopped seconds into userland, with no panic and no log flush (I-094). The kernel shim never ran: it only starts on a sysctl. The likely trigger is IOKit itself. XeGfx's PCI personality matched the live iGPU late, as a second match on that nub, and IOPCIFamily's getResources() then resets the nub's software state (resetNubState(), "effectively a no-op on the first match" in the source).
  • N-209: a safe-boot rescue is impossible on this machine (boot.efi can't allocate kernel memory with -x).
  • N-210: an OCEFI vendor-ID spoof fenced XeGfx off without anyone having to type a command. On that boot XeGfx was uninstalled, and the spoof was removed for N-211, which is back to the N-207 state.

XeGfx b53b58b no longer has a PCI personality at all. It rests on IOResources and will find the iGPU in the registry when its shim is started. Separately, it now links only against declared public KPI libraries: no kpi.private and no symbols the kernel doesn't offer to kexts (tools/kpi_check.py, run by tools/link.sh).

watchthelight 2026-10-21

N-212 (XeGfx with an IOResources personality only): failed the same way as N-208, dead within ~40 s with nothing flushed. N-210 passed with XeGfx in the aux collection only because nothing matched it, so the fault is in XeGfx's load path (_start, static initialisers, the resident start), not in iGPU matching. N-213: a codeless OpenCore-injected XeGfxBlock.kext takes XeGfx's match category at a higher score so it never loads; the Mac session then uninstalls it and tests XeGfx loads by hand outside the aux collection.

watchthelight 2026-10-21

Progress, 2026-10-21. The Intel work now has its own home and is open for collaborators: NvGfx branch xe (docs/XE.md) and XeGfx with issues for every open task.

Kernel driver (XeGfx 0.0.2, Linux i915 through FreeBSD drm-kmod and LinuxKPI on XNU)

  • The two boots that died when XeGfx loaded (N-208, N-212) had one cause: the FreeBSD sysctl code linked into the kext defined sysctl_register_oid, so _start registered an XNU sysctl through the FreeBSD copy and dereferenced NULL. 40 such names are now renamed, and the link fails if a kext symbol shadows a kernel export (XeGfx#1).
  • Nothing loads XeGfx at boot any more. It is armed with kmutil load and runs in stages by sysctl: shim, a read-only GPU probe, bus/DMA, then i915 with its display code off and a check that the panel still scans out. A stalled stage panics on purpose so it leaves a report.
  • No GPL-only code in the build: an overlay replaces drm-kmod's GPL-only dma-buf, sync_file and fence-unwrap files (from NullMoth's old AMD/Intel port, commit bf2dee3), and the build refuses any GPL-only source or header. 418 of 418 sources compile and link against public kernel interfaces only.
  • First hardware run on the Arrow Lake 7D67 is queued (XeGfx#2).

Userspace

  • Mesa ANV (bffd919) and libdrm build natively for macOS with the Darwin patches, LLVM only at build time. On Mesa's simulated driver, every Intel generation from Gen9 to Xe3 (TGL, RKL, ADL, RPL, MTL, ARL incl. 7D67, DG2, LNL, BMG, PTL) creates a Vulkan device and compiles a compute pipeline. That proves the compiler, not the hardware path.
  • The futex code needs macOS 14.4+ for now (XeGfx#10). ANV on i915 also needs sync_file emulated in a small userspace shim (XeGfx#6).

Research and credits. The README of each repo links every project this builds on or learned from, among them FreeBSD drm-kmod and LinuxKPI, Linux i915, Mesa, NullMoth's driver, b00t0x's WhateverXe, NootedGreen, JulioNectar's XPS 13 Plus work, IntelIGPU-Tahoe, VedCloud2's CoreUltraGraphics and LunarBridge.

Testers with any Intel Xe machine: the read-only probe is described in CONTRIBUTING.md (XeGfx#3).

watchthelight 2026-10-22

XeGfx 0.0.11 is built and staged for the next test boot. It hasn't run on hardware yet. (XeGfx 9a024bf, NvGfx xe 3c201e5)

One boot now covers the whole path from i915 bring-up to a userspace batch:

  • Stage 4 fails when i915 attached but a GT is wedged or the GuC isn't running. i915's probe reports success in both cases.
  • Stage 5 runs one kernel request on every engine and counts the interrupts that arrive.
  • Stage 6 has every engine store a known value into its status page through the GGTT, and the CPU reads it back.
  • GEM mapping without mmap(2). XNU won't let a kext implement mmap on a device, so userspace maps GEM objects through two ioctls on the DRM node. They use the same layout and numbers as NullMoth's Darwin libdrm patch, so that patch works unchanged. The pages stay pinned while the mapping lives.
  • xgprobe is the first userspace client. It reads the DRM version and params, queries engines, creates a context, then runs a softpinned store-dword batch on each legacy ring through those mappings. On Arrow Lake this is the only way to get data into a buffer, because i915 refuses pread, pwrite and relocations after Tiger Lake.
  • Fixes from other ports: the upstream signal_irq_work() use-after-free fix (VedCloud hit it on Meteor Lake), FreeBSD LinuxKPI's lost wake-up (also found by VedCloud), and a stolen-memory reservation for the firmware's console image (VedCloud and nmx-intel).

Next on the userspace side: sync_file and dma-buf fd emulation, without which ANV won't start (XeGfx #6).

watchthelight 2026-10-22

XeGfx 0.0.12 (def47c7, NvGfx xe 38d66a2) adds what Mesa ANV needs before it will start. None of it has run on hardware yet.

  • Fences without file descriptors. A kext can't create fds, so libdrm on Darwin now hands out stand-in dma-buf and sync_file descriptors (patches/libdrm-xegfx.patch). The kernel half is two node ioctls that move a buffer's implicit fences to and from a syncobj, with the same rules as Linux's dma-buf sync_file export and import.
  • DRM_IOCTL_GET_PCIINFO. libdrm uses it on the BSDs and Darwin to list devices. drm-kmod declares it but never implemented it.
  • Mesa (patches/mesa-xegfx.patch): ANV's ioctls and buffer mappings now go through libdrm. The drm-shim chip matrix still passes all 28 simulated chips.

The next boot (N-222, Hybrid mode, panel on the iGPU) runs stages 1 to 6, then xgprobe, drmlist and an ANV device plus compute pipeline on the real node.

watchthelight 2026-10-22

Two test boots of XeGfx on the Legion today, N-222 in Hybrid mode and N-221 in Discrete. Both stopped early in stage 4, while i915 was probing. N-221 left a panic report:

xegfx: vm: pmap_qenter could not map 1 pages, from i915_ggtt_probe_hw -> setup_scratch_page -> i915_gem_object_pin_map -> vmap.

So the problem is in our shim, not in the GPU or the display. kva_alloc reserved kernel VA as a pageable buffer, and on Tahoe those live in IOKit's pageable submaps. pmap_qenter then tried to lay physical pages over that reservation with kIOMapOverwrite, which fails. N-219 had stopped before i915 got this far, so this was the first time the code ran.

XeGfx 0.0.14 (59d790f, NvGfx xe 893eb90) changes LinuxKPI's vmap family to map pages at an address IOKit picks, the same way every BAR mapping that already works is made. Stage 1 now tests that path before anything touches the GPU, and fails cleanly instead of panicking. It hasn't run on hardware yet.

watchthelight 2026-10-22

XeGfx now runs Vulkan on the Legion's Arc iGPU (8086:7D67). On N-234 today, Mesa's ANV driver opened the device through the XeGfx render node, created a Vulkan device and built a compute pipeline:

device 0: Intel(R) Graphics (ARL) | vendor 0x8086 device 0x7d67 | API 1.4.363 | driver 0x6802063
  vkCreateDevice: OK (0)
  compute pipeline: OK (0)

That was the 12th boot since my last comment here (N-223 to N-234, all BIOS Hybrid, all run unattended by xe-guard). Each one hit a different problem:

  • N-223 to N-225: the HDCP and writeback helpers were stubs, and i915 still set up display outputs with disable_display. 0.0.15 adds a no-display switch and makes the build stop when a source fails to compile, so missing code can't be linked in as panicking stubs anymore.
  • N-225 to N-228: GT0 got wedged at init with Enabling uc failed (-5). The cause was drm-kmod: it never enables the GuC on FreeBSD builds (uc_expand_default_options is under #ifndef __FreeBSD__), and Meteor Lake and Arrow Lake can't submit work without it. XeGfx now sets enable_guc=3.
  • N-228: with the GuC selected, mtl_guc_70.bin still failed to load (-2). OSKextRequestResource doesn't serve a kext installed in the auxiliary collection, and LinuxKPI asks with NOWARN, which hid the error. 0.0.20 links the image into the kext binary.
  • N-229: GuC firmware version 70.53.0, submission, SLPC and RC enabled. Every engine (rcs0, bcs0, ccs0) completed a kernel request and wrote a dword the CPU read back. Then the first ioctl from a user process panicked on a NULL p_sysent in linux_file_ioctl. Fixed in 0.0.21.
  • N-230: a user process opened renderD128, created a context, mapped buffers and ran execbuffer on the render and copy engines, and the readback matched. libdrm still found no devices.
  • N-231 to N-234: the nodes are XNU devfs clone devices, so every stat and every open gets its own minor. libdrm on Darwin now finds a node by its slot (0.0.22 keeps the slot in each instance minor). In N-233 libdrm listed the device. In N-234 drmGetDevice2 worked on an open fd too, and that was the last thing ANV needed.

What doesn't exist yet: nothing is drawn, and WindowServer still composites on the NVIDIA GPU. Next is an offscreen draw through ANV, then the Metal-to-Vulkan path.

XeGfx 0.0.22 is dac3979. The libdrm changes are gpu/userland/patches/libdrm-xegfx.patch on NvGfx xe (b020742).

Test build: XeGfx v0.0.22 (prerelease).

watchthelight 2026-10-22

Metal now runs on the Legion's Intel GPU. In N-238 a normal user process got a Metal device on the 7D67 through the NvGfx Metal plugin on ANV, and both an MSL compute kernel and a render pipeline came back correct:

Metal devices: 1
  Intel(R) Graphics (ARL) (NVMTL over Intel(R) Graphics (ARL))
  compute pipeline: OK
metal compute: OK
  render pipeline: OK
  draw: status 4, centre 0 255 0 255, corner 255 0 0 255, 512 green pixels
metal draw: OK

How it is wired: XeGfx's new stage 7 sets nmxgpu-claimed on the iGPU and re-runs matching. XeGfxAccel, which is NMXAccel built over XeGfx (build/nvgfx/build_nvaccel.sh xegfxaccel on xe, commit e3fb8c2), attaches as the accelerator. Its personality also has an IOPropertyMatch on that property, so it never matches the live iGPU at boot. In Hybrid mode the RTX 5070 is detached, so this was the only Metal device on the machine.

Four boots of problems on the way (N-235 to N-238):

  • Shared buffers came back with zeroed 64-byte lines. A 4 MB Vulkan test (gpu/userland/tools/vkcoh.c) showed the cause sits below the plugin: ANV's coherent memory type is a write-combining BO with a non-snooping PAT, but XNU keeps ordinary RAM write-back whatever cache mode an IOKit mapping asks for. The CPU caches the BO, the GPU writes around the cache, and dirty lines land on top of the results. On Darwin, ANV now uses the snooping write-back PAT (MTL index 3) for host-visible BOs. After that, zero lines are wrong.
  • No Metal device for apps: renderD128 was 0660 root. Render nodes are 0666 now, as on Linux.
  • Kernel panic once apps used it (N-237, after about ten minutes of apps rendering on the Intel GPU): blocking while holding a spinlock. i915_active_add_request allocates GFP_ATOMIC under its tree lock, and the shim's malloc(M_NOWAIT) still went to IOMalloc. M_NOWAIT now takes blocks from a per-size reserve, which a thread call refills.
  • Mesa's anonymous files used /tmp/xdg-runtime-mesa-<uid>, which breaks when several users and set-uid processes share /tmp. They now go in the per-user temp dir.

Still open: every stat of a /dev/dri node holds a clone instance until devfs reclaims the vnode (662 after 25 minutes). 0.0.26 recycles stale ones and goes into the next boot. Then the desktop: WindowServer compositing through this device and the panel scanned out from it.

Test build: XeGfx v0.0.25. The userland patches are in gpu/userland/patches/ on xe (42c941d).

watchthelight 2026-10-23

IOSurface-backed Metal textures on the Intel GPU no longer go through a CPU copy every frame. This was the main cost when WindowServer composited on the 7D67 (our I-108: about 8 ms per surface per frame).

How: ANV's VK_EXT_external_memory_host now works. That needed i915 userptr, which needed XeGfx to pin pages a user process owns (0.0.32 to 0.0.34, vm_fault_quick_hold_pages through IOKit). On xe (4196c97, e1b2a94) the plugin now makes an IOSurface texture an explicit-layout linear image (DRM linear modifier, pitch = the surface's bytesPerRow) bound straight to the surface's imported pages. Surface-in does nothing, and surface-out only takes the write lock to move the seed.

mtlbench, 2560x1600 BGRA8, average ms, every pixel check right:

clear blit draw
copying path (installed plugin) 13.1 10.2 13.2
image in the surface's pages, release build 3.4 0.8 10.3
private texture, for reference 4.3 1.1 4.3

Two kernel panics on the way, both in the userptr path and both fixed: __get_user_pages_fast read the shim's empty vmspace (0.0.33), and freeing a pinned object hit the last generated stub, vm_page_tryxbusy (0.0.34). N-272 passed the full automated run on 0.0.34. One whole-machine lockup in N-271 has not come back since (I-111, still watched).

Next: WindowServer compositing on the Intel GPU with this plugin.

Test build: XeGfx v0.0.34.

watchthelight 2026-10-24

XeGfx 0.0.35 passed the full automated run (N-276), this time with the zero-copy plugin under the guard instead of by hand. 2560x1600 BGRA8, average ms over 30 runs, every pixel check right:

clear blit draw
installed plugin (copies) 11.6 8.2 7.6
image in the surface's own pages 3.9 0.9 5.3

IOSurface row pitches: a wide plane (RGBA16Float, 45056 bytes per row) takes the zero-copy image; an odd one (R8, 656 bytes, not a multiple of 32) falls back to a private image with copies. Both come back right row for row. Thanks to JulioNectar for pointing out that pitch trap.

0.0.35 also holds a reference on the process's mm in the userptr notifier, as Linux does, and takes four i915 GuC, heartbeat and GSC fixes from later Linux releases. The lockup we saw once in N-271 has not come back.

Next: WindowServer compositing on the Intel GPU with this plugin.

Test build: XeGfx v0.0.35.

watchthelight 2026-10-24

Progress, 2026-10-24.

WindowServer on the Intel GPU (N-277, N-278): it composites for about 20 s and frames reach the panel, then the plugin's Vulkan queue submit returns VK_ERROR_DEVICE_LOST and Core Animation aborts on the next pipeline build. Reproduced twice. i915 logs no GPU hang and no GuC or engine reset in that window, so the likely source is the execbuffer ioctl itself failing, which ANV reports as device lost and prints only on stderr.

XeGfx 0.0.36 writes every failing DRM ioctl (number, pid, errno) to its log ring. N-279 passes stages and userspace checks with it; the only failures in normal use are ANV's GETPARAM/SETPARAM probes. The next WindowServer run should show the errno behind the device lost.

Also seen once: an assertion in Core Animation's push_surface a few seconds after the plugin logged that it could not reflect two of Core Animation's shaders.

watchthelight 2026-10-24

Progress, 2026-10-24 (later).

N-280 ran WindowServer on the Intel GPU again. This time it aborted at 11 s in Core Animation's push_surface, without a device lost. Reading QuartzCore explains why. The assert is found != slist->end(). SkyLight was compositing the panel with its CPU compositor (CompositorSW) while Core Animation's flattening was active. Flattened surfaces come from a shared cache, and the software context never records them in its surface list, so the lookup fails every time. The NVIDIA plugin hit the same assert back when CoreDisplay saw vendor 1 on the boot framebuffer. So it is not in the plugin. The open question is why SkyLight picks CompositorSW for that display.

The "timed out fence" lines before the abort are Core Animation transaction fences (Mach ports with a 0.5 s deadline). WindowServer logs them without XeGfx too, so they are a symptom of slow frames, not a missing GPU signal.

Two causes of the slow frames are fixed but not yet run under WindowServer. Every other swap copied the scanout onto itself through a write-combined mapping, about 0.8 s each on WindowServer's main thread. And the DRM wait plumbing had three timing bugs: wrong ETIME number, a clock counted from kext load, and CLOCK_MONOTONIC on macOS being calendar time minus boottime. XeGfx 0.0.38 fixes the wait side and passes N-281 (stages and userspace checks, normal wait timings). The plugin now also sends Vulkan errors to the system log, so the next device lost should come with ANV's reason.

watchthelight 2026-10-27

Progress, 2026-10-27.

The Intel desktop now runs. WindowServer composites on the Arrow Lake iGPU through XeGfx (i915 under a LinuxKPI shim), XeGfxAccel and the NvGfx xe plugin, and frames reach the built-in panel through NMIntelFB.

What held it back since the last update was a whole-machine freeze, tracked as I-116. Somewhere between 15 seconds and 88 minutes into the Intel desktop, every core stopped, and macOS then panicked because 23 of 24 CPUs did not answer a rendezvous interrupt. GPU interrupts were handled right up to the end. Things that did not fix it: a lock-order fix on WindowServer's mmap path (a real bug, fixed in 0.0.42), an interrupts-off watchdog (never fired), and holding GT forcewake so the GPU never enters RC6 (N-295 froze at 16 minutes).

The main suspect now is our own debug aid. xegfx.nvram_trace=1 wrote a breadcrumb to NVRAM every 250 ms so that a hang would leave a trace. On this firmware those writes run in SMM, and a CPU in SMM does not answer the rendezvous interrupt. With the trace off (N-296), the Intel desktop stayed up for the whole one-hour test, including a Geekbench 6 CPU run (2447 single / 17755 multi). That is one clean run, so N-297 is running now: two hours with disk and GPU load in the background.

Also found on that desktop:

  • Symbol icons (System Settings sidebar, some Dock icons) drew as flat yellow shapes. Textures from automatic Metal heaps lost their channel swizzle, so gray + alpha layers read as red + green. Fixed in the plugin, and the same one-line fix went upstream as nullmoth/nvidia-macos-driver#420.
  • Clear squares follow the cursor and dragged windows over translucent windows. NMIntelFB has no hardware cursor, so WindowServer draws it in software. Still open.

XeGfx 0.0.45 is out as a prerelease: https://github.com/watchthelight-labs/XeGfx/releases/tag/v0.0.45

watchthelight 2026-10-27

Yellow symbol icons on the Intel desktop (Settings sidebar, some Dock icons) are fixed in the plugin, xe-2.6.2 d54b9d9.

The earlier heap swizzle fix was a real gap but not the cause. The buffer texture path (newTextureWithDescriptor:offset:bytesPerRow:) checked that the linear image accepts every memory type unless the buffer was imported. ANV's linear images take types 0-1 of 0-3, so on the iGPU every buffer texture came back nil. RenderBox draws the rasterised symbol through one, so only the bounding box was left, filled yellow by the glass shader. Buffers now record their memory type and the check tests that one type. NVIDIA was never affected, since NVK's linear images accept all types.

Checked with fresh 1024 px renders in iconservicesagent. The desktop check comes with the next WindowServer start.

watchthelight 2026-10-28

Progress since the yellow icon fix, 2026-10-27 to 10-28:

  • No more freezes under load. With the NVRAM trace off, the Intel desktop ran 2 hours (N-297), 4 hours (N-298) and 5.6 hours (N-299) while a script wrote 256 MB plus 200 small files and ran 50 Metal draws every 22 seconds, about 225 GB in total. Draw times stayed around 4.2 ms.
  • Restart panic fixed (XeGfx 0.0.46). Restarting at the end of N-299 panicked with "freeing wired page": a userptr hold could take the last wiring of a page that was already on its way to vm_page_free. The hold now wires only under the table lock and only while the count is above zero. The next restart under the same load was clean. Prerelease: https://github.com/watchthelight-labs/XeGfx/releases/tag/v0.0.46
  • Shared VRAM (XeGfxIntelFB 0.6). System Information showed 15 MB, which was the firmware framebuffer. The framebuffer kext now publishes VRAM,totalMB with the size ANV uses for its heap (3/4 of RAM), so a 32 GB machine shows "VRAM (Dynamic, Max): 24576 MB".
  • Panel identity (XeGfxIntelFB 0.7). The kext reads the panel's EDID over eDP AUX A and measures the refresh rate from the pipe's frame counter. On Intel the panel is no longer "Unknown Display": it has its own name and 165 Hz, and macOS applies the same display override as on the NVIDIA GPU, including the HiDPI 1280x800 mode. Scaled sizes above that need the pipe scaler, which is next.
  • Name (XeGfxIntelFB 0.8, built). About This Mac and Metal will call the 7D67 "Intel Arrow Lake Xe-LPG".

Code: NvGfx branch xe-2.6.2 (b11f1ac, a119b95, 5a0fc33).

watchthelight 2026-10-28

Progress since this morning, 2026-10-28:

  • macOS 26.7.2 (25G332). The Legion moved to 26.7.2 today. XeGfx 0.0.46, XeGfxIntelFB 0.8 and the xe-2.6.2 plugin carried over unchanged: N-305 passed stages 1-7 and WindowServer stayed up on the Intel GPU for the 300 s check.
  • Cursor square (I-119), mostly gone. WindowServer asks for linear views of its cursor textures with 7 mip levels, which ANV refuses. Plugin 0922117 gives each one a single-level stand-in that is refilled from the caller's bytes at every submission. On the 26.7.2 desktop the clear square is down to a faint discoloured edge and a very faint flicker while the cursor moves. Next: give the stand-in its mip levels too.
  • Clear horizontal lines in Terminal, still there. WindowServer asks for 2472x1376, 2472x60 and 88x60 views of buffers that are 1 to 2.5 KB shorter than the rows need (9920 x 1376 = 13,649,920 bytes on a 13,647,872-byte buffer). The plugin refuses them, so those strips draw nothing. Working on a fix that builds the view without reading past the buffer.
watchthelight 2026-10-30

Update, 2026-10-30: the horizontal bands are fixed (I-119).

  • Cause. An app's render pass on ANV could still be running when WindowServer composited that app's IOSurface, so a half-drawn frame reached the screen as full-width bands while typing or scrolling. Making WindowServer wait for its own GPU work (N-318) did nothing. Making the apps wait for their render passes removed the bands (N-319, switched on and off live in the same window).
  • Fix. XeGfx plugin 0c1eb02: on Intel, every process except WindowServer waits at commit for command buffers that render or write an IOSurface. On N-320 (macOS 26.7.2, Arrow Lake 8086:7D67) there were no bands with it on, and they came back within seconds when it was switched off. mtlbench under load stays at 4.6 to 5.0 ms per frame.
  • Moved. The Intel plugin, translator, accelerator and kexts now live in XeGfx as one self-contained driver. The xe branches here stop at 2026-10-30.

Next: the clear box around the cursor.

Reply on GitHub