Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/host-stack-abi-tests.md
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 24 additions & 0 deletions tests/hle_stack_abi_call.S
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions tests/hle_stack_abi_probe.c
Original file line number Diff line number Diff line change
@@ -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 <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/mman.h>
#include <time.h>
#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<top))==switched);
assert((uintptr_t)pthread_getspecific(key)==(switched?top:0));
uint64_t adapted=probe_horizon_call(w);
assert(adapted==(((uint64_t)9<<32)|10));
assert(((observed_sp>=(uintptr_t)stack)&&(observed_sp<top))==switched);
assert((uintptr_t)pthread_getspecific(key)==(switched?top:0));
cases++;
}
}
runtime_hle_trace_set(0);assert(!pthread_key_delete(key));assert(!munmap(stack,length));
printf("{\"status\":\"pass\",\"configurations\":%u,\"narrow_stack_argument_mismatch_demonstrated\":true,\"wide_slot_adapter_correct\":true,\"host_stack_switch_and_trace_checked\":true}\n",cases);
}
33 changes: 33 additions & 0 deletions tests/test_hle_stack_abi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Native Horizon/Darwin stack argument contract, through the production host-stack bridge."""
import json
import platform
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest

ROOT = Path(__file__).resolve().parents[1]


@unittest.skipUnless(sys.platform == 'darwin' and platform.machine() == 'arm64',
'requires Apple silicon and Darwin thread-specific slots')
class HostStackABI(unittest.TestCase):
def test_horizon_slots_switch_trace_and_darwin_adapter(self):
with tempfile.TemporaryDirectory() as directory:
binary = Path(directory) / 'hle-stack-abi'
command = ['xcrun', 'clang', '-std=c11', '-O2', '-Wall', '-Wextra', '-Werror',
str(ROOT / 'games/totk/runtime/hle_stack.c'),
str(ROOT / 'games/totk/runtime/hle_stack.S'),
str(ROOT / 'tests/hle_stack_abi_probe.c'),
str(ROOT / 'tests/hle_stack_abi_call.S'), '-o', str(binary)]
built = subprocess.run(command, capture_output=True, text=True, timeout=90)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=15)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
result = json.loads(ran.stdout)
self.assertEqual(result['status'], 'pass')
self.assertEqual(result['configurations'], 6)
self.assertTrue(result['narrow_stack_argument_mismatch_demonstrated'])
self.assertTrue(result['wide_slot_adapter_correct'])
self.assertTrue(result['host_stack_switch_and_trace_checked'])