An EL2 monitor/hypervisor and toolchain that boots macOS 27 on the ASUS A16 (Snapdragon X2 Elite), with an Adreno Metal plug-in and IOGPU kext, and air2ir3, an AIR to Mesa ir3 shader compiler. MIT, with GPL-2.0 components listed in LICENSE and THIRD_PARTY_NOTICES.md. No Apple, Qualcomm, ASUS or Microsoft binaries are included.
5.9 KiB
A virtual Apple AGX GPU (historical, superseded)
Superseded. This was an early design for graphics: present the stock
AGXG17P driver with a virtual Apple GPU and do the work on the Adreno
behind it. It never ran on the Snapdragon. The project instead uses its own
native Adreno stack: the AdrenoX2 IOGPU kext with AdrenoDisplay, the
AdrenoMetal plug-in, and the air2ir3 shader compiler (see
ARCHITECTURE.md and
air2ir3). The analysis is kept because it explains
why that route was taken, and because the AGX device-tree and UAT findings
remain accurate for this kernelcache.
Target was j700ap / T8140 / G17P, kernelcache.release.mac17p (26A428).
What the kernelcache contains
Of 362 kexts, three matter for rendering: AGXG17P (the driver, about 920 KB
of code), IOGPUFamily (the generic layer Metal talks to) and
AGXFirmwareKextG17PRTBuddy (a 12 KB firmware shim). The rest are display.
When sizing fileset members, use __TEXT_EXEC: each member's __LINKEDIT
load command describes the shared 25 MB __LINKEDIT.
The shim's requestFirmwareBinary returns a pointer and a length, and logs
"GFX firmware was not loaded by iBoot" when it is absent: the driver expects
the boot loader to place firmware in memory. Other strings pin the
architecture: submission is a ring in shared memory with host and firmware
indices, liveness is polled through isGFXBooted, and context IDs are the
driver side of the UAT's SET_CTX_ID.
AGXG17P reads /arm-io/gfx-asc and about 150 gpu-* power and performance
properties, none of which exist in the shipped DeviceTree.j700ap; iBoot
fills them in.
What the device tree describes
| Node | Compatible | Notes |
|---|---|---|
arm-io/sgx |
gpu,t8140 |
64 MiB GPU block; 8 interrupts |
arm-io/gfx-asc |
iop,ascwrap-v6 |
GFX coprocessor, inside sgx |
arm-io/gfx1-asc |
iop,ascwrap-v6 |
second die's coprocessor |
gfx-asc/iop-gfx-nub, gfx1-asc/iop-gfx1-nub |
iop-nub,rtbuddy-v2 |
RTKit nubs |
arm-io/mapper-gfx-asc |
iommu-mapper,gfx |
There are two GPU dies, each with its own coprocessor and firmware. Every
other window lies inside sgx reg[0], so decode order matters: matching
sgx first swallows both mailboxes. The sgx node also carries policy, for
example issue-gmmu-tlbis-at-retype, which ties the GPU MMU to SPTM's frame
retype path.
The current Adreno route withdraws the sgx node and reuses its interrupt
line for the Adreno (see compat/adreno.h).
The UAT interface
XNU reaches the GPU MMU through SPTM domain 0, table 7: 14 endpoints on a
uniform 0x40-byte stub stride. Endpoints 0-12 are INIT_STATE,
DESTROY_STATE, MAP_TABLE, UNMAP_TABLE, MAP_BEGIN/_CONTINUE,
PREP_FW_UNMAP_BEGIN/_CONTINUE, UNMAP_BEGIN/_CONTINUE, SET_CTX_ID,
REMOVE_CTX_ID and GET_INFO. Endpoint 13 has a stub in macOS 27 and no
name in any available source.
The first argument points into XNU's own memory (one state object per GPU
address space) and the monitor writes roots and context ID back into it. The
GPU tables are 16 KiB granule, three levels of 11 index bits. Unlike the CPU
side there is nothing to shadow: the CPU's MMU never walks them. The current
monitor keeps a read-only UAT adapter for the private address-space mode
(compat/uat.c).
Two behaviours held by tests in the prototype:
REMOVE_CTX_IDclears only the valid bit, andSET_CTX_IDrequires an invalid slot, not a zero one; otherwise ordinary context reuse fails much later.- An unmodelled endpoint returns an error and is counted, never a silent success.
The firmware
The restore image ships one GPU firmware payload per die, each a flat
rkosftab table of named sub-images for the GFX coprocessor. Availability was
never the blocker: these are ARM64 images that drive AGX shader hardware this
machine does not have.
The decision
Three options were weighed:
- (a) Run Apple's firmware and model what it touches. This means reimplementing undocumented AGX silicon well enough for code with no error tolerance, and the result would still issue AGX command streams to nothing. It also means executing Apple's firmware.
- (b) Model just enough to attach quietly. A useful first milestone (RTKit handshake, plausible rings) and a machine with no GPU at the end.
- (c) Implement the firmware's protocol. Answer the mailbox, RTKit and
shared-memory ring contracts and translate at the draw/dispatch level. The
firmware ABI is partly documented by Asahi Linux's
drm/asahidriver, but that is the host side; the peer state machine would have to be inverted, and the ABI is locked to one firmware version.
The decisive problem was shader code. Metal compiles to AGX ISA in user space, and the Adreno cannot run it. Every AGX route ends in AGX-to-Adreno shader translation behind an opaque protocol. The native route instead replaces the driver and the Metal plug-in, and compiles Apple's AIR intermediate representation for the Adreno with Mesa's ir3 backend. That is the route the project took.
Files (prototype, retained for reference)
| File | What |
|---|---|
gpu/agx-inventory.py |
evidence tool: kexts, strings, device tree, UAT stubs, firmware |
gpu/agx.h |
register file, UAT geometry and state |
gpu/agx-uat.c |
GPU page tables and the table-7 endpoints |
gpu/agx-asc.c |
ASC mailbox, RTKit handshake, MMIO decode |
gpu/test-agx.c |
native checks, plus the list of unmodelled behaviour |
The UAT descriptor encodings, context-table layout and RTKit/ASC constants in
gpu/agx*.{c,h} come from m1n1 (Asahi Linux, MIT licence). Agreement with
that reference is not hardware evidence; if the reverse engineering were
wrong, the model would reproduce the error and its tests would still pass.
python3 gpu/agx-inventory.py --kexts --dt --uat
cc -std=gnu11 -O1 -g -Wall -Wextra -fsanitize=address,undefined -Igpu \
gpu/test-agx.c gpu/agx-uat.c gpu/agx-asc.c -o build/test-agx && build/test-agx