From 97f58e19d77d58a7aa1cd13d9343245095a9c349 Mon Sep 17 00:00:00 2001 From: Kevin <9866117+kerokline@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:41:55 -0400 Subject: [PATCH 1/3] runtime: present-time scanline post-process (host display enhancement) Adds an optional scanline effect to the GL present path: a soft sinusoidal beam pitched to the PS1 display-line grid (one dark line per native scanline), applied in both the present and interpolation shaders and gated by output scale so it never shimmers below ~2x. Orthogonal to the crt_filter colour LUT (the two compose). Controls: [video] scanlines / scanline_strength (game.toml + settings.toml), PSX_SCANLINES / PSX_SCANLINE_STRENGTH env overrides, an F6 hotkey (HOST_KEYMAP_SCANLINES, rebindable via [KeyMap] Scanlines), and a `scanline` TCP command for live A/B. Launcher Display-card wiring follows separately. Co-Authored-By: Claude Opus 4.8 --- TCP_COMMANDS.md | 2 + recompiler/src/config_loader.cpp | 18 ++++ recompiler/src/config_loader.h | 11 +++ runtime/include/gpu_gl_renderer.h | 8 ++ runtime/include/host_keymap.h | 1 + runtime/src/debug_server.c | 20 ++++ runtime/src/gpu_gl_renderer.c | 153 +++++++++++++++++++++++++++--- runtime/src/host_keymap.c | 3 + runtime/src/main.cpp | 50 ++++++++++ 9 files changed, 252 insertions(+), 14 deletions(-) diff --git a/TCP_COMMANDS.md b/TCP_COMMANDS.md index 6d1caef20..5fa1b328a 100644 --- a/TCP_COMMANDS.md +++ b/TCP_COMMANDS.md @@ -79,6 +79,7 @@ Columns: **N** = native, **D** = DuckStation oracle. | `set_snapshot` | ✓ | ✓ | `slot`, `addr`, `size` | Configure per-frame RAM snapshot region (slots 0-3) | | `get_snapshots` | ✓ | ✓ | — | Show snapshot config | | `screenshot` | ✓ | ✓ | `path` (optional) | Write a **PNG** of the current display to `path` (default `psx_screenshot.png` in the runtime cwd); single metadata response `{path,width,height}`. `screenshot_file` is an alias; the old inline-hex-row `screenshot` is gone (it streamed h+1 response lines per request and poisoned the connection) | +| `scanline` | ✓ | | optional `on` (0/1), optional `pct` (0..100) | Live A/B toggle for the present-time scanline post-process. Omit `on` to keep the current toggle, omit `pct` to keep the current strength; reports `{scanlines, strength_pct}`. Same state the F6 hotkey and `[video] scanlines` drive | | `first_failure` | ✓ | | — | Find first divergence point between runs (native-side tracking) | | `read_frame_ram` | ✓ | | `addr`, `len`, `frame` | Read RAM **as of a specific frame** (from ring buffer) | | `wtrace_range` | ✓ | | `lo`, `hi` | Set RAM-write trace range (ring of 262 144 writes with RA — `WRITE_TRACE_CAP`, `1 << 18`) | @@ -490,6 +491,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `run_to_frame` | ✓ | | ✓ | | `s3_smear_watch` | ✓ | | ✓ | | `savestate` | ✓ | | | +| `scanline` | ✓ | | | | `screenshot` | ✓ | ✓ | ✓ | | `screenshot_file` | ✓ | ✓ | ✓ | | `screenshot_hires` | ✓ | | ✓ | diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index ce4dc10eb..186770bfb 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -635,6 +635,15 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& "[video] crt_filter must be \"raw\"|\"crt\"|\"composite\"|\"trinitron\": {}", mode)); } + if (video.contains("scanlines")) { + rt.video_scanlines = toml::find(video, "scanlines"); + } + if (video.contains("scanline_strength")) { + double s = toml::find(video, "scanline_strength"); + if (s < 0.0) s = 0.0; + if (s > 1.0) s = 1.0; + rt.video_scanline_strength = s; + } if (video.contains("auto_skip_fmv")) { rt.video_auto_skip_fmv = toml::find(video, "auto_skip_fmv"); } @@ -2296,6 +2305,15 @@ UserSettings load_user_settings(const fs::path& path) { else if (m == "composite") { s.screen_kind = 2; s.has_screen_kind = true; } else if (m == "trinitron") { s.screen_kind = 3; s.has_screen_kind = true; } }); + if (v.contains("scanlines")) try_get([&]{ + s.scanlines = toml::find(v, "scanlines"); s.has_scanlines = true; + }); + if (v.contains("scanline_strength")) try_get([&]{ + double d = toml::find(v, "scanline_strength"); + if (d < 0.0) d = 0.0; + if (d > 1.0) d = 1.0; + s.scanline_strength = d; s.has_scanline_strength = true; + }); if (v.contains("auto_skip_fmv")) try_get([&]{ s.auto_skip_fmv = toml::find(v, "auto_skip_fmv"); s.has_auto_skip_fmv = true; }); diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 41724162b..d979d59d3 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -488,6 +488,13 @@ struct RuntimeConfig { // PSX_SCREEN env var overrides this at runtime (debug path). int video_screen_kind = 0; + // scanlines: present-time horizontal scanline darkening (host enhancement, + // orthogonal to crt_filter's colour LUT — the two compose). Off by default; + // scanline_strength 0..1 is the dark-gap depth. PSX_SCANLINES / + // PSX_SCANLINE_STRENGTH override at runtime (debug path). + bool video_scanlines = false; + double video_scanline_strength = 0.5; + // auto_skip_fmv: when true, full-motion videos (streaming XA audio + MDEC // video) are skipped the instant they're detected — presentation + pacing are // suppressed and audio muted for the duration, so an FMV ends in a fraction of @@ -1196,6 +1203,10 @@ struct UserSettings { bool has_geometry_correction = false; bool geometry_correction = false; bool has_perspective_texturing = false; bool perspective_texturing = false; bool has_screen_kind = false; int screen_kind = 0; // 0..3 (ScreenKind) + // Scanline post-process (see RuntimeConfig::video_scanlines). Strength stored + // 0..1; the launcher ABI carries it as an integer percent. + bool has_scanlines = false; bool scanlines = false; + bool has_scanline_strength = false; double scanline_strength = 0.5; bool has_auto_skip_fmv = false; bool auto_skip_fmv = false; // skip FMVs // [video] turbo_loads: DEPRECATED AND IGNORED — the legacy home of the // generic Turbo loads switch, back when the launcher drew a row for it. diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 9f64b4aba..26aa54170 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -122,6 +122,14 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear) * 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); + /* 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); diff --git a/runtime/include/host_keymap.h b/runtime/include/host_keymap.h index dd077093e..c11862d2e 100644 --- a/runtime/include/host_keymap.h +++ b/runtime/include/host_keymap.h @@ -21,6 +21,7 @@ typedef enum HostKeymapAction { HOST_KEYMAP_DISPLAY_PERF, HOST_KEYMAP_REWIND, /* default F8 */ HOST_KEYMAP_SAVE_STATE_MENU, /* default F7 */ + HOST_KEYMAP_SCANLINES, /* default F6 */ HOST_KEYMAP_ACTION_COUNT } HostKeymapAction; diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 5c2278f09..0f5fa5d2f 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -7846,6 +7846,25 @@ static void handle_ws_nw(int id, const char *json) id, psx_ws_get_native_wide(), ws.mode, ws.nw_extra); } +/* Live scanline post-process toggle (A/B): `scanline on=<0|1> pct=<0..100>`. + * Either field is optional — omit `on` to keep the current toggle, omit `pct` + * to keep the current strength. Reports the resulting state. */ +extern void psx_video_set_scanlines(int on, float strength); +extern int psx_video_get_scanlines(float *strength); +static void handle_scanline(int id, const char *json) +{ + int on = json_get_int(json, "on", -1); + int pct = json_get_int(json, "pct", -1); + float cur = 0.f; + int cur_on = psx_video_get_scanlines(&cur); + int new_on = (on >= 0) ? (on ? 1 : 0) : cur_on; + float new_s = (pct >= 0 && pct <= 100) ? (float)pct / 100.f : cur; + psx_video_set_scanlines(new_on, new_s); + cur_on = psx_video_get_scanlines(&cur); + send_fmt("{\"id\":%d,\"ok\":true,\"scanlines\":%d,\"strength_pct\":%d}", + id, cur_on, (int)(cur * 100.f + 0.5f)); +} + /* ws_backdrop_ring: dump the always-on auto_backdrop rewrite ring (which windows * fire, live extent/camera/DL-count, orig vs final bound). Read-only; small * heap envelope so the per-byte stall of a giant read is never in play. */ @@ -13531,6 +13550,7 @@ static const CmdEntry s_commands[] = { { "kernel_bless", handle_kernel_bless }, { "ws_aspect", handle_ws_aspect }, { "ws_nw", handle_ws_nw }, + { "scanline", handle_scanline }, { "ws_backdrop_ring", handle_ws_backdrop_ring }, { "ws_ui_groups", handle_ws_ui_groups }, { "ws_backdrop_margin", handle_ws_backdrop_margin }, diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 31c8346ec..f9e758a38 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -319,10 +319,20 @@ static void gl_swap_with_osd(void); static GLint s_present_uTex = -1, s_present_uUvRect = -1; static GLint s_present_uTexSize = -1, s_present_uSharpScale = -1; static GLint s_present_uSharp = -1; +/* Scanline post-process state (host display setting; see gl_renderer_set_scanlines). + * s_scanline_on/strength are pushed to whichever program draws game content; + * OSD/bezel and the already-composed hold-last re-present force it off so the + * effect is applied exactly once, at native-line pitch. */ +static int s_scanline_on = 0; +static float s_scanline_strength = 0.5f; +static GLint s_present_uScanline = -1, s_present_uScanStrength = -1; +static GLint s_present_uScanLines = -1, s_present_uScanScale = -1; static GLuint s_interp_prog = 0, s_interp_tex[3]; static GLint s_interp_uPrev = -1, s_interp_uCurr = -1; static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1; static GLint s_interp_uBlendMode = -1; +static GLint s_interp_uScanline = -1, s_interp_uScanStrength = -1; +static GLint s_interp_uScanLines = -1, s_interp_uScanScale = -1; static int s_interp_enabled = 0, s_interp_valid = 0; static int s_interp_suspended = 0; static int s_interp_blend_mode = 0; @@ -874,6 +884,30 @@ static const char *PRESENT_VS = * there. The result is clamped to u_uv_rect so the half-texel edge inset the * caller applied still holds — that inset is what keeps LINEAR from bleeding * the border texel into the image (the old reason FMV was pinned to NEAREST). */ +/* Scanline post-process, shared by the present and interpolation shaders. + * Darkens toward the gap between PS1 scanlines with a soft sinusoidal beam + * centred on each line. The pitch is u_scanline_lines (the number of native + * display lines across the source rect), so it tracks the PS1 line grid, not + * the internal SSAA scale or the window size. u_scanline_scale is output pixels + * per PS1 line; the gate fades the effect in from 1x to 2x so it never shimmers + * on a window too small to resolve one line as two rows (Nyquist). Computed + * against v_uv.y (position across the display region) so it is orientation- and + * scale-agnostic. */ +#define PSX_SCANLINE_UNIFORMS \ + "uniform int u_scanline;\n" \ + "uniform float u_scanline_strength;\n" \ + "uniform float u_scanline_lines;\n" \ + "uniform float u_scanline_scale;\n" +#define PSX_SCANLINE_FUNC \ + "vec3 psx_scanline(vec3 rgb, float vy){\n" \ + " if (u_scanline == 0) return rgb;\n" \ + " float gate = clamp(u_scanline_scale - 1.0, 0.0, 1.0);\n" \ + " if (gate <= 0.0) return rgb;\n" \ + " float p = fract(vy * u_scanline_lines);\n" \ + " float beam = sin(3.14159265 * p);\n" \ + " float mult = 1.0 - (u_scanline_strength * gate) * (1.0 - beam);\n" \ + " return rgb * mult;\n" \ + "}\n" static const char *PRESENT_FS = "#version 330\n" "in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n" @@ -881,6 +915,7 @@ static const char *PRESENT_FS = "uniform vec2 u_tex_size;\n" "uniform vec2 u_sharp_scale;\n" "uniform int u_sharp;\n" + PSX_SCANLINE_UNIFORMS /* Catmull-Rom bicubic via 9 bilinear taps. Sharper than plain bilinear at * the same smoothness, with mild overshoot that reads as edge definition. * The present texture holds exactly the source rect and wraps CLAMP_TO_EDGE, @@ -909,27 +944,35 @@ static const char *PRESENT_FS = " r += texture(u_tex, vec2(t3.x , t3.y )) * (w3.x * w3.y );\n" " return r;\n" "}\n" + PSX_SCANLINE_FUNC "void main(){\n" " vec2 uv = v_uv;\n" - " if (u_sharp == 2) { frag = bicubic(uv); return; }\n" - " if (u_sharp == 1) {\n" - " vec2 scale = max(u_sharp_scale, vec2(1.0));\n" - " vec2 texel = uv * u_tex_size;\n" - " vec2 tf = floor(texel);\n" - " vec2 cd = (texel - tf) - 0.5;\n" - " vec2 band = 0.5 - 0.5 / scale;\n" - " vec2 f = (cd - clamp(cd, -band, band)) * scale + 0.5;\n" - " uv = (tf + f) / u_tex_size;\n" - " vec2 lo = min(u_uv_rect.xy, u_uv_rect.zw);\n" - " vec2 hi = max(u_uv_rect.xy, u_uv_rect.zw);\n" - " uv = clamp(uv, lo, hi);\n" + " vec4 c;\n" + " if (u_sharp == 2) { c = bicubic(uv); }\n" + " else {\n" + " if (u_sharp == 1) {\n" + " vec2 scale = max(u_sharp_scale, vec2(1.0));\n" + " vec2 texel = uv * u_tex_size;\n" + " vec2 tf = floor(texel);\n" + " vec2 cd = (texel - tf) - 0.5;\n" + " vec2 band = 0.5 - 0.5 / scale;\n" + " vec2 f = (cd - clamp(cd, -band, band)) * scale + 0.5;\n" + " uv = (tf + f) / u_tex_size;\n" + " vec2 lo = min(u_uv_rect.xy, u_uv_rect.zw);\n" + " vec2 hi = max(u_uv_rect.xy, u_uv_rect.zw);\n" + " uv = clamp(uv, lo, hi);\n" + " }\n" + " c = texture(u_tex, uv);\n" " }\n" - " frag = texture(u_tex, uv);\n" + " c.rgb = psx_scanline(c.rgb, v_uv.y);\n" + " frag = c;\n" "}\n"; static const char *INTERP_FS = "#version 330\n" "in vec2 v_uv; uniform sampler2D u_prev; uniform sampler2D u_curr;\n" "uniform float u_alpha; uniform int u_blend_mode; out vec4 frag;\n" + PSX_SCANLINE_UNIFORMS + PSX_SCANLINE_FUNC "void main(){\n" " vec4 prev=texture(u_prev,v_uv), curr=texture(u_curr,v_uv);\n" " float alpha=u_alpha;\n" @@ -939,7 +982,9 @@ static const char *INTERP_FS = " float safe_blend=1.0-smoothstep(0.08,0.20,change);\n" " alpha=mix(step(0.5,u_alpha),u_alpha,safe_blend);\n" " }\n" - " frag=mix(prev,curr,alpha);\n" + " vec4 c=mix(prev,curr,alpha);\n" + " c.rgb=psx_scanline(c.rgb, v_uv.y);\n" + " frag=c;\n" "}\n"; /* Geometry: position in VRAM pixels (draw offset already applied by gpu.c), @@ -2597,6 +2642,40 @@ static void present_set_sharp(int mode, int tex_w, int tex_h, (float)out_h / (float)tex_h); } +/* Push scanline uniforms into the currently-bound present or interpolation + * program. + * pitch_lines = the height of the TEXTURE that v_uv is normalized against + * (v_uv.y * pitch_lines is the texel-row coordinate, and one + * source texel row is one PS1 scanline). For the VRAM/FBO quad + * that is VRAM_H (v_uv spans only the display sub-range of the + * 512-row texture); for the CPU/interp textures, which hold + * exactly the display rect, it equals the display height. + * disp_lines = PS1 display lines actually shown, for the output-scale gate. + * out_h = letterbox height in window px. + * Pass pitch_lines/disp_lines <= 0 (OSD, bezel, or the already-composed + * hold-last drawable) to force the effect off — program uniforms persist, so + * every content draw must state its own choice. */ +static void present_set_scanline(GLint uOn, GLint uStr, GLint uLines, + GLint uScale, int pitch_lines, int disp_lines, + int out_h) { + if (uOn < 0) return; + int on = (s_scanline_on && pitch_lines > 0 && disp_lines > 0 && out_h > 0) + ? 1 : 0; + p_glUniform1i(uOn, on); + if (!on) return; + if (uStr >= 0) p_glUniform1f(uStr, s_scanline_strength); + if (uLines >= 0) p_glUniform1f(uLines, (float)pitch_lines); + if (uScale >= 0) p_glUniform1f(uScale, (float)out_h / (float)disp_lines); +} +#define PRESENT_SCANLINE(pitch, disp, oh) \ + present_set_scanline(s_present_uScanline, s_present_uScanStrength, \ + s_present_uScanLines, s_present_uScanScale, \ + (pitch), (disp), (oh)) +#define INTERP_SCANLINE(pitch, disp, oh) \ + present_set_scanline(s_interp_uScanline, s_interp_uScanStrength, \ + s_interp_uScanLines, s_interp_uScanScale, \ + (pitch), (disp), (oh)) + /* Display aspect for the present letterbox. Default 4:3 (native). When a wide * aspect is configured the 4:3 frame is stretched into it — paired with the * GTE X-squash (gte_set_display_aspect) this nets a wider field of view. */ @@ -2607,6 +2686,22 @@ void gl_renderer_set_display_aspect(int num, int den) { s_aspect_num = num; s_aspect_den = den; } +/* Scanline post-process toggle (host display setting). strength is the depth of + * the dark gap between lines, 0..1 (0 = off-looking, 1 = fully black gap). The + * actual darkening is applied per-draw in the present/interpolation shaders and + * fades in with output scale — see PSX_SCANLINE_FUNC. */ +void gl_renderer_set_scanlines(int on, float strength) { + s_scanline_on = on ? 1 : 0; + if (strength < 0.f) strength = 0.f; + if (strength > 1.f) strength = 1.f; + s_scanline_strength = strength; +} + +int gl_renderer_get_scanlines(float *strength) { + if (strength) *strength = s_scanline_strength; + return s_scanline_on; +} + /* Letterbox: largest num:den rect centered in the drawable. */ static void letterbox_rect_aspect(int ww, int wh, int num, int den, int *x, int *y, int *w, int *h) { @@ -2867,6 +2962,22 @@ int gl_renderer_init_context(SDL_Window *win) { p_glGetUniformLocation(s_present_prog, "u_sharp_scale"); s_present_uSharp = p_glGetUniformLocation(s_present_prog, "u_sharp"); + s_present_uScanline = + p_glGetUniformLocation(s_present_prog, "u_scanline"); + s_present_uScanStrength = + p_glGetUniformLocation(s_present_prog, "u_scanline_strength"); + s_present_uScanLines = + p_glGetUniformLocation(s_present_prog, "u_scanline_lines"); + s_present_uScanScale = + p_glGetUniformLocation(s_present_prog, "u_scanline_scale"); + s_interp_uScanline = + p_glGetUniformLocation(s_interp_prog, "u_scanline"); + s_interp_uScanStrength = + p_glGetUniformLocation(s_interp_prog, "u_scanline_strength"); + s_interp_uScanLines = + p_glGetUniformLocation(s_interp_prog, "u_scanline_lines"); + s_interp_uScanScale = + p_glGetUniformLocation(s_interp_prog, "u_scanline_scale"); s_interp_uPrev = p_glGetUniformLocation(s_interp_prog, "u_prev"); s_interp_uCurr = p_glGetUniformLocation(s_interp_prog, "u_curr"); s_interp_uAlpha = p_glGetUniformLocation(s_interp_prog, "u_alpha"); @@ -2994,6 +3105,9 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea upload_present_tex(pixels, src_w, src_h, filt_mode >= 0 ? 1 : 0); p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); present_set_sharp(filt_mode, src_w, src_h, lw, lh); + /* CPU present texture holds exactly the display rect, so v_uv spans it and + * pitch == display height == src_h. */ + PRESENT_SCANLINE(src_h, src_h, lh); if (crop) { /* Cropped present keeps left-aligned content; still inset so linear * AA does not blend the cut column with undefined border texels. */ @@ -3820,6 +3934,9 @@ static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh) { p_glUniform1f(s_interp_uAlpha, alpha); p_glUniform1i(s_interp_uBlendMode, s_interp_blend_mode); p_glUniform4f(s_interp_uUvRect, 0.f, 0.f, 1.f, 1.f); + /* Interp textures hold exactly the display rect (uv_rect is 0..1), so pitch + * == display height == s_interp_h. */ + INTERP_SCANLINE(s_interp_h, s_interp_h, lh); p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3); p_glBindVertexArray(0); @@ -3946,6 +4063,7 @@ static void gl_draw_osd_image(const uint32_t *px, int ow, int oh, p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); present_set_sharp(0, 0, 0, 0, 0); /* OSD is authored at output res */ + PRESENT_SCANLINE(0, 0, 0); /* never scanline the host OSD */ /* Host OSD bitmaps are top-down (row 0 = top), same as guest CPU * present with v_flip=1: uv (0,0)-(1,1). (0,1)-(1,0) was the hold-last * cancel for already-oriented captures and made toasts upside-down. */ @@ -4049,6 +4167,12 @@ static void present_target_quad(GLuint tex, int tex_w, int tex_h, /* The rasterized path already renders at the internal scale, so it has no * low-res source to reconstruct — keep the plain sample. */ present_set_sharp(0, 0, 0, 0, 0); + /* Scanline at the native line grid. v_uv is normalized against tex_h (the + * full VRAM/FBO texture), and one texel row is one PS1 scanline, so tex_h is + * the phase pitch; h is the displayed line count for the output-scale gate. + * v_flip=0 is the already-composed hold-last drawable (scanlines, if any, + * are already baked) — skip it. */ + PRESENT_SCANLINE(v_flip ? tex_h : 0, v_flip ? h : 0, lh); /* Half-texel inset: with GL_LINEAR, corner-mapped UVs make the outermost * dest pixels blend the border texel with VRAM outside the content rect * (visible edge stripe with AA on). Center-mapped UVs keep edge samples @@ -4113,6 +4237,7 @@ static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh) { p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); p_glUniform4f(s_present_uUvRect, 0.0f, 0.0f, 1.0f, 1.0f); + PRESENT_SCANLINE(0, 0, 0); /* bezel art is not scanlined */ p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3); p_glBindVertexArray(0); diff --git a/runtime/src/host_keymap.c b/runtime/src/host_keymap.c index 42263a760..848309e20 100644 --- a/runtime/src/host_keymap.c +++ b/runtime/src/host_keymap.c @@ -97,6 +97,8 @@ static void apply_defaults(void) { #endif if (s_actions[HOST_KEYMAP_SAVE_STATE_MENU].count == 0) add_bind(HOST_KEYMAP_SAVE_STATE_MENU, (int)SDLK_F7, (int)SDL_SCANCODE_F7, 0); + if (s_actions[HOST_KEYMAP_SCANLINES].count == 0) + add_bind(HOST_KEYMAP_SCANLINES, (int)SDLK_F6, (int)SDL_SCANCODE_F6, 0); } /* Parse one "Ctrl+Alt+PageUp" token into key+mods. */ @@ -154,6 +156,7 @@ static HostKeymapAction action_for_key(const char *name) { if (ieq(name, "DisplayPerf")) return HOST_KEYMAP_DISPLAY_PERF; if (ieq(name, "Rewind")) return HOST_KEYMAP_REWIND; if (ieq(name, "SaveStateMenu")) return HOST_KEYMAP_SAVE_STATE_MENU; + if (ieq(name, "Scanlines")) return HOST_KEYMAP_SCANLINES; return HOST_KEYMAP_ACTION_COUNT; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 0ed4124f1..83cafcadf 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1157,6 +1157,27 @@ static bool g_video_aa = true; /* linear present filtering */ /* FMV present reconstruction (VIDEO_FMV_FILTER_*), pushed to the GL renderer * once the config is resolved. Only consulted while g_video_aa is on. */ static int g_video_fmv_filter = PSXRecompV4::VIDEO_FMV_FILTER_DEFAULT; +/* Scanline post-process (host display enhancement). Off by default; toggled by + * the launcher Display card, the PSX_SCANLINES env override, the F6 hotkey, or + * the `scanline` TCP command. Strength 0..1 is the dark-gap depth. Pushed to the + * GL renderer each present alongside the FMV filter. */ +static bool g_video_scanlines = false; +static float g_video_scanline_strength = 0.5f; + +/* Single point that changes scanline state: keeps the g_video_* mirror (used by + * the hotkey and startup banner) in lockstep with the GL renderer, so the F6 + * hotkey, the PSX_SCANLINES env override, and the `scanline` TCP command can be + * mixed without drifting. Declared extern "C" so debug_server.c can call it. */ +extern "C" void psx_video_set_scanlines(int on, float strength) { + g_video_scanlines = on ? true : false; + if (strength >= 0.f && strength <= 1.f) g_video_scanline_strength = strength; + gl_renderer_set_scanlines(g_video_scanlines ? 1 : 0, + g_video_scanline_strength); +} +extern "C" int psx_video_get_scanlines(float *strength) { + if (strength) *strength = g_video_scanline_strength; + return g_video_scanlines ? 1 : 0; +} /* recomp-ui stores this 1-based so a zero-initialized (older) host reads as * "unset" rather than pinning nearest; the config enum is 0-based. Convert at @@ -6530,6 +6551,17 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { (int)mod)) { fps_telemetry_toggle(); } + else if (!key_repeat && + host_keymap_match_event(HOST_KEYMAP_SCANLINES, + (int)key, (int)scancode, + (int)mod)) { + psx_video_set_scanlines(g_video_scanlines ? 0 : 1, + g_video_scanline_strength); + char msg[48]; + std::snprintf(msg, sizeof(msg), "Scanlines %s", + g_video_scanlines ? "on" : "off"); + host_osd_push(msg, 1200); + } /* Host volume: config.ini [KeyMap] VolumeUp/VolumeDown * (defaults: keypad +/-). 5% steps; shows right-side bar. */ else if (host_keymap_match_event(HOST_KEYMAP_VOLUME_UP, @@ -11261,6 +11293,9 @@ int main(int argc, char** argv) { g_video_pgxp_tolerance = (float)gc.runtime.video_pgxp_tolerance; g_video_renderer = gc.runtime.video_renderer; g_video_screen = gc.runtime.video_screen_kind; + g_video_scanlines = gc.runtime.video_scanlines; + g_video_scanline_strength = + (float)gc.runtime.video_scanline_strength; g_video_aspect_num = gc.runtime.video_aspect_num; g_video_aspect_den = gc.runtime.video_aspect_den; g_low_latency_input = gc.runtime.video_low_latency_input ? 1 : 0; @@ -11650,6 +11685,9 @@ int main(int argc, char** argv) { if (us.has_perspective_texturing) g_video_perspective_texturing = us.perspective_texturing ? 1 : 0; if (us.has_screen_kind) g_video_screen = us.screen_kind; + if (us.has_scanlines) g_video_scanlines = us.scanlines; + if (us.has_scanline_strength) + g_video_scanline_strength = (float)us.scanline_strength; if (us.has_auto_skip_fmv) g_auto_skip_fmv = us.auto_skip_fmv ? 1 : 0; /* turbo_loads is deliberately NOT restored from settings.toml. It is a * write-only latch: the launcher stopped drawing a Turbo loads row when @@ -13181,6 +13219,18 @@ int main(int argc, char** argv) { gpu_texture_correction_set(g_video_perspective_texturing); pgxp_set_cpu_mode(g_video_pgxp_cpu_mode); pgxp_set_tolerance(g_video_pgxp_tolerance); + /* Scanlines: env override wins over config, same as the corrections above, + * so a headless/free-run boot can be captured with the effect armed from the + * first present. PSX_SCANLINES=0/1; PSX_SCANLINE_STRENGTH=0..1. Pushed to the + * GL renderer, which holds the state and applies it per-draw. */ + if (const char* e = std::getenv("PSX_SCANLINES")) + g_video_scanlines = (*e && *e != '0'); + if (const char* e = std::getenv("PSX_SCANLINE_STRENGTH")) { + float s = (float)atof(e); + if (s >= 0.f && s <= 1.f) g_video_scanline_strength = s; + } + gl_renderer_set_scanlines(g_video_scanlines ? 1 : 0, + g_video_scanline_strength); if (g_video_geometry_correction || g_video_perspective_texturing) { std::fprintf(stdout, "psxrecomp: geometry correction %s, perspective texturing %s%s\n", From 92e5f4b2e2a7f5ee2f43d76a2d49ebd8432c6aa3 Mon Sep 17 00:00:00 2001 From: Kevin <9866117+kerokline@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:52:46 -0400 Subject: [PATCH 2/3] runtime: wire scanlines through the launcher + settings persistence Threads Settings.scanlines / scanline_strength_pct from the recomp-ui launcher Display card through the same paths as screen_kind: seed the launcher from the current state, apply the player's choice back to the video globals (pushing it to the GL renderer immediately), and persist it to settings.toml. All launcher struct accesses are gated on RECOMP_LAUNCHER_HAS_SCANLINES so the runtime still builds against an older recomp-ui. Co-Authored-By: Claude Opus 4.8 --- recompiler/src/config_loader.cpp | 4 ++++ runtime/src/main.cpp | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 186770bfb..6ddffea9a 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -2633,6 +2633,10 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) { : s.screen_kind == 3 ? "trinitron" : "raw"; f << "crt_filter = \"" << k << "\"\n"; } + if (s.has_scanlines) + f << "scanlines = " << (s.scanlines ? "true" : "false") << "\n"; + if (s.has_scanline_strength) + f << "scanline_strength = " << s.scanline_strength << "\n"; if (s.has_auto_skip_fmv) f << "auto_skip_fmv = " << (s.auto_skip_fmv ? "true" : "false") << "\n"; /* turbo_loads is deliberately NOT written back: it is deprecated and no diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 83cafcadf..4b26eed6f 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -12186,6 +12186,9 @@ int main(int argc, char** argv) { seed.perspective_texturing = (g_video_perspective_texturing != 0); seed.has_perspective_texturing = true; seed.screen_kind = g_video_screen; seed.has_screen_kind = true; + seed.scanlines = g_video_scanlines; seed.has_scanlines = true; + seed.scanline_strength = g_video_scanline_strength; + seed.has_scanline_strength = true; seed.auto_skip_fmv = (g_auto_skip_fmv != 0); seed.has_auto_skip_fmv = skip_fmv_offered; seed.turbo_loads = (g_turbo_loads_enabled != 0); @@ -12371,6 +12374,11 @@ int main(int argc, char** argv) { ls.geometry_correction = seed.geometry_correction ? 1 : 0; ls.perspective_texturing = seed.perspective_texturing ? 1 : 0; ls.screen_kind = seed.screen_kind; +#if defined(RECOMP_LAUNCHER_HAS_SCANLINES) + ls.scanlines = seed.scanlines ? 1 : 0; + ls.scanline_strength_pct = seed.has_scanline_strength + ? (int)(seed.scanline_strength * 100.0 + 0.5) : 50; +#endif ls.frame_interp = seed.frame_interpolation ? 1 : 0; ls.frame_interp_fps = seed.frame_interpolation_fps; ls.spu_hq = seed.spu_hq ? 1 : 0; @@ -12647,6 +12655,13 @@ int main(int argc, char** argv) { seed.fmv_filter = launcher_fmv_filter_to_cfg(ls.fmv_filter); seed.has_fmv_filter = true; seed.screen_kind = ls.screen_kind; seed.has_screen_kind = true; +#if defined(RECOMP_LAUNCHER_HAS_SCANLINES) + seed.scanlines = ls.scanlines != 0; seed.has_scanlines = true; + if (ls.scanline_strength_pct > 0) { + seed.scanline_strength = ls.scanline_strength_pct / 100.0; + seed.has_scanline_strength = true; + } +#endif seed.frame_interpolation = ls.frame_interp != 0; seed.has_frame_interpolation = true; seed.frame_interpolation_fps = ls.frame_interp_fps; seed.has_frame_interpolation_fps = true; seed.audio_freq = ls.audio_freq; seed.has_audio_freq = true; @@ -12854,6 +12869,11 @@ int main(int argc, char** argv) { g_video_geometry_correction = seed.geometry_correction ? 1 : 0; g_video_perspective_texturing = seed.perspective_texturing ? 1 : 0; g_video_screen = seed.screen_kind; + if (seed.has_scanlines) g_video_scanlines = seed.scanlines; + if (seed.has_scanline_strength) + g_video_scanline_strength = (float)seed.scanline_strength; + gl_renderer_set_scanlines(g_video_scanlines ? 1 : 0, + g_video_scanline_strength); g_auto_skip_fmv = skip_fmv_offered && seed.auto_skip_fmv ? 1 : 0; g_turbo_loads_enabled = turbo_loads_offered && seed.turbo_loads ? 1 : 0; @@ -14360,6 +14380,10 @@ int main(int argc, char** argv) { ls.geometry_correction = g_video_geometry_correction ? 1 : 0; ls.perspective_texturing = g_video_perspective_texturing ? 1 : 0; ls.screen_kind = g_video_screen; +#if defined(RECOMP_LAUNCHER_HAS_SCANLINES) + ls.scanlines = g_video_scanlines ? 1 : 0; + ls.scanline_strength_pct = (int)(g_video_scanline_strength * 100.0f + 0.5f); +#endif ls.frame_interp = g_frame_interpolation ? 1 : 0; ls.frame_interp_fps = g_frame_interpolation_fps; ls.spu_hq = g_audio_spu_hq ? 1 : 0; @@ -14639,6 +14663,14 @@ int main(int argc, char** argv) { us.has_perspective_texturing = true; us.screen_kind = ls.screen_kind; us.has_screen_kind = true; +#if defined(RECOMP_LAUNCHER_HAS_SCANLINES) + us.scanlines = ls.scanlines != 0; + us.has_scanlines = true; + if (ls.scanline_strength_pct > 0) { + us.scanline_strength = ls.scanline_strength_pct / 100.0; + us.has_scanline_strength = true; + } +#endif us.frame_interpolation = ls.frame_interp != 0; us.has_frame_interpolation = true; us.frame_interpolation_fps = ls.frame_interp_fps; @@ -14694,6 +14726,13 @@ int main(int argc, char** argv) { g_video_geometry_correction = ls.geometry_correction ? 1 : 0; g_video_perspective_texturing = ls.perspective_texturing ? 1 : 0; g_video_screen = ls.screen_kind; +#if defined(RECOMP_LAUNCHER_HAS_SCANLINES) + g_video_scanlines = ls.scanlines != 0; + if (ls.scanline_strength_pct > 0) + g_video_scanline_strength = ls.scanline_strength_pct / 100.0f; + gl_renderer_set_scanlines(g_video_scanlines ? 1 : 0, + g_video_scanline_strength); +#endif /* Load acceleration and FMV skipping are mod-owned on PSX, and the * launcher struct these come from was snapshotted BEFORE * mod_runtime_activate_plugins() ran. Applying them here would From 1438876250d55e08c01c2bcdde6d6ce9a138a86c Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 1 Sep 2026 13:17:19 -0700 Subject: [PATCH 3/3] runtime: clean scanline branch mechanics --- TCP_COMMANDS.md | 1102 ++--- runtime/src/gpu_gl_renderer.c | 8190 ++++++++++++++++----------------- runtime/src/main.cpp | 6 +- 3 files changed, 4649 insertions(+), 4649 deletions(-) diff --git a/TCP_COMMANDS.md b/TCP_COMMANDS.md index 5fa1b328a..99f8a29aa 100644 --- a/TCP_COMMANDS.md +++ b/TCP_COMMANDS.md @@ -1,100 +1,100 @@ -# TCP Debug Server Commands - -Protocol: **JSON over newline**, one object per line, responses on same connection. - -- Request shape: `{"id": N, "cmd": "", ...params}` -- Success: `{"id": N, "ok": true, ...data}` -- Failure: `{"id": N, "ok": false, "error": ""}` - -There are **two live** servers, both implementing this protocol with overlapping command sets: - -| Server | Port | Source | -|---|---|---| -| **Native** (our recompiled runtime) | `4370` | `runtime/src/debug_server.c` | -| **Beetle PSX** (oracle) | `4380` | `runtime/src/beetle_debug_server.c` | - -> **DuckStation (port 4371) was retired as the oracle on 2026-05-05** and is no -> longer built from this repository — there is no `duckstation` entry in -> `.gitmodules`. The **D** column in the curated inventory below is historical -> and reflects DuckStation, not Beetle. For authoritative per-command -> native/Beetle coverage, use the generated -> [Complete command index](#complete-command-index-generated) at the bottom of -> this file, which is derived from the two servers' command tables. - -The `debug_client.py` CLI can target either, or `compare` two at once to diff state live — that's how divergence hunts work. - -```bash -python tools/debug_client.py [args] # native (port 4370) -python tools/debug_client.py --port 4380 # psx-beetle -python tools/debug_client.py --ds [args] # duckstation (port 4371) -python tools/debug_client.py compare # run on both, diff results -``` - -Commands without a bespoke CLI mapping pass through generically: extra -args of the form `key=value` become JSON fields (ints when numeric, else -strings), so every server command is reachable, e.g. -`debug_client.py --port 4370 gpu_frame_dump frame=14528 count=65536`. - ---- - -## Command inventory - -Columns: **N** = native, **D** = DuckStation oracle. - -| Command | N | D | Params | Description | -|---|---|---|---|---| -| `ping` / `frame` | ✓ | ✓ | — | Heartbeat + current frame number | -| `get_registers` (`regs`) | ✓ | ✓ | — | All 32 GPRs + PC + HI + LO (native also: COP0 SR/Cause/EPC, I_STAT, I_MASK) | -| `read_ram` | ✓ | ✓ | `addr`, `len` | Read bytes from PS1 address space as hex string — up to the full 2 MB in ONE response line. `dump_ram` is an alias (the old chunked multi-line variant is gone: it broke the one-request/one-response protocol and wedged the server) | -| `write_ram` | ✓ | ✓ | `addr`, `val` | Write **one byte** to PS1 address space. Note the parameter is `val` (not `hex`), and the write is a single byte per call — this row previously documented both incorrectly | -| `read_scratch` | | ✓ | `addr`, `len` | Read PS1 scratchpad (0x1F800000 region) | -| `read_vram` / `vram_peek` | ✓¹ | ✓ | `x`, `y`, `w`, `h` | Read 16-bit VRAM pixels (max 128×128) | +# TCP Debug Server Commands + +Protocol: **JSON over newline**, one object per line, responses on same connection. + +- Request shape: `{"id": N, "cmd": "", ...params}` +- Success: `{"id": N, "ok": true, ...data}` +- Failure: `{"id": N, "ok": false, "error": ""}` + +There are **two live** servers, both implementing this protocol with overlapping command sets: + +| Server | Port | Source | +|---|---|---| +| **Native** (our recompiled runtime) | `4370` | `runtime/src/debug_server.c` | +| **Beetle PSX** (oracle) | `4380` | `runtime/src/beetle_debug_server.c` | + +> **DuckStation (port 4371) was retired as the oracle on 2026-05-05** and is no +> longer built from this repository — there is no `duckstation` entry in +> `.gitmodules`. The **D** column in the curated inventory below is historical +> and reflects DuckStation, not Beetle. For authoritative per-command +> native/Beetle coverage, use the generated +> [Complete command index](#complete-command-index-generated) at the bottom of +> this file, which is derived from the two servers' command tables. + +The `debug_client.py` CLI can target either, or `compare` two at once to diff state live — that's how divergence hunts work. + +```bash +python tools/debug_client.py [args] # native (port 4370) +python tools/debug_client.py --port 4380 # psx-beetle +python tools/debug_client.py --ds [args] # duckstation (port 4371) +python tools/debug_client.py compare # run on both, diff results +``` + +Commands without a bespoke CLI mapping pass through generically: extra +args of the form `key=value` become JSON fields (ints when numeric, else +strings), so every server command is reachable, e.g. +`debug_client.py --port 4370 gpu_frame_dump frame=14528 count=65536`. + +--- + +## Command inventory + +Columns: **N** = native, **D** = DuckStation oracle. + +| Command | N | D | Params | Description | +|---|---|---|---|---| +| `ping` / `frame` | ✓ | ✓ | — | Heartbeat + current frame number | +| `get_registers` (`regs`) | ✓ | ✓ | — | All 32 GPRs + PC + HI + LO (native also: COP0 SR/Cause/EPC, I_STAT, I_MASK) | +| `read_ram` | ✓ | ✓ | `addr`, `len` | Read bytes from PS1 address space as hex string — up to the full 2 MB in ONE response line. `dump_ram` is an alias (the old chunked multi-line variant is gone: it broke the one-request/one-response protocol and wedged the server) | +| `write_ram` | ✓ | ✓ | `addr`, `val` | Write **one byte** to PS1 address space. Note the parameter is `val` (not `hex`), and the write is a single byte per call — this row previously documented both incorrectly | +| `read_scratch` | | ✓ | `addr`, `len` | Read PS1 scratchpad (0x1F800000 region) | +| `read_vram` / `vram_peek` | ✓¹ | ✓ | `x`, `y`, `w`, `h` | Read 16-bit VRAM pixels (max 128×128) | | `gpu_state` | ✓ | ✓ | — | Display area, display depth, `screen_offset_y`, draw offset, GPUSTAT, clip rect, xfer state. A positive screen offset moves 24-bit scanout down from the PAL or NTSC broadcast centre | | `screenshot_hires` | ✓ | ✓ | `path` | PNG of the **supersampled** surface (the present path the window uses), at `display × gr_scale()`. ⚠ `screenshot`/`screenshot_file` capture native 15-bit VRAM and are **blind to anything that only exists in the hi-res mirror** — geometry correction, SSAA edges, perspective UVs — so they show a clean frame while the player sees a broken one. Use this one to verify those. Falls back to the native resolve (and reports `scale: 1`) when no hi-res surface exists | | `present_shot` | ✓ | | `path` | PNG of the **composed present surface** — the frame after the backend fits the display buffer to the window, so it carries the presented aspect. ⚠ every other capture resolves the display buffer *before* that fit: on a 508×256 display in a 4:3 window they answer 508×256 while the player sees 640×480. Use this one for anything aspect-shaped (widescreen, letterbox), where a pre-fit buffer would hide the very stage the change touches. Staged and fulfilled on the next present, so the ack means *queued* — poll `present_shot_seq`. Unavailable headless and on the Vulkan backend (its swapchain has no readback hook) | | `present_shot_seq` | ✓ | | — | Completion counter for `present_shot`, plus `wrote` (1 = that completion produced a PNG). Sample before staging, poll until `seq` moves. Advances on success *and* failure, so the poll always terminates | -| `geom_correction` | | ✓ | — | `[video] geometry_correction` / `perspective_texturing` engagement: enable flag plus free-running `geometry_vertex_hits` and `perspective_triangles` totals. Both enhancements silently fall back to the faithful path on anything they cannot prove is projected geometry, so a zero counter with the flag on means the title never qualifies — sample twice and diff for a rate | -| `sio_state` | ✓ | ✓ | — | SIO registers + (native only) pad/memcard protocol + TX/RX history | -| `irq_state` | ✓ | ✓ | — | `I_STAT`, `I_MASK` (both), plus chain state on native | -| `dma_state` | ✓ | ✓ | — | DPCR, DICR, all 7 channel states (madr/bcr/chcr) | -| `event_state` | | ✓ | — | EvCB table summary (stub on DS — events are BIOS-level) | -| `overlay_state` | | ✓ | — | Current overlay info | -| `cdrom_sector_dump` | ✓ | | `offset`, `len` | Dump bytes from the last CD-ROM sector observed by the controller, including LBA/mode metadata | -| `cdrom_sector_history` | ✓ | | `count`, optional `lba` | Dump newest CD-ROM sector history entries, including raw XA subheader fields, CPU/audio delivery flags, and the first 128 bytes | -| `cdrom_sector_history_clear` | ✓ | | — | Reset the CD-ROM sector history ring | -| `watch` | ✓ | ✓ | `addr` | Set byte-level memory watchpoint (fires per-frame on change) | -| `unwatch` | ✓ | ✓ | `addr` | Remove memory watchpoint | -| `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry` | Override pad1 buttons and optional analog axes (PS1 inverted bitmask, 0 = pressed; axes 0-255). Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames | -| `clear_input` | ✓ | ✓ | — | Remove input and analog axis overrides | -| `turbo` | ✓ | | `enabled` | Enable/disable TCP-controlled frontend turbo for fast-forward validation | -| `turbo_state` | ✓ | | — | Query TCP-controlled turbo state | -| `pause` | ✓ | | — | **REMOVED** — still registered, but always returns an error. Query a ring buffer (`fn_entry_tail`, `wtrace_dump`, `gpu_frame_dump`) instead of synthesizing a snapshot | -| `continue` (`c`) | ✓ | | — | **REMOVED** — nothing to resume, since `pause` is gone | -| `step` | ✓ | | — | **REMOVED** — query a ring buffer over the window of interest instead of advancing N frames synchronously | -| `run_to_frame` | ✓ | | — | **REMOVED** — use `frame_range` / `read_frame_ram` against the live frame ring instead | -| `history` | ✓ | ✓ | — | Ring buffer stats (frames available) | -| `get_frame` | ✓ | ✓ | `frame` | Full frame record from ring buffer | -| `frame_range` | ✓ | ✓ | `start`, `end` | Range query, max 200 frames | -| `frame_timeseries` | ✓ | ✓ | `start`, `end` | Compact timeseries, max 200 frames | -| `set_snapshot` | ✓ | ✓ | `slot`, `addr`, `size` | Configure per-frame RAM snapshot region (slots 0-3) | -| `get_snapshots` | ✓ | ✓ | — | Show snapshot config | -| `screenshot` | ✓ | ✓ | `path` (optional) | Write a **PNG** of the current display to `path` (default `psx_screenshot.png` in the runtime cwd); single metadata response `{path,width,height}`. `screenshot_file` is an alias; the old inline-hex-row `screenshot` is gone (it streamed h+1 response lines per request and poisoned the connection) | -| `scanline` | ✓ | | optional `on` (0/1), optional `pct` (0..100) | Live A/B toggle for the present-time scanline post-process. Omit `on` to keep the current toggle, omit `pct` to keep the current strength; reports `{scanlines, strength_pct}`. Same state the F6 hotkey and `[video] scanlines` drive | -| `first_failure` | ✓ | | — | Find first divergence point between runs (native-side tracking) | -| `read_frame_ram` | ✓ | | `addr`, `len`, `frame` | Read RAM **as of a specific frame** (from ring buffer) | -| `wtrace_range` | ✓ | | `lo`, `hi` | Set RAM-write trace range (ring of 262 144 writes with RA — `WRITE_TRACE_CAP`, `1 << 18`) | -| `wtrace_dump` | ✓ | beetle | optional `addr_lo`, `addr_hi`, `count`, `newest` | Dump RAM-write trace entries as JSON. The address filter is applied server-side over the FULL ring before the emit cap — always pass it when hunting a specific buffer, otherwise you only see the oldest `count` entries of the whole ring | -| `wtrace_clear` | ✓ | | — | Reset the trace ring | -| `mmio_dump` | ✓ | | optional `addr`, `count`, `newest` | Dump the always-on MMIO write ring (256K entries, ALL 0x1F801xxx writes — SPU/DMA traffic rolls it in well under a minute of gameplay; for display history use `gp1_dump`) | -| `mmio_clear` | ✓ | | — | Reset the MMIO write ring | -| `gp1_dump` | ✓ | | optional `frame_lo`, `frame_hi`, `count`, `newest` | Dump the dedicated ALWAYS-ON GP1 (0x1F801814 display control) ring — 512K entries ≈ 15 min of gameplay (Tomba writes ~10 GP1/frame), survives the general MMIO ring's eviction. Frame filter is server-side over the full ring. Each entry: val + func/pc/cpu_pc/ra/sp/a0/a1/sr/epc/frame | -| `pc_break` | | ✓² | `addr` | DS execute breakpoint, state captured on hit (via `pc_hit_last`) | -| `pc_unbreak` | | ✓² | `addr` | Remove an execute breakpoint | -| `pc_break_list` | | ✓² | — | List active execute breakpoints | -| `pc_hit_last` | | ✓² | — | Captured state (PC, $ra, all GPRs, COP0) from most recent PC break hit | -| `pc_hit_clear` | | ✓² | — | Clear the last-hit record | -| `quit` | ✓ | | — | Shutdown native runtime | - +| `geom_correction` | | ✓ | — | `[video] geometry_correction` / `perspective_texturing` engagement: enable flag plus free-running `geometry_vertex_hits` and `perspective_triangles` totals. Both enhancements silently fall back to the faithful path on anything they cannot prove is projected geometry, so a zero counter with the flag on means the title never qualifies — sample twice and diff for a rate | +| `sio_state` | ✓ | ✓ | — | SIO registers + (native only) pad/memcard protocol + TX/RX history | +| `irq_state` | ✓ | ✓ | — | `I_STAT`, `I_MASK` (both), plus chain state on native | +| `dma_state` | ✓ | ✓ | — | DPCR, DICR, all 7 channel states (madr/bcr/chcr) | +| `event_state` | | ✓ | — | EvCB table summary (stub on DS — events are BIOS-level) | +| `overlay_state` | | ✓ | — | Current overlay info | +| `cdrom_sector_dump` | ✓ | | `offset`, `len` | Dump bytes from the last CD-ROM sector observed by the controller, including LBA/mode metadata | +| `cdrom_sector_history` | ✓ | | `count`, optional `lba` | Dump newest CD-ROM sector history entries, including raw XA subheader fields, CPU/audio delivery flags, and the first 128 bytes | +| `cdrom_sector_history_clear` | ✓ | | — | Reset the CD-ROM sector history ring | +| `watch` | ✓ | ✓ | `addr` | Set byte-level memory watchpoint (fires per-frame on change) | +| `unwatch` | ✓ | ✓ | `addr` | Remove memory watchpoint | +| `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry` | Override pad1 buttons and optional analog axes (PS1 inverted bitmask, 0 = pressed; axes 0-255). Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames | +| `clear_input` | ✓ | ✓ | — | Remove input and analog axis overrides | +| `turbo` | ✓ | | `enabled` | Enable/disable TCP-controlled frontend turbo for fast-forward validation | +| `turbo_state` | ✓ | | — | Query TCP-controlled turbo state | +| `pause` | ✓ | | — | **REMOVED** — still registered, but always returns an error. Query a ring buffer (`fn_entry_tail`, `wtrace_dump`, `gpu_frame_dump`) instead of synthesizing a snapshot | +| `continue` (`c`) | ✓ | | — | **REMOVED** — nothing to resume, since `pause` is gone | +| `step` | ✓ | | — | **REMOVED** — query a ring buffer over the window of interest instead of advancing N frames synchronously | +| `run_to_frame` | ✓ | | — | **REMOVED** — use `frame_range` / `read_frame_ram` against the live frame ring instead | +| `history` | ✓ | ✓ | — | Ring buffer stats (frames available) | +| `get_frame` | ✓ | ✓ | `frame` | Full frame record from ring buffer | +| `frame_range` | ✓ | ✓ | `start`, `end` | Range query, max 200 frames | +| `frame_timeseries` | ✓ | ✓ | `start`, `end` | Compact timeseries, max 200 frames | +| `set_snapshot` | ✓ | ✓ | `slot`, `addr`, `size` | Configure per-frame RAM snapshot region (slots 0-3) | +| `get_snapshots` | ✓ | ✓ | — | Show snapshot config | +| `screenshot` | ✓ | ✓ | `path` (optional) | Write a **PNG** of the current display to `path` (default `psx_screenshot.png` in the runtime cwd); single metadata response `{path,width,height}`. `screenshot_file` is an alias; the old inline-hex-row `screenshot` is gone (it streamed h+1 response lines per request and poisoned the connection) | +| `scanline` | ✓ | | optional `on` (0/1), optional `pct` (0..100) | Live A/B toggle for the present-time scanline post-process. Omit `on` to keep the current toggle, omit `pct` to keep the current strength; reports `{scanlines, strength_pct}`. Same state the F6 hotkey and `[video] scanlines` drive | +| `first_failure` | ✓ | | — | Find first divergence point between runs (native-side tracking) | +| `read_frame_ram` | ✓ | | `addr`, `len`, `frame` | Read RAM **as of a specific frame** (from ring buffer) | +| `wtrace_range` | ✓ | | `lo`, `hi` | Set RAM-write trace range (ring of 262 144 writes with RA — `WRITE_TRACE_CAP`, `1 << 18`) | +| `wtrace_dump` | ✓ | beetle | optional `addr_lo`, `addr_hi`, `count`, `newest` | Dump RAM-write trace entries as JSON. The address filter is applied server-side over the FULL ring before the emit cap — always pass it when hunting a specific buffer, otherwise you only see the oldest `count` entries of the whole ring | +| `wtrace_clear` | ✓ | | — | Reset the trace ring | +| `mmio_dump` | ✓ | | optional `addr`, `count`, `newest` | Dump the always-on MMIO write ring (256K entries, ALL 0x1F801xxx writes — SPU/DMA traffic rolls it in well under a minute of gameplay; for display history use `gp1_dump`) | +| `mmio_clear` | ✓ | | — | Reset the MMIO write ring | +| `gp1_dump` | ✓ | | optional `frame_lo`, `frame_hi`, `count`, `newest` | Dump the dedicated ALWAYS-ON GP1 (0x1F801814 display control) ring — 512K entries ≈ 15 min of gameplay (Tomba writes ~10 GP1/frame), survives the general MMIO ring's eviction. Frame filter is server-side over the full ring. Each entry: val + func/pc/cpu_pc/ra/sp/a0/a1/sr/epc/frame | +| `pc_break` | | ✓² | `addr` | DS execute breakpoint, state captured on hit (via `pc_hit_last`) | +| `pc_unbreak` | | ✓² | `addr` | Remove an execute breakpoint | +| `pc_break_list` | | ✓² | — | List active execute breakpoints | +| `pc_hit_last` | | ✓² | — | Captured state (PC, $ra, all GPRs, COP0) from most recent PC break hit | +| `pc_hit_clear` | | ✓² | — | Clear the last-hit record | +| `quit` | ✓ | | — | Shutdown native runtime | + ¹ Native `vram_peek` is the legacy name; DS calls it `read_vram`. Same semantics. ² The `pc_*` family is specific to the DS oracle: DuckStation's CPU core honours `CPU::AddBreakpointWithCallback`, while our native runtime dispatches whole recompiled functions (no mid-function PC breaks). @@ -119,467 +119,467 @@ and DMA channel. The option is ignored in builds made with debug tools disabled. --- ## Divergence-hunt workflow - -When a recompiled-BIOS bug is suspected, the two servers let you find the **first** divergence instead of chasing symptoms. Standard procedure (inherited from v3's `DEBUG.md`): - -1. **Sync state via PC + registers, not frame number.** Frames drift after even a single timing glitch. Pause both servers; compare `get_registers` until they match. -2. **Dump both sides fully.** Compare `get_frame`, `gpu_state`, `irq_state`, `dma_state` (DS), `dump_ram` over the same regions. -3. **Byte-level comparison.** Tiny mismatches usually point at one subsystem. Use `debug_client.py compare ` for automatic diff. -4. **Find the earliest mismatch**, not a later symptom. Ring-buffer queries (`frame_range`, `read_frame_ram`) help locate which frame went wrong. -5. **Trace the write.** Use `watch` to catch the divergent store, or DS's `pc_break` on the suspect function entry. Look at `$ra` in `pc_hit_last` to identify the caller chain. -6. **Classify.** codegen (recompiler generates wrong instruction), runtime (MMIO or kernel simulation wrong), timing (IRQ cadence), or BIOS (real-hardware quirk we didn't model). -7. **Minimal fix** in the correct subsystem. Never hand-deliver state to hide the symptom (see CLAUDE.md §0). - ---- - -## Server send budget + serve-stall telemetry - -The server is pumped on the **main thread**: every millisecond it spends -sending a response is a millisecond the emulator does not run. Inline -responses are bounded — 2 s per zero-progress chunk and **15 s total per -response**; a client that exceeds the budget is disconnected (the runtime -never stalls indefinitely). Responses bigger than the budget allows must -use the `*_dump_file` variants, which write to disk instead of the socket. - -Cumulative serve-stall is exported as `tcp_send_stall_ms` / -`tcp_clients_dropped` in `psx_freeze_heartbeat.json` (plus per-tick -`tcp_ms` in its ring) and in every wedge/fatal dump header. **Check these -first when diagnosing slow or stalled frames** — on 2026-06-10 two -"attract-idle degradations" turned out to be a TCP client trickle-draining -mega-dumps, throttling the main loop to 6 fps (all 8 watchdog stack -samples inside `WS2_32!send`). A slow-frames wedge with a large -`tcp_send_stall_ms` delta over the same window is observer interference, -not a guest bug. - -## Call-contract (bail) telemetry - -The dispatch call contract (Bug D family fix, 2026-06-10) guards every -generated continuation: it may only run if the guest actually returned to -the call site ($ra == site return address, $sp == caller's sp at the -call). Violations begin a "bail" unwind that abandons stale C frames and -re-dispatches the guest's true target. Counters in -`psx_freeze_heartbeat.json` and dump headers: - -- `bail_first` — contract violations detected (wild returns). Nonzero - during gameplay means the game executed a wild control transfer (e.g. - Tomba's dead jumptable case `jal 0x80120B3C`, the chest freeze). A - small count with the game continuing normally is the fix working. -- `bail_resolved` — unwinds that resolved at an enclosing call site whose - contract matched (multi-level return). -- `bail_flattened` — unwinds that reached the outermost dispatch loop and - re-dispatched the wild target on a clean host stack. -- `bail_anomaly` — bail flag observed at exception entry (must stay 0; - anything else is a runtime bug). - ---- - -## `bios_info` — linked recompiled-BIOS identity (native only) - -Reports which BIOS image this build's recompiled C was generated from -(`psx_bios_image`, emitted into the generated dispatch from the BIOS profile) -and whether the loaded ROM matches it: `image_id`, `sha256`, `crc32`, `size`, -`bundled` (redistributable image shipped with the game), the kernel-bless -window, the HLE anchors (`shell_entry_phys` / `deliver_event_ret`; 0 = -structurally unavailable on this BIOS), and `image_wordsum` vs -`loaded_wordsum` with `match`. With the launch identity gate a running -process always reports `match:1`. - -- `{"cmd":"bios_info"}` - -## `s3_smear_watch` — callee-saved-register smear tripwire (native only) - -Latches the first interpreted instruction in a PC window whose execution -changes `$s3` (`runtime/src/dirty_ram_interp.c`). A `jalr`'s exec_one spans -the entire nested native callee, so the latch names the callee that returned -with a clobbered callee-saved register; the insn ring is frozen at the latch. - -- `{"cmd":"s3_smear_watch","lo":"","hi":""}` — arm (each arming - fully re-specifies the watch). Optional `"excl":""`: exact - encoding to ignore, so a watched loop's own `$s3` advance (e.g. an - `addi s3,s3,8` list walk) doesn't trip the latch. -- `{"cmd":"s3_smear_watch"}` — report the latch: `valid`, `pc`, `insn`, - `s3_old`/`s3_new`, `call_target` (rs at the call site for jr/jalr), - `frame`. -- `{"cmd":"s3_smear_watch","lo":"0"}` — disarm. - -## `callret_watch` — interp JALR call-resolution ring (native only) - -64-entry ring (`runtime/src/dirty_ram_interp.c`) recording, for every -interpreted JALR whose call PC lies in a window, which resolution tier ran -the callee and the full post-call outcome — the complement of -`s3_smear_watch`: the tripwire names the callee that came back smeared, this -ring names the return path that let it come back. - -- `{"cmd":"callret_watch","lo":"","hi":""}` — arm (resets the ring). -- `{"cmd":"callret_watch"}` — dump (newest last): per entry `cyc`, `f` - (frame), `pc`, `tgt`, `path` (`CRES_*` tier code, see the enum in - dirty_ram_interp.c; `|0x100` = finish() escaped), pre-call - `sp_b`/`ra_b`/`s0_b`/`s3_b`, post-call `pc_a`/`ra_a`/`sp_a`/`s0_a`/ - `s3_a`/`v0_a`, `bail`/`rfe`/`esc`/`in_exc` flags, `dstatic`/`dblocks`/ - `dexc` engine-attribution deltas across the call, `last_func`. + +When a recompiled-BIOS bug is suspected, the two servers let you find the **first** divergence instead of chasing symptoms. Standard procedure (inherited from v3's `DEBUG.md`): + +1. **Sync state via PC + registers, not frame number.** Frames drift after even a single timing glitch. Pause both servers; compare `get_registers` until they match. +2. **Dump both sides fully.** Compare `get_frame`, `gpu_state`, `irq_state`, `dma_state` (DS), `dump_ram` over the same regions. +3. **Byte-level comparison.** Tiny mismatches usually point at one subsystem. Use `debug_client.py compare ` for automatic diff. +4. **Find the earliest mismatch**, not a later symptom. Ring-buffer queries (`frame_range`, `read_frame_ram`) help locate which frame went wrong. +5. **Trace the write.** Use `watch` to catch the divergent store, or DS's `pc_break` on the suspect function entry. Look at `$ra` in `pc_hit_last` to identify the caller chain. +6. **Classify.** codegen (recompiler generates wrong instruction), runtime (MMIO or kernel simulation wrong), timing (IRQ cadence), or BIOS (real-hardware quirk we didn't model). +7. **Minimal fix** in the correct subsystem. Never hand-deliver state to hide the symptom (see CLAUDE.md §0). + +--- + +## Server send budget + serve-stall telemetry + +The server is pumped on the **main thread**: every millisecond it spends +sending a response is a millisecond the emulator does not run. Inline +responses are bounded — 2 s per zero-progress chunk and **15 s total per +response**; a client that exceeds the budget is disconnected (the runtime +never stalls indefinitely). Responses bigger than the budget allows must +use the `*_dump_file` variants, which write to disk instead of the socket. + +Cumulative serve-stall is exported as `tcp_send_stall_ms` / +`tcp_clients_dropped` in `psx_freeze_heartbeat.json` (plus per-tick +`tcp_ms` in its ring) and in every wedge/fatal dump header. **Check these +first when diagnosing slow or stalled frames** — on 2026-06-10 two +"attract-idle degradations" turned out to be a TCP client trickle-draining +mega-dumps, throttling the main loop to 6 fps (all 8 watchdog stack +samples inside `WS2_32!send`). A slow-frames wedge with a large +`tcp_send_stall_ms` delta over the same window is observer interference, +not a guest bug. + +## Call-contract (bail) telemetry + +The dispatch call contract (Bug D family fix, 2026-06-10) guards every +generated continuation: it may only run if the guest actually returned to +the call site ($ra == site return address, $sp == caller's sp at the +call). Violations begin a "bail" unwind that abandons stale C frames and +re-dispatches the guest's true target. Counters in +`psx_freeze_heartbeat.json` and dump headers: + +- `bail_first` — contract violations detected (wild returns). Nonzero + during gameplay means the game executed a wild control transfer (e.g. + Tomba's dead jumptable case `jal 0x80120B3C`, the chest freeze). A + small count with the game continuing normally is the fix working. +- `bail_resolved` — unwinds that resolved at an enclosing call site whose + contract matched (multi-level return). +- `bail_flattened` — unwinds that reached the outermost dispatch loop and + re-dispatched the wild target on a clean host stack. +- `bail_anomaly` — bail flag observed at exception entry (must stay 0; + anything else is a runtime bug). + +--- + +## `bios_info` — linked recompiled-BIOS identity (native only) + +Reports which BIOS image this build's recompiled C was generated from +(`psx_bios_image`, emitted into the generated dispatch from the BIOS profile) +and whether the loaded ROM matches it: `image_id`, `sha256`, `crc32`, `size`, +`bundled` (redistributable image shipped with the game), the kernel-bless +window, the HLE anchors (`shell_entry_phys` / `deliver_event_ret`; 0 = +structurally unavailable on this BIOS), and `image_wordsum` vs +`loaded_wordsum` with `match`. With the launch identity gate a running +process always reports `match:1`. + +- `{"cmd":"bios_info"}` + +## `s3_smear_watch` — callee-saved-register smear tripwire (native only) + +Latches the first interpreted instruction in a PC window whose execution +changes `$s3` (`runtime/src/dirty_ram_interp.c`). A `jalr`'s exec_one spans +the entire nested native callee, so the latch names the callee that returned +with a clobbered callee-saved register; the insn ring is frozen at the latch. + +- `{"cmd":"s3_smear_watch","lo":"","hi":""}` — arm (each arming + fully re-specifies the watch). Optional `"excl":""`: exact + encoding to ignore, so a watched loop's own `$s3` advance (e.g. an + `addi s3,s3,8` list walk) doesn't trip the latch. +- `{"cmd":"s3_smear_watch"}` — report the latch: `valid`, `pc`, `insn`, + `s3_old`/`s3_new`, `call_target` (rs at the call site for jr/jalr), + `frame`. +- `{"cmd":"s3_smear_watch","lo":"0"}` — disarm. + +## `callret_watch` — interp JALR call-resolution ring (native only) + +64-entry ring (`runtime/src/dirty_ram_interp.c`) recording, for every +interpreted JALR whose call PC lies in a window, which resolution tier ran +the callee and the full post-call outcome — the complement of +`s3_smear_watch`: the tripwire names the callee that came back smeared, this +ring names the return path that let it come back. + +- `{"cmd":"callret_watch","lo":"","hi":""}` — arm (resets the ring). +- `{"cmd":"callret_watch"}` — dump (newest last): per entry `cyc`, `f` + (frame), `pc`, `tgt`, `path` (`CRES_*` tier code, see the enum in + dirty_ram_interp.c; `|0x100` = finish() escaped), pre-call + `sp_b`/`ra_b`/`s0_b`/`s3_b`, post-call `pc_a`/`ra_a`/`sp_a`/`s0_a`/ + `s3_a`/`v0_a`, `bail`/`rfe`/`esc`/`in_exc` flags, `dstatic`/`dblocks`/ + `dexc` engine-attribution deltas across the call, `last_func`. - `{"cmd":"callret_watch","lo":"0"}` — disarm. - -## `hle_dump` — BIOS-HLE tier call ring (native only) - -Always-on ring (`runtime/src/bios_hle.c`, 16K entries) recording every -A0/B0/C0 kernel-vector dispatch the HLE tier's hook observes, plus the boot -shell-skip event. - -The hook is installed when EITHER axis is on (`bios_hle_plan.h`), so a run with -the boot-skip on but kernel calls left to LLE — the default on the bundled -OpenBIOS, which exports no `deliver_event_ret` — reports -`backend: "LLE (recompiled BIOS)"` and still fills the ring with `route: 0` -vector observations plus the one `route: 2` boot entry. Only a run with BOTH axes -off installs no hook and leaves the ring empty; use `bioscall_dump` for LLE-side -vector observation there. - -- `{"cmd":"hle_dump"}` — status: `backend` (`HLE (LLE fallback)` / - `LLE (recompiled BIOS)`), `boot_skip`, `boot_turbo_active`, `total`. -- `{"cmd":"hle_dump","tail":N}` — last N entries: `seq`, `cycle` (guest - cycle), `vec` (0xA0/0xB0/0xC0, or 0x30000 for the boot skip), `fn` ($t1 - function number), `a0..a3`, `ra`, `v0` (result when HLE-serviced), `route` - (0 = fell through to LLE, 1 = serviced in HLE, 2 = boot shell-skip). -- Filters: `"fn":N`, `"route":0|1|2`. - ---- - -## Rule when the server can't answer your question - -If an inspection need isn't covered by the existing commands, **do not fall back to printf or log files**. Instead: - -1. Add a handler in `runtime/src/debug_server.c` (native) -2. Add the matching handler in `runtime/src/beetle_debug_server.c` (Beetle oracle) - when the question needs a cross-check against hardware behaviour -3. Keep field names parallel between the two -4. Run `python tools/gen_tcp_commands.py` to refresh the generated index, and add - a row to the curated inventory above if the command needs explaining - -> Step 4 used to read "Update this file", and that did not survive contact with -> reality: this document described 47 commands while the servers registered 292. -> The index is now generated from the command tables, and -> `python tools/gen_tcp_commands.py --check` fails when it drifts. Prose in the -> curated inventory is still hand-written and still worth adding. - -The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` is absolute: **no `fprintf(stderr, …)` in source code, ever, for any reason**. - - - -## Complete command index (generated) - + +## `hle_dump` — BIOS-HLE tier call ring (native only) + +Always-on ring (`runtime/src/bios_hle.c`, 16K entries) recording every +A0/B0/C0 kernel-vector dispatch the HLE tier's hook observes, plus the boot +shell-skip event. + +The hook is installed when EITHER axis is on (`bios_hle_plan.h`), so a run with +the boot-skip on but kernel calls left to LLE — the default on the bundled +OpenBIOS, which exports no `deliver_event_ret` — reports +`backend: "LLE (recompiled BIOS)"` and still fills the ring with `route: 0` +vector observations plus the one `route: 2` boot entry. Only a run with BOTH axes +off installs no hook and leaves the ring empty; use `bioscall_dump` for LLE-side +vector observation there. + +- `{"cmd":"hle_dump"}` — status: `backend` (`HLE (LLE fallback)` / + `LLE (recompiled BIOS)`), `boot_skip`, `boot_turbo_active`, `total`. +- `{"cmd":"hle_dump","tail":N}` — last N entries: `seq`, `cycle` (guest + cycle), `vec` (0xA0/0xB0/0xC0, or 0x30000 for the boot skip), `fn` ($t1 + function number), `a0..a3`, `ra`, `v0` (result when HLE-serviced), `route` + (0 = fell through to LLE, 1 = serviced in HLE, 2 = boot shell-skip). +- Filters: `"fn":N`, `"route":0|1|2`. + +--- + +## Rule when the server can't answer your question + +If an inspection need isn't covered by the existing commands, **do not fall back to printf or log files**. Instead: + +1. Add a handler in `runtime/src/debug_server.c` (native) +2. Add the matching handler in `runtime/src/beetle_debug_server.c` (Beetle oracle) + when the question needs a cross-check against hardware behaviour +3. Keep field names parallel between the two +4. Run `python tools/gen_tcp_commands.py` to refresh the generated index, and add + a row to the curated inventory above if the command needs explaining + +> Step 4 used to read "Update this file", and that did not survive contact with +> reality: this document described 47 commands while the servers registered 292. +> The index is now generated from the command tables, and +> `python tools/gen_tcp_commands.py --check` fails when it drifts. Prose in the +> curated inventory is still hand-written and still worth adding. + +The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` is absolute: **no `fprintf(stderr, …)` in source code, ever, for any reason**. + + + +## Complete command index (generated) + **306 commands registered** — 293 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). 51 of 306 have prose above; **255 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. - -Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code. - -| Command | Native | Beetle | Described above | -|---|:--:|:--:|:--:| -| `a0_history` | ✓ | | | -| `audio_events` | ✓ | ✓ | | -| `audio_stats` | ✓ | ✓ | | -| `audio_wav` | ✓ | ✓ | | -| `autocompile_run` | ✓ | | | -| `autocompile_status` | ✓ | | | -| `bios_info` | ✓ | | ✓ | -| `bioscall_dump` | ✓ | | | -| `c0_history` | ✓ | | | -| `call_focus_dump` | ✓ | | | -| `call_focus_reset` | ✓ | | | -| `call_focus_stats` | ✓ | | | -| `callret_watch` | ✓ | | ✓ | -| `capture_freeze` | ✓ | | | -| `capture_quads` | ✓ | | | -| `card_buffer_dump` | ✓ | | | -| `card_data_writes` | ✓ | | | -| `card_data_writes_reset` | ✓ | | | -| `card_handoff` | ✓ | | | -| `card_mgr_clear` | ✓ | | | -| `card_mgr_trace` | ✓ | | | -| `card_read_summary` | ✓ | | | -| `card_read_summary_reset` | ✓ | | | -| `card_trace_dump` | ✓ | | | -| `card_txn_dump` | ✓ | | | -| `cd_overwrite` | ✓ | | | -| `cd_read_log` | ✓ | | | -| `cdc_volume` | | ✓ | | -| `cdrom_bursts` | ✓ | | | -| `cdrom_cmd_dump` | | ✓ | | -| `cdrom_cmd_reset` | | ✓ | | -| `cdrom_command_history` | ✓ | | | -| `cdrom_command_history_clear` | ✓ | | | -| `cdrom_instant_rate` | ✓ | | | -| `cdrom_sector_dump` | ✓ | | ✓ | -| `cdrom_sector_history` | ✓ | | ✓ | -| `cdrom_sector_history_clear` | ✓ | | ✓ | -| `cdrom_state` | ✓ | | | -| `cdrom_timing` | ✓ | | | -| `cdrom_trace_clear` | ✓ | | | -| `cdrom_trace_dump` | ✓ | | | -| `ce_profile` | ✓ | | | -| `chain_trace` | ✓ | | | -| `clear_input` | ✓ | ✓ | ✓ | -| `continue` | ✓ | | ✓ | -| `cyc_watch` | ✓ | ✓ | | -| `cyc_watch_clear` | ✓ | ✓ | | -| `cyc_watch_dump` | ✓ | ✓ | | -| `cycles_to_next_event` | ✓ | | | -| `d44_ring` | ✓ | | | -| `data_shards` | ✓ | | | -| `devtrace_ctl` | ✓ | ✓ | | -| `devtrace_dump` | ✓ | ✓ | | -| `dirty_block_dump_file` | ✓ | | | -| `dirty_block_log` | ✓ | | | -| `dirty_break_clear` | ✓ | | | -| `dirty_break_range` | ✓ | | | -| `dirty_break_state` | ✓ | | | -| `dirty_flow_log` | ✓ | | | -| `dirty_insn_dump_file` | ✓ | | | -| `dirty_insn_gate` | ✓ | | | -| `dirty_insn_log` | ✓ | | | -| `dirty_ram_stats` | ✓ | | | -| `dirty_ram_unsupported` | ✓ | | | -| `disp_ring` | ✓ | | | -| `dispatch_check` | ✓ | | | -| `dispatch_stats` | ✓ | | | -| `dispatch_tail` | ✓ | | | -| `display_ring_aux` | ✓ | | | -| `display_ring_get` | ✓ | | | -| `display_ring_stats` | ✓ | | | -| `dma_cdrom_history` | ✓ | | | -| `dma_state` | ✓ | | ✓ | -| `dma_trace_clear` | ✓ | | | -| `dma_trace_dump` | ✓ | | | -| `dump_buffer` | ✓ | | | -| `dump_ram` | ✓ | ✓ | ✓ | -| `evcb_snapshot` | ✓ | | | -| `evcb_walk_dump` | ✓ | | | -| `evcb_walk_stats` | ✓ | | | -| `event_ring_clear` | ✓ | | | -| `event_ring_dump` | ✓ | | | -| `event_ring_tail` | ✓ | | | -| `exc_ring` | | ✓ | | -| `first_failure` | ✓ | ✓ | ✓ | -| `fmv_state` | ✓ | | | -| `fn_clear` | ✓ | | | -| `fn_disable` | ✓ | | | -| `fn_entry_dump` | ✓ | | | -| `fn_entry_tail` | ✓ | | ✓ | -| `fn_exit_dump` | ✓ | | | -| `fn_filter` | ✓ | | | -| `fn_stats` | ✓ | | | -| `fntrace_arm` | ✓ | ✓ | | -| `fntrace_arm_clear` | ✓ | | | -| `fntrace_armed` | ✓ | | | -| `fntrace_arms` | | ✓ | | -| `fntrace_clear` | ✓ | | | -| `fntrace_disarm` | | ✓ | | -| `fntrace_dump` | ✓ | ✓ | | -| `fntrace_reset` | | ✓ | | -| `fntrace_unfiltered` | | ✓ | | -| `frame` | ✓ | | ✓ | -| `frame_fingerprint` | ✓ | | | -| `frame_perf` | ✓ | | | -| `frame_range` | ✓ | ✓ | ✓ | -| `frame_timeseries` | ✓ | ✓ | ✓ | -| `freeze_check` | ✓ | | | -| `game_options` | ✓ | | | -| `geom_correction` | ✓ | | ✓ | -| `get_frame` | ✓ | ✓ | ✓ | -| `get_quads` | ✓ | | | -| `get_registers` | ✓ | ✓ | ✓ | -| `get_snapshots` | ✓ | ✓ | ✓ | -| `gl_coh_ring` | ✓ | | | -| `gl_fbo_peek` | ✓ | | | -| `gl_interp` | ✓ | | | -| `gl_present_ring` | ✓ | | | -| `gl_vram_diff` | ✓ | | | -| `gl_wide_fast` | ✓ | | | -| `gl_ws_ablate` | ✓ | | | -| `gp1_dump` | ✓ | | ✓ | -| `gpu_frame_dump` | ✓ | | ✓ | -| `gpu_opcodes` | ✓ | | | -| `gpu_ring_stats` | ✓ | | | -| `gpu_state` | ✓ | | ✓ | -| `gte_frame_stats` | ✓ | | | -| `gte_intpl_dump` | ✓ | | | -| `gte_latch_dump` | ✓ | | | -| `gte_ring_dump` | ✓ | | | -| `gte_state` | ✓ | | | -| `history` | ✓ | ✓ | ✓ | -| `hle_dump` | ✓ | | ✓ | -| `idle_skip` | ✓ | | | -| `imask_trace` | ✓ | | | -| `input_route_append` | ✓ | | | -| `input_route_clear` | ✓ | | | -| `input_route_start` | ✓ | | | -| `input_route_status` | ✓ | | | -| `input_route_stop` | ✓ | | | -| `insn_freeze` | ✓ | | | -| `insn_freeze_snapshot` | ✓ | | | -| `insn_freeze_status` | ✓ | | | -| `insn_freeze_target` | ✓ | | | -| `irq_state` | ✓ | | ✓ | -| `irqctx_ring` | ✓ | | | -| `kernel_bless` | ✓ | | | -| `latency` | ✓ | | | -| `load_transitions` | ✓ | | | -| `lockstep` | ✓ | | | -| `lockstep_func` | ✓ | | | -| `mc_status` | ✓ | | | -| `mdec_state` | ✓ | | | -| `mdec_trace` | ✓ | | | -| `mdec_trace_clear` | ✓ | | | -| `mem_words` | ✓ | | | -| `mmio_clear` | ✓ | | ✓ | -| `mmio_dump` | ✓ | | ✓ | -| `mmx6_freshfix` | ✓ | | | -| `overlay_candidates` | ✓ | | | -| `overlay_capture_dump` | ✓ | | | -| `overlay_cps_probe` | ✓ | | | -| `overlay_diff_off` | ✓ | | | -| `overlay_diff_on` | ✓ | | | -| `overlay_dump` | ✓ | | | -| `overlay_fp_dump` | ✓ | | | -| `overlay_irq_ratelimit` | ✓ | | | -| `overlay_irq_suppress_off` | ✓ | | | -| `overlay_irq_suppress_on` | ✓ | | | -| `overlay_loader_status` | ✓ | | | -| `overlay_native_block` | ✓ | | | -| `overlay_native_event_granularity` | ✓ | | | -| `overlay_native_off` | ✓ | | | -| `overlay_native_on` | ✓ | | | -| `overlay_native_ring` | ✓ | | | -| `overlay_rescan` | ✓ | | | -| `overlay_shadow_detail` | ✓ | | | -| `overlay_shadow_dump` | ✓ | | | -| `pace_state` | ✓ | | | -| `pad_status` | ✓ | ✓ | | -| `parity_ctl` | ✓ | ✓ | | -| `parity_dump` | ✓ | ✓ | | -| `pause` | ✓ | | ✓ | -| `pc_probe_arm` | ✓ | | | -| `pc_probe_clear` | ✓ | | | -| `pc_probe_dump` | ✓ | | | -| `pgxp` | ✓ | | | -| `phase_hot` | ✓ | | | -| `phase_profile` | ✓ | | | -| `ping` | ✓ | ✓ | ✓ | -| `present_ring` | ✓ | | | + +Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code. + +| Command | Native | Beetle | Described above | +|---|:--:|:--:|:--:| +| `a0_history` | ✓ | | | +| `audio_events` | ✓ | ✓ | | +| `audio_stats` | ✓ | ✓ | | +| `audio_wav` | ✓ | ✓ | | +| `autocompile_run` | ✓ | | | +| `autocompile_status` | ✓ | | | +| `bios_info` | ✓ | | ✓ | +| `bioscall_dump` | ✓ | | | +| `c0_history` | ✓ | | | +| `call_focus_dump` | ✓ | | | +| `call_focus_reset` | ✓ | | | +| `call_focus_stats` | ✓ | | | +| `callret_watch` | ✓ | | ✓ | +| `capture_freeze` | ✓ | | | +| `capture_quads` | ✓ | | | +| `card_buffer_dump` | ✓ | | | +| `card_data_writes` | ✓ | | | +| `card_data_writes_reset` | ✓ | | | +| `card_handoff` | ✓ | | | +| `card_mgr_clear` | ✓ | | | +| `card_mgr_trace` | ✓ | | | +| `card_read_summary` | ✓ | | | +| `card_read_summary_reset` | ✓ | | | +| `card_trace_dump` | ✓ | | | +| `card_txn_dump` | ✓ | | | +| `cd_overwrite` | ✓ | | | +| `cd_read_log` | ✓ | | | +| `cdc_volume` | | ✓ | | +| `cdrom_bursts` | ✓ | | | +| `cdrom_cmd_dump` | | ✓ | | +| `cdrom_cmd_reset` | | ✓ | | +| `cdrom_command_history` | ✓ | | | +| `cdrom_command_history_clear` | ✓ | | | +| `cdrom_instant_rate` | ✓ | | | +| `cdrom_sector_dump` | ✓ | | ✓ | +| `cdrom_sector_history` | ✓ | | ✓ | +| `cdrom_sector_history_clear` | ✓ | | ✓ | +| `cdrom_state` | ✓ | | | +| `cdrom_timing` | ✓ | | | +| `cdrom_trace_clear` | ✓ | | | +| `cdrom_trace_dump` | ✓ | | | +| `ce_profile` | ✓ | | | +| `chain_trace` | ✓ | | | +| `clear_input` | ✓ | ✓ | ✓ | +| `continue` | ✓ | | ✓ | +| `cyc_watch` | ✓ | ✓ | | +| `cyc_watch_clear` | ✓ | ✓ | | +| `cyc_watch_dump` | ✓ | ✓ | | +| `cycles_to_next_event` | ✓ | | | +| `d44_ring` | ✓ | | | +| `data_shards` | ✓ | | | +| `devtrace_ctl` | ✓ | ✓ | | +| `devtrace_dump` | ✓ | ✓ | | +| `dirty_block_dump_file` | ✓ | | | +| `dirty_block_log` | ✓ | | | +| `dirty_break_clear` | ✓ | | | +| `dirty_break_range` | ✓ | | | +| `dirty_break_state` | ✓ | | | +| `dirty_flow_log` | ✓ | | | +| `dirty_insn_dump_file` | ✓ | | | +| `dirty_insn_gate` | ✓ | | | +| `dirty_insn_log` | ✓ | | | +| `dirty_ram_stats` | ✓ | | | +| `dirty_ram_unsupported` | ✓ | | | +| `disp_ring` | ✓ | | | +| `dispatch_check` | ✓ | | | +| `dispatch_stats` | ✓ | | | +| `dispatch_tail` | ✓ | | | +| `display_ring_aux` | ✓ | | | +| `display_ring_get` | ✓ | | | +| `display_ring_stats` | ✓ | | | +| `dma_cdrom_history` | ✓ | | | +| `dma_state` | ✓ | | ✓ | +| `dma_trace_clear` | ✓ | | | +| `dma_trace_dump` | ✓ | | | +| `dump_buffer` | ✓ | | | +| `dump_ram` | ✓ | ✓ | ✓ | +| `evcb_snapshot` | ✓ | | | +| `evcb_walk_dump` | ✓ | | | +| `evcb_walk_stats` | ✓ | | | +| `event_ring_clear` | ✓ | | | +| `event_ring_dump` | ✓ | | | +| `event_ring_tail` | ✓ | | | +| `exc_ring` | | ✓ | | +| `first_failure` | ✓ | ✓ | ✓ | +| `fmv_state` | ✓ | | | +| `fn_clear` | ✓ | | | +| `fn_disable` | ✓ | | | +| `fn_entry_dump` | ✓ | | | +| `fn_entry_tail` | ✓ | | ✓ | +| `fn_exit_dump` | ✓ | | | +| `fn_filter` | ✓ | | | +| `fn_stats` | ✓ | | | +| `fntrace_arm` | ✓ | ✓ | | +| `fntrace_arm_clear` | ✓ | | | +| `fntrace_armed` | ✓ | | | +| `fntrace_arms` | | ✓ | | +| `fntrace_clear` | ✓ | | | +| `fntrace_disarm` | | ✓ | | +| `fntrace_dump` | ✓ | ✓ | | +| `fntrace_reset` | | ✓ | | +| `fntrace_unfiltered` | | ✓ | | +| `frame` | ✓ | | ✓ | +| `frame_fingerprint` | ✓ | | | +| `frame_perf` | ✓ | | | +| `frame_range` | ✓ | ✓ | ✓ | +| `frame_timeseries` | ✓ | ✓ | ✓ | +| `freeze_check` | ✓ | | | +| `game_options` | ✓ | | | +| `geom_correction` | ✓ | | ✓ | +| `get_frame` | ✓ | ✓ | ✓ | +| `get_quads` | ✓ | | | +| `get_registers` | ✓ | ✓ | ✓ | +| `get_snapshots` | ✓ | ✓ | ✓ | +| `gl_coh_ring` | ✓ | | | +| `gl_fbo_peek` | ✓ | | | +| `gl_interp` | ✓ | | | +| `gl_present_ring` | ✓ | | | +| `gl_vram_diff` | ✓ | | | +| `gl_wide_fast` | ✓ | | | +| `gl_ws_ablate` | ✓ | | | +| `gp1_dump` | ✓ | | ✓ | +| `gpu_frame_dump` | ✓ | | ✓ | +| `gpu_opcodes` | ✓ | | | +| `gpu_ring_stats` | ✓ | | | +| `gpu_state` | ✓ | | ✓ | +| `gte_frame_stats` | ✓ | | | +| `gte_intpl_dump` | ✓ | | | +| `gte_latch_dump` | ✓ | | | +| `gte_ring_dump` | ✓ | | | +| `gte_state` | ✓ | | | +| `history` | ✓ | ✓ | ✓ | +| `hle_dump` | ✓ | | ✓ | +| `idle_skip` | ✓ | | | +| `imask_trace` | ✓ | | | +| `input_route_append` | ✓ | | | +| `input_route_clear` | ✓ | | | +| `input_route_start` | ✓ | | | +| `input_route_status` | ✓ | | | +| `input_route_stop` | ✓ | | | +| `insn_freeze` | ✓ | | | +| `insn_freeze_snapshot` | ✓ | | | +| `insn_freeze_status` | ✓ | | | +| `insn_freeze_target` | ✓ | | | +| `irq_state` | ✓ | | ✓ | +| `irqctx_ring` | ✓ | | | +| `kernel_bless` | ✓ | | | +| `latency` | ✓ | | | +| `load_transitions` | ✓ | | | +| `lockstep` | ✓ | | | +| `lockstep_func` | ✓ | | | +| `mc_status` | ✓ | | | +| `mdec_state` | ✓ | | | +| `mdec_trace` | ✓ | | | +| `mdec_trace_clear` | ✓ | | | +| `mem_words` | ✓ | | | +| `mmio_clear` | ✓ | | ✓ | +| `mmio_dump` | ✓ | | ✓ | +| `mmx6_freshfix` | ✓ | | | +| `overlay_candidates` | ✓ | | | +| `overlay_capture_dump` | ✓ | | | +| `overlay_cps_probe` | ✓ | | | +| `overlay_diff_off` | ✓ | | | +| `overlay_diff_on` | ✓ | | | +| `overlay_dump` | ✓ | | | +| `overlay_fp_dump` | ✓ | | | +| `overlay_irq_ratelimit` | ✓ | | | +| `overlay_irq_suppress_off` | ✓ | | | +| `overlay_irq_suppress_on` | ✓ | | | +| `overlay_loader_status` | ✓ | | | +| `overlay_native_block` | ✓ | | | +| `overlay_native_event_granularity` | ✓ | | | +| `overlay_native_off` | ✓ | | | +| `overlay_native_on` | ✓ | | | +| `overlay_native_ring` | ✓ | | | +| `overlay_rescan` | ✓ | | | +| `overlay_shadow_detail` | ✓ | | | +| `overlay_shadow_dump` | ✓ | | | +| `pace_state` | ✓ | | | +| `pad_status` | ✓ | ✓ | | +| `parity_ctl` | ✓ | ✓ | | +| `parity_dump` | ✓ | ✓ | | +| `pause` | ✓ | | ✓ | +| `pc_probe_arm` | ✓ | | | +| `pc_probe_clear` | ✓ | | | +| `pc_probe_dump` | ✓ | | | +| `pgxp` | ✓ | | | +| `phase_hot` | ✓ | | | +| `phase_profile` | ✓ | | | +| `ping` | ✓ | ✓ | ✓ | +| `present_ring` | ✓ | | | | `present_shot` | ✓ | | ✓ | | `present_shot_seq` | ✓ | | ✓ | -| `press` | ✓ | ✓ | | -| `probe_clear` | ✓ | | | -| `probe_trace` | ✓ | | | -| `quit` | ✓ | | ✓ | -| `ra_load_watch` | ✓ | | | -| `read_frame_ram` | ✓ | ✓ | ✓ | -| `read_ram` | ✓ | ✓ | ✓ | -| `record_frame` | ✓ | | | -| `record_frame_dump` | ✓ | | | -| `record_reads_dump` | ✓ | | | -| `restore_trace` | ✓ | | | -| `restore_trace_clear` | ✓ | | | -| `restore_trace_window` | ✓ | | | -| `rtrace_arm` | ✓ | ✓ | | -| `rtrace_clear` | ✓ | | | -| `rtrace_disarm` | | ✓ | | -| `rtrace_disarm_all` | | ✓ | | -| `rtrace_dump` | ✓ | ✓ | | -| `rtrace_ranges` | ✓ | ✓ | | -| `rtrace_reset` | | ✓ | | -| `rtrace_stats` | ✓ | ✓ | | -| `run_to_frame` | ✓ | | ✓ | -| `s3_smear_watch` | ✓ | | ✓ | -| `savestate` | ✓ | | | -| `scanline` | ✓ | | | -| `screenshot` | ✓ | ✓ | ✓ | -| `screenshot_file` | ✓ | ✓ | ✓ | -| `screenshot_hires` | ✓ | | ✓ | -| `set_input` | ✓ | ✓ | ✓ | -| `set_snapshot` | ✓ | ✓ | ✓ | -| `sio_arm_audit` | ✓ | | | -| `sio_burst_stats` | ✓ | | | -| `sio_ctrl_reg_clear` | ✓ | | | -| `sio_ctrl_reg_trace` | ✓ | | | -| `sio_ctrl_reg_window` | ✓ | | | -| `sio_irq_dump` | ✓ | | | -| `sio_irq_window` | ✓ | | | -| `sio_pc_trace` | ✓ | | | -| `sio_pc_window` | ✓ | | | -| `sio_state` | ✓ | | ✓ | -| `sio_trace` | ✓ | ✓ | | -| `sio_trace_reset` | | ✓ | | -| `sio_trace_window` | ✓ | | | -| `sio_write_window` | | ✓ | | -| `sp_ring` | ✓ | | | -| `spu_events` | ✓ | ✓ | | -| `spu_events_reset` | ✓ | | | -| `spu_ram` | ✓ | | | -| `spu_status` | ✓ | | | -| `spu_voices` | ✓ | ✓ | | -| `sreg_trace_clear` | ✓ | | | -| `sreg_trace_dump` | ✓ | | | -| `sreg_trace_find` | ✓ | | | -| `sreg_trace_stats` | ✓ | | | -| `stack_profile` | ✓ | | | -| `starv_ring` | ✓ | | | -| `step` | ✓ | | ✓ | -| `synth_recurse` | ✓ | | | -| `thread_ctx_ring` | ✓ | | | -| `thread_trace` | ✓ | | | -| `thread_trace_clear` | ✓ | | | -| `timers_state` | ✓ | | | -| `turbo` | ✓ | | ✓ | -| `turbo_audio_sink` | ✓ | | | -| `turbo_loads` | ✓ | | | -| `turbo_state` | ✓ | | ✓ | -| `unknown_dispatch_log` | ✓ | | | -| `unwatch` | ✓ | | ✓ | -| `vblank_rate` | ✓ | | | -| `vk_perf` | ✓ | | | -| `vram_peek` | ✓ | ✓ | ✓ | -| `vsync_query_hle` | ✓ | | | -| `warm_cd_route` | ✓ | | | -| `watch` | ✓ | | ✓ | -| `wide_full` | ✓ | | | -| `wide_shot` | ✓ | | | -| `write_ram` | ✓ | | ✓ | -| `ws_aspect` | ✓ | | | -| `ws_aspect_cone_site` | ✓ | | | -| `ws_backdrop_margin` | ✓ | | | -| `ws_backdrop_ring` | ✓ | | | -| `ws_backdrop_stretch` | ✓ | | | -| `ws_census` | ✓ | | | -| `ws_dbg_stretch` | ✓ | | | -| `ws_dome` | ✓ | | | -| `ws_dome_probe` | ✓ | | | -| `ws_far_threshold` | ✓ | | | -| `ws_hud_mode` | ✓ | | | -| `ws_margin` | ✓ | | | -| `ws_nw` | ✓ | | | -| `wtrace_add` | ✓ | | | -| `wtrace_all_dump` | ✓ | ✓ | | -| `wtrace_all_reset` | ✓ | ✓ | | -| `wtrace_all_stats` | ✓ | ✓ | | -| `wtrace_arm` | ✓ | ✓ | | -| `wtrace_boot_dump` | ✓ | | | -| `wtrace_boot_reset` | ✓ | | | -| `wtrace_boot_stats` | ✓ | | | -| `wtrace_boot_summary` | ✓ | | | -| `wtrace_clear` | ✓ | | ✓ | -| `wtrace_del` | ✓ | | | -| `wtrace_disarm` | ✓ | ✓ | | -| `wtrace_disarm_all` | ✓ | ✓ | | -| `wtrace_dump` | ✓ | ✓ | ✓ | -| `wtrace_range` | ✓ | | ✓ | -| `wtrace_ranges` | ✓ | ✓ | | -| `wtrace_reset` | ✓ | ✓ | | -| `wtrace_stats` | ✓ | ✓ | | -| `wtrace_trans_dump` | ✓ | | | -| `wtrace_trans_reset` | ✓ | | | -| `wtrace_trans_stats` | ✓ | | | -| `xlate` | ✓ | | | -| `xprobe` | ✓ | | | -| `xprobe_arm` | ✓ | | | - - +| `press` | ✓ | ✓ | | +| `probe_clear` | ✓ | | | +| `probe_trace` | ✓ | | | +| `quit` | ✓ | | ✓ | +| `ra_load_watch` | ✓ | | | +| `read_frame_ram` | ✓ | ✓ | ✓ | +| `read_ram` | ✓ | ✓ | ✓ | +| `record_frame` | ✓ | | | +| `record_frame_dump` | ✓ | | | +| `record_reads_dump` | ✓ | | | +| `restore_trace` | ✓ | | | +| `restore_trace_clear` | ✓ | | | +| `restore_trace_window` | ✓ | | | +| `rtrace_arm` | ✓ | ✓ | | +| `rtrace_clear` | ✓ | | | +| `rtrace_disarm` | | ✓ | | +| `rtrace_disarm_all` | | ✓ | | +| `rtrace_dump` | ✓ | ✓ | | +| `rtrace_ranges` | ✓ | ✓ | | +| `rtrace_reset` | | ✓ | | +| `rtrace_stats` | ✓ | ✓ | | +| `run_to_frame` | ✓ | | ✓ | +| `s3_smear_watch` | ✓ | | ✓ | +| `savestate` | ✓ | | | +| `scanline` | ✓ | | | +| `screenshot` | ✓ | ✓ | ✓ | +| `screenshot_file` | ✓ | ✓ | ✓ | +| `screenshot_hires` | ✓ | | ✓ | +| `set_input` | ✓ | ✓ | ✓ | +| `set_snapshot` | ✓ | ✓ | ✓ | +| `sio_arm_audit` | ✓ | | | +| `sio_burst_stats` | ✓ | | | +| `sio_ctrl_reg_clear` | ✓ | | | +| `sio_ctrl_reg_trace` | ✓ | | | +| `sio_ctrl_reg_window` | ✓ | | | +| `sio_irq_dump` | ✓ | | | +| `sio_irq_window` | ✓ | | | +| `sio_pc_trace` | ✓ | | | +| `sio_pc_window` | ✓ | | | +| `sio_state` | ✓ | | ✓ | +| `sio_trace` | ✓ | ✓ | | +| `sio_trace_reset` | | ✓ | | +| `sio_trace_window` | ✓ | | | +| `sio_write_window` | | ✓ | | +| `sp_ring` | ✓ | | | +| `spu_events` | ✓ | ✓ | | +| `spu_events_reset` | ✓ | | | +| `spu_ram` | ✓ | | | +| `spu_status` | ✓ | | | +| `spu_voices` | ✓ | ✓ | | +| `sreg_trace_clear` | ✓ | | | +| `sreg_trace_dump` | ✓ | | | +| `sreg_trace_find` | ✓ | | | +| `sreg_trace_stats` | ✓ | | | +| `stack_profile` | ✓ | | | +| `starv_ring` | ✓ | | | +| `step` | ✓ | | ✓ | +| `synth_recurse` | ✓ | | | +| `thread_ctx_ring` | ✓ | | | +| `thread_trace` | ✓ | | | +| `thread_trace_clear` | ✓ | | | +| `timers_state` | ✓ | | | +| `turbo` | ✓ | | ✓ | +| `turbo_audio_sink` | ✓ | | | +| `turbo_loads` | ✓ | | | +| `turbo_state` | ✓ | | ✓ | +| `unknown_dispatch_log` | ✓ | | | +| `unwatch` | ✓ | | ✓ | +| `vblank_rate` | ✓ | | | +| `vk_perf` | ✓ | | | +| `vram_peek` | ✓ | ✓ | ✓ | +| `vsync_query_hle` | ✓ | | | +| `warm_cd_route` | ✓ | | | +| `watch` | ✓ | | ✓ | +| `wide_full` | ✓ | | | +| `wide_shot` | ✓ | | | +| `write_ram` | ✓ | | ✓ | +| `ws_aspect` | ✓ | | | +| `ws_aspect_cone_site` | ✓ | | | +| `ws_backdrop_margin` | ✓ | | | +| `ws_backdrop_ring` | ✓ | | | +| `ws_backdrop_stretch` | ✓ | | | +| `ws_census` | ✓ | | | +| `ws_dbg_stretch` | ✓ | | | +| `ws_dome` | ✓ | | | +| `ws_dome_probe` | ✓ | | | +| `ws_far_threshold` | ✓ | | | +| `ws_hud_mode` | ✓ | | | +| `ws_margin` | ✓ | | | +| `ws_nw` | ✓ | | | +| `wtrace_add` | ✓ | | | +| `wtrace_all_dump` | ✓ | ✓ | | +| `wtrace_all_reset` | ✓ | ✓ | | +| `wtrace_all_stats` | ✓ | ✓ | | +| `wtrace_arm` | ✓ | ✓ | | +| `wtrace_boot_dump` | ✓ | | | +| `wtrace_boot_reset` | ✓ | | | +| `wtrace_boot_stats` | ✓ | | | +| `wtrace_boot_summary` | ✓ | | | +| `wtrace_clear` | ✓ | | ✓ | +| `wtrace_del` | ✓ | | | +| `wtrace_disarm` | ✓ | ✓ | | +| `wtrace_disarm_all` | ✓ | ✓ | | +| `wtrace_dump` | ✓ | ✓ | ✓ | +| `wtrace_range` | ✓ | | ✓ | +| `wtrace_ranges` | ✓ | ✓ | | +| `wtrace_reset` | ✓ | ✓ | | +| `wtrace_stats` | ✓ | ✓ | | +| `wtrace_trans_dump` | ✓ | | | +| `wtrace_trans_reset` | ✓ | | | +| `wtrace_trans_stats` | ✓ | | | +| `xlate` | ✓ | | | +| `xprobe` | ✓ | | | +| `xprobe_arm` | ✓ | | | + + diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index f9e758a38..3312551e8 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -1,344 +1,344 @@ -/* gpu_gl_renderer.c — hardware OpenGL renderer backend. - * - * ARCHITECTURE (v2 — GPU-authoritative VRAM) - * ------------------------------------------- - * The FBO color texture (`s_hr_tex`, RGBA8, 1024*S x 512*S where S is the - * internal-resolution scale from [video] supersampling) is the single - * authoritative copy of VRAM. EVERY mutation goes through the GPU: - * - * - polys / rects / lines -> rasterized into the hr FBO - * - GP0(02h) fills -> scissored glClear (color + stencil) - * - VRAM->VRAM copies -> hr FBO -> scratch texture blit, then a - * masked quad draw back into the hr FBO - * - CPU->VRAM transfers -> written to the CPU mirror immediately and - * accumulated into a pending-upload rect; the - * rect is flushed (staging texture + quad into - * the hr FBO, plus a direct R16UI subimage) - * before the next GPU op, preserving op order - * - * PS1 MASK BIT (bit15). The FBO alpha channel carries bit15 exactly - * (1.0 = set), and a stencil buffer mirrors it (stencil bit0 == bit15): - * - "set mask" -> fragment alpha + stencil write value - * - "check mask"-> stencil test (pass iff stored == 0). The stencil write - * value is coupled to the test reference in GL, so when checking we use - * GL_INVERT on pass to write a 1 (stored is known 0 when the test - * passes) and GL_KEEP to write a 0. - * - textured prims (and copies/uploads, whose pixels carry per-texel STP - * bits) are drawn in TWO passes split by the STP bit via discard, so - * each pass writes a single known stencil value. The same split already - * existed for semi-transparent texture blending. - * - blending uses glBlendFuncSeparate so the alpha (mask) channel is - * always REPLACED by the source fragment's mask bit, never blended. - * - * TEXTURE SAMPLING / RENDER-TO-TEXTURE. Textured prims sample a native-res - * R16UI mirror (`s_raw_tex`) holding raw 1555 VRAM values (CLUT decode + - * texture window + optional bilinear in the fragment shader). GPU draws - * mark a native-coords dirty union; before any textured draw whose texture - * page or CLUT intersects the union, a PACK pass re-encodes the dirty - * region of the hr FBO into the raw mirror (point-sampled at native - * coords). CPU->VRAM uploads update the raw mirror directly. So content - * rendered by the GPU is immediately valid as a texture source. - * - * CPU READBACKS (VRAM->CPU transfers, GPUREAD, screenshots, 24-bit FMV - * display) flush uploads + pack, then glReadPixels the raw mirror straight - * into the CPU VRAM array (raw 1555, no conversion loop). - * - * PRESENT is deterministic: 15-bit frames always blit the display region - * from the hr FBO into a 4:3 letterboxed rect (single path — no more - * frame-to-frame alternation between FBO and CPU presents). 24-bit (FMV) - * frames sync to CPU and use the quad-present path, also letterboxed. - * PSX_GL_FORCE_CPU_PRESENT=1 (read by main.cpp) forces the CPU path as a - * diagnostic. - * - * Known divergences from the software rasterizer (accepted, documented): - * - GL triangle/line coverage rules differ from the PS1 DDA by ±1px on - * edges; lines use GL_LINES (width S) instead of Bresenham. - * - No dithering (the software path doesn't dither either). - * - Gouraud interpolation happens at 8-bit precision instead of 5-bit - * (smoother gradients; readback re-quantizes to 5-bit). - * - VRAM-wrapping draws are clamped, except GP0(02h) fills which split - * into wrapped segments. Wrapping copies/draws are unused by real SDKs. - * - * Init is all-or-nothing: if any shader/FBO fails, gl_renderer_init_context - * returns 0 and the runtime falls back to the pure software renderer — no - * half-GL hybrid. */ - -#include "gpu.h" -#include "gpu_render.h" +/* gpu_gl_renderer.c — hardware OpenGL renderer backend. + * + * ARCHITECTURE (v2 — GPU-authoritative VRAM) + * ------------------------------------------- + * The FBO color texture (`s_hr_tex`, RGBA8, 1024*S x 512*S where S is the + * internal-resolution scale from [video] supersampling) is the single + * authoritative copy of VRAM. EVERY mutation goes through the GPU: + * + * - polys / rects / lines -> rasterized into the hr FBO + * - GP0(02h) fills -> scissored glClear (color + stencil) + * - VRAM->VRAM copies -> hr FBO -> scratch texture blit, then a + * masked quad draw back into the hr FBO + * - CPU->VRAM transfers -> written to the CPU mirror immediately and + * accumulated into a pending-upload rect; the + * rect is flushed (staging texture + quad into + * the hr FBO, plus a direct R16UI subimage) + * before the next GPU op, preserving op order + * + * PS1 MASK BIT (bit15). The FBO alpha channel carries bit15 exactly + * (1.0 = set), and a stencil buffer mirrors it (stencil bit0 == bit15): + * - "set mask" -> fragment alpha + stencil write value + * - "check mask"-> stencil test (pass iff stored == 0). The stencil write + * value is coupled to the test reference in GL, so when checking we use + * GL_INVERT on pass to write a 1 (stored is known 0 when the test + * passes) and GL_KEEP to write a 0. + * - textured prims (and copies/uploads, whose pixels carry per-texel STP + * bits) are drawn in TWO passes split by the STP bit via discard, so + * each pass writes a single known stencil value. The same split already + * existed for semi-transparent texture blending. + * - blending uses glBlendFuncSeparate so the alpha (mask) channel is + * always REPLACED by the source fragment's mask bit, never blended. + * + * TEXTURE SAMPLING / RENDER-TO-TEXTURE. Textured prims sample a native-res + * R16UI mirror (`s_raw_tex`) holding raw 1555 VRAM values (CLUT decode + + * texture window + optional bilinear in the fragment shader). GPU draws + * mark a native-coords dirty union; before any textured draw whose texture + * page or CLUT intersects the union, a PACK pass re-encodes the dirty + * region of the hr FBO into the raw mirror (point-sampled at native + * coords). CPU->VRAM uploads update the raw mirror directly. So content + * rendered by the GPU is immediately valid as a texture source. + * + * CPU READBACKS (VRAM->CPU transfers, GPUREAD, screenshots, 24-bit FMV + * display) flush uploads + pack, then glReadPixels the raw mirror straight + * into the CPU VRAM array (raw 1555, no conversion loop). + * + * PRESENT is deterministic: 15-bit frames always blit the display region + * from the hr FBO into a 4:3 letterboxed rect (single path — no more + * frame-to-frame alternation between FBO and CPU presents). 24-bit (FMV) + * frames sync to CPU and use the quad-present path, also letterboxed. + * PSX_GL_FORCE_CPU_PRESENT=1 (read by main.cpp) forces the CPU path as a + * diagnostic. + * + * Known divergences from the software rasterizer (accepted, documented): + * - GL triangle/line coverage rules differ from the PS1 DDA by ±1px on + * edges; lines use GL_LINES (width S) instead of Bresenham. + * - No dithering (the software path doesn't dither either). + * - Gouraud interpolation happens at 8-bit precision instead of 5-bit + * (smoother gradients; readback re-quantizes to 5-bit). + * - VRAM-wrapping draws are clamped, except GP0(02h) fills which split + * into wrapped segments. Wrapping copies/draws are unused by real SDKs. + * + * Init is all-or-nothing: if any shader/FBO fails, gl_renderer_init_context + * returns 0 and the runtime falls back to the pure software renderer — no + * half-GL hybrid. */ + +#include "gpu.h" +#include "gpu_render.h" #include "gpu_sw_renderer.h" #include "gpu_gl_renderer.h" #include "frame_interpolation.h" #include "host_osd.h" #include "psx_savestate_menu.h" -#include "host_time.h" -#include "latency_ring.h" -#include "frame_pacing.h" -#include "psx_rewind.h" - -#include "psx_sdl.h" -#if defined(PSX_SDL3) -#include -#else -#include -#endif -#include -#include -#include -#include -#include +#include "host_time.h" +#include "latency_ring.h" +#include "frame_pacing.h" +#include "psx_rewind.h" + +#include "psx_sdl.h" +#if defined(PSX_SDL3) +#include +#else +#include +#endif +#include +#include +#include +#include +#include #include "png_write.h" /* png_write_rgb — present_shot readback */ - -#ifndef GL_BGRA -#define GL_BGRA 0x80E1 -#endif -#ifndef GL_CLAMP_TO_EDGE -#define GL_CLAMP_TO_EDGE 0x812F -#endif -#ifndef GL_RGBA8 -#define GL_RGBA8 0x8058 -#endif -/* GL 1.4+ enums absent from MinGW's GL 1.1 headers. */ -#define PSXGL_FRAGMENT_SHADER 0x8B30 -#define PSXGL_VERTEX_SHADER 0x8B31 -#define PSXGL_COMPILE_STATUS 0x8B81 -#define PSXGL_LINK_STATUS 0x8B82 -#define PSXGL_TEXTURE0 0x84C0 -#define PSXGL_ARRAY_BUFFER 0x8892 -#define PSXGL_STREAM_DRAW 0x88E0 -#define PSXGL_FRAMEBUFFER 0x8D40 -#define PSXGL_READ_FRAMEBUFFER 0x8CA8 -#define PSXGL_DRAW_FRAMEBUFFER 0x8CA9 -#define PSXGL_COLOR_ATTACHMENT0 0x8CE0 -#define PSXGL_DEPTH_STENCIL_ATTACHMENT 0x821A -#define PSXGL_FRAMEBUFFER_COMPLETE 0x8CD5 -#define PSXGL_RENDERBUFFER 0x8D41 -#define PSXGL_DEPTH24_STENCIL8 0x88F0 -#define PSXGL_R16UI 0x8234 -#define PSXGL_RED_INTEGER 0x8D94 -#define PSXGL_FUNC_ADD 0x8006 -#define PSXGL_FUNC_REVERSE_SUBTRACT 0x800B -#define PSXGL_CONSTANT_ALPHA 0x8003 -#define PSXGL_UNPACK_ROW_LENGTH 0x0CF2 -#define PSXGL_SRC1_ALPHA 0x8589 - -#ifndef APIENTRY -#define APIENTRY -#endif - -#define VRAM_W 1024 -#define VRAM_H 512 -#define GL_MAX_INTERNAL_SCALE 4 - -/* ---- Loaded modern-GL entry points ------------------------------------- */ -typedef GLuint (APIENTRY *PFN_glCreateShader)(GLenum); -typedef void (APIENTRY *PFN_glShaderSource)(GLuint, GLsizei, const char *const *, const GLint *); -typedef void (APIENTRY *PFN_glCompileShader)(GLuint); -typedef void (APIENTRY *PFN_glGetShaderiv)(GLuint, GLenum, GLint *); -typedef void (APIENTRY *PFN_glGetShaderInfoLog)(GLuint, GLsizei, GLsizei *, char *); -typedef void (APIENTRY *PFN_glDeleteShader)(GLuint); -typedef GLuint (APIENTRY *PFN_glCreateProgram)(void); -typedef void (APIENTRY *PFN_glAttachShader)(GLuint, GLuint); -typedef void (APIENTRY *PFN_glLinkProgram)(GLuint); -typedef void (APIENTRY *PFN_glGetProgramiv)(GLuint, GLenum, GLint *); -typedef void (APIENTRY *PFN_glGetProgramInfoLog)(GLuint, GLsizei, GLsizei *, char *); -typedef void (APIENTRY *PFN_glUseProgram)(GLuint); -typedef GLint (APIENTRY *PFN_glGetUniformLocation)(GLuint, const char *); -typedef void (APIENTRY *PFN_glUniform1i)(GLint, GLint); -typedef void (APIENTRY *PFN_glUniform1f)(GLint, GLfloat); -typedef void (APIENTRY *PFN_glUniform2i)(GLint, GLint, GLint); -typedef void (APIENTRY *PFN_glUniform4i)(GLint, GLint, GLint, GLint, GLint); + +#ifndef GL_BGRA +#define GL_BGRA 0x80E1 +#endif +#ifndef GL_CLAMP_TO_EDGE +#define GL_CLAMP_TO_EDGE 0x812F +#endif +#ifndef GL_RGBA8 +#define GL_RGBA8 0x8058 +#endif +/* GL 1.4+ enums absent from MinGW's GL 1.1 headers. */ +#define PSXGL_FRAGMENT_SHADER 0x8B30 +#define PSXGL_VERTEX_SHADER 0x8B31 +#define PSXGL_COMPILE_STATUS 0x8B81 +#define PSXGL_LINK_STATUS 0x8B82 +#define PSXGL_TEXTURE0 0x84C0 +#define PSXGL_ARRAY_BUFFER 0x8892 +#define PSXGL_STREAM_DRAW 0x88E0 +#define PSXGL_FRAMEBUFFER 0x8D40 +#define PSXGL_READ_FRAMEBUFFER 0x8CA8 +#define PSXGL_DRAW_FRAMEBUFFER 0x8CA9 +#define PSXGL_COLOR_ATTACHMENT0 0x8CE0 +#define PSXGL_DEPTH_STENCIL_ATTACHMENT 0x821A +#define PSXGL_FRAMEBUFFER_COMPLETE 0x8CD5 +#define PSXGL_RENDERBUFFER 0x8D41 +#define PSXGL_DEPTH24_STENCIL8 0x88F0 +#define PSXGL_R16UI 0x8234 +#define PSXGL_RED_INTEGER 0x8D94 +#define PSXGL_FUNC_ADD 0x8006 +#define PSXGL_FUNC_REVERSE_SUBTRACT 0x800B +#define PSXGL_CONSTANT_ALPHA 0x8003 +#define PSXGL_UNPACK_ROW_LENGTH 0x0CF2 +#define PSXGL_SRC1_ALPHA 0x8589 + +#ifndef APIENTRY +#define APIENTRY +#endif + +#define VRAM_W 1024 +#define VRAM_H 512 +#define GL_MAX_INTERNAL_SCALE 4 + +/* ---- Loaded modern-GL entry points ------------------------------------- */ +typedef GLuint (APIENTRY *PFN_glCreateShader)(GLenum); +typedef void (APIENTRY *PFN_glShaderSource)(GLuint, GLsizei, const char *const *, const GLint *); +typedef void (APIENTRY *PFN_glCompileShader)(GLuint); +typedef void (APIENTRY *PFN_glGetShaderiv)(GLuint, GLenum, GLint *); +typedef void (APIENTRY *PFN_glGetShaderInfoLog)(GLuint, GLsizei, GLsizei *, char *); +typedef void (APIENTRY *PFN_glDeleteShader)(GLuint); +typedef GLuint (APIENTRY *PFN_glCreateProgram)(void); +typedef void (APIENTRY *PFN_glAttachShader)(GLuint, GLuint); +typedef void (APIENTRY *PFN_glLinkProgram)(GLuint); +typedef void (APIENTRY *PFN_glGetProgramiv)(GLuint, GLenum, GLint *); +typedef void (APIENTRY *PFN_glGetProgramInfoLog)(GLuint, GLsizei, GLsizei *, char *); +typedef void (APIENTRY *PFN_glUseProgram)(GLuint); +typedef GLint (APIENTRY *PFN_glGetUniformLocation)(GLuint, const char *); +typedef void (APIENTRY *PFN_glUniform1i)(GLint, GLint); +typedef void (APIENTRY *PFN_glUniform1f)(GLint, GLfloat); +typedef void (APIENTRY *PFN_glUniform2i)(GLint, GLint, GLint); +typedef void (APIENTRY *PFN_glUniform4i)(GLint, GLint, GLint, GLint, GLint); typedef void (APIENTRY *PFN_glUniform2f)(GLint, GLfloat, GLfloat); -typedef void (APIENTRY *PFN_glUniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat); -typedef void (APIENTRY *PFN_glBlendColor)(GLfloat, GLfloat, GLfloat, GLfloat); -typedef void (APIENTRY *PFN_glBlendFuncSeparate)(GLenum, GLenum, GLenum, GLenum); -typedef void (APIENTRY *PFN_glBlendEquationSeparate)(GLenum, GLenum); -typedef void (APIENTRY *PFN_glGenVertexArrays)(GLsizei, GLuint *); -typedef void (APIENTRY *PFN_glBindVertexArray)(GLuint); -typedef void (APIENTRY *PFN_glActiveTexture)(GLenum); -typedef void (APIENTRY *PFN_glGenBuffers)(GLsizei, GLuint *); -typedef void (APIENTRY *PFN_glBindBuffer)(GLenum, GLuint); -typedef void (APIENTRY *PFN_glBufferData)(GLenum, ptrdiff_t, const void *, GLenum); -typedef void (APIENTRY *PFN_glVertexAttribPointer)(GLuint, GLint, GLenum, GLboolean, GLsizei, const void *); -typedef void (APIENTRY *PFN_glEnableVertexAttribArray)(GLuint); -typedef void (APIENTRY *PFN_glBindFragDataLocationIndexed)(GLuint, GLuint, GLuint, const char *); -typedef void (APIENTRY *PFN_glGenFramebuffers)(GLsizei, GLuint *); -typedef void (APIENTRY *PFN_glDeleteFramebuffers)(GLsizei, const GLuint *); -typedef void (APIENTRY *PFN_glBindFramebuffer)(GLenum, GLuint); -typedef void (APIENTRY *PFN_glFramebufferTexture2D)(GLenum, GLenum, GLenum, GLuint, GLint); -typedef GLenum (APIENTRY *PFN_glCheckFramebufferStatus)(GLenum); -typedef void (APIENTRY *PFN_glBlitFramebuffer)(GLint,GLint,GLint,GLint,GLint,GLint,GLint,GLint,GLbitfield,GLenum); -typedef void (APIENTRY *PFN_glGenRenderbuffers)(GLsizei, GLuint *); -typedef void (APIENTRY *PFN_glDeleteRenderbuffers)(GLsizei, const GLuint *); -typedef void (APIENTRY *PFN_glBindRenderbuffer)(GLenum, GLuint); -typedef void (APIENTRY *PFN_glRenderbufferStorage)(GLenum, GLenum, GLsizei, GLsizei); -typedef void (APIENTRY *PFN_glFramebufferRenderbuffer)(GLenum, GLenum, GLenum, GLuint); -/* GPU timer queries (ARB_timer_query / core GL 3.3) — frame_perf instrumentation. */ -typedef void (APIENTRY *PFN_glGenQueries)(GLsizei, GLuint *); -typedef void (APIENTRY *PFN_glDeleteQueries)(GLsizei, const GLuint *); -typedef void (APIENTRY *PFN_glBeginQuery)(GLenum, GLuint); -typedef void (APIENTRY *PFN_glEndQuery)(GLenum); -typedef void (APIENTRY *PFN_glGetQueryObjectui64v)(GLuint, GLenum, GLuint64 *); -typedef void (APIENTRY *PFN_glGetQueryObjectiv)(GLuint, GLenum, GLint *); -typedef void (APIENTRY *PFN_glQueryCounter)(GLuint, GLenum); -#ifndef GL_TIME_ELAPSED -#define GL_TIME_ELAPSED 0x88BF -#endif -#ifndef GL_TIMESTAMP -#define GL_TIMESTAMP 0x8E28 -#endif -#ifndef GL_QUERY_RESULT -#define GL_QUERY_RESULT 0x8866 -#endif -#ifndef GL_QUERY_RESULT_AVAILABLE -#define GL_QUERY_RESULT_AVAILABLE 0x8867 -#endif - -static PFN_glCreateShader p_glCreateShader; -static PFN_glShaderSource p_glShaderSource; -static PFN_glCompileShader p_glCompileShader; -static PFN_glGetShaderiv p_glGetShaderiv; -static PFN_glGetShaderInfoLog p_glGetShaderInfoLog; -static PFN_glDeleteShader p_glDeleteShader; -static PFN_glCreateProgram p_glCreateProgram; -static PFN_glAttachShader p_glAttachShader; -static PFN_glLinkProgram p_glLinkProgram; -static PFN_glGetProgramiv p_glGetProgramiv; -static PFN_glGetProgramInfoLog p_glGetProgramInfoLog; -static PFN_glUseProgram p_glUseProgram; -static PFN_glGetUniformLocation p_glGetUniformLocation; -static PFN_glUniform1i p_glUniform1i; -static PFN_glUniform1f p_glUniform1f; -static PFN_glUniform2i p_glUniform2i; -static PFN_glUniform4i p_glUniform4i; +typedef void (APIENTRY *PFN_glUniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat); +typedef void (APIENTRY *PFN_glBlendColor)(GLfloat, GLfloat, GLfloat, GLfloat); +typedef void (APIENTRY *PFN_glBlendFuncSeparate)(GLenum, GLenum, GLenum, GLenum); +typedef void (APIENTRY *PFN_glBlendEquationSeparate)(GLenum, GLenum); +typedef void (APIENTRY *PFN_glGenVertexArrays)(GLsizei, GLuint *); +typedef void (APIENTRY *PFN_glBindVertexArray)(GLuint); +typedef void (APIENTRY *PFN_glActiveTexture)(GLenum); +typedef void (APIENTRY *PFN_glGenBuffers)(GLsizei, GLuint *); +typedef void (APIENTRY *PFN_glBindBuffer)(GLenum, GLuint); +typedef void (APIENTRY *PFN_glBufferData)(GLenum, ptrdiff_t, const void *, GLenum); +typedef void (APIENTRY *PFN_glVertexAttribPointer)(GLuint, GLint, GLenum, GLboolean, GLsizei, const void *); +typedef void (APIENTRY *PFN_glEnableVertexAttribArray)(GLuint); +typedef void (APIENTRY *PFN_glBindFragDataLocationIndexed)(GLuint, GLuint, GLuint, const char *); +typedef void (APIENTRY *PFN_glGenFramebuffers)(GLsizei, GLuint *); +typedef void (APIENTRY *PFN_glDeleteFramebuffers)(GLsizei, const GLuint *); +typedef void (APIENTRY *PFN_glBindFramebuffer)(GLenum, GLuint); +typedef void (APIENTRY *PFN_glFramebufferTexture2D)(GLenum, GLenum, GLenum, GLuint, GLint); +typedef GLenum (APIENTRY *PFN_glCheckFramebufferStatus)(GLenum); +typedef void (APIENTRY *PFN_glBlitFramebuffer)(GLint,GLint,GLint,GLint,GLint,GLint,GLint,GLint,GLbitfield,GLenum); +typedef void (APIENTRY *PFN_glGenRenderbuffers)(GLsizei, GLuint *); +typedef void (APIENTRY *PFN_glDeleteRenderbuffers)(GLsizei, const GLuint *); +typedef void (APIENTRY *PFN_glBindRenderbuffer)(GLenum, GLuint); +typedef void (APIENTRY *PFN_glRenderbufferStorage)(GLenum, GLenum, GLsizei, GLsizei); +typedef void (APIENTRY *PFN_glFramebufferRenderbuffer)(GLenum, GLenum, GLenum, GLuint); +/* GPU timer queries (ARB_timer_query / core GL 3.3) — frame_perf instrumentation. */ +typedef void (APIENTRY *PFN_glGenQueries)(GLsizei, GLuint *); +typedef void (APIENTRY *PFN_glDeleteQueries)(GLsizei, const GLuint *); +typedef void (APIENTRY *PFN_glBeginQuery)(GLenum, GLuint); +typedef void (APIENTRY *PFN_glEndQuery)(GLenum); +typedef void (APIENTRY *PFN_glGetQueryObjectui64v)(GLuint, GLenum, GLuint64 *); +typedef void (APIENTRY *PFN_glGetQueryObjectiv)(GLuint, GLenum, GLint *); +typedef void (APIENTRY *PFN_glQueryCounter)(GLuint, GLenum); +#ifndef GL_TIME_ELAPSED +#define GL_TIME_ELAPSED 0x88BF +#endif +#ifndef GL_TIMESTAMP +#define GL_TIMESTAMP 0x8E28 +#endif +#ifndef GL_QUERY_RESULT +#define GL_QUERY_RESULT 0x8866 +#endif +#ifndef GL_QUERY_RESULT_AVAILABLE +#define GL_QUERY_RESULT_AVAILABLE 0x8867 +#endif + +static PFN_glCreateShader p_glCreateShader; +static PFN_glShaderSource p_glShaderSource; +static PFN_glCompileShader p_glCompileShader; +static PFN_glGetShaderiv p_glGetShaderiv; +static PFN_glGetShaderInfoLog p_glGetShaderInfoLog; +static PFN_glDeleteShader p_glDeleteShader; +static PFN_glCreateProgram p_glCreateProgram; +static PFN_glAttachShader p_glAttachShader; +static PFN_glLinkProgram p_glLinkProgram; +static PFN_glGetProgramiv p_glGetProgramiv; +static PFN_glGetProgramInfoLog p_glGetProgramInfoLog; +static PFN_glUseProgram p_glUseProgram; +static PFN_glGetUniformLocation p_glGetUniformLocation; +static PFN_glUniform1i p_glUniform1i; +static PFN_glUniform1f p_glUniform1f; +static PFN_glUniform2i p_glUniform2i; +static PFN_glUniform4i p_glUniform4i; static PFN_glUniform2f p_glUniform2f; -static PFN_glUniform4f p_glUniform4f; -static PFN_glBlendColor p_glBlendColor; -static PFN_glBlendFuncSeparate p_glBlendFuncSeparate; -static PFN_glBlendEquationSeparate p_glBlendEquationSeparate; -static PFN_glGenVertexArrays p_glGenVertexArrays; -static PFN_glBindVertexArray p_glBindVertexArray; -static PFN_glActiveTexture p_glActiveTexture; -static PFN_glGenBuffers p_glGenBuffers; -static PFN_glBindBuffer p_glBindBuffer; -static PFN_glBufferData p_glBufferData; -static PFN_glVertexAttribPointer p_glVertexAttribPointer; -static PFN_glEnableVertexAttribArray p_glEnableVertexAttribArray; -static PFN_glBindFragDataLocationIndexed p_glBindFragDataLocationIndexed; -static PFN_glGenFramebuffers p_glGenFramebuffers; -static PFN_glDeleteFramebuffers p_glDeleteFramebuffers; -static PFN_glBindFramebuffer p_glBindFramebuffer; -static PFN_glFramebufferTexture2D p_glFramebufferTexture2D; -static PFN_glCheckFramebufferStatus p_glCheckFramebufferStatus; -static PFN_glBlitFramebuffer p_glBlitFramebuffer; -static PFN_glGenQueries p_glGenQueries; -static PFN_glDeleteQueries p_glDeleteQueries; -static PFN_glBeginQuery p_glBeginQuery; -static PFN_glEndQuery p_glEndQuery; -static PFN_glGetQueryObjectui64v p_glGetQueryObjectui64v; -static PFN_glGetQueryObjectiv p_glGetQueryObjectiv; -static PFN_glQueryCounter p_glQueryCounter; -static void gl_perf_init(void); /* frame_perf — defined below, called from init_gpu_raster */ -static void gl_perf_mirror_begin(void); /* frame_perf: GPU-time bracket around ONE native-wide */ -static void gl_perf_mirror_end(void); /* mirror pass (timestamp pair; splits scene canonical/mirror) */ -/* Native-wide mirror ABLATION (perf attribution, debug cmd gl_ws_ablate): - * 0 = normal, 1 = skip the whole mirror pass, 2 = full mirror state churn but no - * draw calls, 3 = mirror draws land in the hr FBO (no per-pass FBO rebind; wide - * margins go stale + hr gets garbage — perf probe only). */ -static int s_ws_ablate = 0; -static void flush_tex_batch(void); /* textured-prim batch — defined below, flushed from coherency points */ -static void flush_flat_batch(void); /* flat/gouraud GEO batch (MotK starfield 0x68 dots) */ -static PFN_glGenRenderbuffers p_glGenRenderbuffers; -static PFN_glDeleteRenderbuffers p_glDeleteRenderbuffers; -static PFN_glBindRenderbuffer p_glBindRenderbuffer; -static PFN_glRenderbufferStorage p_glRenderbufferStorage; -static PFN_glFramebufferRenderbuffer p_glFramebufferRenderbuffer; - -static int load_modern_gl(void) { - int ok = 1; -#define LOAD(p, n) do { p = (void *)SDL_GL_GetProcAddress(n); if (!p) ok = 0; } while (0) - LOAD(p_glCreateShader, "glCreateShader"); LOAD(p_glShaderSource, "glShaderSource"); - LOAD(p_glCompileShader, "glCompileShader"); LOAD(p_glGetShaderiv, "glGetShaderiv"); - LOAD(p_glGetShaderInfoLog, "glGetShaderInfoLog"); LOAD(p_glDeleteShader, "glDeleteShader"); - LOAD(p_glCreateProgram, "glCreateProgram"); LOAD(p_glAttachShader, "glAttachShader"); - LOAD(p_glLinkProgram, "glLinkProgram"); LOAD(p_glGetProgramiv, "glGetProgramiv"); - LOAD(p_glGetProgramInfoLog, "glGetProgramInfoLog"); LOAD(p_glUseProgram, "glUseProgram"); - LOAD(p_glGetUniformLocation, "glGetUniformLocation"); LOAD(p_glUniform1i, "glUniform1i"); - LOAD(p_glUniform1f, "glUniform1f"); - LOAD(p_glUniform2i, "glUniform2i"); LOAD(p_glUniform4i, "glUniform4i"); +static PFN_glUniform4f p_glUniform4f; +static PFN_glBlendColor p_glBlendColor; +static PFN_glBlendFuncSeparate p_glBlendFuncSeparate; +static PFN_glBlendEquationSeparate p_glBlendEquationSeparate; +static PFN_glGenVertexArrays p_glGenVertexArrays; +static PFN_glBindVertexArray p_glBindVertexArray; +static PFN_glActiveTexture p_glActiveTexture; +static PFN_glGenBuffers p_glGenBuffers; +static PFN_glBindBuffer p_glBindBuffer; +static PFN_glBufferData p_glBufferData; +static PFN_glVertexAttribPointer p_glVertexAttribPointer; +static PFN_glEnableVertexAttribArray p_glEnableVertexAttribArray; +static PFN_glBindFragDataLocationIndexed p_glBindFragDataLocationIndexed; +static PFN_glGenFramebuffers p_glGenFramebuffers; +static PFN_glDeleteFramebuffers p_glDeleteFramebuffers; +static PFN_glBindFramebuffer p_glBindFramebuffer; +static PFN_glFramebufferTexture2D p_glFramebufferTexture2D; +static PFN_glCheckFramebufferStatus p_glCheckFramebufferStatus; +static PFN_glBlitFramebuffer p_glBlitFramebuffer; +static PFN_glGenQueries p_glGenQueries; +static PFN_glDeleteQueries p_glDeleteQueries; +static PFN_glBeginQuery p_glBeginQuery; +static PFN_glEndQuery p_glEndQuery; +static PFN_glGetQueryObjectui64v p_glGetQueryObjectui64v; +static PFN_glGetQueryObjectiv p_glGetQueryObjectiv; +static PFN_glQueryCounter p_glQueryCounter; +static void gl_perf_init(void); /* frame_perf — defined below, called from init_gpu_raster */ +static void gl_perf_mirror_begin(void); /* frame_perf: GPU-time bracket around ONE native-wide */ +static void gl_perf_mirror_end(void); /* mirror pass (timestamp pair; splits scene canonical/mirror) */ +/* Native-wide mirror ABLATION (perf attribution, debug cmd gl_ws_ablate): + * 0 = normal, 1 = skip the whole mirror pass, 2 = full mirror state churn but no + * draw calls, 3 = mirror draws land in the hr FBO (no per-pass FBO rebind; wide + * margins go stale + hr gets garbage — perf probe only). */ +static int s_ws_ablate = 0; +static void flush_tex_batch(void); /* textured-prim batch — defined below, flushed from coherency points */ +static void flush_flat_batch(void); /* flat/gouraud GEO batch (MotK starfield 0x68 dots) */ +static PFN_glGenRenderbuffers p_glGenRenderbuffers; +static PFN_glDeleteRenderbuffers p_glDeleteRenderbuffers; +static PFN_glBindRenderbuffer p_glBindRenderbuffer; +static PFN_glRenderbufferStorage p_glRenderbufferStorage; +static PFN_glFramebufferRenderbuffer p_glFramebufferRenderbuffer; + +static int load_modern_gl(void) { + int ok = 1; +#define LOAD(p, n) do { p = (void *)SDL_GL_GetProcAddress(n); if (!p) ok = 0; } while (0) + LOAD(p_glCreateShader, "glCreateShader"); LOAD(p_glShaderSource, "glShaderSource"); + LOAD(p_glCompileShader, "glCompileShader"); LOAD(p_glGetShaderiv, "glGetShaderiv"); + LOAD(p_glGetShaderInfoLog, "glGetShaderInfoLog"); LOAD(p_glDeleteShader, "glDeleteShader"); + LOAD(p_glCreateProgram, "glCreateProgram"); LOAD(p_glAttachShader, "glAttachShader"); + LOAD(p_glLinkProgram, "glLinkProgram"); LOAD(p_glGetProgramiv, "glGetProgramiv"); + LOAD(p_glGetProgramInfoLog, "glGetProgramInfoLog"); LOAD(p_glUseProgram, "glUseProgram"); + LOAD(p_glGetUniformLocation, "glGetUniformLocation"); LOAD(p_glUniform1i, "glUniform1i"); + LOAD(p_glUniform1f, "glUniform1f"); + LOAD(p_glUniform2i, "glUniform2i"); LOAD(p_glUniform4i, "glUniform4i"); LOAD(p_glUniform2f, "glUniform2f"); - LOAD(p_glUniform4f, "glUniform4f"); - LOAD(p_glBlendColor, "glBlendColor"); - LOAD(p_glBlendFuncSeparate, "glBlendFuncSeparate"); - LOAD(p_glBlendEquationSeparate, "glBlendEquationSeparate"); - LOAD(p_glGenVertexArrays, "glGenVertexArrays"); LOAD(p_glBindVertexArray, "glBindVertexArray"); - LOAD(p_glActiveTexture, "glActiveTexture"); LOAD(p_glGenBuffers, "glGenBuffers"); - LOAD(p_glBindBuffer, "glBindBuffer"); LOAD(p_glBufferData, "glBufferData"); - LOAD(p_glVertexAttribPointer, "glVertexAttribPointer"); - LOAD(p_glEnableVertexAttribArray, "glEnableVertexAttribArray"); - LOAD(p_glBindFragDataLocationIndexed, "glBindFragDataLocationIndexed"); - LOAD(p_glGenFramebuffers, "glGenFramebuffers"); LOAD(p_glBindFramebuffer, "glBindFramebuffer"); - LOAD(p_glDeleteFramebuffers, "glDeleteFramebuffers"); - LOAD(p_glFramebufferTexture2D, "glFramebufferTexture2D"); - LOAD(p_glCheckFramebufferStatus, "glCheckFramebufferStatus"); - LOAD(p_glBlitFramebuffer, "glBlitFramebuffer"); - LOAD(p_glGenRenderbuffers, "glGenRenderbuffers"); - LOAD(p_glDeleteRenderbuffers, "glDeleteRenderbuffers"); - LOAD(p_glBindRenderbuffer, "glBindRenderbuffer"); - LOAD(p_glRenderbufferStorage, "glRenderbufferStorage"); - LOAD(p_glFramebufferRenderbuffer, "glFramebufferRenderbuffer"); - /* GPU timer queries — optional (frame_perf). Don't fail the renderer if - * absent; gl_perf just stays disabled. */ - p_glGenQueries = (void *)SDL_GL_GetProcAddress("glGenQueries"); - p_glDeleteQueries = (void *)SDL_GL_GetProcAddress("glDeleteQueries"); - p_glBeginQuery = (void *)SDL_GL_GetProcAddress("glBeginQuery"); - p_glEndQuery = (void *)SDL_GL_GetProcAddress("glEndQuery"); - p_glGetQueryObjectui64v = (void *)SDL_GL_GetProcAddress("glGetQueryObjectui64v"); - p_glGetQueryObjectiv = (void *)SDL_GL_GetProcAddress("glGetQueryObjectiv"); - p_glQueryCounter = (void *)SDL_GL_GetProcAddress("glQueryCounter"); -#undef LOAD - return ok; -} - -/* ---- state ------------------------------------------------------------- */ -static SDL_Window *s_win = NULL; -static SDL_GLContext s_ctx = NULL; -static uint16_t *s_vram = NULL; /* CPU VRAM array (gpu.c's storage) */ -static int s_swap_interval = 1; /* SDL_GL swap interval (vsync mode) */ - -static int s_scale = 1; /* internal-res scale (hr FBO) */ -/* Netplay: SW@1× + GPU@s_scale dual write; CPU VRAM always authoritative. */ -static int s_cpu_auth_dual = 0; -static int s_req_scale = 1; /* requested before context init */ - -static GLuint s_present_tex = 0; /* CPU-readout present path (24bpp) */ -static int s_present_w = 0, s_present_h = 0; -static GLuint s_osd_tex = 0; -static int s_osd_tw = 0, s_osd_th = 0; -static GLuint s_present_prog = 0, s_present_vao = 0; -static void gl_swap_with_osd(void); -static GLint s_present_uTex = -1, s_present_uUvRect = -1; + LOAD(p_glUniform4f, "glUniform4f"); + LOAD(p_glBlendColor, "glBlendColor"); + LOAD(p_glBlendFuncSeparate, "glBlendFuncSeparate"); + LOAD(p_glBlendEquationSeparate, "glBlendEquationSeparate"); + LOAD(p_glGenVertexArrays, "glGenVertexArrays"); LOAD(p_glBindVertexArray, "glBindVertexArray"); + LOAD(p_glActiveTexture, "glActiveTexture"); LOAD(p_glGenBuffers, "glGenBuffers"); + LOAD(p_glBindBuffer, "glBindBuffer"); LOAD(p_glBufferData, "glBufferData"); + LOAD(p_glVertexAttribPointer, "glVertexAttribPointer"); + LOAD(p_glEnableVertexAttribArray, "glEnableVertexAttribArray"); + LOAD(p_glBindFragDataLocationIndexed, "glBindFragDataLocationIndexed"); + LOAD(p_glGenFramebuffers, "glGenFramebuffers"); LOAD(p_glBindFramebuffer, "glBindFramebuffer"); + LOAD(p_glDeleteFramebuffers, "glDeleteFramebuffers"); + LOAD(p_glFramebufferTexture2D, "glFramebufferTexture2D"); + LOAD(p_glCheckFramebufferStatus, "glCheckFramebufferStatus"); + LOAD(p_glBlitFramebuffer, "glBlitFramebuffer"); + LOAD(p_glGenRenderbuffers, "glGenRenderbuffers"); + LOAD(p_glDeleteRenderbuffers, "glDeleteRenderbuffers"); + LOAD(p_glBindRenderbuffer, "glBindRenderbuffer"); + LOAD(p_glRenderbufferStorage, "glRenderbufferStorage"); + LOAD(p_glFramebufferRenderbuffer, "glFramebufferRenderbuffer"); + /* GPU timer queries — optional (frame_perf). Don't fail the renderer if + * absent; gl_perf just stays disabled. */ + p_glGenQueries = (void *)SDL_GL_GetProcAddress("glGenQueries"); + p_glDeleteQueries = (void *)SDL_GL_GetProcAddress("glDeleteQueries"); + p_glBeginQuery = (void *)SDL_GL_GetProcAddress("glBeginQuery"); + p_glEndQuery = (void *)SDL_GL_GetProcAddress("glEndQuery"); + p_glGetQueryObjectui64v = (void *)SDL_GL_GetProcAddress("glGetQueryObjectui64v"); + p_glGetQueryObjectiv = (void *)SDL_GL_GetProcAddress("glGetQueryObjectiv"); + p_glQueryCounter = (void *)SDL_GL_GetProcAddress("glQueryCounter"); +#undef LOAD + return ok; +} + +/* ---- state ------------------------------------------------------------- */ +static SDL_Window *s_win = NULL; +static SDL_GLContext s_ctx = NULL; +static uint16_t *s_vram = NULL; /* CPU VRAM array (gpu.c's storage) */ +static int s_swap_interval = 1; /* SDL_GL swap interval (vsync mode) */ + +static int s_scale = 1; /* internal-res scale (hr FBO) */ +/* Netplay: SW@1× + GPU@s_scale dual write; CPU VRAM always authoritative. */ +static int s_cpu_auth_dual = 0; +static int s_req_scale = 1; /* requested before context init */ + +static GLuint s_present_tex = 0; /* CPU-readout present path (24bpp) */ +static int s_present_w = 0, s_present_h = 0; +static GLuint s_osd_tex = 0; +static int s_osd_tw = 0, s_osd_th = 0; +static GLuint s_present_prog = 0, s_present_vao = 0; +static void gl_swap_with_osd(void); +static GLint s_present_uTex = -1, s_present_uUvRect = -1; static GLint s_present_uTexSize = -1, s_present_uSharpScale = -1; static GLint s_present_uSharp = -1; -/* Scanline post-process state (host display setting; see gl_renderer_set_scanlines). - * s_scanline_on/strength are pushed to whichever program draws game content; - * OSD/bezel and the already-composed hold-last re-present force it off so the - * effect is applied exactly once, at native-line pitch. */ -static int s_scanline_on = 0; -static float s_scanline_strength = 0.5f; -static GLint s_present_uScanline = -1, s_present_uScanStrength = -1; -static GLint s_present_uScanLines = -1, s_present_uScanScale = -1; +/* Scanline post-process state (host display setting; see gl_renderer_set_scanlines). + * s_scanline_on/strength are pushed to whichever program draws game content; + * OSD/bezel and the already-composed hold-last re-present force it off so the + * effect is applied exactly once, at native-line pitch. */ +static int s_scanline_on = 0; +static float s_scanline_strength = 0.5f; +static GLint s_present_uScanline = -1, s_present_uScanStrength = -1; +static GLint s_present_uScanLines = -1, s_present_uScanScale = -1; static GLuint s_interp_prog = 0, s_interp_tex[3]; -static GLint s_interp_uPrev = -1, s_interp_uCurr = -1; -static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1; -static GLint s_interp_uBlendMode = -1; -static GLint s_interp_uScanline = -1, s_interp_uScanStrength = -1; -static GLint s_interp_uScanLines = -1, s_interp_uScanScale = -1; -static int s_interp_enabled = 0, s_interp_valid = 0; -static int s_interp_suspended = 0; -static int s_interp_blend_mode = 0; -static int s_interp_prev = 0, s_interp_cur = 0; -static int s_interp_w = 0, s_interp_h = 0, s_interp_linear = 0; -static int s_interp_force_4_3 = 0, s_interp_source_path = -1; +static GLint s_interp_uPrev = -1, s_interp_uCurr = -1; +static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1; +static GLint s_interp_uBlendMode = -1; +static GLint s_interp_uScanline = -1, s_interp_uScanStrength = -1; +static GLint s_interp_uScanLines = -1, s_interp_uScanScale = -1; +static int s_interp_enabled = 0, s_interp_valid = 0; +static int s_interp_suspended = 0; +static int s_interp_blend_mode = 0; +static int s_interp_prev = 0, s_interp_cur = 0; +static int s_interp_w = 0, s_interp_h = 0, s_interp_linear = 0; +static int s_interp_force_4_3 = 0, s_interp_source_path = -1; static uint64_t s_interp_swaps = 0; static uint64_t s_interp_captures = 0; static int s_interp_diag = 0; @@ -351,523 +351,523 @@ static int interp_present(float alpha); static void interp_present_source_interval(void); static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh); static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh); - -static int s_raster_ok = 0; /* full GPU pipeline available */ - -/* Authoritative VRAM: hr color texture + stencil (mask bit) FBO. */ -static GLuint s_hr_tex = 0, s_hr_fbo = 0, s_hr_rb = 0; -/* Native raw-1555 sampling mirror + readback source. */ -static GLuint s_raw_tex = 0, s_raw_fbo = 0; -/* CPU->VRAM upload staging (native RGBA8). */ -static GLuint s_up_tex = 0; -/* copy_rect staging (hr-sized RGBA8). */ -static GLuint s_scratch_tex = 0, s_scratch_fbo = 0; - -/* Programs. */ -static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0; -static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0; -/* Textured vertex: pos(2) uv(2) col(4) tpage(2) clut(2) depth(1) raw(1) limits(4) - * semi(1) q(1) - * — per-prim texture state in flat attributes so prims batch (see flush_tex_batch). - * q is the perspective weight ([video] perspective_texturing): 0 = affine, the - * PS1-faithful default, which makes the vertex shader's w exactly 1.0 and the - * fragment shader read the noperspective varying — i.e. bit-identical to the - * pre-feature pipeline. */ -#define TEXV 20 -static GLuint s_blit_prog = 0, s_blit_vao = 0, s_blit_vbo = 0; -static GLuint s_pack_prog = 0, s_stencil_prog = 0, s_empty_vao = 0; - -/* Sub-pixel vertex override + perspective UV weights for the next triangle - * ([video] geometry_correction / perspective_texturing; see gpu_render.h). - * gpu.c sets these immediately before the matching gr_draw_*_triangle and they - * are consumed by it. All-zero == the faithful integer/affine path. */ -static int s_pc_valid = 0; /* sub-pixel positions present */ -static float s_pc_x[3], s_pc_y[3]; /* native VRAM px, fractional */ -static int s_pq_valid = 0; /* perspective weights present */ -static float s_pq[3]; - -/* TEX program uniforms. */ -static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1; -static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1; -static GLint s_uTwin = -1, s_uMaskset = -1, s_uFilter = -1; -static GLint s_uLimits = -1; -/* Native-wide x-projection uniforms (per program). u_xoff = x translation in - * native px (0 canonical), u_xhalf = x clip half-extent in native px (512 - * canonical). When wide is off these stay 0 / 512 so the canonical pass is - * bit-identical to the pre-native-wide projection. */ -static GLint s_geo_uXoff = -1, s_geo_uXhalf = -1; -static GLint s_tex_uXoff = -1, s_tex_uXhalf = -1; -/* Native-wide 2D-backdrop x-stretch uniforms. The far 2D backdrop layer is a - * ~4:3-width set of static prims scrolled by the draw offset; native-wide widens - * 3D via the GTE but never these, so they leave a void in the 16:9 margins. The - * wide mirror scales them about screen centre (u_xscale, u_xcenter) to fill it. - * Applied only to "backdrop-phase" prims (drawn before the first clearly-wide - * prim each frame). 1.0 / 0.0 => no-op, so the canonical pass stays identical. */ -static GLint s_geo_uXscale = -1, s_geo_uXcenter = -1; -static GLint s_tex_uXscale = -1, s_tex_uXcenter = -1; -/* Runtime controls (ws_backdrop_stretch debug command). */ -int g_ws_bd_stretch_on = 1; /* feature on (gated by native-wide + per-prim gate) */ -int g_ws_bd_stretch_pct = 0; /* 0 = auto (g_wide_w/native_w); else pct/100 */ -int g_ws_bd_phase_thresh = 24; /* "narrow" margin (px): a prim overhanging the 4:3 - * region by more than this is treated as already- - * widened GTE geometry and NOT stretched */ -int g_ws_bd_phase_mode = 1; /* (retained for the debug command; unused since the - * gate is now per-prim !tagged && narrow) */ -/* Per-prim 2D-backdrop gate (replaces the old draw-order phase): a prim is - * stretched iff native-wide + feature on + NOT sprite-tagged (foreground chars/ - * HUD are tagged) + NARROW (the GTE far-parallax/3D extend into the margins). - * s_bd_gate is what wide_set_bd_scale reads; s_tb_gate is the open textured - * batch's gate (batch flushes when a prim's gate differs, so a batch is uniform). */ -static int s_bd_gate = 0; -static int s_tb_gate = 0; -/* ws_backdrop_stretch diagnostics: per-frame snapshot reported by the command. */ -int g_bdg_applied = 0, g_bdg_prims = 0, g_bdg_clearx = -999999; -int g_bdg_cur = 0, g_bdg_base = 0, g_bdg_w = 0, g_bdg_off = 0; -static int s_bdg_applied = 0, s_bdg_prims = 0, s_bdg_clearx = -999999; -/* per-prim draw-order trace (ws_backdrop_trace): x-extent + textured flag, in - * draw order, for the last frame -- so we can SEE where the background sits. */ -typedef struct { short x0, x1, y0, y1; unsigned char tex; } PrimRec; -#define PTRACE_CAP 200 -static PrimRec s_ptrace[PTRACE_CAP]; static int s_ptrace_n = 0; -PrimRec g_ptrace[PTRACE_CAP]; int g_ptrace_n = 0; /* snapshot (extern) */ -/* BLIT program uniforms. */ -static GLint s_uBlitSrc = -1, s_uBlitPass = -1, s_uBlitMaskset = -1; -static GLint s_uBlitSrcDiv = -1, s_uBlitSrcOff = -1; -/* PACK program uniforms. */ -static GLint s_uPackHr = -1, s_uPackScale = -1; -static GLint s_uStencilSrc = -1; - -static uint32_t *s_conv = NULL; /* RGBA8 staging for uploads */ -static int s_gpu_dirty = 0; /* CPU VRAM array may be stale */ - -/* Dirty-rect unions, native VRAM coords, inclusive bounds. */ -typedef struct { int x0, y0, x1, y1, set; } DirtyRect; -static DirtyRect s_pack_dirty; /* hr FBO content not in raw mirror */ - -/* CPU writes not yet in the FBO — an EXACT rect list, NOT a single union. - * - * THE FLICKER CLASS BUG (MMX6 GL black-frame flicker, ISSUES.md #7): the old - * single-union s_up_pending merged DISJOINT uploads (e.g. a sprite column at - * x>=320 and a tile row at y>=480) into one bounding box that covered the - * framebuffers in between; flush_cpu_upload then painted that whole box from - * the CPU VRAM array — which is STALE under GL (the FBO is authoritative, - * ensure_cpu only syncs on demand) — stomping freshly-rendered framebuffer - * content with stale (typically black) pixels for 1-2 presents until the game - * redrew each buffer. Fix: track the exact uploaded rects and flush each one; - * only pixels the CPU actually wrote are ever painted. Merging is allowed only - * when it adds NO uncovered pixels (containment / same-band extension). On - * overflow the pending set is flushed and the new rect starts a fresh list — - * order-preserving and still batched for the common poke patterns. - * (Pre-context, s_raster_ok == 0, the CPU array is fully authoritative — the - * software rasterizer mirrors every draw — so union-merging is harmless and - * used as the overflow strategy there.) */ -#define UP_RECTS_MAX 16 -static DirtyRect s_up_rects[UP_RECTS_MAX]; -static int s_up_nrects = 0; -static uint64_t s_rt_up_diag[6]; - -static int runtime_upload_diag_enabled(void) { - static int enabled = -1; - if (enabled < 0) { - const char *e = getenv("PSX_RUNTIME_PERF_DIAG"); - enabled = e && e[0] && e[0] != '0'; - } - return enabled; -} - -void gl_renderer_runtime_diag(uint64_t out[6]) { - for (int i = 0; i < 6; i++) out[i] = s_rt_up_diag[i]; -} - -/* Draw state mirrored from the vtable set_* calls. */ -static int s_off_x = 0, s_off_y = 0; -static int s_area_x1 = 0, s_area_y1 = 0, s_area_x2 = VRAM_W - 1, s_area_y2 = VRAM_H - 1; -static int s_semi_en = 0, s_semi_mode = 0; -static int s_mod_r = 128, s_mod_g = 128, s_mod_b = 128, s_mod_raw = 0; -static int s_mask_set = 0, s_mask_check = 0; -static int s_tw_mask_x = 0, s_tw_mask_y = 0, s_tw_off_x = 0, s_tw_off_y = 0; -static int s_tex_filter = 0; -/* Opaque textured draws carry the exact mask bit in FBO alpha. Keeping the - * duplicate stencil copy current is deferred until mask checking is requested. */ -static int s_stencil_valid = 1; - -/* ---- native-wide compositor (mirrors gpu_sw_renderer.c g_wide_*) ---------- - * Canonical VRAM (the hr FBO) stays 100% faithful. Native-wide lives in - * SEPARATE wide FBOs keyed by framebuffer base_x; each framebuffer-targeting - * primitive is mirrored into the active wide surface at local x = vram_x - - * base_x + OFFSET. Present reads the displayed buffer's wide surface. Textures - * always sample canonical VRAM (s_raw_tex). 4:3 / non-opted games never call - * wide_configure, so g_wide_cur stays 0 and the wide pass never runs. */ -#define WIDE_MAX_SURF 4 -static GLuint s_wide_tex[WIDE_MAX_SURF]; /* color tex per surface (0 = free) */ -static GLuint s_wide_fbo[WIDE_MAX_SURF]; /* FBO per surface */ -static GLuint s_wide_rb[WIDE_MAX_SURF]; /* depth-stencil RB per surface (mask - * mirror, same as hr). PERF-CRITICAL: - * a stencil-less wide FBO made every - * stencil-enabled mirror draw cost - * ~0.6ms of driver work (Tomba2 16:9 - * collapsed to 12fps); with the RB - * attached the pass costs ~2us. */ -static int s_wide_base[WIDE_MAX_SURF]; /* base_x per surface (-1 = free) */ -static int g_wide_w = 0; /* wide width (native px); 0 = disabled */ -static int g_wide_off = 0; /* centering OFFSET (native px) */ -static GLuint g_wide_cur = 0; /* active mirror FBO (0 = no mirror) */ -static int g_wide_cur_base = 0; /* base_x of g_wide_cur */ -/* Set by gpu_flat_rect for the full-screen-overlay case so the generic - * gpu_geometry wide mirror is skipped and the flat path emits its own - * full-wide-width pass instead (mirrors sw_draw_flat_rect). */ -static int s_wide_suppress = 0; - -/* X-translation (native px) from canonical VRAM space into the active wide - * surface: local_x = vram_x - base_x + OFFSET. Same as SW wide_dx(). */ -static inline int wide_dx(void) { return g_wide_off - g_wide_cur_base; } - -/* ---- dirty-rect helpers ------------------------------------------------- */ -static void rect_clear(DirtyRect *r) { r->set = 0; } -static void rect_add(DirtyRect *r, int x0, int y0, int x1, int y1) { - if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; - if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; - if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; - if (x0 > x1 || y0 > y1) return; - if (!r->set) { r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; r->set = 1; return; } - if (x0 < r->x0) r->x0 = x0; - if (y0 < r->y0) r->y0 = y0; - if (x1 > r->x1) r->x1 = x1; - if (y1 > r->y1) r->y1 = y1; -} -static int rect_intersects(const DirtyRect *r, int x0, int y0, int x1, int y1) { - if (!r->set) return 0; - return !(x1 < r->x0 || x0 > r->x1 || y1 < r->y0 || y0 > r->y1); -} - -/* ---- exact pending-upload rect list (see s_up_rects comment) ------------- */ -static void flush_cpu_upload(void); /* fwd: overflow flushes then re-adds */ - -/* Add an uploaded rect. Merges ONLY when the merge adds no uncovered pixels: - * containment either way, or an extension within the same row-band / column- - * band (equal y-range with touching/overlapping x-ranges, or equal x-range - * with touching/overlapping y-ranges — the row-scan / column-scan poke - * patterns). Never unions disjoint rects post-init (that was the flicker bug). */ -static void up_add(int x0, int y0, int x1, int y1) { - if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; - if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; - if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; - if (x0 > x1 || y0 > y1) return; - for (int i = 0; i < s_up_nrects; i++) { - DirtyRect *r = &s_up_rects[i]; - if (x0 >= r->x0 && x1 <= r->x1 && y0 >= r->y0 && y1 <= r->y1) - return; /* contained */ - if (x0 <= r->x0 && x1 >= r->x1 && y0 <= r->y0 && y1 >= r->y1) { - r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; /* contains */ - return; - } - if (y0 == r->y0 && y1 == r->y1 && - x0 <= r->x1 + 1 && x1 >= r->x0 - 1) { /* same row-band extension */ - if (x0 < r->x0) r->x0 = x0; - if (x1 > r->x1) r->x1 = x1; - return; - } - if (x0 == r->x0 && x1 == r->x1 && - y0 <= r->y1 + 1 && y1 >= r->y0 - 1) { /* same column-band extension */ - if (y0 < r->y0) r->y0 = y0; - if (y1 > r->y1) r->y1 = y1; - return; - } - } - if (s_up_nrects >= UP_RECTS_MAX) { - if (s_raster_ok) { - flush_cpu_upload(); /* order-preserving: land the old ones */ - } else { - /* Pre-context: the CPU array is fully authoritative (software - * rasterizer mirrors every op), so a union is harmless. */ - DirtyRect *r = &s_up_rects[0]; - for (int i = 1; i < s_up_nrects; i++) { - if (s_up_rects[i].x0 < r->x0) r->x0 = s_up_rects[i].x0; - if (s_up_rects[i].y0 < r->y0) r->y0 = s_up_rects[i].y0; - if (s_up_rects[i].x1 > r->x1) r->x1 = s_up_rects[i].x1; - if (s_up_rects[i].y1 > r->y1) r->y1 = s_up_rects[i].y1; - } - if (x0 < r->x0) r->x0 = x0; - if (y0 < r->y0) r->y0 = y0; - if (x1 > r->x1) r->x1 = x1; - if (y1 > r->y1) r->y1 = y1; - s_up_nrects = 1; - return; - } - } - DirtyRect *r = &s_up_rects[s_up_nrects++]; - r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; r->set = 1; -} - -/* Add a GP0(A0) transfer's exact touched region. The software reference wraps - * per pixel (px = (x+col) & 1023, py = (y+row) & 511), so a wrapping transfer - * touches up to four exact rects — NOT all of VRAM (the old "wrapped: take - * all" full-VRAM union painted stale CPU content over the framebuffers). */ -static void up_add_transfer(int x, int y, int w, int h) { - x &= VRAM_W - 1; y &= VRAM_H - 1; - if (w > VRAM_W) w = VRAM_W; - if (h > VRAM_H) h = VRAM_H; - if (w <= 0 || h <= 0) return; - int w1 = w, w2 = 0, h1 = h, h2 = 0; - if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; } - if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; } - up_add(x, y, x + w1 - 1, y + h1 - 1); - if (w2) up_add(0, y, w2 - 1, y + h1 - 1); - if (h2) up_add(x, 0, x + w1 - 1, h2 - 1); - if (w2 && h2) up_add(0, 0, w2 - 1, h2 - 1); -} - -/* ---- coherency event ring (always-on, debug server "gl_coh_ring") -------- */ -/* Every coherency-relevant operation — upload flushes, fills, copies, draw - * bboxes, packs, full readbacks, presents, and probe perturbations — is - * recorded with its rect and frame number. Per CLAUDE.md ring-buffer rule: - * capture is continuous, observers query a window after the fact. Trigger - * attribution convention: an op that flushes internally (fill/copy/draw/ - * present/peek) records its own event AFTER the FLUSH event it caused, so - * the event following a FLUSH names the trigger. 16 B * 64 Ki = 1 MB. */ -extern uint64_t s_frame_count; /* defined in debug_server.c */ - -#define GL_COH_RING_CAP (1u << 16) -static GlCohEvent s_coh_ring[GL_COH_RING_CAP]; -static uint64_t s_coh_seq = 0; - -/* 16x16 native-pixel tiles changed since their last on-screen present. This - * lets a double-buffered 30 Hz game avoid swapping the unchanged front buffer - * on the intervening 60 Hz vblank without guessing from game identity. - * - * INVARIANT: eliding a swap also elides whatever that swap was blocking on. - * The frontend calls present once per guest vblank, so when the driver's swap - * block owns the guest cadence (psx_present_vsync_owns_cadence(), the XOR - * partner of the wall-clock pacer) an elided frame is an unthrottled frame: - * a 30 Hz-presenting game then advances two vblanks per block and runs at 2x. - * Every skip site below is therefore gated on that query. */ -#define PRES_TILE 16 -#define PRES_ROWS (VRAM_H / PRES_TILE) -static uint64_t s_present_dirty[PRES_ROWS]; -static int s_last_present_path = -1; -static int s_last_dx, s_last_dy, s_last_dw, s_last_dh; -/* After savestate restore: keep swapping for a few presents even when the - * display rect is byte-identical to the last swap. Double/triple-buffered - * windows otherwise can leave a stale back buffer on screen while vblanks - * (and FPS) keep advancing — especially on a 2nd+ load of the same slot. */ -static int s_force_present_remaining = 0; - -/* §33 rollback resim hold-last: sticky copy of the last Live present so - * mid-resim ticks can Swap a frozen frame without reading mutating VRAM. - * HOLD_DRAWABLE = full backbuffer copy taken just before SwapWindow. - * HOLD_NATIVE = GPU blit of a display band (used when interp owns Swap). */ -#define HOLD_NONE 0 -#define HOLD_DRAWABLE 1 -#define HOLD_NATIVE 2 -static int s_hold_kind = HOLD_NONE; -static GLuint s_hold_tex = 0; -static GLuint s_hold_fbo = 0; -static int s_hold_tw = 0, s_hold_th = 0; /* texture size in texels */ -static int s_hold_force_4_3 = 0; -static int s_hold_linear = 0; - -/* Post-load freeze probe (main.cpp): accumulate skip/swap/dirty marks. */ -static uint64_t s_probe_skip = 0; -static uint64_t s_probe_swap = 0; -static uint64_t s_probe_dirty_marks = 0; - -static void present_dirty_rect(int x0, int y0, int x1, int y1, int set) { - if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; - if (x1 >= VRAM_W) x1 = VRAM_W - 1; if (y1 >= VRAM_H) y1 = VRAM_H - 1; - if (x0 > x1 || y0 > y1) return; - int tx0 = x0 / PRES_TILE, tx1 = x1 / PRES_TILE; - uint64_t mask = (~0ull << tx0) & (~0ull >> (63 - tx1)); - for (int ty = y0 / PRES_TILE; ty <= y1 / PRES_TILE; ty++) { - if (set) s_present_dirty[ty] |= mask; else s_present_dirty[ty] &= ~mask; - } - if (set) s_probe_dirty_marks++; -} - -static int present_dirty_test(int x0, int y0, int x1, int y1) { - if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; - if (x1 >= VRAM_W) x1 = VRAM_W - 1; if (y1 >= VRAM_H) y1 = VRAM_H - 1; - if (x0 > x1 || y0 > y1) return 0; - int tx0 = x0 / PRES_TILE, tx1 = x1 / PRES_TILE; - uint64_t mask = (~0ull << tx0) & (~0ull >> (63 - tx1)); - for (int ty = y0 / PRES_TILE; ty <= y1 / PRES_TILE; ty++) - if (s_present_dirty[ty] & mask) return 1; - return 0; -} - -static void present_force_consumed(void) { - if (s_force_present_remaining > 0) - s_force_present_remaining--; -} - -static void hold_ensure_tex(int w, int h) { - if (w < 1) w = 1; - if (h < 1) h = 1; - if (!s_hold_tex) { - glGenTextures(1, &s_hold_tex); - if (!s_hold_fbo) - p_glGenFramebuffers(1, &s_hold_fbo); - } - if (s_hold_tw == w && s_hold_th == h) - return; - glBindTexture(GL_TEXTURE_2D, s_hold_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, - NULL); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hold_fbo); - p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, - GL_TEXTURE_2D, s_hold_tex, 0); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - s_hold_tw = w; - s_hold_th = h; -} - -/* Snapshot the just-drawn default backbuffer (letterbox + content) before Swap. - * Hold-last can then redraw this exact image without touching guest VRAM. */ -static void hold_capture_drawable(void) { - int ww = 0, wh = 0; - if (!s_ctx || !s_win) - return; - SDL_GL_GetDrawableSize(s_win, &ww, &wh); - if (ww < 1 || wh < 1) - return; - hold_ensure_tex(ww, wh); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - glBindTexture(GL_TEXTURE_2D, s_hold_tex); - glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, ww, wh); - glBindTexture(GL_TEXTURE_2D, 0); - s_hold_kind = HOLD_DRAWABLE; - s_hold_force_4_3 = 0; - s_hold_linear = 0; -} - -/* Snapshot a native display band from an FBO when the main thread will not - * Swap (interpolation owns the cadence). Scale-aware blit into hold tex. */ -static void hold_capture_native_fbo(GLuint src_fbo, int dx, int dy, int dw, int dh, - int force_4_3, int linear) { - int S = s_scale > 0 ? s_scale : 1; - if (!s_ctx || !src_fbo || dw < 1 || dh < 1) - return; - hold_ensure_tex(dw * S, dh * S); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src_fbo); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_hold_fbo); - glDisable(GL_SCISSOR_TEST); - p_glBlitFramebuffer(dx * S, dy * S, (dx + dw) * S, (dy + dh) * S, - 0, 0, dw * S, dh * S, - GL_COLOR_BUFFER_BIT, GL_NEAREST); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); - s_hold_kind = HOLD_NATIVE; - s_hold_force_4_3 = force_4_3 ? 1 : 0; - s_hold_linear = linear ? 1 : 0; -} - -static void hold_invalidate(void) { - s_hold_kind = HOLD_NONE; -} - -/* Defined after present_target_quad / letterbox helpers. */ -static void letterbox_rect_aspect(int ww, int wh, int num, int den, - int *x, int *y, int *w, int *h); -static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h); -static void present_target_quad(GLuint tex, int tex_w, int tex_h, - int x, int y, int w, int h, int linear, - int lx, int ly, int lw, int lh, int v_flip); - -static void coh_record(int kind, int x0, int y0, int x1, int y1) { - GlCohEvent *e = &s_coh_ring[s_coh_seq % GL_COH_RING_CAP]; - e->frame = (uint32_t)s_frame_count; - e->kind = (uint8_t)kind; - e->x0 = (int16_t)x0; e->y0 = (int16_t)y0; - e->x1 = (int16_t)x1; e->y1 = (int16_t)y1; - s_coh_seq++; - if (kind == GL_COH_FLUSH || kind == GL_COH_FILL || - kind == GL_COH_COPY || kind == GL_COH_DRAW) - present_dirty_rect(x0, y0, x1, y1, 1); -} - -uint64_t gl_renderer_coh_total(void) { return s_coh_seq; } -int gl_renderer_coh_get(uint64_t seq, GlCohEvent *out) { - if (seq >= s_coh_seq) return 0; - if (s_coh_seq - seq > GL_COH_RING_CAP) return 0; /* evicted */ - *out = s_coh_ring[seq % GL_COH_RING_CAP]; - return 1; -} - -/* ---- present ring (always-on, debug server "present_ring") --------------- */ -/* Records every SwapWindow (vram/wide/cpu/blank/interpolated) and its - * source + letterbox rects. PSX_GL_PRESENT_PROBE=1 additionally drains - * glGetError and samples one backbuffer pixel before the swap; that synchronous - * diagnostic is intentionally opt-in. Observers query a window after the fact. */ -#define GL_PRES_RING_CAP 4096 -static GlPresEvent s_pres_ring[GL_PRES_RING_CAP]; -static uint64_t s_pres_seq = 0; - -static void pres_record(int path, int dx, int dy, int w, int h, - int lx, int ly, int lw, int lh) { - /* The ring metadata stays always-on, but pixel probing must not: each - * glReadPixels synchronously drains queued GPU work. Two probes per frame - * were enough to make Tomba 2 miss its frame budget. */ - static int probe_pixels = -1; - if (probe_pixels < 0) { - const char *cfg = getenv("PSX_GL_PRESENT_PROBE"); - probe_pixels = (cfg && cfg[0] == '1') ? 1 : 0; - } - GlPresEvent *e = &s_pres_ring[s_pres_seq % GL_PRES_RING_CAP]; - e->frame = (uint32_t)s_frame_count; - e->t_ms = (uint32_t)SDL_GetTicks(); - e->path = (uint8_t)path; - e->glerr = probe_pixels ? (uint16_t)glGetError() : 0; - e->dx = (int16_t)dx; e->dy = (int16_t)dy; - e->w = (int16_t)w; e->h = (int16_t)h; - e->lx = (int16_t)lx; e->ly = (int16_t)ly; - e->lw = (int16_t)lw; e->lh = (int16_t)lh; - /* Backbuffer sample at the letterbox centre (GL bottom-origin; the rects - * we pass in are already bottom-origin GL window coords). */ - uint8_t px[3] = { 0, 0, 0 }; - if (probe_pixels && lw > 0 && lh > 0) { - glReadBuffer(GL_BACK); - glPixelStorei(GL_PACK_ALIGNMENT, 1); - glReadPixels(lx + lw / 2, ly + lh / 2, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, px); - glPixelStorei(GL_PACK_ALIGNMENT, 4); - } - e->px_r = px[0]; e->px_g = px[1]; e->px_b = px[2]; - /* Blit-source sample: the hr FBO pixel at the display-rect centre. Splits - * "FBO content was black" from "the blit malfunctioned". */ - uint8_t sp[3] = { 0, 0, 0 }; - e->src_valid = 0; - if (probe_pixels && (path == GL_PRES_VRAM) && w > 0 && h > 0 && s_hr_fbo) { - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); - glPixelStorei(GL_PACK_ALIGNMENT, 1); - glReadPixels((dx + w / 2) * s_scale, (dy + h / 2) * s_scale, - 1, 1, GL_RGB, GL_UNSIGNED_BYTE, sp); - glPixelStorei(GL_PACK_ALIGNMENT, 4); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - e->src_valid = 1; - } - e->src_r = sp[0]; e->src_g = sp[1]; e->src_b = sp[2]; - s_pres_seq++; -} - -uint64_t gl_renderer_pres_total(void) { return s_pres_seq; } -int gl_renderer_pres_get(uint64_t seq, GlPresEvent *out) { - if (seq >= s_pres_seq) return 0; - if (s_pres_seq - seq > GL_PRES_RING_CAP) return 0; /* evicted */ - *out = s_pres_ring[seq % GL_PRES_RING_CAP]; - return 1; -} - -/* ---- shaders ------------------------------------------------------------ */ -static const char *PRESENT_VS = - "#version 330\n" - "out vec2 v_uv;\n" - "uniform vec4 u_uv_rect;\n" - "void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n" - " v_uv = vec2(mix(u_uv_rect.x,u_uv_rect.z,p.x),\n" - " mix(u_uv_rect.y,u_uv_rect.w,1.0-p.y));\n" - " gl_Position = vec4(p*2.0-1.0,0.0,1.0); }\n"; + +static int s_raster_ok = 0; /* full GPU pipeline available */ + +/* Authoritative VRAM: hr color texture + stencil (mask bit) FBO. */ +static GLuint s_hr_tex = 0, s_hr_fbo = 0, s_hr_rb = 0; +/* Native raw-1555 sampling mirror + readback source. */ +static GLuint s_raw_tex = 0, s_raw_fbo = 0; +/* CPU->VRAM upload staging (native RGBA8). */ +static GLuint s_up_tex = 0; +/* copy_rect staging (hr-sized RGBA8). */ +static GLuint s_scratch_tex = 0, s_scratch_fbo = 0; + +/* Programs. */ +static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0; +static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0; +/* Textured vertex: pos(2) uv(2) col(4) tpage(2) clut(2) depth(1) raw(1) limits(4) + * semi(1) q(1) + * — per-prim texture state in flat attributes so prims batch (see flush_tex_batch). + * q is the perspective weight ([video] perspective_texturing): 0 = affine, the + * PS1-faithful default, which makes the vertex shader's w exactly 1.0 and the + * fragment shader read the noperspective varying — i.e. bit-identical to the + * pre-feature pipeline. */ +#define TEXV 20 +static GLuint s_blit_prog = 0, s_blit_vao = 0, s_blit_vbo = 0; +static GLuint s_pack_prog = 0, s_stencil_prog = 0, s_empty_vao = 0; + +/* Sub-pixel vertex override + perspective UV weights for the next triangle + * ([video] geometry_correction / perspective_texturing; see gpu_render.h). + * gpu.c sets these immediately before the matching gr_draw_*_triangle and they + * are consumed by it. All-zero == the faithful integer/affine path. */ +static int s_pc_valid = 0; /* sub-pixel positions present */ +static float s_pc_x[3], s_pc_y[3]; /* native VRAM px, fractional */ +static int s_pq_valid = 0; /* perspective weights present */ +static float s_pq[3]; + +/* TEX program uniforms. */ +static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1; +static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1; +static GLint s_uTwin = -1, s_uMaskset = -1, s_uFilter = -1; +static GLint s_uLimits = -1; +/* Native-wide x-projection uniforms (per program). u_xoff = x translation in + * native px (0 canonical), u_xhalf = x clip half-extent in native px (512 + * canonical). When wide is off these stay 0 / 512 so the canonical pass is + * bit-identical to the pre-native-wide projection. */ +static GLint s_geo_uXoff = -1, s_geo_uXhalf = -1; +static GLint s_tex_uXoff = -1, s_tex_uXhalf = -1; +/* Native-wide 2D-backdrop x-stretch uniforms. The far 2D backdrop layer is a + * ~4:3-width set of static prims scrolled by the draw offset; native-wide widens + * 3D via the GTE but never these, so they leave a void in the 16:9 margins. The + * wide mirror scales them about screen centre (u_xscale, u_xcenter) to fill it. + * Applied only to "backdrop-phase" prims (drawn before the first clearly-wide + * prim each frame). 1.0 / 0.0 => no-op, so the canonical pass stays identical. */ +static GLint s_geo_uXscale = -1, s_geo_uXcenter = -1; +static GLint s_tex_uXscale = -1, s_tex_uXcenter = -1; +/* Runtime controls (ws_backdrop_stretch debug command). */ +int g_ws_bd_stretch_on = 1; /* feature on (gated by native-wide + per-prim gate) */ +int g_ws_bd_stretch_pct = 0; /* 0 = auto (g_wide_w/native_w); else pct/100 */ +int g_ws_bd_phase_thresh = 24; /* "narrow" margin (px): a prim overhanging the 4:3 + * region by more than this is treated as already- + * widened GTE geometry and NOT stretched */ +int g_ws_bd_phase_mode = 1; /* (retained for the debug command; unused since the + * gate is now per-prim !tagged && narrow) */ +/* Per-prim 2D-backdrop gate (replaces the old draw-order phase): a prim is + * stretched iff native-wide + feature on + NOT sprite-tagged (foreground chars/ + * HUD are tagged) + NARROW (the GTE far-parallax/3D extend into the margins). + * s_bd_gate is what wide_set_bd_scale reads; s_tb_gate is the open textured + * batch's gate (batch flushes when a prim's gate differs, so a batch is uniform). */ +static int s_bd_gate = 0; +static int s_tb_gate = 0; +/* ws_backdrop_stretch diagnostics: per-frame snapshot reported by the command. */ +int g_bdg_applied = 0, g_bdg_prims = 0, g_bdg_clearx = -999999; +int g_bdg_cur = 0, g_bdg_base = 0, g_bdg_w = 0, g_bdg_off = 0; +static int s_bdg_applied = 0, s_bdg_prims = 0, s_bdg_clearx = -999999; +/* per-prim draw-order trace (ws_backdrop_trace): x-extent + textured flag, in + * draw order, for the last frame -- so we can SEE where the background sits. */ +typedef struct { short x0, x1, y0, y1; unsigned char tex; } PrimRec; +#define PTRACE_CAP 200 +static PrimRec s_ptrace[PTRACE_CAP]; static int s_ptrace_n = 0; +PrimRec g_ptrace[PTRACE_CAP]; int g_ptrace_n = 0; /* snapshot (extern) */ +/* BLIT program uniforms. */ +static GLint s_uBlitSrc = -1, s_uBlitPass = -1, s_uBlitMaskset = -1; +static GLint s_uBlitSrcDiv = -1, s_uBlitSrcOff = -1; +/* PACK program uniforms. */ +static GLint s_uPackHr = -1, s_uPackScale = -1; +static GLint s_uStencilSrc = -1; + +static uint32_t *s_conv = NULL; /* RGBA8 staging for uploads */ +static int s_gpu_dirty = 0; /* CPU VRAM array may be stale */ + +/* Dirty-rect unions, native VRAM coords, inclusive bounds. */ +typedef struct { int x0, y0, x1, y1, set; } DirtyRect; +static DirtyRect s_pack_dirty; /* hr FBO content not in raw mirror */ + +/* CPU writes not yet in the FBO — an EXACT rect list, NOT a single union. + * + * THE FLICKER CLASS BUG (MMX6 GL black-frame flicker, ISSUES.md #7): the old + * single-union s_up_pending merged DISJOINT uploads (e.g. a sprite column at + * x>=320 and a tile row at y>=480) into one bounding box that covered the + * framebuffers in between; flush_cpu_upload then painted that whole box from + * the CPU VRAM array — which is STALE under GL (the FBO is authoritative, + * ensure_cpu only syncs on demand) — stomping freshly-rendered framebuffer + * content with stale (typically black) pixels for 1-2 presents until the game + * redrew each buffer. Fix: track the exact uploaded rects and flush each one; + * only pixels the CPU actually wrote are ever painted. Merging is allowed only + * when it adds NO uncovered pixels (containment / same-band extension). On + * overflow the pending set is flushed and the new rect starts a fresh list — + * order-preserving and still batched for the common poke patterns. + * (Pre-context, s_raster_ok == 0, the CPU array is fully authoritative — the + * software rasterizer mirrors every draw — so union-merging is harmless and + * used as the overflow strategy there.) */ +#define UP_RECTS_MAX 16 +static DirtyRect s_up_rects[UP_RECTS_MAX]; +static int s_up_nrects = 0; +static uint64_t s_rt_up_diag[6]; + +static int runtime_upload_diag_enabled(void) { + static int enabled = -1; + if (enabled < 0) { + const char *e = getenv("PSX_RUNTIME_PERF_DIAG"); + enabled = e && e[0] && e[0] != '0'; + } + return enabled; +} + +void gl_renderer_runtime_diag(uint64_t out[6]) { + for (int i = 0; i < 6; i++) out[i] = s_rt_up_diag[i]; +} + +/* Draw state mirrored from the vtable set_* calls. */ +static int s_off_x = 0, s_off_y = 0; +static int s_area_x1 = 0, s_area_y1 = 0, s_area_x2 = VRAM_W - 1, s_area_y2 = VRAM_H - 1; +static int s_semi_en = 0, s_semi_mode = 0; +static int s_mod_r = 128, s_mod_g = 128, s_mod_b = 128, s_mod_raw = 0; +static int s_mask_set = 0, s_mask_check = 0; +static int s_tw_mask_x = 0, s_tw_mask_y = 0, s_tw_off_x = 0, s_tw_off_y = 0; +static int s_tex_filter = 0; +/* Opaque textured draws carry the exact mask bit in FBO alpha. Keeping the + * duplicate stencil copy current is deferred until mask checking is requested. */ +static int s_stencil_valid = 1; + +/* ---- native-wide compositor (mirrors gpu_sw_renderer.c g_wide_*) ---------- + * Canonical VRAM (the hr FBO) stays 100% faithful. Native-wide lives in + * SEPARATE wide FBOs keyed by framebuffer base_x; each framebuffer-targeting + * primitive is mirrored into the active wide surface at local x = vram_x - + * base_x + OFFSET. Present reads the displayed buffer's wide surface. Textures + * always sample canonical VRAM (s_raw_tex). 4:3 / non-opted games never call + * wide_configure, so g_wide_cur stays 0 and the wide pass never runs. */ +#define WIDE_MAX_SURF 4 +static GLuint s_wide_tex[WIDE_MAX_SURF]; /* color tex per surface (0 = free) */ +static GLuint s_wide_fbo[WIDE_MAX_SURF]; /* FBO per surface */ +static GLuint s_wide_rb[WIDE_MAX_SURF]; /* depth-stencil RB per surface (mask + * mirror, same as hr). PERF-CRITICAL: + * a stencil-less wide FBO made every + * stencil-enabled mirror draw cost + * ~0.6ms of driver work (Tomba2 16:9 + * collapsed to 12fps); with the RB + * attached the pass costs ~2us. */ +static int s_wide_base[WIDE_MAX_SURF]; /* base_x per surface (-1 = free) */ +static int g_wide_w = 0; /* wide width (native px); 0 = disabled */ +static int g_wide_off = 0; /* centering OFFSET (native px) */ +static GLuint g_wide_cur = 0; /* active mirror FBO (0 = no mirror) */ +static int g_wide_cur_base = 0; /* base_x of g_wide_cur */ +/* Set by gpu_flat_rect for the full-screen-overlay case so the generic + * gpu_geometry wide mirror is skipped and the flat path emits its own + * full-wide-width pass instead (mirrors sw_draw_flat_rect). */ +static int s_wide_suppress = 0; + +/* X-translation (native px) from canonical VRAM space into the active wide + * surface: local_x = vram_x - base_x + OFFSET. Same as SW wide_dx(). */ +static inline int wide_dx(void) { return g_wide_off - g_wide_cur_base; } + +/* ---- dirty-rect helpers ------------------------------------------------- */ +static void rect_clear(DirtyRect *r) { r->set = 0; } +static void rect_add(DirtyRect *r, int x0, int y0, int x1, int y1) { + if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; + if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; + if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; + if (x0 > x1 || y0 > y1) return; + if (!r->set) { r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; r->set = 1; return; } + if (x0 < r->x0) r->x0 = x0; + if (y0 < r->y0) r->y0 = y0; + if (x1 > r->x1) r->x1 = x1; + if (y1 > r->y1) r->y1 = y1; +} +static int rect_intersects(const DirtyRect *r, int x0, int y0, int x1, int y1) { + if (!r->set) return 0; + return !(x1 < r->x0 || x0 > r->x1 || y1 < r->y0 || y0 > r->y1); +} + +/* ---- exact pending-upload rect list (see s_up_rects comment) ------------- */ +static void flush_cpu_upload(void); /* fwd: overflow flushes then re-adds */ + +/* Add an uploaded rect. Merges ONLY when the merge adds no uncovered pixels: + * containment either way, or an extension within the same row-band / column- + * band (equal y-range with touching/overlapping x-ranges, or equal x-range + * with touching/overlapping y-ranges — the row-scan / column-scan poke + * patterns). Never unions disjoint rects post-init (that was the flicker bug). */ +static void up_add(int x0, int y0, int x1, int y1) { + if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; + if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; + if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; + if (x0 > x1 || y0 > y1) return; + for (int i = 0; i < s_up_nrects; i++) { + DirtyRect *r = &s_up_rects[i]; + if (x0 >= r->x0 && x1 <= r->x1 && y0 >= r->y0 && y1 <= r->y1) + return; /* contained */ + if (x0 <= r->x0 && x1 >= r->x1 && y0 <= r->y0 && y1 >= r->y1) { + r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; /* contains */ + return; + } + if (y0 == r->y0 && y1 == r->y1 && + x0 <= r->x1 + 1 && x1 >= r->x0 - 1) { /* same row-band extension */ + if (x0 < r->x0) r->x0 = x0; + if (x1 > r->x1) r->x1 = x1; + return; + } + if (x0 == r->x0 && x1 == r->x1 && + y0 <= r->y1 + 1 && y1 >= r->y0 - 1) { /* same column-band extension */ + if (y0 < r->y0) r->y0 = y0; + if (y1 > r->y1) r->y1 = y1; + return; + } + } + if (s_up_nrects >= UP_RECTS_MAX) { + if (s_raster_ok) { + flush_cpu_upload(); /* order-preserving: land the old ones */ + } else { + /* Pre-context: the CPU array is fully authoritative (software + * rasterizer mirrors every op), so a union is harmless. */ + DirtyRect *r = &s_up_rects[0]; + for (int i = 1; i < s_up_nrects; i++) { + if (s_up_rects[i].x0 < r->x0) r->x0 = s_up_rects[i].x0; + if (s_up_rects[i].y0 < r->y0) r->y0 = s_up_rects[i].y0; + if (s_up_rects[i].x1 > r->x1) r->x1 = s_up_rects[i].x1; + if (s_up_rects[i].y1 > r->y1) r->y1 = s_up_rects[i].y1; + } + if (x0 < r->x0) r->x0 = x0; + if (y0 < r->y0) r->y0 = y0; + if (x1 > r->x1) r->x1 = x1; + if (y1 > r->y1) r->y1 = y1; + s_up_nrects = 1; + return; + } + } + DirtyRect *r = &s_up_rects[s_up_nrects++]; + r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; r->set = 1; +} + +/* Add a GP0(A0) transfer's exact touched region. The software reference wraps + * per pixel (px = (x+col) & 1023, py = (y+row) & 511), so a wrapping transfer + * touches up to four exact rects — NOT all of VRAM (the old "wrapped: take + * all" full-VRAM union painted stale CPU content over the framebuffers). */ +static void up_add_transfer(int x, int y, int w, int h) { + x &= VRAM_W - 1; y &= VRAM_H - 1; + if (w > VRAM_W) w = VRAM_W; + if (h > VRAM_H) h = VRAM_H; + if (w <= 0 || h <= 0) return; + int w1 = w, w2 = 0, h1 = h, h2 = 0; + if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; } + if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; } + up_add(x, y, x + w1 - 1, y + h1 - 1); + if (w2) up_add(0, y, w2 - 1, y + h1 - 1); + if (h2) up_add(x, 0, x + w1 - 1, h2 - 1); + if (w2 && h2) up_add(0, 0, w2 - 1, h2 - 1); +} + +/* ---- coherency event ring (always-on, debug server "gl_coh_ring") -------- */ +/* Every coherency-relevant operation — upload flushes, fills, copies, draw + * bboxes, packs, full readbacks, presents, and probe perturbations — is + * recorded with its rect and frame number. Per CLAUDE.md ring-buffer rule: + * capture is continuous, observers query a window after the fact. Trigger + * attribution convention: an op that flushes internally (fill/copy/draw/ + * present/peek) records its own event AFTER the FLUSH event it caused, so + * the event following a FLUSH names the trigger. 16 B * 64 Ki = 1 MB. */ +extern uint64_t s_frame_count; /* defined in debug_server.c */ + +#define GL_COH_RING_CAP (1u << 16) +static GlCohEvent s_coh_ring[GL_COH_RING_CAP]; +static uint64_t s_coh_seq = 0; + +/* 16x16 native-pixel tiles changed since their last on-screen present. This + * lets a double-buffered 30 Hz game avoid swapping the unchanged front buffer + * on the intervening 60 Hz vblank without guessing from game identity. + * + * INVARIANT: eliding a swap also elides whatever that swap was blocking on. + * The frontend calls present once per guest vblank, so when the driver's swap + * block owns the guest cadence (psx_present_vsync_owns_cadence(), the XOR + * partner of the wall-clock pacer) an elided frame is an unthrottled frame: + * a 30 Hz-presenting game then advances two vblanks per block and runs at 2x. + * Every skip site below is therefore gated on that query. */ +#define PRES_TILE 16 +#define PRES_ROWS (VRAM_H / PRES_TILE) +static uint64_t s_present_dirty[PRES_ROWS]; +static int s_last_present_path = -1; +static int s_last_dx, s_last_dy, s_last_dw, s_last_dh; +/* After savestate restore: keep swapping for a few presents even when the + * display rect is byte-identical to the last swap. Double/triple-buffered + * windows otherwise can leave a stale back buffer on screen while vblanks + * (and FPS) keep advancing — especially on a 2nd+ load of the same slot. */ +static int s_force_present_remaining = 0; + +/* §33 rollback resim hold-last: sticky copy of the last Live present so + * mid-resim ticks can Swap a frozen frame without reading mutating VRAM. + * HOLD_DRAWABLE = full backbuffer copy taken just before SwapWindow. + * HOLD_NATIVE = GPU blit of a display band (used when interp owns Swap). */ +#define HOLD_NONE 0 +#define HOLD_DRAWABLE 1 +#define HOLD_NATIVE 2 +static int s_hold_kind = HOLD_NONE; +static GLuint s_hold_tex = 0; +static GLuint s_hold_fbo = 0; +static int s_hold_tw = 0, s_hold_th = 0; /* texture size in texels */ +static int s_hold_force_4_3 = 0; +static int s_hold_linear = 0; + +/* Post-load freeze probe (main.cpp): accumulate skip/swap/dirty marks. */ +static uint64_t s_probe_skip = 0; +static uint64_t s_probe_swap = 0; +static uint64_t s_probe_dirty_marks = 0; + +static void present_dirty_rect(int x0, int y0, int x1, int y1, int set) { + if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; + if (x1 >= VRAM_W) x1 = VRAM_W - 1; if (y1 >= VRAM_H) y1 = VRAM_H - 1; + if (x0 > x1 || y0 > y1) return; + int tx0 = x0 / PRES_TILE, tx1 = x1 / PRES_TILE; + uint64_t mask = (~0ull << tx0) & (~0ull >> (63 - tx1)); + for (int ty = y0 / PRES_TILE; ty <= y1 / PRES_TILE; ty++) { + if (set) s_present_dirty[ty] |= mask; else s_present_dirty[ty] &= ~mask; + } + if (set) s_probe_dirty_marks++; +} + +static int present_dirty_test(int x0, int y0, int x1, int y1) { + if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; + if (x1 >= VRAM_W) x1 = VRAM_W - 1; if (y1 >= VRAM_H) y1 = VRAM_H - 1; + if (x0 > x1 || y0 > y1) return 0; + int tx0 = x0 / PRES_TILE, tx1 = x1 / PRES_TILE; + uint64_t mask = (~0ull << tx0) & (~0ull >> (63 - tx1)); + for (int ty = y0 / PRES_TILE; ty <= y1 / PRES_TILE; ty++) + if (s_present_dirty[ty] & mask) return 1; + return 0; +} + +static void present_force_consumed(void) { + if (s_force_present_remaining > 0) + s_force_present_remaining--; +} + +static void hold_ensure_tex(int w, int h) { + if (w < 1) w = 1; + if (h < 1) h = 1; + if (!s_hold_tex) { + glGenTextures(1, &s_hold_tex); + if (!s_hold_fbo) + p_glGenFramebuffers(1, &s_hold_fbo); + } + if (s_hold_tw == w && s_hold_th == h) + return; + glBindTexture(GL_TEXTURE_2D, s_hold_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, + NULL); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hold_fbo); + p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, s_hold_tex, 0); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + s_hold_tw = w; + s_hold_th = h; +} + +/* Snapshot the just-drawn default backbuffer (letterbox + content) before Swap. + * Hold-last can then redraw this exact image without touching guest VRAM. */ +static void hold_capture_drawable(void) { + int ww = 0, wh = 0; + if (!s_ctx || !s_win) + return; + SDL_GL_GetDrawableSize(s_win, &ww, &wh); + if (ww < 1 || wh < 1) + return; + hold_ensure_tex(ww, wh); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + glBindTexture(GL_TEXTURE_2D, s_hold_tex); + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, ww, wh); + glBindTexture(GL_TEXTURE_2D, 0); + s_hold_kind = HOLD_DRAWABLE; + s_hold_force_4_3 = 0; + s_hold_linear = 0; +} + +/* Snapshot a native display band from an FBO when the main thread will not + * Swap (interpolation owns the cadence). Scale-aware blit into hold tex. */ +static void hold_capture_native_fbo(GLuint src_fbo, int dx, int dy, int dw, int dh, + int force_4_3, int linear) { + int S = s_scale > 0 ? s_scale : 1; + if (!s_ctx || !src_fbo || dw < 1 || dh < 1) + return; + hold_ensure_tex(dw * S, dh * S); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src_fbo); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_hold_fbo); + glDisable(GL_SCISSOR_TEST); + p_glBlitFramebuffer(dx * S, dy * S, (dx + dw) * S, (dy + dh) * S, + 0, 0, dw * S, dh * S, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); + s_hold_kind = HOLD_NATIVE; + s_hold_force_4_3 = force_4_3 ? 1 : 0; + s_hold_linear = linear ? 1 : 0; +} + +static void hold_invalidate(void) { + s_hold_kind = HOLD_NONE; +} + +/* Defined after present_target_quad / letterbox helpers. */ +static void letterbox_rect_aspect(int ww, int wh, int num, int den, + int *x, int *y, int *w, int *h); +static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h); +static void present_target_quad(GLuint tex, int tex_w, int tex_h, + int x, int y, int w, int h, int linear, + int lx, int ly, int lw, int lh, int v_flip); + +static void coh_record(int kind, int x0, int y0, int x1, int y1) { + GlCohEvent *e = &s_coh_ring[s_coh_seq % GL_COH_RING_CAP]; + e->frame = (uint32_t)s_frame_count; + e->kind = (uint8_t)kind; + e->x0 = (int16_t)x0; e->y0 = (int16_t)y0; + e->x1 = (int16_t)x1; e->y1 = (int16_t)y1; + s_coh_seq++; + if (kind == GL_COH_FLUSH || kind == GL_COH_FILL || + kind == GL_COH_COPY || kind == GL_COH_DRAW) + present_dirty_rect(x0, y0, x1, y1, 1); +} + +uint64_t gl_renderer_coh_total(void) { return s_coh_seq; } +int gl_renderer_coh_get(uint64_t seq, GlCohEvent *out) { + if (seq >= s_coh_seq) return 0; + if (s_coh_seq - seq > GL_COH_RING_CAP) return 0; /* evicted */ + *out = s_coh_ring[seq % GL_COH_RING_CAP]; + return 1; +} + +/* ---- present ring (always-on, debug server "present_ring") --------------- */ +/* Records every SwapWindow (vram/wide/cpu/blank/interpolated) and its + * source + letterbox rects. PSX_GL_PRESENT_PROBE=1 additionally drains + * glGetError and samples one backbuffer pixel before the swap; that synchronous + * diagnostic is intentionally opt-in. Observers query a window after the fact. */ +#define GL_PRES_RING_CAP 4096 +static GlPresEvent s_pres_ring[GL_PRES_RING_CAP]; +static uint64_t s_pres_seq = 0; + +static void pres_record(int path, int dx, int dy, int w, int h, + int lx, int ly, int lw, int lh) { + /* The ring metadata stays always-on, but pixel probing must not: each + * glReadPixels synchronously drains queued GPU work. Two probes per frame + * were enough to make Tomba 2 miss its frame budget. */ + static int probe_pixels = -1; + if (probe_pixels < 0) { + const char *cfg = getenv("PSX_GL_PRESENT_PROBE"); + probe_pixels = (cfg && cfg[0] == '1') ? 1 : 0; + } + GlPresEvent *e = &s_pres_ring[s_pres_seq % GL_PRES_RING_CAP]; + e->frame = (uint32_t)s_frame_count; + e->t_ms = (uint32_t)SDL_GetTicks(); + e->path = (uint8_t)path; + e->glerr = probe_pixels ? (uint16_t)glGetError() : 0; + e->dx = (int16_t)dx; e->dy = (int16_t)dy; + e->w = (int16_t)w; e->h = (int16_t)h; + e->lx = (int16_t)lx; e->ly = (int16_t)ly; + e->lw = (int16_t)lw; e->lh = (int16_t)lh; + /* Backbuffer sample at the letterbox centre (GL bottom-origin; the rects + * we pass in are already bottom-origin GL window coords). */ + uint8_t px[3] = { 0, 0, 0 }; + if (probe_pixels && lw > 0 && lh > 0) { + glReadBuffer(GL_BACK); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(lx + lw / 2, ly + lh / 2, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, px); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + } + e->px_r = px[0]; e->px_g = px[1]; e->px_b = px[2]; + /* Blit-source sample: the hr FBO pixel at the display-rect centre. Splits + * "FBO content was black" from "the blit malfunctioned". */ + uint8_t sp[3] = { 0, 0, 0 }; + e->src_valid = 0; + if (probe_pixels && (path == GL_PRES_VRAM) && w > 0 && h > 0 && s_hr_fbo) { + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels((dx + w / 2) * s_scale, (dy + h / 2) * s_scale, + 1, 1, GL_RGB, GL_UNSIGNED_BYTE, sp); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + e->src_valid = 1; + } + e->src_r = sp[0]; e->src_g = sp[1]; e->src_b = sp[2]; + s_pres_seq++; +} + +uint64_t gl_renderer_pres_total(void) { return s_pres_seq; } +int gl_renderer_pres_get(uint64_t seq, GlPresEvent *out) { + if (seq >= s_pres_seq) return 0; + if (s_pres_seq - seq > GL_PRES_RING_CAP) return 0; /* evicted */ + *out = s_pres_ring[seq % GL_PRES_RING_CAP]; + return 1; +} + +/* ---- shaders ------------------------------------------------------------ */ +static const char *PRESENT_VS = + "#version 330\n" + "out vec2 v_uv;\n" + "uniform vec4 u_uv_rect;\n" + "void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n" + " v_uv = vec2(mix(u_uv_rect.x,u_uv_rect.z,p.x),\n" + " mix(u_uv_rect.y,u_uv_rect.w,1.0-p.y));\n" + " gl_Position = vec4(p*2.0-1.0,0.0,1.0); }\n"; /* Present sampling. u_sharp==0 is the historical behaviour: sample straight at * v_uv, so the texture's own filter (NEAREST or LINEAR) decides everything. * @@ -884,38 +884,38 @@ static const char *PRESENT_VS = * there. The result is clamped to u_uv_rect so the half-texel edge inset the * caller applied still holds — that inset is what keeps LINEAR from bleeding * the border texel into the image (the old reason FMV was pinned to NEAREST). */ -/* Scanline post-process, shared by the present and interpolation shaders. - * Darkens toward the gap between PS1 scanlines with a soft sinusoidal beam - * centred on each line. The pitch is u_scanline_lines (the number of native - * display lines across the source rect), so it tracks the PS1 line grid, not - * the internal SSAA scale or the window size. u_scanline_scale is output pixels - * per PS1 line; the gate fades the effect in from 1x to 2x so it never shimmers - * on a window too small to resolve one line as two rows (Nyquist). Computed - * against v_uv.y (position across the display region) so it is orientation- and - * scale-agnostic. */ -#define PSX_SCANLINE_UNIFORMS \ - "uniform int u_scanline;\n" \ - "uniform float u_scanline_strength;\n" \ - "uniform float u_scanline_lines;\n" \ - "uniform float u_scanline_scale;\n" -#define PSX_SCANLINE_FUNC \ - "vec3 psx_scanline(vec3 rgb, float vy){\n" \ - " if (u_scanline == 0) return rgb;\n" \ - " float gate = clamp(u_scanline_scale - 1.0, 0.0, 1.0);\n" \ - " if (gate <= 0.0) return rgb;\n" \ - " float p = fract(vy * u_scanline_lines);\n" \ - " float beam = sin(3.14159265 * p);\n" \ - " float mult = 1.0 - (u_scanline_strength * gate) * (1.0 - beam);\n" \ - " return rgb * mult;\n" \ - "}\n" -static const char *PRESENT_FS = - "#version 330\n" - "in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n" +/* Scanline post-process, shared by the present and interpolation shaders. + * Darkens toward the gap between PS1 scanlines with a soft sinusoidal beam + * centred on each line. The pitch is u_scanline_lines (the number of native + * display lines across the source rect), so it tracks the PS1 line grid, not + * the internal SSAA scale or the window size. u_scanline_scale is output pixels + * per PS1 line; the gate fades the effect in from 1x to 2x so it never shimmers + * on a window too small to resolve one line as two rows (Nyquist). Computed + * against v_uv.y (position across the display region) so it is orientation- and + * scale-agnostic. */ +#define PSX_SCANLINE_UNIFORMS \ + "uniform int u_scanline;\n" \ + "uniform float u_scanline_strength;\n" \ + "uniform float u_scanline_lines;\n" \ + "uniform float u_scanline_scale;\n" +#define PSX_SCANLINE_FUNC \ + "vec3 psx_scanline(vec3 rgb, float vy){\n" \ + " if (u_scanline == 0) return rgb;\n" \ + " float gate = clamp(u_scanline_scale - 1.0, 0.0, 1.0);\n" \ + " if (gate <= 0.0) return rgb;\n" \ + " float p = fract(vy * u_scanline_lines);\n" \ + " float beam = sin(3.14159265 * p);\n" \ + " float mult = 1.0 - (u_scanline_strength * gate) * (1.0 - beam);\n" \ + " return rgb * mult;\n" \ + "}\n" +static const char *PRESENT_FS = + "#version 330\n" + "in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n" "uniform vec4 u_uv_rect;\n" "uniform vec2 u_tex_size;\n" "uniform vec2 u_sharp_scale;\n" "uniform int u_sharp;\n" - PSX_SCANLINE_UNIFORMS + PSX_SCANLINE_UNIFORMS /* Catmull-Rom bicubic via 9 bilinear taps. Sharper than plain bilinear at * the same smoothness, with mild overshoot that reads as edge definition. * The present texture holds exactly the source rect and wraps CLAMP_TO_EDGE, @@ -944,1592 +944,1592 @@ static const char *PRESENT_FS = " r += texture(u_tex, vec2(t3.x , t3.y )) * (w3.x * w3.y );\n" " return r;\n" "}\n" - PSX_SCANLINE_FUNC + PSX_SCANLINE_FUNC "void main(){\n" " vec2 uv = v_uv;\n" - " vec4 c;\n" - " if (u_sharp == 2) { c = bicubic(uv); }\n" - " else {\n" - " if (u_sharp == 1) {\n" - " vec2 scale = max(u_sharp_scale, vec2(1.0));\n" - " vec2 texel = uv * u_tex_size;\n" - " vec2 tf = floor(texel);\n" - " vec2 cd = (texel - tf) - 0.5;\n" - " vec2 band = 0.5 - 0.5 / scale;\n" - " vec2 f = (cd - clamp(cd, -band, band)) * scale + 0.5;\n" - " uv = (tf + f) / u_tex_size;\n" - " vec2 lo = min(u_uv_rect.xy, u_uv_rect.zw);\n" - " vec2 hi = max(u_uv_rect.xy, u_uv_rect.zw);\n" - " uv = clamp(uv, lo, hi);\n" - " }\n" - " c = texture(u_tex, uv);\n" + " vec4 c;\n" + " if (u_sharp == 2) { c = bicubic(uv); }\n" + " else {\n" + " if (u_sharp == 1) {\n" + " vec2 scale = max(u_sharp_scale, vec2(1.0));\n" + " vec2 texel = uv * u_tex_size;\n" + " vec2 tf = floor(texel);\n" + " vec2 cd = (texel - tf) - 0.5;\n" + " vec2 band = 0.5 - 0.5 / scale;\n" + " vec2 f = (cd - clamp(cd, -band, band)) * scale + 0.5;\n" + " uv = (tf + f) / u_tex_size;\n" + " vec2 lo = min(u_uv_rect.xy, u_uv_rect.zw);\n" + " vec2 hi = max(u_uv_rect.xy, u_uv_rect.zw);\n" + " uv = clamp(uv, lo, hi);\n" + " }\n" + " c = texture(u_tex, uv);\n" " }\n" - " c.rgb = psx_scanline(c.rgb, v_uv.y);\n" - " frag = c;\n" + " c.rgb = psx_scanline(c.rgb, v_uv.y);\n" + " frag = c;\n" "}\n"; -static const char *INTERP_FS = - "#version 330\n" - "in vec2 v_uv; uniform sampler2D u_prev; uniform sampler2D u_curr;\n" - "uniform float u_alpha; uniform int u_blend_mode; out vec4 frag;\n" - PSX_SCANLINE_UNIFORMS - PSX_SCANLINE_FUNC - "void main(){\n" - " vec4 prev=texture(u_prev,v_uv), curr=texture(u_curr,v_uv);\n" - " float alpha=u_alpha;\n" - " if(u_blend_mode==1){\n" - " vec3 d=abs(prev.rgb-curr.rgb);\n" - " float change=max(max(d.r,d.g),d.b);\n" - " float safe_blend=1.0-smoothstep(0.08,0.20,change);\n" - " alpha=mix(step(0.5,u_alpha),u_alpha,safe_blend);\n" - " }\n" - " vec4 c=mix(prev,curr,alpha);\n" - " c.rgb=psx_scanline(c.rgb, v_uv.y);\n" - " frag=c;\n" - "}\n"; - -/* Geometry: position in VRAM pixels (draw offset already applied by gpu.c), - * color rgb in 0..1, color a = mask bit (0/1). The clip transform is in - * native VRAM space; the viewport at S* the size scales rasterization. - * - * ALL drawn prims shift positions by u_shift = half an HR pixel (0.5/S in - * native units): GL samples coverage/attributes at pixel CENTERS, the PS1 - * DDA at INTEGER coords. The shift aligns GL's sample grid with the PS1 - * grid — without it, any texture mapping with slope != 1 (scaled sprites, - * squished menu fonts) samples one texel off per row/column (striped - * glyphs, seam lines). Half an HR pixel (not half a native pixel!) keeps - * rect coverage exactly [x*S, (x+w)*S) at every scale AND makes the - * top-left subpixel of each S*S block sample the exact PS1 value (which is - * what the PACK pass reads back). */ -static const char *GEO_VS = - "#version 330\n" - "layout(location=0) in vec2 a_pos;\n" - "layout(location=1) in vec4 a_col;\n" - "uniform float u_shift;\n" - "uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n" - "uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n" - "uniform float u_xscale; /* native-wide 2D-backdrop x-stretch; 1 canonical */\n" - "uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n" - "noperspective out vec4 v_col;\n" - "void main(){ v_col = a_col;\n" - " float xb = a_pos.x;\n" - " if (u_xscale < 0.0) {\n" - " float s = -u_xscale; float h = u_xhalf / s;\n" - " float l = u_xcenter - h, r = u_xcenter + h;\n" - " if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n" - " } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n" - " gl_Position = vec4((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n"; -static const char *GEO_FS = - "#version 330\n" - "noperspective in vec4 v_col; out vec4 frag;\n" - "void main(){ frag = v_col; }\n"; - -/* Textured prims: sample raw 1555 VRAM (integer), CLUT decode per depth, - * texture window, optional bilinear, texel-0 discard, STP-split discard, - * PS1 *2-around-0x80 modulation. Output alpha = bit15 of the written pixel. - * Texel coords use floor() to match the software rasterizer's truncation - * (rounding shifted sampling +1 texel half the time: smeared text). */ -/* Textured program. Per-prim texture state (texpage, clut, depth, raw, uv - * limits) is carried in FLAT vertex attributes — constant across a prim's - * vertices — instead of uniforms, so consecutive textured prims with the same - * blend/mask/texture-window state batch into one draw (see flush_tex_batch). - * The remaining uniforms (u_twin/u_maskset/u_filter/u_semipass) are the batch - * keys + per-pass state. */ -static const char *TEX_VS = - "#version 330\n" - "layout(location=0) in vec2 a_pos;\n" - "layout(location=1) in vec2 a_uv;\n" - "layout(location=2) in vec4 a_col;\n" - "layout(location=3) in vec2 a_tpage;\n" - "layout(location=4) in vec2 a_clut;\n" - "layout(location=5) in float a_depth;\n" - "layout(location=6) in float a_raw;\n" - "layout(location=7) in vec4 a_limits;\n" - "layout(location=8) in float a_semi;\n" - "layout(location=9) in float a_q; /* persp weight; 0 = affine (default) */\n" - "uniform float u_shift;\n" - "uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n" - "uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n" - "uniform float u_xscale; /* native-wide 2D-backdrop x-stretch; 1 canonical */\n" - "uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n" - "noperspective out vec2 v_uv; noperspective out vec4 v_col;\n" - "smooth out vec2 v_uv_p; /* perspective-correct UV (used when v_persp!=0) */\n" - "flat out int v_persp;\n" - "flat out ivec2 v_tpage; flat out ivec2 v_clut; flat out int v_depth;\n" - "flat out int v_raw; flat out ivec4 v_limits; flat out int v_semi;\n" - "void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col;\n" - " v_persp = (a_q > 0.0) ? 1 : 0;\n" - " v_tpage = ivec2(a_tpage + 0.5); v_clut = ivec2(a_clut + 0.5);\n" - " v_depth = int(a_depth + 0.5); v_raw = int(a_raw + 0.5);\n" - " v_semi = int(a_semi + 0.5);\n" - " v_limits = ivec4(floor(a_limits + 0.5));\n" - " /* u_shift: align GL's center-sample grid with the PS1 integer grid (see\n" - " * GEO_VS) so interpolated uv at a fragment equals the PS1 DDA value. */\n" - " float xb = a_pos.x;\n" - " if (u_xscale < 0.0) {\n" - " float s = -u_xscale; float h = u_xhalf / s;\n" - " float l = u_xcenter - h, r = u_xcenter + h;\n" - " if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n" - " } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n" - " /* Perspective-correct UV rides the standard w divide: emit clip coords\n" - " * pre-multiplied by w = 1/q so the post-divide NDC is unchanged while\n" - " * the rasterizer interpolates the smooth varying hyperbolically. With\n" - " * a_q == 0 (feature off) w is exactly 1.0 and this is the old expression. */\n" - " float w = (a_q > 0.0) ? (1.0 / a_q) : 1.0;\n" - " vec2 ndc = vec2((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0);\n" - " gl_Position = vec4(ndc * w, 0.0, w); }\n"; -static const char *TEX_FS = - "#version 330\n" - "noperspective in vec2 v_uv; noperspective in vec4 v_col;\n" - "smooth in vec2 v_uv_p; flat in int v_persp;\n" - "out vec4 frag; out vec4 blend_factor;\n" - "flat in ivec2 v_tpage; /* texture page base, VRAM px */\n" - "flat in ivec2 v_clut; /* CLUT base, VRAM px */\n" - "flat in int v_depth; /* 0=4bit 1=8bit 2=15bit */\n" - "flat in int v_raw; /* 1 = no color modulation */\n" - "flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n" - "flat in int v_semi; /* GP0 command has semi-transparency enabled */\n" - "uniform usampler2D u_vram;\n" - "uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n" - "uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n" +static const char *INTERP_FS = + "#version 330\n" + "in vec2 v_uv; uniform sampler2D u_prev; uniform sampler2D u_curr;\n" + "uniform float u_alpha; uniform int u_blend_mode; out vec4 frag;\n" + PSX_SCANLINE_UNIFORMS + PSX_SCANLINE_FUNC + "void main(){\n" + " vec4 prev=texture(u_prev,v_uv), curr=texture(u_curr,v_uv);\n" + " float alpha=u_alpha;\n" + " if(u_blend_mode==1){\n" + " vec3 d=abs(prev.rgb-curr.rgb);\n" + " float change=max(max(d.r,d.g),d.b);\n" + " float safe_blend=1.0-smoothstep(0.08,0.20,change);\n" + " alpha=mix(step(0.5,u_alpha),u_alpha,safe_blend);\n" + " }\n" + " vec4 c=mix(prev,curr,alpha);\n" + " c.rgb=psx_scanline(c.rgb, v_uv.y);\n" + " frag=c;\n" + "}\n"; + +/* Geometry: position in VRAM pixels (draw offset already applied by gpu.c), + * color rgb in 0..1, color a = mask bit (0/1). The clip transform is in + * native VRAM space; the viewport at S* the size scales rasterization. + * + * ALL drawn prims shift positions by u_shift = half an HR pixel (0.5/S in + * native units): GL samples coverage/attributes at pixel CENTERS, the PS1 + * DDA at INTEGER coords. The shift aligns GL's sample grid with the PS1 + * grid — without it, any texture mapping with slope != 1 (scaled sprites, + * squished menu fonts) samples one texel off per row/column (striped + * glyphs, seam lines). Half an HR pixel (not half a native pixel!) keeps + * rect coverage exactly [x*S, (x+w)*S) at every scale AND makes the + * top-left subpixel of each S*S block sample the exact PS1 value (which is + * what the PACK pass reads back). */ +static const char *GEO_VS = + "#version 330\n" + "layout(location=0) in vec2 a_pos;\n" + "layout(location=1) in vec4 a_col;\n" + "uniform float u_shift;\n" + "uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n" + "uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n" + "uniform float u_xscale; /* native-wide 2D-backdrop x-stretch; 1 canonical */\n" + "uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n" + "noperspective out vec4 v_col;\n" + "void main(){ v_col = a_col;\n" + " float xb = a_pos.x;\n" + " if (u_xscale < 0.0) {\n" + " float s = -u_xscale; float h = u_xhalf / s;\n" + " float l = u_xcenter - h, r = u_xcenter + h;\n" + " if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n" + " } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n" + " gl_Position = vec4((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n"; +static const char *GEO_FS = + "#version 330\n" + "noperspective in vec4 v_col; out vec4 frag;\n" + "void main(){ frag = v_col; }\n"; + +/* Textured prims: sample raw 1555 VRAM (integer), CLUT decode per depth, + * texture window, optional bilinear, texel-0 discard, STP-split discard, + * PS1 *2-around-0x80 modulation. Output alpha = bit15 of the written pixel. + * Texel coords use floor() to match the software rasterizer's truncation + * (rounding shifted sampling +1 texel half the time: smeared text). */ +/* Textured program. Per-prim texture state (texpage, clut, depth, raw, uv + * limits) is carried in FLAT vertex attributes — constant across a prim's + * vertices — instead of uniforms, so consecutive textured prims with the same + * blend/mask/texture-window state batch into one draw (see flush_tex_batch). + * The remaining uniforms (u_twin/u_maskset/u_filter/u_semipass) are the batch + * keys + per-pass state. */ +static const char *TEX_VS = + "#version 330\n" + "layout(location=0) in vec2 a_pos;\n" + "layout(location=1) in vec2 a_uv;\n" + "layout(location=2) in vec4 a_col;\n" + "layout(location=3) in vec2 a_tpage;\n" + "layout(location=4) in vec2 a_clut;\n" + "layout(location=5) in float a_depth;\n" + "layout(location=6) in float a_raw;\n" + "layout(location=7) in vec4 a_limits;\n" + "layout(location=8) in float a_semi;\n" + "layout(location=9) in float a_q; /* persp weight; 0 = affine (default) */\n" + "uniform float u_shift;\n" + "uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n" + "uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n" + "uniform float u_xscale; /* native-wide 2D-backdrop x-stretch; 1 canonical */\n" + "uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n" + "noperspective out vec2 v_uv; noperspective out vec4 v_col;\n" + "smooth out vec2 v_uv_p; /* perspective-correct UV (used when v_persp!=0) */\n" + "flat out int v_persp;\n" + "flat out ivec2 v_tpage; flat out ivec2 v_clut; flat out int v_depth;\n" + "flat out int v_raw; flat out ivec4 v_limits; flat out int v_semi;\n" + "void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col;\n" + " v_persp = (a_q > 0.0) ? 1 : 0;\n" + " v_tpage = ivec2(a_tpage + 0.5); v_clut = ivec2(a_clut + 0.5);\n" + " v_depth = int(a_depth + 0.5); v_raw = int(a_raw + 0.5);\n" + " v_semi = int(a_semi + 0.5);\n" + " v_limits = ivec4(floor(a_limits + 0.5));\n" + " /* u_shift: align GL's center-sample grid with the PS1 integer grid (see\n" + " * GEO_VS) so interpolated uv at a fragment equals the PS1 DDA value. */\n" + " float xb = a_pos.x;\n" + " if (u_xscale < 0.0) {\n" + " float s = -u_xscale; float h = u_xhalf / s;\n" + " float l = u_xcenter - h, r = u_xcenter + h;\n" + " if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n" + " } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n" + " /* Perspective-correct UV rides the standard w divide: emit clip coords\n" + " * pre-multiplied by w = 1/q so the post-divide NDC is unchanged while\n" + " * the rasterizer interpolates the smooth varying hyperbolically. With\n" + " * a_q == 0 (feature off) w is exactly 1.0 and this is the old expression. */\n" + " float w = (a_q > 0.0) ? (1.0 / a_q) : 1.0;\n" + " vec2 ndc = vec2((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0);\n" + " gl_Position = vec4(ndc * w, 0.0, w); }\n"; +static const char *TEX_FS = + "#version 330\n" + "noperspective in vec2 v_uv; noperspective in vec4 v_col;\n" + "smooth in vec2 v_uv_p; flat in int v_persp;\n" + "out vec4 frag; out vec4 blend_factor;\n" + "flat in ivec2 v_tpage; /* texture page base, VRAM px */\n" + "flat in ivec2 v_clut; /* CLUT base, VRAM px */\n" + "flat in int v_depth; /* 0=4bit 1=8bit 2=15bit */\n" + "flat in int v_raw; /* 1 = no color modulation */\n" + "flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n" + "flat in int v_semi; /* GP0 command has semi-transparency enabled */\n" + "uniform usampler2D u_vram;\n" + "uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n" + "uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n" "uniform ivec4 u_twin; /* texture window: mask_x, mask_y, off_x, off_y */\n" "uniform int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n" "uniform int u_filter; /* 1 = bilinear */\n" "uniform float u_shift;\n" "int vram_at(int x, int y){\n" - " return int(texelFetch(u_vram, ivec2(x & 1023, y & 511), 0).r);\n" - "}\n" - "int fetch_texel(int u, int v){\n" - " u &= 255; v &= 255;\n" - " if ((u_twin.x | u_twin.y) != 0) {\n" - " u = (u & ~(u_twin.x * 8)) | ((u_twin.z & u_twin.x) * 8);\n" - " v = (v & ~(u_twin.y * 8)) | ((u_twin.w & u_twin.y) * 8);\n" - " } else {\n" - " u = clamp(u, v_limits.x, v_limits.z);\n" - " v = clamp(v, v_limits.y, v_limits.w);\n" - " }\n" - " if (v_depth == 0) {\n" - " int px = vram_at(v_tpage.x + (u >> 2), v_tpage.y + v);\n" - " return vram_at(v_clut.x + ((px >> ((u & 3) * 4)) & 0xF), v_clut.y);\n" - " } else if (v_depth == 1) {\n" - " int px = vram_at(v_tpage.x + (u >> 1), v_tpage.y + v);\n" - " return vram_at(v_clut.x + ((px >> ((u & 1) * 8)) & 0xFF), v_clut.y);\n" - " }\n" - " return vram_at(v_tpage.x + u, v_tpage.y + v);\n" - "}\n" - "vec3 col5(int raw){\n" - " return vec3(float(raw & 31), float((raw >> 5) & 31), float((raw >> 10) & 31)) / 31.0;\n" - "}\n" - "void main(){\n" - " int stp; vec3 rgb;\n" - " /* v_persp is 0 for every prim unless [video] perspective_texturing is on\n" - " * AND this prim's packet carried full GTE projection provenance, so the\n" - " * default is the PS1's affine (noperspective) mapping. */\n" - " vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;\n" - " if (u_filter == 0) {\n" - " int raw = fetch_texel(int(floor(uv.x)), int(floor(uv.y)));\n" - " if (raw == 0) discard;\n" - " rgb = col5(raw);\n" - " stp = (raw >> 15) & 1;\n" - " } else {\n" - " /* Bilinear, Beetle-PSX formulation: the NEAREST texel is the base\n" - " * (cutout + STP authority), the neighbours lie toward the sub-texel\n" - " * offset and clamp to u_limits, and each texel's weight is gated by\n" - " * its opacity with the colour renormalised — so prim edges and\n" - " * cutout borders keep their colour instead of dissolving into the\n" - " * transparent (black) neighbour and discarding whole edge columns.\n" - " * Recenter from PS1 top-left point sampling to the bilinear footprint. */\n" - " uv += vec2(u_shift);\n" - " int iu = int(floor(uv.x)), iv = int(floor(uv.y));\n" - " float fx = uv.x - float(iu) - 0.5, fy = uv.y - float(iv) - 0.5;\n" - " int sx = fx < 0.0 ? -1 : 1, sy = fy < 0.0 ? -1 : 1;\n" - " fx = abs(fx); fy = abs(fy);\n" - " int c00 = fetch_texel(iu, iv);\n" - " if (c00 == 0) discard;\n" - " int c10 = fetch_texel(iu + sx, iv);\n" - " int c01 = fetch_texel(iu, iv + sy);\n" - " int c11 = fetch_texel(iu + sx, iv + sy);\n" - " float w00 = (c00 == 0 ? 0.0 : 1.0) * (1.0 - fx) * (1.0 - fy);\n" - " float w10 = (c10 == 0 ? 0.0 : 1.0) * fx * (1.0 - fy);\n" - " float w01 = (c01 == 0 ? 0.0 : 1.0) * (1.0 - fx) * fy;\n" - " float w11 = (c11 == 0 ? 0.0 : 1.0) * fx * fy;\n" - " float opac = w00 + w10 + w01 + w11;\n" - " rgb = (col5(c00)*w00 + col5(c10)*w10 + col5(c01)*w01 + col5(c11)*w11) / opac;\n" - " float stpf = (float((c00 >> 15) & 1) * w00 + float((c10 >> 15) & 1) * w10\n" - " + float((c01 >> 15) & 1) * w01 + float((c11 >> 15) & 1) * w11) / opac;\n" - " stp = stpf >= 0.5 ? 1 : 0;\n" - " }\n" - " if (u_semipass == 1 && stp == 1) discard;\n" - " if (u_semipass == 2 && stp == 0) discard;\n" - " if (v_raw == 0) rgb = clamp(rgb * v_col.rgb * 2.0, 0.0, 1.0);\n" - " float dst_factor = 0.0;\n" - " if (u_semimode == 4 && v_semi != 0 && stp != 0) {\n" - " dst_factor = v_semi == 1 ? 0.5 : 1.0;\n" - " if (v_semi == 1) rgb *= 0.5; else if (v_semi == 4) rgb *= 0.25;\n" - " }\n" - " frag = vec4(rgb, (stp == 1 || u_maskset == 1) ? 1.0 : 0.0);\n" - " blend_factor = vec4(0.0, 0.0, 0.0, dst_factor);\n" - "}\n"; - -/* Quad blit: used for CPU->VRAM upload flushes and VRAM->VRAM copies. - * Samples an RGBA8 source (alpha = bit15), splits by STP for the stencil - * write, optionally ORs set-mask into the output alpha. */ -static const char *BLIT_VS = - "#version 330\n" - "layout(location=0) in vec2 a_pos; /* native VRAM px */\n" - "uniform float u_shift;\n" - "void main(){\n" - " gl_Position = vec4((a_pos.x+u_shift)/512.0 - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n"; -static const char *BLIT_FS = - "#version 330\n" - "out vec4 frag;\n" - "uniform sampler2D u_src;\n" - "uniform int u_stp_pass; /* 0=all, 1=bit15=0 only, 2=bit15=1 only */\n" - "uniform int u_maskset;\n" - "uniform int u_src_div; /* fragcoord -> src texel divisor (S for native-res\n" - " sources, 1 for hr-res sources) */\n" - "uniform ivec2 u_src_off; /* added after the divide, in src texel units */\n" - "void main(){\n" - " /* Exact integer source fetch — no normalized-uv edge precision. */\n" - " ivec2 p = ivec2(gl_FragCoord.xy);\n" - " vec4 c = texelFetch(u_src, p / u_src_div + u_src_off, 0);\n" - " bool stp = c.a >= 0.5;\n" - " if (u_stp_pass == 1 && stp) discard;\n" - " if (u_stp_pass == 2 && !stp) discard;\n" - " frag = vec4(c.rgb, (stp || u_maskset != 0) ? 1.0 : 0.0);\n" - "}\n"; - -/* Pack: re-encode the hr FBO into the native R16UI raw mirror. Runs over a - * native-res viewport with scissor = dirty rect; each native pixel takes the - * top-left sample of its S*S block (the sample at the exact native coord). */ -static const char *PACK_VS = - "#version 330\n" - "void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n" - " gl_Position = vec4(p*2.0-1.0, 0.0, 1.0); }\n"; -static const char *PACK_FS = - "#version 330\n" - "uniform sampler2D u_hr;\n" - "uniform int u_scale;\n" - "out uint o_pix;\n" - "void main(){\n" - " ivec2 p = ivec2(gl_FragCoord.xy);\n" - " vec4 c = texelFetch(u_hr, p * u_scale, 0);\n" - " uint r = uint(c.r * 255.0 + 0.5) >> 3;\n" - " uint g = uint(c.g * 255.0 + 0.5) >> 3;\n" - " uint b = uint(c.b * 255.0 + 0.5) >> 3;\n" - " o_pix = r | (g << 5) | (b << 10) | (c.a >= 0.5 ? 0x8000u : 0u);\n" - "}\n"; - -/* Rebuild stencil bit 0 from a copied RGBA target's alpha. Color writes are - * disabled while this shader runs; only alpha>=0.5 fragments replace stencil. */ -static const char *STENCIL_FS = - "#version 330\n" - "uniform sampler2D u_src; out vec4 frag;\n" - "void main(){ vec4 c=texelFetch(u_src,ivec2(gl_FragCoord.xy),0);\n" - " if(c.a<0.5) discard; frag=vec4(0.0); }\n"; - -static GLuint compile_shader(GLenum type, const char *src) { - GLuint s = p_glCreateShader(type); - p_glShaderSource(s, 1, &src, NULL); - p_glCompileShader(s); - GLint ok = 0; p_glGetShaderiv(s, PSXGL_COMPILE_STATUS, &ok); - if (!ok) { char log[1024]; log[0]=0; p_glGetShaderInfoLog(s, sizeof log, NULL, log); - fprintf(stdout, "psxrecomp: GL shader compile failed: %s\n", log); - p_glDeleteShader(s); return 0; } - return s; -} -static GLuint build_program_ex(const char *vs, const char *fs, int dual_source) { - GLuint v = compile_shader(PSXGL_VERTEX_SHADER, vs), f = compile_shader(PSXGL_FRAGMENT_SHADER, fs); - if (!v || !f) return 0; - GLuint p = p_glCreateProgram(); - p_glAttachShader(p, v); p_glAttachShader(p, f); - if (dual_source) { - p_glBindFragDataLocationIndexed(p, 0, 0, "frag"); - p_glBindFragDataLocationIndexed(p, 0, 1, "blend_factor"); - } - p_glLinkProgram(p); - p_glDeleteShader(v); p_glDeleteShader(f); - GLint ok = 0; p_glGetProgramiv(p, PSXGL_LINK_STATUS, &ok); - if (!ok) { char log[1024]; log[0]=0; p_glGetProgramInfoLog(p, sizeof log, NULL, log); - fprintf(stdout, "psxrecomp: GL program link failed: %s\n", log); return 0; } - return p; -} -static GLuint build_program(const char *vs, const char *fs) { - return build_program_ex(vs, fs, 0); -} - -/* ---- pixel conversion (PS1 1555: bit15=mask, B[14:10] G[9:5] R[4:0]) ---- */ -static inline uint32_t conv_1555_to_rgba8(uint16_t p) { - uint32_t r = (p & 0x1F) << 3, g = ((p >> 5) & 0x1F) << 3, b = ((p >> 10) & 0x1F) << 3; - uint32_t a = (p >> 15) & 1 ? 0xFF : 0; - return r | (g << 8) | (b << 16) | (a << 24); /* RGBA8 little-endian */ -} - -/* ---- mask-bit stencil --------------------------------------------------- * - * Stencil bit0 mirrors bit15 of every pixel. - * check off: test ALWAYS, op REPLACE with ref = write value. - * check on: test EQUAL 0 (pass iff dest unmasked). GL couples the REPLACE - * value to the test reference, so write a 1 via INVERT (the - * stored value is known to be 0 when the test passed) and a 0 - * via KEEP. */ -static void mask_stencil(int write_val) { - glEnable(GL_STENCIL_TEST); - glStencilMask(0x01); - if (s_mask_check) { - glStencilFunc(GL_EQUAL, 0, 0x01); - glStencilOp(GL_KEEP, GL_KEEP, write_val ? GL_INVERT : GL_KEEP); - } else { - glStencilFunc(GL_ALWAYS, write_val ? 1 : 0, 0x01); - glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); - } -} -/* Like mask_stencil but never checks (uploads: gpu.c already applied mask). */ -static void plain_stencil(int write_val) { - glEnable(GL_STENCIL_TEST); - glStencilMask(0x01); - glStencilFunc(GL_ALWAYS, write_val ? 1 : 0, 0x01); - glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); -} - -/* PS1 semi-transparency as fixed-function blending, RGB only — the alpha - * channel (mask bit) is always replaced by the source fragment's alpha. - * 0: B/2 + F/2 1: B + F 2: B - F 3: B + F/4 */ -static void apply_psx_blend(int mode) { - glEnable(GL_BLEND); - p_glBlendEquationSeparate((mode & 3) == 2 ? PSXGL_FUNC_REVERSE_SUBTRACT - : PSXGL_FUNC_ADD, - PSXGL_FUNC_ADD); - switch (mode & 3) { - case 0: - p_glBlendColor(0.5f, 0.5f, 0.5f, 0.5f); - p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ZERO); - break; - case 1: - case 2: - p_glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ZERO); - break; - case 3: - p_glBlendColor(0.25f, 0.25f, 0.25f, 0.25f); - p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ONE, GL_ZERO); - break; - } -} - -/* ---- hr FBO render-state bracket ---------------------------------------- */ -static void hr_begin(int clip_to_draw_area) { - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); - glViewport(0, 0, VRAM_W * s_scale, VRAM_H * s_scale); - glEnable(GL_SCISSOR_TEST); - if (clip_to_draw_area) { - int sw = s_area_x2 - s_area_x1 + 1, sh = s_area_y2 - s_area_y1 + 1; - if (sw < 0) sw = 0; if (sh < 0) sh = 0; - glScissor(s_area_x1 * s_scale, s_area_y1 * s_scale, - sw * s_scale, sh * s_scale); - } -} -static void hr_end(void) { - glDisable(GL_BLEND); - /* apply_psx_blend mode 2 leaves REVERSE_SUBTRACT armed; reset so later - * host draws (OSD) that re-enable blend do not inherit B-F math. */ - if (p_glBlendEquationSeparate) - p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); - glDisable(GL_STENCIL_TEST); - glDisable(GL_SCISSOR_TEST); - p_glBindVertexArray(0); - p_glUseProgram(0); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); -} - -/* ---- coherency: CPU -> GPU upload flush --------------------------------- */ -/* CPU-side VRAM writes (GP0 A0 transfers, DMA, single pixel pokes) land in - * the CPU array immediately and accumulate s_up_rects. Flushing before the - * next GPU op (or readback/present) preserves PS1 command order. */ -static void flush_cpu_upload(void) { - if (!s_raster_ok || s_up_nrects == 0) return; - const int diag = runtime_upload_diag_enabled(); - if (diag) { s_rt_up_diag[0]++; s_rt_up_diag[1] += (uint64_t)s_up_nrects; } - flush_flat_batch(); /* queued flat GEO before upload mutates VRAM */ - flush_tex_batch(); /* queued textured draws before this upload writes VRAM */ - /* Snapshot + clear first (re-entrancy safe; up_add overflow calls back in). */ - DirtyRect rects[UP_RECTS_MAX]; - int nrects = s_up_nrects; - memcpy(rects, s_up_rects, (size_t)nrects * sizeof(DirtyRect)); - s_up_nrects = 0; - - /* Stage every rect's CPU data first (texture uploads outside the FBO - * bracket), then draw all the quads in one bracket. Only the exact - * uploaded rects are painted — never the union bounding box (stale-CPU - * flicker class bug, see s_up_rects). */ - for (int i = 0; i < nrects; i++) { - int x = rects[i].x0, y = rects[i].y0; - int w = rects[i].x1 - rects[i].x0 + 1; - int h = rects[i].y1 - rects[i].y0 + 1; - if (diag) s_rt_up_diag[2] += (uint64_t)w * (uint64_t)h; - coh_record(GL_COH_FLUSH, x, y, x + w - 1, y + h - 1); - - /* RGBA8 staging for the hr quad draw. */ - uint64_t t0 = diag ? SDL_GetPerformanceCounter() : 0; - for (int row = 0; row < h; row++) { - const uint16_t *src = s_vram + (size_t)(y + row) * VRAM_W + x; - uint32_t *dst = s_conv + (size_t)row * w; - for (int col = 0; col < w; col++) dst[col] = conv_1555_to_rgba8(src[col]); - } - if (diag) s_rt_up_diag[3] += SDL_GetPerformanceCounter() - t0; - t0 = diag ? SDL_GetPerformanceCounter() : 0; - glBindTexture(GL_TEXTURE_2D, s_up_tex); - glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, s_conv); - - /* Raw mirror takes the CPU data directly — current for this rect, so no - * pack is needed for uploaded content. */ - glBindTexture(GL_TEXTURE_2D, s_raw_tex); - glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, VRAM_W); - glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, - PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, - s_vram + (size_t)y * VRAM_W + x); - glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0); - if (diag) s_rt_up_diag[4] += SDL_GetPerformanceCounter() - t0; - } - - /* Quads into the hr FBO; two passes split by bit15 so the stencil mirror - * stays exact. gpu.c applied mask set/check per pixel already — no check - * here, the data is final. up_tex is VRAM-aligned: src texel = frag/S. */ - uint64_t draw_t0 = diag ? SDL_GetPerformanceCounter() : 0; - hr_begin(0); - glDisable(GL_BLEND); - p_glUseProgram(s_blit_prog); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_up_tex); - p_glUniform1i(s_uBlitSrc, 0); - p_glUniform1i(s_uBlitMaskset, 0); - p_glUniform1i(s_uBlitSrcDiv, s_scale); - p_glUniform2i(s_uBlitSrcOff, 0, 0); - p_glBindVertexArray(s_blit_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); - for (int i = 0; i < nrects; i++) { - int x = rects[i].x0, y = rects[i].y0; - int w = rects[i].x1 - rects[i].x0 + 1; - int h = rects[i].y1 - rects[i].y0 + 1; - glScissor(x * s_scale, y * s_scale, w * s_scale, h * s_scale); - float fx0 = (float)x, fy0 = (float)y, fx1 = (float)(x + w), fy1 = (float)(y + h); - float verts[6 * 2] = { - fx0, fy0, fx1, fy0, fx0, fy1, - fx1, fy0, fx0, fy1, fx1, fy1, - }; - p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); - plain_stencil(0); p_glUniform1i(s_uBlitPass, 1); glDrawArrays(GL_TRIANGLES, 0, 6); - plain_stencil(1); p_glUniform1i(s_uBlitPass, 2); glDrawArrays(GL_TRIANGLES, 0, 6); - } - hr_end(); - if (diag) s_rt_up_diag[5] += SDL_GetPerformanceCounter() - draw_t0; -} - -/* Recreate one target's stencil mask from its authoritative alpha channel. - * Sampling an attached render target is undefined, so copy color to the shared - * scratch texture first. Wide targets never exceed the 1024-pixel VRAM width. */ -static void rebuild_target_stencil(GLuint target_fbo, int target_w, int target_h) { - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, target_fbo); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo); - p_glBlitFramebuffer(0, 0, target_w, target_h, 0, 0, target_w, target_h, - GL_COLOR_BUFFER_BIT, GL_NEAREST); - - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, target_fbo); - glViewport(0, 0, target_w, target_h); - glDisable(GL_SCISSOR_TEST); - glDisable(GL_BLEND); - glEnable(GL_STENCIL_TEST); - glStencilMask(0x01); - glClearStencil(0); - glClear(GL_STENCIL_BUFFER_BIT); - glStencilFunc(GL_ALWAYS, 1, 0x01); - glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); - glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); - p_glUseProgram(s_stencil_prog); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_scratch_tex); - p_glUniform1i(s_uStencilSrc, 0); - p_glBindVertexArray(s_empty_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); - glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - glDisable(GL_STENCIL_TEST); - p_glBindVertexArray(0); - p_glUseProgram(0); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); -} - -static void rebuild_mask_stencils(void) { - if (s_stencil_valid || !s_raster_ok) return; - int hw = VRAM_W * s_scale, hh = VRAM_H * s_scale; - rebuild_target_stencil(s_hr_fbo, hw, hh); - for (int i = 0; i < WIDE_MAX_SURF; i++) { - if (s_wide_fbo[i]) - rebuild_target_stencil(s_wide_fbo[i], g_wide_w * s_scale, hh); - } - s_stencil_valid = 1; -} - -/* ---- coherency: hr FBO -> raw mirror (pack) ------------------------------ */ -static void pack_flush(void) { - if (!s_raster_ok || !s_pack_dirty.set) return; - int x = s_pack_dirty.x0, y = s_pack_dirty.y0; - int w = s_pack_dirty.x1 - s_pack_dirty.x0 + 1; - int h = s_pack_dirty.y1 - s_pack_dirty.y0 + 1; - rect_clear(&s_pack_dirty); - coh_record(GL_COH_PACK, x, y, x + w - 1, y + h - 1); - - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo); - glViewport(0, 0, VRAM_W, VRAM_H); - glEnable(GL_SCISSOR_TEST); - glScissor(x, y, w, h); - glDisable(GL_BLEND); - glDisable(GL_STENCIL_TEST); - p_glUseProgram(s_pack_prog); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_hr_tex); - p_glUniform1i(s_uPackHr, 0); - p_glUniform1i(s_uPackScale, s_scale); - p_glBindVertexArray(s_empty_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); - p_glBindVertexArray(0); - p_glUseProgram(0); - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); -} - -/* Make sure the raw mirror is current for a textured draw that samples the - * given texture page / CLUT. */ -static void flush_pack_if_sampling(int tpage_x, int tpage_y, int depth, - int clut_x, int clut_y) { - if (!s_pack_dirty.set) return; - int page_w = depth == 0 ? 64 : depth == 1 ? 128 : 256; /* VRAM columns */ - if (rect_intersects(&s_pack_dirty, tpage_x, tpage_y, - tpage_x + page_w - 1, tpage_y + 255)) { - flush_flat_batch(); - flush_tex_batch(); /* queued draws are part of s_pack_dirty — realise them before packing */ - pack_flush(); return; - } - if (depth <= 1) { - int n = depth == 0 ? 16 : 256; - if (rect_intersects(&s_pack_dirty, clut_x, clut_y, clut_x + n - 1, clut_y)) { - flush_flat_batch(); - flush_tex_batch(); - pack_flush(); - } - } -} - + " return int(texelFetch(u_vram, ivec2(x & 1023, y & 511), 0).r);\n" + "}\n" + "int fetch_texel(int u, int v){\n" + " u &= 255; v &= 255;\n" + " if ((u_twin.x | u_twin.y) != 0) {\n" + " u = (u & ~(u_twin.x * 8)) | ((u_twin.z & u_twin.x) * 8);\n" + " v = (v & ~(u_twin.y * 8)) | ((u_twin.w & u_twin.y) * 8);\n" + " } else {\n" + " u = clamp(u, v_limits.x, v_limits.z);\n" + " v = clamp(v, v_limits.y, v_limits.w);\n" + " }\n" + " if (v_depth == 0) {\n" + " int px = vram_at(v_tpage.x + (u >> 2), v_tpage.y + v);\n" + " return vram_at(v_clut.x + ((px >> ((u & 3) * 4)) & 0xF), v_clut.y);\n" + " } else if (v_depth == 1) {\n" + " int px = vram_at(v_tpage.x + (u >> 1), v_tpage.y + v);\n" + " return vram_at(v_clut.x + ((px >> ((u & 1) * 8)) & 0xFF), v_clut.y);\n" + " }\n" + " return vram_at(v_tpage.x + u, v_tpage.y + v);\n" + "}\n" + "vec3 col5(int raw){\n" + " return vec3(float(raw & 31), float((raw >> 5) & 31), float((raw >> 10) & 31)) / 31.0;\n" + "}\n" + "void main(){\n" + " int stp; vec3 rgb;\n" + " /* v_persp is 0 for every prim unless [video] perspective_texturing is on\n" + " * AND this prim's packet carried full GTE projection provenance, so the\n" + " * default is the PS1's affine (noperspective) mapping. */\n" + " vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;\n" + " if (u_filter == 0) {\n" + " int raw = fetch_texel(int(floor(uv.x)), int(floor(uv.y)));\n" + " if (raw == 0) discard;\n" + " rgb = col5(raw);\n" + " stp = (raw >> 15) & 1;\n" + " } else {\n" + " /* Bilinear, Beetle-PSX formulation: the NEAREST texel is the base\n" + " * (cutout + STP authority), the neighbours lie toward the sub-texel\n" + " * offset and clamp to u_limits, and each texel's weight is gated by\n" + " * its opacity with the colour renormalised — so prim edges and\n" + " * cutout borders keep their colour instead of dissolving into the\n" + " * transparent (black) neighbour and discarding whole edge columns.\n" + " * Recenter from PS1 top-left point sampling to the bilinear footprint. */\n" + " uv += vec2(u_shift);\n" + " int iu = int(floor(uv.x)), iv = int(floor(uv.y));\n" + " float fx = uv.x - float(iu) - 0.5, fy = uv.y - float(iv) - 0.5;\n" + " int sx = fx < 0.0 ? -1 : 1, sy = fy < 0.0 ? -1 : 1;\n" + " fx = abs(fx); fy = abs(fy);\n" + " int c00 = fetch_texel(iu, iv);\n" + " if (c00 == 0) discard;\n" + " int c10 = fetch_texel(iu + sx, iv);\n" + " int c01 = fetch_texel(iu, iv + sy);\n" + " int c11 = fetch_texel(iu + sx, iv + sy);\n" + " float w00 = (c00 == 0 ? 0.0 : 1.0) * (1.0 - fx) * (1.0 - fy);\n" + " float w10 = (c10 == 0 ? 0.0 : 1.0) * fx * (1.0 - fy);\n" + " float w01 = (c01 == 0 ? 0.0 : 1.0) * (1.0 - fx) * fy;\n" + " float w11 = (c11 == 0 ? 0.0 : 1.0) * fx * fy;\n" + " float opac = w00 + w10 + w01 + w11;\n" + " rgb = (col5(c00)*w00 + col5(c10)*w10 + col5(c01)*w01 + col5(c11)*w11) / opac;\n" + " float stpf = (float((c00 >> 15) & 1) * w00 + float((c10 >> 15) & 1) * w10\n" + " + float((c01 >> 15) & 1) * w01 + float((c11 >> 15) & 1) * w11) / opac;\n" + " stp = stpf >= 0.5 ? 1 : 0;\n" + " }\n" + " if (u_semipass == 1 && stp == 1) discard;\n" + " if (u_semipass == 2 && stp == 0) discard;\n" + " if (v_raw == 0) rgb = clamp(rgb * v_col.rgb * 2.0, 0.0, 1.0);\n" + " float dst_factor = 0.0;\n" + " if (u_semimode == 4 && v_semi != 0 && stp != 0) {\n" + " dst_factor = v_semi == 1 ? 0.5 : 1.0;\n" + " if (v_semi == 1) rgb *= 0.5; else if (v_semi == 4) rgb *= 0.25;\n" + " }\n" + " frag = vec4(rgb, (stp == 1 || u_maskset == 1) ? 1.0 : 0.0);\n" + " blend_factor = vec4(0.0, 0.0, 0.0, dst_factor);\n" + "}\n"; + +/* Quad blit: used for CPU->VRAM upload flushes and VRAM->VRAM copies. + * Samples an RGBA8 source (alpha = bit15), splits by STP for the stencil + * write, optionally ORs set-mask into the output alpha. */ +static const char *BLIT_VS = + "#version 330\n" + "layout(location=0) in vec2 a_pos; /* native VRAM px */\n" + "uniform float u_shift;\n" + "void main(){\n" + " gl_Position = vec4((a_pos.x+u_shift)/512.0 - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n"; +static const char *BLIT_FS = + "#version 330\n" + "out vec4 frag;\n" + "uniform sampler2D u_src;\n" + "uniform int u_stp_pass; /* 0=all, 1=bit15=0 only, 2=bit15=1 only */\n" + "uniform int u_maskset;\n" + "uniform int u_src_div; /* fragcoord -> src texel divisor (S for native-res\n" + " sources, 1 for hr-res sources) */\n" + "uniform ivec2 u_src_off; /* added after the divide, in src texel units */\n" + "void main(){\n" + " /* Exact integer source fetch — no normalized-uv edge precision. */\n" + " ivec2 p = ivec2(gl_FragCoord.xy);\n" + " vec4 c = texelFetch(u_src, p / u_src_div + u_src_off, 0);\n" + " bool stp = c.a >= 0.5;\n" + " if (u_stp_pass == 1 && stp) discard;\n" + " if (u_stp_pass == 2 && !stp) discard;\n" + " frag = vec4(c.rgb, (stp || u_maskset != 0) ? 1.0 : 0.0);\n" + "}\n"; + +/* Pack: re-encode the hr FBO into the native R16UI raw mirror. Runs over a + * native-res viewport with scissor = dirty rect; each native pixel takes the + * top-left sample of its S*S block (the sample at the exact native coord). */ +static const char *PACK_VS = + "#version 330\n" + "void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n" + " gl_Position = vec4(p*2.0-1.0, 0.0, 1.0); }\n"; +static const char *PACK_FS = + "#version 330\n" + "uniform sampler2D u_hr;\n" + "uniform int u_scale;\n" + "out uint o_pix;\n" + "void main(){\n" + " ivec2 p = ivec2(gl_FragCoord.xy);\n" + " vec4 c = texelFetch(u_hr, p * u_scale, 0);\n" + " uint r = uint(c.r * 255.0 + 0.5) >> 3;\n" + " uint g = uint(c.g * 255.0 + 0.5) >> 3;\n" + " uint b = uint(c.b * 255.0 + 0.5) >> 3;\n" + " o_pix = r | (g << 5) | (b << 10) | (c.a >= 0.5 ? 0x8000u : 0u);\n" + "}\n"; + +/* Rebuild stencil bit 0 from a copied RGBA target's alpha. Color writes are + * disabled while this shader runs; only alpha>=0.5 fragments replace stencil. */ +static const char *STENCIL_FS = + "#version 330\n" + "uniform sampler2D u_src; out vec4 frag;\n" + "void main(){ vec4 c=texelFetch(u_src,ivec2(gl_FragCoord.xy),0);\n" + " if(c.a<0.5) discard; frag=vec4(0.0); }\n"; + +static GLuint compile_shader(GLenum type, const char *src) { + GLuint s = p_glCreateShader(type); + p_glShaderSource(s, 1, &src, NULL); + p_glCompileShader(s); + GLint ok = 0; p_glGetShaderiv(s, PSXGL_COMPILE_STATUS, &ok); + if (!ok) { char log[1024]; log[0]=0; p_glGetShaderInfoLog(s, sizeof log, NULL, log); + fprintf(stdout, "psxrecomp: GL shader compile failed: %s\n", log); + p_glDeleteShader(s); return 0; } + return s; +} +static GLuint build_program_ex(const char *vs, const char *fs, int dual_source) { + GLuint v = compile_shader(PSXGL_VERTEX_SHADER, vs), f = compile_shader(PSXGL_FRAGMENT_SHADER, fs); + if (!v || !f) return 0; + GLuint p = p_glCreateProgram(); + p_glAttachShader(p, v); p_glAttachShader(p, f); + if (dual_source) { + p_glBindFragDataLocationIndexed(p, 0, 0, "frag"); + p_glBindFragDataLocationIndexed(p, 0, 1, "blend_factor"); + } + p_glLinkProgram(p); + p_glDeleteShader(v); p_glDeleteShader(f); + GLint ok = 0; p_glGetProgramiv(p, PSXGL_LINK_STATUS, &ok); + if (!ok) { char log[1024]; log[0]=0; p_glGetProgramInfoLog(p, sizeof log, NULL, log); + fprintf(stdout, "psxrecomp: GL program link failed: %s\n", log); return 0; } + return p; +} +static GLuint build_program(const char *vs, const char *fs) { + return build_program_ex(vs, fs, 0); +} + +/* ---- pixel conversion (PS1 1555: bit15=mask, B[14:10] G[9:5] R[4:0]) ---- */ +static inline uint32_t conv_1555_to_rgba8(uint16_t p) { + uint32_t r = (p & 0x1F) << 3, g = ((p >> 5) & 0x1F) << 3, b = ((p >> 10) & 0x1F) << 3; + uint32_t a = (p >> 15) & 1 ? 0xFF : 0; + return r | (g << 8) | (b << 16) | (a << 24); /* RGBA8 little-endian */ +} + +/* ---- mask-bit stencil --------------------------------------------------- * + * Stencil bit0 mirrors bit15 of every pixel. + * check off: test ALWAYS, op REPLACE with ref = write value. + * check on: test EQUAL 0 (pass iff dest unmasked). GL couples the REPLACE + * value to the test reference, so write a 1 via INVERT (the + * stored value is known to be 0 when the test passed) and a 0 + * via KEEP. */ +static void mask_stencil(int write_val) { + glEnable(GL_STENCIL_TEST); + glStencilMask(0x01); + if (s_mask_check) { + glStencilFunc(GL_EQUAL, 0, 0x01); + glStencilOp(GL_KEEP, GL_KEEP, write_val ? GL_INVERT : GL_KEEP); + } else { + glStencilFunc(GL_ALWAYS, write_val ? 1 : 0, 0x01); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + } +} +/* Like mask_stencil but never checks (uploads: gpu.c already applied mask). */ +static void plain_stencil(int write_val) { + glEnable(GL_STENCIL_TEST); + glStencilMask(0x01); + glStencilFunc(GL_ALWAYS, write_val ? 1 : 0, 0x01); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); +} + +/* PS1 semi-transparency as fixed-function blending, RGB only — the alpha + * channel (mask bit) is always replaced by the source fragment's alpha. + * 0: B/2 + F/2 1: B + F 2: B - F 3: B + F/4 */ +static void apply_psx_blend(int mode) { + glEnable(GL_BLEND); + p_glBlendEquationSeparate((mode & 3) == 2 ? PSXGL_FUNC_REVERSE_SUBTRACT + : PSXGL_FUNC_ADD, + PSXGL_FUNC_ADD); + switch (mode & 3) { + case 0: + p_glBlendColor(0.5f, 0.5f, 0.5f, 0.5f); + p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ZERO); + break; + case 1: + case 2: + p_glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ZERO); + break; + case 3: + p_glBlendColor(0.25f, 0.25f, 0.25f, 0.25f); + p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ONE, GL_ZERO); + break; + } +} + +/* ---- hr FBO render-state bracket ---------------------------------------- */ +static void hr_begin(int clip_to_draw_area) { + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); + glViewport(0, 0, VRAM_W * s_scale, VRAM_H * s_scale); + glEnable(GL_SCISSOR_TEST); + if (clip_to_draw_area) { + int sw = s_area_x2 - s_area_x1 + 1, sh = s_area_y2 - s_area_y1 + 1; + if (sw < 0) sw = 0; if (sh < 0) sh = 0; + glScissor(s_area_x1 * s_scale, s_area_y1 * s_scale, + sw * s_scale, sh * s_scale); + } +} +static void hr_end(void) { + glDisable(GL_BLEND); + /* apply_psx_blend mode 2 leaves REVERSE_SUBTRACT armed; reset so later + * host draws (OSD) that re-enable blend do not inherit B-F math. */ + if (p_glBlendEquationSeparate) + p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); + glDisable(GL_STENCIL_TEST); + glDisable(GL_SCISSOR_TEST); + p_glBindVertexArray(0); + p_glUseProgram(0); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); +} + +/* ---- coherency: CPU -> GPU upload flush --------------------------------- */ +/* CPU-side VRAM writes (GP0 A0 transfers, DMA, single pixel pokes) land in + * the CPU array immediately and accumulate s_up_rects. Flushing before the + * next GPU op (or readback/present) preserves PS1 command order. */ +static void flush_cpu_upload(void) { + if (!s_raster_ok || s_up_nrects == 0) return; + const int diag = runtime_upload_diag_enabled(); + if (diag) { s_rt_up_diag[0]++; s_rt_up_diag[1] += (uint64_t)s_up_nrects; } + flush_flat_batch(); /* queued flat GEO before upload mutates VRAM */ + flush_tex_batch(); /* queued textured draws before this upload writes VRAM */ + /* Snapshot + clear first (re-entrancy safe; up_add overflow calls back in). */ + DirtyRect rects[UP_RECTS_MAX]; + int nrects = s_up_nrects; + memcpy(rects, s_up_rects, (size_t)nrects * sizeof(DirtyRect)); + s_up_nrects = 0; + + /* Stage every rect's CPU data first (texture uploads outside the FBO + * bracket), then draw all the quads in one bracket. Only the exact + * uploaded rects are painted — never the union bounding box (stale-CPU + * flicker class bug, see s_up_rects). */ + for (int i = 0; i < nrects; i++) { + int x = rects[i].x0, y = rects[i].y0; + int w = rects[i].x1 - rects[i].x0 + 1; + int h = rects[i].y1 - rects[i].y0 + 1; + if (diag) s_rt_up_diag[2] += (uint64_t)w * (uint64_t)h; + coh_record(GL_COH_FLUSH, x, y, x + w - 1, y + h - 1); + + /* RGBA8 staging for the hr quad draw. */ + uint64_t t0 = diag ? SDL_GetPerformanceCounter() : 0; + for (int row = 0; row < h; row++) { + const uint16_t *src = s_vram + (size_t)(y + row) * VRAM_W + x; + uint32_t *dst = s_conv + (size_t)row * w; + for (int col = 0; col < w; col++) dst[col] = conv_1555_to_rgba8(src[col]); + } + if (diag) s_rt_up_diag[3] += SDL_GetPerformanceCounter() - t0; + t0 = diag ? SDL_GetPerformanceCounter() : 0; + glBindTexture(GL_TEXTURE_2D, s_up_tex); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, s_conv); + + /* Raw mirror takes the CPU data directly — current for this rect, so no + * pack is needed for uploaded content. */ + glBindTexture(GL_TEXTURE_2D, s_raw_tex); + glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, VRAM_W); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, + PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, + s_vram + (size_t)y * VRAM_W + x); + glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0); + if (diag) s_rt_up_diag[4] += SDL_GetPerformanceCounter() - t0; + } + + /* Quads into the hr FBO; two passes split by bit15 so the stencil mirror + * stays exact. gpu.c applied mask set/check per pixel already — no check + * here, the data is final. up_tex is VRAM-aligned: src texel = frag/S. */ + uint64_t draw_t0 = diag ? SDL_GetPerformanceCounter() : 0; + hr_begin(0); + glDisable(GL_BLEND); + p_glUseProgram(s_blit_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_up_tex); + p_glUniform1i(s_uBlitSrc, 0); + p_glUniform1i(s_uBlitMaskset, 0); + p_glUniform1i(s_uBlitSrcDiv, s_scale); + p_glUniform2i(s_uBlitSrcOff, 0, 0); + p_glBindVertexArray(s_blit_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); + for (int i = 0; i < nrects; i++) { + int x = rects[i].x0, y = rects[i].y0; + int w = rects[i].x1 - rects[i].x0 + 1; + int h = rects[i].y1 - rects[i].y0 + 1; + glScissor(x * s_scale, y * s_scale, w * s_scale, h * s_scale); + float fx0 = (float)x, fy0 = (float)y, fx1 = (float)(x + w), fy1 = (float)(y + h); + float verts[6 * 2] = { + fx0, fy0, fx1, fy0, fx0, fy1, + fx1, fy0, fx0, fy1, fx1, fy1, + }; + p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); + plain_stencil(0); p_glUniform1i(s_uBlitPass, 1); glDrawArrays(GL_TRIANGLES, 0, 6); + plain_stencil(1); p_glUniform1i(s_uBlitPass, 2); glDrawArrays(GL_TRIANGLES, 0, 6); + } + hr_end(); + if (diag) s_rt_up_diag[5] += SDL_GetPerformanceCounter() - draw_t0; +} + +/* Recreate one target's stencil mask from its authoritative alpha channel. + * Sampling an attached render target is undefined, so copy color to the shared + * scratch texture first. Wide targets never exceed the 1024-pixel VRAM width. */ +static void rebuild_target_stencil(GLuint target_fbo, int target_w, int target_h) { + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, target_fbo); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo); + p_glBlitFramebuffer(0, 0, target_w, target_h, 0, 0, target_w, target_h, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, target_fbo); + glViewport(0, 0, target_w, target_h); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); + glEnable(GL_STENCIL_TEST); + glStencilMask(0x01); + glClearStencil(0); + glClear(GL_STENCIL_BUFFER_BIT); + glStencilFunc(GL_ALWAYS, 1, 0x01); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + p_glUseProgram(s_stencil_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_scratch_tex); + p_glUniform1i(s_uStencilSrc, 0); + p_glBindVertexArray(s_empty_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDisable(GL_STENCIL_TEST); + p_glBindVertexArray(0); + p_glUseProgram(0); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); +} + +static void rebuild_mask_stencils(void) { + if (s_stencil_valid || !s_raster_ok) return; + int hw = VRAM_W * s_scale, hh = VRAM_H * s_scale; + rebuild_target_stencil(s_hr_fbo, hw, hh); + for (int i = 0; i < WIDE_MAX_SURF; i++) { + if (s_wide_fbo[i]) + rebuild_target_stencil(s_wide_fbo[i], g_wide_w * s_scale, hh); + } + s_stencil_valid = 1; +} + +/* ---- coherency: hr FBO -> raw mirror (pack) ------------------------------ */ +static void pack_flush(void) { + if (!s_raster_ok || !s_pack_dirty.set) return; + int x = s_pack_dirty.x0, y = s_pack_dirty.y0; + int w = s_pack_dirty.x1 - s_pack_dirty.x0 + 1; + int h = s_pack_dirty.y1 - s_pack_dirty.y0 + 1; + rect_clear(&s_pack_dirty); + coh_record(GL_COH_PACK, x, y, x + w - 1, y + h - 1); + + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo); + glViewport(0, 0, VRAM_W, VRAM_H); + glEnable(GL_SCISSOR_TEST); + glScissor(x, y, w, h); + glDisable(GL_BLEND); + glDisable(GL_STENCIL_TEST); + p_glUseProgram(s_pack_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_hr_tex); + p_glUniform1i(s_uPackHr, 0); + p_glUniform1i(s_uPackScale, s_scale); + p_glBindVertexArray(s_empty_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); + p_glBindVertexArray(0); + p_glUseProgram(0); + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); +} + +/* Make sure the raw mirror is current for a textured draw that samples the + * given texture page / CLUT. */ +static void flush_pack_if_sampling(int tpage_x, int tpage_y, int depth, + int clut_x, int clut_y) { + if (!s_pack_dirty.set) return; + int page_w = depth == 0 ? 64 : depth == 1 ? 128 : 256; /* VRAM columns */ + if (rect_intersects(&s_pack_dirty, tpage_x, tpage_y, + tpage_x + page_w - 1, tpage_y + 255)) { + flush_flat_batch(); + flush_tex_batch(); /* queued draws are part of s_pack_dirty — realise them before packing */ + pack_flush(); return; + } + if (depth <= 1) { + int n = depth == 0 ? 16 : 256; + if (rect_intersects(&s_pack_dirty, clut_x, clut_y, clut_x + n - 1, clut_y)) { + flush_flat_batch(); + flush_tex_batch(); + pack_flush(); + } + } +} + /* ---- coherency: GPU -> CPU readback -------------------------------------- */ /* Defined with the depth24 policy below; needed here for the readback guard. */ static int s_depth24_skip_up = 0; static DirtyRect s_d24_skip_fb; /* union of skipped MDEC FB rects (VRAM halfwords) */ static void ensure_cpu(void) { - extern int psx_netplay_active(void); - if (!s_raster_ok || !s_gpu_dirty) return; - /* Dual-raster / netplay: CPU VRAM is written on every GP0 (or pure SW). - * Never glReadPixels — that forked peer snaps/resim. */ - if (s_cpu_auth_dual || psx_netplay_active()) { - s_gpu_dirty = 0; - return; - } - flush_flat_batch(); - flush_tex_batch(); /* realise queued textured draws before reading the FBO back */ - flush_cpu_upload(); - pack_flush(); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); - glReadPixels(0, 0, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, s_vram); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - s_gpu_dirty = 0; - coh_record(GL_COH_ENSURE, 0, 0, VRAM_W - 1, VRAM_H - 1); -} - -/* ---- GPU primitives ------------------------------------------------------ */ - -static uint64_t s_scene_prims = 0; /* frame_perf: scene primitives submitted (pre double-draw) */ -static uint64_t s_scene_prims_tex = 0; /* frame_perf: of which textured (vs flat geometry) */ -static void flush_tex_batch(void); /* fwd: drained at the backdrop-phase boundary below */ -static void mark_prim_dirty(const int *xs, const int *ys, int n, int textured) { - s_scene_prims++; - int x0 = xs[0], x1 = xs[0], y0 = ys[0], y1 = ys[0]; - for (int i = 1; i < n; i++) { - if (xs[i] < x0) x0 = xs[i]; if (xs[i] > x1) x1 = xs[i]; - if (ys[i] < y0) y0 = ys[i]; if (ys[i] > y1) y1 = ys[i]; - } - /* A sub-pixel-corrected vertex lies in [int, int+1) of the integer position - * it was rounded from, so the corrected prim can touch one more pixel on - * each max edge than the integer bbox covers. Widen before clipping so the - * pack/readback dirty rect never trails the drawn area. */ - if (s_pc_valid) { x1 += 1; y1 += 1; } - s_bdg_prims++; /* dbg: prims seen this frame (gate is now per-prim, see bd_prim_gate) */ - if (s_ptrace_n < PTRACE_CAP) { - PrimRec *p = &s_ptrace[s_ptrace_n++]; - p->x0 = (short)x0; p->x1 = (short)x1; p->y0 = (short)y0; p->y1 = (short)y1; - p->tex = (unsigned char)textured; - } - if (x0 < s_area_x1) x0 = s_area_x1; - if (y0 < s_area_y1) y0 = s_area_y1; - if (x1 > s_area_x2) x1 = s_area_x2; - if (y1 > s_area_y2) y1 = s_area_y2; - rect_add(&s_pack_dirty, x0, y0, x1, y1); - /* Dual-raster keeps CPU current via SW writes — do not mark GPU-ahead. */ - if (!s_cpu_auth_dual) - s_gpu_dirty = 1; - coh_record(GL_COH_DRAW, x0, y0, x1, y1); -} - -/* ---- native-wide mirror pass plumbing ----------------------------------- * - * Re-issue an already-set-up draw (program/VAO/VBO/blend/stencil bound) into - * the active wide surface. The geometry positions are identical on the host - * side; the x translation (wide_dx) and the wider clip are applied entirely in - * the vertex shader via u_xoff / u_xhalf, so the SAME glDrawArrays produces the - * shifted copy. wide_target_begin binds the wide FBO + viewport + scissor and - * sets the projection uniforms; wide_target_end restores u_xoff=0/u_xhalf=512 - * on the active program so the next canonical pass is bit-identical. The - * caller stays inside hr_begin/hr_end; hr_end unbinds and the next hr_begin - * resets the viewport, so no viewport restore is needed mid-function. - * - * The scissor X is the FULL wide surface (NOT the draw area): the SW reference - * (rt_wide()) deliberately lets the shifted geometry fill the revealed 16:9 - * margins that lie OUTSIDE the game's 4:3 draw-area x-range; scissoring X to - * the (translated) draw area would crop exactly the margin content native-wide - * exists to reveal. The scissor Y stays clamped to the DRAW AREA, exactly like - * rt_wide() (t.cy1/cy2 = g_clip_y1/y2): native-wide only widens X. A full-height - * Y scissor let draws that canonically clip at a vertical double-buffer band - * boundary (MMX6: draw area alternates y=0/y=240, both bands in ONE wide - * surface) bleed into the OTHER band's rows — presented one frame later as - * top/bottom edge flicker (16:9 GL only). */ -static void wide_target_begin(int dx, GLint uXoff, GLint uXhalf) { - if (s_ws_ablate != 3) /* ablate 3: no FBO rebind (draws land in hr — perf probe) */ - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, g_wide_cur); - glViewport(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); - glEnable(GL_SCISSOR_TEST); - { - int sy = s_area_y1, sh = s_area_y2 - s_area_y1 + 1; - if (sy < 0) { sh += sy; sy = 0; } - if (sy + sh > VRAM_H) sh = VRAM_H - sy; - if (sh < 0) sh = 0; - glScissor(0, sy * s_scale, g_wide_w * s_scale, sh * s_scale); - } - p_glUniform1f(uXoff, (float)dx); - p_glUniform1f(uXhalf, (float)g_wide_w / 2.0f); -} -static void wide_target_end(GLint uXoff, GLint uXhalf) { - p_glUniform1f(uXoff, 0.0f); - p_glUniform1f(uXhalf, 512.0f); - if (s_ws_ablate != 3) - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); -} - -extern int psx_ws_prim_is_tagged(void); /* gpu.c: is the current GP0 prim sprite-tagged? */ -extern int psx_ws_prim_in_backdrop(void); /* gpu.c: is its source addr in the flower-field struct? */ -extern int gpu_ws_nw_flat_backdrop_enabled(void); /* gpu.c: per-title flat backdrop opt-in */ - -/* Per-prim gate: stretch this prim iff native-wide + feature on AND the prim's - * source address is inside the flower-field backdrop data structure (precise — - * excludes the 3D rock/foreground, which is untagged AND has narrow prims so the - * earlier tag/narrow heuristic tore it). mode!=0 falls back to the old - * tag+narrow heuristic for A/B. */ -static int bd_prim_gate(const int *xs, int n, int textured) { - if (g_wide_w <= 0 || !g_ws_bd_stretch_on) return 0; - /* Some games draw their authored 4:3 sky/water as flat-colour polygons. - * Stretch those only in the native-wide mirror: the canonical framebuffer - * remains byte-for-byte 4:3, while the flat backdrop reaches the reveal - * margins. Opt-in because flat foreground geometry is title-dependent. */ - if (!textured && gpu_ws_nw_flat_backdrop_enabled()) return 1; - if (g_ws_bd_phase_mode != 0) return psx_ws_prim_in_backdrop(); /* default: precise address gate */ - /* mode 0: legacy tag+narrow heuristic (kept for comparison) */ - int native_w = g_wide_w - 2 * g_wide_off; - if (native_w <= 0) return 0; - if (psx_ws_prim_is_tagged()) return 0; - int base = g_wide_cur_base, lo = xs[0], hi = xs[0]; - for (int i = 1; i < n; i++) { if (xs[i] < lo) lo = xs[i]; if (xs[i] > hi) hi = xs[i]; } - if (lo < base - g_ws_bd_phase_thresh) return 0; /* into left margin -> GTE-wide */ - if (hi > base + native_w + g_ws_bd_phase_thresh) return 0; /* into right margin -> GTE-wide */ - return 1; -} - -/* ---- native-wide FAST path (skip redundant center mirror) ----------------- * - * The wide surface's CENTRE columns [g_wide_off, g_wide_off+native_w) are, by - * construction, identical to the canonical 4:3 framebuffer. So instead of - * re-rasterizing every primitive into the wide surface (the "mirror" pass — the - * dominant native-wide GPU cost, ~2x scene fill), we copy the canonical centre - * into the wide surface once at present (blit_wide_center_from_canonical), and - * the per-prim mirror only needs to produce the reveal MARGINS. Any prim/batch - * whose x-range is fully inside the 4:3 frame contributes nothing to the margins, - * so its mirror is skipped entirely. Correctness does not depend on the skip - * being precise: the centre is authoritatively overwritten by the blit, so the - * ONLY requirement is that a margin-reaching prim is NOT skipped — hence the - * conservative strict-inside test. 4:3 never runs any of this (g_wide_cur == 0). - * Toggle via gl_wide_fast for A/B; default ON. */ -static int s_wide_fast = 1; -void gl_renderer_set_wide_fast(int on) { s_wide_fast = on ? 1 : 0; } -int gl_renderer_get_wide_fast(void) { return s_wide_fast; } -static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h); /* def below */ -/* True if [lo,hi] (canonical draw-x) lies strictly inside the 4:3 frame, so the - * prim adds nothing to either reveal margin and its mirror can be skipped. */ -static int mirror_x_center_only(int lo, int hi) { - if (!s_wide_fast) return 0; - int base = g_wide_cur_base, native_w = g_wide_w - 2 * g_wide_off; - if (native_w <= 0) return 0; - return (lo >= base) && (hi < base + native_w); -} -static int mirror_geo_center_only(const int *xs, int n) { - if (!s_wide_fast) return 0; - int lo = xs[0], hi = xs[0]; - for (int i = 1; i < n; i++) { if (xs[i] < lo) lo = xs[i]; if (xs[i] > hi) hi = xs[i]; } - return mirror_x_center_only(lo, hi); -} -/* mirror_batch_center_only (textured-batch variant) is defined after s_tb below. */ - -/* Set / clear the 2D-backdrop x-stretch for a wide-mirror draw, per the current - * gate (s_bd_gate, set by the caller from bd_prim_gate / the batch gate). */ -static void wide_set_bd_scale(GLint uScale, GLint uCenter) { - extern int g_ws_tex_edge_pct; - float scale = 1.0f, center = 0.0f; - if (s_bd_gate && g_ws_bd_stretch_on && g_wide_w > 0) { - int native_w = g_wide_w - 2 * g_wide_off; - if (native_w > 0) { - scale = g_ws_bd_stretch_pct > 0 ? (float)g_ws_bd_stretch_pct / 100.0f - : (float)g_wide_w / (float)native_w; - if (s_bd_gate == 2) { - scale = g_ws_tex_edge_pct > 0 - ? (float)g_ws_tex_edge_pct / 100.0f : scale; - scale = -scale; /* shader: expand only beyond canonical edges */ - } - center = (float)g_wide_cur_base + (float)native_w / 2.0f; - } - } - if (scale != 1.0f) s_bdg_applied++; - p_glUniform1f(uScale, scale); - p_glUniform1f(uCenter, center); -} -static void wide_clear_bd_scale(GLint uScale, GLint uCenter) { - p_glUniform1f(uScale, 1.0f); - p_glUniform1f(uCenter, 0.0f); -} - -/* ---- textured-prim batching -------------------------------------------- * - * Consecutive textured prims sharing blend/mask/texwindow/filter coalesce into - * one draw. Per-prim texture state (texpage/clut/depth/raw/uv-limits) rides in - * the vertex (TEXV flat attributes), so only `semi` (blend) and the global - * mask/twin/filter are batch keys. flush_tex_batch() draws the queued verts; it - * is called before any op that reads VRAM, writes it outside the batch, or - * changes batch state (see callers: flush_cpu_upload, flush_pack_if_sampling, - * every non-textured glb_ wrapper, and the present path). Drawing reads only the - * already-coherent texture (per-prim coherency was ensured at append time), so - * flush_tex_batch never re-enters those helpers. */ -#define TEXBATCH_MAXV 8190 /* multiple of 3; ~2730 tris */ -static float s_tb[TEXBATCH_MAXV * TEXV]; -static int s_tb_n = 0; /* verts queued */ -static int s_tb_semi = -2; -static int s_tb_mask = 0, s_tb_filter = 0; -static int s_tb_twin[4] = {0, 0, 0, 0}; -static uint64_t s_batch_total = 0, s_batch_reason[7]; - -void gl_renderer_batch_diag(uint64_t out[8]) { - out[0] = s_batch_total; - for (int i = 0; i < 7; i++) out[i + 1] = s_batch_reason[i]; -} - -/* Draw the queued textured batch with correct PSX mask-bit handling AND correct - * painter's order. The mask bit lives in both the colour-attachment alpha - * (frag.a) and the stencil; STP=1 texels must always set it. - * - * OPAQUE batch (semi < 0): the STP bit does NOT gate COLOUR (every texel is - * opaque), so colour is drawn in ONE ordered pass over all texels — frag.a still - * carries the per-texel mask bit, so the alpha mask is correct — followed by a - * COLOUR-MASKED pass that only fixes the STENCIL for STP=1 texels. The old code - * split colour into STP=0 (pass 1) then STP=1 (pass 2) across the WHOLE batch, - * which let a behind prim's STP=1 texels overwrite a front prim's STP=0 colour - * (the Tomba character drew behind an AP-block's letters / a save post on GL - * only). Same draw count, order preserved, mask preserved. - * - * SEMI batch (semi >= 0): genuine two-pass — STP=0 texels opaque, STP=1 texels - * blended per the PSX mode. Cross-prim order is kept by isolating semi prims to - * one per batch (see gpu_textured_triangle), so this batch holds a single prim - * whose two passes do not self-overlap. */ -static void tex_batch_draw_passes(int nverts, int semi) { - p_glUniform1i(s_uSemimode, semi < 0 ? 0 : semi); - if (semi < 0) { - glDisable(GL_BLEND); - mask_stencil(s_tb_mask); - p_glUniform1i(s_uSemipass, 0); /* all texels, one ordered colour pass */ - glDrawArrays(GL_TRIANGLES, 0, nverts); - if (s_mask_check) { - glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); /* stencil-only fixup */ - mask_stencil(1); - p_glUniform1i(s_uSemipass, 2); /* STP=1 texels set the mask bit */ - glDrawArrays(GL_TRIANGLES, 0, nverts); - glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - } else if (!s_tb_mask) { - /* Alpha is already exact; defer its duplicate stencil encoding - * until a later GP0(E6h) actually enables destination masking. */ - s_stencil_valid = 0; - } - } else if (!s_mask_check && semi == 4) { - /* Modes 0/1/3 can select opaque-vs-semi behavior per fragment with - * dual-source factors, so the whole painter-ordered batch is one draw. - * Mode 2 needs a different blend equation and stays on the conservative - * isolated path below. */ - glEnable(GL_BLEND); - p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); - p_glBlendFuncSeparate(GL_ONE, PSXGL_SRC1_ALPHA, GL_ONE, GL_ZERO); - if (s_tb_mask) mask_stencil(1); else glDisable(GL_STENCIL_TEST); - p_glUniform1i(s_uSemipass, 0); - glDrawArrays(GL_TRIANGLES, 0, nverts); - if (!s_tb_mask) s_stencil_valid = 0; - } else { - glDisable(GL_BLEND); /* Pass 1: STP=0 texels (opaque) */ - mask_stencil(s_tb_mask); - p_glUniform1i(s_uSemipass, 1); - glDrawArrays(GL_TRIANGLES, 0, nverts); - apply_psx_blend(semi); /* Pass 2: STP=1 texels (blended) */ - mask_stencil(1); - p_glUniform1i(s_uSemipass, 2); - glDrawArrays(GL_TRIANGLES, 0, nverts); - } -} - -/* ---- frame_perf CPU attribution (native-wide wedge hunt) ----------------- * - * Per-frame CPU wall time spent inside the GL submission paths (driver CPU - * cost surfaces INSIDE our gl* calls) + counters for the wide plumbing, so a - * CPU-bound wide frame (emu_cpu >> scene_gpu) can be attributed without a - * sampling profiler. Reset at present enter; reported by frame_perf. */ -static double cw_ms(void) { - static double freq = 0.0; - if (freq == 0.0) { - uint64_t f = SDL_GetPerformanceFrequency(); - freq = f ? (double)f : 1.0; - } - return (double)SDL_GetPerformanceCounter() * 1000.0 / freq; -} -static double s_cw_flush_ms = 0.0; /* CPU wall inside flush_tex_batch */ -static double s_cw_wide_ms = 0.0; /* CPU wall inside glb_wide_* entry points */ -static int s_cw_batches = 0, s_cw_wide_sets = 0, s_cw_wide_cfgs = 0, - s_cw_wide_clears = 0, s_cw_fbo_creates = 0, s_cw_flush_depth = 0; - -/* Textured-batch variant of mirror_x_center_only: scan the queued verts' x - * (attr 0, stride TEXV). Defined here so s_tb / TEXV are in scope. */ -static int mirror_batch_center_only(int nverts) { - if (!s_wide_fast || nverts <= 0) return 0; - float flo = s_tb[0], fhi = s_tb[0]; - for (int i = 1; i < nverts; i++) { - float x = s_tb[i * TEXV]; - if (x < flo) flo = x; if (x > fhi) fhi = x; - } - /* floor/ceil, not a truncating cast: with geometry_correction these are - * fractional, and (int) rounds toward zero — which WIDENS a negative x - * toward the centre and could wrongly call a margin-touching batch - * centre-only, dropping its wide-mirror draw. Whole values are unchanged. */ - return mirror_x_center_only((int)floorf(flo), (int)ceilf(fhi)); -} - -static void flush_tex_batch(void) { - if (s_tb_n == 0) return; - int nverts = s_tb_n, semi = s_tb_semi; - s_tb_n = 0; /* clear first: re-entrancy safe */ - double cw_t0 = cw_ms(); - s_cw_batches++; s_batch_total++; s_cw_flush_depth++; - - hr_begin(1); - p_glUseProgram(s_tex_prog); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_raw_tex); - p_glUniform1i(s_uVram, 0); - p_glUniform4i(s_uTwin, s_tb_twin[0], s_tb_twin[1], s_tb_twin[2], s_tb_twin[3]); - p_glUniform1i(s_uMaskset, s_tb_mask); - p_glUniform1i(s_uFilter, s_tb_filter); - p_glBindVertexArray(s_tex_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); - p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(nverts * TEXV * sizeof(float)), s_tb, PSXGL_STREAM_DRAW); - - tex_batch_draw_passes(nverts, semi); - - /* Native-wide mirror — skipped for a batch fully inside the 4:3 frame (its - * centre content comes from the present-time canonical blit; nothing to add - * to the margins). A backdrop-stretched batch (s_tb_gate) widens past the - * frame, so it is never treated as centre-only. */ - if (g_wide_cur && s_ws_ablate != 1 && - !(s_tb_gate == 0 && mirror_batch_center_only(nverts))) { /* native-wide mirror */ - int dx = wide_dx(); - s_bd_gate = s_tb_gate; /* this batch is uniform-gate (flushed on change) */ - gl_perf_mirror_begin(); - wide_target_begin(dx, s_tex_uXoff, s_tex_uXhalf); - wide_set_bd_scale(s_tex_uXscale, s_tex_uXcenter); - if (s_ws_ablate != 2) tex_batch_draw_passes(nverts, semi); - wide_clear_bd_scale(s_tex_uXscale, s_tex_uXcenter); - wide_target_end(s_tex_uXoff, s_tex_uXhalf); - gl_perf_mirror_end(); - } - hr_end(); - if (--s_cw_flush_depth == 0) s_cw_flush_ms += cw_ms() - cw_t0; -} - -/* Flat / gouraud GEO batch — MotK title/char-select starfields issue ~30k/s - * GP0(68h) 1x1 dots; each was two immediate gpu_triangle draws (BufferData + - * DrawArrays each). Coalesce opaque/semi-uniform tris into one draw. */ -#define FLATBATCH_MAXV 8190 /* multiple of 3 */ -static float s_fb[FLATBATCH_MAXV * 6]; -static int s_fb_n = 0; -static int s_fb_semi = -2; -static int s_fb_mask = -1; - -static int mirror_flat_batch_center_only(int nverts) { - if (!s_wide_fast || nverts <= 0) return 0; - float flo = s_fb[0], fhi = s_fb[0]; - for (int i = 1; i < nverts; i++) { - float x = s_fb[i * 6]; - if (x < flo) flo = x; if (x > fhi) fhi = x; - } - /* floor/ceil rather than a truncating cast — see mirror_batch_center_only. */ - return mirror_x_center_only((int)floorf(flo), (int)ceilf(fhi)); -} - -static void flush_flat_batch(void) { - if (s_fb_n == 0) return; - int nverts = s_fb_n, semi = s_fb_semi, mask = s_fb_mask; - s_fb_n = 0; - - hr_begin(1); - if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); - mask_stencil(mask); - p_glUseProgram(s_geo_prog); - p_glBindVertexArray(s_geo_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); - p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(nverts * 6 * sizeof(float)), - s_fb, PSXGL_STREAM_DRAW); - glDrawArrays(GL_TRIANGLES, 0, nverts); - - if (g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 && - !(!g_ws_bd_stretch_on && mirror_flat_batch_center_only(nverts))) { - int dx = wide_dx(); - /* Batch may span many prims; use stretch gate off (flat dots/UI). */ - s_bd_gate = 0; - gl_perf_mirror_begin(); - wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf); - wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter); - if (s_ws_ablate != 2) glDrawArrays(GL_TRIANGLES, 0, nverts); - wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter); - wide_target_end(s_geo_uXoff, s_geo_uXhalf); - gl_perf_mirror_end(); - } - hr_end(); -} - -/* Flat / gouraud triangles and lines share the GEO program. mode: GL_TRIANGLES - * or GL_LINES; verts are (x, y, r, g, b, a) tuples with colors as 1555. */ -static void gpu_geometry(GLenum mode, const int *xs, const int *ys, - const uint16_t *cs, int n, int semi) { - flush_tex_batch(); /* flat prim: drain textured draws first (order + program) */ - flush_cpu_upload(); /* also drains flat batch if an upload was pending */ - mark_prim_dirty(xs, ys, n, 0 /* flat */); - /* Sub-pixel positions only describe a 3-vertex projected triangle. */ - const int precise = s_pc_valid && mode == GL_TRIANGLES && n == 3; - - /* Lines stay immediate (rare); tris batch for MotK 0x68 starfields. */ - if (mode != GL_TRIANGLES || n < 3) { - flush_flat_batch(); - float verts[3 * 6]; - float mask_a = s_mask_set ? 1.0f : 0.0f; - for (int i = 0; i < n; i++) { - verts[i*6+0] = precise ? s_pc_x[i] : (float)xs[i]; - verts[i*6+1] = precise ? s_pc_y[i] : (float)ys[i]; - verts[i*6+2] = ((cs[i] & 0x1F) << 3) / 255.0f; - verts[i*6+3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f; - verts[i*6+4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f; - verts[i*6+5] = mask_a; - } - hr_begin(1); - if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); - mask_stencil(s_mask_set); - if (mode == GL_LINES) glLineWidth((float)s_scale); - p_glUseProgram(s_geo_prog); - p_glBindVertexArray(s_geo_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); - p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(n * 6 * sizeof(float)), - verts, PSXGL_STREAM_DRAW); - glDrawArrays(mode, 0, n); - if (g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 && - !(!g_ws_bd_stretch_on && mirror_geo_center_only(xs, n))) { - int dx = wide_dx(); - s_bd_gate = bd_prim_gate(xs, n, 0); - gl_perf_mirror_begin(); - wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf); - wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter); - if (s_ws_ablate != 2) glDrawArrays(mode, 0, n); - wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter); - wide_target_end(s_geo_uXoff, s_geo_uXhalf); - gl_perf_mirror_end(); - } - hr_end(); - return; - } - - if (s_fb_n > 0 && (s_fb_semi != semi || s_fb_mask != (int)s_mask_set)) - flush_flat_batch(); - if (s_fb_n + n > FLATBATCH_MAXV) - flush_flat_batch(); - s_fb_semi = semi; - s_fb_mask = (int)s_mask_set; - - float mask_a = s_mask_set ? 1.0f : 0.0f; - for (int i = 0; i < n; i++) { - float *v = &s_fb[s_fb_n * 6]; - v[0] = precise ? s_pc_x[i] : (float)xs[i]; - v[1] = precise ? s_pc_y[i] : (float)ys[i]; - v[2] = ((cs[i] & 0x1F) << 3) / 255.0f; - v[3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f; - v[4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f; - v[5] = mask_a; - s_fb_n++; - } -} - -static void gpu_triangle(int x0,int y0,uint16_t c0, int x1,int y1,uint16_t c1, - int x2,int y2,uint16_t c2, int semi) { - int xs[3] = {x0, x1, x2}, ys[3] = {y0, y1, y2}; - uint16_t cs[3] = {c0, c1, c2}; - gpu_geometry(GL_TRIANGLES, xs, ys, cs, 3, semi); -} - -static void gpu_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int semi) { - int xs[2] = {x0, x1}, ys[2] = {y0, y1}; - uint16_t cs[2] = {c0, c1}; - gpu_geometry(GL_LINES, xs, ys, cs, 2, semi); -} - -/* Shared PS1 uv-sampling model (limits + mirrored-2D compensation) — one - * implementation for GL/VK/SW, see gpu_uv.h. */ -#include "gpu_uv.h" - -/* Textured triangle. Always two passes split by the per-texel STP bit so the - * stencil (mask) write value is constant within each pass; the semi pass is - * also where PS1 blending applies. lim = uv sampling bounds (see - * tri_uv_limits); NULL computes them from the vertices. */ -static void gpu_textured_triangle(const int *xs, const int *ys, - const int *us, const int *vs, - const float *col, uint16_t texpage, - uint16_t clut_x, uint16_t clut_y, int rawtex, - int semi, const int *lim) { - int lim_buf[4]; - int uv_buf[6]; - if (!lim) { - /* Poly path: exact sampled bounds from the ORIGINAL uvs, then the - * center-sampling mirror compensation (rect prims arrive with their - * own precomputed lim and pre-bumped uvs). */ - int *mu = uv_buf, *mv = uv_buf + 3; - for (int i = 0; i < 3; i++) { mu[i] = us[i]; mv[i] = vs[i]; } - psx_uv_tri_limits(xs, ys, mu, mv, lim_buf); - psx_uv_tri_mirror_offset(xs, ys, mu, mv); - us = mu; vs = mv; - lim = lim_buf; - } - s_scene_prims_tex++; - int base_x = (texpage & 0xF) * 64; - int base_y = ((texpage >> 4) & 1) * 256; - int depth = (texpage >> 7) & 3; if (depth > 2) depth = 2; - - flush_cpu_upload(); /* if a CPU->VRAM upload is pending it flushes the batch first */ - flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y); /* flushes batch iff it must pack */ - mark_prim_dirty(xs, ys, 3, 1 /* textured */); - - /* Append to the textured batch. Flush first if this prim's blend/mask/twin/ - * filter differ from the open batch, or the buffer is full. Per-prim texture - * state goes in the vertex; only these keys force a new draw. */ - { - flush_flat_batch(); /* painter order: flat GEO before textured */ - int twx = s_tw_mask_x, twy = s_tw_mask_y, tox = s_tw_off_x, toy = s_tw_off_y; - int gate = bd_prim_gate(xs, 3, 1); /* backdrop-stretch gate is also a batch key */ - /* Batch key: keep opaque as -1. Dual-source (4) is only for semi modes - * 0/1/3 when mask-check is off — never coalesce opaque into that key. - * Mixing opaque+semi under u_semimode==4 made additive particle/glow - * batches (CTR Naughty Dog intro binary funnel) paint over later - * opaque crate flaps whenever submission order and STP bits disagreed - * with the dual-source path's assumptions. */ - int batch_semi; - if (semi < 0) - batch_semi = -1; - else if (!s_mask_check && semi != 2) - batch_semi = 4; - else - batch_semi = semi; - /* STP draw-ORDER correctness. flush_tex_batch's conservative two-pass - * path draws pass 1 = every prim's STP=0 texels then pass 2 = every - * prim's STP=1 texels — a behind prim's semi texels then overwrite a - * front prim's opaque texels (Tomba AP-block / CTR intro flaps). The - * dual-source single-pass path avoids that WITHIN one prim, but - * batching many overlapping semi quads (digit particles + glow) still - * mis-orders against neighbouring opaque geometry. Isolate EVERY - * semi-transparent textured prim: drain the open batch, draw this - * prim alone (composited fully before the next), let opaque prims - * keep batching. Cost is one draw per semi prim. */ - int isolate = (semi >= 0); - int reason = -1; - if (s_tb_n > 0) { - if (isolate) reason = 0; - else if (batch_semi != s_tb_semi) reason = 1; - else if (s_mask_set != s_tb_mask) reason = 2; - else if (s_tex_filter != s_tb_filter) reason = 3; - else if (gate != s_tb_gate) reason = 4; - else if (twx != s_tb_twin[0] || twy != s_tb_twin[1] || - tox != s_tb_twin[2] || toy != s_tb_twin[3]) reason = 5; - } - if (reason >= 0) { - s_batch_reason[reason]++; - flush_tex_batch(); - } - if (s_tb_n + 3 > TEXBATCH_MAXV) { s_batch_reason[6]++; flush_tex_batch(); } - if (s_tb_n == 0) { /* opening a batch: capture its keyed state */ - s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = s_tex_filter; s_tb_gate = gate; - s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy; - } - float *vp = &s_tb[s_tb_n * TEXV]; - for (int i = 0; i < 3; i++, vp += TEXV) { - vp[0] = s_pc_valid ? s_pc_x[i] : (float)xs[i]; - vp[1] = s_pc_valid ? s_pc_y[i] : (float)ys[i]; - vp[2] = (float)us[i]; vp[3] = (float)vs[i]; - vp[4] = col[i*3+0]; vp[5] = col[i*3+1]; vp[6] = col[i*3+2]; vp[7] = 1.0f; - vp[8] = (float)base_x; vp[9] = (float)base_y; /* a_tpage */ - vp[10] = (float)clut_x; vp[11] = (float)clut_y; /* a_clut */ - vp[12] = (float)depth; vp[13] = (float)rawtex; /* a_depth, a_raw */ - vp[14] = (float)lim[0]; vp[15] = (float)lim[1]; /* a_limits */ - vp[16] = (float)lim[2]; vp[17] = (float)lim[3]; - vp[18] = semi >= 0 ? (float)(semi + 1) : 0.0f; /* a_semi code */ - vp[19] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */ - } - s_tb_n += 3; - if (isolate) flush_tex_batch(); /* draw this semi prim alone, in submission order */ - } -} - -/* Draw a flat-colored rect (GEO program) DIRECTLY into the active wide surface - * at wide-space coords [wx, wx+ww) × [y, y+h). Used only by the full-screen- - * overlay path; positions are already in wide space so u_xoff stays 0. Mirrors - * raster_flat_rect(&wt, ...) in sw_draw_flat_rect. Caller stays inside - * hr_begin/hr_end. */ -static void wide_flat_rect_direct(int wx, int y, int ww, int h, uint16_t c, int semi) { - if (ww <= 0 || h <= 0) return; - float r = ((c & 0x1F) << 3) / 255.0f; - float g = (((c >> 5) & 0x1F) << 3) / 255.0f; - float b = (((c >> 10) & 0x1F) << 3) / 255.0f; - float a = s_mask_set ? 1.0f : 0.0f; - float fx0 = (float)wx, fy0 = (float)y, fx1 = (float)(wx + ww), fy1 = (float)(y + h); - float verts[6 * 6] = { - fx0,fy0,r,g,b,a, fx1,fy0,r,g,b,a, fx0,fy1,r,g,b,a, - fx1,fy0,r,g,b,a, fx0,fy1,r,g,b,a, fx1,fy1,r,g,b,a, - }; - /* Wide target: positions already wide-space so u_xoff = 0; full-width - * scissor; u_xhalf = g_wide_w/2. */ - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, g_wide_cur); - glViewport(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); - glEnable(GL_SCISSOR_TEST); - glScissor(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); /* full surface (rt_wide) */ - if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); - mask_stencil(s_mask_set); - p_glUseProgram(s_geo_prog); - p_glUniform1f(s_geo_uXoff, 0.0f); - p_glUniform1f(s_geo_uXhalf, (float)g_wide_w / 2.0f); - p_glBindVertexArray(s_geo_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); - p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); - glDrawArrays(GL_TRIANGLES, 0, 6); - p_glUniform1f(s_geo_uXoff, 0.0f); - p_glUniform1f(s_geo_uXhalf, 512.0f); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); -} - -static void gpu_flat_rect(int x,int y,int w,int h,uint16_t c,int semi) { - if (w <= 0 || h <= 0) return; - /* Full-screen 2D overlay (pause gray-filter / load fade): a flat rect - * spanning the whole 4:3 framebuffer must cover the whole wide surface too, - * else the revealed 16:9 margins are left undimmed/unfaded. Same detection - * as sw_draw_flat_rect: native_w = the 4:3 framebuffer width (g_wide_w less - * the per-side reveal on both sides); the rect's native screen-X span - * (x - base) must cover [0, native_w). When it does, the two canonical - * triangles are drawn WITHOUT the per-triangle wide mirror (suppressed) and - * a single full-width rect is drawn into the wide surface instead; every - * other rect mirrors 1:1 via the generic gpu_geometry path. Only runs in - * native-wide (g_wide_cur != 0), so 4:3 is unaffected. */ - int overlay = 0; - if (g_wide_cur) { - int native_w = g_wide_w - 2 * g_wide_off; - int lx = x - g_wide_cur_base, rx = x + w - g_wide_cur_base; - overlay = (native_w > 0 && lx <= 0 && rx >= native_w); - } - if (overlay) s_wide_suppress = 1; - gpu_triangle(x, y, c, x+w, y, c, x, y+h, c, semi); - gpu_triangle(x+w, y, c, x, y+h, c, x+w, y+h, c, semi); - if (overlay) { - s_wide_suppress = 0; - /* The two canonical triangles drew into the hr FBO already (each - * gpu_triangle ran its own hr_begin/hr_end). Re-open the bracket just - * for the full-width wide pass so blend/scissor/program state is - * clean. */ - if (s_ws_ablate != 1) { - hr_begin(0); - gl_perf_mirror_begin(); - wide_flat_rect_direct(0, y, g_wide_w, h, c, semi); - gl_perf_mirror_end(); - hr_end(); - } - } -} - -static void gpu_textured_rect(int x,int y,int w,int h, - int u0,int v0,int u1,int v1, - uint16_t clut_x,uint16_t clut_y,uint16_t tp,int semi) { - if (w <= 0 || h <= 0) return; - float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f; - float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb}; - /* gpu.c routes axis-aligned MIRRORED quads (X/Y-flipped 2D sprites, - * e.g. right-facing MMX entities) through THIS path as scaled rects - * with u0>u1 / v0>v1 — they never reach the poly path. Exact bounds - * from the original corners, then the mirror bump (see gpu_uv.h). */ - int lim[4]; - psx_uv_rect_limits(u0, v0, u1, v1, lim); - psx_uv_rect_mirror_offset(&u0, &v0, &u1, &v1); - int xs1[3]={x, x+w, x}, ys1[3]={y, y, y+h}; - int us1[3]={u0,u1,u0}, vs1[3]={v0,v0,v1}; - gpu_textured_triangle(xs1,ys1,us1,vs1,col,tp,clut_x,clut_y,s_mod_raw,semi,lim); - int xs2[3]={x+w, x, x+w}, ys2[3]={y, y+h, y+h}; - int us2[3]={u1,u0,u1}, vs2[3]={v0,v1,v1}; - gpu_textured_triangle(xs2,ys2,us2,vs2,col,tp,clut_x,clut_y,s_mod_raw,semi,lim); -} - -/* GP0(02h) fill: writes color with bit15=0, ignoring draw area, mask and - * offset; coordinates wrap. A scissored clear (color + stencil) per wrapped - * segment is exactly this. */ -static void fill_segment(int x, int y, int w, int h, float r, float g, float b) { - if (w <= 0 || h <= 0) return; - glScissor(x * s_scale, y * s_scale, w * s_scale, h * s_scale); - glClearColor(r, g, b, 0.0f); - glClearStencil(0); - glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1); -} - -static void gpu_fill(int x,int y,int w,int h,uint16_t c) { - if (w <= 0 || h <= 0) return; - flush_flat_batch(); - flush_tex_batch(); - flush_cpu_upload(); - float r=(c&0x1F)/31.0f, g=((c>>5)&0x1F)/31.0f, b=((c>>10)&0x1F)/31.0f; - x &= VRAM_W - 1; y &= VRAM_H - 1; - if (w > VRAM_W) w = VRAM_W; - if (h > VRAM_H) h = VRAM_H; - int w1 = w, w2 = 0, h1 = h, h2 = 0; - if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; } - if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; } - - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); - glViewport(0, 0, VRAM_W * s_scale, VRAM_H * s_scale); - glEnable(GL_SCISSOR_TEST); - fill_segment(x, y, w1, h1, r, g, b); - if (w2) fill_segment(0, y, w2, h1, r, g, b); - if (h2) fill_segment(x, 0, w1, h2, r, g, b); - if (w2 && h2) fill_segment(0, 0, w2, h2, r, g, b); - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - if (!s_cpu_auth_dual) - s_gpu_dirty = 1; - coh_record(GL_COH_FILL, x, y, x + w - 1, y + h - 1); -} - -/* VRAM->VRAM copy: blit the source region to the scratch texture (resolves - * overlap), then draw it back at the destination through the BLIT program so - * mask set/check and the stencil mirror apply, exactly like sw_copy_rect. */ -static void gpu_copy_rect(int sx,int sy,int dx,int dy,int w,int h) { - if (w <= 0 || h <= 0) return; - flush_flat_batch(); - flush_tex_batch(); - flush_cpu_upload(); - /* Clamp to bounds (the software path wraps; wrapping copies are unused - * in practice — see the file header). */ - if (sx < 0) sx = 0; if (sy < 0) sy = 0; - if (dx < 0) dx = 0; if (dy < 0) dy = 0; - if (sx + w > VRAM_W) w = VRAM_W - sx; - if (dx + w > VRAM_W) w = VRAM_W - dx; - if (sy + h > VRAM_H) h = VRAM_H - sy; - if (dy + h > VRAM_H) h = VRAM_H - dy; - if (w <= 0 || h <= 0) return; - - int S = s_scale; - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo); - glDisable(GL_SCISSOR_TEST); - p_glBlitFramebuffer(sx*S, sy*S, (sx+w)*S, (sy+h)*S, - sx*S, sy*S, (sx+w)*S, (sy+h)*S, - GL_COLOR_BUFFER_BIT, GL_NEAREST); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); - - hr_begin(0); /* copies ignore the draw area */ - glScissor(dx*S, dy*S, w*S, h*S); - glDisable(GL_BLEND); - p_glUseProgram(s_blit_prog); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_scratch_tex); - p_glUniform1i(s_uBlitSrc, 0); - p_glUniform1i(s_uBlitMaskset, s_mask_set); - /* Scratch holds the source at its own hr coords: texel = frag + (src-dst)*S. */ - p_glUniform1i(s_uBlitSrcDiv, 1); - p_glUniform2i(s_uBlitSrcOff, (sx - dx) * S, (sy - dy) * S); - float fx0 = (float)dx, fy0 = (float)dy, fx1 = (float)(dx + w), fy1 = (float)(dy + h); - float verts[6 * 2] = { - fx0, fy0, fx1, fy0, fx0, fy1, - fx1, fy0, fx0, fy1, fx1, fy1, - }; - p_glBindVertexArray(s_blit_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); - p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); - /* Two passes split by source bit15 so stencil tracks the copied mask. */ - mask_stencil(s_mask_set); p_glUniform1i(s_uBlitPass, 1); glDrawArrays(GL_TRIANGLES, 0, 6); - mask_stencil(1); p_glUniform1i(s_uBlitPass, 2); glDrawArrays(GL_TRIANGLES, 0, 6); - hr_end(); - - rect_add(&s_pack_dirty, dx, dy, dx + w - 1, dy + h - 1); - if (!s_cpu_auth_dual) - s_gpu_dirty = 1; - coh_record(GL_COH_COPY_SRC, sx, sy, sx + w - 1, sy + h - 1); - coh_record(GL_COH_COPY, dx, dy, dx + w - 1, dy + h - 1); -} - -/* ---- backend vtable wrappers ------------------------------------------- */ -static void glb_init(uint16_t *vram) { s_vram = vram; sw_renderer_init(vram); } - -/* Under GL the internal-resolution scale lives in the hr FBO; the CPU-side - * (software mirror, readbacks, screenshots) stays native, so the reported - * scale is 1 and the software hi-res mirror stays off. */ -static void glb_set_scale(int s) { - if (s < 1) s = 1; - if (s > GL_MAX_INTERNAL_SCALE) s = GL_MAX_INTERNAL_SCALE; - s_req_scale = s; - sw_renderer_set_scale(1); -} -static int glb_scale(void) { return s_scale; } /* real internal SSAA scale (was a stub 1; the - native-wide CPU present path + gr_scale() callers - need the true scale — the FBO-direct present is - unaffected since it never reads gr_scale()) */ -static void glb_set_texture_filter(int b) { s_tex_filter = b ? 1 : 0; sw_set_texture_filter(b); } -static int glb_texture_filter(void) { return s_tex_filter; } - -static void glb_set_semi_transparency(int e, int m) { s_semi_en = e; s_semi_mode = m & 3; sw_set_semi_transparency(e, m); } -static void glb_set_mask_bits(int s, int c) { - int next_check = c ? 1 : 0; - if (next_check && !s_mask_check) { - /* Land all alpha-authoritative work before deriving stencil from it. */ - flush_tex_batch(); - flush_cpu_upload(); - rebuild_mask_stencils(); - } - s_mask_set = s ? 1 : 0; - s_mask_check = next_check; - sw_set_mask_bits(s, c); -} -static void glb_set_texture_window(uint32_t r) { - s_tw_mask_x = (int)(r & 0x1F); - s_tw_mask_y = (int)((r >> 5) & 0x1F); - s_tw_off_x = (int)((r >> 10) & 0x1F); - s_tw_off_y = (int)((r >> 15) & 0x1F); - sw_set_texture_window(r); -} -static void glb_set_color_modulation(int r,int g,int b,int raw) { s_mod_r=r; s_mod_g=g; s_mod_b=b; s_mod_raw=raw; sw_set_color_modulation(r,g,b,raw); } -/* Sub-pixel / perspective overrides for the next triangle. Forwarded to the - * software rasterizer too: pre-context draws (s_raster_ok == 0) still go there, - * and the GL backend delegates whole prim classes to it. */ -static void glb_set_precise_triangle(int enabled, - int32_t x0,int32_t y0, int32_t x1,int32_t y1, - int32_t x2,int32_t y2) { - s_pc_valid = enabled ? 1 : 0; - if (s_pc_valid) { - /* 16.16 native VRAM px -> float. The GL pipeline is float end-to-end, - * so the fraction survives to the rasterizer at any internal scale. */ - s_pc_x[0] = (float)x0 / 65536.0f; s_pc_y[0] = (float)y0 / 65536.0f; - s_pc_x[1] = (float)x1 / 65536.0f; s_pc_y[1] = (float)y1 / 65536.0f; - s_pc_x[2] = (float)x2 / 65536.0f; s_pc_y[2] = (float)y2 / 65536.0f; - } - sw_set_precise_triangle(enabled, x0,y0, x1,y1, x2,y2); -} -static void glb_set_perspective_triangle(int enabled, float q0, float q1, float q2) { - s_pq_valid = (enabled && q0 > 0.0f && q1 > 0.0f && q2 > 0.0f) ? 1 : 0; - s_pq[0] = q0; s_pq[1] = q1; s_pq[2] = q2; - sw_set_perspective_triangle(enabled, q0, q1, q2); -} -static void glb_set_draw_area(int x1,int y1,int x2,int y2) { flush_flat_batch(); flush_tex_batch(); s_area_x1=x1; s_area_y1=y1; s_area_x2=x2; s_area_y2=y2; sw_set_draw_area(x1,y1,x2,y2); } -static void glb_get_draw_area(int *x1,int *y1,int *x2,int *y2) { sw_get_draw_area(x1,y1,x2,y2); } -static void glb_set_draw_offset(int x,int y) { flush_flat_batch(); flush_tex_batch(); s_off_x=x; s_off_y=y; sw_set_draw_offset(x,y); } - -/* Pre-context draws (s_raster_ok == 0) fall back to the software rasterizer - * over CPU VRAM; the initial full-VRAM upload at context init folds them in. - * Offline post-init is GPU-only (FBO-auth). Netplay dual-raster always writes - * SW @ 1× for authority, then GPU @ s_scale for present quality. */ -/* The sub-pixel / perspective override describes exactly one triangle; drop it - * once that triangle has been submitted so a later prim can never inherit it. */ -static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; } - -static void glb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col) { - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col); - if (!s_raster_ok) return; - gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1); - precise_consumed(); -} -static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2) { - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2); - if (!s_raster_ok) return; - gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1); - precise_consumed(); -} -static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_fill_rect(x,y,w,h,c); - if (!s_raster_ok) return; - gpu_fill(x,y,w,h,c); -} -static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_copy_rect(sx,sy,dx,dy,w,h); - if (!s_raster_ok) return; - gpu_copy_rect(sx,sy,dx,dy,w,h); -} -static void glb_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,int x2,int y2,int u2,int v2,uint16_t cx,uint16_t cy,uint16_t tp){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp); - if (!s_raster_ok) return; - int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2}; - float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f; - float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb}; - gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL); - precise_consumed(); -} -static void glb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,int x1,int y1,int u1,int v1,uint32_t c1,int x2,int y2,int u2,int v2,uint32_t c2,uint16_t cx,uint16_t cy,uint16_t tp,int raw){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); - if (!s_raster_ok) return; - int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2}; - uint32_t cc[3]={c0,c1,c2}; float col[9]; - for (int i=0;i<3;i++){ col[i*3+0]=(cc[i]&0xFF)/255.0f; col[i*3+1]=((cc[i]>>8)&0xFF)/255.0f; col[i*3+2]=((cc[i]>>16)&0xFF)/255.0f; } - gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL); - precise_consumed(); -} -static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_flat_rect(x,y,w,h,c); - if (!s_raster_ok) return; - gpu_flat_rect(x,y,w,h,c, s_semi_en?s_semi_mode:-1); -} -static void glb_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t cx,uint16_t cy,uint16_t tp){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp); - if (!s_raster_ok) return; - gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1); -} -static void glb_draw_textured_rect_scaled(int x,int y,int w,int h,int u0,int v0,int u1,int v1,uint16_t cx,uint16_t cy,uint16_t tp){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp); - if (!s_raster_ok) return; - gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1); -} -static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_line(x0,y0,x1,y1,c); - if (!s_raster_ok) return; - gpu_line(x0,y0,c, x1,y1,c, s_semi_en?s_semi_mode:-1); -} -static void glb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_shaded_line(x0,y0,c0,x1,y1,c1); - if (!s_raster_ok) return; - gpu_line(x0,y0,c0, x1,y1,c1, s_semi_en?s_semi_mode:-1); -} -static int glb_render_display(uint32_t *o,int p,int dx,int dy,int dw,int dh){ ensure_cpu(); return sw_render_display(o,p,dx,dy,dw,dh); } -static int glb_render_display_hires(uint32_t *o,int p,int dx,int dy,int dw,int dh){ ensure_cpu(); return sw_render_display_hires(o,p,dx,dy,dw,dh); } -/* While GP1 depth24 is on, packed RGB888 lives in the CPU mirror and is - * presented via gl_renderer_present — never as 1555 FBO texels. Queuing those - * MDEC A0 rects hits UP_RECTS_MAX (16) and force-flushes mid-movie (MotK intro - * ~50→~30 FPS). Skip ONLY framebuffer-sized transfers (RGB888); still upload - * smaller texture A0s so post-FMV menus keep VRAM pages coherent. - * On leave: clear the skipped FB union in the FBO — do NOT restage CPU RGB888 - * as 1555 (that painted MotK title rainbow/static). */ + extern int psx_netplay_active(void); + if (!s_raster_ok || !s_gpu_dirty) return; + /* Dual-raster / netplay: CPU VRAM is written on every GP0 (or pure SW). + * Never glReadPixels — that forked peer snaps/resim. */ + if (s_cpu_auth_dual || psx_netplay_active()) { + s_gpu_dirty = 0; + return; + } + flush_flat_batch(); + flush_tex_batch(); /* realise queued textured draws before reading the FBO back */ + flush_cpu_upload(); + pack_flush(); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); + glReadPixels(0, 0, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, s_vram); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + s_gpu_dirty = 0; + coh_record(GL_COH_ENSURE, 0, 0, VRAM_W - 1, VRAM_H - 1); +} + +/* ---- GPU primitives ------------------------------------------------------ */ + +static uint64_t s_scene_prims = 0; /* frame_perf: scene primitives submitted (pre double-draw) */ +static uint64_t s_scene_prims_tex = 0; /* frame_perf: of which textured (vs flat geometry) */ +static void flush_tex_batch(void); /* fwd: drained at the backdrop-phase boundary below */ +static void mark_prim_dirty(const int *xs, const int *ys, int n, int textured) { + s_scene_prims++; + int x0 = xs[0], x1 = xs[0], y0 = ys[0], y1 = ys[0]; + for (int i = 1; i < n; i++) { + if (xs[i] < x0) x0 = xs[i]; if (xs[i] > x1) x1 = xs[i]; + if (ys[i] < y0) y0 = ys[i]; if (ys[i] > y1) y1 = ys[i]; + } + /* A sub-pixel-corrected vertex lies in [int, int+1) of the integer position + * it was rounded from, so the corrected prim can touch one more pixel on + * each max edge than the integer bbox covers. Widen before clipping so the + * pack/readback dirty rect never trails the drawn area. */ + if (s_pc_valid) { x1 += 1; y1 += 1; } + s_bdg_prims++; /* dbg: prims seen this frame (gate is now per-prim, see bd_prim_gate) */ + if (s_ptrace_n < PTRACE_CAP) { + PrimRec *p = &s_ptrace[s_ptrace_n++]; + p->x0 = (short)x0; p->x1 = (short)x1; p->y0 = (short)y0; p->y1 = (short)y1; + p->tex = (unsigned char)textured; + } + if (x0 < s_area_x1) x0 = s_area_x1; + if (y0 < s_area_y1) y0 = s_area_y1; + if (x1 > s_area_x2) x1 = s_area_x2; + if (y1 > s_area_y2) y1 = s_area_y2; + rect_add(&s_pack_dirty, x0, y0, x1, y1); + /* Dual-raster keeps CPU current via SW writes — do not mark GPU-ahead. */ + if (!s_cpu_auth_dual) + s_gpu_dirty = 1; + coh_record(GL_COH_DRAW, x0, y0, x1, y1); +} + +/* ---- native-wide mirror pass plumbing ----------------------------------- * + * Re-issue an already-set-up draw (program/VAO/VBO/blend/stencil bound) into + * the active wide surface. The geometry positions are identical on the host + * side; the x translation (wide_dx) and the wider clip are applied entirely in + * the vertex shader via u_xoff / u_xhalf, so the SAME glDrawArrays produces the + * shifted copy. wide_target_begin binds the wide FBO + viewport + scissor and + * sets the projection uniforms; wide_target_end restores u_xoff=0/u_xhalf=512 + * on the active program so the next canonical pass is bit-identical. The + * caller stays inside hr_begin/hr_end; hr_end unbinds and the next hr_begin + * resets the viewport, so no viewport restore is needed mid-function. + * + * The scissor X is the FULL wide surface (NOT the draw area): the SW reference + * (rt_wide()) deliberately lets the shifted geometry fill the revealed 16:9 + * margins that lie OUTSIDE the game's 4:3 draw-area x-range; scissoring X to + * the (translated) draw area would crop exactly the margin content native-wide + * exists to reveal. The scissor Y stays clamped to the DRAW AREA, exactly like + * rt_wide() (t.cy1/cy2 = g_clip_y1/y2): native-wide only widens X. A full-height + * Y scissor let draws that canonically clip at a vertical double-buffer band + * boundary (MMX6: draw area alternates y=0/y=240, both bands in ONE wide + * surface) bleed into the OTHER band's rows — presented one frame later as + * top/bottom edge flicker (16:9 GL only). */ +static void wide_target_begin(int dx, GLint uXoff, GLint uXhalf) { + if (s_ws_ablate != 3) /* ablate 3: no FBO rebind (draws land in hr — perf probe) */ + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, g_wide_cur); + glViewport(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); + glEnable(GL_SCISSOR_TEST); + { + int sy = s_area_y1, sh = s_area_y2 - s_area_y1 + 1; + if (sy < 0) { sh += sy; sy = 0; } + if (sy + sh > VRAM_H) sh = VRAM_H - sy; + if (sh < 0) sh = 0; + glScissor(0, sy * s_scale, g_wide_w * s_scale, sh * s_scale); + } + p_glUniform1f(uXoff, (float)dx); + p_glUniform1f(uXhalf, (float)g_wide_w / 2.0f); +} +static void wide_target_end(GLint uXoff, GLint uXhalf) { + p_glUniform1f(uXoff, 0.0f); + p_glUniform1f(uXhalf, 512.0f); + if (s_ws_ablate != 3) + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); +} + +extern int psx_ws_prim_is_tagged(void); /* gpu.c: is the current GP0 prim sprite-tagged? */ +extern int psx_ws_prim_in_backdrop(void); /* gpu.c: is its source addr in the flower-field struct? */ +extern int gpu_ws_nw_flat_backdrop_enabled(void); /* gpu.c: per-title flat backdrop opt-in */ + +/* Per-prim gate: stretch this prim iff native-wide + feature on AND the prim's + * source address is inside the flower-field backdrop data structure (precise — + * excludes the 3D rock/foreground, which is untagged AND has narrow prims so the + * earlier tag/narrow heuristic tore it). mode!=0 falls back to the old + * tag+narrow heuristic for A/B. */ +static int bd_prim_gate(const int *xs, int n, int textured) { + if (g_wide_w <= 0 || !g_ws_bd_stretch_on) return 0; + /* Some games draw their authored 4:3 sky/water as flat-colour polygons. + * Stretch those only in the native-wide mirror: the canonical framebuffer + * remains byte-for-byte 4:3, while the flat backdrop reaches the reveal + * margins. Opt-in because flat foreground geometry is title-dependent. */ + if (!textured && gpu_ws_nw_flat_backdrop_enabled()) return 1; + if (g_ws_bd_phase_mode != 0) return psx_ws_prim_in_backdrop(); /* default: precise address gate */ + /* mode 0: legacy tag+narrow heuristic (kept for comparison) */ + int native_w = g_wide_w - 2 * g_wide_off; + if (native_w <= 0) return 0; + if (psx_ws_prim_is_tagged()) return 0; + int base = g_wide_cur_base, lo = xs[0], hi = xs[0]; + for (int i = 1; i < n; i++) { if (xs[i] < lo) lo = xs[i]; if (xs[i] > hi) hi = xs[i]; } + if (lo < base - g_ws_bd_phase_thresh) return 0; /* into left margin -> GTE-wide */ + if (hi > base + native_w + g_ws_bd_phase_thresh) return 0; /* into right margin -> GTE-wide */ + return 1; +} + +/* ---- native-wide FAST path (skip redundant center mirror) ----------------- * + * The wide surface's CENTRE columns [g_wide_off, g_wide_off+native_w) are, by + * construction, identical to the canonical 4:3 framebuffer. So instead of + * re-rasterizing every primitive into the wide surface (the "mirror" pass — the + * dominant native-wide GPU cost, ~2x scene fill), we copy the canonical centre + * into the wide surface once at present (blit_wide_center_from_canonical), and + * the per-prim mirror only needs to produce the reveal MARGINS. Any prim/batch + * whose x-range is fully inside the 4:3 frame contributes nothing to the margins, + * so its mirror is skipped entirely. Correctness does not depend on the skip + * being precise: the centre is authoritatively overwritten by the blit, so the + * ONLY requirement is that a margin-reaching prim is NOT skipped — hence the + * conservative strict-inside test. 4:3 never runs any of this (g_wide_cur == 0). + * Toggle via gl_wide_fast for A/B; default ON. */ +static int s_wide_fast = 1; +void gl_renderer_set_wide_fast(int on) { s_wide_fast = on ? 1 : 0; } +int gl_renderer_get_wide_fast(void) { return s_wide_fast; } +static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h); /* def below */ +/* True if [lo,hi] (canonical draw-x) lies strictly inside the 4:3 frame, so the + * prim adds nothing to either reveal margin and its mirror can be skipped. */ +static int mirror_x_center_only(int lo, int hi) { + if (!s_wide_fast) return 0; + int base = g_wide_cur_base, native_w = g_wide_w - 2 * g_wide_off; + if (native_w <= 0) return 0; + return (lo >= base) && (hi < base + native_w); +} +static int mirror_geo_center_only(const int *xs, int n) { + if (!s_wide_fast) return 0; + int lo = xs[0], hi = xs[0]; + for (int i = 1; i < n; i++) { if (xs[i] < lo) lo = xs[i]; if (xs[i] > hi) hi = xs[i]; } + return mirror_x_center_only(lo, hi); +} +/* mirror_batch_center_only (textured-batch variant) is defined after s_tb below. */ + +/* Set / clear the 2D-backdrop x-stretch for a wide-mirror draw, per the current + * gate (s_bd_gate, set by the caller from bd_prim_gate / the batch gate). */ +static void wide_set_bd_scale(GLint uScale, GLint uCenter) { + extern int g_ws_tex_edge_pct; + float scale = 1.0f, center = 0.0f; + if (s_bd_gate && g_ws_bd_stretch_on && g_wide_w > 0) { + int native_w = g_wide_w - 2 * g_wide_off; + if (native_w > 0) { + scale = g_ws_bd_stretch_pct > 0 ? (float)g_ws_bd_stretch_pct / 100.0f + : (float)g_wide_w / (float)native_w; + if (s_bd_gate == 2) { + scale = g_ws_tex_edge_pct > 0 + ? (float)g_ws_tex_edge_pct / 100.0f : scale; + scale = -scale; /* shader: expand only beyond canonical edges */ + } + center = (float)g_wide_cur_base + (float)native_w / 2.0f; + } + } + if (scale != 1.0f) s_bdg_applied++; + p_glUniform1f(uScale, scale); + p_glUniform1f(uCenter, center); +} +static void wide_clear_bd_scale(GLint uScale, GLint uCenter) { + p_glUniform1f(uScale, 1.0f); + p_glUniform1f(uCenter, 0.0f); +} + +/* ---- textured-prim batching -------------------------------------------- * + * Consecutive textured prims sharing blend/mask/texwindow/filter coalesce into + * one draw. Per-prim texture state (texpage/clut/depth/raw/uv-limits) rides in + * the vertex (TEXV flat attributes), so only `semi` (blend) and the global + * mask/twin/filter are batch keys. flush_tex_batch() draws the queued verts; it + * is called before any op that reads VRAM, writes it outside the batch, or + * changes batch state (see callers: flush_cpu_upload, flush_pack_if_sampling, + * every non-textured glb_ wrapper, and the present path). Drawing reads only the + * already-coherent texture (per-prim coherency was ensured at append time), so + * flush_tex_batch never re-enters those helpers. */ +#define TEXBATCH_MAXV 8190 /* multiple of 3; ~2730 tris */ +static float s_tb[TEXBATCH_MAXV * TEXV]; +static int s_tb_n = 0; /* verts queued */ +static int s_tb_semi = -2; +static int s_tb_mask = 0, s_tb_filter = 0; +static int s_tb_twin[4] = {0, 0, 0, 0}; +static uint64_t s_batch_total = 0, s_batch_reason[7]; + +void gl_renderer_batch_diag(uint64_t out[8]) { + out[0] = s_batch_total; + for (int i = 0; i < 7; i++) out[i + 1] = s_batch_reason[i]; +} + +/* Draw the queued textured batch with correct PSX mask-bit handling AND correct + * painter's order. The mask bit lives in both the colour-attachment alpha + * (frag.a) and the stencil; STP=1 texels must always set it. + * + * OPAQUE batch (semi < 0): the STP bit does NOT gate COLOUR (every texel is + * opaque), so colour is drawn in ONE ordered pass over all texels — frag.a still + * carries the per-texel mask bit, so the alpha mask is correct — followed by a + * COLOUR-MASKED pass that only fixes the STENCIL for STP=1 texels. The old code + * split colour into STP=0 (pass 1) then STP=1 (pass 2) across the WHOLE batch, + * which let a behind prim's STP=1 texels overwrite a front prim's STP=0 colour + * (the Tomba character drew behind an AP-block's letters / a save post on GL + * only). Same draw count, order preserved, mask preserved. + * + * SEMI batch (semi >= 0): genuine two-pass — STP=0 texels opaque, STP=1 texels + * blended per the PSX mode. Cross-prim order is kept by isolating semi prims to + * one per batch (see gpu_textured_triangle), so this batch holds a single prim + * whose two passes do not self-overlap. */ +static void tex_batch_draw_passes(int nverts, int semi) { + p_glUniform1i(s_uSemimode, semi < 0 ? 0 : semi); + if (semi < 0) { + glDisable(GL_BLEND); + mask_stencil(s_tb_mask); + p_glUniform1i(s_uSemipass, 0); /* all texels, one ordered colour pass */ + glDrawArrays(GL_TRIANGLES, 0, nverts); + if (s_mask_check) { + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); /* stencil-only fixup */ + mask_stencil(1); + p_glUniform1i(s_uSemipass, 2); /* STP=1 texels set the mask bit */ + glDrawArrays(GL_TRIANGLES, 0, nverts); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + } else if (!s_tb_mask) { + /* Alpha is already exact; defer its duplicate stencil encoding + * until a later GP0(E6h) actually enables destination masking. */ + s_stencil_valid = 0; + } + } else if (!s_mask_check && semi == 4) { + /* Modes 0/1/3 can select opaque-vs-semi behavior per fragment with + * dual-source factors, so the whole painter-ordered batch is one draw. + * Mode 2 needs a different blend equation and stays on the conservative + * isolated path below. */ + glEnable(GL_BLEND); + p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); + p_glBlendFuncSeparate(GL_ONE, PSXGL_SRC1_ALPHA, GL_ONE, GL_ZERO); + if (s_tb_mask) mask_stencil(1); else glDisable(GL_STENCIL_TEST); + p_glUniform1i(s_uSemipass, 0); + glDrawArrays(GL_TRIANGLES, 0, nverts); + if (!s_tb_mask) s_stencil_valid = 0; + } else { + glDisable(GL_BLEND); /* Pass 1: STP=0 texels (opaque) */ + mask_stencil(s_tb_mask); + p_glUniform1i(s_uSemipass, 1); + glDrawArrays(GL_TRIANGLES, 0, nverts); + apply_psx_blend(semi); /* Pass 2: STP=1 texels (blended) */ + mask_stencil(1); + p_glUniform1i(s_uSemipass, 2); + glDrawArrays(GL_TRIANGLES, 0, nverts); + } +} + +/* ---- frame_perf CPU attribution (native-wide wedge hunt) ----------------- * + * Per-frame CPU wall time spent inside the GL submission paths (driver CPU + * cost surfaces INSIDE our gl* calls) + counters for the wide plumbing, so a + * CPU-bound wide frame (emu_cpu >> scene_gpu) can be attributed without a + * sampling profiler. Reset at present enter; reported by frame_perf. */ +static double cw_ms(void) { + static double freq = 0.0; + if (freq == 0.0) { + uint64_t f = SDL_GetPerformanceFrequency(); + freq = f ? (double)f : 1.0; + } + return (double)SDL_GetPerformanceCounter() * 1000.0 / freq; +} +static double s_cw_flush_ms = 0.0; /* CPU wall inside flush_tex_batch */ +static double s_cw_wide_ms = 0.0; /* CPU wall inside glb_wide_* entry points */ +static int s_cw_batches = 0, s_cw_wide_sets = 0, s_cw_wide_cfgs = 0, + s_cw_wide_clears = 0, s_cw_fbo_creates = 0, s_cw_flush_depth = 0; + +/* Textured-batch variant of mirror_x_center_only: scan the queued verts' x + * (attr 0, stride TEXV). Defined here so s_tb / TEXV are in scope. */ +static int mirror_batch_center_only(int nverts) { + if (!s_wide_fast || nverts <= 0) return 0; + float flo = s_tb[0], fhi = s_tb[0]; + for (int i = 1; i < nverts; i++) { + float x = s_tb[i * TEXV]; + if (x < flo) flo = x; if (x > fhi) fhi = x; + } + /* floor/ceil, not a truncating cast: with geometry_correction these are + * fractional, and (int) rounds toward zero — which WIDENS a negative x + * toward the centre and could wrongly call a margin-touching batch + * centre-only, dropping its wide-mirror draw. Whole values are unchanged. */ + return mirror_x_center_only((int)floorf(flo), (int)ceilf(fhi)); +} + +static void flush_tex_batch(void) { + if (s_tb_n == 0) return; + int nverts = s_tb_n, semi = s_tb_semi; + s_tb_n = 0; /* clear first: re-entrancy safe */ + double cw_t0 = cw_ms(); + s_cw_batches++; s_batch_total++; s_cw_flush_depth++; + + hr_begin(1); + p_glUseProgram(s_tex_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_raw_tex); + p_glUniform1i(s_uVram, 0); + p_glUniform4i(s_uTwin, s_tb_twin[0], s_tb_twin[1], s_tb_twin[2], s_tb_twin[3]); + p_glUniform1i(s_uMaskset, s_tb_mask); + p_glUniform1i(s_uFilter, s_tb_filter); + p_glBindVertexArray(s_tex_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); + p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(nverts * TEXV * sizeof(float)), s_tb, PSXGL_STREAM_DRAW); + + tex_batch_draw_passes(nverts, semi); + + /* Native-wide mirror — skipped for a batch fully inside the 4:3 frame (its + * centre content comes from the present-time canonical blit; nothing to add + * to the margins). A backdrop-stretched batch (s_tb_gate) widens past the + * frame, so it is never treated as centre-only. */ + if (g_wide_cur && s_ws_ablate != 1 && + !(s_tb_gate == 0 && mirror_batch_center_only(nverts))) { /* native-wide mirror */ + int dx = wide_dx(); + s_bd_gate = s_tb_gate; /* this batch is uniform-gate (flushed on change) */ + gl_perf_mirror_begin(); + wide_target_begin(dx, s_tex_uXoff, s_tex_uXhalf); + wide_set_bd_scale(s_tex_uXscale, s_tex_uXcenter); + if (s_ws_ablate != 2) tex_batch_draw_passes(nverts, semi); + wide_clear_bd_scale(s_tex_uXscale, s_tex_uXcenter); + wide_target_end(s_tex_uXoff, s_tex_uXhalf); + gl_perf_mirror_end(); + } + hr_end(); + if (--s_cw_flush_depth == 0) s_cw_flush_ms += cw_ms() - cw_t0; +} + +/* Flat / gouraud GEO batch — MotK title/char-select starfields issue ~30k/s + * GP0(68h) 1x1 dots; each was two immediate gpu_triangle draws (BufferData + + * DrawArrays each). Coalesce opaque/semi-uniform tris into one draw. */ +#define FLATBATCH_MAXV 8190 /* multiple of 3 */ +static float s_fb[FLATBATCH_MAXV * 6]; +static int s_fb_n = 0; +static int s_fb_semi = -2; +static int s_fb_mask = -1; + +static int mirror_flat_batch_center_only(int nverts) { + if (!s_wide_fast || nverts <= 0) return 0; + float flo = s_fb[0], fhi = s_fb[0]; + for (int i = 1; i < nverts; i++) { + float x = s_fb[i * 6]; + if (x < flo) flo = x; if (x > fhi) fhi = x; + } + /* floor/ceil rather than a truncating cast — see mirror_batch_center_only. */ + return mirror_x_center_only((int)floorf(flo), (int)ceilf(fhi)); +} + +static void flush_flat_batch(void) { + if (s_fb_n == 0) return; + int nverts = s_fb_n, semi = s_fb_semi, mask = s_fb_mask; + s_fb_n = 0; + + hr_begin(1); + if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); + mask_stencil(mask); + p_glUseProgram(s_geo_prog); + p_glBindVertexArray(s_geo_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); + p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(nverts * 6 * sizeof(float)), + s_fb, PSXGL_STREAM_DRAW); + glDrawArrays(GL_TRIANGLES, 0, nverts); + + if (g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 && + !(!g_ws_bd_stretch_on && mirror_flat_batch_center_only(nverts))) { + int dx = wide_dx(); + /* Batch may span many prims; use stretch gate off (flat dots/UI). */ + s_bd_gate = 0; + gl_perf_mirror_begin(); + wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf); + wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter); + if (s_ws_ablate != 2) glDrawArrays(GL_TRIANGLES, 0, nverts); + wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter); + wide_target_end(s_geo_uXoff, s_geo_uXhalf); + gl_perf_mirror_end(); + } + hr_end(); +} + +/* Flat / gouraud triangles and lines share the GEO program. mode: GL_TRIANGLES + * or GL_LINES; verts are (x, y, r, g, b, a) tuples with colors as 1555. */ +static void gpu_geometry(GLenum mode, const int *xs, const int *ys, + const uint16_t *cs, int n, int semi) { + flush_tex_batch(); /* flat prim: drain textured draws first (order + program) */ + flush_cpu_upload(); /* also drains flat batch if an upload was pending */ + mark_prim_dirty(xs, ys, n, 0 /* flat */); + /* Sub-pixel positions only describe a 3-vertex projected triangle. */ + const int precise = s_pc_valid && mode == GL_TRIANGLES && n == 3; + + /* Lines stay immediate (rare); tris batch for MotK 0x68 starfields. */ + if (mode != GL_TRIANGLES || n < 3) { + flush_flat_batch(); + float verts[3 * 6]; + float mask_a = s_mask_set ? 1.0f : 0.0f; + for (int i = 0; i < n; i++) { + verts[i*6+0] = precise ? s_pc_x[i] : (float)xs[i]; + verts[i*6+1] = precise ? s_pc_y[i] : (float)ys[i]; + verts[i*6+2] = ((cs[i] & 0x1F) << 3) / 255.0f; + verts[i*6+3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f; + verts[i*6+4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f; + verts[i*6+5] = mask_a; + } + hr_begin(1); + if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); + mask_stencil(s_mask_set); + if (mode == GL_LINES) glLineWidth((float)s_scale); + p_glUseProgram(s_geo_prog); + p_glBindVertexArray(s_geo_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); + p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(n * 6 * sizeof(float)), + verts, PSXGL_STREAM_DRAW); + glDrawArrays(mode, 0, n); + if (g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 && + !(!g_ws_bd_stretch_on && mirror_geo_center_only(xs, n))) { + int dx = wide_dx(); + s_bd_gate = bd_prim_gate(xs, n, 0); + gl_perf_mirror_begin(); + wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf); + wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter); + if (s_ws_ablate != 2) glDrawArrays(mode, 0, n); + wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter); + wide_target_end(s_geo_uXoff, s_geo_uXhalf); + gl_perf_mirror_end(); + } + hr_end(); + return; + } + + if (s_fb_n > 0 && (s_fb_semi != semi || s_fb_mask != (int)s_mask_set)) + flush_flat_batch(); + if (s_fb_n + n > FLATBATCH_MAXV) + flush_flat_batch(); + s_fb_semi = semi; + s_fb_mask = (int)s_mask_set; + + float mask_a = s_mask_set ? 1.0f : 0.0f; + for (int i = 0; i < n; i++) { + float *v = &s_fb[s_fb_n * 6]; + v[0] = precise ? s_pc_x[i] : (float)xs[i]; + v[1] = precise ? s_pc_y[i] : (float)ys[i]; + v[2] = ((cs[i] & 0x1F) << 3) / 255.0f; + v[3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f; + v[4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f; + v[5] = mask_a; + s_fb_n++; + } +} + +static void gpu_triangle(int x0,int y0,uint16_t c0, int x1,int y1,uint16_t c1, + int x2,int y2,uint16_t c2, int semi) { + int xs[3] = {x0, x1, x2}, ys[3] = {y0, y1, y2}; + uint16_t cs[3] = {c0, c1, c2}; + gpu_geometry(GL_TRIANGLES, xs, ys, cs, 3, semi); +} + +static void gpu_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int semi) { + int xs[2] = {x0, x1}, ys[2] = {y0, y1}; + uint16_t cs[2] = {c0, c1}; + gpu_geometry(GL_LINES, xs, ys, cs, 2, semi); +} + +/* Shared PS1 uv-sampling model (limits + mirrored-2D compensation) — one + * implementation for GL/VK/SW, see gpu_uv.h. */ +#include "gpu_uv.h" + +/* Textured triangle. Always two passes split by the per-texel STP bit so the + * stencil (mask) write value is constant within each pass; the semi pass is + * also where PS1 blending applies. lim = uv sampling bounds (see + * tri_uv_limits); NULL computes them from the vertices. */ +static void gpu_textured_triangle(const int *xs, const int *ys, + const int *us, const int *vs, + const float *col, uint16_t texpage, + uint16_t clut_x, uint16_t clut_y, int rawtex, + int semi, const int *lim) { + int lim_buf[4]; + int uv_buf[6]; + if (!lim) { + /* Poly path: exact sampled bounds from the ORIGINAL uvs, then the + * center-sampling mirror compensation (rect prims arrive with their + * own precomputed lim and pre-bumped uvs). */ + int *mu = uv_buf, *mv = uv_buf + 3; + for (int i = 0; i < 3; i++) { mu[i] = us[i]; mv[i] = vs[i]; } + psx_uv_tri_limits(xs, ys, mu, mv, lim_buf); + psx_uv_tri_mirror_offset(xs, ys, mu, mv); + us = mu; vs = mv; + lim = lim_buf; + } + s_scene_prims_tex++; + int base_x = (texpage & 0xF) * 64; + int base_y = ((texpage >> 4) & 1) * 256; + int depth = (texpage >> 7) & 3; if (depth > 2) depth = 2; + + flush_cpu_upload(); /* if a CPU->VRAM upload is pending it flushes the batch first */ + flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y); /* flushes batch iff it must pack */ + mark_prim_dirty(xs, ys, 3, 1 /* textured */); + + /* Append to the textured batch. Flush first if this prim's blend/mask/twin/ + * filter differ from the open batch, or the buffer is full. Per-prim texture + * state goes in the vertex; only these keys force a new draw. */ + { + flush_flat_batch(); /* painter order: flat GEO before textured */ + int twx = s_tw_mask_x, twy = s_tw_mask_y, tox = s_tw_off_x, toy = s_tw_off_y; + int gate = bd_prim_gate(xs, 3, 1); /* backdrop-stretch gate is also a batch key */ + /* Batch key: keep opaque as -1. Dual-source (4) is only for semi modes + * 0/1/3 when mask-check is off — never coalesce opaque into that key. + * Mixing opaque+semi under u_semimode==4 made additive particle/glow + * batches (CTR Naughty Dog intro binary funnel) paint over later + * opaque crate flaps whenever submission order and STP bits disagreed + * with the dual-source path's assumptions. */ + int batch_semi; + if (semi < 0) + batch_semi = -1; + else if (!s_mask_check && semi != 2) + batch_semi = 4; + else + batch_semi = semi; + /* STP draw-ORDER correctness. flush_tex_batch's conservative two-pass + * path draws pass 1 = every prim's STP=0 texels then pass 2 = every + * prim's STP=1 texels — a behind prim's semi texels then overwrite a + * front prim's opaque texels (Tomba AP-block / CTR intro flaps). The + * dual-source single-pass path avoids that WITHIN one prim, but + * batching many overlapping semi quads (digit particles + glow) still + * mis-orders against neighbouring opaque geometry. Isolate EVERY + * semi-transparent textured prim: drain the open batch, draw this + * prim alone (composited fully before the next), let opaque prims + * keep batching. Cost is one draw per semi prim. */ + int isolate = (semi >= 0); + int reason = -1; + if (s_tb_n > 0) { + if (isolate) reason = 0; + else if (batch_semi != s_tb_semi) reason = 1; + else if (s_mask_set != s_tb_mask) reason = 2; + else if (s_tex_filter != s_tb_filter) reason = 3; + else if (gate != s_tb_gate) reason = 4; + else if (twx != s_tb_twin[0] || twy != s_tb_twin[1] || + tox != s_tb_twin[2] || toy != s_tb_twin[3]) reason = 5; + } + if (reason >= 0) { + s_batch_reason[reason]++; + flush_tex_batch(); + } + if (s_tb_n + 3 > TEXBATCH_MAXV) { s_batch_reason[6]++; flush_tex_batch(); } + if (s_tb_n == 0) { /* opening a batch: capture its keyed state */ + s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = s_tex_filter; s_tb_gate = gate; + s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy; + } + float *vp = &s_tb[s_tb_n * TEXV]; + for (int i = 0; i < 3; i++, vp += TEXV) { + vp[0] = s_pc_valid ? s_pc_x[i] : (float)xs[i]; + vp[1] = s_pc_valid ? s_pc_y[i] : (float)ys[i]; + vp[2] = (float)us[i]; vp[3] = (float)vs[i]; + vp[4] = col[i*3+0]; vp[5] = col[i*3+1]; vp[6] = col[i*3+2]; vp[7] = 1.0f; + vp[8] = (float)base_x; vp[9] = (float)base_y; /* a_tpage */ + vp[10] = (float)clut_x; vp[11] = (float)clut_y; /* a_clut */ + vp[12] = (float)depth; vp[13] = (float)rawtex; /* a_depth, a_raw */ + vp[14] = (float)lim[0]; vp[15] = (float)lim[1]; /* a_limits */ + vp[16] = (float)lim[2]; vp[17] = (float)lim[3]; + vp[18] = semi >= 0 ? (float)(semi + 1) : 0.0f; /* a_semi code */ + vp[19] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */ + } + s_tb_n += 3; + if (isolate) flush_tex_batch(); /* draw this semi prim alone, in submission order */ + } +} + +/* Draw a flat-colored rect (GEO program) DIRECTLY into the active wide surface + * at wide-space coords [wx, wx+ww) × [y, y+h). Used only by the full-screen- + * overlay path; positions are already in wide space so u_xoff stays 0. Mirrors + * raster_flat_rect(&wt, ...) in sw_draw_flat_rect. Caller stays inside + * hr_begin/hr_end. */ +static void wide_flat_rect_direct(int wx, int y, int ww, int h, uint16_t c, int semi) { + if (ww <= 0 || h <= 0) return; + float r = ((c & 0x1F) << 3) / 255.0f; + float g = (((c >> 5) & 0x1F) << 3) / 255.0f; + float b = (((c >> 10) & 0x1F) << 3) / 255.0f; + float a = s_mask_set ? 1.0f : 0.0f; + float fx0 = (float)wx, fy0 = (float)y, fx1 = (float)(wx + ww), fy1 = (float)(y + h); + float verts[6 * 6] = { + fx0,fy0,r,g,b,a, fx1,fy0,r,g,b,a, fx0,fy1,r,g,b,a, + fx1,fy0,r,g,b,a, fx0,fy1,r,g,b,a, fx1,fy1,r,g,b,a, + }; + /* Wide target: positions already wide-space so u_xoff = 0; full-width + * scissor; u_xhalf = g_wide_w/2. */ + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, g_wide_cur); + glViewport(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); + glEnable(GL_SCISSOR_TEST); + glScissor(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); /* full surface (rt_wide) */ + if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); + mask_stencil(s_mask_set); + p_glUseProgram(s_geo_prog); + p_glUniform1f(s_geo_uXoff, 0.0f); + p_glUniform1f(s_geo_uXhalf, (float)g_wide_w / 2.0f); + p_glBindVertexArray(s_geo_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); + p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); + glDrawArrays(GL_TRIANGLES, 0, 6); + p_glUniform1f(s_geo_uXoff, 0.0f); + p_glUniform1f(s_geo_uXhalf, 512.0f); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); +} + +static void gpu_flat_rect(int x,int y,int w,int h,uint16_t c,int semi) { + if (w <= 0 || h <= 0) return; + /* Full-screen 2D overlay (pause gray-filter / load fade): a flat rect + * spanning the whole 4:3 framebuffer must cover the whole wide surface too, + * else the revealed 16:9 margins are left undimmed/unfaded. Same detection + * as sw_draw_flat_rect: native_w = the 4:3 framebuffer width (g_wide_w less + * the per-side reveal on both sides); the rect's native screen-X span + * (x - base) must cover [0, native_w). When it does, the two canonical + * triangles are drawn WITHOUT the per-triangle wide mirror (suppressed) and + * a single full-width rect is drawn into the wide surface instead; every + * other rect mirrors 1:1 via the generic gpu_geometry path. Only runs in + * native-wide (g_wide_cur != 0), so 4:3 is unaffected. */ + int overlay = 0; + if (g_wide_cur) { + int native_w = g_wide_w - 2 * g_wide_off; + int lx = x - g_wide_cur_base, rx = x + w - g_wide_cur_base; + overlay = (native_w > 0 && lx <= 0 && rx >= native_w); + } + if (overlay) s_wide_suppress = 1; + gpu_triangle(x, y, c, x+w, y, c, x, y+h, c, semi); + gpu_triangle(x+w, y, c, x, y+h, c, x+w, y+h, c, semi); + if (overlay) { + s_wide_suppress = 0; + /* The two canonical triangles drew into the hr FBO already (each + * gpu_triangle ran its own hr_begin/hr_end). Re-open the bracket just + * for the full-width wide pass so blend/scissor/program state is + * clean. */ + if (s_ws_ablate != 1) { + hr_begin(0); + gl_perf_mirror_begin(); + wide_flat_rect_direct(0, y, g_wide_w, h, c, semi); + gl_perf_mirror_end(); + hr_end(); + } + } +} + +static void gpu_textured_rect(int x,int y,int w,int h, + int u0,int v0,int u1,int v1, + uint16_t clut_x,uint16_t clut_y,uint16_t tp,int semi) { + if (w <= 0 || h <= 0) return; + float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f; + float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb}; + /* gpu.c routes axis-aligned MIRRORED quads (X/Y-flipped 2D sprites, + * e.g. right-facing MMX entities) through THIS path as scaled rects + * with u0>u1 / v0>v1 — they never reach the poly path. Exact bounds + * from the original corners, then the mirror bump (see gpu_uv.h). */ + int lim[4]; + psx_uv_rect_limits(u0, v0, u1, v1, lim); + psx_uv_rect_mirror_offset(&u0, &v0, &u1, &v1); + int xs1[3]={x, x+w, x}, ys1[3]={y, y, y+h}; + int us1[3]={u0,u1,u0}, vs1[3]={v0,v0,v1}; + gpu_textured_triangle(xs1,ys1,us1,vs1,col,tp,clut_x,clut_y,s_mod_raw,semi,lim); + int xs2[3]={x+w, x, x+w}, ys2[3]={y, y+h, y+h}; + int us2[3]={u1,u0,u1}, vs2[3]={v0,v1,v1}; + gpu_textured_triangle(xs2,ys2,us2,vs2,col,tp,clut_x,clut_y,s_mod_raw,semi,lim); +} + +/* GP0(02h) fill: writes color with bit15=0, ignoring draw area, mask and + * offset; coordinates wrap. A scissored clear (color + stencil) per wrapped + * segment is exactly this. */ +static void fill_segment(int x, int y, int w, int h, float r, float g, float b) { + if (w <= 0 || h <= 0) return; + glScissor(x * s_scale, y * s_scale, w * s_scale, h * s_scale); + glClearColor(r, g, b, 0.0f); + glClearStencil(0); + glStencilMask(0xFF); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1); +} + +static void gpu_fill(int x,int y,int w,int h,uint16_t c) { + if (w <= 0 || h <= 0) return; + flush_flat_batch(); + flush_tex_batch(); + flush_cpu_upload(); + float r=(c&0x1F)/31.0f, g=((c>>5)&0x1F)/31.0f, b=((c>>10)&0x1F)/31.0f; + x &= VRAM_W - 1; y &= VRAM_H - 1; + if (w > VRAM_W) w = VRAM_W; + if (h > VRAM_H) h = VRAM_H; + int w1 = w, w2 = 0, h1 = h, h2 = 0; + if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; } + if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; } + + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); + glViewport(0, 0, VRAM_W * s_scale, VRAM_H * s_scale); + glEnable(GL_SCISSOR_TEST); + fill_segment(x, y, w1, h1, r, g, b); + if (w2) fill_segment(0, y, w2, h1, r, g, b); + if (h2) fill_segment(x, 0, w1, h2, r, g, b); + if (w2 && h2) fill_segment(0, 0, w2, h2, r, g, b); + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + if (!s_cpu_auth_dual) + s_gpu_dirty = 1; + coh_record(GL_COH_FILL, x, y, x + w - 1, y + h - 1); +} + +/* VRAM->VRAM copy: blit the source region to the scratch texture (resolves + * overlap), then draw it back at the destination through the BLIT program so + * mask set/check and the stencil mirror apply, exactly like sw_copy_rect. */ +static void gpu_copy_rect(int sx,int sy,int dx,int dy,int w,int h) { + if (w <= 0 || h <= 0) return; + flush_flat_batch(); + flush_tex_batch(); + flush_cpu_upload(); + /* Clamp to bounds (the software path wraps; wrapping copies are unused + * in practice — see the file header). */ + if (sx < 0) sx = 0; if (sy < 0) sy = 0; + if (dx < 0) dx = 0; if (dy < 0) dy = 0; + if (sx + w > VRAM_W) w = VRAM_W - sx; + if (dx + w > VRAM_W) w = VRAM_W - dx; + if (sy + h > VRAM_H) h = VRAM_H - sy; + if (dy + h > VRAM_H) h = VRAM_H - dy; + if (w <= 0 || h <= 0) return; + + int S = s_scale; + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo); + glDisable(GL_SCISSOR_TEST); + p_glBlitFramebuffer(sx*S, sy*S, (sx+w)*S, (sy+h)*S, + sx*S, sy*S, (sx+w)*S, (sy+h)*S, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); + + hr_begin(0); /* copies ignore the draw area */ + glScissor(dx*S, dy*S, w*S, h*S); + glDisable(GL_BLEND); + p_glUseProgram(s_blit_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_scratch_tex); + p_glUniform1i(s_uBlitSrc, 0); + p_glUniform1i(s_uBlitMaskset, s_mask_set); + /* Scratch holds the source at its own hr coords: texel = frag + (src-dst)*S. */ + p_glUniform1i(s_uBlitSrcDiv, 1); + p_glUniform2i(s_uBlitSrcOff, (sx - dx) * S, (sy - dy) * S); + float fx0 = (float)dx, fy0 = (float)dy, fx1 = (float)(dx + w), fy1 = (float)(dy + h); + float verts[6 * 2] = { + fx0, fy0, fx1, fy0, fx0, fy1, + fx1, fy0, fx0, fy1, fx1, fy1, + }; + p_glBindVertexArray(s_blit_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); + p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); + /* Two passes split by source bit15 so stencil tracks the copied mask. */ + mask_stencil(s_mask_set); p_glUniform1i(s_uBlitPass, 1); glDrawArrays(GL_TRIANGLES, 0, 6); + mask_stencil(1); p_glUniform1i(s_uBlitPass, 2); glDrawArrays(GL_TRIANGLES, 0, 6); + hr_end(); + + rect_add(&s_pack_dirty, dx, dy, dx + w - 1, dy + h - 1); + if (!s_cpu_auth_dual) + s_gpu_dirty = 1; + coh_record(GL_COH_COPY_SRC, sx, sy, sx + w - 1, sy + h - 1); + coh_record(GL_COH_COPY, dx, dy, dx + w - 1, dy + h - 1); +} + +/* ---- backend vtable wrappers ------------------------------------------- */ +static void glb_init(uint16_t *vram) { s_vram = vram; sw_renderer_init(vram); } + +/* Under GL the internal-resolution scale lives in the hr FBO; the CPU-side + * (software mirror, readbacks, screenshots) stays native, so the reported + * scale is 1 and the software hi-res mirror stays off. */ +static void glb_set_scale(int s) { + if (s < 1) s = 1; + if (s > GL_MAX_INTERNAL_SCALE) s = GL_MAX_INTERNAL_SCALE; + s_req_scale = s; + sw_renderer_set_scale(1); +} +static int glb_scale(void) { return s_scale; } /* real internal SSAA scale (was a stub 1; the + native-wide CPU present path + gr_scale() callers + need the true scale — the FBO-direct present is + unaffected since it never reads gr_scale()) */ +static void glb_set_texture_filter(int b) { s_tex_filter = b ? 1 : 0; sw_set_texture_filter(b); } +static int glb_texture_filter(void) { return s_tex_filter; } + +static void glb_set_semi_transparency(int e, int m) { s_semi_en = e; s_semi_mode = m & 3; sw_set_semi_transparency(e, m); } +static void glb_set_mask_bits(int s, int c) { + int next_check = c ? 1 : 0; + if (next_check && !s_mask_check) { + /* Land all alpha-authoritative work before deriving stencil from it. */ + flush_tex_batch(); + flush_cpu_upload(); + rebuild_mask_stencils(); + } + s_mask_set = s ? 1 : 0; + s_mask_check = next_check; + sw_set_mask_bits(s, c); +} +static void glb_set_texture_window(uint32_t r) { + s_tw_mask_x = (int)(r & 0x1F); + s_tw_mask_y = (int)((r >> 5) & 0x1F); + s_tw_off_x = (int)((r >> 10) & 0x1F); + s_tw_off_y = (int)((r >> 15) & 0x1F); + sw_set_texture_window(r); +} +static void glb_set_color_modulation(int r,int g,int b,int raw) { s_mod_r=r; s_mod_g=g; s_mod_b=b; s_mod_raw=raw; sw_set_color_modulation(r,g,b,raw); } +/* Sub-pixel / perspective overrides for the next triangle. Forwarded to the + * software rasterizer too: pre-context draws (s_raster_ok == 0) still go there, + * and the GL backend delegates whole prim classes to it. */ +static void glb_set_precise_triangle(int enabled, + int32_t x0,int32_t y0, int32_t x1,int32_t y1, + int32_t x2,int32_t y2) { + s_pc_valid = enabled ? 1 : 0; + if (s_pc_valid) { + /* 16.16 native VRAM px -> float. The GL pipeline is float end-to-end, + * so the fraction survives to the rasterizer at any internal scale. */ + s_pc_x[0] = (float)x0 / 65536.0f; s_pc_y[0] = (float)y0 / 65536.0f; + s_pc_x[1] = (float)x1 / 65536.0f; s_pc_y[1] = (float)y1 / 65536.0f; + s_pc_x[2] = (float)x2 / 65536.0f; s_pc_y[2] = (float)y2 / 65536.0f; + } + sw_set_precise_triangle(enabled, x0,y0, x1,y1, x2,y2); +} +static void glb_set_perspective_triangle(int enabled, float q0, float q1, float q2) { + s_pq_valid = (enabled && q0 > 0.0f && q1 > 0.0f && q2 > 0.0f) ? 1 : 0; + s_pq[0] = q0; s_pq[1] = q1; s_pq[2] = q2; + sw_set_perspective_triangle(enabled, q0, q1, q2); +} +static void glb_set_draw_area(int x1,int y1,int x2,int y2) { flush_flat_batch(); flush_tex_batch(); s_area_x1=x1; s_area_y1=y1; s_area_x2=x2; s_area_y2=y2; sw_set_draw_area(x1,y1,x2,y2); } +static void glb_get_draw_area(int *x1,int *y1,int *x2,int *y2) { sw_get_draw_area(x1,y1,x2,y2); } +static void glb_set_draw_offset(int x,int y) { flush_flat_batch(); flush_tex_batch(); s_off_x=x; s_off_y=y; sw_set_draw_offset(x,y); } + +/* Pre-context draws (s_raster_ok == 0) fall back to the software rasterizer + * over CPU VRAM; the initial full-VRAM upload at context init folds them in. + * Offline post-init is GPU-only (FBO-auth). Netplay dual-raster always writes + * SW @ 1× for authority, then GPU @ s_scale for present quality. */ +/* The sub-pixel / perspective override describes exactly one triangle; drop it + * once that triangle has been submitted so a later prim can never inherit it. */ +static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; } + +static void glb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col) { + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col); + if (!s_raster_ok) return; + gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1); + precise_consumed(); +} +static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2) { + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2); + if (!s_raster_ok) return; + gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1); + precise_consumed(); +} +static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_fill_rect(x,y,w,h,c); + if (!s_raster_ok) return; + gpu_fill(x,y,w,h,c); +} +static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_copy_rect(sx,sy,dx,dy,w,h); + if (!s_raster_ok) return; + gpu_copy_rect(sx,sy,dx,dy,w,h); +} +static void glb_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,int x2,int y2,int u2,int v2,uint16_t cx,uint16_t cy,uint16_t tp){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp); + if (!s_raster_ok) return; + int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2}; + float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f; + float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb}; + gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL); + precise_consumed(); +} +static void glb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,int x1,int y1,int u1,int v1,uint32_t c1,int x2,int y2,int u2,int v2,uint32_t c2,uint16_t cx,uint16_t cy,uint16_t tp,int raw){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); + if (!s_raster_ok) return; + int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2}; + uint32_t cc[3]={c0,c1,c2}; float col[9]; + for (int i=0;i<3;i++){ col[i*3+0]=(cc[i]&0xFF)/255.0f; col[i*3+1]=((cc[i]>>8)&0xFF)/255.0f; col[i*3+2]=((cc[i]>>16)&0xFF)/255.0f; } + gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL); + precise_consumed(); +} +static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_flat_rect(x,y,w,h,c); + if (!s_raster_ok) return; + gpu_flat_rect(x,y,w,h,c, s_semi_en?s_semi_mode:-1); +} +static void glb_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t cx,uint16_t cy,uint16_t tp){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp); + if (!s_raster_ok) return; + gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1); +} +static void glb_draw_textured_rect_scaled(int x,int y,int w,int h,int u0,int v0,int u1,int v1,uint16_t cx,uint16_t cy,uint16_t tp){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp); + if (!s_raster_ok) return; + gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1); +} +static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_line(x0,y0,x1,y1,c); + if (!s_raster_ok) return; + gpu_line(x0,y0,c, x1,y1,c, s_semi_en?s_semi_mode:-1); +} +static void glb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_shaded_line(x0,y0,c0,x1,y1,c1); + if (!s_raster_ok) return; + gpu_line(x0,y0,c0, x1,y1,c1, s_semi_en?s_semi_mode:-1); +} +static int glb_render_display(uint32_t *o,int p,int dx,int dy,int dw,int dh){ ensure_cpu(); return sw_render_display(o,p,dx,dy,dw,dh); } +static int glb_render_display_hires(uint32_t *o,int p,int dx,int dy,int dw,int dh){ ensure_cpu(); return sw_render_display_hires(o,p,dx,dy,dw,dh); } +/* While GP1 depth24 is on, packed RGB888 lives in the CPU mirror and is + * presented via gl_renderer_present — never as 1555 FBO texels. Queuing those + * MDEC A0 rects hits UP_RECTS_MAX (16) and force-flushes mid-movie (MotK intro + * ~50→~30 FPS). Skip ONLY framebuffer-sized transfers (RGB888); still upload + * smaller texture A0s so post-FMV menus keep VRAM pages coherent. + * On leave: clear the skipped FB union in the FBO — do NOT restage CPU RGB888 + * as 1555 (that painted MotK title rainbow/static). */ static int depth24_is_fb_transfer(int x, int y, int w, int h) { - if (!gpu_display_is_depth24() || w <= 0 || h <= 0) return 0; - GpuDisplayInfo di; - gpu_get_display_info(&di); - int fb_w = (int)((di.width * 3u + 1u) / 2u); /* RGB W → halfwords */ - int fb_h = (int)di.height; - if (fb_w < 8) fb_w = 8; - if (fb_h < 1) fb_h = 1; - /* Classic VRAM texture pages. The area heuristic below treats 256×256 as - * "half of a 320-wide RGB888 FB" (480×240/2) and would skip staging them - * during FMV — TM4 post-intro loading text then samples an empty FBO. */ - if (w <= 256 && h <= 256) return 0; - /* Must target the CRTC scanout band (halfword coords). */ - { - int dx = (int)(di.display_x & 1023u); - int dy = (int)(di.display_y & 511u); - int x0 = x & (VRAM_W - 1), y0 = y & (VRAM_H - 1); - if (x0 + w <= dx || x0 >= dx + fb_w || y0 + h <= dy || y0 >= dy + fb_h) - return 0; - } - /* MotK: 768×128 class blits; allow slack. */ - if (h >= fb_h - 8 && h <= fb_h + 16 && w >= (fb_w * 3) / 4) return 1; - if ((int64_t)w * (int64_t)h >= ((int64_t)fb_w * fb_h) / 2) return 1; - return 0; -} - -/* Remember only the depth24 scanout band for clear-on-leave. Used after a - * full-VRAM savestate restage so texture pages stay in the FBO while the - * RGB888 movie band is still wiped when GP1 leaves 24-bit (avoids MotK - * rainbow/static from treating packed RGB as 1555). */ -static void depth24_mark_scanout_band(void) { - GpuDisplayInfo di; - int fb_w, fb_h, x0, y0, x1, y1; - if (!gpu_display_is_depth24()) return; - gpu_get_display_info(&di); - fb_w = (int)((di.width * 3u + 1u) / 2u); - fb_h = (int)di.height; - if (fb_w < 8) fb_w = 8; - if (fb_h < 1) fb_h = 1; - x0 = (int)(di.display_x & 1023u); - y0 = (int)(di.display_y & 511u); - x1 = x0 + fb_w - 1; - y1 = y0 + fb_h - 1; - if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; - if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; - rect_add(&s_d24_skip_fb, x0, y0, x1, y1); -} - -static void depth24_clear_skipped_fb(void) { - if (!s_raster_ok || !s_d24_skip_fb.set) return; - flush_flat_batch(); - flush_tex_batch(); - int x0 = s_d24_skip_fb.x0, y0 = s_d24_skip_fb.y0; - int x1 = s_d24_skip_fb.x1, y1 = s_d24_skip_fb.y1; - int rw = x1 - x0 + 1, rh = y1 - y0 + 1; - int S = s_scale; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); - glDisable(GL_BLEND); - glDisable(GL_STENCIL_TEST); - glEnable(GL_SCISSOR_TEST); - glViewport(0, 0, VRAM_W * S, VRAM_H * S); - glScissor(x0 * S, y0 * S, rw * S, rh * S); - glClearColor(0.f, 0.f, 0.f, 0.f); - glClearStencil(0); - glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - if (s_raw_fbo) { - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo); - glViewport(0, 0, VRAM_W, VRAM_H); - glScissor(x0, y0, rw, rh); - glClear(GL_COLOR_BUFFER_BIT); - } - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - rect_add(&s_pack_dirty, x0, y0, x1, y1); - present_dirty_rect(x0, y0, x1, y1, 1); - rect_clear(&s_d24_skip_fb); -} - + if (!gpu_display_is_depth24() || w <= 0 || h <= 0) return 0; + GpuDisplayInfo di; + gpu_get_display_info(&di); + int fb_w = (int)((di.width * 3u + 1u) / 2u); /* RGB W → halfwords */ + int fb_h = (int)di.height; + if (fb_w < 8) fb_w = 8; + if (fb_h < 1) fb_h = 1; + /* Classic VRAM texture pages. The area heuristic below treats 256×256 as + * "half of a 320-wide RGB888 FB" (480×240/2) and would skip staging them + * during FMV — TM4 post-intro loading text then samples an empty FBO. */ + if (w <= 256 && h <= 256) return 0; + /* Must target the CRTC scanout band (halfword coords). */ + { + int dx = (int)(di.display_x & 1023u); + int dy = (int)(di.display_y & 511u); + int x0 = x & (VRAM_W - 1), y0 = y & (VRAM_H - 1); + if (x0 + w <= dx || x0 >= dx + fb_w || y0 + h <= dy || y0 >= dy + fb_h) + return 0; + } + /* MotK: 768×128 class blits; allow slack. */ + if (h >= fb_h - 8 && h <= fb_h + 16 && w >= (fb_w * 3) / 4) return 1; + if ((int64_t)w * (int64_t)h >= ((int64_t)fb_w * fb_h) / 2) return 1; + return 0; +} + +/* Remember only the depth24 scanout band for clear-on-leave. Used after a + * full-VRAM savestate restage so texture pages stay in the FBO while the + * RGB888 movie band is still wiped when GP1 leaves 24-bit (avoids MotK + * rainbow/static from treating packed RGB as 1555). */ +static void depth24_mark_scanout_band(void) { + GpuDisplayInfo di; + int fb_w, fb_h, x0, y0, x1, y1; + if (!gpu_display_is_depth24()) return; + gpu_get_display_info(&di); + fb_w = (int)((di.width * 3u + 1u) / 2u); + fb_h = (int)di.height; + if (fb_w < 8) fb_w = 8; + if (fb_h < 1) fb_h = 1; + x0 = (int)(di.display_x & 1023u); + y0 = (int)(di.display_y & 511u); + x1 = x0 + fb_w - 1; + y1 = y0 + fb_h - 1; + if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; + if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; + rect_add(&s_d24_skip_fb, x0, y0, x1, y1); +} + +static void depth24_clear_skipped_fb(void) { + if (!s_raster_ok || !s_d24_skip_fb.set) return; + flush_flat_batch(); + flush_tex_batch(); + int x0 = s_d24_skip_fb.x0, y0 = s_d24_skip_fb.y0; + int x1 = s_d24_skip_fb.x1, y1 = s_d24_skip_fb.y1; + int rw = x1 - x0 + 1, rh = y1 - y0 + 1; + int S = s_scale; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); + glDisable(GL_BLEND); + glDisable(GL_STENCIL_TEST); + glEnable(GL_SCISSOR_TEST); + glViewport(0, 0, VRAM_W * S, VRAM_H * S); + glScissor(x0 * S, y0 * S, rw * S, rh * S); + glClearColor(0.f, 0.f, 0.f, 0.f); + glClearStencil(0); + glStencilMask(0xFF); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + if (s_raw_fbo) { + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo); + glViewport(0, 0, VRAM_W, VRAM_H); + glScissor(x0, y0, rw, rh); + glClear(GL_COLOR_BUFFER_BIT); + } + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + rect_add(&s_pack_dirty, x0, y0, x1, y1); + present_dirty_rect(x0, y0, x1, y1, 1); + rect_clear(&s_d24_skip_fb); +} + static void depth24_upload_policy(void) { int d24 = gpu_display_is_depth24(); if (d24 && !s_depth24_skip_up) { @@ -2556,531 +2556,531 @@ static void depth24_upload_policy(void) { depth24_clear_skipped_fb(); flush_cpu_upload(); gpu_depth24_upload_span_reset(); - /* Force a fresh present after FMV→15-bit so menus/loading screens - * are not held as a stale depth24 frame. */ - gl_renderer_invalidate_present(); - } - s_depth24_skip_up = d24; -} - -static void glb_vram_write(int x,int y,uint16_t px){ - sw_vram_write(x,y,px); - depth24_upload_policy(); - /* Point pokes are never MDEC frames — always stage to FBO. */ - up_add(x & (VRAM_W-1), y & (VRAM_H-1), x & (VRAM_W-1), y & (VRAM_H-1)); -} -static uint16_t glb_vram_read(int x,int y){ ensure_cpu(); return sw_vram_read(x,y); } -static void glb_vram_transfer_in(int x,int y,int w,int h,const uint16_t *d){ - sw_vram_transfer_in(x,y,w,h,d); - depth24_upload_policy(); - if (s_depth24_skip_up && depth24_is_fb_transfer(x, y, w, h)) { - /* Full-VRAM restore (boot_state): must stage into the FBO or every - * texture page outside the movie band is missing after FMV→menus. - * Only the scanout band is remembered for clear-on-leave — do not - * union every skipped rect (misclassified texture pages would be - * wiped from the FBO when leaving depth24). */ - if (w >= VRAM_W && h >= VRAM_H) { - up_add_transfer(x, y, w, h); - rect_clear(&s_d24_skip_fb); - depth24_mark_scanout_band(); - coh_record(GL_COH_UPLOAD, x, y, x + w - 1, y + h - 1); - return; - } - depth24_mark_scanout_band(); - coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1); - return; - } - up_add_transfer(x, y, w, h); /* exact touched rects, incl. per-pixel wrap */ - coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1); -} -static void glb_vram_transfer_out(int x,int y,int w,int h,uint16_t *d){ ensure_cpu(); sw_vram_transfer_out(x,y,w,h,d); } - -/* ---- context init / present -------------------------------------------- */ -static void upload_present_tex(const uint32_t *pixels, int w, int h, int linear) { - glBindTexture(GL_TEXTURE_2D, s_present_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); - /* Re-assert clamp every upload: a stale REPEAT wrap samples past the - * right edge into garbage (MotK FMV right-strip flicker). */ - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - if (w != s_present_w || h != s_present_h) { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels); - s_present_w = w; s_present_h = h; - } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_BGRA, GL_UNSIGNED_BYTE, pixels); - } + /* Force a fresh present after FMV→15-bit so menus/loading screens + * are not held as a stale depth24 frame. */ + gl_renderer_invalidate_present(); + } + s_depth24_skip_up = d24; +} + +static void glb_vram_write(int x,int y,uint16_t px){ + sw_vram_write(x,y,px); + depth24_upload_policy(); + /* Point pokes are never MDEC frames — always stage to FBO. */ + up_add(x & (VRAM_W-1), y & (VRAM_H-1), x & (VRAM_W-1), y & (VRAM_H-1)); +} +static uint16_t glb_vram_read(int x,int y){ ensure_cpu(); return sw_vram_read(x,y); } +static void glb_vram_transfer_in(int x,int y,int w,int h,const uint16_t *d){ + sw_vram_transfer_in(x,y,w,h,d); + depth24_upload_policy(); + if (s_depth24_skip_up && depth24_is_fb_transfer(x, y, w, h)) { + /* Full-VRAM restore (boot_state): must stage into the FBO or every + * texture page outside the movie band is missing after FMV→menus. + * Only the scanout band is remembered for clear-on-leave — do not + * union every skipped rect (misclassified texture pages would be + * wiped from the FBO when leaving depth24). */ + if (w >= VRAM_W && h >= VRAM_H) { + up_add_transfer(x, y, w, h); + rect_clear(&s_d24_skip_fb); + depth24_mark_scanout_band(); + coh_record(GL_COH_UPLOAD, x, y, x + w - 1, y + h - 1); + return; + } + depth24_mark_scanout_band(); + coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1); + return; + } + up_add_transfer(x, y, w, h); /* exact touched rects, incl. per-pixel wrap */ + coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1); +} +static void glb_vram_transfer_out(int x,int y,int w,int h,uint16_t *d){ ensure_cpu(); sw_vram_transfer_out(x,y,w,h,d); } + +/* ---- context init / present -------------------------------------------- */ +static void upload_present_tex(const uint32_t *pixels, int w, int h, int linear) { + glBindTexture(GL_TEXTURE_2D, s_present_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); + /* Re-assert clamp every upload: a stale REPEAT wrap samples past the + * right edge into garbage (MotK FMV right-strip flicker). */ + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + if (w != s_present_w || h != s_present_h) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels); + s_present_w = w; s_present_h = h; + } else { + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_BGRA, GL_UNSIGNED_BYTE, pixels); + } } -static int s_fmv_filter_cfg = 0; /* VIDEO_FMV_FILTER_NEAREST */ +static int s_fmv_filter_cfg = 0; /* VIDEO_FMV_FILTER_NEAREST */ + +void gl_renderer_set_fmv_filter(int cfg_value) { + if (cfg_value >= 0 && cfg_value <= 3) s_fmv_filter_cfg = cfg_value; +} + +static int fmv_filter_mode(void) { + return s_fmv_filter_cfg - 1; +} + +/* Select the present-program sampling mode. s_present_prog is shared by the CPU + * present and both VRAM/FBO quad paths, and program uniforms persist, so every + * user states its choice rather than inheriting the last one's. sharp=0 keeps + * the historical straight sample. */ +static void present_set_sharp(int mode, int tex_w, int tex_h, + int out_w, int out_h) { + if (s_present_uSharp < 0) return; + if (mode <= 0 || tex_w <= 0 || tex_h <= 0) { + p_glUniform1i(s_present_uSharp, 0); + return; + } + p_glUniform1i(s_present_uSharp, mode); + if (s_present_uTexSize >= 0) + p_glUniform2f(s_present_uTexSize, (float)tex_w, (float)tex_h); + if (s_present_uSharpScale >= 0) + p_glUniform2f(s_present_uSharpScale, + (float)out_w / (float)tex_w, + (float)out_h / (float)tex_h); +} + +/* Push scanline uniforms into the currently-bound present or interpolation + * program. + * pitch_lines = the height of the TEXTURE that v_uv is normalized against + * (v_uv.y * pitch_lines is the texel-row coordinate, and one + * source texel row is one PS1 scanline). For the VRAM/FBO quad + * that is VRAM_H (v_uv spans only the display sub-range of the + * 512-row texture); for the CPU/interp textures, which hold + * exactly the display rect, it equals the display height. + * disp_lines = PS1 display lines actually shown, for the output-scale gate. + * out_h = letterbox height in window px. + * Pass pitch_lines/disp_lines <= 0 (OSD, bezel, or the already-composed + * hold-last drawable) to force the effect off — program uniforms persist, so + * every content draw must state its own choice. */ +static void present_set_scanline(GLint uOn, GLint uStr, GLint uLines, + GLint uScale, int pitch_lines, int disp_lines, + int out_h) { + if (uOn < 0) return; + int on = (s_scanline_on && pitch_lines > 0 && disp_lines > 0 && out_h > 0) + ? 1 : 0; + p_glUniform1i(uOn, on); + if (!on) return; + if (uStr >= 0) p_glUniform1f(uStr, s_scanline_strength); + if (uLines >= 0) p_glUniform1f(uLines, (float)pitch_lines); + if (uScale >= 0) p_glUniform1f(uScale, (float)out_h / (float)disp_lines); +} +#define PRESENT_SCANLINE(pitch, disp, oh) \ + present_set_scanline(s_present_uScanline, s_present_uScanStrength, \ + s_present_uScanLines, s_present_uScanScale, \ + (pitch), (disp), (oh)) +#define INTERP_SCANLINE(pitch, disp, oh) \ + present_set_scanline(s_interp_uScanline, s_interp_uScanStrength, \ + s_interp_uScanLines, s_interp_uScanScale, \ + (pitch), (disp), (oh)) + +/* Display aspect for the present letterbox. Default 4:3 (native). When a wide + * aspect is configured the 4:3 frame is stretched into it — paired with the + * GTE X-squash (gte_set_display_aspect) this nets a wider field of view. */ +static int s_aspect_num = 4, s_aspect_den = 3; + +void gl_renderer_set_display_aspect(int num, int den) { + if (num <= 0 || den <= 0) { num = 4; den = 3; } + s_aspect_num = num; s_aspect_den = den; +} + +/* Scanline post-process toggle (host display setting). strength is the depth of + * the dark gap between lines, 0..1 (0 = off-looking, 1 = fully black gap). The + * actual darkening is applied per-draw in the present/interpolation shaders and + * fades in with output scale — see PSX_SCANLINE_FUNC. */ +void gl_renderer_set_scanlines(int on, float strength) { + s_scanline_on = on ? 1 : 0; + if (strength < 0.f) strength = 0.f; + if (strength > 1.f) strength = 1.f; + s_scanline_strength = strength; +} + +int gl_renderer_get_scanlines(float *strength) { + if (strength) *strength = s_scanline_strength; + return s_scanline_on; +} + +/* Letterbox: largest num:den rect centered in the drawable. */ +static void letterbox_rect_aspect(int ww, int wh, int num, int den, + int *x, int *y, int *w, int *h) { + int dw = ww, dh = (ww * den) / num; + if (dh > wh) { dh = wh; dw = (wh * num) / den; } + *x = (ww - dw) / 2; + *y = (wh - dh) / 2; + *w = dw; *h = dh; +} +static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h) { + letterbox_rect_aspect(ww, wh, s_aspect_num, s_aspect_den, x, y, w, h); +} + +static GLuint make_tex(GLenum internal, int w, int h, GLenum fmt, GLenum type) { + GLuint t = 0; + glGenTextures(1, &t); + glBindTexture(GL_TEXTURE_2D, t); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, (GLint)internal, w, h, 0, fmt, type, NULL); + return t; +} + +static int make_fbo(GLuint *out_fbo, GLuint color_tex, GLuint stencil_rb) { + p_glGenFramebuffers(1, out_fbo); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, *out_fbo); + p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_tex, 0); + if (stencil_rb) + p_glFramebufferRenderbuffer(PSXGL_FRAMEBUFFER, PSXGL_DEPTH_STENCIL_ATTACHMENT, + PSXGL_RENDERBUFFER, stencil_rb); + GLenum st = p_glCheckFramebufferStatus(PSXGL_FRAMEBUFFER); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + if (st != PSXGL_FRAMEBUFFER_COMPLETE) { + fprintf(stdout, "psxrecomp: GL FBO incomplete (0x%X)\n", st); + return 0; + } + return 1; +} + +static int init_gpu_raster(void) { + s_scale = s_req_scale; + + s_geo_prog = build_program(GEO_VS, GEO_FS); + s_tex_prog = build_program_ex(TEX_VS, TEX_FS, 1); + s_blit_prog = build_program(BLIT_VS, BLIT_FS); + s_pack_prog = build_program(PACK_VS, PACK_FS); + s_stencil_prog = build_program(PACK_VS, STENCIL_FS); + if (!s_geo_prog || !s_tex_prog || !s_blit_prog || !s_pack_prog || !s_stencil_prog) return 0; + + s_conv = (uint32_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint32_t)); + if (!s_conv) return 0; + + int hw = VRAM_W * s_scale, hh = VRAM_H * s_scale; + s_hr_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE); + s_scratch_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE); + s_up_tex = make_tex(GL_RGBA8, VRAM_W, VRAM_H, GL_RGBA, GL_UNSIGNED_BYTE); + s_raw_tex = make_tex(PSXGL_R16UI, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT); + /* Force the driver's first texture-upload allocation while the renderer is + * initializing. NVIDIA otherwise defers it until the first MDEC frame, + * producing a measured ~33 ms glTexSubImage hitch and an audible underrun. */ + { + const uint32_t zero_rgba = 0; + const uint16_t zero_raw = 0; + glBindTexture(GL_TEXTURE_2D, s_up_tex); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, + GL_RGBA, GL_UNSIGNED_BYTE, &zero_rgba); + glBindTexture(GL_TEXTURE_2D, s_raw_tex); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, + PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, &zero_raw); + glFinish(); + } + + p_glGenRenderbuffers(1, &s_hr_rb); + p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_hr_rb); + p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, hw, hh); + p_glBindRenderbuffer(PSXGL_RENDERBUFFER, 0); + + if (!make_fbo(&s_hr_fbo, s_hr_tex, s_hr_rb)) return 0; + if (!make_fbo(&s_raw_fbo, s_raw_tex, 0)) return 0; + if (!make_fbo(&s_scratch_fbo, s_scratch_tex, 0)) return 0; + + s_uVram = p_glGetUniformLocation(s_tex_prog, "u_vram"); + s_uTpage = p_glGetUniformLocation(s_tex_prog, "u_tpage"); + s_uClut = p_glGetUniformLocation(s_tex_prog, "u_clut"); + s_uDepth = p_glGetUniformLocation(s_tex_prog, "u_depth"); + s_uRaw = p_glGetUniformLocation(s_tex_prog, "u_raw"); + s_uSemipass = p_glGetUniformLocation(s_tex_prog, "u_semipass"); + s_uSemimode = p_glGetUniformLocation(s_tex_prog, "u_semimode"); + s_uTwin = p_glGetUniformLocation(s_tex_prog, "u_twin"); + s_uMaskset = p_glGetUniformLocation(s_tex_prog, "u_maskset"); + s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter"); + s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits"); + s_uBlitSrc = p_glGetUniformLocation(s_blit_prog, "u_src"); + s_uBlitPass = p_glGetUniformLocation(s_blit_prog, "u_stp_pass"); + s_uBlitMaskset = p_glGetUniformLocation(s_blit_prog, "u_maskset"); + s_uBlitSrcDiv = p_glGetUniformLocation(s_blit_prog, "u_src_div"); + s_uBlitSrcOff = p_glGetUniformLocation(s_blit_prog, "u_src_off"); + s_uPackHr = p_glGetUniformLocation(s_pack_prog, "u_hr"); + s_uPackScale = p_glGetUniformLocation(s_pack_prog, "u_scale"); + s_uStencilSrc = p_glGetUniformLocation(s_stencil_prog, "u_src"); + s_geo_uXoff = p_glGetUniformLocation(s_geo_prog, "u_xoff"); + s_geo_uXhalf = p_glGetUniformLocation(s_geo_prog, "u_xhalf"); + s_tex_uXoff = p_glGetUniformLocation(s_tex_prog, "u_xoff"); + s_tex_uXhalf = p_glGetUniformLocation(s_tex_prog, "u_xhalf"); + s_geo_uXscale = p_glGetUniformLocation(s_geo_prog, "u_xscale"); + s_geo_uXcenter = p_glGetUniformLocation(s_geo_prog, "u_xcenter"); + s_tex_uXscale = p_glGetUniformLocation(s_tex_prog, "u_xscale"); + s_tex_uXcenter = p_glGetUniformLocation(s_tex_prog, "u_xcenter"); + /* Default the new uniforms to the no-op (1.0 scale, 0 centre) -- GLSL would + * otherwise zero them, collapsing all x to 0. */ + p_glUseProgram(s_geo_prog); + p_glUniform1f(s_geo_uXscale, 1.0f); p_glUniform1f(s_geo_uXcenter, 0.0f); + p_glUseProgram(s_tex_prog); + p_glUniform1f(s_tex_uXscale, 1.0f); p_glUniform1f(s_tex_uXcenter, 0.0f); + + /* Sample-grid alignment shift: half an HR pixel, set once (S is fixed + * for the lifetime of the pipeline). Backed off by 1/64 native px so + * primitive edges never land EXACTLY on sample centers — that float tie + * dropped 1px columns at quad seams (e.g. the 256px texture-page seam in + * Tomba's title background). The 1/64 bias keeps floor(uv) on the exact + * PS1 texel for |uv slope| < 64; mirrored (negative-slope) mappings can + * be off by one texel at exact-integer uv — accepted. */ + { + float shift = 0.5f / (float)s_scale - 1.0f / 64.0f; + p_glUseProgram(s_geo_prog); + p_glUniform1f(p_glGetUniformLocation(s_geo_prog, "u_shift"), shift); + /* Native-wide projection defaults: x translation 0, clip half-extent + * 512 — so the GEO_VS x term reduces to (x+u_shift)/512-1, bit-identical + * to the pre-native-wide projection. The wide passes set these, then + * restore these defaults. */ + p_glUniform1f(s_geo_uXoff, 0.0f); + p_glUniform1f(s_geo_uXhalf, 512.0f); + p_glUseProgram(s_tex_prog); + p_glUniform1f(p_glGetUniformLocation(s_tex_prog, "u_shift"), shift); + p_glUniform1f(s_tex_uXoff, 0.0f); + p_glUniform1f(s_tex_uXhalf, 512.0f); + p_glUseProgram(s_blit_prog); + p_glUniform1f(p_glGetUniformLocation(s_blit_prog, "u_shift"), shift); + p_glUseProgram(0); + } + + p_glGenVertexArrays(1, &s_geo_vao); + p_glBindVertexArray(s_geo_vao); + p_glGenBuffers(1, &s_geo_vbo); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); + p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0); + p_glEnableVertexAttribArray(0); + p_glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(2 * sizeof(float))); + p_glEnableVertexAttribArray(1); -void gl_renderer_set_fmv_filter(int cfg_value) { - if (cfg_value >= 0 && cfg_value <= 3) s_fmv_filter_cfg = cfg_value; -} + p_glGenVertexArrays(1, &s_tex_vao); + p_glBindVertexArray(s_tex_vao); + p_glGenBuffers(1, &s_tex_vbo); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); + { + GLsizei st = TEXV * sizeof(float); + p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, st, (void*)0); p_glEnableVertexAttribArray(0); /* pos */ + p_glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, st, (void*)(2*sizeof(float))); p_glEnableVertexAttribArray(1); /* uv */ + p_glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, st, (void*)(4*sizeof(float))); p_glEnableVertexAttribArray(2); /* col */ + p_glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, st, (void*)(8*sizeof(float))); p_glEnableVertexAttribArray(3); /* tpage */ + p_glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, st, (void*)(10*sizeof(float))); p_glEnableVertexAttribArray(4); /* clut */ + p_glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, st, (void*)(12*sizeof(float))); p_glEnableVertexAttribArray(5); /* depth */ + p_glVertexAttribPointer(6, 1, GL_FLOAT, GL_FALSE, st, (void*)(13*sizeof(float))); p_glEnableVertexAttribArray(6); /* raw */ + p_glVertexAttribPointer(7, 4, GL_FLOAT, GL_FALSE, st, (void*)(14*sizeof(float))); p_glEnableVertexAttribArray(7); /* limits */ + p_glVertexAttribPointer(8, 1, GL_FLOAT, GL_FALSE, st, (void*)(18*sizeof(float))); p_glEnableVertexAttribArray(8); /* semi */ + p_glVertexAttribPointer(9, 1, GL_FLOAT, GL_FALSE, st, (void*)(19*sizeof(float))); p_glEnableVertexAttribArray(9); /* q */ + } -static int fmv_filter_mode(void) { - return s_fmv_filter_cfg - 1; + p_glGenVertexArrays(1, &s_blit_vao); + p_glBindVertexArray(s_blit_vao); + p_glGenBuffers(1, &s_blit_vbo); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); + p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); + p_glEnableVertexAttribArray(0); + + p_glGenVertexArrays(1, &s_empty_vao); + p_glBindVertexArray(0); + + /* Clear the authoritative surface (color + stencil) and queue a full + * upload of whatever the CPU VRAM already holds (pre-context software + * draws, BIOS logo state, ...). */ + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); + glDisable(GL_SCISSOR_TEST); + glClearColor(0, 0, 0, 0); + glClearStencil(0); + glStencilMask(0xFF); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + rect_clear(&s_pack_dirty); + s_up_nrects = 0; + up_add(0, 0, VRAM_W - 1, VRAM_H - 1); + s_gpu_dirty = 0; + s_stencil_valid = 1; + for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull; + s_last_present_path = -1; + + /* Native-wide compositor surfaces start unallocated (lazily created when a + * widescreen game calls wide_configure + wide_set_target). */ + for (int i = 0; i < WIDE_MAX_SURF; i++) { + s_wide_tex[i] = 0; s_wide_fbo[i] = 0; s_wide_base[i] = -1; + } + g_wide_w = 0; g_wide_off = 0; g_wide_cur = 0; g_wide_cur_base = 0; + + s_raster_ok = 1; + gl_perf_init(); /* frame_perf GPU/CPU phase timing (no-op if queries absent) */ + if (s_cpu_auth_dual) { + fprintf(stdout, "psxrecomp: GL GPU pipeline ready (dual-raster, " + "internal scale %dx, SW@1x authority, no FBO readback)\n", + s_scale); + } else { + fprintf(stdout, "psxrecomp: GL GPU pipeline ready (internal scale %dx, " + "mask-bit stencil, texture window, GPU copy/upload)\n", s_scale); + } + return 1; } -/* Select the present-program sampling mode. s_present_prog is shared by the CPU - * present and both VRAM/FBO quad paths, and program uniforms persist, so every - * user states its choice rather than inheriting the last one's. sharp=0 keeps - * the historical straight sample. */ -static void present_set_sharp(int mode, int tex_w, int tex_h, - int out_w, int out_h) { - if (s_present_uSharp < 0) return; - if (mode <= 0 || tex_w <= 0 || tex_h <= 0) { - p_glUniform1i(s_present_uSharp, 0); - return; +int gl_renderer_init_context(SDL_Window *win) { + s_win = win; + s_present_w = 0; + s_present_h = 0; + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + s_ctx = SDL_GL_CreateContext(win); + if (!s_ctx) { fprintf(stdout, "psxrecomp: GL context creation failed (%s)\n", SDL_GetError()); return 0; } + if (SDL_GL_MakeCurrent(win, s_ctx) != 0) { fprintf(stdout, "psxrecomp: MakeCurrent failed (%s)\n", SDL_GetError()); SDL_GL_DeleteContext(s_ctx); s_ctx=NULL; return 0; } + /* Swap interval: 1=vsync (tear-free, default), 0=immediate (lowest display + * latency, may tear; our wall-clock pacer still holds 59.94Hz), -1=adaptive. + * Adaptive falls back to vsync if the driver rejects it. */ + if (SDL_GL_SetSwapInterval(s_swap_interval) != 0 && s_swap_interval < 0) { + SDL_GL_SetSwapInterval(1); + s_swap_interval = 1; } - p_glUniform1i(s_present_uSharp, mode); - if (s_present_uTexSize >= 0) - p_glUniform2f(s_present_uTexSize, (float)tex_w, (float)tex_h); - if (s_present_uSharpScale >= 0) - p_glUniform2f(s_present_uSharpScale, - (float)out_w / (float)tex_w, - (float)out_h / (float)tex_h); -} - -/* Push scanline uniforms into the currently-bound present or interpolation - * program. - * pitch_lines = the height of the TEXTURE that v_uv is normalized against - * (v_uv.y * pitch_lines is the texel-row coordinate, and one - * source texel row is one PS1 scanline). For the VRAM/FBO quad - * that is VRAM_H (v_uv spans only the display sub-range of the - * 512-row texture); for the CPU/interp textures, which hold - * exactly the display rect, it equals the display height. - * disp_lines = PS1 display lines actually shown, for the output-scale gate. - * out_h = letterbox height in window px. - * Pass pitch_lines/disp_lines <= 0 (OSD, bezel, or the already-composed - * hold-last drawable) to force the effect off — program uniforms persist, so - * every content draw must state its own choice. */ -static void present_set_scanline(GLint uOn, GLint uStr, GLint uLines, - GLint uScale, int pitch_lines, int disp_lines, - int out_h) { - if (uOn < 0) return; - int on = (s_scanline_on && pitch_lines > 0 && disp_lines > 0 && out_h > 0) - ? 1 : 0; - p_glUniform1i(uOn, on); - if (!on) return; - if (uStr >= 0) p_glUniform1f(uStr, s_scanline_strength); - if (uLines >= 0) p_glUniform1f(uLines, (float)pitch_lines); - if (uScale >= 0) p_glUniform1f(uScale, (float)out_h / (float)disp_lines); -} -#define PRESENT_SCANLINE(pitch, disp, oh) \ - present_set_scanline(s_present_uScanline, s_present_uScanStrength, \ - s_present_uScanLines, s_present_uScanScale, \ - (pitch), (disp), (oh)) -#define INTERP_SCANLINE(pitch, disp, oh) \ - present_set_scanline(s_interp_uScanline, s_interp_uScanStrength, \ - s_interp_uScanLines, s_interp_uScanScale, \ - (pitch), (disp), (oh)) - -/* Display aspect for the present letterbox. Default 4:3 (native). When a wide - * aspect is configured the 4:3 frame is stretched into it — paired with the - * GTE X-squash (gte_set_display_aspect) this nets a wider field of view. */ -static int s_aspect_num = 4, s_aspect_den = 3; - -void gl_renderer_set_display_aspect(int num, int den) { - if (num <= 0 || den <= 0) { num = 4; den = 3; } - s_aspect_num = num; s_aspect_den = den; -} - -/* Scanline post-process toggle (host display setting). strength is the depth of - * the dark gap between lines, 0..1 (0 = off-looking, 1 = fully black gap). The - * actual darkening is applied per-draw in the present/interpolation shaders and - * fades in with output scale — see PSX_SCANLINE_FUNC. */ -void gl_renderer_set_scanlines(int on, float strength) { - s_scanline_on = on ? 1 : 0; - if (strength < 0.f) strength = 0.f; - if (strength > 1.f) strength = 1.f; - s_scanline_strength = strength; -} - -int gl_renderer_get_scanlines(float *strength) { - if (strength) *strength = s_scanline_strength; - return s_scanline_on; -} - -/* Letterbox: largest num:den rect centered in the drawable. */ -static void letterbox_rect_aspect(int ww, int wh, int num, int den, - int *x, int *y, int *w, int *h) { - int dw = ww, dh = (ww * den) / num; - if (dh > wh) { dh = wh; dw = (wh * num) / den; } - *x = (ww - dw) / 2; - *y = (wh - dh) / 2; - *w = dw; *h = dh; -} -static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h) { - letterbox_rect_aspect(ww, wh, s_aspect_num, s_aspect_den, x, y, w, h); -} - -static GLuint make_tex(GLenum internal, int w, int h, GLenum fmt, GLenum type) { - GLuint t = 0; - glGenTextures(1, &t); - glBindTexture(GL_TEXTURE_2D, t); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, (GLint)internal, w, h, 0, fmt, type, NULL); - return t; -} - -static int make_fbo(GLuint *out_fbo, GLuint color_tex, GLuint stencil_rb) { - p_glGenFramebuffers(1, out_fbo); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, *out_fbo); - p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_tex, 0); - if (stencil_rb) - p_glFramebufferRenderbuffer(PSXGL_FRAMEBUFFER, PSXGL_DEPTH_STENCIL_ATTACHMENT, - PSXGL_RENDERBUFFER, stencil_rb); - GLenum st = p_glCheckFramebufferStatus(PSXGL_FRAMEBUFFER); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - if (st != PSXGL_FRAMEBUFFER_COMPLETE) { - fprintf(stdout, "psxrecomp: GL FBO incomplete (0x%X)\n", st); - return 0; - } - return 1; -} - -static int init_gpu_raster(void) { - s_scale = s_req_scale; - - s_geo_prog = build_program(GEO_VS, GEO_FS); - s_tex_prog = build_program_ex(TEX_VS, TEX_FS, 1); - s_blit_prog = build_program(BLIT_VS, BLIT_FS); - s_pack_prog = build_program(PACK_VS, PACK_FS); - s_stencil_prog = build_program(PACK_VS, STENCIL_FS); - if (!s_geo_prog || !s_tex_prog || !s_blit_prog || !s_pack_prog || !s_stencil_prog) return 0; - - s_conv = (uint32_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint32_t)); - if (!s_conv) return 0; - - int hw = VRAM_W * s_scale, hh = VRAM_H * s_scale; - s_hr_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE); - s_scratch_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE); - s_up_tex = make_tex(GL_RGBA8, VRAM_W, VRAM_H, GL_RGBA, GL_UNSIGNED_BYTE); - s_raw_tex = make_tex(PSXGL_R16UI, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT); - /* Force the driver's first texture-upload allocation while the renderer is - * initializing. NVIDIA otherwise defers it until the first MDEC frame, - * producing a measured ~33 ms glTexSubImage hitch and an audible underrun. */ - { - const uint32_t zero_rgba = 0; - const uint16_t zero_raw = 0; - glBindTexture(GL_TEXTURE_2D, s_up_tex); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, - GL_RGBA, GL_UNSIGNED_BYTE, &zero_rgba); - glBindTexture(GL_TEXTURE_2D, s_raw_tex); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, - PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, &zero_raw); - glFinish(); - } - - p_glGenRenderbuffers(1, &s_hr_rb); - p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_hr_rb); - p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, hw, hh); - p_glBindRenderbuffer(PSXGL_RENDERBUFFER, 0); - - if (!make_fbo(&s_hr_fbo, s_hr_tex, s_hr_rb)) return 0; - if (!make_fbo(&s_raw_fbo, s_raw_tex, 0)) return 0; - if (!make_fbo(&s_scratch_fbo, s_scratch_tex, 0)) return 0; - - s_uVram = p_glGetUniformLocation(s_tex_prog, "u_vram"); - s_uTpage = p_glGetUniformLocation(s_tex_prog, "u_tpage"); - s_uClut = p_glGetUniformLocation(s_tex_prog, "u_clut"); - s_uDepth = p_glGetUniformLocation(s_tex_prog, "u_depth"); - s_uRaw = p_glGetUniformLocation(s_tex_prog, "u_raw"); - s_uSemipass = p_glGetUniformLocation(s_tex_prog, "u_semipass"); - s_uSemimode = p_glGetUniformLocation(s_tex_prog, "u_semimode"); - s_uTwin = p_glGetUniformLocation(s_tex_prog, "u_twin"); - s_uMaskset = p_glGetUniformLocation(s_tex_prog, "u_maskset"); - s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter"); - s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits"); - s_uBlitSrc = p_glGetUniformLocation(s_blit_prog, "u_src"); - s_uBlitPass = p_glGetUniformLocation(s_blit_prog, "u_stp_pass"); - s_uBlitMaskset = p_glGetUniformLocation(s_blit_prog, "u_maskset"); - s_uBlitSrcDiv = p_glGetUniformLocation(s_blit_prog, "u_src_div"); - s_uBlitSrcOff = p_glGetUniformLocation(s_blit_prog, "u_src_off"); - s_uPackHr = p_glGetUniformLocation(s_pack_prog, "u_hr"); - s_uPackScale = p_glGetUniformLocation(s_pack_prog, "u_scale"); - s_uStencilSrc = p_glGetUniformLocation(s_stencil_prog, "u_src"); - s_geo_uXoff = p_glGetUniformLocation(s_geo_prog, "u_xoff"); - s_geo_uXhalf = p_glGetUniformLocation(s_geo_prog, "u_xhalf"); - s_tex_uXoff = p_glGetUniformLocation(s_tex_prog, "u_xoff"); - s_tex_uXhalf = p_glGetUniformLocation(s_tex_prog, "u_xhalf"); - s_geo_uXscale = p_glGetUniformLocation(s_geo_prog, "u_xscale"); - s_geo_uXcenter = p_glGetUniformLocation(s_geo_prog, "u_xcenter"); - s_tex_uXscale = p_glGetUniformLocation(s_tex_prog, "u_xscale"); - s_tex_uXcenter = p_glGetUniformLocation(s_tex_prog, "u_xcenter"); - /* Default the new uniforms to the no-op (1.0 scale, 0 centre) -- GLSL would - * otherwise zero them, collapsing all x to 0. */ - p_glUseProgram(s_geo_prog); - p_glUniform1f(s_geo_uXscale, 1.0f); p_glUniform1f(s_geo_uXcenter, 0.0f); - p_glUseProgram(s_tex_prog); - p_glUniform1f(s_tex_uXscale, 1.0f); p_glUniform1f(s_tex_uXcenter, 0.0f); - - /* Sample-grid alignment shift: half an HR pixel, set once (S is fixed - * for the lifetime of the pipeline). Backed off by 1/64 native px so - * primitive edges never land EXACTLY on sample centers — that float tie - * dropped 1px columns at quad seams (e.g. the 256px texture-page seam in - * Tomba's title background). The 1/64 bias keeps floor(uv) on the exact - * PS1 texel for |uv slope| < 64; mirrored (negative-slope) mappings can - * be off by one texel at exact-integer uv — accepted. */ - { - float shift = 0.5f / (float)s_scale - 1.0f / 64.0f; - p_glUseProgram(s_geo_prog); - p_glUniform1f(p_glGetUniformLocation(s_geo_prog, "u_shift"), shift); - /* Native-wide projection defaults: x translation 0, clip half-extent - * 512 — so the GEO_VS x term reduces to (x+u_shift)/512-1, bit-identical - * to the pre-native-wide projection. The wide passes set these, then - * restore these defaults. */ - p_glUniform1f(s_geo_uXoff, 0.0f); - p_glUniform1f(s_geo_uXhalf, 512.0f); - p_glUseProgram(s_tex_prog); - p_glUniform1f(p_glGetUniformLocation(s_tex_prog, "u_shift"), shift); - p_glUniform1f(s_tex_uXoff, 0.0f); - p_glUniform1f(s_tex_uXhalf, 512.0f); - p_glUseProgram(s_blit_prog); - p_glUniform1f(p_glGetUniformLocation(s_blit_prog, "u_shift"), shift); - p_glUseProgram(0); - } - - p_glGenVertexArrays(1, &s_geo_vao); - p_glBindVertexArray(s_geo_vao); - p_glGenBuffers(1, &s_geo_vbo); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); - p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0); - p_glEnableVertexAttribArray(0); - p_glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(2 * sizeof(float))); - p_glEnableVertexAttribArray(1); - - p_glGenVertexArrays(1, &s_tex_vao); - p_glBindVertexArray(s_tex_vao); - p_glGenBuffers(1, &s_tex_vbo); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); - { - GLsizei st = TEXV * sizeof(float); - p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, st, (void*)0); p_glEnableVertexAttribArray(0); /* pos */ - p_glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, st, (void*)(2*sizeof(float))); p_glEnableVertexAttribArray(1); /* uv */ - p_glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, st, (void*)(4*sizeof(float))); p_glEnableVertexAttribArray(2); /* col */ - p_glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, st, (void*)(8*sizeof(float))); p_glEnableVertexAttribArray(3); /* tpage */ - p_glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, st, (void*)(10*sizeof(float))); p_glEnableVertexAttribArray(4); /* clut */ - p_glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, st, (void*)(12*sizeof(float))); p_glEnableVertexAttribArray(5); /* depth */ - p_glVertexAttribPointer(6, 1, GL_FLOAT, GL_FALSE, st, (void*)(13*sizeof(float))); p_glEnableVertexAttribArray(6); /* raw */ - p_glVertexAttribPointer(7, 4, GL_FLOAT, GL_FALSE, st, (void*)(14*sizeof(float))); p_glEnableVertexAttribArray(7); /* limits */ - p_glVertexAttribPointer(8, 1, GL_FLOAT, GL_FALSE, st, (void*)(18*sizeof(float))); p_glEnableVertexAttribArray(8); /* semi */ - p_glVertexAttribPointer(9, 1, GL_FLOAT, GL_FALSE, st, (void*)(19*sizeof(float))); p_glEnableVertexAttribArray(9); /* q */ - } - - p_glGenVertexArrays(1, &s_blit_vao); - p_glBindVertexArray(s_blit_vao); - p_glGenBuffers(1, &s_blit_vbo); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); - p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); - p_glEnableVertexAttribArray(0); - - p_glGenVertexArrays(1, &s_empty_vao); - p_glBindVertexArray(0); - - /* Clear the authoritative surface (color + stencil) and queue a full - * upload of whatever the CPU VRAM already holds (pre-context software - * draws, BIOS logo state, ...). */ - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); - glDisable(GL_SCISSOR_TEST); - glClearColor(0, 0, 0, 0); - glClearStencil(0); - glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - rect_clear(&s_pack_dirty); - s_up_nrects = 0; - up_add(0, 0, VRAM_W - 1, VRAM_H - 1); - s_gpu_dirty = 0; - s_stencil_valid = 1; - for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull; - s_last_present_path = -1; - - /* Native-wide compositor surfaces start unallocated (lazily created when a - * widescreen game calls wide_configure + wide_set_target). */ - for (int i = 0; i < WIDE_MAX_SURF; i++) { - s_wide_tex[i] = 0; s_wide_fbo[i] = 0; s_wide_base[i] = -1; - } - g_wide_w = 0; g_wide_off = 0; g_wide_cur = 0; g_wide_cur_base = 0; - - s_raster_ok = 1; - gl_perf_init(); /* frame_perf GPU/CPU phase timing (no-op if queries absent) */ - if (s_cpu_auth_dual) { - fprintf(stdout, "psxrecomp: GL GPU pipeline ready (dual-raster, " - "internal scale %dx, SW@1x authority, no FBO readback)\n", - s_scale); - } else { - fprintf(stdout, "psxrecomp: GL GPU pipeline ready (internal scale %dx, " - "mask-bit stencil, texture window, GPU copy/upload)\n", s_scale); - } - return 1; -} - -int gl_renderer_init_context(SDL_Window *win) { - s_win = win; - s_present_w = 0; - s_present_h = 0; - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); - s_ctx = SDL_GL_CreateContext(win); - if (!s_ctx) { fprintf(stdout, "psxrecomp: GL context creation failed (%s)\n", SDL_GetError()); return 0; } - if (SDL_GL_MakeCurrent(win, s_ctx) != 0) { fprintf(stdout, "psxrecomp: MakeCurrent failed (%s)\n", SDL_GetError()); SDL_GL_DeleteContext(s_ctx); s_ctx=NULL; return 0; } - /* Swap interval: 1=vsync (tear-free, default), 0=immediate (lowest display - * latency, may tear; our wall-clock pacer still holds 59.94Hz), -1=adaptive. - * Adaptive falls back to vsync if the driver rejects it. */ - if (SDL_GL_SetSwapInterval(s_swap_interval) != 0 && s_swap_interval < 0) { - SDL_GL_SetSwapInterval(1); - s_swap_interval = 1; - } - glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); - const char *ver = (const char *)glGetString(GL_VERSION); - fprintf(stdout, "psxrecomp: OpenGL context created (%s)\n", ver ? ver : "?"); - - /* All-or-nothing: any missing entry point / failed shader / bad FBO means - * the whole GL renderer is unavailable and the runtime stays on the pure - * software path — no half-GL hybrid (that mixed mode is what produced - * the alternating-present menu jitter). */ - int ok = load_modern_gl(); - if (ok) { - glGenTextures(1, &s_present_tex); - glBindTexture(GL_TEXTURE_2D, s_present_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - s_present_prog = build_program(PRESENT_VS, PRESENT_FS); - s_interp_prog = build_program(PRESENT_VS, INTERP_FS); - if (s_present_prog && s_interp_prog) { - p_glGenVertexArrays(1, &s_present_vao); - s_present_uTex = p_glGetUniformLocation(s_present_prog, "u_tex"); - s_present_uUvRect = p_glGetUniformLocation(s_present_prog, "u_uv_rect"); + glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); + const char *ver = (const char *)glGetString(GL_VERSION); + fprintf(stdout, "psxrecomp: OpenGL context created (%s)\n", ver ? ver : "?"); + + /* All-or-nothing: any missing entry point / failed shader / bad FBO means + * the whole GL renderer is unavailable and the runtime stays on the pure + * software path — no half-GL hybrid (that mixed mode is what produced + * the alternating-present menu jitter). */ + int ok = load_modern_gl(); + if (ok) { + glGenTextures(1, &s_present_tex); + glBindTexture(GL_TEXTURE_2D, s_present_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + s_present_prog = build_program(PRESENT_VS, PRESENT_FS); + s_interp_prog = build_program(PRESENT_VS, INTERP_FS); + if (s_present_prog && s_interp_prog) { + p_glGenVertexArrays(1, &s_present_vao); + s_present_uTex = p_glGetUniformLocation(s_present_prog, "u_tex"); + s_present_uUvRect = p_glGetUniformLocation(s_present_prog, "u_uv_rect"); s_present_uTexSize = p_glGetUniformLocation(s_present_prog, "u_tex_size"); s_present_uSharpScale = p_glGetUniformLocation(s_present_prog, "u_sharp_scale"); s_present_uSharp = p_glGetUniformLocation(s_present_prog, "u_sharp"); - s_present_uScanline = - p_glGetUniformLocation(s_present_prog, "u_scanline"); - s_present_uScanStrength = - p_glGetUniformLocation(s_present_prog, "u_scanline_strength"); - s_present_uScanLines = - p_glGetUniformLocation(s_present_prog, "u_scanline_lines"); - s_present_uScanScale = - p_glGetUniformLocation(s_present_prog, "u_scanline_scale"); - s_interp_uScanline = - p_glGetUniformLocation(s_interp_prog, "u_scanline"); - s_interp_uScanStrength = - p_glGetUniformLocation(s_interp_prog, "u_scanline_strength"); - s_interp_uScanLines = - p_glGetUniformLocation(s_interp_prog, "u_scanline_lines"); - s_interp_uScanScale = - p_glGetUniformLocation(s_interp_prog, "u_scanline_scale"); - s_interp_uPrev = p_glGetUniformLocation(s_interp_prog, "u_prev"); - s_interp_uCurr = p_glGetUniformLocation(s_interp_prog, "u_curr"); - s_interp_uAlpha = p_glGetUniformLocation(s_interp_prog, "u_alpha"); - s_interp_uUvRect = p_glGetUniformLocation(s_interp_prog, "u_uv_rect"); - s_interp_uBlendMode = - p_glGetUniformLocation(s_interp_prog, "u_blend_mode"); - glGenTextures(3, s_interp_tex); - for (int i = 0; i < 3; i++) { - glBindTexture(GL_TEXTURE_2D, s_interp_tex[i]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } - } else ok = 0; - } - if (ok) ok = init_gpu_raster(); - if (!ok) { - fprintf(stdout, "psxrecomp: GL pipeline init failed — falling back to software renderer\n"); - SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; - s_raster_ok = 0; - return 0; - } - return 1; -} - -/* Set the GL swap interval (vsync mode): 1=vsync, 0=immediate, -1=adaptive. - * Safe to call before or after context creation; applies live when a context - * exists. Adaptive falls back to vsync if unsupported. */ -void gl_renderer_set_swap_interval(int interval) { - s_swap_interval = interval; - if (s_ctx) { - if (SDL_GL_SetSwapInterval(interval) != 0 && interval < 0) { - SDL_GL_SetSwapInterval(1); - s_swap_interval = 1; - } - } -} - + s_present_uScanline = + p_glGetUniformLocation(s_present_prog, "u_scanline"); + s_present_uScanStrength = + p_glGetUniformLocation(s_present_prog, "u_scanline_strength"); + s_present_uScanLines = + p_glGetUniformLocation(s_present_prog, "u_scanline_lines"); + s_present_uScanScale = + p_glGetUniformLocation(s_present_prog, "u_scanline_scale"); + s_interp_uScanline = + p_glGetUniformLocation(s_interp_prog, "u_scanline"); + s_interp_uScanStrength = + p_glGetUniformLocation(s_interp_prog, "u_scanline_strength"); + s_interp_uScanLines = + p_glGetUniformLocation(s_interp_prog, "u_scanline_lines"); + s_interp_uScanScale = + p_glGetUniformLocation(s_interp_prog, "u_scanline_scale"); + s_interp_uPrev = p_glGetUniformLocation(s_interp_prog, "u_prev"); + s_interp_uCurr = p_glGetUniformLocation(s_interp_prog, "u_curr"); + s_interp_uAlpha = p_glGetUniformLocation(s_interp_prog, "u_alpha"); + s_interp_uUvRect = p_glGetUniformLocation(s_interp_prog, "u_uv_rect"); + s_interp_uBlendMode = + p_glGetUniformLocation(s_interp_prog, "u_blend_mode"); + glGenTextures(3, s_interp_tex); + for (int i = 0; i < 3; i++) { + glBindTexture(GL_TEXTURE_2D, s_interp_tex[i]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + } else ok = 0; + } + if (ok) ok = init_gpu_raster(); + if (!ok) { + fprintf(stdout, "psxrecomp: GL pipeline init failed — falling back to software renderer\n"); + SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; + s_raster_ok = 0; + return 0; + } + return 1; +} + +/* Set the GL swap interval (vsync mode): 1=vsync, 0=immediate, -1=adaptive. + * Safe to call before or after context creation; applies live when a context + * exists. Adaptive falls back to vsync if unsupported. */ +void gl_renderer_set_swap_interval(int interval) { + s_swap_interval = interval; + if (s_ctx) { + if (SDL_GL_SetSwapInterval(interval) != 0 && interval < 0) { + SDL_GL_SetSwapInterval(1); + s_swap_interval = 1; + } + } +} + void gl_renderer_shutdown(void) { if (s_ctx) { ensure_cpu(); SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; } - free(s_conv); s_conv = NULL; - s_raster_ok = 0; - /* New context regenerates s_present_tex empty; a stale size makes - * upload_present_tex take glTexSubImage2D into an unallocated texture - * (rematch 24-bit FMV → black picture, audio still runs). */ - s_present_w = 0; - s_present_h = 0; - s_osd_tex = 0; - s_osd_tw = 0; - s_osd_th = 0; - s_depth24_skip_up = 0; - rect_clear(&s_d24_skip_fb); - hold_invalidate(); - s_hold_tex = 0; - s_hold_fbo = 0; - s_hold_tw = 0; - s_hold_th = 0; -} - -/* CPU-readout present (24-bit FMV frames and the PSX_GL_FORCE_CPU_PRESENT - * diagnostic): full-window clear, then a quad into the letterbox rect. - * force_4_3 pins the rect to native 4:3 regardless of the display aspect — - * FMVs are authored 4:3 and have no GTE squash to compensate a stretch, so - * widescreen presents them pillarboxed instead of distorted. */ -void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear, - int force_4_3, int content_w) { - if (!s_ctx) return; - interp_reset_history(); - int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); - glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); - int lx, ly, lw, lh; - if (force_4_3) - letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); - else - letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); - /* Short GP1(07h) bands (MotK FMV is 128 lines) only fill a fraction of - * NTSC active height on hardware. Stretching them to the full letterbox - * doubles vertical scale vs horizontal and makes the frame look too wide - * with the right edge clipped. Letterbox within the present rect instead. - * Apply whenever the source is short — not only when force_4_3 — so a - * misclassified FMV frame still keeps correct pixel aspect. */ - /* Genuinely windowed video bands only (<80% of the 240-line field, e.g. - * MotK's 128-line FMV). A game's native short display mode (216/224) - * fills the rect as on hardware. */ - if (src_h > 0 && src_h < 192) { - int content_h = (lh * src_h) / 240; - if (content_h < 1) content_h = 1; - ly += (lh - content_h) / 2; - lh = content_h; - } - /* Optional trailing-column crop (depth24 margin): shrink the draw width - * left-aligned so cleared black remains on the right — never stretch. */ - float uv_x1 = 1.f; - int crop = (content_w > 0 && content_w < src_w && src_w > 0); - if (crop) { - uv_x1 = (float)content_w / (float)src_w; - lw = (lw * content_w) / src_w; - if (lw < 1) lw = 1; - } + free(s_conv); s_conv = NULL; + s_raster_ok = 0; + /* New context regenerates s_present_tex empty; a stale size makes + * upload_present_tex take glTexSubImage2D into an unallocated texture + * (rematch 24-bit FMV → black picture, audio still runs). */ + s_present_w = 0; + s_present_h = 0; + s_osd_tex = 0; + s_osd_tw = 0; + s_osd_th = 0; + s_depth24_skip_up = 0; + rect_clear(&s_d24_skip_fb); + hold_invalidate(); + s_hold_tex = 0; + s_hold_fbo = 0; + s_hold_tw = 0; + s_hold_th = 0; +} + +/* CPU-readout present (24-bit FMV frames and the PSX_GL_FORCE_CPU_PRESENT + * diagnostic): full-window clear, then a quad into the letterbox rect. + * force_4_3 pins the rect to native 4:3 regardless of the display aspect — + * FMVs are authored 4:3 and have no GTE squash to compensate a stretch, so + * widescreen presents them pillarboxed instead of distorted. */ +void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear, + int force_4_3, int content_w) { + if (!s_ctx) return; + interp_reset_history(); + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); + glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); + int lx, ly, lw, lh; + if (force_4_3) + letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); + else + letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); + /* Short GP1(07h) bands (MotK FMV is 128 lines) only fill a fraction of + * NTSC active height on hardware. Stretching them to the full letterbox + * doubles vertical scale vs horizontal and makes the frame look too wide + * with the right edge clipped. Letterbox within the present rect instead. + * Apply whenever the source is short — not only when force_4_3 — so a + * misclassified FMV frame still keeps correct pixel aspect. */ + /* Genuinely windowed video bands only (<80% of the 240-line field, e.g. + * MotK's 128-line FMV). A game's native short display mode (216/224) + * fills the rect as on hardware. */ + if (src_h > 0 && src_h < 192) { + int content_h = (lh * src_h) / 240; + if (content_h < 1) content_h = 1; + ly += (lh - content_h) / 2; + lh = content_h; + } + /* Optional trailing-column crop (depth24 margin): shrink the draw width + * left-aligned so cleared black remains on the right — never stretch. */ + float uv_x1 = 1.f; + int crop = (content_w > 0 && content_w < src_w && src_w > 0); + if (crop) { + uv_x1 = (float)content_w / (float)src_w; + lw = (lw * content_w) / src_w; + if (lw < 1) lw = 1; + } /* This is the low-res source path (24-bit FMV, and the forced-CPU present * diagnostic): a 320x192-class image blown up to fill the window, so how it * is reconstructed is very visible. `linear` (the video AA setting) allows @@ -3101,720 +3101,720 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea int filt_mode = linear ? fmv_filter_mode() : -1; /* AA off: nearest, no shader work */ glViewport(lx, ly, lw, lh); - p_glActiveTexture(PSXGL_TEXTURE0); + p_glActiveTexture(PSXGL_TEXTURE0); upload_present_tex(pixels, src_w, src_h, filt_mode >= 0 ? 1 : 0); - p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); + p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); present_set_sharp(filt_mode, src_w, src_h, lw, lh); - /* CPU present texture holds exactly the display rect, so v_uv spans it and - * pitch == display height == src_h. */ - PRESENT_SCANLINE(src_h, src_h, lh); - if (crop) { - /* Cropped present keeps left-aligned content; still inset so linear - * AA does not blend the cut column with undefined border texels. */ - float u0 = (src_w > 0) ? (0.5f / (float)src_w) : 0.f; - float v0 = (src_h > 0) ? (0.5f / (float)src_h) : 0.f; - p_glUniform4f(s_present_uUvRect, u0, v0, uv_x1 - u0, 1.f - v0); - } else if (src_w > 0 && src_h > 0) { - /* Half-texel UV inset for both nearest and linear. Corner-mapped - * UV=1.0 grazes past the last texel (driver-dependent border sample); - * with GL_LINEAR that also blends an edge stripe into the image. - * Matches present_target_quad / MotK present UV edge-bleed fix. */ - float u0 = 0.5f / (float)src_w, v0 = 0.5f / (float)src_h; - p_glUniform4f(s_present_uUvRect, u0, v0, 1.f - u0, 1.f - v0); - } else { - p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f); - } - p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3); - p_glBindVertexArray(0); p_glUseProgram(0); + /* CPU present texture holds exactly the display rect, so v_uv spans it and + * pitch == display height == src_h. */ + PRESENT_SCANLINE(src_h, src_h, lh); + if (crop) { + /* Cropped present keeps left-aligned content; still inset so linear + * AA does not blend the cut column with undefined border texels. */ + float u0 = (src_w > 0) ? (0.5f / (float)src_w) : 0.f; + float v0 = (src_h > 0) ? (0.5f / (float)src_h) : 0.f; + p_glUniform4f(s_present_uUvRect, u0, v0, uv_x1 - u0, 1.f - v0); + } else if (src_w > 0 && src_h > 0) { + /* Half-texel UV inset for both nearest and linear. Corner-mapped + * UV=1.0 grazes past the last texel (driver-dependent border sample); + * with GL_LINEAR that also blends an edge stripe into the image. + * Matches present_target_quad / MotK present UV edge-bleed fix. */ + float u0 = 0.5f / (float)src_w, v0 = 0.5f / (float)src_h; + p_glUniform4f(s_present_uUvRect, u0, v0, 1.f - u0, 1.f - v0); + } else { + p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f); + } + p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3); + p_glBindVertexArray(0); p_glUseProgram(0); pres_record(GL_PRES_CPU, 0, 0, src_w, src_h, lx, ly, lw, lh); - hold_capture_drawable(); - latency_ring_mark(LAT_SWAP_BEGIN); - gl_swap_with_osd(); - latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; - present_force_consumed(); - s_last_present_path = GL_PRES_CPU; -} - -void gl_renderer_present_blank(void) { - if (!s_ctx) return; - interp_reset_history(); - int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); - pres_record(GL_PRES_BLANK, 0, 0, 0, 0, 0, 0, ww, wh); - hold_capture_drawable(); - latency_ring_mark(LAT_SWAP_BEGIN); - gl_swap_with_osd(); - latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; - present_force_consumed(); - s_last_present_path = GL_PRES_BLANK; -} - -/* Sync the authoritative FBO down into CPU VRAM (no-op when current). - * Screenshots / debug server. Not for 24-bit FMV scanout (see flush). */ -void gl_renderer_sync_cpu(void) { - ensure_cpu(); -} - -void gl_renderer_invalidate_present(void) { - for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull; - s_last_present_path = -1; - s_force_present_remaining = 8; - hold_invalidate(); - interp_reset_history(); -} - -void gl_renderer_restage_vram_after_savestate(void) { - if (!s_raster_ok || !s_vram) return; - /* Belt-and-suspenders after boot_state VRAM apply: force CPU mirror → FBO - * even if a depth24 skip swallowed the restore, then re-arm scanout-band - * clear so leaving FMV does not keep RGB888-as-1555 junk. */ - s_up_nrects = 0; - rect_clear(&s_d24_skip_fb); - s_depth24_skip_up = 0; - up_add_transfer(0, 0, VRAM_W, VRAM_H); - flush_cpu_upload(); - if (gpu_display_is_depth24()) { - s_depth24_skip_up = 1; - depth24_mark_scanout_band(); - } - /* Dual-raster: CPU is authority after snap; FBO is cosmetics only. */ - if (s_cpu_auth_dual) - s_gpu_dirty = 0; -} - -void gl_renderer_set_cpu_auth_dual(int on) { - s_cpu_auth_dual = on ? 1 : 0; - if (s_cpu_auth_dual) - s_gpu_dirty = 0; -} - -int gl_renderer_cpu_auth_dual(void) { - return s_cpu_auth_dual; -} - -void gl_renderer_present_probe_reset(void) { - s_probe_skip = 0; - s_probe_swap = 0; - s_probe_dirty_marks = 0; -} - -void gl_renderer_present_probe_take(uint64_t *skip_delta, uint64_t *swap_delta, - uint64_t *dirty_mark_delta, - int *force_remaining) { - if (skip_delta) { *skip_delta = s_probe_skip; s_probe_skip = 0; } - if (swap_delta) { *swap_delta = s_probe_swap; s_probe_swap = 0; } - if (dirty_mark_delta) { - *dirty_mark_delta = s_probe_dirty_marks; - s_probe_dirty_marks = 0; - } - if (force_remaining) *force_remaining = s_force_present_remaining; -} - -int gl_renderer_present_rect_dirty(int disp_x, int disp_y, int w, int h) { - if (!s_raster_ok || w <= 0 || h <= 0) return 0; - return present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1); -} - -void gl_renderer_flush_cpu_uploads(void) { - if (!s_raster_ok) return; - flush_flat_batch(); - flush_tex_batch(); - flush_cpu_upload(); -} - -/* Diagnostic (debug server "gl_fbo_peek"): read a rect of the GPU-side - * authoritative VRAM (via the pack pass + raw mirror) WITHOUT writing CPU - * VRAM — lets a probe diff FBO truth against CPU truth. Returns 0 when the - * GL pipeline is inactive (software backend). */ -int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out) { - if (!s_raster_ok || !s_ctx) return 0; - if (x < 0 || y < 0 || w < 1 || h < 1 || - x + w > VRAM_W || y + h > VRAM_H) return 0; - flush_cpu_upload(); - rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1); - pack_flush(); - coh_record(GL_COH_PEEK, x, y, x + w - 1, y + h - 1); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); - glPixelStorei(GL_PACK_ALIGNMENT, 2); - glReadPixels(x, y, w, h, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, out); - glPixelStorei(GL_PACK_ALIGNMENT, 4); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - return 1; -} - -/* Diagnostic (debug server "gl_vram_diff"): full-VRAM comparison of the - * GPU-side truth (FBO via pack) against the CPU array, WITHOUT writing - * either. Reports mismatch count + bounding box + a few sample coords. - * Divergence is expected where the GPU is legitimately ahead (gpu_dirty); - * at upload-only scenes the two must match exactly. */ -int gl_renderer_vram_diff(uint32_t *count, int bbox[4], - int samples[8][2], uint16_t samples_px[8][2]) { - if (!s_raster_ok || !s_ctx) return 0; - uint16_t *tmp = (uint16_t *)malloc((size_t)VRAM_W * VRAM_H * 2); - if (!tmp) return 0; - flush_cpu_upload(); - /* Force a full pack: the diff must read FBO truth even where the - * raw-mirror invariant (raw == FBO outside s_pack_dirty) is broken — - * a broken invariant is exactly what this tool hunts. */ - rect_add(&s_pack_dirty, 0, 0, VRAM_W - 1, VRAM_H - 1); - pack_flush(); - coh_record(GL_COH_DIFF, 0, 0, VRAM_W - 1, VRAM_H - 1); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); - glReadPixels(0, 0, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, tmp); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - uint32_t n = 0; - int x0 = VRAM_W, y0 = VRAM_H, x1 = -1, y1 = -1, ns = 0; - for (int y = 0; y < VRAM_H; y++) { - for (int x = 0; x < VRAM_W; x++) { - uint16_t f = tmp[y * VRAM_W + x], c = s_vram[y * VRAM_W + x]; - if (f == c) continue; - n++; - if (x < x0) x0 = x; if (x > x1) x1 = x; - if (y < y0) y0 = y; if (y > y1) y1 = y; - if (ns < 8 && (n % 977) == 1) { /* spread samples */ - samples[ns][0] = x; samples[ns][1] = y; - samples_px[ns][0] = f; samples_px[ns][1] = c; - ns++; - } - } - } - free(tmp); - *count = n; - bbox[0] = x0; bbox[1] = y0; bbox[2] = x1; bbox[3] = y1; - return 1 + ns; /* >=1 means valid; ns = samples filled */ -} - -/* Diagnostic state for the debug server: coherency flags + dirty rects. */ -void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]) { - if (gpu_dirty) *gpu_dirty = s_gpu_dirty; - if (pending) { - /* [0] = pending rect count; [1..4] = union bbox (diagnostic only — - * the flush itself paints the exact rects, never this union). */ - pending[0] = s_up_nrects; - pending[1] = pending[2] = pending[3] = pending[4] = 0; - for (int i = 0; i < s_up_nrects; i++) { - if (i == 0) { - pending[1] = s_up_rects[i].x0; pending[2] = s_up_rects[i].y0; - pending[3] = s_up_rects[i].x1; pending[4] = s_up_rects[i].y1; - } else { - if (s_up_rects[i].x0 < pending[1]) pending[1] = s_up_rects[i].x0; - if (s_up_rects[i].y0 < pending[2]) pending[2] = s_up_rects[i].y0; - if (s_up_rects[i].x1 > pending[3]) pending[3] = s_up_rects[i].x1; - if (s_up_rects[i].y1 > pending[4]) pending[4] = s_up_rects[i].y1; - } - } - } - if (pack) { - pack[0] = s_pack_dirty.set; - pack[1] = s_pack_dirty.x0; pack[2] = s_pack_dirty.y0; - pack[3] = s_pack_dirty.x1; pack[4] = s_pack_dirty.y1; - } -} - -/* ------------------------------------------------------------------------- * - * Native-wide compositor (GL). Mirrors gpu_sw_renderer.c's wide functions: - * canonical VRAM (the hr FBO) is untouched; framebuffer draws are also mirrored - * into per-base_x wide FBOs (see the draw funcs' wide passes). Present reads the - * displayed buffer's wide FBO via glReadPixels into the CPU present buffer, then - * the existing CPU present path uploads/letterboxes it (Option B: reuse the CPU - * present path). Self-gates on the GL pipeline being live; never runs for - * 4:3 / non-opted games (gpu.c never calls wide_configure for those). - * ------------------------------------------------------------------------- */ - -static void wide_free_all(void) { - for (int i = 0; i < WIDE_MAX_SURF; i++) { - if (s_wide_fbo[i]) { p_glDeleteFramebuffers(1, &s_wide_fbo[i]); s_wide_fbo[i] = 0; } - if (s_wide_tex[i]) { glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0; } - if (s_wide_rb[i]) { p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0; } - s_wide_base[i] = -1; - } - g_wide_cur = 0; -} - -/* Lazily allocate (or find) the wide FBO+tex for base_x. Returns the FBO id, or - * 0 on failure / more distinct buffers than WIDE_MAX_SURF. */ -static GLuint wide_fbo_for(int base_x) { - if (g_wide_w <= 0) return 0; - for (int i = 0; i < WIDE_MAX_SURF; i++) - if (s_wide_fbo[i] && s_wide_base[i] == base_x) return s_wide_fbo[i]; - for (int i = 0; i < WIDE_MAX_SURF; i++) { - if (!s_wide_fbo[i]) { - int w = g_wide_w * s_scale, h = VRAM_H * s_scale; - s_cw_fbo_creates++; - s_wide_tex[i] = make_tex(GL_RGBA8, w, h, GL_RGBA, GL_UNSIGNED_BYTE); - /* Depth-stencil RB, same as the hr FBO: the stencil carries the - * PSX mask-bit mirror for the wide surface, and (the hard lesson) - * a stencil-less FBO turns every stencil-enabled mirror draw into - * ~0.6ms of driver-side work — the 16:9 GL perf collapse. */ - p_glGenRenderbuffers(1, &s_wide_rb[i]); - p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_wide_rb[i]); - p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, w, h); - if (!make_fbo(&s_wide_fbo[i], s_wide_tex[i], s_wide_rb[i])) { - glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0; - p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0; - return 0; - } - /* Clear to black so unwritten margins are clean (not stale). */ - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_wide_fbo[i]); - glDisable(GL_SCISSOR_TEST); - glClearColor(0, 0, 0, 0); - glClearStencil(0); - glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - s_wide_base[i] = base_x; - return s_wide_fbo[i]; - } - } - return 0; /* more distinct buffers than WIDE_MAX_SURF — shouldn't happen */ -} - -/* Enable native-wide with a wide width + centering offset (native px), or - * disable (wide_w <= 0). Re-allocates if the width changed. Mirrors - * sw_wide_configure. */ -static void glb_wide_configure(int wide_w, int offset) { - if (!s_raster_ok) return; - double t0 = cw_ms(); s_cw_wide_cfgs++; - flush_tex_batch(); /* a queued batch's wide mirror targets the CURRENT surfaces */ - if (wide_w <= 0) { wide_free_all(); g_wide_w = 0; g_wide_off = 0; s_cw_wide_ms += cw_ms() - t0; return; } - if (wide_w != g_wide_w) wide_free_all(); - g_wide_w = wide_w; - g_wide_off = offset; - s_cw_wide_ms += cw_ms() - t0; -} - -/* Select the wide surface to mirror into for the back buffer at base_x. */ -static void glb_wide_set_target(int base_x) { - if (!s_raster_ok) { g_wide_cur = 0; return; } - double t0 = cw_ms(); s_cw_wide_sets++; - flush_tex_batch(); /* drain into the OLD target before switching */ - g_wide_cur = wide_fbo_for(base_x); - g_wide_cur_base = base_x; - s_cw_wide_ms += cw_ms() - t0; -} - -/* Stop mirroring (offscreen draws that don't target a framebuffer). */ -static void glb_wide_disable_target(void) { flush_tex_batch(); g_wide_cur = 0; } - -/* Mirror a framebuffer clear: fill the full wide width over [y, y+h) of the - * surface for base_x, so the revealed margins are clean. Mirrors sw_wide_clear: - * a scissored glClear with the 1555 color converted to RGBA8 (alpha = bit15). */ -static void glb_wide_clear(int base_x, int y, int h, uint16_t color) { - if (!s_raster_ok || s_ws_ablate == 1) return; - double t0 = cw_ms(); s_cw_wide_clears++; - flush_tex_batch(); - GLuint fbo = wide_fbo_for(base_x); - if (!fbo) { s_cw_wide_ms += cw_ms() - t0; return; } - gl_perf_mirror_begin(); - int H = VRAM_H * s_scale; - int y0 = y * s_scale, y1 = (y + h) * s_scale; - if (y0 < 0) y0 = 0; - if (y1 > H) y1 = H; - if (y1 <= y0) return; - float r = (color & 0x1F) / 31.0f; - float g = ((color >> 5) & 0x1F) / 31.0f; - float b = ((color >> 10) & 0x1F) / 31.0f; - float a = (color >> 15) & 1 ? 1.0f : 0.0f; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo); - glViewport(0, 0, g_wide_w * s_scale, H); - glEnable(GL_SCISSOR_TEST); - glScissor(0, y0, g_wide_w * s_scale, y1 - y0); - glClearColor(r, g, b, a); - glClearStencil((color >> 15) & 1); /* stencil mirrors bit15, like the color alpha */ - glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - gl_perf_mirror_end(); - s_cw_wide_ms += cw_ms() - t0; -} - -/* Clear only the two synthetic reveal strips, preserving the centred canonical - * framebuffer. This is an opt-in transition cleanup driven by gpu.c. */ -static void glb_wide_clear_margins(int base_x, int y, int h, uint16_t color, int sides) { - if (!s_raster_ok || s_ws_ablate == 1 || g_wide_off <= 0) return; - double t0 = cw_ms(); s_cw_wide_clears++; - flush_tex_batch(); - GLuint fbo = wide_fbo_for(base_x); - if (!fbo) { s_cw_wide_ms += cw_ms() - t0; return; } - gl_perf_mirror_begin(); - int H = VRAM_H * s_scale; - int W = g_wide_w * s_scale; - int margin = g_wide_off * s_scale; - int y0 = y * s_scale, y1 = (y + h) * s_scale; - if (y0 < 0) y0 = 0; - if (y1 > H) y1 = H; - if (y1 <= y0 || margin * 2 >= W) { - gl_perf_mirror_end(); - s_cw_wide_ms += cw_ms() - t0; - return; - } - float r = (color & 0x1F) / 31.0f; - float g = ((color >> 5) & 0x1F) / 31.0f; - float b = ((color >> 10) & 0x1F) / 31.0f; - float a = (color >> 15) & 1 ? 1.0f : 0.0f; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo); - glViewport(0, 0, W, H); - glEnable(GL_SCISSOR_TEST); - glClearColor(r, g, b, a); - glClearStencil((color >> 15) & 1); - glStencilMask(0xFF); - if (sides & 1) { - glScissor(0, y0, margin, y1 - y0); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - } - if (sides & 2) { - glScissor(W - margin, y0, margin, y1 - y0); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - } - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - gl_perf_mirror_end(); - s_cw_wide_ms += cw_ms() - t0; -} - -/* Present source: read the wide FBO for the displayed buffer (base_x) into the - * CPU present buffer as ARGB8888, byte-identical to sw_render_wide_display's - * output so the shared CPU present path consumes it the same way. Output is - * (g_wide_w*scale) wide × (disp_h*scale) tall. Returns pixel count (>0), or 0 - * if no surface exists for base_x (caller falls back to the canonical present). - * - * PIXEL FORMAT: glReadPixels(GL_BGRA, GL_UNSIGNED_BYTE) yields, per pixel, the - * little-endian uint32 0xAARRGGBB == ARGB8888 — exactly what rgb555_to_argb - * produces and what upload_present_tex feeds to glTexImage2D(GL_BGRA,...). The - * SW path forces alpha to 0xFF; we OR it in to match (present ignores alpha, but - * we keep the two paths bit-identical). GL's read origin is bottom-left, so the - * block is read bottom-to-top and copied into `out` reversed (out row 0 = the - * display's top scanline, as the SW path and the PRESENT_VS V-flip expect). */ -static int glb_render_wide_display(uint32_t *out, int pitch, int base_x, - int disp_y, int disp_h) { - if (!s_raster_ok || !s_ctx || g_wide_w <= 0) return 0; - GLuint fbo = 0; - for (int i = 0; i < WIDE_MAX_SURF; i++) - if (s_wide_fbo[i] && s_wide_base[i] == base_x) { fbo = s_wide_fbo[i]; break; } - if (!fbo) return 0; - - /* Fold any pending CPU->VRAM uploads into the canonical FBO first (uploads - * are never mirrored to wide, but draws after them are; keep op order) and - * make sure all wide-FBO draws have completed before the readback. */ - flush_tex_batch(); - flush_cpu_upload(); - wide_blit_center(fbo, base_x, disp_y, disp_h); /* fast-path: authoritative centre before readback */ - glFinish(); - - int W = g_wide_w * s_scale; - int H = VRAM_H * s_scale; - int out_h = disp_h * s_scale; - int ry0 = disp_y * s_scale; - if (ry0 < 0) ry0 = 0; - if (ry0 + out_h > H) out_h = H - ry0; - if (out_h <= 0) return 0; - - uint32_t *tmp = (uint32_t *)malloc((size_t)W * out_h * sizeof(uint32_t)); - if (!tmp) return 0; - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo); - glPixelStorei(GL_PACK_ALIGNMENT, 4); - glReadPixels(0, ry0, W, out_h, GL_BGRA, GL_UNSIGNED_BYTE, tmp); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - - /* Orientation: the wide FBO stores PS1 y inverted (geo shader maps vram_y=0 - * to NDC y=-1 = FBO BOTTOM), and glReadPixels reads bottom-up — the two - * inversions CANCEL, so glReadPixels row 0 already = PS1 top scanline. Copy - * straight (NO flip) so `out` is top-down, matching sw_render_wide_display - * (which the shared CPU present path + its PRESENT_VS V-flip expect). Force - * alpha = 0xFF (match SW). [An earlier reversal here made the frame - * upside-down.] */ - int count = 0; - for (int row = 0; row < out_h; row++) { - const uint32_t *src = tmp + (size_t)row * W; - uint32_t *dst = (uint32_t *)((uint8_t *)out + (size_t)row * pitch); - for (int col = 0; col < W; col++) { dst[col] = src[col] | 0xFF000000u; count++; } - } - free(tmp); - return count; -} - -/* Dump the ENTIRE wide compositor surface for base_x (all double-buffer bands + - * both reveal margins), g_wide_w x VRAM_H at scale. Debug/inspection tool (TCP - * wide_full) — the GL analog of sw_wide_dump_full, so native-wide can be - * inspected without touching the game window. Runs the same authoritative - * centre blit first so the dump matches what present shows. Top-down, alpha=FF - * (matches sw_wide_dump_full / render_wide_display orientation). */ -static int glb_wide_dump_full(uint32_t *out, int cap_pixels, int *ow, int *oh, - int base_x) { - if (!s_raster_ok || !s_ctx || g_wide_w <= 0) return 0; - GLuint fbo = 0; - for (int i = 0; i < WIDE_MAX_SURF; i++) - if (s_wide_fbo[i] && s_wide_base[i] == base_x) { fbo = s_wide_fbo[i]; break; } - if (!fbo) return 0; - flush_tex_batch(); - flush_cpu_upload(); - wide_blit_center(fbo, base_x, 0, VRAM_H); /* authoritative centre (full height) */ - glFinish(); - int W = g_wide_w * s_scale; - int H = VRAM_H * s_scale; - if (cap_pixels > 0 && (long)W * H > cap_pixels) { H = cap_pixels / W; if (H <= 0) return 0; } - uint32_t *tmp = (uint32_t *)malloc((size_t)W * H * sizeof(uint32_t)); - if (!tmp) return 0; - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo); - glPixelStorei(GL_PACK_ALIGNMENT, 4); - glReadPixels(0, 0, W, H, GL_BGRA, GL_UNSIGNED_BYTE, tmp); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - /* Same orientation reasoning as glb_render_wide_display: glReadPixels row 0 = - * PS1 top scanline, so copy straight (top-down). */ - int count = 0; - for (int i = 0; i < W * H; i++) { out[i] = tmp[i] | 0xFF000000u; count++; } - free(tmp); - if (ow) *ow = W; - if (oh) *oh = H; - return count; -} - -/* THE present path for 15-bit frames: blit the display region from the - * authoritative hr FBO into a letterboxed rect. Deterministic — runs - * every 15-bit frame regardless of what mix of ops produced it. - * force_4_3 pins to native 4:3 (15-bit MDEC FMV frames on a wide aspect). */ -/* ===================== frame_perf: per-frame GPU/CPU phase timing ============ - * Developer builds use two GL_TIME_ELAPSED queries per frame to bracket (a) the - * scene draws (all GP0 raster issued between two presents) and (b) the present - * clear+blit, giving TRUE GPU time per phase independent of CPU/GPU overlap - * (glFinish would only catch the non-overlapped tail and mislead). CPU wall time - * (present-to-present total, and the present call) comes from SDL perf counters. - * Results are read back GLPERF_NBUF frames late (no pipeline stall) into a ring - * the debug server's frame_perf command aggregates. Release builds compile out - * the debug server, so they also leave this instrumentation disabled: a native- - * wide frame can otherwise issue hundreds of unused mirror timestamp queries. - * One question this answers in a diagnostics build: - * where does a 16:9 frame go vs 4:3 — scene fill, wide composite, or CPU. */ -#define GLPERF_NBUF 4 -#define GLPERF_RING 256 -typedef struct { - double total_ms; /* present-entry to present-entry (full frame) */ - double present_wall_ms; /* CPU wall time inside the present call */ - double scene_gpu_ms; /* GPU: all scene draws this frame */ - double present_gpu_ms; /* GPU: the present clear+blit */ - double mirror_gpu_ms; /* GPU: of scene_gpu, the native-wide mirror passes */ - double prims; /* scene primitives submitted this frame */ - double mirror_passes; /* mirror passes this frame (measured + overflow) */ - double cw_flush_ms; /* CPU wall inside flush_tex_batch this frame */ - double cw_wide_ms; /* CPU wall inside glb_wide_* this frame */ - double batches; /* flush_tex_batch draws this frame */ - double wide_sets; /* glb_wide_set_target calls this frame */ - double fbo_creates; /* wide FBO+tex creations this frame */ - int wide; /* 1 = native-wide (16:9) present, 0 = 4:3 */ - uint64_t frame; -} GlPerfSample; - -/* Native-wide mirror-pass GPU attribution: the scene TIME_ELAPSED query spans - * the whole frame (queries of one target cannot nest), so each mirror pass is - * bracketed with a GL_TIMESTAMP pair instead (glQueryCounter does not conflict - * with an active TIME_ELAPSED query). Pairs are pooled per buffered frame and - * summed at readback, splitting scene_gpu into canonical vs mirror cost. */ -#define GLPERF_MIRQ 1024 /* measured mirror passes per frame */ -static GLuint s_mq_q[GLPERF_NBUF][GLPERF_MIRQ * 2]; -static int s_mq_n[GLPERF_NBUF]; /* pairs recorded this frame */ -static int s_mq_over[GLPERF_NBUF]; /* passes beyond the pool (counted, untimed) */ -static int s_mq_open = 0; /* begin issued, end pending */ -static int s_mq_ok = 0; /* glQueryCounter available */ - -static int s_pf_on = 0; -static GLuint s_pf_scene_q[GLPERF_NBUF]; -static GLuint s_pf_present_q[GLPERF_NBUF]; -static int s_pf_b = 0; /* buffer for the CURRENT frame */ -static int s_pf_scene_active = 0; -static uint64_t s_pf_count = 0; -static uint64_t s_pf_freq = 1; -static uint64_t s_pf_last_enter = 0; -static uint64_t s_pf_enter = 0; -static double s_pf_total_pending = 0.0; -static double s_pf_buf_total[GLPERF_NBUF]; -static double s_pf_buf_pwall[GLPERF_NBUF]; -static double s_pf_buf_prims[GLPERF_NBUF]; -static int s_pf_buf_wide[GLPERF_NBUF]; -static uint64_t s_pf_buf_frame[GLPERF_NBUF]; -static uint64_t s_pf_prims_last = 0; -static double s_pf_prims_pending = 0.0; -static double s_pf_cw_pending[5]; /* flush_ms, wide_ms, batches, wide_sets, fbo_creates */ -static double s_pf_buf_cw[GLPERF_NBUF][5]; -static GlPerfSample s_pf_ring[GLPERF_RING]; -static uint64_t s_pf_ring_seq = 0; - -static void gl_perf_init(void) { -#ifdef PSX_NO_DEBUG_TOOLS - return; -#else - /* Timer queries are intentionally available in diagnostics builds, but a - * driver may serialize command submission while collecting them. Keep an - * escape hatch so frame cadence can be A/B tested without rebuilding or - * losing the rest of the debug server telemetry. */ - { - const char *enabled = getenv("PSX_GL_PERF"); - if (enabled && enabled[0] == '0') return; - } - if (!p_glGenQueries || !p_glBeginQuery || !p_glEndQuery || !p_glGetQueryObjectui64v) return; - p_glGenQueries(GLPERF_NBUF, s_pf_scene_q); - p_glGenQueries(GLPERF_NBUF, s_pf_present_q); - s_mq_ok = (p_glQueryCounter != NULL); - if (s_mq_ok) - for (int i = 0; i < GLPERF_NBUF; i++) { - p_glGenQueries(GLPERF_MIRQ * 2, s_mq_q[i]); - s_mq_n[i] = 0; s_mq_over[i] = 0; - } - s_mq_open = 0; - s_pf_freq = SDL_GetPerformanceFrequency(); - if (!s_pf_freq) s_pf_freq = 1; - s_pf_b = 0; s_pf_scene_active = 0; s_pf_count = 0; s_pf_ring_seq = 0; - s_pf_last_enter = 0; - s_pf_on = 1; -#endif -} - -/* Bracket ONE native-wide mirror pass (called from the wide-mirror draw sites). - * Timestamp pairs, not TIME_ELAPSED — see the pool comment above. */ -static void gl_perf_mirror_begin(void) { - if (!s_pf_on || !s_mq_ok) return; - int b = s_pf_b; - if (s_mq_n[b] >= GLPERF_MIRQ) { s_mq_over[b]++; return; } - p_glQueryCounter(s_mq_q[b][s_mq_n[b] * 2], GL_TIMESTAMP); - s_mq_open = 1; -} -static void gl_perf_mirror_end(void) { - if (!s_pf_on || !s_mq_ok || !s_mq_open) return; - int b = s_pf_b; - p_glQueryCounter(s_mq_q[b][s_mq_n[b] * 2 + 1], GL_TIMESTAMP); - s_mq_n[b]++; - s_mq_open = 0; -} - -/* Top of present (after flush_cpu_upload, before clear/blit). */ -static void gl_perf_present_enter(void) { - /* Per-frame boundary for the 2D-backdrop stretch — runs from BOTH present - * paths (4:3 present_vram AND native-wide present_wide_fbo), before the - * perf-on gate so native-wide frames reset too. Snapshot this frame's - * backdrop-stretch diagnostics, then reset the per-frame counters. (The gate - * is per-prim now — no draw-order phase to reset.) Final batch already flushed - * by the caller. */ - g_bdg_applied = s_bdg_applied; g_bdg_prims = s_bdg_prims; g_bdg_clearx = s_bdg_clearx; - g_bdg_cur = (g_wide_cur != 0); g_bdg_base = g_wide_cur_base; g_bdg_w = g_wide_w; g_bdg_off = g_wide_off; - s_bdg_applied = 0; s_bdg_prims = 0; s_bdg_clearx = -999999; - { extern void psx_ws_dbg_gate_frame_snapshot(void); psx_ws_dbg_gate_frame_snapshot(); } - if (!s_pf_on) return; - uint64_t now = SDL_GetPerformanceCounter(); - s_pf_enter = now; - s_pf_total_pending = s_pf_last_enter - ? (double)(now - s_pf_last_enter) * 1000.0 / (double)s_pf_freq : 0.0; - s_pf_last_enter = now; - s_pf_prims_pending = (double)(s_scene_prims - s_pf_prims_last); /* prims drawn this frame */ - s_pf_prims_last = s_scene_prims; - s_pf_cw_pending[0] = s_cw_flush_ms; s_pf_cw_pending[1] = s_cw_wide_ms; - s_pf_cw_pending[2] = (double)s_cw_batches; - s_pf_cw_pending[3] = (double)s_cw_wide_sets; - s_pf_cw_pending[4] = (double)s_cw_fbo_creates; - s_cw_flush_ms = 0.0; s_cw_wide_ms = 0.0; - s_cw_batches = 0; s_cw_wide_sets = 0; s_cw_wide_cfgs = 0; - s_cw_wide_clears = 0; s_cw_fbo_creates = 0; - if (s_pf_scene_active) { p_glEndQuery(GL_TIME_ELAPSED); s_pf_scene_active = 0; } /* end frame b's scene draws */ - p_glBeginQuery(GL_TIME_ELAPSED, s_pf_present_q[s_pf_b]); /* time frame b's present */ -} - -/* End of present (after SwapWindow). wide = native-wide path. */ -static void gl_perf_present_exit(int wide) { - if (!s_pf_on) return; - uint64_t now = SDL_GetPerformanceCounter(); - p_glEndQuery(GL_TIME_ELAPSED); /* end present_q[b] */ - s_pf_buf_total[s_pf_b] = s_pf_total_pending; - s_pf_buf_pwall[s_pf_b] = (double)(now - s_pf_enter) * 1000.0 / (double)s_pf_freq; - s_pf_buf_prims[s_pf_b] = s_pf_prims_pending; - for (int ci = 0; ci < 5; ci++) s_pf_buf_cw[s_pf_b][ci] = s_pf_cw_pending[ci]; - s_pf_buf_wide[s_pf_b] = wide; - s_pf_buf_frame[s_pf_b] = s_pf_count; - int rd = (s_pf_b + 1) % GLPERF_NBUF; /* oldest buffer (frame count+1-NBUF), now done */ - if (s_pf_count >= (uint64_t)GLPERF_NBUF) { - GLuint64 sc = 0, pr = 0; - p_glGetQueryObjectui64v(s_pf_scene_q[rd], GL_QUERY_RESULT, &sc); - p_glGetQueryObjectui64v(s_pf_present_q[rd], GL_QUERY_RESULT, &pr); - double mir = 0.0; - for (int i = 0; i < s_mq_n[rd]; i++) { /* sum that frame's mirror pairs */ - GLuint64 t0 = 0, t1 = 0; - p_glGetQueryObjectui64v(s_mq_q[rd][i * 2], GL_QUERY_RESULT, &t0); - p_glGetQueryObjectui64v(s_mq_q[rd][i * 2 + 1], GL_QUERY_RESULT, &t1); - if (t1 > t0) mir += (double)(t1 - t0); - } - GlPerfSample *s = &s_pf_ring[s_pf_ring_seq % GLPERF_RING]; - s->total_ms = s_pf_buf_total[rd]; - s->present_wall_ms = s_pf_buf_pwall[rd]; - s->scene_gpu_ms = (double)sc / 1.0e6; - s->present_gpu_ms = (double)pr / 1.0e6; - s->mirror_gpu_ms = mir / 1.0e6; - s->prims = s_pf_buf_prims[rd]; - s->mirror_passes = (double)(s_mq_n[rd] + s_mq_over[rd]); - s->cw_flush_ms = s_pf_buf_cw[rd][0]; - s->cw_wide_ms = s_pf_buf_cw[rd][1]; - s->batches = s_pf_buf_cw[rd][2]; - s->wide_sets = s_pf_buf_cw[rd][3]; - s->fbo_creates = s_pf_buf_cw[rd][4]; - s->wide = s_pf_buf_wide[rd]; - s->frame = s_pf_buf_frame[rd]; - s_pf_ring_seq++; - } - s_mq_n[rd] = 0; s_mq_over[rd] = 0; /* rd becomes the next frame's buffer */ - s_pf_count++; - s_pf_b = rd; /* reuse oldest for next frame */ - p_glBeginQuery(GL_TIME_ELAPSED, s_pf_scene_q[s_pf_b]); /* open next frame's scene draws */ - s_pf_scene_active = 1; -} - -/* Aggregate the ring for the debug server. wide_filter: -1 all, 0 = 4:3, 1 = wide. - * out[0]=count, [1]=total_avg, [2]=total_max, [3]=emu_cpu_avg (total-present_wall), - * [4]=present_wall_avg, [5]=scene_gpu_avg, [6]=scene_gpu_max, [7]=present_gpu_avg, - * [8]=present_gpu_max. Returns the sample count. */ -/* Cumulative textured fraction of scene prims (decides flat vs textured batching - * priority). out_tex_frac = textured/total since boot; returns total prim count. */ -uint64_t gl_renderer_perf_prim_split(double *out_tex_frac) { - if (out_tex_frac) *out_tex_frac = s_scene_prims ? (double)s_scene_prims_tex / (double)s_scene_prims : 0.0; - return s_scene_prims; -} - -int gl_renderer_perf_aggregate(int wide_filter, double out[18]) { - for (int i = 0; i < 18; i++) out[i] = 0.0; - if (!s_pf_on) return 0; - int navail = (int)(s_pf_ring_seq < (uint64_t)GLPERF_RING ? s_pf_ring_seq : GLPERF_RING); - uint64_t start = s_pf_ring_seq - (uint64_t)navail; - int n = 0; - for (int i = 0; i < navail; i++) { - const GlPerfSample *s = &s_pf_ring[(start + i) % GLPERF_RING]; - if (wide_filter >= 0 && s->wide != wide_filter) continue; - double emu = s->total_ms - s->present_wall_ms; if (emu < 0) emu = 0; - out[1] += s->total_ms; if (s->total_ms > out[2]) out[2] = s->total_ms; - out[3] += emu; - out[4] += s->present_wall_ms; - out[5] += s->scene_gpu_ms; if (s->scene_gpu_ms > out[6]) out[6] = s->scene_gpu_ms; - out[7] += s->present_gpu_ms; if (s->present_gpu_ms > out[8]) out[8] = s->present_gpu_ms; - out[9] += s->prims; - out[10] += s->mirror_gpu_ms; if (s->mirror_gpu_ms > out[11]) out[11] = s->mirror_gpu_ms; - out[12] += s->mirror_passes; - out[13] += s->cw_flush_ms; - out[14] += s->cw_wide_ms; - out[15] += s->batches; - out[16] += s->wide_sets; - out[17] += s->fbo_creates; - n++; - } - if (n) { - out[1]/=n; out[3]/=n; out[4]/=n; out[5]/=n; out[7]/=n; out[9]/=n; out[10]/=n; out[12]/=n; - out[13]/=n; out[14]/=n; out[15]/=n; out[16]/=n; out[17]/=n; - } - out[0] = (double)n; - return n; -} - -/* Native-wide mirror ablation (perf attribution): see s_ws_ablate. */ -void gl_renderer_set_ws_ablate(int mode) { s_ws_ablate = (mode >= 0 && mode <= 3) ? mode : 0; } -int gl_renderer_get_ws_ablate(void) { return s_ws_ablate; } - + hold_capture_drawable(); + latency_ring_mark(LAT_SWAP_BEGIN); + gl_swap_with_osd(); + latency_ring_mark(LAT_SWAP_END); + s_probe_swap++; + present_force_consumed(); + s_last_present_path = GL_PRES_CPU; +} + +void gl_renderer_present_blank(void) { + if (!s_ctx) return; + interp_reset_history(); + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); + pres_record(GL_PRES_BLANK, 0, 0, 0, 0, 0, 0, ww, wh); + hold_capture_drawable(); + latency_ring_mark(LAT_SWAP_BEGIN); + gl_swap_with_osd(); + latency_ring_mark(LAT_SWAP_END); + s_probe_swap++; + present_force_consumed(); + s_last_present_path = GL_PRES_BLANK; +} + +/* Sync the authoritative FBO down into CPU VRAM (no-op when current). + * Screenshots / debug server. Not for 24-bit FMV scanout (see flush). */ +void gl_renderer_sync_cpu(void) { + ensure_cpu(); +} + +void gl_renderer_invalidate_present(void) { + for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull; + s_last_present_path = -1; + s_force_present_remaining = 8; + hold_invalidate(); + interp_reset_history(); +} + +void gl_renderer_restage_vram_after_savestate(void) { + if (!s_raster_ok || !s_vram) return; + /* Belt-and-suspenders after boot_state VRAM apply: force CPU mirror → FBO + * even if a depth24 skip swallowed the restore, then re-arm scanout-band + * clear so leaving FMV does not keep RGB888-as-1555 junk. */ + s_up_nrects = 0; + rect_clear(&s_d24_skip_fb); + s_depth24_skip_up = 0; + up_add_transfer(0, 0, VRAM_W, VRAM_H); + flush_cpu_upload(); + if (gpu_display_is_depth24()) { + s_depth24_skip_up = 1; + depth24_mark_scanout_band(); + } + /* Dual-raster: CPU is authority after snap; FBO is cosmetics only. */ + if (s_cpu_auth_dual) + s_gpu_dirty = 0; +} + +void gl_renderer_set_cpu_auth_dual(int on) { + s_cpu_auth_dual = on ? 1 : 0; + if (s_cpu_auth_dual) + s_gpu_dirty = 0; +} + +int gl_renderer_cpu_auth_dual(void) { + return s_cpu_auth_dual; +} + +void gl_renderer_present_probe_reset(void) { + s_probe_skip = 0; + s_probe_swap = 0; + s_probe_dirty_marks = 0; +} + +void gl_renderer_present_probe_take(uint64_t *skip_delta, uint64_t *swap_delta, + uint64_t *dirty_mark_delta, + int *force_remaining) { + if (skip_delta) { *skip_delta = s_probe_skip; s_probe_skip = 0; } + if (swap_delta) { *swap_delta = s_probe_swap; s_probe_swap = 0; } + if (dirty_mark_delta) { + *dirty_mark_delta = s_probe_dirty_marks; + s_probe_dirty_marks = 0; + } + if (force_remaining) *force_remaining = s_force_present_remaining; +} + +int gl_renderer_present_rect_dirty(int disp_x, int disp_y, int w, int h) { + if (!s_raster_ok || w <= 0 || h <= 0) return 0; + return present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1); +} + +void gl_renderer_flush_cpu_uploads(void) { + if (!s_raster_ok) return; + flush_flat_batch(); + flush_tex_batch(); + flush_cpu_upload(); +} + +/* Diagnostic (debug server "gl_fbo_peek"): read a rect of the GPU-side + * authoritative VRAM (via the pack pass + raw mirror) WITHOUT writing CPU + * VRAM — lets a probe diff FBO truth against CPU truth. Returns 0 when the + * GL pipeline is inactive (software backend). */ +int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out) { + if (!s_raster_ok || !s_ctx) return 0; + if (x < 0 || y < 0 || w < 1 || h < 1 || + x + w > VRAM_W || y + h > VRAM_H) return 0; + flush_cpu_upload(); + rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1); + pack_flush(); + coh_record(GL_COH_PEEK, x, y, x + w - 1, y + h - 1); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); + glPixelStorei(GL_PACK_ALIGNMENT, 2); + glReadPixels(x, y, w, h, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, out); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + return 1; +} + +/* Diagnostic (debug server "gl_vram_diff"): full-VRAM comparison of the + * GPU-side truth (FBO via pack) against the CPU array, WITHOUT writing + * either. Reports mismatch count + bounding box + a few sample coords. + * Divergence is expected where the GPU is legitimately ahead (gpu_dirty); + * at upload-only scenes the two must match exactly. */ +int gl_renderer_vram_diff(uint32_t *count, int bbox[4], + int samples[8][2], uint16_t samples_px[8][2]) { + if (!s_raster_ok || !s_ctx) return 0; + uint16_t *tmp = (uint16_t *)malloc((size_t)VRAM_W * VRAM_H * 2); + if (!tmp) return 0; + flush_cpu_upload(); + /* Force a full pack: the diff must read FBO truth even where the + * raw-mirror invariant (raw == FBO outside s_pack_dirty) is broken — + * a broken invariant is exactly what this tool hunts. */ + rect_add(&s_pack_dirty, 0, 0, VRAM_W - 1, VRAM_H - 1); + pack_flush(); + coh_record(GL_COH_DIFF, 0, 0, VRAM_W - 1, VRAM_H - 1); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); + glReadPixels(0, 0, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, tmp); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + uint32_t n = 0; + int x0 = VRAM_W, y0 = VRAM_H, x1 = -1, y1 = -1, ns = 0; + for (int y = 0; y < VRAM_H; y++) { + for (int x = 0; x < VRAM_W; x++) { + uint16_t f = tmp[y * VRAM_W + x], c = s_vram[y * VRAM_W + x]; + if (f == c) continue; + n++; + if (x < x0) x0 = x; if (x > x1) x1 = x; + if (y < y0) y0 = y; if (y > y1) y1 = y; + if (ns < 8 && (n % 977) == 1) { /* spread samples */ + samples[ns][0] = x; samples[ns][1] = y; + samples_px[ns][0] = f; samples_px[ns][1] = c; + ns++; + } + } + } + free(tmp); + *count = n; + bbox[0] = x0; bbox[1] = y0; bbox[2] = x1; bbox[3] = y1; + return 1 + ns; /* >=1 means valid; ns = samples filled */ +} + +/* Diagnostic state for the debug server: coherency flags + dirty rects. */ +void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]) { + if (gpu_dirty) *gpu_dirty = s_gpu_dirty; + if (pending) { + /* [0] = pending rect count; [1..4] = union bbox (diagnostic only — + * the flush itself paints the exact rects, never this union). */ + pending[0] = s_up_nrects; + pending[1] = pending[2] = pending[3] = pending[4] = 0; + for (int i = 0; i < s_up_nrects; i++) { + if (i == 0) { + pending[1] = s_up_rects[i].x0; pending[2] = s_up_rects[i].y0; + pending[3] = s_up_rects[i].x1; pending[4] = s_up_rects[i].y1; + } else { + if (s_up_rects[i].x0 < pending[1]) pending[1] = s_up_rects[i].x0; + if (s_up_rects[i].y0 < pending[2]) pending[2] = s_up_rects[i].y0; + if (s_up_rects[i].x1 > pending[3]) pending[3] = s_up_rects[i].x1; + if (s_up_rects[i].y1 > pending[4]) pending[4] = s_up_rects[i].y1; + } + } + } + if (pack) { + pack[0] = s_pack_dirty.set; + pack[1] = s_pack_dirty.x0; pack[2] = s_pack_dirty.y0; + pack[3] = s_pack_dirty.x1; pack[4] = s_pack_dirty.y1; + } +} + +/* ------------------------------------------------------------------------- * + * Native-wide compositor (GL). Mirrors gpu_sw_renderer.c's wide functions: + * canonical VRAM (the hr FBO) is untouched; framebuffer draws are also mirrored + * into per-base_x wide FBOs (see the draw funcs' wide passes). Present reads the + * displayed buffer's wide FBO via glReadPixels into the CPU present buffer, then + * the existing CPU present path uploads/letterboxes it (Option B: reuse the CPU + * present path). Self-gates on the GL pipeline being live; never runs for + * 4:3 / non-opted games (gpu.c never calls wide_configure for those). + * ------------------------------------------------------------------------- */ + +static void wide_free_all(void) { + for (int i = 0; i < WIDE_MAX_SURF; i++) { + if (s_wide_fbo[i]) { p_glDeleteFramebuffers(1, &s_wide_fbo[i]); s_wide_fbo[i] = 0; } + if (s_wide_tex[i]) { glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0; } + if (s_wide_rb[i]) { p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0; } + s_wide_base[i] = -1; + } + g_wide_cur = 0; +} + +/* Lazily allocate (or find) the wide FBO+tex for base_x. Returns the FBO id, or + * 0 on failure / more distinct buffers than WIDE_MAX_SURF. */ +static GLuint wide_fbo_for(int base_x) { + if (g_wide_w <= 0) return 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + if (s_wide_fbo[i] && s_wide_base[i] == base_x) return s_wide_fbo[i]; + for (int i = 0; i < WIDE_MAX_SURF; i++) { + if (!s_wide_fbo[i]) { + int w = g_wide_w * s_scale, h = VRAM_H * s_scale; + s_cw_fbo_creates++; + s_wide_tex[i] = make_tex(GL_RGBA8, w, h, GL_RGBA, GL_UNSIGNED_BYTE); + /* Depth-stencil RB, same as the hr FBO: the stencil carries the + * PSX mask-bit mirror for the wide surface, and (the hard lesson) + * a stencil-less FBO turns every stencil-enabled mirror draw into + * ~0.6ms of driver-side work — the 16:9 GL perf collapse. */ + p_glGenRenderbuffers(1, &s_wide_rb[i]); + p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_wide_rb[i]); + p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, w, h); + if (!make_fbo(&s_wide_fbo[i], s_wide_tex[i], s_wide_rb[i])) { + glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0; + p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0; + return 0; + } + /* Clear to black so unwritten margins are clean (not stale). */ + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_wide_fbo[i]); + glDisable(GL_SCISSOR_TEST); + glClearColor(0, 0, 0, 0); + glClearStencil(0); + glStencilMask(0xFF); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + s_wide_base[i] = base_x; + return s_wide_fbo[i]; + } + } + return 0; /* more distinct buffers than WIDE_MAX_SURF — shouldn't happen */ +} + +/* Enable native-wide with a wide width + centering offset (native px), or + * disable (wide_w <= 0). Re-allocates if the width changed. Mirrors + * sw_wide_configure. */ +static void glb_wide_configure(int wide_w, int offset) { + if (!s_raster_ok) return; + double t0 = cw_ms(); s_cw_wide_cfgs++; + flush_tex_batch(); /* a queued batch's wide mirror targets the CURRENT surfaces */ + if (wide_w <= 0) { wide_free_all(); g_wide_w = 0; g_wide_off = 0; s_cw_wide_ms += cw_ms() - t0; return; } + if (wide_w != g_wide_w) wide_free_all(); + g_wide_w = wide_w; + g_wide_off = offset; + s_cw_wide_ms += cw_ms() - t0; +} + +/* Select the wide surface to mirror into for the back buffer at base_x. */ +static void glb_wide_set_target(int base_x) { + if (!s_raster_ok) { g_wide_cur = 0; return; } + double t0 = cw_ms(); s_cw_wide_sets++; + flush_tex_batch(); /* drain into the OLD target before switching */ + g_wide_cur = wide_fbo_for(base_x); + g_wide_cur_base = base_x; + s_cw_wide_ms += cw_ms() - t0; +} + +/* Stop mirroring (offscreen draws that don't target a framebuffer). */ +static void glb_wide_disable_target(void) { flush_tex_batch(); g_wide_cur = 0; } + +/* Mirror a framebuffer clear: fill the full wide width over [y, y+h) of the + * surface for base_x, so the revealed margins are clean. Mirrors sw_wide_clear: + * a scissored glClear with the 1555 color converted to RGBA8 (alpha = bit15). */ +static void glb_wide_clear(int base_x, int y, int h, uint16_t color) { + if (!s_raster_ok || s_ws_ablate == 1) return; + double t0 = cw_ms(); s_cw_wide_clears++; + flush_tex_batch(); + GLuint fbo = wide_fbo_for(base_x); + if (!fbo) { s_cw_wide_ms += cw_ms() - t0; return; } + gl_perf_mirror_begin(); + int H = VRAM_H * s_scale; + int y0 = y * s_scale, y1 = (y + h) * s_scale; + if (y0 < 0) y0 = 0; + if (y1 > H) y1 = H; + if (y1 <= y0) return; + float r = (color & 0x1F) / 31.0f; + float g = ((color >> 5) & 0x1F) / 31.0f; + float b = ((color >> 10) & 0x1F) / 31.0f; + float a = (color >> 15) & 1 ? 1.0f : 0.0f; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo); + glViewport(0, 0, g_wide_w * s_scale, H); + glEnable(GL_SCISSOR_TEST); + glScissor(0, y0, g_wide_w * s_scale, y1 - y0); + glClearColor(r, g, b, a); + glClearStencil((color >> 15) & 1); /* stencil mirrors bit15, like the color alpha */ + glStencilMask(0xFF); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + gl_perf_mirror_end(); + s_cw_wide_ms += cw_ms() - t0; +} + +/* Clear only the two synthetic reveal strips, preserving the centred canonical + * framebuffer. This is an opt-in transition cleanup driven by gpu.c. */ +static void glb_wide_clear_margins(int base_x, int y, int h, uint16_t color, int sides) { + if (!s_raster_ok || s_ws_ablate == 1 || g_wide_off <= 0) return; + double t0 = cw_ms(); s_cw_wide_clears++; + flush_tex_batch(); + GLuint fbo = wide_fbo_for(base_x); + if (!fbo) { s_cw_wide_ms += cw_ms() - t0; return; } + gl_perf_mirror_begin(); + int H = VRAM_H * s_scale; + int W = g_wide_w * s_scale; + int margin = g_wide_off * s_scale; + int y0 = y * s_scale, y1 = (y + h) * s_scale; + if (y0 < 0) y0 = 0; + if (y1 > H) y1 = H; + if (y1 <= y0 || margin * 2 >= W) { + gl_perf_mirror_end(); + s_cw_wide_ms += cw_ms() - t0; + return; + } + float r = (color & 0x1F) / 31.0f; + float g = ((color >> 5) & 0x1F) / 31.0f; + float b = ((color >> 10) & 0x1F) / 31.0f; + float a = (color >> 15) & 1 ? 1.0f : 0.0f; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo); + glViewport(0, 0, W, H); + glEnable(GL_SCISSOR_TEST); + glClearColor(r, g, b, a); + glClearStencil((color >> 15) & 1); + glStencilMask(0xFF); + if (sides & 1) { + glScissor(0, y0, margin, y1 - y0); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + } + if (sides & 2) { + glScissor(W - margin, y0, margin, y1 - y0); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + } + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + gl_perf_mirror_end(); + s_cw_wide_ms += cw_ms() - t0; +} + +/* Present source: read the wide FBO for the displayed buffer (base_x) into the + * CPU present buffer as ARGB8888, byte-identical to sw_render_wide_display's + * output so the shared CPU present path consumes it the same way. Output is + * (g_wide_w*scale) wide × (disp_h*scale) tall. Returns pixel count (>0), or 0 + * if no surface exists for base_x (caller falls back to the canonical present). + * + * PIXEL FORMAT: glReadPixels(GL_BGRA, GL_UNSIGNED_BYTE) yields, per pixel, the + * little-endian uint32 0xAARRGGBB == ARGB8888 — exactly what rgb555_to_argb + * produces and what upload_present_tex feeds to glTexImage2D(GL_BGRA,...). The + * SW path forces alpha to 0xFF; we OR it in to match (present ignores alpha, but + * we keep the two paths bit-identical). GL's read origin is bottom-left, so the + * block is read bottom-to-top and copied into `out` reversed (out row 0 = the + * display's top scanline, as the SW path and the PRESENT_VS V-flip expect). */ +static int glb_render_wide_display(uint32_t *out, int pitch, int base_x, + int disp_y, int disp_h) { + if (!s_raster_ok || !s_ctx || g_wide_w <= 0) return 0; + GLuint fbo = 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + if (s_wide_fbo[i] && s_wide_base[i] == base_x) { fbo = s_wide_fbo[i]; break; } + if (!fbo) return 0; + + /* Fold any pending CPU->VRAM uploads into the canonical FBO first (uploads + * are never mirrored to wide, but draws after them are; keep op order) and + * make sure all wide-FBO draws have completed before the readback. */ + flush_tex_batch(); + flush_cpu_upload(); + wide_blit_center(fbo, base_x, disp_y, disp_h); /* fast-path: authoritative centre before readback */ + glFinish(); + + int W = g_wide_w * s_scale; + int H = VRAM_H * s_scale; + int out_h = disp_h * s_scale; + int ry0 = disp_y * s_scale; + if (ry0 < 0) ry0 = 0; + if (ry0 + out_h > H) out_h = H - ry0; + if (out_h <= 0) return 0; + + uint32_t *tmp = (uint32_t *)malloc((size_t)W * out_h * sizeof(uint32_t)); + if (!tmp) return 0; + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glReadPixels(0, ry0, W, out_h, GL_BGRA, GL_UNSIGNED_BYTE, tmp); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + + /* Orientation: the wide FBO stores PS1 y inverted (geo shader maps vram_y=0 + * to NDC y=-1 = FBO BOTTOM), and glReadPixels reads bottom-up — the two + * inversions CANCEL, so glReadPixels row 0 already = PS1 top scanline. Copy + * straight (NO flip) so `out` is top-down, matching sw_render_wide_display + * (which the shared CPU present path + its PRESENT_VS V-flip expect). Force + * alpha = 0xFF (match SW). [An earlier reversal here made the frame + * upside-down.] */ + int count = 0; + for (int row = 0; row < out_h; row++) { + const uint32_t *src = tmp + (size_t)row * W; + uint32_t *dst = (uint32_t *)((uint8_t *)out + (size_t)row * pitch); + for (int col = 0; col < W; col++) { dst[col] = src[col] | 0xFF000000u; count++; } + } + free(tmp); + return count; +} + +/* Dump the ENTIRE wide compositor surface for base_x (all double-buffer bands + + * both reveal margins), g_wide_w x VRAM_H at scale. Debug/inspection tool (TCP + * wide_full) — the GL analog of sw_wide_dump_full, so native-wide can be + * inspected without touching the game window. Runs the same authoritative + * centre blit first so the dump matches what present shows. Top-down, alpha=FF + * (matches sw_wide_dump_full / render_wide_display orientation). */ +static int glb_wide_dump_full(uint32_t *out, int cap_pixels, int *ow, int *oh, + int base_x) { + if (!s_raster_ok || !s_ctx || g_wide_w <= 0) return 0; + GLuint fbo = 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + if (s_wide_fbo[i] && s_wide_base[i] == base_x) { fbo = s_wide_fbo[i]; break; } + if (!fbo) return 0; + flush_tex_batch(); + flush_cpu_upload(); + wide_blit_center(fbo, base_x, 0, VRAM_H); /* authoritative centre (full height) */ + glFinish(); + int W = g_wide_w * s_scale; + int H = VRAM_H * s_scale; + if (cap_pixels > 0 && (long)W * H > cap_pixels) { H = cap_pixels / W; if (H <= 0) return 0; } + uint32_t *tmp = (uint32_t *)malloc((size_t)W * H * sizeof(uint32_t)); + if (!tmp) return 0; + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glReadPixels(0, 0, W, H, GL_BGRA, GL_UNSIGNED_BYTE, tmp); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + /* Same orientation reasoning as glb_render_wide_display: glReadPixels row 0 = + * PS1 top scanline, so copy straight (top-down). */ + int count = 0; + for (int i = 0; i < W * H; i++) { out[i] = tmp[i] | 0xFF000000u; count++; } + free(tmp); + if (ow) *ow = W; + if (oh) *oh = H; + return count; +} + +/* THE present path for 15-bit frames: blit the display region from the + * authoritative hr FBO into a letterboxed rect. Deterministic — runs + * every 15-bit frame regardless of what mix of ops produced it. + * force_4_3 pins to native 4:3 (15-bit MDEC FMV frames on a wide aspect). */ +/* ===================== frame_perf: per-frame GPU/CPU phase timing ============ + * Developer builds use two GL_TIME_ELAPSED queries per frame to bracket (a) the + * scene draws (all GP0 raster issued between two presents) and (b) the present + * clear+blit, giving TRUE GPU time per phase independent of CPU/GPU overlap + * (glFinish would only catch the non-overlapped tail and mislead). CPU wall time + * (present-to-present total, and the present call) comes from SDL perf counters. + * Results are read back GLPERF_NBUF frames late (no pipeline stall) into a ring + * the debug server's frame_perf command aggregates. Release builds compile out + * the debug server, so they also leave this instrumentation disabled: a native- + * wide frame can otherwise issue hundreds of unused mirror timestamp queries. + * One question this answers in a diagnostics build: + * where does a 16:9 frame go vs 4:3 — scene fill, wide composite, or CPU. */ +#define GLPERF_NBUF 4 +#define GLPERF_RING 256 +typedef struct { + double total_ms; /* present-entry to present-entry (full frame) */ + double present_wall_ms; /* CPU wall time inside the present call */ + double scene_gpu_ms; /* GPU: all scene draws this frame */ + double present_gpu_ms; /* GPU: the present clear+blit */ + double mirror_gpu_ms; /* GPU: of scene_gpu, the native-wide mirror passes */ + double prims; /* scene primitives submitted this frame */ + double mirror_passes; /* mirror passes this frame (measured + overflow) */ + double cw_flush_ms; /* CPU wall inside flush_tex_batch this frame */ + double cw_wide_ms; /* CPU wall inside glb_wide_* this frame */ + double batches; /* flush_tex_batch draws this frame */ + double wide_sets; /* glb_wide_set_target calls this frame */ + double fbo_creates; /* wide FBO+tex creations this frame */ + int wide; /* 1 = native-wide (16:9) present, 0 = 4:3 */ + uint64_t frame; +} GlPerfSample; + +/* Native-wide mirror-pass GPU attribution: the scene TIME_ELAPSED query spans + * the whole frame (queries of one target cannot nest), so each mirror pass is + * bracketed with a GL_TIMESTAMP pair instead (glQueryCounter does not conflict + * with an active TIME_ELAPSED query). Pairs are pooled per buffered frame and + * summed at readback, splitting scene_gpu into canonical vs mirror cost. */ +#define GLPERF_MIRQ 1024 /* measured mirror passes per frame */ +static GLuint s_mq_q[GLPERF_NBUF][GLPERF_MIRQ * 2]; +static int s_mq_n[GLPERF_NBUF]; /* pairs recorded this frame */ +static int s_mq_over[GLPERF_NBUF]; /* passes beyond the pool (counted, untimed) */ +static int s_mq_open = 0; /* begin issued, end pending */ +static int s_mq_ok = 0; /* glQueryCounter available */ + +static int s_pf_on = 0; +static GLuint s_pf_scene_q[GLPERF_NBUF]; +static GLuint s_pf_present_q[GLPERF_NBUF]; +static int s_pf_b = 0; /* buffer for the CURRENT frame */ +static int s_pf_scene_active = 0; +static uint64_t s_pf_count = 0; +static uint64_t s_pf_freq = 1; +static uint64_t s_pf_last_enter = 0; +static uint64_t s_pf_enter = 0; +static double s_pf_total_pending = 0.0; +static double s_pf_buf_total[GLPERF_NBUF]; +static double s_pf_buf_pwall[GLPERF_NBUF]; +static double s_pf_buf_prims[GLPERF_NBUF]; +static int s_pf_buf_wide[GLPERF_NBUF]; +static uint64_t s_pf_buf_frame[GLPERF_NBUF]; +static uint64_t s_pf_prims_last = 0; +static double s_pf_prims_pending = 0.0; +static double s_pf_cw_pending[5]; /* flush_ms, wide_ms, batches, wide_sets, fbo_creates */ +static double s_pf_buf_cw[GLPERF_NBUF][5]; +static GlPerfSample s_pf_ring[GLPERF_RING]; +static uint64_t s_pf_ring_seq = 0; + +static void gl_perf_init(void) { +#ifdef PSX_NO_DEBUG_TOOLS + return; +#else + /* Timer queries are intentionally available in diagnostics builds, but a + * driver may serialize command submission while collecting them. Keep an + * escape hatch so frame cadence can be A/B tested without rebuilding or + * losing the rest of the debug server telemetry. */ + { + const char *enabled = getenv("PSX_GL_PERF"); + if (enabled && enabled[0] == '0') return; + } + if (!p_glGenQueries || !p_glBeginQuery || !p_glEndQuery || !p_glGetQueryObjectui64v) return; + p_glGenQueries(GLPERF_NBUF, s_pf_scene_q); + p_glGenQueries(GLPERF_NBUF, s_pf_present_q); + s_mq_ok = (p_glQueryCounter != NULL); + if (s_mq_ok) + for (int i = 0; i < GLPERF_NBUF; i++) { + p_glGenQueries(GLPERF_MIRQ * 2, s_mq_q[i]); + s_mq_n[i] = 0; s_mq_over[i] = 0; + } + s_mq_open = 0; + s_pf_freq = SDL_GetPerformanceFrequency(); + if (!s_pf_freq) s_pf_freq = 1; + s_pf_b = 0; s_pf_scene_active = 0; s_pf_count = 0; s_pf_ring_seq = 0; + s_pf_last_enter = 0; + s_pf_on = 1; +#endif +} + +/* Bracket ONE native-wide mirror pass (called from the wide-mirror draw sites). + * Timestamp pairs, not TIME_ELAPSED — see the pool comment above. */ +static void gl_perf_mirror_begin(void) { + if (!s_pf_on || !s_mq_ok) return; + int b = s_pf_b; + if (s_mq_n[b] >= GLPERF_MIRQ) { s_mq_over[b]++; return; } + p_glQueryCounter(s_mq_q[b][s_mq_n[b] * 2], GL_TIMESTAMP); + s_mq_open = 1; +} +static void gl_perf_mirror_end(void) { + if (!s_pf_on || !s_mq_ok || !s_mq_open) return; + int b = s_pf_b; + p_glQueryCounter(s_mq_q[b][s_mq_n[b] * 2 + 1], GL_TIMESTAMP); + s_mq_n[b]++; + s_mq_open = 0; +} + +/* Top of present (after flush_cpu_upload, before clear/blit). */ +static void gl_perf_present_enter(void) { + /* Per-frame boundary for the 2D-backdrop stretch — runs from BOTH present + * paths (4:3 present_vram AND native-wide present_wide_fbo), before the + * perf-on gate so native-wide frames reset too. Snapshot this frame's + * backdrop-stretch diagnostics, then reset the per-frame counters. (The gate + * is per-prim now — no draw-order phase to reset.) Final batch already flushed + * by the caller. */ + g_bdg_applied = s_bdg_applied; g_bdg_prims = s_bdg_prims; g_bdg_clearx = s_bdg_clearx; + g_bdg_cur = (g_wide_cur != 0); g_bdg_base = g_wide_cur_base; g_bdg_w = g_wide_w; g_bdg_off = g_wide_off; + s_bdg_applied = 0; s_bdg_prims = 0; s_bdg_clearx = -999999; + { extern void psx_ws_dbg_gate_frame_snapshot(void); psx_ws_dbg_gate_frame_snapshot(); } + if (!s_pf_on) return; + uint64_t now = SDL_GetPerformanceCounter(); + s_pf_enter = now; + s_pf_total_pending = s_pf_last_enter + ? (double)(now - s_pf_last_enter) * 1000.0 / (double)s_pf_freq : 0.0; + s_pf_last_enter = now; + s_pf_prims_pending = (double)(s_scene_prims - s_pf_prims_last); /* prims drawn this frame */ + s_pf_prims_last = s_scene_prims; + s_pf_cw_pending[0] = s_cw_flush_ms; s_pf_cw_pending[1] = s_cw_wide_ms; + s_pf_cw_pending[2] = (double)s_cw_batches; + s_pf_cw_pending[3] = (double)s_cw_wide_sets; + s_pf_cw_pending[4] = (double)s_cw_fbo_creates; + s_cw_flush_ms = 0.0; s_cw_wide_ms = 0.0; + s_cw_batches = 0; s_cw_wide_sets = 0; s_cw_wide_cfgs = 0; + s_cw_wide_clears = 0; s_cw_fbo_creates = 0; + if (s_pf_scene_active) { p_glEndQuery(GL_TIME_ELAPSED); s_pf_scene_active = 0; } /* end frame b's scene draws */ + p_glBeginQuery(GL_TIME_ELAPSED, s_pf_present_q[s_pf_b]); /* time frame b's present */ +} + +/* End of present (after SwapWindow). wide = native-wide path. */ +static void gl_perf_present_exit(int wide) { + if (!s_pf_on) return; + uint64_t now = SDL_GetPerformanceCounter(); + p_glEndQuery(GL_TIME_ELAPSED); /* end present_q[b] */ + s_pf_buf_total[s_pf_b] = s_pf_total_pending; + s_pf_buf_pwall[s_pf_b] = (double)(now - s_pf_enter) * 1000.0 / (double)s_pf_freq; + s_pf_buf_prims[s_pf_b] = s_pf_prims_pending; + for (int ci = 0; ci < 5; ci++) s_pf_buf_cw[s_pf_b][ci] = s_pf_cw_pending[ci]; + s_pf_buf_wide[s_pf_b] = wide; + s_pf_buf_frame[s_pf_b] = s_pf_count; + int rd = (s_pf_b + 1) % GLPERF_NBUF; /* oldest buffer (frame count+1-NBUF), now done */ + if (s_pf_count >= (uint64_t)GLPERF_NBUF) { + GLuint64 sc = 0, pr = 0; + p_glGetQueryObjectui64v(s_pf_scene_q[rd], GL_QUERY_RESULT, &sc); + p_glGetQueryObjectui64v(s_pf_present_q[rd], GL_QUERY_RESULT, &pr); + double mir = 0.0; + for (int i = 0; i < s_mq_n[rd]; i++) { /* sum that frame's mirror pairs */ + GLuint64 t0 = 0, t1 = 0; + p_glGetQueryObjectui64v(s_mq_q[rd][i * 2], GL_QUERY_RESULT, &t0); + p_glGetQueryObjectui64v(s_mq_q[rd][i * 2 + 1], GL_QUERY_RESULT, &t1); + if (t1 > t0) mir += (double)(t1 - t0); + } + GlPerfSample *s = &s_pf_ring[s_pf_ring_seq % GLPERF_RING]; + s->total_ms = s_pf_buf_total[rd]; + s->present_wall_ms = s_pf_buf_pwall[rd]; + s->scene_gpu_ms = (double)sc / 1.0e6; + s->present_gpu_ms = (double)pr / 1.0e6; + s->mirror_gpu_ms = mir / 1.0e6; + s->prims = s_pf_buf_prims[rd]; + s->mirror_passes = (double)(s_mq_n[rd] + s_mq_over[rd]); + s->cw_flush_ms = s_pf_buf_cw[rd][0]; + s->cw_wide_ms = s_pf_buf_cw[rd][1]; + s->batches = s_pf_buf_cw[rd][2]; + s->wide_sets = s_pf_buf_cw[rd][3]; + s->fbo_creates = s_pf_buf_cw[rd][4]; + s->wide = s_pf_buf_wide[rd]; + s->frame = s_pf_buf_frame[rd]; + s_pf_ring_seq++; + } + s_mq_n[rd] = 0; s_mq_over[rd] = 0; /* rd becomes the next frame's buffer */ + s_pf_count++; + s_pf_b = rd; /* reuse oldest for next frame */ + p_glBeginQuery(GL_TIME_ELAPSED, s_pf_scene_q[s_pf_b]); /* open next frame's scene draws */ + s_pf_scene_active = 1; +} + +/* Aggregate the ring for the debug server. wide_filter: -1 all, 0 = 4:3, 1 = wide. + * out[0]=count, [1]=total_avg, [2]=total_max, [3]=emu_cpu_avg (total-present_wall), + * [4]=present_wall_avg, [5]=scene_gpu_avg, [6]=scene_gpu_max, [7]=present_gpu_avg, + * [8]=present_gpu_max. Returns the sample count. */ +/* Cumulative textured fraction of scene prims (decides flat vs textured batching + * priority). out_tex_frac = textured/total since boot; returns total prim count. */ +uint64_t gl_renderer_perf_prim_split(double *out_tex_frac) { + if (out_tex_frac) *out_tex_frac = s_scene_prims ? (double)s_scene_prims_tex / (double)s_scene_prims : 0.0; + return s_scene_prims; +} + +int gl_renderer_perf_aggregate(int wide_filter, double out[18]) { + for (int i = 0; i < 18; i++) out[i] = 0.0; + if (!s_pf_on) return 0; + int navail = (int)(s_pf_ring_seq < (uint64_t)GLPERF_RING ? s_pf_ring_seq : GLPERF_RING); + uint64_t start = s_pf_ring_seq - (uint64_t)navail; + int n = 0; + for (int i = 0; i < navail; i++) { + const GlPerfSample *s = &s_pf_ring[(start + i) % GLPERF_RING]; + if (wide_filter >= 0 && s->wide != wide_filter) continue; + double emu = s->total_ms - s->present_wall_ms; if (emu < 0) emu = 0; + out[1] += s->total_ms; if (s->total_ms > out[2]) out[2] = s->total_ms; + out[3] += emu; + out[4] += s->present_wall_ms; + out[5] += s->scene_gpu_ms; if (s->scene_gpu_ms > out[6]) out[6] = s->scene_gpu_ms; + out[7] += s->present_gpu_ms; if (s->present_gpu_ms > out[8]) out[8] = s->present_gpu_ms; + out[9] += s->prims; + out[10] += s->mirror_gpu_ms; if (s->mirror_gpu_ms > out[11]) out[11] = s->mirror_gpu_ms; + out[12] += s->mirror_passes; + out[13] += s->cw_flush_ms; + out[14] += s->cw_wide_ms; + out[15] += s->batches; + out[16] += s->wide_sets; + out[17] += s->fbo_creates; + n++; + } + if (n) { + out[1]/=n; out[3]/=n; out[4]/=n; out[5]/=n; out[7]/=n; out[9]/=n; out[10]/=n; out[12]/=n; + out[13]/=n; out[14]/=n; out[15]/=n; out[16]/=n; out[17]/=n; + } + out[0] = (double)n; + return n; +} + +/* Native-wide mirror ablation (perf attribution): see s_ws_ablate. */ +void gl_renderer_set_ws_ablate(int mode) { s_ws_ablate = (mode >= 0 && mode <= 3) ? mode : 0; } +int gl_renderer_get_ws_ablate(void) { return s_ws_ablate; } + static void interp_reset_history_unlocked(void) { s_interp_valid = 0; s_interp_w = s_interp_h = 0; @@ -3862,55 +3862,55 @@ void gl_renderer_set_interpolation_suspended(int suspended) { int gl_renderer_interpolation_owns_cadence(void) { return s_ctx && s_interp_enabled && !s_interp_suspended; } - -void gl_renderer_interpolation_diag(int *enabled, int *suspended, - int *history_frames, - double *host_hz, double *target_hz, - uint64_t *swaps) { + +void gl_renderer_interpolation_diag(int *enabled, int *suspended, + int *history_frames, + double *host_hz, double *target_hz, + uint64_t *swaps) { if (enabled) *enabled = s_interp_enabled; - if (suspended) *suspended = s_interp_suspended; - if (history_frames) *history_frames = s_interp_valid; - if (host_hz) *host_hz = s_interp_host_hz; + if (suspended) *suspended = s_interp_suspended; + if (history_frames) *history_frames = s_interp_valid; + if (host_hz) *host_hz = s_interp_host_hz; if (target_hz) *target_hz = s_interp_target_hz; if (swaps) *swaps = s_interp_swaps; } - + /* Copy a stable display image out of the mutable VRAM/wide render target. * Returns true when temporal blending owns this source-frame interval. */ -static int interp_capture(GLuint fbo, int x, int y, int w, int h, - int linear, int force_4_3, int source_path) { +static int interp_capture(GLuint fbo, int x, int y, int w, int h, + int linear, int force_4_3, int source_path) { if (!s_interp_enabled || s_interp_suspended || !fbo || w <= 0 || h <= 0) return 0; - int pw = w * s_scale, ph = h * s_scale; - if (pw != s_interp_w || ph != s_interp_h || - source_path != s_interp_source_path || force_4_3 != s_interp_force_4_3) { - s_interp_valid = 0; - s_interp_w = pw; s_interp_h = ph; - s_interp_prev = s_interp_cur = 0; - for (int i = 0; i < 3; i++) { - glBindTexture(GL_TEXTURE_2D, s_interp_tex[i]); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, pw, ph, 0, - GL_RGBA, GL_UNSIGNED_BYTE, NULL); - } - } - - int dst = 0; - if (s_interp_valid == 1) dst = s_interp_cur == 0 ? 1 : 0; - else if (s_interp_valid >= 2) dst = s_interp_prev; + int pw = w * s_scale, ph = h * s_scale; + if (pw != s_interp_w || ph != s_interp_h || + source_path != s_interp_source_path || force_4_3 != s_interp_force_4_3) { + s_interp_valid = 0; + s_interp_w = pw; s_interp_h = ph; + s_interp_prev = s_interp_cur = 0; + for (int i = 0; i < 3; i++) { + glBindTexture(GL_TEXTURE_2D, s_interp_tex[i]); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, pw, ph, 0, + GL_RGBA, GL_UNSIGNED_BYTE, NULL); + } + } + + int dst = 0; + if (s_interp_valid == 1) dst = s_interp_cur == 0 ? 1 : 0; + else if (s_interp_valid >= 2) dst = s_interp_prev; p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo); - glBindTexture(GL_TEXTURE_2D, s_interp_tex[dst]); - glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, - x * s_scale, y * s_scale, pw, ph); + glBindTexture(GL_TEXTURE_2D, s_interp_tex[dst]); + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, + x * s_scale, y * s_scale, pw, ph); p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - if (s_interp_valid == 0) { - s_interp_cur = dst; - s_interp_valid = 1; - } else { - s_interp_prev = s_interp_cur; - s_interp_cur = dst; - s_interp_valid = 2; - } - s_interp_linear = linear; - s_interp_force_4_3 = force_4_3; + if (s_interp_valid == 0) { + s_interp_cur = dst; + s_interp_valid = 1; + } else { + s_interp_prev = s_interp_cur; + s_interp_cur = dst; + s_interp_valid = 2; + } + s_interp_linear = linear; + s_interp_force_4_3 = force_4_3; s_interp_source_path = source_path; s_interp_captures++; return 1; @@ -3918,54 +3918,54 @@ static int interp_capture(GLuint fbo, int x, int y, int w, int h, static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh) { int prev = s_interp_prev, curr = s_interp_cur; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - glViewport(lx, ly, lw, lh); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_interp_tex[prev]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); - p_glActiveTexture(PSXGL_TEXTURE0 + 1); - glBindTexture(GL_TEXTURE_2D, s_interp_tex[curr]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); - p_glUseProgram(s_interp_prog); - p_glUniform1i(s_interp_uPrev, 0); - p_glUniform1i(s_interp_uCurr, 1); - p_glUniform1f(s_interp_uAlpha, alpha); - p_glUniform1i(s_interp_uBlendMode, s_interp_blend_mode); - p_glUniform4f(s_interp_uUvRect, 0.f, 0.f, 1.f, 1.f); - /* Interp textures hold exactly the display rect (uv_rect is 0..1), so pitch - * == display height == s_interp_h. */ - INTERP_SCANLINE(s_interp_h, s_interp_h, lh); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + glViewport(lx, ly, lw, lh); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_interp_tex[prev]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); + p_glActiveTexture(PSXGL_TEXTURE0 + 1); + glBindTexture(GL_TEXTURE_2D, s_interp_tex[curr]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); + p_glUseProgram(s_interp_prog); + p_glUniform1i(s_interp_uPrev, 0); + p_glUniform1i(s_interp_uCurr, 1); + p_glUniform1f(s_interp_uAlpha, alpha); + p_glUniform1i(s_interp_uBlendMode, s_interp_blend_mode); + p_glUniform4f(s_interp_uUvRect, 0.f, 0.f, 1.f, 1.f); + /* Interp textures hold exactly the display rect (uv_rect is 0..1), so pitch + * == display height == s_interp_h. */ + INTERP_SCANLINE(s_interp_h, s_interp_h, lh); p_glBindVertexArray(s_present_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); - p_glBindVertexArray(0); - p_glUseProgram(0); - p_glActiveTexture(PSXGL_TEXTURE0); -} - + glDrawArrays(GL_TRIANGLES, 0, 3); + p_glBindVertexArray(0); + p_glUseProgram(0); + p_glActiveTexture(PSXGL_TEXTURE0); +} + static int interp_present(float alpha) { if (!s_ctx || !s_interp_enabled || s_interp_suspended || s_interp_valid < 1) return 0; - int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - int lx, ly, lw, lh; - if (s_interp_force_4_3) - letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); - else - letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + int lx, ly, lw, lh; + if (s_interp_force_4_3) + letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); + else + letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); if (lx != 0 || ly != 0 || lw != ww || lh != wh) { glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT); } present_bezel(ww, wh, lx, ly, lw, lh); interp_draw_quad(alpha, lx, ly, lw, lh); - pres_record(GL_PRES_INTERP, 0, 0, s_interp_w, s_interp_h, - lx, ly, lw, lh); + pres_record(GL_PRES_INTERP, 0, 0, s_interp_w, s_interp_h, + lx, ly, lw, lh); gl_swap_with_osd(); - s_interp_swaps++; - return 1; + s_interp_swaps++; + return 1; } static void interp_wait_until(uint64_t deadline, uint64_t frequency) { @@ -4021,98 +4021,98 @@ static void interp_present_source_interval(void) { diag_swaps = s_interp_swaps; } } - -/* Draw one host OSD ARGB image into the default framebuffer at (vx,vy) - * in top-left window coordinates (y down). Bitmap is ow×oh; viewport is - * dw×dh (may upscale for HiDPI / large windows). */ -static void gl_draw_osd_image(const uint32_t *px, int ow, int oh, - int dw, int dh, int vx, int vy, int ww, int wh) { - if (!px || ow <= 0 || oh <= 0 || dw <= 0 || dh <= 0 || - !s_present_prog || ww <= 0 || wh <= 0) - return; - if (!s_osd_tex) glGenTextures(1, &s_osd_tex); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_osd_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - if (s_osd_tw != ow || s_osd_th != oh) { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, ow, oh, 0, - GL_BGRA, GL_UNSIGNED_BYTE, px); - s_osd_tw = ow; - s_osd_th = oh; - } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, ow, oh, - GL_BGRA, GL_UNSIGNED_BYTE, px); - } - if (vx + dw > ww) dw = ww - vx; - if (vy + dh > wh) dh = wh - vy; - if (dw < 1 || dh < 1) return; - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); - glDisable(GL_SCISSOR_TEST); - glDisable(GL_STENCIL_TEST); - glDisable(GL_DEPTH_TEST); - /* host_osd bakes opaque panels (A=0xFF). Do not blend — PSX mode-2 - * REVERSE_SUBTRACT left armed across FMV present made toasts solid black. */ - glDisable(GL_BLEND); - if (p_glBlendEquationSeparate) - p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); - /* GL viewport origin is bottom-left. */ - glViewport(vx, wh - vy - dh, dw, dh); - p_glUseProgram(s_present_prog); - p_glUniform1i(s_present_uTex, 0); + +/* Draw one host OSD ARGB image into the default framebuffer at (vx,vy) + * in top-left window coordinates (y down). Bitmap is ow×oh; viewport is + * dw×dh (may upscale for HiDPI / large windows). */ +static void gl_draw_osd_image(const uint32_t *px, int ow, int oh, + int dw, int dh, int vx, int vy, int ww, int wh) { + if (!px || ow <= 0 || oh <= 0 || dw <= 0 || dh <= 0 || + !s_present_prog || ww <= 0 || wh <= 0) + return; + if (!s_osd_tex) glGenTextures(1, &s_osd_tex); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_osd_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + if (s_osd_tw != ow || s_osd_th != oh) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, ow, oh, 0, + GL_BGRA, GL_UNSIGNED_BYTE, px); + s_osd_tw = ow; + s_osd_th = oh; + } else { + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, ow, oh, + GL_BGRA, GL_UNSIGNED_BYTE, px); + } + if (vx + dw > ww) dw = ww - vx; + if (vy + dh > wh) dh = wh - vy; + if (dw < 1 || dh < 1) return; + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_STENCIL_TEST); + glDisable(GL_DEPTH_TEST); + /* host_osd bakes opaque panels (A=0xFF). Do not blend — PSX mode-2 + * REVERSE_SUBTRACT left armed across FMV present made toasts solid black. */ + glDisable(GL_BLEND); + if (p_glBlendEquationSeparate) + p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); + /* GL viewport origin is bottom-left. */ + glViewport(vx, wh - vy - dh, dw, dh); + p_glUseProgram(s_present_prog); + p_glUniform1i(s_present_uTex, 0); present_set_sharp(0, 0, 0, 0, 0); /* OSD is authored at output res */ - PRESENT_SCANLINE(0, 0, 0); /* never scanline the host OSD */ - /* Host OSD bitmaps are top-down (row 0 = top), same as guest CPU - * present with v_flip=1: uv (0,0)-(1,1). (0,1)-(1,0) was the hold-last - * cancel for already-oriented captures and made toasts upside-down. */ - p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f); - { + PRESENT_SCANLINE(0, 0, 0); /* never scanline the host OSD */ + /* Host OSD bitmaps are top-down (row 0 = top), same as guest CPU + * present with v_flip=1: uv (0,0)-(1,1). (0,1)-(1,0) was the hold-last + * cancel for already-oriented captures and made toasts upside-down. */ + p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f); + { p_glBindVertexArray(s_present_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); - p_glBindVertexArray(0); - } - p_glUseProgram(0); -} - -/* Composite host toast + volume bar into the default framebuffer, then swap. */ -static void gl_swap_with_osd(void) { - if (s_present_prog && s_ctx) { - int ww = 0, wh = 0; - SDL_GL_GetDrawableSize(s_win, &ww, &wh); - if (ww > 0 && wh > 0) { - const uint32_t *px = NULL; - int ow = 0, oh = 0; - /* OSD authored for ~480-tall present; grow with drawable height. */ - int ui = wh / 480; - int margin; - if (ui < 1) ui = 1; - if (ui > 8) ui = 8; - margin = 8 * ui; - if (host_osd_image(&px, &ow, &oh) && px) - gl_draw_osd_image(px, ow, oh, ow * ui, oh * ui, - margin, margin, ww, wh); - if (host_osd_volume_image(&px, &ow, &oh) && px) { - const int dw = ow * ui, dh = oh * ui; - int vx = (ww > dw + margin) ? (ww - dw - margin) : margin; - int vy = (wh > dh) ? ((wh - dh) / 2) : margin; - gl_draw_osd_image(px, ow, oh, dw, dh, vx, vy, ww, wh); - } + glDrawArrays(GL_TRIANGLES, 0, 3); + p_glBindVertexArray(0); + } + p_glUseProgram(0); +} + +/* Composite host toast + volume bar into the default framebuffer, then swap. */ +static void gl_swap_with_osd(void) { + if (s_present_prog && s_ctx) { + int ww = 0, wh = 0; + SDL_GL_GetDrawableSize(s_win, &ww, &wh); + if (ww > 0 && wh > 0) { + const uint32_t *px = NULL; + int ow = 0, oh = 0; + /* OSD authored for ~480-tall present; grow with drawable height. */ + int ui = wh / 480; + int margin; + if (ui < 1) ui = 1; + if (ui > 8) ui = 8; + margin = 8 * ui; + if (host_osd_image(&px, &ow, &oh) && px) + gl_draw_osd_image(px, ow, oh, ow * ui, oh * ui, + margin, margin, ww, wh); + if (host_osd_volume_image(&px, &ow, &oh) && px) { + const int dw = ow * ui, dh = oh * ui; + int vx = (ww > dw + margin) ? (ww - dw - margin) : margin; + int vy = (wh > dh) ? ((wh - dh) / 2) : margin; + gl_draw_osd_image(px, ow, oh, dw, dh, vx, vy, ww, wh); + } if (psx_rewind_overlay_image(&px, &ow, &oh) && px) { float slide = psx_rewind_slide(); int dw = ww; - int dh = (wh * oh) / 480; - int vy; - if (dh < 8) dh = oh; - vy = wh - (int)((float)dh * slide + 0.5f); + int dh = (wh * oh) / 480; + int vy; + if (dh < 8) dh = oh; + vy = wh - (int)((float)dh * slide + 0.5f); gl_draw_osd_image(px, ow, oh, dw, dh, 0, vy, ww, wh); } if (psx_savestate_menu_overlay_image(&px, &ow, &oh) && px) gl_draw_osd_image(px, ow, oh, ww, wh, 0, 0, ww, wh); } } - host_osd_present_done(); + host_osd_present_done(); /* present_shot (GL backend): the default framebuffer now holds the composed * frame — display quad fitted to the window, plus OSD — so this is the only * capture that carries the presented aspect. Buffer-level captures resolve @@ -4149,51 +4149,51 @@ static void gl_swap_with_osd(void) { present_shot_done(wrote); } } - SDL_GL_SwapWindow(s_win); -} - + SDL_GL_SwapWindow(s_win); +} + static void present_target_quad(GLuint tex, int tex_w, int tex_h, int x, int y, int w, int h, int linear, int lx, int ly, int lw, int lh, int v_flip) { - float u0, v0, u1, v1; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - glViewport(lx, ly, lw, lh); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); - p_glUseProgram(s_present_prog); - p_glUniform1i(s_present_uTex, 0); + float u0, v0, u1, v1; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + glViewport(lx, ly, lw, lh); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); + p_glUseProgram(s_present_prog); + p_glUniform1i(s_present_uTex, 0); /* The rasterized path already renders at the internal scale, so it has no * low-res source to reconstruct — keep the plain sample. */ present_set_sharp(0, 0, 0, 0, 0); - /* Scanline at the native line grid. v_uv is normalized against tex_h (the - * full VRAM/FBO texture), and one texel row is one PS1 scanline, so tex_h is - * the phase pitch; h is the displayed line count for the output-scale gate. - * v_flip=0 is the already-composed hold-last drawable (scanlines, if any, - * are already baked) — skip it. */ - PRESENT_SCANLINE(v_flip ? tex_h : 0, v_flip ? h : 0, lh); - /* Half-texel inset: with GL_LINEAR, corner-mapped UVs make the outermost - * dest pixels blend the border texel with VRAM outside the content rect - * (visible edge stripe with AA on). Center-mapped UVs keep edge samples - * inside the rect; interior sampling is unchanged. */ - u0 = ((float)x + 0.5f) / (float)tex_w; - v0 = ((float)y + 0.5f) / (float)tex_h; - u1 = ((float)(x + w) - 0.5f) / (float)tex_w; - v1 = ((float)(y + h) - 0.5f) / (float)tex_h; - /* PRESENT_VS always samples with mix(v0,v1,1-p.y). For CPU/FBO guest - * bands that is the correct PSX top-down → GL mapping (v_flip=1). For a - * glCopyTexSubImage2D of the already-presented drawable, the texture - * already matches screen orientation — swapping v ends cancels the - * shader flip so hold-last is not upside-down for one frame. */ - if (!v_flip) { - float t = v0; - v0 = v1; - v1 = t; - } - p_glUniform4f(s_present_uUvRect, u0, v0, u1, v1); - p_glBindVertexArray(s_present_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); + /* Scanline at the native line grid. v_uv is normalized against tex_h (the + * full VRAM/FBO texture), and one texel row is one PS1 scanline, so tex_h is + * the phase pitch; h is the displayed line count for the output-scale gate. + * v_flip=0 is the already-composed hold-last drawable (scanlines, if any, + * are already baked) — skip it. */ + PRESENT_SCANLINE(v_flip ? tex_h : 0, v_flip ? h : 0, lh); + /* Half-texel inset: with GL_LINEAR, corner-mapped UVs make the outermost + * dest pixels blend the border texel with VRAM outside the content rect + * (visible edge stripe with AA on). Center-mapped UVs keep edge samples + * inside the rect; interior sampling is unchanged. */ + u0 = ((float)x + 0.5f) / (float)tex_w; + v0 = ((float)y + 0.5f) / (float)tex_h; + u1 = ((float)(x + w) - 0.5f) / (float)tex_w; + v1 = ((float)(y + h) - 0.5f) / (float)tex_h; + /* PRESENT_VS always samples with mix(v0,v1,1-p.y). For CPU/FBO guest + * bands that is the correct PSX top-down → GL mapping (v_flip=1). For a + * glCopyTexSubImage2D of the already-presented drawable, the texture + * already matches screen orientation — swapping v ends cancels the + * shader flip so hold-last is not upside-down for one frame. */ + if (!v_flip) { + float t = v0; + v0 = v1; + v1 = t; + } + p_glUniform4f(s_present_uUvRect, u0, v0, u1, v1); + p_glBindVertexArray(s_present_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); p_glBindVertexArray(0); p_glUseProgram(0); } @@ -4237,7 +4237,7 @@ static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh) { p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); p_glUniform4f(s_present_uUvRect, 0.0f, 0.0f, 1.0f, 1.0f); - PRESENT_SCANLINE(0, 0, 0); /* bezel art is not scanlined */ + PRESENT_SCANLINE(0, 0, 0); /* bezel art is not scanlined */ p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3); p_glBindVertexArray(0); @@ -4245,69 +4245,69 @@ static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh) { } int gl_renderer_present_hold_last(void) { - int ww = 0, wh = 0; - int lx, ly, lw, lh; - if (!s_ctx || !s_win || s_hold_kind == HOLD_NONE || !s_hold_tex) - return 0; - SDL_GL_GetDrawableSize(s_win, &ww, &wh); - if (ww < 1 || wh < 1) - return 0; - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); - glClearColor(0.f, 0.f, 0.f, 1.f); - glClear(GL_COLOR_BUFFER_BIT); - if (s_hold_kind == HOLD_DRAWABLE) { - /* Captured image already includes letterbox bars. Stretch 1:1, or - * letterbox to preserve aspect if the drawable resized mid-resim. */ - lx = 0; - ly = 0; - lw = ww; - lh = wh; - if (s_hold_tw > 0 && s_hold_th > 0 && - ((int64_t)s_hold_tw * wh != (int64_t)s_hold_th * ww)) { - if ((int64_t)ww * s_hold_th > (int64_t)wh * s_hold_tw) { - lw = (int)(((int64_t)wh * s_hold_tw) / s_hold_th); - if (lw < 1) lw = 1; - lx = (ww - lw) / 2; - lh = wh; - ly = 0; - } else { - lh = (int)(((int64_t)ww * s_hold_th) / s_hold_tw); - if (lh < 1) lh = 1; - ly = (wh - lh) / 2; - lw = ww; - lx = 0; - } - } - /* v_flip=0: drawable capture is already screen-oriented. */ - present_target_quad(s_hold_tex, s_hold_tw, s_hold_th, - 0, 0, s_hold_tw, s_hold_th, 0, lx, ly, lw, lh, 0); - } else { - if (s_hold_force_4_3) - letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); - else - letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); - present_target_quad(s_hold_tex, s_hold_tw, s_hold_th, - 0, 0, s_hold_tw, s_hold_th, s_hold_linear, - lx, ly, lw, lh, 1); - /* Upgrade to drawable after letterboxing once. Live+interp leaves - * HOLD_NATIVE (no Swap on main); drawable path is the soak-proven - * full-window image (GL without interp). */ - hold_capture_drawable(); - } - latency_ring_mark(LAT_SWAP_BEGIN); - gl_swap_with_osd(); - latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; - return 1; -} - -void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear, - int force_4_3) { - if (!s_ctx || !s_raster_ok) return; - flush_flat_batch(); - flush_tex_batch(); - flush_cpu_upload(); + int ww = 0, wh = 0; + int lx, ly, lw, lh; + if (!s_ctx || !s_win || s_hold_kind == HOLD_NONE || !s_hold_tex) + return 0; + SDL_GL_GetDrawableSize(s_win, &ww, &wh); + if (ww < 1 || wh < 1) + return 0; + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); + glClearColor(0.f, 0.f, 0.f, 1.f); + glClear(GL_COLOR_BUFFER_BIT); + if (s_hold_kind == HOLD_DRAWABLE) { + /* Captured image already includes letterbox bars. Stretch 1:1, or + * letterbox to preserve aspect if the drawable resized mid-resim. */ + lx = 0; + ly = 0; + lw = ww; + lh = wh; + if (s_hold_tw > 0 && s_hold_th > 0 && + ((int64_t)s_hold_tw * wh != (int64_t)s_hold_th * ww)) { + if ((int64_t)ww * s_hold_th > (int64_t)wh * s_hold_tw) { + lw = (int)(((int64_t)wh * s_hold_tw) / s_hold_th); + if (lw < 1) lw = 1; + lx = (ww - lw) / 2; + lh = wh; + ly = 0; + } else { + lh = (int)(((int64_t)ww * s_hold_th) / s_hold_tw); + if (lh < 1) lh = 1; + ly = (wh - lh) / 2; + lw = ww; + lx = 0; + } + } + /* v_flip=0: drawable capture is already screen-oriented. */ + present_target_quad(s_hold_tex, s_hold_tw, s_hold_th, + 0, 0, s_hold_tw, s_hold_th, 0, lx, ly, lw, lh, 0); + } else { + if (s_hold_force_4_3) + letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); + else + letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); + present_target_quad(s_hold_tex, s_hold_tw, s_hold_th, + 0, 0, s_hold_tw, s_hold_th, s_hold_linear, + lx, ly, lw, lh, 1); + /* Upgrade to drawable after letterboxing once. Live+interp leaves + * HOLD_NATIVE (no Swap on main); drawable path is the soak-proven + * full-window image (GL without interp). */ + hold_capture_drawable(); + } + latency_ring_mark(LAT_SWAP_BEGIN); + gl_swap_with_osd(); + latency_ring_mark(LAT_SWAP_END); + s_probe_swap++; + return 1; +} + +void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear, + int force_4_3) { + if (!s_ctx || !s_raster_ok) return; + flush_flat_batch(); + flush_tex_batch(); + flush_cpu_upload(); if (s_force_present_remaining <= 0 && s_last_present_path == GL_PRES_VRAM && s_last_dx == disp_x && s_last_dy == disp_y && @@ -4316,191 +4316,191 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear, !host_osd_needs_present() && !psx_present_vsync_owns_cadence() && !gl_renderer_interpolation_owns_cadence()) { - s_probe_skip++; - gl_perf_present_enter(); - gl_perf_present_exit(0); - return; - } - gl_perf_present_enter(); /* per-frame backdrop-phase reset + dbg snapshot live in here */ - int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - int lx, ly, lw, lh; - if (force_4_3) - letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); - else - letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); - /* No short-band adjustment on the 15-bit FBO path: a game's native short - * display mode (e.g. 216/224-line menus) must fill the rect as before. - * The FMV band fix lives in the depth24/CPU present paths only. */ - - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); - if (lx != 0 || ly != 0 || lw != ww || lh != wh) { - glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); - } - int interp_pair = interp_capture(s_hr_fbo, disp_x, disp_y, w, h, - linear, force_4_3, GL_PRES_VRAM); + s_probe_skip++; + gl_perf_present_enter(); + gl_perf_present_exit(0); + return; + } + gl_perf_present_enter(); /* per-frame backdrop-phase reset + dbg snapshot live in here */ + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + int lx, ly, lw, lh; + if (force_4_3) + letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); + else + letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); + /* No short-band adjustment on the 15-bit FBO path: a game's native short + * display mode (e.g. 216/224-line menus) must fill the rect as before. + * The FMV band fix lives in the depth24/CPU present paths only. */ + + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); + if (lx != 0 || ly != 0 || lw != ww || lh != wh) { + glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); + } + int interp_pair = interp_capture(s_hr_fbo, disp_x, disp_y, w, h, + linear, force_4_3, GL_PRES_VRAM); if (interp_pair) { /* Temporal blending owns this stock frame interval. Capture hold-last * before pacing; every blend and Swap remains on this context/thread. */ hold_capture_native_fbo(s_hr_fbo, disp_x, disp_y, w, h, force_4_3, linear); interp_present_source_interval(); - gl_perf_present_exit(0); - present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0); - present_force_consumed(); - s_last_present_path = GL_PRES_VRAM; - s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = w; s_last_dh = h; - return; - } + gl_perf_present_exit(0); + present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0); + present_force_consumed(); + s_last_present_path = GL_PRES_VRAM; + s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = w; s_last_dh = h; + return; + } present_bezel(ww, wh, lx, ly, lw, lh); present_target_quad(s_hr_tex, VRAM_W, VRAM_H, disp_x, disp_y, w, h, linear, lx, ly, lw, lh, 1); - pres_record(GL_PRES_VRAM, disp_x, disp_y, w, h, lx, ly, lw, lh); - hold_capture_drawable(); - latency_ring_mark(LAT_SWAP_BEGIN); - gl_swap_with_osd(); - latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; - gl_perf_present_exit(0); - present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0); - present_force_consumed(); - s_last_present_path = GL_PRES_VRAM; - s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = w; s_last_dh = h; - coh_record(GL_COH_PRESENT, disp_x, disp_y, disp_x + w - 1, disp_y + h - 1); -} - -/* Native-wide fast path: authoritatively copy the canonical 4:3 framebuffer - * into the wide surface's CENTRE columns [g_wide_off, g_wide_off+native_w) for - * the displayed Y band, so the per-prim mirror could skip every centre-only - * prim (the dominant native-wide GPU saving). The reveal margins were already - * produced by the mirror; this leaves them untouched. One FBO->FBO blit, - * x-translated by the reveal offset. No-op when s_wide_fast is off (then the - * mirror drew the full surface, as before). Shared by both present paths. */ -static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h) { - if (!s_wide_fast || g_wide_w <= 0) return; - int native_w = g_wide_w - 2 * g_wide_off; - if (native_w <= 0) return; - int S = s_scale; - (void)disp_y; (void)disp_h; - /* Copy the canonical framebuffer column into the wide surface CENTRE over the - * FULL VRAM height, not just the current display band [disp_y, disp_y+disp_h]. - * The wide surface holds BOTH vertical double-buffer bands (Ape flips - * display_y 0<->256), and the game's draw area / display band can differ per - * scene (the cityscape intro exposed rows outside disp_h). Copying the whole - * column makes the wide CENTRE bit-identical to what the full mirror would - * have drawn there for every band, so nothing the present reads is ever left - * black. The margins (x outside the centre) are untouched. */ - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, wide_fbo); - glDisable(GL_SCISSOR_TEST); - p_glBlitFramebuffer(base_x * S, 0, - (base_x + native_w) * S, VRAM_H * S, - g_wide_off * S, 0, - (g_wide_off + native_w) * S, VRAM_H * S, - GL_COLOR_BUFFER_BIT, GL_NEAREST); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); -} - -/* GPU-direct native-wide present: blit the displayed buffer's wide FBO straight - * to the window (no glReadPixels / glFinish CPU round-trip). The wide surface is - * g_wide_w wide × VRAM_H tall (at scale S); present its [0,g_wide_w] × [disp_y, - * disp_y+disp_h] region into the letterbox, V-flipped like present_vram (FBO y - * bottom-origin → window top). Returns 0 if there's no wide surface for base_x - * (caller falls back). disp_x is the displayed buffer base (the wide-surface key). */ -int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear) { - if (!s_ctx || !s_raster_ok || g_wide_w <= 0) return 0; - GLuint fbo = 0, tex = 0; - for (int i = 0; i < WIDE_MAX_SURF; i++) - if (s_wide_fbo[i] && s_wide_base[i] == disp_x) { - fbo = s_wide_fbo[i]; tex = s_wide_tex[i]; break; - } - if (!fbo) return 0; - flush_flat_batch(); - flush_tex_batch(); - flush_cpu_upload(); + pres_record(GL_PRES_VRAM, disp_x, disp_y, w, h, lx, ly, lw, lh); + hold_capture_drawable(); + latency_ring_mark(LAT_SWAP_BEGIN); + gl_swap_with_osd(); + latency_ring_mark(LAT_SWAP_END); + s_probe_swap++; + gl_perf_present_exit(0); + present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0); + present_force_consumed(); + s_last_present_path = GL_PRES_VRAM; + s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = w; s_last_dh = h; + coh_record(GL_COH_PRESENT, disp_x, disp_y, disp_x + w - 1, disp_y + h - 1); +} + +/* Native-wide fast path: authoritatively copy the canonical 4:3 framebuffer + * into the wide surface's CENTRE columns [g_wide_off, g_wide_off+native_w) for + * the displayed Y band, so the per-prim mirror could skip every centre-only + * prim (the dominant native-wide GPU saving). The reveal margins were already + * produced by the mirror; this leaves them untouched. One FBO->FBO blit, + * x-translated by the reveal offset. No-op when s_wide_fast is off (then the + * mirror drew the full surface, as before). Shared by both present paths. */ +static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h) { + if (!s_wide_fast || g_wide_w <= 0) return; + int native_w = g_wide_w - 2 * g_wide_off; + if (native_w <= 0) return; + int S = s_scale; + (void)disp_y; (void)disp_h; + /* Copy the canonical framebuffer column into the wide surface CENTRE over the + * FULL VRAM height, not just the current display band [disp_y, disp_y+disp_h]. + * The wide surface holds BOTH vertical double-buffer bands (Ape flips + * display_y 0<->256), and the game's draw area / display band can differ per + * scene (the cityscape intro exposed rows outside disp_h). Copying the whole + * column makes the wide CENTRE bit-identical to what the full mirror would + * have drawn there for every band, so nothing the present reads is ever left + * black. The margins (x outside the centre) are untouched. */ + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, wide_fbo); + glDisable(GL_SCISSOR_TEST); + p_glBlitFramebuffer(base_x * S, 0, + (base_x + native_w) * S, VRAM_H * S, + g_wide_off * S, 0, + (g_wide_off + native_w) * S, VRAM_H * S, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); +} + +/* GPU-direct native-wide present: blit the displayed buffer's wide FBO straight + * to the window (no glReadPixels / glFinish CPU round-trip). The wide surface is + * g_wide_w wide × VRAM_H tall (at scale S); present its [0,g_wide_w] × [disp_y, + * disp_y+disp_h] region into the letterbox, V-flipped like present_vram (FBO y + * bottom-origin → window top). Returns 0 if there's no wide surface for base_x + * (caller falls back). disp_x is the displayed buffer base (the wide-surface key). */ +int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear) { + if (!s_ctx || !s_raster_ok || g_wide_w <= 0) return 0; + GLuint fbo = 0, tex = 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + if (s_wide_fbo[i] && s_wide_base[i] == disp_x) { + fbo = s_wide_fbo[i]; tex = s_wide_tex[i]; break; + } + if (!fbo) return 0; + flush_flat_batch(); + flush_tex_batch(); + flush_cpu_upload(); if (s_force_present_remaining <= 0 && s_last_present_path == GL_PRES_WIDE && - s_last_dx == disp_x && s_last_dy == disp_y && - s_last_dw == g_wide_w && s_last_dh == disp_h && - !present_dirty_test(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1) && + s_last_dx == disp_x && s_last_dy == disp_y && + s_last_dw == g_wide_w && s_last_dh == disp_h && + !present_dirty_test(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1) && !host_osd_needs_present() && !psx_present_vsync_owns_cadence() && !gl_renderer_interpolation_owns_cadence()) { - s_probe_skip++; - gl_perf_present_enter(); - gl_perf_present_exit(1); - return 1; - } - gl_perf_present_enter(); - int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - int lx, ly, lw, lh; - letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); - wide_blit_center(fbo, disp_x, disp_y, disp_h); /* fast-path: authoritative centre */ - - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); - if (lx != 0 || ly != 0 || lw != ww || lh != wh) { - glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT); - } - int interp_pair = interp_capture(fbo, 0, disp_y, g_wide_w, disp_h, - linear, 0, GL_PRES_WIDE); + s_probe_skip++; + gl_perf_present_enter(); + gl_perf_present_exit(1); + return 1; + } + gl_perf_present_enter(); + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + int lx, ly, lw, lh; + letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); + wide_blit_center(fbo, disp_x, disp_y, disp_h); /* fast-path: authoritative centre */ + + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); + if (lx != 0 || ly != 0 || lw != ww || lh != wh) { + glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT); + } + int interp_pair = interp_capture(fbo, 0, disp_y, g_wide_w, disp_h, + linear, 0, GL_PRES_WIDE); if (interp_pair) { hold_capture_native_fbo(fbo, 0, disp_y, g_wide_w, disp_h, 0, linear); interp_present_source_interval(); - gl_perf_present_exit(1); - present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0); - present_force_consumed(); - s_last_present_path = GL_PRES_WIDE; - s_last_dx = disp_x; s_last_dy = disp_y; - s_last_dw = g_wide_w; s_last_dh = disp_h; - return 1; - } - present_target_quad(tex, g_wide_w, VRAM_H, - 0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh, 1); - pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh); - hold_capture_drawable(); - latency_ring_mark(LAT_SWAP_BEGIN); - gl_swap_with_osd(); - latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; - gl_perf_present_exit(1); - present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0); - present_force_consumed(); - s_last_present_path = GL_PRES_WIDE; - s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = g_wide_w; s_last_dh = disp_h; - coh_record(GL_COH_PRESENT, 0, disp_y, g_wide_w - 1, disp_y + disp_h - 1); - return 1; -} - -static const GpuRenderBackend GL_BACKEND = { - .name = "opengl", - .init = glb_init, .set_scale = glb_set_scale, .scale = glb_scale, - .set_texture_filter = glb_set_texture_filter, .texture_filter = glb_texture_filter, - .set_semi_transparency = glb_set_semi_transparency, .set_mask_bits = glb_set_mask_bits, - .set_texture_window = glb_set_texture_window, .set_color_modulation = glb_set_color_modulation, - .set_precise_triangle = glb_set_precise_triangle, - .set_perspective_triangle = glb_set_perspective_triangle, - .fill_rect = glb_fill_rect, .copy_rect = glb_copy_rect, - .draw_flat_triangle = glb_draw_flat_triangle, .draw_gouraud_triangle = glb_draw_gouraud_triangle, - .draw_textured_triangle = glb_draw_textured_triangle, - .draw_shaded_textured_triangle = glb_draw_shaded_textured_triangle, - .draw_flat_rect = glb_draw_flat_rect, .draw_textured_rect = glb_draw_textured_rect, - .draw_textured_rect_scaled = glb_draw_textured_rect_scaled, - .draw_line = glb_draw_line, .draw_shaded_line = glb_draw_shaded_line, - .render_display = glb_render_display, .render_display_hires = glb_render_display_hires, - .vram_write = glb_vram_write, .vram_read = glb_vram_read, - .vram_transfer_in = glb_vram_transfer_in, .vram_transfer_out = glb_vram_transfer_out, - .set_draw_area = glb_set_draw_area, .get_draw_area = glb_get_draw_area, - .set_draw_offset = glb_set_draw_offset, - .wide_configure = glb_wide_configure, - .wide_set_target = glb_wide_set_target, - .wide_disable_target = glb_wide_disable_target, - .wide_clear = glb_wide_clear, - .wide_clear_margins = glb_wide_clear_margins, - .render_wide_display = glb_render_wide_display, - .wide_dump_full = glb_wide_dump_full, -}; - -const GpuRenderBackend *gl_backend_get(void) { return &GL_BACKEND; } + gl_perf_present_exit(1); + present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0); + present_force_consumed(); + s_last_present_path = GL_PRES_WIDE; + s_last_dx = disp_x; s_last_dy = disp_y; + s_last_dw = g_wide_w; s_last_dh = disp_h; + return 1; + } + present_target_quad(tex, g_wide_w, VRAM_H, + 0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh, 1); + pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh); + hold_capture_drawable(); + latency_ring_mark(LAT_SWAP_BEGIN); + gl_swap_with_osd(); + latency_ring_mark(LAT_SWAP_END); + s_probe_swap++; + gl_perf_present_exit(1); + present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0); + present_force_consumed(); + s_last_present_path = GL_PRES_WIDE; + s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = g_wide_w; s_last_dh = disp_h; + coh_record(GL_COH_PRESENT, 0, disp_y, g_wide_w - 1, disp_y + disp_h - 1); + return 1; +} + +static const GpuRenderBackend GL_BACKEND = { + .name = "opengl", + .init = glb_init, .set_scale = glb_set_scale, .scale = glb_scale, + .set_texture_filter = glb_set_texture_filter, .texture_filter = glb_texture_filter, + .set_semi_transparency = glb_set_semi_transparency, .set_mask_bits = glb_set_mask_bits, + .set_texture_window = glb_set_texture_window, .set_color_modulation = glb_set_color_modulation, + .set_precise_triangle = glb_set_precise_triangle, + .set_perspective_triangle = glb_set_perspective_triangle, + .fill_rect = glb_fill_rect, .copy_rect = glb_copy_rect, + .draw_flat_triangle = glb_draw_flat_triangle, .draw_gouraud_triangle = glb_draw_gouraud_triangle, + .draw_textured_triangle = glb_draw_textured_triangle, + .draw_shaded_textured_triangle = glb_draw_shaded_textured_triangle, + .draw_flat_rect = glb_draw_flat_rect, .draw_textured_rect = glb_draw_textured_rect, + .draw_textured_rect_scaled = glb_draw_textured_rect_scaled, + .draw_line = glb_draw_line, .draw_shaded_line = glb_draw_shaded_line, + .render_display = glb_render_display, .render_display_hires = glb_render_display_hires, + .vram_write = glb_vram_write, .vram_read = glb_vram_read, + .vram_transfer_in = glb_vram_transfer_in, .vram_transfer_out = glb_vram_transfer_out, + .set_draw_area = glb_set_draw_area, .get_draw_area = glb_get_draw_area, + .set_draw_offset = glb_set_draw_offset, + .wide_configure = glb_wide_configure, + .wide_set_target = glb_wide_set_target, + .wide_disable_target = glb_wide_disable_target, + .wide_clear = glb_wide_clear, + .wide_clear_margins = glb_wide_clear_margins, + .render_wide_display = glb_render_wide_display, + .wide_dump_full = glb_wide_dump_full, +}; + +const GpuRenderBackend *gl_backend_get(void) { return &GL_BACKEND; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 4b26eed6f..91bba52de 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -12657,7 +12657,7 @@ int main(int argc, char** argv) { seed.screen_kind = ls.screen_kind; seed.has_screen_kind = true; #if defined(RECOMP_LAUNCHER_HAS_SCANLINES) seed.scanlines = ls.scanlines != 0; seed.has_scanlines = true; - if (ls.scanline_strength_pct > 0) { + if (ls.scanline_strength_pct >= 0) { seed.scanline_strength = ls.scanline_strength_pct / 100.0; seed.has_scanline_strength = true; } @@ -14666,7 +14666,7 @@ int main(int argc, char** argv) { #if defined(RECOMP_LAUNCHER_HAS_SCANLINES) us.scanlines = ls.scanlines != 0; us.has_scanlines = true; - if (ls.scanline_strength_pct > 0) { + if (ls.scanline_strength_pct >= 0) { us.scanline_strength = ls.scanline_strength_pct / 100.0; us.has_scanline_strength = true; } @@ -14728,7 +14728,7 @@ int main(int argc, char** argv) { g_video_screen = ls.screen_kind; #if defined(RECOMP_LAUNCHER_HAS_SCANLINES) g_video_scanlines = ls.scanlines != 0; - if (ls.scanline_strength_pct > 0) + if (ls.scanline_strength_pct >= 0) g_video_scanline_strength = ls.scanline_strength_pct / 100.0f; gl_renderer_set_scanlines(g_video_scanlines ? 1 : 0, g_video_scanline_strength);