Files
goldeneye-pc-port/src/str.c
T
James Dansereau 57e24a374b M-28: watch-menu text (D150/D151) + audio-lock deadlock mitigation (D152)
D150 (src/str.c) — NULL-tolerant strcpy/strncpy/strcat under #ifdef PORT.
The watch BRIEF/OBJECTIVES pages build text with strcat(buf, langGet(id));
langGet() returns NULL on PC for a string bank the in-level flow never
loaded, and str.c's primitives are __nonnull__ builtins so a plain param
NULL check is optimised away — the guard launders the ptr through an empty
asm (GE_IS_NULL). NULL src -> empty string instead of an AV crash.

D151 (src/bondtypes.h) — watch data (objectives / mission background / M /
Q / Moneypenny) all rendered blank. propDef types 35 (WatchMenuObjectiveText)
and 23 (ObjectiveStart) store a plain s32 lang slot id in their 3rd word;
the decomp structs decode it as [u16 reserved][u16 text] reading text at
offset 0xA, which only works because the id is in the low 16 bits of a
big-endian word. d88_propdefs.py bswap32's the whole word -> text @0xA
reads 0 -> langGet(0) -> NULL. Fix: text is a full u32 at the word offset
under #ifdef PORT (N64 layout kept under #else). No converter change, no
sidecar regen. This is the D143/D150 "briefing text renders blank" root cause.

D152 (port/src/libultra.c) — mission-failed / objective-failed permanent
black screen. The D147 recursive-mutex behind osSetIntMask can be leaked
(libaudio has unbalanced / early-return mask calls; a transient thread can
acquire OS_IM_NONE and exit) -> mainThread + amMain both block forever in
alEvtq* -> gfx pipeline stalls. Live gdb.txt from the user's session
confirmed the deadlock (mainThread in the mission-fail audio fade-out
sndSetScalerApplyVolumeAllSfxSlot -> alEvtqPostEvent). Mitigation: model
the mask as a self-healing logical lock (owner/depth under s_imMx + condvar)
— normal microsecond critical sections behave as before, but a waiter
blocked > 2s concludes the holder leaked it, logs the stale owner + the
stealing caller's return address, and steals the section. Worst case is a
~2s audio hiccup + a log line naming the leak, instead of a hung game.
Proper narrow fix (dedicated ALEventQueue lock) still owed.

D153 investigated and dismissed — the "-level_09 crashes at frame ~900"
seen while regression-testing was D117/D134 host-scheduling flakiness under
heavy concurrent build+run load; on a quiet machine -level_09 runs past
frame 2400 clean with all three changes applied.

Build: green (ntsc-final). Golden framediff not run clean (same machine-load
flakiness); one quiet -level_09 run = 2400+ frames, 0 heartbeats.
2026-08-30 20:25:02 -04:00

203 lines
5.1 KiB
C

