Files
psxrecomp/runtime/include/gpu_gl_renderer.h
T
Shokunin 2211ecabf3 GL: true 8K past a 16384 texture limit with a high-resolution window
8K is 18x at a 240-line reference, and a full-VRAM surface at 18x is
18432 px wide: over the 16384 limit of Apple's GL, Intel and MoltenVK. A
title only needs its displayed frame at 18x, so past the full-VRAM limit
the GL backend now splits the work:

- The authoritative VRAM stays the ordinary hr surface at 1x, i.e. the
  native renderer unchanged: pack, CPU readback, render-to-texture and VRAM
  copies are exactly the native results (s_hr_scale; s_scale stays the
  presented scale gr_scale reports).
- A presentation-only surface at S covers the displayed columns W =
  [x0, x1) x all 512 rows (R4: 0..319, 5760x9216 at 18x, 405 MiB). Every
  GPU write touching W is mirrored into it: textured and flat batches,
  lines (quads), fills, uploads, the depth24 clear, and VRAM copies (hi to
  hi when the source is in W, otherwise the 1x source upscaled; both staged
  before drawing so overlapping copies read pre-copy pixels). It keeps its
  own stencil. A separate blit program carries the window projection, so
  the canonical shaders are untouched.
- W grows (64-column aligned) the first time a display rectangle outside it
  is presented, seeded by upscaling the 1x content. Present, hold-last,
  interpolation capture, screenshot_hires and the native-wide centre read
  it; a display it cannot hold presents at 1x.

It engages only when the full-VRAM surface cannot hold the request (driver
limit or budget), sized so a 320-wide window fits both; up to 16x on the
M4 nothing changes. PSX_GL_HIRES_WINDOW=0/1 disables or forces it. The
launcher note and video_info (hr_scale, windowed, hires_window_*, hires_fbo)
report it.

Default-off: at 1x and whenever the full-VRAM surface fits, the paths are
the previous ones (same native-VRAM and present digests on a fixed scene at
1x and 2x).

Tests: gl_scale_invariance_test now also forces the window at 2/3/5/9x and
requires its frame at internal resolution to be byte-identical to the
full-VRAM surface at the same scale (copies inside, into and across the
window edge, fills/uploads across it, mask, all blend modes) with native
VRAM identical to 1x, and runs it at 18x; gl_scale_limits_test covers the
window clamp; guards pin that it only engages past the full-VRAM limit.
Verified in R4 on an M4: the 8K preset renders 5760x4320 in a race (rear-
view mirror present) and 5760x8640 on a 480i menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:52:23 -07:00

