mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
3445 lines
170 KiB
C
3445 lines
170 KiB
C
/* 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_render.h"
|
||
#include "gpu_sw_renderer.h"
|
||
#include "gpu_gl_renderer.h"
|
||
#include "latency_ring.h"
|
||
|
||
#include <SDL.h>
|
||
#include <SDL_opengl.h>
|
||
#include <stddef.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
#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
|
||
#define PSXGL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
|
||
#define PSXGL_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFFull
|
||
|
||
#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_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 GLsync (APIENTRY *PFN_glFenceSync)(GLenum, GLbitfield);
|
||
typedef void (APIENTRY *PFN_glWaitSync)(GLsync, GLbitfield, GLuint64);
|
||
typedef void (APIENTRY *PFN_glDeleteSync)(GLsync);
|
||
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_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_glFenceSync p_glFenceSync;
|
||
static PFN_glWaitSync p_glWaitSync;
|
||
static PFN_glDeleteSync p_glDeleteSync;
|
||
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 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_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_glFenceSync, "glFenceSync");
|
||
LOAD(p_glWaitSync, "glWaitSync");
|
||
LOAD(p_glDeleteSync, "glDeleteSync");
|
||
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) */
|
||
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_present_prog = 0, s_present_vao = 0;
|
||
static GLint s_present_uTex = -1, s_present_uUvRect = -1;
|
||
static GLuint s_interp_prog = 0, s_interp_tex[3];
|
||
static GLsync s_interp_fence[3];
|
||
static GLsync s_interp_draw_fence = NULL;
|
||
static GLint s_interp_uPrev = -1, s_interp_uCurr = -1;
|
||
static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1;
|
||
static int s_interp_enabled = 0, s_interp_valid = 0;
|
||
static int s_interp_suspended = 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_start = 0, s_interp_duration = 1;
|
||
static uint64_t s_interp_last_capture = 0, s_interp_swaps = 0;
|
||
static uint64_t s_interp_captures = 0;
|
||
static int s_interp_diag = 0;
|
||
static double s_interp_host_hz = 0.0;
|
||
static double s_interp_target_hz = 0.0;
|
||
static SDL_GLContext s_interp_ctx = NULL;
|
||
static SDL_Thread *s_interp_thread = NULL;
|
||
static SDL_mutex *s_interp_mutex = NULL;
|
||
static SDL_atomic_t s_interp_thread_run;
|
||
static GLuint s_interp_thread_vao = 0;
|
||
static void interp_reset_history(void);
|
||
static int interp_thread_main(void *opaque);
|
||
static int interp_present(void);
|
||
static void interp_draw_quad(float alpha, 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)
|
||
* — per-prim texture state in flat attributes so prims batch (see flush_tex_batch). */
|
||
#define TEXV 19
|
||
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;
|
||
|
||
/* 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. */
|
||
#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;
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
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 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 const char *PRESENT_FS =
|
||
"#version 330\n"
|
||
"in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n"
|
||
"void main(){ frag = texture(u_tex, v_uv); }\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; out vec4 frag;\n"
|
||
"void main(){ frag=mix(texture(u_prev,v_uv),texture(u_curr,v_uv),u_alpha); }\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"
|
||
"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"
|
||
"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_col = a_col;\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"
|
||
" 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 *TEX_FS =
|
||
"#version 330\n"
|
||
"noperspective in vec2 v_uv; noperspective in vec4 v_col;\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"
|
||
"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"
|
||
" if (u_filter == 0) {\n"
|
||
" int raw = fetch_texel(int(floor(v_uv.x)), int(floor(v_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"
|
||
" int iu = int(floor(v_uv.x)), iv = int(floor(v_uv.y));\n"
|
||
" float fx = v_uv.x - float(iu) - 0.5, fy = v_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"
|
||
" 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"
|
||
" if (opac < 0.5) discard;\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);
|
||
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_tex_batch(); /* queued draws must land 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_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_tex_batch();
|
||
pack_flush();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- coherency: GPU -> CPU readback -------------------------------------- */
|
||
static void ensure_cpu(void) {
|
||
if (!s_raster_ok || !s_gpu_dirty) return;
|
||
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];
|
||
}
|
||
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);
|
||
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;
|
||
int lo = (int)s_tb[0], hi = (int)s_tb[0];
|
||
for (int i = 1; i < nverts; i++) {
|
||
int x = (int)s_tb[i * TEXV];
|
||
if (x < lo) lo = x; if (x > hi) hi = x;
|
||
}
|
||
return mirror_x_center_only(lo, hi);
|
||
}
|
||
|
||
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 triangles and lines share the GEO program. mode: GL_TRIANGLES
|
||
* or GL_LINES; verts are (x, y, r, g, b) 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 queued textured draws first (order + program) */
|
||
flush_cpu_upload();
|
||
mark_prim_dirty(xs, ys, n, 0 /* flat */);
|
||
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] = (float)xs[i];
|
||
verts[i*6+1] = (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);
|
||
|
||
/* Native-wide mirror pass: re-issue the same draw into the active wide
|
||
* surface, x-translated by wide_dx (program/VAO/VBO/blend/stencil still
|
||
* bound). Geometry positions are unchanged on the host side — the x shift
|
||
* is applied in the vertex shader via u_xoff, and the wider clip via
|
||
* u_xhalf. Canonical pass above is untouched (u_xoff=0/u_xhalf=512). */
|
||
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); /* flat prims are immediate -> gate per prim */
|
||
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();
|
||
}
|
||
|
||
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. */
|
||
{
|
||
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 */
|
||
/* With mask checking off, opaque and mode-0 semi primitives use the
|
||
* same dual-source blend state. The per-vertex a_semi flag selects
|
||
* replace vs half-blend without breaking painter order. */
|
||
int batch_semi = (!s_mask_check && semi != 2) ? 4 : semi;
|
||
/* STP draw-ORDER correctness. flush_tex_batch draws a batch in two passes
|
||
* over the WHOLE batch (pass 1 = every prim's STP=0/opaque texels, pass 2
|
||
* = every prim's STP=1/semi texels with the PSX blend). For overlapping
|
||
* prims that share a batch, a BEHIND prim's semi texels (pass 2) then
|
||
* paint OVER a FRONT prim's opaque texels (pass 1) — a painter's-order
|
||
* violation that only exists on GL (Tomba: the character drew behind the
|
||
* AP-block letters / a save post on GL, correct on software). So a
|
||
* semi-transparent prim must NOT coalesce with its neighbours: drain the
|
||
* open batch, draw this prim alone (its own STP=0+STP=1 passes, which do
|
||
* not self-overlap → composited fully before the next prim, exactly like
|
||
* the software renderer), and let opaque prims keep batching. Opaque
|
||
* content (terrain/foliage — the batching perf win) is untouched; the cost
|
||
* is one draw per semi prim, which are sparse. (A future single-pass
|
||
* optimization for modes 0/1/3 via GL_ONE,GL_SRC_ALPHA with a per-fragment
|
||
* destination factor in alpha could re-batch semi prims — see memory
|
||
* tomba_sprite_zorder_bug; mode 2 subtractive still needs isolation.) */
|
||
int isolate = (semi >= 0 && (semi == 2 || s_mask_check));
|
||
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] = (float)xs[i]; vp[1] = (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 */
|
||
}
|
||
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_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);
|
||
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_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);
|
||
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); }
|
||
static void glb_set_draw_area(int x1,int y1,int x2,int y2) { 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_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.
|
||
* Post-init, the GPU pipeline is all-or-nothing — no per-prim fallback. */
|
||
static void glb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col) {
|
||
if (!s_raster_ok) { sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col); return; }
|
||
gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1);
|
||
}
|
||
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_raster_ok) { sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2); return; }
|
||
gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1);
|
||
}
|
||
static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){
|
||
if (!s_raster_ok) { sw_fill_rect(x,y,w,h,c); 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_raster_ok) { sw_copy_rect(sx,sy,dx,dy,w,h); 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_raster_ok) { sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp); 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);
|
||
}
|
||
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_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); 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);
|
||
}
|
||
static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){
|
||
if (!s_raster_ok) { sw_draw_flat_rect(x,y,w,h,c); 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_raster_ok) { sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp); 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_raster_ok) { sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp); 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_raster_ok) { sw_draw_line(x0,y0,x1,y1,c); 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_raster_ok) { sw_draw_shaded_line(x0,y0,c0,x1,y1,c1); 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); }
|
||
static void glb_vram_write(int x,int y,uint16_t px){
|
||
sw_vram_write(x,y,px);
|
||
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);
|
||
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);
|
||
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);
|
||
}
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* 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_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) */
|
||
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;
|
||
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");
|
||
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");
|
||
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_interp_thread) {
|
||
SDL_AtomicSet(&s_interp_thread_run, 0);
|
||
SDL_WaitThread(s_interp_thread, NULL);
|
||
s_interp_thread = NULL;
|
||
}
|
||
if (s_interp_ctx) {
|
||
SDL_GL_DeleteContext(s_interp_ctx);
|
||
s_interp_ctx = NULL;
|
||
}
|
||
if (s_ctx) {
|
||
SDL_GL_MakeCurrent(s_win, s_ctx);
|
||
if (s_interp_draw_fence) {
|
||
p_glDeleteSync(s_interp_draw_fence);
|
||
s_interp_draw_fence = NULL;
|
||
}
|
||
for (int i = 0; i < 3; i++) {
|
||
if (s_interp_fence[i]) {
|
||
p_glDeleteSync(s_interp_fence[i]);
|
||
s_interp_fence[i] = NULL;
|
||
}
|
||
}
|
||
ensure_cpu();
|
||
SDL_GL_DeleteContext(s_ctx); s_ctx = NULL;
|
||
}
|
||
if (s_interp_mutex) {
|
||
SDL_DestroyMutex(s_interp_mutex);
|
||
s_interp_mutex = NULL;
|
||
}
|
||
free(s_conv); s_conv = NULL;
|
||
s_raster_ok = 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) {
|
||
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);
|
||
glViewport(lx, ly, lw, lh);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
upload_present_tex(pixels, src_w, src_h, linear);
|
||
p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0);
|
||
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);
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
SDL_GL_SwapWindow(s_win);
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
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);
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
SDL_GL_SwapWindow(s_win);
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
s_last_present_path = GL_PRES_BLANK;
|
||
}
|
||
|
||
/* Sync the authoritative FBO down into CPU VRAM (no-op when current).
|
||
* Called by the 24-bit present path, screenshots, and the debug server. */
|
||
void gl_renderer_sync_cpu(void) {
|
||
ensure_cpu();
|
||
}
|
||
|
||
/* 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;
|
||
s_interp_start = s_interp_last_capture = 0;
|
||
s_interp_duration = 1;
|
||
s_interp_source_path = -1;
|
||
}
|
||
|
||
static void interp_reset_history(void) {
|
||
if (s_interp_mutex) SDL_LockMutex(s_interp_mutex);
|
||
interp_reset_history_unlocked();
|
||
if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex);
|
||
}
|
||
|
||
void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz) {
|
||
int active = (enabled && host_hz >= 90.0) ? 1 : 0;
|
||
double effective_hz = target_hz >= 90.0 ? target_hz : host_hz;
|
||
const char *diag = getenv("PSX_GL_INTERP_DIAG");
|
||
s_interp_diag = diag && diag[0] && diag[0] != '0';
|
||
if (active && !s_interp_ctx && s_ctx) {
|
||
if (!s_interp_mutex) s_interp_mutex = SDL_CreateMutex();
|
||
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
|
||
s_interp_ctx = SDL_GL_CreateContext(s_win);
|
||
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
|
||
SDL_GL_MakeCurrent(s_win, s_ctx);
|
||
/* The wall-clock pacer and interpolation scheduler own cadence. Keeping
|
||
* driver vsync on the main context adds a 6-11 ms block whenever an
|
||
* FMV temporarily suspends interpolation and the main context presents. */
|
||
SDL_GL_SetSwapInterval(0);
|
||
if (s_interp_ctx && s_interp_mutex) {
|
||
SDL_AtomicSet(&s_interp_thread_run, 1);
|
||
s_interp_thread = SDL_CreateThread(interp_thread_main,
|
||
"psx-gl-interp", NULL);
|
||
}
|
||
if (!s_interp_thread) {
|
||
SDL_AtomicSet(&s_interp_thread_run, 0);
|
||
if (s_interp_ctx) SDL_GL_DeleteContext(s_interp_ctx);
|
||
s_interp_ctx = NULL;
|
||
active = 0;
|
||
}
|
||
}
|
||
if (s_interp_mutex) SDL_LockMutex(s_interp_mutex);
|
||
if (active != s_interp_enabled) interp_reset_history_unlocked();
|
||
s_interp_enabled = active;
|
||
s_interp_host_hz = host_hz;
|
||
s_interp_target_hz = active ? effective_hz : 0.0;
|
||
if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex);
|
||
if (active)
|
||
fprintf(stdout, "psxrecomp: GL frame interpolation enabled: %.1f FPS "
|
||
"target on %.1f Hz display\n", effective_hz, host_hz);
|
||
else
|
||
fprintf(stdout, "psxrecomp: GL frame interpolation disabled (host %.1f Hz)\n", host_hz);
|
||
}
|
||
|
||
void gl_renderer_set_interpolation_suspended(int suspended) {
|
||
suspended = suspended ? 1 : 0;
|
||
if (s_interp_mutex) SDL_LockMutex(s_interp_mutex);
|
||
if (suspended != s_interp_suspended) interp_reset_history_unlocked();
|
||
s_interp_suspended = suspended;
|
||
if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex);
|
||
}
|
||
|
||
void gl_renderer_interpolation_diag(int *enabled, int *suspended,
|
||
int *history_frames,
|
||
double *host_hz, double *target_hz,
|
||
uint64_t *swaps) {
|
||
if (s_interp_mutex) SDL_LockMutex(s_interp_mutex);
|
||
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 (target_hz) *target_hz = s_interp_target_hz;
|
||
if (swaps) *swaps = s_interp_swaps;
|
||
if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex);
|
||
}
|
||
|
||
/* Copy a stable display image out of the mutable VRAM/wide render target.
|
||
* Returns true once both previous and current images are available. */
|
||
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;
|
||
SDL_LockMutex(s_interp_mutex);
|
||
/* The presentation context may still have a draw queued which samples one
|
||
* of the shared history textures. Order this context's next allocation or
|
||
* copy after that draw before recycling a texture. glWaitSync keeps the
|
||
* dependency on the GPU; unlike glFinish it does not stall the guest CPU. */
|
||
if (s_interp_draw_fence) {
|
||
p_glWaitSync(s_interp_draw_fence, 0, PSXGL_TIMEOUT_IGNORED);
|
||
p_glDeleteSync(s_interp_draw_fence);
|
||
s_interp_draw_fence = NULL;
|
||
}
|
||
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;
|
||
if (s_interp_fence[dst]) {
|
||
p_glDeleteSync(s_interp_fence[dst]);
|
||
s_interp_fence[dst] = NULL;
|
||
}
|
||
|
||
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);
|
||
s_interp_fence[dst] = p_glFenceSync(PSXGL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||
glFlush();
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
|
||
uint64_t now = SDL_GetPerformanceCounter();
|
||
uint64_t freq = SDL_GetPerformanceFrequency();
|
||
if (s_interp_valid > 0 && s_interp_last_capture && now > s_interp_last_capture) {
|
||
uint64_t d = now - s_interp_last_capture;
|
||
uint64_t lo = freq / 240u, hi = freq / 10u;
|
||
if (d < lo) d = lo;
|
||
if (d > hi) d = hi;
|
||
s_interp_duration = d;
|
||
}
|
||
s_interp_last_capture = now;
|
||
s_interp_start = now;
|
||
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++;
|
||
int ready = s_interp_valid >= 2;
|
||
SDL_UnlockMutex(s_interp_mutex);
|
||
return ready;
|
||
}
|
||
|
||
static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh) {
|
||
int prev = s_interp_prev, curr = s_interp_cur;
|
||
if (s_interp_fence[prev])
|
||
p_glWaitSync(s_interp_fence[prev], 0, PSXGL_TIMEOUT_IGNORED);
|
||
if (s_interp_fence[curr])
|
||
p_glWaitSync(s_interp_fence[curr], 0, PSXGL_TIMEOUT_IGNORED);
|
||
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_glUniform4f(s_interp_uUvRect, 0.f, 0.f, 1.f, 1.f);
|
||
p_glBindVertexArray(s_interp_thread_vao);
|
||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||
p_glBindVertexArray(0);
|
||
p_glUseProgram(0);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
}
|
||
|
||
static int interp_present(void) {
|
||
if (!s_ctx || !s_interp_enabled || s_interp_suspended || s_interp_valid < 2) return 0;
|
||
uint64_t now = SDL_GetPerformanceCounter();
|
||
if (now <= s_interp_start || !s_interp_duration) return 0;
|
||
double a = (double)(now - s_interp_start) / (double)s_interp_duration;
|
||
/* Keep swapping at the host cadence after the blend completes. Holding
|
||
* alpha at one is visually identical to leaving the current image on the
|
||
* front buffer, but avoids an irregular 2/3-swap pattern on 120/144/165 Hz
|
||
* displays while the next 59.94 Hz guest frame is being produced. */
|
||
if (a > 1.0) a = 1.0;
|
||
if (a < 0.0) a = 0.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);
|
||
if (lx != 0 || ly != 0 || lw != ww || lh != wh) {
|
||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||
glClear(GL_COLOR_BUFFER_BIT);
|
||
}
|
||
interp_draw_quad((float)a, lx, ly, lw, lh);
|
||
if (s_interp_draw_fence) p_glDeleteSync(s_interp_draw_fence);
|
||
s_interp_draw_fence = p_glFenceSync(PSXGL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||
glFlush();
|
||
pres_record(GL_PRES_INTERP, 0, 0, s_interp_w, s_interp_h,
|
||
lx, ly, lw, lh);
|
||
SDL_GL_SwapWindow(s_win);
|
||
s_interp_swaps++;
|
||
return 1;
|
||
}
|
||
|
||
static int interp_thread_main(void *opaque) {
|
||
(void)opaque;
|
||
if (SDL_GL_MakeCurrent(s_win, s_interp_ctx) != 0) return -1;
|
||
SDL_GL_SetSwapInterval(0); /* host-period scheduler owns cadence */
|
||
p_glGenVertexArrays(1, &s_interp_thread_vao);
|
||
uint64_t freq = SDL_GetPerformanceFrequency();
|
||
uint64_t deadline = SDL_GetPerformanceCounter();
|
||
uint64_t diag_start = deadline, diag_swaps = 0, diag_captures = 0;
|
||
|
||
while (SDL_AtomicGet(&s_interp_thread_run)) {
|
||
SDL_LockMutex(s_interp_mutex);
|
||
double hz = s_interp_target_hz >= 90.0 ? s_interp_target_hz : 120.0;
|
||
SDL_UnlockMutex(s_interp_mutex);
|
||
uint64_t period = (uint64_t)((double)freq / hz);
|
||
if (!period) period = 1;
|
||
deadline += period;
|
||
uint64_t now = SDL_GetPerformanceCounter();
|
||
if (now > deadline + period * 4u) deadline = now + period;
|
||
for (;;) {
|
||
now = SDL_GetPerformanceCounter();
|
||
if (now >= deadline) break;
|
||
uint64_t remain = deadline - now;
|
||
uint32_t ms = (uint32_t)((remain * 1000u) / (freq ? freq : 1u));
|
||
if (ms > 1) SDL_Delay(ms - 1);
|
||
}
|
||
while (SDL_GetPerformanceCounter() < deadline) {}
|
||
|
||
SDL_LockMutex(s_interp_mutex);
|
||
if (SDL_AtomicGet(&s_interp_thread_run) && s_interp_enabled)
|
||
interp_present();
|
||
if (s_interp_diag && now - diag_start >= freq * 5u) {
|
||
double seconds = (double)(now - diag_start) / (double)freq;
|
||
fprintf(stdout, "psxrecomp: GL interpolation cadence: "
|
||
"%.2f captures/s, %.2f presents/s\n",
|
||
(double)(s_interp_captures - diag_captures) / seconds,
|
||
(double)(s_interp_swaps - diag_swaps) / seconds);
|
||
fflush(stdout);
|
||
diag_start = now;
|
||
diag_captures = s_interp_captures;
|
||
diag_swaps = s_interp_swaps;
|
||
}
|
||
SDL_UnlockMutex(s_interp_mutex);
|
||
}
|
||
p_glBindVertexArray(0);
|
||
SDL_GL_MakeCurrent(s_win, NULL);
|
||
return 0;
|
||
}
|
||
|
||
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) {
|
||
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);
|
||
p_glUniform4f(s_present_uUvRect,
|
||
(float)x / (float)tex_w, (float)y / (float)tex_h,
|
||
(float)(x + w) / (float)tex_w, (float)(y + h) / (float)tex_h);
|
||
p_glBindVertexArray(s_present_vao);
|
||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||
p_glBindVertexArray(0);
|
||
p_glUseProgram(0);
|
||
}
|
||
|
||
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_tex_batch();
|
||
flush_cpu_upload();
|
||
if (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 &&
|
||
!present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1)) {
|
||
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);
|
||
|
||
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) {
|
||
gl_perf_present_exit(0);
|
||
present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0);
|
||
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_target_quad(s_hr_tex, VRAM_W, VRAM_H,
|
||
disp_x, disp_y, w, h, linear, lx, ly, lw, lh);
|
||
pres_record(GL_PRES_VRAM, disp_x, disp_y, w, h, lx, ly, lw, lh);
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
SDL_GL_SwapWindow(s_win);
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
gl_perf_present_exit(0);
|
||
present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0);
|
||
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_tex_batch();
|
||
flush_cpu_upload();
|
||
if (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)) {
|
||
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) {
|
||
gl_perf_present_exit(1);
|
||
present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 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;
|
||
return 1;
|
||
}
|
||
present_target_quad(tex, g_wide_w, VRAM_H,
|
||
0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh);
|
||
pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh);
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
SDL_GL_SwapWindow(s_win);
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
gl_perf_present_exit(1);
|
||
present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 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;
|
||
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,
|
||
.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; }
|