Skip to content

aowlspt_nativeui.h

Source: abi/aowlspt_nativeui.h — 1475 lines, 85 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_nativeui.h -- the NATIVE UNITY UI CONSTRUCTION LAYER.

===========================================================================
WHAT THIS IS, AND WHY IT IS NOT THE INVOKE2 LADDER
===========================================================================

`aowlspt_invoke2.h` proved, live, that managed Unity code can be CALLED
directly at a static RVA from inside a detour: a real GameObject was
allocated, constructed, named, round-tripped through `get_name`, given a
RectTransform, cloned, parented and activated. Every one of those steps was
verified, not merely "returned non-null".

And nothing appeared on screen.

That is the whole reason this file exists, and the shape of the bug is the
one CLAUDE.md 9b names: eight checks that could each only say yes.

THE FIRST HYPOTHESIS WAS WRONG, AND THIS LAYER MEASURED IT WRONG ITSELF
----------------------------------------------------------------------
The hypothesis was: creation works, LAYOUT is missing -- a `RectTransform`
fresh out of `AddComponent` has `sizeDelta == (0,0)`, and a zero-area rect
renders nothing while every call reports success.

`nuProofRun` was written to MEASURE that rather than assume it: read the
rect back before laying out, and again after. Live, first run, it read

    rect BEFORE layout = (-50.0, -50.0, 100.0, 100.0)  renderable=true

So a fresh RectTransform on this build is Unity's default 100x100 centred on
its anchor -- NOT zero-area, and renderable by any measure this layer has.
The zero-area hypothesis is FALSIFIED. Whatever made the invoke2 label
invisible, it was not a zero-size rect.

That is the before/after measurement doing its job, and it is worth being
blunt about: had the proof asserted the hypothesis instead of measuring it,
this file would now contain a confident, wrong explanation with live
"evidence" behind it. Layout is still REQUIRED -- an element at Unity's
default position and size is not where a caller wants it -- but it is no
longer claimed as the cause of anything. The cause is still open; the log
lines that would narrow it are in `nuProofRun`.

So this layer is built around three commitments:

  1. Every element it creates is LAID OUT before it is shown -- anchors,
     pivot, size and position are not optional arguments, they are part of
     creating a thing at all. (Not because a missing layout was proven to
     cause invisibility -- see above, it was not -- but because Unity's
     default 100x100 at the anchor centre is never what a caller meant.)
  2. Every element it creates is PROVED against the finished state: read the
     rect back, read the text back, read `activeInHierarchy` back. A
     self-comparison ("the size I wrote is the size I meant") is banned.
  3. Nothing is trusted because it is named plausibly. A component slot is
     refused until the component it produced has been shown to be of the
     class it claims.

===========================================================================
THE API, IN ONE PARAGRAPH
===========================================================================

`aowl_nu_create(name)` makes a GameObject with a RectTransform.
`aowl_nu_add(go, kind)` attaches a component by KIND (an enum, never a
name and never a `System.Type`). `aowl_nu_parent(child, parentTransform)`
puts it in a live hierarchy. `aowl_nu_layout(rt, ...)` sets anchors, pivot,
size and position through the `_Injected` setters. `aowl_nu_set_text` writes
TMP text through the real setters. `aowl_nu_clone(obj)` duplicates a live
element. `aowl_nu_destroy(obj)` tears one down, and `aowl_nu_destroy_all()`
tears down everything this layer ever made. All of it is read-back-verified
by the Nim surface in `nativeui.nim`.

===========================================================================
THE COMPONENT PROBLEM, AND HOW IT IS GENERALISED
===========================================================================

`GameObject::AddComponent<T>()` at 0x2A9AE90 is SHARED GENERIC CODE: one
body serves every T, and T lives entirely in the hidden trailing
`MethodInfo*`. A NULL MethodInfo is an immediate access violation there (the
body dereferences it at +0x38), so the generic route needs a real one.

We do not synthesise it and we do not ask reflection for it. Per
docs/IL2CPP_EXPORTS.md the reflection exports are TOKEN-GATED: 38 of them
take a trailing 32-byte token we never pass, and on mismatch they return a
uniform random non-zero uint64 from a per-thread MT19937-64. That value
passes a nil check and kills the client on first dereference. `AddComponent
(System.Type)` sits behind exactly that door (`il2cpp_class_get_type` +
`il2cpp_type_get_object`), which is why the invoke2 ladder's step 4b
faulted. It is still the only step that ever faulted.

