mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
Port-layer only (no game-code edits): - main.c seeds debug_enable_all_levels_flag + debug_007_unlock_flag after configLoad() when Game.AllUnlocked is set (same class as the existing GE_UNLOCK_ALL getenv hook). - libultra.c eep shim patches the save block at read time (the exact fileValidateSaves pattern: block 4, 5x96 bytes): sets all 20 progression-gated cheat-unlock bits per slot and fills every empty stage/difficulty completion time with the max (0x3FF), preserving existing player times, then recomputes each slot's CRC with the game's own fileGenerateCRC so validation passes. The completion-time half is required: with any SP cheat active, g_AppendCheatSinglePlayer makes mission select accept only STAGESTATUS_COMPLETED levels, so an unlock-only save locks every level once a cheat is turned on. - optionsoverlay.c: 'All unlocked' toggle row (default ON for this cut). Verified: fresh-eep GE_SAVELOG shows all 5 slots stored==calc; user playtest confirms all levels at each difficulty, 007 mode, populated cheat menu, cheats working, mission select intact with all cheats on.
1755 lines
67 KiB
C
1755 lines
67 KiB
C
/*
|
|
* libultra OS shims for the PC port, plus the cooperative thread kernel.
|
|
*
|
|
* The game code calls the libultra OS API (threads, message queues, VI, PI,
|
|
* SI, SP, AI, cache, memory). On the PC none of that hardware exists:
|
|
*
|
|
* - Threads are REAL host threads (pthreads). The N64 OS is a preemptive
|
|
* priority scheduler, so this is both faithful and robust: blocking
|
|
* osRecvMesg() waits on a per-queue condition variable. (An earlier
|
|
* setjmp/longjmp green-thread kernel corrupted FPU state across longjmps
|
|
* on Windows x64/MinGW — synthetic STATUS_FLOATING_POINT_INVALID_OPERATION
|
|
* crashes at PC=0; see docs/internals.md.)
|
|
* - RSP tasks (osSpTaskStartGo) run the software RSP (fast3d) inline and
|
|
* post the RSP_DONE / RDP_DONE messages sched.c waits for.
|
|
* - VI retrace is a vsync-paced tick posted by a dedicated kernel thread,
|
|
* once per frame at the console's frame rate.
|
|
* - Controllers are filled from the keyboard in osContStartQuery().
|
|
*
|
|
* Modelled on the PD port's port/src/libultra.c.
|
|
*/
|
|
|
|
/* NOTE: no <sched.h> here — angle-bracket form would resolve to the game's
|
|
* src/sched.h (it is on the include path), not MinGW's. osYieldThread uses a
|
|
* short sleep instead of sched_yield(). */
|
|
#include <pthread.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <time.h>
|
|
|
|
#include <SDL.h>
|
|
|
|
/* N64 headers declare struct fields named `errno`; the C macro (pulled in
|
|
* by pthread.h/sched.h -> errno.h) must not be active while they parse. */
|
|
#pragma push_macro("errno")
|
|
#undef errno
|
|
#include <PR/os.h>
|
|
#include <PR/os_internal.h>
|
|
#include <PR/rcp.h>
|
|
#include <PR/rdb.h>
|
|
#include <PR/sptask.h>
|
|
#include <PR/gbi.h>
|
|
#pragma pop_macro("errno")
|
|
|
|
#include "platform.h"
|
|
#include "system.h"
|
|
#include "crash.h" /* D38: crashDumpThreads() */
|
|
#include "video.h"
|
|
#include "audio.h"
|
|
#include "input.h"
|
|
#include "fs.h"
|
|
#include "romdata.h"
|
|
#include "crash.h"
|
|
|
|
#if defined(PLATFORM_WINDOWS)
|
|
#include <windows.h> /* GetCurrentThreadId for thread-dump labels */
|
|
#endif
|
|
|
|
/* fast3d (C++): the software RSP entry point. */
|
|
extern void gfx_run(Gfx *commands);
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* Globals the game expects to exist (normally set by osInitialize). */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
u64 osClockRate = 6250000; /* RSP counter rate (Hz) — see PD port */
|
|
u32 osMemSize = 16 * 1024 * 1024; /* pretend 16MB RDRAM (+expansion) */
|
|
/* TV type (normally set from console hardware in os/initialize.c, excluded).
|
|
* The game reads it to pick PAL/NTSC behaviour (e.g. schedulerInitThread
|
|
* checks osTvType == OS_TV_MPAL). Set to match the emulated region: PAL
|
|
* consoles report MPAL, NTSC consoles report NTSC. */
|
|
#ifdef REFRESH_PAL
|
|
u32 osTvType = OS_TV_MPAL; /* EU: PAL (0=PAL 1=NTSC 2=MPAL) */
|
|
#else
|
|
u32 osTvType = OS_TV_NTSC; /* US/JP: NTSC */
|
|
#endif
|
|
u32 osResetType = 0; /* GE's PR/os.h declares it u32 */
|
|
s32 osViClock = 0;
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* Time */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
OSTime osGetTime(void)
|
|
{
|
|
return (OSTime)sysGetMicroseconds();
|
|
}
|
|
|
|
/* GE_DETERM=1 — fixed-tick deterministic mode (speed-ups plan Step 7 / R1,
|
|
* design in docs/dev/findings.md §D117). Test-only, default OFF: replaces
|
|
* the wall-clock osGetCount() below with a virtual clock advanced exactly
|
|
* once per virtual VI retrace (generated synchronously in osRecvMesg() —
|
|
* see the GE_DETERM block there — when __scMain, the sole consumer of
|
|
* g_viRetraceMQ, asks for the next message; NOT paced by an independent
|
|
* thread) instead of real elapsed time, so two runs of the same build take
|
|
* the same simulation path. Never the shipping timing model — the default
|
|
* (GE_DETERM unset) path below is byte-for-byte what it always was. */
|
|
static int g_determEnabled = 0;
|
|
static u32 g_determQuantum = 775875; /* NTSC; portKernelInit sets 931050 PAL */
|
|
static u32 g_determTicks = 0;
|
|
/* M-52 3rd attempt: __scMain is the sole consumer of g_viRetraceMQ, but
|
|
* before the first real gfx/audio task ever runs (src/boss.c's bounded
|
|
* osSetTimer(100ms)-based controller-detection loop -- genuinely real-time,
|
|
* out of scope by design), NOTHING is consuming its forwards, so unbounded
|
|
* per-ask generation free-spins for that whole real-time window and however
|
|
* many ticks accumulate is real-scheduling-dependent (confirmed: 2nd
|
|
* attempt's 33/38/14% divergence). waitForNextFrame() (frametiming.c) is a
|
|
* tight busy-spin on osGetCount(), not a queue wait -- so FREEZING the
|
|
* clock during that window (0 extra ticks) hangs it forever, and letting it
|
|
* free-run is exactly the bug. The fix: cap the clock to exactly ONE extra
|
|
* quantum beyond the seed before the first task runs -- fixed, identical
|
|
* every run, and >=1 quantum is already enough to clear
|
|
* waitForNextFrame()'s first-call threshold (see the arithmetic in
|
|
* frametiming.c), so it can't hang either. Set true by osSpTaskStartGo. */
|
|
static int g_determTaskEverRun = 0;
|
|
static int g_determPreTaskTicksGranted = 0;
|
|
/* GE_DETERM_TRACE=1 (with GE_DETERM=1): log every virtual-clock advance with a
|
|
* monotonic sequence number, for diffing two runs' logs to find the exact
|
|
* first point they diverge -- see docs/dev/findings.md §D117 M-52. Test-only,
|
|
* both env-gated off by default; zero cost when unset. */
|
|
static int g_determTraceEnabled = 0;
|
|
static u32 g_determTraceSeq = 0;
|
|
|
|
u32 osGetCount(void)
|
|
{
|
|
if (g_determEnabled) {
|
|
return g_determTicks;
|
|
}
|
|
/* The N64 RSP core counter advances at ~OS_CPU_COUNTER (~46.5 MHz), NOT
|
|
* microseconds. GE's pacing assumes this rate: waitForNextFrame() in
|
|
* frametiming.c waits for 775,875 - 387,937 ticks per NTSC frame (931,050
|
|
* per PAL frame) and bossMainloop gates DL builds on
|
|
* MAIN_LOOP_TICK_INTERVAL = 387,937 ticks. Feeding it microseconds made
|
|
* waitForNextFrame block ~388 ms/frame and the gate never pass in steady
|
|
* state (hang after the first couple of frames). Scale real time to
|
|
* 46.5525 ticks/us (= 775875/16666.67us = 931050/20000us, both regions)
|
|
* and wrap like the 32-bit hardware counter. */
|
|
return (u32)(((uint64_t)sysGetMicroseconds() * 465525ull) / 10000ull);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* Real-OS-thread kernel */
|
|
/* ------------------------------------------------------------------------
|
|
*
|
|
* Each game thread becomes a real pthread with its own 8 MB stack (the N64
|
|
* gave each thread a dedicated sp_* stack; sharing one host stack across
|
|
* green threads is what broke the setjmp design). Message queues carry a
|
|
* side-table entry with a mutex + condition variable: osRecvMesg(BLOCK)
|
|
* waits on it, osSendMesg() signals it. A dedicated tick thread paces the
|
|
* VI retrace message and services software timers.
|
|
*/
|
|
|
|
#define PORT_MAX_THREADS 16
|
|
#define PORT_MAX_QUEUES 64
|
|
#define PORT_THREAD_STACK (8u * 1024u * 1024u)
|
|
|
|
typedef struct PortThread {
|
|
OSThread *os; /* game-visible thread object */
|
|
OSId id;
|
|
void (*entry)(void *);
|
|
void *arg;
|
|
pthread_t th;
|
|
unsigned long tid; /* OS thread id (thread dumps) */
|
|
int started; /* osStartThread called */
|
|
int exited; /* entry returned / stopped / parked (idle) */
|
|
} PortThread;
|
|
|
|
static PortThread g_pt[PORT_MAX_THREADS];
|
|
|
|
/* Side table: OSMesgQueue* -> lock/cond. The game's OSMesgQueue struct has a
|
|
* fixed N64 layout, so the synchronization state lives here instead. */
|
|
typedef struct PortQueue {
|
|
OSMesgQueue *os;
|
|
pthread_mutex_t lock;
|
|
pthread_cond_t cond;
|
|
int loggedFirstBlock; /* heartbeat aid: log each queue's first blocker */
|
|
} PortQueue;
|
|
|
|
static PortQueue g_pq[PORT_MAX_QUEUES];
|
|
static int g_pqCount = 0;
|
|
|
|
/* Heartbeat: if no frame has rendered for a while, dump thread + queue
|
|
* states so a hang shows up in the log instead of as a silent
|
|
* "Not Responding" window. */
|
|
static uint64_t g_lastFrameUs = 0;
|
|
static uint64_t g_lastHeartbeatUs = 0;
|
|
static int g_framesRendered = 0;
|
|
|
|
/* Forward decls: the VI retrace registration lives further down. */
|
|
static OSMesgQueue *g_viRetraceMQ = NULL;
|
|
static OSMesg g_viRetraceMsg = 0;
|
|
|
|
/* Vsync tick state: one VI retrace message per FRAME, at the console's
|
|
* frame rate (the N64 VI interrupt fires once per frame; GE calls
|
|
* osViSetEvent with NUM_FIELDS=1, i.e. "every retrace"). */
|
|
static uint64_t g_nextTickUs = 0;
|
|
static uint32_t g_tickIntervalUs = 1000000 / 60; /* NTSC frame rate */
|
|
|
|
static PortThread *portFind(OSThread *t)
|
|
{
|
|
for (int i = 0; i < PORT_MAX_THREADS; ++i) {
|
|
if (g_pt[i].os == t) return &g_pt[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static PortQueue *portQueueGet(OSMesgQueue *mq);
|
|
static void portPostVIEvent(void);
|
|
static void portServiceTimers(void);
|
|
static uint64_t portNextTimerUs(void);
|
|
|
|
static void portHeartbeatCheck(void)
|
|
{
|
|
/* A real hang is a permanent stall; transient >3 s gaps are normal on a
|
|
* slow/software-GL/debugger setup (level loads, gdb pauses, SIGTERM
|
|
* teardown) and were spamming ge007.crash.log with dozens of thread
|
|
* dumps. Use a longer window and stop after a few reports. */
|
|
static int fired = 0;
|
|
uint64_t now = sysGetMicroseconds();
|
|
if (fired < 3 && now - g_lastFrameUs > 8000000 && now - g_lastHeartbeatUs > 5000000) {
|
|
fired++;
|
|
g_lastHeartbeatUs = now;
|
|
sysLogPrintf(LOG_ERROR,
|
|
"kernel heartbeat: no frame rendered for %llu ms (frames=%d); state:",
|
|
(unsigned long long)(now - g_lastFrameUs), g_framesRendered);
|
|
for (int i = 0; i < PORT_MAX_THREADS; ++i) {
|
|
PortThread *t = &g_pt[i];
|
|
if (!t->os) continue;
|
|
sysLogPrintf(LOG_ERROR,
|
|
" T%d(id=%d) os=%p entry_rel=%p started=%d exited=%d",
|
|
i, (int)t->id, (void *)t->os,
|
|
(void *)((uintptr_t)t->entry - sysImageBase()),
|
|
t->started, t->exited);
|
|
}
|
|
for (int i = 0; i < g_pqCount; ++i) {
|
|
OSMesgQueue *mq = g_pq[i].os;
|
|
if (!mq) continue;
|
|
sysLogPrintf(LOG_ERROR,
|
|
" mq=%p valid=%d/%d first=%d",
|
|
(void *)mq, mq->validCount, mq->msgCount, mq->first);
|
|
}
|
|
sysLogPrintf(LOG_ERROR,
|
|
" viRetraceMQ=%p msg=%llu tickInterval=%u us",
|
|
(void *)g_viRetraceMQ,
|
|
(unsigned long long)g_viRetraceMsg, g_tickIntervalUs);
|
|
|
|
/* Where is every thread actually stuck? Unwind them all. */
|
|
/* Indexed by game OSId (thread_config.h): RMON=0 IDLE=1 SCHED=2
|
|
* MAIN=3 AUDI=4 TLB=5. */
|
|
static const char *threadNames[6] = {
|
|
"rmonThread", "idleThread", "shedThread",
|
|
"mainThread", "audioThread", "tlbThread"
|
|
};
|
|
unsigned long tids[PORT_MAX_THREADS];
|
|
const char *names[PORT_MAX_THREADS];
|
|
int n = 0;
|
|
for (int i = 0; i < PORT_MAX_THREADS && n < PORT_MAX_THREADS; ++i) {
|
|
if (g_pt[i].os && g_pt[i].started && !g_pt[i].exited) {
|
|
tids[n] = g_pt[i].tid;
|
|
names[n] = threadNames[g_pt[i].id >= 0 && g_pt[i].id < 6 ? g_pt[i].id : 3];
|
|
++n;
|
|
}
|
|
}
|
|
crashDumpThreads(tids, names, n);
|
|
}
|
|
}
|
|
|
|
/* Dedicated pacemaker: posts the VI retrace message once per frame and
|
|
* services software timers. Runs forever on its own pthread so pacing
|
|
* continues no matter which game threads are blocked where. */
|
|
static void *portTickThread(void *arg)
|
|
{
|
|
(void)arg;
|
|
for (;;) {
|
|
/* GE_DETERM (§D117, M-52 2nd attempt): retrace generation lives in
|
|
* osRecvMesg() now (see there) -- synchronous with the sole
|
|
* consumer asking for the next message, not paced by this thread.
|
|
* The M-52 FIRST attempt polled g_viRetraceMQ's empty/non-empty
|
|
* state from here on a real-time cadence; that poll's own
|
|
* OS-scheduling latency varied run-to-run and leaked directly into
|
|
* the tick count (measured: 90%/28%/16% frame divergence between
|
|
* two runs, worse than the port's baseline wall-clock
|
|
* nondeterminism this mode exists to remove). This thread still
|
|
* only does what osGetTime()-based real-time bookkeeping needs
|
|
* (timers, hang-heartbeat) -- deliberately NOT touching
|
|
* g_viRetraceMQ at all in this mode. */
|
|
if (g_determEnabled) {
|
|
portServiceTimers();
|
|
portHeartbeatCheck();
|
|
sysSleep(g_tickIntervalUs);
|
|
continue;
|
|
}
|
|
|
|
uint64_t now = sysGetMicroseconds();
|
|
if (!g_nextTickUs) g_nextTickUs = now + g_tickIntervalUs;
|
|
|
|
/* Sleep until the earlier of the next tick and a due timer, in
|
|
* small chunks (simple, portable, accurate enough for 16.7 ms). */
|
|
uint64_t wake = g_nextTickUs;
|
|
uint64_t tnext = portNextTimerUs();
|
|
if (tnext < wake) wake = tnext;
|
|
while ((now = sysGetMicroseconds()) < wake) {
|
|
uint64_t chunk = wake - now;
|
|
if (chunk > 4000) chunk = 4000;
|
|
sysSleep((uint32_t)chunk);
|
|
}
|
|
|
|
now = sysGetMicroseconds();
|
|
if (now >= g_nextTickUs) {
|
|
g_nextTickUs += g_tickIntervalUs;
|
|
if (g_nextTickUs <= now) g_nextTickUs = now + g_tickIntervalUs;
|
|
portPostVIEvent();
|
|
}
|
|
portServiceTimers();
|
|
portHeartbeatCheck();
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Call once from main() before running game code. */
|
|
void portKernelInit(void)
|
|
{
|
|
memset(g_pt, 0, sizeof(g_pt));
|
|
g_pqCount = 0;
|
|
/* Anchor the heartbeat clock so the first check doesn't compare against
|
|
* epoch 0 (the host QPC has a large absolute offset). */
|
|
g_lastFrameUs = sysGetMicroseconds();
|
|
g_lastHeartbeatUs = g_lastFrameUs;
|
|
|
|
if (osTvType == OS_TV_MPAL || osTvType == OS_TV_PAL) {
|
|
g_tickIntervalUs = 1000000 / 50; /* PAL: 50 frames/s -> 20ms */
|
|
g_determQuantum = 931050;
|
|
} else {
|
|
g_tickIntervalUs = 1000000 / 60; /* NTSC: 60 frames/s -> ~16.7ms */
|
|
g_determQuantum = 775875;
|
|
}
|
|
|
|
/* GE_DETERM=1: seed so the first waitForNextFrame() computes exactly
|
|
* (quantum + 387937) / quantum == 1 tick, per the §D117 design. */
|
|
g_determEnabled = getenv("GE_DETERM") != NULL;
|
|
if (g_determEnabled) {
|
|
g_determTicks = g_determQuantum;
|
|
g_determTraceEnabled = getenv("GE_DETERM_TRACE") != NULL;
|
|
sysLogPrintf(LOG_NOTE,
|
|
"GE_DETERM=1: fixed-tick deterministic mode (test-only, "
|
|
"quantum=%u) -- osGetCount() is now frame-locked, not wall-clock",
|
|
g_determQuantum);
|
|
if (g_determTraceEnabled) {
|
|
sysLogPrintf(LOG_NOTE, "DETERMTRACE seq=0 ticks=%u event=seed", g_determTicks);
|
|
}
|
|
}
|
|
|
|
pthread_t th;
|
|
if (pthread_create(&th, NULL, portTickThread, NULL) != 0) {
|
|
sysFatalError("portKernelInit: pthread_create(tick) failed");
|
|
}
|
|
pthread_detach(th);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* Threads */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
void imThreadExitRelease(void); /* D152: defined in the interrupt-mask section below */
|
|
|
|
/* pthread entry wrapper. */
|
|
static void *portThreadWrapper(void *arg)
|
|
{
|
|
PortThread *pt = (PortThread *)arg;
|
|
|
|
#if defined(PLATFORM_WINDOWS)
|
|
pt->tid = GetCurrentThreadId();
|
|
#else
|
|
pt->tid = (unsigned long)pthread_self();
|
|
#endif
|
|
|
|
/* The N64 idle thread is an infinite no-yield loop (idleproc). On the
|
|
* host it would just burn a core and nothing depends on it running, so
|
|
* park it instead. */
|
|
if (pt->id == 1 /* IDLE_THREAD_ID */) {
|
|
pt->exited = 1;
|
|
for (;;) sysSleep(1000000);
|
|
return NULL;
|
|
}
|
|
|
|
pt->entry(pt->arg);
|
|
imThreadExitRelease(); /* D152: don't leak an OS_IM_NONE section on exit */
|
|
pt->exited = 1;
|
|
if (pt->os) pt->os->state = OS_STATE_STOPPED;
|
|
return NULL;
|
|
}
|
|
|
|
void osCreateThread(OSThread *t, OSId id, void (*entry)(void *), void *arg,
|
|
void *sp, OSPri prio)
|
|
{
|
|
PortThread *pt = NULL;
|
|
(void)sp; /* each host thread gets its own stack */
|
|
for (int i = 0; i < PORT_MAX_THREADS; ++i) {
|
|
if (!g_pt[i].os) { pt = &g_pt[i]; break; }
|
|
}
|
|
if (!pt) {
|
|
sysFatalError("osCreateThread: out of port thread slots");
|
|
return;
|
|
}
|
|
|
|
memset(pt, 0, sizeof(*pt));
|
|
pt->os = t;
|
|
pt->id = id;
|
|
pt->entry = entry;
|
|
pt->arg = arg;
|
|
|
|
t->id = id;
|
|
t->priority = prio;
|
|
t->state = OS_STATE_STOPPED;
|
|
}
|
|
|
|
#if !defined(_WIN32)
|
|
#include <sys/mman.h>
|
|
/* The decomp aligns/compares stack-buffer pointers by truncating to u32
|
|
* (e.g. `(u32)compbuffer` in image.c texLoad) — an N64 assumption. glibc
|
|
* puts pthread stacks above 4 GiB, so those truncations corrupt. Force each
|
|
* game-thread stack into the low 2 GiB with MAP_32BIT so every such idiom
|
|
* works exactly as on the console. Windows pthread stacks are already low. */
|
|
static void *portAllocLowStack(size_t sz)
|
|
{
|
|
#if defined(MAP_32BIT)
|
|
void *p = mmap(NULL, sz, PROT_READ | PROT_WRITE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT
|
|
#if defined(MAP_STACK)
|
|
| MAP_STACK
|
|
#endif
|
|
, -1, 0);
|
|
if (p != MAP_FAILED)
|
|
return p;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
#endif
|
|
|
|
void osStartThread(OSThread *t)
|
|
{
|
|
PortThread *pt = portFind(t);
|
|
if (!pt || pt->started) return;
|
|
pt->started = 1;
|
|
t->state = OS_STATE_RUNNABLE;
|
|
|
|
pthread_attr_t attr;
|
|
int rc;
|
|
pthread_attr_init(&attr);
|
|
#if !defined(_WIN32)
|
|
void *lowStack = portAllocLowStack(PORT_THREAD_STACK);
|
|
if (lowStack)
|
|
pthread_attr_setstack(&attr, lowStack, PORT_THREAD_STACK);
|
|
else
|
|
pthread_attr_setstacksize(&attr, PORT_THREAD_STACK);
|
|
#else
|
|
pthread_attr_setstacksize(&attr, PORT_THREAD_STACK);
|
|
#endif
|
|
rc = pthread_create(&pt->th, &attr, portThreadWrapper, pt);
|
|
pthread_attr_destroy(&attr);
|
|
if (rc != 0) {
|
|
sysFatalError("osStartThread: pthread_create failed (%d)", rc);
|
|
}
|
|
}
|
|
|
|
void osStopThread(OSThread *t)
|
|
{
|
|
PortThread *pt = portFind(t);
|
|
if (!pt) return;
|
|
/* A host thread cannot be killed mid-flight; mark it and let the entry
|
|
* run to completion (game code only stops threads at quiescent points). */
|
|
pt->exited = 1;
|
|
t->state = OS_STATE_STOPPED;
|
|
}
|
|
|
|
void osDestroyThread(OSThread *t)
|
|
{
|
|
PortThread *pt = portFind(t);
|
|
if (!pt) return;
|
|
pt->os = NULL;
|
|
t->id = 0;
|
|
}
|
|
|
|
void osYieldThread(void)
|
|
{
|
|
/* GE rarely yields explicitly; a 1 ms sleep is a fine host equivalent
|
|
* (sched_yield() would need MinGW's <sched.h>, which the game's own
|
|
* src/sched.h shadows on the include path). */
|
|
sysSleep(1);
|
|
}
|
|
|
|
OSPri osGetThreadPri(OSThread *t)
|
|
{
|
|
return t ? t->priority : 0;
|
|
}
|
|
|
|
void osSetThreadPri(OSThread *t, OSPri prio)
|
|
{
|
|
if (t) t->priority = prio;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* Message queues */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
/* D59: the lookup/create below MUST be serialized. Two threads first
|
|
* touching the same (or different) OSMesgQueue concurrently used to race on
|
|
* g_pqCount and end up with two PortQueues — or one re-initialized slot —
|
|
* for a single queue. Sends then signal a cond the receiver never waits on
|
|
* (lost wakeup: permanent stall after frame 2), and re-initing a live mutex
|
|
* is UB that can corrupt unrelated state (wild-pointer crashes). Boot-time
|
|
* osCreateMesgQueue() pre-registers most queues, but first-touches still race
|
|
* for any queue created later or touched before its create call. */
|
|
static pthread_mutex_t s_pqLock = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
static PortQueue *portQueueGet(OSMesgQueue *mq)
|
|
{
|
|
pthread_mutex_lock(&s_pqLock);
|
|
for (int i = 0; i < g_pqCount; ++i) {
|
|
if (g_pq[i].os == mq) {
|
|
pthread_mutex_unlock(&s_pqLock);
|
|
return &g_pq[i];
|
|
}
|
|
}
|
|
if (g_pqCount >= PORT_MAX_QUEUES) {
|
|
sysFatalError("portQueueGet: out of port queue slots");
|
|
}
|
|
PortQueue *pq = &g_pq[g_pqCount++];
|
|
pq->os = mq;
|
|
pthread_mutex_init(&pq->lock, NULL);
|
|
pthread_cond_init(&pq->cond, NULL);
|
|
pq->loggedFirstBlock = 0;
|
|
pthread_mutex_unlock(&s_pqLock);
|
|
return pq;
|
|
}
|
|
|
|
void osCreateMesgQueue(OSMesgQueue *mq, OSMesg *first, s32 count)
|
|
{
|
|
memset(mq, 0, sizeof(*mq));
|
|
mq->msg = first;
|
|
mq->msgCount = count;
|
|
(void)portQueueGet(mq); /* register lock/cond */
|
|
}
|
|
|
|
OSMesg osDequeueMesg(OSMesgQueue *mq)
|
|
{
|
|
PortQueue *pq = portQueueGet(mq);
|
|
pthread_mutex_lock(&pq->lock);
|
|
OSMesg m = NULL;
|
|
if (mq->validCount > 0) {
|
|
m = mq->msg[mq->first];
|
|
mq->first = (mq->first + 1) % mq->msgCount;
|
|
--mq->validCount;
|
|
pthread_cond_signal(&pq->cond);
|
|
}
|
|
pthread_mutex_unlock(&pq->lock);
|
|
return m;
|
|
}
|
|
|
|
OSMesg osMesgQueueLast(OSMesgQueue *mq)
|
|
{
|
|
PortQueue *pq = portQueueGet(mq);
|
|
pthread_mutex_lock(&pq->lock);
|
|
OSMesg m = (mq->validCount > 0)
|
|
? mq->msg[(mq->first + mq->validCount - 1) % mq->msgCount]
|
|
: NULL;
|
|
pthread_mutex_unlock(&pq->lock);
|
|
return m;
|
|
}
|
|
|
|
void osEnqueueMesg(OSMesgQueue *mq, OSMesg msg)
|
|
{
|
|
PortQueue *pq = portQueueGet(mq);
|
|
pthread_mutex_lock(&pq->lock);
|
|
if (mq->validCount < mq->msgCount) {
|
|
mq->msg[(mq->first + mq->validCount) % mq->msgCount] = msg;
|
|
++mq->validCount;
|
|
pthread_cond_signal(&pq->cond);
|
|
}
|
|
pthread_mutex_unlock(&pq->lock);
|
|
}
|
|
|
|
/* TEMP D62: full message-flow trace (env GE_D62=1 -> d62mesg.log). */
|
|
static FILE *s_d62log = NULL;
|
|
static int s_d62opened = 0;
|
|
static pthread_mutex_t s_d62lock = PTHREAD_MUTEX_INITIALIZER;
|
|
static void d62log(const char *op, OSMesgQueue *mq, OSMesg msg)
|
|
{
|
|
if (!s_d62opened) {
|
|
s_d62opened = 1;
|
|
if (getenv("GE_D62"))
|
|
s_d62log = fopen("d62mesg.log", "w");
|
|
}
|
|
if (s_d62log) {
|
|
pthread_mutex_lock(&s_d62lock);
|
|
fprintf(s_d62log, "%s mq=%p msg=%p valid=%d/%d first=%d\n",
|
|
op, (void *)mq, (void *)msg, mq->validCount,
|
|
mq->msgCount, mq->first);
|
|
fflush(s_d62log);
|
|
pthread_mutex_unlock(&s_d62lock);
|
|
}
|
|
}
|
|
|
|
|
|
s32 osSendMesg(OSMesgQueue *mq, OSMesg msg, s32 flag)
|
|
{
|
|
PortQueue *pq = portQueueGet(mq);
|
|
d62log("SEND", mq, msg); /* TEMP D62 */
|
|
/* TEMP D51: watch sends to the 32-slot queues (sched cmdQ + client Qs) */
|
|
if (getenv("GE_D51") && mq->msgCount == 32 && !(g_viRetraceMQ && mq == g_viRetraceMQ)) {
|
|
void *ra = __builtin_return_address(0);
|
|
sysLogPrintf(LOG_NOTE, "D51 send32 mq=%p from %p msg=%p valid=%d/%d",
|
|
(void *)mq, ra, (void *)msg, mq->validCount, mq->msgCount);
|
|
}
|
|
pthread_mutex_lock(&pq->lock);
|
|
while (mq->validCount >= mq->msgCount) {
|
|
if (flag != OS_MESG_BLOCK) {
|
|
pthread_mutex_unlock(&pq->lock);
|
|
return -1;
|
|
}
|
|
pthread_cond_wait(&pq->cond, &pq->lock);
|
|
}
|
|
mq->msg[(mq->first + mq->validCount) % mq->msgCount] = msg;
|
|
++mq->validCount;
|
|
pthread_cond_signal(&pq->cond);
|
|
pthread_mutex_unlock(&pq->lock);
|
|
return 0;
|
|
}
|
|
|
|
/* TEMP D60: gfxFrameMsgQ receive watch (env GE_D60=1). The main loop only
|
|
* acts on msg type 1 (retrace) / 4 (RSP done); anything else means the
|
|
* message object's .type field was clobbered. */
|
|
extern OSMesgQueue gfxFrameMsgQ; /* src/init.c */
|
|
static void d60logRecv(OSMesgQueue *mq, OSMesg m) {
|
|
if (mq != &gfxFrameMsgQ || !getenv("GE_D60")) return;
|
|
static uint64_t n = 0;
|
|
const u16 type = *(const u16 *)m; /* OSScMsg.type */
|
|
if (n < 20 || (n % 300) == 0 || (type != 1 && type != 4))
|
|
sysLogPrintf(LOG_NOTE, "D60 recv #%llu mq=%p msg=%p type=%u",
|
|
(unsigned long long)n, (void *)mq, (void *)m, type);
|
|
++n;
|
|
}
|
|
|
|
s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag)
|
|
{
|
|
PortQueue *pq = portQueueGet(mq);
|
|
d62log("RECV", mq, 0); /* TEMP D62 */
|
|
pthread_mutex_lock(&pq->lock);
|
|
while (mq->validCount == 0) {
|
|
if (flag != OS_MESG_BLOCK) {
|
|
pthread_mutex_unlock(&pq->lock);
|
|
return -1;
|
|
}
|
|
/* GE_DETERM (§D117, M-52 2nd attempt): __scMain (sched.c) is the
|
|
* SOLE consumer of g_viRetraceMQ, and everything the rest of the
|
|
* boot/frame chain waits on (task execution via osSpTaskStartGo,
|
|
* joyPoll() -- which is also what unblocks joyCheckStatusThreadSafe
|
|
* -- and the forward to mainThread's gfxFrameMsgQ) happens
|
|
* synchronously inside __scMain's handling of THAT message, on this
|
|
* same thread, before it comes back to ask for the next one. So
|
|
* "the consumer is asking and none exists" is itself the correct,
|
|
* purely call-sequenced trigger for the next virtual retrace --
|
|
* driven by program order, not by an independent thread polling on
|
|
* a wall-clock cadence (the M-52 first attempt's mistake: that
|
|
* poll's timing varied run-to-run and leaked into the tick count).
|
|
* Enqueue inline (not via osEnqueueMesg -- would re-lock pq->lock,
|
|
* which we already hold). */
|
|
if (g_determEnabled && mq == g_viRetraceMQ) {
|
|
/* Still synthesize the message every time (keeps __scMain/
|
|
* joyPoll unblocked -- no deadlock), but only advance the clock
|
|
* up to a fixed cap before the first real task runs. See the
|
|
* g_determTaskEverRun comment above. */
|
|
if (g_determTaskEverRun || g_determPreTaskTicksGranted < 1) {
|
|
g_determTicks += g_determQuantum;
|
|
if (!g_determTaskEverRun) ++g_determPreTaskTicksGranted;
|
|
if (g_determTraceEnabled) {
|
|
sysLogPrintf(LOG_NOTE,
|
|
"DETERMTRACE seq=%u ticks=%u event=advance taskEverRun=%d",
|
|
++g_determTraceSeq, g_determTicks, g_determTaskEverRun);
|
|
}
|
|
} else if (g_determTraceEnabled) {
|
|
sysLogPrintf(LOG_NOTE,
|
|
"DETERMTRACE seq=%u ticks=%u event=capped taskEverRun=%d",
|
|
++g_determTraceSeq, g_determTicks, g_determTaskEverRun);
|
|
}
|
|
mq->msg[(mq->first + mq->validCount) % mq->msgCount] = g_viRetraceMsg;
|
|
++mq->validCount;
|
|
break;
|
|
}
|
|
/* Log each queue's first blocking call site (the return address is
|
|
* the caller); symbolicate with addr2line to find where in game code
|
|
* this wait happens. */
|
|
if (!pq->loggedFirstBlock) {
|
|
pq->loggedFirstBlock = 1;
|
|
void *ra = __builtin_return_address(0);
|
|
sysLogPrintf(LOG_NOTE, "first block on mq=%p from %p (rel %p)",
|
|
(void *)mq, ra,
|
|
(void *)((uintptr_t)ra - sysImageBase()));
|
|
}
|
|
pthread_cond_wait(&pq->cond, &pq->lock);
|
|
}
|
|
OSMesg m = mq->msg[mq->first];
|
|
mq->first = (mq->first + 1) % mq->msgCount;
|
|
--mq->validCount;
|
|
if (msg) *msg = m;
|
|
d60logRecv(mq, m); /* TEMP D60 */
|
|
pthread_cond_signal(&pq->cond);
|
|
pthread_mutex_unlock(&pq->lock);
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* Events (osSetEventMesg: "interrupt" -> message queue mapping) */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
typedef struct {
|
|
OSMesgQueue *mq;
|
|
OSMesg msg;
|
|
int used;
|
|
} PortEvent;
|
|
|
|
static PortEvent g_events[16];
|
|
|
|
void osSetEventMesg(OSEvent e, OSMesgQueue *mq, OSMesg msg)
|
|
{
|
|
if (e < 16) {
|
|
g_events[e].mq = mq;
|
|
g_events[e].msg = msg;
|
|
g_events[e].used = 1;
|
|
}
|
|
}
|
|
|
|
/* D134: an SP/DP "task done" event must NEVER be dropped.
|
|
*
|
|
* These land in the scheduler's 8-slot interruptQ, which the 60 Hz VI
|
|
* pacemaker also posts VIDEO_MSG into. osSpTaskStartGo runs the whole frame
|
|
* synchronously on the sched thread (fast3d), so a slow frame (the first two
|
|
* are 30-80 ms) lets the pacemaker queue several retraces meanwhile. Once the
|
|
* queue is full the NOBLOCK done-post is silently discarded, __scMain never
|
|
* clears sc->curRSPTask, the client never gets OS_SC_DONE_MSG, and the main
|
|
* loop's pendingGfx never clears -> permanent stall at frames=2.
|
|
*
|
|
* OS_MESG_BLOCK is NOT the fix: the done event is posted from the sched thread
|
|
* itself, the only consumer of that queue, so blocking self-deadlocks. Instead
|
|
* make room by dropping the OLDEST message (always a stale retrace -- retrace
|
|
* drops are already normal, N64 osViSetEvent posts NOBLOCK too). */
|
|
static void portPostEventForce(OSId e)
|
|
{
|
|
PortEvent *ev = &g_events[e];
|
|
if (!ev->used || !ev->mq) return;
|
|
if (osSendMesg(ev->mq, ev->msg, OS_MESG_NOBLOCK) == 0) return;
|
|
(void)osDequeueMesg(ev->mq);
|
|
if (osSendMesg(ev->mq, ev->msg, OS_MESG_NOBLOCK) != 0) {
|
|
sysLogPrintf(LOG_ERROR, "D134: task-done event %d lost (mq=%p full)",
|
|
(int)e, (void *)ev->mq);
|
|
}
|
|
}
|
|
|
|
static void portPostEvent(OSId e)
|
|
{
|
|
PortEvent *ev = &g_events[e];
|
|
if (ev->used && ev->mq) {
|
|
osSendMesg(ev->mq, ev->msg, OS_MESG_NOBLOCK);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* VI (video) — retrace pacing + framebuffer bookkeeping */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
static int g_viBlack = 1; /* start black, like the N64 VI */
|
|
|
|
void osViSetEvent(OSMesgQueue *mq, OSMesg msg, u32 retraceCount)
|
|
{
|
|
(void)retraceCount;
|
|
g_viRetraceMQ = mq;
|
|
g_viRetraceMsg = msg;
|
|
}
|
|
|
|
/* TEMP D51: instrument the retrace pacemaker */
|
|
static uint64_t g_viPostCount = 0;
|
|
static void portPostVIEvent(void)
|
|
{
|
|
if (g_viRetraceMQ) {
|
|
/* D134: keep two slots free for the SP/DP done events posted at the
|
|
* end of a synchronous fast3d frame. A retrace posted into a backlog
|
|
* is stale anyway -- sched has not drained the previous one yet. */
|
|
if (g_viRetraceMQ->validCount >= g_viRetraceMQ->msgCount - 2) return;
|
|
s32 r = osSendMesg(g_viRetraceMQ, g_viRetraceMsg, OS_MESG_NOBLOCK);
|
|
if (++g_viPostCount % 60 == 1 || r != 0) {
|
|
sysLogPrintf(r != 0 ? LOG_ERROR : LOG_NOTE,
|
|
"D51 vi post #%llu mq=%p msg=%d valid=%d/%d ret=%d",
|
|
(unsigned long long)g_viPostCount, (void *)g_viRetraceMQ,
|
|
(int)g_viRetraceMsg, g_viRetraceMQ->validCount,
|
|
g_viRetraceMQ->msgCount, r);
|
|
}
|
|
}
|
|
}
|
|
/* END TEMP D51 */
|
|
|
|
void osViSetMode(OSViMode *vm)
|
|
{
|
|
if (!vm) return;
|
|
int pal = (vm->comRegs.ctrl & OS_VI_BIT_PAL) != 0;
|
|
/* D103: fast3d's SCREEN_WIDTH/SCREEN_HEIGHT (== the native viewport) must
|
|
* be the CFB space GE authors its gSPViewport / G_SETSCISSOR / 2D texrect
|
|
* commands in — that is bufx x bufy (320x240 NTSC, 320x272 PAL), NOT the
|
|
* visible scanline count. comRegs.width already carries bufx; recover the
|
|
* matching CFB height from fldRegs.yScale (GE sets it to
|
|
* bufy * YSCALE_MAX(0x800) / SCREEN_HEIGHT_MAX(480), fr.c) instead of the
|
|
* old hard-coded 480/400, which made RATIO_Y half of RATIO_X and squashed
|
|
* every frame into a half-height band. */
|
|
u32 w = vm->comRegs.width;
|
|
s32 h = (s32)(((vm->fldRegs[0].yScale & 0xFFF) * 480u) / 0x800u);
|
|
if (h <= 0 || h > 480) h = pal ? 272 : 240; /* fallback: LAN1 CFB height */
|
|
if (w > 640) w = 640;
|
|
videoUpdateNativeResolution((s32)w, h);
|
|
}
|
|
|
|
void osViSetSpecialFeatures(u32 f) { (void)f; }
|
|
void osViVSyncCallback(OSMesgQueue *mq, OSMesg msg)
|
|
{
|
|
/* Legacy VI sync hook; retrace comes from osViSetEvent instead. */
|
|
(void)mq; (void)msg;
|
|
}
|
|
void osViWaitVSync(void) { /* pacing is handled by the kernel tick */ }
|
|
void osViSetSync(OSMesgQueue *mq, OSMesg msg) { (void)mq; (void)msg; }
|
|
void osViBlack(u8 active) { g_viBlack = active; (void)g_viBlack; }
|
|
void osViSetYScale(f32 value) { (void)value; }
|
|
void osViSetXScale(f32 value) { (void)value; }
|
|
|
|
/* __scTaskReady() requires these to compare equal, otherwise no gfx task
|
|
* ever runs. The software RSP draws straight into the GL back buffer, so a
|
|
* single constant "framebuffer" is all that's needed. */
|
|
static u32 g_dummyFramebuffer = 0x1;
|
|
void *osViGetCurrentFramebuffer(void) { return &g_dummyFramebuffer; }
|
|
void *osViGetNextFramebuffer(void) { return &g_dummyFramebuffer; }
|
|
|
|
/* The frame was already presented by videoEndFrame() inside
|
|
* osSpTaskStartGo(); nothing to do here. */
|
|
void osViSwapBuffer(void *fb) { (void)fb; }
|
|
void osViRepeatLine(u8 line) { (void)line; }
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* AI (audio) — map onto the port audio layer. Phase 3. */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
s32 osAiSetFrequency(u32 hz)
|
|
{
|
|
return (s32)hz;
|
|
}
|
|
s32 osAiSetNextBuffer(void *buf, u32 size)
|
|
{
|
|
audioSetNextBuffer((const s16 *)buf, size);
|
|
return 1;
|
|
}
|
|
u32 osAiGetLength(void)
|
|
{
|
|
/* D204/F1: AI_LEN_REG is the bytes remaining in the buffer the DAC is
|
|
* CURRENTLY playing -- not the whole queue. Reporting the full SDL queue
|
|
* depth here pinned src/audi.c:531's frame-size regulator at its lower
|
|
* clamp and let its u32 subtraction wrap into a 74x heap overrun. See the
|
|
* long comment on audioGetAiLengthBytes() in port/src/audio.c. */
|
|
return audioGetAiLengthBytes();
|
|
}
|
|
void osAiSetConvert(u32 convert) { (void)convert; }
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* PI (peripheral) — ROM/cart reads come from the loaded ROM image. */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
/*
|
|
* PI DMA is synchronous on the PC: romdata.c maps the .z64 at the N64 cart
|
|
* base (0x10000000), so an OS_READ from a cart address is a plain memcpy.
|
|
* The N64 PI hardware posts the caller's OSMesgPI to the message queue on
|
|
* completion; osPiStartDma() below replicates that, because the game blocks
|
|
* in romReceiveMesg()/osRecvMesg() after every romCopy().
|
|
*/
|
|
/* TEMP D60: identify sidecar model reads (env GE_D60=1). srcPA-base is the
|
|
* manifest offset; cross-reference data/pcmodels-<region>/manifest.csv for
|
|
* the file name. */
|
|
extern uintptr_t pcmodelsSidecarBase(void);
|
|
extern uint32_t pcmodelsTotalSize(void);
|
|
/* D69: same diagnostic for the bg/stan sidecar image. */
|
|
extern uintptr_t pccgSidecarBase(void);
|
|
extern uint32_t pccgTotalSize(void);
|
|
static void d60logSidecarRead(u32 srcPA, void *dstVA, u32 size) {
|
|
if (!getenv("GE_D60")) return;
|
|
uintptr_t base = pcmodelsSidecarBase();
|
|
if (base && srcPA >= base && srcPA < base + pcmodelsTotalSize()) {
|
|
sysLogPrintf(LOG_NOTE, "D60 sidecar read off=%llu size=0x%X dst=%p",
|
|
(unsigned long long)(srcPA - base), size, dstVA);
|
|
return;
|
|
}
|
|
uintptr_t cgBase = pccgSidecarBase();
|
|
if (cgBase && srcPA >= cgBase && srcPA < cgBase + pccgTotalSize())
|
|
sysLogPrintf(LOG_NOTE, "D69 pccg sidecar read off=%llu size=0x%X dst=%p",
|
|
(unsigned long long)(srcPA - cgBase), size, dstVA);
|
|
}
|
|
|
|
/* TEMP D60/D61: a ROM-read DMA target must land in the game DRAM views
|
|
* (V1/V2) or the current thread's stack (texLoad's compbuffer). Anything
|
|
* else is unmapped host memory on PC. */
|
|
static FILE *s_d61log = NULL;
|
|
static int s_d61opened = 0;
|
|
|
|
static int dramHostAddrValid(uintptr_t addr, u32 size)
|
|
{
|
|
static const uintptr_t bases[2] = { 0x70000000UL, 0x80000000UL };
|
|
for (int i = 0; i < 2; i++) {
|
|
if (addr >= bases[i] && addr + size <= bases[i] + 0x00800000UL)
|
|
return 1;
|
|
}
|
|
/* Any other host-committed region is a legitimate DMA target: .bss/.data
|
|
* buffers (e.g. ramrom_data_target), stack compbuffers, sidecar images.
|
|
* Truncated wild addresses (0x40xxxxxx from s32 pointer math) are not
|
|
* committed, so VirtualQuery still catches them. */
|
|
#if defined(PLATFORM_WINDOWS)
|
|
{
|
|
MEMORY_BASIC_INFORMATION mbi;
|
|
if (VirtualQuery((LPCVOID)addr, &mbi, sizeof(mbi)) == sizeof(mbi) &&
|
|
mbi.State == MEM_COMMIT &&
|
|
(uintptr_t)mbi.BaseAddress <= addr &&
|
|
(uintptr_t)mbi.BaseAddress + mbi.RegionSize >= addr + size)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
#else
|
|
/* No cheap committed-memory probe on POSIX; this is a TEMP D60 diagnostic.
|
|
* Be permissive so legitimate .bss/stack/sidecar DMA targets are not
|
|
* flagged as fatal (a genuinely wild target still faults in the memcpy). */
|
|
(void)size;
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size)
|
|
{
|
|
if (size == 0)
|
|
return;
|
|
if (direction == OS_READ) {
|
|
d60logSidecarRead(srcPA, dstVA, size); /* TEMP D60 */
|
|
if (!romdataCartAddrValid(srcPA, size)) {
|
|
sysLogPrintf(LOG_WARNING,
|
|
"osPiStartDma: ROM read out of range "
|
|
"(src=0x%08X size=0x%X); skipped",
|
|
srcPA, size);
|
|
return;
|
|
}
|
|
/* TEMP D60: validate the DMA target too. The N64 PI happily DMAs to
|
|
* any KSEG address; on PC an unmapped target is a wild memcpy.
|
|
* Log the whole call chain (romCopy <- doRomCopy <- osPiStartDma) so
|
|
* the game-side caller can be symbolicated offline. */
|
|
/* TEMP D61: log every ROM read (dst/src/size). The last line before
|
|
* a crash identifies the culprit; dst symbolizes offline via nm. */
|
|
if (!s_d61opened && getenv("GE_D61")) {
|
|
s_d61log = fopen("d61dma.log", "w");
|
|
s_d61opened = 1;
|
|
}
|
|
if (s_d61log) {
|
|
static int d61count = 0;
|
|
fprintf(s_d61log, "D61 %06d dst=%p src=0x%08X size=0x%X\n",
|
|
++d61count, dstVA, srcPA, size);
|
|
fflush(s_d61log);
|
|
}
|
|
if (!dramHostAddrValid((uintptr_t)dstVA, size)) {
|
|
/* No __builtin_return_address here: the caller's frame chain is
|
|
* not always walkable (it faulted). Log the raw stack window
|
|
* instead — return addresses are in it and symbolicate offline.
|
|
* The FATAL below re-dumps RSP/registers via the crash handler. */
|
|
const uint64_t *sp = (const uint64_t *)__builtin_frame_address(0);
|
|
char win[1200] = "";
|
|
char *wp = win;
|
|
for (int i = 0; i < 32; i++) {
|
|
wp += snprintf(wp, win + sizeof(win) - (wp - win),
|
|
" %p", (void *)sp[i]);
|
|
}
|
|
#if defined(PLATFORM_WINDOWS)
|
|
{
|
|
PVOID tlow = NULL, thigh = NULL;
|
|
GetCurrentThreadStackLimits(&tlow, &thigh);
|
|
sysLogPrintf(LOG_ERROR,
|
|
"D60 thread stack: %p..%p dst-in-stack=%d\n",
|
|
(void *)tlow, (void *)thigh,
|
|
((uintptr_t)dstVA >= (uintptr_t)tlow &&
|
|
(uintptr_t)dstVA < (uintptr_t)thigh));
|
|
}
|
|
#else
|
|
/* TEMP D60 diagnostic: no portable committed-stack query without
|
|
* _GNU_SOURCE (pthread_getattr_np); the raw stack window above and
|
|
* the crash handler's register dump are enough to symbolicate. */
|
|
sysLogPrintf(LOG_ERROR, "D60 thread stack: (n/a on POSIX)\n");
|
|
#endif
|
|
sysLogPrintf(LOG_ERROR,
|
|
"D60 BAD DMA TARGET dst=%p src=0x%08X size=0x%X "
|
|
"stack@rbp:%s\n",
|
|
dstVA, srcPA, size, win);
|
|
sysFatalError("D60: ROM-read target %p + 0x%X not host-mapped "
|
|
"(src=0x%08X)", dstVA, size, srcPA);
|
|
}
|
|
memcpy(dstVA, (const void *)(uintptr_t)srcPA, size);
|
|
} else {
|
|
/* OS_WRITE: the game never writes the cart (saves go to EEPROM via
|
|
* osEeprom*, shimmed separately). Log and drop. */
|
|
sysLogPrintf(LOG_WARNING,
|
|
"osPiStartDma: cart write dropped (src=0x%08X size=0x%X)",
|
|
srcPA, size);
|
|
}
|
|
}
|
|
|
|
void osPiCreateManager(OSMesgQueue *mq, int prio) { (void)mq; (void)prio; }
|
|
s32 osPiStartDma(OSIoMesg *mesg, s32 prio, s32 direction, u32 addr,
|
|
void *buf, u32 size, OSMesgQueue *mq)
|
|
{
|
|
piServiceDma(direction, addr, buf, size);
|
|
(void)prio;
|
|
/* Post the completion message exactly like the PI hardware would —
|
|
* even when the DMA was skipped/dropped above, so callers never
|
|
* deadlock in osRecvMesg(). */
|
|
if (mq) {
|
|
osSendMesg(mq, mesg ? (OSMesg)mesg : (OSMesg)1, OS_MESG_BLOCK);
|
|
}
|
|
return 1; /* done */
|
|
}
|
|
s32 osPiRawStartDma(s32 direction, u32 srcPA, void *dstVA, u32 size)
|
|
{
|
|
piServiceDma(direction, srcPA, dstVA, size);
|
|
return 1; /* done */
|
|
}
|
|
u32 osPiGetStatus(void) { return 0; } /* not busy */
|
|
|
|
/*
|
|
* PI device register read (normally src/libultra/io, EXCLUDED). token.c uses
|
|
* it to read the cartridge token string in 32-bit words starting at
|
|
* 0xFFB000. On the PC there is no cartridge token register, so serve the
|
|
* bytes of the host command line (sysGetTokenString) instead — this makes
|
|
* N64 debug switches like "-level_09" / "-hard1" work on PC. Each word is
|
|
* packed so byte N of the string lands at the lower address (matches the
|
|
* N64's in-order byte delivery once read back through the char buffer).
|
|
* Any other address reads as 0.
|
|
*/
|
|
#define PORT_TOKEN_IO_BASE 0xFFB000u
|
|
|
|
s32 osPiReadIo(u32 devAddr, u32 *data)
|
|
{
|
|
if (!data)
|
|
return 0;
|
|
|
|
if (devAddr >= PORT_TOKEN_IO_BASE &&
|
|
devAddr < PORT_TOKEN_IO_BASE + 0x1000) {
|
|
const char *tok = sysGetTokenString();
|
|
u32 off = devAddr - PORT_TOKEN_IO_BASE;
|
|
u32 len = (u32)strlen(tok);
|
|
u32 w = 0;
|
|
for (u32 i = 0; i < 4; ++i) {
|
|
u8 c = (off + i < len) ? (u8)tok[off + i] : 0;
|
|
w |= (u32)c << (8 * i);
|
|
}
|
|
*data = w;
|
|
return 0;
|
|
}
|
|
|
|
*data = 0;
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* SI (controller) — keyboard-backed. */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
/* errno is a macro in C (errno.h); the N64 structs have an `errno` field.
|
|
* Undefine it for this section and restore it afterwards. */
|
|
#pragma push_macro("errno")
|
|
#undef errno
|
|
|
|
static OSContStatus g_contStatus[MAXCONTROLLERS];
|
|
static OSContPad g_contPad[MAXCONTROLLERS];
|
|
static u8 g_contConnected = 0x1; /* controller 0 connected */
|
|
|
|
/* Snapshot all controllers from the SDL input module (port/src/input.c).
|
|
* That module owns the keyboard/mouse/gamepad -> N64 mapping and the
|
|
* mouse-look -> C-button bridge; this just marshals its output into the
|
|
* OSContPad / OSContStatus arrays joy.c reads via osContGetReadData(). */
|
|
static void contSnapshotFromKeyboard(void)
|
|
{
|
|
inputUpdate();
|
|
|
|
const s32 mask = inputConnectedMask();
|
|
g_contConnected = (u8)mask;
|
|
|
|
for (int i = 0; i < MAXCONTROLLERS; ++i) {
|
|
const int connected = (mask & (1 << i)) != 0;
|
|
s8 sx = 0, sy = 0;
|
|
u16 button = connected ? (u16)inputComputePad(i, &sx, &sy) : 0;
|
|
|
|
g_contStatus[i].type = connected ? CONT_TYPE_NORMAL : 0;
|
|
g_contStatus[i].status = 0;
|
|
g_contStatus[i].errno = connected ? 0 : CONT_NO_RESPONSE_ERROR;
|
|
|
|
g_contPad[i].button = button;
|
|
g_contPad[i].stick_x = connected ? sx : 0;
|
|
g_contPad[i].stick_y = connected ? sy : 0;
|
|
g_contPad[i].errno = connected ? 0 : CONT_NO_RESPONSE_ERROR;
|
|
}
|
|
}
|
|
|
|
#pragma pop_macro("errno")
|
|
|
|
s32 osContInit(OSMesgQueue *mesgq, u8 *bitpattern, OSContStatus *data)
|
|
{
|
|
(void)data; /* the game passes its own array; osContGetQuery fills it */
|
|
if (bitpattern) *bitpattern = g_contConnected;
|
|
return 0;
|
|
}
|
|
|
|
s32 osContStartReadData(OSMesgQueue *mesgq)
|
|
{
|
|
contSnapshotFromKeyboard();
|
|
/* Complete immediately: post the SI "done" message so a blocking
|
|
* osRecvMesg() right after (joy.c) returns at once. */
|
|
if (mesgq) osSendMesg(mesgq, NULL, OS_MESG_NOBLOCK);
|
|
return 0;
|
|
}
|
|
|
|
s32 osContStartQuery(OSMesgQueue *mq)
|
|
{
|
|
contSnapshotFromKeyboard();
|
|
if (mq) osSendMesg(mq, NULL, OS_MESG_NOBLOCK);
|
|
return 0;
|
|
}
|
|
|
|
void osContGetQuery(OSContStatus *status)
|
|
{
|
|
memcpy(status, g_contStatus, sizeof(g_contStatus));
|
|
}
|
|
|
|
void osContGetReadData(OSContPad *pad)
|
|
{
|
|
/* N64 semantics: fill one OSContPad per controller channel. joy.c passes
|
|
* g_ContData[0].samples[i].pads (a MAXCONTROLLERS-long array). */
|
|
memcpy(pad, g_contPad, sizeof(g_contPad));
|
|
}
|
|
|
|
s32 osContReset(OSMesgQueue *mq, OSContStatus *status)
|
|
{
|
|
(void)mq;
|
|
if (status) memcpy(status, g_contStatus, sizeof(g_contStatus));
|
|
return 0;
|
|
}
|
|
|
|
/* EEPROM: file-backed 16Kbit save (Phase 4, BACKLOG B5). GE addresses the
|
|
* device in 8-byte blocks (src/game/file2.c: checksum @block 0, five
|
|
* save_data slots from block 4). Backed by $S/ge007.eep, loaded lazily on
|
|
* first access and rewritten on every write. Pattern mirrors the PD port
|
|
* (pd_port/port/src/libultra.c). */
|
|
#define GE_EEP_BLOCKS EEP16K_MAXBLOCKS /* 256 */
|
|
#define GE_EEP_SIZE (GE_EEP_BLOCKS * 8) /* 2048 bytes */
|
|
#define GE_EEP_PATH "$S/ge007.eep"
|
|
|
|
static u8 s_eeprom[GE_EEP_SIZE];
|
|
static int s_eepromLoaded = 0;
|
|
|
|
static void geEepromLoad(void)
|
|
{
|
|
if (s_eepromLoaded) return;
|
|
s_eepromLoaded = 1;
|
|
const char *path = sysResolvePath(GE_EEP_PATH);
|
|
FILE *fp = fopen(path, "rb");
|
|
if (fp) {
|
|
fread(s_eeprom, 1, GE_EEP_SIZE, fp);
|
|
fclose(fp);
|
|
sysLogPrintf(LOG_INFO, "eeprom: loaded %s", path);
|
|
} else {
|
|
memset(s_eeprom, 0, GE_EEP_SIZE);
|
|
sysLogPrintf(LOG_INFO, "eeprom: no %s yet (fresh save)", path);
|
|
}
|
|
}
|
|
|
|
static void geEepromStore(void)
|
|
{
|
|
const char *path = sysResolvePath(GE_EEP_PATH);
|
|
FILE *fp = fopen(path, "wb");
|
|
if (fp) {
|
|
fwrite(s_eeprom, 1, GE_EEP_SIZE, fp);
|
|
fclose(fp);
|
|
} else {
|
|
sysLogPrintf(LOG_ERROR, "eeprom: cannot write %s", path);
|
|
}
|
|
}
|
|
|
|
/* D257: Game.AllUnlocked -- mirror of src/game/file.h save_data (96 bytes;
|
|
* keep in sync). Two things are patched in at read time:
|
|
* - the progression-gated cheat-unlock bits (front.c
|
|
* frontCheckIfCheatIsUnlocked) test per-LEVEL bits in
|
|
* unlocked_cheats_1/2/3 that normal play sets when each level is
|
|
* completed, so a fresh save has an empty cheat menu;
|
|
* - every stage/difficulty completion time. When ANY single-player cheat
|
|
* is active, g_AppendCheatSinglePlayer makes the mission-select screen
|
|
* (front.c get_highest_unlocked_difficulty_for_level) only accept levels
|
|
* whose status is STAGESTATUS_COMPLETED (3), not merely UNLOCKED (1) --
|
|
* so AllUnlocked must read as a fully-completed campaign or enabling a
|
|
* cheat locks every level. Existing (nonzero) player times are kept;
|
|
* only empty slots get the max time (0x3FF). */
|
|
typedef struct ge_save_slot {
|
|
s32 chksum1;
|
|
s32 chksum2;
|
|
u8 completion_bitflags;
|
|
u8 flag_007;
|
|
u8 music_vol;
|
|
u8 sfx_vol;
|
|
u16 options;
|
|
u8 unlocked_cheats_1;
|
|
u8 unlocked_cheats_2;
|
|
u8 unlocked_cheats_3;
|
|
u8 padding;
|
|
u8 times[76]; /* (SP_LEVEL_MAX-1)*4: 19 levels x 4 difficulties */
|
|
} ge_save_slot; /* sizeof == 96 == save_data (file.h) -- enforced below */
|
|
|
|
/* If this ever fires, the mirror drifted from save_data and the AllUnlocked
|
|
* cheat patch silently no-ops (the block-read size check stops matching). */
|
|
typedef char ge_save_slot_size_check[(sizeof(ge_save_slot) == 96) ? 1 : -1];
|
|
|
|
/* Game-side CRC (src/game/crc.c) over [completion_bitflags, next slot),
|
|
* stored in the slot's own chksum1/2 -- exactly what fileValidateSaves
|
|
* re-checks after the block read. */
|
|
extern void fileGenerateCRC(u8 *addressA, u8 *addressB, void *retval);
|
|
|
|
extern s32 portAllUnlocked; /* port/src/video.c */
|
|
|
|
/* Bit math mirrors fileGetSaveStageDifficultyTime / fileSetDifficultyStageTime
|
|
* (src/game/file2.c): 10-bit time fields, offset = (difficulty*20+level)*10,
|
|
* difficulties 0..2 (agent/secret/00) only -- 007 times are virtual. */
|
|
static u32 geSaveGetTime(const u8 *t, s32 difficulty, s32 level)
|
|
{
|
|
s32 offset = (difficulty * 20 + level) * 10;
|
|
s32 index = offset >> 3;
|
|
switch (7 - (offset & 7)) {
|
|
case 7: return ((u32)(t[index] & 0xFF) << 2) | ((u32)(t[index + 1] & 0xc0) >> 6);
|
|
case 5: return ((u32)(t[index] & 0x3f) << 4) | ((u32)(t[index + 1] & 0xf0) >> 4);
|
|
case 3: return ((u32)(t[index] & 0x0f) << 6) | ((u32)(t[index + 1] & 0xfc) >> 2);
|
|
case 1: return ((u32)(t[index] & 0x03) << 8) | (u32)(t[index + 1] & 0xFF);
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
static void geSaveSetTime(u8 *t, s32 difficulty, s32 level, u32 newtime)
|
|
{
|
|
s32 offset = (difficulty * 20 + level) * 10;
|
|
s32 index = offset >> 3;
|
|
switch (7 - (offset & 7)) {
|
|
case 7:
|
|
t[index] = (u8)((t[index] & 0x00) | ((newtime >> 2) & 0xFF));
|
|
t[index + 1] = (u8)((t[index + 1] & 0x3F) | ((newtime << 6) & 0xC0));
|
|
break;
|
|
case 5:
|
|
t[index] = (u8)((t[index] & 0xC0) | ((newtime >> 4) & 0x3F));
|
|
t[index + 1] = (u8)((t[index + 1] & 0x0F) | ((newtime << 4) & 0xF0));
|
|
break;
|
|
case 3:
|
|
t[index] = (u8)((t[index] & 0xF0) | ((newtime >> 6) & 0x0F));
|
|
t[index + 1] = (u8)((t[index + 1] & 0x03) | ((newtime << 2) & 0xFC));
|
|
break;
|
|
case 1:
|
|
t[index] = (u8)((t[index] & 0xFC) | ((newtime >> 8) & 0x03));
|
|
t[index + 1] = (u8)(newtime & 0xFF);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void geEepromPatchAllCheats(u8 *buf)
|
|
{
|
|
/* buf holds the five save slots read from block 4. The sixth local slot
|
|
* is only a CRC end-boundary for the last one (fileGenerateCRC reads
|
|
* [A, B), so its contents are irrelevant). */
|
|
static ge_save_slot slots[6];
|
|
memcpy(slots, buf, sizeof(ge_save_slot) * 5);
|
|
memset(&slots[5], 0, sizeof(ge_save_slot));
|
|
|
|
int changed = 0;
|
|
for (int i = 0; i < 5; i++) {
|
|
/* Cheat ids are level ids 0..19 (CHEAT_INPUT_BUFFER_SIZE == 20):
|
|
* bits 0-7 in _1, 8-15 in _2, 16-19 in the low nibble of _3. */
|
|
if (slots[i].unlocked_cheats_1 != 0xFF ||
|
|
slots[i].unlocked_cheats_2 != 0xFF ||
|
|
(slots[i].unlocked_cheats_3 & 0x0F) != 0x0F) {
|
|
slots[i].unlocked_cheats_1 = 0xFF;
|
|
slots[i].unlocked_cheats_2 = 0xFF;
|
|
slots[i].unlocked_cheats_3 |= 0x0F;
|
|
changed = 1;
|
|
}
|
|
/* Fully-completed campaign: fill empty completion times with the
|
|
* max (0x3FF). Needed so mission select still works while any cheat
|
|
* is active (see header comment); player records are preserved. */
|
|
for (s32 diff = 0; diff < 3; diff++) {
|
|
for (s32 lvl = 0; lvl < 20; lvl++) {
|
|
if (geSaveGetTime(slots[i].times, diff, lvl) == 0) {
|
|
geSaveSetTime(slots[i].times, diff, lvl, 0x3FF);
|
|
changed = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!changed) return;
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
fileGenerateCRC(&slots[i].completion_bitflags, (u8 *)&slots[i + 1],
|
|
&slots[i]);
|
|
memcpy(buf, slots, sizeof(ge_save_slot) * 5);
|
|
}
|
|
|
|
static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write)
|
|
{
|
|
u32 off = (u32)block * 8;
|
|
if (!buf || nbytes < 0 || off + (u32)nbytes > GE_EEP_SIZE) return -1;
|
|
geEepromLoad();
|
|
if (write) {
|
|
memcpy(s_eeprom + off, buf, nbytes);
|
|
geEepromStore();
|
|
} else {
|
|
memcpy(buf, s_eeprom + off, nbytes);
|
|
/* fileValidateSaves' block read: five save slots from block 4. */
|
|
if (portAllUnlocked && block == 4 &&
|
|
nbytes == (int)(sizeof(ge_save_slot) * 5)) {
|
|
geEepromPatchAllCheats(buf);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
s32 osEepromProbe(OSMesgQueue *mq) { (void)mq; return EEPROM_TYPE_16K; }
|
|
s32 osEepromRead(OSMesgQueue *mq, u8 addr, u8 *buf)
|
|
{ (void)mq; return geEepromRW(addr, buf, 8, 0); }
|
|
s32 osEepromWrite(OSMesgQueue *mq, u8 addr, u8 *buf)
|
|
{ (void)mq; return geEepromRW(addr, buf, 8, 1); }
|
|
s32 osEepromLongRead(OSMesgQueue *mq, u8 addr, u8 *buf, int nbytes)
|
|
{ (void)mq; return geEepromRW(addr, buf, nbytes, 0); }
|
|
s32 osEepromLongWrite(OSMesgQueue *mq, u8 addr, u8 *buf, int nbytes)
|
|
{ (void)mq; return geEepromRW(addr, buf, nbytes, 1); }
|
|
|
|
/* Memory Pak (PFS) + Rumble Pak (motor): no accessories on the PC. */
|
|
s32 osPfsInit(OSMesgQueue *queue, OSPfs *pfs, int channel)
|
|
{ (void)queue; (void)pfs; (void)channel; return PFS_ERR_NOPACK; }
|
|
s32 osPfsIsPlug(OSMesgQueue *queue, u8 *pattern)
|
|
{ (void)queue; if (pattern) *pattern = 0; return 0; }
|
|
s32 osMotorInit(OSMesgQueue *mq, OSPfs *pfs, int channel)
|
|
{ (void)mq; (void)pfs; (void)channel; return -1; }
|
|
s32 osMotorStart(OSPfs *pfs) { (void)pfs; return -1; }
|
|
s32 osMotorStop(OSPfs *pfs) { (void)pfs; return -1; }
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* SP (RSP) — runs the software RSP inline, then posts the done messages */
|
|
/* that sched.c's __scMain waits for. */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
void osSpTaskLoad(OSTask *t) { (void)t; /* nothing to load on the host */ }
|
|
|
|
void osSpTaskStartGo(OSTask *t)
|
|
{
|
|
if (g_determTraceEnabled && !g_determTaskEverRun) {
|
|
sysLogPrintf(LOG_NOTE,
|
|
"DETERMTRACE seq=%u ticks=%u event=first_task_run tasktype=%d",
|
|
++g_determTraceSeq, g_determTicks, (int)t->t.type);
|
|
}
|
|
g_determTaskEverRun = 1; /* GE_DETERM: §D117 M-52 3rd attempt */
|
|
if (t->t.type == M_AUDTASK) {
|
|
/* Phase 3: execute the audio ucode against t->t.data_ptr (an Acmd
|
|
* list). For now the task simply completes; libaudio's amMain still
|
|
* runs its per-frame bookkeeping. */
|
|
} else {
|
|
/* Graphics task: run the software RSP on the display list. */
|
|
uint64_t t0 = sysGetMicroseconds();
|
|
videoStartFrame();
|
|
gfx_run((Gfx *)t->t.data_ptr);
|
|
videoEndFrame();
|
|
g_lastFrameUs = sysGetMicroseconds();
|
|
if (++g_framesRendered <= 5 || (g_framesRendered % 300) == 0)
|
|
sysLogPrintf(LOG_NOTE, "frame %d rendered in %llu us",
|
|
g_framesRendered,
|
|
(unsigned long long)(sysGetMicroseconds() - t0));
|
|
}
|
|
|
|
portPostEventForce(OS_EVENT_SP); /* D134: must not be dropped */
|
|
if (t->t.type != M_AUDTASK) {
|
|
/* A gfx task is also its own DP task (sp == dp in __scExec): the
|
|
* DP "finishes" right after the RSP. */
|
|
portPostEventForce(OS_EVENT_DP);
|
|
}
|
|
}
|
|
|
|
void osSpTaskYield(void) { /* no cooperative RSP on the host */ }
|
|
OSYieldResult osSpTaskYielded(OSTask *t) { (void)t; return 0; }
|
|
void osSpFlush(void) { /* no-op */ }
|
|
void osSpSetFifo(void *fifo, int size, int flags) { (void)fifo; (void)size; (void)flags; }
|
|
void *osSpGetFifo(void) { return NULL; }
|
|
int osSpTaskDone(void) { return 1; }
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* RDP — bypassed (the software RSP emits GL directly). */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
void osDpSetStatus(u32 status) { (void)status; }
|
|
u32 osDpGetStatus(void) { return 0; }
|
|
s32 osDpSetNextBuffer(void *buf, u64 size)
|
|
{
|
|
/* DP-only task: "rendering" is done by the time the RSP finished. */
|
|
(void)buf; (void)size;
|
|
portPostEventForce(OS_EVENT_DP); /* D134 */
|
|
return 1;
|
|
}
|
|
void osDpGetCounters(u32 *counters)
|
|
{
|
|
if (counters) memset(counters, 0, sizeof(u32) * 4);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* Cache — no-op on the host (no split I/D cache to manage). */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
void osWritebackDCache(void *addr, int size) { (void)addr; (void)size; }
|
|
void osWritebackDCacheAll(void) { }
|
|
void osInvalICache(void *addr, int size) { (void)addr; (void)size; }
|
|
void osInvalDCache(void *addr, int size) { (void)addr; (void)size; }
|
|
u32 osVirtualToPhysical(void *va) { return (u32)(uintptr_t)va; }
|
|
void *osPhysicalToVirtual(u32 pa) { return (void *)(uintptr_t)pa; }
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* Misc */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
void osInitialize(void)
|
|
{
|
|
/* The game calls this early. On the PC there is little to do; the port
|
|
* has already set up video/audio/input/rom in main(). */
|
|
}
|
|
|
|
void osExit(void) { sysExit(0); }
|
|
|
|
u32 osGetFpcCsr(void) { return 0; }
|
|
void osSetFpcCsr(u32 csr) { (void)csr; }
|
|
|
|
/*
|
|
* FPU CSR accessors used by init.c (the __os* variants are the raw libultra
|
|
* names; init() saves/restores the FPU control/status register around the
|
|
* decompress step). Unused on the PC but must link.
|
|
*/
|
|
u32 __osGetFpcCsr(void) { return 0; }
|
|
u32 __osSetFpcCsr(u32 csr) { (void)csr; return 0; }
|
|
|
|
/* TLB unmap (libultra/os/unmaptlb.s). The PC has its own MMU; no-op. */
|
|
void osUnmapTLB(int index) { (void)index; }
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* Timers / interrupts */
|
|
/* ------------------------------------------------------------------------
|
|
*
|
|
* Software timers. bossInitMainthreadData() paces controller init with a
|
|
* one-shot 100 ms osSetTimer()/osRecvMesg() pair, so timers must actually
|
|
* fire. They are serviced by the kernel tick thread: portServiceTimers()
|
|
* posts due messages and portNextTimerUs() lets the tick thread shorten its
|
|
* sleep to the earliest deadline.
|
|
*/
|
|
|
|
#define PORT_MAX_TIMERS 8
|
|
|
|
typedef struct PortTimer {
|
|
OSTimer *os;
|
|
uint64_t fireUs; /* absolute deadline (sysGetMicroseconds domain) */
|
|
uint32_t periodUs; /* 0 = one-shot */
|
|
int active;
|
|
} PortTimer;
|
|
|
|
static PortTimer g_ptimers[PORT_MAX_TIMERS];
|
|
|
|
/* cycles -> microseconds at the RSP counter rate. Inverse of
|
|
* OS_USEC_TO_CYCLES(). */
|
|
static uint64_t portCyclesToUs(OSTime cycles)
|
|
{
|
|
return (uint64_t)((double)cycles * 1000000.0 / (double)osClockRate);
|
|
}
|
|
|
|
int osSetTimer(OSTimer *timer, OSTime start, OSTime period,
|
|
OSMesgQueue *mq, OSMesg msg)
|
|
{
|
|
PortTimer *pt = NULL;
|
|
int i;
|
|
|
|
timer->interval = period;
|
|
timer->value = start;
|
|
timer->mq = mq;
|
|
timer->msg = msg;
|
|
|
|
for (i = 0; i < PORT_MAX_TIMERS; ++i) {
|
|
if (g_ptimers[i].os == timer) { pt = &g_ptimers[i]; break; }
|
|
if (!g_ptimers[i].active && !pt) pt = &g_ptimers[i];
|
|
}
|
|
if (!pt) return -1;
|
|
|
|
pt->os = timer;
|
|
pt->active = 1;
|
|
pt->periodUs = (uint32_t)portCyclesToUs(period);
|
|
pt->fireUs = sysGetMicroseconds() + portCyclesToUs(start);
|
|
sysLogPrintf(LOG_NOTE, "timer set: t=%p start=%lluus period=%uus mq=%p",
|
|
(void *)timer, (unsigned long long)portCyclesToUs(start),
|
|
pt->periodUs, (void *)mq);
|
|
return 0;
|
|
}
|
|
|
|
int osStopTimer(OSTimer *timer)
|
|
{
|
|
int i;
|
|
for (i = 0; i < PORT_MAX_TIMERS; ++i) {
|
|
if (g_ptimers[i].os == timer) {
|
|
g_ptimers[i].active = 0;
|
|
return 0;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void portServiceTimers(void)
|
|
{
|
|
uint64_t now = sysGetMicroseconds();
|
|
int i;
|
|
for (i = 0; i < PORT_MAX_TIMERS; ++i) {
|
|
PortTimer *pt = &g_ptimers[i];
|
|
if (!pt->active) continue;
|
|
while (pt->fireUs <= now) {
|
|
sysLogPrintf(LOG_NOTE, "timer fire: t=%p mq=%p",
|
|
(void *)pt->os, (void *)pt->os->mq);
|
|
osSendMesg(pt->os->mq, pt->os->msg, OS_MESG_NOBLOCK);
|
|
if (!pt->periodUs) { pt->active = 0; break; }
|
|
pt->fireUs += pt->periodUs;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Earliest timer deadline, or UINT64_MAX. */
|
|
static uint64_t portNextTimerUs(void)
|
|
{
|
|
uint64_t earliest = ~0ull;
|
|
int i;
|
|
for (i = 0; i < PORT_MAX_TIMERS; ++i) {
|
|
PortTimer *pt = &g_ptimers[i];
|
|
if (pt->active && pt->fireUs < earliest) earliest = pt->fireUs;
|
|
}
|
|
return earliest;
|
|
}
|
|
|
|
/* Interrupt mask -> global recursive lock (D147), self-healing (D152).
|
|
*
|
|
* On N64 `osSetIntMask(OS_IM_NONE)` disables interrupts so the following
|
|
* region cannot be preempted; the paired `osSetIntMask(saved)` restores it.
|
|
* libaudio relies on this to serialise the event queue (alEvtqPostEvent,
|
|
* sndRemoveEvents, alEvtqNextEvent, ...) between the game thread and the
|
|
* audio-manager "interrupt". On PC that audio manager is a real preemptible
|
|
* thread (amMain), and a no-op shim let both threads mutate the same
|
|
* ALEventQueue linked list concurrently -> list corruption -> the game
|
|
* thread spins forever in alEvtqPostEvent's insert walk (hang seen when a
|
|
* door finishes opening and posts its close SFX, propobj.c objTick).
|
|
*
|
|
* Model the mask as one process-wide recursive critical section: OS_IM_NONE
|
|
* acquires, OS_IM_ALL (the value we hand back, so every paired restore
|
|
* passes it) releases. Other specific masks (OS_IM_VI in sched.c) are not
|
|
* lock ops and pass through.
|
|
*
|
|
* D152: the recursive-mutex form CAN wedge permanently. libaudio has
|
|
* unbalanced / early-return mask calls, and a transient host thread can
|
|
* acquire OS_IM_NONE and exit without the paired OS_IM_ALL, leaving the
|
|
* lock owned forever -> every later alEvtq* on mainThread + amMain blocks
|
|
* -> black screen (repro: mission-failed audio fade-out,
|
|
* sndSetScalerApplyVolumeAllSfxSlot posts a release event per active sound
|
|
* per frame). Real audio critical sections are microseconds; anything
|
|
* holding for OS_IM_STUCK_NS is leaked, so a waiter that has blocked that
|
|
* long STEALS the section (logging the stale owner + the caller so the leak
|
|
* site can be found and fixed narrowly later). Bookkeeping lives under
|
|
* s_imMx (only ever held briefly); s_imHeld/s_imOwner/s_imDepth are the
|
|
* logical lock. */
|
|
#define OS_IM_STUCK_NS (2000ll * 1000 * 1000) /* 2 s */
|
|
|
|
static pthread_mutex_t s_imMx = PTHREAD_MUTEX_INITIALIZER;
|
|
static pthread_cond_t s_imCv = PTHREAD_COND_INITIALIZER;
|
|
static int s_imHeld;
|
|
static unsigned long s_imOwner;
|
|
static int s_imDepth;
|
|
|
|
static unsigned long imSelf(void)
|
|
{
|
|
return (unsigned long)(uintptr_t)pthread_self();
|
|
}
|
|
|
|
static void imAcquire(void *caller)
|
|
{
|
|
unsigned long self = imSelf();
|
|
struct timespec now;
|
|
|
|
pthread_mutex_lock(&s_imMx);
|
|
|
|
if (s_imHeld && s_imOwner == self) { /* recursive re-entry */
|
|
s_imDepth++;
|
|
pthread_mutex_unlock(&s_imMx);
|
|
return;
|
|
}
|
|
|
|
long long startNs = 0; /* set on first contended iteration */
|
|
|
|
while (s_imHeld) {
|
|
struct timespec dl;
|
|
if (startNs == 0) {
|
|
clock_gettime(CLOCK_REALTIME, &now);
|
|
startNs = (long long)now.tv_sec * 1000000000ll + now.tv_nsec;
|
|
}
|
|
clock_gettime(CLOCK_REALTIME, &now);
|
|
long long nowNs = (long long)now.tv_sec * 1000000000ll + now.tv_nsec;
|
|
if (nowNs - startNs >= OS_IM_STUCK_NS) {
|
|
sysLogPrintf(LOG_ERROR,
|
|
"D152: osSetIntMask lock stuck >2s -- stealing from owner=%lu "
|
|
"depth=%d (this caller=%p). A prior OS_IM_NONE was never "
|
|
"restored; find that call site.",
|
|
s_imOwner, s_imDepth, caller);
|
|
break; /* fall through, steal it */
|
|
}
|
|
/* wake ~10x/s to re-check the steal deadline */
|
|
long long wakeNs = nowNs + 100ll * 1000 * 1000;
|
|
dl.tv_sec = (time_t)(wakeNs / 1000000000ll);
|
|
dl.tv_nsec = (long)(wakeNs % 1000000000ll);
|
|
pthread_cond_timedwait(&s_imCv, &s_imMx, &dl);
|
|
}
|
|
|
|
s_imHeld = 1;
|
|
s_imOwner = self;
|
|
s_imDepth = 1;
|
|
pthread_mutex_unlock(&s_imMx);
|
|
}
|
|
|
|
static void imRelease(void)
|
|
{
|
|
unsigned long self = imSelf();
|
|
|
|
pthread_mutex_lock(&s_imMx);
|
|
if (!s_imHeld || s_imOwner != self) { /* spurious / already stolen: no-op */
|
|
pthread_mutex_unlock(&s_imMx);
|
|
return;
|
|
}
|
|
if (--s_imDepth <= 0) {
|
|
s_imHeld = 0;
|
|
s_imOwner = 0;
|
|
s_imDepth = 0;
|
|
pthread_cond_signal(&s_imCv);
|
|
}
|
|
pthread_mutex_unlock(&s_imMx);
|
|
}
|
|
|
|
/* D152: a host thread that returns from its entry function while still
|
|
* "inside" an OS_IM_NONE section (an unbalanced acquire on a code path only
|
|
* that transient thread takes, or an osStopThread that let it bail early)
|
|
* would leave s_imHeld owned forever -> mainThread + amMain both wedge in
|
|
* alEvtq* until the 2 s steal fires, and can re-wedge every cycle once a new
|
|
* host thread reuses the dead thread's pthread id (imAcquire then
|
|
* mis-detects recursion). The steal-lock recovers from it but with a visible
|
|
* hitch; releasing the orphaned section the instant its owner dies removes
|
|
* the hitch entirely. Called from portThreadWrapper on thread exit. */
|
|
void imThreadExitRelease(void)
|
|
{
|
|
unsigned long self = imSelf();
|
|
pthread_mutex_lock(&s_imMx);
|
|
if (s_imHeld && s_imOwner == self) {
|
|
sysLogPrintf(LOG_ERROR,
|
|
"D152: thread %lu exited still holding osSetIntMask section "
|
|
"(depth=%d) -- releasing the orphan. An OS_IM_NONE on this "
|
|
"thread's path was never restored.", self, s_imDepth);
|
|
s_imHeld = 0;
|
|
s_imOwner = 0;
|
|
s_imDepth = 0;
|
|
pthread_cond_signal(&s_imCv);
|
|
}
|
|
pthread_mutex_unlock(&s_imMx);
|
|
}
|
|
|
|
OSIntMask osSetIntMask(OSIntMask mask)
|
|
{
|
|
if (mask == OS_IM_NONE) { /* enter critical section */
|
|
imAcquire(__builtin_return_address(0));
|
|
return OS_IM_ALL; /* paired restore will pass this back */
|
|
}
|
|
if (mask == OS_IM_ALL) { /* leave critical section */
|
|
imRelease();
|
|
return OS_IM_NONE;
|
|
}
|
|
return mask; /* OS_IM_VI etc: not a lock operation */
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* PI / VI managers referenced by init.c (pi.c / vi.c are not compiled) */
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
void piCreateManager(OSMesgQueue *mq, int prio)
|
|
{
|
|
/* ROM access is handled by romdata.c; no PI manager needed on the PC. */
|
|
(void)mq; (void)prio;
|
|
}
|
|
void viDebugRemoved(void)
|
|
{
|
|
/* N64 debug VI hook; no-op on the PC. */
|
|
}
|
|
void viInit(void)
|
|
{
|
|
/* VI init (normally src/vi.c, EXCLUDED). The port video layer is
|
|
* already up by the time the game calls this. */
|
|
}
|
|
|
|
/* VI debug message queue (normally src/vi.c, EXCLUDED). fr.c references it. */
|
|
OSMesgQueue vi_c_debug_MQ;
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
/* D202/M-70: serialized trace writer for the GE_AUDIOTRACE / WIRE probes. */
|
|
/* See port/include/audiotrace.h for why (cross-thread mid-line */
|
|
/* interleaving corrupted the M-69 corpus). */
|
|
/* ------------------------------------------------------------------------ */
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
|
|
static volatile int geTraceLock = 0;
|
|
|
|
struct geTraceFile { const char *path; FILE *f; };
|
|
static struct geTraceFile geTraceFiles[4];
|
|
|
|
void geTracePrintf(const char *path, const char *fmt, ...)
|
|
{
|
|
struct geTraceFile *tf = NULL;
|
|
va_list ap;
|
|
|
|
while (__sync_lock_test_and_set(&geTraceLock, 1)) { /* spin */ }
|
|
|
|
for (int i = 0; i < (int)(sizeof(geTraceFiles) / sizeof(geTraceFiles[0])); i++) {
|
|
if (!geTraceFiles[i].path) {
|
|
if (!tf) tf = &geTraceFiles[i];
|
|
continue;
|
|
}
|
|
if (strcmp(geTraceFiles[i].path, path) == 0) { tf = &geTraceFiles[i]; break; }
|
|
}
|
|
if (tf && !tf->f) {
|
|
tf->path = path;
|
|
tf->f = fopen(path, "a");
|
|
if (tf->f) setvbuf(tf->f, NULL, _IONBF, 0);
|
|
}
|
|
|
|
if (tf && tf->f) {
|
|
va_start(ap, fmt);
|
|
vfprintf(tf->f, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
__sync_lock_release(&geTraceLock);
|
|
}
|