An implementation of the Wii U Cafe OS coreinit API on top of
libnx, so that statically
recompiled Wii U code can run natively on Nintendo Switch homebrew.
=======
| Corpus | coreinit symbols | Title requirements covered |
|---|---|---|
| Black Ops 2 (Wii U) | 100% | 162 / 162 |
| How to Train Your Dragon 2 (Wii U) | 100% | 179 / 179 combined |
| Homebrew (4 titles) | 61.3% | 180 / 347 |
Complete coreinit coverage on two independent commercial titles. The second was never targeted — it came in at 95% before any work, and the 13 missing symbols took one session.
Every existing Wii U reimplementation — Cemu, decaf-emu — targets PC. Nobody has implemented Cafe OS on Horizon.
That matters, because Cafe OS and Horizon are both Nintendo RTOS designs built a few years apart, and their primitives correspond almost one to one. Implementing Cafe OS on libnx is closer to writing an adapter between two related operating systems than to writing an emulator.
Static recompilation tools such as DolRecomp can translate Wii U PowerPC code to C — but they are explicitly CPU-code-only and ship no runtime. This project aims to be part of that missing runtime.
A port. Black Ops 2 imports 440 symbols from 16 libraries; coreinit is 162
of them. Still absent: gx2 (102 symbols — a graphics driver, not a
function list), snd_core (42), padscore (25), the networking families,
and the entire static recompilation layer that would turn PowerPC code into
something Horizon can execute.
What is done is the foundation those all sit on: threads, synchronisation, memory, filesystem, timing. Nothing else can be built without it.
When recompiled game code calls OSInitMutex(&mutex), it passes a pointer
to a structure it allocated itself, in its own memory, with the Wii U
layout and in big-endian byte order.
Two approaches were possible:
| Store state inside the guest struct | Use the struct address as a key | |
|---|---|---|
| Endianness | byte swap on every field access | never reads the struct at all |
| ABI constraint | full field-by-field layout | size only |
| Locking logic | reimplemented from scratch | delegated to the Horizon kernel |
This project takes the second approach. A host-side hash table maps the guest structure's address to a real libnx primitive. The cost is one lookup per call; the benefit is that endianness disappears from the problem and we inherit tested, fast kernel primitives.
The only remaining ABI constraint is size, because the guest allocates the structure and may embed it inside its own structures. Sizes are taken from wut, the Wii U homebrew toolchain, which is the authoritative public source.
OSSystemInfo is the first exception: its contents matter, not just its
size, because the OSTimerClockSpeed macro expands into game code as
OSGetSystemInfo()->busClockSpeed / 4. Heaps are expected to be the next
place this approach strains, since games inspect heap structures directly.
HandleTable::get() creates the host object on first use rather than
requiring explicit initialisation. Game code may use a primitive that was
statically initialised and never passed to an OSInit* call. Degrading
cleanly beats crashing.
OSInitMutex, OSInitMutexEx, OSLockMutex, OSUnlockMutex,
OSTryLockMutex — OSMutex is 0x2c bytes.
Verified on hardware: Cafe OS mutexes are recursive; the same thread may acquire one repeatedly. Getting this wrong does not fail loudly — it surfaces later as intermittent deadlocks.
OSInitCond, OSInitCondEx, OSWaitCond, OSSignalCond —
OSCondition is 0x1c bytes.
OSSignalCond is a broadcast, mapped to condvarWakeAll. Not an
assumption: wut documents it as equivalent to notify_all.
Verified on hardware: a thread holding the mutex at recursion depth 2 can wait on a condition, be woken by another thread, and resume with its depth intact. Verified by a third thread attempting to acquire the mutex after a single unlock — it must fail. Testing this from the waiting thread would always succeed, since the mutex is recursive.
OSCreateThread, OSResumeThread, OSSuspendThread, OSJoinThread,
OSExitThread, OSGetCurrentThread, OSGetCoreId,
OSSetThreadCleanupCallback — OSThread is 0x6a0 bytes.
Verified on hardware: OSCreateThread creates a thread suspended, as
Cafe OS does — it does not run until OSResumeThread. Argument passing and
the exit value survive the entry point trampoline.
Mapping decisions, all arbitrary and open to revision:
- Stack. The guest-supplied stack pointer is stored but unused; libnx
allocates the host stack, sized from the guest's
stackSizerequest. Recompiled code will need the guest stack as its emulated PPC stack, but how that separates from the ARM64 host stack depends on how the recompiler emits code — unknown at time of writing. Input welcome. - Priority. Cafe OS 0–31 (lower is higher) is mapped linearly onto the
window between the process priority and
0x3F. Relative ordering is preserved, absolute values are not. - Affinity. Cafe OS affinity is a 3-core bitmask; libnx wants a single
core id. First set bit wins,
-2for "any", and a failed create retries on-2rather than giving up.OSGetCoreIdclamps Horizon's core 3 to 2, since it does not exist on Cafe OS. - Entry point. Cafe OS uses
int(int, const char**), libnx usesvoid(void*). A trampoline bridges them and captures the return value. - Main thread. It was never created through
OSCreateThread, so it has no guestOSThread. A synthetic one is registered lazily on firstOSGetCurrentThreadcall. Game code uses the pointer as an identity, not as a structure to inspect.
Not implemented: suspending an already-running thread.
OSSuspendThread maintains the counter but does not stop execution.
OSGetTime, OSGetSystemTime, OSGetTick, OSGetSystemTick,
OSSleepTicks, OSCalendarTimeToTicks, OSTicksToCalendarTime,
__OSSetAbsoluteSystemTime, OSGetSystemInfo, OSGetTitleID —
OSCalendarTime is 0x28, OSSystemInfo is 0x20.
Horizon counts at 19.2 MHz, the Wii U timer at busClockSpeed / 4. The
ratio reduces exactly to 3315/1024 — integer conversion, no floating
point, no overflow before roughly nine years of uptime. Tick-to-nanosecond
conversion reduces exactly to 32000/1989, matching wut's own macro.
Calendar conversion uses the days-from-civil algorithm rather than
gmtime/mktime, to avoid timezone handling and libc global state. Note
that OSCalendarTime::tm_year is the full AD year, not year - 1900.
OSGetTitleID returns 0. There is no real Wii U title ID here; it should
become configurable, set by the port at runtime.
Verified on hardware: the clock is monotonic and advances at the intended rate; calendar conversion round-trips correctly across leap years, weekdays and day-of-year.
Assumed, NOT verified: the Cafe OS epoch (2000-01-01) and the bus clock constant (248.625 MHz). Both are internally consistent with the tests, which means the tests would keep passing even if the constants were wrong. Anyone able to confirm these against decaf-emu or real hardware is very welcome to.
DCFlushRange, DCStoreRange — mapped to armDCacheFlush and
armDCacheClean.
NOT verified. From userspace there is no way to observe the cache; the test only shows that the data is not corrupted. On the Wii U these matter because the GPU reads main memory directly and the CPU cache is not coherent with it. Under Horizon the situation differs, and for recompiled code these may end up closer to no-ops than to real cache operations.
MEMCreateFrmHeapEx, MEMDestroyFrmHeap, MEMAllocFromFrmHeapEx,
MEMFreeToFrmHeap, MEMRecordStateForFrmHeap, MEMFreeByStateToFrmHeap,
MEMAdjustFrmHeap, MEMGetAllocatableSizeForFrmHeapEx —
MEMFrmHeap is 0x4c bytes, MEMHeapHeader 0x40.
The first module that implements rather than maps. A frame heap is a
two-ended stack: allocation moves head up from the start or tail down
from the end, freeing resets either to its limit or back to a recorded
state. No free list, no coalescing.
malloc could not be delegated to: MEMHeapHandle is a pointer to the
guest-supplied region, and returned pointers must fall inside it because
games do arithmetic on them and compare them.
Verified on hardware: allocations land inside the guest region;
alignment up to 256 bytes is honoured even from a deliberately misaligned
head; MEMFreeByStateToFrmHeap restores free space to exactly its prior
value.
Assumed, NOT verified: that a negative alignment means "allocate from
the tail". This is the Revolution SDK convention and the signature takes a
signed int, but it has not been confirmed against decaf-emu.
Deviations from Cafe OS:
- The
MEMFrmHeapstructure at the start of the region is reserved but not populated. Recompiled code would read it big-endian while we write little-endian, so writing it would be worse than leaving it alone. Anything that inspectsdataStart/dataEnddirectly will see garbage. - State records are kept host-side. Cafe OS spends
0x10bytes of heap per recorded state; we spend none, so reported free space is slightly larger afterMEMRecordStateForFrmHeap.
MEMCreateExpHeapEx, MEMDestroyExpHeap, MEMAllocFromExpHeapEx,
MEMFreeToExpHeap, MEMGetTotalFreeSizeForExpHeap,
MEMGetAllocatableSizeForExpHeapEx, MEMSetAllocModeForExpHeap,
MEMGetAllocModeForExpHeap, MEMGetBaseHeapHandle,
MEMSetBaseHeapHandle — MEMExpHeap is 0x54, MEMExpHeapBlock 0x14.
A general allocator over a guest-supplied region: block list, coalescing on free, first-fit and nearest-fit modes, allocation from either end.
Cafe OS puts a 0x14 block header inline before every allocation. We keep
the bookkeeping host-side but charge the same 0x14 anyway. Without it
MEMGetTotalFreeSizeForExpHeap would report more free space than Cafe OS
does, and a game allocating until failure would behave differently.
Over-reporting available memory fails far from its cause.
Verified on hardware: freed blocks are reused; freeing three adjacent blocks coalesces back to exactly the initial free size; alignment up to 256 is honoured; a 100-byte request consumes exactly 120 bytes of region.
Scope is deliberate. The full ExpHeap API includes groups, visitors,
debug modes, block resizing and integrity checks. The census shows games
use six functions. The rest is not implemented — MEMGetSizeForMBlockExpHeap
notably, since it takes only a pointer with no heap handle and would need a
global block registry.
Base heaps do not exist here as they do on Cafe OS, where the system
creates them before the game starts. MEMGetBaseHeapHandle lazily builds an
8 MB expanded heap on host memory. The size is arbitrary and should become
configurable, set by the port.
OSReport, OSReportVerbose, OSReportInfo, OSReportWarn, OSVReport,
OSConsoleWrite, OSFatal, __os_snprintf, OSSavesDone_ReadyToRelease
Output goes through a settable sink (coreinitNxSetLogSink), defaulting to
stdout. OSFatal never returns on Cafe OS — it shows an error screen the
user powers off from — so the default handler logs and blocks. A port will
want coreinitNxSetFatalHandler.
OSSavesDone_ReadyToRelease is a deliberate no-op: nothing here owns the
foreground. It is present because every title calls it.
On symbol collisions with the C library. coreinit exports exit,
_Exit, memcpy, memmove and memset, because on the Wii U it was
the system C library. These are not Nintendo APIs and this project does not
implement them: the linker resolves them against newlib, whose semantics
match and whose ARM64 implementations are better than anything written here.
That is the correct answer rather than a workaround, and it means those
symbols genuinely count as covered. implemented.txt lists them in a
separate section so the distinction stays visible.
Only Cafe OS-specific names need writing — __os_snprintf, memclr — plus
the Green Hills compiler runtime (__ghsLock, __ghs_flock_file,
__cpp_exception_init_ptr), which no host library provides. Those remain
unimplemented.
Note __os_snprintf takes size_t, not uint32_t. On aarch64 those are 64
and 32 bits respectively — unlike int and int32_t, they are genuinely
different types.
OSGetThreadPriority, OSSetThreadPriority, OSGetThreadAffinity,
OSSetThreadAffinity, OSGetThreadName, OSSetThreadName,
OSGetThreadSpecific, OSSetThreadSpecific, OSSetThreadDeallocator
Getters return what the game set, not what Horizon granted. The priority
mapping is lossy — 32 Cafe levels compressed into the available Horizon
window — so querying the kernel and inverting the formula would return a
different number than the one passed to OSCreateThread. Read-modify-write
patterns ("raise yourself one above where you are") would drift on every
iteration. Same reasoning for affinity: the game passes a 3-core mask, we
pick a single core, and the original mask is the only sensible answer.
Thread-specific storage uses a 16-slot thread_local array rather than
hanging off HostThread, so it works for threads we did not create — the
main thread, or anything started by the C library. OSGetThreadSpecific
and OSSetThreadSpecific take only an ID and act on the current thread.
Verified on hardware: slots are genuinely per-thread — a child thread starts with them empty while the parent holds a value.
Not implemented: changing priority or affinity of an already-running thread. Both are recorded and reported back, neither is applied.
FSInit, FSShutdown, FSAddClient, FSDelClient, FSGetClientNum,
FSInitCmdBlock, FSOpenFile, FSCloseFile, FSReadFile,
FSReadFileWithPos, FSGetPosFile, FSSetPosFile, FSGetStatFile
Cafe OS's file API carries three objects most POSIX programmers do not
expect: an FSClient session (0x1700 bytes), an FSCmdBlock per-command
context (0xA80 bytes) — the API was designed asynchronous — and an
FSErrorFlag mask on nearly every call.
Both structures are treated as opaque and never populated. FSFileHandle
is a uint32_t rather than a pointer, so handles are minted here and map
to host FILE* in a side table.
Path mapping is configurable, not hardcoded. Wii U paths look like
/vol/content/...; where that content lives on a Switch SD card is the
port's decision. coreinitNxAddVolumeMapping("/vol/content", "/wiiu/bo2")
registers a prefix translation, applied in one place.
Scope. Only the synchronous read path. The *Async variants do not
appear in the filtered census — games use this API synchronously despite
its asynchronous design. Writing, directories and mounting are not
implemented yet.
Verified on hardware, end to end: a real file written to the SD card,
opened through a /vol/content/... path, read back with correct bytes,
seeked and re-read.
Assumed, NOT verified: that FSReadFile returns the number of items
read rather than FS_STATUS_OK. This is the Cafe OS convention — positive
is a count, negative is an error — but it has not been confirmed against
decaf-emu.
FSWriteFile, FSChangeDir, FSGetCwd, FSOpenDir, FSReadDir,
FSCloseDir, FSMakeDir, FSRemove, FSRename, FSGetLastError,
FSGetLastErrorCodeForViewer
FSChangeDir forced the first real per-client state in the project: a
relative path must resolve against that client's current directory, not a
global one. HostClient now carries a working directory and the last error,
keyed by the guest FSClient pointer.
Note that FS_STATUS_* and FS_ERROR_* are distinct enums — the first is
the call's outcome, the second the diagnostic code retrievable afterwards.
FSGetLastError returns the latter, so operations map their status into it
as they complete.
FSRemove deletes both files and empty directories, matching Cafe OS.
FSReadDir returns FS_STATUS_END at the end of a directory rather than an
error.
Verified on hardware, end to end: write a file, read it back byte for
byte, change directory, open the same file by relative path, list the
directory and find it, rename it, delete it, and confirm that opening a
missing file leaves FS_ERROR_NOT_FOUND in the client's error state.
Still not implemented: mounting and volume enumeration (FSMount,
FSGetMountSource, FSGetVolumeState) and change notifications
(FSSetStateChangeNotification). These concern removable media, which has
no meaningful equivalent here; they will be documented stubs rather than
fake implementations.
OSGetAtomic64, OSSetAtomic64, OSSwapAtomic64,
OSCompareAndSwapAtomic64, OSAddAtomic64, OSAndAtomic64,
OSOrAtomic64, OSXorAtomic64, OSTestAndSetAtomic64,
OSTestAndClearAtomic64
Implemented with compiler intrinsics (__atomic_*, __ATOMIC_SEQ_CST),
not locks: GCC emits native ARM64 atomics directly. Wrapping these in a
mutex would be an order of magnitude slower and would deadlock if a game
used them from an interrupt-like context. Sequential consistency matches
the PowerPC sync/lwarx-stwcx pairs Cafe OS builds these from, which
are full barriers.
Verified on hardware: two threads performing 10,000 increments each on a shared counter end at exactly 20,000 — a non-atomic implementation would lose increments.
Assumed, NOT verified: which functions return the previous value and
which return the new one. OSAddAtomic64 is implemented as returning the
new value, OSSetAtomic64 and the test-and-* pair as returning the old,
following the Revolution SDK convention. Getting these backwards would not
fail any test here — it would silently corrupt a game's counters.
FSMount, FSGetMountSource, FSGetVolumeState,
FSSetStateChangeNotification
FSMount is not a stub: it registers a volume mapping. Mounting an SD
source on /vol/external01 makes paths under that prefix resolve on the
host filesystem, so paths the game constructs afterwards actually work.
This costs no more code than a stub and is considerably more useful.
FSGetVolumeState always reports READY, and state-change notifications
are never fired. There is no removable media here, so nothing can change —
that is the correct behaviour for this environment, not a pretence.
FSOpenFile now distinguishes FS_STATUS_NOT_FILE from
FS_STATUS_NOT_FOUND by stat-ing the path on failure. Games can branch on
the difference.
OSCreateAlarm, OSCreateAlarmEx, OSSetAlarm, OSSetPeriodicAlarm,
OSCancelAlarm, OSCancelAlarms, OSSetAlarmTag, OSSetAlarmUserData,
OSGetAlarmUserData, OSWaitAlarm — OSAlarm is 0x58 bytes.
One scheduler thread for all alarms, not one thread per alarm: a game
may create dozens, and dozens of sleeping threads waste memory and
scheduling. It sleeps on a condition variable with a timeout until the
nearest due alarm, so there is no polling loop, and setting a sooner alarm
signals it to recompute. The thread starts lazily on the first
OSSetAlarm, so a game that never uses alarms pays nothing.
Callbacks are invoked without holding the internal lock. A callback that sets another alarm would otherwise deadlock — the classic failure mode for this kind of code.
The OSContext * passed to callbacks is always nullptr: there is no
PowerPC thread context to hand over, and inventing one would be worse than
admitting its absence.
Verified on hardware: one-shot alarms fire once and not early;
cancelled alarms never fire; periodic alarms repeat and stop on
OSCancelAlarms by tag.
The first version of this module passed every test while being wrong.
Implementation and tests shared the same assumption — that OSSetAlarm
takes an absolute deadline — so they agreed with each other and proved
nothing. wut documents OSSetPeriodicAlarm's start as "the duration
until the alarm should first be triggered": the time is relative.
What caught it was a test designed so it could not pass by coincidence: pass a value that only makes sense as a delta and would be long expired as an absolute instant. It failed, the assumption was wrong, and the code was corrected.
Three assumptions in this repository are still in that state — self-consistent but unverified: the Cafe OS epoch, the bus clock constant, and which atomics return the old value versus the new. Treat them accordingly.
coreinitNxAlarmShutdown stops and joins the scheduler thread. Without it
the process terminates while that thread still sleeps on static state being
destroyed, and Horizon reports an abnormal exit.
This is the project's first explicit teardown. Modules owning resources —
threads, open handles — must expose one, and the port is responsible for
calling it. FSShutdown already plays the same role.
LCAlloc, LCDealloc, LCGetMaxSize, LCGetAllocatableSize,
LCGetUnallocated, LCHardwareIsAvailable, LCEnableDMA, LCDisableDMA,
LCIsDMAEnabled, LCLoadDMABlocks, LCStoreDMABlocks, LCWaitDMAQueue,
LCGetDMAQueueLength
The Espresso can lock 16 KB of its L1 cache and use it as very low latency scratch memory, with a DMA engine moving data to and from main RAM. ARM64 has no equivalent — L1 cannot be locked from userspace and there is no program-controlled DMA.
Real memory is allocated instead, aligned to 64 bytes (the A57 cache line). A stub allocating nothing would make games read garbage: they write data here and read it back. The latency advantage is lost, but it never existed on this hardware.
The 16 KB budget is enforced. LCAlloc fails beyond it, exactly as the
Espresso would. Offering unlimited memory would hide a real constraint until
it surfaced somewhere unfixable.
DMA is synchronous — the copy completes before the call returns, so
LCGetDMAQueueLength is always 0 and LCWaitDMAQueue has nothing to wait
for. An asynchronous queue would add a thread and latency for no benefit.
OSInitSemaphore, OSInitSemaphoreEx, OSGetSemaphoreCount,
OSSignalSemaphore, OSWaitSemaphore, OSTryWaitSemaphore,
OSInitSpinLock, OSAcquireSpinLock, OSTryAcquireSpinLock,
OSTryAcquireSpinLockWithTimeout, OSReleaseSpinLock, and the four
OSUninterruptibleSpinLock_* variants — OSSemaphore 0x20,
OSSpinLock 0x10.
Semaphores are built on mutex + condvar rather than libnx's Semaphore,
because OSGetSemaphoreCount must read the count without modifying it
and libnx does not expose the value. Ten extra lines buys the counter in
plain sight.
Note that OSSignalSemaphore and OSTryWaitSemaphore return the
previous count, not a boolean. wut documents the latter as "if the value
is >0 then it means the call was successful".
Spinlocks are mapped to ordinary mutexes. On Cafe OS they disable interrupts and busy-wait, which suits a system with few cores and cooperative scheduling. Under Horizon interrupts cannot be disabled from userspace, and busy-waiting would be worse than a mutex: the Tegra has fewer usable cores than the Wii U, so a spinning thread steals time from the very thread holding the lock. On preemptive scheduling, spinning is a pessimisation.
The _Uninterruptible variants are therefore identical to the plain ones —
the only difference on Cafe OS was interrupt masking, which does not exist
here.
Verified on hardware: a worker blocks on a zero-count semaphore and
resumes only after OSSignalSemaphore; two threads performing 5,000
guarded increments each end at exactly 10,000.
DCInvalidateRange, DCZeroRange, DCTouchRange, ICInvalidateRange,
OSBlockMove, OSBlockSet, OSCancelThread, OSTestThreadCancel,
OSSetThreadCancelState, OSDetachThread, OSGetStackPointer,
OSBlockThreadsOnExit
Cache ranges are rounded up to 32 bytes, the Espresso line size, as wut documents. A game zeroing one byte expects the whole line zeroed, and may rely on it.
DCTouchRange is a deliberate no-op. On PowerPC it prefetched lines into
cache; faking that with dummy reads would waste bandwidth for a hint the
hardware is free to ignore anyway.
OSBlockMove honours its flush flag — wut notes the function "makes use
of the cache to speed up the copy, so a flush is recommended", so games pass
it expecting the synchronisation. It uses memmove, not memcpy: the
regions may overlap.
Thread cancellation is cooperative, and implemented faithfully. wut
documents that a cancelled thread "will be terminated next time
OSTestThreadCancel is called" — there is no asynchronous kill to emulate.
Which is fortunate: killing a thread mid-flight leaves locks held and memory
unfreed, and libnx rightly offers no way to do it.
Verified on hardware: a worker looping with cancellation points stops
after OSCancelThread instead of running to completion; DCZeroRange on a
single byte clears exactly 32.
OSGetStackPointer returns the host stack pointer. The value is not
comparable to anything on Cafe OS, but games use it for depth estimates and
logging rather than as a meaningful address.
OSInitEvent, OSInitEventEx, OSSignalEvent, OSSignalEventAll,
OSWaitEvent, OSWaitEventWithTimeout, OSResetEvent,
OSInitMessageQueue, OSInitMessageQueueEx, OSSendMessage,
OSReceiveMessage, OSPeekMessage, OSGetSystemMessageQueue —
OSEvent 0x24, OSMessage 0x10, OSMessageQueue 0x3c.
Cafe OS events are Win32-style, and wut's documentation links each function
to its Windows counterpart. Manual-reset events stay signalled until
OSResetEvent; auto-reset events release one waiter and clear themselves.
The message ring is kept host-side rather than written into the buffer
the guest supplies. That buffer lives in guest memory with guest byte order,
and OSMessage holds a pointer plus three words that would need swapping on
every access. Duplicating a few hundred bytes keeps endianness out of the
hot path.
OSGetSystemMessageQueue returns an empty queue. On Cafe OS the system
publishes application lifecycle events there; nothing feeds it here, and an
empty queue lets games proceed rather than blocking on it.
Verified on hardware: two threads waiting on a manual-reset event are
both released by a single OSSignalEvent — with auto-reset semantics only
one would pass; a high-priority message jumps the queue; a non-blocking
receive on an empty queue fails instead of hanging.
Also added: OSIsThreadTerminated, OSIsDebuggerPresent and
OSIsDebuggerInitialized (both false — there is no Cafe OS debugger here,
and games use them only to enable extra logging), OSPanic (routed through
the same handler as OSFatal, with file and line prepended), and
OSMemoryBarrier (a full fence, matching PowerPC sync).
OSDynLoad_Acquire, OSDynLoad_FindExport, OSDynLoad_Release,
OSDynLoad_SetAllocator, OSDynLoad_GetAllocator,
OSDynLoad_GetNumberOfRPLs, OSDynLoad_GetRPLInfo
This looked like the hardest problem in the project: resolving symbols by name at runtime sits awkwardly with static recompilation, which assumes every call target is known ahead of time.
The tension dissolves on inspection. Games do not load arbitrary plugins
through OSDynLoad — they acquire the system RPLs, coreinit, gx2,
nsysnet, which are exactly what this library implements. Nothing needs
loading; the code is already linked in. Acquire becomes a lookup and
FindExport a table search.
source/symbol_registry.cpp holds that table, mapping
("coreinit", "OSGetTime") to the address of our implementation. It doubles
as the authoritative list of the library's public surface — more reliable
than the hand-maintained implemented.txt. It currently holds a
representative subset; completing it is mechanical work, well suited to
outside contributions.
Module names carrying a .rpl suffix are accepted. Acquiring a library we
do not implement returns OS_DYNLOAD_MODULE_NOT_FOUND rather than a handle
that would resolve nothing later — an honest failure beats a broken pointer.
OSDynLoad_GetNumberOfRPLs and OSDynLoad_GetRPLInfo return 0 and FALSE.
That is not a stub: wut documents them as "always returns 0 on release
versions of CafeOS, requires OSGetSecurityLevel() > 0". They never worked
on a retail console, so any game calling them already has a fallback path.
SetAllocator stores the callbacks and never calls them, since nothing is
ever allocated for loading.
Verified on hardware: a function resolved by name through
FindExport is then called through the returned pointer and returns a
valid result — a wrong address would crash rather than answer.
MEMAllocFromDefaultHeap, MEMAllocFromDefaultHeapEx,
MEMFreeToDefaultHeap, __gh_errno_ptr, __cpp_exception_init_ptr,
__cpp_exception_cleanup_ptr, __atexit_cleanup
Some coreinit exports are data, not functions. The default heap allocators are global variables holding function pointers: a game reads the variable and calls through it, and may replace it to hook allocation — a common pattern in commercial engines that track memory usage.
They are therefore exported as real variables, initialised to
implementations backed by the MEM2 base heap. The symbol registry now
distinguishes Function from Data, so OSDynLoad_FindExport finally
honours its exportType parameter, which it previously ignored.
Verified on hardware: replacing MEMAllocFromDefaultHeap with a test
function redirects subsequent allocations through it. A disguised function
could not do that.
The Green Hills C++ exception hooks (__cpp_exception_init_ptr and
friends) are null. We do not know what they do, and inventing behaviour
would be worse than admitting absence — they are there so linking succeeds,
documented as unimplemented for anyone who can find out.
__gh_errno_ptr maps to newlib's __errno(). _iob, environ and
__gh_FOPEN_MAX are not defined here: newlib already provides them, and
they belong in the host-provided section alongside memcpy and exit.
__ghsLock, __ghsUnlock, __ghs_flock_create, __ghs_flock_destroy,
__ghs_flock_file, __ghs_funlock_file, __ghs_ftrylock_file,
__gh_errno_ptr, __gh_get_errno, __gh_set_errno
The Wii U SDK used the Green Hills compiler, and coreinit provided its C
library runtime: the locks that make FILE* operations and errno access
thread-safe. wut does not declare these — they are internal GHS names — so
the reference is decaf-emu's coreinit_ghs.cpp.
Reading that source settled three things guesswork would have got wrong:
__ghsLockuses an uninterruptible spinlock, not a mutex. Spinlocks are not recursive, which tells us the GHS C library never nests this lock — the obvious worry,printftaking it and thenfwritetaking it again, does not happen.- Flocks are indices into a fixed pool of 100 mutexes, not pointers.
__ghs_ftrylock_filereturns 0 on success and 1234 on failure. An arbitrary value, but the real one.
decaf also excludes __ghsLock, __ghsUnlock and __gh_errno_ptr from its
HLE trace by default — what you do for functions called thousands of times a
second. They sit on the hot path of every printf and every errno access,
which is why they are implemented with kernel primitives and nothing more.
__gh_errno_ptr is per-thread on Cafe OS (decaf returns
&thread->context.error). newlib's __errno() is already per-thread, so
this matches without extra work.
UCOpen, UCClose, UCReadSysConfig, UCWriteSysConfig,
UCDeleteSysConfig, MCP_Open, MCP_Close, MCP_GetSysProdSettings
On Cafe OS both families talk to the IOSU coprocessor over IPC. There is no IOSU here, so handles are tokens — Cafe OS accepts any non-negative value.
Where a Cafe OS key has a real equivalent, we read the real thing.
Console language and region come from Horizon's set service, so a game
picking text by system language behaves correctly rather than guessing.
Horizon's language enum is larger than Cafe OS's, so regional variants
(British English, Canadian French, Latin American Spanish) collapse onto
their base language — Cafe OS has no separate code for them.
Fields describing Wii U hardware that does not exist here — serial number, model, EEPROM version — are filled with plausible synthetic values. Games read them for display and region gating, not for anything checkable.
Writes are accepted and discarded. A game changing system configuration should not fail — an error might send it down a recovery path nobody tested — but it has no business altering the Switch's actual settings.
Unknown keys fail per-entry, not per-call, and are logged once. That matters for discovery: the log tells a port developer which keys a title actually asks for, information nobody currently has. The key surface is large and only a handful are implemented.
Note that MCPSysProdSettings::serial_id is a fixed-width 12-character
field with no terminator, so it is filled with memcpy rather than
snprintf. model_number may well be the same on real hardware; it is
written as a terminated string here since games only display it.
tools/rpx-imports/ extracts the named imports from Wii U .rpx files and
aggregates them across a corpus, so that implementation order follows
measured demand rather than intuition.
This is possible because RPX files carry named import tables: Cafe OS
uses dynamic linking, so a title declares exactly which functions it needs
from coreinit, gx2, nsysnet and the rest. No recompilation required —
it is an ELF walk.
python3 tools/rpx-imports/extract_imports.py <file.rpx>
python3 tools/rpx-imports/census.py <corpus-dir> --label homebrewFrequency is counted as number of titles importing a symbol, not total call sites. A function every title needs matters more than one called a thousand times inside a single game. Coverage is weighted the same way, which is why implementing seven well-chosen functions moved it from 9.6% to 14.2%.
Current results are in census/.
An RPX import table lists every stub the linker pulled in, not the
functions the code actually calls. Black Ops 2 declares 959 coreinit
imports — including kernel internals like PPCMfhid0 and
__KernelSetUserModeExHandler that no game calls. That is essentially
coreinit's entire export table.
Relocations are the ground truth: if nothing relocates against a symbol,
nothing calls it. Filtering the symbol table against .rela.* entries
brings Black Ops 2 down to 157 — an 83% reduction — while leaving the
wut homebrew figures untouched, because wut's linker already garbage-
collects unused stubs and Treyarch's did not.
Anyone analysing Wii U binaries should apply this filter. Without it the numbers are an upper bound roughly five times the real demand.
The homebrew corpus put the 25 FSA* functions at the top of the
filesystem priority list. Black Ops 2 calls none of them — it uses the
high-level FS* API (FSInit, FSAddClient, FSInitCmdBlock,
FSOpenFile). FSA* are the low-level primitives wut's own runtime uses
internally.
Implementing the filesystem from homebrew data alone would have meant writing 25 functions the game never calls. This is the clearest argument for measuring against real titles.
4 homebrew titles: 681 distinct imports across 8 libraries, 348 in
coreinit alone.
This corpus measures homebrew, not games. It over-represents
OSScreen, since WHBLogConsole draws through it, and under-represents
GX2, which is how commercial titles actually render. Treat the ranking as
"the floor any Cafe OS binary needs", not as a picture of what games do.
A commercial corpus is the obvious next step. Contributions of import lists from titles you own — the CSV output, never the binaries — are very welcome.
Only derived data is published here: function names and counts. A list of imported symbol names is factual metadata, not game content.
On homebrew, OSScreen (8 functions at 75%) is the last compact group —
and the only graphics code anywhere in this project. Below it, the FSA*
family: the low-level filesystem primitives wut's runtime uses internally,
absent from Black Ops 2.
libc symbol collision. coreinit exports exit, __os_snprintf and
presumably other names that newlib already defines. Implementing them here
would collide at link time. This needs a project-wide linking strategy —
symbol prefixing, ld --wrap, or a freestanding build — not a per-function
workaround. exit appears in 100% of the corpus, so this blocks real
coverage.
Function pointers imported as data. MEMAllocFromDefaultHeap,
MEMAllocFromDefaultHeapEx and MEMFreeToDefaultHeap appear as
.dimport_ entries, meaning games read a function pointer from
coreinit's data segment and call through it. That requires exporting
global variables holding addresses, not function symbols — a mechanism this
project does not yet have.
Guest stacks. See the thread section above. This one needs someone who knows how the recompiler emits code.
Dynamic module loading. OSDynLoad_Acquire and OSDynLoad_FindExport
are used by Black Ops 2. The game resolves symbols at runtime, which sits
awkwardly with static recompilation — that approach assumes everything is
known ahead of time. What gets loaded, and when, needs investigating.
Requires devkitPro with the switch-dev group.
makeProduces coreinit-nx.nro. Copy it to /switch/ on your SD card and run it
from the Homebrew Menu. It runs the test suite and prints results to screen.
Adding a function means, in order:
- Check the census in
census/frequency.csv. If it is not used by any title in the corpus, it is probably not the best use of your time. - Read the semantics in decaf-emu, under
libdecaf/src/cafe/libraries/coreinit/. This is a full HLE implementation and serves as the specification. Do not guess. - Check the ABI size in wut's headers
(
WUT_CHECK_SIZE(TypeName, ...)), and mirror wut's declarations literally, even where types are equivalent. - Map it onto libnx. Prefer libnx kernel primitives over C++ standard
library equivalents — the toolchain builds with
-fno-exceptions, and depending on the standard library's threading support adds a failure mode unrelated to this project. - Write a test that runs on device. Prefer tests where a wrong implementation hangs or fails visibly rather than passing silently.
- Update
tools/rpx-imports/implemented.txtso coverage stays honest. - Open a pull request, stating what you verified on hardware and what you only inferred from source.
That last distinction matters more than anything else here. The value of this repository is not the code — it is knowing which behaviours have actually been confirmed on a real console.
MIT.
Cemu is MPL-2.0 and decaf-emu is GPL. Neither is copied from; both are read as documentation of semantics, and the implementation here is written independently against libnx.