D489-D502: settings persistence, collision, A/B harnesses, D441 census

F10 pad slider persistence (D489), glass collision hull (D490), A/B input
harnesses (1964 virtual pad, GE_INPUTSCRIPT, live one-pad launcher), adjacent
global struct reads (D491), vibration percent, D441 complete pointer-width
census with high-arena tier-1 build, fix batches 1-5 and an ABI warning
ratchet, playtest verification of D493-D502.

Refs: D489 D490 D491 D492-D502 D441
This commit is contained in:
James Dansereau committed 2026-10-06 18:35:42 -04:00
1 parent 4b38c2566e
commit daa2522511
63 files changed
+2693 -1419

No files matched your search

+9 -4
View File
@@ -744,7 +744,8 @@ static void gfx_opengl_upload_texture(const uint8_t* rgba32_buf, uint32_t width,
}
#endif
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba32_buf);
if (gen_mipmaps || current_filter_mode == FILTER_THREE_POINT) {
/* Trilinear option (playtest 2026-10-03): force mips for TRILINEAR too. */
if (gen_mipmaps || current_filter_mode == FILTER_THREE_POINT || current_filter_mode == FILTER_TRILINEAR) {
glGenerateMipmap(GL_TEXTURE_2D);
}
}
@@ -764,24 +765,28 @@ static uint32_t gfx_cm_to_opengl(uint32_t val) {
}
static void gfx_opengl_set_sampler_parameters(int tile, bool linear_filter, uint32_t cms, uint32_t cmt, bool mipmaps) {
const GLint min_filters[3][3] = {
const GLint min_filters[4][3] = {
// MIPMAP_DISABLED MIPMAP_NEAREST MIPMAP_LINEAR
{ GL_NEAREST, GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR }, // FILTER_NONE
{ GL_LINEAR, GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR }, // FILTER_BILINEAR
{ GL_NEAREST, GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR }, // FILTER_THREE_POINT
{ GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR }, // FILTER_TRILINEAR (Trilinear option, playtest 2026-10-03)
};
// B1: the shader does 3-point on magnification; for minification let the
// hardware do trilinear mips. THREE_POINT force-generates mips at upload
// (see gfx_opengl_upload_texture) so it can always sample them here -- this
// is what kills the grazing-angle shimmer (Depot roof, docs/BRIEF-B2).
if (current_filter_mode == FILTER_THREE_POINT) {
/* Trilinear option (playtest 2026-10-03): TRILINEAR force-generates mips
* at upload too, so it can always sample them here. */
if (current_filter_mode == FILTER_THREE_POINT || current_filter_mode == FILTER_TRILINEAR) {
mipmaps = true;
}
mipmaps = mipmaps && (current_mipmap_filter_mode != MIPMAP_DISABLED);
const int mip_idx = mipmaps ? current_mipmap_filter_mode : 0;
const GLint min_filter = linear_filter ? min_filters[current_filter_mode][mip_idx] : GL_NEAREST;
const GLint max_filter = linear_filter && (current_filter_mode == FILTER_LINEAR) ? GL_LINEAR : GL_NEAREST;
/* Trilinear option (playtest 2026-10-03): TRILINEAR magnifies LINEAR too. */
const GLint max_filter = linear_filter && (current_filter_mode == FILTER_LINEAR || current_filter_mode == FILTER_TRILINEAR) ? GL_LINEAR : GL_NEAREST;
glActiveTexture(GL_TEXTURE0 + tile);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
+24 -40
View File
@@ -810,7 +810,11 @@ void gfx_texture_cache_delete_range(const uint8_t* start, const uint8_t* end) {
static bool gfx_tex_source_is_c_array(const uint8_t* addr) {
const uintptr_t a = (uintptr_t)addr;
if (a >= 0x10000000u && a < 0x20000000u) return false; // cart map + sidecar
if (a >= PORT_DRAM_V1_BASE && a < 0x90000000u) return false; // V1 dram + KSEG0 mirror (portaddr.h)
// V1 dram + KSEG0 mirror: the two mapped views (port/src/dram.c). D441:
// was `a >= V1 && a < 0x90000000`, which went empty with the arena at
// 0x90000000 and bswapped every DRAM texture (mirrored texel pairs).
if (a >= PORT_DRAM_V1_BASE && a < PORT_DRAM_V1_BASE + PORT_DRAM_SIZE) return false;
if (a >= PORT_DRAM_K0_BASE && a < PORT_DRAM_K0_BASE + PORT_DRAM_SIZE) return false;
return true; // exe image: C-compiled array
}
@@ -830,13 +834,6 @@ static const uint8_t* gfx_tex_normalize_source(const uint8_t* addr, uint32_t ext
uint32_t* dst = (uint32_t*)buf.data();
for (uint32_t i = 0; i < n / 4; i++)
dst[i] = PD_BE32(src[i]);
{
static int ge_d71log = -1;
if (ge_d71log < 0) ge_d71log = getenv("GE_D71LOG") != NULL;
if (ge_d71log)
fprintf(stderr, "[D71] normalized C-array texture source %p (%u bytes)\n",
(const void*)addr, extent);
}
return s_c_array_tex_norms.emplace(addr, std::move(buf)).first->second.data();
}
@@ -2800,23 +2797,6 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
buf_vbo[buf_vbo_len++] = u / tex_width[t];
buf_vbo[buf_vbo_len++] = v / tex_height[t];
#ifdef PORT
{
static int ge_d116 = -1;
if (ge_d116 < 0) ge_d116 = getenv("GE_D116") ? 1 : 0;
if (ge_d116 && is_rect && t == 0 && tex_width[t] > 0 && tex_width[t] <= 32) {
fprintf(stderr,
"[D116/vbo] rect vtx%d x=%.4f y=%.4f u=%.4f v=%.4f "
"(raw v->u=%.1f texw=%.0f texw2=%u tile.uls=%d lrs=%d cms=%d)\n",
i, v_arr[i]->x, v_arr[i]->y, u / tex_width[t], v / tex_height[t],
(double)v_arr[i]->u, (double)tex_width[t], (unsigned)tex_width2[i],
(int)rdp.texture_tile[rdp.first_tile_index + tile].uls,
(int)rdp.texture_tile[rdp.first_tile_index + tile].lrs,
(int)rdp.texture_tile[rdp.first_tile_index + tile].cms);
}
}
#endif
bool clampS = tm & (1 << 2 * t);
bool clampT = tm & (1 << (2 * t + 1));
@@ -3157,6 +3137,25 @@ static void gfx_dp_set_scissor(uint32_t mode, uint32_t ulx, uint32_t uly, uint32
float width = (lrx - ulx) / 4.0f;
float height = (lry - uly) / 4.0f;
// D493b: with the overscan crop off (the Original N64 preset turns it
// off) D246's 1-unit trim is off too, and its gap at the left/right
// edges showed live scene/background pixels. Keep the geometry unscaled
// and scissor those two logical columns out instead, so they show the
// frame clear (black) like the rest of the overscan border.
if (!g_safe_area_crop_enabled && !g_split_screen) {
const float edge = 1.0f;
if (x < edge) {
width -= edge - x;
x = edge;
}
if (x + width > (float)SCREEN_WIDTH - edge) {
width = (float)SCREEN_WIDTH - edge - x;
}
if (width < 0.0f) {
width = 0.0f;
}
}
rdp.scissor.x = x;
rdp.scissor.y = y;
rdp.scissor.width = width;
@@ -3606,21 +3605,6 @@ static void gfx_dp_texture_rectangle(int32_t ulx, int32_t uly, int32_t lrx, int3
ur->v = lrt;
}
{
static int ge_d116 = -1;
if (ge_d116 < 0) ge_d116 = getenv("GE_D116") ? 1 : 0;
if (ge_d116) {
const auto& tt = rdp.texture_tile[tile];
fprintf(stderr,
"[D116/f3d] tile=%d flip=%d ul(%d,%d) lr(%d,%d) uls=%d ult=%d dsdx=%d dtdy=%d "
"-> lrs=%.2f lrt=%.2f ul.u=%.2f lr.u=%.2f | TILE siz=%d fmt=%d line_bytes=%d "
"uls=%d lrs=%d width=%d height=%d cms=%d\n",
tile, (int)flip, ulx, uly, lrx, lry, (int)uls, (int)ult, (int)dsdx, (int)dtdy,
lrs, lrt, ul->u, lr->u,
tt.siz, tt.fmt, tt.line_size_bytes, tt.uls, tt.lrs, tt.width, tt.height, tt.cms);
}
}
uint8_t saved_tile = rdp.first_tile_index;
if (saved_tile != tile) {
rdp.textures_changed[0] = true;
+2 -1
View File
@@ -12,7 +12,8 @@ struct GfxClipParameters {
bool invert_y;
};
enum FilteringMode { FILTER_NONE, FILTER_LINEAR, FILTER_THREE_POINT };
/* Trilinear option (playtest 2026-10-03) */
enum FilteringMode { FILTER_NONE, FILTER_LINEAR, FILTER_THREE_POINT, FILTER_TRILINEAR };
enum MipmapFilteringMode { MIPMAP_DISABLED, MIPMAP_NEAREST, MIPMAP_LINEAR };
struct GfxRenderingAPI {
+2
View File
@@ -21,6 +21,8 @@ extern "C" {
/* Initialize SDL input. Returns 0 on success. */
int inputInit(void);
void inputDestroy(void);
/* D493: stop all pad rumble and block new rumble (clean exit). */
void inputRumbleStopAll(void);
/* Poll once per frame; updates the connected-controller state that
* osContStartReadData / osContRead will return. */
+1
View File
@@ -86,6 +86,7 @@ void optionsRowSetFraction(int i, double f);
void optionsRowCommit(int i); /* slider release; watch rows persist once */
void optionsRowValueText(int i, char *out, int n);
void optionsRowAdjust(int i, int dir);
void optionsAdjustCommitPending(void); /* D489: save a stepped watch slider on release */
/* D356: per-section "Reset to defaults" rows (ROW_ACTION, keys
* "__Reset*"). Activation is edge-triggered and two-step (arm -> confirm
+9
View File
@@ -7,8 +7,17 @@
* address uses these instead of a literal, so moving the arena later (the
* high-arena test flavour, macOS/ARM port_addr) is a change in one header.
* Compile-time constants: no codegen change from the old literals. */
/* Each define is #ifndef-guarded so a -D on the command line can override it
* (the D441 high-arena test flavour: GE_HIGHARENA_BASE=0x90000000 adds
* -DPORT_DRAM_V1_BASE=0x90000000 for C, CXX and ASM). Values unchanged. */
#ifndef PORT_DRAM_V1_BASE
#define PORT_DRAM_V1_BASE 0x70000000UL /* s32-safe "virtual" view (game RAM symbols) */
#endif
#ifndef PORT_DRAM_K0_BASE
#define PORT_DRAM_K0_BASE 0x80000000UL /* KSEG0 mirror of V1 (same pages) */
#endif
#ifndef PORT_DRAM_SIZE
#define PORT_DRAM_SIZE 0x00800000UL /* 8 MB, like an 8-MB-RAM N64 */
#endif
#endif /* PORT_PORTADDR_H */
+11 -6
View File
@@ -1,7 +1,10 @@
/*
* dram_syms.s — absolute symbols inside the "virtual" (s32-safe) DRAM view.
* See port/src/dram.c: one 8 MB backing store is mapped at BOTH 0x70000000
* (V1, where these live) and 0x80000000 (V2, the KSEG0 mirror). These are
* dram_syms.S — absolute symbols inside the "virtual" (s32-safe) DRAM view.
* Preprocessed by gcc (.S): the base comes from port/include/portaddr.h, so
* the high-arena test flavour (-DPORT_DRAM_V1_BASE=..., GE_HIGHARENA_BASE)
* moves these symbols along with dram.c's mapping.
* See port/src/dram.c: one 8 MB backing store is mapped at BOTH PORT_DRAM_V1_BASE
* (V1, where these live) and PORT_DRAM_K0_BASE (V2, the KSEG0 mirror). These are
* committed host addresses, so they are live pointers on the PC.
*
* cfb_16 replaces src/cfb.c (excluded from the build): the two
@@ -9,13 +12,15 @@
* _bssSegmentEnd end-of-.bss marker; boss.c:217 starts the mempools at
* PHYS_TO_K0(osVirtualToPhysical(&_bssSegmentEnd)).
*/
#include "portaddr.h"
.section .data
.global cfb_16
.set cfb_16, 0x70000000
.set cfb_16, PORT_DRAM_V1_BASE + 0x0
.global _bssSegmentEnd
.set _bssSegmentEnd, 0x70050000
.set _bssSegmentEnd, PORT_DRAM_V1_BASE + 0x50000
/* D59: animation-frame scratch buffer (src/game/initanitable.c). The game
* addresses it through s32 fields (Model.unk34/38/64/68) and loadAnimationFrame
@@ -23,4 +28,4 @@
* the BSS copy at 0x140xxxxxx truncates to wild 0x40xxxxxx. Parked at the top
* of DRAM V1 (above the mempool sentinel at +0x2F4400). */
.global animations_frame_buffer
.set animations_frame_buffer, 0x707FFD30
.set animations_frame_buffer, PORT_DRAM_V1_BASE + 0x7FFD30
+56 -51
View File
@@ -52,6 +52,8 @@
extern Gfx *frontSetupMenuBackground(Gfx *DL);
extern Gfx *frontAddPreviousTabText(Gfx *DL);
extern s32 frontCheckCursorOnPreviousTab(void);
extern Gfx *frontAddNextTabText(Gfx *DL);
extern u32 frontCheckCursorOnNextTab(void);
extern Gfx *frontDrawCursor(Gfx *DL);
extern Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, struct fontchar *second_font_table,
struct font *first_font_table, s32 arg6, s32 view_x, s32 view_y,
@@ -83,7 +85,7 @@ extern struct rectbbox folder_option_ERASE_bound; /* front.c:439 */
#define VAL_R 372 /* values right-aligned here */
#define BAR_X0 232
#define BAR_X1 316
#define ROW_HIT_X0 40.0f
#define ROW_HIT_X0 24.0f /* D502: covers the pad crosshair snap at x = 32 */
#define ROW_HIT_X1 385.0f
/* ---- file-select label: right end of the Select / Copy / Erase bar ---- */
@@ -245,12 +247,12 @@ static void cursorToItemRaw(int k)
{
/* D345(g): snap into the left gutter (x=44), not onto the row text
* (the old ROW_X+20 centred the sprite over the label/highlight box).
* 44 stays inside [ROW_HIT_X0..1] so the highlight hit-test still
* 32 (D502; was 44) stays inside [ROW_HIT_X0..1] so the hit-test still
* resolves row k, and clears the number column (NUM_X=55) / highlight
* boxes (from x=53/73). The PREVIOUS-tab hitbox (x>390 && y>223) is
* far away. Mouse users are unaffected: the pointer sits under the
* real mouse, never at this snap position. */
cursor_h_pos = 44.0f;
cursor_h_pos = 32.0f; /* D502: was 44; the sprite overlapped the "1." / row text */
cursor_v_pos = (f32)(rowY(k) + 6);
s_prevH = cursor_h_pos; /* D444: our own snap is not pointer motion */
s_prevV = cursor_v_pos;
@@ -267,6 +269,14 @@ static void cursorToItem(int k)
cursorToItemRaw(k);
}
/* D502: highlight for a screen change. Pad/keyboard users get item k
* highlighted (the crosshair was just snapped onto it); with a live mouse the
* pointer owns the selection, so start unselected and let hover pick. */
static int navHighlight(int k)
{
return inputMenuPointerLive() ? -1 : k;
}
/* ------------------------------------------------------------------------ */
/* MENU_PC_OPTIONS: init / update / interface / constructor (front.c cases) */
@@ -358,7 +368,7 @@ static void goBack(void)
for (int k = 0; k < s_rowN; k++)
if (optionsRowChildHeader(s_rowIdx[k]) == child) selected = k + 1;
cursorToItem(selected);
s_hl = -1; s_lastHover = -2;
s_hl = navHighlight(selected); s_lastHover = -2; /* D502 */
sysLogPrintf(LOG_INFO, "frontoptions: back to %s", optionsRowLabel(activeHeader()));
return;
}
@@ -366,6 +376,10 @@ static void goBack(void)
s_level = 0;
s_pageno = 0; /* D406 */
cursorToItem(s_page);
/* D502: the highlight is an item index of the level being shown;
* leaving it at the section's row index landed pad users on another
* section (row 1 inside Input -> "Graphics"). */
s_hl = navHighlight(s_page); s_lastHover = -2;
sysLogPrintf(LOG_INFO, "frontoptions: back to section list");
return;
}
@@ -374,22 +388,32 @@ static void goBack(void)
frontChangeMenu(MENU_FILE_SELECT, FALSE);
}
/* D444: Previous/Next page controls on the bottom hint row of a multi-page
* section. Returns -1 (previous), +1 (next) or 0 (none / not over an enabled
* control). Same geometry the draw code uses. */
static const char kPrevPage[] = "Previous page";
static const char kNextPage[] = "Next page";
static int pageCtlAt(void)
/* D497: paging mirrors the mission dossiers (front.c briefing screen): the
* folder's PREVIOUS tab (and B) turns back a page, or leaves the section from
* its first page; a NEXT tab is shown while a later page exists. This replaced
* D444's "Previous page / Next page" links under the rows. */
static int hasNextPage(void)
{
if (s_level < 1 || s_rowTotal <= ROWS_PER_PAGE || tab_prev_highlight) return 0;
if (cursor_v_pos < (f32)(rowY(s_rowN + 1) - 3) ||
cursor_v_pos >= (f32)(rowY(s_rowN + 1) + ROW_DY - 3)) return 0;
int last = s_pageno * ROWS_PER_PAGE + s_rowN >= s_rowTotal;
if (s_pageno > 0 && cursor_h_pos >= ROW_X - 2 &&
cursor_h_pos <= ROW_X + measureW(kPrevPage) + 4) return -1;
if (!last && cursor_h_pos <= VAL_R + 2 &&
cursor_h_pos >= VAL_R - measureW(kNextPage) - 4) return +1;
return 0;
return s_level >= 1 && s_pageno * ROWS_PER_PAGE + s_rowN < s_rowTotal;
}
static void turnPage(int dir)
{
playSfx(DOOR_METAL_CLOSE2_SFX);
if (s_dragRow >= 0) optionsRowCommit(s_dragRow);
s_dragRow = -1;
optionsResetClear();
s_pageno += dir;
buildPages();
cursorToItem(dir > 0 ? 1 : 0);
s_hl = navHighlight(dir > 0 ? 1 : 0); s_lastHover = -2; /* D502 */
}
/* PREVIOUS tab / B: previous page, else back out. */
static void goPrevious(void)
{
if (s_level >= 1 && s_pageno > 0) turnPage(-1);
else goBack();
}
void frontOptionsMenuInterface(void)
@@ -414,8 +438,11 @@ void frontOptionsMenuInterface(void)
int hit = -1;
int moved = (cursor_h_pos != s_prevH || cursor_v_pos != s_prevV);
tab_prev_highlight = FALSE;
tab_next_highlight = FALSE;
if (frontCheckCursorOnPreviousTab()) {
tab_prev_highlight = TRUE;
} else if (hasNextPage() && frontCheckCursorOnNextTab()) {
tab_next_highlight = TRUE; /* D497 */
} else if (cursor_h_pos >= ROW_HIT_X0 && cursor_h_pos <= ROW_HIT_X1) {
for (int k = itemCount() - 1; k >= 0; k--) {
if (cursor_v_pos >= (f32)(rowY(k) - 3) &&
@@ -445,13 +472,9 @@ void frontOptionsMenuInterface(void)
s_prevV = cursor_v_pos;
}
/* D444: Previous/Next page controls (mouse click; Up/Down keep paging). */
int pageCtl = pageCtlAt();
if (pageCtl != 0 && joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG)) {
playSfx(DOOR_LOCK_SFX);
s_pageno += pageCtl;
buildPages();
s_hl = -1; /* wheel/keys/hover pick a row afresh; no snap */
/* D497: NEXT folder tab (mouse click; Up/Down still page too). */
if (tab_next_highlight && joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG)) {
turnPage(+1);
} else if (joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG | START_BUTTON)) {
/* D444: a click acts on the row under the pointer, not on a stale
* wheel/key selection (keyboard Enter: the snap put the pointer on
@@ -459,7 +482,7 @@ void frontOptionsMenuInterface(void)
if (!tab_prev_highlight && joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG))
s_hl = s_hoverHit;
if (tab_prev_highlight) {
goBack();
goPrevious(); /* D497 */
} else if (s_hl >= 0 && s_level == 0) {
playSfx(DOOR_METAL_CLOSE2_SFX);
s_page = s_hl;
@@ -468,7 +491,7 @@ void frontOptionsMenuInterface(void)
sysLogPrintf(LOG_INFO, "frontoptions: section %d", s_page + 1);
buildPages();
cursorToItem(0);
s_hl = -1; s_lastHover = -2;
s_hl = navHighlight(0); s_lastHover = -2; /* D502 */
} else if (s_hl == 0 && s_level >= 1) {
/* D356: the top save-file row -- A steps to the next file (the
* D352 chooser; folders only, "none" retired, plan §5.5). */
@@ -493,7 +516,7 @@ void frontOptionsMenuInterface(void)
sysLogPrintf(LOG_INFO, "frontoptions: opened %s (depth %d)",
optionsRowLabel(child), s_level);
cursorToItem(0);
s_hl = -1; s_lastHover = -2;
s_hl = navHighlight(0); s_lastHover = -2; /* D502 */
} else if (optionsRowIsSlider(i) && bx0 <= BAR_X1 - 12 &&
cursor_h_pos >= bx0 - 4 && cursor_h_pos <= BAR_X1 + 4) {
optionsRowSetFraction(i, ((double)cursor_h_pos - bx0) / (BAR_X1 - bx0));
@@ -517,7 +540,7 @@ void frontOptionsMenuInterface(void)
}
}
} else if (joyGetButtonsPressedThisFrame(PLAYER_1, B_BUTTON)) {
goBack();
goPrevious(); /* D497: as the dossiers, B turns back a page first */
}
/* Drag a slider while A is held. */
@@ -564,8 +587,10 @@ void frontOptionsMenuInterface(void)
}
}
s_repeatDir = dir;
if (dir == 0) optionsAdjustCommitPending(); /* D489: save once on release */
} else {
s_repeatDir = 0;
optionsAdjustCommitPending();
}
/* D356: reset arm state -- the screen's selected rows[] index (s_hl 0 is
@@ -794,31 +819,11 @@ Gfx *frontOptionsMenuDraw(Gfx *DL)
? "Press pad button tap {B} cancel hold Back clear\n"
: "{A}: bind Left/Right: slot {Y}: clear {B}: back\n");
DL = ink(DL, ROW_X, rowY(s_rowN + 2), d471buf1, INK_DIM);
} else if (s_rowTotal > ROWS_PER_PAGE) {
/* D406b: bottom hint on every page of a multi-page section (11
* rows per page leaves the last line of the paper for it). */
int last = s_pageno * ROWS_PER_PAGE + s_rowN >= s_rowTotal;
int first = s_pageno == 0;
/* D406d: one row below the last content row (rowY(s_rowN + 1))
* -- pitch-aligned with the list; +2 sat ~26px clear of it and
* looked orphaned at the paper's bottom edge. */
int hov = pageCtlAt(), hy = rowY(s_rowN + 1);
if (!first) {
if (hov < 0)
DL = microcode_constructor_related_to_menus(DL, ROW_X - 2, hy - 1,
ROW_X + measureW(kPrevPage) + 4, hy + 0xE, HILITE);
DL = ink(DL, ROW_X, hy, "Previous page\n", INK);
}
if (!last) {
if (hov > 0)
DL = microcode_constructor_related_to_menus(DL, VAL_R - measureW(kNextPage) - 4, hy - 1,
VAL_R + 2, hy + 0xE, HILITE);
DL = inkR(DL, VAL_R, hy, "Next page\n", INK);
}
}
}
DL = frontAddPreviousTabText(DL);
if (hasNextPage()) DL = frontAddNextTabText(DL); /* D497 */
DL = frontDrawCursor(DL);
return DL;
}
+126 -44
View File
@@ -403,9 +403,10 @@ int inputOverlayOwnerPad(void)
* Sources are SDL controller buttons plus the two analog triggers treated as
* digital (padTriggerPct). Each action is bound to up to two sources, written
* in the ini as SDL names joined by a comma ("a,x", "righttrigger", "none").
* Input.PadPreset: 0 = Jinx 1.1 (the pre-D469 hard-wired mapping; default),
* 1/2 = Jinx 1.2/1.3 (NOT defined by D394: reserved, resolve to 1.1),
* 3 = Custom (the per-seat Input.Pad.* strings). Seat 0 keys are
* Input.PadPreset: 0 = 1.1 Jinx (the pre-D469 hard-wired mapping; default),
* 1 = 1.2 Christmas, 2 = 1.3 Frost (D498: D394 stick roles, same buttons),
* 3 = Custom (the per-seat Input.Pad.* strings), 4 = 1.4 Elektra (D498).
* Seat 0 keys are
* Input.Pad.<Action>; seats 1..3 are Input.Pad2/3/4.<Action>. Seat = pads[]
* index (D448), not the controller slot.
*
@@ -458,6 +459,11 @@ typedef struct { u32 mask[PA_COUNT]; } PadBinds;
typedef struct { PadBinds seat[MAX_PADS]; } PadTable;
static int padPreset = 0; /* Input.PadPreset */
/* D194/D238: 1 = force GE's own 1.2/SOLITARE
* control style for continuous analog pitch;
* 0 = legacy 1.1/HONEY + D166 digital pulse.
* (D498: declared here so the stick-style helpers below can read it.) */
static int naturalPitchMode = 1; /* Input.NaturalPitch */
static char padStr[MAX_PADS][PA_COUNT][32]; /* Input.Pad[N].<Action> */
static char padStrKey[MAX_PADS][PA_COUNT][40];
static PadTable padTabBuf[2];
@@ -496,7 +502,21 @@ static void padBuildBinds(PadBinds *b, int seat, int custom, int *bad)
if (!b->mask[PA_START]) b->mask[PA_START] = PSM(PS_START); /* lockout guard: keep a pause path */
}
static int padPresetCustom(void) { return padPreset >= 3; }
/* D498: preset 3 is Custom; 4 = 1.4 Elektra is a fixed mapping, not custom. */
static int padPresetCustom(void) { return padPreset == 3; }
/* D498: stick style for the active preset. 1.2 Christmas / 1.3 Frost force
* HONEY and 1.4 Elektra forces SOLITARE (for mouse and keyboard too, since
* the game runs one control style per player). 1.1 Jinx (the default) and
* Custom keep Input.NaturalPitch, so the NaturalPitch=0 escape hatch still
* works exactly as before D498. */
static int stickSolitare(void) {
if (padPreset == 1 || padPreset == 2) return 0;
if (padPreset == 4) return 1;
return naturalPitchMode;
}
/* D498: 1.3 Frost / 1.4 Elektra swap the two sticks (never in menus). */
static int stickSwap(void) { return padPreset == 2 || padPreset == 4; }
/* Signature of everything the table depends on (preset only unless Custom). */
static unsigned padComputeSig(void)
@@ -527,9 +547,8 @@ static const PadTable *padTabGet(void)
SDL_AtomicSetPtr((void **)&padTabPub, spare);
padTabSig = sig;
padTabValid = 1;
sysLogPrintf(LOG_INFO, "input: pad table rebuilt (preset %d%s%s, %d unknown name(s))",
padPreset, padPresetCustom() ? " custom" : "",
padPreset == 1 || padPreset == 2 ? " [1.2/1.3 undefined -> 1.1]" : "", bad);
sysLogPrintf(LOG_INFO, "input: pad table rebuilt (preset %d%s, %d unknown name(s))",
padPreset, padPresetCustom() ? " custom" : "", bad); /* D498 */
SDL_AtomicUnlock(&rebuildLock);
return spare;
}
@@ -557,7 +576,7 @@ static u32 padActHeld(const PadBinds *b, u32 raw)
return h;
}
typedef struct { int facePlayable; int slotNative; int southpaw; } PadMapCtx;
typedef struct { int facePlayable; int slotNative; int southpaw; int tank; } PadMapCtx;
typedef struct { unsigned button; int crouch; int gadgetEdge; u32 actHeld; } PadMapOut;
/* The pure mapping: raw source bits + bindings + previous action state ->
@@ -588,6 +607,15 @@ static void padMapPure(const PadBinds *b, u32 raw, u32 actPrev, unsigned buttonI
if (edge & (1u << PA_PREVWPN)) button |= GE_CONT_A | GE_CONT_G; /* same backward-cycle idiom as the mouse wheel */
if (raw & b->mask[PA_AIM] & ~PS_TRIGGERS) button |= GE_CONT_R; /* digital sources (LB aim): playable only, after the swap */
if (raw & b->mask[PA_FIRE] & ~PS_TRIGGERS) button |= GE_CONT_G;
} else if (c->tank) {
/* D495: driving the tank. The native A (change weapon) and B (exit,
* sent by the locked use path in inputComputePadSlot) follow the
* player's bindings like on foot, instead of the fixed menu faces
* (A/X = A, B/Y = B): with the default preset X switched the tank
* weapon and Y left the tank. */
if (edge & ((1u << PA_NEXTWPN) | (1u << PA_PREVWPN))) button |= GE_CONT_A;
if (raw & b->mask[PA_AIM] & ~PS_TRIGGERS) button |= GE_CONT_R;
if (raw & b->mask[PA_FIRE] & ~PS_TRIGGERS) button |= GE_CONT_G;
} else {
if (raw & (PSM(PS_A) | PSM(PS_X))) button |= GE_CONT_A; /* menu lockout guard: fixed native accept/cancel */
if (raw & (PSM(PS_B) | PSM(PS_Y))) button |= GE_CONT_B;
@@ -940,9 +968,6 @@ static int padDirectCompute(int dx, int dy); /* D404: pad twin of the above */
static int s_absAimSuspend = 0;
static int aimAbsCompute(double dxPx, double dyPx, int *outSx, int *outSy); /* D194 */
static int naturalPitchMode = 1; /* D194/D238: 1 = force GE's own 1.2/SOLITARE
* control style for continuous analog pitch;
* 0 = legacy 1.1/HONEY + D166 digital pulse. */
static Uint64 s_lastLookPollCounter = 0; /* D194(b): monotonic clock, gameplay-look drain only */
/* Gamepad tuning -- defaults reproduce the old hardcoded constants exactly.
@@ -1178,13 +1203,27 @@ static void inputOpenPads(void)
* Same entry syntax; "frame" = count of THAT controller's reads since
* launch. Slots 1-3 have no mouse path, so mouse tokens (MDX/MDY) and
* CHOLD/CREL are ignored there (one warning per script if they appear).
* Unset => the seat stays idle exactly as before. */
#define INPUTSCRIPT_MAX 64
#define INPUTSCRIPT_PULSE 6
* Unset => the seat stays idle exactly as before.
*
* GE_INPUTSCRIPT_MS=1 (abpad, tools_pc/abpad): every slot's "frame" is
* instead milliseconds since SDL init (SDL_GetTicks), and buttons pulse for
* INPUTSCRIPT_PULSE_MS. Read rates vary across the intro and menus, so a
* read count can't be lined up with a wall-clock reference (1964). */
#define INPUTSCRIPT_MAX 64
#define INPUTSCRIPT_PULSE 6
#define INPUTSCRIPT_PULSE_MS 50
static int scriptMsMode(void)
{
static int ms = -1;
if (ms < 0) ms = GE_ENVFLAG("GE_INPUTSCRIPT_MS") ? 1 : 0;
return ms;
}
struct scriptEntry { long frame; unsigned mask; int sx, sy; int hasStick;
int hasMouse, mdx, mdy; int hasHold, hold; /* D337 mouse/aim tokens */
int hasZHold, zhold; /* D207: sustained fire */
int hasCHold; unsigned chold; /* abpad: sustained C buttons (1.2 walk/strafe) */
int hasCrouch, crouch; /* D377: free-crouch QA */
int hasUse, use, hasReload, reload; }; /* D378: split-action QA */
/* Per-slot state. Slot 0 is the original GE_INPUTSCRIPT harness (byte-for-
@@ -1197,6 +1236,7 @@ struct scriptSlot {
int curSX, curSY; /* stick set by the last scriptApply() for this slot */
int mouseOn, mdx, mdy; /* sustained scripted mouse (slot 0 only) */
int hold, zhold, crouch, use, reload; /* latest sustained holds */
unsigned chold; /* latest sustained C-button mask */
int ignoredWarned; /* P1..P3: warned once about mouse/crouch tokens */
};
static struct scriptSlot scriptSlots[4] = {
@@ -1261,6 +1301,13 @@ static void scriptApplyToken(struct scriptEntry *e, const char *s, int n,
* independent of RHOLD so aim + fire can be held together. */
if (n == 5 && SDL_strncasecmp("ZHOLD", s, 5) == 0) { e->hasZHold = 1; e->zhold = 1; return; }
if (n == 4 && SDL_strncasecmp("ZREL", s, 4) == 0) { e->hasZHold = 1; e->zhold = 0; return; }
/* abpad: CUPHOLD/CDOWNHOLD/CLEFTHOLD/CRIGHTHOLD hold C buttons until CNONE
* (the port forces 1.2 SOLITARE, where C-up/down walk and C-left/right strafe). */
if (n == 7 && SDL_strncasecmp("CUPHOLD", s, 7) == 0) { e->hasCHold = 1; e->chold |= GE_CONT_E; return; }
if (n == 9 && SDL_strncasecmp("CDOWNHOLD", s, 9) == 0) { e->hasCHold = 1; e->chold |= GE_CONT_D; return; }
if (n == 9 && SDL_strncasecmp("CLEFTHOLD", s, 9) == 0) { e->hasCHold = 1; e->chold |= GE_CONT_C; return; }
if (n == 10 && SDL_strncasecmp("CRIGHTHOLD", s, 10) == 0) { e->hasCHold = 1; e->chold |= GE_CONT_F; return; }
if (n == 5 && SDL_strncasecmp("CNONE", s, 5) == 0) { e->hasCHold = 1; e->chold = 0; return; }
if ((n == 5 && SDL_strncasecmp("CHOLD", s, 5) == 0) ||
(n == 4 && SDL_strncasecmp("CREL", s, 4) == 0)) {
if (slot > 0) { /* no crouch path on slots 1-3: ignore, warn once */
@@ -1315,6 +1362,7 @@ static void scriptParse(int slot)
e->hasMouse = e->mdx = e->mdy = 0;
e->hasHold = e->hold = 0;
e->hasZHold = e->zhold = 0;
e->hasCHold = 0; e->chold = 0;
e->hasCrouch = e->crouch = 0;
e->hasUse = e->use = e->hasReload = e->reload = 0;
while (*p && *p != ';') {
@@ -1351,7 +1399,7 @@ static int scriptIsActive(int slot)
static void scriptRefreshHolds(int slot)
{
struct scriptSlot *st = &scriptSlots[slot];
long bestM = -1, bestH = -1, bestZ = -1, bestC = -1, bestU = -1, bestR = -1;
long bestM = -1, bestH = -1, bestZ = -1, bestC = -1, bestU = -1, bestR = -1, bestCH = -1;
for (int i = 0; i < st->count; ++i) {
long d = st->frame - st->entries[i].frame;
if (d < 0) continue;
@@ -1369,6 +1417,10 @@ static void scriptRefreshHolds(int slot)
bestZ = st->entries[i].frame;
st->zhold = st->entries[i].zhold;
}
if (st->entries[i].hasCHold && st->entries[i].frame > bestCH) {
bestCH = st->entries[i].frame;
st->chold = st->entries[i].chold;
}
if (st->entries[i].hasCrouch && st->entries[i].frame > bestC) {
bestC = st->entries[i].frame;
st->crouch = st->entries[i].crouch;
@@ -1390,12 +1442,15 @@ static unsigned scriptApply(int slot, unsigned button)
if (!scriptIsActive(slot)) {
return button;
}
int ms = scriptMsMode();
if (ms) st->frame = (long)SDL_GetTicks();
scriptRefreshHolds(slot); /* D207: refresh sustained holds even off the mouse path (idempotent) */
unsigned m = 0;
long bestStickFrame = -1;
long pulse = ms ? INPUTSCRIPT_PULSE_MS : INPUTSCRIPT_PULSE;
for (int i = 0; i < st->count; ++i) {
long d = st->frame - st->entries[i].frame;
if (d >= 0 && d < INPUTSCRIPT_PULSE) {
if (d >= 0 && d < pulse) {
m |= st->entries[i].mask;
}
/* sustained stick: the latest-starting entry that carried a stick token */
@@ -1408,7 +1463,8 @@ static unsigned scriptApply(int slot, unsigned button)
if (st->hold == 1) m |= GE_CONT_R; /* D337 RHOLD */
if (st->hold == 2) m |= GE_CONT_L; /* D337 LHOLD (Q / "LeanLeft") */
if (st->zhold) m |= GE_CONT_G; /* D207 ZHOLD (fire) */
st->frame++;
m |= st->chold; /* abpad C-button holds */
if (!ms) st->frame++;
return m;
}
@@ -1435,6 +1491,11 @@ int inputInit(void)
* be set before the pads below are opened. */
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
/* abpad live A/B: GE_PAD_BACKGROUND=1 keeps pad input flowing while the
* window is unfocused, so one real pad can drive the port and 1964 at once.
* Off by default (SDL ignores pads in unfocused windows). */
if (getenv("GE_PAD_BACKGROUND") && getenv("GE_PAD_BACKGROUND")[0] == '1')
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
for (int i = 0; i < MAX_PADS; ++i) {
pads[i] = NULL;
@@ -2026,7 +2087,7 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
* anything else ever calls the setter (menu, save load) in between. */
if ((idx == 0 || inputMpSplit()) && slotPlayer() != NULL &&
(idx == 0 || idx < getPlayerCount())) { /* D416: each slot rewrites only its own player's style */
int wantSolitare = naturalPitchMode ? CONTROLLER_CONFIG_SOLITARE_ : CONTROLLER_CONFIG_HONEY_;
int wantSolitare = stickSolitare() ? CONTROLLER_CONFIG_SOLITARE_ : CONTROLLER_CONFIG_HONEY_; /* D498 */
struct player *sp = slotPlayer();
if (!inputMpSplit()) {
if (cur_player_get_control_type() != wantSolitare)
@@ -2162,7 +2223,7 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
* front-end screen (file select, cheat, MENU_PC_OPTIONS) can be
* driven vertically from the keyboard. Watch/pause keeps the C-button
* mapping: it is current_menu==RUN_STAGE (menuMode 0), N64-style. */
if (naturalPitchMode && !menuMode) {
if (stickSolitare() && !menuMode) { /* D498 */
if (actHeld(ks, IA_FORWARD)) button |= GE_CONT_E; /* C-up = forward */
if (actHeld(ks, IA_BACK)) button |= GE_CONT_D; /* C-down = back */
} else {
@@ -2580,7 +2641,7 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
sx += (int)(hipEdx * hipSens * MOUSE_TURN_GAIN * (inTankHip ? tankAimScale / 100.0 : 1.0));
if (inTankHip) {
/* no pitch/drive from the mouse in a tank */
} else if (naturalPitchMode) {
} else if (stickSolitare()) { /* D498 */
/* D194/D238: SOLITARE gives hipfire pitch the same
* continuous analog stick treatment as yaw -- same
* formula as the sx line above, so X and Y are, by
@@ -2666,12 +2727,15 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
int padMenuMode = (current_menu != GE_MENU_RUN_STAGE &&
current_menu != GE_MENU_INVALID);
/* D498: 1.3 Frost / 1.4 Elektra swap the two sticks; menus never swap. */
if (!padMenuMode && stickSwap()) { int tx = lx, ty = ly; lx = rx; ly = ry; rx = tx; ry = ty; }
if (padMenuMode) {
int px = scaleAxis(lx, padDeadzoneL);
int py = -scaleAxis(ly, padDeadzoneL); /* SDL up = negative -> N64 up = positive */
if (px) sx = px;
if (py) sy = py;
} else if (naturalPitchMode) {
} else if (stickSolitare()) { /* D498 */
/* D194/D238: SOLITARE swaps stick roles -- left stick becomes
* digital-step movement (same analog-for-movement tradeoff as
* the keyboard remap above), right stick becomes continuous
@@ -2742,6 +2806,8 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
pc.facePlayable = (idx == 0 || inputMpSplit()) && inputCanUseGameplayActions(padMenuMode);
pc.slotNative = (idx > 0 || inputMpPadsOwnP1());
pc.southpaw = padSouthpaw;
pc.tank = !pc.facePlayable && idx == 0 && !padMenuMode && g_PlayerIsInTank == 1 &&
slotPlayer() && slotPlayer()->outside_watch_menu && !slotPlayer()->pause_state;
const PadBinds *pb = &padTabGet()->seat[padSeatForSlot(idx)];
padMapPure(pb, padReadRaw(pad), padActPrev[idx], button, &pc, &po);
button = po.button;
@@ -2785,37 +2851,30 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
!p->pause_state && !p->mpmenuon && !g_PlayerIsInTank &&
!lvlGetControlsLockedFlag() &&
!gameScriptedCameraActive();
/* D483/D494 (#136): a weapon with DISABLE_CROUCH (the sniper rifle)
* makes the native code turn aim-mode C-up/C-down into zoom, so the
* N64 pad cannot change stance with it out. GEPD's crouch key (the
* mouse injector, the reference for this preset) writes the stance
* directly and ignores the flag: Bond crouches and stands freely with
* the sniper (maintainer A/B on 1964 GEPD, 2026-10-03). Do the same,
* but never feed C-down for such a weapon (it would zoom out). */
int weaponBlocks = 0;
if (canCrouch && (crouchNow || s_crouchApplied)) { /* weapon lookup only when it can matter */
if (canCrouch && (crouchNow || s_crouchApplied)) /* weapon lookup only when it can matter */
weaponBlocks = bondwalkItemCheckBitflags(p->hands[GUNRIGHT].weaponnum,
WEAPONSTATBITFLAG_DISABLE_CROUCH);
canCrouch = !weaponBlocks;
}
static int frozenLogged = 0;
if (!weaponBlocks) frozenLogged = 0;
if (weaponBlocks) {
/* D483 (#136): a weapon with DISABLE_CROUCH (the sniper rifle)
* blocks the native crouch-down AND crouch-up (bondview2.c), so
* on the N64 Bond keeps whatever stance he had when he switched
* to it. Freeze the port stance the same way: don't apply a new
* crouch, don't stand up, and never feed C-down (it zooms out
* with this weapon). Switching back resumes the normal rules. */
if (!frozenLogged && configGetInputLog())
sysLogPrintf(LOG_NOTE, "GE_INPUTLOG free crouch frozen (weapon %d disables crouch, crouchpos=%d, key=%d)",
(int)p->hands[GUNRIGHT].weaponnum, (int)p->crouchpos, crouchNow);
frozenLogged = 1;
} else if (canCrouch && crouchNow) {
if (canCrouch && crouchNow) {
/* In aim mode the native crouchUp branch runs every tick without
* C-down, undoing the port stance by one step. Feed the native
* axis ONLY while aiming; hipfire still crouches by stance alone. */
if (p->insightaimmode || (button & (GE_CONT_R | GE_CONT_L)))
if (!weaponBlocks && (p->insightaimmode || (button & (GE_CONT_R | GE_CONT_L))))
button |= GE_CONT_D;
p->crouchpos = CROUCH_SQUAT;
if (!s_crouchApplied && configGetInputLog())
sysLogPrintf(LOG_NOTE, "GE_INPUTLOG free crouch applied (aim=%d)", p->insightaimmode);
sysLogPrintf(LOG_NOTE, "GE_INPUTLOG free crouch applied (aim=%d, weapon disables crouch=%d)",
p->insightaimmode, weaponBlocks);
s_crouchApplied = 1;
s_crouchPlayer = p;
} else if (!(canCrouch && (button & GE_CONT_D) && p->insightaimmode)) {
} else if (!(canCrouch && !weaponBlocks && (button & GE_CONT_D) && p->insightaimmode)) {
/* If native C-down is still held in aim, let the game retain its
* own crouch on this tick rather than force a stand. */
if (s_crouchApplied && configGetInputLog())
@@ -3092,8 +3151,13 @@ int inputRumbleSupported(int idx)
return padRumbleOn[idx];
}
/* D493: set once inputRumbleStopAll has run at exit, so the still-running
* game thread cannot start a new rumble after the pads were silenced. */
static SDL_atomic_t s_rumbleStopped;
void inputRumble(int idx, f32 strength, f32 time)
{
if (SDL_AtomicGet(&s_rumbleStopped)) return;
if (idx < 0 || idx >= MAX_PADS) return;
SDL_GameController *rpad = slotPad(idx);
const int rIdx = (inputMpSplit() && inputMpKbmP1()) ? idx - 1 : idx;
@@ -3112,6 +3176,21 @@ void inputRumble(int idx, f32 strength, f32 time)
}
}
/* D493: silence every open pad at a clean exit. SDL_GameControllerRumble
* runs for its duration on the pad itself, so a rumble started just before
* the window closed kept going after the process ended (seen on a DualSense
* when quitting mid-firefight). inputDestroy never runs: the host loop exits
* through exit(0), so this is called from main.c's atexit handler. */
void inputRumbleStopAll(void)
{
SDL_AtomicSet(&s_rumbleStopped, 1);
for (int i = 0; i < MAX_PADS; ++i) {
if (pads[i] && padRumbleOn[i]) {
SDL_GameControllerRumble(pads[i], 0, 0, 0);
}
}
}
f32 inputRumbleGetScale(void)
{
return gRumbleScale;
@@ -3520,8 +3599,11 @@ unsigned inputPadHeldSources(int seat)
}
int inputPadPresetStep(int cur, int dir)
{
(void)dir; /* only Jinx 1.1 (0) and Custom (3) are defined; 1/2 are reserved (D394 does not define 1.2/1.3) */
return cur == 0 ? 3 : 0;
/* D498: display order Jinx, Christmas, Frost, Elektra, Custom (stored 0,1,2,4,3). */
static const int order[5] = { 0, 1, 2, 4, 3 };
for (int i = 0; i < 5; i++)
if (order[i] == cur) return order[(i + (dir > 0 ? 1 : 4)) % 5];
return 0; /* unknown stored value -> Jinx */
}
/* "a,x" -> src[0]=A, src[1]=X; empty/none/unknown -> -1 (positional). */
@@ -3687,7 +3769,7 @@ static void padMapSelfTest(void)
PD_CONSTRUCTOR static void inputConfigInit(void)
{
configRegisterInt("Input.PadPreset", &padPreset, 0, 3); /* D469: 0 Jinx 1.1, 1/2 reserved (= 1.1), 3 Custom */
configRegisterInt("Input.PadPreset", &padPreset, 0, 4); /* D498: 0 Jinx 1.1, 1 Christmas 1.2, 2 Frost 1.3, 3 Custom, 4 Elektra 1.4 */
for (int seat = 0; seat < MAX_PADS; seat++)
for (int a = 0; a < PA_COUNT; a++) {
if (seat == 0) snprintf(padStrKey[seat][a], sizeof(padStrKey[seat][a]), "Input.Pad.%s", kPadAct[a].key);
+4 -101
View File
@@ -586,39 +586,9 @@ void osEnqueueMesg(OSMesgQueue *mq, OSMesg msg)
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) */
static int s_d51 = -1; if (s_d51 < 0) s_d51 = getenv("GE_D51") != NULL; /* D302: per-message path */
if (s_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) {
@@ -634,25 +604,11 @@ s32 osSendMesg(OSMesgQueue *mq, OSMesg msg, s32 flag)
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) {
static int s_d60r = -1; if (s_d60r < 0) s_d60r = getenv("GE_D60") != NULL; /* D302 */
if (mq != &gfxFrameMsgQ || !s_d60r) 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) {
@@ -711,7 +667,6 @@ s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag)
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);
/* gfxFrameMsgQ is consumed only by boss.c's game thread. Apply queued
@@ -789,8 +744,6 @@ void osViSetEvent(OSMesgQueue *mq, OSMesg msg, u32 retraceCount)
g_viRetraceMsg = msg;
}
/* TEMP D51: instrument the retrace pacemaker */
static uint64_t g_viPostCount = 0;
static void portPostVIEvent(void)
{
if (g_viRetraceMQ) {
@@ -798,17 +751,9 @@ static void portPostVIEvent(void)
* 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);
}
osSendMesg(g_viRetraceMQ, g_viRetraceMsg, OS_MESG_NOBLOCK);
}
}
/* END TEMP D51 */
void osViSetMode(OSViMode *vm)
{
@@ -910,35 +855,9 @@ void osAiSetConvert(u32 convert) { (void)convert; }
* 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) {
static int s_d60s = -1; if (s_d60s < 0) s_d60s = getenv("GE_D60") != NULL; /* D302 */
if (!s_d60s) 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
/* 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] = { PORT_DRAM_V1_BASE, PORT_DRAM_K0_BASE }; /* portaddr.h */
@@ -974,7 +893,6 @@ 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 "
@@ -982,23 +900,8 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size)
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. */
static int s_d61env = -1; if (s_d61env < 0) s_d61env = getenv("GE_D61") != NULL; /* D302 */
if (!s_d61opened && s_d61env) {
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);
}
/* Validate the DMA target too. The N64 PI happily DMAs to any KSEG
* address; on PC an unmapped target is a wild memcpy. */
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
+5 -5
View File
@@ -83,6 +83,7 @@ static void portAtExit(void)
{
/* Clean-exit only (exit(0) from videoPumpEvents). Crash/fatal paths call
* abort(), which does not run atexit handlers. */
inputRumbleStopAll(); /* D493: no pad left rumbling after exit */
videoSaveWindowState();
configSave();
/* D443: Restart game -- runs only on the orderly D344 exit path, after the
@@ -146,11 +147,10 @@ int main(int argc, char **argv)
* writes only -- no game-logic edits (AGENTS rule 2), same class as
* the existing GE_UNLOCK_ALL getenv hook in the getter. No active
* cheats (invincibility / all guns) are enabled; weapons remain
* per-mission pickups as on the N64. Separately, the eep shim
* (libultra.c geEepromPatchAllCheats) sets every progression-gated
* cheat-unlock bit in the save block at read time (per-slot CRC
* recomputed via the game's own fileGenerateCRC), so the cheat
* menu is fully populated without completed levels. */
* per-mission pickups as on the N64. D442: the cheat menu and every
* COMPLETED check come from the query-time hooks in file2.c
* (fileGetIsCheatUnlocked, fileIsStageUnlockedAtDifficulty); the
* save is never patched. */
#if defined(LEFTOVERDEBUG) /* the debug unlock flags only exist with LEFTOVERDEBUG (not PAL); D442's query-time hooks cover All unlocked everywhere */
{
extern s32 portAllUnlocked; /* port/src/video.c */
+45 -3
View File
@@ -93,8 +93,8 @@ static const char *const kOnOff[] = { "Off", "On", NULL };
static const char *const kReverse[] = { "Reverse", "Upright", NULL };
static const char *const kHold[] = { "Hold", "Toggle", NULL };
static const char *const kAspectMode[] = { "Window", "Original", NULL }; /* D447: Original = 4:3 (16:9 with the game Ratio) bars */
static const char *const kTexFilter[] = { "Nearest", "Bilinear", "3-Point", NULL };
static const char *const kPadPreset[] = { "Jinx 1.1", "Jinx 1.2 (n/a)", "Jinx 1.3 (n/a)", "Custom", NULL }; /* D469: 1.2/1.3 undefined by D394 -> behave as 1.1 */
static const char *const kTexFilter[] = { "Nearest", "Bilinear", "3-Point", "Trilinear", NULL }; /* Trilinear option (playtest 2026-10-03) */
static const char *const kPadPreset[] = { "1.1 Jinx", "1.2 Christmas", "1.3 Frost", "Custom", "1.4 Elektra", NULL }; /* D498: index = stored value; inputPadPresetStep gives the display order */
static int s_padSeat = 0; /* D469: seat edited by the Controller page rows */
static const char *const kAimMode[] = { "N64", "Centred (PC)", NULL }; /* D337 */
static const char *const kAimRange[] = { "PC", "N64", NULL }; /* D338 */
@@ -146,6 +146,12 @@ static int s_resFitN = 0;
static int s_resSel = 0; /* index into s_resFit */
static int s_dragRow = -1; /* scheduler-thread mouse drag */
static SDL_atomic_t s_dragWatchField; /* field+1, host may close F10 mid-drag */
/* D489: field+1 of a watch slider stepped by D-pad/stick/arrows. Each commit
* is three joyDisablePoll/joyEnablePoll handshakes plus an EEPROM file
* rewrite on the game thread, so committing every held-repeat detent stalled
* frames. Steps stage like a mouse drag; optionsAdjustCommitPending() saves
* once when the direction is released. */
static SDL_atomic_t s_adjWatchField;
struct Row {
const char *key;
@@ -1386,10 +1392,21 @@ static void rowAdjust(struct Row *r, int dir)
}
case ROW_ENUM: {
double lo = r->cfgMin, hi = r->cfgMax;
if (!strcmp(r->key, "Input.PadPreset")) { /* D469: only Jinx 1.1 and Custom are defined */
if (!strcmp(r->key, "Input.PadPreset")) { /* D498: step through the preset display order (0,1,2,4,3) */
rowSet(r, (double)inputPadPresetStep((int)lround(v), dir));
break;
}
if (!strcmp(r->key, "Video.TextureFilter")) {
/* D499: menu order Nearest, Bilinear, Trilinear, 3-Point (stored
* 0, 1, 3, 2: 2 stays 3-Point for existing inis and the Original
* N64 preset), so Bilinear and Trilinear sit side by side for A/B. */
static const int order[4] = { 0, 1, 3, 2 };
int cur = (int)lround(v), i = 0;
while (i < 4 && order[i] != cur) i++;
if (i == 4) i = 0;
rowSet(r, (double)order[(i + (dir > 0 ? 1 : 3)) % 4]);
break;
}
v += dir;
if (v < lo) v = hi;
if (v > hi) v = lo;
@@ -1468,11 +1485,27 @@ static void rowAdjust(struct Row *r, int dir)
rowSet(r, (double)p);
break;
}
{
int field = watchSettingsFieldForKey(r->key);
if (field >= 0) {
/* D489: stage only; a different pending field saves first. */
int prev = SDL_AtomicSet(&s_adjWatchField, field + 1);
if (prev > 0 && prev != field + 1) watchSettingsCommit(prev - 1);
rowSetCommit(r, v + dir * r->step, 0);
break;
}
}
rowSet(r, v + dir * r->step);
break;
}
}
void optionsAdjustCommitPending(void)
{
int pending = SDL_AtomicSet(&s_adjWatchField, 0);
if (pending > 0) watchSettingsCommit(pending - 1);
}
/* ------------------------------------------------------------------------ */
/* D356: per-section "Reset to defaults" (plan Gate E, made concrete). */
/* The activation contract (plan §5.4): edge-triggered, two-step */
@@ -1721,6 +1754,7 @@ void optionsOverlayToggle(void)
if (optionsBindingCaptureActive()) return; /* Escape cancels modal */
int pending = SDL_AtomicSet(&s_dragWatchField, 0);
if (pending > 0) watchSettingsCommit(pending - 1);
optionsAdjustCommitPending(); /* D489 */
}
s_open = !s_open;
if (s_open) {
@@ -1833,6 +1867,7 @@ static void overlayHandleInputLocked(void)
prevUp = prevDn = prevLf = prevRt = prevLmb = prevRmb = 0;
s_dragRow = -1;
SDL_AtomicSet(&s_dragWatchField, 0);
optionsAdjustCommitPending(); /* D489: closed by the host mid-hold */
navDir = adjDir = 0;
SDL_AtomicSet(&s_wheelPending, 0);
return;
@@ -2015,6 +2050,9 @@ static void overlayHandleInputLocked(void)
} else {
adjDir = 0;
}
/* D489: save a stepped watch slider once, on release (also covers
* mouse click/right-click steps, which never hold left/right). */
if (!(rt || lf)) optionsAdjustCommitPending();
}
/* ---- mouse ----
@@ -2260,6 +2298,10 @@ static void valueText(int i, char *out, int n)
snprintf(out, n, "%.1fx", v / 100.0);
return;
}
if (!strcmp(r->key, "Input.RumbleScale")) { /* D357: whole %, like deadzone/volume; storage stays 0..1 */
snprintf(out, n, "%d%%", (int)lround(v * 100.0));
return;
}
if (r->kind == ROW_SLIDER && r->type == CONFIG_OPT_FLOAT) {
snprintf(out, n, "%.2f", v);
return;
+7 -3
View File
@@ -60,7 +60,7 @@ static int initDone = 0;
static int cfgVSync = 1; /* swap interval: 0 = off, 1 = on */
static int cfgFpsCap = 60; /* frame cap in fps; 0 = uncapped (vsync); menu only exposes 30/60 */
static int cfgMSAA = 2; /* 1/2/4/8/16 samples; 2x default is lighter on low-end GPUs */
static int cfgTexFilter = 1; /* 0 = nearest, 1 = bilinear (default), 2 = N64 3-point + trilinear */
static int cfgTexFilter = 1; /* 0 = nearest, 1 = bilinear (default), 2 = N64 3-point + trilinear, 3 = trilinear (Trilinear option, playtest 2026-10-03) */
static int cfgFixMipTex = 1; /* RC2: clip mip-contaminated texture uploads to base height */
static int cfgDetailBaseTile = 1; /* D236: TEXTURETYPE_DETAIL -> sample the base image, not the detail tile */
static int cfgWrapFix = 0; /* D74 sub-tile UV pre-wrap + RC3/D167 non-PoT mask-period wrap (opt-in; GE_WRAPFIX env overrides) */
@@ -489,7 +489,7 @@ PD_CONSTRUCTOR static void videoConfigInit(void)
configRegisterInt("Video.VSync", &cfgVSync, 0, 1);
configRegisterInt("Video.FpsCap", &cfgFpsCap, 0, 1000);
configRegisterInt("Video.MSAA", &cfgMSAA, 1, 16); /* D443: 16x added */
configRegisterInt("Video.TextureFilter", &cfgTexFilter, 0, 2);
configRegisterInt("Video.TextureFilter", &cfgTexFilter, 0, 3); /* Trilinear option (playtest 2026-10-03) */
configRegisterInt("Video.FixMipTextures", &cfgFixMipTex, 0, 1);
configRegisterInt("Video.DetailBaseTile", &cfgDetailBaseTile, 0, 1);
configRegisterInt("Video.WrapFix", &cfgWrapFix, 0, 1);
@@ -762,7 +762,11 @@ static void videoApplyImageOptions(void)
static void videoApplyTexFilter(void)
{
if (cfgTexFilter >= 2) {
/* Trilinear option (playtest 2026-10-03): value 3 = bilinear + generated mips. */
if (cfgTexFilter == 3) {
gfx_set_texture_filter(FILTER_TRILINEAR);
gfx_set_mipmap_filter(MIPMAP_LINEAR);
} else if (cfgTexFilter >= 2) {
gfx_set_texture_filter(FILTER_THREE_POINT);
gfx_set_mipmap_filter(MIPMAP_LINEAR);
} else if (cfgTexFilter == 1) {