Appearance
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_HAOWL_NU_ADDCOMPONENT_GENAOWL_NU_CANVAS_GET_RENDERMODEAOWL_NU_CANVAS_SET_RENDERMODEAOWL_NU_CANVAS_SET_SORTORDERAOWL_NU_GO_ACTIVE_INHIERAOWL_NU_GO_CTOR_STRINGAOWL_NU_GO_GET_TRANSFORMAOWL_NU_GO_SETACTIVEAOWL_NU_GO_SET_LAYERAOWL_NU_GR_GET_CANVASRENDAOWL_NU_GR_SETALLDIRTYAOWL_NU_GR_SET_COLORAOWL_NU_INPUT_GET_MOUSEBTNAOWL_NU_INPUT_GET_MOUSEPOSAOWL_NU_KIND_BUTTONAOWL_NU_KIND_CANVASAOWL_NU_KIND_IMAGEAOWL_NU_KIND_RECTTRANSFORMAOWL_NU_KIND_TMPTEXTAOWL_NU_LOC_SET_LABEL_TEXTAOWL_NU_MAX_EXTENTAOWL_NU_MAX_FAULTSAOWL_NU_MAX_INTERNAOWL_NU_MAX_INTERN_LENAOWL_NU_MAX_KINDSAOWL_NU_MAX_OWNEDAOWL_NU_META_INITAOWL_NU_MIN_EXTENTAOWL_NU_OBJ_ALIVEAOWL_NU_OBJ_DESTROYAOWL_NU_OBJ_GET_NAMEAOWL_NU_OBJ_INSTANTIATEAOWL_NU_RENDERMODE_CAMERAAOWL_NU_RENDERMODE_OVERLAYAOWL_NU_RENDERMODE_UNKNOWNAOWL_NU_RENDERMODE_WORLDAOWL_NU_REVERIFY_MSAOWL_NU_RT_GET_ANCHOREDPOSAOWL_NU_RT_GET_RECTAOWL_NU_RT_GET_SIZEDELTAAOWL_NU_RT_SET_ANCHOREDPOSAOWL_NU_RT_SET_ANCHORMAXAOWL_NU_RT_SET_ANCHORMINAOWL_NU_RT_SET_PIVOTAOWL_NU_RT_SET_SIZEDELTAAOWL_NU_SET_PARENT_ALIGNAOWL_NU_SLOT_ATTESTEDAOWL_NU_SLOT_NOREFAOWL_NU_SLOT_POISONEDAOWL_NU_SLOT_UNKNOWNAOWL_NU_SLOT_VERIFIEDAOWL_NU_TARGET_COUNTAOWL_NU_TMP_GET_TEXTAOWL_NU_TMP_SET_FONTSIZEAOWL_NU_TMP_SET_TEXTAOWL_NU_TR_GET_LOSSYSCALEAOWL_NU_TR_GET_POSITIONAOWL_NU_TR_SETASFIRSTSIBAOWL_NU_TR_SETPARENT2AOWL_NU_WHY_BADINDEXAOWL_NU_WHY_DISABLEDAOWL_NU_WHY_MISMATCHAOWL_NU_WHY_NOT_COMMITAOWL_NU_WHY_NOT_EXECAOWL_NU_WHY_NO_MODULEAOWL_NU_WHY_OKAOWL_NU_WHY_PROFULL
Types
struct AowlNuInternstruct AowlNuKindstruct AowlNuTarget
Functions
| Signature | Line |
|---|---|
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 |

