Skip to content

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_H
  • AOWL_FRAME_DEPTH
  • AOWL_FRAME_EXPIRED
  • AOWL_FRAME_F_POSTFIX
  • AOWL_FRAME_F_STATIC
  • AOWL_FRAME_KIND
  • AOWL_FRAME_NOFRAME
  • AOWL_FRAME_NOTPOST
  • AOWL_FRAME_OFF_GPR
  • AOWL_FRAME_OFF_RET
  • AOWL_FRAME_OFF_RETF
  • AOWL_FRAME_OFF_XMM
  • AOWL_FRAME_OK
  • AOWL_FRAME_RANGE
  • AOWL_FRAME_SLOTS

Types

  • struct AowlPatchFrame

Functions

SignatureLine
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

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