Skip to content

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_H
  • AOWL_REGION_ABI
  • AOWL_REGION_DEFAULT_BUDGET
  • AOWL_REGION_FAULT_LIMIT
  • AOWL_REGION_MAX
  • AOWL_REGION_MAX_CMDS
  • AOWL_REGION_NAME_LEN
  • AOWL_REGION_OVERRUN_LIMIT
  • AOWL_REGION_REASON_LEN
  • AOWL_REGION_TEXT_LEN
  • AOWL_REGION_TEX_MAX
  • AOWL_REGION_TEX_SLOT_BYTES
  • AOWL_REGION_THROTTLE_EVERY

Types

  • struct AowlRegionCall
  • struct AowlRegionCmd
  • struct AowlRegionDesc
  • struct AowlRegionPart
  • struct AowlRegionState
  • struct AowlRegionStatus
  • struct AowlRegionTexSlot

Functions

SignatureLine
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

aoughwl — self-hosted platform for things n stuff. Contact / Support on Discord for access to the private backends.