Appearance
aowlspt_region.h
Source: abi/aowlspt_region.h — 1709 lines, 70 file-scope functions.
What this header owns
Reproduced verbatim from the header's own banner comment — these notes are frequently the only written record of why the subsystem is shaped the way it is.
text
aowlspt_region.h -- THE SHARED PER-FRAME REGION.
A first-class, mod-facing per-frame DRAW + TICK facility. Any number of
participants register a callback; the region owns the ONE dispatch point and
calls them. Nobody needs a second detour, and nobody needs to borrow another
mod's private header to get a pixel on screen.
---------------------------------------------------------------------------
WHY THIS FILE EXISTS
---------------------------------------------------------------------------
Two detours on one function have the second overwrite the first's trampoline
and silently kill the first feature. The consequence, on this build, is that
there is effectively ONE main-thread hook in this project: the rider chain on
`EFT.UI.PreloaderUI::Update`. It already carries the debug overlay, the menu
mode text, the live inspector, the mods tab and the version brand, and each
of those rides it by hard-coding a slot global into `patchFired` in
`aowlhost.nim`. That works, and it does not scale: it means every new
per-frame feature is a source edit to the host, in a file five other agents
are also editing, and a mod that is not part of the host cannot participate
at all.
Measured today: the maps/radar/indicators mod reported its in-game draw
BLOCKED because "the only generic region is `abi/aowlspt_admin.h`" -- another
mod's private surface. Five features (maps, radar, accessibility indicators,
admin ESP, the F3 debug overlay) all want the same thing and cannot share it.
So this header is the thing they were all reaching for. It is deliberately
ONE more rider on the existing chain -- not a new detour -- and everything
else registers with it.
---------------------------------------------------------------------------
WHAT THIS FILE DOES NOT DO, ON PURPOSE
---------------------------------------------------------------------------
**It resolves no IL2CPP name, calls no game method and patches no byte.**
Fact #145, which is an OBSERVATION and is not restated here as a mechanism:
on this build a by-NAME IL2CPP route is fatal the moment it is USED, not when
it is resolved, and the client log of a death by that route is byte-identical
to a healthy run. (For what is actually going on underneath -- a token-gated
export ABI, not an absent one -- see `docs/IL2CPP_EXPORTS.md`; that work is
another agent's and this file deliberately does not encode a guess about it.)
The safest answer to an observation with that failure signature is to have no
such route in the file at all, and that is the answer taken here. This header is a registry, a QPC pair, a dispatch loop and a POD
command buffer. It cannot be fatal in the way #145 describes, because there
is nothing in it to be fatal.
Everything that DOES need the game -- the camera projection, the actual
rasterisation -- arrives as a function pointer INSTALLED BY THE HOST, from
code that has already byte-verified its own target against the startup
prologue snapshot. See `aowl_region_set_projector` / `aowl_region_set_sink`.
If nothing is installed, projection REFUSES and drawing is a no-op; it never
guesses.
It also does not resolve a name to an RVA, and does not implement a
byte-verified call-by-RVA path. Two other agents are building exactly those
(an offline name -> unique-RVA symbol table that fails the BUILD on a shared
or prologue-mismatched target, and a general byte-verified call-by-RVA path
for mods). Duplicating either would give this project two answers to one
question. ASSUMED, and stated so it can be corrected: when they land, the
host's projector installation is the single place that changes -- it becomes
"look the symbol up in the table, take the verified RVA, install the thunk"
instead of "take debugui's already-verified thunk". No mod-facing API in this
file changes, because no mod-facing API in this file names a symbol.
---------------------------------------------------------------------------
THE GUARD, AND WHY IT CANNOT NEST
---------------------------------------------------------------------------
`aowl_p_p_seh` (abi/aowlspt_shim.h) is a VEH + setjmp guard with exactly ONE
thread-local `jmp_buf` and ONE thread-local `active` flag. It is NOT
re-entrant: a nested guard clobbers the buffer, and worse, restores
`active = 0` when the INNER call returns, silently disarming the outer one.
A "guard per callback" placed inside a "guard around the loop" is therefore
not extra safety -- it is the removal of all safety, plus a false sense of it.
But a single guard AROUND the loop gives no isolation either: the first
callback to fault longjmps out of the whole dispatch, and every participant
after it in the ordering silently loses its frame. That is precisely the
failure this facility exists to prevent.
The structure that satisfies both:
* `aowl_region_frame()` -- the dispatcher -- opens NO guard at all. Its
entire body touches only this header's own fixed arrays and integers.
There is no game pointer, no dereference of anything a participant
supplied, and no allocation. It has nothing to be guarded from.
* Each participant is invoked through ONE `aowl_p_p_seh`, entered and
EXITED before the next participant is entered. The guards are SIBLINGS
in sequence, never parent and child. N callbacks means N guards, and the
maximum guard depth at any instant is 1.
Two things enforce that rather than merely asserting it:
1. `aowl_region_frame()` REFUSES to run if a guard is already active on
this thread (`aowl_region_guard_active()`, which reads the shim's own
`aowl_seh_active`). If some future caller wraps us in a guard, we
decline loudly and set `AOWL_REGION_REFUSE_NESTED` instead of quietly
building a nest. This is the check that would catch the mistake.
2. A re-entrancy flag refuses a second `aowl_region_frame()` while one is
in progress, so a participant that (wrongly) drives the region from
inside its own callback gets a refusal, not recursion.
The fault DETECTOR has the property CLAUDE.md 9b asks for: it cannot be made
to say "fine" by the thing it is checking. `aowl_p_p_seh` returns whatever
the body returned, or 0 if it longjmped. The body returns the constant 1
AFTER the callback returns normally. A callback cannot cause a 1; only
surviving can. So `result == 0` is a fault, always, and a callback that dies
cannot report success.
---------------------------------------------------------------------------
BUDGET, ORDER, ALLOCATION
---------------------------------------------------------------------------
Ordering is deterministic: ascending `order`, ties broken by registration
sequence, both fixed at registration. The dispatch order does not depend on
load order, hash order, or which mod happened to register first within an
order class.
Every participant declares a per-frame budget in microseconds. The dispatcher
times each call with QueryPerformanceCounter and, on an overrun, increments
`overruns`, records the worst, and RAISES it -- once immediately and then at
a decaying rate, always naming the participant. Silent tolerance is how a
frame-time regression hides for a month; a busy log line is cheaper than that.
A participant that overruns `AOWL_REGION_OVERRUN_LIMIT` frames in a row is
additionally THROTTLED to one frame in `AOWL_REGION_THROTTLE_EVERY` -- and
`throttled` is published, so the state is visible rather than mysterious.
Iteration is capped everywhere: `AOWL_REGION_MAX` participants, and the draw
buffer is a fixed `AOWL_REGION_MAX_CMDS` POD array that REFUSES past its end
and counts the refusals (`dropped`) rather than growing.
There is no allocation on the frame path, managed or native. The registry is
a fixed array, names are write-once `char[32]`, the command buffers are two
fixed arrays swapped by index, and text commands carry an inline
`char[AOWL_REGION_TEXT_LEN]` rather than a pointer to memory whose lifetime
nobody owns.
---------------------------------------------------------------------------
WHO COMPILES WHAT
---------------------------------------------------------------------------
Two C modules cannot share a header `static`, so there is exactly ONE owner:
* The host DLL defines `AOWL_REGION_HOST` before including this file. That
TU gets the state, the dispatcher, and the `aowl_region_*` exports.
* Everybody else -- a mod DLL, the overlay -- includes it plainly and gets
a thin client that resolves those exports by `GetProcAddress` on
`aowlspt-host-il2cpp.dll`, lazily. An older host without the exports is a
clean, reported "unavailable", never a load failure and never a crash.
Registration is legal FROM ANY THREAD AND AT ANY TIME, including before the
Unity thread exists. It simply does not fire until the host arms the region
from the rider. That is requirement 1 and it falls out of the design: the
registry is guarded by a critical section, the frame path takes it only to
snapshot the order, and `armed` starts 0.
---------------------------------------------------------------------------
WHAT A MOD WRITES
---------------------------------------------------------------------------
static void my_draw(void* user, int64_t frame) {
float sx, sy;
if (aowl_region_project(wx, wy, wz, &sx, &sy))
aowl_region_box(sx - 20, sy - 40, 40, 80, 2.0f, 0xFF00FF00u);
aowl_region_text(8, 8, "maps: 3 extracts", 0xFFFFFFFFu);
}
AowlRegionDesc d; memset(&d, 0, sizeof(d));
d.size = (int32_t)sizeof(d);
strcpy(d.name, "maps"); // shows up in every log line and in
d.mask = AOWL_REGION_DRAW; // the profiler as this exact string
d.order = 100;
d.budgetUs = 400;
d.fn = my_draw;
int32_t h = aowl_region_register(&d);
if (h < 0) log(aowl_region_refusal_text(h)); // never print the number
That is the whole API. There is no detour to install, no prologue to verify,
no slot to add to `patchFired`, and no other mod's header to include.
---------------------------------------------------------------------------
MIGRATION NOTES, PER EXISTING CONSUMER
---------------------------------------------------------------------------
NONE of these migrations were performed. Their owners are mid-flight and a
migration done TO somebody rather than BY them is how a working feature dies
quietly. Each is what that owner would have to change, and each is optional:
the five hand-written riders keep working untouched, because the region was
added as a SIXTH rider after them rather than in place of them.
maps / radar / indicators (mods/maps, not yet landed)
The one with nothing to lose. It has no rider today and was blocked on
having no generic region. It registers with `AOWL_REGION_DRAW`, submits
screen-space commands, and deletes whatever it was going to borrow from
`aowlspt_admin.h`. No host edit at all.
admin ESP (mods/admin + the HUD in aowlspt_overlay.h)
The largest change and the least urgent. Today the mod publishes a
seqlocked frame of PRE-PROJECTED entities into its own named shared
region and the overlay's `aowl_ov_admin_append` reads it. That is a
sound design and it is not broken. Migrating means: register a DRAW
participant, move the body of `aowl_ov_admin_append`'s ESP half into
it, and emit `aowl_region_box`/`_text` instead of writing entities. It
gains per-mod budget accounting and fault isolation and loses its
private shared-memory hop. The F6 MENU half should NOT migrate: it is
input-driven overlay chrome, not a per-frame world draw.
the F3 debug overlay (host/.../debugui.nim)
It does not draw through the D3D overlay at all -- it clones Unity UI
objects and writes into them, which is a different mechanism and the
right one for menu-space text. What it WOULD gain by migrating is the
dispatch, not the drawing: `debugUiFired` becomes a registered TICK
participant and `gDebugUiSlot` disappears from `patchFired`. Its own
`aowl_du_body_guarded` thunk must then be DELETED, not kept -- keeping
it would nest a guard inside the region's per-participant guard, which
is the exact bug this facility is built around. That deletion is the
whole risk of this migration and it is why it is not being done here.
the live inspector (host/.../inspect.nim)
Should probably NOT migrate. It already runs one guard per COMMAND
rather than one per frame, it must keep ticking inside a raid via its
second slot alias (`gInspSlot2`) where `PreloaderUI::Update` does not
run, and it is the instrument used to debug everything else -- putting
it behind a budget that can throttle it would mean the tool goes quiet
exactly when a frame is slow. If it ever does migrate, it wants a
budget of 0 meaning "unbudgeted", which this header does not offer
today and would have to grow.
the version brand (host/.../modstab.nim, polling half)
A one-line migration and a good first one: it is a throttled poll with
no drawing. Register TICK, order early, budget ~200us. Its Awake-postfix
half is a different detour on a different function and is unaffected.
the mods tab (host/.../modstab.nim)
Register TICK. It builds and refreshes cloned settings rows, which is
bursty rather than steady, so it wants a generous budget (2000us+) or
it will report overruns on the frames it actually builds on. That is
the honest reading of its cost, not a false alarm -- but a caller that
has not thought about it will read the log as a regression.
---------------------------------------------------------------------------
PROFILER
---------------------------------------------------------------------------
If `abi/aowlspt_profile.h` is present (another agent owns it; it is not
required), each participant gets a profiler slot named after it and its
dispatch is wrapped in `aowl_prof_begin`/`aowl_prof_end`, so per-mod frame
cost is attributable there as well as here. ASSUMED about that header, from
reading it: `aowl_prof_slot(name, kind)` is called ONCE at registration and
returns a slot index or negative; `aowl_prof_begin`/`_end` take that index
and are safe to call when the profiler is disabled or unmapped. If any of
that is wrong, `AOWL_REGION_NO_PROFILE` compiles it out and nothing else
changes.Constants
AOWLSPT_REGION_HAOWL_REGION_ABIAOWL_REGION_DEFAULT_BUDGETAOWL_REGION_FAULT_LIMITAOWL_REGION_MAXAOWL_REGION_MAX_CMDSAOWL_REGION_NAME_LENAOWL_REGION_OVERRUN_LIMITAOWL_REGION_REASON_LENAOWL_REGION_TEXT_LENAOWL_REGION_TEX_MAXAOWL_REGION_TEX_SLOT_BYTESAOWL_REGION_THROTTLE_EVERY
Types
struct AowlRegionCallstruct AowlRegionCmdstruct AowlRegionDescstruct AowlRegionPartstruct AowlRegionStatestruct AowlRegionStatusstruct AowlRegionTexSlot
Functions
| Signature | Line |
|---|---|
char aowl_region_refusal_text(int32_t r) | 363 |
uint32_t aowl_region_tex_bytes(int32_t fmt, int32_t w, int32_t h) | 477 |
void aowl_region_say(const char* line) | 659 |
void aowl_region_sayf(const char* fmt, ...) | 663 |
int32_t aowl_region_guard_active(void) | 684 |
int64_t aowl_region_now(void) | 697 |
int64_t aowl_region_us(int64_t ticks) | 703 |
void aowl_region_reorder(void) | 712 |
int32_t aowl_region_abi(void) | 732 |
int32_t aowl_region_register(const AowlRegionDesc* d) | 734 |
int32_t aowl_region_unregister(int32_t h) | 791 |
int32_t aowl_region_set_enabled(int32_t h, int32_t on) | 805 |
int32_t aowl_region_status(int32_t h, AowlRegionStatus* out) | 812 |
void aowl_region_set_projector(AowlRegionProjFn f) | 836 |
void aowl_region_set_screen(int32_t w, int32_t h) | 861 |
int32_t aowl_region_screen_w(void) | 865 |
int32_t aowl_region_screen_h(void) | 866 |
int32_t aowl_region_screen_known(void) | 869 |
void aowl_region_set_armed(int32_t on) | 872 |
int32_t aowl_region_push(int32_t kind, float x, float y, float w, float h, float t, uint32_t col, const char* text) | 887 |
AowlRegionTexSlot aowl_region_tex_find(uint64_t key) | 958 |
int32_t aowl_region_texture_have(uint64_t key) | 968 |
int32_t aowl_region_texture_define(uint64_t key, int32_t fmt, int32_t w, int32_t h, const void* data, uint32_t bytes) | 972 |
int32_t aowl_region_texture_forget(uint64_t key) | 1045 |
AowlRegionTexSlot aowl_region_texture_slot(int32_t i) | 1056 |
int32_t aowl_region_texture_count(void) | 1061 |
int64_t aowl_region_texture_evictions(void) | 1066 |
int32_t aowl_region_quad(float x, float y, float w, float h, float u0, float v0, float u1, float v1, uint64_t key, uint32_t tint) | 1072 |
int32_t aowl_region_quadr(float x, float y, float w, float h, float u0, float v0, float u1, float v1, uint64_t key, uint32_t tint, float rcos, float rsin, float rpx, float rpy, float clipX, float clipY, float clipW, float clipH) | 1106 |
int32_t aowl_region_line(float x0, float y0, float x1, float y1, float t, uint32_t col) | 1140 |
int32_t aowl_region_box(float x, float y, float w, float h, float t, uint32_t col) | 1144 |
int32_t aowl_region_fill(float x, float y, float w, float h, uint32_t col) | 1148 |
int32_t aowl_region_text(float x, float y, const char* s, uint32_t col) | 1152 |
int32_t aowl_region_text_scaled(float x, float y, const char* s, uint32_t col, int32_t scale) | 1159 |
int32_t aowl_region_project_ex(float wx, float wy, float wz, float* sx, float* sy, int32_t* flags, float* depth) | 1169 |
int32_t aowl_region_project(float wx, float wy, float wz, float* sx, float* sy) | 1217 |
void aowl_region_body(void* a) | 1237 |
int32_t aowl_region_frame(void) | 1248 |
int32_t aowl_region_commands(const AowlRegionCmd** out) | 1397 |
int64_t aowl_region_frames(void) | 1404 |
int64_t aowl_region_dropped(void) | 1405 |
int64_t aowl_region_frame_us(void) | 1406 |
int32_t aowl_region_count(void) | 1407 |
int32_t aowl_region_is_armed(void) | 1408 |
int32_t aowl_region_last_refusal(void) | 1409 |
char aowl_region_name_of(int32_t h) | 1410 |
void aowl_region_resolve(void) | 1437 |
int32_t aowl_region_register(const AowlRegionDesc* d) | 1456 |
int32_t aowl_region_unregister(int32_t h) | 1463 |
int32_t aowl_region_status(int32_t h, AowlRegionStatus* o) | 1467 |
int32_t aowl_region_line(float x0, float y0, float x1, float y1, float t, uint32_t c) | 1471 |
int32_t aowl_region_box(float x, float y, float w, float h, float t, uint32_t c) | 1476 |
int32_t aowl_region_fill(float x, float y, float w, float h, uint32_t c) | 1481 |
int32_t aowl_region_text(float x, float y, const char* s, uint32_t c) | 1485 |
int32_t aowl_region_project(float wx, float wy, float wz, float* sx, float* sy) | 1489 |
int32_t aowl_region_project_ex(float wx, float wy, float wz, float* sx, float* sy, int32_t* flags, float* depth) | 1504 |
void aowl_region_resolve_tex(void) | 1544 |
int32_t aowl_region_texture_define(uint64_t key, int32_t fmt, int32_t w, int32_t h, const void* data, uint32_t bytes) | 1561 |
int32_t aowl_region_texture_have(uint64_t key) | 1568 |
int32_t aowl_region_texture_forget(uint64_t key) | 1572 |
int32_t aowl_region_quad(float x, float y, float w, float h, float u0, float v0, float u1, float v1, uint64_t key, uint32_t tint) | 1577 |
int32_t aowl_region_quadr(float x, float y, float w, float h, float u0, float v0, float u1, float v1, uint64_t key, uint32_t tint, float rcos, float rsin, float rpx, float rpy, float clipX, float clipY, float clipW, float clipH) | 1588 |
int32_t aowl_region_commands(const AowlRegionCmd** out) | 1612 |
int32_t aowl_region_is_armed(void) | 1627 |
int64_t aowl_region_frames(void) | 1641 |
AowlRegionTexSlot aowl_region_texture_slot(int32_t i) | 1659 |
void aowl_region_resolve_screen(void) | 1680 |
int32_t aowl_region_screen_known(void) | 1690 |
int32_t aowl_region_screen_w(void) | 1694 |
int32_t aowl_region_screen_h(void) | 1698 |

