Files
itaskaev2andClaude Opus 5.5 1dfe05679e Expect the base language in the probe's automatic menu language check
resolve_menu_lang falls back from a region tag the catalog lacks to its base language (ru-RU: ru), but probe_menu_text expected English for such a tag, so --probe-services failed on any Windows whose UI language is ru-RU, de-DE and the like; en-US maps directly and passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:20:20 +01:00

1099 lines
64 KiB
C

// --probe-services: every override of ov_system.c, ov_input.c, ov_media.c, swkbd.c and
// ov_gfx.c called the way guest code calls it: by its synthetic address through rt_call / rt_dispatch, with AAPCS64
// arguments, on the guest main thread. Checks that each one matches a relocation of the loaded modules, then the
// values: language, operation mode, notification messages, account, time round trips, controllers, codec state,
// graphics records, the system events behind nfp and vi, the software keyboard with arguments built by the guest SDK
// (answers from TOTK_SWKBD_ANSWER, then the window pressed by TOTK_SWKBD_TEST) and the shared fonts when
// tools/extract_fonts.py has written them. The named fatal stubs, and the account UID of a second run, are checked in
// child processes (this executable again, TOTK_PROBE_SERVICES_CHILD=<case>).
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "amiibo.h"
#include "cfg.h"
#include "fs.h"
#include "gfx.h"
#include "host.h"
#include "input.h"
#include "kernel.h"
#include "ov.h"
#include "platform/platform.h"
#include "utf16.h"
#include "ui_text.h"
#define CHILD_ENV "TOTK_PROBE_SERVICES_CHILD"
#define TRY_WAIT_SYSTEM_EVENT "_ZN2nn2os18TryWaitSystemEventEPNS0_15SystemEventTypeE"
#define DESTROY_SYSTEM_EVENT "_ZN2nn2os18DestroySystemEventEPNS0_15SystemEventTypeE"
typedef struct { const char *key, *name; int fatal; uint64_t addr; int bound, calls; } Ov;
static Ov ovs[] = {
// ov_system.c: nn::oe
{"oe.Initialize", "_ZN2nn2oe10InitializeEv"},
{"oe.GetDesiredLanguage", "_ZN2nn2oe18GetDesiredLanguageEv"},
{"oe.SetPerformanceConfiguration", "_ZN2nn2oe27SetPerformanceConfigurationENS0_15PerformanceModeEi"},
{"oe.SetCpuBoostMode", "_ZN2nn2oe15SetCpuBoostModeENS0_12CpuBoostModeE"},
{"oe.GetOperationMode", "_ZN2nn2oe16GetOperationModeEv"},
{"oe.GetPerformanceMode", "_ZN2nn2oe18GetPerformanceModeEv"},
{"oe.SetFocusHandlingMode", "_ZN2nn2oe20SetFocusHandlingModeENS0_17FocusHandlingModeE"},
{"oe.GetCurrentFocusState", "_ZN2nn2oe20GetCurrentFocusStateEv"},
{"oe.SetResumeNotificationEnabled", "_ZN2nn2oe28SetResumeNotificationEnabledEb"},
{"oe.SetOperationModeChangedNotificationEnabled", "_ZN2nn2oe42SetOperationModeChangedNotificationEnabledEb"},
{"oe.SetPerformanceModeChangedNotificationEnabled", "_ZN2nn2oe44SetPerformanceModeChangedNotificationEnabledEb"},
// applet, am, oe (applet_host.c)
{"applet.GetPreviousProgramIndex", "_ZN2nn6applet23GetPreviousProgramIndexEv"},
{"applet.GetAppletResourceUserId", "_ZN2nn6applet23GetAppletResourceUserIdEv"},
{"am.GetAppletResourceUserId", "_ZN2nn2am23GetAppletResourceUserIdEv"},
{"oe.TryPopLaunchParameter", "_ZN2nn2oe21TryPopLaunchParameterEPmPvm"},
{"oe.GetExpectedVolumeBalance", "_ZN2nn2oe24GetExpectedVolumeBalanceEPfS1_"},
{"oe.SetExpectedVolumeBalance", "_ZN2nn2oe24SetExpectedVolumeBalanceEff"},
{"oe.GetProgramTotalActiveTime", "_ZN2nn2oe25GetProgramTotalActiveTimeEv"},
{"oe.SetUserInactivityDetectionTimeExtended", "_ZN2nn2oe38SetUserInactivityDetectionTimeExtendedEb"},
{"oe.SetUserInactivityDetectionTimeExtendedUnsafe", "_ZN2nn2oe44SetUserInactivityDetectionTimeExtendedUnsafeEb"},
{"oe.FinishStartupLogo", "_ZN2nn2oe17FinishStartupLogoEv"},
{"oe.TryPopNotificationMessage", "_ZN2nn2oe25TryPopNotificationMessageEPj"},
{"oe.PopNotificationMessage", "_ZN2nn2oe22PopNotificationMessageEv"},
{"prepo.Save", "_ZN2nn5prepo10PlayReport4SaveEv"},
{"prepo.SaveUid", "_ZN2nn5prepo10PlayReport4SaveERKNS_7account3UidE"},
{"prepo.RequestImmediateTransmission", "_ZN2nn5prepo28RequestImmediateTransmissionEv"},
{"prepo.AddInternetConnectionStatus", "_ZN2nn5prepo27AddInternetConnectionStatusEPNS0_10PlayReportEPKcS4_S4_"},
{"nfp.Initialize", "_ZN2nn3nfp10InitializeEv"},
{"nfp.Finalize", "_ZN2nn3nfp8FinalizeEv"},
{"nfp.GetState", "_ZN2nn3nfp8GetStateEv"},
{"nfp.ListDevices", "_ZN2nn3nfp11ListDevicesEPNS0_12DeviceHandleEPii"},
{"nfp.AttachAvailabilityChangeEvent", "_ZN2nn3nfp29AttachAvailabilityChangeEventEPNS_2os15SystemEventTypeE"},
{"nfp.GetDeviceState", "_ZN2nn3nfp14GetDeviceStateERKNS0_12DeviceHandleE"},
{"nfp.Mount", "_ZN2nn3nfp5MountERKNS0_12DeviceHandleENS0_9ModelTypeENS0_11MountTargetE"},
{"nfp.GetNpadId", "_ZN2nn3nfp9GetNpadIdEPjRKNS0_12DeviceHandleE"},
{"nfp.AttachActivateEvent", "_ZN2nn3nfp19AttachActivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE"},
{"nfp.AttachDeactivateEvent", "_ZN2nn3nfp21AttachDeactivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE"},
{"nfp.StartDetection", "_ZN2nn3nfp14StartDetectionERKNS0_12DeviceHandleE"},
{"nfp.StopDetection", "_ZN2nn3nfp13StopDetectionERKNS0_12DeviceHandleE"},
{"nfp.GetTagInfo", "_ZN2nn3nfp10GetTagInfoEPNS0_7TagInfoERKNS0_12DeviceHandleE"},
{"nfp.GetModelInfo", "_ZN2nn3nfp12GetModelInfoEPNS0_9ModelInfoERKNS0_12DeviceHandleE"},
{"err.ShowError", "_ZN2nn3err9ShowErrorENS_6ResultE"},
// SDK-internal (section 5.2)
{"nndiagStartup", "nndiagStartup"},
{"aoc.Initialize", "_ZN2nn3aoc10InitializeEv"},
{"aoc.ListAddOnContent", "_ZN2nn3aoc16ListAddOnContentEPiii"},
{"err.InitializeContextWriterApi", "_ZN2nn3err26InitializeContextWriterApiEv"},
{"pctl.Initialize", "_ZN2nn4pctl10InitializeEv"},
{"diag.InitializeApplicationAbortObserver", "_ZN2nn4diag34InitializeApplicationAbortObserverEv"},
// account
{"account.Initialize", "_ZN2nn7account10InitializeEv"},
{"account.TryOpenPreselectedUser", "_ZN2nn7account22TryOpenPreselectedUserEPNS0_10UserHandleE"},
{"account.CloseUser", "_ZN2nn7account9CloseUserERKNS0_10UserHandleE"},
{"account.GetUserId", "_ZN2nn7account9GetUserIdEPNS0_3UidERKNS0_10UserHandleE"},
{"account.GetUserExistence", "_ZN2nn7account16GetUserExistenceEPbRKNS0_3UidE"},
{"account.GetNickname", "_ZN2nn7account11GetNicknameEPNS0_8NicknameERKNS0_3UidE"},
// time
{"time.Initialize", "_ZN2nn4time10InitializeEv"},
{"time.Finalize", "_ZN2nn4time8FinalizeEv"},
{"time.IsInitialized", "_ZN2nn4time13IsInitializedEv"},
{"time.UserClock.GetCurrentTime", "_ZN2nn4time23StandardUserSystemClock14GetCurrentTimeEPNS0_9PosixTimeE"},
{"time.NetworkClock.GetCurrentTime", "_ZN2nn4time26StandardNetworkSystemClock14GetCurrentTimeEPNS0_9PosixTimeE"},
{"time.SteadyClock.now", "_ZN2nn4time19StandardSteadyClock3nowEv"},
{"time.ToCalendarTime",
"_ZN2nn4time14ToCalendarTimeEPNS0_12CalendarTimeEPNS0_22CalendarAdditionalInfoERKNS0_9PosixTimeE"},
{"time.ToCalendarTimeInUtc", "_ZN2nn4time19ToCalendarTimeInUtcERKNS0_9PosixTimeE"},
{"time.ToPosixTimeFromUtc", "_ZN2nn4time18ToPosixTimeFromUtcERKNS0_12CalendarTimeE"},
{"time.ClockSnapshot.C1", "_ZN2nn4time13ClockSnapshotC1Ev"},
{"time.ClockSnapshot.Create", "_ZN2nn4time13ClockSnapshot29CreateWithStandardSystemClockEPS1_"},
{"time.CalculateSpanBetween", "_ZN2nn4time20CalculateSpanBetweenEPNS_8TimeSpanERKNS0_13ClockSnapshotES5_"},
{"time.UserDifference",
"_ZN2nn4time48CalculateStandardUserSystemClockDifferenceByUserERKNS0_13ClockSnapshotES3_"},
// pl
{"pl.GetSharedFontLoadState", "_ZN2nn2pl22GetSharedFontLoadStateENS0_14SharedFontTypeE"},
{"pl.GetSharedFontAddress", "_ZN2nn2pl20GetSharedFontAddressENS0_14SharedFontTypeE", 1},
{"pl.GetSharedFontSize", "_ZN2nn2pl17GetSharedFontSizeENS0_14SharedFontTypeE", 1},
// ov_input.c
{"hid.InitializeNpad", "_ZN2nn3hid14InitializeNpadEv"},
{"hid.SetSupportedNpadIdType", "_ZN2nn3hid22SetSupportedNpadIdTypeEPKjm"},
{"hid.SetSupportedNpadStyleSet",
"_ZN2nn3hid24SetSupportedNpadStyleSetENS_4util10BitFlagSetILi32ENS0_12NpadStyleTagEEE"},
{"hid.GetNpadStyleSet", "_ZN2nn3hid15GetNpadStyleSetERKj"},
{"hid.GetNpadStates.FullKey", "_ZN2nn3hid13GetNpadStatesEPNS0_16NpadFullKeyStateEiRKj"},
{"hid.GetNpadStates.JoyDual", "_ZN2nn3hid13GetNpadStatesEPNS0_16NpadJoyDualStateEiRKj"},
{"hid.GetNpadStates.JoyLeft", "_ZN2nn3hid13GetNpadStatesEPNS0_16NpadJoyLeftStateEiRKj"},
{"hid.GetNpadStates.JoyRight", "_ZN2nn3hid13GetNpadStatesEPNS0_17NpadJoyRightStateEiRKj"},
{"hid.GetNpadStates.Handheld", "_ZN2nn3hid13GetNpadStatesEPNS0_17NpadHandheldStateEiRKj"},
{"hid.SetNpadJoyHoldType", "_ZN2nn3hid18SetNpadJoyHoldTypeENS0_15NpadJoyHoldTypeE"},
{"hid.DisconnectNpad", "_ZN2nn3hid14DisconnectNpadERKj"},
{"hid.ShowControllerSupport",
"_ZN2nn3hid21ShowControllerSupportEPNS0_27ControllerSupportResultInfoERKNS0_20ControllerSupportArgE"},
{"hid.InitializeMouse", "_ZN2nn3hid15InitializeMouseEv"},
{"hid.InitializeKeyboard", "_ZN2nn3hid18InitializeKeyboardEv"},
{"hid.GetMouseStates", "_ZN2nn3hid14GetMouseStatesEPNS0_10MouseStateEi"},
{"hid.GetKeyboardState", "_ZN2nn3hid16GetKeyboardStateEPNS0_13KeyboardStateE"},
{"hid.GetVibrationDeviceHandles",
"_ZN2nn3hid25GetVibrationDeviceHandlesEPNS0_21VibrationDeviceHandleEiRKjNS_4util10BitFlagSetILi32ENS0_"
"12NpadStyleTagEEE"},
{"hid.GetSixAxisSensorHandles",
"_ZN2nn3hid23GetSixAxisSensorHandlesEPNS0_19SixAxisSensorHandleEiRKjNS_4util10BitFlagSetILi32ENS0_"
"12NpadStyleTagEEE"},
{"hid.InitializeVibrationDevice", "_ZN2nn3hid6detail25InitializeVibrationDeviceERKNS0_21VibrationDeviceHandleE"},
{"hid.SendVibrationValue",
"_ZN2nn3hid6detail18SendVibrationValueERKNS0_21VibrationDeviceHandleERKNS0_14VibrationValueE"},
{"hid.SendVibrationValues",
"_ZN2nn3hid6detail19SendVibrationValuesEPKNS0_21VibrationDeviceHandleEPKNS0_14VibrationValueEi"},
{"hid.GetActualVibrationValue",
"_ZN2nn3hid6detail23GetActualVibrationValueEPNS0_14VibrationValueERKNS0_21VibrationDeviceHandleE"},
{"hid.GetVibrationDeviceInfo",
"_ZN2nn3hid6detail22GetVibrationDeviceInfoEPNS0_19VibrationDeviceInfoERKNS0_21VibrationDeviceHandleE"},
{"hid.IsVibrationDeviceMounted", "_ZN2nn3hid6detail24IsVibrationDeviceMountedEPbRKNS0_21VibrationDeviceHandleE"},
{"hid.IsVibrationPermitted", "_ZN2nn3hid6detail20IsVibrationPermittedEPb"},
{"hid.PermitVibration", "_ZN2nn3hid6detail15PermitVibrationEb"},
{"hid.StartSixAxisSensor", "_ZN2nn3hid6detail18StartSixAxisSensorERKNS0_19SixAxisSensorHandleE"},
{"hid.StopSixAxisSensor", "_ZN2nn3hid6detail17StopSixAxisSensorERKNS0_19SixAxisSensorHandleE"},
{"hid.GetSixAxisSensorStates",
"_ZN2nn3hid6detail22GetSixAxisSensorStatesEPiPNS0_18SixAxisSensorStateEiRKNS0_19SixAxisSensorHandleE"},
{"hid.GetSixAxisSensorState",
"_ZN2nn3hid6detail21GetSixAxisSensorStateEPNS0_18SixAxisSensorStateERKNS0_19SixAxisSensorHandleE"},
// ov_media.c
{"codec.GetWorkBufferSize", "_ZN2nn5codec36GetHardwareOpusDecoderWorkBufferSizeEiii"},
{"codec.Initialize", "_ZN2nn5codec29InitializeHardwareOpusDecoderEPNS0_23HardwareOpusDecoderTypeEiiPvmi"},
{"codec.Finalize", "_ZN2nn5codec27FinalizeHardwareOpusDecoderEPNS0_23HardwareOpusDecoderTypeE"},
{"codec.GetSampleRate", "_ZN2nn5codec32GetHardwareOpusDecoderSampleRateEPKNS0_23HardwareOpusDecoderTypeE"},
{"codec.GetChannelCount", "_ZN2nn5codec34GetHardwareOpusDecoderChannelCountEPKNS0_23HardwareOpusDecoderTypeE"},
{"codec.Reset", "_ZN2nn5codec24ResetHardwareOpusDecoderEPNS0_23HardwareOpusDecoderTypeE"},
{"codec.Decode", "_ZN2nn5codec33DecodeOpusInterleavedWithHardwareEPNS0_23HardwareOpusDecoderTypeEPmPiPsmPKvm", 1},
{"swkbd.ShowKeyboard", "_ZN2nn5swkbd12ShowKeyboardEPNS0_6StringERKNS0_15ShowKeyboardArgE", 1},
// ov_gfx.c
{"nv.SetGraphicsAllocator", "_ZN2nv20SetGraphicsAllocatorEPFPvmmS0_EPFvS0_S0_EPFS0_S0_mS0_ES0_"},
{"nv.InitializeGraphics", "_ZN2nv18InitializeGraphicsEPvm"},
{"nv.SetGraphicsDevtoolsAllocator", "_ZN2nv28SetGraphicsDevtoolsAllocatorEPFPvmmS0_EPFvS0_S0_EPFS0_S0_mS0_ES0_"},
{"vi.Initialize", "_ZN2nn2vi10InitializeEv"},
{"vi.OpenDefaultDisplay", "_ZN2nn2vi18OpenDefaultDisplayEPPNS0_7DisplayE"},
{"vi.CreateLayer", "_ZN2nn2vi11CreateLayerEPPNS0_5LayerEPNS0_7DisplayE"},
{"vi.GetNativeWindow", "_ZN2nn2vi15GetNativeWindowEPPvPNS0_5LayerE"},
{"vi.GetDisplayVsyncEvent", "_ZN2nn2vi20GetDisplayVsyncEventEPNS_2os15SystemEventTypeEPNS0_7DisplayE"},
{"nvnBootstrapLoader", "nvnBootstrapLoader", 1},
};
#define NOV ((int)(sizeof ovs / sizeof *ovs))
static const char *const observers[] = {"_ZN2nn2os21AllocateAddressRegionEPmm", "_ZN2nn2os19AllocateMemoryPagesEmm",
"nnosInitialize", "nninitInitializeSdkModule", "nninitStartup", "nnMain"};
#define NOBS ((int)(sizeof observers / sizeof *observers))
// Fatal stubs, each in its own child process: the case name, the override, what its FATAL line must say and an
// environment variable the child runs with (the shared fonts: a font directory that does not exist).
#define NO_FONTS "TOTK_SHARED_FONTS=:probe-services:no-such-dir"
static const struct { const char *child, *key, *fatal, *env; } fatals[] = {
{"opus", "codec.Decode", "FATAL: nn::codec::DecodeOpusInterleavedWithHardware"},
{"font-address", "pl.GetSharedFontAddress", "FATAL: nn::pl::GetSharedFontAddress(0)", NO_FONTS},
{"font-size", "pl.GetSharedFontSize", "FATAL: nn::pl::GetSharedFontSize(0)", NO_FONTS},
};
#define NFATAL ((int)(sizeof fatals / sizeof *fatals))
static int failures, checks;
#define CHECK(cond, ...) \
do { checks++; if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
static Cpu *gc; // the guest thread the calls run on
static uint64_t probe_uid[2];
static Ov *ov(const char *key) {
for (int i = 0; i < NOV; i++)
if (!strcmp(ovs[i].key, key)) return &ovs[i];
rt_fatal("probe-services: no case %s", key);
}
static uint64_t U(const void *p) { return (uint64_t)(uintptr_t)p; }
// The override called through its synthetic address with x0..x7 = a[0..7]; caller-saved registers hold garbage, the
// callee-saved ones a pattern that must survive. Returns x0.
static uint64_t call8(const char *key, const uint64_t a[8]) {
Ov *o = ov(key);
for (int r = 8; r < 19; r++) gc->x[r] = 0xdead0000u + r;
for (int r = 19; r < 30; r++) gc->x[r] = 0x1900u + r;
uint64_t sp = gc->sp;
uint64_t v = rt_call(gc, o->addr, 8, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]);
int kept = gc->sp == sp;
for (int r = 19; r < 30; r++) kept &= gc->x[r] == 0x1900u + r;
CHECK(kept, "%s lost a callee-saved register or sp", key);
o->calls++;
return v;
}
#define CALL(key, ...) call8(key, (const uint64_t[8]){__VA_ARGS__})
static void set_s(int i, float f) {
uint32_t b;
memcpy(&b, &f, 4);
gc->v[i] = b;
}
static void bind_addresses(void) {
for (int i = 0; i < NOV; i++) {
GuestFn fn = hostfn_override(ovs[i].name);
if (!fn) rt_fatal("probe-services: no HOST_OVERRIDE for %s", ovs[i].name);
ovs[i].addr = hostfn_register(ovs[i].name, fn);
}
}
static void *guest_alloc(uint64_t n) { return mem_kernel_alloc(n, 16); }
// ---- the guest-thread part
static char *lang(void) {
static char s[9];
uint64_t v = CALL("oe.GetDesiredLanguage", 0);
memcpy(s, &v, 8);
s[8] = 0;
return s;
}
static void set_env(const char *name, const char *value) { plat_setenv(name, value); } // NULL removes it
static void probe_oe(void) {
char *keep_lang = cfg_get("TOTK_LANG") ? strdup(cfg_get("TOTK_LANG")) : NULL;
char *keep_locale = cfg_get("TOTK_LOCALE") ? strdup(cfg_get("TOTK_LOCALE")) : NULL;
static const struct { const char *lang, *locale, *want; } cases[] = {
{NULL, "en-US", "en-US"}, {NULL, "be-BY", "en-US"}, {NULL, "xx", "en-US"}, {NULL, "ja-JP", "ja"},
{NULL, "fr-CA", "fr-CA"}, {NULL, "es-MX", "es-419"}, {NULL, "es_ES", "es"}, {NULL, "zh-Hant-TW", "zh-Hant"},
{NULL, "zh-CN", "zh-Hans"}, {NULL, "en-GB", "en-GB"}, {NULL, "ru", "ru"}, {"ja", "fr-FR", "ja"},
{"en-US", NULL, "en-US"}, {"zh-CN", NULL, "zh-Hans"}, {"zh-TW", NULL, "zh-Hant"},
{"zh-Hans", NULL, "zh-Hans"}, {"zh-Hant", NULL, "zh-Hant"}};
int bad = 0;
printf("oe: configured language (TOTK_LANG=%s) is %s\n", keep_lang ? keep_lang : "", lang());
for (size_t i = 0; i < sizeof cases / sizeof *cases; i++) {
set_env("TOTK_LANG", cases[i].lang);
set_env("TOTK_LOCALE", cases[i].locale);
const char *got = lang();
if (strcmp(got, cases[i].want)) {
bad++;
printf(" FAIL: language for TOTK_LANG=%s TOTK_LOCALE=%s is %s, not %s\n", cases[i].lang ? cases[i].lang : "",
cases[i].locale ? cases[i].locale : "", got, cases[i].want);
}
}
set_env("TOTK_LANG", NULL);
set_env("TOTK_LOCALE", NULL);
char sys[16];
snprintf(sys, sizeof sys, "%s", lang());
set_env("TOTK_LOCALE", "en-US");
char def[16];
snprintf(def, sizeof def, "%s", lang());
set_env("TOTK_LANG", keep_lang);
set_env("TOTK_LOCALE", keep_locale);
free(keep_lang);
free(keep_locale);
checks++;
failures += bad > 0;
printf("oe: language %s by default (en-US locale), %s for this user's locale; %d locale/TOTK_LANG cases %s\n",
def, sys, (int)(sizeof cases / sizeof *cases), bad ? "FAILED" : "ok");
CALL("oe.Initialize", 0);
CALL("oe.SetPerformanceConfiguration", 1, 0x92220008);
CALL("oe.SetCpuBoostMode", 0);
uint64_t mode = CALL("oe.GetOperationMode", 0), perf = CALL("oe.GetPerformanceMode", 0);
CHECK((uint32_t)mode == (uint32_t)!ov_handheld() && (uint32_t)perf == (uint32_t)!ov_handheld(),
"operation/performance mode %llu/%llu", (unsigned long long)mode, (unsigned long long)perf);
CALL("oe.SetFocusHandlingMode", 1);
CHECK((uint32_t)CALL("oe.GetCurrentFocusState", 0) == 1, "focus state");
CALL("oe.SetResumeNotificationEnabled", 1);
CALL("oe.SetOperationModeChangedNotificationEnabled", 1);
CALL("oe.SetPerformanceModeChangedNotificationEnabled", 1);
CHECK((int32_t)CALL("applet.GetPreviousProgramIndex", 0) == -1, "previous program index");
CHECK(CALL("applet.GetAppletResourceUserId", 0) == 1 && CALL("am.GetAppletResourceUserId", 0) == 1, "ARUID");
uint64_t size = 99;
uint8_t param[16];
CHECK((uint8_t)CALL("oe.TryPopLaunchParameter", U(&size), U(param), sizeof param) == 0 && size == 0,
"launch parameter");
float l = 0, r = 0;
set_s(0, 0.25f);
set_s(1, 0.75f);
CALL("oe.SetExpectedVolumeBalance", 0);
CALL("oe.GetExpectedVolumeBalance", U(&l), U(&r));
CHECK(l == 0.25f && r == 0.75f, "volume balance %g/%g", (double)l, (double)r);
int64_t t1 = (int64_t)CALL("oe.GetProgramTotalActiveTime", 0);
plat_sleep_ms(2);
int64_t t2 = (int64_t)CALL("oe.GetProgramTotalActiveTime", 0);
CHECK(t1 > 0 && t2 > t1, "program total active time %lld, %lld", (long long)t1, (long long)t2);
CALL("oe.SetUserInactivityDetectionTimeExtended", 1);
CALL("oe.SetUserInactivityDetectionTimeExtendedUnsafe", 1);
CALL("oe.FinishStartupLogo", 0);
}
static void late_push(void *p) {
(void)p;
plat_sleep_ms(50);
oe_push_message(0x21);
return;
}
static void probe_messages(void) {
uint32_t m = 0;
CHECK((uint8_t)CALL("oe.TryPopNotificationMessage", U(&m)) == 0, "a message before any was pushed");
oe_push_message(0x1f);
oe_push_message(0x20);
CHECK((uint8_t)CALL("oe.TryPopNotificationMessage", U(&m)) == 1 && m == 0x1f, "TryPop after a push: %#x", m);
CHECK((uint32_t)CALL("oe.PopNotificationMessage", 0) == 0x20, "Pop after a push");
PlatThread *h = plat_thread_new(late_push, NULL, "late push");
uint64_t t0 = kern_now_ns();
uint32_t late = (uint32_t)CALL("oe.PopNotificationMessage", 0);
uint64_t waited = kern_now_ns() - t0;
plat_thread_join(h);
CHECK(late == 0x21 && waited >= 40000000, "blocking Pop got %#x after %llu ms", late,
(unsigned long long)(waited / 1000000));
printf("oe: notification messages pushed then popped (TryPop, Pop, Pop blocked %llu ms until a host push): %s\n",
(unsigned long long)(waited / 1000000), late == 0x21 ? "ok" : "FAILED");
}
// A system event bound by an override: not signalled, then signalled through the kernel, then (auto-clear) cleared.
static int event_works(uint64_t ev, int autoclear) {
static uint64_t try_wait;
if (!try_wait && !(try_wait = ldr_symbol(TRY_WAIT_SYSTEM_EVENT))) rt_fatal("probe-services: no %s", TRY_WAIT_SYSTEM_EVENT);
int before = (uint8_t)rt_call(gc, try_wait, 1, ev);
kern_signal_system_event(ev);
int after = (uint8_t)rt_call(gc, try_wait, 1, ev);
int again = (uint8_t)rt_call(gc, try_wait, 1, ev);
return !before && after && again == !autoclear;
}
// A manual-clear system event an override signalled when binding it: set, and still set on a second wait.
static int event_set(uint64_t ev) {
static uint64_t try_wait;
if (!try_wait && !(try_wait = ldr_symbol(TRY_WAIT_SYSTEM_EVENT))) rt_fatal("probe-services: no %s", TRY_WAIT_SYSTEM_EVENT);
return (uint8_t)rt_call(gc, try_wait, 1, ev) && (uint8_t)rt_call(gc, try_wait, 1, ev);
}
static void probe_applet(void) {
uint8_t *report = guest_alloc(0x100);
uint64_t uid[2] = {1, 2};
CHECK(CALL("prepo.Save", U(report)) == 0 && CALL("prepo.SaveUid", U(report), U(uid)) == 0, "PlayReport::Save");
CHECK(CALL("prepo.RequestImmediateTransmission", 0) == 0, "RequestImmediateTransmission");
CHECK(CALL("prepo.AddInternetConnectionStatus", U(report), U("a"), U("b"), U("c")) == 0,
"AddInternetConnectionStatus");
CHECK(CALL("nfp.Initialize", 0) == 0 && (uint32_t)CALL("nfp.GetState", 0) == 1, "nfp initialize");
uint8_t handles[64];
int32_t count = 0;
CHECK((uint32_t)CALL("nfp.ListDevices", U(handles), U(&count), 4) == 0 && count == 1, "nfp ListDevices");
uint64_t ev = U(guest_alloc(0x100));
CHECK(CALL("nfp.AttachAvailabilityChangeEvent", ev) == 0, "AttachAvailabilityChangeEvent");
// The emulated reader is there from the start (amiibo.c), so the availability event is set when it is attached.
int nfp_event = event_set(ev);
CHECK(nfp_event, "the nfp availability event is not set for the emulated reader, or clears itself");
CHECK((uint32_t)CALL("nfp.GetDeviceState", U(handles)) == 0, "nfp device state");
static const char *const none[] = {"nfp.Mount", "nfp.GetTagInfo", "nfp.GetModelInfo"};
uint8_t out[0x100];
for (size_t i = 0; i < sizeof none / sizeof *none; i++) {
int first_is_handle = !strcmp(none[i], "nfp.Mount");
uint64_t r = first_is_handle ? CALL(none[i], U(handles), 0, 0) : CALL(none[i], U(out), U(handles));
CHECK((uint32_t)r == 0x8073, "%s: %#x, not device-not-found", none[i], (uint32_t)r);
}
CHECK((uint32_t)CALL("nfp.GetNpadId", U(out), U(handles)) == 0, "nfp GetNpadId");
uint64_t activate = U(guest_alloc(0x100)), deactivate = U(guest_alloc(0x100));
CHECK(CALL("nfp.AttachActivateEvent", activate, U(handles)) == 0, "nfp AttachActivateEvent");
CHECK(CALL("nfp.AttachDeactivateEvent", deactivate, U(handles)) == 0, "nfp AttachDeactivateEvent");
CHECK(event_works(activate, 0), "the nfp activate event is set without an amiibo on the reader");
CHECK((uint32_t)CALL("nfp.StartDetection", U(handles)) == 0, "nfp StartDetection");
CHECK((uint32_t)CALL("nfp.StopDetection", U(handles)) == 0, "nfp StopDetection");
// The game attaches again before each detection: DestroySystemEvent on the attached event, then Attach* on the
// same SystemEventType. Both events must work again afterwards.
static uint64_t destroy;
if (!destroy && !(destroy = ldr_symbol(DESTROY_SYSTEM_EVENT))) rt_fatal("probe-services: no %s", DESTROY_SYSTEM_EVENT);
rt_call(gc, destroy, 1, activate);
rt_call(gc, destroy, 1, deactivate);
CHECK(CALL("nfp.AttachActivateEvent", activate, U(handles)) == 0, "nfp AttachActivateEvent again");
CHECK(CALL("nfp.AttachDeactivateEvent", deactivate, U(handles)) == 0, "nfp AttachDeactivateEvent again");
int reattached = event_works(activate, 0) && event_works(deactivate, 0);
CHECK(reattached, "the nfp events do not work after DestroySystemEvent and a second Attach");
CHECK((uint32_t)CALL("nfp.StartDetection", U(handles)) == 0, "nfp StartDetection again");
// An amiibo's NTAG215 header: UID with its two check bytes, static lock 0F E0, capability container F1 10 FF EE.
// The loader takes it and refuses the same file with a plain NDEF container (E1).
static const uint8_t head[16] = {0x04, 0x11, 0x22, 0x88 ^ 0x04 ^ 0x11 ^ 0x22, 0x33, 0x44, 0x55, 0x66,
0x33 ^ 0x44 ^ 0x55 ^ 0x66, 0x48, 0x0F, 0xE0, 0xF1, 0x10, 0xFF, 0xEE};
uint8_t dump[AMIIBO_DUMP_SIZE] = {0};
memcpy(dump, head, sizeof head);
// Link (Archer): character 0x0100, variant 0, figure type 0, model number 0x0353, series 9, format 02
static const uint8_t id[8] = {0x01, 0x00, 0x00, 0x00, 0x03, 0x53, 0x09, 0x02};
memcpy(dump + 0x54, id, sizeof id);
const char *dump_path = "probe-amiibo.bin";
int amiibo_ok = plat_file_write(dump_path, dump, sizeof dump) && amiibo_load_file(dump_path);
// TagInfo carries the 7-byte UID without its check byte; ModelInfo has the series at 3, the model number
// little-endian at 4 and the figure type at 6
uint8_t ti[0x58], mi[0x40];
amiibo_get_tag_info(ti);
amiibo_get_model_info(mi);
static const uint8_t want_uid[7] = {0x04, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
static const uint8_t want_model[7] = {0x01, 0x00, 0x00, 0x09, 0x53, 0x03, 0x00};
CHECK(amiibo_ok && !memcmp(ti, want_uid, 7) && ti[0x0A] == 7, "amiibo TagInfo UID %02x %02x %02x %02x %02x %02x %02x",
ti[0], ti[1], ti[2], ti[3], ti[4], ti[5], ti[6]);
CHECK(amiibo_ok && !memcmp(mi, want_model, 7), "amiibo ModelInfo %02x %02x %02x %02x %02x %02x %02x", mi[0], mi[1],
mi[2], mi[3], mi[4], mi[5], mi[6]);
dump[0x0C] = 0xE1;
amiibo_ok = amiibo_ok && plat_file_write(dump_path, dump, sizeof dump) && !amiibo_load_file(dump_path);
plat_fs_unlink(dump_path);
CHECK(amiibo_ok, "the amiibo loader refuses a dump with the amiibo capability container, or takes a plain NDEF one");
CHECK(CALL("nfp.Finalize", 0) == 0 && (uint32_t)CALL("nfp.GetState", 0) == 0, "nfp finalize");
CALL("err.ShowError", 0x2a8);
CALL("nndiagStartup", 0);
CALL("aoc.Initialize", 0);
CHECK(CALL("aoc.ListAddOnContent", U(out), 0, 8) == 0, "aoc: no add-on content");
CALL("err.InitializeContextWriterApi", 0);
CALL("pctl.Initialize", 0);
CALL("diag.InitializeApplicationAbortObserver", 0);
printf("applet/prepo/nfp/err: launch parameter none, volume balance, play reports local, an NFC reader without "
"an amiibo, availability event (bound through the SDK's CreateSystemEvent) %s, activate and deactivate events "
"destroyed and attached again %s\n", nfp_event ? "ok" : "FAILED", reattached ? "ok" : "FAILED");
}
static void probe_account(void) {
uint64_t h[3] = {0}, uid[2] = {0}, other[2] = {5, 6}, direct[2];
uint8_t exists = 0;
char nick[33];
CALL("account.Initialize", 0);
CHECK((uint8_t)CALL("account.TryOpenPreselectedUser", U(h)) == 1, "TryOpenPreselectedUser");
CHECK(CALL("account.GetUserId", U(uid), U(h)) == 0 && (uid[0] || uid[1]), "GetUserId");
account_uid(direct);
CHECK(uid[0] == direct[0] && uid[1] == direct[1], "GetUserId differs from account_uid");
CHECK(CALL("account.GetUserExistence", U(&exists), U(uid)) == 0 && exists == 1, "GetUserExistence");
CHECK(CALL("account.GetUserExistence", U(&exists), U(other)) == 0 && exists == 0, "GetUserExistence (other)");
CHECK(CALL("account.GetNickname", U(nick), U(uid)) == 0 && !strcmp(nick, "Local"), "GetNickname");
CALL("account.CloseUser", U(h));
CHECK(CALL("account.GetUserId", U(uid), U(h)) != 0, "GetUserId on a closed handle");
probe_uid[0] = direct[0];
probe_uid[1] = direct[1];
}
typedef struct { int16_t year; int8_t month, day, hour, minute, second, pad; } Cal;
typedef struct { int32_t day_of_week, day_of_year; char name[8]; uint8_t dst, pad[3]; int32_t utc_offset; } CalInfo;
static int64_t posix_of(const Cal *c) { return (int64_t)CALL("time.ToPosixTimeFromUtc", U(c)); }
static Cal utc_of(int64_t t) {
uint64_t v = CALL("time.ToCalendarTimeInUtc", U(&t));
Cal c;
memcpy(&c, &v, 8);
return c;
}
static void probe_time(void) {
CHECK(CALL("time.Initialize", 0) == 0 && (uint8_t)CALL("time.IsInitialized", 0) == 1, "time initialize");
int64_t user = 0, net = 0, now = (int64_t)time(NULL);
CHECK(CALL("time.UserClock.GetCurrentTime", U(&user)) == 0 && CALL("time.NetworkClock.GetCurrentTime", U(&net)) == 0 &&
llabs(user - now) <= 2 && llabs(net - now) <= 2, "system clocks %lld/%lld vs %lld", (long long)user,
(long long)net, (long long)now);
int64_t s1 = (int64_t)CALL("time.SteadyClock.now", 0), s2 = (int64_t)CALL("time.SteadyClock.now", 0);
CHECK(s1 > 0 && s2 >= s1, "steady clock %lld, %lld", (long long)s1, (long long)s2);
static const int64_t ts[] = {0, 86399, 951782400, 1700000000, 4102444800ll, -31536000, -62135596800ll, 253402300799ll};
int rt_bad = 0;
for (size_t i = 0; i < sizeof ts / sizeof *ts; i++) {
Cal c = utc_of(ts[i]);
if (posix_of(&c) != ts[i]) rt_bad++;
}
Cal leap = utc_of(951782400); // 2000-02-29 00:00:00 UTC
CHECK(leap.year == 2000 && leap.month == 2 && leap.day == 29 && !leap.hour && !leap.minute && !leap.second,
"951782400 is %d-%d-%d", leap.year, leap.month, leap.day);
Cal carry = {2023, 13, 1, 0, 0, 0, 0}; // month 13 carries into the next year
CHECK(posix_of(&carry) == 1704067200, "ToPosixTimeFromUtc does not carry month 13");
CHECK(!rt_bad, "%d of %d UTC round trips failed", rt_bad, (int)(sizeof ts / sizeof *ts));
Cal local = {0};
CalInfo info = {0};
int64_t t = 1700000000;
CHECK(CALL("time.ToCalendarTime", U(&local), U(&info), U(&t)) == 0, "ToCalendarTime");
int64_t back = posix_of(&local);
int64_t day = (t + info.utc_offset) / 86400;
CHECK(back - t == info.utc_offset && info.day_of_week == (int32_t)((day + 4) % 7),
"local calendar time does not round trip (offset %d, day of week %d)", info.utc_offset, info.day_of_week);
int64_t neg = -1;
CHECK(CALL("time.ToCalendarTime", U(&local), U(&info), U(&neg)) != 0, "ToCalendarTime of an invalid time succeeded");
uint8_t *a = guest_alloc(0xd0), *b = guest_alloc(0xd0);
memset(a, 0xaa, 0xd0);
CALL("time.ClockSnapshot.C1", U(a));
int zero = 1;
for (int i = 0; i < 0xd0; i++) zero &= !a[i];
CHECK(zero, "ClockSnapshot() did not clear the snapshot");
CHECK(CALL("time.ClockSnapshot.Create", U(a)) == 0 && CALL("time.ClockSnapshot.Create", U(b)) == 0, "snapshots");
int64_t span = -1;
CHECK(CALL("time.CalculateSpanBetween", U(&span), U(a), U(b)) == 0 && span >= 0, "span between snapshots");
CHECK(CALL("time.UserDifference", U(a), U(b)) == 0, "user clock difference");
b[0x98] ^= 1; // the steady clock's source id (ClockSnapshot +0x90: value, +0x98: source)
CHECK((uint32_t)CALL("time.CalculateSpanBetween", U(&span), U(a), U(b)) == 0x18074u, "span across sources");
CHECK(CALL("time.Finalize", 0) == 0 && (uint8_t)CALL("time.IsInitialized", 0) == 0, "time finalize");
printf("time: ToCalendarTimeInUtc/ToPosixTimeFromUtc round trip %d values %s; local %04d-%02d-%02d %02d:%02d zone %s "
"offset %+d s; snapshot location %s\n", (int)(sizeof ts / sizeof *ts), rt_bad ? "FAILED" : "ok", local.year,
local.month, local.day, local.hour, local.minute, info.name, info.utc_offset, (const char *)(a + 0xa8));
}
#define STD_GRAVITY 9.80665f // SDL_STANDARD_GRAVITY
static int near3(const float *v, double x, double y, double z, double tol) {
return fabs(v[0] - x) <= tol && fabs(v[1] - y) <= tol && fabs(v[2] - z) <= tol;
}
static int at_rest(const MotionState *s) {
return !s->attributes && !s->sampling && near3(s->acc, 0, 0, -1, 0) && near3(s->gyro, 0, 0, 0, 0) &&
near3(s->dir, 1, 0, 0, 0) && near3(s->dir + 3, 0, 1, 0, 0) && near3(s->dir + 6, 0, 0, 1, 0);
}
// Vibration and six-axis handles, values and states with no gamepad motion (the sensor is stopped before the reads that
// check the rest state), the SDL -> Switch axis mapping and the sensor fusion on a private instance.
static void probe_feedback(uint32_t pid, uint32_t style) {
uint32_t six = ov_handheld() ? 4 : 3, vh[2] = {0}, sh[2] = {0}, dual[2] = {0}, zero = 0;
CHECK((int32_t)CALL("hid.GetVibrationDeviceHandles", U(vh), 2, U(&pid), style) == 2 && vh[0] == (six | pid << 8) &&
vh[1] == (0x10000 | six | pid << 8), "vibration handles %#x %#x", vh[0], vh[1]);
CHECK((int32_t)CALL("hid.GetSixAxisSensorHandles", U(sh), 2, U(&pid), style) == 1 && sh[0] == (0x20000 | six | pid << 8),
"six-axis handle %#x", sh[0]);
CHECK((int32_t)CALL("hid.GetVibrationDeviceHandles", U(dual), 2, U(&zero), 4) == 2 && dual[0] == 5 &&
dual[1] == 0x10005, "Joy-Con pair vibration handles %#x %#x (SDK: 0x5 0x10005)", dual[0], dual[1]);
char *keep_r = cfg_get("TOTK_RUMBLE") ? strdup(cfg_get("TOTK_RUMBLE")) : NULL;
char *keep_m = cfg_get("TOTK_MOTION") ? strdup(cfg_get("TOTK_MOTION")) : NULL;
set_env("TOTK_RUMBLE", "0");
set_env("TOTK_MOTION", "0");
uint32_t none[2] = {0xdead, 0xdead}, six_off[2] = {0};
CHECK((int32_t)CALL("hid.GetVibrationDeviceHandles", U(none), 2, U(&pid), style) == 0 && none[0] == 0xdead,
"TOTK_RUMBLE=0 still reports vibration handles");
// TOTK_MOTION is live: the handle stays (the sensor reads at rest while it is 0)
CHECK((int32_t)CALL("hid.GetSixAxisSensorHandles", U(six_off), 2, U(&pid), style) == 1 && six_off[0] == sh[0],
"TOTK_MOTION=0 drops the six-axis handle %#x", six_off[0]);
set_env("TOTK_RUMBLE", keep_r);
set_env("TOTK_MOTION", keep_m);
free(keep_r);
free(keep_m);
// Values with no amplitude: a connected gamepad stays still
float v[2][4] = {{0, 100, 0, 400}, {0, 150, 0, 300}}, got[4] = {0};
uint32_t info[2] = {9, 9};
uint8_t flag = 0;
CHECK(CALL("hid.InitializeVibrationDevice", U(&vh[0])) == 0 && CALL("hid.InitializeVibrationDevice", U(&vh[1])) == 0,
"InitializeVibrationDevice");
CHECK(CALL("hid.SendVibrationValue", U(&vh[0]), U(v[0])) == 0 && CALL("hid.GetActualVibrationValue", U(got), U(&vh[0])) == 0 &&
!memcmp(got, v[0], sizeof got), "the actual value is not the one sent");
CHECK(CALL("hid.SendVibrationValues", U(vh), U(v), 2) == 0 && CALL("hid.GetActualVibrationValue", U(got), U(&vh[1])) == 0 &&
!memcmp(got, v[1], sizeof got), "a batch does not reach the right device");
CHECK(CALL("hid.GetVibrationDeviceInfo", U(info), U(&vh[1])) == 0 && info[0] == 1 && info[1] == 2,
"right device info %u/%u", info[0], info[1]);
CHECK(CALL("hid.IsVibrationDeviceMounted", U(&flag), U(&vh[0])) == 0 && flag == 1 &&
CALL("hid.IsVibrationDeviceMounted", U(&flag), U(&dual[0])) == 0 && flag == 0, "mounted devices");
CHECK(CALL("hid.PermitVibration", 1) == 0 && CALL("hid.IsVibrationPermitted", U(&flag)) == 0 && flag == 1,
"vibration permitted");
MotionState *st = guest_alloc(17 * sizeof *st);
memset(st, 0xa5, 17 * sizeof *st);
int32_t count = -1;
CALL("hid.StartSixAxisSensor", U(&sh[0]));
CALL("hid.StopSixAxisSensor", U(&sh[0]));
CHECK(CALL("hid.GetSixAxisSensorStates", U(&count), U(st), 16, U(&sh[0])) == 0 && count == 16, "six-axis count %d",
count);
int rest = 1, guard = 1;
for (int i = 0; i < 16; i++) rest &= at_rest(&st[i]);
for (size_t i = 0; i < sizeof *st; i++) guard &= ((uint8_t *)&st[16])[i] == 0xa5;
CHECK(rest && guard, "stopped sensor: 16 states at rest %d, guard kept %d", rest, guard);
memset(st, 0xa5, sizeof *st);
CHECK(CALL("hid.GetSixAxisSensorState", U(st), U(&dual[0])) == 0 && at_rest(st), "a foreign handle reads at rest");
// SDL axes: lying flat (push +y) is Switch gravity -z, standing on the bottom edge is -y; yaw about SDL +y is +z.
double acc[3], gyro[3];
input_motion_axes((const float[3]){0, STD_GRAVITY, 0}, (const float[3]){0, 1, 0}, acc, gyro);
int flat = fabs(acc[0]) < 1e-6 && fabs(acc[1]) < 1e-6 && fabs(acc[2] + 1) < 1e-6 && gyro[2] == 1 && !gyro[0] && !gyro[1];
input_motion_axes((const float[3]){0, 0, -STD_GRAVITY}, (const float[3]){0, 0, 1}, acc, gyro);
int upright = fabs(acc[1] + 1) < 1e-6 && gyro[1] == -1;
CHECK(flat && upright, "SDL -> Switch axes (flat %d, upright %d)", flat, upright);
// Fusion: flat, then 90 degrees a second about z for 1 s: a quarter turn, x axis now along y
MotionFusion *m = motion_new();
uint64_t t = 1000000000ull;
const double down[3] = {0, 0, -1}, still[3] = {0, 0, 0}, turn[3] = {0, 0, M_PI / 2};
motion_feed(m, down, still, t);
for (int i = 0; i < 250; i++) motion_feed(m, down, turn, t += 4000000);
MotionState ms[16];
int n = motion_read(m, ms, 16, t + 1000000);
CHECK(n == 16 && ms[0].sampling == 251 && ms[15].sampling == 236 && ms[0].attributes == 1 &&
fabs(ms[0].angle[2] - 0.25) < 0.002 && fabs(ms[0].gyro[2] - 0.25) < 1e-6 && near3(ms[0].dir, 0, 1, 0, 0.01) &&
near3(ms[0].dir + 3, -1, 0, 0, 0.01) && near3(ms[0].dir + 6, 0, 0, 1, 0.01),
"fusion quarter turn: %d states, sampling %lld, angle %.4f rev, x axis (%.3f %.3f %.3f)", n,
(long long)ms[0].sampling, (double)ms[0].angle[2], (double)ms[0].dir[0], (double)ms[0].dir[1], (double)ms[0].dir[2]);
double quarter = ms[0].angle[2];
CHECK(motion_read(m, ms, 16, t + 300000000ull) == 0, "fusion states 300 ms after the last sample");
// StartSixAxisSensor again in the middle of the turn (the bow drawn again): the states from before it are gone, and
// the first one after it starts a fresh angle in a fresh frame; sampling numbers go on.
for (int i = 0; i < 250; i++) motion_feed(m, down, turn, t += 4000000);
motion_restart(m);
int same = motion_read(m, ms, 16, t + 1000000);
motion_feed(m, down, turn, t += 4000000);
int next = motion_read(m, ms, 16, t + 1000000);
CHECK(same == 0 && next == 1 && ms[0].sampling == 502 && fabs(ms[0].angle[2]) < 0.002 &&
near3(ms[0].dir, 1, 0, 0, 0.01),
"fusion restart: %d states in the frame of the Start, %d after one sample (sampling %lld, angle %.4f rev)", same,
next, (long long)ms[0].sampling, (double)ms[0].angle[2]);
printf("hid feedback: vibration handles %#x %#x, six-axis %#x, at rest when stopped; fusion quarter turn %.4f rev\n",
vh[0], vh[1], sh[0], quarter);
}
static void probe_input(void) {
uint32_t pid = ov_handheld() ? 0x20 : 0, other = ov_handheld() ? 0 : 0x20, ids[2] = {0, 0x20};
uint32_t style = ov_handheld() ? 2 : 1;
CALL("hid.InitializeNpad", 0);
CALL("hid.SetSupportedNpadIdType", U(ids), 2);
CALL("hid.SetSupportedNpadStyleSet", 3);
CHECK((uint32_t)CALL("hid.GetNpadStyleSet", U(&pid)) == style && (uint32_t)CALL("hid.GetNpadStyleSet", U(&other)) == 0,
"style sets");
uint8_t st[5][40];
memset(st, 0x55, sizeof st);
static const char *const kinds[] = {"hid.GetNpadStates.FullKey", "hid.GetNpadStates.Handheld",
"hid.GetNpadStates.JoyDual", "hid.GetNpadStates.JoyLeft",
"hid.GetNpadStates.JoyRight"};
int connected = 0, all_one = 1, no_buttons = 1;
for (int i = 0; i < 5; i++) {
all_one &= (int32_t)CALL(kinds[i], U(st[i]), 1, U(&pid)) == 1;
uint32_t attr;
uint64_t buttons;
memcpy(&attr, st[i] + 32, 4);
memcpy(&buttons, st[i] + 8, 8);
connected += attr & 1;
no_buttons &= !buttons;
if (attr & 1) CHECK(i == (ov_handheld() ? 1 : 0), "%s reports a connected controller", kinds[i]);
}
CHECK(all_one && connected == 1 && no_buttons, "controller states: %d connected", connected);
CHECK((int32_t)CALL("hid.GetNpadStates.FullKey", U(st[0]), 0, U(&pid)) == 0, "zero states requested");
CALL("hid.SetNpadJoyHoldType", 1);
uint8_t res[8] = {0}, arg[0x80] = {0};
CHECK(CALL("hid.ShowControllerSupport", U(res), U(arg)) == 0 && res[0] == 1, "controller support applet");
CALL("hid.InitializeMouse", 0);
CALL("hid.InitializeKeyboard", 0);
uint8_t mouse[40], kb[48];
CHECK((int32_t)CALL("hid.GetMouseStates", U(mouse), 1) == 1, "mouse states");
CALL("hid.GetKeyboardState", U(kb));
probe_feedback(pid, style);
CALL("hid.DisconnectNpad", U(&pid));
CHECK((uint32_t)CALL("hid.GetNpadStyleSet", U(&pid)) == 0, "a disconnected controller keeps its style set");
printf("hid: one connected %s controller (id %#x), no buttons, no mouse/keyboard input\n",
ov_handheld() ? "handheld" : "full-key", pid);
}
static void probe_media_gfx(void) {
uint8_t *dec = guest_alloc(0x40), *work = guest_alloc(0x9000);
uint64_t ws = CALL("codec.GetWorkBufferSize", 48000, 2, 0);
CHECK(ws > 0 && CALL("codec.GetWorkBufferSize", 44100, 2, 0) == 0, "Opus work buffer size");
CHECK(CALL("codec.Initialize", U(dec), 48000, 2, U(work), ws, 0) == 0, "InitializeHardwareOpusDecoder");
CHECK(CALL("codec.Initialize", U(dec), 48000, 3, U(work), ws, 0) != 0, "3 channels accepted");
CHECK((int32_t)CALL("codec.GetSampleRate", U(dec)) == 48000 && (int32_t)CALL("codec.GetChannelCount", U(dec)) == 2,
"decoder rate/channels");
CHECK(CALL("codec.Reset", U(dec)) == 0, "ResetHardwareOpusDecoder");
CALL("codec.Finalize", U(dec));
CALL("nv.SetGraphicsAllocator", 0x7000001000, 0x7000002000, 0x7000003000, 0x1234);
CALL("nv.SetGraphicsDevtoolsAllocator", 0x7000004000, 0x7000005000, 0x7000006000, 0x5678);
uint8_t *mem = guest_alloc(0x1000);
CALL("nv.InitializeGraphics", U(mem), 0x1000);
const OvNvState *s = ov_nv_state();
CHECK(s->graphics.alloc == 0x7000001000 && s->graphics.realloc == 0x7000003000 && s->graphics.user == 0x1234 &&
s->devtools.free == 0x7000005000 && s->devtools.user == 0x5678 && s->memory == U(mem) &&
s->memory_size == 0x1000 && s->initialized, "nv state not recorded");
void *display = NULL, *layer = NULL, *window = NULL;
CHECK(CALL("vi.OpenDefaultDisplay", U(&display)) != 0, "OpenDefaultDisplay before vi::Initialize");
CALL("vi.Initialize", 0);
CHECK(CALL("vi.OpenDefaultDisplay", U(&display)) == 0 && display, "OpenDefaultDisplay");
CHECK(CALL("vi.CreateLayer", U(&layer), U(display)) == 0 && layer, "CreateLayer");
CHECK(CALL("vi.CreateLayer", U(&layer), U(&layer)) != 0, "CreateLayer on an unknown display");
CHECK(CALL("vi.GetNativeWindow", U(&window), U(layer)) == 0 && window, "GetNativeWindow");
uint64_t ev = U(guest_alloc(0x100));
CHECK(CALL("vi.GetDisplayVsyncEvent", ev, U(display)) == 0, "GetDisplayVsyncEvent");
int vsync = event_works(ev, 1);
CHECK(vsync, "the vsync event does not work as an auto-clear system event");
printf("codec/nv/vi: Opus decoder state kept, allocators and work memory recorded, display %p layer %p window %p, "
"vsync event %s\n", display, layer, window, vsync ? "bound (auto-clear)" : "FAILED");
}
// ---- software keyboard (swkbd.c): ShowKeyboardArg built by the SDK's own setters, as the horse naming does
enum { KB_TYPE = 0x00, KB_DISABLED = 0x1c, KB_HEADER = 0x24, KB_GUIDE = 0x1a8, KB_MAX_LEN = 0x3ac, KB_MIN_LEN = 0x3b0,
KB_UTF8 = 0x3bd, KB_INIT_LEN = 0x3c4, STRING_SIZE = 0x7d4, SWKBD_CANCELED = 0x29f };
#define ARG_WORK (RT_V143 ? 0x518 : 0x4d0) // SDK 20.5 grew KeyboardConfig (swkbd.c)
#define ARG_WORK_SIZE (ARG_WORK + 8)
#define ARG_SIZE (RT_V143 ? 0x548 : 0x500)
static uint64_t sdk(const char *name) {
uint64_t a = ldr_symbol(name);
if (!a) rt_fatal("probe-services: the SDK exports no %s", name);
return a;
}
// NUL-terminated UTF-16 copy of utf8 in guest memory
static uint16_t *u16(const char *utf8) {
uint16_t *g = guest_alloc(0x200);
utf8_to_utf16(utf8, g, 128);
return g;
}
static int u16_is(const uint16_t *a, const char *utf8) {
uint16_t b[128];
utf8_to_utf16(utf8, b, 128);
for (size_t i = 0;; i++) {
if (a[i] != b[i]) return 0;
if (!a[i]) return 1;
}
}
typedef struct {
int preset;
uint32_t type, max, min, disabled;
int utf8, window;
const char *initial, *answer, *want;
uint32_t result;
} KbCase;
static void probe_swkbd(void) {
uint64_t make = sdk("_ZN2nn5swkbd10MakePresetEPNS0_14KeyboardConfigENS0_6PresetE");
uint64_t header = sdk("_ZN2nn5swkbd13SetHeaderTextEPNS0_14KeyboardConfigEPKDs");
uint64_t guide = sdk("_ZN2nn5swkbd12SetGuideTextEPNS0_14KeyboardConfigEPKDs");
uint64_t initial = sdk("_ZN2nn5swkbd14SetInitialTextEPNS0_15ShowKeyboardArgEPKDs");
uint64_t string_size = sdk("_ZN2nn5swkbd27GetRequiredStringBufferSizeEv");
uint64_t work_size = sdk("_ZN2nn5swkbd25GetRequiredWorkBufferSizeEb");
CHECK(rt_call(gc, string_size, 0) == STRING_SIZE && rt_call(gc, work_size, 1, 0) == 0x1000, "SDK swkbd buffer sizes");
uint8_t *arg = guest_alloc(ARG_SIZE), *work = guest_alloc(0x1000);
uint64_t *str = guest_alloc(16);
uint16_t *out = guest_alloc(STRING_SIZE);
str[0] = U(out), str[1] = STRING_SIZE;
uint64_t w = U(work), ws = 0x1000;
// the SDK setters write where swkbd.c reads
rt_call(gc, make, 2, U(arg), 2);
rt_call(gc, header, 2, U(arg), U(u16("Hdr")));
rt_call(gc, guide, 2, U(arg), U(u16("Gd")));
memcpy(arg + ARG_WORK, &w, 8), memcpy(arg + ARG_WORK_SIZE, &ws, 8);
rt_call(gc, initial, 2, U(arg), U(u16("Zelda")));
int32_t init_len;
memcpy(&init_len, arg + KB_INIT_LEN, 4);
CHECK(u16_is((uint16_t *)(arg + KB_HEADER), "Hdr") && u16_is((uint16_t *)(arg + KB_GUIDE), "Gd") && init_len == 5,
"ShowKeyboardArg layout: header, guide or initial text not where swkbd.c reads them");
const char *def = ui_text(UI_SWKBD_DEFAULT_NAME);
const KbCase cases[] = {
{2, 8, 9, 0, 0x2c, 0, 0, NULL, "Epona", "Epona", 0}, // TotK's horse naming (main+0x1ecbf64)
{2, 8, 9, 0, 0x2c, 0, 0, NULL, "ABCDEFGHIJKL", "ABCDEFGHI", 0}, // at most 9
{2, 8, 9, 0, 0x2c, 0, 0, NULL, "a@b%c\\d e/f", "abcd e/f", 0}, // '@' '%' '\' disabled
{2, 8, 9, 0, 0x2c, 0, 0, NULL, "<cancel>", "", SWKBD_CANCELED},
{2, 8, 9, 0, 0x2c, 0, 0, NULL, "<initial>", def, 0}, // no initial text: the default name
{0, 2, 0, 0, 0, 0, 0, "Zelda", "<initial>", "Zelda", 0}, // initial text from the work buffer
{0, 2, 0, 0, 0, 1, 0, NULL, "\xe5\xb0\x8f\xe9\xa9\xac" "Ab", "\xe5\xb0\x8f\xe9\xa9\xac" "Ab", 0}, // UTF-8 out
{0, 2, 3, 0, 0, 0, 0, NULL, "\xe5\xb0\x8f\xe9\xa9\xac\xf0\x9f\x90\xb4", "\xe5\xb0\x8f\xe9\xa9\xac", 0}, // no half pair
{0, 1, 0, 0, 0, 0, 0, NULL, "a1b2c3", "123", 0}, // number pad
{0, 2, 9, 3, 0, 0, 0, NULL, "ab", "", SWKBD_CANCELED}, // shorter than the minimum
{2, 8, 9, 0, 0x2c, 0, 1, NULL, "ok", def, 0}, // the window, OK pressed
{2, 8, 9, 0, 0x2c, 0, 1, NULL, "cancel", "", SWKBD_CANCELED}, // the window, Cancel pressed
};
int bad = 0, n = (int)(sizeof cases / sizeof *cases);
for (int i = 0; i < n; i++) {
const KbCase *k = &cases[i];
memset(arg, 0, ARG_SIZE), memset(work, 0, 0x1000), memset(out, 0x55, STRING_SIZE);
rt_call(gc, make, 2, U(arg), k->preset);
memcpy(arg + KB_TYPE, &k->type, 4), memcpy(arg + KB_MAX_LEN, &k->max, 4), memcpy(arg + KB_MIN_LEN, &k->min, 4);
if (k->disabled) memcpy(arg + KB_DISABLED, &k->disabled, 4);
arg[KB_UTF8] = (uint8_t)k->utf8;
memcpy(arg + ARG_WORK, &w, 8), memcpy(arg + ARG_WORK_SIZE, &ws, 8);
if (k->initial) rt_call(gc, initial, 2, U(arg), U(u16(k->initial)));
set_env(k->window ? "TOTK_SWKBD_TEST" : "TOTK_SWKBD_ANSWER", k->answer);
uint32_t r = (uint32_t)CALL("swkbd.ShowKeyboard", U(str), U(arg));
set_env("TOTK_SWKBD_TEST", NULL), set_env("TOTK_SWKBD_ANSWER", NULL);
int ok = r == k->result && (k->utf8 ? !strcmp((const char *)out, k->want) : u16_is(out, k->want));
checks++;
if (!ok) bad++, printf(" FAIL: swkbd case %d (%s%s): result %#x\n", i, k->window ? "window " : "", k->answer, r);
}
failures += bad;
// after the window the game reads nothing until everything is released; with nothing held one read lifts that
uint64_t b;
int32_t lx, ly, rx, ry;
input_poll_ui(&b, &lx, &ly, &rx, &ry);
if (!b) input_poll(&b, &lx, &ly, &rx, &ry);
printf("swkbd: %d of %d cases (arguments from the SDK's MakePreset/SetHeaderText/SetGuideText/SetInitialText; "
"horse-name limits, disabled keys, cancel 0x29f, default name \"%s\", UTF-8 out, surrogate pairs, number pad, "
"minimum length, the Win32 window with OK and Cancel)\n", n - bad, n, def);
}
// ---- the menu's input gate: Minus+Plus is consumed, a menu that is open leaves the game a neutral pad
static void probe_menu_gate(void) {
const uint64_t minus = 1ull << 11, plus = 1ull << 10, a = 1ull;
int held = 0;
int32_t st[4] = {100, -200, 300, -400};
uint64_t b = minus | a; // Minus alone, a frame before Plus: reaches the guest unchanged
int t1 = input_menu_gate(&held, 0, &b, st);
CHECK(!t1 && b == (minus | a) && st[0] == 100 && st[3] == -400 && !held, "Minus alone changed: toggle %d buttons %#llx", t1,
(unsigned long long)b);
b = minus | plus | a; // both down: the guest sees neither, the menu toggles once
int t2 = input_menu_gate(&held, 0, &b, st);
CHECK(t2 == 1 && b == a && held, "Minus+Plus: toggle %d buttons %#llx", t2, (unsigned long long)b);
b = minus | plus; // still held with the menu open: no second toggle, a neutral pad
int t3 = input_menu_gate(&held, 1, &b, st);
CHECK(!t3 && b == 0 && st[0] == 0 && st[1] == 0 && st[2] == 0 && st[3] == 0, "menu open: toggle %d buttons %#llx", t3,
(unsigned long long)b);
b = a | plus; st[0] = 5; // the menu is open and the pad is neutral whatever is pressed
input_menu_gate(&held, 1, &b, st);
CHECK(b == 0 && st[0] == 0, "an open menu left buttons %#llx", (unsigned long long)b);
b = 0; // released, then pressed again toggles again
input_menu_gate(&held, 0, &b, st);
b = minus | plus;
CHECK(input_menu_gate(&held, 0, &b, st) == 1, "the second hold did not toggle");
// Plus released first while Minus is still down: Minus must not leak; a new Minus press after both are up passes
held = 0;
b = minus | plus;
input_menu_gate(&held, 0, &b, st);
b = minus;
int t4 = input_menu_gate(&held, 0, &b, st);
CHECK(!t4 && b == 0 && held, "Minus left down after Plus leaked: buttons %#llx", (unsigned long long)b);
b = 0;
input_menu_gate(&held, 0, &b, st);
b = minus | a;
int t5 = input_menu_gate(&held, 0, &b, st);
CHECK(!t5 && b == (minus | a) && !held, "Minus after the release did not pass: buttons %#llx", (unsigned long long)b);
printf("menu gate: Minus alone passes, Minus+Plus is consumed and toggles once per hold, an open menu gives a neutral pad\n");
}
// ---- shared fonts: the extracted files when present (the missing case is a fatal child)
static void probe_fonts(void) {
char dir[PLAT_PATH_MAX], path[PLAT_PATH_MAX + 32];
snprintf(path, sizeof path, "%s/0-standard.bfttf", ov_shared_font_dir(dir, sizeof dir));
FILE *f = fopen(path, "rb");
if (!f) {
printf("pl: no shared fonts in %s (tools/extract_fonts.py); Address/Size checked as named fatals only\n", dir);
return;
}
uint8_t head[12];
size_t got = fread(head, 1, sizeof head, f);
plat_fseek64(f, 0, SEEK_END);
int64_t len = plat_ftell64(f);
fclose(f);
uint64_t size = CALL("pl.GetSharedFontSize", 0), addr = CALL("pl.GetSharedFontAddress", 0);
CHECK(got == sizeof head && size && size + 8 == (uint64_t)len, "shared font 0: size %llu, file %lld bytes",
(unsigned long long)size, (long long)len);
const uint8_t *p = (const uint8_t *)(uintptr_t)addr;
CHECK(addr && !memcmp(p - 8, head, sizeof head) && ov_bfttf_size(p - 8, size + 8) == size,
"shared font 0: the address is not 8 bytes into the BFTTF file");
CHECK(CALL("pl.GetSharedFontAddress", 0) == addr, "shared font 0: the address changed");
printf("pl: shared font 0 (Standard) at %#llx, %llu bytes, BFTTF header checked (%s)\n", (unsigned long long)addr,
(unsigned long long)size, path);
}
// The host catalog (ui_text.c): every menu id has its own English, Russian, Brazilian Portuguese and Simplified
// Chinese text, the software keyboard keeps its game-language rows, and menu_text follows TOTK_MENU_LANG.
static void probe_menu_text(void) {
static const char *const langs[] = {"en", "ja", "fr", "de", "it", "es", "nl", "pt", "ru", "zh-Hans", "zh-Hant", "ko",
"pt-BR"};
for (int id = UI_MENU_FIRST; id <= UI_MENU_LAST_ID; id++) {
CHECK(ui_text_defined("en", (UiTextId)id) && ui_text_defined("ru", (UiTextId)id) &&
ui_text_defined("pt-BR", (UiTextId)id), "menu text id %d lacks en, ru or pt-BR", id);
CHECK(*ui_text_in("en", (UiTextId)id) && *ui_text_in("ru", (UiTextId)id) && *ui_text_in("pt-BR", (UiTextId)id),
"menu text id %d is empty", id);
// the description pane keeps the whole text and a note after it (option_rows.cpp desc_set)
CHECK(strlen(ui_text_in("en", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
strlen(ui_text_in("ru", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
strlen(ui_text_in("pt-BR", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
strlen(ui_text_in("zh-Hans", (UiTextId)id)) + 256 < UI_DESC_BYTES,
"menu text id %d is too long for the description pane", id);
CHECK(ui_text_defined("zh-Hans", (UiTextId)id), "menu text id %d lacks Simplified Chinese", id);
}
CHECK(!ui_text_defined("en", UI_TEXT_COUNT) && !*ui_text_in("ru", UI_TEXT_COUNT), "an id past the catalog gives text");
// other languages fall back to English for the menu, and keep their own keyboard texts
CHECK(!strcmp(ui_text_in("ja", UI_MENU_CONTINUE), ui_text_in("en", UI_MENU_CONTINUE)), "ja menu text is not English");
CHECK(!strcmp(ui_text_in("ru", UI_SWKBD_OK), "ОК") && !strcmp(ui_text_in("de", UI_SWKBD_CANCEL), "Abbrechen") &&
!strcmp(ui_text_in("en", UI_SWKBD_OK), "OK"), "keyboard texts changed");
// language names: the same string in every row
for (int id = UI_MENU_LANGNAME_EN; id <= UI_MENU_LANGNAME_ES_419; id++)
for (size_t i = 1; i < sizeof langs / sizeof *langs; i++)
CHECK(!strcmp(ui_text_in(langs[i], (UiTextId)id), ui_text_in("en", (UiTextId)id)), "language name %d differs in %s", id, langs[i]);
CHECK(!strcmp(ui_text_in("en", UI_MENU_LANGNAME_RU), "Русский"), "Russian is not shown as Russian");
// game language list: pt-BR only where the build ships USpt; the Switch variants are entries of their own
{
UiGameLang gl[16];
int n121 = ui_text_game_langs(121, gl, 16), has[16] = {0};
for (int i = 0; i < n121; i++) CHECK(strcmp(gl[i].code, "pt-BR"), "1.2.1 game language list offers pt-BR");
int n143 = ui_text_game_langs(143, gl, 16), pt = 0;
for (int i = 0; i < n143; i++) {
pt |= !strcmp(gl[i].code, "pt-BR");
for (int j = 0; j < i; j++) CHECK(strcmp(gl[i].code, gl[j].code), "duplicate game language %s", gl[i].code);
}
CHECK(pt && n143 == n121 + 1, "1.4.3 game language list lacks pt-BR");
static const char *const want[] = {"en-GB", "fr-CA", "es-419"};
for (int w = 0; w < 3; w++)
for (int i = 0; i < n143; i++) has[w] |= !strcmp(gl[i].code, want[w]);
CHECK(has[0] && has[1] && has[2], "game language list lacks en-GB, fr-CA or es-419");
}
// the overlay formats keep their conversions
CHECK(strstr(ui_text_in("ru", UI_MENU_OV_GAME_PCT), "%.0f%%") && strstr(ui_text_in("ru", UI_MENU_OV_MAX_MS), "%.1f") &&
strstr(ui_text_in("ru", UI_MENU_OV_LOW1), "%.0f") && strstr(ui_text_in("ru", UI_MENU_OV_LOW01), "%.0f"),
"overlay format lost its conversion");
CHECK(strstr(ui_text_in("pt-BR", UI_MENU_OV_GAME_PCT), "%.0f%%") &&
strstr(ui_text_in("pt-BR", UI_MENU_OV_MAX_MS), "%.1f") &&
strstr(ui_text_in("pt-BR", UI_MENU_OV_LOW1), "%.0f") && strstr(ui_text_in("zh-Hans", UI_MENU_OV_LOW01), "%.0f"), "pt-BR overlay format lost its conversion");
// a Brazilian game language gets the Brazilian keyboard row, Portugal's Portuguese keeps its own
CHECK(!strcmp(ui_text_lang("pt-BR"), "pt-BR") && !strcmp(ui_text_lang("pt"), "pt") &&
strcmp(ui_text_in("pt-BR", UI_SWKBD_PROMPT), ui_text_in("pt", UI_SWKBD_PROMPT)), "pt-BR keyboard row not used");
// menu_text follows the setting live
cfg_set("TOTK_MENU_LANG", "ru");
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("ru", UI_MENU_CONTINUE), "menu_text ignores MENU_LANG=ru");
cfg_set("TOTK_MENU_LANG", "pt-BR");
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("pt-BR", UI_MENU_CONTINUE), "menu_text ignores MENU_LANG=pt-BR");
cfg_set("TOTK_MENU_LANG", "en");
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("en", UI_MENU_CONTINUE), "menu_text did not follow MENU_LANG=en");
cfg_set("TOTK_MENU_LANG", "zh-Hans");
CHECK(menu_text(UI_MENU_TITLE) == ui_text_in("zh-Hans", UI_MENU_TITLE), "menu_text ignores MENU_LANG=zh-Hans");
cfg_set("TOTK_MENU_LANG", "");
// the system language as ui_text.c resolve_menu_lang takes it: the tag, else its base language (ru-RU: ru), else en
const char *sys_lang = ui_text_lang(plat_ui_language());
char sys_base[16] = "";
size_t base_n = strcspn(sys_lang, "-");
if (sys_lang[base_n] && base_n < sizeof sys_base) memcpy(sys_base, sys_lang, base_n);
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang
: *sys_base && ui_text_defined(sys_base, UI_MENU_TITLE) ? sys_base
: "en";
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in(auto_lang, UI_MENU_CONTINUE), "menu_text auto ignores the system language");
}
static void probe_main(Cpu *c) {
gc = c;
probe_oe();
probe_messages();
probe_applet();
probe_account();
probe_time();
CHECK((uint32_t)CALL("pl.GetSharedFontLoadState", 0) == 1, "shared font load state");
probe_input();
probe_media_gfx();
probe_swkbd();
probe_menu_text();
probe_menu_gate();
probe_fonts();
}
// ---- child processes
// Runs this executable again with the same command line and TOTK_PROBE_SERVICES_CHILD=<mode>; its stdout and stderr
// into out. Returns the exit code.
static uint32_t run_child(const char *mode, const char *env, char *out, size_t cap) {
char name[64] = "";
const char *eq = env ? strchr(env, '=') : NULL;
if (eq) snprintf(name, sizeof name, "%.*s", (int)(eq - env), env), plat_setenv(name, eq + 1);
int code = plat_run_self(CHILD_ENV, mode, out, cap, 0);
if (eq) plat_setenv(name, NULL);
if (code < 0) rt_fatal("probe-services: cannot start the %s child", mode);
return (uint32_t)code;
}
static const char *child_mode;
static void child_main(Cpu *c) {
gc = c;
if (!strcmp(child_mode, "uid")) {
uint64_t h[3] = {0}, uid[2] = {0};
CALL("account.Initialize", 0);
uint8_t opened = (uint8_t)CALL("account.TryOpenPreselectedUser", U(h));
uint32_t r = (uint32_t)CALL("account.GetUserId", U(uid), U(h));
printf("child uid %016llx%016llx (open %u, result %#x)\n", (unsigned long long)uid[1], (unsigned long long)uid[0],
opened, r);
return;
}
uint8_t *buf = guest_alloc(0x1000);
if (!strcmp(child_mode, "nvn")) {
uint64_t known = CALL("nvnBootstrapLoader", U("nvnDeviceGetProcAddress"));
uint64_t unknown = CALL("nvnBootstrapLoader", U("nvnNoSuchProc"));
printf("child nvn: known %#llx expected %#llx unknown %#llx\n", (unsigned long long)known,
(unsigned long long)nvn_proc_address("nvnDeviceGetProcAddress"), (unsigned long long)unknown);
return;
}
if (!strcmp(child_mode, "opus")) CALL("codec.Decode", U(buf), U(buf + 8), U(buf + 16), U(buf + 64), 0x100, U(buf + 0x800), 16);
else if (!strcmp(child_mode, "font-address")) CALL("pl.GetSharedFontAddress", 0);
else if (!strcmp(child_mode, "font-size")) CALL("pl.GetSharedFontSize", 0);
else rt_fatal("probe-services: unknown child case %s", child_mode);
printf("child %s: the call returned\n", child_mode);
}
static int run_as_child(const char *mode) {
child_mode = mode;
bind_addresses();
hostfn_seal();
kern_run_on_guest_main(child_main);
return 0;
}
static void probe_children(void) {
static char out[16384];
uint32_t code = run_child("uid", NULL, out, sizeof out);
unsigned long long hi = 0, lo = 0;
const char *p = strstr(out, "child uid ");
int parsed = p && sscanf(p + 10, "%16llx%16llx", &hi, &lo) == 2;
int same = parsed && code == 0 && hi == probe_uid[1] && lo == probe_uid[0];
CHECK(same, "the second run's account UID differs (exit %u): %s", code, out);
printf("account: UID %016llx%016llx in %s/account.uid, the same in a second run: %s\n",
(unsigned long long)probe_uid[1], (unsigned long long)probe_uid[0], rt_config()->profile, same ? "yes" : "NO");
// nvnBootstrapLoader returns the synthetic address of a proc (nvn_procs.c), 0 for an unknown name
code = run_child("nvn", NULL, out, sizeof out);
unsigned long long known = 0, expected = 0, unknown = 1;
p = strstr(out, "child nvn: ");
parsed = p && sscanf(p, "child nvn: known %llx expected %llx unknown %llx", &known, &expected, &unknown) == 3;
int nvn_ok = parsed && code == 0 && known && known == expected && unknown == 0;
CHECK(nvn_ok, "child nvn: exit %u, output: %s", code, out);
if (nvn_ok) ov("nvnBootstrapLoader")->calls++;
printf("nvnBootstrapLoader: nvnDeviceGetProcAddress -> %#llx (proc table %#llx), unknown name -> %#llx\n", known,
expected, unknown);
for (int i = 0; i < NFATAL; i++) {
code = run_child(fatals[i].child, fatals[i].env, out, sizeof out);
int ok = code == 70 && strstr(out, fatals[i].fatal);
CHECK(ok, "child %s: exit %u, output: %s", fatals[i].child, code, out);
if (ok) ov(fatals[i].key)->calls++;
const char *line = strstr(out, "FATAL: ");
int len = line ? (int)strcspn(line, "\r\n") : 0;
printf("fatal stub %-20s child exit %u: %.*s\n", fatals[i].key, code, len, line ? line : "(no FATAL line)");
}
}
int probe_services(void) {
const char *child = getenv(CHILD_ENV);
if (child && *child) return run_as_child(child);
ldr_load(rt_config()->exefs);
// Names bound by the loader: each override and observer probed here must match a relocation.
int bound = 0, obs_bound = 0;
for (int i = 0; i < 4096; i++) {
const char *n = hostfn_name(RT_HOSTFN_BASE + 16ull * (uint64_t)i);
if (!n) break;
for (int k = 0; k < NOV; k++)
if (!strcmp(ovs[k].name, n) && !ovs[k].bound) ovs[k].bound = 1, bound++;
for (int k = 0; k < NOBS; k++)
if (!strcmp(observers[k], n)) obs_bound++;
}
for (int k = 0; k < NOV; k++) // 1.2.1 lists no add-on content
CHECK(ovs[k].bound || (!RT_V143 && !strcmp(ovs[k].key, "aoc.ListAddOnContent")), "override %s matches no relocation",
ovs[k].name);
for (int k = 0; k < NOBS; k++) CHECK(hostfn_observer(observers[k]), "observer %s is not linked", observers[k]);
CHECK(obs_bound == NOBS, "%d of %d observers match a JUMP_SLOT", obs_bound, NOBS);
// Every linked override is either probed here or belongs to another probe or owner (nn::fs: fs probe, fibers: kernel probe,
// movie::Decoder: --probe-movie, memcpy: the graphics write-back in nvn_api_core.c).
int linked = hostfn_table_size(0), others = 0;
for (int i = 0; i < linked; i++) {
const char *n = hostfn_table_name(0, i);
int mine = 0;
for (int k = 0; k < NOV; k++) mine |= !strcmp(ovs[k].name, n);
int theirs = !strncmp(n, "_ZN2nn2fs", 9) || !strncmp(n, "_ZN5movie", 9) || !strcmp(n, "memcpy") ||
!strcmp(n, KERN_SYM_INITIALIZE_FIBER) ||
!strcmp(n, KERN_SYM_FINALIZE_FIBER) || !strcmp(n, KERN_SYM_SWITCH_TO_FIBER) ||
!strcmp(n, KERN_SYM_GET_CURRENT_FIBER);
others += theirs;
CHECK(mine || theirs, "override %s is linked but not probed", n);
}
printf("bound: %d of %d overrides and %d of %d observers match relocations of the loaded modules\n", bound, NOV,
obs_bound, NOBS);
bind_addresses();
hostfn_seal();
kern_run_on_guest_main(probe_main);
probe_children();
int called = 0;
for (int k = 0; k < NOV; k++) {
called += ovs[k].calls > 0;
CHECK(ovs[k].calls > 0, "override %s was not called", ovs[k].key);
}
printf("called: %d of %d overrides through rt_dispatch (%d named fatal stubs in child processes); %d other "
"overrides belong to the kernel and fs probes\n", called, NOV, NFATAL, others);
printf("overrides %d, observers %d\n", linked, hostfn_table_size(1));
if (failures) {
printf("probe-services: FAILED (%d of %d checks)\n", failures, checks);
return 1;
}
printf("probe-services: passed\n");
return 0;
}