Instead we read the pointer THE GAME ITSELF COMPUTED. IL2CPP never embeds a
`MethodInfo*` as an immediate; it emits a load from a per-token `.data` slot
that a metadata initialiser fills the first time the owning method runs:

    mov rcx, [rip+X]        ; the receiver's Il2CppClass*
    mov rdx, [rip+Y]        ; <- Y is the MethodInfo* slot for THIS T
    call 0x2A9AE90          ; AddComponent<T>

The invoke2 ladder hardcoded ONE such slot, for `RectTransform`, found by
hand inside `TMP_DefaultControls::CreateUIElementRoot`. `tools/addcompslots.py`
generalises that: it finds every `E8 rel32` in the `il2cpp` section whose
destination is 0x2A9AE90, recovers the last `mov rdx,[rip+X]` before each,
range-checks the result into a data section, and attributes each call site
to its enclosing method from the per-image `methodPointers` tables. It
reproduces 0x6E19580 for RectTransform independently, which is what makes
its other answers worth reading.

MEASURED, 500 call sites, this build (1.1.0.1.46777). The attribution below
is by INTERSECTION of the call sites' enclosing methods against Unity's
documented `TMP_DefaultControls` / `Dropdown` sources -- i.e. the only
component every listed creator has in common:

  0x6E19580  RectTransform     8 sites: CreateUIElementRoot, CreateUIObject,
                               CreateButton, TextMeshProUGUI::Awake,
                               TextContainer::OnRectTransformDimensionsChange,
                               TMP_Dropdown::CreateBlocker,
                               UI.Dropdown::CreateBlocker
  0x6D50040  TextMeshProUGUI   6 sites: CreateText (which adds ONLY this),
                               CreateButton, CreateInputField x2,
                               CreateDropdown x2
  0x6D50070  Image            12 sites: CreateScrollbar x2 (bg + handle),
                               CreateButton, CreateInputField, CreateDropdown x4
  0x6D50038  Button            3 sites: CreateButton, TMP_Dropdown::CreateBlocker,
                               UI.Dropdown::CreateBlocker -- Button is the
                               only component all three add

There is deliberately NO CanvasRenderer slot: `Image` derives from `Graphic`,
which carries `[RequireComponent(typeof(CanvasRenderer))]`, and Unity's
native `AddComponent` honours RequireComponent. Adding one by hand would be
a second, unverifiable slot for no gain.

===========================================================================
WHY THE ATTRIBUTION ABOVE IS NOT TRUSTED, AND WHAT SETTLES IT
===========================================================================

All of that is EVIDENCE. None of it is proof, because the slot holds a
runtime pointer whose generic argument is not knowable offline, and because
"the enclosing method is called CreateText" is precisely the kind of
name-shaped reasoning that produced `ForceMeshUpdate` landing on a universal
empty stub shared by 6,438 methods.

So the last step is settled at RUNTIME, against the finished state:

  * a slot starts UNVERIFIED and is REFUSED;
  * the host registers a REFERENCE INSTANCE for a kind -- a live object of
    that class, reached by WALKING from something already validated, never
    by an offset that can read null -- via `aowl_nu_ref_set`. The class
    pointer is the object header's first qword, which is what
    `il2cpp_object_get_class` itself is (`mov rax,[rcx]; ret`, three
    instructions, no gate, no validation);
  * the first `AddComponent` through a slot compares the RESULT's header
    klass against that reference klass. Equal -> the slot is VERIFIED and
    usable. Unequal -> the slot is POISONED, permanently, and every later
    use of that kind is refused with the two class pointers logged.

A kind with no reference instance is INCONCLUSIVE, not "probably fine": the
component is created, checked as far as it can be, then DESTROYED again and
the kind stays refused. Three outcomes, never two.

That is the falsifiable check the whole design turns on. Ask what input
makes it fail: a slot attributed to the wrong T attaches the wrong
component, whose klass differs from the reference, and the layer disables
that kind. It cannot silently succeed.

===========================================================================
STRUCT ABI: WHY EVERY VECTOR GOES THROUGH `_Injected`
===========================================================================

`UnityEngine.RectTransform` declares exactly ONE il2cpp field
(`reapplyDrivenProperties`, static). `m_AnchoredPosition`, `m_SizeDelta`,
`m_Pivot`, `m_AnchorMin`, `m_AnchorMax` are NATIVE-side -- there is no field
offset to write. They must go through property setters.

