feat(D211-D213): PC QoL image options — FOV slider, anisotropic filtering, on-screen FPS

Three port-only knobs, each defaulting to byte-identical behaviour:

- Video.FovScale (D211): multiplicative half-angle scale on every perspective
  projection matrix as it is loaded (gfx_apply_fov_scale, gfx_pc.cpp). Columns
  0+1 scaled together so H/V FOV widen uniformly and aspect is preserved; ortho
  (HUD/menu) matrices untouched. Composes with the game's per-frame aim-zoom
  rewrites for free. Percent of vertical FOV, 50-150, default 100 => guarded
  early-return (zero FP ops, golden dumps byte-identical). WIDESCREEN-FOV-PLAN
  Phase 4, landed standalone.

- Video.Anisotropy (D212): expose fast3d's already-implemented GL anisotropy
  hook as config (1-16). New gfx_set_anisotropy_level() extern-C wrapper clamps
  to the GL max. Default 4 = the value fast3d already hardcoded => no delta.

- Video.DisplayFPS (D213): PD-parity top-right FPS readout, sampled once per
  emitted frame (optionsoverlay.c fpsTick). Config-only; default 0 => emit
  still returns NULL with the panel closed.

F10 overlay gains 'Anisotropic' and 'FOV scale %' rows (DisplayFPS is file-only
- the panel is at its 13-row layout limit, matching PD).

Build clean (ntsc-final); bunker1 framediff worst_cell 12.219 vs 12.221
baseline = render noise, no-op confirmed. Interactive eyeballs owed (see
docs/HANDOFF.md M-83).

findings.md: D211-D213 rows + index regen. WIDESCREEN-FOV-PLAN Phase 4 and
QOL-INVENTORY rows marked done.
This commit is contained in:
James Dansereau committed 2026-09-09 00:02:47 -04:00
1 parent 8742b93f29
commit 7bcfbf313c
8 files changed
+159 -10

No files matched your search

+63
View File
@@ -916,6 +916,50 @@ bool g_fix_mip_textures = true;
* off. Video.WrapFix = 1. */
bool g_wrap_fix = false;
/* D211 port-only FOV slider. Multiplies the half-angle of every *perspective*
* projection matrix as it is loaded, so it composes with the game's per-frame
* aim-zoom rewrites for free (see docs/dev/WIDESCREEN-FOV-PLAN.md "FOV
* adjustment design"). 1.0 = byte-identical to the original (guarded skip);
* Video.FovScale is a percent, 100 = 1.0. */
static float g_fov_scale = 1.0f;
extern "C" void gfx_set_fov_scale(float s) {
if (!(s > 0.4f) || !(s < 2.01f)) {
s = 1.0f;
}
g_fov_scale = s;
}
/* Widen/narrow a freshly-loaded projection matrix in place. No-op unless the
* matrix is a perspective projection (row-vector guPerspective layout: only
* P[2][3] == -1, P[3][3] == 0 — an ortho matrix has P[3][3] == 1 and is left
* alone so HUD/menu 2D is untouched). Scaling columns 0 and 1 by the same
* factor scales the effective cotangent, i.e. widens H and V FOV uniformly —
* aspect ratio is preserved, no stretch. */
static void gfx_apply_fov_scale(float m[4][4]) {
if (g_fov_scale == 1.0f) {
return;
}
if (!(fabsf(m[3][3]) < 0.01f) || !(fabsf(m[2][3] + 1.0f) < 0.05f)) {
return; /* not a perspective projection */
}
const float cot = m[1][1];
if (!(cot > 1e-4f)) {
return;
}
const float half = atanf(1.0f / cot);
float nhalf = half * g_fov_scale;
const float LIMIT = 1.48f; /* ~85 deg half-angle; keep cot well away from 0 */
if (nhalf > LIMIT) {
nhalf = LIMIT;
}
const float f = tanf(half) / tanf(nhalf);
for (int k = 0; k < 4; k++) {
m[k][0] *= f;
m[k][1] *= f;
}
}
/* D183 source-pitch de-stride (see import_texture). Default on;
* GE_TEXPITCH=0 restores the old flat read for A/B. */
static bool gfx_tex_pitch_fix(void) {
@@ -1212,6 +1256,11 @@ static void gfx_sp_matrix(uint8_t parameters, const int32_t* addr) {
if (parameters & G_MTX_PROJECTION) {
if (parameters & G_MTX_LOAD) {
memcpy(rsp.P_matrix, matrix, sizeof(matrix));
/* D211: apply the FOV slider on the load path only. GE always
* (re)loads its view projection with G_MTX_LOAD (src/fr.c:725), so
* a relative G_MTX_MUL onto an already-scaled matrix never
* happens and cannot compound. */
gfx_apply_fov_scale(rsp.P_matrix);
} else {
gfx_matrix_mul(rsp.P_matrix, matrix, rsp.P_matrix);
}
@@ -3227,6 +3276,20 @@ extern "C" void gfx_set_mipmap_filter(enum MipmapFilteringMode mode) {
}
extern "C" void gfx_set_fix_mip_textures(int on) { g_fix_mip_textures = !!on; }
/* D212: expose the (already-implemented) rendering-API anisotropy hook to the
* port layer. Clamp to [1, GL max] so a stale ini value can't feed an invalid
* GL_TEXTURE_MAX_ANISOTROPY. 1 = isotropic (driver default). */
extern "C" void gfx_set_anisotropy_level(int level) {
reset_texture_state();
int max = gfx_rapi->get_max_anisotropy_level ? gfx_rapi->get_max_anisotropy_level() : 1;
if (max < 1) max = 1;
if (level < 1) level = 1;
if (level > max) level = max;
if (gfx_rapi->set_anisotropy_level) {
gfx_rapi->set_anisotropy_level(level);
}
}
extern "C" void gfx_set_wrap_fix(int on) {
const char* e = getenv("GE_WRAPFIX"); /* RC3 test override */
g_wrap_fix = e ? (atoi(e) != 0) : !!on;