From 62762840672d83783e63d591db660b6d01589abb Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:46:09 -0400 Subject: [PATCH 1/4] compat: add the head rule for page-unaligned client buffers IOGPU sizes a client buffer's memory as round_page(page offset + length) (IOGPUSysMemory::withClientBuffer). A buffer at a page-unaligned address gets a map one page past what the tail rule admits, so it is refused -83. In b12v23 and org2 one such WindowServer buffer was a 16 KiB descriptor under a 32 KiB map. It was refused at the onset of the Displays-pane glitch. Every composite that listed it then failed its preparation (-115), until the run ended. The rules, header-only beside the tail rule: - admission: the map is exactly the pages that [head, head + bytes) touches; - segments: the first run starts at its page, and the last one runs to its page end; - cache: only the descriptor's lines, with partial first and last lines cleaned and invalidated; - ranges: a client offset lands at head + offset, inside the descriptor's bytes. With head 0 every function gives the tail rule's answer. tools/test-adreno-residency.c models the descriptor's head. - Scenario 33 is org2's buffer at seven heads. With re-pin only it is refused cleanly (E_PIN) every round. With the head rule it is admitted, the GPU reads each client byte where the CPU wrote it, and it survives unwire, eviction and a replaced descriptor. - Scenario 34 checks what is still refused: a map a page too long or too short, and boot-arg adreno_head_offset=0. It also checks head-0 equivalence with the tail rule over every byte count up to three pages, and misplaced segments. All parts of the model pass in four builds: base, re-pin, re-pin plus head rule, and that with the arena and VA reclaim. --- compat/adreno-resource-tail.h | 119 ++++++++++++++++++- tools/test-adreno-residency.c | 217 ++++++++++++++++++++++++++++++++-- 2 files changed, 327 insertions(+), 9 deletions(-) diff --git a/compat/adreno-resource-tail.h b/compat/adreno-resource-tail.h index 1b6ff51..8d89928 100644 --- a/compat/adreno-resource-tail.h +++ b/compat/adreno-resource-tail.h @@ -20,7 +20,10 @@ * holds the whole page, and native DMA maps whole pages too; * cache nothing past the line that holds the descriptor's last byte; that line, when partial, is cleaned and * invalidated (TO_DEVICE, dc civac) instead of invalidated (FROM_DEVICE, dc ivac), which would discard - * CPU data the client keeps beside the buffer in the same line. */ + * CPU data the client keeps beside the buffer in the same line. + * + * The head rule (-DADRENO_RESIDENT_HEAD_OFFSET=1 with ADRENO_RESIDENT_REPIN, below): the same arithmetic for a + * descriptor that starts `head` bytes into its first page, which the tail rule leaves refused. */ #pragma once #include #include "adreno-resource.h" @@ -91,3 +94,117 @@ static inline int adreno_tail_cache(const struct adreno_resource *r, uint64_t by } return 0; } + +/* ---- the head rule: a descriptor that starts inside its first page (-DADRENO_RESIDENT_HEAD_OFFSET=1) ---- + * + * Why: withClientBuffer sizes the memory as round_page(page offset + length) (above), so a client buffer at a + * page-unaligned address gets a map that reaches one page past round_page(length). The tail rule refuses it: the + * shortfall is more than a page, and the first segment does not start a page. OBSERVED twice on the A16 (b12v23 and + * org2, 26A428), at the onset of the glitch the owner ties to opening System Settings > Displays: a WindowServer + * buffer with a 16 KiB descriptor under a 32 KiB map is refused -83 (kIOReturnBadArgument), and every later + * WindowServer submission that lists it fails its preparation (-115, about one per frame to the end of both runs), + * so stale framebuffers alternate on the panel (89% of scanout rows rewritten per present instead of 2-5%). + * + * Map offset m holds descriptor byte m - head. The map covers the whole pages the descriptor touches: the descriptor + * is [head, head + bytes) of the map; the bytes before head and after head + bytes are the same task's bytes in the + * same wired pages, reachable by the GPU exactly as native DMA reaches them (a DART maps whole pages too). + * admission head + bytes fits the map, and the map is exactly the pages that range touches; + * segments the first run is moved back to its page's start; a run that reaches the descriptor's end goes on to + * that page's end; a segment that does not sit where the head says is refused (0); + * cache only the lines the descriptor touches; a partial first or last line is cleaned and invalidated; + * ranges a client's buffer offset (IB relocation, FILL32) is inside the descriptor's bytes and lands at + * head + offset of the map. + * head 0 is the tail rule: the functions below give exactly the tail rule's answers (tools/test-adreno-residency.c). */ +static inline int adreno_head_admit(uint64_t head, uint64_t bytes, uint64_t logical) +{ + if (head >= ADRENO_TAIL_PAGE || !bytes || bytes > UINT64_MAX - 2 * ADRENO_TAIL_PAGE) return 0; + if (!head) return adreno_tail_admit(bytes, logical); + return !(logical & (ADRENO_TAIL_PAGE - 1)) && head + bytes <= logical && + logical <= adreno_tail_round_page(head + bytes); +} +/* Before the wire, when the head is not known yet: 1 when some head could make `bytes` fit `logical`. */ +static inline int adreno_head_fit(uint64_t bytes, uint64_t logical) +{ + return bytes && bytes < logical && !(logical & (ADRENO_TAIL_PAGE - 1)) && + bytes <= UINT64_MAX - 2 * ADRENO_TAIL_PAGE && logical <= adreno_tail_round_page(bytes) + ADRENO_TAIL_PAGE; +} +/* The descriptor offset the physical callback asks for at page-aligned map offset `offset`. */ +static inline uint64_t adreno_head_descriptor_offset(uint64_t offset, uint64_t head) +{ + return offset ? offset - head : 0; +} +/* The run the core's physical callback reports at page-aligned map offset `offset`, from the descriptor's own segment + * (pa, span) at adreno_head_descriptor_offset(offset, head): its physical start and, in *length, its bytes. 0 refuses. */ +static inline uint64_t adreno_head_run(uint64_t offset, uint64_t head, uint64_t bytes, uint64_t pa, uint64_t span, + uint64_t *length) +{ + const uint64_t at = adreno_head_descriptor_offset(offset, head), lead = offset ? 0 : head; + *length = 0; + if (!pa || !span || head >= ADRENO_TAIL_PAGE || (offset & (ADRENO_TAIL_PAGE - 1)) || (offset && offset < head) || + at >= bytes || span > bytes - at || pa < lead || ((pa - lead) & (ADRENO_TAIL_PAGE - 1))) + return 0; + *length = span == bytes - at ? adreno_tail_round_page(head + bytes) - offset : span + lead; + return pa - lead; +} +/* Cache maintenance of [offset, offset + len) of the map (64-byte aligned) for a descriptor of `bytes` at `head`: at + * most three ranges, in order. 0 when nothing of the descriptor is inside. */ +static inline unsigned adreno_head_cache_plan(uint64_t offset, uint64_t len, uint64_t head, uint64_t bytes, + unsigned direction, struct adreno_tail_range out[3]) +{ + const uint64_t first = head & ~(ADRENO_TAIL_LINE - 1); /* the line of the first byte */ + const uint64_t lead = (head + ADRENO_TAIL_LINE - 1) & ~(ADRENO_TAIL_LINE - 1); /* the first whole line */ + const uint64_t whole = (head + bytes) & ~(ADRENO_TAIL_LINE - 1); /* the end of the whole lines */ + const uint64_t touched = (head + bytes + ADRENO_TAIL_LINE - 1) & ~(ADRENO_TAIL_LINE - 1); + const uint64_t start = offset > first ? offset : first; + const uint64_t stop = len > touched || offset > touched - len ? touched : offset + len; + unsigned n = 0; + if (start >= stop) return 0; + if (direction != ADRENO_CACHE_FROM_DEVICE || (lead == first && whole == touched)) { + out[n++] = (struct adreno_tail_range){start, stop - start, direction}; + return n; + } + if (lead >= whole) { /* no line is wholly the descriptor's */ + out[n++] = (struct adreno_tail_range){start, stop - start, ADRENO_CACHE_TO_DEVICE}; + return n; + } + if (start < lead) out[n++] = (struct adreno_tail_range){start, (stop < lead ? stop : lead) - start, ADRENO_CACHE_TO_DEVICE}; + const uint64_t a = start > lead ? start : lead, b = stop < whole ? stop : whole; + if (a < b) out[n++] = (struct adreno_tail_range){a, b - a, direction}; + const uint64_t c = start > whole ? start : whole; + if (c < stop) out[n++] = (struct adreno_tail_range){c, stop - c, ADRENO_CACHE_TO_DEVICE}; + return n; +} +/* adreno_tail_cache with the head: validation first, then the plan in chunks of at most ADRENO_CACHE_MAX_BYTES. */ +static inline int adreno_head_cache(const struct adreno_resource *r, uint64_t head, uint64_t bytes, unsigned direction, + uint64_t offset, uint64_t len, + int (*maintain)(void *, uint64_t, uint64_t, unsigned), void *context) +{ + int e = adreno_resource_cache(r, 0, 0, 0); + if (e) return e; + if (direction > ADRENO_CACHE_FROM_DEVICE || (direction && !maintain)) return ADRENO_RESOURCE_E_STATE; + if (!len || offset >= r->logical_length || len > r->logical_length - offset) return ADRENO_RESOURCE_E_RANGE; + if (head >= ADRENO_TAIL_PAGE || !bytes || bytes > r->logical_length || head > r->logical_length - bytes) + return ADRENO_RESOURCE_E_RANGE; + if (!direction) return 0; + const uint64_t start = offset & ~(ADRENO_TAIL_LINE - 1); + const uint64_t end = (offset + len + ADRENO_TAIL_LINE - 1) & ~(ADRENO_TAIL_LINE - 1); /* <= logical_length */ + struct adreno_tail_range plan[3]; + const unsigned n = adreno_head_cache_plan(start, end - start, head, bytes, direction, plan); + for (unsigned i = 0; i < n; i++) + for (uint64_t at = plan[i].offset; at < plan[i].offset + plan[i].bytes;) { + uint64_t chunk = plan[i].offset + plan[i].bytes - at; + if (chunk > ADRENO_CACHE_MAX_BYTES) chunk = ADRENO_CACHE_MAX_BYTES; + if (maintain(context, r->address + at, chunk, plan[i].direction)) return ADRENO_RESOURCE_E_CACHE; + at += chunk; + } + return 0; +} +/* A client's range [offset, offset + len) of a head-offset buffer: 1 and its map offset when it lies inside the + * descriptor's bytes; 0 otherwise (the caller refuses the IB or FILL32 that names it). */ +static inline int adreno_head_range(uint64_t head, uint64_t bytes, uint64_t offset, uint64_t len, uint64_t *map_offset) +{ + *map_offset = 0; + if (!len || head >= ADRENO_TAIL_PAGE || offset > bytes || len > bytes - offset) return 0; + *map_offset = head + offset; + return 1; +} diff --git a/tools/test-adreno-residency.c b/tools/test-adreno-residency.c index 561d970..b481cf7 100644 --- a/tools/test-adreno-residency.c +++ b/tools/test-adreno-residency.c @@ -69,6 +69,9 @@ #if ADRENO_RESIDENT_REPIN #include "adreno-resource-tail.h" #endif +#if ADRENO_RESIDENT_HEAD_OFFSET && !ADRENO_RESIDENT_REPIN +#error "ADRENO_RESIDENT_HEAD_OFFSET needs ADRENO_RESIDENT_REPIN, as in the kext" +#endif #if !defined(RESOURCE_BASE) && !ADRENO_RESOURCE_POOL #error "build with -DADRENO_RESOURCE_POOL=1 (both translation units), or -DRESOURCE_BASE against the base core" #endif @@ -99,6 +102,7 @@ struct desc { int live; unsigned refs, npages, dir, ro_other, wires_ro, wires_rw, our_pins, native; uint64_t logical; + uint64_t head; /* display-modeset: the first byte's offset in pages[0] (a page-unaligned client) */ uint32_t *pages; }; struct mem { int live, desc, wired, fb; unsigned prep, idle, nmaps; int maps[3]; @@ -110,6 +114,7 @@ struct map { int native; /* map+0x10 bit 0: CB0 range held */ int refuse_pin, refusal; /* resident-evict: this map's pins fail; the kext's fRefusal */ uint64_t tail; /* resident-evict: the kext's fBackingBytes */ + uint64_t head; /* display-modeset: the kext's fBackingHead */ uint64_t native_addr, native_len; }; struct counts { @@ -144,6 +149,8 @@ static struct { /* faults: insert number (1-based) and how many of its pages install first; one-shot others */ unsigned fault_insert_at, fault_insert_partial, fault_clear, fault_unpin, fault_free, fault_pin; unsigned window_pages; /* the IODART allocator's pages from the window base (0: all of it) */ + int head_off; /* display-modeset: boot-arg adreno_head_offset=0 */ + uint64_t head_admitted, head_refused; #if ADRENO_VA_RECLAIM struct adreno_va_filler filler; int use_filler, pool_locked; @@ -193,19 +200,37 @@ static void free_pages(struct desc *d) { for (unsigned i = 0; i < d->npages; i++) { CHECK(M.page_owner[d->pages[i]] == d - M.descs); M.page_owner[d->pages[i]] = -1; } } -static int new_desc(uint64_t logical, unsigned dir) +/* A descriptor of `logical` bytes whose first byte is `head` bytes into its first page (0: page-aligned). */ +static int new_desc_at(uint64_t logical, unsigned dir, uint64_t head) { - CHECK(M.ndescs < MAX_DESCS); + CHECK(M.ndescs < MAX_DESCS && head < PAGE); int id = M.ndescs++; struct desc *d = &M.descs[id]; memset(d, 0, sizeof(*d)); - d->live = 1; d->logical = logical; d->dir = dir; - d->npages = (unsigned)((logical + PAGE - 1) / PAGE); + d->live = 1; d->logical = logical; d->dir = dir; d->head = head; + d->npages = (unsigned)((head + logical + PAGE - 1) / PAGE); d->pages = calloc(d->npages, sizeof(*d->pages)); CHECK(d->pages); alloc_pages(d, id); return id; } +static int new_desc(uint64_t logical, unsigned dir) { return new_desc_at(logical, dir, 0); } +#if ADRENO_RESIDENT_HEAD_OFFSET +/* IOMemoryDescriptor::getPhysicalSegment(at): the physical address of descriptor byte `at` and the bytes physically + * contiguous from it, up to the descriptor's end. */ +static uint64_t segment(const struct desc *d, uint64_t at, uint64_t *length) +{ + CHECK(at < d->logical); + const uint64_t p = d->head + at; + unsigned i = (unsigned)(p / PAGE), run = 1; + CHECK(i < d->npages); + while (i + run < d->npages && d->pages[i + run] == d->pages[i] + run) run++; + uint64_t bytes = (uint64_t)(i + run) * PAGE - p; + if (bytes > d->logical - at) bytes = d->logical - at; + *length = bytes; + return pa_of(d->pages[i]) + p % PAGE; +} +#endif static int wired(const struct desc *d) { return d->our_pins || d->native || d->wires_ro || d->wires_rw || d->ro_other; } static void put_desc(int id) { @@ -370,7 +395,12 @@ static int cb_pin(void *ctx) if (id < 0) return -1; struct desc *d = &M.descs[id]; if (m->refuse_pin) return -1; -#if ADRENO_RESIDENT_REPIN +#if ADRENO_RESIDENT_HEAD_OFFSET + /* the kext's pinBacking: the head is read from the wired descriptor's first segment */ + const int tail = d->logical < m->r.logical_length && adreno_tail_admit(d->logical, m->r.logical_length); + const int headed = d->logical < m->r.logical_length && !M.head_off && adreno_head_fit(d->logical, m->r.logical_length); + if (d->logical < m->r.logical_length && !tail && !headed) return -1; +#elif ADRENO_RESIDENT_REPIN const int tail = d->logical < m->r.logical_length && adreno_tail_admit(d->logical, m->r.logical_length); if (d->logical < m->r.logical_length && !tail) return -1; #else @@ -382,9 +412,21 @@ static int cb_pin(void *ctx) dir = DIR_OUT; if (prepare(d, dir)) return -1; } +#if ADRENO_RESIDENT_HEAD_OFFSET + uint64_t head = 0; + if (headed) { + uint64_t span = 0; + head = segment(d, 0, &span) & (PAGE - 1); + if (!adreno_head_admit(head, d->logical, m->r.logical_length)) { complete(d, dir); M.head_refused++; return -1; } + if (head) M.head_admitted++; + } +#endif d->refs++; d->our_pins++; m->backing = id; m->backing_dir = dir; -#if ADRENO_RESIDENT_REPIN +#if ADRENO_RESIDENT_HEAD_OFFSET + m->tail = tail || headed ? d->logical : 0; + m->head = head; +#elif ADRENO_RESIDENT_REPIN m->tail = tail ? d->logical : 0; #endif M.c.pins++; @@ -402,7 +444,7 @@ static int cb_unpin(void *ctx) complete(d, m->backing_dir); CHECK(d->our_pins); d->our_pins--; int id = m->backing; - m->backing = -1; m->tail = 0; + m->backing = -1; m->tail = 0; m->head = 0; put_desc(id); M.c.unpins++; return 0; @@ -413,6 +455,13 @@ static uint64_t cb_physical(void *ctx, uint64_t offset, uint64_t *length) CHECK(m->backing >= 0); /* only while prepared */ struct desc *d = &M.descs[m->backing]; CHECK(!(offset % PAGE) && offset < (uint64_t)d->npages * PAGE); +#if ADRENO_RESIDENT_HEAD_OFFSET + if (m->head) { /* the kext's physicalPage, head rule */ + uint64_t span = 0; + const uint64_t pa = segment(d, adreno_head_descriptor_offset(offset, m->head), &span); + return adreno_head_run(offset, m->head, m->tail, pa, span, length); + } +#endif unsigned i = (unsigned)(offset / PAGE), run = 1; while (i + run < d->npages && d->pages[i + run] == d->pages[i] + run) run++; uint64_t bytes = (uint64_t)run * PAGE; @@ -865,7 +914,7 @@ static void replace_backing(struct mem *me) { if (!me->live || me->wired || me->prep) return; const struct desc *old = &M.descs[me->desc]; - int fresh = new_desc(old->logical, old->dir); + int fresh = new_desc_at(old->logical, old->dir, old->head); /* the client's address keeps its page offset */ M.descs[fresh].refs = 1; int previous = me->desc; me->desc = fresh; @@ -1841,6 +1890,20 @@ static int tail_mem(uint64_t bytes) new_map(id); return id; } +/* display-modeset: a client buffer of `bytes` at a page-unaligned address: its first byte `head` bytes into its first + * page, IOGPU's memory (and map) length round_page(page offset + length). */ +static int head_mem(uint64_t bytes, uint64_t head) +{ + CHECK(M.nmems < MAX_MEMS); + int id = M.nmems++; + struct mem *me = &M.mems[id]; + memset(me, 0, sizeof(*me)); + me->live = 1; me->maplen = (head + bytes + PAGE - 1) & ~(PAGE - 1); + me->desc = new_desc_at(bytes, DIR_INOUT, head); + M.descs[me->desc].refs = 1; + new_map(id); + return id; +} #if ADRENO_RESIDENT_REPIN struct cache_call { uint64_t va, bytes; unsigned direction; }; static struct cache_call calls[8]; @@ -1946,6 +2009,144 @@ static void repin_scenarios(void) #endif check_all(1); free_mem(s); check_all(1); } + /* 33. org2 and b12v23: WindowServer's 16 KiB buffer at a page-unaligned address under IOGPU's 32 KiB map. Without the + * head rule every wire reports it committed (clean refusal, E_PIN) and every submission that lists it is refused, + * round after round, as the -115 lines of both runs; with it the pin is admitted, the map's two pages are the + * descriptor's two pages, every client byte the GPU reaches through the head-moved offset is the CPU's byte, the + * cache passes touch only the descriptor's lines, and it survives unwire, eviction and a replaced descriptor. */ + { static const uint64_t heads[] = {0x10, 0x40, 0x200, 0x1000, 0x2a0c, 0x3fc0, 0x3ff0}; + const uint64_t bytes = 0x4000; + for (unsigned h = 0; h < sizeof heads / sizeof heads[0]; h++) { + const uint64_t head = heads[h]; + reset(2, RUNS, 512ULL << 20, 33 + h); + int t = head_mem(bytes, head); + struct mem *me = &M.mems[t]; + CHECK(map_of(t)->r.logical_length == 2 * PAGE && M.descs[me->desc].npages == 2); + int list[1] = {t}, writes[1] = {1}; + for (int round = 0; round < 3; round++) { +#if ADRENO_RESIDENT_HEAD_OFFSET + CHECK(submit(list, writes, 1)); + const struct map *m = map_of(t); + CHECK(admitted(m) && m->head == head && m->tail == bytes && m->r.mapped_length == 2 * PAGE); + me->prep++; check_gpu_view(m, 1); + for (uint64_t k = 0; k < bytes; k += (k < 256 || k > bytes - 256) ? 1 : 61) { + uint64_t at = 0; + CHECK(adreno_head_range(m->head, m->tail, k, 1, &at) && at == head + k); /* resolveRange */ + const uint64_t va = m->r.address + at, leaf = M.leaves[leaf_index(va & ~(PAGE - 1))]; + const struct desc *d = &M.descs[me->desc]; + CHECK((leaf & 1) && (leaf & ~(NO_WRITE | 1ULL)) + va % PAGE == + pa_of(d->pages[(head + k) / PAGE]) + (head + k) % PAGE); /* GPU byte == CPU byte */ + } + uint64_t at = 0; + CHECK(adreno_head_range(head, bytes, bytes - 4, 4, &at) && at == head + bytes - 4); + CHECK(!adreno_head_range(head, bytes, bytes, 4, &at) && !adreno_head_range(head, bytes, bytes - 4, 8, &at) && + !adreno_head_range(head, bytes, 0, bytes + 1, &at) && !adreno_head_range(head, bytes, 0, 0, &at)); + for (unsigned dir = ADRENO_CACHE_TO_DEVICE; dir <= ADRENO_CACHE_FROM_DEVICE; dir++) { + ncalls = 0; + CHECK(!adreno_head_cache(&m->r, m->head, m->tail, dir, 0, m->r.logical_length, record_cache, NULL)); + const uint64_t first = m->r.address + (head & ~63ULL), end = m->r.address + ((head + bytes + 63) & ~63ULL); + CHECK(ncalls && calls[0].va == first && calls[ncalls - 1].va + calls[ncalls - 1].bytes == end); + for (unsigned i = 1; i < ncalls; i++) CHECK(calls[i].va == calls[i - 1].va + calls[i - 1].bytes); + for (unsigned i = 0; i < ncalls; i++) { + const uint64_t a = calls[i].va - m->r.address, b = a + calls[i].bytes; + /* invalidate only lines wholly the descriptor's; partial lines are cleaned and invalidated */ + if (calls[i].direction == ADRENO_CACHE_FROM_DEVICE) CHECK(a >= head && b <= head + bytes); + else CHECK(calls[i].direction == ADRENO_CACHE_TO_DEVICE); + } + if (dir == ADRENO_CACHE_FROM_DEVICE) + CHECK(ncalls == 1u + !!(head & 63) + !!((head + bytes) & 63) && + calls[!!(head & 63)].direction == ADRENO_CACHE_FROM_DEVICE); + else CHECK(ncalls == 1 && calls[0].direction == ADRENO_CACHE_TO_DEVICE); + } + me->prep--; + check_all(1); + if (round == 0) native_unwire(me); /* resident */ + if (round == 1) { native_unwire(me); replace_backing(me); } /* stale: evicted, re-pinned */ + if (round == 2) { M.res.budget = 0; native_unwire(me); } /* released: re-pinned at free */ +#elif ADRENO_RESIDENT_REPIN + const uint64_t pins = M.c.pins; + CHECK(!submit(list, writes, 1) && me->wired); /* the memory wires; the map stays unadmitted */ + CHECK(!admitted(map_of(t)) && map_of(t)->refusal == ADRENO_RESOURCE_E_PIN && M.c.pins == pins); + check_all(1); + native_unwire(me); +#else + CHECK(!submit(list, writes, 1) && !me->wired && !admitted(map_of(t))); + check_all(1); +#endif + } +#if ADRENO_RESIDENT_HEAD_OFFSET + if (!(M.res.evicted == 1 && M.head_admitted == 2 && !M.head_refused)) + fatal(__LINE__, "head %llx: evicted %llu admitted %llu refused %llu", (unsigned long long)head, + (unsigned long long)M.res.evicted, (unsigned long long)M.head_admitted, (unsigned long long)M.head_refused); +#endif + free_mem(t); check_all(1); + } + } +#if ADRENO_RESIDENT_HEAD_OFFSET + /* 34. What the head rule still refuses, and its head-0 equivalence to the tail rule. A map a page longer than the + * pages the descriptor touches, a map too short for head + bytes, and boot-arg adreno_head_offset=0 are refused + * cleanly (nothing pinned, wired once and completed); every submission of them stays refused. */ + reset(2, RUNS, 512ULL << 20, 40); + { const uint64_t bytes = 0x4000, head = 0x1000; + int longer = head_mem(bytes, head), shorter = head_mem(bytes + 0x3000, 0x1100); + M.mems[longer].maplen = 0; /* the model's map length is fixed at creation: */ + CHECK(map_of(longer)->r.logical_length == 2 * PAGE); + struct map *lm = map_of(longer); + /* stretch the refused map by one page the way a wrong IOGPU length would: refuse, never map past the pages */ + lm->r.logical_length = 3 * PAGE; + int a[1] = {longer}, w[1] = {0}; + CHECK(!submit(a, w, 1) && !admitted(lm) && lm->refusal == ADRENO_RESOURCE_E_PIN && lm->backing < 0); + lm->r.logical_length = 2 * PAGE; /* restored: admitted */ + CHECK(submit(a, w, 1) && admitted(lm) && lm->head == head); + struct map *sm = map_of(shorter); + sm->r.logical_length = 2 * PAGE; /* 0x1100 + 0x7000 needs 3 pages */ + int b[1] = {shorter}; + CHECK(!submit(b, w, 1) && !admitted(sm) && sm->backing < 0); + sm->r.logical_length = 3 * PAGE; + CHECK(submit(b, w, 1) && admitted(sm)); + check_all(1); + native_unwire(&M.mems[longer]); native_unwire(&M.mems[shorter]); + free_mem(longer); free_mem(shorter); check_all(1); + M.head_off = 1; /* boot-arg adreno_head_offset=0 */ + int off = head_mem(bytes, head), c[1] = {off}; + CHECK(!submit(c, w, 1) && !admitted(map_of(off)) && map_of(off)->refusal == ADRENO_RESOURCE_E_PIN); + native_unwire(&M.mems[off]); free_mem(off); check_all(1); + M.head_off = 0; + CHECK(M.head_refused == 2); } + /* head 0: admission, runs and cache plans are the tail rule's, over every byte count of a few pages. */ + for (uint64_t bytes = 1; bytes <= 3 * PAGE; bytes += bytes < 300 ? 1 : 251) + for (uint64_t logical = PAGE; logical <= 5 * PAGE; logical += PAGE) { + CHECK(adreno_head_admit(0, bytes, logical) == adreno_tail_admit(bytes, logical)); + if (!adreno_tail_admit(bytes, logical)) continue; + for (uint64_t off = 0; off < logical; off += PAGE) { + if (off >= bytes) continue; + const uint64_t span = bytes - off < 2 * PAGE ? bytes - off : 2 * PAGE; + uint64_t len = 0; + CHECK(adreno_head_run(off, 0, bytes, PA_BASE + off, span, &len) == PA_BASE + off && + len == adreno_tail_span(off, span, bytes)); + } + for (unsigned dir = 0; dir <= ADRENO_CACHE_FROM_DEVICE; dir++) + for (uint64_t o = 0; o < logical; o += 64 * 37) + for (uint64_t l = 64; o + l <= logical; l += 64 * 53) { + struct adreno_tail_range t[2], h[3]; + const unsigned nt = adreno_tail_cache_plan(o, l, bytes, dir, t); + const unsigned nh = adreno_head_cache_plan(o, l, 0, bytes, dir, h); + CHECK(nt == nh); + for (unsigned i = 0; i < nt; i++) + CHECK(t[i].offset == h[i].offset && t[i].bytes == h[i].bytes && t[i].direction == h[i].direction); + } + } + /* A segment that does not sit where the head says is refused page by page (the core then unwinds). */ + { uint64_t len = 1; + CHECK(!adreno_head_run(0, 0x200, 0x4000, PA_BASE + 0x300, 0x3d00, &len) && !len); /* not the head */ + CHECK(!adreno_head_run(PAGE, 0x200, 0x4000, PA_BASE + 0x10, 0x210, &len) && !len); /* unaligned page */ + CHECK(adreno_head_run(0, 0x200, 0x4000, PA_BASE + 0x200, 0x3e00, &len) == PA_BASE && len == PAGE); + CHECK(adreno_head_run(PAGE, 0x200, 0x4000, PA_BASE + 7 * PAGE, 0x200, &len) == PA_BASE + 7 * PAGE && + len == PAGE); /* the last page, whole */ + CHECK(!adreno_head_admit(0x200, 0x4000, 3 * PAGE) && !adreno_head_admit(0x200, 0x4000, PAGE) && + adreno_head_admit(0x200, 0x4000, 2 * PAGE) && !adreno_head_admit(PAGE, 0x4000, 2 * PAGE)); } + printf("PASS head\n"); +#endif printf("PASS repin\n"); } #endif From 19c38ed6413b871b4766917a47845f6bf22a3d92 Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:46:09 -0400 Subject: [PATCH 2/4] tools: let check-iomfb-vtable build AdrenoDisplay with defines --display-define NAME[=VALUE] (repeatable) adds a -D to the AdrenoDisplay.cpp compile only. The checker can then verify a profile's display object, including option-only overrides such as the IOMFB call trace, slot by slot against the kernelcache. Without the option it builds exactly as before. --- tools/check-iomfb-vtable.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/check-iomfb-vtable.py b/tools/check-iomfb-vtable.py index 96e0018..16f0e6a 100755 --- a/tools/check-iomfb-vtable.py +++ b/tools/check-iomfb-vtable.py @@ -21,7 +21,7 @@ (kernel, IOMobileGraphicsFamily, IOAVFamily, IOSurface, IOGPUFamily). Writes /iomfb-vtable-check.json (the full per-slot table) and exits 1 on any failure. -Usage: tools/check-iomfb-vtable.py [--out DIR] [--kc PATH] [--kdk PATH] [--no-display] +Usage: tools/check-iomfb-vtable.py [--out DIR] [--kc PATH] [--kdk PATH] [--no-display] [--display-define NAME=V ...] """ import argparse import hashlib @@ -83,7 +83,7 @@ def sh(cmd, **kw): return r.stdout -def compile_bundle(out, sources): +def compile_bundle(out, sources, extra=None): sdk = sh(['xcrun', '--show-sdk-path']).strip() kh = f'{sdk}/System/Library/Frameworks/Kernel.framework/Headers' common = ['-target', 'arm64e-apple-macos27.0', '-isysroot', sdk, '-mkernel', '-nostdinc', @@ -95,6 +95,7 @@ def compile_bundle(out, sources): for src in sources: obj = out / (Path(src).stem + '.o') lang = cxx if str(src).endswith('.cpp') else common + lang = lang + list((extra or {}).get(str(src), ())) sh(['xcrun', 'clang++' if str(src).endswith('.cpp') else 'clang'] + lang + ['-c', str(src), '-o', str(obj)]) objs.append(str(obj)) binary = out / 'IOMFBCheck' @@ -198,6 +199,9 @@ def main(): ap.add_argument('--kdk', type=Path, default=gen.DEFAULT_KDK) ap.add_argument('--kc', type=Path, default=gen.DEFAULT_KC) ap.add_argument('--no-display', action='store_true', help='check only the stub subclass') + ap.add_argument('--display-define', action='append', default=[], metavar='NAME[=VALUE]', + help='a -D for AdrenoDisplay.cpp only, repeatable (e.g. a profile\'s AdrenoDisplay set, or ' + 'ADRENO_DISPLAY_TRACE_MODES=1, whose overrides add slots to check)') args = ap.parse_args() out = args.out out.mkdir(parents=True, exist_ok=True) @@ -217,7 +221,8 @@ def main(): if not args.no_display: sources.insert(1, KEXT / 'src/AdrenoDisplay.cpp') classes.append('AdrenoDisplay') - binary_path = compile_bundle(out, sources) + binary_path = compile_bundle(out, sources, {str(KEXT / 'src/AdrenoDisplay.cpp'): + ['-D' + d for d in args.display_define]}) binary = gen.MachOKext(binary_path) results = {} From 40a150a538b10ad7a84debd167617566c2743326 Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:46:09 -0400 Subject: [PATCH 3/4] repo: move SnapAdreno to the head rule and IOMFB trace SnapAdreno feature/display-modes (c1b4ad5): - moves the re-pin refusal report into an ordered part; - adds ADRENO_RESIDENT_HEAD_OFFSET: page-unaligned client buffers are mapped in whole pages, using compat/adreno-resource-tail.h; - adds ADRENO_DISPLAY_TRACE_MODES: the IOMFB call and swap geometry trace; - builds both in the desktop profiles. With the defines off, the default build and both profiles are section-identical to SnapAdreno 79de60e. --- gpu/adreno-kext/kext | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpu/adreno-kext/kext b/gpu/adreno-kext/kext index 79de60e..c1b4ad5 160000 --- a/gpu/adreno-kext/kext +++ b/gpu/adreno-kext/kext @@ -1 +1 @@ -Subproject commit 79de60eaf13249b9e60fc232fd77f8f3bd46f41e +Subproject commit c1b4ad57a9e241745c875d36564466f65a206195 From 981b1d399e283309d932c991e91922b70ee65302 Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:46:09 -0400 Subject: [PATCH 4/4] tests: cover the head rule glue and the IOMFB trace - test-adreno-residency.py HeadOffset: - scenario 33 is built with re-pin only (org2's refusal reproduced) and with the head rule (admitted and mapped); - nine weakened head rules each fail; - the kext glue calls the rule, under its define, after the wire; - the desktop profiles build the define in every unit. - test-adreno-display-present.c geometry runs adreno_display_swap_geometry over the panel's mode, macOS's scaled resolutions of a 2880x1800 panel, placement, cropping, negative rectangles and an unknown surface. - test-adreno-display-trace.py: - four weakened geometry rules each fail; - every traced IOMFB method forwards its arguments unchanged and returns the family's result; - every trace identifier is behind its define; - check-iomfb-vtable.py with the desktop profile's display defines keeps all 483 slots. That check is SKIP without the KDK and the J700 kernelcache. --- tools/test-adreno-display-present.c | 42 ++++++- tools/test-adreno-display-trace.py | 180 ++++++++++++++++++++++++++++ tools/test-adreno-residency.py | 109 +++++++++++++++++ 3 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 tools/test-adreno-display-trace.py diff --git a/tools/test-adreno-display-present.c b/tools/test-adreno-display-present.c index 3d842d4..a79835e 100644 --- a/tools/test-adreno-display-present.c +++ b/tools/test-adreno-display-present.c @@ -36,7 +36,14 @@ * min(255, s + round(u * (255 - a) / 255)) per byte). The PRECHANGE build models the * 7ec040b4 presentGated, which drops layers 1..3; its cursor run must fail. * + * `geometry` runs adreno_display_swap_geometry (AdrenoDisplayCopy.h): what a layer-0 swap asks that the 1:1, top-left + * copy does not do, as the kext's -DADRENO_DISPLAY_TRACE_MODES logs it (agents/display-modeset). The cases are the + * panel's own mode, macOS's scaled resolutions of a 2880x1800 panel (1280x800 to 1920x1200 points at 2x: the + * framebuffer is the points' size twice, presented to the full panel), a placed destination, a cropped source, + * negative rectangles and an unknown surface. + * * test-adreno-display-present selftest + * test-adreno-display-present geometry (prints a JSON line) * test-adreno-display-present cursor (prints a JSON line) * test-adreno-display-present surface DUMP.adsf PANEL.bin (prints a JSON line) */ @@ -861,14 +868,47 @@ static int surface_mode(const char *dump_path, const char *panel_path) return e ? 1 : 0; } +static int geometry_mode(void) +{ + enum { S = ADRENO_DISPLAY_GEOMETRY_SCALED, P = ADRENO_DISPLAY_GEOMETRY_PLACED, C = ADRENO_DISPLAY_GEOMETRY_CROPPED, + Z = ADRENO_DISPLAY_GEOMETRY_SIZED }; + static const struct { int32_t sx, sy, sw, sh, dx, dy, dw, dh; uint64_t w, h; unsigned want; const char *what; } k[] = { + {0, 0, 2880, 1800, 0, 0, 2880, 1800, 2880, 1800, 0, "native 1440x900 at 2x"}, + {0, 0, 2880, 1800, 0, 0, 2880, 1800, 0, 0, 0, "native, surface unknown"}, + {0, 0, 2560, 1600, 0, 0, 2880, 1800, 2560, 1600, S | Z, "1280x800 at 2x (larger text)"}, + {0, 0, 3360, 2100, 0, 0, 2880, 1800, 3360, 2100, S | Z, "1680x1050 at 2x (more space)"}, + {0, 0, 3840, 2400, 0, 0, 2880, 1800, 3840, 2400, S | Z, "1920x1200 at 2x"}, + {0, 0, 2048, 1280, 0, 0, 2880, 1800, 2048, 1280, S | Z, "1024x640 at 2x"}, + {0, 0, 2880, 1800, 0, 0, 2880, 1800, 3360, 2100, C | Z, "a panel-sized source inside a larger surface"}, + {0, 0, 1440, 900, 0, 0, 2880, 1800, 1440, 900, S | Z, "1440x900 at 1x, scaled up by the display"}, + {0, 0, 1440, 900, 720, 450, 1440, 900, 1440, 900, P | Z, "unscaled, centred"}, + {16, 8, 2880, 1800, 0, 0, 2880, 1800, 2896, 1808, C | Z, "source offset"}, + {16, 8, 2880, 1800, 0, 0, 2880, 1800, 2880, 1800, C, "source origin past the surface's top-left"}, + {0, 0, -1, 1800, 0, 0, -1, 1800, 2880, 1800, P | C, "negative width"}, + {0, 0, 2880, 1800, 0, 0, 2880, 1800, 2880, 1792, C | Z, "surface one row short of its source"}, + }; + unsigned bad = 0; + for (unsigned i = 0; i < sizeof k / sizeof k[0]; i++) { + const unsigned got = adreno_display_swap_geometry(k[i].sx, k[i].sy, k[i].sw, k[i].sh, k[i].dx, k[i].dy, k[i].dw, + k[i].dh, k[i].w, k[i].h, 2880, 1800); + CHECK(got == k[i].want); + if (got != k[i].want) { bad++; fprintf(stderr, "geometry %s: 0x%x, want 0x%x\n", k[i].what, got, k[i].want); } + } + printf("{\"variant\":\"%s\",\"geometry_cases\":%u,\"failures\":%u}\n", VARIANT, + (unsigned)(sizeof k / sizeof k[0]), bad); + return bad ? 1 : 0; +} + int main(int argc, char **argv) { + if (argc == 2 && !strcmp(argv[1], "geometry")) + return geometry_mode(); if (argc >= 3 && !strcmp(argv[1], "surface")) return surface_mode(argv[2], argc > 3 ? argv[3] : NULL); if (argc == 2 && !strcmp(argv[1], "cursor")) return cursor_mode(); if (argc != 2 || strcmp(argv[1], "selftest")) { - fprintf(stderr, "usage: %s selftest | cursor | surface DUMP [PANEL]\n", argv[0]); + fprintf(stderr, "usage: %s selftest | geometry | cursor | surface DUMP [PANEL]\n", argv[0]); return 64; } case_full_panel_stale_cache(); diff --git a/tools/test-adreno-display-trace.py b/tools/test-adreno-display-trace.py new file mode 100644 index 0000000..ef55899 --- /dev/null +++ b/tools/test-adreno-display-trace.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""The IOMFB call trace (-DADRENO_DISPLAY_TRACE_MODES=1, gpu/adreno-kext/kext/src/AdrenoDisplay/trace.cpp) and the +swap-geometry rule it logs (AdrenoDisplayCopy.h adreno_display_swap_geometry). Offline: no hardware. + + python3 tools/test-adreno-display-trace.py [-v] + +- geometry: tools/test-adreno-display-present.c `geometry` runs the rule over the panel's mode and macOS's scaled + resolutions of a 2880x1800 panel; weakened rules must fail. +- the trace is logging only: every traced IOMFB method forwards its arguments unchanged to IOMobileFramebufferLegacy and + returns that result; every identifier it adds sits behind its define (the define-off kext is section-identical, + checked with tools/macho-sections.py by the change's handoff, not here). +- ABI: tools/check-iomfb-vtable.py with the desktop profile's AdrenoDisplay defines plus the trace: every one of the 483 + slots keeps Apple's PAC diversity and binds to the right target, and every IOMFB symbol the overrides call is + exported by the kernelcache. SKIP without the KDK or the J700 kernelcache (SNAP_KERNELCACHE, default + downloads/extracted/j700ap/kernelcache.release.mac17p). +""" +from pathlib import Path +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from source_text import read_source # noqa: E402 + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / 'gpu/adreno-kext/kext/src' +HARNESS = ROOT / 'tools/test-adreno-display-present.c' +COPY = SRC / 'AdrenoDisplayCopy.h' +TRACE = SRC / 'AdrenoDisplay/trace.cpp' +HEADER = SRC / 'AdrenoDisplay.h' +PROFILE = ROOT / 'gpu/adreno-kext/kext/profiles/desktop-nocoherent.sh' +KDK = Path('/Library/Developer/KDKs/KDK_27.0_26A428.kdk') +KC = Path(os.environ.get('SNAP_KERNELCACHE', ROOT / 'downloads/extracted/j700ap/kernelcache.release.mac17p')) +FLAGS = ['-O2', '-Wall', '-Wextra', '-Werror', '-Wno-deprecated-declarations'] +DEFINE = 'ADRENO_DISPLAY_TRACE_MODES' + +# Each weakens one clause of adreno_display_swap_geometry; the geometry cases must notice. +GEOMETRY_MUTANTS = { + 'scaling not seen': (' if (sw != dw || sh != dh)\n', ' if (0)\n'), + 'placement judged by origin only': ( + ' if (dx || dy || dw < 0 || dh < 0 || (uint64_t)dw != panel_w || (uint64_t)dh != panel_h)\n', + ' if (dx || dy)\n'), + 'source offset ignored': (' if (sx || sy || sw < 0 || sh < 0 ||\n', ' if (sw < 0 || sh < 0 ||\n'), + 'surface size ignored': ( + ' if ((surface_w || surface_h) && (surface_w != panel_w || surface_h != panel_h))\n', ' if (0)\n'), +} + + +def geometry(header_text=None): + """Build the present harness (against a replacement AdrenoDisplayCopy.h when given) and run `geometry`.""" + with tempfile.TemporaryDirectory() as d: + d = Path(d) + harness = HARNESS + if header_text is not None: + # The harness includes ../gpu/adreno-kext/kext/src/
: mirror that layout around a copy. + (d / 'tools').mkdir() + (d / 'gpu/adreno-kext/kext/src').mkdir(parents=True) + harness = d / 'tools' / HARNESS.name + shutil.copy(HARNESS, harness) + for h in SRC.glob('AdrenoDisplay*.h'): + shutil.copy(h, d / 'gpu/adreno-kext/kext/src' / h.name) + (d / 'gpu/adreno-kext/kext/src' / COPY.name).write_text(header_text) + exe = d / 'present' + # A mutant may leave a parameter unused; that must not be why it fails. + extra = ['-Wno-unused-parameter'] if header_text is not None else [] + subprocess.run(['xcrun', 'clang', *FLAGS, *extra, str(harness), '-o', str(exe)], check=True, + capture_output=True, timeout=300) + return subprocess.run([str(exe), 'geometry'], capture_output=True, text=True, timeout=120) + + +def body(text, signature): + start = text.index(signature) + return text[start:text.index('\n}\n', start)] + + +class Geometry(unittest.TestCase): + def test_cases_pass(self): + r = geometry() + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + row = json.loads(r.stdout.strip().splitlines()[-1]) + self.assertEqual(row['failures'], 0) + self.assertGreaterEqual(row['geometry_cases'], 12) + + def test_weakened_rules_fail(self): + text = COPY.read_text() + for name, (old, new) in GEOMETRY_MUTANTS.items(): + with self.subTest(name): + self.assertEqual(text.count(old), 1, name) + r = geometry(text.replace(old, new)) + self.assertNotEqual(r.returncode, 0, name) + + +class TraceIsLoggingOnly(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.trace = TRACE.read_text() + cls.header = HEADER.read_text() + + def overrides(self): + block = self.header[self.header.index('#if ' + DEFINE + '\n // The IOMFB call trace'):] + block = block[:block.index('#endif')] + return re.findall(r'virtual IOReturn (\w+)\(', block) + + def test_every_traced_method_forwards_and_returns_the_family_result(self): + names = self.overrides() + self.assertGreaterEqual(len(names), 20) + for name in names: + with self.subTest(name): + fn = body(self.trace, f'IOReturn AdrenoDisplay::{name}(') + signature = fn[:fn.index('\n{')] + args = re.findall(r'\b(a[0-9]|properties|owningTask|securityID|type|handler)\b', signature) + call = f'const IOReturn r = IOMobileFramebufferLegacy::{name}({", ".join(args)});' + self.assertIn(call, fn) + self.assertTrue(fn.rstrip().endswith('return r;'), name) + self.assertEqual(fn.count('return'), 2 if name == 'setProperties' else 1, name) + mode = body(self.trace, 'IOReturn AdrenoDisplay::set_digital_out_mode(') + self.assertIn('const IOReturn r = digitalOutMode(colorID, timingID);', mode) + + def test_mode_set_body_is_the_untraced_one(self): + modeset = (SRC / 'AdrenoDisplay/modeset.cpp').read_text() + self.assertIn('#if ' + DEFINE + '\nIOReturn AdrenoDisplay::digitalOutMode(unsigned int colorID, unsigned int ' + 'timingID)', modeset) + self.assertIn('#else\nIOReturn AdrenoDisplay::set_digital_out_mode(unsigned int colorID, unsigned int timingID)' + '\n#endif\n{', modeset) + + def test_every_trace_identifier_is_behind_the_define(self): + words = ('traceSwapGeometryGated', 'traceWindowGated', 'digitalOutMode', 'TRACE_CALL', 'gTraceCalls', + 'gTraceMark', 'traceCount', 'traceDue', 'adreno_display_swap_geometry') + unit = read_source(SRC / 'AdrenoDisplay.cpp') + for name, text in (('AdrenoDisplay unit', unit), ('AdrenoDisplay.h', self.header)): + inside = [] + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith('#if'): + inside.append(DEFINE in stripped) + elif stripped.startswith('#el') and inside: + inside[-1] = DEFINE in stripped and stripped.startswith('#elif') + elif stripped.startswith('#endif'): + inside.pop() + elif any(w in line for w in words) and not stripped.startswith('//'): + self.assertTrue(any(inside), f'{name}: {line}') + + def test_the_desktop_profiles_build_the_trace(self): + profile = PROFILE.read_text() + display = re.search(r'^U_AdrenoDisplay="([^"]*)"', profile, re.M).group(1) + self.assertIn('-D' + DEFINE + '=1', display.split()) + + +class VTable(unittest.TestCase): + def test_trace_keeps_apple_slots(self): + if not KDK.is_dir() or not KC.is_file(): + self.skipTest(f'needs {KDK} and the J700 kernelcache ({KC}; set SNAP_KERNELCACHE)') + env = dict(os.environ, D=str(PROFILE.parent.parent)) + defines = subprocess.run(['sh', '-c', f'. "{PROFILE}"; echo "$U_AdrenoDisplay"'], capture_output=True, text=True, + env=env, check=True).stdout.split() + args = [] + for d in [x[2:] for x in defines if x.startswith('-D')]: + args += ['--display-define', d] + self.assertIn(DEFINE + '=1', args) + with tempfile.TemporaryDirectory() as out: + subprocess.run([sys.executable, str(ROOT / 'tools/check-iomfb-vtable.py'), '--kc', str(KC), '--out', out, + *args], capture_output=True, text=True, timeout=1800) + report = json.loads((Path(out) / 'iomfb-vtable-check.json').read_text()) + display = report['classes']['AdrenoDisplay'] + self.assertEqual((display['slots'], display['errors'], len(display['extra_slots'])), (483, 0, 0)) + # The bundle links AdrenoDisplay.o alone, so the profile's references into AdrenoX2.o stay undefined there; + # none of them may be an IOMFB or IOKit symbol the overrides call. + missing = report['undefined_not_exported'] + self.assertFalse([m for m in missing if 'IOMobileFramebuffer' in m or 'IOService' in m or 'OSCollection' in m], + missing) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/test-adreno-residency.py b/tools/test-adreno-residency.py index 13c7f85..2cb358a 100755 --- a/tools/test-adreno-residency.py +++ b/tools/test-adreno-residency.py @@ -657,5 +657,114 @@ class Replays(unittest.TestCase): self.assertTrue(result.stdout.strip().splitlines()[-1].startswith('PASS'), result.stdout[-500:]) +# agents/display-modeset: the head rule (compat/adreno-resource-tail.h, -DADRENO_RESIDENT_HEAD_OFFSET=1 with +# ADRENO_RESIDENT_REPIN). org2 and b12v23: WindowServer's 16 KiB buffer at a page-unaligned address under IOGPU's 32 KiB +# map, refused -83 at the onset of the Displays-pane glitch, then every composite listing it failed (-115). +TAIL = ROOT / 'compat/adreno-resource-tail.h' +REPIN = ['-DADRENO_RESIDENT_REPIN=1'] +HEAD = REPIN + ['-DADRENO_RESIDENT_HEAD_OFFSET=1'] +# Each weakens one head rule; scenario 33 (org2's buffer) or 34 (refusals, head-0 equivalence) must notice. +HEAD_MUTANTS = { + 'admission without the page bound': ( + ' logical <= adreno_tail_round_page(head + bytes);\n}', ' 1;\n}'), + 'first page not moved back to its start': ( + 'lead = offset ? 0 : head;', 'lead = 0;'), + 'last page not mapped whole': ( + '*length = span == bytes - at ? adreno_tail_round_page(head + bytes) - offset : span + lead;', + '*length = span + lead;'), + 'a misplaced segment accepted': ( + 'pa < lead || ((pa - lead) & (ADRENO_TAIL_PAGE - 1)))', 'pa < lead)'), + 'descriptor offset ignores the head': ( + ' return offset ? offset - head : 0;', ' return offset;'), + 'partial first line only invalidated': ( + '(stop < lead ? stop : lead) - start, ADRENO_CACHE_TO_DEVICE};', + '(stop < lead ? stop : lead) - start, direction};'), + 'maintenance from the map start, not the first line': ( + ' const uint64_t start = offset > first ? offset : first;', ' const uint64_t start = offset;'), + 'client offset not moved by the head': (' *map_offset = head + offset;', ' *map_offset = offset;'), + 'client range bounded by the map, not the descriptor': ( + 'offset > bytes || len > bytes - offset) return 0;', + 'offset > bytes + head || len > bytes + head - offset) return 0;'), +} + + +def run_repin(directory, extra, header=None): + """Build the model with `extra` (and a replacement adreno-resource-tail.h when given) and run its re-pin part.""" + include = [] + if header is not None: + (Path(directory) / 'adreno-resource-tail.h').write_text(header) + include = ['-I' + str(directory)] + exe = Path(directory) / 'test-adreno-residency' + subprocess.run(['cc', *FLAGS, *MUTANT_FLAGS, *extra, *include, '-Icompat', str(HARNESS), str(CORE), '-o', str(exe)], + cwd=ROOT, check=True, timeout=300, capture_output=True) + return subprocess.run([str(exe), 'repin'], capture_output=True, text=True, timeout=600) + + +class HeadOffset(unittest.TestCase): + def test_org2_buffer_refused_without_the_rule(self): + # The re-pin build alone: scenario 33 asserts the clean refusal (E_PIN) and a refused submission every round. + with tempfile.TemporaryDirectory() as d: + result = run_repin(d, REPIN) + self.assertEqual(result.returncode, 0, result.stdout[-2000:] + result.stderr[-2000:]) + self.assertIn('PASS: repin', result.stdout) + self.assertNotIn('PASS head', result.stdout) + + def test_org2_buffer_admitted_and_mapped_with_the_rule(self): + with tempfile.TemporaryDirectory() as d: + result = run_repin(d, HEAD) + self.assertEqual(result.returncode, 0, result.stdout[-2000:] + result.stderr[-2000:]) + self.assertIn('PASS head', result.stdout) + self.assertIn('PASS: repin', result.stdout) + + def test_weakened_head_rules_fail(self): + header = TAIL.read_text() + for name, (old, new) in HEAD_MUTANTS.items(): + with self.subTest(name), tempfile.TemporaryDirectory() as d: + self.assertEqual(header.count(old), 1, name) + result = run_repin(d, HEAD, header.replace(old, new)) + self.assertNotEqual(result.returncode, 0, name) + self.assertNotIn('PASS: repin', result.stdout, name) + + def test_kext_glue_uses_the_rule_behind_its_define(self): + source = read_source(MAP) + pin = between(source, 'int AdrenoMemoryMap::pinBacking(void *ctx)') + self.assertIn('adreno_head_fit(bytes, map->fResource.logical_length)', pin) + self.assertIn('map->admitHead(backing, direction, bytes, &head)', pin) + self.assertLess(pin.index('backing->prepare(direction)'), pin.index('map->admitHead(')) # head known once wired + physical = between(source, 'uint64_t AdrenoMemoryMap::physicalPage(') + self.assertIn('adreno_head_run(offset, map->fBackingHead, map->fBackingBytes, pa, span, length)', physical) + self.assertIn('adreno_head_descriptor_offset(offset, map->fBackingHead)', physical) + admit = between(source, 'bool AdrenoMemoryMap::admitHead(') + self.assertIn('adreno_head_admit(*head, bytes, fResource.logical_length)', admit) + self.assertLess(admit.index('backing->complete(direction)'), admit.index('backing->release()')) + for method in ('int AdrenoMemoryMap::resolveFill(', 'int AdrenoMemoryMap::resolveRange(', + 'int AdrenoMemoryMap::cacheRange('): + self.assertIn('headRange(offset,bytes,&at)', between(source, method), method) + self.assertIn('adreno_head_cache(&fResource,fBackingHead,fBackingBytes,direction,0,fResource.logical_length', + between(source, 'int AdrenoMemoryMap::cacheSubmission(')) + self.assertIn('map->fBackingHead = 0;', between(source, 'int AdrenoMemoryMap::unpinBacking(')) + # Every head-rule identifier sits inside #if ADRENO_RESIDENT_HEAD_OFFSET (the define-off build is unchanged). + words = ('fBackingHead', 'admitHead', 'headRange', 'headOffsetWanted', 'adreno_head_', 'kRepinHead', 'gHead') + for name, text in (('AdrenoMemoryMap', source), ('AdrenoX2.h', (KEXT / 'src/AdrenoX2.h').read_text())): + inside = [] + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith('#if'): + inside.append('ADRENO_RESIDENT_HEAD_OFFSET' in stripped) + elif stripped.startswith(('#elif', '#else')) and inside: + inside[-1] = 'ADRENO_RESIDENT_HEAD_OFFSET' in stripped and stripped.startswith('#elif') + elif stripped.startswith('#endif'): + inside.pop() + elif any(word in line for word in words) and not stripped.startswith('//'): + self.assertTrue(any(inside), name + ': ' + line) + + def test_desktop_profiles_build_the_rule_in_every_unit(self): + profile = (KEXT / 'profiles/desktop-nocoherent.sh').read_text() + common = re.search(r'^PROFILE_ALL="([^"]*)"', profile, re.M).group(1) + self.assertIn('-DADRENO_RESIDENT_HEAD_OFFSET=1', common.split()) + self.assertIn('-DADRENO_RESIDENT_REPIN=1', common.split()) + self.assertIn('. "$D/profiles/desktop-nocoherent.sh"', (KEXT / 'profiles/desktop.sh').read_text()) + + if __name__ == '__main__': unittest.main(verbosity=2)