summaryrefslogtreecommitdiffstats
path: root/kernel
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-31 07:21:28 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-31 07:21:28 -0400
commitf8f0a86a07949f913e546de4459c779a9ccbc3f3 (patch)
tree01d80fcfa750aa7bfd49aa3ea031c9aca75b3d00 /kernel
parentdf072897fef8421c7f41d7a9ebc9f3415210b402 (diff)
downloadopen-iov-f8f0a86a07949f913e546de4459c779a9ccbc3f3.tar.gz
open-iov-f8f0a86a07949f913e546de4459c779a9ccbc3f3.zip
vgsp+diag: SA-RAM table captured unbounded (entry[64], sema-mme-sa log), surf geometry accessor + completeness recs, fill seeds RT candidate 0 full span (<=1024 pages, completeness only on fully-resolved run); MME slot stub (called-but-uncaptured slots end-noop IR 0xC0, sema_gl_mme_sa_stub=1), reloc prefers tracked rec bounce VA; pin family += 0x4200000 - 0x238c wall root-caused (capture truncated buffer full=28 words=512, SA[0x22]=0 jump IR 0; sim proves slot-0 emits EMIT 0x238c=0x20020); fixes never executed (setup-push read lost, unreadable=1 first8=00040000 00001000); next: trustworthy pagemap resolve or accept ipin first-chunk - SESSION 153 r1117-r1120
Diffstat (limited to 'kernel')
-rw-r--r--kernel/gsp_phys_booter.c159
-rw-r--r--kernel/gsp_sema_service.c191
-rw-r--r--kernel/gsp_sema_service.h28
3 files changed, 375 insertions, 3 deletions
diff --git a/kernel/gsp_phys_booter.c b/kernel/gsp_phys_booter.c
index ad8f9aa..cf6e33c 100644
--- a/kernel/gsp_phys_booter.c
+++ b/kernel/gsp_phys_booter.c
@@ -17204,6 +17204,26 @@ MODULE_PARM_DESC(sema_gl_mme_prelude,
"Preplay the captured client MME context-setup runs once per capture generation ahead of the first 3D window replay (default 1)");
/*
+ * SESSION 153-CONTINUED-63: MME START-ADDRESS RAM completion + stub knob.
+ * The captured SA table (svc_mme_sa_*) may be incomplete (the prelude
+ * buffer is capped at 512 words); every CALLED slot the table does not
+ * cover is loaded with a harmless end-noop stub at a fixed IR address so
+ * the call becomes a no-op instead of a jump to IR 0 with garbage state
+ * (the r1116 0x238c poison). Default 1; A/B with 0.
+ */
+#define GSP_PHYS_RMCALL_GR_MME_STUB_PC 0xC0u
+static bool gsp_phys_rmcall_gr_sa_stub = true;
+module_param_named(sema_gl_mme_sa_stub, gsp_phys_rmcall_gr_sa_stub, bool,
+ 0644);
+MODULE_PARM_DESC(sema_gl_mme_sa_stub,
+ "Load a harmless end-noop stub into every MME macro slot the window calls but the captured START-ADDRESS RAM table does not cover (default 1; the r1116 0x238c lane poison class)");
+
+static inline bool gsp_sema_gr_sa_stub_value(void)
+{
+ return READ_ONCE(gsp_phys_rmcall_gr_sa_stub);
+}
+
+/*
* SESSION 153-CONTINUED-61: generation of the MME prelude already emitted
* into the engine context (0 = none). The r1110 evidence: the prelude was
* emitted once on the first beef launch (gen=1, 512 words), that launch
@@ -17366,17 +17386,85 @@ static u32 gsp_phys_rmcall_build_gr_launch(u32 *pb,
bool emit_prelude = false;
const u32 *pl = NULL;
u32 pl_n = 0, pl_gen = 0;
+ /*
+ * SESSION 153-CONTINUED-63: MME START-ADDRESS RAM completion.
+ * The prelude capture is capped at 512 words, so the client's
+ * SA table (macro slot -> IR address) may be incomplete; on a
+ * fresh context the missing slots read 0 and CALL_MME_MACRO(j)
+ * jumps to IR 0 - the slot-0 macro - which with the caller's
+ * args emitted LOAD_CONSTANT_BUFFER_OFFSET <- 0x20020 and
+ * poisoned the lane (r1116; verified offline with the MME64
+ * simulator). Stubbing every CALLED slot the capture's SA
+ * table does not cover with a harmless end-noop makes the
+ * unknown call a no-op instead of a lane-killer. The called
+ * slots come from the window being emitted (IMMD writes to
+ * 0x3800+j*8 and INC/ONEINC runs writing any 0x3800..0x39f8
+ * method, the .Macro half of each pair).
+ */
+ u32 sa_ent[64];
+ u64 sa_mask = 0;
+ u32 sa_stub[24];
+ u32 sa_nstub = 0, sa_block = 0;
+
+ if (gsp_sema_gr_sa_stub_value() && win3d && win3d_n) {
+ u32 so = 0;
+
+ sa_mask = gsp_sema_mme_sa_get(sa_ent, 64);
+ while (so < win3d_n && sa_nstub < 24u) {
+ u32 h = win3d[so];
+ u32 sop = (h >> 29) & 7u;
+ u32 scnt = (h >> 16) & 0x1fffu;
+ u32 sspan = 1u;
+ u32 k, slo, shi;
+
+ if ((sop == 1u || sop == 3u || sop == 5u) &&
+ scnt && so + 1u + scnt <= win3d_n) {
+ sspan = 1u + scnt;
+ slo = 0;
+ shi = scnt - 1u;
+ } else if (sop == 4u) {
+ slo = 0;
+ shi = 0;
+ } else {
+ so++;
+ continue;
+ }
+ for (k = slo; k <= shi && sa_nstub < 24u; k++) {
+ u32 mthd = ((h & 0xfffu) << 2) + 4u * k;
+ u32 j, t;
+ bool have = false;
+
+ if (mthd < 0x3800u || mthd >= 0x3a00u ||
+ (mthd & 7u))
+ continue;
+ j = (mthd - 0x3800u) / 8u;
+ if (j >= 64u)
+ continue;
+ if (sa_mask & (1ull << j))
+ continue;
+ for (t = 0; t < sa_nstub; t++)
+ if (sa_stub[t] == j)
+ have = true;
+ if (!have)
+ sa_stub[sa_nstub++] = j;
+ }
+ so += sspan;
+ }
+ if (sa_nstub)
+ sa_block = 5u + 3u * sa_nstub;
+ }
if (READ_ONCE(gsp_phys_rmcall_gr_mme_prelude)) {
pl = gsp_sema_mme_prelude_get(&pl_n, &pl_gen);
if (pl && pl_n && pl_gen &&
pl_gen != READ_ONCE(gsp_phys_gr_mme_prelude_gen)) {
- if (win3d_n + 2u + 8u + pl_n <=
+ if (win3d_n + 2u + 8u + pl_n + sa_block <=
GSP_PHYS_RMCALL_GR_METHOD_WORDS) {
emit_prelude = true;
} else {
- pr_info("vgsp-gr: MME-PRELUDE deferred (window=%u prelude=%u exceeds block; replaying window alone) h=0x%x\n",
- win3d_n, pl_n, req->h_object);
+ pr_info("vgsp-gr: MME-PRELUDE deferred (window=%u prelude=%u sa=%u exceeds block; replaying window alone) h=0x%x\n",
+ win3d_n, pl_n, sa_block,
+ req->h_object);
}
}
}
@@ -17436,6 +17524,40 @@ static u32 gsp_phys_rmcall_build_gr_launch(u32 *pb,
}
}
/*
+ * SESSION 153-CONTINUED-63: emit the SA-RAM completion computed
+ * above - a one-instruction end-noop stub in the IR plus one
+ * ONE_INC 0x11c run loading [slot, stub] for every called slot
+ * the capture's SA table does not cover (see the sa_stub
+ * comment at the emit decision). Emitted with the prelude (same
+ * generation latch), so it lands once per capture generation
+ * and the context keeps it for every later window.
+ */
+ if (emit_prelude && sa_block) {
+ u32 t;
+
+ *p++ = ((u32)GSP_PHYS_RMCALL_METHOD_IMM << 29) |
+ (4u << 16) | (0x114u >> 2);
+ *p++ = GSP_PHYS_RMCALL_GR_MME_STUB_PC;
+ *p++ = 0u;
+ *p++ = 0u;
+ *p++ = 1u; /* end_next, no emits (harmless) */
+ /*
+ * Each [slot, stub] pair is its own INC 0x11c run: the
+ * pair methods are re-written per slot (a single longer
+ * run would walk onto 0x124 SetMmeShadowRamControl and
+ * beyond).
+ */
+ for (t = 0; t < sa_nstub; t++) {
+ *p++ = ((u32)GSP_PHYS_RMCALL_METHOD_IMM << 29) |
+ (2u << 16) | (0x11cu >> 2);
+ *p++ = sa_stub[t];
+ *p++ = GSP_PHYS_RMCALL_GR_MME_STUB_PC;
+ }
+ pr_info("vgsp-gr: MME-SA-STUB slots=%u words=%u h=0x%x push=0x%llx\n",
+ sa_nstub, sa_block, req->h_object,
+ req->push_va);
+ }
+ /*
* SESSION 153-CONTINUED-57: attr/pipeline state prelude (see the
* sema_gl_attr_prelude knob above). Emit the patched captured
* runs after the MME prelude and ahead of the window,
@@ -20017,6 +20139,37 @@ static bool gsp_phys_rmcall_gr_3d_reloc(u32 *up_w, u32 *lo_w,
if (!va || !page)
return false;
/*
+ * SESSION 153-CONTINUED-63: rec-over-alias preference. When the fill
+ * tracked the render target through the bounce lane (every page
+ * resolved into contiguous data recs), rewrite the operand to the
+ * rec's bounce VA so the engine's stores land where the retire wait
+ * can count them and the copy-out can ship them back to the guest's
+ * own pages. The completeness gate (gsp_sema_gr_surf_recs) makes a
+ * partial run impossible to point at. Runs before the alias so the
+ * identity alias (direct guest writes, invisible to the witness)
+ * cannot win for a tracked target.
+ */
+ {
+ u64 sva = 0;
+ u32 sn = 0;
+
+ if (gsp_sema_gr_surf_recs(&sva, &sn) &&
+ va >= sva && va < sva + ((u64)sn << 12)) {
+ for (i = 0; i < req->n_data; i++) {
+ if (req->data[i].va != page ||
+ !req->data[i].reloc_va)
+ continue;
+ sh = req->data[i].reloc_va |
+ (va & (GSP_PHYS_PAGE_SIZE - 1));
+ *lo_w = (*lo_w & 0x3u) |
+ ((u32)sh & 0xfffffffcu);
+ *up_w = (*up_w & ~0xffu) |
+ ((u32)(sh >> 32) & 0xffu);
+ return true;
+ }
+ }
+ }
+ /*
* SESSION 153-CONTINUED-27: a big surface (the X screen) is aliased
* as ONE contiguous run by the pre-pass; rewrite the operand to the
* alias base | offset. A one-page shadow relocation of it would
diff --git a/kernel/gsp_sema_service.c b/kernel/gsp_sema_service.c
index 4d32caa..856947f 100644
--- a/kernel/gsp_sema_service.c
+++ b/kernel/gsp_sema_service.c
@@ -14052,6 +14052,45 @@ module_param_named(sema_mme_prelude_cap, svc_mme_prelude_flag, uint, 0644);
MODULE_PARM_DESC(sema_mme_prelude_cap,
"Capture the client's MME context-setup method runs (0x114..0x124, 0x234, 0x550..0x568, 0x3400..0x34fc) from trusted beef pushes for the GR launch builder's one-time preplay (default 1)");
+/*
+ * SESSION 153-CONTINUED-63: the MME START-ADDRESS RAM table (macro slot ->
+ * IR instruction address), captured unbounded by the 512-word prelude
+ * buffer. The r1116 0x238c wall proved the replay's SA table was
+ * incomplete (the capture buffer filled with the first 11 macros' code and
+ * 28 further setup runs - the remaining slots - never entered the prelude):
+ * the window's CALL_MME_MACRO(0x22) then read SA[0x22] = 0 on the fresh
+ * context, the MME jumped to IR 0 (the slot-0 macro, simulating with the
+ * caller's args emits LOAD_CONSTANT_BUFFER_OFFSET <- 0x20020; verified
+ * with the offline MME64 simulator), and the class error poisoned the
+ * lane. The builder uses this table to complete the SA RAM it replays and
+ * to stub every called-but-unknown slot with a harmless end-noop.
+ */
+static u64 svc_mme_sa_present;
+static u32 svc_mme_sa_entry[64];
+static u32 svc_mme_sa_log;
+
+u64 gsp_sema_mme_sa_get(u32 *entries, u32 n)
+{
+ u32 i;
+
+ if (entries)
+ for (i = 0; i < n && i < 64u; i++)
+ entries[i] = svc_mme_sa_entry[i];
+ return READ_ONCE(svc_mme_sa_present);
+}
+
+static void svc_mme_sa_record(u32 slot, u32 entry)
+{
+ if (slot >= 64u)
+ return;
+ if (!(svc_mme_sa_present & (1ull << slot)) && svc_mme_sa_log < 64) {
+ svc_mme_sa_log++;
+ pr_info("sema-mme-sa: slot[%u] entry=0x%x\n", slot, entry);
+ }
+ svc_mme_sa_entry[slot] = entry;
+ svc_mme_sa_present |= 1ull << slot;
+}
+
static bool svc_mme_setup_run_addr(u32 addr)
{
return (addr >= 0x114u && addr <= 0x124u) || addr == 0x234u ||
@@ -14107,6 +14146,25 @@ static void svc_mme_prelude_capture(const u8 *buf, u32 len, u64 push_va,
break;
span = 1u + cnt;
}
+ /*
+ * SESSION 153-CONTINUED-63: record the START-ADDRESS RAM
+ * pairs independent of the prelude buffer's 512-word bound
+ * (see svc_mme_sa_present). The client's LoadMmeStartAddress
+ * Ram pair run is INC 0x11c with [slot, IR-address] pairs; a
+ * second run for the same slots overwrites (last wins), the
+ * same order the engine would see.
+ */
+ if (addr == 0x11cu && cnt >= 2u && span > 1u) {
+ u32 k;
+
+ for (k = 0; k + 1u < cnt; k += 2u) {
+ u32 sv, ev;
+
+ memcpy(&sv, buf + off + 4u + 4u * k, 4);
+ memcpy(&ev, buf + off + 8u + 4u * k, 4);
+ svc_mme_sa_record(sv, ev);
+ }
+ }
if (!svc_mme_setup_run_addr(addr)) {
off += span * 4u;
continue;
@@ -14396,6 +14454,15 @@ static u32 svc_surf_pre_gen;
static u64 svc_surf_pre_sig;
static u32 svc_surf_pre_log;
static u32 svc_surf_pre_flag = 1;
+/*
+ * SESSION 153-CONTINUED-63: the captured colour target's geometry, for the
+ * fill's render-target tracking (see svc_gr_data_fill and
+ * gsp_sema_gr_surf_recs): the fill seeds the surface base VA as a tracked
+ * data candidate so the engine's stores into the target become a retire
+ * witness and the copy-out ships them back to the guest's own pages.
+ */
+static u64 svc_surf_pre_va;
+static u32 svc_surf_pre_npages;
module_param_named(sema_surf_prelude_cap, svc_surf_pre_flag, uint, 0644);
MODULE_PARM_DESC(sema_surf_prelude_cap,
"Capture the last C597 colour-target bind (INC 0x800+j*0x40, fmt 0xe6) from trusted pushes so the GR launch builder can replay it ahead of 3D windows that carry no render target (default 1)");
@@ -14409,6 +14476,19 @@ const u32 *gsp_sema_surf_prelude_get(u32 *n, u32 *gen)
return svc_surf_pre_buf;
}
+bool gsp_sema_surf_va_get(u64 *va, u32 *npages)
+{
+ u32 n = READ_ONCE(svc_surf_pre_npages);
+
+ if (!n)
+ return false;
+ if (va)
+ *va = svc_surf_pre_va;
+ if (npages)
+ *npages = n;
+ return true;
+}
+
static void svc_surf_prelude_capture(const u8 *buf, u32 len)
{
u32 off = 0;
@@ -14458,6 +14538,8 @@ static void svc_surf_prelude_capture(const u8 *buf, u32 len)
memcpy(&svc_surf_pre_buf[k], buf + off + 4u * k, 4);
WRITE_ONCE(svc_surf_pre_n, n);
svc_surf_pre_sig = sig;
+ svc_surf_pre_va = va;
+ WRITE_ONCE(svc_surf_pre_npages, (u32)npages);
WRITE_ONCE(svc_surf_pre_gen, svc_surf_pre_gen + 1);
if (svc_surf_pre_log < 8) {
svc_surf_pre_log++;
@@ -29499,6 +29581,34 @@ static bool svc_gr_page_inline_covers(u64 page)
*/
#define SVC_GR_DATA_BOUNCE_MAX (GSP_PHYS_RMCALL_GR_DATA_BOUNCE_PAGES / 2u)
#define SVC_GR_DATA_BOUNCE_MAX_OLD 205u
+/*
+ * SESSION 153-CONTINUED-63: render-target page tracking (see
+ * gsp_sema_surf_va_get above). The captured colour target is seeded as a
+ * tracked data candidate so (a) GR-DEC n_data >= 1, (b) the retire wait
+ * counts the engine's stores into the target ("N/N pages changed") and
+ * (c) the copy-out ships the changed pages back to the guest's own screen
+ * pages. Bounded to one full 1280x720 RGB surface plus slack; the
+ * completeness record (va + n) arms the booter's rec-over-alias operand
+ * preference only when EVERY page resolved, so a partial span can never
+ * hand the engine a base pointer walking past the tracked run.
+ */
+#define SVC_GR_SURF_TRACK_MAX 1024u
+static u64 svc_gr_surf_rec_va;
+static u32 svc_gr_surf_rec_n;
+static u32 svc_gr_surf_rec_ok;
+static u32 svc_gr_surf_rec_log;
+
+bool gsp_sema_gr_surf_recs(u64 *va, u32 *n)
+{
+ if (!READ_ONCE(svc_gr_surf_rec_ok))
+ return false;
+ if (va)
+ *va = svc_gr_surf_rec_va;
+ if (n)
+ *n = svc_gr_surf_rec_n;
+ return true;
+}
+
static u32 svc_gr_data_log; /* m142: bounded per-page attribution log */
static u32 svc_gr_src_log; /* m150: bounded per-candidate source log */
@@ -30387,6 +30497,33 @@ static void svc_gr_data_fill(struct nv_vgpu_device *vgpu,
*/
n_cand = 0;
/*
+ * SESSION 153-CONTINUED-63: the captured colour target (the surface
+ * the replayed draw rasterizes into) becomes a tracked data
+ * candidate FIRST, so its pages are in the launch's data set even
+ * when the window/content scans fill every other slot (the beef
+ * windows' 24 scan candidates are all unresolvable junk). Odd: the
+ * copy-in stages the guest page into the bounce, the engine's
+ * stores land in the bounce, the retire wait sees them (GR-DEC
+ * n_data>=1 -> "N/N pages changed") and the copy-out writes the
+ * changed pages back to the guest's own pages. Completeness is
+ * recorded for the booter's rec-over-alias preference.
+ */
+ u64 surf_cand_va = 0;
+ u32 surf_cand_span = 0;
+ bool surf_cand_used = false;
+
+ WRITE_ONCE(svc_gr_surf_rec_ok, 0);
+ if (gsp_sema_surf_va_get(&surf_cand_va, &surf_cand_span)) {
+ if (surf_cand_span > SVC_GR_SURF_TRACK_MAX)
+ surf_cand_span = SVC_GR_SURF_TRACK_MAX;
+ if (surf_cand_va && !(surf_cand_va & (SEMA_PAGE_SIZE - 1)) &&
+ surf_cand_span && n_cand < SVC_GR_CAND_MAX) {
+ wins[n_cand] = surf_cand_va;
+ is_arg[n_cand] = false;
+ n_cand++;
+ }
+ }
+ /*
* m121: kernel-argument pointers first. The launched kernel's device
* pointers live in the param page (qmd_word 32, c[0x0]) at byte
* offsets >= 0x150 (m120: mail_kernel out@0x160, msg@0x168; vadd
@@ -30777,6 +30914,18 @@ static void svc_gr_data_fill(struct nv_vgpu_device *vgpu,
continue;
}
span = (base >= 0x40000000000ULL) ? per_ptr : 1u;
+ /*
+ * SESSION 153-CONTINUED-63: the render-target candidate keeps
+ * its full surface span (it is a low VA, so the generic span
+ * rule above would give it ONE page and the engine's raster
+ * walk would leave the tracked run at the first page
+ * crossing). surf_cand_used marks the pass so the tail can
+ * record completeness only for the run that actually filled.
+ */
+ if (surf_cand_va && base == surf_cand_va) {
+ span = surf_cand_span;
+ surf_cand_used = true;
+ }
if (is_arg[w] && base >= 0x40000000000ULL)
n_arg_hi++;
@@ -30958,6 +31107,19 @@ static void svc_gr_data_fill(struct nv_vgpu_device *vgpu,
svc_log_stalled_va(va);
if (is_arg[w])
svc_fwd_req.gr_arg_partial = true;
+ /*
+ * SESSION 153-CONTINUED-63: a
+ * render-target page that does
+ * not resolve must NOT hold
+ * the launch (the target is
+ * an output, not an operand;
+ * its alias fallback keeps
+ * today's behaviour), and the
+ * broken span must not be
+ * recorded as complete.
+ */
+ if (surf_cand_va && wins[w] == surf_cand_va)
+ break;
if (dropped_pages < 4) {
drop_va[dropped_pages] = va;
drop_kind[dropped_pages] =
@@ -31180,6 +31342,35 @@ static void svc_gr_data_fill(struct nv_vgpu_device *vgpu,
(n_arg_hi > 0 && n_arg_hi_res == 0);
svc_fwd_req.gr_dropped = dropped_pages;
/*
+ * SESSION 153-CONTINUED-63: record the render-target tracking run -
+ * only when the pass actually seeded it AND every requested page
+ * produced a data rec contiguous from the base (the booter then
+ * rewrites the target's operand pair to the bounce base; a partial
+ * run falls back to the identity alias so the engine can never walk
+ * past the tracked pages).
+ */
+ if (surf_cand_used) {
+ u32 hit = 0;
+
+ for (i = 0; i < svc_fwd_req.gr_n_data; i++) {
+ u64 rva = svc_fwd_req.gr_data[i].va;
+
+ if (rva >= surf_cand_va &&
+ rva < surf_cand_va +
+ ((u64)surf_cand_span << 12))
+ hit++;
+ }
+ if (hit == surf_cand_span) {
+ svc_gr_surf_rec_va = surf_cand_va;
+ svc_gr_surf_rec_n = surf_cand_span;
+ WRITE_ONCE(svc_gr_surf_rec_ok, 1);
+ } else if (svc_gr_surf_rec_log < 16) {
+ svc_gr_surf_rec_log++;
+ pr_info("vgsp-sema: surf-rec incomplete va=0x%llx want=%u got=%u (target falls back to the identity alias)\n",
+ surf_cand_va, surf_cand_span, hit);
+ }
+ }
+ /*
* GRFIX FIX1e (r215): gsp_phys_gr_fill_hold is the master gate.
* Default 0 = LOG-ONLY: the fill NEVER holds on dropped pages - the
* stalled verdict keeps only the historical pre-8b058884 aggregate
diff --git a/kernel/gsp_sema_service.h b/kernel/gsp_sema_service.h
index 0f32265..2b99274 100644
--- a/kernel/gsp_sema_service.h
+++ b/kernel/gsp_sema_service.h
@@ -507,6 +507,34 @@ const u64 *gsp_sema_attr_prelude_ptrs(u32 *n);
const u32 *gsp_sema_surf_prelude_get(u32 *n, u32 *gen);
/*
+ * SESSION 153-CONTINUED-63: surface geometry for render-target tracking
+ * (base VA + page count of the captured colour target).
+ */
+bool gsp_sema_surf_va_get(u64 *va, u32 *npages);
+
+/*
+ * SESSION 153-CONTINUED-63: the fill's completed render-target tracking run
+ * (base VA + page count), true only when every page of the surface resolved
+ * into contiguous data recs this pass. The booter prefers these recs over
+ * the identity surface alias when rewriting the target's operand pair, so
+ * the engine's stores land in the bounce (witness + copy-out) instead of
+ * the guest pages directly.
+ */
+bool gsp_sema_gr_surf_recs(u64 *va, u32 *n);
+
+/*
+ * SESSION 153-CONTINUED-63: the captured MME START-ADDRESS RAM table
+ * (macro slot -> IR instruction address; present-bitmask returned, entries
+ * copied into `entries`). The prelude buffer is capped at 512 words, so
+ * the SA table is recorded independently; the GR launch builder completes
+ * the SA RAM it replays and stubs every called-but-unknown slot with a
+ * harmless end-noop (the r1116 0x238c wall: SA[0x22] = 0 on a fresh
+ * context made CALL_MME_MACRO(0x22) jump to IR 0 and the slot-0 macro's
+ * emission trapped the lane).
+ */
+u64 gsp_sema_mme_sa_get(u32 *entries, u32 n);
+
+/*
* r378 write guards. Both re-validate a guest-memory write target that was
* resolved earlier (ledger/pin/walk/counter registration, or the GR submit's
* per-page resolution) against the CURRENT resolution before the bytes land,