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.
297 lines
13 KiB
C
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);
|