From 21e5e9bad7609d6ac4f7a93d2645df5a913b693e Mon Sep 17 00:00:00 2001 From: baromas Date: Sat, 7 Nov 2026 22:43:16 +0300 Subject: [PATCH] test: verify Horizon stack arguments through the host bridge Isolated from 41f4cb10 for review on the OoT bring-up branch. --- docs/host-stack-abi-tests.md | 42 +++++++++++++++++++++++++++++++ tests/hle_stack_abi_call.S | 24 ++++++++++++++++++ tests/hle_stack_abi_probe.c | 49 ++++++++++++++++++++++++++++++++++++ tests/test_hle_stack_abi.py | 33 ++++++++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 docs/host-stack-abi-tests.md create mode 100644 tests/hle_stack_abi_call.S create mode 100644 tests/hle_stack_abi_probe.c create mode 100644 tests/test_hle_stack_abi.py diff --git a/docs/host-stack-abi-tests.md b/docs/host-stack-abi-tests.md new file mode 100644 index 000000000..192d6e37e --- /dev/null +++ b/docs/host-stack-abi-tests.md @@ -0,0 +1,42 @@ +# Native host-stack ABI contract + +Run `python -m unittest discover -s tests -p test_hle_stack_abi.py -v` on an Apple silicon Mac. This builds the real +`runtime/hle_stack.c` and `runtime/hle_stack.S`, with a synthetic assembly caller. No game data, decoder, window, +GPU or profile is used. It also runs in the root unittest suite and skips unsupported hosts. + +## Why this matters for SDK adapters + +Horizon and Darwin use the same ARM64 instructions but do not place every argument identically. Horizon gives the +ninth and tenth integer arguments separate eight-byte stack slots. Darwin packs two trailing `uint32_t` arguments +into offsets 0 and 4. The runtime's generic host-stack switch copies the caller's stack slots; it cannot infer the +types of an arbitrary target function and repack them. + +The negative control deliberately passes values 9 and 10 in Horizon-style slots to a Darwin function with trailing +`uint32_t` parameters. The first argument arrives as 9 and the second reads the padding at offset 4 (zero in this +fixture), rather than 10 at offset 8. This mismatch is expected and is not a claim of a new failing game path. + +An adapter accepting two full-width trailing slots, then narrowing their low words, receives both values correctly. +SDK 22's AV1 transfer-memory initializer has ten parameters, including two trailing enums; an implementation must +account for their guest ABI. A local inspection of the v131072 SDK confirmed caller-SP offsets 0 and 8 for these two +parameters; no game instructions or bytes are included in the test. A successful synthetic AV1 decode alone does not +check this boundary. [Mario Kart World's independent codec backend](https://github.com/ly05010419/MacNativeGames/blob/5843ee56f3ab6911e32d61d46e96fee4ce8137de/games/mkw/runtime/codec_host.c) +also uses full-width trailing parameters for guest stack arguments; its SDK structure +layouts and signatures must not be assumed to match a different SDK. + +## What the probe checks + +- The eight register arguments and the two stack arguments. +- No host-stack switch versus an allocated host stack, with tracing Off, counting and first-call logging: six configurations. +- Execution takes place on the expected stack and the Darwin host-stack slot is restored after each call. +- Repeated wrapper lookup preserves function identity; a null target remains null. +- The narrow negative control and the full-width adapter behave as described in all six configurations. + +This is a test-only change. It does not alter the generic bridge, add AV1 handlers, or establish movie playback +correctness. It does not cover floating-point arguments, aggregate returns or every Horizon/Darwin ABI difference. + +## Validation record + +MacBook Pro M3 Pro, macOS 27.0, Xcode 27.2: the focused native test passes all six configurations. The full root suite +reports 1458 tests, 169 skipped, no failures/errors. Five skips were due to missing optional fontTools in the isolated +environment; after installing fontTools 4.67.0, those five named tests were rerun and all passed. The other 164 skips +are 163 Tk-dependent tests and the opt-in real Metal trace. The game runtime and its graphics code are unchanged. diff --git a/tests/hle_stack_abi_call.S b/tests/hle_stack_abi_call.S new file mode 100644 index 000000000..212b41b23 --- /dev/null +++ b/tests/hle_stack_abi_call.S @@ -0,0 +1,24 @@ +.text +.globl _probe_horizon_call +.p2align 2 +_probe_horizon_call: + sub sp, sp, #64 + stp x29, x30, [sp, #32] + mov x29, sp + mov x16, x0 + mov x0, #1 + mov x1, #2 + mov x2, #3 + mov x3, #4 + mov x4, #5 + mov x5, #6 + mov x6, #7 + mov x7, #8 + mov x9, #9 + str x9, [sp] + mov x9, #10 + str x9, [sp, #8] + blr x16 + ldp x29, x30, [sp, #32] + add sp, sp, #64 + ret diff --git a/tests/hle_stack_abi_probe.c b/tests/hle_stack_abi_probe.c new file mode 100644 index 000000000..b7b0082aa --- /dev/null +++ b/tests/hle_stack_abi_probe.c @@ -0,0 +1,49 @@ +/* CPU-only host ABI experiment: Horizon's two stack slots vs Darwin's packed uint32 arguments. + Uses production hle_stack.c/.S; no game code, decoder, profile or window. */ +#include +#include +#include +#include +#include +#include +#include +#include "../games/totk/runtime/runtime.h" +extern uint64_t probe_horizon_call(void *); +_Noreturn void runtime_fail(const char *why) { fprintf(stderr,"%s\n",why);exit(2); } +uint64_t runtime_now_ns(void) { struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (uint64_t)t.tv_sec*1000000000+t.tv_nsec; } +void runtime_thread_note_hle(unsigned index) { (void)index; } +uint32_t runtime_thread_current_handle(void) { return 0; } +#define FIRST8 uint64_t a,uint64_t b,uint64_t c,uint64_t d,uint64_t e,uint64_t f,uint64_t g,uint64_t h +static uintptr_t observed_sp; +static __attribute__((noinline)) uint64_t narrow(FIRST8,uint32_t nine,uint32_t ten) { + assert(a+b+c+d+e+f+g+h==36);__asm__ volatile("mov %0, sp":"=r"(observed_sp));return ((uint64_t)nine<<32)|ten; +} +static __attribute__((noinline)) uint64_t wide(FIRST8,uint64_t nine,uint64_t ten) { + assert(a+b+c+d+e+f+g+h==36);__asm__ volatile("mov %0, sp":"=r"(observed_sp));return ((uint64_t)(uint32_t)nine<<32)|(uint32_t)ten; +} +int main(void) { + pthread_key_t key;assert(!pthread_key_create(&key,NULL)&&key); + runtime_hle_stack_init((uint64_t)key*8); + size_t length=1u<<20;uint8_t *stack=mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_ANON|MAP_PRIVATE,-1,0);assert(stack!=MAP_FAILED); + uintptr_t top=(uintptr_t)stack+length-64; + void *n=runtime_hle_wrap(narrow),*w=runtime_hle_wrap(wide); + assert(runtime_hle_wrap(wide)==w&&runtime_hle_wrap(NULL)==NULL); + unsigned cases=0; + for(int trace=0;trace<3;trace++) { + runtime_hle_trace_set(trace); + for(int switched=0;switched<2;switched++) { + runtime_hle_stack_set(switched?top:0); + uint64_t raw=probe_horizon_call(n); + assert(raw==((uint64_t)9<<32)); /* Darwin reads the padding at +4 as the tenth argument. */ + assert(((observed_sp>=(uintptr_t)stack)&&(observed_sp=(uintptr_t)stack)&&(observed_sp