Files
Acelogic 1510d8c6ce Snapintosh public release
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.
2026-10-21 21:26:36 -04:00

297 lines
13 KiB
C

/* SPDX-License-Identifier: MIT */
/* Portions (constants and expressions marked [M1N1]) from m1n1, https://github.com/AsahiLinux/m1n1:
* Copyright The Asahi Linux Contributors, MIT. */
/* A virtual Apple AGX GPU, for the stock AGX kexts to bind to.
*
* Two separate things live behind that name, and they are separate here:
*
* 1. The GMMU/UAT -- the GPU's page tables. XNU never writes them. It asks
* the monitor to, through SPTM domain 0, table 7, and the kernelcache has
* 14 stubs for it. Those calls arrive at EL2 whether or not anything
* resembling a GPU exists, which is why this half has the near deadline.
* See agx-uat.c.
*
* 2. The GFX coprocessor -- an ASC (`iop,ascwrap-v6`) running Apple's own
* firmware, reached through a mailbox and then RTKit. Command submission
* is a ring in shared memory that the firmware reads; the kext does not
* poke a command register. See agx-asc.c.
*
* Everything here is addressed by physical address and reaches memory through
* callbacks, so the whole model runs natively under sanitizers in test-agx.c
* with no hardware and no EL2 -- the same arrangement shadow/mmu.h uses.
*
* WHAT IS EVIDENCE HERE. Every address and bit in this file came from one of:
* [ADT] DeviceTree.j700ap, printed by gpu/agx-inventory.py --dt
* [KC] kernelcache.release.mac17p, printed by gpu/agx-inventory.py
* [M1N1] m1n1 (AsahiLinux) and the author's J700 research fork (asc.c, rtkit.c, sptm/uat.c), which was
* reverse-engineered against this same SoC
* Anything with no tag is a guess and must not be here. agx_unmodelled() lists
* the things known to exist that are deliberately absent.
*/
#pragma once
#include <stdint.h>
/* ------------------------------------------------------------------ memory */
/* Read/write 8 bytes of guest physical memory. Non-zero means the access could
* not be performed, which is not the same as reading zero and must never be
* collapsed into it: a UAT endpoint that cannot read a table has to refuse,
* not install a mapping over whatever was in the register. */
typedef int (*agx_read64_fn)(void *ctx, uint64_t pa, uint64_t *out);
typedef int (*agx_write64_fn)(void *ctx, uint64_t pa, uint64_t value);
struct agx_mem {
agx_read64_fn read64;
agx_write64_fn write64;
void *ctx;
};
/* ------------------------------------------------------- MMIO windows [ADT] */
/* /device-tree/arm-io/sgx, compatible "gpu,t8140". Two reg entries, and the
* second lies inside the first -- Apple names a sub-window separately rather
* than describing a second block. Both are reproduced; nothing is merged. */
#define AGX_SGX_BASE 0x270000000ULL
#define AGX_SGX_SIZE 0x4000000ULL
#define AGX_SGX_SUB_BASE 0x270d00000ULL
#define AGX_SGX_SUB_SIZE 0x12c000ULL
/* /device-tree/arm-io/gfx-asc and gfx1-asc, compatible "iop,ascwrap-v6".
* Two dies, one ASC each, each with its own firmware image. */
#define AGX_ASC0_BASE 0x272600000ULL
#define AGX_ASC1_BASE 0x272e00000ULL
#define AGX_ASC_SIZE 0x88000ULL
#define AGX_ASC0_AUX_BASE 0x272050000ULL /* reg[1], 8 bytes, purpose unknown */
#define AGX_ASC1_AUX_BASE 0x272850000ULL
#define AGX_ASC_AUX_SIZE 8ULL
/* The mailbox block sits at +0x8000 inside the ASC window. [M1N1 asc.c:53] */
#define AGX_ASC_MBOX_OFFSET 0x8000ULL
/* The two offsets /device-tree/arm-io/sgx names in `meta-sw-interrupt`:
* 0x1014048 0x3 0x1014248 0x3 0x4000
* Both land inside sgx reg[0]. The pairs are read as (offset, value) and the
* trailing 0x4000 as a stride, but THAT READING IS NOT ESTABLISHED -- see
* agx_unmodelled(). The offsets themselves are quoted, not inferred. */
#define AGX_SGX_META_SW_IRQ0 0x1014048ULL
#define AGX_SGX_META_SW_IRQ1 0x1014248ULL
/* ------------------------------------------------- ASC mailbox registers [M1N1] */
#define AGX_ASC_CPU_CONTROL 0x44
#define AGX_ASC_CPU_CONTROL_START 0x10
#define AGX_ASC_MBOX_CTRL_FULL (1U << 16)
#define AGX_ASC_MBOX_CTRL_EMPTY (1U << 17)
#define AGX_ASC_MBOX_A2I_CONTROL 0x110 /* AP -> IOP */
#define AGX_ASC_MBOX_A2I_SEND0 0x800
#define AGX_ASC_MBOX_A2I_SEND1 0x808
#define AGX_ASC_MBOX_A2I_RECV0 0x810
#define AGX_ASC_MBOX_A2I_RECV1 0x818
#define AGX_ASC_MBOX_I2A_CONTROL 0x114 /* IOP -> AP */
#define AGX_ASC_MBOX_I2A_SEND0 0x820
#define AGX_ASC_MBOX_I2A_SEND1 0x828
#define AGX_ASC_MBOX_I2A_RECV0 0x830
#define AGX_ASC_MBOX_I2A_RECV1 0x838
/* An ASC message is 96 bits: 64 of payload and a 32-bit tag whose low 8 bits
* are the RTKit endpoint. [M1N1 asc.h, rtkit.c] */
struct agx_msg {
uint64_t msg0;
uint32_t msg1;
};
#define AGX_MBOX_DEPTH 16
struct agx_fifo {
struct agx_msg slot[AGX_MBOX_DEPTH];
uint32_t head, tail, count;
uint32_t overflow; /* messages dropped because the FIFO was full */
};
/* ------------------------------------------------------- RTKit [M1N1 rtkit.c] */
#define AGX_RTKIT_EP_MGMT 0
#define AGX_RTKIT_EP_CRASHLOG 1
#define AGX_RTKIT_EP_SYSLOG 2
#define AGX_RTKIT_EP_DEBUG 3
#define AGX_RTKIT_EP_IOREPORT 4
#define AGX_RTKIT_EP_OSLOG 8
#define AGX_MGMT_MSG_HELLO 1
#define AGX_MGMT_MSG_HELLO_ACK 2
#define AGX_MGMT_MSG_START_EP 5
#define AGX_MGMT_MSG_IOP_PWR_STATE 6
#define AGX_MGMT_MSG_IOP_PWR_ACK 7
#define AGX_MGMT_MSG_EPMAP 8
#define AGX_MGMT_MSG_AP_PWR_STATE 0xb
#define AGX_RTKIT_MIN_VERSION 11
#define AGX_RTKIT_MAX_VERSION 12
/* How far the coprocessor has got. The stock kext calls AGXFirmware::
* isGFXBooted() [KC], so this is the state that question reads. */
enum agx_asc_state {
AGX_ASC_OFF = 0, /* CPU_CONTROL.START clear */
AGX_ASC_BOOTING, /* started, HELLO sent, waiting for HELLO_ACK */
AGX_ASC_EPMAP, /* version agreed, publishing the endpoint map */
AGX_ASC_RUNNING, /* endpoints started; ready for traffic */
};
struct agx_asc {
uint64_t base; /* window base PA (AGX_ASC0_BASE or ASC1) */
uint32_t cpu_control;
enum agx_asc_state state;
struct agx_fifo a2i; /* what the AP wrote, waiting for us */
struct agx_fifo i2a; /* what we produced, waiting for the AP */
uint32_t ep_started; /* bitmap of endpoints the AP has started */
uint32_t version; /* agreed RTKit version, 0 until HELLO_ACK */
uint32_t unknown_writes; /* writes this model decoded nowhere */
};
/* --------------------------------------------------------------- UAT [M1N1] */
/* The GPU page tables are 16K-granule, three levels, 11 index bits each:
* L1 = va[46:36] L2 = va[35:25] L3 = va[24:14] [M1N1 uat.c:604-617]
* Same granule XNU uses for the CPU, which the hypervisor already cannot walk
* natively -- but the GPU side is not walked by this machine's MMU at all, so
* unlike shadow/mmu.c there is nothing to re-translate. These tables are data. */
#define AGX_UAT_PAGE_SHIFT 14
#define AGX_UAT_PAGE_SIZE (1ULL << AGX_UAT_PAGE_SHIFT)
#define AGX_UAT_INDEX_BITS 11
#define AGX_UAT_L1_SHIFT 36
#define AGX_UAT_L2_SHIFT 25
#define AGX_UAT_L3_SHIFT 14
#define AGX_UAT_PTE_ADDR_MASK 0x0000fffffffff000ULL
#define AGX_UAT_VALID_LEAF_BITS 0x0080000000000400ULL
#define AGX_UAT_TTBR_ADDR_MASK 0x000003ffffffc000ULL
/* The GMMU context table: 64 slots of {TTBR0, TTBR1}, 16 bytes each, which
* SET_CTX_ID writes and REMOVE_CTX_ID invalidates. [M1N1 uat.c:1378-1449] */
#define AGX_UAT_MAX_CTX 64
#define AGX_UAT_CTX_STRIDE 16
#define AGX_UAT_CTX_NONE 0xffffU
/* Byte offsets inside the state object XNU passes as regs[0]. The handle is a
* pointer to XNU's own memory and the monitor writes back into it, so these
* are part of the ABI, not bookkeeping. [M1N1 uat.c:15-27] */
#define AGX_UAT_STATE_ROOT0 0x08
#define AGX_UAT_STATE_ROOT1 0x10
#define AGX_UAT_STATE_CTX_ID 0x18
#define AGX_UAT_STATE_OPTIONS 0x40
#define AGX_UAT_STATE_FLUSH_LEVEL 0x44
/* The 14 endpoints of SPTM domain 0, table 7, as the kernelcache's own stubs
* enumerate them. 0..12 carry the names m1n1-j700/src/sptm/private.h gives
* them; 13 has a stub in mac17p and no name anywhere, and is not guessed at. */
#define AGX_UAT_FN_INIT_STATE 0
#define AGX_UAT_FN_DESTROY_STATE 1
#define AGX_UAT_FN_MAP_TABLE 2
#define AGX_UAT_FN_UNMAP_TABLE 3
#define AGX_UAT_FN_MAP_BEGIN 4
#define AGX_UAT_FN_MAP_CONTINUE 5
#define AGX_UAT_FN_PREP_FW_UNMAP_BEGIN 6
#define AGX_UAT_FN_PREP_FW_UNMAP_CONTINUE 7
#define AGX_UAT_FN_UNMAP_BEGIN 8
#define AGX_UAT_FN_UNMAP_CONTINUE 9
#define AGX_UAT_FN_SET_CTX_ID 10
#define AGX_UAT_FN_REMOVE_CTX_ID 11
#define AGX_UAT_FN_GET_INFO 12
#define AGX_UAT_FN_UNNAMED_13 13
#define AGX_UAT_ENDPOINTS 14
#define AGX_UAT_MAX_STATES 32
struct agx_uat_state {
uint64_t handle; /* XNU's state object PA; 0 means the slot is free */
uint64_t root0_pa;
uint64_t root1_pa;
uint64_t options;
uint16_t ctx_id; /* AGX_UAT_CTX_NONE until SET_CTX_ID */
uint8_t type;
uint8_t used;
};
struct agx_uat {
struct agx_mem mem;
uint64_t ctx_table_pa; /* the 64-slot GMMU context table */
struct agx_uat_state slot[AGX_UAT_MAX_STATES];
uint64_t calls[AGX_UAT_ENDPOINTS]; /* per-endpoint counters */
uint64_t refused; /* calls rejected on validation */
uint64_t unhandled; /* calls to endpoints not modelled */
uint32_t tlbi_asid; /* ASID-scoped invalidations requested */
uint32_t tlbi_all; /* global invalidations requested */
};
/* ------------------------------------------------------------ the whole device */
struct agx_device {
struct agx_asc asc[2];
struct agx_uat uat;
uint64_t mmio_reads, mmio_writes;
uint64_t mmio_undecoded; /* accesses in range that decode nowhere */
};
void agx_init(struct agx_device *dev, const struct agx_mem *mem);
/* MMIO. `size` is 4 or 8; anything else is refused rather than widened.
* Returns 0 if the access was decoded, -1 if the address is ours but no
* register lives there, and -2 if the address is not ours at all. A read that
* returns -1 still writes 0 to *out, because that is what an unclaimed Apple
* MMIO window does, but it is counted so the count can be looked at. */
int agx_mmio_read(struct agx_device *dev, uint64_t pa, unsigned size, uint64_t *out);
int agx_mmio_write(struct agx_device *dev, uint64_t pa, unsigned size, uint64_t value);
/* --------------------------------------------------------------- ASC / RTKit */
void agx_asc_init(struct agx_asc *asc, uint64_t base);
/* Run the coprocessor: drain whatever the AP put in the A2I mailbox and
* produce the replies RTKit requires. Returns how many messages it consumed.
* Nothing here is timed; the model advances only when this is called. */
int agx_asc_step(struct agx_asc *asc);
int agx_asc_booted(const struct agx_asc *asc);
/* FIFO primitives, exposed because the tests drive them directly. */
int agx_fifo_push(struct agx_fifo *f, const struct agx_msg *m);
int agx_fifo_pop(struct agx_fifo *f, struct agx_msg *m);
/* ----------------------------------------------------------------- UAT calls */
void agx_uat_init(struct agx_uat *uat, const struct agx_mem *mem);
/* Index into the table at `level` (1, 2 or 3) for `va`. */
uint32_t agx_uat_index(uint32_t level, uint64_t va);
/* The leaf descriptor SPTM installs for `pa` under `options`, for a state of
* `type`. Bit-for-bit the expression in m1n1-j700's uat_leaf_pte(). */
uint64_t agx_uat_leaf_pte(uint8_t type, uint64_t pa, uint64_t options);
/* The TTBR value SET_CTX_ID writes into the context table. */
uint64_t agx_uat_ttbr(uint64_t root_pa, uint32_t ctx_id);
/* Walk `va` through the 16K tables rooted at `root_pa`, in software.
* Returns 0 and sets *pte_out to the leaf (0 if unmapped), or -1 if a table
* could not be read -- which is not the same as unmapped. */
int agx_uat_walk(struct agx_uat *uat, uint64_t root_pa, uint64_t va, uint64_t *pte_out);
/* Dispatch one SPTM domain-0 table-7 call. `regs` is x0..x4 as the stub
* marshalled them; regs[0] is the handle for every endpoint except INIT_STATE.
* Returns 0 if the endpoint is modelled (with *rc_out set to what XNU is told),
* and -1 if it is not -- the caller must then decide, loudly, what to do. */
int agx_uat_call(struct agx_uat *uat, uint32_t endpoint, uint64_t *regs, uint64_t *rc_out);
const char *agx_uat_endpoint_name(uint32_t endpoint);
/* ------------------------------------------------------------------ honesty */
/* Behaviours known to exist and deliberately not modelled, in the shape
* emuasi/device uses: a NULL-terminated list every test run prints, so a green
* result reads as "no known regressions", never as "matches hardware".
* Removing an entry needs evidence, not an argument. */
const char *const *agx_unmodelled(void);