Files
nxnative-easy/dm/host/tests/t_oe.c
T
seedds 79d82f7b64 Remove the D2R code and the legacy frame step
D2R (Diablo II Resurrected) has no profile and its code could not run here: the
host-call svcs its cuts emitted only died, the touch-run-v4 rule set needed a
missing dm/PRODUCT, and the host files its hooks called were never in this
repository. Removed with it:
- rewriter: D2R cuts, observation gates and parameter slots, the touch-run-v4
  rule set and private gates, the boot list, the cut scope column and the
  KRES / KASPECT key slots (fixups.tsv keeps the four thread slots; no
  params.tsv any more); dm/tools/product.sh
- host: D2R save edits, add-on content, render-scale controller, time()
  takeover and Terror Zone clock, crash decoding at D2R offsets, host-call
  registry and stubs, i18n, UI-screen, shell-app loader, zero-stripped
  archives, boot profiling hooks with no implementation; D2R headers, tests
  and scripts; dm/ios/Sources/dm_touch_controls; dm/third_party/libdeflate
- graphics: runtime extrapolation, world freeze, iPad UI shift, shop drag
  image, brightness LUT, visual ruler, bloom / FXAA shader hashes, DM_GFXUSE,
  the eco-fps setting, the loading-screen gate; shader_map.py,
  nometal_run.py, nvn_slots.py, nvn_slots.tsv and the PC-texture samples

The legacy frame step (frame_step legacy, DM_FRAME_STEP) is retired: the
Breath of the Wild and Tears of the Kingdom stubs lose their mode test.
Breath of the Wild's interval-controller cuts stay: the framework keeps the
controller's interval while the game's VFR override holds.

patch_nso.py output is byte-identical for all 15 games on this machine except
the two frame-step stubs; all 18 profiles build; the host tests pass (their
Makefile now builds the general tests only).
2026-10-21 02:09:49 +08:00

115 lines
8.7 KiB
C

