Commit Graph

5 Commits

Author SHA1 Message Date
Matthew Stanley 0792086f6c widescreen: FMV 4:3 pillarbox + menu-2D coherence + HUD edge anchoring
- FMV: 24-bit frames AND streamed 15-bit MDEC video (Tomba plays its
  movies 15-bit; depth24 alone missed them — user-reported) present
  pinned to native 4:3. Detector = colour MDEC decode within the last
  30 vblanks (mdec_recently_active); texture-decompression luma
  decodes do not stamp. Both GL present paths take force_4_3; SW path
  centers a 4:3 dst rect. User-verified on the intro movies.
- Menu garble: full-2D screens (no tagged prims in flight for 2
  frames) now squash lines, polylines, flat rects and mono sprites
  too — dialog borders are drawn with these and stuck out past the
  squashed SPRT boxes. Full-width TILEs (fades) stay exempt; in-game
  non-textured prims stay untouched (may be world-anchored).
- HUD positioning: in-game untagged SPRTs pivot by thirds — outer-
  third elements anchor to their screen edge (16:9 corner position,
  native proportions), middle third to centre. Full-2D screens keep
  the single centre pivot so composites stay coherent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:55:44 -07:00
Matthew Stanley f8a29e8a97 widescreen: [video] aspect_ratio — GTE X-squash + stretched present
Adds the DuckStation-style widescreen hack as a config option,
implemented in the runtime (no regen needed):

- gte.cpp: gte_set_display_aspect(num, den). RTPS/RTPT scale the
  IR1*h/sz screen-X term by (4*den)/(3*num) around the game's own
  OFX projection centre (identity at 4:3). Games cull against the
  SXY values they read back from us, so post-projection screen-
  bounds culls stay aligned with the visible frame.
- present: GL letterbox_rect generalised to the configured aspect
  (gl_renderer_set_display_aspect); SDL_Renderer path gets an
  aspect-derived logical size + dst rect. Window opens at the
  configured aspect (clamp_window_4x3 -> clamp_window_aspect).
- config: [video] aspect_ratio = W:H (4:3 default, 16:9, up
  to 32:9) in game.toml, settings.toml (load+save), launcher seed.

World geometry keeps correct proportions on the stretched present;
screen-space 2D (HUD, FMV, sprite quad widths) stretches — the
known limitation of the squash tier. EXPERIMENTAL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:54:49 -07:00
Matthew Stanley 0631538407 gl-renderer v2: GPU-authoritative VRAM FBO + Beetle-style filtered sampling
Full rewrite of the GL backend around a single authoritative hr VRAM FBO
(RGBA8 + stencil mask-bit mirror, internal scale from [video]
supersampling): GPU fills/copies/draws rasterize into it, CPU uploads
batch through a pending-rect quad blit, a PACK pass re-encodes dirty
regions into the native R16UI raw mirror for texture sampling and
readback, and gpu.c routes GPUREAD/mask-check/peek reads through the
facade so the CPU array syncs before any CPU-side read.

Texture sampling adopts the Beetle-PSX model (structure only, studied
from beetle-psx-libretro):
- Per-primitive uv sampling bounds, clamped in the fragment shader when
  no texture window is active. Filtered neighbours and S>1 interpolation
  overshoot can never read outside the prim's own texture rect. Bounds
  back off the exclusive max edge for axis-aligned (2D) uv mappings and
  widen to the full page when the uv range crosses a 256 wrap boundary.
- Bilinear takes the NEAREST texel as base (cutout + STP authority),
  neighbours toward the sub-texel offset, with per-texel opacity weights
  and renormalised colour. The old "-0.5 then floor" base sampled one
  texel OUTSIDE the prim on top/left edges; where that neighbour was
  empty VRAM (e.g. tiled menu backgrounds with u0=0) the whole edge
  column/row discarded — the vertical line on Tomba's title and the
  cross seams on the memory-card screens.

The software renderer's bilinear gets the same reformulation so GL and
SW render filtered output identically; the SW nearest path (shipping
default) is untouched.

Also: always-on coherency event ring (flushes, fills, copies, draw
bboxes, packs, readbacks, presents, probe perturbations, with rects +
frame numbers) queryable via the debug server, and gl_vram_diff now
forces a full pack so it reads FBO truth even where the raw-mirror
invariant is broken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 05:19:01 -07:00
Matthew Stanley af49fb02d6 gl-renderer: GPU rects/fills/lines + present straight from the FBO
Move the remaining 2D primitives (flat/textured rects, fill, lines) onto
the GPU, and present directly from the FBO color texture cropped to the
display region instead of reading the whole 1 MB FBO back to CPU VRAM
every frame.

Why both at once: the native present read the `vram` array directly and
relied on an incidental software op each frame (the HUD/sprites, drawn
last) to trigger the FBO->CPU sync. Moving every primitive to the GPU
removed that trigger, so a fully-GPU frame left `vram` stale -> black
present (the earlier FMV->black regression). The fix is to present from
the FBO when the GPU holds the freshest frame (gl_renderer_have_gpu_frame)
and to sync explicitly (gl_renderer_sync_cpu) on the software/24-bit path.

Result: the per-frame readback is gone for GPU scenes (the forest-lag
source), software/FMV frames keep the CPU present path (verified: the
MDEC intro renders correctly), and loads are faster.

KNOWN-FOLLOWUP cosmetic artifacts to clean up: the GPU textured shader
doesn't yet implement the texture window (wrap/mask), so tiled/wrapped
sprites show seams; bilinear texture_filtering isn't honored on the GPU
path; and the debug screenshot still reads CPU VRAM (stale under the
FBO-present path) so captures can lag the on-screen image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:39:51 -07:00
Matthew Stanley 1eaee11299 gl-renderer phase 2a: real GL context + VRAM-texture present
The opengl backend now creates a genuine OpenGL context and presents through
it. Verified: Tomba boots and renders gameplay correctly via GL (context
"4.6.0 NVIDIA"), pixel-identical to software.

- gpu_gl_renderer.c: gl_renderer_init_context (SDL_GL context on a
  SDL_WINDOW_OPENGL window), gl_renderer_present (upload ARGB image -> GL
  texture -> full-screen quad -> SDL_GL_SwapWindow), present_blank, shutdown.
  Legacy GL 1.x (immediate mode) so no GL loader needed yet (-lopengl32).
  Backend vtable still delegates rasterization to sw_* this phase, so the GL
  path is pixel-identical — this isolates/validates the context+present.
- main.cpp: window gains SDL_WINDOW_OPENGL when renderer=opengl; GL context
  created with graceful fallback to the SDL_Renderer path on failure; present
  + disabled-frame + cleanup branch on g_gl_active. SW path untouched.
- runtime.cmake: link opengl32 (Win) / OpenGL::GL (other).

Phase 2b (next): move rasterization onto the GPU — VRAM as a GL texture/FBO,
modern-GL loader + shaders, primitives at internal resolution (the actual
scaling-without-perf-loss win).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:34:46 -07:00