The by-value setters (`set_sizeDelta(Vector2)` @0x52B5550 and friends) put
an 8-byte struct in an integer register and a 16-byte one behind a hidden
sret buffer, and getting that subtly wrong yields a call that returns
cleanly having written garbage. The `_Injected` variants take a POINTER
instead -- `(this, Vector2* value, MethodInfo*)` -- so there is no struct
ABI left to get wrong in either direction. This layer uses ONLY those:

  set_anchorMin_Injected        0x52B6D80
  set_anchorMax_Injected        0x52B6E40
  set_anchoredPosition_Injected 0x52B6F00
  set_sizeDelta_Injected        0x52B6FC0
  set_pivot_Injected            0x52B7080
  get_anchoredPosition_Injected 0x52B6EA0   (this, Vector2* ret, MethodInfo*)
  get_sizeDelta_Injected        0x52B6F60   (this, Vector2* ret, MethodInfo*)
  get_rect_Injected             0x52B6CC0   (this, Rect*    ret, MethodInfo*)

The `_Injected` GETTERS are what make the visual proof possible at all: the
finished rect comes back in a buffer we own, with no RAX-packing and no
sret shape to decode.

===========================================================================
TEXT
===========================================================================

Writing `m_text` raw does not stick: `LocalizedText` clobbers it. The real
setters are `TMP_Text::set_text` @0x51BC1E0 and, when a `LocalizedText`
component is present, `LocalizedText::SetLabelText` @0x140FE70 -- and the
write must be RE-APPLIED, because the clobber can land after ours.
`aowl_nu_set_text` therefore only does the call; `nuSetText` in
`nativeui.nim` does the re-apply and reads back through
`TMP_Text::get_text` @0x51BC100.

`ForceMeshUpdate` is NOT called anywhere here. Resolving it lands on
0x628110, which is `C2 00 00` (`ret 0`) -- this build's universal empty-body
stub, shared by 6,438 methods. It is not that method's code and calling it
has no effect. A stub that passes a signature check is the worst case there
is, so it is named here rather than quietly omitted.

===========================================================================
INPUT
===========================================================================

There is NO delegate path in v1, on purpose. Hooking a `Button.onClick`
needs a managed `UnityAction`, and a hand-built delegate needs both a valid
`invoke_impl` and a valid `MethodInfo*` for a method that does not exist in
any assembly. Nothing about that has been demonstrated on this build, and
an unproven delegate handed to Unity's event system is a fault on a frame we
do not control.

v1 uses POLLED state instead: the host already runs every frame on the Unity
thread through the `TarkovApplication::Update` bridge, so a control's
interaction is a per-frame read (pointer-over / pressed state, or a key), not
a callback. `aowl_nu_poll_rect_contains` supports that with pure arithmetic.
This is a documented v1 limitation, not an oversight.

===========================================================================
SAFETY (CLAUDE.md 5, all eight, non-negotiable)
===========================================================================

 1. Every RVA is 16-byte prologue-verified through `aowl_pro_verify` --
    against the STARTUP SNAPSHOT, never live memory, so a target another
    feature has already detoured does not make this layer self-reject.
 2. `VirtualQuery` on every hop: `aowl_nu_fn` checks committed+executable
    before it compares bytes; `aowl_nu_data_ptr` checks committed+readable
    and range-checks into `.data` before it dereferences a slot.
 3. ONE `aowl_p_p_seh` per body, never nested -- the guard is not re-entrant
    and an inner guard DISARMS the outer one. This header contains no guard
    at all; `nativeui.nim` wraps whole operations, once.
 4. Every loop is capped (`AOWL_NU_MAX_OWNED`, `AOWL_NU_MAX_KINDS`).
 5. Flag-gated, default OFF (`nativeUi`, `nativeUiProof`).
 6. Self-disables after `AOWL_NU_MAX_FAULTS`, and per-kind on a klass
    mismatch.
 7. NO per-frame managed allocation. Managed strings are allocated ONCE at
    first use and cached (`aowl_nu_intern`); the Vector2/Rect marshalling
    buffers are file-scope statics, reused. A UI layer is exactly where a
    per-frame `il2cpp_string_new` would bite.
 8. Never blind-write. Every setter here is a CALL into the game's own
    property setter; the only raw writes this layer performs are into its
    own C structs.

SHAREDNESS: measured with the fixed `Resolver.sharedness` (three outcomes:
shared / unique / unknown). All 24 managed targets below report `unique, 1`.
0x2A9AE90 reports `unknown, 0` -- it is shared generic code and is not in
the methodPointers histogram at all. That is fine here because this layer
CALLS it and never detours it; calling a shared address is correct code for
the receiver you pass. It would be unacceptable as a patch target.

THREADING: every entry point must be called on the Unity main thread. The
marshalling buffers are file-scope and not re-entrant.