281 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#ifndef PSX_GPU_GL_RENDERER_H
#define PSX_GPU_GL_RENDERER_H
/* GL backend context + present entry points, called from main.cpp's window
* setup and present path when [video] renderer = "opengl". The backend's
* rasterization vtable is obtained separately via gl_backend_get()
* (gpu_render.h). SDL_Window is forward-declared (SDL typedefs it from this
* same struct tag) so this header needs no SDL include. */
#include <stdint.h>
struct SDL_Window;
#ifdef __cplusplus
extern "C" {
#endif
/* Create the GL context on a window made with SDL_WINDOW_OPENGL.
* Returns 1 on success, 0 to fall back to the SDL_Renderer present path. */
int gl_renderer_init_context(struct SDL_Window *win);
/* Select a retained immutable bank for the next textured submission; zero
* selects live VRAM. Emulation/GL owning thread only. Returns 0 if unavailable. */
int gl_renderer_select_texture_bank(uint16_t id);
int gl_renderer_texture_banks_supported(void);
/* Set the GL swap interval / vsync mode (1=vsync, 0=immediate, -1=adaptive).
* Safe before or after context creation; applies live when a context exists. */
void gl_renderer_set_swap_interval(int interval);
/* Presentation-only temporal blending. High-refresh sub-presents blend the two
* most recent stable display images on the owning render thread/context; this
* does not generate motion vectors or true intermediate object positions. */
void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz,
double source_hz, int blend_mode);
void gl_renderer_set_interpolation_suspended(int suspended);
int gl_renderer_interpolation_owns_cadence(void);
void gl_renderer_interpolation_diag(int *enabled, int *suspended,
int *history_frames,
double *host_hz, double *target_hz,
uint64_t *swaps);
/* Cumulative CPU-upload diagnostics: calls, rects, pixels, conversion ticks,
* texture-upload ticks, FBO-draw ticks. Active only with PSX_RUNTIME_PERF_DIAG. */
void gl_renderer_runtime_diag(uint64_t out[6]);
/* Present an ARGB8888 image (BGRA byte order) as a letterboxed quad + swap.
* Used for 24-bit (FMV) frames and the PSX_GL_FORCE_CPU_PRESENT diagnostic.
* force_4_3 = pillarbox at native 4:3 even on a wide display aspect (FMVs
* are authored 4:3 and get no GTE squash to compensate the stretch).
* content_w: if 0 < content_w < src_w, only columns [0, content_w) are shown
* (left-aligned in the letterbox; the rest stays cleared black). Used to hide
* a trailing depth24 margin without changing CRTC width / stretching. */
void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear,
int force_4_3, int content_w);
/* Bezel art shown in the letterbox/pillarbox margins. Takes RGBA8 pixels; the
* caller owns them and may free them on return. Passing NULL clears it.
* Returns 0 only if a texture could not be created. */
int gl_renderer_set_bezel(const void *rgba, int w, int h);
int gl_renderer_has_bezel(void);
/* Clear to black + swap (display-disabled frame). */
void gl_renderer_present_blank(void);
/* §33: re-present the last Live frame captured before Swap (or from a VRAM
* snapshot when interpolation owned the last present). Used during rollback
* resim so the window keeps a wall-clock present cadence without reading
* mid-resim VRAM. Returns 1 if a Swap happened, 0 if no hold is available. */
int gl_renderer_present_hold_last(void);
/* Sync the authoritative FBO down to CPU VRAM if the GPU side is ahead (else
* a no-op). Screenshots and the debug server call this before reading CPU
* VRAM. Do NOT use before 24-bit (FMV) scanout — a full readback can clobber
* packed RGB888 MDEC bytes in the CPU mirror (use flush_cpu_uploads instead). */
void gl_renderer_sync_cpu(void);
/* Land pending CPU→VRAM uploads into the FBO without reading the FBO back.
* Safe before 24-bit (FMV) CPU scanout of the mirror. */
void gl_renderer_flush_cpu_uploads(void);
/* Mark the whole display dirty, drop present-path latches, reset frame-
* interpolation history, and force the next several SwapWindow calls even if
* VRAM tiles match the last present. Call after savestate restore so a
* reloaded identical frame still reaches the window (double/triple buffer). */
void gl_renderer_invalidate_present(void);
/* After savestate restore: push the CPU VRAM mirror into the GL FBO. Needed
* when the load happened while GP1 depth24 was on — the normal path skips
* framebuffer-sized uploads, which also skipped the full-VRAM boot_state
* blit and left post-FMV menus without texture pages. */
void gl_renderer_restage_vram_after_savestate(void);
/* Netplay dual-raster: every GP0 also writes software VRAM @ 1× (snaps /
* digests / GPUREAD authority) while the OpenGL hr FBO keeps settings-scale
* SSAA for present-only. Never enables glReadPixels; CPU stays current. */
void gl_renderer_set_cpu_auth_dual(int on);
/* FMV present reconstruction, settings.toml [video] fmv_filter. Takes the
* config enum VIDEO_FMV_FILTER_* (0 nearest, 1 bilinear, 2 sharp, 3 bicubic).
* Only consulted while video antialiasing is on; AA off is always nearest. */
void gl_renderer_set_fmv_filter(int cfg_value);
int gl_renderer_cpu_auth_dual(void);
/* Post-savestate freeze probe: skip/swap/dirty-mark counters (GL present path).
* take() returns deltas since the previous take/reset. Safe no-ops when GL is
* inactive. rect_dirty tests the current present-tile dirty bits. */
void gl_renderer_present_probe_reset(void);
void gl_renderer_present_probe_take(uint64_t *skip_delta, uint64_t *swap_delta,
uint64_t *dirty_mark_delta,
int *force_remaining);
int gl_renderer_present_rect_dirty(int disp_x, int disp_y, int w, int h);
/* THE present path for 15-bit frames: blit the display region straight from
* the authoritative VRAM FBO into a letterboxed rect (no readback).
* Deterministic — used for every 15-bit frame. linear = filter on scale.
* force_4_3 pins to native 4:3 (15-bit MDEC FMV frames on a wide aspect). */
void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
int force_4_3);
/* GPU-direct native-wide present: blit the displayed buffer's wide FBO (key =
* disp_x) straight to the window, no CPU readback. Returns 0 if no wide surface
* exists for disp_x (caller falls back to the CPU readout path). */
int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear);
/* Display aspect for the present letterbox (default 4:3). A wide aspect
* stretches the 4:3 frame; pair with gte_set_display_aspect (cpu_state.h)
* for the widescreen field-of-view hack. */
void gl_renderer_set_display_aspect(int num, int den);
/* Scanline post-process (host display setting). on toggles the effect; strength
* (0..1) is the depth of the dark gap between PS1 scanlines. Applied at the
* native display-line pitch in the present/interpolation shaders, and faded in
* with output scale so it never shimmers on a sub-2x window. gl_renderer_get_
* scanlines returns the on flag and (via out-param) the current strength. */
void gl_renderer_set_scanlines(int on, float strength);
int gl_renderer_get_scanlines(float *strength);
/* Presentation-only gamma adjustment. gamma = 1.0 is the identity; values
* above 1.0 lift shadow detail and values below 1.0 darken it. The adjustment
* is applied once to game content in the final GL presentation pass, including
* temporal interpolation, but not to the bezel, host OSD, black margins, or an
* already-composed hold-last image. Non-finite and out-of-range values are
* clamped to a safe range. Safe to call before GL context creation. */
void gl_renderer_set_post_gamma(float gamma);
float gl_renderer_get_post_gamma(void);
/* Select full native-wide mirror rendering instead of the centre-splice fast
* path. Textured edge expansion needs the complete mirror surface. */
void gl_renderer_set_wide_fast(int on);
/* Internal-resolution scale state of the live GL context (0 before init). */
typedef struct GlScaleInfo {
int requested; /* scale requested before context init (gr_set_scale) */
int effective; /* scale the hr surface was allocated at */
int max_scale; /* largest scale this context could allocate */
int max_dim; /* min(GL_MAX_TEXTURE_SIZE, _RENDERBUFFER_SIZE, _VIEWPORT_DIMS) */
int clamp_reason; /* PSX_GL_SCALE_* mask (gl_scale_limits.h) */
int alloc_retries; /* FBO allocations that failed and stepped the scale down */
int budget_mib; /* memory budget for the hr surface (0 = none) */
int fbo_w, fbo_h; /* hr surface size in pixels */
int drawable_w, drawable_h; /* window drawable in pixels */
/* Windowed high-resolution mode (true 8K on 16384-limit GPUs): the hr
* surface stays at hr_scale (1) and a window of columns [window_x,
* window_x + window_w) is kept at `effective` scale. */
int hr_scale;
int windowed;
int window_x, window_w;
int window_fbo_w, window_fbo_h;
int window_grows;
} GlScaleInfo;
int gl_renderer_scale_info(GlScaleInfo *out);
/* Narrow a native-wide display aspect num:den to the widest whose surface
* this context can allocate at its internal scale (psx_gl_fit_wide_aspect;
* disp_w is the display width the margins derive from). Returns 1 when it
* changed num:den; 0, leaving it alone, before init or with no known limit. */
int gl_renderer_fit_wide_aspect(int disp_w, int *num, int *den);
/* Read the display rect at internal resolution from the hr FBO: (w*S) x (h*S)
* ARGB8888, top row first. Returns the pixel count, 0 if unavailable or larger
* than cap_px. Debug/verification only (screenshot_hires under GL). */
int gl_renderer_read_display_hires(int x, int y, int w, int h, uint32_t *out,
int cap_px, int *ow, int *oh);
void gl_renderer_shutdown(void);
/* Diagnostics (debug server): read GPU-side VRAM without touching the CPU
* array; report coherency flags + dirty rects. fbo_peek returns 0 when the
* GL pipeline is inactive. */
int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out);
void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]);
/* Always-on coherency event ring (debug server "gl_coh_ring"): every upload
* flush, fill, copy, draw bbox, pack, full readback, present, and probe
* perturbation, with rect + frame. An op that flushes internally records its
* own event AFTER the FLUSH it caused (the event after a FLUSH = trigger). */
enum {
GL_COH_FLUSH = 1, /* CPU->FBO upload flush (pending box) */
GL_COH_FILL = 2, /* GP0(02) fill rect */
GL_COH_COPY_SRC = 3, /* GP0(80) copy, source rect */
GL_COH_COPY = 4, /* GP0(80) copy, dest rect */
GL_COH_DRAW = 5, /* drawn prim bbox (clipped to draw area) */
GL_COH_PACK = 6, /* hr FBO -> raw mirror pack (dirty box) */
GL_COH_ENSURE = 7, /* full FBO -> CPU VRAM readback */
GL_COH_PRESENT = 8, /* 15-bit present blit (display rect) */
GL_COH_UPLOAD = 9, /* bulk CPU->VRAM transfer_in dest rect */
GL_COH_PEEK = 10, /* gl_fbo_peek probe (perturbs: flushes) */
GL_COH_DIFF = 11, /* gl_vram_diff probe (perturbs: flushes) */
};
typedef struct {
uint32_t frame;
uint8_t kind;
int16_t x0, y0, x1, y1; /* native VRAM coords, inclusive */
} GlCohEvent;
uint64_t gl_renderer_coh_total(void);
/* Fetch event by absolute sequence number; 0 if evicted or out of range. */
int gl_renderer_coh_get(uint64_t seq, GlCohEvent *out);
/* Always-on present ring (debug server "gl_present_ring"): EVERY SwapWindow —
* including blank (display-disabled) and CPU-quad presents, which the coherency
* ring does not record — with the path taken, source display rect, letterbox
* dest rect, a glGetError sample, wall-clock ms, and a backbuffer pixel sampled
* at the letterbox centre right before the swap (the ground truth for "did this
* swap present black"). */
enum {
GL_PRES_VRAM = 0, /* 15-bit FBO blit present (gl_renderer_present_vram) */
GL_PRES_WIDE = 1, /* native-wide FBO blit present */
GL_PRES_CPU = 2, /* CPU-readout quad present (24-bit FMV / forced) */
GL_PRES_BLANK = 3, /* display-disabled black present */
GL_PRES_INTERP = 4, /* host-refresh interpolation sub-present */
};
typedef struct {
uint32_t frame; /* s_frame_count at swap */
uint32_t t_ms; /* SDL_GetTicks() at swap */
uint8_t path; /* GL_PRES_* */
uint8_t px_r, px_g, px_b; /* backbuffer sample at letterbox centre */
uint16_t glerr; /* glGetError() drained just before the swap */
int16_t dx, dy, w, h; /* source display rect (native px; 0 for blank) */
int16_t lx, ly, lw, lh; /* letterbox dest rect (window px) */
uint8_t src_r, src_g, src_b, src_valid; /* blit SOURCE (hr FBO) sample
* at the display-rect centre */
} GlPresEvent;
uint64_t gl_renderer_pres_total(void);
int gl_renderer_pres_get(uint64_t seq, GlPresEvent *out);
/* frame_perf: aggregate the per-frame GPU/CPU phase-timing ring (debug server
* "frame_perf"). wide_filter: -1 = all frames, 0 = 4:3 present, 1 = native-wide.
* Fills out[13]: [0]=count, [1]=total_ms avg, [2]=total_ms max, [3]=emu_cpu_ms avg
* (frame minus the present call), [4]=present_wall_ms avg, [5]=scene_gpu_ms avg,
* [6]=scene_gpu_ms max, [7]=present_gpu_ms avg, [8]=present_gpu_ms max,
* [9]=scene primitives/frame avg (pre double-draw), [10]=mirror_gpu_ms avg (of
* scene_gpu, the native-wide mirror passes; GL_TIMESTAMP pairs), [11]=mirror_gpu_ms
* max, [12]=mirror passes/frame avg, [13]=CPU wall in flush_tex_batch avg,
* [14]=CPU wall in glb_wide_* avg, [15]=batches/frame avg, [16]=wide target
* sets/frame avg, [17]=wide FBO creations/frame avg. GPU phases are true
* GL_TIME_ELAPSED times (CPU-overhead independent). Returns the count. */
int gl_renderer_perf_aggregate(int wide_filter, double out[18]);
/* Native-wide mirror ablation (perf attribution, debug cmd gl_ws_ablate):
* 0 = normal, 1 = skip the whole mirror pass (incl. wide_clear), 2 = full mirror
* state churn without the draw calls, 3 = mirror draws stay on the hr FBO (no
* per-pass FBO rebind; diagnostic only — corrupts both surfaces' content). */
void gl_renderer_set_ws_ablate(int mode);
int gl_renderer_get_ws_ablate(void);
/* Cumulative textured fraction of scene primitives since boot (flat vs textured
* batching decision). Sets *out_tex_frac; returns total prim count. */
uint64_t gl_renderer_perf_prim_split(double *out_tex_frac);
/* Cumulative textured-batch diagnostics: total, then flushes caused by
* isolation, blend-mode, mask, filter, backdrop-gate, texture-window, capacity. */
void gl_renderer_batch_diag(uint64_t out[8]);
#ifdef __cplusplus
}
#endif
#endif /* PSX_GPU_GL_RENDERER_H */