#include "str.h"
#ifdef PORT
/* D150: strcpy/strncpy/strcat are __nonnull__ builtins to GCC, so a plain
* `if (src == NULL)` on the parameter is optimised away as provably-dead.
* Launder the pointer through an empty asm so the compiler cannot prove it
* non-NULL and the guard survives (-Og elided it otherwise — verified in
* the disassembly). */
static inline const void *ge_launder_ptr(const void *p) {
__asm__("" : "+r"(p));
return p;
}
#define GE_IS_NULL(p) (ge_launder_ptr(p) == NULL)
#endif
char *strcpy(char *dst, const char *src) {
unsigned char *ptr = dst;
#ifdef PORT
/* D150: the watch briefing/objective pages assemble their text with
* strcpy/strcat(buf, langGet(id)); langGet() returns NULL for a string
* bank the PC menu flow never loaded (D129/D143), so a NULL src here
* faults. Treat NULL as the empty string (blank text, not a crash) —
* same philosophy as the D143 textRender/textMeasure NULL guards.
* N64 langGet never returns NULL for these ids, so this is inert there. */
if (GE_IS_NULL(src)) { if (!GE_IS_NULL(dst)) *ptr = '\0'; return dst; }
#endif
while(*ptr++ = *src++);
return dst;
}
char *strncpy(char *dst, const char *src, size_t n) {
unsigned char *ptr = dst;
#ifdef PORT
if (GE_IS_NULL(src)) { while (n--) *ptr++ = '\0'; return dst; } /* D150 */
#endif
while((*ptr++ = *src++)) {
if (--n == 0) {
break;
}
}
while(n--) {
(*ptr++) = '\0';
}
return dst;
}
char *strcat(char *dst, const char *src) {
unsigned char *ptr = dst;
#ifdef PORT
if (GE_IS_NULL(dst)) { return dst; } /* D150 */
if (GE_IS_NULL(src)) { return dst; } /* D150 — see strcpy note */
#endif
while (*ptr) { ptr++; };
while(*ptr++ = *src++);
return dst;
}
int strcmp(const char* str1, const char* str2) {
unsigned int var;
unsigned char c1;
unsigned char c2;
while (TRUE) {
var = c1 = *(str1++);
if (var != (c2 = *str2)) {
if (c1 < c2) {
return -1;
} else {
return 1;
}
}
if (c1 == '\0') {
return 0;
}
str2++;
}
}
int strncmp(const char *str1, const char *str2, size_t n) {
unsigned int var;
unsigned char c1;
unsigned char c2;
while (TRUE) {
if (n == 0) {
return 0;
}
n--;
var = c1 = *str1++;
if (var != (c2 = *str2)) {
if (c1 < c2) {
return -1;
} else {
return 1;
}
}
if (c1 == '\0') {
return 0;
}
str2++;
}
}
unsigned char toupper(unsigned char c) {
if ((c >= 'a') && (c <= 'z')) {
return ('A' + c - 'a');
} else {
return c;
}
}
int isdigit(unsigned char c) {
return ((c >= '0') && (c <= '9'));
}
int isalpha(unsigned char c) {
return (((c >= 'a') && (c <= 'z')) ||
((c >= 'A') && (c <= 'Z')));
}
int isspace(unsigned char c) {
return ((c == ' ') || (c == '\t') || (c == '\n') || (c == '\f') || (c == '\v'));
}
#define ULONG_MAX ((unsigned long)(~0L)) /* 0xFFFFFFFF */
long int strtol(const char *str, char **endptr, int base) {
int neg;
unsigned char *ptr;
unsigned int cutoff;
unsigned int cutlim;
unsigned int accum;
unsigned char c;
unsigned char *before;
int overflow;
if ((base < 0) || (base == 1) || (base > 36)) {
base = 10;
}
ptr = str;
while (isspace(*ptr)) { ptr++; };
if ((int)*ptr) {
if (*ptr == '-') {
neg = 1;
ptr++;
} else if (*ptr == '+') {
neg = 0;
ptr++;
} else {
neg = 0;
}
if (base == 16) {
if ((ptr[0] == '0') && (toupper(ptr[1]) == 'X')) {
ptr += 2;
}
}
if (base == 0) {
if (ptr[0] == '0') {
if (toupper(ptr[1]) == 'X') {
ptr += 2;
base = 16;
} else {
base = 8;
}
} else {
base = 10;
}
}
before = ptr;
overflow = 0;
accum = 0;
cutoff = ULONG_MAX / base;
cutlim = ULONG_MAX % base;
for (; (int)(c = *ptr); ptr++) {
if (isdigit(c)) {
c -= '0';
} else if (isalpha(c)) {
c = (toupper(c) - ('A' - 0xA));
} else {
break;
}
if (c >= base) {
break;
}
if ((accum > cutoff) || ((accum == cutoff) && ((unsigned int)c > cutlim))) {
overflow = 1;
} else {
accum *= base;
accum += (unsigned int)c;
}
}
if (ptr != before) {
if (endptr != NULL) {
*endptr = ptr;
}
if (overflow) {
return -1;
}
return (neg ? -accum : accum);
}
}
if (endptr != NULL) {
*endptr = str;
}
return 0;
}