Constants

  • AOWLSPT_NATIVEUI_H
  • AOWL_NU_ADDCOMPONENT_GEN
  • AOWL_NU_CANVAS_GET_RENDERMODE
  • AOWL_NU_CANVAS_SET_RENDERMODE
  • AOWL_NU_CANVAS_SET_SORTORDER
  • AOWL_NU_GO_ACTIVE_INHIER
  • AOWL_NU_GO_CTOR_STRING
  • AOWL_NU_GO_GET_TRANSFORM
  • AOWL_NU_GO_SETACTIVE
  • AOWL_NU_GO_SET_LAYER
  • AOWL_NU_GR_GET_CANVASREND
  • AOWL_NU_GR_SETALLDIRTY
  • AOWL_NU_GR_SET_COLOR
  • AOWL_NU_INPUT_GET_MOUSEBTN
  • AOWL_NU_INPUT_GET_MOUSEPOS
  • AOWL_NU_KIND_BUTTON
  • AOWL_NU_KIND_CANVAS
  • AOWL_NU_KIND_IMAGE
  • AOWL_NU_KIND_RECTTRANSFORM
  • AOWL_NU_KIND_TMPTEXT
  • AOWL_NU_LOC_SET_LABEL_TEXT
  • AOWL_NU_MAX_EXTENT
  • AOWL_NU_MAX_FAULTS
  • AOWL_NU_MAX_INTERN
  • AOWL_NU_MAX_INTERN_LEN
  • AOWL_NU_MAX_KINDS
  • AOWL_NU_MAX_OWNED
  • AOWL_NU_META_INIT
  • AOWL_NU_MIN_EXTENT
  • AOWL_NU_OBJ_ALIVE
  • AOWL_NU_OBJ_DESTROY
  • AOWL_NU_OBJ_GET_NAME
  • AOWL_NU_OBJ_INSTANTIATE
  • AOWL_NU_RENDERMODE_CAMERA
  • AOWL_NU_RENDERMODE_OVERLAY
  • AOWL_NU_RENDERMODE_UNKNOWN
  • AOWL_NU_RENDERMODE_WORLD
  • AOWL_NU_REVERIFY_MS
  • AOWL_NU_RT_GET_ANCHOREDPOS
  • AOWL_NU_RT_GET_RECT
  • AOWL_NU_RT_GET_SIZEDELTA
  • AOWL_NU_RT_SET_ANCHOREDPOS
  • AOWL_NU_RT_SET_ANCHORMAX
  • AOWL_NU_RT_SET_ANCHORMIN
  • AOWL_NU_RT_SET_PIVOT
  • AOWL_NU_RT_SET_SIZEDELTA
  • AOWL_NU_SET_PARENT_ALIGN
  • AOWL_NU_SLOT_ATTESTED
  • AOWL_NU_SLOT_NOREF
  • AOWL_NU_SLOT_POISONED
  • AOWL_NU_SLOT_UNKNOWN
  • AOWL_NU_SLOT_VERIFIED
  • AOWL_NU_TARGET_COUNT
  • AOWL_NU_TMP_GET_TEXT
  • AOWL_NU_TMP_SET_FONTSIZE
  • AOWL_NU_TMP_SET_TEXT
  • AOWL_NU_TR_GET_LOSSYSCALE
  • AOWL_NU_TR_GET_POSITION
  • AOWL_NU_TR_SETASFIRSTSIB
  • AOWL_NU_TR_SETPARENT2
  • AOWL_NU_WHY_BADINDEX
  • AOWL_NU_WHY_DISABLED
  • AOWL_NU_WHY_MISMATCH
  • AOWL_NU_WHY_NOT_COMMIT
  • AOWL_NU_WHY_NOT_EXEC
  • AOWL_NU_WHY_NO_MODULE
  • AOWL_NU_WHY_OK
  • AOWL_NU_WHY_PROFULL

Types

  • struct AowlNuIntern
  • struct AowlNuKind
  • struct AowlNuTarget

Functions