// oe state notification unit test: when not consumed, each type keeps only its first queue position; after a pop a new change is queued again.
// The three messages only signal "query the current state" and carry no historical value. Expected order/state are hand-written, not generated by the implementation.
#ifndef HLE_OE_SOURCE
#define HLE_OE_SOURCE "../hle_oe.c"
#endif
#include HLE_OE_SOURCE
#include <signal.h>
#include <sys/wait.h>
uint64_t now_ns(void) { return 0; }
void dm_audio_suspend(int on) { (void)on; } // stubs for the bg item of DM_OE_SCRIPT (hle_audio.c / hle_os.c / hle_nvn.c)
void guest_clock_freeze_for(int reason, int on) { (void)reason; (void)on; }
void dm_gfx_suspend(int on) { (void)on; }
// The notification event (hle_thread.c binds a kernel event): the unit test only tracks whether it is signalled
static int ev_bound, ev_sig;
void hle_bind_system_event(void *g, int autoclear) { (void)g; (void)autoclear; ev_bound = 1; }
void hle_signal_system_event(void *g) { (void)g; ev_sig = 1; }
void hle_clear_system_event(void *g) { (void)g; ev_sig = 0; }
static int checks, fails;
#define CHECK(c, s) do { checks++; int ok = !!(c); fails += !ok; printf("%s %s\n", ok ? "✓" : "✗", s); } while (0)
static void reset(void) { g_mqh = g_mqn = 0; g_focus = 1; g_opmode = 1; g_ntf_opmode = g_ntf_perfmode = 0; }
static int pop_is(uint32_t want) { uint32_t m = 999; return h_oe_TryPopMsg(&m) && m == want; }
static int empty(void) { uint32_t m = 999; return !h_oe_TryPopMsg(&m) && m == 999; }
static int child_kind;
static void abort_exit(int sig) { (void)sig; dprintf(2, "[RED] t_oe child kind=%d: 10 s watchdog timeout\n", child_kind); _exit(97); } // the old-version die in the child process can be checked, avoiding a crash report or core
static void *focus_burst(void *p) { (void)p; for (int i = 0; i < 17; i++) { dm_focus_set(2); dm_focus_set(1); } return NULL; }
static void *mode_burst(void *p) { (void)p; for (int i = 0; i < 17; i++) dm_set_opmode(i % 2); return NULL; }
static pthread_mutex_t stage_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t stage_cond = PTHREAD_COND_INITIALIZER;
static int stage, interleaved_ok;
static void stage_set(int n) { pthread_mutex_lock(&stage_lock); stage = n; pthread_cond_broadcast(&stage_cond); pthread_mutex_unlock(&stage_lock); }
static void stage_wait(int n) { pthread_mutex_lock(&stage_lock); while (stage < n) pthread_cond_wait(&stage_cond, &stage_lock); pthread_mutex_unlock(&stage_lock); }
static void *consume_interleaved(void *p) {
(void)p; stage_wait(1); int ok = pop_is(15); stage_set(2); // popped but not yet queried: let the producer update exactly here
stage_wait(3); interleaved_ok = ok && h_oe_GetFocus() == 1 && pop_is(15) && pop_is(30) && pop_is(31) && empty() && h_oe_GetOpMode() == 0 && h_oe_GetPerfMode() == 0;
return NULL;
}
static int child_burst(int kind) {
fflush(stdout); pid_t p = fork(); if (p < 0) return 0;
if (!p) {
child_kind = kind; signal(SIGABRT, abort_exit); signal(SIGALRM, abort_exit); alarm(10); reset();
g_ntf_opmode = g_ntf_perfmode = 1;
if (kind == 0) { focus_burst(NULL); _exit(h_oe_GetFocus() == 1 && pop_is(15) && empty() ? 0 : 1); }
if (kind == 1) { mode_burst(NULL); _exit(h_oe_GetOpMode() == 0 && h_oe_GetPerfMode() == 0 && pop_is(30) && pop_is(31) && empty() ? 0 : 1); }
if (kind == 3) {
pthread_t c; pthread_create(&c, NULL, consume_interleaved, NULL);
dm_focus_set(2); stage_set(1); stage_wait(2);
dm_focus_set(1); dm_set_opmode(0); stage_set(3); pthread_join(c, NULL);
_exit(interleaved_ok ? 0 : 1);
}
pthread_t f, m; pthread_create(&f, NULL, focus_burst, NULL); pthread_create(&m, NULL, mode_burst, NULL);
pthread_join(f, NULL); pthread_join(m, NULL);
uint32_t msg; int nf = 0, no = 0, np = 0, order = 1;
while (h_oe_TryPopMsg(&msg)) { if (msg == 15) nf++; else if (msg == 30) no++; else if (msg == 31) { order &= no == 1; np++; } else order = 0; }
_exit(nf == 1 && no == 1 && np == 1 && order && h_oe_GetFocus() == 1 && h_oe_GetOpMode() == 0 ? 0 : 1);
}
int st; if (waitpid(p, &st, 0) != p) return 0;
return WIFEXITED(st) && WEXITSTATUS(st) == 0;
}
int main(void) {
unsetenv("DM_OE_SCRIPT"); reset();
CHECK(empty(), "empty queue at start: returns false and leaves the output alone");
dm_focus_set(1); dm_set_opmode(1); CHECK(empty(), "same state sends no notification");
dm_focus_set(3); CHECK(h_oe_GetFocus() == 3, "background state 3 can be queried before popping");
CHECK(pop_is(15) && empty(), "background change notification 15 exactly once");
dm_focus_set(2); dm_focus_set(1);
CHECK(pop_is(15) && empty() && h_oe_GetFocus() == 1, "unpopped focus-lost/resume merged, final focus queried as 1");
reset(); g_ntf_opmode = g_ntf_perfmode = 1;
dm_focus_set(2); dm_set_opmode(0); dm_focus_set(3); dm_set_opmode(1);
CHECK(pop_is(15) && pop_is(30) && pop_is(31) && empty(), "first positions of the three kinds FIFO=15,30,31, repeats do not move them");
CHECK(h_oe_GetFocus() == 3 && h_oe_GetOpMode() == 1 && h_oe_GetPerfMode() == 1, "repeated notifications keep no stale value, latest 3/1/1 queried");
reset(); g_ntf_opmode = g_ntf_perfmode = 1;
dm_set_opmode(0); dm_focus_set(2); dm_set_opmode(1);
CHECK(pop_is(30) && pop_is(31) && pop_is(15) && empty(), "when the mode changes first FIFO=30,31,15, the pair order is unchanged");
reset(); g_ntf_opmode = g_ntf_perfmode = 1;
dm_focus_set(2); CHECK(pop_is(15), "focus notification pops first");
dm_set_opmode(0); dm_focus_set(1);
CHECK(pop_is(30) && pop_is(31) && pop_is(15) && empty(), "a resume after the pop notifies again and queues after the pending mode messages");
CHECK(h_oe_GetFocus() == 1, "current focus after resume=1");
reset(); dm_set_opmode(0);
CHECK(empty() && h_oe_GetOpMode() == 0, "notifications off only update state, nothing queued");
h_oe_SetOpModeNtf(0x100); h_oe_SetPerfModeNtf(0x101); dm_set_opmode(1);
CHECK(pop_is(31) && empty(), "notification bool reads only the low 8 bits, performance alone sends only 31");
h_oe_SetOpModeNtf(0x101); h_oe_SetPerfModeNtf(0x100); dm_set_opmode(0);
CHECK(pop_is(30) && empty(), "operation mode alone sends only 30");
reset(); mq_push(16); mq_push(16);
CHECK(pop_is(16) && pop_is(16) && empty(), "per-occurrence event 16 not merged, both kept in FIFO");
reset(); g_ntf_opmode = g_ntf_perfmode = 1; dm_set_opmode(0);
CHECK(pop_is(30), "of the mode pair 30 popped first, 31 not yet");
dm_set_opmode(1);
CHECK(pop_is(31) && pop_is(30) && empty() && h_oe_GetOpMode() == 1, "popping across a new change: old 31 keeps its place, popped 30 queued again, final mode 1");
CHECK(child_burst(0), "34 focus changes across the old 32 slots, no abort, final resume 1 / one 15");
CHECK(child_burst(1), "17 mode changes across the old 32 slots, no abort, final 0 / 30,31");
CHECK(child_burst(2), "two concurrent producers, exactly one of each kind, 30 before 31, final focus 1 / mode 0");
reset(); g_ntf_opmode = g_ntf_perfmode = 1; int rotated = 1;
for (int i = 0; i < 31; i++) { dm_focus_set(2 - i % 2); rotated &= pop_is(15) && empty(); }
CHECK(rotated, "31 pops one at a time, moving the ring head to the last slot");
dm_focus_set(1); dm_set_opmode(0); dm_focus_set(3); dm_set_opmode(1);
CHECK(pop_is(15) && pop_is(30) && pop_is(31) && empty(), "after wrapping across 32 slots still deduplicated by 15,30,31 in first-seen order");
CHECK(child_burst(3), "new change after the consumer pops and before it queries: the new resume is not missed, final focus 1 / mode 0");
// The notification event follows the queue, as the am service's does: signalled while a message is queued, cleared when the last one is popped. Monster Hunter Rise's
// MessageSuperviseThread (main+0x6bc8fc4) waits on it with WaitAny, which does not clear it, then pops until the queue is empty: a signal left behind made it spin on a core
reset(); g_ntf_opmode = g_ntf_perfmode = 1;
h_oe_GetMsgEv();
CHECK(ev_bound && !ev_sig, "notification event: bound on first use, not signalled while the queue is empty");
dm_focus_set(2);
CHECK(ev_sig, "notification event: signalled when a notification is queued");
CHECK(pop_is(15) && !ev_sig, "notification event: cleared when the last notification is popped");
dm_focus_set(1); dm_set_opmode(0);
CHECK(pop_is(15) && ev_sig, "notification event: still signalled after a pop that leaves notifications queued");
CHECK(pop_is(30) && ev_sig && pop_is(31) && !ev_sig, "notification event: cleared only when the queue is empty");
ev_sig = 1;
CHECK(empty() && !ev_sig, "notification event: a pop from an empty queue clears a signal left behind");
printf("== oe %d checks, %d failed ==\n", checks, fails); return fails != 0;
}