From c77dd9974efeeb7138e6e356b5dcfce5af5c098a Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:54:41 -0400 Subject: [PATCH 1/6] repo: move SnapAdreno to early swap completion SnapAdreno a2d43d0: ADRENO_PRESENT_EARLY_DONE in AdrenoDisplay (src/AdrenoDisplayEarlyDone.h, boot-arg adreno_present_early_done) and the desktop-early-done profile (desktop-backlight plus the define). Define off, the desktop-next profile still builds the deployed swap-path-20261024 AdrenoX2 section for section. --- 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 e1cf072..a2d43d0 160000 --- a/gpu/adreno-kext/kext +++ b/gpu/adreno-kext/kext @@ -1 +1 @@ -Subproject commit e1cf0729c63488204cc56df9414ebf22948b9d20 +Subproject commit a2d43d0c9d9d6b0035d3da9f361d7ccfa5bfd37e From 1fffff784dacb82cd1f62bd9e91178e50b5ca253 Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:54:41 -0400 Subject: [PATCH 2/6] tools: replay early swap completion in the pacing world With -DADRENO_PRESENT_EARLY_DONE=1 the presentation replay mirrors AdrenoDisplayEarlyDone.h step for step (the completion at the copy in l_present, arrivals in l_swap_begin, the tick state in l_tick) and runs early_done_scenarios (early-done.json): the 2026-10-24 busy desktop (org2's 117 windows: 49.8 copies/s with a SLOT WindowServer of 5-9 ms frames, 20% of them 14-30 ms), the same with b12v23's coherent maintenance, a FREE WindowServer, a quiet cursor desktop and next2c's sporadic heavy frames, each with the mode off, always and adaptive, under the deployed switches (relatch, urgent polling, EP1/EP2, no EP3 assist). Every earlier output (beam, damage, latency, pacing, pipeline, presentation, presentsync, relatch, vsync .json) is byte for byte the define-off build's, and the define-off build's equal the previous tree's. Busy SLOT: 49.8 -> 56.3 copies/s (coherent 51.8 -> 57.9), input latency mean +7 ms; quiet: unchanged in adaptive mode, +14 ms always; sporadic: 56.5 -> 58.1 adaptive, 59.4 always. --- tools/replay-adreno-early-done.c | 99 ++++++++++++++++++++++++++++++ tools/replay-adreno-latency.c | 36 +++++++++++ tools/replay-adreno-pacing.c | 22 +++++++ tools/replay-adreno-presentation.c | 3 + 4 files changed, 160 insertions(+) create mode 100644 tools/replay-adreno-early-done.c diff --git a/tools/replay-adreno-early-done.c b/tools/replay-adreno-early-done.c new file mode 100644 index 0000000..06318c0 --- /dev/null +++ b/tools/replay-adreno-early-done.c @@ -0,0 +1,99 @@ +/* SPDX-License-Identifier: MIT */ +/* Early completion (gpu/adreno-kext/kext/src/AdrenoDisplayEarlyDone.h): included at the end of + * tools/replay-adreno-pacing.c under -DADRENO_PRESENT_EARLY_DONE=1, after relatch_scenarios. Without the define this + * file adds nothing; with it every other output is byte for byte the define-off build's (the knobs below are 0 there). + * + * The world is the present-pacing one (the A16 beam and latch, the sync gate, the relatch, the pipelined submit lock, + * app IBs) with the deployed kext's switches: relatch and urgent polling on, early release EP1/EP2 on, EP3's assist off + * (the sparse kext compiles it out). AdrenoDisplay's early completion is mirrored step for step in + * replay-adreno-latency.c (l_present, l_swap_begin's arrival, l_tick); mode 1 completes every layer-0 swap at its copy, + * mode 2 only while a late frame engaged it (hold 2 s). + * + * Calibration (the 2026-10-24 desktop runs): + * - busy: installed-fresh-desktop-20261024-org2's 117 busy windows (gfx-probe load): 49.9 copies/s median, 18% of the + * ticks without a new swap, q 6.2 ms, one 1.35 ms IB per frame, TO_DEVICE + FROM_DEVICE 3.4 ms per IB (98 MiB). Only + * input-latency's SLOT WindowServer (two swaps outstanding, the next frame when one completes) reproduces that rate, + * with a render time of 5-9 ms and 20% of the frames 14-30 ms (FREE gives 57-59 copies/s for the same frames). + * - busy_coherent: the same frames with b12v23's maintenance (--gpu-coherent: 1.05 ms and 40 MiB per IB); + * - quiet: the cursor moving over an idle desktop, a layer-0 swap every refresh and no IB (b12v20/b12v21: 59.8/s); + * - sporadic: next2c's idle windows (57.9/s): the quiet frames plus 5% of frames with an IB whose kext time is 13 ms + * (TO_DEVICE 7 ms over 208 MiB and 6 ms of preparation) and 3.4 ms of GPU. + * The model has no guest-wide stalls, so its intervals over 34 ms stay under the hardware's (org2: 5.4%). */ +static void early_done_scenarios(const char *dir) +{ + unsigned c0 = checks, f0 = failures, cc0 = cost_checks, cf0 = cost_failures; + char path[4096]; snprintf(path, sizeof path, "%s/early-done.json", dir); + RJ = fopen(path, "w"); assert(RJ); rfirst = 0; + fprintf(RJ, "{\"scenarios\":{"); + const uint64_t M = MS; + struct ecase { struct rcase c; unsigned spike_pct; uint64_t spike_min, spike_max; }; + const struct ecase cases[] = { + {{"busy", WS_SLOT, 5 * M, 9 * M, 100, {900 * US, 1400 * US, 3 * M}, {1400 * US, 3 * M, 8 * M}, {75, 23, 2}, + 2200 * US, 3400 * US, 500 * US, 1 * M, 1200 * M, {1 * M, 3 * M, 10 * M}, {3 * M, 10 * M, 30 * M}, {80, 15, 5}, + 1 * M, 300 * US, 2, 1 * M, 6 * M, 3}, 20, 14 * M, 30 * M}, + {{"busy_coherent", WS_SLOT, 5 * M, 9 * M, 100, {900 * US, 1400 * US, 3 * M}, {1400 * US, 3 * M, 8 * M}, {75, 23, 2}, + 400 * US, 900 * US, 300 * US, 700 * US, 1200 * M, {1 * M, 3 * M, 10 * M}, {3 * M, 10 * M, 30 * M}, {80, 15, 5}, + 1 * M, 300 * US, 2, 1 * M, 6 * M, 3}, 20, 14 * M, 30 * M}, + {{"busy", WS_FREE, 5 * M, 9 * M, 100, {900 * US, 1400 * US, 3 * M}, {1400 * US, 3 * M, 8 * M}, {75, 23, 2}, + 2200 * US, 3400 * US, 500 * US, 1 * M, 1200 * M, {1 * M, 3 * M, 10 * M}, {3 * M, 10 * M, 30 * M}, {80, 15, 5}, + 1 * M, 300 * US, 2, 1 * M, 6 * M, 3}, 20, 14 * M, 30 * M}, + {{"quiet", WS_SLOT, 2 * M, 3600 * US, 0, {0, 0, 0}, {0, 0, 0}, {100, 0, 0}, + 0, 0, 0, 0, 0, {0, 0, 0}, {0, 0, 0}, {100, 0, 0}, 0, 0, 0, 0, 0, 0}, 0, 0, 0}, + {{"sporadic", WS_SLOT, 2 * M, 3600 * US, 5, {3 * M, 3 * M, 3 * M}, {4 * M, 4 * M, 4 * M}, {100, 0, 0}, + 12 * M, 14 * M, 1 * M, 2 * M, 0, {0, 0, 0}, {0, 0, 0}, {100, 0, 0}, 0, 0, 0, 0, 0, 0}, 0, 0, 0}, + }; + const unsigned ncases = sizeof cases / sizeof cases[0]; + for (unsigned i = 0; i < ncases; i++) { + const struct ecase *e = &cases[i]; + struct rres x[3]; + for (int mode = 0; mode < 3; mode++) { + char tag[32]; snprintf(tag, sizeof tag, "_done%d", mode); + rcase_noassist = 1; rcase_copydone = mode; rcase_ed_hold = 2000 * M; + rcase_spike_pct = e->spike_pct; rcase_spike_min = e->spike_min; rcase_spike_max = e->spike_max; + x[mode] = r_case(&e->c, RP_ALL, 1500, 1000, 0, tag); + } + rcase_noassist = 0; rcase_copydone = 0; rcase_ed_hold = 0; rcase_spike_pct = 0; rcase_spike_min = rcase_spike_max = 0; + const struct rres *t = &x[0], *a = &x[1], *d = &x[2]; + /* mode 0 measures only: nothing completed at a copy; the others never complete without a copy */ + CHECK(t->ed.early == 0 && t->ed.engages == 0); + CHECK(a->ed.early <= a->copies && d->ed.early <= d->copies); + CHECK(a->ed.engages == 0); /* mode 1 has nothing to engage */ + /* never fewer copies than today (1% noise) */ + COST(a->cps_x10 * 100 >= t->cps_x10 * 99 && d->cps_x10 * 100 >= t->cps_x10 * 99); + if (e->c.ws == WS_SLOT && t->ed.late * 50 > t->copies) { + /* WindowServer late on more than 2% of the frames: both modes recover most of the missed refreshes */ + COST(a->cps_x10 >= t->cps_x10 + 30 && d->cps_x10 >= t->cps_x10 + 25); + COST(d->ed.engages > 0 && d->ed.early > 0); + } + if (!t->ed.late) { + /* nothing late: the adaptive mode never engages and the run is today's frame for frame */ + COST(d->ed.engages == 0 && d->ed.early == 0 && d->cps_x10 == t->cps_x10 && d->r.in_mean_us == t->r.in_mean_us); + } + char line[512]; + int n = adreno_perf_line_done(line, sizeof line, i + 1, &d->ed, 0); + CHECK(n > 0 && n <= ADRENO_PERF_LINE_MAX); + printf("early-done %s_%s: copies/s today %llu.%llu, always %llu.%llu, adaptive %llu.%llu; >34 ms %llu.%llu%% / " + "%llu.%llu%% / %llu.%llu%%; late frames today %llu of %llu copies; input mean %llu / %llu / %llu us, " + "p95 %llu / " + "%llu / %llu us; q %llu / %llu / %llu us; adaptive engaged %llu times, %llu of %llu copies completed early\n", + e->c.name, ws_name[e->c.ws], (unsigned long long)t->cps_x10 / 10, (unsigned long long)t->cps_x10 % 10, + (unsigned long long)a->cps_x10 / 10, (unsigned long long)a->cps_x10 % 10, + (unsigned long long)d->cps_x10 / 10, (unsigned long long)d->cps_x10 % 10, + (unsigned long long)t->over34_pm / 10, (unsigned long long)t->over34_pm % 10, + (unsigned long long)a->over34_pm / 10, (unsigned long long)a->over34_pm % 10, + (unsigned long long)d->over34_pm / 10, (unsigned long long)d->over34_pm % 10, + (unsigned long long)t->ed.late, (unsigned long long)t->copies, + (unsigned long long)t->r.in_mean_us, (unsigned long long)a->r.in_mean_us, (unsigned long long)d->r.in_mean_us, + (unsigned long long)t->r.in_p95_us, (unsigned long long)a->r.in_p95_us, (unsigned long long)d->r.in_p95_us, + (unsigned long long)t->r.q_us, (unsigned long long)a->r.q_us, (unsigned long long)d->r.q_us, + (unsigned long long)d->ed.engages, (unsigned long long)d->ed.early, (unsigned long long)d->copies); + n = adreno_perf_line_done(line, sizeof line, i + 1, &t->ed, 0); + printf("early-done %s_%s today's arrivals: %s\n", e->c.name, ws_name[e->c.ws], line); + } + fprintf(RJ, "},\"checks\":%u,\"failures\":%u,\"cost_checks\":%u,\"cost_failures\":%u}\n", checks - c0, failures - f0, + cost_checks - cc0, cost_failures - cf0); + fclose(RJ); + printf("{\"early_done_checks\":%u,\"early_done_failures\":%u,\"early_done_cost_checks\":%u," + "\"early_done_cost_failures\":%u}\n", + checks - c0, failures - f0, cost_checks - cc0, cost_failures - cf0); +} diff --git a/tools/replay-adreno-latency.c b/tools/replay-adreno-latency.c index e40322f..bac6361 100644 --- a/tools/replay-adreno-latency.c +++ b/tools/replay-adreno-latency.c @@ -57,6 +57,16 @@ /* agents/present-pacing FAILURE-MODES.md: the b12v17 pipeline with its submit lock, and the relatch (relatch_scenarios) */ #include "../gpu/adreno-kext/kext/src/AdrenoDisplayRelatch.h" #endif +#ifndef ADRENO_PRESENT_EARLY_DONE +#define ADRENO_PRESENT_EARLY_DONE 0 +#endif +#if ADRENO_PRESENT_EARLY_DONE +#if !ADRENO_PRESENT_RELATCH +#error "the early-completion scenarios run in the present-pacing world (-DADRENO_PRESENT_RELATCH=1)" +#endif +/* AdrenoDisplayEarlyDone.h: a layer-0 swap completed right after its copy, always or while WindowServer is behind (early_done_scenarios) */ +#include "../gpu/adreno-kext/kext/src/AdrenoDisplayEarlyDone.h" +#endif enum { WS_SLOT = 0, WS_EVENT = 1, WS_TIMER = 2, WS_FREE = 3 }; enum { LP_BEAM = 0, LP_LATCH = 1, LP_HOLD = 2, LP_HOLD_NOGATE = 3 }; @@ -144,6 +154,10 @@ struct lworld { uint64_t c_ovl, c_tmr, c_pre, c_ast, c_app, c_apps, c_polls, c_late_unmark; uint64_t ret_at[LMAX]; /* the submission's return: G1 busy until then, S returns then */ #endif +#if ADRENO_PRESENT_EARLY_DONE + /* AdrenoDisplay's fEarly: mode = complete_at_copy (1 always, as the pipeline what-if; 2 adaptive), its hold */ + struct adreno_early_done ed; uint64_t ed_hold; +#endif }; /* ---- WindowServer ---- */ @@ -334,13 +348,25 @@ static void l_present(struct lworld *l, uint32_t id, uint8_t layers) /* presen l->presented = 1; if (layers & 1) l_copy(l, id); #if ADRENO_PIPELINE +#if ADRENO_PRESENT_EARLY_DONE + /* earlyDoneNowGated by mode (1 is also gpu-pipeline's what-if, whose hold rows keep it: the kext refuses the hold + * with early completion, the early-completion scenarios never run the hold) */ + if ((layers & 1) && l->complete_at_copy && adreno_early_active(&l->ed, l->w.now)) { + l->early_done = id; adreno_early_completed(&l->ed); ws_completed(l, id); + } +#else if ((layers & 1) && l->complete_at_copy) { l->early_done = id; ws_completed(l, id); } /* what-if */ #endif +#endif } static void l_pop(struct lworld *l) { memmove(l->q, l->q + 1, (l->qn - 1) * sizeof l->q[0]); l->qn--; } static void l_swap_begin(struct lworld *l, int in_tick) /* swap_begin_gated */ { struct bworld *w = &l->w; +#if ADRENO_PRESENT_EARLY_DONE + if (!in_tick && l->qn && !l->pending_since && (l->q[0].layers & 1)) + adreno_early_arrival(&l->ed, w->now, l->qn == 1); /* swap_begin_gated, F1's read */ +#endif if (!in_tick && l->qn && !l->pending_since) l->pending_since = w->now; /* F1 */ #if ADRENO_LATCH if (!in_tick && l->pending_since && l->h.released_at) adreno_hold_arrival(&l->h, tb(w, l->pending_since)); @@ -462,12 +488,19 @@ static void l_tick(struct lworld *l) /* vsyncGated */ w->in_tick = 0; if (!held) { if (l->rel_armed && l->cur && l->presented) l->rel_armed = 0; +#endif +#if ADRENO_PRESENT_EARLY_DONE + const int ed_had = l->cur && l->presented && (l->cur_layers & 1); /* completed here, or held since its copy */ + const uint64_t ed_copies = l->copies; #endif if (l->cur && l->presented) l_complete(l); w->in_tick = 1; if (l_gate(l)) { l->gated++; l_arm(l); } else l_swap_begin(l, 1); #if ADRENO_PRESENT_RELATCH if (l->p2) p2_relatch_arm(l); /* RL4: the GO's frame unused and its head deferred */ +#endif +#if ADRENO_PRESENT_EARLY_DONE + adreno_early_tick(&l->ed, w->now, ed_had, l->copies != ed_copies || (l->qn && (l->q[0].layers & 1))); #endif w->b.go_row0 = 0; w->in_tick = 0; #if ADRENO_LATCH @@ -545,6 +578,9 @@ static void l_start(struct lworld *l) adreno_beam_latch(&w->b, l->policy == LP_BEAM ? 0 : 3 * ms); adreno_hold_init(&l->h, l->policy == LP_HOLD || l->policy == LP_HOLD_NOGATE, ms * 3 / 2, ms * ADRENO_HOLD_FLOOR_US / 1000); l->gate_ns = l->policy == LP_LATCH || l->policy == LP_HOLD ? 4 * MS : 0; +#endif +#if ADRENO_PRESENT_EARLY_DONE + adreno_early_init(&l->ed, (uint32_t)l->complete_at_copy, 16666667, l->ed_hold ? l->ed_hold : 2000 * MS, MS); #endif l->started_at = w->now; if (l->ws_kind == WS_EVENT) l->events = 1; diff --git a/tools/replay-adreno-pacing.c b/tools/replay-adreno-pacing.c index 177bf47..62edd15 100644 --- a/tools/replay-adreno-pacing.c +++ b/tools/replay-adreno-pacing.c @@ -277,8 +277,19 @@ struct rres { uint64_t copies, iv[5], over34_pm, cps_x10, defer, swaps, max_wait_us, gpu_frames, marked, apps; uint64_t ovl, tmr, pre, ast, app_fin, polls, rl_armed, rl_go, rl_late, rl_gone, rl_gated, rl_ovt, rl_polls, rl_lead_min_us; uint64_t hist[ADRENO_RELATCH_BINS]; +#if ADRENO_PRESENT_EARLY_DONE + struct adreno_early_done ed; /* the run's early completions and arrivals (not windowed) */ +#endif }; static FILE *RJ; static int rfirst; +#if ADRENO_PRESENT_EARLY_DONE +/* replay-adreno-early-done.c's knobs, all 0 for every row above (their outputs stay byte for byte as without the define): + * the swap completed right after its copy (lworld complete_at_copy, AdrenoDisplay's adreno_present_early_done=1), + * WindowServer's render spikes, and EP3's assist off (the deployed sparse kext compiles it out). */ +static int rcase_copydone, rcase_noassist; +static unsigned rcase_spike_pct; +static uint64_t rcase_spike_min, rcase_spike_max, rcase_ed_hold; +#endif static struct rres r_case(const struct rcase *c, int rp, unsigned swaps, uint64_t guard_us, unsigned seed, const char *tag) { static struct lworld l; @@ -297,6 +308,11 @@ static struct rres r_case(const struct rcase *c, int rp, unsigned swaps, uint64_ l.early = rp == RP_EARLY || rp == RP_EARLY_NOASSIST || rp == RP_ALL || rp == RP_ALL_PIPE2; l.assist = l.early && rp != RP_EARLY_NOASSIST ? 250 * US : 0; l.tmr_period = 500 * US; l.urgent_period = relatch && rp != RP_RELATCH_NOURGENT ? 100 * US : 0; +#if ADRENO_PRESENT_EARLY_DONE + l.complete_at_copy = rcase_copydone; l.ed_hold = rcase_ed_hold; + l.spike_pct = rcase_spike_pct; l.spike_min = rcase_spike_min; l.spike_max = rcase_spike_max; + if (rcase_noassist) l.assist = 0; +#endif l.multi_pct = c->multi_pct; l.w.stall_pct = c->stall_pct; l.w.stall_min = c->stall_min; l.w.stall_max = c->stall_max; l.tmr_at = l.enter_at = l.rl_at = LINF; @@ -331,6 +347,9 @@ static struct rres r_case(const struct rcase *c, int rp, unsigned swaps, uint64_ x.rl_armed = l.rl.armed; x.rl_go = l.rl.go; x.rl_late = l.rl.late; x.rl_gone = l.rl.gone; x.rl_gated = l.rl.gated; x.rl_ovt = l.rl.ovt; x.rl_polls = l.rl.polls; x.rl_lead_min_us = tb_ns(&l.w, l.w.tb_off + l.rl.lead_min) / 1000; memcpy(x.hist, l.rl.hist, sizeof x.hist); +#if ADRENO_PRESENT_EARLY_DONE + x.ed = l.ed; +#endif char name[128]; snprintf(name, sizeof name, "%s_%s_%s%s", c->name, ws_name[c->ws], rp_name[rp], tag ? tag : ""); const struct lres *r = &x.r; fprintf(RJ, "%s\"%s\":{\"frames\":%llu,\"torn\":%llu,\"ovt\":%llu,\"beam_forced\":%llu,\"fps_x10\":%llu,\"copies_x10\":%llu," @@ -493,4 +512,7 @@ static void relatch_scenarios(const char *dir) printf("{\"relatch_checks\":%u,\"relatch_failures\":%u,\"relatch_cost_checks\":%u,\"relatch_cost_failures\":%u}\n", checks - c0, failures - f0, cost_checks - cc0, cost_failures - cf0); } +#if ADRENO_PRESENT_EARLY_DONE +#include "replay-adreno-early-done.c" +#endif #endif diff --git a/tools/replay-adreno-presentation.c b/tools/replay-adreno-presentation.c index fdb62ec..efe2767 100644 --- a/tools/replay-adreno-presentation.c +++ b/tools/replay-adreno-presentation.c @@ -1153,6 +1153,9 @@ int main(int argc,char **argv) { #endif #if ADRENO_PRESENT_RELATCH relatch_scenarios(argv[1]); +#if ADRENO_PRESENT_EARLY_DONE + early_done_scenarios(argv[1]); +#endif #endif #endif #endif From 6605245ba513912305a49eadcc4cd50ec5e7947e Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:54:41 -0400 Subject: [PATCH 3/6] cycle: add --present-early-done and its AuxKC slot --present-early-done MODE (needs --edp-backlight and its chain) adds the guest boot-arg adreno_present_early_done=MODE and --present-early-hold-ms (mode 2 only) adreno_present_early_hold_ms; both are refused with --present-hold on. --edp-backlight with --present-early-done or --gpu-coherent now takes the collection pinned as INPUT_AUXKC_DESKTOP_EARLY_DONE (SnapAdreno desktop-early-done: the coherent desktop kext with the backlight service and early completion) instead of the nocoherent INPUT_AUXKC_DESKTOP_BACKLIGHT, and is refused until that collection is linked and pinned. Without the new flags the boot arguments, the AuxKC and the monitor are unchanged: the next3 argument list still builds monitor 97be2b80 (459,904 bytes). --- docs/RUNNING.md | 25 ++++++++++++++++++--- tools/cycle.py | 10 +++++++++ tools/cycle_lib/installed_fresh.py | 36 ++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/docs/RUNNING.md b/docs/RUNNING.md index a1cc2a3..630c41c 100644 --- a/docs/RUNNING.md +++ b/docs/RUNNING.md @@ -147,7 +147,7 @@ The flags fall into these groups: | --- | --- | | Console and early stop | `--console`, `--single-user`, `--early-stop-delay`, `--let-panic`, `--panic-serve` | | Input | `--input`, `--input-touchpad`, `--input-ptp`, `--input-active-us`, `--input-fnkeys` | -| Present path | `--present-dartpool`, `--present-resident`, `--present-beam`, `--present-va-reclaim`, `--present-maint`, `--present-latch`, `--present-pipeline` | +| Present path | `--present-dartpool`, `--present-resident`, `--present-beam`, `--present-va-reclaim`, `--present-maint`, `--present-latch`, `--present-pipeline`, `--present-early-done` | | GPU | `--ib-async`, `--gpu-recover`, `--gpu-va-wide`, `--gpu-coherent`, `--resident-mib`, `--adreno-fill-br`, `--adreno-fence-br`, `--adreno-cs-samplers`, `--gpu-x290-survey` | | Panel brightness | `--edp-backlight`, `--edp-backlight-mode` | | Clocks and bandwidth | `--thermal-guard`, `--gpu-dvfs`, `--gpu-dvfs-ceiling`, `--cpu-dvfs`, `--ddr-vote`, `--llcc`, `--bw-probe` | @@ -214,12 +214,31 @@ nits); either boot-arg moves the curve without rebuilding the monitor. The collection is built with `KEXT_PROFILE=desktop-next I2C_PROFILE=desktop-fnkeys tools/build-auxkc-desktop.sh NAME` for a monitor without `--gpu-coherent` (`desktop-next` is -`desktop-nocoherent` plus the service); a `--gpu-coherent` run needs a -`KEXT_PROFILE=desktop-backlight` collection pinned there instead. `--edp-backlight-mode` lets the first change switch a panel left in +`desktop-nocoherent` plus the service). With `--gpu-coherent` or +`--present-early-done` the run takes the collection pinned as +`INPUT_AUXKC_DESKTOP_EARLY_DONE` instead (`KEXT_PROFILE=desktop-early-done +I2C_PROFILE=desktop-fnkeys tools/build-auxkc-desktop.sh NAME`; `desktop-early-done` is +`desktop-backlight`, the coherent `desktop` plus the service, with early +completion), and is refused until that collection is pinned. `--edp-backlight-mode` lets the first change switch a panel left in PWM-pin mode to DPCD control. `tools/survey-edp-aux.py` checks, read-only from q1n1, that the clocks and the AUX controller the call needs are on. `tools/test-edp-backlight.py` is the offline test. +`--present-early-done MODE` (needs `--edp-backlight` and its chain) adds the +guest boot-arg `adreno_present_early_done=MODE` for the `desktop-early-done` +AdrenoX2 (`src/AdrenoDisplayEarlyDone.h` in the kext). AdrenoDisplay then +completes a layer-0 swap right after its scanout copy instead of at the next +frame tick. WindowServer keeps at most two swaps outstanding, so completing at +the copy frees one of its entries a refresh earlier and a frame that runs past +one refresh can still make the next latch; WindowServer may then run up to one +frame further ahead. `1` completes at every copy, `2` only while WindowServer +is late (for `--present-early-hold-ms`, default 2000, after each late frame), +`0` only measures. Every mode adds one `AdrenoDisplay: perf done` line per perf +window: completions at the copy, late frames, engagements, and histograms of +WindowServer's arrivals after each tick and of each late frame's lateness. It +excludes `--present-hold on`. `tools/test-present-early-done.py` is the offline +test (with the presentation replay's early-done scenarios). + `--view-reclaim` gives the monitor `SNAP_VIEW_RECLAIM`. The kernel's per-CPU private page-table copies (`shadow/sprr-view.h`) hold 256 tables instead of 128, and when a protecting RETYPE (read-only zone, TXM) finds them full, the diff --git a/tools/cycle.py b/tools/cycle.py index d51f629..6ec247f 100755 --- a/tools/cycle.py +++ b/tools/cycle.py @@ -295,6 +295,16 @@ def parse(argv=None): help='with --present-latch: guest boot-arg adreno_hold (default off since b12v11: on completes the swap ' 'on screen just before the next latch and copies the pending one early; b12v11 showed early ' 'copies of frames whose IB had not finished)') + p.add_argument('--present-early-done', type=int, choices=(0, 1, 2), default=None, + help='with --edp-backlight and its chain: the AuxKC INPUT_AUXKC_DESKTOP_EARLY_DONE and guest ' + 'boot-arg adreno_present_early_done: AdrenoDisplay completes a layer-0 swap right after its ' + 'scanout copy instead of at the next tick, so WindowServer\'s two swap entries free a refresh ' + 'earlier (1 always; 2 only while WindowServer is late, for --present-early-hold-ms after each late ' + 'frame; 0 measures only). Every mode adds the perf done line (WindowServer\'s arrivals against ' + 'the ticks). Not with --present-hold on; give it to every mode of the attempt') + p.add_argument('--present-early-hold-ms', type=int, default=None, + help='with --present-early-done 2: guest boot-arg adreno_present_early_hold_ms, how long a late frame ' + 'keeps early completion on (default 2000; 100..10000)') p.add_argument('--present-gate-us', type=int, default=None, help='with --present-latch: guest boot-arg adreno_gate_us, the age under which a layer-0 swap is not ' 'copied while a GPU submission is in the kext (default 4000; 0 = no gate)') diff --git a/tools/cycle_lib/installed_fresh.py b/tools/cycle_lib/installed_fresh.py index a8ebff8..06adb70 100644 --- a/tools/cycle_lib/installed_fresh.py +++ b/tools/cycle_lib/installed_fresh.py @@ -244,6 +244,17 @@ INPUT_AUXKC_PTP_DARTPOOL_RESIDENT_BEAM_VA_WIDE_LATCH_SHA = 'ccf0c8f4da4f63fbe25c # (57965a84); without --edp-backlight the latch collection above (org-desktop-nocoherent-20261023, ccf0c8f4) is used. INPUT_AUXKC_DESKTOP_BACKLIGHT = 'build/current-assets/swap-path-20261024/adreno-input.kc' INPUT_AUXKC_DESKTOP_BACKLIGHT_SHA = '9e916a4185bc609178fe80a3110e020d9f3167625da7a76a9bc9ff22412743d5' +# --edp-backlight with --gpu-coherent or --present-early-done: the --edp-backlight collection rebuilt from +# SnapAdreno's desktop-early-done profile, i.e. desktop-backlight (desktop's IO-coherent maintenance skipping plus +# AdrenoBacklight) plus -DADRENO_PRESENT_EARLY_DONE=1 in AdrenoDisplay.o (src/AdrenoDisplayEarlyDone.h: a layer-0 swap +# completed right after its scanout copy, boot-arg adreno_present_early_done 1 always / 2 while WindowServer is behind, +# 0 measures only; the perf done line), and SnapI2CHID desktop-fnkeys. Link it with +# KEXT_PROFILE=desktop-early-done I2C_PROFILE=desktop-fnkeys tools/build-auxkc-desktop.sh NAME (sudo for kmutil's staging) +# and put the two lines it prints here. Every addition is inert without its switch (the monitor's coherent ON with +# adreno_coherent=1; adreno_backlight=1; adreno_present_early_done), so the collection also serves --edp-backlight runs +# without them. None until linked and pinned: those flag sets are refused rather than run on the nocoherent kext. +INPUT_AUXKC_DESKTOP_EARLY_DONE = None +INPUT_AUXKC_DESKTOP_EARLY_DONE_SHA = None # --guest-net: INPUT_AUXKC's objects with SnapintoshVirtualEthernet's datapath (docs/GUEST-NETWORK-20261019.md). # None until that collection is built and pinned; --guest-net refuses meanwhile. INPUT_AUXKC_NET = None @@ -787,6 +798,9 @@ def route_selected(args): edp_backlight = getattr(args, 'edp_backlight', False) need(not edp_backlight or latch, '--edp-backlight needs --present-latch and its chain (its AdrenoX2 is that collection\'s with ADRENO_BACKLIGHT)') + early = getattr(args, 'present_early_done', None) + need(early is None or edp_backlight, + '--present-early-done needs --edp-backlight and its chain (its AdrenoX2 is the early-done collection)') need(not (vsync60 and (damage or cached or gpumap)), '--present-vsync60 already includes --present-gpumap\'s (and so --present-cached-read\'s) kext: give one of them') need(not (damage and cached), '--present-cached-read already includes --present-damage\'s present: give one of them') @@ -812,6 +826,14 @@ def route_selected(args): if vsync60: return ((INPUT_TOUCHPAD_PROFILE if touchpad else INPUT_PROFILE), INPUT_AUXKC_VSYNC60, INPUT_AUXKC_VSYNC60_SHA, INPUT_BUNDLES) + if dartpool and ptp and resident and beam and va_reclaim and va_wide and latch and edp_backlight and ( + getattr(args, 'gpu_coherent', False) or early is not None): + need(INPUT_AUXKC_DESKTOP_EARLY_DONE is not None and INPUT_AUXKC_DESKTOP_EARLY_DONE_SHA is not None, + '--edp-backlight with --gpu-coherent or --present-early-done: its AuxKC is not pinned yet ' + '(INPUT_AUXKC_DESKTOP_EARLY_DONE_SHA; KEXT_PROFILE=desktop-early-done I2C_PROFILE=desktop-fnkeys ' + 'tools/build-auxkc-desktop.sh NAME links it)') + return (INPUT_TOUCHPAD_PROFILE, INPUT_AUXKC_DESKTOP_EARLY_DONE, INPUT_AUXKC_DESKTOP_EARLY_DONE_SHA, + INPUT_BUNDLES) if dartpool and ptp and resident and beam and va_reclaim and va_wide and latch and edp_backlight: need(INPUT_AUXKC_DESKTOP_BACKLIGHT is not None and INPUT_AUXKC_DESKTOP_BACKLIGHT_SHA is not None, '--edp-backlight: its AuxKC is not pinned yet (INPUT_AUXKC_DESKTOP_BACKLIGHT_SHA; ' @@ -1244,6 +1266,20 @@ def boot_addition(args): if active_us is not None: need(active_us == 0 or 2000 <= active_us <= 100000, '--input-active-us must be 0 or 2000..100000') extra.append(f'snap_i2c_active_us={active_us}') + # --present-early-done: AdrenoDisplay's early completion (src/AdrenoDisplayEarlyDone.h), read only by the + # desktop-early-done AdrenoX2 that --edp-backlight then selects; the kext turns it off with the release hold. + early, early_hold = getattr(args, 'present_early_done', None), getattr(args, 'present_early_hold_ms', None) + need(early is None or getattr(args, 'edp_backlight', False), + '--present-early-done needs --edp-backlight (the early-done collection is that chain\'s)') + need(early is None or early in (0, 1, 2), '--present-early-done is 0 (measure only), 1 (always) or 2 (while behind)') + need(early_hold is None or early == 2, '--present-early-hold-ms needs --present-early-done 2') + need(not early or hold != 'on', '--present-early-done 1/2 and --present-hold on exclude each other (the hold ' + 'keeps the swap current until its release)') + if early is not None: + extra.append(f'adreno_present_early_done={early}') + if early_hold is not None: + need(100 <= early_hold <= 10000, '--present-early-hold-ms must be 100..10000') + extra.append(f'adreno_present_early_hold_ms={early_hold}') # --input-fnkeys: the keyboard's Fn-key layer (drivers/snap-i2c-hid/src/hid-keyboard.h HID_FNKEYS_*), read by a # SnapI2CHID built with SNAP_I2C_HID_FNKEYS; the same sets the kext refuses are refused here. fnkeys = getattr(args, 'input_fnkeys', None) From 63d237b6bb93a63540ce6bb14c0b7c8b6122ba7b Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:54:41 -0400 Subject: [PATCH 4/6] tests: cover early swap completion and its cycle flags tools/test-adreno-early-done.c unit-tests AdrenoDisplayEarlyDone.h (modes, arrival bins, late frames and what is not one, adaptive engagement, renewal and expiry, windows, the perf done line at its caps). tools/test-present-early-done.py runs it, requires it to fail on eight mutants of the header, checks that the kext touches the feature only under ADRENO_PRESENT_EARLY_DONE, that desktop-early-done differs from desktop-backlight only in AdrenoDisplay.o, the cycle flags, refusals and AuxKC choice, and the replay (earlier outputs unchanged by the define, no failure or cost failure in early-done.json). --- tools/test-adreno-early-done.c | 157 ++++++++++++++++++++ tools/test-present-early-done.py | 240 +++++++++++++++++++++++++++++++ 2 files changed, 397 insertions(+) create mode 100644 tools/test-adreno-early-done.c create mode 100644 tools/test-present-early-done.py diff --git a/tools/test-adreno-early-done.c b/tools/test-adreno-early-done.c new file mode 100644 index 0000000..f619668 --- /dev/null +++ b/tools/test-adreno-early-done.c @@ -0,0 +1,157 @@ +/* SPDX-License-Identifier: MIT */ +/* Host test for gpu/adreno-kext/kext/src/AdrenoDisplayEarlyDone.h (AdrenoDisplay's early completion and arrival + * measurements). No kext, no A16. Times are plain units with one millisecond = 1000. + * + * cc -std=c11 -Wall -Wextra -Werror tools/test-adreno-early-done.c -o test && ./test + * (-DEARLY_DONE_HEADER='""' tests another copy of the header: tools/test-present-early-done.py runs mutants) + * + * Exit 0 when every check passes; each failure is printed. */ +#include +#include +#include +#ifndef EARLY_DONE_HEADER +#define EARLY_DONE_HEADER "../gpu/adreno-kext/kext/src/AdrenoDisplayEarlyDone.h" +#endif +#include EARLY_DONE_HEADER + +static unsigned checks, failures; +#define CHECK(c) \ + do { checks++; if (!(c)) { failures++; fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #c); } } while (0) + +enum { MSU = 1000, PERIOD = 16667, HOLD = 2000 * MSU }; + +static struct adreno_early_done fresh(uint32_t mode) +{ + struct adreno_early_done e; + adreno_early_init(&e, mode, PERIOD, HOLD, MSU); + return e; +} + +/* Modes: OFF never completes early, ALWAYS always, ADAPTIVE only once engaged; an unknown mode is OFF. */ +static void modes(void) +{ + struct adreno_early_done off = fresh(ADRENO_EARLY_OFF), always = fresh(ADRENO_EARLY_ALWAYS); + struct adreno_early_done adaptive = fresh(ADRENO_EARLY_ADAPTIVE), bad = fresh(7); + CHECK(!adreno_early_active(&off, 1) && !adreno_early_active(&off, 1u << 30)); + CHECK(adreno_early_active(&always, 0) && adreno_early_active(&always, 123456789)); + CHECK(!adreno_early_active(&adaptive, 5 * MSU)); + CHECK(bad.mode == ADRENO_EARLY_OFF && !adreno_early_active(&bad, 5)); + struct adreno_early_done z = fresh(ADRENO_EARLY_ADAPTIVE); + adreno_early_init(&z, ADRENO_EARLY_ADAPTIVE, PERIOD, HOLD, 0); + CHECK(z.ms == 1); /* never a zero millisecond (the bins divide by it) */ +} + +/* Arrivals: the offset after the last proceeding tick in 2 ms bins, the last bin open; none before the first tick. */ +static void arrivals(void) +{ + struct adreno_early_done e = fresh(ADRENO_EARLY_OFF); + CHECK(!adreno_early_arrival(&e, 5 * MSU, 1) && e.arrivals == 0); /* no tick yet: nothing to measure from */ + adreno_early_tick(&e, 100 * MSU, 1, 1); + const uint64_t offsets[] = {0, 1999, 2000, 7 * MSU, 15999, 16 * MSU, 40 * MSU}; + const unsigned bins[] = {0, 0, 1, 3, 7, 8, 8}; + for (unsigned i = 0; i < sizeof offsets / sizeof offsets[0]; i++) { + struct adreno_early_done x = e; + adreno_early_arrival(&x, 100 * MSU + offsets[i], 1); + CHECK(x.arrivals == 1 && x.arr[bins[i]] == 1); + } + struct adreno_early_done x = e; + CHECK(!adreno_early_arrival(&x, 99 * MSU, 1) && x.arrivals == 0); /* before the tick (a clock race): ignored */ + CHECK(!adreno_early_arrival(&x, 105 * MSU, 0) && x.ahead == 1 && x.arrivals == 0); /* behind another: ahead */ +} + +/* Late frames: a tick that had a frame and neither copied nor kept a layer-0 head, then an arrival within one refresh. + * Not late: a served tick, an idle tick (no frame on screen), an arrival a refresh or more later, a second arrival. */ +static void late_frames(void) +{ + struct adreno_early_done e = fresh(ADRENO_EARLY_OFF); + adreno_early_tick(&e, 1000 * MSU, 1, 0); /* missed */ + CHECK(adreno_early_arrival(&e, 1000 * MSU + 3 * MSU, 1) == 1); + CHECK(e.late == 1 && e.lat[1] == 1 && e.engages == 0 && !adreno_early_active(&e, 1004 * MSU)); + CHECK(adreno_early_arrival(&e, 1000 * MSU + 5 * MSU, 1) == 0 && e.late == 1); /* once per missed tick */ + const uint64_t lateness[] = {500, 2500, 6000, 9000, PERIOD - 1}; + const unsigned lbins[] = {0, 1, 2, 3, 3}; + for (unsigned i = 0; i < sizeof lateness / sizeof lateness[0]; i++) { + struct adreno_early_done x = fresh(ADRENO_EARLY_OFF); + adreno_early_tick(&x, 50 * MSU, 1, 0); + CHECK(adreno_early_arrival(&x, 50 * MSU + lateness[i], 1) == 1 && x.lat[lbins[i]] == 1); + } + struct adreno_early_done x = fresh(ADRENO_EARLY_OFF); + adreno_early_tick(&x, 50 * MSU, 1, 0); + CHECK(adreno_early_arrival(&x, 50 * MSU + PERIOD, 1) == 0 && x.late == 0); /* a refresh later: not this tick's */ + adreno_early_tick(&x, 80 * MSU, 1, 1); /* served: a copy, or a deferred head kept */ + CHECK(adreno_early_arrival(&x, 81 * MSU, 1) == 0 && x.late == 0); + adreno_early_tick(&x, 90 * MSU, 0, 0); /* idle: nothing on screen for that refresh */ + CHECK(adreno_early_arrival(&x, 91 * MSU, 1) == 0 && x.late == 0); + adreno_early_tick(&x, 100 * MSU, 1, 0); + CHECK(adreno_early_arrival(&x, 101 * MSU, 0) == 0 && x.late == 0 && x.ahead == 1); /* behind another: no late */ +} + +/* Adaptive: a late frame engages it for `hold`, every later late frame renews it, it ends by itself; ALWAYS and OFF + * never count engagements. */ +static void adaptive(void) +{ + struct adreno_early_done e = fresh(ADRENO_EARLY_ADAPTIVE); + adreno_early_tick(&e, 10 * MSU, 1, 0); + adreno_early_arrival(&e, 12 * MSU, 1); + CHECK(e.engages == 1 && adreno_early_active(&e, 12 * MSU) && adreno_early_active(&e, 12 * MSU + HOLD - 1)); + CHECK(!adreno_early_active(&e, 12 * MSU + HOLD)); + adreno_early_tick(&e, 1000 * MSU, 1, 0); /* renewed while engaged: not a new engagement */ + adreno_early_arrival(&e, 1001 * MSU, 1); + CHECK(e.engages == 1 && adreno_early_active(&e, 1001 * MSU + HOLD - 1) && !adreno_early_active(&e, 1001 * MSU + HOLD)); + adreno_early_tick(&e, 5000 * MSU, 1, 0); /* after it ended: a new engagement */ + adreno_early_arrival(&e, 5001 * MSU, 1); + CHECK(e.engages == 2 && adreno_early_active(&e, 5002 * MSU)); + adreno_early_reset(&e); /* stop / mode set: nothing engaged, no tick */ + CHECK(!adreno_early_active(&e, 5002 * MSU) && e.last_tick == 0 && !e.tick_missed && e.mode == ADRENO_EARLY_ADAPTIVE); + struct adreno_early_done a = fresh(ADRENO_EARLY_ALWAYS); + adreno_early_tick(&a, 10 * MSU, 1, 0); + adreno_early_arrival(&a, 11 * MSU, 1); + CHECK(a.late == 1 && a.engages == 0); +} + +/* Windows: counters start over, the tick state and an engagement carry across. */ +static void windows(void) +{ + struct adreno_early_done e = fresh(ADRENO_EARLY_ADAPTIVE); + adreno_early_tick(&e, 10 * MSU, 1, 0); + adreno_early_arrival(&e, 11 * MSU, 1); + adreno_early_completed(&e); + adreno_early_tick(&e, 27 * MSU, 1, 0); + adreno_early_next_window(&e); + CHECK(!e.early && !e.late && !e.engages && !e.arrivals && !e.ahead && !e.arr[0] && !e.lat[0]); + CHECK(e.last_tick == 27 * MSU && e.tick_missed && adreno_early_active(&e, 30 * MSU)); +} + +/* The perf done line: at most ADRENO_PERF_LINE_MAX bytes at every cap, and the exact text of a known window. */ +static void perf_line(void) +{ + char line[512]; + struct adreno_early_done big; + memset(&big, 0xff, sizeof big); + int n = adreno_perf_line_done(line, sizeof line, ~0ull, &big, 1); + CHECK(n > 0 && n <= ADRENO_PERF_LINE_MAX); + printf("perf done line at caps: %d bytes\n", n); + struct adreno_early_done e = fresh(ADRENO_EARLY_ADAPTIVE); + adreno_early_tick(&e, 10 * MSU, 1, 0); + adreno_early_arrival(&e, 13 * MSU, 1); + adreno_early_completed(&e); + adreno_early_tick(&e, 27 * MSU, 1, 1); + adreno_early_arrival(&e, 36 * MSU, 1); + adreno_early_arrival(&e, 37 * MSU, 0); + n = adreno_perf_line_done(line, sizeof line, 42, &e, adreno_early_active(&e, 40 * MSU)); + CHECK(!strcmp(line, "AdrenoDisplay: perf done s=42 mode=2 on=1 done=1 late=1 eng=1 ahd=1 " + "arr=0/1/0/0/1/0/0/0/0 lat=0/1/0/0")); + printf("%s\n", line); +} + +int main(void) +{ + modes(); + arrivals(); + late_frames(); + adaptive(); + windows(); + perf_line(); + printf("{\"checks\":%u,\"failures\":%u}\n", checks, failures); + return failures ? 1 : 0; +} diff --git a/tools/test-present-early-done.py b/tools/test-present-early-done.py new file mode 100644 index 0000000..fe98585 --- /dev/null +++ b/tools/test-present-early-done.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Host test for --present-early-done: AdrenoDisplay completes a layer-0 swap right after its scanout copy +(gpu/adreno-kext/kext/src/AdrenoDisplayEarlyDone.h, -DADRENO_PRESENT_EARLY_DONE=1, profile desktop-early-done). No A16. + +- the header's unit test (tools/test-adreno-early-done.c, ASan/UBSan) passes, and fails on each deliberate mutant; +- the kext uses the feature only inside ADRENO_PRESENT_EARLY_DONE blocks, and the desktop-early-done profile differs from + desktop-backlight only in AdrenoDisplay.o (built when the private *26A428.h headers are present, else SKIP); +- cycle.py: without the new flags the boot arguments and the AuxKC choice are what they were; with them, the boot-args, + the refusals and the early-done collection (refused until pinned); +- the presentation replay: without the define every output is the define-off build's; with it, the earlier outputs are + unchanged and early-done.json has no failure and no cost failure (--no-replay skips this part, about 40 s). +""" +import argparse +import contextlib +import importlib.util +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import types +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +KEXT = ROOT / 'gpu/adreno-kext/kext' +HEADER = KEXT / 'src/AdrenoDisplayEarlyDone.h' +sys.path.insert(0, str(ROOT / 'tools')) +from cycle_lib import installed_fresh as fresh # noqa: E402 +from cycle_lib.common import Refused # noqa: E402 + +spec = importlib.util.spec_from_file_location('cycle_cli', ROOT / 'tools/cycle.py') +cli = importlib.util.module_from_spec(spec) +spec.loader.exec_module(cli) + +CC = ['cc', '-std=c11', '-Wall', '-Wextra', '-Werror', '-fsanitize=address,undefined'] +# (name, anchor in the header, replacement): each must make the unit test fail. +MUTANTS = [ + ('late counted twice', ' e->tick_missed = 0; /* one late frame per missed tick */\n', ''), + ('late window too wide', 'const int late = e->tick_missed && d < e->period;', + 'const int late = e->tick_missed && d < 2 * e->period;'), + ('idle ticks missed', 'e->tick_missed = had_frame && !served;', 'e->tick_missed = !served;'), + ('no renewal', ' e->until = now + e->hold;', ' if (!e->until) e->until = now + e->hold;'), + ('hold one tick long', 'e->until && now < e->until;', 'e->until && now <= e->until;'), + ('ahead not counted', 'if (!alone) { e->ahead++; return 0; }', 'if (!alone) return 0;'), + ('always is adaptive', 'if (e->mode == ADRENO_EARLY_ALWAYS) return 1;', ''), + ('arrival bins of 1 ms', 'two = 2 * e->ms;', 'two = e->ms;'), +] +REPLAY_DEFINES = ['-DADRENO_BEAM=1', '-DADRENO_PRESENT_CURSOR_STAGE=1', '-DADRENO_LATCH=1', '-DADRENO_PRESENT_SYNC=1', + '-DADRENO_PIPELINE=1', '-DADRENO_PRESENT_RELATCH=1'] +RUN_REPLAY = True + + +def run_unit(header=None, out=None): + with tempfile.TemporaryDirectory() as d: + binary = Path(d) / 'early-done' + extra = [f'-DEARLY_DONE_HEADER="{header}"'] if header else [] + build = subprocess.run(CC + extra + [str(ROOT / 'tools/test-adreno-early-done.c'), '-o', str(binary)], + capture_output=True, text=True) + if build.returncode: + return 'build', build.stderr + r = subprocess.run([str(binary)], capture_output=True, text=True, cwd=ROOT / 'tools') + return r.returncode, r.stdout + r.stderr + + +def scope_violations(path, pattern, define): + """Lines matching pattern outside every '#if...define' block (or in its #else).""" + stack, bad = [], [] + for n, line in enumerate(path.read_text().split('\n'), 1): + s = line.strip() + if re.match(r'#\s*if', s): + stack.append([define in s and not s.startswith('#ifndef'), s]) + continue + if re.match(r'#\s*(else|elif)', s) and stack: + stack[-1][0] = False + continue + if re.match(r'#\s*endif', s) and stack: + stack.pop() + continue + if s.startswith('*') or s.startswith('/*') or s.startswith('//'): + continue + code = re.sub(r'//.*|/\*.*?(\*/|$)', '', s) + if re.search(pattern, code) and not any(on for on, _ in stack) and not re.match(r'#\s*define', s): + bad.append(f'{path.relative_to(ROOT)}:{n}: {s}') + return bad + + +class Header(unittest.TestCase): + def test_unit_passes(self): + code, out = run_unit() + self.assertEqual(code, 0, out) + self.assertIn('"failures":0', out) + + def test_mutants_fail(self): + text = HEADER.read_text() + for name, old, new in MUTANTS: + with self.subTest(name), tempfile.TemporaryDirectory() as d: + self.assertEqual(text.count(old), 1, f'{name}: anchor not found once') + mutant = Path(d) / 'AdrenoDisplayEarlyDone.h' + mutant.write_text(text.replace(old, new)) + code, out = run_unit(mutant) + self.assertNotEqual(code, 0, f'{name}: the unit test passed a mutant\n{out}') + + +class Kext(unittest.TestCase): + def test_scope(self): + bad = [] + for path in sorted((KEXT / 'src').rglob('*')): + if path.suffix in ('.cpp', '.h') and path.name != 'AdrenoDisplayEarlyDone.h': + bad += scope_violations(path, r'\bfEarly\b|\bfScreenHeld\b|adreno_early_|earlyDoneNowGated|' + r'AdrenoDisplayEarlyDone\.h', 'ADRENO_PRESENT_EARLY_DONE') + self.assertEqual(bad, []) + + def test_profile(self): + text = (KEXT / 'profiles/desktop-early-done.sh').read_text() + self.assertIn('. "$D/profiles/desktop-backlight.sh"', text) + self.assertEqual(re.findall(r'^U_\w+=.*$', text, re.M), + ['U_AdrenoDisplay="$U_AdrenoDisplay -DADRENO_PRESENT_EARLY_DONE=1"']) + + def test_profile_build(self): + if not list((KEXT / 'include').glob('*26A428.h')) or not shutil.which('xcrun'): + self.skipTest('SKIP: the private *26A428.h headers or Xcode are not present') + with tempfile.TemporaryDirectory() as d: + outs = {} + for profile in ('desktop-backlight', 'desktop-early-done'): + out = Path(d) / profile + r = subprocess.run(['sh', str(KEXT / 'build.sh')], capture_output=True, text=True, + env=dict(os.environ, OUT=str(out), PROFILE=profile, ZERO_AR_DATE='1')) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + outs[profile] = out + a, b = outs['desktop-backlight'] / 'obj', outs['desktop-early-done'] / 'obj' + differ = sorted(o.name for o in a.iterdir() if o.read_bytes() != (b / o.name).read_bytes()) + self.assertEqual(differ, ['AdrenoDisplay.o']) + da = (outs['desktop-backlight'] / 'DEFINES.txt').read_text().split('\n') + db = (outs['desktop-early-done'] / 'DEFINES.txt').read_text().split('\n') + changed = [(x, y) for x, y in zip(da, db) if x != y] + self.assertEqual(len(changed), 2) # the profile name and AdrenoDisplay's line + self.assertTrue(changed[1][1].startswith('AdrenoDisplay: ') and + changed[1][1] == changed[1][0] + ' -DADRENO_PRESENT_EARLY_DONE=1') + + +def chain(**kw): + base = dict(variant='fast', input=True, input_touchpad=True, input_ptp=True, present_dartpool=True, + present_resident=True, present_beam=True, present_va_reclaim=True, gpu_va_wide=True, + present_latch=True, edp_backlight=True) + base.update(kw) + return types.SimpleNamespace(**base) + + +class Cycle(unittest.TestCase): + def test_default_unchanged(self): + args = chain() + self.assertEqual(fresh.selected(args)[1], fresh.INPUT_AUXKC_DESKTOP_BACKLIGHT) + self.assertNotIn('adreno_present_early', fresh.boot_addition(args)) + next3 = json.loads((ROOT.parent.parent / 'run/desktop-run-args-next3.json').read_text()) \ + if (ROOT.parent.parent / 'run/desktop-run-args-next3.json').is_file() else None + if next3: + a = cli.parse(next3 + ['--check']) + self.assertNotIn('adreno_present_early', fresh.boot_addition(a)) + self.assertEqual(fresh.selected(a)[1], fresh.INPUT_AUXKC_DESKTOP_BACKLIGHT) + + def test_boot_args(self): + for mode in (0, 1, 2): + self.assertTrue(fresh.boot_addition(chain(present_early_done=mode)).endswith( + f' adreno_present_early_done={mode}')) + self.assertTrue(fresh.boot_addition(chain(present_early_done=2, present_early_hold_ms=5000)).endswith( + ' adreno_present_early_done=2 adreno_present_early_hold_ms=5000')) + self.assertIn('adreno_present_early_done=0', fresh.boot_addition(chain(present_early_done=0, + present_hold='on'))) + + def test_refusals(self): + for bad in (dict(present_early_done=3), dict(present_early_done=1, present_hold='on'), + dict(present_early_done=2, present_hold='on'), dict(present_early_done=1, present_early_hold_ms=500), + dict(present_early_hold_ms=500), dict(present_early_done=2, present_early_hold_ms=99), + dict(present_early_done=2, present_early_hold_ms=10001), dict(present_early_done=1, edp_backlight=False)): + with self.subTest(bad), self.assertRaises(Refused): + fresh.boot_addition(chain(**bad)) + with self.assertRaises(Refused): # without the --edp-backlight chain: no collection has it + fresh.selected(types.SimpleNamespace(variant='fast', input=True, present_early_done=1)) + + def test_collection(self): + for kw in (dict(present_early_done=0), dict(present_early_done=2), dict(gpu_coherent=True)): + with self.subTest(kw): + if fresh.INPUT_AUXKC_DESKTOP_EARLY_DONE is None: + with self.assertRaises(Refused): + fresh.selected(chain(**kw)) + else: + self.assertEqual(fresh.selected(chain(**kw))[1:3], (fresh.INPUT_AUXKC_DESKTOP_EARLY_DONE, + fresh.INPUT_AUXKC_DESKTOP_EARLY_DONE_SHA)) + # --gpu-coherent without --edp-backlight keeps its collection (the latch chain's) + self.assertEqual(fresh.selected(chain(edp_backlight=False, gpu_coherent=True))[1], + fresh.INPUT_AUXKC_PTP_DARTPOOL_RESIDENT_BEAM_VA_WIDE_LATCH) + + def test_cli(self): + base = ['installed-fresh', '--prefix', 'x', '--variant', 'fast', '--check', '--edp-backlight'] + a = cli.parse(base + ['--present-early-done', '2', '--present-early-hold-ms', '3000']) + self.assertEqual((a.present_early_done, a.present_early_hold_ms), (2, 3000)) + self.assertIsNone(cli.parse(base).present_early_done) + with self.assertRaises(SystemExit), open(os.devnull, 'w') as quiet, contextlib.redirect_stderr(quiet): + cli.parse(base + ['--present-early-done', '5']) + + +class Replay(unittest.TestCase): + def test_replay(self): + if not RUN_REPLAY: + self.skipTest('SKIP: --no-replay') + with tempfile.TemporaryDirectory() as d: + outs = {} + for name, extra in (('off', []), ('on', ['-DADRENO_PRESENT_EARLY_DONE=1'])): + binary = Path(d) / f'replay-{name}' + build = subprocess.run(['xcrun', 'clang', '-std=c11', '-O2', '-Wall', '-Wextra', '-Werror'] + + REPLAY_DEFINES + extra + ['tools/replay-adreno-presentation.c', '-o', str(binary)], + cwd=ROOT, capture_output=True, text=True) + self.assertEqual(build.returncode, 0, build.stderr) + out = Path(d) / name + out.mkdir() + r = subprocess.run([str(binary), str(out)], capture_output=True, text=True) + self.assertEqual(r.returncode, 0, r.stdout[-2000:] + r.stderr[-2000:]) + outs[name] = (out, r.stdout) + off, on = outs['off'][0], outs['on'][0] + for j in sorted(off.glob('*.json')): + self.assertEqual(j.read_bytes(), (on / j.name).read_bytes(), f'{j.name} changed with the define') + self.assertFalse((off / 'early-done.json').exists()) + result = json.loads((on / 'early-done.json').read_text()) + self.assertEqual((result['failures'], result['cost_failures']), (0, 0)) + self.assertGreater(result['checks'], 0) + for line in outs['on'][1].split('\n'): + if line.startswith('early-done ') and 'arrivals' not in line: + print(line) + + +if __name__ == '__main__': + ap = argparse.ArgumentParser(add_help=False) + ap.add_argument('--no-replay', action='store_true') + known, rest = ap.parse_known_args() + RUN_REPLAY = not known.no_replay + unittest.main(argv=[sys.argv[0]] + rest) From 00cecdf061756192608cf564efb41be2df2ec13a Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:54:41 -0400 Subject: [PATCH 5/6] tools: summarize a run's present pacing windows tools/summarize-present-pacing.py reduces a console's AdrenoDisplay perf windows to busy and quiet groups: copies per second, the copy interval buckets, ready / missed / true-miss ticks, per-IB GPU, submission, cache and preparation time, the copy, tick lateness and q, the perf done line's early completions and arrival histograms, and with --report the GPU's time per clock (DVFSREP1). Its host test runs it on a synthetic console. --- tools/summarize-present-pacing.py | 154 +++++++++++++++++++++++++ tools/test-summarize-present-pacing.py | 66 +++++++++++ 2 files changed, 220 insertions(+) create mode 100644 tools/summarize-present-pacing.py create mode 100644 tools/test-summarize-present-pacing.py diff --git a/tools/summarize-present-pacing.py b/tools/summarize-present-pacing.py new file mode 100644 index 0000000..e7f59ad --- /dev/null +++ b/tools/summarize-present-pacing.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Summarize a run's AdrenoDisplay perf windows: frame pacing and where a busy window's time went. Host only. + + python3 tools/summarize-present-pacing.py build/PREFIX/console.txt [--report build/PREFIX/report.bin] [--json] + +A perf window is the set of "AdrenoDisplay: perf s=" lines one perfLineGated call prints (600 layer-0 +copies, AdrenoDisplay/stats.cpp). Windows are split into busy (at least --busy-ibs completed IBs, default 200: about +20 IBs a second) and quiet ones, and each group is reduced to: +- copies per second (median, min) and the copy intervals by the pace line's buckets (<= 17, <= 20, <= 34, <= 50, + > 50 ms; <= 20 ms is on time, the tick firing late; 20-34 ms is one refresh shown twice; more is worse); +- the ticks: ready (a layer-0 copy started in the tick), missed (a frame was on screen, nothing new copied), idle; + missed less the relatch's copies (the head the tick deferred, copied into the same frame) is the share of refreshes + WindowServer did not deliver in time ("true misses"); +- per IB: GPU time (the monitor's poll ticks at 19.2 MHz), the kext's submission time with the lock held, its cache + maintenance time and bytes, its resource preparations; the copy time, the tick's lateness and q (swap to copy); +- with the early-completion kext (the perf done line): completions at the copy, late frames, engagements, and the + arrival and lateness histograms summed over the group. +With --report, the GPU's time per clock from the DVFSREP1 record (tools/dvfs_report.py) is added.""" +import argparse +import collections +import json +import re +import statistics +import sys +from pathlib import Path + +HOST_HZ = 19_200_000 # the monitor's IB tick counter (gtk) +NEED = ('why', 'pace', 'map', 'gpu', 'pipe', 'sync', 'vsync') + + +def windows(text): + w = collections.defaultdict(dict) + for line in text.split('\n'): + m = re.search(r'AdrenoDisplay: perf why=(\w+) s=(\d+) (.*)', line) + if m: + w[int(m.group(2))]['why'] = dict(re.findall(r'(\w+)=(\S+)', m.group(3)), kind=m.group(1)) + continue + m = re.search(r'AdrenoDisplay: perf (\w+) s=(\d+) (.*)', line) + if m: + w[int(m.group(2))][m.group(1)] = dict(re.findall(r'(\w+)=(\S+)', m.group(3))) + return w + + +def ints(text): + return [int(x) for x in text.split('/')] + + +def window_row(s, r): + """One periodic window's numbers, or None (an incomplete or non-periodic window).""" + if not all(k in r for k in NEED) or r['why'].get('kind') != 'periodic': + return None + p, mp, g, pi, sy, v = (r[k] for k in ('pace', 'map', 'gpu', 'pipe', 'sync', 'vsync')) + pp, tk = ints(p['pp']), ints(p['tk']) + span = sum(pp) * int(p['mean']) / 1e6 + ib = int(mp['ib']) + gtk, sub, cache, prep = ints(g['gtk']), ints(g['sub_us']), ints(g['cache']), ints(g['prep']) + relatch = r.get('relatch', {}) + row = dict(s=s, pps=float(p['pps']), span=span, ib=ib, buckets=pp, ticks=tk[0], ready=tk[1], missed=tk[2], idle=tk[3], + relatched=int(relatch.get('go', 0)), defer=int(sy['defer']), + gpu_ms_ib=gtk[0] / HOST_HZ * 1e3 / ib if ib else 0.0, + gpu_busy=gtk[0] / HOST_HZ / span if span else 0.0, + sub_ms_ib=sub[0] / 1e3 / ib if ib else 0.0, cache_ms_ib=cache[2] / 1e3 / ib if ib else 0.0, + cache_mib_ib=cache[1] / ib if ib else 0.0, prep_ms_ib=prep[1] / 1e3 / ib if ib else 0.0, + copy_us=int(r['why']['mean_us']), late_us=ints(v['grid'])[0], q_us=int(p['q'])) + if 'done' in r: + d = r['done'] + row['done'] = dict(mode=int(d['mode']), on=int(d['on']), early=int(d['done']), late=int(d['late']), + engages=int(d['eng']), ahead=int(d['ahd']), arr=ints(d['arr']), lat=ints(d['lat'])) + return row + + +def group(rows): + if not rows: + return None + ticks = sum(r['ticks'] for r in rows) or 1 + buckets = [sum(r['buckets'][i] for r in rows) for i in range(5)] + intervals = sum(buckets) or 1 + med = lambda k: round(statistics.median(r[k] for r in rows), 3) + out = dict(windows=len(rows), pps_median=med('pps'), pps_min=min(r['pps'] for r in rows), + ticks_per_s=round(ticks / sum(r['span'] for r in rows), 2), + ready_pct=round(100 * sum(r['ready'] for r in rows) / ticks, 1), + missed_pct=round(100 * sum(r['missed'] for r in rows) / ticks, 1), + true_miss_pct=round(100 * sum(r['missed'] - r['relatched'] for r in rows) / ticks, 1), + deferred_pct=round(100 * sum(r['defer'] for r in rows) / ticks, 1), + intervals_pct=[round(100 * b / intervals, 1) for b in buckets], + over34_pct=round(100 * (buckets[3] + buckets[4]) / intervals, 1), + ibs_per_s=round(sum(r['ib'] for r in rows) / sum(r['span'] for r in rows), 1)) + for k in ('gpu_ms_ib', 'gpu_busy', 'sub_ms_ib', 'cache_ms_ib', 'cache_mib_ib', 'prep_ms_ib', 'copy_us', 'late_us', + 'q_us'): + out[k + '_median'] = med(k) + done = [r['done'] for r in rows if 'done' in r] + if done: + out['done'] = dict(modes=sorted({d['mode'] for d in done}), engaged_windows=sum(1 for d in done if d['on']), + early=sum(d['early'] for d in done), late=sum(d['late'] for d in done), + engages=sum(d['engages'] for d in done), ahead=sum(d['ahead'] for d in done), + arrival_2ms_bins=[sum(d['arr'][i] for d in done) for i in range(len(done[0]['arr']))], + late_bins_2_4_8_more=[sum(d['lat'][i] for d in done) for i in range(len(done[0]['lat']))]) + return out + + +def summarize(text, busy_ibs=200): + rows = [x for x in (window_row(s, r) for s, r in sorted(windows(text).items())) if x] + return dict(busy=group([r for r in rows if r['ib'] >= busy_ibs]), + quiet=group([r for r in rows if r['ib'] < busy_ibs]), busy_ibs=busy_ibs) + + +def gpu_clocks(report): + sys.path.insert(0, str(Path(__file__).resolve().parent)) + import dvfs_report + raw = Path(report).read_bytes() + decoded = dvfs_report.decode_block(raw[dvfs_report.OFFSET:dvfs_report.OFFSET + 8 * 190]) # as its main reads it + gpu = decoded.get('gpu', {}) if isinstance(decoded, dict) else {} + keep = ('status', 'ceiling_mhz', 'seconds_at_mhz', 'mean_mhz', 'errors', 'thermal') + return {k: gpu[k] for k in keep if k in gpu} + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('console', type=Path) + ap.add_argument('--report', type=Path) + ap.add_argument('--busy-ibs', type=int, default=200) + ap.add_argument('--json', action='store_true') + a = ap.parse_args() + out = summarize(a.console.read_text(errors='replace'), a.busy_ibs) + if a.report: + out['gpu_clock'] = gpu_clocks(a.report) + if a.json: + print(json.dumps(out, indent=1)) + return + for name in ('busy', 'quiet'): + g = out[name] + if not g: + print(f'{name}: no windows') + continue + print(f"{name}: {g['windows']} windows, copies/s median {g['pps_median']} (min {g['pps_min']}), ticks/s " + f"{g['ticks_per_s']}, IBs/s {g['ibs_per_s']}; ticks ready {g['ready_pct']}% missed {g['missed_pct']}% " + f"(true misses {g['true_miss_pct']}%), deferred {g['deferred_pct']}%; intervals <=17/20/34/50/>50 ms " + f"{'/'.join(str(x) for x in g['intervals_pct'])}%, over 34 ms {g['over34_pct']}%") + print(f" per IB: GPU {g['gpu_ms_ib_median']} ms (GPU busy {g['gpu_busy_median']}), submit {g['sub_ms_ib_median']} ms, " + f"cache {g['cache_ms_ib_median']} ms / {g['cache_mib_ib_median']} MiB, preparation {g['prep_ms_ib_median']} ms; " + f"copy {g['copy_us_median']} us, tick late {g['late_us_median']} us, q {g['q_us_median']} us") + if 'done' in g: + d = g['done'] + print(f" early completion: modes {d['modes']}, engaged at {d['engaged_windows']} window ends, {d['early']} " + f"completions at the copy, {d['late']} late frames ({d['engages']} engagements), {d['ahead']} arrivals " + f"behind another; arrivals by 2 ms {d['arrival_2ms_bins']}, lateness <2/<4/<8/more " + f"{d['late_bins_2_4_8_more']}") + if 'gpu_clock' in out: + print(f"gpu clock: {json.dumps(out['gpu_clock'])}") + + +if __name__ == '__main__': + main() diff --git a/tools/test-summarize-present-pacing.py b/tools/test-summarize-present-pacing.py new file mode 100644 index 0000000..f791ef9 --- /dev/null +++ b/tools/test-summarize-present-pacing.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Host test for tools/summarize-present-pacing.py on a synthetic console: two complete periodic windows (one busy, one +quiet, the quiet one with the early-completion perf done line), one incomplete window and one reject window, which +must both be left out.""" +import importlib.util +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location('summarize', ROOT / 'tools/summarize-present-pacing.py') +tool = importlib.util.module_from_spec(spec) +spec.loader.exec_module(tool) + + +def window(s, ib, pp, mean, tk, gtk, sub, cache, prep, go, defer, done=None, why='periodic'): + lines = [ + f'AdrenoDisplay: perf why={why} s={s} presents=600 copies=600 win=600 mean_us=2000 max_us=3000 all_mean_us=2000 ' + 'all_max_us=9000', + f'AdrenoDisplay: perf pace s={s} pps=50.0 pp={pp} mean={mean} p95=34 tk={tk} late=700 off=0 gpu=1/2000 q=6000', + f'AdrenoDisplay: perf vsync s={s} period_ns=16666666 grid=900/3000 early=1/100 skip=0 rs=0/0 lead=900 arm=1/0 ' + 'unarmed=0 ticks=1', + f'AdrenoDisplay: perf sync s={s} defer={defer} swaps={defer} forced=0 max_us=2000 marks=1 busy=1 ovf=0 stale=0 unm=0', + f'AdrenoDisplay: perf pipe s={s} on=3 fl={ib} ovl=0 imm=0 sync=0 fold=0 drain=0 tmr=0 polls=0 spin_us=0 ' + 'gap_us=0 max_ms=1 fail=0', + f'AdrenoDisplay: perf relatch s={s} on=7 arm={go} go={go} late=0 gone=0 gate=0 polls=0 ovt=0 lead=0/0 wait=0 ' + 'rel=0/0/0/0/0/0/0/0 ef=0/0/0 unm=0', + f'AdrenoDisplay: perf map s={s} ins=0 pg=0/0/0 clr=0 sfree=0 pool=0/0/0/0 va=0/0/0/0 ib={ib}', + f'AdrenoDisplay: perf gpu s={s} gtk={gtk}/0 sub_us={sub}/0 prep={prep} unw=0/0 cache={cache} res=0/0/0/0 ' + 'mib=0/0', + ] + if done: + lines.append(f'AdrenoDisplay: perf done s={s} {done}') + return lines + + +class Summary(unittest.TestCase): + def test_groups(self): + text = '\n'.join( + window(1, 600, '300/150/100/40/10', 20000, '600/400/180/20', 19200000, 1200000, '60000/60000/1800000', + '120000/600000', 60, 70) + + window(2, 30, '500/90/10/0/0', 16900, '600/590/8/2', 192000, 30000, '3000/1000/30000', '3000/60000', 0, 1, + done='mode=2 on=1 done=5 late=2 eng=1 ahd=3 arr=1/2/3/4/5/6/7/8/9 lat=1/0/1/0') + + window(3, 600, '1/0/0/0/0', 1, '1/1/0/0', 1, 1, '1/1/1', '1/1', 0, 0)[:3] + # incomplete: left out + window(4, 600, '1/0/0/0/0', 1, '1/1/0/0', 1, 1, '1/1/1', '1/1', 0, 0, why='reject')) + out = tool.summarize(text, 200) + busy, quiet = out['busy'], out['quiet'] + self.assertEqual((busy['windows'], quiet['windows']), (1, 1)) + self.assertEqual(busy['intervals_pct'], [50.0, 25.0, 16.7, 6.7, 1.7]) + self.assertEqual(busy['over34_pct'], 8.3) + self.assertEqual((busy['ready_pct'], busy['missed_pct'], busy['true_miss_pct']), (66.7, 30.0, 20.0)) + self.assertAlmostEqual(busy['gpu_ms_ib_median'], 1.667, places=3) # 1 s of host ticks over 600 IBs + self.assertAlmostEqual(busy['sub_ms_ib_median'], 2.0, places=3) + self.assertAlmostEqual(busy['cache_ms_ib_median'], 3.0, places=3) + self.assertAlmostEqual(busy['cache_mib_ib_median'], 100.0, places=3) + self.assertAlmostEqual(busy['prep_ms_ib_median'], 1.0, places=3) + self.assertNotIn('done', busy) + self.assertEqual(quiet['done'], dict(modes=[2], engaged_windows=1, early=5, late=2, engages=1, ahead=3, + arrival_2ms_bins=[1, 2, 3, 4, 5, 6, 7, 8, 9], late_bins_2_4_8_more=[1, 0, 1, 0])) + + def test_empty(self): + self.assertEqual(tool.summarize('nothing here'), dict(busy=None, quiet=None, busy_ibs=200)) + + +if __name__ == '__main__': + unittest.main() From b2f1fa031fefe3d4174c4e81446cf970ecde5224 Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:56:03 -0400 Subject: [PATCH 6/6] tools: replay next2c's sporadic frames with coherent maintenance The quiet desktop's sporadic heavy frames (next2c: 3 IBs a second, 57.9 copies/s) with b12v23's coherent maintenance: the kext time per IB falls from 13 ms to 6.5-8.5 ms (the preparation stays), and the model then delivers every refresh (59.5 copies/s) in every mode, so the adaptive mode never engages there. --- tools/replay-adreno-early-done.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/replay-adreno-early-done.c b/tools/replay-adreno-early-done.c index 06318c0..d69da94 100644 --- a/tools/replay-adreno-early-done.c +++ b/tools/replay-adreno-early-done.c @@ -17,7 +17,8 @@ * - busy_coherent: the same frames with b12v23's maintenance (--gpu-coherent: 1.05 ms and 40 MiB per IB); * - quiet: the cursor moving over an idle desktop, a layer-0 swap every refresh and no IB (b12v20/b12v21: 59.8/s); * - sporadic: next2c's idle windows (57.9/s): the quiet frames plus 5% of frames with an IB whose kext time is 13 ms - * (TO_DEVICE 7 ms over 208 MiB and 6 ms of preparation) and 3.4 ms of GPU. + * (TO_DEVICE 7 ms over 208 MiB and 6 ms of preparation) and 3.4 ms of GPU; + * - sporadic_coherent: the same with the coherent maintenance (the preparation stays: 6.5-8.5 ms in the kext). * The model has no guest-wide stalls, so its intervals over 34 ms stay under the hardware's (org2: 5.4%). */ static void early_done_scenarios(const char *dir) { @@ -41,6 +42,8 @@ static void early_done_scenarios(const char *dir) 0, 0, 0, 0, 0, {0, 0, 0}, {0, 0, 0}, {100, 0, 0}, 0, 0, 0, 0, 0, 0}, 0, 0, 0}, {{"sporadic", WS_SLOT, 2 * M, 3600 * US, 5, {3 * M, 3 * M, 3 * M}, {4 * M, 4 * M, 4 * M}, {100, 0, 0}, 12 * M, 14 * M, 1 * M, 2 * M, 0, {0, 0, 0}, {0, 0, 0}, {100, 0, 0}, 0, 0, 0, 0, 0, 0}, 0, 0, 0}, + {{"sporadic_coherent", WS_SLOT, 2 * M, 3600 * US, 5, {3 * M, 3 * M, 3 * M}, {4 * M, 4 * M, 4 * M}, {100, 0, 0}, + 6500 * US, 8500 * US, 300 * US, 700 * US, 0, {0, 0, 0}, {0, 0, 0}, {100, 0, 0}, 0, 0, 0, 0, 0, 0}, 0, 0, 0}, }; const unsigned ncases = sizeof cases / sizeof cases[0]; for (unsigned i = 0; i < ncases; i++) {