Appearance
aowlspt_frame.h
Source: abi/aowlspt_frame.h — 646 lines, 32 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_frame.h — the typed, allocation-free view of a patch firing.
`AowlHostApi.patch` hands a mod its arguments as JSON. That was the right
first answer -- it is self-describing, it crosses the C ABI as one string,
and it is what made reading a hook's arguments possible at all. It is the
wrong answer for a method the game runs per entity per frame. Measured
against the stand-in runtime: an unpatched bound call is 3 ns, a postfix that
only watches the return value is 443 ns, and a postfix that reads the
arguments and replaces the result is 2320 ns. At forty bots and 60 fps that
last figure is 5.6 ms a frame, which is a third of the budget for one mod's
hooks. (Those two figures read 418 and 2245 here for a while, from an older
run; the live ones are the `examples/highlevel` rows in `docs/PERF.md`,
which `aowl test` re-measures.)
The thunk is not the cost. Building the payload is: a JSON string per firing
on the host side, a GC handle per reference argument, a copy into a mod-side
string, and a parse. Every one of those is an allocation, and all of them
exist to describe a handful of machine words the thunk already saved.
This header is the other path. It does not replace the JSON one -- a mod that
wants convenience keeps `hookArgs` -- it sits beside it for the mod that has
measured a per-frame hook and needs the machine words.
**The mechanism, in one sentence.** The detour thunk already saves RCX, RDX,
R8, R9 and XMM0-3 into a frame on its own stack; the host already knows,
from the method's declared signature, which of those registers holds what.
So the host hands the mod a *borrowed view* of that frame plus the declared
shapes, and the mod reads a slot by index and by kind. Nothing is built,
nothing is copied, nothing is freed.
**Shapes are computed once.** `kinds` points at an array the host built at
registration, out of `il2cpp_method_get_param` and `shapeOfType`. Per firing
the host stores six words into a pooled frame and calls the handler. That is
where the JSON path's cost really went: not the encoding, but asking the
runtime what everything is, every time.
**The view must not outlive the handler**, and that is a refusal rather than
a comment. A frame comes from a fixed pool the host owns, and the host clears
`live` when the handler returns. Every accessor below checks it, refuses, and
records `AOWL_FRAME_EXPIRED` -- which `aowl_frame_why_text` turns into a
sentence naming that specific mistake. This is the same standard
`handle_pointer` already holds a stored patch handle to: not "do not do
that", but "that is refused and here is what you did".
**What was rejected.** The obvious cheaper JSON is CBOR -- the ABI already
names `AOWLSPT_ENC_CBOR`, it is the same data model, and the encoder is
maybe five times faster. It was rejected because the shape of the cost does
not change: it is still an encode and a decode proportional to the argument
count, still a buffer somebody owns, and still a GC handle per reference. A
five-fold cut on 2320 ns is 464 ns, which is the *cheap* postfix's price for
the expensive postfix's work -- an improvement that would have to be
re-litigated the next time a mod put a hook on a per-bot method. Reading the
register the argument is already in costs nothing and cannot be made to cost
something later.
Include it on its own; it needs nothing but stdint/stddef/string.Constants
AOWLSPT_FRAME_HAOWL_FRAME_DEPTHAOWL_FRAME_EXPIREDAOWL_FRAME_F_POSTFIXAOWL_FRAME_F_STATICAOWL_FRAME_KINDAOWL_FRAME_NOFRAMEAOWL_FRAME_NOTPOSTAOWL_FRAME_OFF_GPRAOWL_FRAME_OFF_RETAOWL_FRAME_OFF_RETFAOWL_FRAME_OFF_XMMAOWL_FRAME_OKAOWL_FRAME_RANGEAOWL_FRAME_SLOTS
Types
struct AowlPatchFrame
Functions
| Signature | Line |
|---|---|
int32_t aowl_frame_why(void) | 175 |
char aowl_frame_why_text(int32_t why) | 181 |
AowlPatchFrame aowl_frame_of(void* p) | 219 |
int32_t aowl_frame_live(void* p) | 227 |
int32_t aowl_frame_size(void* p) | 231 |
int32_t aowl_frame_argc(void* p) | 235 |
uint32_t aowl_frame_flags(void* p) | 239 |
uint32_t aowl_frame_serial(void* p) | 243 |
int32_t aowl_frame_is_postfix(void* p) | 247 |
int32_t aowl_frame_is_static(void* p) | 251 |
int32_t aowl_frame_kind(void* p, int32_t i) | 260 |
int32_t aowl_frame_ret_kind(void* p) | 270 |
uint64_t aowl_frame_self(void* p) | 282 |
int32_t aowl_frame_pos(const AowlPatchFrame* f, int32_t i) | 292 |
uint64_t aowl_frame_gpr(const AowlPatchFrame* f, int32_t pos) | 296 |
uint64_t aowl_frame_xmm(const AowlPatchFrame* f, int32_t pos) | 301 |
int64_t aowl_frame_int(void* p, int32_t i, void* okRaw) | 308 |
double aowl_frame_flt(void* p, int32_t i, void* okRaw) | 339 |
uint64_t aowl_frame_ptr(void* p, int32_t i, void* okRaw) | 385 |
AowlPatchFrame aowl_frame_post(void* p) | 422 |
int64_t aowl_frame_ret_int(void* p, void* okRaw) | 432 |
double aowl_frame_ret_flt(void* p, void* okRaw) | 453 |
uint64_t aowl_frame_ret_ptr(void* p, void* okRaw) | 480 |
int32_t aowl_frame_set_ret_int(void* p, int64_t v) | 502 |
int32_t aowl_frame_set_ret_flt(void* p, double v) | 520 |
int32_t aowl_frame_set_ret_ptr(void* p, uint64_t v) | 539 |
int32_t aowl_frame_set_ret_void(void* p) | 553 |
int32_t aowl_frame_depth_max(void) | 598 |
int32_t aowl_frame_sizeof(void) | 599 |
void aowl_frame_slot(int32_t depth) | 602 |
void aowl_frame_arm(void* p, void* regs, const void* kinds, int32_t argc, int32_t retKind, uint32_t flags, uint64_t self) | 610 |
void aowl_frame_disarm(void* p) | 632 |