SignatureLine
char aowl_nu_target_name(int32_t i)568
uint32_t aowl_nu_target_rva(int32_t i)572
int32_t aowl_nu_base_ok(void)621
int32_t aowl_nu_why_of(int32_t i)624
int32_t aowl_nu_mismatch_count(void)631
int32_t aowl_nu_disabled(void)642
void aowl_nu_note_fault(void)645
int32_t aowl_nu_fault_count(void)646
int32_t aowl_nu_target_count(void)647
int32_t aowl_nu_profull_count(void)648
int32_t aowl_nu_ok_count(void)649
int32_t aowl_nu_bad_count(void)650
char aowl_nu_name(int32_t i)652
uint32_t aowl_nu_rva(int32_t i)656
void aowl_nu_fn_full(int32_t i)672
void aowl_nu_fn(int32_t i)745
int64_t aowl_nu_cache_hit_count(void)764
int64_t aowl_nu_cache_verify_count(void)765
void aowl_nu_prime_all(void)770
void aowl_nu_call_v_pp(void* fn, void* self, void* a0)805
void aowl_nu_call_v_pb(void* fn, void* self, int32_t a0)809
void aowl_nu_call_v_pi(void* fn, void* self, int32_t a0)813
void aowl_nu_call_v_pf(void* fn, void* self, float a0)817
void aowl_nu_call_v_ppb(void* fn, void* self, void* a0, int32_t a1)821
void aowl_nu_call_p_p(void* fn, void* self)825
void aowl_nu_call_p_pp(void* fn, void* self, void* a0)829
void aowl_nu_call_p_s1(void* fn, void* a0)833
void aowl_nu_call_v_s1(void* fn, void* a0)837
void aowl_nu_call_v_s2(void* fn, void* a0, void* a1)841
int32_t aowl_nu_call_b_p(void* fn, void* self)845
int32_t aowl_nu_call_i_p(void* fn, void* self, int32_t defVal)853
int32_t aowl_nu_call_b_s1(void* fn, void* a0)857
int32_t aowl_nu_call_b_si(void* fn, int32_t a0)869
void aowl_nu_call_generic0(void* fn, void* self, void* mi)876
void aowl_nu_v2in_set(float x, float y)897
void aowl_nu_v2in_ptr(void)898
void aowl_nu_v2out_ptr(void)899
float aowl_nu_v2out_x(void)901
float aowl_nu_v2out_y(void)902
void aowl_nu_rect_ptr(void)903
void aowl_nu_c4in_set(float r, float g, float b, float a)911
void aowl_nu_c4in_ptr(void)914
void aowl_nu_v3out_ptr(void)925
float aowl_nu_v3out_x(void)928
float aowl_nu_v3out_y(void)929
float aowl_nu_v3out_z(void)930
float aowl_nu_rect_x(void)932
float aowl_nu_rect_y(void)933
float aowl_nu_rect_w(void)934
float aowl_nu_rect_h(void)935
int32_t aowl_nu_rect_contains(float rx, float ry, float rw, float rh, float px, float py)940
int32_t aowl_nu_rect_renderable(float w, float h)954
char aowl_nu_kind_name(int32_t k)1057
char aowl_nu_kind_evidence(int32_t k)1061
uint32_t aowl_nu_kind_slot_rva(int32_t k)1065
int32_t aowl_nu_kind_attested(int32_t k)1069
int32_t aowl_nu_kind_count(void)1073
int32_t aowl_nu_slot_state(int32_t k)1074
void aowl_nu_ref_klass(int32_t k)1078
void aowl_nu_got_klass(int32_t k)1082
void aowl_nu_data_ptr(uint32_t rva)1097
void aowl_nu_kind_mi(int32_t k)1110
int32_t aowl_nu_token_kind(uint32_t tok)1139
uint32_t aowl_nu_token_index(uint32_t tok)1140
int32_t aowl_nu_is_cold_token(uintptr_t v)1146
void aowl_nu_warm_slot(int32_t k, void* metaInitFn)1164
void aowl_nu_klass_of(void* obj)1203
int32_t aowl_nu_ref_set(int32_t k, void* liveInstance)1219
int32_t aowl_nu_verdict(int32_t attested, void* refKlass, void* gotKlass)1241
int32_t aowl_nu_slot_judge(int32_t k, void* got)1249
void aowl_nu_exports_init(void)1291
void aowl_nu_intern(const char* s)1301
int32_t aowl_nu_intern_count(void)1325
int32_t aowl_nu_intern_overflowed(void)1326
int32_t aowl_nu_own(void* go)1343
int32_t aowl_nu_owned_count(void)1349
void aowl_nu_owned_at(int32_t i)1354
void aowl_nu_owned_clear(void)1358
void aowl_nu_disown(void* go)1365
void aowl_nu_object_new(void* klass)1386
int32_t aowl_nu_region_ok(void* p, int32_t size, int32_t needWrite)1417
int32_t aowl_nu_is_writable(void* p, int32_t size)1436
void aowl_nu_get_ref(void* obj, int32_t off)1440
int32_t aowl_nu_set_ref(void* obj, int32_t off, void* val)1450
float aowl_nu_get_f32(void* obj, int32_t off, int32_t* ok)1463

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