mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
bceadc6124
Load modern texpage/texupload PNG replacements independently of DuckStation source. Integrate pack folders and options with mods, preserve native VRAM, and validate GL rendering, format matching, dump safety, and upload invalidation.
8109 lines
385 KiB
C
8109 lines
385 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 during normal 15-bit rendering. Every mutation
|
||
* goes through the GPU; depth24 scanout additionally keeps the CPU mirror
|
||
* authoritative because packed RGB888 framebuffer uploads are not staged as
|
||
* 1555 FBO pixels. GP0 clears/draws/copies must also update that mirror:
|
||
*
|
||
* - 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) in 15-bit mode
|
||
* flush uploads + pack, then glReadPixels the raw mirror straight into CPU
|
||
* VRAM (raw 1555, no conversion loop). Depth24 entry synchronizes once before
|
||
* the first write; readback is then suppressed until 15-bit rendering resumes.
|
||
*
|
||
* 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 use the authoritative CPU mirror and quad-present, 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 at 1x and, above 1x, a quad one native pixel
|
||
* thick that covers both endpoints (line_to_quad) 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.
|
||
*
|
||
* INTERNAL SCALE LIMITS. S is clamped at init to the driver's texture,
|
||
* renderbuffer and viewport limits and to a memory budget (gl_scale_limits.h),
|
||
* and an FBO that still fails to allocate is retried one scale lower. A large
|
||
* S therefore never costs the GL backend itself.
|
||
*
|
||
* Init is all-or-nothing: if any shader/FBO fails, gl_renderer_init_context
|
||
* returns 0 and the runtime falls back to the pure software renderer — no
|
||
* half-GL hybrid. */
|
||
|
||
#include "gpu.h"
|
||
#include "gpu_render.h"
|
||
#include "gl_scale_limits.h"
|
||
#include "gpu_sw_renderer.h"
|
||
#include "gpu_gl_renderer.h"
|
||
#include "gpu_hd_textures.h"
|
||
#include "mod_texture_banks.h"
|
||
#include "frame_interpolation.h"
|
||
#include "render_pass_plan.h"
|
||
#include "render_pass.h"
|
||
#include "psx_cycles.h"
|
||
#include "psx_video_timing.h"
|
||
#include "mod_plugins.h" /* PSX_MOD_RENDER_PASS_* reasons */
|
||
#include "host_osd.h"
|
||
#include "psx_savestate_menu.h"
|
||
#include "host_time.h"
|
||
#include "latency_ring.h"
|
||
#include "gpu_timeline.h"
|
||
#include "frame_pacing.h"
|
||
#include "psx_rewind.h"
|
||
#include "psx_openxr.h"
|
||
#include "openxr_color.h"
|
||
|
||
#include "psx_sdl.h"
|
||
#if defined(PSX_SDL3)
|
||
#include <SDL3/SDL_opengl.h>
|
||
#else
|
||
#include <SDL_opengl.h>
|
||
#endif
|
||
#include <limits.h>
|
||
#include <math.h>
|
||
#include <stddef.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include "png_write.h" /* png_write_rgb — present_shot readback */
|
||
|
||
#ifndef GL_BGRA
|
||
#define GL_BGRA 0x80E1
|
||
#endif
|
||
#ifndef GL_CLAMP_TO_EDGE
|
||
#define GL_CLAMP_TO_EDGE 0x812F
|
||
#endif
|
||
#ifndef GL_RGBA8
|
||
#define GL_RGBA8 0x8058
|
||
#endif
|
||
/* GL 1.4+ enums absent from MinGW's GL 1.1 headers. */
|
||
#define PSXGL_FRAGMENT_SHADER 0x8B30
|
||
#define PSXGL_VERTEX_SHADER 0x8B31
|
||
#define PSXGL_COMPILE_STATUS 0x8B81
|
||
#define PSXGL_LINK_STATUS 0x8B82
|
||
#define PSXGL_TEXTURE0 0x84C0
|
||
#define PSXGL_ARRAY_BUFFER 0x8892
|
||
#define PSXGL_STREAM_DRAW 0x88E0
|
||
#define PSXGL_FRAMEBUFFER 0x8D40
|
||
#define PSXGL_READ_FRAMEBUFFER 0x8CA8
|
||
#define PSXGL_DRAW_FRAMEBUFFER 0x8CA9
|
||
#define PSXGL_COLOR_ATTACHMENT0 0x8CE0
|
||
#define PSXGL_DEPTH_STENCIL_ATTACHMENT 0x821A
|
||
#define PSXGL_FRAMEBUFFER_COMPLETE 0x8CD5
|
||
#define PSXGL_RENDERBUFFER 0x8D41
|
||
#define PSXGL_DEPTH24_STENCIL8 0x88F0
|
||
#define PSXGL_R16UI 0x8234
|
||
#define PSXGL_RED_INTEGER 0x8D94
|
||
#define PSXGL_FUNC_ADD 0x8006
|
||
#define PSXGL_FUNC_REVERSE_SUBTRACT 0x800B
|
||
#define PSXGL_CONSTANT_ALPHA 0x8003
|
||
#define PSXGL_UNPACK_ROW_LENGTH 0x0CF2
|
||
#define PSXGL_PACK_ROW_LENGTH 0x0D02
|
||
#define PSXGL_SRC1_ALPHA 0x8589
|
||
|
||
#ifndef APIENTRY
|
||
#define APIENTRY
|
||
#endif
|
||
|
||
#define VRAM_W 1024
|
||
#define VRAM_H 512
|
||
/* GL_MAX_INTERNAL_SCALE (the compile-time ceiling) lives in gpu_render.h; the
|
||
* effective scale is clamped to the driver and memory limits at init. */
|
||
#ifndef PSXGL_MAX_RENDERBUFFER_SIZE
|
||
#define PSXGL_MAX_RENDERBUFFER_SIZE 0x84E8
|
||
#endif
|
||
#ifndef GL_MAX_VIEWPORT_DIMS
|
||
#define GL_MAX_VIEWPORT_DIMS 0x0D3A
|
||
#endif
|
||
/* Scratch tile edge (hr px) for the tiled stencil rebuild at S > 1. */
|
||
#define GL_SCRATCH_TILE 2048
|
||
|
||
/* ---- 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 void (APIENTRY *PFN_glDeleteProgram)(GLuint);
|
||
typedef GLuint (APIENTRY *PFN_glCreateProgram)(void);
|
||
typedef void (APIENTRY *PFN_glAttachShader)(GLuint, GLuint);
|
||
typedef void (APIENTRY *PFN_glLinkProgram)(GLuint);
|
||
typedef void (APIENTRY *PFN_glGetProgramiv)(GLuint, GLenum, GLint *);
|
||
typedef void (APIENTRY *PFN_glGetProgramInfoLog)(GLuint, GLsizei, GLsizei *, char *);
|
||
typedef void (APIENTRY *PFN_glUseProgram)(GLuint);
|
||
typedef GLint (APIENTRY *PFN_glGetUniformLocation)(GLuint, const char *);
|
||
typedef void (APIENTRY *PFN_glUniform1i)(GLint, GLint);
|
||
typedef void (APIENTRY *PFN_glUniform1f)(GLint, GLfloat);
|
||
typedef void (APIENTRY *PFN_glUniform2i)(GLint, GLint, GLint);
|
||
typedef void (APIENTRY *PFN_glUniform4i)(GLint, GLint, GLint, GLint, GLint);
|
||
typedef void (APIENTRY *PFN_glUniform2f)(GLint, GLfloat, GLfloat);
|
||
typedef void (APIENTRY *PFN_glUniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat);
|
||
typedef void (APIENTRY *PFN_glBlendColor)(GLfloat, GLfloat, GLfloat, GLfloat);
|
||
typedef void (APIENTRY *PFN_glBlendFuncSeparate)(GLenum, GLenum, GLenum, GLenum);
|
||
typedef void (APIENTRY *PFN_glBlendEquationSeparate)(GLenum, GLenum);
|
||
typedef void (APIENTRY *PFN_glGenVertexArrays)(GLsizei, GLuint *);
|
||
typedef void (APIENTRY *PFN_glBindVertexArray)(GLuint);
|
||
typedef void (APIENTRY *PFN_glActiveTexture)(GLenum);
|
||
typedef void (APIENTRY *PFN_glGenBuffers)(GLsizei, GLuint *);
|
||
typedef void (APIENTRY *PFN_glBindBuffer)(GLenum, GLuint);
|
||
typedef void (APIENTRY *PFN_glBufferData)(GLenum, ptrdiff_t, const void *, GLenum);
|
||
typedef void (APIENTRY *PFN_glVertexAttribPointer)(GLuint, GLint, GLenum, GLboolean, GLsizei, const void *);
|
||
typedef void (APIENTRY *PFN_glEnableVertexAttribArray)(GLuint);
|
||
typedef void (APIENTRY *PFN_glBindFragDataLocationIndexed)(GLuint, GLuint, GLuint, const char *);
|
||
typedef void (APIENTRY *PFN_glGenFramebuffers)(GLsizei, GLuint *);
|
||
typedef void (APIENTRY *PFN_glDeleteFramebuffers)(GLsizei, const GLuint *);
|
||
typedef void (APIENTRY *PFN_glBindFramebuffer)(GLenum, GLuint);
|
||
typedef void (APIENTRY *PFN_glFramebufferTexture2D)(GLenum, GLenum, GLenum, GLuint, GLint);
|
||
typedef GLenum (APIENTRY *PFN_glCheckFramebufferStatus)(GLenum);
|
||
typedef void (APIENTRY *PFN_glBlitFramebuffer)(GLint,GLint,GLint,GLint,GLint,GLint,GLint,GLint,GLbitfield,GLenum);
|
||
typedef void (APIENTRY *PFN_glGenRenderbuffers)(GLsizei, GLuint *);
|
||
typedef void (APIENTRY *PFN_glDeleteRenderbuffers)(GLsizei, const GLuint *);
|
||
typedef void (APIENTRY *PFN_glBindRenderbuffer)(GLenum, GLuint);
|
||
typedef void (APIENTRY *PFN_glRenderbufferStorage)(GLenum, GLenum, GLsizei, GLsizei);
|
||
typedef void (APIENTRY *PFN_glFramebufferRenderbuffer)(GLenum, GLenum, GLenum, GLuint);
|
||
/* GPU timer queries (ARB_timer_query / core GL 3.3) — frame_perf instrumentation. */
|
||
typedef void (APIENTRY *PFN_glGenQueries)(GLsizei, GLuint *);
|
||
typedef void (APIENTRY *PFN_glDeleteQueries)(GLsizei, const GLuint *);
|
||
typedef void (APIENTRY *PFN_glBeginQuery)(GLenum, GLuint);
|
||
typedef void (APIENTRY *PFN_glEndQuery)(GLenum);
|
||
typedef void (APIENTRY *PFN_glGetQueryObjectui64v)(GLuint, GLenum, GLuint64 *);
|
||
typedef void (APIENTRY *PFN_glGetQueryObjectiv)(GLuint, GLenum, GLint *);
|
||
typedef void (APIENTRY *PFN_glQueryCounter)(GLuint, GLenum);
|
||
/* Buffer mapping for asynchronous pixel-pack readback (presented-image ring). */
|
||
typedef void * (APIENTRY *PFN_glMapBuffer)(GLenum, GLenum);
|
||
typedef GLboolean (APIENTRY *PFN_glUnmapBuffer)(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_glMapBuffer p_glMapBuffer;
|
||
static PFN_glUnmapBuffer p_glUnmapBuffer;
|
||
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_glDeleteProgram p_glDeleteProgram;
|
||
static PFN_glCreateProgram p_glCreateProgram;
|
||
static PFN_glAttachShader p_glAttachShader;
|
||
static PFN_glLinkProgram p_glLinkProgram;
|
||
static PFN_glGetProgramiv p_glGetProgramiv;
|
||
static PFN_glGetProgramInfoLog p_glGetProgramInfoLog;
|
||
static PFN_glUseProgram p_glUseProgram;
|
||
static PFN_glGetUniformLocation p_glGetUniformLocation;
|
||
static PFN_glUniform1i p_glUniform1i;
|
||
static PFN_glUniform1f p_glUniform1f;
|
||
static PFN_glUniform2i p_glUniform2i;
|
||
static PFN_glUniform4i p_glUniform4i;
|
||
static PFN_glUniform2f p_glUniform2f;
|
||
static PFN_glUniform4f p_glUniform4f;
|
||
static PFN_glBlendColor p_glBlendColor;
|
||
static PFN_glBlendFuncSeparate p_glBlendFuncSeparate;
|
||
static PFN_glBlendEquationSeparate p_glBlendEquationSeparate;
|
||
static PFN_glGenVertexArrays p_glGenVertexArrays;
|
||
static PFN_glBindVertexArray p_glBindVertexArray;
|
||
static PFN_glActiveTexture p_glActiveTexture;
|
||
static PFN_glGenBuffers p_glGenBuffers;
|
||
static PFN_glBindBuffer p_glBindBuffer;
|
||
static PFN_glBufferData p_glBufferData;
|
||
static PFN_glVertexAttribPointer p_glVertexAttribPointer;
|
||
static PFN_glEnableVertexAttribArray p_glEnableVertexAttribArray;
|
||
static PFN_glBindFragDataLocationIndexed p_glBindFragDataLocationIndexed;
|
||
static PFN_glGenFramebuffers p_glGenFramebuffers;
|
||
static PFN_glDeleteFramebuffers p_glDeleteFramebuffers;
|
||
static PFN_glBindFramebuffer p_glBindFramebuffer;
|
||
static PFN_glFramebufferTexture2D p_glFramebufferTexture2D;
|
||
static PFN_glCheckFramebufferStatus p_glCheckFramebufferStatus;
|
||
static PFN_glBlitFramebuffer p_glBlitFramebuffer;
|
||
static PFN_glGenQueries p_glGenQueries;
|
||
static PFN_glDeleteQueries p_glDeleteQueries;
|
||
static PFN_glBeginQuery p_glBeginQuery;
|
||
static PFN_glEndQuery p_glEndQuery;
|
||
static PFN_glGetQueryObjectui64v p_glGetQueryObjectui64v;
|
||
static PFN_glGetQueryObjectiv p_glGetQueryObjectiv;
|
||
static PFN_glQueryCounter p_glQueryCounter;
|
||
static void gl_perf_init(void); /* frame_perf — defined below, called from init_gpu_raster */
|
||
static void gl_perf_mirror_begin(void); /* frame_perf: GPU-time bracket around ONE native-wide */
|
||
static void gl_perf_mirror_end(void); /* mirror pass (timestamp pair; splits scene canonical/mirror) */
|
||
/* Native-wide mirror ABLATION (perf attribution, debug cmd gl_ws_ablate):
|
||
* 0 = normal, 1 = skip the whole mirror pass, 2 = full mirror state churn but no
|
||
* draw calls, 3 = mirror draws land in the hr FBO (no per-pass FBO rebind; wide
|
||
* margins go stale + hr gets garbage — perf probe only). */
|
||
static int s_ws_ablate = 0;
|
||
static void flush_tex_batch(void); /* textured-prim batch — defined below, flushed from coherency points */
|
||
static void flush_flat_batch(void); /* flat/gouraud GEO batch (MotK starfield 0x68 dots) */
|
||
static PFN_glGenRenderbuffers p_glGenRenderbuffers;
|
||
static PFN_glDeleteRenderbuffers p_glDeleteRenderbuffers;
|
||
static PFN_glBindRenderbuffer p_glBindRenderbuffer;
|
||
static PFN_glRenderbufferStorage p_glRenderbufferStorage;
|
||
static PFN_glFramebufferRenderbuffer p_glFramebufferRenderbuffer;
|
||
|
||
static int load_modern_gl(void) {
|
||
int ok = 1;
|
||
#define LOAD(p, n) do { p = (void *)SDL_GL_GetProcAddress(n); if (!p) ok = 0; } while (0)
|
||
LOAD(p_glCreateShader, "glCreateShader"); LOAD(p_glShaderSource, "glShaderSource");
|
||
LOAD(p_glCompileShader, "glCompileShader"); LOAD(p_glGetShaderiv, "glGetShaderiv");
|
||
LOAD(p_glGetShaderInfoLog, "glGetShaderInfoLog"); LOAD(p_glDeleteShader, "glDeleteShader");
|
||
LOAD(p_glCreateProgram, "glCreateProgram"); LOAD(p_glAttachShader, "glAttachShader");
|
||
LOAD(p_glLinkProgram, "glLinkProgram"); LOAD(p_glGetProgramiv, "glGetProgramiv");
|
||
LOAD(p_glGetProgramInfoLog, "glGetProgramInfoLog"); LOAD(p_glUseProgram, "glUseProgram");
|
||
LOAD(p_glGetUniformLocation, "glGetUniformLocation"); LOAD(p_glUniform1i, "glUniform1i");
|
||
LOAD(p_glUniform1f, "glUniform1f");
|
||
LOAD(p_glUniform2i, "glUniform2i"); LOAD(p_glUniform4i, "glUniform4i");
|
||
LOAD(p_glUniform2f, "glUniform2f");
|
||
LOAD(p_glUniform4f, "glUniform4f");
|
||
LOAD(p_glBlendColor, "glBlendColor");
|
||
LOAD(p_glBlendFuncSeparate, "glBlendFuncSeparate");
|
||
LOAD(p_glBlendEquationSeparate, "glBlendEquationSeparate");
|
||
LOAD(p_glGenVertexArrays, "glGenVertexArrays"); LOAD(p_glBindVertexArray, "glBindVertexArray");
|
||
LOAD(p_glActiveTexture, "glActiveTexture"); LOAD(p_glGenBuffers, "glGenBuffers");
|
||
LOAD(p_glBindBuffer, "glBindBuffer"); LOAD(p_glBufferData, "glBufferData");
|
||
LOAD(p_glVertexAttribPointer, "glVertexAttribPointer");
|
||
LOAD(p_glEnableVertexAttribArray, "glEnableVertexAttribArray");
|
||
LOAD(p_glBindFragDataLocationIndexed, "glBindFragDataLocationIndexed");
|
||
LOAD(p_glGenFramebuffers, "glGenFramebuffers"); LOAD(p_glBindFramebuffer, "glBindFramebuffer");
|
||
LOAD(p_glDeleteFramebuffers, "glDeleteFramebuffers");
|
||
LOAD(p_glFramebufferTexture2D, "glFramebufferTexture2D");
|
||
LOAD(p_glCheckFramebufferStatus, "glCheckFramebufferStatus");
|
||
LOAD(p_glBlitFramebuffer, "glBlitFramebuffer");
|
||
LOAD(p_glGenRenderbuffers, "glGenRenderbuffers");
|
||
LOAD(p_glDeleteRenderbuffers, "glDeleteRenderbuffers");
|
||
LOAD(p_glBindRenderbuffer, "glBindRenderbuffer");
|
||
LOAD(p_glRenderbufferStorage, "glRenderbufferStorage");
|
||
LOAD(p_glFramebufferRenderbuffer, "glFramebufferRenderbuffer");
|
||
/* GPU timer queries — optional (frame_perf). Don't fail the renderer if
|
||
* absent; gl_perf just stays disabled. */
|
||
p_glGenQueries = (void *)SDL_GL_GetProcAddress("glGenQueries");
|
||
p_glDeleteQueries = (void *)SDL_GL_GetProcAddress("glDeleteQueries");
|
||
p_glBeginQuery = (void *)SDL_GL_GetProcAddress("glBeginQuery");
|
||
p_glEndQuery = (void *)SDL_GL_GetProcAddress("glEndQuery");
|
||
p_glGetQueryObjectui64v = (void *)SDL_GL_GetProcAddress("glGetQueryObjectui64v");
|
||
p_glGetQueryObjectiv = (void *)SDL_GL_GetProcAddress("glGetQueryObjectiv");
|
||
p_glQueryCounter = (void *)SDL_GL_GetProcAddress("glQueryCounter");
|
||
/* Optional (XR colour program cleanup only): never fails modern-GL init. */
|
||
p_glDeleteProgram = (void *)SDL_GL_GetProcAddress("glDeleteProgram");
|
||
/* Optional: only the debug presented-image ring maps pack buffers. */
|
||
p_glMapBuffer = (void *)SDL_GL_GetProcAddress("glMapBuffer");
|
||
p_glUnmapBuffer = (void *)SDL_GL_GetProcAddress("glUnmapBuffer");
|
||
#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) */
|
||
|
||
/* ---- Two scales ------------------------------------------------------------
|
||
* s_hr_scale the scale of s_hr_fbo, the authoritative VRAM surface, and of
|
||
* everything that reads, mirrors, backs up or restores it: hr
|
||
* draws and scissors, pack, CPU readback, VRAM copies and their
|
||
* scratch, mask stencils on hr.
|
||
* s_out_scale the internal resolution presented (gr_scale/glb_scale): the
|
||
* high-resolution window (s_hiw_t tiles), the native-wide surfaces,
|
||
* present, hold-last, interpolation captures, screenshot_hires.
|
||
* They are equal except in windowed high-resolution mode (s_hiw, below),
|
||
* where s_hr_scale is 1 and the presented image lives in the window, not in
|
||
* s_hr_fbo. Code that reads s_hr_fbo at s_out_scale, or takes the presented
|
||
* image from s_hr_fbo, is wrong only in that mode. The old single name
|
||
* s_scale is therefore poisoned below: code written against it (an older
|
||
* branch) fails to build instead of merging silently, and each use must pick
|
||
* one of the two. A transaction that backs up and restores VRAM must also
|
||
* back up and restore the window's rows while hiw_on(), or refuse to run. */
|
||
static int s_out_scale = 1;
|
||
static int s_hr_scale = 1;
|
||
#if defined(__GNUC__) || defined(__clang__)
|
||
#pragma GCC poison s_scale
|
||
#endif
|
||
/* Netplay: SW@1× + GPU@s_out_scale dual write; CPU VRAM always authoritative. */
|
||
static int s_cpu_auth_dual = 0;
|
||
/* Independent of netplay: the replacement surface must never become VRAM. */
|
||
static int s_hd_native_authority = 0;
|
||
static int s_req_scale = 1; /* requested before context init */
|
||
/* Driver limits queried at init: min(GL_MAX_TEXTURE_SIZE,
|
||
* GL_MAX_RENDERBUFFER_SIZE, GL_MAX_VIEWPORT_DIMS); 0 before init. */
|
||
static int s_gl_max_dim = 0;
|
||
static int s_scale_clamp_reason = 0; /* PSX_GL_SCALE_* mask */
|
||
static int s_scale_alloc_retries = 0; /* FBO failures stepped down */
|
||
static uint64_t s_vram_budget = 0; /* bytes; 0 = none */
|
||
static int s_wide_refused_logged = 0;
|
||
|
||
/* ---- windowed high-resolution surface (true 8K on 16384-limit GPUs) -------
|
||
* The full-VRAM hr surface is 1024*S wide, so a 16384 texture limit stops it
|
||
* at 16x (3840 lines). 8K needs 18x. In windowed mode the AUTHORITATIVE VRAM
|
||
* stays the ordinary hr surface at 1x — exactly the native renderer, so pack,
|
||
* CPU readback, render-to-texture and copies are the 1x results — and a
|
||
* second, presentation-only surface at S covers just the columns the game
|
||
* displays: W = [x0, x1) x full VRAM height, W*S x 512*S. Every
|
||
* GPU write that touches W (draws, fills, copies, uploads, the depth24 clear)
|
||
* is mirrored into it, like the native-wide surfaces; present, hold-last,
|
||
* interpolation and the wide centre read it. W starts empty and grows to the
|
||
* union of displayed rects (rounded to 64 px) the first time each is shown,
|
||
* seeded by upscaling the 1x content, so it never needs to guess the layout.
|
||
* When the union no longer fits one surface (side-by-side 512-wide buffers at
|
||
* 18x need 18432 px), W becomes up to HIW_MAX_TILES tiles, each a column
|
||
* range of its own surface; every write goes to each tile it touches, so a
|
||
* game flipping between horizontally adjacent buffers presents every buffer
|
||
* at S. Tiles may overlap; overlapping columns hold identical pixels.
|
||
* A copy whose source lies outside every tile is taken from the 1x surface
|
||
* (nearest). Engaged only when the full-VRAM surface cannot hold the
|
||
* requested scale (driver limit or memory budget), or with
|
||
* PSX_GL_HIRES_WINDOW=1. */
|
||
#define HIW_ALIGN 64
|
||
#define HIW_MAX_TILES 4
|
||
typedef struct HiwTile {
|
||
int x0, x1; /* native columns [x0, x1) */
|
||
GLuint tex, fbo, rb;
|
||
} HiwTile;
|
||
static int s_hiw = 0; /* windowed mode engaged */
|
||
static HiwTile s_hiw_t[HIW_MAX_TILES]; /* empty until a display is shown */
|
||
static int s_hiw_n = 0;
|
||
static int s_hiw_last_x0 = 0, s_hiw_last_x1 = 0; /* last presented columns */
|
||
/* Staging for a copy's S-scaled source (hiw_mirror_copy), apart from the
|
||
* shared scratch that holds the 1x source. */
|
||
static GLuint s_hiw_scratch_tex = 0, s_hiw_scratch_fbo = 0;
|
||
static int s_hiw_scratch_w = 0, s_hiw_scratch_h = 0;
|
||
static int s_hiw_refused_logged = 0;
|
||
static int s_hiw_grows = 0; /* tile allocations (diagnostic) */
|
||
static int hiw_on(void);
|
||
static const HiwTile *hiw_ensure(int x0, int x1);
|
||
static void hiw_flush_queue(void);
|
||
static GLuint make_tex(GLenum internal, int w, int h, GLenum fmt, GLenum type);
|
||
static int make_fbo(GLuint *out_fbo, GLuint color_tex, GLuint stencil_rb);
|
||
static GLenum s_last_fbo_status;
|
||
static void hiw_clear_rect(int x, int y, int w, int h, float r, float g, float b,
|
||
float a, int stencil);
|
||
static void hiw_mirror_copy(int sx, int sy, int dx, int dy, int w, int h);
|
||
|
||
static GLuint s_present_tex = 0; /* CPU-readout present path (24bpp) */
|
||
static int s_present_w = 0, s_present_h = 0;
|
||
static GLuint s_osd_tex = 0;
|
||
static int s_osd_tw = 0, s_osd_th = 0;
|
||
static GLuint s_present_prog = 0, s_present_vao = 0;
|
||
static GLuint s_xr_color_prog = 0, s_xr_native_tex = 0;
|
||
static void gl_swap_with_osd(void);
|
||
static int s_native_surface_enabled, s_native_surface_pending;
|
||
static int s_native_surface_rect[4]; /* Fresh native backbuffer content, GL coordinates. */
|
||
static double s_native_surface_distance, s_native_surface_width, s_native_surface_units;
|
||
static void openxr_present_native(void);
|
||
static GLint s_present_uTex = -1, s_present_uUvRect = -1;
|
||
static GLint s_present_uTexSize = -1, s_present_uSharpScale = -1;
|
||
static GLint s_present_uSharp = -1;
|
||
static GLint s_present_uGamma = -1;
|
||
/* Host-side presentation state. The shader applies this only to the source
|
||
* image being presented; overlays and already-composed hold-last images set
|
||
* the uniform back to the identity explicitly. */
|
||
static float s_present_gamma = 1.0f;
|
||
/* Scanline post-process state (host display setting; see gl_renderer_set_scanlines).
|
||
* s_scanline_on/strength are pushed to whichever program draws game content;
|
||
* OSD/bezel and the already-composed hold-last re-present force it off so the
|
||
* effect is applied exactly once, at native-line pitch. */
|
||
static int s_scanline_on = 0;
|
||
static float s_scanline_strength = 0.5f;
|
||
static GLint s_present_uScanline = -1, s_present_uScanStrength = -1;
|
||
static GLint s_present_uScanLines = -1, s_present_uScanScale = -1;
|
||
static GLuint s_interp_prog = 0, s_interp_tex[3];
|
||
static GLint s_interp_uPrev = -1, s_interp_uCurr = -1;
|
||
static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1;
|
||
static GLint s_interp_uBlendMode = -1, s_interp_uGamma = -1;
|
||
static GLint s_interp_uScanline = -1, s_interp_uScanStrength = -1;
|
||
static GLint s_interp_uScanLines = -1, s_interp_uScanScale = -1;
|
||
static int s_interp_enabled = 0, s_interp_valid = 0;
|
||
static int s_interp_suspended = 0;
|
||
static int s_interp_blend_mode = 0;
|
||
static int s_interp_prev = 0, s_interp_cur = 0;
|
||
static int s_interp_w = 0, s_interp_h = 0, s_interp_linear = 0;
|
||
static int s_interp_src_w = 0, s_interp_src_h = 0; /* source band, S px */
|
||
static GLuint s_interp_fbo = 0; /* draw target of a scaled capture */
|
||
static int s_interp_force_4_3 = 0, s_interp_source_path = -1;
|
||
static uint64_t s_interp_swaps = 0;
|
||
static uint64_t s_interp_captures = 0;
|
||
static int s_interp_diag = 0;
|
||
static double s_interp_host_hz = 0.0;
|
||
static double s_interp_target_hz = 0.0;
|
||
static double s_interp_source_hz = 0.0;
|
||
static FrameInterpolationSchedule s_interp_schedule;
|
||
/* Blend source (psx_mod_set_frame_interpolation_source). VBLANK (0, the
|
||
* default) treats every guest VBlank present as a new frame, exactly as
|
||
* before. FLIP (1) rotates history only on a real flip -- the display origin
|
||
* moved or the displayed rect was redrawn -- and spreads one crossfade over
|
||
* the measured flip period, so 30 Hz content blends for the whole frame
|
||
* instead of blending for one VBlank and holding for the next. */
|
||
static int s_interp_source = 0;
|
||
static FrameFlipTracker s_interp_flip;
|
||
static int s_interp_origin_x = -1, s_interp_origin_y = -1;
|
||
static double s_interp_phase_lo = 0.0, s_interp_phase_hi = 1.0;
|
||
static uint64_t s_interp_duplicates = 0;
|
||
/* PSX_MOD_FRAME_INTERPOLATION_HOLD: no crossfade, repeat the newest frame
|
||
* wherever render passes supply no in-between image. */
|
||
static int s_interp_hold = 0;
|
||
static void pass_gens_invalidate(void);
|
||
static void pass_note_new_frame(int origin_x, int origin_y, int source_path,
|
||
int pw, int ph);
|
||
static void pass_apply_promotion(void);
|
||
static int pass_gen_present(uint64_t deadline);
|
||
static int stereo_present(int w, int h);
|
||
static void stereo_resources_release(void);
|
||
static void stereo_invalidate(void);
|
||
static uint64_t s_idle_ticks_accum_fwd(uint64_t add);
|
||
/* Render-pass VRAM transaction (see "Render passes" below). While a pass is
|
||
* open, GPU writes are confined to its rect: the scissor is intersected with
|
||
* it and writes that bypass the scissor are refused and counted. */
|
||
static int s_pass_active = 0;
|
||
/* Textures/framebuffers made for passes (pass_make_color_fbo, pass image
|
||
* slots), and the count when the current pass began: a pass that allocated
|
||
* is left out of the pass-cost average (render_pass_cost_add). */
|
||
static uint32_t s_pass_allocs = 0, s_pass_allocs_begin = 0;
|
||
static int s_pass_x = 0, s_pass_y = 0, s_pass_w = 0, s_pass_h = 0;
|
||
static uint32_t s_pass_leaks = 0;
|
||
/* Policy and CPU rows of the out-of-rect journal (render_pass_plan.c,
|
||
* render_pass_sandbox_test); the GPU copies live in s_pj below. */
|
||
static RenderPassJournal s_pj_cpu;
|
||
static int pass_journal_protect(int x, int y, int w, int h);
|
||
/* A fill, copy or upload outside the pass rect (e.g. a game that moves a
|
||
* few pixels of VRAM as part of every frame) is journaled -- its destination
|
||
* backed up and restored at the end of the pass -- or, when the journal is
|
||
* full, refused and counted (the pass is then rolled back without an image).
|
||
* Native-wide needs nothing more: fills, copies, uploads and pokes write the
|
||
* hr surface, the raw mirror and the CPU rows only, never a wide surface
|
||
* (the wide margins come from mirrored draws, which the pass scissor keeps
|
||
* inside the pass band). */
|
||
static int pass_refuse_write(const char *what, int x, int y, int w, int h) {
|
||
int policy;
|
||
if (!s_pass_active) return 0;
|
||
policy = render_pass_vram_policy(&s_pj_cpu, s_pass_x, s_pass_y, s_pass_w,
|
||
s_pass_h, VRAM_W, VRAM_H, 1,
|
||
&x, &y, &w, &h);
|
||
if (policy == RENDER_PASS_VRAM_ALLOW) return 0;
|
||
if (policy == RENDER_PASS_VRAM_JOURNAL && pass_journal_protect(x, y, w, h))
|
||
return 0;
|
||
if (s_pass_leaks < 8)
|
||
fprintf(stderr, "psxrecomp: render pass refused %s %d,%d %dx%d outside "
|
||
"%d,%d %dx%d\n", what, x, y, w, h, s_pass_x, s_pass_y,
|
||
s_pass_w, s_pass_h);
|
||
s_pass_leaks++;
|
||
return 1;
|
||
}
|
||
static void interp_reset_history(void);
|
||
static int interp_present(float alpha);
|
||
static void interp_present_source_interval(void);
|
||
static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh);
|
||
|
||
static int s_raster_ok = 0; /* full GPU pipeline available */
|
||
static GLuint s_bank_tex[65536];
|
||
static GLuint s_selected_bank_tex;
|
||
static int s_selected_bank_live_clut, s_tb_bank_live_clut;
|
||
|
||
/* 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;
|
||
static int s_scratch_w = 0, s_scratch_h = 0;
|
||
|
||
/* Programs. */
|
||
static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0;
|
||
static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0;
|
||
/* Textured vertex: pos(2) uv(2) col(4) tpage(2) clut(2) depth(1) raw(1) limits(4)
|
||
* semi(1) q(1) twin(1)
|
||
* — per-prim texture state in flat attributes so prims batch (see flush_tex_batch).
|
||
* q is the perspective weight ([video] perspective_texturing): 0 = affine, the
|
||
* PS1-faithful default, which makes the vertex shader's w exactly 1.0 and the
|
||
* fragment shader read the noperspective varying — i.e. bit-identical to the
|
||
* pre-feature pipeline. twin is the prim's GP0(E2h) texture window, its low 20
|
||
* bits as a whole float (mask x, mask y, offset x, offset y; 5 bits each). */
|
||
#define TEXV 26
|
||
static GLuint s_blit_prog = 0, s_blit_vao = 0, s_blit_vbo = 0;
|
||
static GLuint s_blit_hi_prog = 0; /* windowed hi surface blit */
|
||
static GLint s_uBhSrc = -1, s_uBhPass = -1, s_uBhMaskset = -1, s_uBhSrcDiv = -1;
|
||
static GLint s_uBhSrcOff = -1, s_uBhShift = -1, s_uBhXoff = -1, s_uBhXhalf = -1;
|
||
/* Sample-grid shift per surface scale (see GEO_VS): the hr surface at
|
||
* s_hr_scale, the hi window and the wide surfaces at s_out_scale. */
|
||
static float s_shift_hr = 0.0f, s_shift_hi = 0.0f;
|
||
static GLint s_geo_uShift = -1, s_tex_uShift = -1;
|
||
static GLuint s_pack_prog = 0, s_stencil_prog = 0, s_empty_vao = 0;
|
||
|
||
/* Sub-pixel vertex override + perspective UV weights for the next triangle
|
||
* ([video] geometry_correction / perspective_texturing; see gpu_render.h).
|
||
* gpu.c sets these immediately before the matching gr_draw_*_triangle and they
|
||
* are consumed by it. All-zero == the faithful integer/affine path. */
|
||
static int s_pc_valid = 0; /* sub-pixel positions present */
|
||
static int s_projected_uv_valid;
|
||
static float s_projected_u[3], s_projected_v[3];
|
||
static float s_pc_x[3], s_pc_y[3]; /* native VRAM px, fractional */
|
||
static int s_pq_valid = 0; /* perspective weights present */
|
||
static float s_pq[3];
|
||
|
||
/* TEX program uniforms. */
|
||
static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1;
|
||
static GLint s_uHdTexture = -1;
|
||
static GLint s_uPalette = -1;
|
||
static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1;
|
||
static GLint 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, s_uStencilOff = -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_cpu_dirty; /* conservative GPU-written area not yet read into CPU VRAM */
|
||
static DirtyRect s_pack_dirty; /* hr FBO content not in raw mirror */
|
||
/* Native-coords union of primitive bboxes since the last stencil rebuild: the
|
||
* only area whose stencil can lag its alpha (deferred-stencil draws). Used at
|
||
* S > 1 so a mask-check toggle rebuilds that rect, not the whole surface. */
|
||
static DirtyRect s_stencil_stale;
|
||
static void hiw_mirror_uploads(const DirtyRect *rects, int nrects);
|
||
|
||
/* 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;
|
||
/* Diagnostic only. The frontend admits proven world geometry into both the
|
||
* canonical and wide passes so a face cannot split at the center-copy edge. */
|
||
static uint64_t s_wide_triangle_recovery_count;
|
||
void gl_renderer_note_wide_triangle_recovery(int recovered) {
|
||
if (recovered && s_raster_ok && g_wide_cur) ++s_wide_triangle_recovery_count;
|
||
}
|
||
uint64_t gl_renderer_wide_triangle_recovery_count(void) {
|
||
return s_wide_triangle_recovery_count;
|
||
}
|
||
|
||
|
||
/* 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 int view_enabled, view_shift, view_pad_left, view_pad_right;
|
||
static inline int wide_dx(void) { return g_wide_off + view_shift - g_wide_cur_base; }
|
||
|
||
/* ---- dirty-rect helpers ------------------------------------------------- */
|
||
static void rect_clear(DirtyRect *r) { r->set = 0; }
|
||
static void rect_add(DirtyRect *r, int x0, int y0, int x1, int y1) {
|
||
if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0;
|
||
if (x1 > VRAM_W - 1) x1 = VRAM_W - 1;
|
||
if (y1 > VRAM_H - 1) y1 = VRAM_H - 1;
|
||
if (x0 > x1 || y0 > y1) return;
|
||
if (!r->set) { r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; r->set = 1; return; }
|
||
if (x0 < r->x0) r->x0 = x0;
|
||
if (y0 < r->y0) r->y0 = y0;
|
||
if (x1 > r->x1) r->x1 = x1;
|
||
if (y1 > r->y1) r->y1 = y1;
|
||
}
|
||
static int rect_intersects(const DirtyRect *r, int x0, int y0, int x1, int y1) {
|
||
if (!r->set) return 0;
|
||
return !(x1 < r->x0 || x0 > r->x1 || y1 < r->y0 || y0 > r->y1);
|
||
}
|
||
|
||
/* ---- exact pending-upload rect list (see s_up_rects comment) ------------- */
|
||
static void flush_cpu_upload(void); /* fwd: overflow flushes then re-adds */
|
||
|
||
/* Add an uploaded rect. Merges ONLY when the merge adds no uncovered pixels:
|
||
* containment either way, or an extension within the same row-band / column-
|
||
* band (equal y-range with touching/overlapping x-ranges, or equal x-range
|
||
* with touching/overlapping y-ranges — the row-scan / column-scan poke
|
||
* patterns). Never unions disjoint rects post-init (that was the flicker bug). */
|
||
static void up_add(int x0, int y0, int x1, int y1) {
|
||
if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0;
|
||
if (x1 > VRAM_W - 1) x1 = VRAM_W - 1;
|
||
if (y1 > VRAM_H - 1) y1 = VRAM_H - 1;
|
||
if (x0 > x1 || y0 > y1) return;
|
||
for (int i = 0; i < s_up_nrects; i++) {
|
||
DirtyRect *r = &s_up_rects[i];
|
||
if (x0 >= r->x0 && x1 <= r->x1 && y0 >= r->y0 && y1 <= r->y1)
|
||
return; /* contained */
|
||
if (x0 <= r->x0 && x1 >= r->x1 && y0 <= r->y0 && y1 >= r->y1) {
|
||
r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; /* contains */
|
||
return;
|
||
}
|
||
if (y0 == r->y0 && y1 == r->y1 &&
|
||
x0 <= r->x1 + 1 && x1 >= r->x0 - 1) { /* same row-band extension */
|
||
if (x0 < r->x0) r->x0 = x0;
|
||
if (x1 > r->x1) r->x1 = x1;
|
||
return;
|
||
}
|
||
if (x0 == r->x0 && x1 == r->x1 &&
|
||
y0 <= r->y1 + 1 && y1 >= r->y0 - 1) { /* same column-band extension */
|
||
if (y0 < r->y0) r->y0 = y0;
|
||
if (y1 > r->y1) r->y1 = y1;
|
||
return;
|
||
}
|
||
}
|
||
if (s_up_nrects >= UP_RECTS_MAX) {
|
||
if (s_raster_ok) {
|
||
flush_cpu_upload(); /* order-preserving: land the old ones */
|
||
} else {
|
||
/* Pre-context: the CPU array is fully authoritative (software
|
||
* rasterizer mirrors every op), so a union is harmless. */
|
||
DirtyRect *r = &s_up_rects[0];
|
||
for (int i = 1; i < s_up_nrects; i++) {
|
||
if (s_up_rects[i].x0 < r->x0) r->x0 = s_up_rects[i].x0;
|
||
if (s_up_rects[i].y0 < r->y0) r->y0 = s_up_rects[i].y0;
|
||
if (s_up_rects[i].x1 > r->x1) r->x1 = s_up_rects[i].x1;
|
||
if (s_up_rects[i].y1 > r->y1) r->y1 = s_up_rects[i].y1;
|
||
}
|
||
if (x0 < r->x0) r->x0 = x0;
|
||
if (y0 < r->y0) r->y0 = y0;
|
||
if (x1 > r->x1) r->x1 = x1;
|
||
if (y1 > r->y1) r->y1 = y1;
|
||
s_up_nrects = 1;
|
||
return;
|
||
}
|
||
}
|
||
DirtyRect *r = &s_up_rects[s_up_nrects++];
|
||
r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; r->set = 1;
|
||
}
|
||
|
||
/* Add a GP0(A0) transfer's exact touched region. The software reference wraps
|
||
* per pixel (px = (x+col) & 1023, py = (y+row) & 511), so a wrapping transfer
|
||
* touches up to four exact rects — NOT all of VRAM (the old "wrapped: take
|
||
* all" full-VRAM union painted stale CPU content over the framebuffers). */
|
||
static void up_add_transfer(int x, int y, int w, int h) {
|
||
x &= VRAM_W - 1; y &= VRAM_H - 1;
|
||
if (w > VRAM_W) w = VRAM_W;
|
||
if (h > VRAM_H) h = VRAM_H;
|
||
if (w <= 0 || h <= 0) return;
|
||
int w1 = w, w2 = 0, h1 = h, h2 = 0;
|
||
if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; }
|
||
if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; }
|
||
up_add(x, y, x + w1 - 1, y + h1 - 1);
|
||
if (w2) up_add(0, y, w2 - 1, y + h1 - 1);
|
||
if (h2) up_add(x, 0, x + w1 - 1, h2 - 1);
|
||
if (w2 && h2) up_add(0, 0, w2 - 1, h2 - 1);
|
||
}
|
||
|
||
/* ---- coherency event ring (always-on, debug server "gl_coh_ring") -------- */
|
||
/* Every coherency-relevant operation — upload flushes, fills, copies, draw
|
||
* bboxes, packs, full readbacks, presents, and probe perturbations — is
|
||
* recorded with its rect and frame number. Per CLAUDE.md ring-buffer rule:
|
||
* capture is continuous, observers query a window after the fact. Trigger
|
||
* attribution convention: an op that flushes internally (fill/copy/draw/
|
||
* present/peek) records its own event AFTER the FLUSH event it caused, so
|
||
* the event following a FLUSH names the trigger. 16 B * 64 Ki = 1 MB. */
|
||
extern uint64_t s_frame_count; /* defined in debug_server.c */
|
||
|
||
#define GL_COH_RING_CAP (1u << 16)
|
||
static GlCohEvent s_coh_ring[GL_COH_RING_CAP];
|
||
static uint64_t s_coh_seq = 0;
|
||
|
||
/* 16x16 native-pixel tiles changed since their last on-screen present. This
|
||
* lets a double-buffered 30 Hz game avoid swapping the unchanged front buffer
|
||
* on the intervening 60 Hz vblank without guessing from game identity.
|
||
*
|
||
* INVARIANT: eliding a swap also elides whatever that swap was blocking on.
|
||
* The frontend calls present once per guest vblank, so when the driver's swap
|
||
* block owns the guest cadence (psx_present_vsync_owns_cadence(), the XOR
|
||
* partner of the wall-clock pacer) an elided frame is an unthrottled frame:
|
||
* a 30 Hz-presenting game then advances two vblanks per block and runs at 2x.
|
||
* Every skip site below is therefore gated on that query. */
|
||
#define PRES_TILE 16
|
||
#define PRES_ROWS (VRAM_H / PRES_TILE)
|
||
static uint64_t s_present_dirty[PRES_ROWS];
|
||
static int s_last_present_path = -1;
|
||
static int s_last_dx, s_last_dy, s_last_dw, s_last_dh;
|
||
/* After savestate restore: keep swapping for a few presents even when the
|
||
* display rect is byte-identical to the last swap. Double/triple-buffered
|
||
* windows otherwise can leave a stale back buffer on screen while vblanks
|
||
* (and FPS) keep advancing — especially on a 2nd+ load of the same slot. */
|
||
static int s_force_present_remaining = 0;
|
||
|
||
/* §33 rollback resim hold-last: sticky copy of the last Live present so
|
||
* mid-resim ticks can Swap a frozen frame without reading mutating VRAM.
|
||
* HOLD_DRAWABLE = full backbuffer copy taken just before SwapWindow.
|
||
* HOLD_NATIVE = GPU blit of a display band (used when interp owns Swap). */
|
||
#define HOLD_NONE 0
|
||
#define HOLD_DRAWABLE 1
|
||
#define HOLD_NATIVE 2
|
||
static int s_hold_kind = HOLD_NONE;
|
||
static GLuint s_hold_tex = 0;
|
||
static GLuint s_hold_fbo = 0;
|
||
static int s_hold_tw = 0, s_hold_th = 0; /* texture size in texels */
|
||
static int s_hold_force_4_3 = 0;
|
||
static int s_hold_linear = 0;
|
||
|
||
/* Post-load freeze probe (main.cpp): accumulate skip/swap/dirty marks. */
|
||
static uint64_t s_probe_skip = 0;
|
||
static uint64_t s_probe_swap = 0;
|
||
static uint64_t s_probe_dirty_marks = 0;
|
||
|
||
static void present_dirty_rect(int x0, int y0, int x1, int y1, int set) {
|
||
if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0;
|
||
if (x1 >= VRAM_W) x1 = VRAM_W - 1; if (y1 >= VRAM_H) y1 = VRAM_H - 1;
|
||
if (x0 > x1 || y0 > y1) return;
|
||
int tx0 = x0 / PRES_TILE, tx1 = x1 / PRES_TILE;
|
||
uint64_t mask = (~0ull << tx0) & (~0ull >> (63 - tx1));
|
||
for (int ty = y0 / PRES_TILE; ty <= y1 / PRES_TILE; ty++) {
|
||
if (set) s_present_dirty[ty] |= mask; else s_present_dirty[ty] &= ~mask;
|
||
}
|
||
if (set) s_probe_dirty_marks++;
|
||
}
|
||
|
||
static int present_dirty_test(int x0, int y0, int x1, int y1) {
|
||
if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0;
|
||
if (x1 >= VRAM_W) x1 = VRAM_W - 1; if (y1 >= VRAM_H) y1 = VRAM_H - 1;
|
||
if (x0 > x1 || y0 > y1) return 0;
|
||
int tx0 = x0 / PRES_TILE, tx1 = x1 / PRES_TILE;
|
||
uint64_t mask = (~0ull << tx0) & (~0ull >> (63 - tx1));
|
||
for (int ty = y0 / PRES_TILE; ty <= y1 / PRES_TILE; ty++)
|
||
if (s_present_dirty[ty] & mask) return 1;
|
||
return 0;
|
||
}
|
||
|
||
static void present_force_consumed(void) {
|
||
if (s_force_present_remaining > 0)
|
||
s_force_present_remaining--;
|
||
}
|
||
|
||
static void hold_ensure_tex(int w, int h) {
|
||
if (w < 1) w = 1;
|
||
if (h < 1) h = 1;
|
||
if (!s_hold_tex) {
|
||
glGenTextures(1, &s_hold_tex);
|
||
if (!s_hold_fbo)
|
||
p_glGenFramebuffers(1, &s_hold_fbo);
|
||
}
|
||
if (s_hold_tw == w && s_hold_th == h)
|
||
return;
|
||
glBindTexture(GL_TEXTURE_2D, s_hold_tex);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||
NULL);
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hold_fbo);
|
||
p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0,
|
||
GL_TEXTURE_2D, s_hold_tex, 0);
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||
s_hold_tw = w;
|
||
s_hold_th = h;
|
||
}
|
||
|
||
/* Snapshot the just-drawn default backbuffer (letterbox + content) before Swap.
|
||
* Hold-last can then redraw this exact image without touching guest VRAM. */
|
||
static void hold_capture_drawable(void) {
|
||
int ww = 0, wh = 0;
|
||
if (!s_ctx || !s_win)
|
||
return;
|
||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||
if (ww < 1 || wh < 1)
|
||
return;
|
||
hold_ensure_tex(ww, wh);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
glBindTexture(GL_TEXTURE_2D, s_hold_tex);
|
||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, ww, wh);
|
||
glBindTexture(GL_TEXTURE_2D, 0);
|
||
s_hold_kind = HOLD_DRAWABLE;
|
||
s_hold_force_4_3 = 0;
|
||
s_hold_linear = 0;
|
||
}
|
||
|
||
/* Defined after present_target_quad (letterbox helpers). */
|
||
static void letterbox_rect_aspect(int ww, int wh, int num, int den,
|
||
int *x, int *y, int *w, int *h);
|
||
static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h);
|
||
|
||
/* Size of a display-band capture (temporal-blend history, hold-last) whose
|
||
* source band is sw x sh pixels at the output scale. Normally that size. In
|
||
* the windowed high-resolution mode the band is up to 15372x4320 (8K at
|
||
* 32:9), and copying it at full size every game frame (twice: blend history
|
||
* and hold-last) cost more than the frame itself, only for the presenter to
|
||
* draw it at the letterbox size. There the capture is taken at the letterbox
|
||
* size instead (one filtered blit; the presenter then draws it 1:1), unless
|
||
* the band is already smaller. force_4_3 picks the letterbox the present
|
||
* uses. */
|
||
static void hiw_capture_size(int sw, int sh, int force_4_3, int *cw, int *ch) {
|
||
int ww = 0, wh = 0, lx, ly, lw, lh;
|
||
*cw = sw; *ch = sh;
|
||
if (!s_hiw || !s_win) return;
|
||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||
if (ww < 1 || wh < 1) return;
|
||
if (force_4_3) letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh);
|
||
else letterbox_rect(ww, wh, &lx, &ly, &lw, &lh);
|
||
if (lw > 0 && lh > 0 && (int64_t)lw * lh < (int64_t)sw * sh) {
|
||
*cw = lw; *ch = lh;
|
||
}
|
||
}
|
||
|
||
/* Snapshot a native display band from an FBO when the main thread will not
|
||
* Swap (interpolation owns the cadence). Scale-aware blit into hold tex (at
|
||
* the presented size in the windowed high-resolution mode). */
|
||
static void hold_capture_native_fbo(GLuint src_fbo, int dx, int dy, int dw, int dh,
|
||
int force_4_3, int linear) {
|
||
int S = s_out_scale > 0 ? s_out_scale : 1, cw, ch;
|
||
if (!s_ctx || !src_fbo || dw < 1 || dh < 1)
|
||
return;
|
||
hiw_capture_size(dw * S, dh * S, force_4_3, &cw, &ch);
|
||
hold_ensure_tex(cw, ch);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src_fbo);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_hold_fbo);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBlitFramebuffer(dx * S, dy * S, (dx + dw) * S, (dy + dh) * S,
|
||
0, 0, cw, ch, GL_COLOR_BUFFER_BIT,
|
||
(cw == dw * S && ch == dh * S) || !linear ? GL_NEAREST : GL_LINEAR);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||
s_hold_kind = HOLD_NATIVE;
|
||
s_hold_force_4_3 = force_4_3 ? 1 : 0;
|
||
s_hold_linear = linear ? 1 : 0;
|
||
}
|
||
|
||
static void hold_invalidate(void) {
|
||
s_hold_kind = HOLD_NONE;
|
||
}
|
||
|
||
/* Defined after present_target_quad / letterbox helpers. */
|
||
static void present_target_quad(GLuint tex, int tex_w, int tex_h,
|
||
int x, int y, int w, int h, int linear,
|
||
int lx, int ly, int lw, int lh, int v_flip,
|
||
int apply_gamma, int src_scale);
|
||
|
||
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) {
|
||
s_native_surface_pending=s_native_surface_enabled &&
|
||
(path==GL_PRES_VRAM || path==GL_PRES_WIDE || path==GL_PRES_CPU || path==GL_PRES_BLANK);
|
||
s_native_surface_rect[0]=lx;s_native_surface_rect[1]=ly;
|
||
s_native_surface_rect[2]=lw;s_native_surface_rect[3]=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_hr_scale, (dy + h / 2) * s_hr_scale,
|
||
1, 1, GL_RGB, GL_UNSIGNED_BYTE, sp);
|
||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
e->src_valid = 1;
|
||
}
|
||
e->src_r = sp[0]; e->src_g = sp[1]; e->src_b = sp[2];
|
||
s_pres_seq++;
|
||
}
|
||
|
||
uint64_t gl_renderer_pres_total(void) { return s_pres_seq; }
|
||
int gl_renderer_pres_get(uint64_t seq, GlPresEvent *out) {
|
||
if (seq >= s_pres_seq) return 0;
|
||
if (s_pres_seq - seq > GL_PRES_RING_CAP) return 0; /* evicted */
|
||
*out = s_pres_ring[seq % GL_PRES_RING_CAP];
|
||
return 1;
|
||
}
|
||
|
||
/* ---- shaders ------------------------------------------------------------ */
|
||
static const char *PRESENT_VS =
|
||
"#version 330\n"
|
||
"out vec2 v_uv;\n"
|
||
"uniform vec4 u_uv_rect;\n"
|
||
"void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n"
|
||
" v_uv = vec2(mix(u_uv_rect.x,u_uv_rect.z,p.x),\n"
|
||
" mix(u_uv_rect.y,u_uv_rect.w,1.0-p.y));\n"
|
||
" gl_Position = vec4(p*2.0-1.0,0.0,1.0); }\n";
|
||
/* Present sampling. u_sharp==0 is the historical behaviour: sample straight at
|
||
* v_uv, so the texture's own filter (NEAREST or LINEAR) decides everything.
|
||
*
|
||
* u_sharp==1 selects SHARP-BILINEAR, for upscaling a low-res source (FMV) to a
|
||
* much larger window. Plain GL_LINEAR blends across the whole texel and turns a
|
||
* 320x192 movie to mush; plain GL_NEAREST keeps it crisp but blocky and makes
|
||
* the non-integer scale factor beat (some source pixels land 3 window pixels
|
||
* wide, their neighbours 4). Sharp-bilinear keeps each texel flat across its
|
||
* interior and confines the linear ramp to a ONE-OUTPUT-PIXEL-wide band at the
|
||
* texel boundary: crisp like nearest, but without the uneven pixel widths.
|
||
*
|
||
* u_sharp_scale is output pixels per texel. At <=1 (downscale) the band covers
|
||
* the whole texel and this degrades to plain bilinear, which is what you want
|
||
* there. The result is clamped to u_uv_rect so the half-texel edge inset the
|
||
* caller applied still holds — that inset is what keeps LINEAR from bleeding
|
||
* the border texel into the image (the old reason FMV was pinned to NEAREST). */
|
||
/* Scanline post-process, shared by the present and interpolation shaders.
|
||
* Darkens toward the gap between PS1 scanlines with a soft sinusoidal beam
|
||
* centred on each line. The pitch is u_scanline_lines (the number of native
|
||
* display lines across the source rect), so it tracks the PS1 line grid, not
|
||
* the internal SSAA scale or the window size. u_scanline_scale is output pixels
|
||
* per PS1 line; the gate fades the effect in from 1x to 2x so it never shimmers
|
||
* on a window too small to resolve one line as two rows (Nyquist). Computed
|
||
* against v_uv.y (position across the display region) so it is orientation- and
|
||
* scale-agnostic. */
|
||
#define PSX_SCANLINE_UNIFORMS \
|
||
"uniform int u_scanline;\n" \
|
||
"uniform float u_scanline_strength;\n" \
|
||
"uniform float u_scanline_lines;\n" \
|
||
"uniform float u_scanline_scale;\n"
|
||
#define PSX_SCANLINE_FUNC \
|
||
"vec3 psx_scanline(vec3 rgb, float vy){\n" \
|
||
" if (u_scanline == 0) return rgb;\n" \
|
||
" float gate = clamp(u_scanline_scale - 1.0, 0.0, 1.0);\n" \
|
||
" if (gate <= 0.0) return rgb;\n" \
|
||
" float p = fract(vy * u_scanline_lines);\n" \
|
||
" float beam = sin(3.14159265 * p);\n" \
|
||
" float mult = 1.0 - (u_scanline_strength * gate) * (1.0 - beam);\n" \
|
||
" return rgb * mult;\n" \
|
||
"}\n"
|
||
static const char *PRESENT_FS =
|
||
"#version 330\n"
|
||
"in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n"
|
||
"uniform vec4 u_uv_rect;\n"
|
||
"uniform vec2 u_tex_size;\n"
|
||
"uniform vec2 u_sharp_scale;\n"
|
||
"uniform int u_sharp;\n"
|
||
"uniform float u_gamma;\n"
|
||
PSX_SCANLINE_UNIFORMS
|
||
/* Catmull-Rom bicubic via 9 bilinear taps. Sharper than plain bilinear at
|
||
* the same smoothness, with mild overshoot that reads as edge definition.
|
||
* The present texture holds exactly the source rect and wraps CLAMP_TO_EDGE,
|
||
* so the +/-2 texel footprint clamps to real edge pixels, never garbage. */
|
||
"vec4 bicubic(vec2 uv){\n"
|
||
" vec2 sp = uv * u_tex_size;\n"
|
||
" vec2 t1 = floor(sp - 0.5) + 0.5;\n"
|
||
" vec2 f = sp - t1;\n"
|
||
" vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f));\n"
|
||
" vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f);\n"
|
||
" vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f));\n"
|
||
" vec2 w3 = f * f * (-0.5 + 0.5 * f);\n"
|
||
" vec2 w12 = w1 + w2;\n"
|
||
" vec2 t0 = (t1 - 1.0) / u_tex_size;\n"
|
||
" vec2 t3 = (t1 + 2.0) / u_tex_size;\n"
|
||
" vec2 t12 = (t1 + w2 / w12) / u_tex_size;\n"
|
||
" vec4 r = vec4(0.0);\n"
|
||
" r += texture(u_tex, vec2(t0.x , t0.y )) * (w0.x * w0.y );\n"
|
||
" r += texture(u_tex, vec2(t12.x, t0.y )) * (w12.x * w0.y );\n"
|
||
" r += texture(u_tex, vec2(t3.x , t0.y )) * (w3.x * w0.y );\n"
|
||
" r += texture(u_tex, vec2(t0.x , t12.y)) * (w0.x * w12.y);\n"
|
||
" r += texture(u_tex, vec2(t12.x, t12.y)) * (w12.x * w12.y);\n"
|
||
" r += texture(u_tex, vec2(t3.x , t12.y)) * (w3.x * w12.y);\n"
|
||
" r += texture(u_tex, vec2(t0.x , t3.y )) * (w0.x * w3.y );\n"
|
||
" r += texture(u_tex, vec2(t12.x, t3.y )) * (w12.x * w3.y );\n"
|
||
" r += texture(u_tex, vec2(t3.x , t3.y )) * (w3.x * w3.y );\n"
|
||
" return r;\n"
|
||
"}\n"
|
||
PSX_SCANLINE_FUNC
|
||
/* Area resolve (u_sharp == 3) for a supersampled source larger than the
|
||
* output: average the whole footprint of the output pixel instead of one
|
||
* tap, which skips source texels (aliasing) once the ratio passes ~1.25.
|
||
* u_sharp_scale is output px per texel, so 1/u_sharp_scale is the
|
||
* footprint in texels. Each tap is bilinear and taps are spaced up to two
|
||
* texels apart, so every tap averages a 2x2 texel block: an exact box at
|
||
* even ratios, a close tent otherwise. Taps clamp to u_uv_rect so the
|
||
* neighbouring VRAM never bleeds in. */
|
||
"vec4 area_resolve(vec2 uv){\n"
|
||
" vec2 fp = 1.0 / max(u_sharp_scale, vec2(1.0e-6));\n"
|
||
" vec2 nf = clamp(ceil(fp * 0.5), vec2(1.0), vec2(8.0));\n"
|
||
" ivec2 n = ivec2(nf);\n"
|
||
" vec2 st = fp / nf;\n"
|
||
" vec2 base = uv * u_tex_size - 0.5 * fp + 0.5 * st;\n"
|
||
" vec2 lo = min(u_uv_rect.xy, u_uv_rect.zw);\n"
|
||
" vec2 hi = max(u_uv_rect.xy, u_uv_rect.zw);\n"
|
||
" vec4 acc = vec4(0.0);\n"
|
||
" for (int j = 0; j < n.y; j++)\n"
|
||
" for (int i = 0; i < n.x; i++)\n"
|
||
" acc += texture(u_tex, clamp((base + vec2(i, j) * st) / u_tex_size, lo, hi));\n"
|
||
" return acc / float(n.x * n.y);\n"
|
||
"}\n"
|
||
"void main(){\n"
|
||
" vec2 uv = v_uv;\n"
|
||
" vec4 c;\n"
|
||
" if (u_sharp == 2) { c = bicubic(uv); }\n"
|
||
" else if (u_sharp == 3) { c = area_resolve(uv); }\n"
|
||
" else {\n"
|
||
" if (u_sharp == 1) {\n"
|
||
" vec2 scale = max(u_sharp_scale, vec2(1.0));\n"
|
||
" vec2 texel = uv * u_tex_size;\n"
|
||
" vec2 tf = floor(texel);\n"
|
||
" vec2 cd = (texel - tf) - 0.5;\n"
|
||
" vec2 band = 0.5 - 0.5 / scale;\n"
|
||
" vec2 f = (cd - clamp(cd, -band, band)) * scale + 0.5;\n"
|
||
" uv = (tf + f) / u_tex_size;\n"
|
||
" vec2 lo = min(u_uv_rect.xy, u_uv_rect.zw);\n"
|
||
" vec2 hi = max(u_uv_rect.xy, u_uv_rect.zw);\n"
|
||
" uv = clamp(uv, lo, hi);\n"
|
||
" }\n"
|
||
" c = texture(u_tex, uv);\n"
|
||
" }\n"
|
||
" c.rgb = psx_scanline(c.rgb, v_uv.y);\n"
|
||
" if (u_gamma > 0.0 && u_gamma != 1.0) c.rgb = pow(max(c.rgb, vec3(0.0)), vec3(1.0 / u_gamma));\n"
|
||
" frag = c;\n"
|
||
"}\n";
|
||
static const char *INTERP_FS =
|
||
"#version 330\n"
|
||
"in vec2 v_uv; uniform sampler2D u_prev; uniform sampler2D u_curr;\n"
|
||
"uniform float u_alpha; uniform int u_blend_mode; uniform float u_gamma; out vec4 frag;\n"
|
||
PSX_SCANLINE_UNIFORMS
|
||
PSX_SCANLINE_FUNC
|
||
"void main(){\n"
|
||
" vec4 prev=texture(u_prev,v_uv), curr=texture(u_curr,v_uv);\n"
|
||
" float alpha=u_alpha;\n"
|
||
" if(u_blend_mode==1){\n"
|
||
" vec3 d=abs(prev.rgb-curr.rgb);\n"
|
||
" float change=max(max(d.r,d.g),d.b);\n"
|
||
" float safe_blend=1.0-smoothstep(0.08,0.20,change);\n"
|
||
" alpha=mix(step(0.5,u_alpha),u_alpha,safe_blend);\n"
|
||
" }\n"
|
||
" vec4 c=mix(prev,curr,alpha);\n"
|
||
" c.rgb=psx_scanline(c.rgb, v_uv.y);\n"
|
||
" if (u_gamma > 0.0 && u_gamma != 1.0) c.rgb=pow(max(c.rgb, vec3(0.0)), vec3(1.0/u_gamma));\n"
|
||
" frag=c;\n"
|
||
"}\n";
|
||
|
||
/* Geometry: position in VRAM pixels (draw offset already applied by gpu.c),
|
||
* color rgb in 0..1, color a = mask bit (0/1). The clip transform is in
|
||
* native VRAM space; the viewport at S* the size scales rasterization.
|
||
*
|
||
* ALL drawn prims shift positions by u_shift = half an HR pixel (0.5/S in
|
||
* native units): GL samples coverage/attributes at pixel CENTERS, the PS1
|
||
* DDA at INTEGER coords. The shift aligns GL's sample grid with the PS1
|
||
* grid — without it, any texture mapping with slope != 1 (scaled sprites,
|
||
* squished menu fonts) samples one texel off per row/column (striped
|
||
* glyphs, seam lines). Half an HR pixel (not half a native pixel!) keeps
|
||
* rect coverage exactly [x*S, (x+w)*S) at every scale AND makes the
|
||
* top-left subpixel of each S*S block sample the exact PS1 value (which is
|
||
* what the PACK pass reads back). */
|
||
static const char *GEO_VS =
|
||
"#version 330\n"
|
||
"layout(location=0) in vec2 a_pos;\n"
|
||
"layout(location=1) in vec4 a_col;\n"
|
||
"uniform float u_shift;\n"
|
||
"uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n"
|
||
"uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n"
|
||
"uniform float u_xscale; /* native-wide 2D-backdrop x-stretch; 1 canonical */\n"
|
||
"uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n"
|
||
"noperspective out vec4 v_col;\n"
|
||
"void main(){ v_col = a_col;\n"
|
||
" float xb = a_pos.x;\n"
|
||
" if (u_xscale < 0.0) {\n"
|
||
" float s = -u_xscale; float h = u_xhalf / s;\n"
|
||
" float l = u_xcenter - h, r = u_xcenter + h;\n"
|
||
" if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n"
|
||
" } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n"
|
||
" gl_Position = vec4((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n";
|
||
static const char *GEO_FS =
|
||
"#version 330\n"
|
||
"noperspective in vec4 v_col; out vec4 frag;\n"
|
||
"void main(){ frag = v_col; }\n";
|
||
|
||
/* Textured prims: sample raw 1555 VRAM (integer), CLUT decode per depth,
|
||
* texture window, optional bilinear, texel-0 discard, STP-split discard,
|
||
* PS1 *2-around-0x80 modulation. Output alpha = bit15 of the written pixel.
|
||
* Texel coords use floor() to match the software rasterizer's truncation
|
||
* (rounding shifted sampling +1 texel half the time: smeared text). */
|
||
/* Textured program. Per-prim texture state (texpage, clut, depth, raw, uv
|
||
* limits) is carried in FLAT vertex attributes — constant across a prim's
|
||
* vertices — instead of uniforms, so consecutive textured prims with the same
|
||
* blend/mask state batch into one draw (see flush_tex_batch). The texture
|
||
* window rides in the vertex too (a_twin), so a GP0(E2h) change need not end
|
||
* a batch. The remaining uniforms (u_maskset/u_filter/u_semipass) are the
|
||
* batch keys + per-pass state. */
|
||
static const char *TEX_VS =
|
||
"#version 330\n"
|
||
"layout(location=0) in vec2 a_pos;\n"
|
||
"layout(location=1) in vec2 a_uv;\n"
|
||
"layout(location=2) in vec4 a_col;\n"
|
||
"layout(location=3) in vec2 a_tpage;\n"
|
||
"layout(location=4) in vec2 a_clut;\n"
|
||
"layout(location=5) in float a_depth;\n"
|
||
"layout(location=6) in float a_raw;\n"
|
||
"layout(location=7) in vec4 a_limits;\n"
|
||
"layout(location=8) in float a_semi;\n"
|
||
"layout(location=9) in float a_q; /* persp weight; 0 = affine (default) */\n"
|
||
"layout(location=10) in float a_twin; /* GP0(E2h) bits 0..19 */\n"
|
||
"layout(location=11) in vec4 a_hd_source; /* page origin + native extent */\n"
|
||
"layout(location=12) in float a_hd_mode;\n"
|
||
"uniform float u_shift;\n"
|
||
"uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n"
|
||
"uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n"
|
||
"uniform float u_xscale; /* native-wide 2D-backdrop x-stretch; 1 canonical */\n"
|
||
"uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n"
|
||
"noperspective out vec2 v_uv; noperspective out vec4 v_col;\n"
|
||
"smooth out vec2 v_uv_p; /* perspective-correct UV (used when v_persp!=0) */\n"
|
||
"flat out int v_persp;\n"
|
||
"flat out ivec2 v_tpage; flat out ivec2 v_clut; flat out int v_depth;\n"
|
||
"flat out int v_raw; flat out ivec4 v_limits; flat out int v_semi;\n"
|
||
"flat out int v_twin;\n"
|
||
"flat out vec4 v_hd_source; flat out int v_hd_mode;\n"
|
||
"void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col;\n"
|
||
" v_persp = (a_q > 0.0) ? 1 : 0;\n"
|
||
" v_tpage = ivec2(a_tpage + 0.5); v_clut = ivec2(a_clut + 0.5);\n"
|
||
" v_depth = int(a_depth + 0.5); v_raw = int(a_raw + 0.5);\n"
|
||
" v_semi = int(a_semi + 0.5);\n"
|
||
" v_twin = int(a_twin + 0.5);\n"
|
||
" v_hd_source=a_hd_source; v_hd_mode=int(a_hd_mode+0.5);\n"
|
||
" v_limits = ivec4(floor(a_limits + 0.5));\n"
|
||
" /* u_shift: align GL's center-sample grid with the PS1 integer grid (see\n"
|
||
" * GEO_VS) so interpolated uv at a fragment equals the PS1 DDA value. */\n"
|
||
" float xb = a_pos.x;\n"
|
||
" if (u_xscale < 0.0) {\n"
|
||
" float s = -u_xscale; float h = u_xhalf / s;\n"
|
||
" float l = u_xcenter - h, r = u_xcenter + h;\n"
|
||
" if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n"
|
||
" } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n"
|
||
" /* Perspective-correct UV rides the standard w divide: emit clip coords\n"
|
||
" * pre-multiplied by w = 1/q so the post-divide NDC is unchanged while\n"
|
||
" * the rasterizer interpolates the smooth varying hyperbolically. With\n"
|
||
" * a_q == 0 (feature off) w is exactly 1.0 and this is the old expression. */\n"
|
||
" float w = (a_q > 0.0) ? (1.0 / a_q) : 1.0;\n"
|
||
" vec2 ndc = vec2((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0);\n"
|
||
" gl_Position = vec4(ndc * w, 0.0, w); }\n";
|
||
static const char *TEX_FS =
|
||
"#version 330\n"
|
||
"noperspective in vec2 v_uv; noperspective in vec4 v_col;\n"
|
||
"smooth in vec2 v_uv_p; flat in int v_persp;\n"
|
||
"out vec4 frag; out vec4 blend_factor;\n"
|
||
"flat in ivec2 v_tpage; /* texture page base, VRAM px */\n"
|
||
"flat in ivec2 v_clut; /* CLUT base, VRAM px */\n"
|
||
"flat in int v_depth; /* 0=4bit 1=8bit 2=15bit */\n"
|
||
"flat in int v_raw; /* 1 = no color modulation */\n"
|
||
"flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n"
|
||
"flat in int v_semi; /* GP0 command has semi-transparency enabled */\n"
|
||
"flat in int v_twin; /* texture window, GP0(E2h) bits 0..19 */\n"
|
||
"flat in vec4 v_hd_source; flat in int v_hd_mode;\n"
|
||
"uniform sampler2D u_hd_texture;\n"
|
||
"uniform usampler2D u_vram;\n"
|
||
"uniform usampler2D u_palette;\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 int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n"
|
||
"uniform int u_filter; /* 0 nearest, 1 bilinear, 2 stable */\n"
|
||
"uniform float u_shift;\n"
|
||
"int vram_at(int x, int y){\n"
|
||
" ivec2 p = ivec2(x & 1023, y & 511);\n"
|
||
" if (any(greaterThanEqual(p, textureSize(u_vram, 0)))) return 0;\n"
|
||
" return int(texelFetch(u_vram, p, 0).r);\n"
|
||
"}\n"
|
||
"int palette_at(int x, int y){\n"
|
||
" ivec2 p = ivec2(x & 1023, y & 511);\n"
|
||
" if (any(greaterThanEqual(p, textureSize(u_palette, 0)))) return 0;\n"
|
||
" return int(texelFetch(u_palette, p, 0).r);\n"
|
||
"}\n"
|
||
"int fetch_texel(int u, int v){\n"
|
||
" u &= 255; v &= 255;\n"
|
||
" /* texture window: mask_x, mask_y, off_x, off_y (8-px units) */\n"
|
||
" ivec4 tw = ivec4(v_twin & 31, (v_twin >> 5) & 31,\n"
|
||
" (v_twin >> 10) & 31, (v_twin >> 15) & 31);\n"
|
||
" if ((tw.x | tw.y) != 0) {\n"
|
||
" u = (u & ~(tw.x * 8)) | ((tw.z & tw.x) * 8);\n"
|
||
" v = (v & ~(tw.y * 8)) | ((tw.w & tw.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 palette_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 palette_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"
|
||
"int stable_texel(ivec2 p){\n"
|
||
" /* Clamp before wrap, so a footprint left of u=0 cannot pick up u=255. */\n"
|
||
" if ((v_twin & 1023) == 0) p=clamp(p,v_limits.xy,v_limits.zw);\n"
|
||
" return fetch_texel(p.x,p.y);\n"
|
||
"}\n"
|
||
"vec4 stable_bilinear(vec2 uv,int stp){\n"
|
||
" vec2 p=uv-vec2(0.5), f=fract(p); ivec2 b=ivec2(floor(p));\n"
|
||
" vec4 sum=vec4(0.0);\n"
|
||
" for(int y=0;y<2;++y) for(int x=0;x<2;++x){\n"
|
||
" int raw=stable_texel(b+ivec2(x,y));\n"
|
||
" float w=(x==0 ? 1.0-f.x : f.x)*(y==0 ? 1.0-f.y : f.y);\n"
|
||
" if(raw!=0 && ((raw>>15)&1)==stp) sum+=vec4(col5(raw),1.0)*w;\n"
|
||
" }\n"
|
||
" return sum;\n"
|
||
"}\n"
|
||
"vec4 hd_texel(vec2 uv){\n"
|
||
" ivec2 p=ivec2(floor(uv)); vec2 f=fract(uv);\n"
|
||
" p &= ivec2(255);\n"
|
||
" ivec4 tw=ivec4(v_twin&31,(v_twin>>5)&31,(v_twin>>10)&31,(v_twin>>15)&31);\n"
|
||
" if((tw.x|tw.y)!=0) p=(p & ~(tw.xy*8)) | ((tw.zw & tw.xy)*8);\n"
|
||
" else p=clamp(p,v_limits.xy,v_limits.zw);\n"
|
||
" vec2 t=(vec2(p)+f-v_hd_source.xy)/v_hd_source.zw;\n"
|
||
" ivec2 size=textureSize(u_hd_texture,0);\n"
|
||
" ivec2 at=clamp(ivec2(floor(t*vec2(size))),ivec2(0),size-ivec2(1));\n"
|
||
" return texelFetch(u_hd_texture,at,0);\n"
|
||
"}\n"
|
||
"void main(){\n"
|
||
" int stp; vec3 rgb;\n"
|
||
" /* v_persp is 0 for every prim unless [video] perspective_texturing is on\n"
|
||
" * AND this prim's packet carried full GTE projection provenance, so the\n"
|
||
" * default is the PS1's affine (noperspective) mapping. */\n"
|
||
" vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;\n"
|
||
" vec2 dx=dFdx(uv), dy=dFdy(uv);\n"
|
||
" if (v_hd_mode != 0) {\n"
|
||
" vec4 hd=hd_texel(uv);\n"
|
||
" rgb=hd.rgb;\n"
|
||
" if(v_hd_mode==1){\n"
|
||
" if(hd.a==0.0) discard;\n"
|
||
" int native=fetch_texel(int(floor(uv.x)),int(floor(uv.y)));\n"
|
||
" if(native==0) discard; stp=(native>>15)&1;\n"
|
||
" } else if(v_hd_mode==2){\n"
|
||
" if(hd.a<128.0/255.0) discard; stp=0;\n"
|
||
" } else {\n"
|
||
" stp=hd.a<=242.0/255.0 ? 1 : 0;\n"
|
||
" if(all(equal(hd,vec4(0.0))) || (stp==0 && all(equal(hd.rgb,vec3(0.0))))) discard;\n"
|
||
" }\n"
|
||
" } else if (u_filter == 0) {\n"
|
||
" int raw = fetch_texel(int(floor(uv.x)), int(floor(uv.y)));\n"
|
||
" if (raw == 0) discard;\n"
|
||
" rgb = col5(raw);\n"
|
||
" stp = (raw >> 15) & 1;\n"
|
||
" } else if (u_filter == 2) {\n"
|
||
" uv+=vec2(u_shift); int raw=stable_texel(ivec2(floor(uv)));\n"
|
||
" if(raw==0) discard; stp=(raw>>15)&1;\n"
|
||
" float lx=length(dx), ly=length(dy);\n"
|
||
" dx*=min(1.0,8.0/max(lx,0.001)); dy*=min(1.0,8.0/max(ly,0.001));\n"
|
||
" int nx=clamp(int(ceil(lx)),1,4), ny=clamp(int(ceil(ly)),1,4);\n"
|
||
" vec4 sum=vec4(0.0);\n"
|
||
" for(int y=0;y<ny;++y) for(int x=0;x<nx;++x){\n"
|
||
" vec2 offset=dx*((float(x)+0.5)/float(nx)-0.5)\n"
|
||
" +dy*((float(y)+0.5)/float(ny)-0.5);\n"
|
||
" sum+=stable_bilinear(uv+offset,stp);\n"
|
||
" }\n"
|
||
" rgb=sum.a>0.00001 ? sum.rgb/sum.a : col5(raw);\n"
|
||
" } else {\n"
|
||
" /* Bilinear, Beetle-PSX formulation: the NEAREST texel is the base\n"
|
||
" * (cutout + STP authority), the neighbours lie toward the sub-texel\n"
|
||
" * offset and clamp to u_limits, and each texel's weight is gated by\n"
|
||
" * its opacity with the colour renormalised — so prim edges and\n"
|
||
" * cutout borders keep their colour instead of dissolving into the\n"
|
||
" * transparent (black) neighbour and discarding whole edge columns.\n"
|
||
" * Recenter from PS1 top-left point sampling to the bilinear footprint. */\n"
|
||
" uv += vec2(u_shift);\n"
|
||
" int iu = int(floor(uv.x)), iv = int(floor(uv.y));\n"
|
||
" float fx = uv.x - float(iu) - 0.5, fy = uv.y - float(iv) - 0.5;\n"
|
||
" int sx = fx < 0.0 ? -1 : 1, sy = fy < 0.0 ? -1 : 1;\n"
|
||
" fx = abs(fx); fy = abs(fy);\n"
|
||
" int c00 = fetch_texel(iu, iv);\n"
|
||
" if (c00 == 0) discard;\n"
|
||
" int c10 = fetch_texel(iu + sx, iv);\n"
|
||
" int c01 = fetch_texel(iu, iv + sy);\n"
|
||
" int c11 = fetch_texel(iu + sx, iv + sy);\n"
|
||
" float w00 = (c00 == 0 ? 0.0 : 1.0) * (1.0 - fx) * (1.0 - fy);\n"
|
||
" float w10 = (c10 == 0 ? 0.0 : 1.0) * fx * (1.0 - fy);\n"
|
||
" float w01 = (c01 == 0 ? 0.0 : 1.0) * (1.0 - fx) * fy;\n"
|
||
" float w11 = (c11 == 0 ? 0.0 : 1.0) * fx * fy;\n"
|
||
" float opac = w00 + w10 + w01 + w11;\n"
|
||
" rgb = (col5(c00)*w00 + col5(c10)*w10 + col5(c01)*w01 + col5(c11)*w11) / opac;\n"
|
||
" float stpf = (float((c00 >> 15) & 1) * w00 + float((c10 >> 15) & 1) * w10\n"
|
||
" + float((c01 >> 15) & 1) * w01 + float((c11 >> 15) & 1) * w11) / opac;\n"
|
||
" stp = stpf >= 0.5 ? 1 : 0;\n"
|
||
" }\n"
|
||
" if (u_semipass == 1 && stp == 1) discard;\n"
|
||
" if (u_semipass == 2 && stp == 0) discard;\n"
|
||
" if (v_raw == 0) rgb = clamp(rgb * v_col.rgb * 2.0, 0.0, 1.0);\n"
|
||
" float dst_factor = 0.0;\n"
|
||
" if (u_semimode == 4 && v_semi != 0 && stp != 0) {\n"
|
||
" dst_factor = v_semi == 1 ? 0.5 : 1.0;\n"
|
||
" if (v_semi == 1) rgb *= 0.5; else if (v_semi == 4) rgb *= 0.25;\n"
|
||
" }\n"
|
||
" frag = vec4(rgb, (stp == 1 || u_maskset == 1) ? 1.0 : 0.0);\n"
|
||
" blend_factor = vec4(0.0, 0.0, 0.0, dst_factor);\n"
|
||
"}\n";
|
||
|
||
/* Quad blit: used for CPU->VRAM upload flushes and VRAM->VRAM copies.
|
||
* Samples an RGBA8 source (alpha = bit15), splits by STP for the stencil
|
||
* write, optionally ORs set-mask into the output alpha. */
|
||
static const char *BLIT_VS =
|
||
"#version 330\n"
|
||
"layout(location=0) in vec2 a_pos; /* native VRAM px */\n"
|
||
"uniform float u_shift;\n"
|
||
"void main(){\n"
|
||
" gl_Position = vec4((a_pos.x+u_shift)/512.0 - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n";
|
||
/* The same blit into the windowed high-resolution surface: its viewport is the
|
||
* window (W*S px), so x is translated by u_xoff and clipped at u_xhalf like
|
||
* the native-wide passes. A separate program keeps BLIT_VS (the canonical
|
||
* path) untouched. */
|
||
static const char *BLIT_VS_HI =
|
||
"#version 330\n"
|
||
"layout(location=0) in vec2 a_pos; /* native VRAM px */\n"
|
||
"uniform float u_shift;\n"
|
||
"uniform float u_xoff;\n"
|
||
"uniform float u_xhalf;\n"
|
||
"void main(){\n"
|
||
" gl_Position = vec4((a_pos.x+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 *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"
|
||
"uniform ivec2 u_off; /* tile origin in the target; (0,0) for a full copy */\n"
|
||
"void main(){ vec4 c=texelFetch(u_src,ivec2(gl_FragCoord.xy)-u_off,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_ex(int write_val, int check) {
|
||
glEnable(GL_STENCIL_TEST);
|
||
glStencilMask(0x01);
|
||
if (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);
|
||
}
|
||
}
|
||
static void mask_stencil(int write_val) { mask_stencil_ex(write_val, s_mask_check); }
|
||
/* 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_hr_scale, VRAM_H * s_hr_scale);
|
||
glEnable(GL_SCISSOR_TEST);
|
||
if (clip_to_draw_area) {
|
||
int ax1 = s_area_x1, ay1 = s_area_y1, ax2 = s_area_x2, ay2 = s_area_y2;
|
||
if (s_pass_active) {
|
||
if (ax1 < s_pass_x) ax1 = s_pass_x;
|
||
if (ay1 < s_pass_y) ay1 = s_pass_y;
|
||
if (ax2 > s_pass_x + s_pass_w - 1) ax2 = s_pass_x + s_pass_w - 1;
|
||
if (ay2 > s_pass_y + s_pass_h - 1) ay2 = s_pass_y + s_pass_h - 1;
|
||
}
|
||
int sw = ax2 - ax1 + 1, sh = ay2 - ay1 + 1;
|
||
if (sw < 0) sw = 0; if (sh < 0) sh = 0;
|
||
glScissor(ax1 * s_hr_scale, ay1 * s_hr_scale,
|
||
sw * s_hr_scale, sh * s_hr_scale);
|
||
}
|
||
}
|
||
static void hr_end(void) {
|
||
glDisable(GL_BLEND);
|
||
/* apply_psx_blend mode 2 leaves REVERSE_SUBTRACT armed; reset so later
|
||
* host draws (OSD) that re-enable blend do not inherit B-F math. */
|
||
if (p_glBlendEquationSeparate)
|
||
p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD);
|
||
glDisable(GL_STENCIL_TEST);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBindVertexArray(0);
|
||
p_glUseProgram(0);
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||
}
|
||
|
||
/* ---- coherency: CPU -> GPU upload flush --------------------------------- */
|
||
/* CPU-side VRAM writes (GP0 A0 transfers, DMA, single pixel pokes) land in
|
||
* the CPU array immediately and accumulate s_up_rects. Flushing before the
|
||
* next GPU op (or readback/present) preserves PS1 command order. */
|
||
static void flush_cpu_upload(void) {
|
||
if (!s_raster_ok || s_up_nrects == 0) return;
|
||
const int diag = runtime_upload_diag_enabled();
|
||
if (diag) { s_rt_up_diag[0]++; s_rt_up_diag[1] += (uint64_t)s_up_nrects; }
|
||
flush_flat_batch(); /* queued flat GEO before upload mutates VRAM */
|
||
flush_tex_batch(); /* queued textured draws before this upload writes VRAM */
|
||
hiw_flush_queue(); /* ...and their high-resolution-window replay */
|
||
/* 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_hr_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_hr_scale, y * s_hr_scale, w * s_hr_scale, h * s_hr_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();
|
||
hiw_mirror_uploads(rects, nrects);
|
||
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. Uncapped Fit can exceed the native VRAM width. */
|
||
static void rebuild_target_stencil(GLuint target_fbo, int target_w, int target_h) {
|
||
if (target_w > s_scratch_w || target_h > s_scratch_h) {
|
||
if (target_w > s_scratch_w) s_scratch_w = target_w;
|
||
if (target_h > s_scratch_h) s_scratch_h = target_h;
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
glBindTexture(GL_TEXTURE_2D, s_scratch_tex);
|
||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, s_scratch_w, s_scratch_h,
|
||
0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||
}
|
||
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);
|
||
}
|
||
|
||
/* Grow a staging texture to at least w x h (never shrinks). The recorded
|
||
* size is committed only after the driver accepted the new storage: a size
|
||
* past the GPU limit is refused, and a failed allocation puts the old storage
|
||
* back, so the recorded size always matches the texture. Logs the first
|
||
* refusal. Returns 1 when the texture holds at least w x h. */
|
||
static int stage_tex_grow(GLuint tex, int *cur_w, int *cur_h, int w, int h,
|
||
const char *what, int *logged) {
|
||
if (w <= *cur_w && h <= *cur_h) return 1;
|
||
int nw = w > *cur_w ? w : *cur_w, nh = h > *cur_h ? h : *cur_h;
|
||
int ok = !(s_gl_max_dim > 0 && (nw > s_gl_max_dim || nh > s_gl_max_dim)) &&
|
||
nw > 0 && nh > 0;
|
||
if (ok) {
|
||
while (glGetError() != GL_NO_ERROR) {}
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
glBindTexture(GL_TEXTURE_2D, tex);
|
||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, nw, nh, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||
if (glGetError() != GL_NO_ERROR) {
|
||
ok = 0;
|
||
if (*cur_w > 0 && *cur_h > 0)
|
||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, *cur_w, *cur_h, 0, GL_RGBA,
|
||
GL_UNSIGNED_BYTE, NULL);
|
||
while (glGetError() != GL_NO_ERROR) {}
|
||
}
|
||
}
|
||
if (!ok) {
|
||
if (!*logged) {
|
||
fprintf(stdout, "psxrecomp: GL %s %dx%d refused (GPU limit %d); kept at "
|
||
"%dx%d\n", what, nw, nh, s_gl_max_dim, *cur_w, *cur_h);
|
||
*logged = 1;
|
||
}
|
||
return 0;
|
||
}
|
||
*cur_w = nw; *cur_h = nh;
|
||
return 1;
|
||
}
|
||
|
||
/* Grow the shared scratch texture to at least w x h (never shrinks). The
|
||
* scratch holds copy sources and stencil-rebuild tiles; at S > 1 it is sized
|
||
* to what those operations actually need instead of the whole hr surface.
|
||
* Returns 0 (scratch unchanged) when w x h cannot be allocated. */
|
||
static int s_scratch_refused_logged = 0;
|
||
static int scratch_ensure(int w, int h) {
|
||
return stage_tex_grow(s_scratch_tex, &s_scratch_w, &s_scratch_h, w, h,
|
||
"copy/stencil scratch", &s_scratch_refused_logged);
|
||
}
|
||
|
||
/* Rebuild the stencil mask of [x, x+w) x [y, y+h) (target px) from alpha, in
|
||
* tiles no larger than GL_SCRATCH_TILE so the scratch never has to match a
|
||
* multi-gigapixel surface. Same result as rebuild_target_stencil over that
|
||
* rect; used only at S > 1. */
|
||
static void rebuild_target_stencil_tiled(GLuint target_fbo, int target_w, int target_h,
|
||
int x, int y, int w, int h) {
|
||
if (x < 0) { w += x; x = 0; }
|
||
if (y < 0) { h += y; y = 0; }
|
||
if (x + w > target_w) w = target_w - x;
|
||
if (y + h > target_h) h = target_h - y;
|
||
if (w <= 0 || h <= 0) return;
|
||
int tile_w = w < GL_SCRATCH_TILE ? w : GL_SCRATCH_TILE;
|
||
int tile_h = h < GL_SCRATCH_TILE ? h : GL_SCRATCH_TILE;
|
||
if (!scratch_ensure(tile_w, tile_h)) return;
|
||
for (int ty = y; ty < y + h; ty += tile_h) {
|
||
for (int tx = x; tx < x + w; tx += tile_w) {
|
||
int cw = (x + w - tx) < tile_w ? (x + w - tx) : tile_w;
|
||
int ch = (y + h - ty) < tile_h ? (y + h - ty) : tile_h;
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, target_fbo);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBlitFramebuffer(tx, ty, tx + cw, ty + ch, 0, 0, cw, ch,
|
||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, target_fbo);
|
||
glViewport(0, 0, target_w, target_h);
|
||
glEnable(GL_SCISSOR_TEST);
|
||
glScissor(tx, ty, cw, ch);
|
||
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_glUniform2i(s_uStencilOff, tx, ty);
|
||
p_glBindVertexArray(s_empty_vao);
|
||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||
}
|
||
}
|
||
p_glUniform2i(s_uStencilOff, 0, 0);
|
||
glDisable(GL_STENCIL_TEST);
|
||
glDisable(GL_SCISSOR_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;
|
||
hiw_flush_queue();
|
||
int hw = VRAM_W * s_hr_scale, hh = VRAM_H * s_hr_scale;
|
||
if (s_out_scale <= 1) {
|
||
/* Native: the historical whole-surface rebuild, unchanged. */
|
||
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_out_scale, hh);
|
||
}
|
||
} else {
|
||
/* S > 1: only primitives can leave stencil behind alpha, so rebuild
|
||
* their bbox union, tiled. Wide surfaces keep a full rebuild (their
|
||
* margins are not in canonical coordinates), also tiled. In windowed
|
||
* mode the hr surface is 1x and the hi window takes the same rect. */
|
||
if (s_stencil_stale.set) {
|
||
int S = s_hr_scale;
|
||
rebuild_target_stencil_tiled(s_hr_fbo, hw, hh,
|
||
s_stencil_stale.x0 * S, s_stencil_stale.y0 * S,
|
||
(s_stencil_stale.x1 - s_stencil_stale.x0 + 1) * S,
|
||
(s_stencil_stale.y1 - s_stencil_stale.y0 + 1) * S);
|
||
for (int t = 0; hiw_on() && t < s_hiw_n; t++) {
|
||
const HiwTile *T = &s_hiw_t[t];
|
||
int x0 = s_stencil_stale.x0 > T->x0 ? s_stencil_stale.x0 : T->x0;
|
||
int x1 = s_stencil_stale.x1 + 1 < T->x1 ? s_stencil_stale.x1 + 1 : T->x1;
|
||
if (x1 > x0)
|
||
rebuild_target_stencil_tiled(T->fbo, (T->x1 - T->x0) * s_out_scale,
|
||
VRAM_H * s_out_scale, (x0 - T->x0) * s_out_scale,
|
||
s_stencil_stale.y0 * s_out_scale, (x1 - x0) * s_out_scale,
|
||
(s_stencil_stale.y1 - s_stencil_stale.y0 + 1) * s_out_scale);
|
||
}
|
||
}
|
||
for (int i = 0; i < WIDE_MAX_SURF; i++) {
|
||
if (s_wide_fbo[i])
|
||
rebuild_target_stencil_tiled(s_wide_fbo[i], g_wide_w * s_out_scale,
|
||
VRAM_H * s_out_scale, 0, 0,
|
||
g_wide_w * s_out_scale, VRAM_H * s_out_scale);
|
||
}
|
||
}
|
||
rect_clear(&s_stencil_stale);
|
||
s_stencil_valid = 1;
|
||
}
|
||
|
||
/* ---- windowed high-resolution surface: mirror plumbing ------------------ */
|
||
static int hiw_on(void) { return s_hiw && s_hiw_n > 0; }
|
||
|
||
/* The first tile that holds all of [x0, x1), or NULL. */
|
||
static const HiwTile *hiw_tile_holding(int x0, int x1) {
|
||
for (int t = 0; t < s_hiw_n; t++)
|
||
if (x0 >= s_hiw_t[t].x0 && x1 <= s_hiw_t[t].x1) return &s_hiw_t[t];
|
||
return NULL;
|
||
}
|
||
|
||
/* Scissored clear of [x, x+w) x [y, y+h) (native) in every tile it touches.
|
||
* Leaves the scissor test enabled and a tile FBO bound; callers rebind. */
|
||
static void hiw_clear_rect(int x, int y, int w, int h, float r, float g, float b,
|
||
float a, int stencil) {
|
||
if (!hiw_on() || w <= 0 || h <= 0) return;
|
||
hiw_flush_queue();
|
||
int S = s_out_scale;
|
||
for (int t = 0; t < s_hiw_n; t++) {
|
||
const HiwTile *T = &s_hiw_t[t];
|
||
int x0 = x > T->x0 ? x : T->x0, x1 = x + w < T->x1 ? x + w : T->x1;
|
||
if (x1 <= x0) continue;
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, T->fbo);
|
||
glViewport(0, 0, (T->x1 - T->x0) * S, VRAM_H * S);
|
||
glEnable(GL_SCISSOR_TEST);
|
||
glScissor((x0 - T->x0) * S, y * S, (x1 - x0) * S, h * S);
|
||
glClearColor(r, g, b, a);
|
||
glClearStencil(stencil);
|
||
glStencilMask(0xFF);
|
||
glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||
}
|
||
}
|
||
|
||
/* Bind the hi blit program for tile T: projection and shift set, the source
|
||
* texture on unit 0. */
|
||
static void hiw_blit_begin(const HiwTile *T, GLuint src_tex, int src_div, int off_x,
|
||
int off_y, int maskset) {
|
||
int S = s_out_scale, ww = T->x1 - T->x0;
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, T->fbo);
|
||
glViewport(0, 0, ww * S, VRAM_H * S);
|
||
glEnable(GL_SCISSOR_TEST);
|
||
glDisable(GL_BLEND);
|
||
p_glUseProgram(s_blit_hi_prog);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
glBindTexture(GL_TEXTURE_2D, src_tex);
|
||
p_glUniform1i(s_uBhSrc, 0);
|
||
p_glUniform1i(s_uBhMaskset, maskset);
|
||
p_glUniform1i(s_uBhSrcDiv, src_div);
|
||
p_glUniform2i(s_uBhSrcOff, off_x, off_y);
|
||
p_glUniform1f(s_uBhShift, s_shift_hi);
|
||
p_glUniform1f(s_uBhXoff, (float)(-T->x0));
|
||
p_glUniform1f(s_uBhXhalf, (float)ww / 2.0f);
|
||
p_glBindVertexArray(s_blit_vao);
|
||
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo);
|
||
}
|
||
|
||
static void blit_quad_verts(int x, int y, int w, int h) {
|
||
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);
|
||
}
|
||
|
||
/* CPU->VRAM uploads into the tiles: the same staged native texels (s_up_tex,
|
||
* VRAM-aligned) drawn at S, texel = frag/S + tile origin. */
|
||
static void hiw_mirror_uploads(const DirtyRect *rects, int nrects) {
|
||
if (!hiw_on() || nrects <= 0) return;
|
||
hiw_flush_queue();
|
||
int S = s_out_scale;
|
||
for (int t = 0; t < s_hiw_n; t++) {
|
||
const HiwTile *T = &s_hiw_t[t];
|
||
int any = 0;
|
||
for (int i = 0; i < nrects; i++) {
|
||
int x0 = rects[i].x0 > T->x0 ? rects[i].x0 : T->x0;
|
||
int x1 = rects[i].x1 + 1 < T->x1 ? rects[i].x1 + 1 : T->x1;
|
||
if (x1 <= x0) continue;
|
||
if (!any) { hiw_blit_begin(T, s_up_tex, S, T->x0, 0, 0); any = 1; }
|
||
int y = rects[i].y0, h = rects[i].y1 - rects[i].y0 + 1;
|
||
glScissor((x0 - T->x0) * S, y * S, (x1 - x0) * S, h * S);
|
||
blit_quad_verts(x0, y, x1 - x0, h);
|
||
plain_stencil(0); p_glUniform1i(s_uBhPass, 1); glDrawArrays(GL_TRIANGLES, 0, 6);
|
||
plain_stencil(1); p_glUniform1i(s_uBhPass, 2); glDrawArrays(GL_TRIANGLES, 0, 6);
|
||
}
|
||
if (any) hr_end();
|
||
}
|
||
}
|
||
|
||
/* One column chunk [a, b) (dest columns) of hiw_mirror_copy. Dest columns
|
||
* whose source column lies in a tile take it at S: every tile holding part
|
||
* of the source stages it into the window scratch at ((c - a) * S, 0) before
|
||
* anything is drawn. The rest take the 1x source the canonical copy staged
|
||
* at the shared scratch origin, upscaled nearest. Two passes by source bit15,
|
||
* so each tile's stencil tracks the mask. */
|
||
static int s_hiw_scratch_refused_logged = 0;
|
||
static void hiw_mirror_copy_chunk(int a, int b, int shift, int sy, int dx, int dy,
|
||
int h) {
|
||
static uint8_t hi_src[VRAM_W];
|
||
int S = s_out_scale, staged = 0;
|
||
memset(hi_src + a, 0, (size_t)(b - a));
|
||
for (int t = 0; t < s_hiw_n; t++) {
|
||
const HiwTile *T = &s_hiw_t[t];
|
||
int s0 = a + shift > T->x0 ? a + shift : T->x0;
|
||
int s1 = b + shift < T->x1 ? b + shift : T->x1;
|
||
if (s1 <= s0) continue;
|
||
if (!staged) {
|
||
if (!s_hiw_scratch_fbo ||
|
||
!stage_tex_grow(s_hiw_scratch_tex, &s_hiw_scratch_w, &s_hiw_scratch_h,
|
||
(b - a) * S, h * S, "high-resolution copy scratch",
|
||
&s_hiw_scratch_refused_logged))
|
||
break; /* every column takes the 1x source */
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_hiw_scratch_fbo);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
staged = 1;
|
||
}
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, T->fbo);
|
||
p_glBlitFramebuffer((s0 - T->x0) * S, sy * S, (s1 - T->x0) * S, (sy + h) * S,
|
||
(s0 - shift - a) * S, 0, (s1 - shift - a) * S, h * S,
|
||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||
memset(hi_src + (s0 - shift), 1, (size_t)(s1 - s0));
|
||
}
|
||
if (staged) {
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||
}
|
||
for (int t = 0; t < s_hiw_n; t++) {
|
||
const HiwTile *T = &s_hiw_t[t];
|
||
int d0 = a > T->x0 ? a : T->x0, d1 = b < T->x1 ? b : T->x1;
|
||
if (d1 <= d0) continue;
|
||
for (int from_hi = 0; from_hi < 2; from_hi++) {
|
||
int began = 0;
|
||
for (int c = d0; c < d1;) {
|
||
int e = c + 1;
|
||
while (e < d1 && hi_src[e] == hi_src[c]) e++;
|
||
if (hi_src[c] == from_hi) {
|
||
if (!began) {
|
||
if (from_hi) /* texel = frag - chunk origin in the tile */
|
||
hiw_blit_begin(T, s_hiw_scratch_tex, 1, -(a - T->x0) * S,
|
||
-dy * S, s_mask_set);
|
||
else /* texel = frag/S + tile origin - dest */
|
||
hiw_blit_begin(T, s_scratch_tex, S, T->x0 - dx, -dy,
|
||
s_mask_set);
|
||
began = 1;
|
||
}
|
||
glScissor((c - T->x0) * S, dy * S, (e - c) * S, h * S);
|
||
blit_quad_verts(c, dy, e - c, h);
|
||
mask_stencil(s_mask_set); p_glUniform1i(s_uBhPass, 1); glDrawArrays(GL_TRIANGLES, 0, 6);
|
||
mask_stencil(1); p_glUniform1i(s_uBhPass, 2); glDrawArrays(GL_TRIANGLES, 0, 6);
|
||
}
|
||
c = e;
|
||
}
|
||
if (began) hr_end();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* VRAM->VRAM copy into the tiles. The canonical copy (1x) has just staged its
|
||
* source at the shared scratch origin. The dest columns any tile holds are
|
||
* copied in chunks narrow enough for the window scratch at S (the GPU limit),
|
||
* in memmove order: a source right of its dest walks the chunks left to
|
||
* right, one left of it right to left, so no chunk overwrites a later chunk's
|
||
* source and an overlapping copy reads pre-copy pixels like the canonical
|
||
* one. */
|
||
static void hiw_mirror_copy(int sx, int sy, int dx, int dy, int w, int h) {
|
||
if (!hiw_on()) return;
|
||
hiw_flush_queue();
|
||
int S = s_out_scale, shift = sx - dx, lo = VRAM_W, hi = 0;
|
||
for (int t = 0; t < s_hiw_n; t++) {
|
||
int a = dx > s_hiw_t[t].x0 ? dx : s_hiw_t[t].x0;
|
||
int b = dx + w < s_hiw_t[t].x1 ? dx + w : s_hiw_t[t].x1;
|
||
if (b > a) { if (a < lo) lo = a; if (b > hi) hi = b; }
|
||
}
|
||
if (hi <= lo) return;
|
||
int cols = s_gl_max_dim > 0 ? s_gl_max_dim / S : hi - lo;
|
||
if (cols < 1) cols = 1;
|
||
int n = (hi - lo + cols - 1) / cols;
|
||
for (int k = 0; k < n; k++) {
|
||
int i = shift < 0 ? n - 1 - k : k;
|
||
int a = lo + i * cols, b = a + cols < hi ? a + cols : hi;
|
||
hiw_mirror_copy_chunk(a, b, shift, sy, dx, dy, h);
|
||
}
|
||
}
|
||
|
||
static uint64_t hiw_tile_bytes(int x0, int x1) {
|
||
return psx_gl_window_bytes(s_out_scale, x1 - x0);
|
||
}
|
||
|
||
/* Allocate a tile over [nx0, nx1) (64-aligned) and release the tiles in
|
||
* `drop` (a bitmask of indices). The new surface is seeded by upscaling the
|
||
* 1x content (nearest), then every existing tile's overlapping columns are
|
||
* carried over at S, and the stencil is rebuilt from alpha. Returns NULL,
|
||
* leaving the tiles as they were, when the new tile would exceed the driver
|
||
* limit, the memory budget (all tiles kept plus the new one) or
|
||
* HIW_MAX_TILES, or fails to allocate. */
|
||
static const HiwTile *hiw_alloc_tile(int nx0, int nx1, unsigned drop) {
|
||
int S = s_out_scale, W = (nx1 - nx0) * S, H = VRAM_H * S;
|
||
uint64_t bytes = hiw_tile_bytes(nx0, nx1);
|
||
int kept = 0;
|
||
for (int t = 0; t < s_hiw_n; t++)
|
||
if (!(drop & (1u << t))) { kept++; bytes += hiw_tile_bytes(s_hiw_t[t].x0, s_hiw_t[t].x1); }
|
||
if (kept >= HIW_MAX_TILES) return NULL;
|
||
if ((s_gl_max_dim > 0 && (W > s_gl_max_dim || H > s_gl_max_dim)) ||
|
||
(s_vram_budget && bytes > s_vram_budget))
|
||
return NULL;
|
||
while (glGetError() != GL_NO_ERROR) {}
|
||
GLuint tex = make_tex(GL_RGBA8, W, H, GL_RGBA, GL_UNSIGNED_BYTE), rb = 0, fbo = 0;
|
||
p_glGenRenderbuffers(1, &rb);
|
||
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, rb);
|
||
p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, W, H);
|
||
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, 0);
|
||
int ok = glGetError() == GL_NO_ERROR && make_fbo(&fbo, tex, rb);
|
||
if (!ok) {
|
||
if (fbo) p_glDeleteFramebuffers(1, &fbo);
|
||
p_glDeleteRenderbuffers(1, &rb);
|
||
glDeleteTextures(1, &tex);
|
||
if (!s_hiw_refused_logged) {
|
||
fprintf(stdout, "psxrecomp: GL high-resolution window %dx%d failed to "
|
||
"allocate; that display presents at 1x\n", W, H);
|
||
s_hiw_refused_logged = 1;
|
||
}
|
||
return NULL;
|
||
}
|
||
/* Seed from the authoritative 1x surface, then carry the tiles over. */
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, fbo);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBlitFramebuffer(nx0 * s_hr_scale, 0, nx1 * s_hr_scale, VRAM_H * s_hr_scale,
|
||
0, 0, W, H, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||
for (int t = 0; t < s_hiw_n; t++) {
|
||
const HiwTile *T = &s_hiw_t[t];
|
||
int i0 = nx0 > T->x0 ? nx0 : T->x0, i1 = nx1 < T->x1 ? nx1 : T->x1;
|
||
if (i1 <= i0) continue;
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, T->fbo);
|
||
p_glBlitFramebuffer((i0 - T->x0) * S, 0, (i1 - T->x0) * S, H,
|
||
(i0 - nx0) * S, 0, (i1 - nx0) * S, H,
|
||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||
}
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||
int n = 0;
|
||
for (int t = 0; t < s_hiw_n; t++) {
|
||
if (drop & (1u << t)) {
|
||
p_glDeleteFramebuffers(1, &s_hiw_t[t].fbo);
|
||
p_glDeleteRenderbuffers(1, &s_hiw_t[t].rb);
|
||
glDeleteTextures(1, &s_hiw_t[t].tex);
|
||
} else {
|
||
s_hiw_t[n++] = s_hiw_t[t];
|
||
}
|
||
}
|
||
HiwTile *nt = &s_hiw_t[n];
|
||
nt->x0 = nx0; nt->x1 = nx1; nt->tex = tex; nt->rb = rb; nt->fbo = fbo;
|
||
s_hiw_n = n + 1;
|
||
s_hiw_grows++;
|
||
rebuild_target_stencil_tiled(fbo, W, H, 0, 0, W, H);
|
||
fprintf(stdout, "psxrecomp: GL high-resolution window x=[%d,%d) at %dx: %dx%d "
|
||
"(%llu MiB; %d tile%s)\n", nx0, nx1, S, W, H,
|
||
(unsigned long long)(hiw_tile_bytes(nx0, nx1) >> 20), s_hiw_n,
|
||
s_hiw_n == 1 ? "" : "s");
|
||
return nt;
|
||
}
|
||
|
||
/* The tile holding columns [x0, x1), created or grown on first sight:
|
||
* 1. one surface over the union of every display shown so far (a single
|
||
* window growing with the layout, as long as it fits);
|
||
* 2. else this display merged with the tiles it overlaps;
|
||
* 3. else a tile of its own (side-by-side buffers too wide together).
|
||
* Returns NULL (logged once) when none fits the driver limit, the memory
|
||
* budget or HIW_MAX_TILES; that display then presents from the 1x surface. */
|
||
static const HiwTile *hiw_ensure(int x0, int x1) {
|
||
if (!s_hiw || !s_raster_ok) return NULL;
|
||
if (x0 < 0) x0 = 0;
|
||
if (x1 > VRAM_W) x1 = VRAM_W;
|
||
if (x1 <= x0) return NULL;
|
||
hiw_flush_queue(); /* every caller is about to read the window */
|
||
const HiwTile *T = hiw_tile_holding(x0, x1);
|
||
if (T) return T;
|
||
int a0 = x0 / HIW_ALIGN * HIW_ALIGN;
|
||
int a1 = (x1 + HIW_ALIGN - 1) / HIW_ALIGN * HIW_ALIGN;
|
||
if (a1 > VRAM_W) a1 = VRAM_W;
|
||
int u0 = a0, u1 = a1, m0 = a0, m1 = a1;
|
||
unsigned all = 0, over = 0;
|
||
for (int t = 0; t < s_hiw_n; t++) {
|
||
all |= 1u << t;
|
||
if (s_hiw_t[t].x0 < u0) u0 = s_hiw_t[t].x0;
|
||
if (s_hiw_t[t].x1 > u1) u1 = s_hiw_t[t].x1;
|
||
if (s_hiw_t[t].x1 > a0 && s_hiw_t[t].x0 < a1) {
|
||
over |= 1u << t;
|
||
if (s_hiw_t[t].x0 < m0) m0 = s_hiw_t[t].x0;
|
||
if (s_hiw_t[t].x1 > m1) m1 = s_hiw_t[t].x1;
|
||
}
|
||
}
|
||
T = hiw_alloc_tile(u0, u1, all);
|
||
if (!T && over && over != all) T = hiw_alloc_tile(m0, m1, over);
|
||
if (!T && s_hiw_n) T = hiw_alloc_tile(a0, a1, 0);
|
||
if (T) return T;
|
||
if (!s_hiw_refused_logged) {
|
||
fprintf(stdout, "psxrecomp: GL high-resolution window [%d,%d) at %dx does not "
|
||
"fit beside %d tile%s (GPU limit %d, %llu MiB budget); that display "
|
||
"presents at 1x\n", a0, a1, s_out_scale, s_hiw_n, s_hiw_n == 1 ? "" : "s",
|
||
s_gl_max_dim, (unsigned long long)(s_vram_budget >> 20));
|
||
s_hiw_refused_logged = 1;
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* ---- coherency: hr FBO -> raw mirror (pack) ------------------------------ */
|
||
static void pack_flush(void) {
|
||
if (!s_raster_ok || !s_pack_dirty.set) return;
|
||
hiw_flush_queue(); /* queued window draws sample the raw mirror as it is now */
|
||
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);
|
||
|
||
if (s_hd_native_authority) {
|
||
/* All pending draws consumed the previous raw version before this
|
||
* refresh. Never encode the HD presentation image into native words. */
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
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);
|
||
return;
|
||
}
|
||
|
||
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_hr_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 (tpage_x >= 0 && rect_intersects(&s_pack_dirty, tpage_x, tpage_y,
|
||
tpage_x + page_w - 1, tpage_y + 255)) {
|
||
flush_flat_batch();
|
||
flush_tex_batch(); /* queued draws are part of s_pack_dirty — realise them before packing */
|
||
pack_flush(); return;
|
||
}
|
||
if (depth <= 1) {
|
||
int n = depth == 0 ? 16 : 256;
|
||
if (rect_intersects(&s_pack_dirty, clut_x, clut_y, clut_x + n - 1, clut_y)) {
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
pack_flush();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- coherency: GPU -> CPU readback -------------------------------------- */
|
||
/* Defined with the depth24 policy below; needed here for the readback guard. */
|
||
static int s_depth24_skip_up = 0;
|
||
static DirtyRect s_d24_skip_fb; /* union of skipped MDEC FB rects (VRAM halfwords) */
|
||
static int cpu_vram_authoritative(void) {
|
||
return s_cpu_auth_dual || s_hd_native_authority || s_depth24_skip_up;
|
||
}
|
||
|
||
static void ensure_cpu(void) {
|
||
extern int psx_netplay_active(void);
|
||
if (!s_raster_ok || !s_gpu_dirty) return;
|
||
/* Dual-raster / netplay and depth24: CPU VRAM is written on every GP0.
|
||
* Packed RGB888 movie uploads are deliberately absent from the FBO;
|
||
* reading it back would overwrite the movie with stale 1555 words. */
|
||
if (cpu_vram_authoritative() || psx_netplay_active()) {
|
||
s_gpu_dirty = 0;
|
||
rect_clear(&s_cpu_dirty);
|
||
return;
|
||
}
|
||
flush_flat_batch();
|
||
flush_tex_batch(); /* realise queued textured draws before reading the FBO back */
|
||
flush_cpu_upload();
|
||
pack_flush();
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo);
|
||
/* CPU uploads have already landed; raw packing preserves command order.
|
||
* A union is safe for readback (GPU -> CPU), unlike CPU upload unions.
|
||
* Do not reuse s_pack_dirty: texture sampling can consume it before CPU reads. */
|
||
int rx = s_cpu_dirty.set ? s_cpu_dirty.x0 : 0;
|
||
int ry = s_cpu_dirty.set ? s_cpu_dirty.y0 : 0;
|
||
int rw = s_cpu_dirty.set ? s_cpu_dirty.x1 - rx + 1 : VRAM_W;
|
||
int rh = s_cpu_dirty.set ? s_cpu_dirty.y1 - ry + 1 : VRAM_H;
|
||
glPixelStorei(PSXGL_PACK_ROW_LENGTH, VRAM_W);
|
||
glReadPixels(rx, ry, rw, rh, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT,
|
||
s_vram + (size_t)ry * VRAM_W + rx);
|
||
glPixelStorei(PSXGL_PACK_ROW_LENGTH, 0);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
s_gpu_dirty = 0;
|
||
rect_clear(&s_cpu_dirty);
|
||
coh_record(GL_COH_ENSURE, rx, ry, rx + rw - 1, ry + rh - 1);
|
||
|
||
}
|
||
|
||
/* ---- GPU primitives ------------------------------------------------------ */
|
||
|
||
static uint64_t s_scene_prims = 0; /* frame_perf: scene primitives submitted (pre double-draw) */
|
||
static uint64_t s_scene_prims_tex = 0; /* frame_perf: of which textured (vs flat geometry) */
|
||
static void flush_tex_batch(void); /* fwd: drained at the backdrop-phase boundary below */
|
||
static void mark_prim_dirty(const int *xs, const int *ys, int n, int textured) {
|
||
s_scene_prims++;
|
||
int x0 = xs[0], x1 = xs[0], y0 = ys[0], y1 = ys[0];
|
||
for (int i = 1; i < n; i++) {
|
||
if (xs[i] < x0) x0 = xs[i]; if (xs[i] > x1) x1 = xs[i];
|
||
if (ys[i] < y0) y0 = ys[i]; if (ys[i] > y1) y1 = ys[i];
|
||
}
|
||
/* A sub-pixel-corrected vertex lies in [int, int+1) of the integer position
|
||
* it was rounded from, so the corrected prim can touch one more pixel on
|
||
* each max edge than the integer bbox covers. Widen before clipping so the
|
||
* pack/readback dirty rect never trails the drawn area. */
|
||
if (s_pc_valid) { x1 += 1; y1 += 1; }
|
||
s_bdg_prims++; /* dbg: prims seen this frame (gate is now per-prim, see bd_prim_gate) */
|
||
if (s_ptrace_n < PTRACE_CAP) {
|
||
PrimRec *p = &s_ptrace[s_ptrace_n++];
|
||
p->x0 = (short)x0; p->x1 = (short)x1; p->y0 = (short)y0; p->y1 = (short)y1;
|
||
p->tex = (unsigned char)textured;
|
||
}
|
||
if (x0 < s_area_x1) x0 = s_area_x1;
|
||
if (y0 < s_area_y1) y0 = s_area_y1;
|
||
if (x1 > s_area_x2) x1 = s_area_x2;
|
||
if (y1 > s_area_y2) y1 = s_area_y2;
|
||
rect_add(&s_pack_dirty, x0, y0, x1, y1);
|
||
if (s_out_scale > 1) rect_add(&s_stencil_stale, x0, y0, x1, y1);
|
||
if (!cpu_vram_authoritative()) rect_add(&s_cpu_dirty, x0, y0, x1, y1);
|
||
/* Dual-raster keeps CPU current via SW writes — do not mark GPU-ahead. */
|
||
if (!cpu_vram_authoritative())
|
||
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). */
|
||
/* Wide-surface scissor: full wide width, draw-area rows (see above). Every
|
||
* draw into a wide surface uses it, including the full-screen-overlay pass. */
|
||
static void wide_band_scissor_x(int x, int w) {
|
||
int y1 = s_area_y1, y2 = s_area_y2;
|
||
if (s_pass_active) { /* render pass: stay inside the backed-up band */
|
||
if (y1 < s_pass_y) y1 = s_pass_y;
|
||
if (y2 > s_pass_y + s_pass_h - 1) y2 = s_pass_y + s_pass_h - 1;
|
||
}
|
||
int sy = y1, sh = y2 - y1 + 1;
|
||
if (sy < 0) { sh += sy; sy = 0; }
|
||
if (sy + sh > VRAM_H) sh = VRAM_H - sy;
|
||
if (sh < 0) sh = 0;
|
||
glEnable(GL_SCISSOR_TEST);
|
||
glScissor(x * s_out_scale, sy * s_out_scale, w * s_out_scale, sh * s_out_scale);
|
||
}
|
||
static void wide_band_scissor(void) { wide_band_scissor_x(0, g_wide_w); }
|
||
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);
|
||
/* Windowed high-resolution mode: the wide surfaces are at s_out_scale while
|
||
* the canonical pass ran at 1x, so they take the S sample-grid shift. */
|
||
if (s_hiw)
|
||
p_glUniform1f(uXoff == s_tex_uXoff ? s_tex_uShift : s_geo_uShift, s_shift_hi);
|
||
glViewport(0, 0, g_wide_w * s_out_scale, VRAM_H * s_out_scale);
|
||
wide_band_scissor_x(view_pad_left, g_wide_w - view_pad_left - view_pad_right);
|
||
p_glUniform1f(uXoff, (float)dx);
|
||
p_glUniform1f(uXhalf, (float)g_wide_w / 2.0f);
|
||
}
|
||
static void wide_target_end(GLint uXoff, GLint uXhalf) {
|
||
if (s_hiw)
|
||
p_glUniform1f(uXoff == s_tex_uXoff ? s_tex_uShift : s_geo_uShift, s_shift_hr);
|
||
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 int wide_fast_center_valid(void) {
|
||
/* An explicitly stretched sky differs inside the canonical viewport too.
|
||
* Keep the full composite for those scenes, including later foreground
|
||
* draws. Tags are installed before DMA, so no earlier center draws skip. */
|
||
// The native split seam occupies canonical edge columns that the expanded
|
||
// camera renders as world. Copying those columns reinstates the divider.
|
||
return s_wide_fast && gpu_ws_netplay_local_viewport_width() <= 0 &&
|
||
!gpu_ws_background_requires_full_composite();
|
||
}
|
||
/* The one predicate for "the wide surface's centre columns [g_wide_off,
|
||
* g_wide_off+native_w) are the canonical framebuffer, unshifted". A camera view
|
||
* (glb_wide_set_view: anchored or shifted reveal) translates every world draw by
|
||
* view_shift, so the canonical frame no longer lands at g_wide_off; the mirror
|
||
* then draws the whole surface and no site may skip it or copy the canonical
|
||
* frame over it. Every centre skip and every centre copy (present AND render
|
||
* pass capture) must ask this, never wide_fast_center_valid() alone. */
|
||
static int wide_center_is_canonical(void) {
|
||
return wide_fast_center_valid() && !view_enabled;
|
||
}
|
||
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 (!wide_center_is_canonical()) 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 (!wide_center_is_canonical()) 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. */
|
||
|
||
/* The 2D-backdrop x-stretch for a wide-mirror draw under gate `gate`. */
|
||
static void wide_bd_scale(int gate, float *out_scale, float *out_center) {
|
||
extern int g_ws_tex_edge_pct;
|
||
float scale = 1.0f, center = 0.0f;
|
||
if (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 (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++;
|
||
*out_scale = scale;
|
||
*out_center = center;
|
||
}
|
||
|
||
/* 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) {
|
||
float scale, center;
|
||
wide_bd_scale(s_bd_gate, &scale, ¢er);
|
||
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/texture
|
||
* window) rides in the vertex (TEXV flat attributes), so only `semi` (blend)
|
||
* and the global mask/filter are batch keys, plus the texture window unless
|
||
* texture-window batching is on (below). 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 GLuint s_tb_bank_tex;
|
||
static GLuint s_tb_hd_tex;
|
||
|
||
#define HD_GL_CACHE_CAP 64
|
||
#define HD_GL_CACHE_BUDGET (128u * 1024u * 1024u)
|
||
typedef struct HdGlTexture {
|
||
uint64_t generation, key;
|
||
uint64_t last_use;
|
||
GLuint texture;
|
||
size_t bytes;
|
||
} HdGlTexture;
|
||
static HdGlTexture s_hd_gl_cache[HD_GL_CACHE_CAP];
|
||
static size_t s_hd_gl_cache_bytes;
|
||
static uint64_t s_hd_gl_cache_clock;
|
||
|
||
void gl_renderer_clear_hd_texture_cache(void) {
|
||
flush_flat_batch(); flush_tex_batch(); hiw_flush_queue();
|
||
for (int i = 0; i < HD_GL_CACHE_CAP; ++i) {
|
||
if (s_ctx && s_hd_gl_cache[i].texture)
|
||
glDeleteTextures(1, &s_hd_gl_cache[i].texture);
|
||
}
|
||
memset(s_hd_gl_cache, 0, sizeof(s_hd_gl_cache));
|
||
s_hd_gl_cache_bytes = 0; s_tb_hd_tex = 0;
|
||
}
|
||
static GLuint hd_gl_texture(const GpuHdTextureImage* image) {
|
||
if (!image->rgba || !image->width || !image->height || !image->source_width ||
|
||
!image->source_height || image->stride != (uint64_t)image->width * 4u ||
|
||
image->width > (uint32_t)s_gl_max_dim || image->height > (uint32_t)s_gl_max_dim)
|
||
return 0;
|
||
size_t bytes = (size_t)image->stride * image->height;
|
||
if (bytes > HD_GL_CACHE_BUDGET) return 0;
|
||
int free_slot = -1;
|
||
for (int i = 0; i < HD_GL_CACHE_CAP; ++i) {
|
||
HdGlTexture* texture = &s_hd_gl_cache[i];
|
||
if (texture->texture && texture->key == image->cache_key &&
|
||
texture->generation == image->generation) {
|
||
texture->last_use = ++s_hd_gl_cache_clock; return texture->texture;
|
||
}
|
||
if (!texture->texture && free_slot < 0) free_slot = i;
|
||
}
|
||
while (free_slot < 0 || s_hd_gl_cache_bytes + bytes > HD_GL_CACHE_BUDGET) {
|
||
int oldest = -1;
|
||
for (int i = 0; i < HD_GL_CACHE_CAP; ++i)
|
||
if (s_hd_gl_cache[i].texture && (oldest < 0 ||
|
||
s_hd_gl_cache[i].last_use < s_hd_gl_cache[oldest].last_use)) oldest = i;
|
||
if (oldest < 0) return 0;
|
||
flush_flat_batch(); flush_tex_batch(); hiw_flush_queue();
|
||
HdGlTexture* victim = &s_hd_gl_cache[oldest];
|
||
glDeleteTextures(1, &victim->texture);
|
||
s_hd_gl_cache_bytes -= victim->bytes;
|
||
memset(victim, 0, sizeof(*victim)); free_slot = oldest;
|
||
}
|
||
/* A newly allocated image cannot affect pending draws, but TextureImage
|
||
* changes GL bindings; their flush always binds its own complete state. */
|
||
HdGlTexture* texture = &s_hd_gl_cache[free_slot];
|
||
glGenTextures(1, &texture->texture);
|
||
p_glActiveTexture(PSXGL_TEXTURE0 + 2);
|
||
glBindTexture(GL_TEXTURE_2D, texture->texture);
|
||
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);
|
||
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0);
|
||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image->width, image->height, 0,
|
||
GL_RGBA, GL_UNSIGNED_BYTE, image->rgba);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
if (glGetError() != GL_NO_ERROR) {
|
||
glDeleteTextures(1, &texture->texture); texture->texture = 0; return 0;
|
||
}
|
||
texture->key = image->cache_key; texture->generation = image->generation;
|
||
texture->last_use = ++s_hd_gl_cache_clock;
|
||
texture->bytes = bytes; s_hd_gl_cache_bytes += bytes;
|
||
return texture->texture;
|
||
}
|
||
|
||
static void bind_textured_resources(GLuint source, GLuint palette, GLuint hd) {
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
glBindTexture(GL_TEXTURE_2D, source); p_glUniform1i(s_uVram, 0);
|
||
p_glActiveTexture(PSXGL_TEXTURE0 + 1);
|
||
glBindTexture(GL_TEXTURE_2D, palette); p_glUniform1i(s_uPalette, 1);
|
||
p_glActiveTexture(PSXGL_TEXTURE0 + 2);
|
||
glBindTexture(GL_TEXTURE_2D, hd ? hd : s_up_tex); p_glUniform1i(s_uHdTexture, 2);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
}
|
||
static int s_tb_twin[4] = {0, 0, 0, 0};
|
||
static uint64_t s_batch_total = 0, s_batch_reason[7];
|
||
|
||
/* Texture-window batching ([video] texture_window_batching, OpenGL; off by
|
||
* default). Off: a GP0(E2h) texture-window change ends the open textured batch
|
||
* (batch reason 5), as before. On: prims with different windows share a batch,
|
||
* since each vertex carries its own window. Games that tile far textures with
|
||
* per-prim windows (R4's split screen: ~515 E2 changes and ~180 batches a game
|
||
* frame, ~17 with this on) then draw in a tenth of the batches, which matters
|
||
* most on the native-wide path, where a batch reaching the margins is drawn
|
||
* again into the wide surface.
|
||
* The result is pixel-identical: an opaque batch draws its colour in one
|
||
* painter-ordered pass and semi-transparent prims are still drawn one per
|
||
* batch. The one exception is mask checking (GP0(E6h) bit 1): an opaque batch
|
||
* fixes the stencil for its STP=1 texels after its colour pass, so a later
|
||
* prim in the same batch is not stopped by an earlier one's mask bit. The
|
||
* window therefore stays a batch key while the check is on, and this mode
|
||
* never makes a batch longer than the default mode would there. */
|
||
static int s_twin_batching = 0;
|
||
void gl_renderer_set_texture_window_batching(int on) { s_twin_batching = on ? 1 : 0; }
|
||
int gl_renderer_get_texture_window_batching(void) { return s_twin_batching; }
|
||
|
||
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. */
|
||
/* tb_mask / check are the batch's set-mask and the mask-check state it was
|
||
* drawn under; track_stencil lets the canonical pass record deferred stencil
|
||
* (a replay into the high-resolution window must not touch that flag). */
|
||
static void tex_draw_passes_ex(int nverts, int semi, int tb_mask, int check,
|
||
int track_stencil) {
|
||
p_glUniform1i(s_uSemimode, semi < 0 ? 0 : semi);
|
||
if (semi < 0) {
|
||
glDisable(GL_BLEND);
|
||
mask_stencil_ex(tb_mask, check);
|
||
p_glUniform1i(s_uSemipass, 0); /* all texels, one ordered colour pass */
|
||
glDrawArrays(GL_TRIANGLES, 0, nverts);
|
||
if (check) {
|
||
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); /* stencil-only fixup */
|
||
mask_stencil_ex(1, check);
|
||
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 (!tb_mask) {
|
||
/* Alpha is already exact; defer its duplicate stencil encoding
|
||
* until a later GP0(E6h) actually enables destination masking. */
|
||
if (track_stencil) s_stencil_valid = 0;
|
||
}
|
||
} else if (!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 (tb_mask) mask_stencil_ex(1, check); else glDisable(GL_STENCIL_TEST);
|
||
p_glUniform1i(s_uSemipass, 0);
|
||
glDrawArrays(GL_TRIANGLES, 0, nverts);
|
||
if (!tb_mask && track_stencil) s_stencil_valid = 0;
|
||
} else {
|
||
glDisable(GL_BLEND); /* Pass 1: STP=0 texels (opaque) */
|
||
mask_stencil_ex(tb_mask, check);
|
||
p_glUniform1i(s_uSemipass, 1);
|
||
glDrawArrays(GL_TRIANGLES, 0, nverts);
|
||
apply_psx_blend(semi); /* Pass 2: STP=1 texels (blended) */
|
||
mask_stencil_ex(1, check);
|
||
p_glUniform1i(s_uSemipass, 2);
|
||
glDrawArrays(GL_TRIANGLES, 0, nverts);
|
||
}
|
||
}
|
||
static void tex_batch_draw_passes(int nverts, int semi) {
|
||
tex_draw_passes_ex(nverts, semi, s_tb_mask, s_mask_check, 1);
|
||
}
|
||
|
||
/* ---- 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 (!wide_center_is_canonical() || nverts <= 0) return 0;
|
||
float flo = s_tb[0], fhi = s_tb[0];
|
||
for (int i = 1; i < nverts; i++) {
|
||
float x = s_tb[i * TEXV];
|
||
if (x < flo) flo = x; if (x > fhi) fhi = x;
|
||
}
|
||
/* floor/ceil, not a truncating cast: with geometry_correction these are
|
||
* fractional, and (int) rounds toward zero — which WIDENS a negative x
|
||
* toward the centre and could wrongly call a margin-touching batch
|
||
* centre-only, dropping its wide-mirror draw. Whole values are unchanged. */
|
||
return mirror_x_center_only((int)floorf(flo), (int)ceilf(fhi));
|
||
}
|
||
|
||
/* ---- windowed high-resolution surface: deferred mirror queue ------------
|
||
* Mirroring each batch into the window as it is flushed switches framebuffers
|
||
* twice per batch, and on Apple's GL (Metal underneath) every switch ends a
|
||
* render pass: about 80 us each, ~10 ms per R4 frame. So the window's copies
|
||
* of textured / flat / line draws are queued (vertices + the state they were
|
||
* drawn with) and replayed into the window in ONE pass at the next sync
|
||
* point: anything that changes what they sample (the raw mirror: pack,
|
||
* uploads, the depth24 clear), any other write to the window (fill, copy,
|
||
* upload, stencil rebuild), and every read of it (present, capture, growth).
|
||
* Order within the window is preserved; its content equals an immediate
|
||
* mirror at every point it is observed.
|
||
* The native-wide mirror of the same draws switched to the wide surface and
|
||
* back per batch as well; at 8K that surface is up to 15372x9216 and the
|
||
* switches held a 21:9 race to ~30 frames/s. In windowed mode a draw's wide
|
||
* mirror rides in its queue entry (wfbo: target surface, wdx: its x shift,
|
||
* wscale/wcenter: its backdrop stretch) and is replayed in the same flush,
|
||
* one pass per wide surface. Every other write to a wide surface (clears,
|
||
* the full-screen overlay rect, reallocation) and every read of one
|
||
* (present, capture, dumps) flushes the queue first. */
|
||
enum { HQ_TEX = 1, HQ_GEO = 2 };
|
||
typedef struct {
|
||
uint8_t kind;
|
||
int8_t semi; /* tex: batch semi key; geo: blend mode, -1 opaque */
|
||
uint8_t mask; /* tex: batch set-mask; geo: stencil write value */
|
||
uint8_t check; /* mask-check state at draw time */
|
||
uint8_t filter;
|
||
GLuint tex; /* sampled texture (bank or raw mirror) */
|
||
GLuint palette, hd_tex;
|
||
int ax0, ay0, ax1, ay1; /* draw area, inclusive */
|
||
size_t vfirst; /* float offset into s_hq_v */
|
||
int vcount; /* vertices */
|
||
GLuint wfbo; /* native-wide mirror target, 0: none */
|
||
int wdx; /* its x shift (wide_dx()) */
|
||
float wscale, wcenter; /* its backdrop stretch (wide_bd_scale) */
|
||
} HiCmd;
|
||
static HiCmd *s_hq = NULL;
|
||
static int s_hq_n = 0, s_hq_cap = 0;
|
||
static float *s_hq_v = NULL;
|
||
static size_t s_hq_vn = 0, s_hq_vcap = 0;
|
||
static uint64_t s_hq_flushes = 0, s_hq_cmds = 0;
|
||
|
||
static HiCmd *hiw_enqueue(int kind, const float *verts, int nverts, int stride) {
|
||
size_t nf = (size_t)nverts * (size_t)stride;
|
||
if (s_hq_n == s_hq_cap) {
|
||
int cap = s_hq_cap ? s_hq_cap * 2 : 256;
|
||
HiCmd *q = (HiCmd *)realloc(s_hq, (size_t)cap * sizeof(HiCmd));
|
||
if (!q) return NULL;
|
||
s_hq = q; s_hq_cap = cap;
|
||
}
|
||
if (s_hq_vn + nf > s_hq_vcap) {
|
||
size_t cap = s_hq_vcap ? s_hq_vcap : 65536;
|
||
while (cap < s_hq_vn + nf) cap *= 2;
|
||
float *v = (float *)realloc(s_hq_v, cap * sizeof(float));
|
||
if (!v) return NULL;
|
||
s_hq_v = v; s_hq_vcap = cap;
|
||
}
|
||
HiCmd *c = &s_hq[s_hq_n++];
|
||
memset(c, 0, sizeof *c);
|
||
c->kind = (uint8_t)kind;
|
||
c->vfirst = s_hq_vn;
|
||
c->vcount = nverts;
|
||
memcpy(s_hq_v + s_hq_vn, verts, nf * sizeof(float));
|
||
s_hq_vn += nf;
|
||
c->check = (uint8_t)s_mask_check;
|
||
c->ax0 = s_area_x1; c->ay0 = s_area_y1; c->ax1 = s_area_x2; c->ay1 = s_area_y2;
|
||
s_hq_cmds++;
|
||
return c;
|
||
}
|
||
|
||
/* The draw area intersects a tile at all (else nothing to mirror). */
|
||
static int hiw_area_touches(void) {
|
||
if (s_area_y2 < s_area_y1) return 0;
|
||
for (int t = 0; t < s_hiw_n; t++)
|
||
if (s_area_x2 >= s_hiw_t[t].x0 && s_area_x1 < s_hiw_t[t].x1) return 1;
|
||
return 0;
|
||
}
|
||
|
||
/* A draw's native-wide mirror goes in its queue entry when the window's
|
||
* queue is live and the mirror is not being ablated (a perf probe). */
|
||
static int hiw_wide_queue_ok(int mirror) {
|
||
return mirror && g_wide_cur && s_ws_ablate == 0;
|
||
}
|
||
static void hiw_wide_set(HiCmd *c, int gate) {
|
||
c->wfbo = g_wide_cur;
|
||
c->wdx = wide_dx();
|
||
wide_bd_scale(gate, &c->wscale, &c->wcenter);
|
||
}
|
||
|
||
/* Queue a textured batch for the window, and its native-wide mirror when
|
||
* `mirror` (gate: its backdrop-stretch gate). Returns 1 when the mirror was
|
||
* queued (the caller then skips its immediate mirror). */
|
||
static int hiw_enqueue_tex(int nverts, int semi, int mirror, int gate) {
|
||
int wq = hiw_wide_queue_ok(mirror);
|
||
if (!wq && !hiw_area_touches()) return 0;
|
||
HiCmd *c = hiw_enqueue(HQ_TEX, s_tb, nverts, TEXV);
|
||
if (!c) { hiw_flush_queue(); return 0; }
|
||
c->semi = (int8_t)semi;
|
||
c->mask = (uint8_t)s_tb_mask;
|
||
c->filter = (uint8_t)s_tb_filter;
|
||
c->tex = s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex;
|
||
c->palette = s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex;
|
||
c->hd_tex = s_tb_hd_tex;
|
||
if (wq) hiw_wide_set(c, gate);
|
||
return wq;
|
||
}
|
||
|
||
static int hiw_enqueue_geo(const float *verts, int nverts, int semi, int mask,
|
||
int mirror, int gate) {
|
||
int wq = hiw_wide_queue_ok(mirror);
|
||
if (!wq && !hiw_area_touches()) return 0;
|
||
HiCmd *c = hiw_enqueue(HQ_GEO, verts, nverts, 6);
|
||
if (!c) { hiw_flush_queue(); return 0; }
|
||
c->semi = (int8_t)semi;
|
||
c->mask = (uint8_t)(mask ? 1 : 0);
|
||
if (wq) hiw_wide_set(c, gate);
|
||
return wq;
|
||
}
|
||
|
||
/* Replay the queued native-wide mirrors, in order, into their wide surfaces:
|
||
* the state wide_target_begin sets (full-width scissor, the draw area's rows,
|
||
* the S sample-grid shift) plus each entry's shift and backdrop stretch. */
|
||
static void hiw_replay_wide(void) {
|
||
int S = s_out_scale, any = 0, cur = 0;
|
||
GLuint bound = 0;
|
||
for (int i = 0; i < s_hq_n; i++) {
|
||
const HiCmd *c = &s_hq[i];
|
||
if (!c->wfbo) continue;
|
||
if (!any) { gl_perf_mirror_begin(); any = 1; }
|
||
if (c->wfbo != bound) {
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, c->wfbo);
|
||
glViewport(0, 0, g_wide_w * S, VRAM_H * S);
|
||
glEnable(GL_SCISSOR_TEST);
|
||
p_glUseProgram(s_geo_prog);
|
||
p_glUniform1f(s_geo_uShift, s_shift_hi);
|
||
p_glUniform1f(s_geo_uXhalf, (float)g_wide_w / 2.0f);
|
||
p_glUseProgram(s_tex_prog);
|
||
p_glUniform1f(s_tex_uShift, s_shift_hi);
|
||
p_glUniform1f(s_tex_uXhalf, (float)g_wide_w / 2.0f);
|
||
p_glUniform1i(s_uVram, 0);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
bound = c->wfbo;
|
||
cur = 0;
|
||
}
|
||
int sy = c->ay0, sh = c->ay1 - c->ay0 + 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, g_wide_w * S, sh * S);
|
||
if (c->kind == HQ_TEX) {
|
||
if (cur != HQ_TEX) {
|
||
p_glUseProgram(s_tex_prog);
|
||
p_glBindVertexArray(s_tex_vao);
|
||
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo);
|
||
cur = HQ_TEX;
|
||
}
|
||
p_glUniform1f(s_tex_uXoff, (float)c->wdx);
|
||
p_glUniform1f(s_tex_uXscale, c->wscale);
|
||
p_glUniform1f(s_tex_uXcenter, c->wcenter);
|
||
bind_textured_resources(c->tex, c->palette, c->hd_tex);
|
||
p_glUniform1i(s_uMaskset, c->mask);
|
||
p_glUniform1i(s_uFilter, c->filter);
|
||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * TEXV * sizeof(float)),
|
||
s_hq_v + c->vfirst, PSXGL_STREAM_DRAW);
|
||
tex_draw_passes_ex(c->vcount, c->semi, c->mask, c->check, 0);
|
||
} else {
|
||
if (cur != HQ_GEO) {
|
||
p_glUseProgram(s_geo_prog);
|
||
p_glBindVertexArray(s_geo_vao);
|
||
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo);
|
||
cur = HQ_GEO;
|
||
}
|
||
p_glUniform1f(s_geo_uXoff, (float)c->wdx);
|
||
p_glUniform1f(s_geo_uXscale, c->wscale);
|
||
p_glUniform1f(s_geo_uXcenter, c->wcenter);
|
||
if (c->semi >= 0) apply_psx_blend(c->semi); else glDisable(GL_BLEND);
|
||
mask_stencil_ex(c->mask, c->check);
|
||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * 6 * sizeof(float)),
|
||
s_hq_v + c->vfirst, PSXGL_STREAM_DRAW);
|
||
glDrawArrays(GL_TRIANGLES, 0, c->vcount);
|
||
}
|
||
}
|
||
if (!any) return;
|
||
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);
|
||
gl_perf_mirror_end();
|
||
}
|
||
|
||
/* Replay the queued draws into every tile they touch: the window projection
|
||
* and each command's draw area (clipped to the tile) as the scissor. A tile no
|
||
* command touches is not bound. */
|
||
static void hiw_flush_queue(void) {
|
||
if (s_hq_n == 0) return;
|
||
if (!hiw_on()) { s_hq_n = 0; s_hq_vn = 0; return; }
|
||
int S = s_out_scale;
|
||
s_hq_flushes++;
|
||
for (int t = 0; t < s_hiw_n; t++) {
|
||
const HiwTile *T = &s_hiw_t[t];
|
||
int ww = T->x1 - T->x0, bound = 0, cur = 0;
|
||
for (int i = 0; i < s_hq_n; i++) {
|
||
const HiCmd *c = &s_hq[i];
|
||
int cx0 = c->ax0 > T->x0 ? c->ax0 : T->x0;
|
||
int cx1 = c->ax1 + 1 < T->x1 ? c->ax1 + 1 : T->x1;
|
||
int cy0 = c->ay0 < 0 ? 0 : c->ay0;
|
||
int cy1 = c->ay1 + 1 > VRAM_H ? VRAM_H : c->ay1 + 1;
|
||
if (cx1 <= cx0 || cy1 <= cy0) continue; /* (a wide-only entry) */
|
||
if (!bound) {
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, T->fbo);
|
||
glViewport(0, 0, ww * S, VRAM_H * S);
|
||
glEnable(GL_SCISSOR_TEST);
|
||
/* The tile projection on both programs for its replay. */
|
||
p_glUseProgram(s_geo_prog);
|
||
p_glUniform1f(s_geo_uShift, s_shift_hi);
|
||
p_glUniform1f(s_geo_uXoff, (float)(-T->x0));
|
||
p_glUniform1f(s_geo_uXhalf, (float)ww / 2.0f);
|
||
p_glUseProgram(s_tex_prog);
|
||
p_glUniform1f(s_tex_uShift, s_shift_hi);
|
||
p_glUniform1f(s_tex_uXoff, (float)(-T->x0));
|
||
p_glUniform1f(s_tex_uXhalf, (float)ww / 2.0f);
|
||
p_glUniform1i(s_uVram, 0);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
bound = 1;
|
||
}
|
||
glScissor((cx0 - T->x0) * S, cy0 * S, (cx1 - cx0) * S, (cy1 - cy0) * S);
|
||
if (c->kind == HQ_TEX) {
|
||
if (cur != HQ_TEX) {
|
||
p_glUseProgram(s_tex_prog);
|
||
p_glBindVertexArray(s_tex_vao);
|
||
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo);
|
||
cur = HQ_TEX;
|
||
}
|
||
bind_textured_resources(c->tex, c->palette, c->hd_tex);
|
||
p_glUniform1i(s_uMaskset, c->mask);
|
||
p_glUniform1i(s_uFilter, c->filter);
|
||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * TEXV * sizeof(float)),
|
||
s_hq_v + c->vfirst, PSXGL_STREAM_DRAW);
|
||
tex_draw_passes_ex(c->vcount, c->semi, c->mask, c->check, 0);
|
||
} else {
|
||
if (cur != HQ_GEO) {
|
||
p_glUseProgram(s_geo_prog);
|
||
p_glBindVertexArray(s_geo_vao);
|
||
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo);
|
||
cur = HQ_GEO;
|
||
}
|
||
if (c->semi >= 0) apply_psx_blend(c->semi); else glDisable(GL_BLEND);
|
||
mask_stencil_ex(c->mask, c->check);
|
||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * 6 * sizeof(float)),
|
||
s_hq_v + c->vfirst, PSXGL_STREAM_DRAW);
|
||
glDrawArrays(GL_TRIANGLES, 0, c->vcount);
|
||
}
|
||
}
|
||
}
|
||
hiw_replay_wide();
|
||
/* Canonical projection back on both programs. */
|
||
p_glUseProgram(s_geo_prog);
|
||
p_glUniform1f(s_geo_uShift, s_shift_hr);
|
||
p_glUniform1f(s_geo_uXoff, 0.0f);
|
||
p_glUniform1f(s_geo_uXhalf, 512.0f);
|
||
p_glUseProgram(s_tex_prog);
|
||
p_glUniform1f(s_tex_uShift, s_shift_hr);
|
||
p_glUniform1f(s_tex_uXoff, 0.0f);
|
||
p_glUniform1f(s_tex_uXhalf, 512.0f);
|
||
hr_end();
|
||
s_hq_n = 0;
|
||
s_hq_vn = 0;
|
||
}
|
||
|
||
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);
|
||
bind_textured_resources(s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex,
|
||
s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex, s_tb_hd_tex);
|
||
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. */
|
||
int mirror = g_wide_cur && s_ws_ablate != 1 &&
|
||
!(s_tb_gate == 0 && mirror_batch_center_only(nverts));
|
||
/* Windowed high-resolution surface: the same batch at S, queued and
|
||
* replayed in one render pass at the next sync point (see s_hq), with its
|
||
* native-wide mirror. No-op unless that mode is engaged. */
|
||
if (hiw_on() && hiw_enqueue_tex(nverts, semi, mirror, s_tb_gate)) mirror = 0;
|
||
|
||
if (mirror) { /* native-wide mirror */
|
||
int dx = wide_dx();
|
||
s_bd_gate = s_tb_gate; /* this batch is uniform-gate (flushed on change) */
|
||
gl_perf_mirror_begin();
|
||
wide_target_begin(dx, s_tex_uXoff, s_tex_uXhalf);
|
||
wide_set_bd_scale(s_tex_uXscale, s_tex_uXcenter);
|
||
if (s_ws_ablate != 2) tex_batch_draw_passes(nverts, semi);
|
||
wide_clear_bd_scale(s_tex_uXscale, s_tex_uXcenter);
|
||
wide_target_end(s_tex_uXoff, s_tex_uXhalf);
|
||
gl_perf_mirror_end();
|
||
}
|
||
hr_end();
|
||
if (--s_cw_flush_depth == 0) s_cw_flush_ms += cw_ms() - cw_t0;
|
||
}
|
||
|
||
/* Flat / gouraud GEO batch — MotK title/char-select starfields issue ~30k/s
|
||
* GP0(68h) 1x1 dots; each was two immediate gpu_triangle draws (BufferData +
|
||
* DrawArrays each). Coalesce opaque/semi-uniform tris into one draw. */
|
||
#define FLATBATCH_MAXV 8190 /* multiple of 3 */
|
||
#define FLATBATCH_MAXL (FLATBATCH_MAXV / 6) /* line quads the batch can hold */
|
||
/* The batch, then room for its windowed lines' GL_LINES vertices (s_fbl),
|
||
* which flush_flat_batch uploads right after it. */
|
||
static float s_fb[(FLATBATCH_MAXV + 2 * FLATBATCH_MAXL) * 6];
|
||
static int s_fb_n = 0;
|
||
static int s_fb_semi = -2;
|
||
static int s_fb_mask = -1;
|
||
/* GL_TRIANGLES, or GL_LINES for a batch of native-wide lines (gpu_geometry). */
|
||
static GLenum s_fb_mode = GL_TRIANGLES;
|
||
/* Windowed high-resolution mode (s_hiw): the hr surface is the 1x
|
||
* authoritative VRAM, where a line stays GL_LINES, while the window and the
|
||
* wide surfaces draw its one-native-pixel quad. A batched line therefore
|
||
* carries two vertex sets: its quad in s_fb with the triangles (everything
|
||
* the S surfaces draw) and its two GL_LINES vertices in s_fbl, s_fbl_at[i]
|
||
* being the s_fb vertex line i's quad starts at, so the 1x draw
|
||
* (flat_batch_draw_hr_lines) keeps painter order. Empty outside windowed
|
||
* mode. */
|
||
static float s_fbl[FLATBATCH_MAXL * 2 * 6];
|
||
static int s_fbl_at[FLATBATCH_MAXL];
|
||
static int s_fbl_n = 0;
|
||
static int s_fb_gate = 0;
|
||
|
||
static int mirror_flat_batch_center_only(int nverts) {
|
||
if (!wide_center_is_canonical() || nverts <= 0) return 0;
|
||
float flo = s_fb[0], fhi = s_fb[0];
|
||
for (int i = 1; i < nverts; i++) {
|
||
float x = s_fb[i * 6];
|
||
if (x < flo) flo = x; if (x > fhi) fhi = x;
|
||
}
|
||
/* floor/ceil rather than a truncating cast — see mirror_batch_center_only. */
|
||
return mirror_x_center_only((int)floorf(flo), (int)ceilf(fhi));
|
||
}
|
||
|
||
/* A batch holding windowed lines (nl > 0) on the 1x hr surface, in painter
|
||
* order as runs: triangles from s_fb, lines as GL_LINES from the s_fbl copy
|
||
* uploaded after the batch's nverts vertices. */
|
||
static void flat_batch_draw_hr_lines(int nverts, int nl) {
|
||
glLineWidth((float)s_hr_scale);
|
||
int pos = 0;
|
||
for (int i = 0; i < nl;) {
|
||
int j = i + 1; /* lines i..j-1 are back to back in the batch */
|
||
while (j < nl && s_fbl_at[j] == s_fbl_at[j - 1] + 6) j++;
|
||
if (s_fbl_at[i] > pos) glDrawArrays(GL_TRIANGLES, pos, s_fbl_at[i] - pos);
|
||
glDrawArrays(GL_LINES, nverts + 2 * i, 2 * (j - i));
|
||
pos = s_fbl_at[j - 1] + 6;
|
||
i = j;
|
||
}
|
||
if (nverts > pos) glDrawArrays(GL_TRIANGLES, pos, nverts - pos);
|
||
}
|
||
|
||
static void flush_flat_batch(void) {
|
||
if (s_fb_n == 0) return;
|
||
int nverts = s_fb_n, semi = s_fb_semi, mask = s_fb_mask, nl = s_fbl_n;
|
||
GLenum fmode = s_fb_mode;
|
||
s_fb_n = 0;
|
||
s_fbl_n = 0;
|
||
|
||
hr_begin(1);
|
||
if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND);
|
||
mask_stencil(mask);
|
||
if (fmode == GL_LINES) glLineWidth((float)s_hr_scale);
|
||
p_glUseProgram(s_geo_prog);
|
||
p_glBindVertexArray(s_geo_vao);
|
||
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo);
|
||
if (nl) memcpy(&s_fb[nverts * 6], s_fbl, (size_t)nl * 2 * 6 * sizeof(float));
|
||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((nverts + 2 * nl) * 6 * sizeof(float)),
|
||
s_fb, PSXGL_STREAM_DRAW);
|
||
if (nl) flat_batch_draw_hr_lines(nverts, nl);
|
||
else glDrawArrays(fmode, 0, nverts);
|
||
int mirror = g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 &&
|
||
!(!g_ws_bd_stretch_on && mirror_flat_batch_center_only(nverts));
|
||
if (hiw_on() && hiw_enqueue_geo(s_fb, nverts, semi, mask, mirror, s_fb_gate)) mirror = 0;
|
||
|
||
if (mirror) {
|
||
int dx = wide_dx();
|
||
s_bd_gate = s_fb_gate;
|
||
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(fmode, 0, nverts);
|
||
wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter);
|
||
wide_target_end(s_geo_uXoff, s_geo_uXhalf);
|
||
gl_perf_mirror_end();
|
||
}
|
||
hr_end();
|
||
}
|
||
|
||
/* A PS1 line as a quad one NATIVE pixel thick (used at S > 1).
|
||
*
|
||
* glLineWidth(S) was the old way to keep a line's thickness at internal scale,
|
||
* but core-profile contexts only guarantee width 1 (macOS reports
|
||
* GL_ALIASED_LINE_WIDTH_RANGE 1..1 and rejects anything wider), so every line
|
||
* came out one HR pixel thick: 1/9 of native at S=9. The quad follows the PS1
|
||
* rule instead: one pixel per step along the major axis, one pixel across the
|
||
* minor axis. It spans pixel centres p0..p1 extended half a pixel along the
|
||
* major axis at both ends (so both endpoint pixels are covered) and offset
|
||
* +-0.5 px across the minor axis. Positions are native VRAM px, the space the
|
||
* GEO shader already takes. v = two (x, y, r, g, b, a) vertices; q = six. */
|
||
static void line_to_quad(const float *v, float *q) {
|
||
float x0 = v[0] + 0.5f, y0 = v[1] + 0.5f;
|
||
float x1 = v[6] + 0.5f, y1 = v[7] + 0.5f;
|
||
float dx = x1 - x0, dy = y1 - y0;
|
||
float ax = fabsf(dx), ay = fabsf(dy);
|
||
float ex, ey, nx, ny; /* half-pixel extension along, offset across */
|
||
if (ax >= ay) { /* X-major (also the single-pixel case) */
|
||
float sx = dx < 0.0f ? -1.0f : 1.0f;
|
||
ex = 0.5f * sx; ey = ax > 0.0f ? 0.5f * sx * dy / dx : 0.0f;
|
||
nx = 0.0f; ny = 0.5f;
|
||
} else { /* Y-major */
|
||
float sy = dy < 0.0f ? -1.0f : 1.0f;
|
||
ey = 0.5f * sy; ex = 0.5f * sy * dx / dy;
|
||
nx = 0.5f; ny = 0.0f;
|
||
}
|
||
const float a[2] = { x0 - ex, y0 - ey }, b[2] = { x1 + ex, y1 + ey };
|
||
/* corners: a-, b-, a+ / b-, a+, b+ ; colour follows the nearer endpoint */
|
||
const float cx[6] = { a[0]-nx, b[0]-nx, a[0]+nx, b[0]-nx, a[0]+nx, b[0]+nx };
|
||
const float cy[6] = { a[1]-ny, b[1]-ny, a[1]+ny, b[1]-ny, a[1]+ny, b[1]+ny };
|
||
const int end[6] = { 0, 1, 0, 1, 0, 1 };
|
||
for (int i = 0; i < 6; i++) {
|
||
const float *src = v + end[i] * 6;
|
||
q[i*6+0] = cx[i]; q[i*6+1] = cy[i];
|
||
q[i*6+2] = src[2]; q[i*6+3] = src[3]; q[i*6+4] = src[4]; q[i*6+5] = src[5];
|
||
}
|
||
}
|
||
|
||
/* Flat / gouraud triangles and lines share the GEO program. mode: GL_TRIANGLES
|
||
* or GL_LINES; verts are (x, y, r, g, b, a) tuples with colors as 1555. */
|
||
static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
|
||
const uint16_t *cs, int n, int semi) {
|
||
flush_tex_batch(); /* flat prim: drain textured draws first (order + program) */
|
||
flush_cpu_upload(); /* also drains flat batch if an upload was pending */
|
||
mark_prim_dirty(xs, ys, n, 0 /* flat */);
|
||
/* Sub-pixel positions only describe a 3-vertex projected triangle. */
|
||
const int precise = s_pc_valid && mode == GL_TRIANGLES && n == 3;
|
||
|
||
/* Native-wide: while the wide mirror is live (g_wide_cur), a line joins
|
||
* the flat batch as a GL_LINES batch: same vertices, program, line width
|
||
* and painter order, keyed on mode, semi and mask-set, and drained by
|
||
* flush_line_batch() before the mask-check, mirror-suppress or wide
|
||
* target changes, so every line is drawn and mirrored under the state it
|
||
* arrived with. Drawn one by one, every line flushed the textured batch
|
||
* and rebound the hr and wide surfaces for its mirror; R4 draws hundreds
|
||
* of lines per race frame, which held its 21:9 race at 1x to about 53
|
||
* frames/s. A line the backdrop-stretch gate would widen keeps the
|
||
* immediate path (the flat batch mirrors unstretched). With no live
|
||
* mirror (native-wide off, back at 4:3, or an offscreen draw) nothing
|
||
* changes. 1x only: above 1x a line is drawn as its own shape (the
|
||
* internal-resolution work draws it as a one-native-pixel quad and
|
||
* batches that itself), so it is left to that path. */
|
||
if (mode == GL_LINES && n == 2 && s_hr_scale == 1 && !s_hiw && g_wide_cur &&
|
||
!bd_prim_gate(xs, n, 0)) {
|
||
if (s_fb_n > 0 && (s_fb_mode != GL_LINES || s_fb_semi != semi ||
|
||
s_fb_mask != (int)s_mask_set || s_fb_gate != 0))
|
||
flush_flat_batch();
|
||
if (s_fb_n + 2 > FLATBATCH_MAXV)
|
||
flush_flat_batch();
|
||
s_fb_mode = GL_LINES;
|
||
s_fb_gate = 0;
|
||
s_fb_semi = semi;
|
||
s_fb_mask = (int)s_mask_set;
|
||
float mask_a = s_mask_set ? 1.0f : 0.0f;
|
||
for (int i = 0; i < 2; i++) {
|
||
float *v = &s_fb[s_fb_n * 6];
|
||
v[0] = (float)xs[i];
|
||
v[1] = (float)ys[i];
|
||
v[2] = ((cs[i] & 0x1F) << 3) / 255.0f;
|
||
v[3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f;
|
||
v[4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f;
|
||
v[5] = mask_a;
|
||
s_fb_n++;
|
||
}
|
||
return;
|
||
}
|
||
/* Above 1x a line is a quad of two triangles (line_to_quad), so it joins
|
||
* the flat batch like any gouraud triangle: same vertices, same program,
|
||
* same painter order and batch keys, so the same pixels. Drawn one by
|
||
* one, every line flushed the textured batch and, with native-wide,
|
||
* rebound the hr and wide surfaces twice; R4 draws hundreds of lines per
|
||
* race frame, and at 9x those render-pass switches held its 21:9 race to
|
||
* 22-30 frames/s (at 8K in windowed mode, ~10). In windowed mode the 1x
|
||
* hr surface still draws the line as GL_LINES, from the second vertex
|
||
* set kept in s_fbl; the window and the wide surfaces take the quad. A
|
||
* line the backdrop-stretch gate would widen keeps the immediate path
|
||
* (the flat batch mirrors unstretched). 1x lines take the GL_LINES
|
||
* batch above. */
|
||
if (mode == GL_LINES && n == 2 && s_out_scale > 1 && !bd_prim_gate(xs, n, 0)) {
|
||
float lv[2 * 6], quad[6 * 6];
|
||
float mask_a = s_mask_set ? 1.0f : 0.0f;
|
||
for (int i = 0; i < 2; i++) {
|
||
lv[i*6+0] = (float)xs[i];
|
||
lv[i*6+1] = (float)ys[i];
|
||
lv[i*6+2] = ((cs[i] & 0x1F) << 3) / 255.0f;
|
||
lv[i*6+3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f;
|
||
lv[i*6+4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f;
|
||
lv[i*6+5] = mask_a;
|
||
}
|
||
line_to_quad(lv, quad);
|
||
if (s_fb_n > 0 && (s_fb_mode != GL_TRIANGLES || s_fb_semi != semi ||
|
||
s_fb_mask != (int)s_mask_set))
|
||
flush_flat_batch();
|
||
if (s_fb_n + 6 > FLATBATCH_MAXV)
|
||
flush_flat_batch();
|
||
s_fb_mode = GL_TRIANGLES;
|
||
s_fb_semi = semi;
|
||
s_fb_mask = (int)s_mask_set;
|
||
if (s_hiw) { /* the 1x hr surface draws the line itself */
|
||
s_fbl_at[s_fbl_n] = s_fb_n;
|
||
memcpy(&s_fbl[s_fbl_n * 2 * 6], lv, sizeof lv);
|
||
s_fbl_n++;
|
||
}
|
||
memcpy(&s_fb[s_fb_n * 6], quad, sizeof quad);
|
||
s_fb_n += 6;
|
||
return;
|
||
}
|
||
/* Backdrop-stretched lines and 1x lines with no live mirror stay
|
||
* immediate; tris batch for MotK 0x68 starfields. */
|
||
if (mode != GL_TRIANGLES || n < 3) {
|
||
flush_flat_batch();
|
||
float verts[3 * 6];
|
||
float mask_a = s_mask_set ? 1.0f : 0.0f;
|
||
for (int i = 0; i < n; i++) {
|
||
verts[i*6+0] = precise ? s_pc_x[i] : (float)xs[i];
|
||
verts[i*6+1] = precise ? s_pc_y[i] : (float)ys[i];
|
||
verts[i*6+2] = ((cs[i] & 0x1F) << 3) / 255.0f;
|
||
verts[i*6+3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f;
|
||
verts[i*6+4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f;
|
||
verts[i*6+5] = mask_a;
|
||
}
|
||
/* S > 1: a line becomes a one-native-pixel quad (see line_to_quad);
|
||
* native keeps GL_LINES exactly as before. The hr surface draws at
|
||
* s_hr_scale; the presentation surfaces (the high-resolution window,
|
||
* the wide surfaces) at s_out_scale, which differ only in windowed mode. */
|
||
float quad[6 * 6];
|
||
const int is_line = (mode == GL_LINES && n == 2);
|
||
if (is_line && s_out_scale > 1) line_to_quad(verts, quad);
|
||
GLenum draw_mode = mode;
|
||
const float *draw_verts = verts;
|
||
int draw_n = n;
|
||
if (is_line && s_hr_scale > 1) {
|
||
draw_mode = GL_TRIANGLES; draw_verts = quad; draw_n = 6;
|
||
}
|
||
hr_begin(1);
|
||
if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND);
|
||
mask_stencil(s_mask_set);
|
||
if (draw_mode == GL_LINES) glLineWidth((float)s_hr_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)(draw_n * 6 * sizeof(float)),
|
||
draw_verts, PSXGL_STREAM_DRAW);
|
||
glDrawArrays(draw_mode, 0, draw_n);
|
||
if (is_line && s_out_scale > 1 && draw_mode == GL_LINES) {
|
||
/* windowed: hr drew a 1x line; the S surfaces take the quad */
|
||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(6 * 6 * sizeof(float)),
|
||
quad, PSXGL_STREAM_DRAW);
|
||
draw_mode = GL_TRIANGLES; draw_n = 6;
|
||
}
|
||
int mirror = g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 &&
|
||
!(!g_ws_bd_stretch_on && mirror_geo_center_only(xs, n));
|
||
if (hiw_on() &&
|
||
hiw_enqueue_geo(draw_mode == GL_TRIANGLES && is_line ? quad : verts,
|
||
draw_n, semi, s_mask_set, mirror, bd_prim_gate(xs, n, 0)))
|
||
mirror = 0;
|
||
if (mirror) {
|
||
int dx = wide_dx();
|
||
s_bd_gate = bd_prim_gate(xs, n, 0);
|
||
gl_perf_mirror_begin();
|
||
wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf);
|
||
wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter);
|
||
if (s_ws_ablate != 2) glDrawArrays(draw_mode, 0, draw_n);
|
||
wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter);
|
||
wide_target_end(s_geo_uXoff, s_geo_uXhalf);
|
||
gl_perf_mirror_end();
|
||
}
|
||
hr_end();
|
||
return;
|
||
}
|
||
|
||
int gate = bd_prim_gate(xs, n, 0);
|
||
if (s_fb_n > 0 && (s_fb_mode != GL_TRIANGLES || s_fb_semi != semi ||
|
||
s_fb_mask != (int)s_mask_set || s_fb_gate != gate))
|
||
flush_flat_batch();
|
||
if (s_fb_n + n > FLATBATCH_MAXV)
|
||
flush_flat_batch();
|
||
s_fb_mode = GL_TRIANGLES;
|
||
s_fb_semi = semi;
|
||
s_fb_mask = (int)s_mask_set;
|
||
s_fb_gate = gate;
|
||
|
||
float mask_a = s_mask_set ? 1.0f : 0.0f;
|
||
for (int i = 0; i < n; i++) {
|
||
float *v = &s_fb[s_fb_n * 6];
|
||
v[0] = precise ? s_pc_x[i] : (float)xs[i];
|
||
v[1] = precise ? s_pc_y[i] : (float)ys[i];
|
||
v[2] = ((cs[i] & 0x1F) << 3) / 255.0f;
|
||
v[3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f;
|
||
v[4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f;
|
||
v[5] = mask_a;
|
||
s_fb_n++;
|
||
}
|
||
}
|
||
|
||
/* Drain a pending native-wide line batch (gpu_geometry). flush_flat_batch()
|
||
* reads the mask-check, mirror-suppress and wide-target state when it runs,
|
||
* not when a primitive was queued, so a line batch is drained before any of
|
||
* them changes: each line is then drawn and mirrored under the state it
|
||
* arrived with, as on the immediate path. A pending triangle batch keeps its
|
||
* existing behaviour at these points. */
|
||
static void flush_line_batch(void) {
|
||
if (s_fb_n > 0 && s_fb_mode == GL_LINES) flush_flat_batch();
|
||
}
|
||
|
||
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) {
|
||
// Mode 2 needs proven world geometry. Sprites, HUD and untracked packets
|
||
// retain point sampling; their cutout pixels must stay sharp.
|
||
const int filter=s_tex_filter==2 && ((lim && !s_projected_uv_valid) || (!s_pc_valid && !s_pq_valid)) ? 0 : s_tex_filter;
|
||
int lim_buf[4];
|
||
int uv_buf[6];
|
||
if (!lim) {
|
||
/* World polygons keep authored UVs and inclusive atlas bounds.
|
||
* Affine primitives keep the PS1 center-sampling compensation (rects
|
||
* arrive with their own precomputed bounds and compensated 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_center_sample(xs, ys, mu, mv, s_pq_valid, lim_buf);
|
||
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 */
|
||
if (!s_selected_bank_tex)
|
||
flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y);
|
||
else if (s_selected_bank_live_clut)
|
||
flush_pack_if_sampling(-1, 0, depth, clut_x, clut_y);
|
||
GpuHdTextureImage hd = {0};
|
||
GLuint hd_tex = 0;
|
||
if (s_hd_native_authority && !s_selected_bank_tex) {
|
||
uint32_t window = (uint32_t)(s_tw_mask_x | (s_tw_mask_y << 5) |
|
||
(s_tw_off_x << 10) | (s_tw_off_y << 15));
|
||
if (gpu_hd_textures_acquire_draw(texpage, clut_x, clut_y, lim, window,
|
||
semi >= 0, &hd)) {
|
||
hd_tex = hd_gl_texture(&hd);
|
||
if (!hd_tex) hd.alpha_mode = 0;
|
||
else gpu_hd_textures_note_applied();
|
||
}
|
||
}
|
||
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 (the window
|
||
* not at all under texture-window batching, see s_twin_batching). */
|
||
{
|
||
flush_flat_batch(); /* painter order: flat GEO before textured */
|
||
int twx = s_tw_mask_x, twy = s_tw_mask_y, tox = s_tw_off_x, toy = s_tw_off_y;
|
||
int gate = bd_prim_gate(xs, 3, 1); /* backdrop-stretch gate is also a batch key */
|
||
/* Batch key: keep opaque as -1. Dual-source (4) is only for semi modes
|
||
* 0/1/3 when mask-check is off — never coalesce opaque into that key.
|
||
* Mixing opaque+semi under u_semimode==4 made additive particle/glow
|
||
* batches (CTR Naughty Dog intro binary funnel) paint over later
|
||
* opaque crate flaps whenever submission order and STP bits disagreed
|
||
* with the dual-source path's assumptions. */
|
||
int batch_semi;
|
||
if (semi < 0)
|
||
batch_semi = -1;
|
||
else if (!s_mask_check && semi != 2)
|
||
batch_semi = 4;
|
||
else
|
||
batch_semi = semi;
|
||
/* STP draw-ORDER correctness. flush_tex_batch's conservative two-pass
|
||
* path draws pass 1 = every prim's STP=0 texels then pass 2 = every
|
||
* prim's STP=1 texels — a behind prim's semi texels then overwrite a
|
||
* front prim's opaque texels (Tomba AP-block / CTR intro flaps). The
|
||
* dual-source single-pass path avoids that WITHIN one prim, but
|
||
* batching many overlapping semi quads (digit particles + glow) still
|
||
* mis-orders against neighbouring opaque geometry. Isolate EVERY
|
||
* semi-transparent textured prim: drain the open batch, draw this
|
||
* prim alone (composited fully before the next), let opaque prims
|
||
* keep batching. Cost is one draw per semi prim. A separately opted-in
|
||
* immutable bank may batch the single-pass dual-source cases: it
|
||
* cannot alias a render target, keeps painter order, and still splits
|
||
* on opaque transitions, bank/state changes, masking or subtraction. */
|
||
int isolate = semi >= 0 &&
|
||
!mod_texture_bank_batchable(s_selected_bank_tex != 0, s_mask_check, semi);
|
||
int reason = -1;
|
||
if (s_tb_n > 0) {
|
||
if (s_tb_bank_tex != s_selected_bank_tex || s_tb_bank_live_clut != s_selected_bank_live_clut || s_tb_hd_tex != hd_tex) reason = 0;
|
||
else 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 (filter != s_tb_filter) reason = 3;
|
||
else if (gate != s_tb_gate) reason = 4;
|
||
else if ((!s_twin_batching || s_mask_check) &&
|
||
(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 = filter; s_tb_gate = gate;
|
||
s_tb_bank_tex = s_selected_bank_tex;
|
||
s_tb_bank_live_clut = s_selected_bank_live_clut;
|
||
s_tb_hd_tex = hd_tex;
|
||
s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy;
|
||
}
|
||
/* The prim's window, exact as a float (20 bits). */
|
||
float twin = (float)(twx | (twy << 5) | (tox << 10) | (toy << 15));
|
||
float *vp = &s_tb[s_tb_n * TEXV];
|
||
for (int i = 0; i < 3; i++, vp += TEXV) {
|
||
vp[0] = s_pc_valid ? s_pc_x[i] : (float)xs[i];
|
||
vp[1] = s_pc_valid ? s_pc_y[i] : (float)ys[i];
|
||
vp[2] = s_projected_uv_valid ? s_projected_u[i] : (float)us[i];
|
||
vp[3] = s_projected_uv_valid ? s_projected_v[i] : (float)vs[i];
|
||
vp[4] = col[i*3+0]; vp[5] = col[i*3+1]; vp[6] = col[i*3+2]; vp[7] = 1.0f;
|
||
vp[8] = (float)base_x; vp[9] = (float)base_y; /* a_tpage */
|
||
vp[10] = (float)clut_x; vp[11] = (float)clut_y; /* a_clut */
|
||
vp[12] = (float)depth; vp[13] = (float)rawtex; /* a_depth, a_raw */
|
||
vp[14] = (float)lim[0]; vp[15] = (float)lim[1]; /* a_limits */
|
||
vp[16] = (float)lim[2]; vp[17] = (float)lim[3];
|
||
vp[18] = semi >= 0 ? (float)(semi + 1) : 0.0f; /* a_semi code */
|
||
vp[19] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */
|
||
vp[20] = twin; /* a_twin */
|
||
vp[21] = (float)hd.origin_u; vp[22] = (float)hd.origin_v;
|
||
vp[23] = (float)hd.source_width; vp[24] = (float)hd.source_height;
|
||
vp[25] = (float)hd.alpha_mode;
|
||
}
|
||
s_tb_n += 3;
|
||
if (isolate) flush_tex_batch(); /* draw this semi prim alone, in submission order */
|
||
}
|
||
gpu_hd_textures_release_image(&hd);
|
||
}
|
||
|
||
/* 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, including its draw-area row
|
||
* clip. 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,
|
||
* draw-area-rows scissor like rt_wide() — a full-height scissor let an
|
||
* overlay-width rect that canonically clips at a vertical double-buffer
|
||
* band boundary paint the OTHER band's rows; u_xhalf = g_wide_w/2. */
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, g_wide_cur);
|
||
glViewport(0, 0, g_wide_w * s_out_scale, VRAM_H * s_out_scale);
|
||
wide_band_scissor();
|
||
if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND);
|
||
mask_stencil(s_mask_set);
|
||
p_glUseProgram(s_geo_prog);
|
||
if (s_hiw) p_glUniform1f(s_geo_uShift, s_shift_hi);
|
||
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);
|
||
if (s_hiw) p_glUniform1f(s_geo_uShift, s_shift_hr);
|
||
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) {
|
||
flush_line_batch(); /* queued lines mirror as they arrived */
|
||
/* Earlier world geometry still needs its ordinary wide mirror. Drain
|
||
* it before suppressing only this overlay's canonical triangles. */
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
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) {
|
||
/* gpu_triangle queues flat geometry. Finish these two triangles while
|
||
* their mirror remains suppressed, before the direct wide overlay. */
|
||
flush_flat_batch();
|
||
s_wide_suppress = 0;
|
||
/* Re-open the bracket for the full-width wide pass so
|
||
* blend/scissor/program state is clean. */
|
||
if (s_ws_ablate != 1) {
|
||
hiw_flush_queue(); /* windowed: queued wide mirrors land first */
|
||
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_hr_scale, y * s_hr_scale, w * s_hr_scale, h * s_hr_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);
|
||
if (!cpu_vram_authoritative()) rect_add(&s_cpu_dirty, x, y, x + w - 1, y + h - 1);
|
||
}
|
||
|
||
static void gpu_fill(int x,int y,int w,int h,uint16_t c) {
|
||
if (w <= 0 || h <= 0) return;
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
flush_cpu_upload();
|
||
float r=(c&0x1F)/31.0f, g=((c>>5)&0x1F)/31.0f, b=((c>>10)&0x1F)/31.0f;
|
||
x &= VRAM_W - 1; y &= VRAM_H - 1;
|
||
if (w > VRAM_W) w = VRAM_W;
|
||
if (h > VRAM_H) h = VRAM_H;
|
||
int w1 = w, w2 = 0, h1 = h, h2 = 0;
|
||
if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; }
|
||
if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; }
|
||
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo);
|
||
glViewport(0, 0, VRAM_W * s_hr_scale, VRAM_H * s_hr_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);
|
||
if (hiw_on()) {
|
||
hiw_clear_rect(x, y, w1, h1, r, g, b, 0.0f, 0);
|
||
if (w2) hiw_clear_rect(0, y, w2, h1, r, g, b, 0.0f, 0);
|
||
if (h2) hiw_clear_rect(x, 0, w1, h2, r, g, b, 0.0f, 0);
|
||
if (w2 && h2) hiw_clear_rect(0, 0, w2, h2, r, g, b, 0.0f, 0);
|
||
}
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||
if (!cpu_vram_authoritative())
|
||
s_gpu_dirty = 1;
|
||
coh_record(GL_COH_FILL, x, y, x + w - 1, y + h - 1);
|
||
}
|
||
|
||
/* VRAM->VRAM copy: blit the source region to the scratch texture (resolves
|
||
* overlap), then draw it back at the destination through the BLIT program so
|
||
* mask set/check and the stencil mirror apply, exactly like sw_copy_rect. */
|
||
static void gpu_copy_rect(int sx,int sy,int dx,int dy,int w,int h) {
|
||
if (w <= 0 || h <= 0) return;
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
flush_cpu_upload();
|
||
/* Clamp to bounds (the software path wraps; wrapping copies are unused
|
||
* in practice — see the file header). */
|
||
if (sx < 0) sx = 0; if (sy < 0) sy = 0;
|
||
if (dx < 0) dx = 0; if (dy < 0) dy = 0;
|
||
if (sx + w > VRAM_W) w = VRAM_W - sx;
|
||
if (dx + w > VRAM_W) w = VRAM_W - dx;
|
||
if (sy + h > VRAM_H) h = VRAM_H - sy;
|
||
if (dy + h > VRAM_H) h = VRAM_H - dy;
|
||
if (w <= 0 || h <= 0) return;
|
||
|
||
int S = s_hr_scale;
|
||
/* Stage the source at the scratch ORIGIN (not at its own hr coords), so
|
||
* the scratch only has to be as large as the biggest copy. The integer
|
||
* texelFetch in BLIT_FS makes the placement invisible in the result.
|
||
* w*S and h*S fit the GPU limit whenever the hr surface does; the
|
||
* high-resolution window stages its S-scaled source in a scratch of its
|
||
* own (hiw_mirror_copy), so this one never grows past the hr surface. */
|
||
if (!scratch_ensure(w * S, h * S)) return;
|
||
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,
|
||
0, 0, w*S, 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 origin: texel = frag - dst*S. */
|
||
p_glUniform1i(s_uBlitSrcDiv, 1);
|
||
p_glUniform2i(s_uBlitSrcOff, -dx * S, -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();
|
||
hiw_mirror_copy(sx, sy, dx, dy, w, h); /* scratch still holds the 1x source */
|
||
|
||
rect_add(&s_pack_dirty, dx, dy, dx + w - 1, dy + h - 1);
|
||
if (!cpu_vram_authoritative()) rect_add(&s_cpu_dirty, dx, dy, dx + w - 1, dy + h - 1);
|
||
if (!cpu_vram_authoritative())
|
||
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) {
|
||
/* Only the compile-time ceiling here: the driver and memory limits are
|
||
* known at context init (init_gpu_raster), which clamps further. */
|
||
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_out_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 >= 0 && b <= 2 ? b : 0; sw_set_texture_filter(b != 0); }
|
||
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) {
|
||
/* The flat and textured batches take the mask check that is current
|
||
* when they are drawn (mask_stencil reads s_mask_check), so draw what
|
||
* was queued under the old check bit before it changes. */
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
}
|
||
if (next_check && !s_mask_check) {
|
||
/* Land all alpha-authoritative work before deriving stencil from it. */
|
||
flush_cpu_upload();
|
||
rebuild_mask_stencils();
|
||
}
|
||
s_mask_set = s ? 1 : 0;
|
||
s_mask_check = next_check;
|
||
sw_set_mask_bits(s, c);
|
||
}
|
||
static void glb_set_texture_window(uint32_t r) {
|
||
s_tw_mask_x = (int)(r & 0x1F);
|
||
s_tw_mask_y = (int)((r >> 5) & 0x1F);
|
||
s_tw_off_x = (int)((r >> 10) & 0x1F);
|
||
s_tw_off_y = (int)((r >> 15) & 0x1F);
|
||
sw_set_texture_window(r);
|
||
}
|
||
static void glb_set_color_modulation(int r,int g,int b,int raw) { s_mod_r=r; s_mod_g=g; s_mod_b=b; s_mod_raw=raw; sw_set_color_modulation(r,g,b,raw); }
|
||
/* Sub-pixel / perspective overrides for the next triangle. Forwarded to the
|
||
* software rasterizer too: pre-context draws (s_raster_ok == 0) still go there,
|
||
* and the GL backend delegates whole prim classes to it. */
|
||
static void glb_set_precise_triangle(int enabled,
|
||
int32_t x0,int32_t y0, int32_t x1,int32_t y1,
|
||
int32_t x2,int32_t y2) {
|
||
s_pc_valid = enabled ? 1 : 0;
|
||
if (s_pc_valid) {
|
||
/* 16.16 native VRAM px -> float. The GL pipeline is float end-to-end,
|
||
* so the fraction survives to the rasterizer at any internal scale. */
|
||
s_pc_x[0] = (float)x0 / 65536.0f; s_pc_y[0] = (float)y0 / 65536.0f;
|
||
s_pc_x[1] = (float)x1 / 65536.0f; s_pc_y[1] = (float)y1 / 65536.0f;
|
||
s_pc_x[2] = (float)x2 / 65536.0f; s_pc_y[2] = (float)y2 / 65536.0f;
|
||
}
|
||
sw_set_precise_triangle(enabled, x0,y0, x1,y1, x2,y2);
|
||
}
|
||
static void glb_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
|
||
s_pq_valid = (enabled && q0 > 0.0f && q1 > 0.0f && q2 > 0.0f) ? 1 : 0;
|
||
s_pq[0] = q0; s_pq[1] = q1; s_pq[2] = q2;
|
||
sw_set_perspective_triangle(enabled, q0, q1, q2);
|
||
}
|
||
static void glb_set_draw_area(int x1,int y1,int x2,int y2) {
|
||
flush_flat_batch(); flush_tex_batch(); s_area_x1=x1; s_area_y1=y1; s_area_x2=x2; s_area_y2=y2; sw_set_draw_area(x1,y1,x2,y2); }
|
||
static void glb_get_draw_area(int *x1,int *y1,int *x2,int *y2) { sw_get_draw_area(x1,y1,x2,y2); }
|
||
static void glb_set_draw_offset(int x,int y) { flush_flat_batch(); flush_tex_batch(); s_off_x=x; s_off_y=y; sw_set_draw_offset(x,y); }
|
||
|
||
/* Pre-context draws (s_raster_ok == 0) fall back to the software rasterizer
|
||
* over CPU VRAM; the initial full-VRAM upload at context init folds them in.
|
||
* Offline 15-bit rendering is GPU-only (FBO-auth). Netplay dual-raster and
|
||
* depth24 scanout write SW @ 1× for authority, then GPU @ s_out_scale. Movie
|
||
* players can clear with tiles between movies without leaving depth24; those
|
||
* clears must reach the CPU mirror the RGB888 presenter reads. */
|
||
static void depth24_upload_policy(void);
|
||
static int cpu_raster_required(void) {
|
||
depth24_upload_policy();
|
||
/* A render pass admitted under dual-raster netplay (title opt-in,
|
||
* render_pass_netplay_enabled) draws a presentation-only image into the
|
||
* GPU surface. The CPU mirror stays the authoritative record of the real
|
||
* frame, so the pass never rasterizes into it. depth24 scanout still
|
||
* presents from the CPU mirror and keeps its write-through. */
|
||
return !s_raster_ok || s_depth24_skip_up ||
|
||
((s_cpu_auth_dual || s_hd_native_authority) && !s_pass_active);
|
||
}
|
||
/* Bracket each authoritative draw so a later netplay/session policy change
|
||
* cannot accidentally disable the faithful floor for HD replacement mode. */
|
||
static int native_draw_begin(void) {
|
||
int previous = sw_faithful_authority();
|
||
if (s_hd_native_authority) sw_set_faithful_authority(1);
|
||
return previous;
|
||
}
|
||
static void native_draw_end(int previous) { sw_set_faithful_authority(previous); }
|
||
/* The sub-pixel / perspective override describes exactly one triangle; drop it
|
||
* once that triangle has been submitted so a later prim can never inherit it. */
|
||
static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; s_projected_uv_valid = 0; }
|
||
|
||
int gl_renderer_projective_supported(void) {
|
||
return s_raster_ok && g_wide_cur && !cpu_vram_authoritative();
|
||
}
|
||
void gl_renderer_draw_projected_triangle(const PSXProjectedVertex vertices[3],
|
||
uint16_t texpage, uint16_t cx, uint16_t cy, int raw, int semi, int perspective) {
|
||
PSXProjectedVertex polygon[12];
|
||
const double left=fmin(s_area_x1, -wide_dx())-1.0;
|
||
const double right=fmax(s_area_x2+1, g_wide_w-wide_dx())+1.0;
|
||
const int n=psx_projective_clip_triangle(vertices,polygon,
|
||
left,right,s_area_y1-1.0,s_area_y2+1.0);
|
||
int limits[4]={(int)vertices[0].u,(int)vertices[0].v,
|
||
(int)vertices[0].u,(int)vertices[0].v};
|
||
for (int i=1;i<3;++i) {
|
||
if (vertices[i].u<limits[0]) limits[0]=(int)vertices[i].u;
|
||
if (vertices[i].v<limits[1]) limits[1]=(int)vertices[i].v;
|
||
if (vertices[i].u>limits[2]) limits[2]=(int)vertices[i].u;
|
||
if (vertices[i].v>limits[3]) limits[3]=(int)vertices[i].v;
|
||
}
|
||
for (int k=1;k+1<n;++k) {
|
||
const PSXProjectedVertex v[3]={polygon[0],polygon[k],polygon[k+1]};
|
||
int xs[3],ys[3],us[3],vs[3]; float col[9];
|
||
s_pc_valid=1; s_pq_valid=perspective; s_projected_uv_valid=1;
|
||
for (int i=0;i<3;++i) {
|
||
s_pc_x[i]=(float)(v[i].x/v[i].z);
|
||
s_pc_y[i]=(float)(v[i].y/v[i].z);
|
||
xs[i]=(int)floorf(s_pc_x[i]); ys[i]=(int)floorf(s_pc_y[i]);
|
||
s_pq[i]=(float)(1.0/v[i].z);
|
||
s_projected_u[i]=(float)v[i].u; s_projected_v[i]=(float)v[i].v;
|
||
us[i]=(int)v[i].u; vs[i]=(int)v[i].v;
|
||
col[i*3]=(float)v[i].r; col[i*3+1]=(float)v[i].g; col[i*3+2]=(float)v[i].b;
|
||
}
|
||
gpu_textured_triangle(xs,ys,us,vs,col,texpage,cx,cy,raw,semi,limits);
|
||
precise_consumed();
|
||
}
|
||
precise_consumed();
|
||
}
|
||
|
||
static void glb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col) {
|
||
if (cpu_raster_required()) {
|
||
int faithful = native_draw_begin();
|
||
sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col);
|
||
native_draw_end(faithful);
|
||
}
|
||
if (!s_raster_ok) return;
|
||
gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1);
|
||
precise_consumed();
|
||
}
|
||
static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2) {
|
||
if (cpu_raster_required()) {
|
||
int faithful = native_draw_begin();
|
||
sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2);
|
||
native_draw_end(faithful);
|
||
}
|
||
if (!s_raster_ok) return;
|
||
gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1);
|
||
precise_consumed();
|
||
}
|
||
static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){
|
||
if (pass_refuse_write("fill", x, y, w, h)) return;
|
||
if (cpu_raster_required()) {
|
||
int faithful = native_draw_begin();
|
||
sw_fill_rect(x,y,w,h,c);
|
||
native_draw_end(faithful);
|
||
}
|
||
if (!s_raster_ok) return;
|
||
gpu_fill(x,y,w,h,c);
|
||
}
|
||
static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){
|
||
if (pass_refuse_write("copy", dx, dy, w, h)) return;
|
||
if (cpu_raster_required()) {
|
||
int faithful = native_draw_begin();
|
||
sw_copy_rect(sx,sy,dx,dy,w,h);
|
||
native_draw_end(faithful);
|
||
}
|
||
if (!s_raster_ok) return;
|
||
gpu_copy_rect(sx,sy,dx,dy,w,h);
|
||
}
|
||
static void glb_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,int x2,int y2,int u2,int v2,uint16_t cx,uint16_t cy,uint16_t tp){
|
||
const int native_required = cpu_raster_required();
|
||
if (native_required && !s_hd_native_authority)
|
||
sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp);
|
||
if (s_raster_ok) {
|
||
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);
|
||
}
|
||
if (native_required && s_hd_native_authority) {
|
||
int faithful = native_draw_begin();
|
||
sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp);
|
||
native_draw_end(faithful);
|
||
}
|
||
precise_consumed();
|
||
}
|
||
static void glb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,int x1,int y1,int u1,int v1,uint32_t c1,int x2,int y2,int u2,int v2,uint32_t c2,uint16_t cx,uint16_t cy,uint16_t tp,int raw){
|
||
const int native_required = cpu_raster_required();
|
||
if (native_required && !s_hd_native_authority)
|
||
sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw);
|
||
if (s_raster_ok) {
|
||
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);
|
||
}
|
||
if (native_required && s_hd_native_authority) {
|
||
int faithful = native_draw_begin();
|
||
sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw);
|
||
native_draw_end(faithful);
|
||
}
|
||
precise_consumed();
|
||
}
|
||
static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){
|
||
if (cpu_raster_required()) {
|
||
int faithful = native_draw_begin();
|
||
sw_draw_flat_rect(x,y,w,h,c);
|
||
native_draw_end(faithful);
|
||
}
|
||
if (!s_raster_ok) return;
|
||
gpu_flat_rect(x,y,w,h,c, s_semi_en?s_semi_mode:-1);
|
||
}
|
||
static void glb_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t cx,uint16_t cy,uint16_t tp){
|
||
const int native_required = cpu_raster_required();
|
||
if (native_required && !s_hd_native_authority)
|
||
sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp);
|
||
if (s_raster_ok)
|
||
gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||
if (native_required && s_hd_native_authority) {
|
||
int faithful = native_draw_begin();
|
||
sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp);
|
||
native_draw_end(faithful);
|
||
}
|
||
}
|
||
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){
|
||
const int native_required = cpu_raster_required();
|
||
if (native_required && !s_hd_native_authority)
|
||
sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp);
|
||
if (s_raster_ok)
|
||
gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||
if (native_required && s_hd_native_authority) {
|
||
int faithful = native_draw_begin();
|
||
sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp);
|
||
native_draw_end(faithful);
|
||
}
|
||
}
|
||
static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){
|
||
if (cpu_raster_required()) {
|
||
int faithful = native_draw_begin();
|
||
sw_draw_line(x0,y0,x1,y1,c);
|
||
native_draw_end(faithful);
|
||
}
|
||
if (!s_raster_ok) return;
|
||
gpu_line(x0,y0,c, x1,y1,c, s_semi_en?s_semi_mode:-1);
|
||
}
|
||
static void glb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1){
|
||
if (cpu_raster_required()) {
|
||
int faithful = native_draw_begin();
|
||
sw_draw_shaded_line(x0,y0,c0,x1,y1,c1);
|
||
native_draw_end(faithful);
|
||
}
|
||
if (!s_raster_ok) return;
|
||
gpu_line(x0,y0,c0, x1,y1,c1, s_semi_en?s_semi_mode:-1);
|
||
}
|
||
static int gl_read_display_argb(int x, int y, int w, int h, uint32_t *out,
|
||
int pitch_bytes, int cap_px, int *ow, int *oh);
|
||
static int glb_render_display(uint32_t *o,int p,int dx,int dy,int dw,int dh){ depth24_upload_policy(); 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){ depth24_upload_policy(); ensure_cpu(); return sw_render_display_hires(o,p,dx,dy,dw,dh); }
|
||
/* Capture the presented surface at internal resolution (--headless-opengl
|
||
* presentation ring). Read it straight from the GL surface (hr FBO, or the
|
||
* high-resolution window tile) without ever writing CPU VRAM: in dual-raster
|
||
* netplay ensure_cpu deliberately keeps the software mirror, which cannot
|
||
* validate the OpenGL image shown to the player. depth24 scanout presents the
|
||
* CPU mirror (packed RGB888 never reaches the FBO), so that mode, and any rect
|
||
* the GL surface cannot serve, keeps the mirror resolve. The backend's
|
||
* render_display_hires (present fallback, screenshot_hires) is unchanged. */
|
||
int gl_renderer_capture_display_hires(uint32_t *o, int p, int dx, int dy, int dw, int dh) {
|
||
/* A pitch shorter than one scaled row would make either path below write
|
||
* rows over each other and past a pitch*height buffer. */
|
||
if (!o || dw <= 0 || dh <= 0 || (int64_t)p < (int64_t)dw * s_out_scale * 4)
|
||
return 0;
|
||
depth24_upload_policy();
|
||
if (s_raster_ok && !s_depth24_skip_up && p > 0 && p % 4 == 0) {
|
||
int n = gl_read_display_argb(dx, dy, dw, dh, o, p, INT_MAX, NULL, NULL);
|
||
if (n > 0) return n;
|
||
}
|
||
ensure_cpu();
|
||
return sw_render_display_hires(o, p, dx, dy, dw, dh);
|
||
}
|
||
/* While GP1 depth24 is on, packed RGB888 lives in the CPU mirror and is
|
||
* presented via gl_renderer_present — never as 1555 FBO texels. Queuing those
|
||
* MDEC A0 rects hits UP_RECTS_MAX (16) and force-flushes mid-movie (MotK intro
|
||
* ~50→~30 FPS). Skip ONLY framebuffer-sized transfers (RGB888); still upload
|
||
* smaller texture A0s so post-FMV menus keep VRAM pages coherent.
|
||
* On leave: clear the skipped FB union in the FBO — do NOT restage CPU RGB888
|
||
* as 1555 (that painted MotK title rainbow/static). */
|
||
static int depth24_is_fb_transfer(int x, int y, int w, int h) {
|
||
if (!gpu_display_is_depth24() || w <= 0 || h <= 0) return 0;
|
||
GpuDisplayInfo di;
|
||
gpu_get_display_info(&di);
|
||
int fb_w = (int)((di.width * 3u + 1u) / 2u); /* RGB W → halfwords */
|
||
int fb_h = (int)di.height;
|
||
if (fb_w < 8) fb_w = 8;
|
||
if (fb_h < 1) fb_h = 1;
|
||
/* Classic VRAM texture pages. The area heuristic below treats 256×256 as
|
||
* "half of a 320-wide RGB888 FB" (480×240/2) and would skip staging them
|
||
* during FMV — TM4 post-intro loading text then samples an empty FBO. */
|
||
if (w <= 256 && h <= 256) return 0;
|
||
/* Must target the CRTC scanout band (halfword coords). */
|
||
{
|
||
int dx = (int)(di.display_x & 1023u);
|
||
int dy = (int)(di.display_y & 511u);
|
||
int x0 = x & (VRAM_W - 1), y0 = y & (VRAM_H - 1);
|
||
if (x0 + w <= dx || x0 >= dx + fb_w || y0 + h <= dy || y0 >= dy + fb_h)
|
||
return 0;
|
||
}
|
||
/* MotK: 768×128 class blits; allow slack. */
|
||
if (h >= fb_h - 8 && h <= fb_h + 16 && w >= (fb_w * 3) / 4) return 1;
|
||
if ((int64_t)w * (int64_t)h >= ((int64_t)fb_w * fb_h) / 2) return 1;
|
||
return 0;
|
||
}
|
||
|
||
/* Remember only the depth24 scanout band for clear-on-leave. Used after a
|
||
* full-VRAM savestate restage so texture pages stay in the FBO while the
|
||
* RGB888 movie band is still wiped when GP1 leaves 24-bit (avoids MotK
|
||
* rainbow/static from treating packed RGB as 1555). */
|
||
static void depth24_mark_scanout_band(void) {
|
||
GpuDisplayInfo di;
|
||
int fb_w, fb_h, x0, y0, x1, y1;
|
||
if (!gpu_display_is_depth24()) return;
|
||
gpu_get_display_info(&di);
|
||
fb_w = (int)((di.width * 3u + 1u) / 2u);
|
||
fb_h = (int)di.height;
|
||
if (fb_w < 8) fb_w = 8;
|
||
if (fb_h < 1) fb_h = 1;
|
||
x0 = (int)(di.display_x & 1023u);
|
||
y0 = (int)(di.display_y & 511u);
|
||
x1 = x0 + fb_w - 1;
|
||
y1 = y0 + fb_h - 1;
|
||
if (x1 > VRAM_W - 1) x1 = VRAM_W - 1;
|
||
if (y1 > VRAM_H - 1) y1 = VRAM_H - 1;
|
||
rect_add(&s_d24_skip_fb, x0, y0, x1, y1);
|
||
}
|
||
|
||
static void depth24_clear_skipped_fb(void) {
|
||
if (!s_raster_ok || !s_d24_skip_fb.set) return;
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
hiw_flush_queue();
|
||
int x0 = s_d24_skip_fb.x0, y0 = s_d24_skip_fb.y0;
|
||
int x1 = s_d24_skip_fb.x1, y1 = s_d24_skip_fb.y1;
|
||
int rw = x1 - x0 + 1, rh = y1 - y0 + 1;
|
||
int S = s_hr_scale;
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo);
|
||
glDisable(GL_BLEND);
|
||
glDisable(GL_STENCIL_TEST);
|
||
glEnable(GL_SCISSOR_TEST);
|
||
glViewport(0, 0, VRAM_W * S, VRAM_H * S);
|
||
glScissor(x0 * S, y0 * S, rw * S, rh * S);
|
||
glClearColor(0.f, 0.f, 0.f, 0.f);
|
||
glClearStencil(0);
|
||
glStencilMask(0xFF);
|
||
glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||
hiw_clear_rect(x0, y0, rw, rh, 0.f, 0.f, 0.f, 0.f, 0);
|
||
if (s_raw_fbo) {
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo);
|
||
glViewport(0, 0, VRAM_W, VRAM_H);
|
||
glScissor(x0, y0, rw, rh);
|
||
glClear(GL_COLOR_BUFFER_BIT);
|
||
}
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||
rect_add(&s_pack_dirty, x0, y0, x1, y1);
|
||
if (!cpu_vram_authoritative()) {
|
||
rect_add(&s_cpu_dirty, x0, y0, x1, y1);
|
||
s_gpu_dirty = 1; /* The clear must reach an immediate CPU read too. */
|
||
}
|
||
present_dirty_rect(x0, y0, x1, y1, 1);
|
||
rect_clear(&s_d24_skip_fb);
|
||
}
|
||
|
||
static void depth24_upload_policy(void) {
|
||
if (!s_raster_ok) return;
|
||
int d24 = gpu_display_is_depth24();
|
||
if (d24 && !s_depth24_skip_up) {
|
||
/* Entering 24-bit: from here the frame is presented from the CPU
|
||
* mirror, and MDEC only writes the movie's own rows. A letterboxed
|
||
* movie leaves the bars untouched, so they scan out whatever the
|
||
* mirror already held; pre-movie primitive clears live only in the FBO
|
||
* until this sync.
|
||
*
|
||
* WipEout 3's intro is 320x192 written inside a 320x240 band. Without
|
||
* syncing here, stale pre-FMV pixels can remain in the CPU mirror's
|
||
* top/bottom bars and flicker between double-buffered movie frames.
|
||
* Cost is one full-VRAM readback per 24-bit entry, not per frame, and
|
||
* ensure_cpu is a no-op when the FBO is already clean. */
|
||
ensure_cpu();
|
||
s_up_nrects = 0;
|
||
rect_clear(&s_d24_skip_fb);
|
||
} else if (!d24 && s_depth24_skip_up) {
|
||
/* GPU writes own the 15-bit image again, including the clear below. */
|
||
s_depth24_skip_up = 0;
|
||
/* Small texture uploads are still queued while depth24 scanout uses
|
||
* the CPU mirror. Clear only the skipped movie band first, then land
|
||
* those newer uploads so texture pages which overlap that band win in
|
||
* PS1 command order. Dropping the queue here loses GT2's boot menu
|
||
* atlas; flushing before the clear would wipe it again. */
|
||
depth24_clear_skipped_fb();
|
||
flush_cpu_upload();
|
||
gpu_depth24_upload_span_reset();
|
||
/* Force a fresh present after FMV→15-bit so menus/loading screens
|
||
* are not held as a stale depth24 frame. */
|
||
gl_renderer_invalidate_present();
|
||
}
|
||
s_depth24_skip_up = d24;
|
||
}
|
||
|
||
static void glb_vram_write(int x,int y,uint16_t px){
|
||
if (pass_refuse_write("poke", x & (VRAM_W-1), y & (VRAM_H-1), 1, 1)) return;
|
||
depth24_upload_policy();
|
||
sw_vram_write(x,y,px);
|
||
/* Point pokes are never MDEC frames — always stage to FBO. */
|
||
up_add(x & (VRAM_W-1), y & (VRAM_H-1), x & (VRAM_W-1), y & (VRAM_H-1));
|
||
}
|
||
static uint16_t glb_vram_read(int x,int y){ depth24_upload_policy(); 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){
|
||
if (pass_refuse_write("upload", x, y, w, h)) return;
|
||
depth24_upload_policy();
|
||
sw_vram_transfer_in(x,y,w,h,d);
|
||
if (s_depth24_skip_up && depth24_is_fb_transfer(x, y, w, h)) {
|
||
/* Full-VRAM restore (boot_state): must stage into the FBO or every
|
||
* texture page outside the movie band is missing after FMV→menus.
|
||
* Only the scanout band is remembered for clear-on-leave — do not
|
||
* union every skipped rect (misclassified texture pages would be
|
||
* wiped from the FBO when leaving depth24). */
|
||
if (w >= VRAM_W && h >= VRAM_H) {
|
||
up_add_transfer(x, y, w, h);
|
||
rect_clear(&s_d24_skip_fb);
|
||
depth24_mark_scanout_band();
|
||
coh_record(GL_COH_UPLOAD, x, y, x + w - 1, y + h - 1);
|
||
return;
|
||
}
|
||
depth24_mark_scanout_band();
|
||
coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1);
|
||
return;
|
||
}
|
||
up_add_transfer(x, y, w, h); /* exact touched rects, incl. per-pixel wrap */
|
||
coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1);
|
||
}
|
||
static void glb_vram_transfer_out(int x,int y,int w,int h,uint16_t *d){ depth24_upload_policy(); ensure_cpu(); sw_vram_transfer_out(x,y,w,h,d); }
|
||
|
||
/* ---- context init / present -------------------------------------------- */
|
||
static void upload_present_tex(const uint32_t *pixels, int w, int h, int linear) {
|
||
glBindTexture(GL_TEXTURE_2D, s_present_tex);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST);
|
||
/* Re-assert clamp every upload: a stale REPEAT wrap samples past the
|
||
* right edge into garbage (MotK FMV right-strip flicker). */
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||
if (w != s_present_w || h != s_present_h) {
|
||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels);
|
||
s_present_w = w; s_present_h = h;
|
||
} else {
|
||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_BGRA, GL_UNSIGNED_BYTE, pixels);
|
||
}
|
||
}
|
||
|
||
static int s_fmv_filter_cfg = 0; /* VIDEO_FMV_FILTER_NEAREST */
|
||
|
||
void gl_renderer_set_fmv_filter(int cfg_value) {
|
||
if (cfg_value >= 0 && cfg_value <= 3) s_fmv_filter_cfg = cfg_value;
|
||
}
|
||
|
||
static int fmv_filter_mode(void) {
|
||
return s_fmv_filter_cfg - 1;
|
||
}
|
||
|
||
/* Select the present-program sampling mode. s_present_prog is shared by the CPU
|
||
* present and both VRAM/FBO quad paths, and program uniforms persist, so every
|
||
* user states its choice rather than inheriting the last one's. sharp=0 keeps
|
||
* the historical straight sample. */
|
||
static void present_set_sharp(int mode, int tex_w, int tex_h,
|
||
int out_w, int out_h) {
|
||
if (s_present_uSharp < 0) return;
|
||
if (mode <= 0 || tex_w <= 0 || tex_h <= 0) {
|
||
p_glUniform1i(s_present_uSharp, 0);
|
||
return;
|
||
}
|
||
p_glUniform1i(s_present_uSharp, mode);
|
||
if (s_present_uTexSize >= 0)
|
||
p_glUniform2f(s_present_uTexSize, (float)tex_w, (float)tex_h);
|
||
if (s_present_uSharpScale >= 0)
|
||
p_glUniform2f(s_present_uSharpScale,
|
||
(float)out_w / (float)tex_w,
|
||
(float)out_h / (float)tex_h);
|
||
}
|
||
|
||
/* The presentation shader is shared by game content, bezel art, host OSD, and
|
||
* hold-last redraws. Uniform state persists across draws, so every draw must
|
||
* explicitly choose whether it is raw game content or an already-composed
|
||
* image. */
|
||
static void present_set_gamma(GLint uniform, int apply) {
|
||
if (uniform >= 0)
|
||
p_glUniform1f(uniform, apply ? s_present_gamma : 1.0f);
|
||
}
|
||
|
||
/* Push scanline uniforms into the currently-bound present or interpolation
|
||
* program.
|
||
* pitch_lines = the height of the TEXTURE that v_uv is normalized against
|
||
* (v_uv.y * pitch_lines is the texel-row coordinate, and one
|
||
* source texel row is one PS1 scanline). For the VRAM/FBO quad
|
||
* that is VRAM_H (v_uv spans only the display sub-range of the
|
||
* 512-row texture); for the CPU/interp textures, which hold
|
||
* exactly the display rect, it equals the display height.
|
||
* disp_lines = PS1 display lines actually shown, for the output-scale gate.
|
||
* out_h = letterbox height in window px.
|
||
* Pass pitch_lines/disp_lines <= 0 (OSD, bezel, or the already-composed
|
||
* hold-last drawable) to force the effect off — program uniforms persist, so
|
||
* every content draw must state its own choice. */
|
||
static void present_set_scanline(GLint uOn, GLint uStr, GLint uLines,
|
||
GLint uScale, int pitch_lines, int disp_lines,
|
||
int out_h) {
|
||
if (uOn < 0) return;
|
||
int on = (s_scanline_on && pitch_lines > 0 && disp_lines > 0 && out_h > 0)
|
||
? 1 : 0;
|
||
p_glUniform1i(uOn, on);
|
||
if (!on) return;
|
||
if (uStr >= 0) p_glUniform1f(uStr, s_scanline_strength);
|
||
if (uLines >= 0) p_glUniform1f(uLines, (float)pitch_lines);
|
||
if (uScale >= 0) p_glUniform1f(uScale, (float)out_h / (float)disp_lines);
|
||
}
|
||
#define PRESENT_SCANLINE(pitch, disp, oh) \
|
||
present_set_scanline(s_present_uScanline, s_present_uScanStrength, \
|
||
s_present_uScanLines, s_present_uScanScale, \
|
||
(pitch), (disp), (oh))
|
||
#define INTERP_SCANLINE(pitch, disp, oh) \
|
||
present_set_scanline(s_interp_uScanline, s_interp_uScanStrength, \
|
||
s_interp_uScanLines, s_interp_uScanScale, \
|
||
(pitch), (disp), (oh))
|
||
|
||
/* Display aspect for the present letterbox. Default 4:3 (native). When a wide
|
||
* aspect is configured the 4:3 frame is stretched into it — paired with the
|
||
* GTE X-squash (gte_set_display_aspect) this nets a wider field of view. */
|
||
static int s_aspect_num = 4, s_aspect_den = 3;
|
||
|
||
void gl_renderer_set_display_aspect(int num, int den) {
|
||
if (num <= 0 || den <= 0) { num = 4; den = 3; }
|
||
s_aspect_num = num; s_aspect_den = den;
|
||
}
|
||
|
||
/* Scanline post-process toggle (host display setting). strength is the depth of
|
||
* the dark gap between lines, 0..1 (0 = off-looking, 1 = fully black gap). The
|
||
* actual darkening is applied per-draw in the present/interpolation shaders and
|
||
* fades in with output scale — see PSX_SCANLINE_FUNC. */
|
||
void gl_renderer_set_scanlines(int on, float strength) {
|
||
s_scanline_on = on ? 1 : 0;
|
||
if (strength < 0.f) strength = 0.f;
|
||
if (strength > 1.f) strength = 1.f;
|
||
s_scanline_strength = strength;
|
||
}
|
||
|
||
int gl_renderer_get_scanlines(float *strength) {
|
||
if (strength) *strength = s_scanline_strength;
|
||
return s_scanline_on;
|
||
}
|
||
|
||
void gl_renderer_set_post_gamma(float gamma) {
|
||
if (!isfinite(gamma))
|
||
gamma = 1.0f;
|
||
if (gamma < 0.5f) gamma = 0.5f;
|
||
if (gamma > 3.0f) gamma = 3.0f;
|
||
if (fabsf(gamma - 1.0f) < 0.005f) gamma = 1.0f;
|
||
if (gamma == s_present_gamma)
|
||
return;
|
||
s_present_gamma = gamma;
|
||
|
||
/* Gamma is presentation state, not guest VRAM state. Invalidate the
|
||
* presentation latches so a hot change is visible on an unchanged frame.
|
||
* Force at least 2 frames so all backbuffers in the swapchain update. */
|
||
for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull;
|
||
s_last_present_path = -1;
|
||
s_force_present_remaining = 2;
|
||
hold_invalidate();
|
||
}
|
||
|
||
float gl_renderer_get_post_gamma(void) {
|
||
return s_present_gamma;
|
||
}
|
||
|
||
/* 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);
|
||
s_last_fbo_status = st;
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||
if (st != PSXGL_FRAMEBUFFER_COMPLETE) {
|
||
fprintf(stdout, "psxrecomp: GL FBO incomplete (0x%X)\n", st);
|
||
return 0;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* Release the scale-dependent render targets (hr colour + depth-stencil +
|
||
* copy scratch). Used when an allocation fails and init steps S down. */
|
||
static void free_hr_targets(void) {
|
||
if (s_hr_fbo) { p_glDeleteFramebuffers(1, &s_hr_fbo); s_hr_fbo = 0; }
|
||
if (s_scratch_fbo) { p_glDeleteFramebuffers(1, &s_scratch_fbo); s_scratch_fbo = 0; }
|
||
if (s_hr_rb) { p_glDeleteRenderbuffers(1, &s_hr_rb); s_hr_rb = 0; }
|
||
if (s_hr_tex) { glDeleteTextures(1, &s_hr_tex); s_hr_tex = 0; }
|
||
if (s_scratch_tex) { glDeleteTextures(1, &s_scratch_tex); s_scratch_tex = 0; }
|
||
s_scratch_w = s_scratch_h = 0;
|
||
}
|
||
|
||
/* Allocate the authoritative hr surface at scale S. Returns 1 when every
|
||
* target is complete and the driver reported no error (a too-large texture
|
||
* surfaces as GL_INVALID_VALUE or GL_OUT_OF_MEMORY, not always as an
|
||
* incomplete FBO). */
|
||
static int alloc_hr_targets(int S) {
|
||
while (glGetError() != GL_NO_ERROR) {}
|
||
int hw = VRAM_W * S, hh = VRAM_H * S;
|
||
s_hr_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE);
|
||
/* Copy/stencil scratch. Up to 2x it covers the whole surface as before
|
||
* (<= 8 MiB); above that it starts at one tile and grows to the largest
|
||
* copy on demand (scratch_ensure), instead of costing 4 MiB*S^2 up front. */
|
||
if (S <= 2) { s_scratch_w = hw; s_scratch_h = hh; }
|
||
else {
|
||
s_scratch_w = hw < GL_SCRATCH_TILE ? hw : GL_SCRATCH_TILE;
|
||
s_scratch_h = hh < GL_SCRATCH_TILE ? hh : GL_SCRATCH_TILE;
|
||
}
|
||
s_scratch_tex = make_tex(GL_RGBA8, s_scratch_w, s_scratch_h, GL_RGBA, GL_UNSIGNED_BYTE);
|
||
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);
|
||
int ok = glGetError() == GL_NO_ERROR;
|
||
if (ok) ok = make_fbo(&s_hr_fbo, s_hr_tex, s_hr_rb);
|
||
if (ok) ok = make_fbo(&s_scratch_fbo, s_scratch_tex, 0);
|
||
if (ok) ok = glGetError() == GL_NO_ERROR;
|
||
if (!ok) free_hr_targets();
|
||
return ok;
|
||
}
|
||
|
||
static int init_gpu_raster(void) {
|
||
/* Effective scale: the request, clamped to what the driver can allocate
|
||
* for the full-VRAM surface and to the memory budget. Logged whenever it
|
||
* differs from the request so a preset that cannot be honoured says so. */
|
||
{
|
||
GLint max_tex = 0, max_rb = 0, max_vp[2] = { 0, 0 };
|
||
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_tex);
|
||
glGetIntegerv(PSXGL_MAX_RENDERBUFFER_SIZE, &max_rb);
|
||
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, max_vp);
|
||
while (glGetError() != GL_NO_ERROR) {}
|
||
int lim = max_tex > 0 ? max_tex : 0;
|
||
if (max_rb > 0 && (lim == 0 || max_rb < lim)) lim = max_rb;
|
||
if (max_vp[0] > 0 && (lim == 0 || max_vp[0] < lim)) lim = max_vp[0];
|
||
if (max_vp[1] > 0 && (lim == 0 || max_vp[1] < lim)) lim = max_vp[1];
|
||
s_gl_max_dim = lim;
|
||
{
|
||
/* PSX_GL_MAX_DIM lowers the limit, never raises it: it stands in
|
||
* for a smaller GPU (tests, and checking a layout's fallback). */
|
||
const char *md = getenv("PSX_GL_MAX_DIM");
|
||
int v = md ? atoi(md) : 0;
|
||
if (v > 0 && (s_gl_max_dim == 0 || v < s_gl_max_dim)) s_gl_max_dim = v;
|
||
}
|
||
s_vram_budget = psx_gl_budget_bytes_from_env(getenv("PSX_GL_VRAM_BUDGET_MB"));
|
||
s_out_scale = psx_gl_clamp_full_vram_scale(s_req_scale, GL_MAX_INTERNAL_SCALE,
|
||
s_gl_max_dim, s_vram_budget,
|
||
&s_scale_clamp_reason);
|
||
s_hr_scale = s_out_scale;
|
||
/* Beyond the full-VRAM limit, keep the full request for the displayed
|
||
* area only: a 1x authoritative surface plus the high-resolution
|
||
* window (see s_hiw). PSX_GL_HIRES_WINDOW=0 disables it (clamp as
|
||
* above); =1 forces it at any scale above 1 (testing). */
|
||
s_hiw = 0;
|
||
memset(s_hiw_t, 0, sizeof s_hiw_t); /* the old context took the surfaces */
|
||
s_hiw_n = 0;
|
||
s_hiw_last_x0 = s_hiw_last_x1 = 0;
|
||
s_hiw_scratch_tex = s_hiw_scratch_fbo = 0;
|
||
s_hiw_scratch_w = s_hiw_scratch_h = 0;
|
||
s_hiw_refused_logged = 0;
|
||
s_hiw_scratch_refused_logged = 0;
|
||
s_scratch_refused_logged = 0;
|
||
s_hiw_grows = 0;
|
||
{
|
||
const char *env = getenv("PSX_GL_HIRES_WINDOW");
|
||
int force = env && env[0] == '1', allow = !(env && env[0] == '0');
|
||
/* Sized for the smallest framebuffer a window must hold (320). */
|
||
int want = psx_gl_clamp_window_scale(s_req_scale, GL_MAX_INTERNAL_SCALE,
|
||
s_gl_max_dim, s_vram_budget, 320);
|
||
if (allow && want > 1 && (force || want > s_out_scale)) {
|
||
fprintf(stdout, "psxrecomp: GL internal scale %dx: the full-VRAM surface "
|
||
"stops at %dx here (GPU max texture %d, budget %llu MiB); keeping "
|
||
"VRAM at 1x and the displayed area at %dx (high-resolution window)\n",
|
||
s_req_scale, s_out_scale, s_gl_max_dim,
|
||
(unsigned long long)(s_vram_budget >> 20), want);
|
||
s_hiw = 1;
|
||
s_out_scale = want;
|
||
s_hr_scale = 1;
|
||
s_scale_clamp_reason = want < s_req_scale ? PSX_GL_SCALE_TEXTURE : 0;
|
||
}
|
||
}
|
||
if (!s_hiw && s_out_scale != s_req_scale)
|
||
fprintf(stdout, "psxrecomp: GL internal scale %dx clamped to %dx "
|
||
"(%s%s%s; GPU max texture %d, budget %llu MiB)\n",
|
||
s_req_scale, s_out_scale,
|
||
(s_scale_clamp_reason & PSX_GL_SCALE_TEXTURE) ? "texture limit " : "",
|
||
(s_scale_clamp_reason & PSX_GL_SCALE_BUDGET) ? "memory budget " : "",
|
||
(s_scale_clamp_reason & PSX_GL_SCALE_CEILING) ? "ceiling " : "",
|
||
s_gl_max_dim, (unsigned long long)(s_vram_budget >> 20));
|
||
s_scale_alloc_retries = 0;
|
||
}
|
||
|
||
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_blit_hi_prog = build_program(BLIT_VS_HI, 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 ||
|
||
!s_blit_hi_prog) return 0;
|
||
|
||
s_conv = (uint32_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint32_t));
|
||
if (!s_conv) return 0;
|
||
|
||
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();
|
||
}
|
||
|
||
/* The hr surface: step the scale down inside GL when the driver cannot
|
||
* allocate it (out of memory, or a limit it under-reported). Only a
|
||
* failure at 1x takes the whole backend down, as before. */
|
||
while (!alloc_hr_targets(s_hr_scale)) {
|
||
if (s_hr_scale <= 1) return 0;
|
||
fprintf(stdout, "psxrecomp: GL hr surface %dx%d (%dx) failed to allocate; "
|
||
"retrying at %dx\n", VRAM_W * s_hr_scale, VRAM_H * s_hr_scale,
|
||
s_hr_scale, s_hr_scale - 1);
|
||
s_hr_scale--;
|
||
s_out_scale = s_hr_scale;
|
||
s_scale_alloc_retries++;
|
||
}
|
||
if (!make_fbo(&s_raw_fbo, s_raw_tex, 0)) return 0;
|
||
if (s_hiw) {
|
||
/* The window's copy staging; grown on demand (hiw_mirror_copy). */
|
||
s_hiw_scratch_w = s_hiw_scratch_h = HIW_ALIGN;
|
||
s_hiw_scratch_tex = make_tex(GL_RGBA8, s_hiw_scratch_w, s_hiw_scratch_h,
|
||
GL_RGBA, GL_UNSIGNED_BYTE);
|
||
if (!make_fbo(&s_hiw_scratch_fbo, s_hiw_scratch_tex, 0)) {
|
||
/* Not fatal: window copies then take the 1x source. */
|
||
if (s_hiw_scratch_fbo) p_glDeleteFramebuffers(1, &s_hiw_scratch_fbo);
|
||
glDeleteTextures(1, &s_hiw_scratch_tex);
|
||
s_hiw_scratch_tex = s_hiw_scratch_fbo = 0;
|
||
s_hiw_scratch_w = s_hiw_scratch_h = 0;
|
||
}
|
||
}
|
||
|
||
s_uVram = p_glGetUniformLocation(s_tex_prog, "u_vram");
|
||
s_uPalette = p_glGetUniformLocation(s_tex_prog, "u_palette");
|
||
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_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_uHdTexture = p_glGetUniformLocation(s_tex_prog, "u_hd_texture");
|
||
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_uStencilOff = p_glGetUniformLocation(s_stencil_prog, "u_off");
|
||
s_uBhSrc = p_glGetUniformLocation(s_blit_hi_prog, "u_src");
|
||
s_uBhPass = p_glGetUniformLocation(s_blit_hi_prog, "u_stp_pass");
|
||
s_uBhMaskset = p_glGetUniformLocation(s_blit_hi_prog, "u_maskset");
|
||
s_uBhSrcDiv = p_glGetUniformLocation(s_blit_hi_prog, "u_src_div");
|
||
s_uBhSrcOff = p_glGetUniformLocation(s_blit_hi_prog, "u_src_off");
|
||
s_uBhShift = p_glGetUniformLocation(s_blit_hi_prog, "u_shift");
|
||
s_uBhXoff = p_glGetUniformLocation(s_blit_hi_prog, "u_xoff");
|
||
s_uBhXhalf = p_glGetUniformLocation(s_blit_hi_prog, "u_xhalf");
|
||
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. */
|
||
{
|
||
/* hr at s_hr_scale; the high-resolution window and wide surfaces at
|
||
* s_out_scale (the same value unless windowed). */
|
||
float shift = 0.5f / (float)s_hr_scale - 1.0f / 64.0f;
|
||
s_shift_hr = shift;
|
||
s_shift_hi = 0.5f / (float)s_out_scale - 1.0f / 64.0f;
|
||
s_geo_uShift = p_glGetUniformLocation(s_geo_prog, "u_shift");
|
||
s_tex_uShift = p_glGetUniformLocation(s_tex_prog, "u_shift");
|
||
p_glUseProgram(s_geo_prog);
|
||
p_glUniform1f(s_geo_uShift, 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(s_tex_uShift, shift);
|
||
p_glUniform1f(s_tex_uXoff, 0.0f);
|
||
p_glUniform1f(s_tex_uXhalf, 512.0f);
|
||
p_glUseProgram(s_blit_prog);
|
||
p_glUniform1f(p_glGetUniformLocation(s_blit_prog, "u_shift"), shift);
|
||
p_glUseProgram(0);
|
||
}
|
||
|
||
p_glGenVertexArrays(1, &s_geo_vao);
|
||
p_glBindVertexArray(s_geo_vao);
|
||
p_glGenBuffers(1, &s_geo_vbo);
|
||
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo);
|
||
p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);
|
||
p_glEnableVertexAttribArray(0);
|
||
p_glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(2 * sizeof(float)));
|
||
p_glEnableVertexAttribArray(1);
|
||
|
||
p_glGenVertexArrays(1, &s_tex_vao);
|
||
p_glBindVertexArray(s_tex_vao);
|
||
p_glGenBuffers(1, &s_tex_vbo);
|
||
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo);
|
||
{
|
||
GLsizei st = TEXV * sizeof(float);
|
||
p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, st, (void*)0); p_glEnableVertexAttribArray(0); /* pos */
|
||
p_glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, st, (void*)(2*sizeof(float))); p_glEnableVertexAttribArray(1); /* uv */
|
||
p_glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, st, (void*)(4*sizeof(float))); p_glEnableVertexAttribArray(2); /* col */
|
||
p_glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, st, (void*)(8*sizeof(float))); p_glEnableVertexAttribArray(3); /* tpage */
|
||
p_glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, st, (void*)(10*sizeof(float))); p_glEnableVertexAttribArray(4); /* clut */
|
||
p_glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, st, (void*)(12*sizeof(float))); p_glEnableVertexAttribArray(5); /* depth */
|
||
p_glVertexAttribPointer(6, 1, GL_FLOAT, GL_FALSE, st, (void*)(13*sizeof(float))); p_glEnableVertexAttribArray(6); /* raw */
|
||
p_glVertexAttribPointer(7, 4, GL_FLOAT, GL_FALSE, st, (void*)(14*sizeof(float))); p_glEnableVertexAttribArray(7); /* limits */
|
||
p_glVertexAttribPointer(8, 1, GL_FLOAT, GL_FALSE, st, (void*)(18*sizeof(float))); p_glEnableVertexAttribArray(8); /* semi */
|
||
p_glVertexAttribPointer(9, 1, GL_FLOAT, GL_FALSE, st, (void*)(19*sizeof(float))); p_glEnableVertexAttribArray(9); /* q */
|
||
p_glVertexAttribPointer(10, 1, GL_FLOAT, GL_FALSE, st, (void*)(20*sizeof(float))); p_glEnableVertexAttribArray(10); /* twin */
|
||
p_glVertexAttribPointer(11, 4, GL_FLOAT, GL_FALSE, st, (void*)(21*sizeof(float))); p_glEnableVertexAttribArray(11); /* HD source */
|
||
p_glVertexAttribPointer(12, 1, GL_FLOAT, GL_FALSE, st, (void*)(25*sizeof(float))); p_glEnableVertexAttribArray(12); /* HD alpha mode */
|
||
}
|
||
|
||
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);
|
||
rect_clear(&s_stencil_stale);
|
||
s_up_nrects = 0;
|
||
up_add(0, 0, VRAM_W - 1, VRAM_H - 1);
|
||
s_gpu_dirty = 0;
|
||
rect_clear(&s_cpu_dirty);
|
||
s_stencil_valid = 1;
|
||
for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull;
|
||
s_last_present_path = -1;
|
||
|
||
/* Native-wide compositor surfaces start unallocated (lazily created when a
|
||
* widescreen game calls wide_configure + wide_set_target). */
|
||
for (int i = 0; i < WIDE_MAX_SURF; i++) {
|
||
s_wide_tex[i] = 0; s_wide_fbo[i] = 0; s_wide_base[i] = -1;
|
||
}
|
||
g_wide_w = 0; g_wide_off = 0; g_wide_cur = 0; g_wide_cur_base = 0;
|
||
|
||
s_raster_ok = 1;
|
||
gl_perf_init(); /* frame_perf GPU/CPU phase timing (no-op if queries absent) */
|
||
if (s_cpu_auth_dual) {
|
||
fprintf(stdout, "psxrecomp: GL GPU pipeline ready (dual-raster, "
|
||
"internal scale %dx%s, SW@1x authority, no FBO readback)\n",
|
||
s_out_scale, s_hiw ? " windowed" : "");
|
||
} else {
|
||
fprintf(stdout, "psxrecomp: GL GPU pipeline ready (internal scale %dx%s, "
|
||
"mask-bit stencil, texture window, GPU copy/upload)\n", s_out_scale,
|
||
s_hiw ? " windowed" : "");
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
int gl_renderer_texture_banks_supported(void) { return s_raster_ok && !s_cpu_auth_dual && !s_hd_native_authority; }
|
||
|
||
int gl_renderer_fit_wide_aspect(int disp_w, int *num, int *den) {
|
||
if (!s_raster_ok || s_gl_max_dim <= 0) return 0;
|
||
int max_w = psx_gl_max_wide_width(s_out_scale, s_gl_max_dim);
|
||
return psx_gl_fit_wide_aspect(disp_w, max_w > 0 ? max_w : 1, num, den);
|
||
}
|
||
|
||
int gl_renderer_scale_info(GlScaleInfo *out) {
|
||
if (!out) return 0;
|
||
memset(out, 0, sizeof(*out));
|
||
out->requested = s_req_scale;
|
||
out->effective = s_raster_ok ? s_out_scale : 0;
|
||
out->max_dim = s_gl_max_dim;
|
||
out->clamp_reason = s_scale_clamp_reason;
|
||
out->alloc_retries = s_scale_alloc_retries;
|
||
out->budget_mib = (int)(s_vram_budget >> 20);
|
||
out->fbo_w = s_raster_ok ? VRAM_W * s_hr_scale : 0;
|
||
out->fbo_h = s_raster_ok ? VRAM_H * s_hr_scale : 0;
|
||
out->hr_scale = s_raster_ok ? s_hr_scale : 0;
|
||
out->windowed = s_hiw;
|
||
{
|
||
/* The tile of the last presented display (else the first tile). */
|
||
const HiwTile *T = hiw_tile_holding(s_hiw_last_x0, s_hiw_last_x1);
|
||
if (!T && s_hiw_n) T = &s_hiw_t[0];
|
||
if (T) {
|
||
out->window_x = T->x0;
|
||
out->window_w = T->x1 - T->x0;
|
||
out->window_fbo_w = (T->x1 - T->x0) * s_out_scale;
|
||
out->window_fbo_h = VRAM_H * s_out_scale;
|
||
}
|
||
out->window_tiles = s_hiw_n;
|
||
uint64_t b = 0;
|
||
for (int t = 0; t < s_hiw_n; t++) b += hiw_tile_bytes(s_hiw_t[t].x0, s_hiw_t[t].x1);
|
||
out->window_mib = (int)(b >> 20);
|
||
}
|
||
out->window_grows = s_hiw_grows;
|
||
out->max_scale = psx_gl_clamp_full_vram_scale(GL_MAX_INTERNAL_SCALE,
|
||
GL_MAX_INTERNAL_SCALE,
|
||
s_gl_max_dim, s_vram_budget, NULL);
|
||
/* The high-resolution window lifts the ceiling to what one 512-row
|
||
* column of VRAM can hold. */
|
||
if (s_gl_max_dim > 0 && s_gl_max_dim / VRAM_H > out->max_scale) {
|
||
int wmax = s_gl_max_dim / VRAM_H;
|
||
out->max_scale = wmax > GL_MAX_INTERNAL_SCALE ? GL_MAX_INTERNAL_SCALE : wmax;
|
||
}
|
||
if (s_ctx && s_win) SDL_GL_GetDrawableSize(s_win, &out->drawable_w, &out->drawable_h);
|
||
return s_raster_ok;
|
||
}
|
||
|
||
/* Read the display rect at internal resolution straight from the hr FBO:
|
||
* (w*S) x (h*S) ARGB8888, top row first. The CPU-side hires path under GL is
|
||
* native (the software mirror stays 1x), so this is the only capture that
|
||
* shows what the internal resolution actually rendered. cap_px bounds the
|
||
* output; returns the pixel count, 0 when unavailable or too large. */
|
||
/* pitch_bytes: output row stride (>= W*4, multiple of 4). Saves and restores
|
||
* the read framebuffer, pixel-pack buffer and pack state, so it is safe from
|
||
* any capture point (present ring, debug server) mid-frame. */
|
||
static int gl_read_display_argb(int x, int y, int w, int h, uint32_t *out,
|
||
int pitch_bytes, int cap_px, int *ow, int *oh) {
|
||
if (!s_raster_ok || !s_ctx || !out || w <= 0 || h <= 0) return 0;
|
||
if (x < 0 || y < 0 || x + w > VRAM_W || y + h > VRAM_H) return 0;
|
||
int S = s_out_scale, W = w * S, H = h * S;
|
||
if ((int64_t)W * H > (int64_t)cap_px) return 0;
|
||
if (pitch_bytes < W * 4 || pitch_bytes % 4) return 0;
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
flush_cpu_upload();
|
||
GLuint fbo = s_hr_fbo;
|
||
int rx = x;
|
||
if (s_hiw) {
|
||
/* Windowed: the S-scaled pixels exist only inside the tiles. */
|
||
hiw_flush_queue();
|
||
const HiwTile *T = hiw_on() ? hiw_tile_holding(x, x + w) : NULL;
|
||
if (!T) return 0;
|
||
fbo = T->fbo;
|
||
rx = x - T->x0;
|
||
}
|
||
if (!fbo) return 0;
|
||
GLint read_fbo = 0, pack_buffer = 0, pack_row = 0, pack_align = 4;
|
||
glGetIntegerv(0x8CAA /* READ_FRAMEBUFFER_BINDING */, &read_fbo);
|
||
glGetIntegerv(0x88ED /* PIXEL_PACK_BUFFER_BINDING */, &pack_buffer);
|
||
glGetIntegerv(PSXGL_PACK_ROW_LENGTH, &pack_row);
|
||
glGetIntegerv(GL_PACK_ALIGNMENT, &pack_align);
|
||
p_glBindBuffer(0x88EB /* PIXEL_PACK_BUFFER */, 0);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo);
|
||
glPixelStorei(PSXGL_PACK_ROW_LENGTH, pitch_bytes / 4);
|
||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||
glReadPixels(rx * S, y * S, W, H, GL_BGRA, GL_UNSIGNED_BYTE, out);
|
||
glPixelStorei(PSXGL_PACK_ROW_LENGTH, pack_row);
|
||
glPixelStorei(GL_PACK_ALIGNMENT, pack_align);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, (GLuint)read_fbo);
|
||
p_glBindBuffer(0x88EB /* PIXEL_PACK_BUFFER */, (GLuint)pack_buffer);
|
||
/* The hr FBO stores PS1 y=0 at GL row 0 and glReadPixels reads bottom-up,
|
||
* so row 0 is already the display's top line (see glb_render_wide_display). */
|
||
for (int row = 0; row < H; row++) {
|
||
uint32_t *r = (uint32_t *)((uint8_t *)out + (size_t)row * (size_t)pitch_bytes);
|
||
for (int col = 0; col < W; col++) r[col] |= 0xFF000000u;
|
||
}
|
||
if (ow) *ow = W;
|
||
if (oh) *oh = H;
|
||
return W * H;
|
||
}
|
||
|
||
int gl_renderer_read_display_hires(int x, int y, int w, int h, uint32_t *out,
|
||
int cap_px, int *ow, int *oh) {
|
||
return gl_read_display_argb(x, y, w, h, out, w * s_out_scale * 4,
|
||
cap_px, ow, oh);
|
||
}
|
||
|
||
int gl_renderer_select_texture_bank(uint16_t id) {
|
||
uint32_t width, height;
|
||
const uint16_t* pixels;
|
||
GLint alignment, row_length;
|
||
s_selected_bank_live_clut = 0;
|
||
if (!id) { s_selected_bank_tex = 0; return 1; }
|
||
if (!gl_renderer_texture_banks_supported()) return 0;
|
||
if (!s_bank_tex[id]) {
|
||
pixels = mod_texture_bank_pixels(id, &width, &height);
|
||
if (!pixels) return 0;
|
||
glGenTextures(1, &s_bank_tex[id]);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
glBindTexture(GL_TEXTURE_2D, s_bank_tex[id]);
|
||
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);
|
||
glGetIntegerv(GL_UNPACK_ALIGNMENT, &alignment);
|
||
glGetIntegerv(PSXGL_UNPACK_ROW_LENGTH, &row_length);
|
||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0);
|
||
glTexImage2D(GL_TEXTURE_2D, 0, PSXGL_R16UI, (GLsizei)width,
|
||
(GLsizei)height, 0, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, pixels);
|
||
glPixelStorei(GL_UNPACK_ALIGNMENT, alignment);
|
||
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, row_length);
|
||
if (glGetError() != GL_NO_ERROR) {
|
||
glDeleteTextures(1, &s_bank_tex[id]); s_bank_tex[id] = 0;
|
||
return 0;
|
||
}
|
||
}
|
||
s_selected_bank_tex = s_bank_tex[id];
|
||
return 1;
|
||
}
|
||
|
||
int gl_renderer_select_texture_bank_live_clut(uint16_t id) {
|
||
if (!gl_renderer_select_texture_bank(id)) return 0;
|
||
s_selected_bank_live_clut = id != 0;
|
||
return 1;
|
||
}
|
||
|
||
int gl_renderer_init_context(SDL_Window *win) {
|
||
s_win = win;
|
||
s_present_w = 0;
|
||
s_present_h = 0;
|
||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||
s_ctx = SDL_GL_CreateContext(win);
|
||
if (!s_ctx) { fprintf(stdout, "psxrecomp: GL context creation failed (%s)\n", SDL_GetError()); return 0; }
|
||
if (SDL_GL_MakeCurrent(win, s_ctx) != 0) { fprintf(stdout, "psxrecomp: MakeCurrent failed (%s)\n", SDL_GetError()); SDL_GL_DeleteContext(s_ctx); s_ctx=NULL; return 0; }
|
||
/* Swap interval: 1=vsync (tear-free, default), 0=immediate (lowest display
|
||
* latency, may tear; our wall-clock pacer still holds 59.94Hz), -1=adaptive.
|
||
* Adaptive falls back to vsync if the driver rejects it. */
|
||
if (SDL_GL_SetSwapInterval(s_swap_interval) != 0 && s_swap_interval < 0) {
|
||
SDL_GL_SetSwapInterval(1);
|
||
s_swap_interval = 1;
|
||
}
|
||
glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE);
|
||
const char *ver = (const char *)glGetString(GL_VERSION);
|
||
fprintf(stdout, "psxrecomp: OpenGL context created (%s)\n", ver ? ver : "?");
|
||
|
||
/* All-or-nothing: any missing entry point / failed shader / bad FBO means
|
||
* the whole GL renderer is unavailable and the runtime stays on the pure
|
||
* software path — no half-GL hybrid (that mixed mode is what produced
|
||
* the alternating-present menu jitter). */
|
||
int ok = load_modern_gl();
|
||
if (ok) {
|
||
glGenTextures(1, &s_present_tex);
|
||
glBindTexture(GL_TEXTURE_2D, s_present_tex);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||
s_present_prog = build_program(PRESENT_VS, PRESENT_FS);
|
||
s_interp_prog = build_program(PRESENT_VS, INTERP_FS);
|
||
if (s_present_prog && s_interp_prog) {
|
||
p_glGenVertexArrays(1, &s_present_vao);
|
||
s_present_uTex = p_glGetUniformLocation(s_present_prog, "u_tex");
|
||
s_present_uUvRect = p_glGetUniformLocation(s_present_prog, "u_uv_rect");
|
||
s_present_uTexSize =
|
||
p_glGetUniformLocation(s_present_prog, "u_tex_size");
|
||
s_present_uSharpScale =
|
||
p_glGetUniformLocation(s_present_prog, "u_sharp_scale");
|
||
s_present_uSharp =
|
||
p_glGetUniformLocation(s_present_prog, "u_sharp");
|
||
s_present_uGamma =
|
||
p_glGetUniformLocation(s_present_prog, "u_gamma");
|
||
s_present_uScanline =
|
||
p_glGetUniformLocation(s_present_prog, "u_scanline");
|
||
s_present_uScanStrength =
|
||
p_glGetUniformLocation(s_present_prog, "u_scanline_strength");
|
||
s_present_uScanLines =
|
||
p_glGetUniformLocation(s_present_prog, "u_scanline_lines");
|
||
s_present_uScanScale =
|
||
p_glGetUniformLocation(s_present_prog, "u_scanline_scale");
|
||
s_interp_uScanline =
|
||
p_glGetUniformLocation(s_interp_prog, "u_scanline");
|
||
s_interp_uScanStrength =
|
||
p_glGetUniformLocation(s_interp_prog, "u_scanline_strength");
|
||
s_interp_uScanLines =
|
||
p_glGetUniformLocation(s_interp_prog, "u_scanline_lines");
|
||
s_interp_uScanScale =
|
||
p_glGetUniformLocation(s_interp_prog, "u_scanline_scale");
|
||
s_interp_uPrev = p_glGetUniformLocation(s_interp_prog, "u_prev");
|
||
s_interp_uCurr = p_glGetUniformLocation(s_interp_prog, "u_curr");
|
||
s_interp_uAlpha = p_glGetUniformLocation(s_interp_prog, "u_alpha");
|
||
s_interp_uUvRect = p_glGetUniformLocation(s_interp_prog, "u_uv_rect");
|
||
s_interp_uBlendMode =
|
||
p_glGetUniformLocation(s_interp_prog, "u_blend_mode");
|
||
s_interp_uGamma =
|
||
p_glGetUniformLocation(s_interp_prog, "u_gamma");
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
int gl_renderer_get_swap_interval(void) {
|
||
if(!s_ctx)return -2;
|
||
#if defined(PSX_SDL3)
|
||
int interval=0;
|
||
return SDL_GL_GetSwapInterval(&interval)?interval:-2;
|
||
#else
|
||
return SDL_GL_GetSwapInterval();
|
||
#endif
|
||
}
|
||
|
||
static void pass_resources_release(void);
|
||
|
||
void gl_renderer_shutdown(void) {
|
||
s_native_surface_enabled=s_native_surface_pending=0;
|
||
gl_renderer_clear_hd_texture_cache();
|
||
pass_resources_release();
|
||
if (s_ctx) {
|
||
for (unsigned i = 1; i < 65536u; ++i)
|
||
if (s_bank_tex[i]) glDeleteTextures(1, &s_bank_tex[i]);
|
||
/* Bezel artwork belongs to the session that loaded it: a lobby
|
||
* rematch creates a new context and loads its own artwork (or none),
|
||
* and present_bezel() must not bind this name in that context. */
|
||
gl_renderer_set_bezel(NULL, 0, 0);
|
||
}
|
||
memset(s_bank_tex, 0, sizeof s_bank_tex);
|
||
s_selected_bank_tex = s_tb_bank_tex = 0;
|
||
s_selected_bank_live_clut = s_tb_bank_live_clut = 0;
|
||
if (s_ctx) {
|
||
ensure_cpu();
|
||
SDL_GL_DeleteContext(s_ctx); s_ctx = NULL;
|
||
}
|
||
free(s_conv); s_conv = NULL;
|
||
s_hq_n = 0; s_hq_vn = 0; /* queued window draws die with the context */
|
||
s_raster_ok = 0;
|
||
/* New context regenerates s_present_tex empty; a stale size makes
|
||
* upload_present_tex take glTexSubImage2D into an unallocated texture
|
||
* (rematch 24-bit FMV → black picture, audio still runs). */
|
||
s_present_w = 0;
|
||
s_present_h = 0;
|
||
s_osd_tex = 0;
|
||
s_osd_tw = 0;
|
||
s_osd_th = 0;
|
||
s_depth24_skip_up = 0;
|
||
rect_clear(&s_d24_skip_fb);
|
||
hold_invalidate();
|
||
s_hold_tex = 0;
|
||
s_hold_fbo = 0;
|
||
s_hold_tw = 0;
|
||
s_hold_th = 0;
|
||
s_interp_fbo = 0; /* died with the context */
|
||
}
|
||
|
||
/* CPU-readout present (24-bit FMV frames and the PSX_GL_FORCE_CPU_PRESENT
|
||
* diagnostic): full-window clear, then a quad into the letterbox rect.
|
||
* force_4_3 pins the rect to native 4:3 regardless of the display aspect —
|
||
* FMVs are authored 4:3 and have no GTE squash to compensate a stretch, so
|
||
* widescreen presents them pillarboxed instead of distorted. */
|
||
void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear,
|
||
int force_4_3, int content_w) {
|
||
if (!s_ctx) return;
|
||
interp_reset_history();
|
||
int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
glViewport(0, 0, ww, wh);
|
||
glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT);
|
||
int lx, ly, lw, lh;
|
||
if (force_4_3)
|
||
letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh);
|
||
else
|
||
letterbox_rect(ww, wh, &lx, &ly, &lw, &lh);
|
||
/* Short GP1(07h) bands (MotK FMV is 128 lines) only fill a fraction of
|
||
* NTSC active height on hardware. Stretching them to the full letterbox
|
||
* doubles vertical scale vs horizontal and makes the frame look too wide
|
||
* with the right edge clipped. Letterbox within the present rect instead.
|
||
* Apply whenever the source is short — not only when force_4_3 — so a
|
||
* misclassified FMV frame still keeps correct pixel aspect. */
|
||
/* Genuinely windowed video bands only (<80% of the 240-line field, e.g.
|
||
* MotK's 128-line FMV). A game's native short display mode (216/224)
|
||
* fills the rect as on hardware. */
|
||
if (src_h > 0 && src_h < 192) {
|
||
int content_h = (lh * src_h) / 240;
|
||
if (content_h < 1) content_h = 1;
|
||
ly += (lh - content_h) / 2;
|
||
lh = content_h;
|
||
}
|
||
/* Optional trailing-column crop (depth24 margin): shrink the draw width
|
||
* left-aligned so cleared black remains on the right — never stretch. */
|
||
float uv_x1 = 1.f;
|
||
int crop = (content_w > 0 && content_w < src_w && src_w > 0);
|
||
if (crop) {
|
||
uv_x1 = (float)content_w / (float)src_w;
|
||
lw = (lw * content_w) / src_w;
|
||
if (lw < 1) lw = 1;
|
||
}
|
||
/* This is the low-res source path (24-bit FMV, and the forced-CPU present
|
||
* diagnostic): a 320x192-class image blown up to fill the window, so how it
|
||
* is reconstructed is very visible. `linear` (the video AA setting) allows
|
||
* filtered reconstruction when [video] fmv_filter opts into it:
|
||
*
|
||
* nearest hard pixels, uneven pixel widths at non-integer scale
|
||
* bilinear plain GL_LINEAR — smoothest, but blurs the whole texel
|
||
* sharp sharp-bilinear: flat texel interiors, ramp confined to a
|
||
* one-output-pixel band at the boundary
|
||
* bicubic Catmull-Rom
|
||
*
|
||
* Measured on this intro at 1280x960 (fraction of adjacent pixel pairs
|
||
* differing by >=24 luma = visible staircase, vs mean |dx| = overall
|
||
* sharpness): nearest 1.00%/1.028, sharp 0.87%/1.008, bicubic 0.34%/1.038,
|
||
* bilinear 0.14%/0.930. Bicubic removes two thirds of the staircase while
|
||
* holding gradient at the nearest level; bilinear removes the most but
|
||
* costs 10% of it, which reads as blur. Still a taste call, hence the knob. */
|
||
int filt_mode = linear ? fmv_filter_mode()
|
||
: -1; /* AA off: nearest, no shader work */
|
||
glViewport(lx, ly, lw, lh);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
upload_present_tex(pixels, src_w, src_h, filt_mode >= 0 ? 1 : 0);
|
||
p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0);
|
||
present_set_gamma(s_present_uGamma, 1);
|
||
present_set_sharp(filt_mode, src_w, src_h, lw, lh);
|
||
/* CPU present texture holds exactly the display rect, so v_uv spans it and
|
||
* pitch == display height == src_h. */
|
||
PRESENT_SCANLINE(src_h, src_h, lh);
|
||
if (crop) {
|
||
/* Cropped present keeps left-aligned content; still inset so linear
|
||
* AA does not blend the cut column with undefined border texels. */
|
||
float u0 = (src_w > 0) ? (0.5f / (float)src_w) : 0.f;
|
||
float v0 = (src_h > 0) ? (0.5f / (float)src_h) : 0.f;
|
||
p_glUniform4f(s_present_uUvRect, u0, v0, uv_x1 - u0, 1.f - v0);
|
||
} else if (src_w > 0 && src_h > 0) {
|
||
/* Half-texel UV inset for both nearest and linear. Corner-mapped
|
||
* UV=1.0 grazes past the last texel (driver-dependent border sample);
|
||
* with GL_LINEAR that also blends an edge stripe into the image.
|
||
* Matches present_target_quad / MotK present UV edge-bleed fix. */
|
||
float u0 = 0.5f / (float)src_w, v0 = 0.5f / (float)src_h;
|
||
p_glUniform4f(s_present_uUvRect, u0, v0, 1.f - u0, 1.f - v0);
|
||
} else {
|
||
p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f);
|
||
}
|
||
p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3);
|
||
p_glBindVertexArray(0); p_glUseProgram(0);
|
||
pres_record(GL_PRES_CPU, 0, 0, src_w, src_h, lx, ly, lw, lh);
|
||
hold_capture_drawable();
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
gl_swap_with_osd();
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
s_probe_swap++;
|
||
present_force_consumed();
|
||
s_last_present_path = GL_PRES_CPU;
|
||
}
|
||
|
||
void gl_renderer_present_blank(void) {
|
||
if (!s_ctx) return;
|
||
interp_reset_history();
|
||
int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
glViewport(0, 0, ww, wh); glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT);
|
||
pres_record(GL_PRES_BLANK, 0, 0, 0, 0, 0, 0, ww, wh);
|
||
hold_capture_drawable();
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
gl_swap_with_osd();
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
s_probe_swap++;
|
||
present_force_consumed();
|
||
s_last_present_path = GL_PRES_BLANK;
|
||
}
|
||
|
||
/* Sync the authoritative FBO down into CPU VRAM (no-op when current).
|
||
* Screenshots / debug server. Not for 24-bit FMV scanout (see flush). */
|
||
void gl_renderer_sync_cpu(void) {
|
||
ensure_cpu();
|
||
}
|
||
|
||
void gl_renderer_invalidate_present(void) {
|
||
stereo_invalidate();
|
||
for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull;
|
||
s_last_present_path = -1;
|
||
s_force_present_remaining = 8;
|
||
hold_invalidate();
|
||
interp_reset_history();
|
||
}
|
||
|
||
void gl_renderer_restage_vram_after_savestate(void) {
|
||
gpu_hd_textures_reset_tracking();
|
||
if (!s_raster_ok || !s_vram) return;
|
||
/* Belt-and-suspenders after boot_state VRAM apply: force CPU mirror → FBO
|
||
* even if a depth24 skip swallowed the restore, then re-arm scanout-band
|
||
* clear so leaving FMV does not keep RGB888-as-1555 junk. */
|
||
s_up_nrects = 0;
|
||
rect_clear(&s_d24_skip_fb);
|
||
s_depth24_skip_up = 0;
|
||
up_add_transfer(0, 0, VRAM_W, VRAM_H);
|
||
flush_cpu_upload();
|
||
if (gpu_display_is_depth24()) {
|
||
s_depth24_skip_up = 1;
|
||
depth24_mark_scanout_band();
|
||
}
|
||
/* Dual-raster: CPU is authority after snap; FBO is cosmetics only. */
|
||
if (s_cpu_auth_dual || s_hd_native_authority) {
|
||
s_gpu_dirty = 0;
|
||
rect_clear(&s_cpu_dirty);
|
||
}
|
||
}
|
||
|
||
void gl_renderer_set_cpu_auth_dual(int on) {
|
||
s_cpu_auth_dual = on ? 1 : 0;
|
||
if (s_cpu_auth_dual) {
|
||
s_gpu_dirty = 0;
|
||
rect_clear(&s_cpu_dirty);
|
||
}
|
||
}
|
||
|
||
void gl_renderer_set_hd_texture_mode(int on) {
|
||
on = on ? 1 : 0;
|
||
if (on == s_hd_native_authority) return;
|
||
if (s_raster_ok) {
|
||
flush_flat_batch(); flush_tex_batch(); flush_cpu_upload(); hiw_flush_queue();
|
||
if (on) ensure_cpu();
|
||
else {
|
||
/* Return GPU authority only after replacing the presentation
|
||
* image with native VRAM; an HD frame must never be read back. */
|
||
gl_renderer_restage_vram_after_savestate();
|
||
}
|
||
}
|
||
s_hd_native_authority = on;
|
||
s_gpu_dirty = 0; rect_clear(&s_cpu_dirty);
|
||
if (on) s_selected_bank_tex = 0;
|
||
}
|
||
|
||
int gl_renderer_cpu_auth_dual(void) {
|
||
return s_cpu_auth_dual;
|
||
}
|
||
|
||
void gl_renderer_present_probe_reset(void) {
|
||
s_probe_skip = 0;
|
||
s_probe_swap = 0;
|
||
s_probe_dirty_marks = 0;
|
||
}
|
||
|
||
void gl_renderer_present_probe_take(uint64_t *skip_delta, uint64_t *swap_delta,
|
||
uint64_t *dirty_mark_delta,
|
||
int *force_remaining) {
|
||
if (skip_delta) { *skip_delta = s_probe_skip; s_probe_skip = 0; }
|
||
if (swap_delta) { *swap_delta = s_probe_swap; s_probe_swap = 0; }
|
||
if (dirty_mark_delta) {
|
||
*dirty_mark_delta = s_probe_dirty_marks;
|
||
s_probe_dirty_marks = 0;
|
||
}
|
||
if (force_remaining) *force_remaining = s_force_present_remaining;
|
||
}
|
||
|
||
int gl_renderer_present_rect_dirty(int disp_x, int disp_y, int w, int h) {
|
||
if (!s_raster_ok || w <= 0 || h <= 0) return 0;
|
||
return present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1);
|
||
}
|
||
|
||
void gl_renderer_flush_cpu_uploads(void) {
|
||
if (!s_raster_ok) return;
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
flush_cpu_upload();
|
||
}
|
||
|
||
/* Diagnostic (debug server "gl_fbo_peek"): read a rect of the GPU-side
|
||
* authoritative VRAM (via the pack pass + raw mirror) WITHOUT writing CPU
|
||
* VRAM — lets a probe diff FBO truth against CPU truth. Returns 0 when the
|
||
* GL pipeline is inactive (software backend). */
|
||
int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out) {
|
||
if (!s_raster_ok || !s_ctx) return 0;
|
||
if (x < 0 || y < 0 || w < 1 || h < 1 ||
|
||
x + w > VRAM_W || y + h > VRAM_H) return 0;
|
||
flush_cpu_upload();
|
||
rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1);
|
||
pack_flush();
|
||
coh_record(GL_COH_PEEK, x, y, x + w - 1, y + h - 1);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo);
|
||
glPixelStorei(GL_PACK_ALIGNMENT, 2);
|
||
glReadPixels(x, y, w, h, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, out);
|
||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
return 1;
|
||
}
|
||
|
||
/* Diagnostic (debug server "gl_vram_diff"): full-VRAM comparison of the
|
||
* GPU-side truth (FBO via pack) against the CPU array, WITHOUT writing
|
||
* either. Reports mismatch count + bounding box + a few sample coords.
|
||
* Divergence is expected where the GPU is legitimately ahead (gpu_dirty);
|
||
* at upload-only scenes the two must match exactly. */
|
||
int gl_renderer_vram_diff(uint32_t *count, int bbox[4],
|
||
int samples[8][2], uint16_t samples_px[8][2]) {
|
||
if (!s_raster_ok || !s_ctx) return 0;
|
||
uint16_t *tmp = (uint16_t *)malloc((size_t)VRAM_W * VRAM_H * 2);
|
||
if (!tmp) return 0;
|
||
flush_cpu_upload();
|
||
/* Force a full pack: the diff must read FBO truth even where the
|
||
* raw-mirror invariant (raw == FBO outside s_pack_dirty) is broken —
|
||
* a broken invariant is exactly what this tool hunts. */
|
||
rect_add(&s_pack_dirty, 0, 0, VRAM_W - 1, VRAM_H - 1);
|
||
pack_flush();
|
||
coh_record(GL_COH_DIFF, 0, 0, VRAM_W - 1, VRAM_H - 1);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo);
|
||
glReadPixels(0, 0, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, tmp);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
uint32_t n = 0;
|
||
int x0 = VRAM_W, y0 = VRAM_H, x1 = -1, y1 = -1, ns = 0;
|
||
for (int y = 0; y < VRAM_H; y++) {
|
||
for (int x = 0; x < VRAM_W; x++) {
|
||
uint16_t f = tmp[y * VRAM_W + x], c = s_vram[y * VRAM_W + x];
|
||
if (f == c) continue;
|
||
n++;
|
||
if (x < x0) x0 = x; if (x > x1) x1 = x;
|
||
if (y < y0) y0 = y; if (y > y1) y1 = y;
|
||
if (ns < 8 && (n % 977) == 1) { /* spread samples */
|
||
samples[ns][0] = x; samples[ns][1] = y;
|
||
samples_px[ns][0] = f; samples_px[ns][1] = c;
|
||
ns++;
|
||
}
|
||
}
|
||
}
|
||
free(tmp);
|
||
*count = n;
|
||
bbox[0] = x0; bbox[1] = y0; bbox[2] = x1; bbox[3] = y1;
|
||
return 1 + ns; /* >=1 means valid; ns = samples filled */
|
||
}
|
||
|
||
/* Diagnostic state for the debug server: coherency flags + dirty rects. */
|
||
void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]) {
|
||
if (gpu_dirty) *gpu_dirty = s_gpu_dirty;
|
||
if (pending) {
|
||
/* [0] = pending rect count; [1..4] = union bbox (diagnostic only —
|
||
* the flush itself paints the exact rects, never this union). */
|
||
pending[0] = s_up_nrects;
|
||
pending[1] = pending[2] = pending[3] = pending[4] = 0;
|
||
for (int i = 0; i < s_up_nrects; i++) {
|
||
if (i == 0) {
|
||
pending[1] = s_up_rects[i].x0; pending[2] = s_up_rects[i].y0;
|
||
pending[3] = s_up_rects[i].x1; pending[4] = s_up_rects[i].y1;
|
||
} else {
|
||
if (s_up_rects[i].x0 < pending[1]) pending[1] = s_up_rects[i].x0;
|
||
if (s_up_rects[i].y0 < pending[2]) pending[2] = s_up_rects[i].y0;
|
||
if (s_up_rects[i].x1 > pending[3]) pending[3] = s_up_rects[i].x1;
|
||
if (s_up_rects[i].y1 > pending[4]) pending[4] = s_up_rects[i].y1;
|
||
}
|
||
}
|
||
}
|
||
if (pack) {
|
||
pack[0] = s_pack_dirty.set;
|
||
pack[1] = s_pack_dirty.x0; pack[2] = s_pack_dirty.y0;
|
||
pack[3] = s_pack_dirty.x1; pack[4] = s_pack_dirty.y1;
|
||
}
|
||
}
|
||
|
||
/* ------------------------------------------------------------------------- *
|
||
* Native-wide compositor (GL). Mirrors gpu_sw_renderer.c's wide functions:
|
||
* canonical VRAM (the hr FBO) is untouched; framebuffer draws are also mirrored
|
||
* into per-base_x wide FBOs (see the draw funcs' wide passes). Present reads the
|
||
* displayed buffer's wide FBO via glReadPixels into the CPU present buffer, then
|
||
* the existing CPU present path uploads/letterboxes it (Option B: reuse the CPU
|
||
* present path). Self-gates on the GL pipeline being live; never runs for
|
||
* 4:3 / non-opted games (gpu.c never calls wide_configure for those).
|
||
* ------------------------------------------------------------------------- */
|
||
|
||
static void wide_free_all(void) {
|
||
for (int i = 0; i < WIDE_MAX_SURF; i++) {
|
||
if (s_wide_fbo[i]) { p_glDeleteFramebuffers(1, &s_wide_fbo[i]); s_wide_fbo[i] = 0; }
|
||
if (s_wide_tex[i]) { glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0; }
|
||
if (s_wide_rb[i]) { p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0; }
|
||
s_wide_base[i] = -1;
|
||
}
|
||
g_wide_cur = 0;
|
||
}
|
||
|
||
/* Lazily allocate (or find) the wide FBO+tex for base_x. Returns the FBO id, or
|
||
* 0 on failure / more distinct buffers than WIDE_MAX_SURF. */
|
||
static GLuint wide_fbo_for(int base_x) {
|
||
if (g_wide_w <= 0) return 0;
|
||
for (int i = 0; i < WIDE_MAX_SURF; i++)
|
||
if (s_wide_fbo[i] && s_wide_base[i] == base_x) return s_wide_fbo[i];
|
||
/* A native-wide surface is g_wide_w*S wide, and the driver limit bounds
|
||
* it: on a 16384 limit, 1024 native columns at 16x (about 38:9 at a
|
||
* 320-px display) and 910 at 18x in the window mode (about 34:9). Fit to
|
||
* Window narrows its aspect to fit (gl_renderer_fit_wide_aspect,
|
||
* main.cpp); a fixed aspect on a wide display mode can still ask for more.
|
||
* Refuse that with one log line instead of failing mid-draw: the frame
|
||
* then presents 4:3 through the CPU path at 1x, without the native-wide
|
||
* margins. */
|
||
if (s_gl_max_dim > 0 &&
|
||
((int64_t)g_wide_w * s_out_scale > s_gl_max_dim ||
|
||
(int64_t)VRAM_H * s_out_scale > s_gl_max_dim)) {
|
||
if (!s_wide_refused_logged) {
|
||
fprintf(stdout, "psxrecomp: GL native-wide surface %dx%d at %dx exceeds "
|
||
"the GPU limit %d; presenting 4:3\n", g_wide_w * s_out_scale,
|
||
VRAM_H * s_out_scale, s_out_scale, s_gl_max_dim);
|
||
s_wide_refused_logged = 1;
|
||
}
|
||
return 0;
|
||
}
|
||
for (int i = 0; i < WIDE_MAX_SURF; i++) {
|
||
if (!s_wide_fbo[i]) {
|
||
int w = g_wide_w * s_out_scale, h = VRAM_H * s_out_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_set_view(int enabled, int shift, int pad_left, int pad_right) {
|
||
if (!enabled) shift = pad_left = pad_right = 0;
|
||
if (view_enabled == enabled && view_shift == shift &&
|
||
view_pad_left == pad_left && view_pad_right == pad_right) return;
|
||
flush_flat_batch(); flush_tex_batch();
|
||
view_enabled = enabled;
|
||
view_shift = shift;
|
||
view_pad_left = pad_left;
|
||
view_pad_right = pad_right;
|
||
}
|
||
|
||
static void glb_wide_configure(int wide_w, int offset) {
|
||
if (!s_raster_ok) return;
|
||
double t0 = cw_ms(); s_cw_wide_cfgs++;
|
||
flush_line_batch();
|
||
flush_tex_batch(); /* a queued batch's wide mirror targets the CURRENT surfaces */
|
||
hiw_flush_queue(); /* ...and so does a windowed-mode queued one */
|
||
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(); s_wide_refused_logged = 0; }
|
||
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_flat_batch(); /* drain into the OLD target before switching */
|
||
flush_tex_batch();
|
||
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_flat_batch(); 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();
|
||
hiw_flush_queue(); /* queued wide mirrors land before the clear */
|
||
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_out_scale;
|
||
int y0 = y * s_out_scale, y1 = (y + h) * s_out_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_out_scale, H);
|
||
glEnable(GL_SCISSOR_TEST);
|
||
glScissor(0, y0, g_wide_w * s_out_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_flat_batch();
|
||
flush_tex_batch();
|
||
hiw_flush_queue(); /* queued wide mirrors land before the clear */
|
||
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_out_scale;
|
||
int W = g_wide_w * s_out_scale;
|
||
int margin = g_wide_off * s_out_scale;
|
||
int y0 = y * s_out_scale, y1 = (y + h) * s_out_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_flat_batch();
|
||
flush_tex_batch();
|
||
flush_cpu_upload();
|
||
hiw_flush_queue(); /* windowed: queued wide mirrors */
|
||
wide_blit_center(fbo, base_x, disp_y, disp_h); /* fast-path: authoritative centre before readback */
|
||
glFinish();
|
||
|
||
int W = g_wide_w * s_out_scale;
|
||
int H = VRAM_H * s_out_scale;
|
||
int out_h = disp_h * s_out_scale;
|
||
int ry0 = disp_y * s_out_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();
|
||
hiw_flush_queue(); /* windowed: queued wide mirrors */
|
||
wide_blit_center(fbo, base_x, 0, VRAM_H); /* authoritative centre (full height) */
|
||
glFinish();
|
||
int W = g_wide_w * s_out_scale;
|
||
int H = VRAM_H * s_out_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_src_w = s_interp_src_h = 0;
|
||
s_interp_source_path = -1;
|
||
frame_interpolation_schedule_reset(&s_interp_schedule);
|
||
frame_flip_tracker_reset(&s_interp_flip);
|
||
s_interp_origin_x = s_interp_origin_y = -1;
|
||
s_interp_phase_lo = 0.0;
|
||
s_interp_phase_hi = 1.0;
|
||
pass_gens_invalidate();
|
||
}
|
||
|
||
static void interp_reset_history(void) {
|
||
interp_reset_history_unlocked();
|
||
}
|
||
|
||
void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz,
|
||
double source_hz, int blend_mode) {
|
||
double effective_hz = target_hz > 0.0 ? target_hz : host_hz;
|
||
if (effective_hz < source_hz) effective_hz = source_hz;
|
||
int active = (enabled && source_hz >= 1.0 && source_hz <= 1000.0 &&
|
||
effective_hz >= source_hz && effective_hz <= 1000.0) ? 1 : 0;
|
||
const char *diag = getenv("PSX_GL_INTERP_DIAG");
|
||
s_interp_diag = diag && diag[0] && diag[0] != '0';
|
||
if (active != s_interp_enabled || source_hz != s_interp_source_hz ||
|
||
effective_hz != s_interp_target_hz)
|
||
interp_reset_history_unlocked();
|
||
s_interp_enabled = active;
|
||
s_interp_host_hz = host_hz;
|
||
s_interp_target_hz = active ? effective_hz : 0.0;
|
||
s_interp_source_hz = active ? source_hz : 0.0;
|
||
s_interp_blend_mode = blend_mode == 1 ? 1 : 0;
|
||
s_interp_hold = blend_mode == 2 ? 1 : 0;
|
||
if (active)
|
||
fprintf(stdout, "psxrecomp: GL temporal frame blending enabled: %.1f "
|
||
"presents/s from %.3f guest frames/s on the render thread "
|
||
"(%s; no motion vectors)\n",
|
||
effective_hz, source_hz,
|
||
s_interp_hold ? "hold, in-between frames from render passes"
|
||
: s_interp_blend_mode ? "change-adaptive blend"
|
||
: "linear blend");
|
||
else
|
||
fprintf(stdout, "psxrecomp: GL temporal frame blending disabled "
|
||
"(host %.1f Hz)\n", host_hz);
|
||
}
|
||
|
||
void gl_renderer_set_interpolation_blend(int blend_mode) {
|
||
s_interp_blend_mode = blend_mode == 1 ? 1 : 0;
|
||
s_interp_hold = blend_mode == 2 ? 1 : 0;
|
||
}
|
||
|
||
void gl_renderer_set_interpolation_suspended(int suspended) {
|
||
suspended = suspended ? 1 : 0;
|
||
if (suspended != s_interp_suspended) interp_reset_history_unlocked();
|
||
s_interp_suspended = suspended;
|
||
}
|
||
|
||
void gl_renderer_set_interpolation_source(int source) {
|
||
source = source == 1 ? 1 : 0;
|
||
if (source != s_interp_source) interp_reset_history_unlocked();
|
||
s_interp_source = source;
|
||
if (s_interp_enabled && source)
|
||
fprintf(stdout, "psxrecomp: GL temporal blending follows guest frame "
|
||
"flips (flip-aware source)\n");
|
||
}
|
||
|
||
void gl_renderer_interpolation_source_diag(int *source, uint32_t *flip_period,
|
||
uint64_t *captures,
|
||
uint64_t *duplicates) {
|
||
if (source) *source = s_interp_source;
|
||
if (flip_period) *flip_period = s_interp_flip.period;
|
||
if (captures) *captures = s_interp_captures;
|
||
if (duplicates) *duplicates = s_interp_duplicates;
|
||
}
|
||
|
||
/* Host suspension OR a running OpenXR session: every XR present would otherwise
|
||
* wait/begin/submit a frame per interpolated sub-present. The compositor
|
||
* reprojects instead. Constant 0 contribution without an active XR session;
|
||
* history is reset on each transition like the FMV suspension. */
|
||
static int s_interp_xr_gated;
|
||
static int interp_suspended_now(void) {
|
||
int xr = psx_openxr_session_active();
|
||
if (xr != s_interp_xr_gated) { s_interp_xr_gated = xr; interp_reset_history_unlocked(); }
|
||
return s_interp_suspended || xr;
|
||
}
|
||
|
||
int gl_renderer_interpolation_owns_cadence(void) {
|
||
return s_ctx && s_interp_enabled && !interp_suspended_now();
|
||
}
|
||
|
||
void gl_renderer_interpolation_diag(int *enabled, int *suspended,
|
||
int *history_frames,
|
||
double *host_hz, double *target_hz,
|
||
uint64_t *swaps) {
|
||
if (enabled) *enabled = s_interp_enabled;
|
||
if (suspended) *suspended = s_interp_suspended;
|
||
if (history_frames) *history_frames = s_interp_valid;
|
||
if (host_hz) *host_hz = s_interp_host_hz;
|
||
if (target_hz) *target_hz = s_interp_target_hz;
|
||
if (swaps) *swaps = s_interp_swaps;
|
||
}
|
||
|
||
/* Copy a stable display image out of the mutable VRAM/wide render target.
|
||
* Returns true when temporal blending owns this source-frame interval. */
|
||
static int interp_capture(GLuint fbo, int x, int y, int w, int h,
|
||
int linear, int force_4_3, int source_path,
|
||
int origin_x, int origin_y, int redrawn) {
|
||
if (!s_interp_enabled || interp_suspended_now() || !fbo || w <= 0 || h <= 0) return 0;
|
||
int sw = w * s_out_scale, sh = h * s_out_scale, pw, ph;
|
||
hiw_capture_size(sw, sh, force_4_3, &pw, &ph);
|
||
int geometry_changed =
|
||
pw != s_interp_w || ph != s_interp_h ||
|
||
sw != s_interp_src_w || sh != s_interp_src_h ||
|
||
source_path != s_interp_source_path || force_4_3 != s_interp_force_4_3;
|
||
if (s_interp_source == 1) {
|
||
/* FLIP source: a VBlank that re-presents the same displayed image is
|
||
* not a new source frame. Keep history and advance the phase window. */
|
||
int new_frame = frame_flip_is_new_frame(
|
||
geometry_changed, s_interp_valid == 0, redrawn, origin_x, origin_y,
|
||
s_interp_origin_x, s_interp_origin_y);
|
||
s_interp_origin_x = origin_x;
|
||
s_interp_origin_y = origin_y;
|
||
(void)frame_flip_tracker_vblank(&s_interp_flip, new_frame,
|
||
&s_interp_phase_lo, &s_interp_phase_hi);
|
||
if (!new_frame) {
|
||
s_interp_duplicates++;
|
||
return 1;
|
||
}
|
||
pass_note_new_frame(origin_x, origin_y, source_path, pw, ph);
|
||
} else {
|
||
s_interp_phase_lo = 0.0;
|
||
s_interp_phase_hi = 1.0;
|
||
}
|
||
if (geometry_changed) {
|
||
s_interp_valid = 0;
|
||
s_interp_w = pw; s_interp_h = ph;
|
||
s_interp_src_w = sw; s_interp_src_h = sh;
|
||
s_interp_prev = s_interp_cur = 0;
|
||
for (int i = 0; i < 3; i++) {
|
||
glBindTexture(GL_TEXTURE_2D, s_interp_tex[i]);
|
||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, pw, ph, 0,
|
||
GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||
}
|
||
}
|
||
|
||
int dst = 0;
|
||
if (s_interp_valid == 1) dst = s_interp_cur == 0 ? 1 : 0;
|
||
else if (s_interp_valid >= 2) dst = s_interp_prev;
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo);
|
||
if (pw == sw && ph == sh) {
|
||
glBindTexture(GL_TEXTURE_2D, s_interp_tex[dst]);
|
||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,
|
||
x * s_out_scale, y * s_out_scale, pw, ph);
|
||
} else { /* windowed high-resolution mode: at the presented size */
|
||
if (!s_interp_fbo) p_glGenFramebuffers(1, &s_interp_fbo);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_interp_fbo);
|
||
p_glFramebufferTexture2D(PSXGL_DRAW_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0,
|
||
GL_TEXTURE_2D, s_interp_tex[dst], 0);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBlitFramebuffer(x * s_out_scale, y * s_out_scale,
|
||
x * s_out_scale + sw, y * s_out_scale + sh,
|
||
0, 0, pw, ph, GL_COLOR_BUFFER_BIT,
|
||
linear ? GL_LINEAR : GL_NEAREST);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||
}
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
if (s_interp_valid == 0) {
|
||
s_interp_cur = dst;
|
||
s_interp_valid = 1;
|
||
} else {
|
||
s_interp_prev = s_interp_cur;
|
||
s_interp_cur = dst;
|
||
s_interp_valid = 2;
|
||
}
|
||
s_interp_linear = linear;
|
||
s_interp_force_4_3 = force_4_3;
|
||
s_interp_source_path = source_path;
|
||
s_interp_captures++;
|
||
return 1;
|
||
}
|
||
|
||
static void interp_draw_textures(GLuint prev_tex, GLuint curr_tex, float alpha,
|
||
int blend_mode, 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, prev_tex);
|
||
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, curr_tex);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST);
|
||
p_glUseProgram(s_interp_prog);
|
||
p_glUniform1i(s_interp_uPrev, 0);
|
||
p_glUniform1i(s_interp_uCurr, 1);
|
||
p_glUniform1f(s_interp_uAlpha, alpha);
|
||
p_glUniform1i(s_interp_uBlendMode, blend_mode);
|
||
present_set_gamma(s_interp_uGamma, 1);
|
||
p_glUniform4f(s_interp_uUvRect, 0.f, 0.f, 1.f, 1.f);
|
||
/* Interp textures hold exactly the display rect (uv_rect is 0..1), so pitch
|
||
* == display height == s_interp_src_h rows at the output scale (the
|
||
* texture itself can be smaller, see hiw_capture_size). */
|
||
INTERP_SCANLINE(s_interp_src_h, s_interp_src_h, lh);
|
||
p_glBindVertexArray(s_present_vao);
|
||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||
p_glBindVertexArray(0);
|
||
p_glUseProgram(0);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
}
|
||
|
||
static uint64_t s_present_ticks_accum_fwd(uint64_t add);
|
||
static int interp_present_pair(GLuint a, GLuint b, float t, int blend_mode) {
|
||
if (!s_ctx || !s_interp_enabled || interp_suspended_now() || s_interp_valid < 1)
|
||
return 0;
|
||
uint64_t present_t0 = SDL_GetPerformanceCounter();
|
||
int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||
int lx, ly, lw, lh;
|
||
if (s_interp_force_4_3)
|
||
letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh);
|
||
else
|
||
letterbox_rect(ww, wh, &lx, &ly, &lw, &lh);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
glViewport(0, 0, ww, wh);
|
||
if (lx != 0 || ly != 0 || lw != ww || lh != wh) {
|
||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||
glClear(GL_COLOR_BUFFER_BIT);
|
||
}
|
||
present_bezel(ww, wh, lx, ly, lw, lh);
|
||
interp_draw_textures(a, b, t, blend_mode, lx, ly, lw, lh);
|
||
pres_record(GL_PRES_INTERP, 0, 0, s_interp_w, s_interp_h,
|
||
lx, ly, lw, lh);
|
||
gl_swap_with_osd();
|
||
s_interp_swaps++;
|
||
(void)s_present_ticks_accum_fwd(SDL_GetPerformanceCounter() - present_t0);
|
||
return 1;
|
||
}
|
||
|
||
static int interp_present(float alpha) {
|
||
if (s_interp_hold) alpha = 1.0f;
|
||
return interp_present_pair(s_interp_tex[s_interp_prev],
|
||
s_interp_tex[s_interp_cur], alpha,
|
||
s_interp_blend_mode);
|
||
}
|
||
|
||
static void interp_wait_until(uint64_t deadline, uint64_t frequency) {
|
||
uint64_t now, start;
|
||
if (!deadline || !frequency) return;
|
||
start = SDL_GetPerformanceCounter();
|
||
for (;;) {
|
||
now = SDL_GetPerformanceCounter();
|
||
if (now >= deadline) {
|
||
/* Idle host time feeds the render-pass budget. */
|
||
(void)s_idle_ticks_accum_fwd(now - start);
|
||
return;
|
||
}
|
||
uint64_t remain = deadline - now;
|
||
uint32_t ms = (uint32_t)((remain * 1000u) / frequency);
|
||
if (ms > 1) psx_host_sleep_ms(ms - 1);
|
||
}
|
||
}
|
||
|
||
static void interp_present_source_interval(void) {
|
||
static uint64_t diag_start, diag_swaps, diag_captures;
|
||
uint64_t frequency = SDL_GetPerformanceFrequency();
|
||
uint64_t now = SDL_GetPerformanceCounter();
|
||
uint64_t deadline;
|
||
float alpha;
|
||
|
||
if (!frame_interpolation_schedule_begin_phase(
|
||
&s_interp_schedule, now, frequency,
|
||
s_interp_source_hz, s_interp_target_hz,
|
||
s_interp_phase_lo, s_interp_phase_hi))
|
||
return;
|
||
pass_apply_promotion();
|
||
|
||
while (frame_interpolation_schedule_next(
|
||
&s_interp_schedule, SDL_GetPerformanceCounter(),
|
||
&deadline, &alpha)) {
|
||
interp_wait_until(deadline, frequency);
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
if (!pass_gen_present(deadline))
|
||
(void)interp_present(alpha);
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
}
|
||
interp_wait_until(frame_interpolation_schedule_end(&s_interp_schedule),
|
||
frequency);
|
||
|
||
now = SDL_GetPerformanceCounter();
|
||
if (!diag_start) {
|
||
diag_start = now;
|
||
diag_swaps = s_interp_swaps;
|
||
diag_captures = s_interp_captures;
|
||
} else if (s_interp_diag && frequency &&
|
||
now - diag_start >= frequency * 5u) {
|
||
double seconds = (double)(now - diag_start) / (double)frequency;
|
||
fprintf(stdout, "psxrecomp: GL temporal-blend cadence: "
|
||
"%.2f captures/s, %.2f presents/s (single context)\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;
|
||
}
|
||
}
|
||
|
||
/* ==== Render passes (render_pass.c, docs/RENDER_PASSES.md) ==================
|
||
*
|
||
* VRAM transaction. A pass draws into the guest's own display rect, which
|
||
* still holds the finished stock image the next flip shows. Begin backs that
|
||
* rect up on the GPU (hr color + mask stencil, the raw 16-bit mirror, the
|
||
* native-wide band) and in the CPU VRAM array, plus the renderer's coherency
|
||
* bookkeeping; end captures the drawn rect into a pass slot and puts all of it
|
||
* back. While a pass is open, GPU writes outside the rect are refused and
|
||
* counted (the pass is then rolled back without an image).
|
||
*
|
||
* Presentation. A generation holds one game frame's images: slot 0 is the
|
||
* game's own image (captured when the generation opens, before the first
|
||
* pass draws), then one slot per pass phase. It opens at the plugin's pass
|
||
* point and becomes current when the FLIP source sees the display flip to its
|
||
* rect; its phases then map onto host time from that interval's start. At
|
||
* each output deadline the presenter shows the newest image at or before the
|
||
* deadline's phase, crossfading to the next one when passes were shed. Two
|
||
* generations exist: the one on screen and the one being built for the next
|
||
* flip. */
|
||
|
||
#define PASS_SLOTS (RENDER_PASS_MAX_PHASES + 1u)
|
||
typedef struct PassGen {
|
||
int valid;
|
||
int promoted;
|
||
int x, y, w, h; /* guest VRAM rect */
|
||
int tex_w, tex_h; /* slot size (hr pixels; wide band if wide) */
|
||
int source_path; /* GL_PRES_VRAM or GL_PRES_WIDE */
|
||
uint32_t n; /* images: [0] = the game's own */
|
||
uint32_t phase[PASS_SLOTS]; /* Q16, ascending */
|
||
uint32_t period; /* guest VBlanks the frame stays on screen */
|
||
int shown; /* built for a frame already on screen */
|
||
double t_start, t_len; /* host ticks, set on promotion */
|
||
} PassGen;
|
||
static int s_pass_flip_shown = 0;
|
||
static PassGen s_pgen[2];
|
||
/* Double-buffer complete pairs: the unpublished set may be overwritten while
|
||
* the last complete pair remains visible. No temporal phases or generation. */
|
||
typedef struct StereoPair {
|
||
GLuint tex[2], fbo[2];
|
||
int tw[2], th[2];
|
||
int x, y, w, h;
|
||
uint32_t mask;
|
||
uint64_t id, cycle;
|
||
int32_t view[2][3];
|
||
} StereoPair;
|
||
static StereoPair s_stereo_pair[2];
|
||
static int s_stereo_current, s_stereo_valid, s_stereo_mode;
|
||
static uint64_t s_stereo_presents;
|
||
static char s_stereo_dump_dir[400];
|
||
static int s_stereo_dump_left;
|
||
enum { STEREO_CAPTURE_MAX = 100 };
|
||
static GLRenderStereoCapture s_stereo_captures[STEREO_CAPTURE_MAX];
|
||
static uint32_t s_stereo_capture_count;
|
||
/* Slot textures are made as slots fill: [0, s_pgen_alloc_n) exist, all at
|
||
* s_pgen_alloc_w x h. A generation never fills past pass_slot_cap, so two
|
||
* generations stay inside its budget whatever the internal scale. */
|
||
static GLuint s_pgen_tex[2][PASS_SLOTS];
|
||
static uint32_t s_pgen_alloc_n[2];
|
||
static int s_pgen_alloc_w[2], s_pgen_alloc_h[2];
|
||
static int s_pgen_cur = 0;
|
||
static int s_pgen_promote = 0;
|
||
static uint64_t s_pgen_promotions = 0, s_pgen_presents = 0, s_pgen_blends = 0;
|
||
static uint64_t s_pgen_expired = 0, s_pgen_unmatched = 0, s_pgen_early = 0;
|
||
static uint64_t s_pgen_late = 0; /* presents past the frame's planned end */
|
||
|
||
static GLuint s_pb_hr_tex = 0, s_pb_hr_rb = 0, s_pb_hr_fbo = 0;
|
||
static int s_pb_hr_w = 0, s_pb_hr_h = 0;
|
||
static GLuint s_pb_raw_tex = 0, s_pb_raw_fbo = 0;
|
||
static int s_pb_raw_w = 0, s_pb_raw_h = 0;
|
||
static GLuint s_pb_wide_tex = 0, s_pb_wide_rb = 0, s_pb_wide_fbo = 0;
|
||
static int s_pb_wide_w = 0, s_pb_wide_h = 0;
|
||
static GLuint s_pb_wide_src = 0;
|
||
/* The backup above still equals VRAM (the last pass restored it): rect and
|
||
* scale it was taken at. A later pass of the same frame reuses it. */
|
||
static int s_pb_valid = 0;
|
||
static int s_pb_x = 0, s_pb_y = 0, s_pb_w = 0, s_pb_h = 0, s_pb_scale = 0;
|
||
static uint64_t s_pb_reused = 0;
|
||
static uint16_t *s_pb_cpu = NULL;
|
||
static size_t s_pb_cpu_cap = 0;
|
||
static DirtyRect s_pb_cpu_dirty, s_pb_pack_dirty, s_pb_up_rects[UP_RECTS_MAX];
|
||
static int s_pb_up_n = 0, s_pb_gpu_dirty = 0, s_pb_stencil_valid = 1;
|
||
static uint64_t s_pb_present_dirty[PRES_ROWS];
|
||
static int s_pb_force_present = 0, s_pb_last_path = -1;
|
||
static int s_pb_last_dx = 0, s_pb_last_dy = 0, s_pb_last_dw = 0, s_pb_last_dh = 0;
|
||
|
||
/* Per-pass host cost (render_pass_plan.h RenderPassCost), and the presented
|
||
* image size it was measured at. Another size (an aspect or internal-
|
||
* resolution change) starts it over: pass cost does not scale with pixels
|
||
* alone, and while it is unknown a plan asks for one pass. */
|
||
static RenderPassCost s_pass_cost;
|
||
static int s_pass_cost_w = 0, s_pass_cost_h = 0;
|
||
static uint64_t s_pass_cost_rewarms = 0; /* stale estimates re-measured */
|
||
static uint64_t s_pass_ticks_accum = 0, s_idle_ticks_accum = 0;
|
||
static uint64_t s_pass_ticks_last = 0, s_idle_ticks_last = 0;
|
||
static uint64_t s_present_ticks_accum = 0, s_present_ticks_last = 0;
|
||
static double s_present_cost_ema = 0.0; /* host ticks per present */
|
||
static uint32_t s_intervals_since_plan = 0;
|
||
static int s_pass_budget_pct = -1;
|
||
|
||
static int s_pass_verify = -1;
|
||
static uint8_t *s_pv_hr = NULL, *s_pv_raw = NULL;
|
||
static size_t s_pv_hr_cap = 0, s_pv_raw_cap = 0;
|
||
static int s_pv_ok = 1;
|
||
|
||
uint64_t gl_renderer_perf_ticks(void) { return SDL_GetPerformanceCounter(); }
|
||
uint64_t gl_renderer_perf_frequency(void) { return SDL_GetPerformanceFrequency(); }
|
||
|
||
static void pass_gens_invalidate(void) {
|
||
s_pgen[0].valid = s_pgen[1].valid = 0;
|
||
s_pgen[0].promoted = s_pgen[1].promoted = 0;
|
||
s_pgen_promote = 0;
|
||
}
|
||
|
||
static int s_pass_force_refuse = -1; /* -1: read PSX_RENDER_PASS_REFUSE */
|
||
static GLRenderPassBeginDiag s_pass_begin_diag;
|
||
|
||
void gl_renderer_pass_begin_diag(GLRenderPassBeginDiag *out) {
|
||
if (out) *out = s_pass_begin_diag;
|
||
}
|
||
|
||
static int pass_begin_refuse(const char *reason) {
|
||
s_pass_begin_diag.reason = reason;
|
||
return 0;
|
||
}
|
||
|
||
/* Keep pre-existing errors separate from this allocation's errors. */
|
||
static uint32_t pass_gl_errors(void) {
|
||
GLenum e;
|
||
uint32_t first = 0;
|
||
while ((e = glGetError()) != GL_NO_ERROR) {
|
||
if (!first) first = (uint32_t)e;
|
||
}
|
||
return first;
|
||
}
|
||
|
||
void gl_renderer_pass_force_refuse(int on) {
|
||
s_pass_force_refuse = on ? 1 : 0;
|
||
}
|
||
|
||
static void pass_refusal_init(void) {
|
||
if (s_pass_force_refuse < 0) {
|
||
const char *e = getenv("PSX_RENDER_PASS_REFUSE");
|
||
s_pass_force_refuse = (e && e[0] && e[0] != '0') ? 1 : 0;
|
||
if (s_pass_force_refuse)
|
||
fprintf(stderr, "psxrecomp: render passes refused by the backend "
|
||
"(PSX_RENDER_PASS_REFUSE, debug)\n");
|
||
}
|
||
}
|
||
|
||
uint32_t gl_renderer_pass_unavailable(void) {
|
||
pass_refusal_init();
|
||
if (!s_ctx || !s_raster_ok || !s_interp_enabled || s_interp_suspended ||
|
||
s_interp_source != 1 || !(s_interp_source_hz > 0.0))
|
||
return PSX_MOD_RENDER_PASS_NO_PRESENTER;
|
||
/* Dual raster (netplay CPU-authoritative VRAM) or a debug refusal. */
|
||
if (s_hd_native_authority || (s_cpu_auth_dual && !render_pass_netplay_enabled()) || s_pass_force_refuse)
|
||
return PSX_MOD_RENDER_PASS_BACKEND;
|
||
/* Windowed high-resolution mode: the presented surfaces are the window
|
||
* tiles at s_out_scale, which a pass (backing up s_hr_fbo at
|
||
* s_hr_scale) would not restore. */
|
||
if (s_hiw)
|
||
return PSX_MOD_RENDER_PASS_BACKEND;
|
||
/* The history restarts when the display mode changes (e.g. title ->
|
||
* race); passes wait for its first frame, a VBlank or two. */
|
||
if (s_interp_valid <= 0)
|
||
return PSX_MOD_RENDER_PASS_BUSY;
|
||
return PSX_MOD_RENDER_PASS_READY;
|
||
}
|
||
|
||
int gl_renderer_pass_ready(void) {
|
||
return gl_renderer_pass_unavailable() == PSX_MOD_RENDER_PASS_READY;
|
||
}
|
||
|
||
/* Slots per generation that fit a 256 MiB budget for both generations. */
|
||
static uint32_t pass_slot_cap(int tex_w, int tex_h) {
|
||
double bytes = (double)tex_w * (double)tex_h * 4.0 * 2.0;
|
||
uint32_t cap = bytes > 0.0 ? (uint32_t)((256.0 * 1024.0 * 1024.0) / bytes)
|
||
: PASS_SLOTS;
|
||
if (cap > PASS_SLOTS) cap = PASS_SLOTS;
|
||
return cap;
|
||
}
|
||
|
||
uint32_t gl_renderer_pass_plan(uint32_t period_vblanks,
|
||
uint32_t shown_after_vblanks,
|
||
uint32_t *alpha_q16, uint32_t max,
|
||
uint32_t *wanted) {
|
||
RenderPassPlanInput in;
|
||
double freq, sp;
|
||
uint32_t cap;
|
||
int live;
|
||
|
||
if (wanted) *wanted = 0;
|
||
/* One plan per game frame: close the previous frame's host-time books. */
|
||
s_idle_ticks_last = s_idle_ticks_accum;
|
||
s_pass_ticks_last = s_pass_ticks_accum;
|
||
s_present_ticks_last = s_present_ticks_accum;
|
||
s_idle_ticks_accum = s_pass_ticks_accum = s_present_ticks_accum = 0;
|
||
live = s_intervals_since_plan > 0; /* turbo/headless present nothing */
|
||
s_intervals_since_plan = 0;
|
||
if (!gl_renderer_pass_ready() || !live || !alpha_q16 || max == 0) return 0;
|
||
if (s_interp_schedule.target_period <= 0.0 ||
|
||
s_interp_schedule.source_deadline <= 0.0)
|
||
return 0;
|
||
if (s_pass_budget_pct < 0) {
|
||
const char *e = getenv("PSX_RENDER_PASS_BUDGET");
|
||
int v = e ? atoi(e) : 0;
|
||
s_pass_budget_pct = (v >= 5 && v <= 100) ? v : 80;
|
||
}
|
||
freq = (double)SDL_GetPerformanceFrequency();
|
||
sp = freq / s_interp_source_hz;
|
||
cap = pass_slot_cap(s_interp_w, s_interp_h);
|
||
if (cap < 2) return 0;
|
||
memset(&in, 0, sizeof in);
|
||
in.next_deadline = s_interp_schedule.next_present_deadline;
|
||
in.target_period = s_interp_schedule.target_period;
|
||
in.frame_start = s_interp_schedule.source_deadline +
|
||
(double)shown_after_vblanks * sp;
|
||
in.frame_length = (double)period_vblanks * sp;
|
||
in.pass_cost = (s_pass_cost_w == s_interp_w && s_pass_cost_h == s_interp_h)
|
||
? render_pass_cost_estimate(&s_pass_cost) : 0.0;
|
||
{
|
||
/* Presents beyond two per frame are traded for passes: a shed
|
||
* present is coalesced, a shed pass is a missing motion sample. */
|
||
double spare = (double)s_idle_ticks_last + (double)s_present_ticks_last -
|
||
2.0 * s_present_cost_ema;
|
||
if (spare < 0.0) spare = 0.0;
|
||
in.budget = render_pass_budget(spare, (double)s_pass_ticks_last,
|
||
in.frame_length,
|
||
(double)s_pass_budget_pct / 100.0);
|
||
}
|
||
in.max = max < cap - 1u ? max : cap - 1u;
|
||
{
|
||
uint32_t want = 0, n = render_pass_plan_phases(&in, alpha_q16, &want);
|
||
if (wanted) *wanted = want;
|
||
/* An estimate no pass has confirmed for a while is measured again
|
||
* (render_pass_cost_note_plan): its plans then ask for one pass. */
|
||
if (want && s_pass_cost_w == s_interp_w && s_pass_cost_h == s_interp_h &&
|
||
render_pass_cost_note_plan(&s_pass_cost)) {
|
||
s_pass_cost_rewarms++;
|
||
in.pass_cost = 0.0;
|
||
n = render_pass_plan_phases(&in, alpha_q16, NULL);
|
||
}
|
||
return n;
|
||
}
|
||
}
|
||
|
||
void gl_renderer_pass_note_cost(uint64_t ticks) {
|
||
if (s_pass_cost_w != s_interp_w || s_pass_cost_h != s_interp_h) {
|
||
memset(&s_pass_cost, 0, sizeof s_pass_cost);
|
||
s_pass_cost_w = s_interp_w;
|
||
s_pass_cost_h = s_interp_h;
|
||
}
|
||
/* A pass that made pass textures or framebuffers (first use, a size
|
||
* change) is not a cost sample: see render_pass_cost_add(). */
|
||
render_pass_cost_add(&s_pass_cost, (double)ticks,
|
||
s_pass_allocs != s_pass_allocs_begin);
|
||
s_pass_ticks_accum += ticks;
|
||
}
|
||
|
||
uint32_t gl_renderer_pass_leaks(void) { return s_pass_leaks; }
|
||
int gl_renderer_pass_verify_vram(void) { return s_pv_ok; }
|
||
|
||
|
||
static GLuint pass_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];
|
||
return 0;
|
||
}
|
||
|
||
static int pass_make_color_fbo(GLuint *tex, GLuint *rb, GLuint *fbo,
|
||
int *cur_w, int *cur_h, int w, int h,
|
||
GLenum internal, GLenum fmt, GLenum type) {
|
||
if (*fbo && *cur_w == w && *cur_h == h) return 1;
|
||
s_pass_begin_diag.gl_error_before = pass_gl_errors();
|
||
s_pass_begin_diag.gl_error = 0;
|
||
s_pass_begin_diag.fbo_status = 0;
|
||
if (*fbo) p_glDeleteFramebuffers(1, fbo);
|
||
if (*tex) glDeleteTextures(1, tex);
|
||
if (rb && *rb) p_glDeleteRenderbuffers(1, rb);
|
||
*fbo = 0; *tex = 0; if (rb) *rb = 0;
|
||
s_pass_allocs++;
|
||
*tex = make_tex(internal, w, h, fmt, type);
|
||
if (rb) {
|
||
p_glGenRenderbuffers(1, rb);
|
||
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, *rb);
|
||
p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, w, h);
|
||
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, 0);
|
||
}
|
||
int complete = make_fbo(fbo, *tex, rb ? *rb : 0);
|
||
s_pass_begin_diag.fbo_status = (uint32_t)s_last_fbo_status;
|
||
s_pass_begin_diag.gl_error = pass_gl_errors();
|
||
if (!complete) {
|
||
*cur_w = *cur_h = 0;
|
||
return 0;
|
||
}
|
||
*cur_w = w; *cur_h = h;
|
||
return 1;
|
||
}
|
||
|
||
static void pass_gen_release(int gi) {
|
||
if (s_ctx && s_pgen_alloc_n[gi])
|
||
glDeleteTextures((GLsizei)s_pgen_alloc_n[gi], s_pgen_tex[gi]);
|
||
memset(s_pgen_tex[gi], 0, sizeof s_pgen_tex[gi]);
|
||
s_pgen_alloc_n[gi] = 0;
|
||
s_pgen_alloc_w[gi] = s_pgen_alloc_h[gi] = 0;
|
||
}
|
||
|
||
/* Make sure slots [0, need) of generation gi exist at w x h. A size change
|
||
* frees the old set first (the slot cap depends on the size only). */
|
||
static int pass_gen_reserve(int gi, uint32_t need, int w, int h) {
|
||
if (need > PASS_SLOTS || need > pass_slot_cap(w, h)) {
|
||
s_pass_begin_diag.resource = "generation_slot_cap";
|
||
return 0;
|
||
}
|
||
if (s_pgen_alloc_w[gi] != w || s_pgen_alloc_h[gi] != h) {
|
||
pass_gen_release(gi);
|
||
s_pgen_alloc_w[gi] = w;
|
||
s_pgen_alloc_h[gi] = h;
|
||
}
|
||
while (s_pgen_alloc_n[gi] < need) {
|
||
GLuint *t = &s_pgen_tex[gi][s_pgen_alloc_n[gi]];
|
||
s_pass_begin_diag.gl_error_before = pass_gl_errors();
|
||
s_pass_begin_diag.resource = "generation_texture";
|
||
glGenTextures(1, t);
|
||
if (!*t) {
|
||
s_pass_begin_diag.gl_error = pass_gl_errors();
|
||
return 0;
|
||
}
|
||
glBindTexture(GL_TEXTURE_2D, *t);
|
||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA,
|
||
GL_UNSIGNED_BYTE, NULL);
|
||
s_pass_begin_diag.gl_error = pass_gl_errors();
|
||
s_pgen_alloc_n[gi]++;
|
||
s_pass_allocs++;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* Copy the rect as the presenter would see it into `tex`. Native-wide first
|
||
* refreshes the band's canonical centre, as the wide present does. */
|
||
static void pass_capture_into(GLuint tex, const PassGen *g) {
|
||
int S = s_hr_scale;
|
||
if (g->source_path == GL_PRES_WIDE) {
|
||
GLuint wf = pass_wide_fbo_for(g->x);
|
||
int native_w = g_wide_w - 2 * g_wide_off;
|
||
if (!wf) return;
|
||
if (wide_center_is_canonical() && native_w > 0) {
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, wf);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBlitFramebuffer(g->x * S, g->y * S, (g->x + native_w) * S,
|
||
(g->y + g->h) * S, g_wide_off * S, g->y * S,
|
||
(g_wide_off + native_w) * S, (g->y + g->h) * S,
|
||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||
}
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, wf);
|
||
glBindTexture(GL_TEXTURE_2D, tex);
|
||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, g->y * S,
|
||
g_wide_w * S, g->h * S);
|
||
} else {
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo);
|
||
glBindTexture(GL_TEXTURE_2D, tex);
|
||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, g->x * S, g->y * S,
|
||
g->w * S, g->h * S);
|
||
}
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
}
|
||
|
||
static void pass_blit(GLuint src, GLuint dst, int sx, int sy, int dx, int dy,
|
||
int w, int h, GLbitfield mask) {
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, dst);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBlitFramebuffer(sx, sy, sx + w, sy + h, dx, dy, dx + w, dy + h, mask,
|
||
GL_NEAREST);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||
}
|
||
|
||
static void pass_verify_read(uint8_t **hr, size_t *hr_cap, uint8_t **raw,
|
||
size_t *raw_cap) {
|
||
int S = s_hr_scale;
|
||
size_t hn = (size_t)s_pass_w * S * (size_t)s_pass_h * S * 4u;
|
||
size_t rn = (size_t)s_pass_w * (size_t)s_pass_h * 2u;
|
||
if (*hr_cap < hn) { free(*hr); *hr = (uint8_t *)malloc(hn); *hr_cap = *hr ? hn : 0; }
|
||
if (*raw_cap < rn) { free(*raw); *raw = (uint8_t *)malloc(rn); *raw_cap = *raw ? rn : 0; }
|
||
if (!*hr || !*raw) return;
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo);
|
||
glReadPixels(s_pass_x * S, s_pass_y * S, s_pass_w * S, s_pass_h * S,
|
||
GL_RGBA, GL_UNSIGNED_BYTE, *hr);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo);
|
||
glReadPixels(s_pass_x, s_pass_y, s_pass_w, s_pass_h, PSXGL_RED_INTEGER,
|
||
GL_UNSIGNED_SHORT, *raw);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
}
|
||
|
||
/* VRAM journal for out-of-rect writes during a pass (see pass_refuse_write). */
|
||
#define PASS_JOURNAL_MAX RENDER_PASS_JOURNAL_MAX
|
||
typedef struct PassJournal {
|
||
GLuint hr_tex, hr_rb, hr_fbo, raw_tex, raw_fbo;
|
||
int hr_w, hr_h, raw_w, raw_h;
|
||
} PassJournal;
|
||
static PassJournal s_pj[PASS_JOURNAL_MAX];
|
||
static uint64_t s_pj_total = 0;
|
||
|
||
static void pass_free_color_fbo(GLuint *tex, GLuint *rb, GLuint *fbo,
|
||
int *w, int *h) {
|
||
if (s_ctx) {
|
||
if (*fbo) p_glDeleteFramebuffers(1, fbo);
|
||
if (*tex) glDeleteTextures(1, tex);
|
||
if (rb && *rb) p_glDeleteRenderbuffers(1, rb);
|
||
}
|
||
*fbo = 0; *tex = 0;
|
||
if (rb) *rb = 0;
|
||
*w = *h = 0;
|
||
}
|
||
|
||
/* Context teardown (gl_renderer_shutdown): free the pass images, backups
|
||
* and journal, and forget their names so a new context makes fresh ones. */
|
||
static void pass_resources_release(void) {
|
||
psx_openxr_shutdown();
|
||
if(s_ctx) {
|
||
if(s_xr_color_prog && p_glDeleteProgram) p_glDeleteProgram(s_xr_color_prog);
|
||
if(s_xr_native_tex) glDeleteTextures(1,&s_xr_native_tex);
|
||
}
|
||
s_xr_color_prog=s_xr_native_tex=0;
|
||
stereo_resources_release();
|
||
pass_gen_release(0);
|
||
pass_gen_release(1);
|
||
pass_gens_invalidate();
|
||
pass_free_color_fbo(&s_pb_hr_tex, &s_pb_hr_rb, &s_pb_hr_fbo,
|
||
&s_pb_hr_w, &s_pb_hr_h);
|
||
pass_free_color_fbo(&s_pb_raw_tex, NULL, &s_pb_raw_fbo,
|
||
&s_pb_raw_w, &s_pb_raw_h);
|
||
pass_free_color_fbo(&s_pb_wide_tex, &s_pb_wide_rb, &s_pb_wide_fbo,
|
||
&s_pb_wide_w, &s_pb_wide_h);
|
||
for (int i = 0; i < PASS_JOURNAL_MAX; i++) {
|
||
PassJournal *e = &s_pj[i];
|
||
pass_free_color_fbo(&e->hr_tex, &e->hr_rb, &e->hr_fbo, &e->hr_w, &e->hr_h);
|
||
pass_free_color_fbo(&e->raw_tex, NULL, &e->raw_fbo, &e->raw_w, &e->raw_h);
|
||
}
|
||
render_pass_journal_free(&s_pj_cpu);
|
||
s_pb_valid = 0;
|
||
}
|
||
|
||
static int pass_journal_protect(int x, int y, int w, int h) {
|
||
int S = s_hr_scale, i = s_pj_cpu.n;
|
||
PassJournal *j;
|
||
if (i >= PASS_JOURNAL_MAX) return 0;
|
||
j = &s_pj[i];
|
||
if (!pass_make_color_fbo(&j->hr_tex, &j->hr_rb, &j->hr_fbo, &j->hr_w,
|
||
&j->hr_h, w * S, h * S, GL_RGBA8, GL_RGBA,
|
||
GL_UNSIGNED_BYTE) ||
|
||
!pass_make_color_fbo(&j->raw_tex, NULL, &j->raw_fbo, &j->raw_w,
|
||
&j->raw_h, w, h, PSXGL_R16UI, PSXGL_RED_INTEGER,
|
||
GL_UNSIGNED_SHORT))
|
||
return 0;
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
flush_cpu_upload();
|
||
if (render_pass_journal_add(&s_pj_cpu, s_vram, VRAM_W, x, y, w, h) != i)
|
||
return 0;
|
||
pass_blit(s_hr_fbo, j->hr_fbo, x * S, y * S, 0, 0, w * S, h * S,
|
||
GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||
pass_blit(s_raw_fbo, j->raw_fbo, x, y, 0, 0, w, h, GL_COLOR_BUFFER_BIT);
|
||
s_pj_total++;
|
||
return 1;
|
||
}
|
||
|
||
static void pass_journal_rollback(void) {
|
||
int S = s_hr_scale;
|
||
for (int i = s_pj_cpu.n - 1; i >= 0; i--) {
|
||
PassJournal *j = &s_pj[i];
|
||
int x = s_pj_cpu.x[i], y = s_pj_cpu.y[i];
|
||
int w = s_pj_cpu.w[i], h = s_pj_cpu.h[i];
|
||
pass_blit(j->hr_fbo, s_hr_fbo, 0, 0, x * S, y * S, w * S, h * S,
|
||
GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||
pass_blit(j->raw_fbo, s_raw_fbo, 0, 0, x, y, w, h, GL_COLOR_BUFFER_BIT);
|
||
}
|
||
render_pass_journal_rollback(&s_pj_cpu, s_vram, VRAM_W);
|
||
}
|
||
|
||
static int transaction_begin(int x, int y, int w, int h, int open_gen,
|
||
uint32_t period_vblanks, int reuse_backup, int stereo) {
|
||
int S = s_hr_scale, gi, wide, tw, th;
|
||
PassGen *g;
|
||
memset(&s_pass_begin_diag, 0, sizeof s_pass_begin_diag);
|
||
s_pass_begin_diag.status = stereo ? gl_renderer_stereo_unavailable()
|
||
: gl_renderer_pass_unavailable();
|
||
s_pass_begin_diag.active = s_pass_active;
|
||
s_pass_begin_diag.open_gen = open_gen;
|
||
s_pass_begin_diag.hr_scale = S;
|
||
s_pass_begin_diag.out_scale = s_out_scale;
|
||
s_pass_begin_diag.source_path = s_interp_source_path;
|
||
s_pass_begin_diag.capture_w = s_interp_w;
|
||
s_pass_begin_diag.capture_h = s_interp_h;
|
||
gi = 1 - s_pgen_cur;
|
||
g = &s_pgen[gi];
|
||
s_pass_begin_diag.generation = gi;
|
||
s_pass_begin_diag.valid = g->valid;
|
||
s_pass_begin_diag.promoted = g->promoted;
|
||
s_pass_begin_diag.generation_x = g->x;
|
||
s_pass_begin_diag.generation_y = g->y;
|
||
s_pass_begin_diag.generation_w = g->w;
|
||
s_pass_begin_diag.generation_h = g->h;
|
||
if (s_pass_begin_diag.status != PSX_MOD_RENDER_PASS_READY)
|
||
return pass_begin_refuse("gl_status");
|
||
if (s_pass_active) return pass_begin_refuse("gl_active");
|
||
if (x < 0 || y < 0 || w <= 0 || h <= 0 || x + w > VRAM_W || y + h > VRAM_H)
|
||
return pass_begin_refuse("rect_range");
|
||
s_pass_allocs_begin = s_pass_allocs;
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
flush_cpu_upload();
|
||
|
||
wide = s_interp_source_path == GL_PRES_WIDE && g_wide_w > 0 &&
|
||
pass_wide_fbo_for(x) != 0;
|
||
tw = (wide ? g_wide_w : w) * S;
|
||
th = h * S;
|
||
s_pass_begin_diag.wide = wide;
|
||
s_pass_begin_diag.requested_w = tw;
|
||
s_pass_begin_diag.requested_h = th;
|
||
gi = 1 - s_pgen_cur;
|
||
g = &s_pgen[gi];
|
||
if (stereo) {
|
||
StereoPair *pair = &s_stereo_pair[1 - s_stereo_current];
|
||
pair->x = x; pair->y = y; pair->w = w; pair->h = h;
|
||
} else if (open_gen) {
|
||
if (tw != s_interp_w || th != s_interp_h)
|
||
return pass_begin_refuse("capture_size"); /* not what is presented */
|
||
if (!pass_gen_reserve(gi, 1u, tw, th))
|
||
return pass_begin_refuse("generation_reserve");
|
||
memset(g, 0, sizeof *g);
|
||
g->x = x; g->y = y; g->w = w; g->h = h;
|
||
g->tex_w = tw; g->tex_h = th;
|
||
g->source_path = wide ? GL_PRES_WIDE : GL_PRES_VRAM;
|
||
g->period = period_vblanks ? period_vblanks : 1u;
|
||
g->shown = s_pass_flip_shown;
|
||
pass_capture_into(s_pgen_tex[gi][0], g); /* the game's own image */
|
||
g->phase[0] = 0;
|
||
g->n = 1;
|
||
g->valid = 1;
|
||
} else if (!g->valid || g->promoted || g->x != x || g->y != y ||
|
||
g->w != w || g->h != h) {
|
||
return pass_begin_refuse("generation_state");
|
||
}
|
||
|
||
/* The previous pass of this frame restored exactly this backup and no
|
||
* guest code ran since: VRAM and the coherency state already equal it. */
|
||
if (reuse_backup && s_pb_valid && s_pb_x == x && s_pb_y == y &&
|
||
s_pb_w == w && s_pb_h == h && s_pb_scale == S &&
|
||
s_pb_wide_src == (g_wide_w > 0 ? pass_wide_fbo_for(x) : 0)) {
|
||
s_pb_reused++;
|
||
goto backed_up;
|
||
}
|
||
s_pb_valid = 0;
|
||
/* Back up the rect: hr color + stencil, raw mirror, wide band, CPU rows. */
|
||
s_pass_begin_diag.resource = "backup_hr";
|
||
if (!pass_make_color_fbo(&s_pb_hr_tex, &s_pb_hr_rb, &s_pb_hr_fbo,
|
||
&s_pb_hr_w, &s_pb_hr_h, w * S, h * S,
|
||
GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE))
|
||
return pass_begin_refuse("backup_hr");
|
||
s_pass_begin_diag.resource = "backup_raw";
|
||
if (!pass_make_color_fbo(&s_pb_raw_tex, NULL, &s_pb_raw_fbo,
|
||
&s_pb_raw_w, &s_pb_raw_h, w, h, PSXGL_R16UI,
|
||
PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT))
|
||
return pass_begin_refuse("backup_raw");
|
||
pass_blit(s_hr_fbo, s_pb_hr_fbo, x * S, y * S, 0, 0, w * S, h * S,
|
||
GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||
pass_blit(s_raw_fbo, s_pb_raw_fbo, x, y, 0, 0, w, h, GL_COLOR_BUFFER_BIT);
|
||
s_pb_wide_src = g_wide_w > 0 ? pass_wide_fbo_for(x) : 0;
|
||
if (s_pb_wide_src) {
|
||
s_pass_begin_diag.resource = "backup_wide";
|
||
if (!pass_make_color_fbo(&s_pb_wide_tex, &s_pb_wide_rb, &s_pb_wide_fbo,
|
||
&s_pb_wide_w, &s_pb_wide_h, g_wide_w * S,
|
||
h * S, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE))
|
||
return pass_begin_refuse("backup_wide");
|
||
pass_blit(s_pb_wide_src, s_pb_wide_fbo, 0, y * S, 0, 0, g_wide_w * S,
|
||
h * S, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||
}
|
||
if (s_pb_cpu_cap < (size_t)w * (size_t)h) {
|
||
free(s_pb_cpu);
|
||
s_pb_cpu = (uint16_t *)malloc((size_t)w * (size_t)h * sizeof(uint16_t));
|
||
s_pb_cpu_cap = s_pb_cpu ? (size_t)w * (size_t)h : 0;
|
||
if (!s_pb_cpu) return pass_begin_refuse("backup_cpu");
|
||
}
|
||
for (int row = 0; row < h; row++)
|
||
memcpy(s_pb_cpu + (size_t)row * w, s_vram + (size_t)(y + row) * VRAM_W + x,
|
||
(size_t)w * sizeof(uint16_t));
|
||
s_pb_cpu_dirty = s_cpu_dirty;
|
||
s_pb_pack_dirty = s_pack_dirty;
|
||
memcpy(s_pb_up_rects, s_up_rects, sizeof s_pb_up_rects);
|
||
s_pb_up_n = s_up_nrects;
|
||
s_pb_gpu_dirty = s_gpu_dirty;
|
||
s_pb_stencil_valid = s_stencil_valid;
|
||
memcpy(s_pb_present_dirty, s_present_dirty, sizeof s_pb_present_dirty);
|
||
s_pb_force_present = s_force_present_remaining;
|
||
s_pb_last_path = s_last_present_path;
|
||
s_pb_last_dx = s_last_dx; s_pb_last_dy = s_last_dy;
|
||
s_pb_last_dw = s_last_dw; s_pb_last_dh = s_last_dh;
|
||
s_pb_x = x; s_pb_y = y; s_pb_w = w; s_pb_h = h; s_pb_scale = S;
|
||
|
||
backed_up:
|
||
s_pass_x = x; s_pass_y = y; s_pass_w = w; s_pass_h = h;
|
||
if (s_pass_verify < 0) {
|
||
const char *e = getenv("PSX_RENDER_PASS_VERIFY");
|
||
s_pass_verify = (e && e[0] && e[0] != '0') ? 1 : 0;
|
||
}
|
||
if (s_pass_verify) pass_verify_read(&s_pv_hr, &s_pv_hr_cap, &s_pv_raw, &s_pv_raw_cap);
|
||
s_pj_cpu.n = 0;
|
||
s_pass_active = 1;
|
||
return 1;
|
||
}
|
||
|
||
static void transaction_restore(void) {
|
||
int S = s_hr_scale, gi = 1 - s_pgen_cur;
|
||
(void)gi;
|
||
if (!s_pass_active) return;
|
||
/* Roll the journal, then the rect back. */
|
||
pass_journal_rollback();
|
||
pass_blit(s_pb_hr_fbo, s_hr_fbo, 0, 0, s_pass_x * S, s_pass_y * S,
|
||
s_pass_w * S, s_pass_h * S,
|
||
GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||
pass_blit(s_pb_raw_fbo, s_raw_fbo, 0, 0, s_pass_x, s_pass_y, s_pass_w,
|
||
s_pass_h, GL_COLOR_BUFFER_BIT);
|
||
if (s_pb_wide_src && s_pb_wide_src == pass_wide_fbo_for(s_pass_x))
|
||
pass_blit(s_pb_wide_fbo, s_pb_wide_src, 0, 0, 0, s_pass_y * S,
|
||
g_wide_w * S, s_pass_h * S,
|
||
GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||
for (int row = 0; row < s_pass_h; row++)
|
||
memcpy(s_vram + (size_t)(s_pass_y + row) * VRAM_W + s_pass_x,
|
||
s_pb_cpu + (size_t)row * s_pass_w,
|
||
(size_t)s_pass_w * sizeof(uint16_t));
|
||
s_cpu_dirty = s_pb_cpu_dirty;
|
||
s_pack_dirty = s_pb_pack_dirty;
|
||
memcpy(s_up_rects, s_pb_up_rects, sizeof s_up_rects);
|
||
s_up_nrects = s_pb_up_n;
|
||
s_gpu_dirty = s_pb_gpu_dirty;
|
||
s_stencil_valid = s_pb_stencil_valid;
|
||
memcpy(s_present_dirty, s_pb_present_dirty, sizeof s_present_dirty);
|
||
s_force_present_remaining = s_pb_force_present;
|
||
s_last_present_path = s_pb_last_path;
|
||
s_last_dx = s_pb_last_dx; s_last_dy = s_pb_last_dy;
|
||
s_last_dw = s_pb_last_dw; s_last_dh = s_pb_last_dh;
|
||
s_pass_active = 0;
|
||
s_pb_valid = 1;
|
||
|
||
if (s_pass_verify) {
|
||
static uint8_t *after_hr = NULL, *after_raw = NULL;
|
||
static size_t after_hr_cap = 0, after_raw_cap = 0;
|
||
size_t hn = (size_t)s_pass_w * S * (size_t)s_pass_h * S * 4u;
|
||
size_t rn = (size_t)s_pass_w * (size_t)s_pass_h * 2u;
|
||
pass_verify_read(&after_hr, &after_hr_cap, &after_raw, &after_raw_cap);
|
||
s_pv_ok = s_pv_hr && after_hr && s_pv_raw && after_raw &&
|
||
memcmp(s_pv_hr, after_hr, hn) == 0 &&
|
||
memcmp(s_pv_raw, after_raw, rn) == 0;
|
||
}
|
||
}
|
||
|
||
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
|
||
uint32_t period, int reuse) {
|
||
return transaction_begin(x, y, w, h, open_gen, period, reuse, 0);
|
||
}
|
||
int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse) {
|
||
return transaction_begin(x, y, w, h, 0, 0, reuse, 1);
|
||
}
|
||
void gl_renderer_pass_end(uint32_t alpha_q16, int keep) {
|
||
int gi = 1 - s_pgen_cur;
|
||
PassGen *g = &s_pgen[gi];
|
||
if (!s_pass_active) return;
|
||
flush_flat_batch(); flush_tex_batch(); flush_cpu_upload();
|
||
if (keep && alpha_q16 && g->valid && !g->promoted && g->n < PASS_SLOTS &&
|
||
g->n < pass_slot_cap(g->tex_w, g->tex_h) &&
|
||
pass_gen_reserve(gi, g->n + 1u, g->tex_w, g->tex_h)) {
|
||
uint32_t slot = g->n, at = g->n;
|
||
GLuint t;
|
||
pass_capture_into(s_pgen_tex[gi][slot], g);
|
||
while (at > 1 && g->phase[at - 1] > alpha_q16) at--;
|
||
t = s_pgen_tex[gi][slot];
|
||
for (uint32_t i = slot; i > at; i--) {
|
||
s_pgen_tex[gi][i] = s_pgen_tex[gi][i - 1];
|
||
g->phase[i] = g->phase[i - 1];
|
||
}
|
||
s_pgen_tex[gi][at] = t; g->phase[at] = alpha_q16; g->n++;
|
||
}
|
||
transaction_restore();
|
||
}
|
||
|
||
uint32_t gl_renderer_stereo_unavailable(void) {
|
||
pass_refusal_init();
|
||
if (!s_ctx || !s_raster_ok || !s_hr_fbo || gpu_display_is_depth24())
|
||
return PSX_MOD_RENDER_PASS_NO_PRESENTER;
|
||
if (s_cpu_auth_dual || s_hd_native_authority || s_hiw || g_wide_w > 0 || s_pass_force_refuse > 0)
|
||
return PSX_MOD_RENDER_PASS_BACKEND;
|
||
return PSX_MOD_RENDER_PASS_READY;
|
||
}
|
||
void gl_renderer_stereo_stage_reset(void) {
|
||
s_stereo_pair[1 - s_stereo_current].mask = 0;
|
||
}
|
||
static void stereo_invalidate(void) {
|
||
s_stereo_valid = 0;
|
||
s_stereo_pair[0].mask = s_stereo_pair[1].mask = 0;
|
||
}
|
||
void gl_renderer_stereo_reset(void) {
|
||
stereo_invalidate(); s_stereo_mode = 0; s_stereo_dump_left = 0;
|
||
s_stereo_capture_count = 0;
|
||
s_stereo_presents = 0;
|
||
}
|
||
int gl_renderer_stereo_set_presentation(uint32_t mode) {
|
||
if (mode > 1u) return 0;
|
||
s_stereo_mode = (int)mode;
|
||
return 1;
|
||
}
|
||
static void stereo_resources_release(void) {
|
||
for (int gi = 0; gi < 2; gi++) for (int eye = 0; eye < 2; eye++) {
|
||
StereoPair *p = &s_stereo_pair[gi];
|
||
pass_free_color_fbo(&p->tex[eye], NULL, &p->fbo[eye],
|
||
&p->tw[eye], &p->th[eye]);
|
||
}
|
||
stereo_invalidate();
|
||
}
|
||
int gl_renderer_stereo_end(uint32_t eye, int keep) {
|
||
StereoPair *p = &s_stereo_pair[1 - s_stereo_current];
|
||
PassGen g;
|
||
int ok = 0, tw = s_pass_w * s_hr_scale, th = s_pass_h * s_hr_scale;
|
||
if (!s_pass_active) return 0;
|
||
flush_flat_batch(); flush_tex_batch(); flush_cpu_upload();
|
||
if (keep && eye < 2u && (double)tw * th * 16.0 <= 256.0 * 1024.0 * 1024.0 &&
|
||
pass_make_color_fbo(&p->tex[eye], NULL, &p->fbo[eye],
|
||
&p->tw[eye], &p->th[eye], tw, th,
|
||
GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE)) {
|
||
memset(&g, 0, sizeof g);
|
||
g.x = s_pass_x; g.y = s_pass_y; g.w = s_pass_w; g.h = s_pass_h;
|
||
g.source_path = GL_PRES_VRAM;
|
||
(void)pass_gl_errors(); /* distinguish this copy from preceding errors */
|
||
pass_capture_into(p->tex[eye], &g);
|
||
ok = pass_gl_errors() == 0;
|
||
if (ok) p->mask |= 1u << eye;
|
||
}
|
||
transaction_restore();
|
||
return keep ? ok : 1;
|
||
}
|
||
void gl_renderer_stereo_diag(GLRenderStereoDiag *out) {
|
||
StereoPair *p = &s_stereo_pair[s_stereo_current];
|
||
if (!out) return;
|
||
memset(out, 0, sizeof *out);
|
||
out->valid = (uint32_t)s_stereo_valid; out->mode = (uint32_t)s_stereo_mode;
|
||
out->staged_mask = s_stereo_pair[1 - s_stereo_current].mask;
|
||
out->pair_id = p->id; out->guest_cycle = p->cycle;
|
||
out->width = (uint32_t)p->tw[0]; out->height = (uint32_t)p->th[0];
|
||
out->presents = s_stereo_presents;
|
||
}
|
||
void gl_renderer_stereo_dump_arm(const char *dir, int pairs) {
|
||
if (!dir || !*dir || pairs <= 0) { s_stereo_dump_left = 0; return; }
|
||
snprintf(s_stereo_dump_dir, sizeof s_stereo_dump_dir, "%s", dir);
|
||
s_stereo_dump_left = pairs > STEREO_CAPTURE_MAX ? STEREO_CAPTURE_MAX : pairs;
|
||
s_stereo_capture_count = 0;
|
||
}
|
||
uint32_t gl_renderer_stereo_capture_records(GLRenderStereoCapture *out, uint32_t capacity) {
|
||
uint32_t count = s_stereo_capture_count;
|
||
if (!out) return 0;
|
||
if (count > capacity) count = capacity;
|
||
memcpy(out, s_stereo_captures, count * sizeof *out);
|
||
return count;
|
||
}
|
||
static void stereo_dump(const StereoPair *p) {
|
||
int w = p->tw[0], h = p->th[0];
|
||
size_t n = (size_t)w * h * 3u;
|
||
uint8_t *rgb = (uint8_t *)malloc(n * 2u);
|
||
uint8_t *sbs = (uint8_t *)malloc(n * 2u);
|
||
char path[512]; FILE *f;
|
||
if (!rgb || !sbs) { free(rgb); free(sbs); return; }
|
||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||
for (int eye = 0; eye < 2; eye++) {
|
||
glBindTexture(GL_TEXTURE_2D, p->tex[eye]);
|
||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, rgb + eye * n);
|
||
snprintf(path, sizeof path, "%s/p%06llu_%s.png", s_stereo_dump_dir,
|
||
(unsigned long long)p->id, eye ? "right" : "left");
|
||
f = fopen(path, "wb");
|
||
if (f) { (void)png_write_rgb(f, rgb + eye * n, w, h); fclose(f); }
|
||
}
|
||
glBindTexture(GL_TEXTURE_2D, 0);
|
||
for (int row = 0; row < h; row++) for (int eye = 0; eye < 2; eye++)
|
||
memcpy(sbs + ((size_t)row * w * 2u + eye * w) * 3u,
|
||
rgb + eye * n + (size_t)row * w * 3u, (size_t)w * 3u);
|
||
snprintf(path, sizeof path, "%s/p%06llu_sbs.png", s_stereo_dump_dir,
|
||
(unsigned long long)p->id);
|
||
f = fopen(path, "wb");
|
||
if (f) { (void)png_write_rgb(f, sbs, w * 2u, h); fclose(f); }
|
||
if (s_stereo_capture_count < STEREO_CAPTURE_MAX) {
|
||
GLRenderStereoCapture *record = &s_stereo_captures[s_stereo_capture_count++];
|
||
record->pair_id = p->id; record->guest_cycle = p->cycle;
|
||
record->width = (uint32_t)w; record->height = (uint32_t)h;
|
||
memcpy(record->view_offset, p->view, sizeof record->view_offset);
|
||
}
|
||
free(rgb); free(sbs); s_stereo_dump_left--;
|
||
}
|
||
int gl_renderer_stereo_publish(uint64_t id, uint64_t cycle, const int32_t view[2][3]) {
|
||
StereoPair *p = &s_stereo_pair[1 - s_stereo_current];
|
||
if (p->mask != 3u || p->tw[0] != p->tw[1] || p->th[0] != p->th[1]) return 0;
|
||
p->id = id; p->cycle = cycle;
|
||
memcpy(p->view, view, sizeof p->view);
|
||
s_stereo_current = 1 - s_stereo_current; s_stereo_valid = 1;
|
||
if (s_stereo_dump_left > 0) stereo_dump(p);
|
||
return 1;
|
||
}
|
||
|
||
static uint64_t s_xr_begin_pair;
|
||
int psx_mod_openxr_enable(int enabled) {
|
||
PSXOpenXRStats s; psx_openxr_stats(&s);
|
||
if (s_pass_active || s.frame_open) return 0;
|
||
return psx_openxr_enable(enabled);
|
||
}
|
||
void psx_mod_openxr_recenter(void) { psx_openxr_recenter(); }
|
||
int psx_mod_openxr_quad(double distance,double width,double height) {
|
||
if(s_pass_active)return 0;
|
||
return psx_openxr_quad(distance,width,height);
|
||
}
|
||
int psx_mod_openxr_native_surface(double distance,double width,double units) {
|
||
PSXOpenXRStats stats;psx_openxr_stats(&stats);
|
||
if(s_pass_active || stats.frame_open || !stats.compiled ||
|
||
!isfinite(distance) || !isfinite(width) || !isfinite(units))return 0;
|
||
if(distance && (distance<.25 || distance>20 || width<=0 || width>10 || units<1 || units>65536))return 0;
|
||
s_native_surface_enabled=distance!=0;s_native_surface_pending=0;
|
||
s_native_surface_distance=distance;s_native_surface_width=width;s_native_surface_units=units;
|
||
return 1;
|
||
}
|
||
int psx_mod_openxr_begin(uint32_t width, uint32_t height, double units) {
|
||
if (s_native_surface_enabled || s_pass_active || gl_renderer_stereo_unavailable() != PSX_MOD_RENDER_PASS_READY ||
|
||
!width || !height || width > VRAM_W || height > VRAM_H) return 0;
|
||
s_xr_begin_pair = s_stereo_valid ? s_stereo_pair[s_stereo_current].id : 0;
|
||
return psx_openxr_begin((int)width, (int)height, units);
|
||
}
|
||
int psx_mod_openxr_view(uint32_t eye, PSXModRenderView *view) {
|
||
return psx_openxr_view(eye, view);
|
||
}
|
||
int psx_mod_openxr_input(PSXModOpenXRInput *input) {
|
||
if (s_pass_active) return 0;
|
||
return psx_openxr_input(input);
|
||
}
|
||
int psx_mod_openxr_hands(PSXModOpenXRHands *hands) {
|
||
return psx_openxr_hands(hands); /* Snapshot only, also safe during replay. */
|
||
}
|
||
/* The target stores display-encoded RGB for an sRGB swapchain, or explicitly
|
||
* decoded RGB for a linear-only runtime. Never apply hardware sRGB encoding
|
||
* to these outputs. All shader state is restored before returning to native. */
|
||
static int openxr_color_draw(GLuint source, int w, int h, int flip, float gamma, int linear) {
|
||
if (!s_xr_color_prog) s_xr_color_prog = build_program(PRESENT_VS, PSX_XR_COLOR_FS);
|
||
if (!s_xr_color_prog || !s_present_vao) return 0;
|
||
GLint program, vao, viewport[4], active, binding, min_filter, mag_filter;
|
||
GLboolean mask[4];
|
||
const GLenum caps[] = {GL_BLEND, GL_DEPTH_TEST, GL_STENCIL_TEST, GL_CULL_FACE};
|
||
GLboolean enabled[4];
|
||
glGetIntegerv(0x8B8D, &program);glGetIntegerv(0x85B5, &vao);
|
||
glGetIntegerv(GL_VIEWPORT, viewport);glGetIntegerv(0x84E0, &active);
|
||
glGetBooleanv(GL_COLOR_WRITEMASK, mask);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);glGetIntegerv(GL_TEXTURE_BINDING_2D, &binding);
|
||
glBindTexture(GL_TEXTURE_2D, source);
|
||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, &min_filter);
|
||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, &mag_filter);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||
for (int i=0;i<4;i++) {enabled[i]=glIsEnabled(caps[i]);glDisable(caps[i]);}
|
||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);glViewport(0,0,w,h);
|
||
p_glUseProgram(s_xr_color_prog);
|
||
p_glUniform1i(p_glGetUniformLocation(s_xr_color_prog,"u_tex"),0);
|
||
p_glUniform1i(p_glGetUniformLocation(s_xr_color_prog,"u_linear"),linear);
|
||
p_glUniform1f(p_glGetUniformLocation(s_xr_color_prog,"u_gamma"),gamma);
|
||
p_glUniform4f(p_glGetUniformLocation(s_xr_color_prog,"u_uv_rect"),0,flip?0:1,1,flip?1:0);
|
||
p_glBindVertexArray(s_present_vao);glDrawArrays(GL_TRIANGLES,0,3);
|
||
p_glBindVertexArray((GLuint)vao);p_glUseProgram((GLuint)program);
|
||
glViewport(viewport[0],viewport[1],viewport[2],viewport[3]);
|
||
glColorMask(mask[0],mask[1],mask[2],mask[3]);
|
||
for (int i=0;i<4;i++) if(enabled[i]) glEnable(caps[i]);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
|
||
glBindTexture(GL_TEXTURE_2D,(GLuint)binding);p_glActiveTexture((GLenum)active);
|
||
return 1;
|
||
}
|
||
static int openxr_copy_eye(uint32_t eye, uint32_t texture, int w, int h) {
|
||
StereoPair *p = &s_stereo_pair[s_stereo_current];
|
||
GLint read_fbo, draw_fbo; GLuint target = 0;
|
||
GLboolean scissor = glIsEnabled(GL_SCISSOR_TEST);
|
||
GLboolean srgb = glIsEnabled(0x8DB9); /* GL_FRAMEBUFFER_SRGB */
|
||
PSXOpenXRStats xr;psx_openxr_stats(&xr);
|
||
glGetIntegerv(0x8CAA, &read_fbo);
|
||
glGetIntegerv(0x8CA6, &draw_fbo);
|
||
p_glGenFramebuffers(1, &target);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, target);
|
||
p_glFramebufferTexture2D(PSXGL_DRAW_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0,GL_TEXTURE_2D, texture, 0);
|
||
int ok = p_glCheckFramebufferStatus(PSXGL_DRAW_FRAMEBUFFER) == PSXGL_FRAMEBUFFER_COMPLETE;
|
||
if (ok) {
|
||
glDisable(GL_SCISSOR_TEST);
|
||
glDisable(0x8DB9);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, p->fbo[eye]);
|
||
/* Stereo capture texture row convention is opposite the XR layer.
|
||
* User confirmed inverted headset output with the original direct blit.
|
||
* Flip only submission; eye dumps and desktop presentation stay intact. */
|
||
if (xr.swapchain_format == PSX_XR_SRGB8_ALPHA8 && s_present_gamma == 1.0f)
|
||
p_glBlitFramebuffer(0, p->th[eye], p->tw[eye], 0, 0, 0, w, h,
|
||
GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||
else
|
||
ok = openxr_color_draw(p->tex[eye], w, h, 1, s_present_gamma,
|
||
xr.swapchain_format != PSX_XR_SRGB8_ALPHA8);
|
||
glFlush();
|
||
}
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, (GLuint)read_fbo);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, (GLuint)draw_fbo);
|
||
if (scissor) glEnable(GL_SCISSOR_TEST);
|
||
if (srgb) glEnable(0x8DB9);
|
||
p_glDeleteFramebuffers(1, &target);return ok;
|
||
}
|
||
int psx_mod_openxr_end(int rendered) {
|
||
int fresh = rendered && s_stereo_valid && !s_pass_active &&
|
||
s_stereo_pair[s_stereo_current].id != s_xr_begin_pair;
|
||
if (fresh) psx_openxr_pair_metadata(s_stereo_pair[s_stereo_current].id,
|
||
s_stereo_pair[s_stereo_current].cycle);
|
||
return psx_openxr_end(fresh, openxr_copy_eye);
|
||
}
|
||
|
||
static int openxr_copy_native(uint32_t eye,uint32_t texture,int w,int h) {
|
||
GLint read_fbo,draw_fbo,read_buffer;GLuint target=0;
|
||
GLboolean scissor=glIsEnabled(GL_SCISSOR_TEST);
|
||
GLboolean srgb=glIsEnabled(0x8DB9);
|
||
PSXOpenXRStats xr;psx_openxr_stats(&xr);
|
||
(void)eye;
|
||
glGetIntegerv(0x8CAA,&read_fbo);glGetIntegerv(0x8CA6,&draw_fbo);
|
||
glGetIntegerv(GL_READ_BUFFER,&read_buffer);
|
||
p_glGenFramebuffers(1,&target);p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER,target);
|
||
p_glFramebufferTexture2D(PSXGL_DRAW_FRAMEBUFFER,PSXGL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,texture,0);
|
||
int ok=p_glCheckFramebufferStatus(PSXGL_DRAW_FRAMEBUFFER)==PSXGL_FRAMEBUFFER_COMPLETE;
|
||
if(ok) {
|
||
int *r=s_native_surface_rect;
|
||
glDisable(GL_SCISSOR_TEST);p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,0);glReadBuffer(GL_BACK);
|
||
glDisable(0x8DB9);
|
||
/* Default framebuffer is already upright: unlike native VRAM/eye
|
||
* textures, its bottom GL row is the displayed bottom row. */
|
||
(void)pass_gl_errors();
|
||
if (xr.swapchain_format == PSX_XR_SRGB8_ALPHA8)
|
||
p_glBlitFramebuffer(r[0],r[1],r[0]+r[2],r[1]+r[3],0,0,w,h,GL_COLOR_BUFFER_BIT,GL_LINEAR);
|
||
else {
|
||
GLint active,binding;glGetIntegerv(0x84E0,&active);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);glGetIntegerv(GL_TEXTURE_BINDING_2D,&binding);
|
||
if(!s_xr_native_tex) glGenTextures(1,&s_xr_native_tex);
|
||
glBindTexture(GL_TEXTURE_2D,s_xr_native_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);
|
||
glCopyTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,r[0],r[1],r[2],r[3],0);
|
||
glBindTexture(GL_TEXTURE_2D,(GLuint)binding);p_glActiveTexture((GLenum)active);
|
||
/* Desktop gamma is already baked into GL_BACK; decode it once. */
|
||
ok=openxr_color_draw(s_xr_native_tex,w,h,0,1.0f,1);
|
||
}
|
||
glFlush();ok=ok && pass_gl_errors()==0;
|
||
}
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,(GLuint)read_fbo);glReadBuffer((GLenum)read_buffer);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER,(GLuint)draw_fbo);
|
||
if(scissor)glEnable(GL_SCISSOR_TEST);
|
||
if(srgb)glEnable(0x8DB9);
|
||
p_glDeleteFramebuffers(1,&target);return ok;
|
||
}
|
||
static void openxr_present_native(void) {
|
||
int ww=0,wh=0,*r=s_native_surface_rect;
|
||
if(!s_native_surface_enabled || !s_native_surface_pending || s_pass_active)return;
|
||
s_native_surface_pending=0;
|
||
SDL_GL_GetDrawableSize(s_win,&ww,&wh);
|
||
if(r[0]<0 || r[1]<0 || r[2]<1 || r[3]<1 || r[0]+r[2]>ww || r[1]+r[3]>wh)return;
|
||
/* No replay or retained pair: this exact native present is the source.
|
||
* Direct lifecycle permits native 24-bit videos as well as 15-bit UI. */
|
||
/* The shared locator also builds PSX projection matrices, whose domain
|
||
* is native VRAM dimensions. Quad geometry does not use those matrices;
|
||
* keep that domain bounded while copying the full drawable rectangle. */
|
||
if(!psx_openxr_begin(512,240,s_native_surface_units))return;
|
||
if(!psx_openxr_quad(s_native_surface_distance,s_native_surface_width,
|
||
s_native_surface_width*(double)r[3]/r[2])) {
|
||
(void)psx_openxr_end(0,NULL);return;
|
||
}
|
||
psx_openxr_native_metadata(s_frame_count);
|
||
(void)psx_openxr_end(1,openxr_copy_native);
|
||
}
|
||
|
||
static int stereo_present(int w, int h) {
|
||
StereoPair *p = &s_stereo_pair[s_stereo_current];
|
||
int ww, wh, lx, ly, lw, lh;
|
||
/* A pair is shown only while the plugin keeps submitting; menus, FMV at the
|
||
* same size and state loads stop publishing, and the last pair must not
|
||
* stick on screen. Stale pairs fall back to the flat present below. */
|
||
if (s_stereo_valid &&
|
||
!render_pass_stereo_pair_fresh(p->cycle, psx_cycle_count, g_psx_vblank_cycles))
|
||
return 0;
|
||
if (!s_stereo_mode || !s_stereo_valid || s_pass_active ||
|
||
p->w != w || p->h != h || p->tw[0] != w * s_hr_scale ||
|
||
gl_renderer_stereo_unavailable() != PSX_MOD_RENDER_PASS_READY) return 0;
|
||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||
gl_perf_present_enter();
|
||
letterbox_rect_aspect(ww, wh, s_aspect_num * 2, s_aspect_den, &lx, &ly, &lw, &lh);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||
glDisable(GL_SCISSOR_TEST); glViewport(0, 0, ww, wh);
|
||
glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT);
|
||
for (int eye = 0; eye < 2; eye++) {
|
||
int start = eye ? lw / 2 : 0, width = eye ? lw - lw / 2 : lw / 2;
|
||
present_target_quad(p->tex[eye], p->tw[eye], p->th[eye],
|
||
0, 0, p->tw[eye], p->th[eye], 0,
|
||
lx + start, ly, width, lh, 1, 1, 1);
|
||
}
|
||
pres_record(GL_PRES_STEREO, p->x, p->y, w, h, lx, ly, lw, lh);
|
||
hold_capture_drawable();
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
gl_swap_with_osd();
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
s_stereo_presents++; s_probe_swap++;
|
||
gl_perf_present_exit(0);
|
||
present_dirty_rect(p->x, p->y, p->x + w - 1, p->y + h - 1, 0);
|
||
present_force_consumed();
|
||
s_last_present_path = GL_PRES_STEREO;
|
||
return 1;
|
||
}
|
||
|
||
/* Draw two presented images crossfaded (t = weight of b) into the window. */
|
||
static int interp_present_pair(GLuint a, GLuint b, float t, int blend_mode);
|
||
|
||
static int pass_gen_present(uint64_t deadline) {
|
||
PassGen *g = &s_pgen[s_pgen_cur];
|
||
uint32_t lo = 0, hi = 0;
|
||
float t = 0.0f;
|
||
double p;
|
||
if (!g->valid || !g->promoted || !(g->t_len > 0.0)) return 0;
|
||
if (g->source_path != s_interp_source_path || g->tex_w != s_interp_w ||
|
||
g->tex_h != s_interp_h)
|
||
return 0;
|
||
p = ((double)deadline - g->t_start) / g->t_len;
|
||
/* Past phase 1 the next flip is late (a lagging tick): the newest image
|
||
* holds until it comes. Only a game that stops flipping for several
|
||
* frame lengths expires the generation. */
|
||
if (!render_pass_gen_select(g->phase, g->n, p, &lo, &hi, &t)) {
|
||
g->valid = 0;
|
||
s_pgen_expired++;
|
||
return 0;
|
||
}
|
||
if (!interp_present_pair(s_pgen_tex[s_pgen_cur][lo],
|
||
s_pgen_tex[s_pgen_cur][hi], t, 0))
|
||
return 0;
|
||
s_pgen_presents++;
|
||
if (lo != hi) s_pgen_blends++;
|
||
/* A frame's own last deadline falls at p = 1 (the end of its planned
|
||
* VBlanks); one past it by more than 1/64 is in a VBlank it was not
|
||
* planned to cover. */
|
||
if (p > 1.0 + 1.0 / 64.0) s_pgen_late++;
|
||
return 1;
|
||
}
|
||
|
||
void gl_renderer_pass_service_presents(void) {
|
||
uint64_t deadline;
|
||
double sp;
|
||
if (!gl_renderer_pass_ready() || s_pass_active) return;
|
||
sp = (double)SDL_GetPerformanceFrequency() / s_interp_source_hz;
|
||
while (frame_interpolation_schedule_due(
|
||
&s_interp_schedule, SDL_GetPerformanceCounter(),
|
||
s_interp_schedule.frame_end + sp, &deadline)) {
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
if (!pass_gen_present(deadline)) { latency_ring_mark(LAT_SWAP_END); break; }
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
frame_interpolation_schedule_consume(&s_interp_schedule);
|
||
s_pgen_early++;
|
||
}
|
||
}
|
||
|
||
void gl_renderer_pass_diag(uint64_t out[10]) {
|
||
out[0] = s_pgen_promotions;
|
||
out[1] = s_pgen_presents;
|
||
out[2] = s_pgen_blends;
|
||
out[3] = s_pgen_expired;
|
||
out[4] = s_pgen_unmatched;
|
||
out[5] = s_pgen_early;
|
||
out[6] = (uint64_t)(render_pass_cost_estimate(&s_pass_cost) * 1e6 /
|
||
(double)SDL_GetPerformanceFrequency()); /* us */
|
||
out[7] = (uint64_t)s_pgen[s_pgen_cur].n;
|
||
out[8] = s_pgen_late;
|
||
out[9] = s_pass_cost_rewarms;
|
||
}
|
||
|
||
uint64_t gl_renderer_pass_journaled(void) { return s_pj_total; }
|
||
uint64_t gl_renderer_pass_backups_reused(void) { return s_pb_reused; }
|
||
|
||
uint32_t gl_renderer_pass_image_textures(uint64_t *bytes) {
|
||
uint32_t n = s_pgen_alloc_n[0] + s_pgen_alloc_n[1];
|
||
if (bytes) {
|
||
*bytes = 0;
|
||
for (int gi = 0; gi < 2; gi++)
|
||
*bytes += (uint64_t)s_pgen_alloc_n[gi] * (uint64_t)s_pgen_alloc_w[gi] *
|
||
(uint64_t)s_pgen_alloc_h[gi] * 4u;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
static uint64_t s_idle_ticks_accum_fwd(uint64_t add) {
|
||
s_idle_ticks_accum += add;
|
||
return s_idle_ticks_accum;
|
||
}
|
||
|
||
static uint64_t s_present_ticks_accum_fwd(uint64_t add) {
|
||
s_present_ticks_accum += add;
|
||
s_present_cost_ema = render_pass_ema(s_present_cost_ema, (double)add);
|
||
return s_present_ticks_accum;
|
||
}
|
||
|
||
void gl_renderer_pass_set_flip_shown(int shown) { s_pass_flip_shown = shown ? 1 : 0; }
|
||
|
||
/* FLIP source saw a new frame: it is the flip a pending generation was built
|
||
* for (render_pass_gen_flip_matches) or a frame without passes. */
|
||
static void pass_note_new_frame(int origin_x, int origin_y, int source_path,
|
||
int pw, int ph) {
|
||
PassGen *pend = &s_pgen[1 - s_pgen_cur];
|
||
if (pend->valid && !pend->promoted &&
|
||
render_pass_gen_flip_matches(pend->shown, pend->x, pend->y,
|
||
pend->source_path, pend->tex_w, pend->tex_h,
|
||
origin_x, origin_y, source_path, pw, ph)) {
|
||
s_pgen_promote = 1;
|
||
} else {
|
||
s_pgen_promote = 0;
|
||
if (s_pgen[s_pgen_cur].valid) s_pgen_unmatched++;
|
||
s_pgen[s_pgen_cur].valid = 0;
|
||
s_pgen[s_pgen_cur].promoted = 0;
|
||
}
|
||
}
|
||
|
||
/* render_pass_dump (debug server): write every image of the next promoted
|
||
* generations as PNGs, <dir>/g<gen>_<index>_a<phase q16>.png. */
|
||
static char s_pdump_dir[400];
|
||
static int s_pdump_left = 0;
|
||
static uint64_t s_pdump_gen = 0;
|
||
void gl_renderer_pass_dump_arm(const char *dir, int generations) {
|
||
if (!dir || !*dir || generations <= 0) { s_pdump_left = 0; return; }
|
||
snprintf(s_pdump_dir, sizeof s_pdump_dir, "%s", dir);
|
||
s_pdump_left = generations;
|
||
}
|
||
static void pass_dump_generation(int gi) {
|
||
PassGen *g = &s_pgen[gi];
|
||
size_t n = (size_t)g->tex_w * (size_t)g->tex_h * 3u;
|
||
uint8_t *rgb = (uint8_t *)malloc(n);
|
||
if (!rgb) return;
|
||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||
for (uint32_t i = 0; i < g->n; i++) {
|
||
char path[512];
|
||
FILE *f;
|
||
glBindTexture(GL_TEXTURE_2D, s_pgen_tex[gi][i]);
|
||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, rgb);
|
||
snprintf(path, sizeof path, "%s/g%03llu_%02u_a%05u.png", s_pdump_dir,
|
||
(unsigned long long)s_pdump_gen, (unsigned)i,
|
||
(unsigned)g->phase[i]);
|
||
f = fopen(path, "wb");
|
||
if (f) { (void)png_write_rgb(f, rgb, (uint32_t)g->tex_w, (uint32_t)g->tex_h); fclose(f); }
|
||
}
|
||
glBindTexture(GL_TEXTURE_2D, 0);
|
||
free(rgb);
|
||
s_pdump_gen++;
|
||
s_pdump_left--;
|
||
}
|
||
|
||
static void pass_apply_promotion(void) {
|
||
s_intervals_since_plan++;
|
||
if (!s_pgen_promote) return;
|
||
s_pgen_promote = 0;
|
||
s_pgen[s_pgen_cur].valid = 0;
|
||
s_pgen[s_pgen_cur].promoted = 0;
|
||
s_pgen_cur = 1 - s_pgen_cur;
|
||
{
|
||
PassGen *g = &s_pgen[s_pgen_cur];
|
||
double sp = s_interp_schedule.frame_end - s_interp_schedule.frame_start;
|
||
g->promoted = 1;
|
||
g->t_start = s_interp_schedule.frame_start;
|
||
g->t_len = (double)g->period * sp;
|
||
/* Phase 0 is the image the game actually flipped to. The capture at
|
||
* the pass point is the same for a game that draws nothing more into
|
||
* the rect before its flip; a game that finishes the frame later (V8:2
|
||
* draws its HUD at the next submit, onto the rect about to be shown)
|
||
* would otherwise show its own frame without that last layer. The
|
||
* flip was matched to this generation's rect and presented geometry,
|
||
* and no guest code ran since it, so the rect holds that image. A
|
||
* FLIP_SHOWN generation's rect is not the one flipped to: it keeps
|
||
* the image taken when it opened. */
|
||
if (!g->shown) {
|
||
pass_capture_into(s_pgen_tex[s_pgen_cur][0], g);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
}
|
||
s_pgen_promotions++;
|
||
if (s_pdump_left > 0) pass_dump_generation(s_pgen_cur);
|
||
}
|
||
}
|
||
|
||
/* Draw one host OSD ARGB image into the default framebuffer at (vx,vy)
|
||
* in top-left window coordinates (y down). Bitmap is ow×oh; viewport is
|
||
* dw×dh (may upscale for HiDPI / large windows). */
|
||
static void gl_draw_osd_image(const uint32_t *px, int ow, int oh,
|
||
int dw, int dh, int vx, int vy, int ww, int wh) {
|
||
if (!px || ow <= 0 || oh <= 0 || dw <= 0 || dh <= 0 ||
|
||
!s_present_prog || ww <= 0 || wh <= 0)
|
||
return;
|
||
if (!s_osd_tex) glGenTextures(1, &s_osd_tex);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
glBindTexture(GL_TEXTURE_2D, s_osd_tex);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||
if (s_osd_tw != ow || s_osd_th != oh) {
|
||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, ow, oh, 0,
|
||
GL_BGRA, GL_UNSIGNED_BYTE, px);
|
||
s_osd_tw = ow;
|
||
s_osd_th = oh;
|
||
} else {
|
||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, ow, oh,
|
||
GL_BGRA, GL_UNSIGNED_BYTE, px);
|
||
}
|
||
if (vx + dw > ww) dw = ww - vx;
|
||
if (vy + dh > wh) dh = wh - vy;
|
||
if (dw < 1 || dh < 1) return;
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
glDisable(GL_STENCIL_TEST);
|
||
glDisable(GL_DEPTH_TEST);
|
||
/* host_osd bakes opaque panels (A=0xFF). Do not blend — PSX mode-2
|
||
* REVERSE_SUBTRACT left armed across FMV present made toasts solid black. */
|
||
glDisable(GL_BLEND);
|
||
if (p_glBlendEquationSeparate)
|
||
p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD);
|
||
/* GL viewport origin is bottom-left. */
|
||
glViewport(vx, wh - vy - dh, dw, dh);
|
||
p_glUseProgram(s_present_prog);
|
||
p_glUniform1i(s_present_uTex, 0);
|
||
present_set_gamma(s_present_uGamma, 0);
|
||
present_set_sharp(0, 0, 0, 0, 0); /* OSD is authored at output res */
|
||
PRESENT_SCANLINE(0, 0, 0); /* never scanline the host OSD */
|
||
/* Host OSD bitmaps are top-down (row 0 = top), same as guest CPU
|
||
* present with v_flip=1: uv (0,0)-(1,1). (0,1)-(1,0) was the hold-last
|
||
* cancel for already-oriented captures and made toasts upside-down. */
|
||
p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f);
|
||
{
|
||
p_glBindVertexArray(s_present_vao);
|
||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||
p_glBindVertexArray(0);
|
||
}
|
||
p_glUseProgram(0);
|
||
}
|
||
|
||
/* Composite host toast + volume bar into the default framebuffer, then swap. */
|
||
/* ---- presented-image ring feed (debug tools) ------------------------------
|
||
* Every present, before the host OSD is drawn: blit the composed default
|
||
* framebuffer (what the window shows, letterbox included) down to a
|
||
* PRESENT_IMAGE_RING_H-tall thumbnail FBO, and read it back through a pair of
|
||
* pixel-pack buffers so the GPU->CPU copy of frame N completes while frame
|
||
* N+1 is presented (no pipeline stall). Tagged with the frame number at issue
|
||
* time. Absent buffer-mapping entry points disable only this feed. */
|
||
#ifndef PSX_NO_DEBUG_TOOLS
|
||
#include "present_image_ring.h"
|
||
extern uint64_t s_frame_count;
|
||
#define PSXGL_PIXEL_PACK_BUFFER 0x88EB
|
||
#define PSXGL_STREAM_READ 0x88E1
|
||
#define PSXGL_READ_ONLY 0x88B8
|
||
static GLuint s_pir_fbo, s_pir_tex, s_pir_pbo[2];
|
||
static int s_pir_w, s_pir_pending[2], s_pir_pending_w[2], s_pir_idx;
|
||
static uint32_t s_pir_frame[2];
|
||
static int s_pir_disabled;
|
||
|
||
static void present_image_ring_capture_gl(void) {
|
||
if (s_pir_disabled || !p_glMapBuffer || !p_glUnmapBuffer) return;
|
||
if (!present_image_ring_accepting()) return;
|
||
int ww = 0, wh = 0;
|
||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||
const int tw = present_image_ring_thumb_w(ww, wh);
|
||
const int th = PRESENT_IMAGE_RING_H;
|
||
if (tw <= 0) return;
|
||
if (!s_pir_fbo) {
|
||
glGenTextures(1, &s_pir_tex);
|
||
glBindTexture(GL_TEXTURE_2D, s_pir_tex);
|
||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, PRESENT_IMAGE_RING_MAX_W, th, 0,
|
||
GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||
glBindTexture(GL_TEXTURE_2D, 0);
|
||
p_glGenFramebuffers(1, &s_pir_fbo);
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_pir_fbo);
|
||
p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0,
|
||
GL_TEXTURE_2D, s_pir_tex, 0);
|
||
const GLenum st = p_glCheckFramebufferStatus(PSXGL_FRAMEBUFFER);
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||
if (st != PSXGL_FRAMEBUFFER_COMPLETE) { s_pir_disabled = 1; return; }
|
||
p_glGenBuffers(2, s_pir_pbo);
|
||
for (int i = 0; i < 2; i++) {
|
||
p_glBindBuffer(PSXGL_PIXEL_PACK_BUFFER, s_pir_pbo[i]);
|
||
p_glBufferData(PSXGL_PIXEL_PACK_BUFFER,
|
||
(ptrdiff_t)PRESENT_IMAGE_RING_MAX_W * th * 4, NULL,
|
||
PSXGL_STREAM_READ);
|
||
}
|
||
p_glBindBuffer(PSXGL_PIXEL_PACK_BUFFER, 0);
|
||
}
|
||
/* Downscale the presented image (default framebuffer back buffer). */
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_pir_fbo);
|
||
p_glBlitFramebuffer(0, 0, ww, wh, 0, 0, tw, th, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||
/* Queue this frame's readback into the current pack buffer. */
|
||
const int cur = s_pir_idx;
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_pir_fbo);
|
||
p_glBindBuffer(PSXGL_PIXEL_PACK_BUFFER, s_pir_pbo[cur]);
|
||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||
glReadPixels(0, 0, tw, th, GL_RGBA, GL_UNSIGNED_BYTE, (void *)0);
|
||
s_pir_pending[cur] = 1;
|
||
s_pir_pending_w[cur] = tw;
|
||
s_pir_frame[cur] = (uint32_t)s_frame_count;
|
||
/* Harvest the previous frame's buffer (its copy finished during this one). */
|
||
const int prev = cur ^ 1;
|
||
if (s_pir_pending[prev]) {
|
||
p_glBindBuffer(PSXGL_PIXEL_PACK_BUFFER, s_pir_pbo[prev]);
|
||
const uint8_t *px = (const uint8_t *)p_glMapBuffer(PSXGL_PIXEL_PACK_BUFFER,
|
||
PSXGL_READ_ONLY);
|
||
if (px) {
|
||
present_image_ring_push(s_pir_frame[prev], px, s_pir_pending_w[prev],
|
||
s_pir_pending_w[prev] * 4, 1 /* GL rows bottom-up */);
|
||
p_glUnmapBuffer(PSXGL_PIXEL_PACK_BUFFER);
|
||
}
|
||
s_pir_pending[prev] = 0;
|
||
}
|
||
p_glBindBuffer(PSXGL_PIXEL_PACK_BUFFER, 0);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||
s_pir_idx = prev;
|
||
s_pir_w = tw;
|
||
}
|
||
#endif
|
||
|
||
static void gl_swap_with_osd(void) {
|
||
openxr_present_native(); /* Copy guest content before host-only overlays. */
|
||
s_native_surface_pending=0; /* Hold-last/resim cannot reuse a native source. */
|
||
#ifndef PSX_NO_DEBUG_TOOLS
|
||
if (s_ctx) present_image_ring_capture_gl();
|
||
#endif
|
||
if (s_present_prog && s_ctx) {
|
||
int ww = 0, wh = 0;
|
||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||
if (ww > 0 && wh > 0) {
|
||
const uint32_t *px = NULL;
|
||
int ow = 0, oh = 0;
|
||
/* OSD authored for ~480-tall present; grow with drawable height. */
|
||
int ui = wh / 480;
|
||
int margin;
|
||
if (ui < 1) ui = 1;
|
||
if (ui > 8) ui = 8;
|
||
margin = 8 * ui;
|
||
if (host_osd_image(&px, &ow, &oh) && px)
|
||
gl_draw_osd_image(px, ow, oh, ow * ui, oh * ui,
|
||
margin, margin, ww, wh);
|
||
if (host_osd_volume_image(&px, &ow, &oh) && px) {
|
||
const int dw = ow * ui, dh = oh * ui;
|
||
int vx = (ww > dw + margin) ? (ww - dw - margin) : margin;
|
||
int vy = (wh > dh) ? ((wh - dh) / 2) : margin;
|
||
gl_draw_osd_image(px, ow, oh, dw, dh, vx, vy, ww, wh);
|
||
}
|
||
if (psx_rewind_overlay_image(&px, &ow, &oh) && px) {
|
||
float slide = psx_rewind_slide();
|
||
int dw = ww;
|
||
int dh = (wh * oh) / 480;
|
||
int vy;
|
||
if (dh < 8) dh = oh;
|
||
vy = wh - (int)((float)dh * slide + 0.5f);
|
||
gl_draw_osd_image(px, ow, oh, dw, dh, 0, vy, ww, wh);
|
||
}
|
||
if (psx_savestate_menu_overlay_image(&px, &ow, &oh) && px)
|
||
gl_draw_osd_image(px, ow, oh, ww, wh, 0, 0, ww, wh);
|
||
}
|
||
}
|
||
host_osd_present_done();
|
||
/* present_shot (GL backend): the default framebuffer now holds the composed
|
||
* frame — display quad fitted to the window, plus OSD — so this is the only
|
||
* capture that carries the presented aspect. Buffer-level captures resolve
|
||
* before the fit and answer the raw display size instead. Read back before
|
||
* the swap; GL rows come out bottom-up, so flip into the PNG. */
|
||
{
|
||
extern int present_shot_take(char *out, int n);
|
||
extern void present_shot_done(int ok);
|
||
char shot_path[512];
|
||
if (present_shot_take(shot_path, (int)sizeof(shot_path))) {
|
||
int ww = 0, wh = 0;
|
||
int wrote = 0;
|
||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||
if (ww > 0 && wh > 0) {
|
||
uint8_t *rows = (uint8_t *)malloc((size_t)ww * wh * 3);
|
||
uint8_t *flip = rows ? (uint8_t *)malloc((size_t)ww * wh * 3) : NULL;
|
||
if (rows && flip) {
|
||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||
glReadPixels(0, 0, ww, wh, GL_RGB, GL_UNSIGNED_BYTE, rows);
|
||
for (int y = 0; y < wh; y++)
|
||
memcpy(flip + (size_t)y * ww * 3,
|
||
rows + (size_t)(wh - 1 - y) * ww * 3,
|
||
(size_t)ww * 3);
|
||
FILE *pf = fopen(shot_path, "wb");
|
||
if (pf) {
|
||
wrote = png_write_rgb(pf, flip, (uint32_t)ww, (uint32_t)wh);
|
||
fclose(pf);
|
||
}
|
||
}
|
||
/* free() tolerates NULL, so both exits are covered. */
|
||
free(flip);
|
||
free(rows);
|
||
}
|
||
present_shot_done(wrote);
|
||
}
|
||
}
|
||
SDL_GL_SwapWindow(s_win);
|
||
}
|
||
|
||
/* Output pixels per source texel below which a supersampled present switches
|
||
* from one tap to the area resolve (1 / 1.25: the source is 25% larger than the
|
||
* output). Only used for S > 1 sources, so native presents never change. */
|
||
#define PRESENT_AREA_MIN_RATIO 1.25f
|
||
|
||
/* tex_w/tex_h and x/y/w/h are in the texture's NATIVE units; src_scale is how
|
||
* many texels one native unit spans (the internal scale for the hr FBO and the
|
||
* wide surfaces, 1 for textures that hold native or already-composed pixels).
|
||
* src_scale <= 1 is exactly the historical behaviour. */
|
||
static void present_target_quad(GLuint tex, int tex_w, int tex_h,
|
||
int x, int y, int w, int h, int linear,
|
||
int lx, int ly, int lw, int lh, int v_flip,
|
||
int apply_gamma, int src_scale) {
|
||
float u0, v0, u1, v1;
|
||
int area = 0;
|
||
if (src_scale > 1 && lw > 0 && lh > 0 && w > 0 && h > 0) {
|
||
float rx = (float)(w * src_scale) / (float)lw;
|
||
float ry = (float)(h * src_scale) / (float)lh;
|
||
area = (rx > PRESENT_AREA_MIN_RATIO || ry > PRESENT_AREA_MIN_RATIO);
|
||
}
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||
glViewport(lx, ly, lw, lh);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
glBindTexture(GL_TEXTURE_2D, tex);
|
||
/* The area resolve averages bilinear taps; it needs LINEAR whatever the
|
||
* AA setting (the source is already the supersampled image). */
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, (linear || area) ? GL_LINEAR : GL_NEAREST);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, (linear || area) ? GL_LINEAR : GL_NEAREST);
|
||
p_glUseProgram(s_present_prog);
|
||
p_glUniform1i(s_present_uTex, 0);
|
||
present_set_gamma(s_present_uGamma, apply_gamma);
|
||
/* The rasterized path already renders at the internal scale, so it has no
|
||
* low-res source to reconstruct — keep the plain sample, unless the
|
||
* internal image is larger than the output: then resolve its area. */
|
||
if (area && s_present_uSharp >= 0) {
|
||
p_glUniform1i(s_present_uSharp, 3);
|
||
if (s_present_uTexSize >= 0)
|
||
p_glUniform2f(s_present_uTexSize, (float)(tex_w * src_scale),
|
||
(float)(tex_h * src_scale));
|
||
if (s_present_uSharpScale >= 0) /* output px per source texel */
|
||
p_glUniform2f(s_present_uSharpScale,
|
||
(float)lw / (float)(w * src_scale),
|
||
(float)lh / (float)(h * src_scale));
|
||
} else {
|
||
present_set_sharp(0, 0, 0, 0, 0);
|
||
}
|
||
/* Scanline at the native line grid. v_uv is normalized against tex_h (the
|
||
* full VRAM/FBO texture), and one texel row is one PS1 scanline, so tex_h is
|
||
* the phase pitch; h is the displayed line count for the output-scale gate.
|
||
* v_flip=0 is the already-composed hold-last drawable (scanlines, if any,
|
||
* are already baked) — skip it. */
|
||
PRESENT_SCANLINE(v_flip ? tex_h : 0, v_flip ? h : 0, lh);
|
||
/* Half-texel inset: with GL_LINEAR, corner-mapped UVs make the outermost
|
||
* dest pixels blend the border texel with VRAM outside the content rect
|
||
* (visible edge stripe with AA on). Center-mapped UVs keep edge samples
|
||
* inside the rect; interior sampling is unchanged. */
|
||
/* The inset is half a TEXEL. For a supersampled source that is 0.5/S of a
|
||
* native unit; the old 0.5 native units cropped (S-1)/2 texels per edge
|
||
* (about 4 px at 9x) and zoomed the image. src_scale <= 1: unchanged. */
|
||
{
|
||
float in = src_scale > 1 ? 0.5f / (float)src_scale : 0.5f;
|
||
u0 = ((float)x + in) / (float)tex_w;
|
||
v0 = ((float)y + in) / (float)tex_h;
|
||
u1 = ((float)(x + w) - in) / (float)tex_w;
|
||
v1 = ((float)(y + h) - in) / (float)tex_h;
|
||
}
|
||
/* PRESENT_VS always samples with mix(v0,v1,1-p.y). For CPU/FBO guest
|
||
* bands that is the correct PSX top-down → GL mapping (v_flip=1). For a
|
||
* glCopyTexSubImage2D of the already-presented drawable, the texture
|
||
* already matches screen orientation — swapping v ends cancels the
|
||
* shader flip so hold-last is not upside-down for one frame. */
|
||
if (!v_flip) {
|
||
float t = v0;
|
||
v0 = v1;
|
||
v1 = t;
|
||
}
|
||
p_glUniform4f(s_present_uUvRect, u0, v0, u1, v1);
|
||
p_glBindVertexArray(s_present_vao);
|
||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||
p_glBindVertexArray(0);
|
||
p_glUseProgram(0);
|
||
}
|
||
|
||
/* Bezel art: a still image behind the frame, filling whatever the letterbox or
|
||
* pillarbox leaves over. It never samples the frame, so unlike an edge-stretch
|
||
* backdrop it is independent of what the game is currently drawing. */
|
||
static GLuint s_bezel_tex = 0;
|
||
|
||
int gl_renderer_set_bezel(const void *rgba, int w, int h) {
|
||
if (s_bezel_tex) { glDeleteTextures(1, &s_bezel_tex); s_bezel_tex = 0; }
|
||
if (!rgba || w <= 0 || h <= 0) return 1;
|
||
if (!s_ctx || !s_raster_ok) return 0;
|
||
glGenTextures(1, &s_bezel_tex);
|
||
if (!s_bezel_tex) return 0;
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
glBindTexture(GL_TEXTURE_2D, s_bezel_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);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA,
|
||
GL_UNSIGNED_BYTE, rgba);
|
||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||
return 1;
|
||
}
|
||
|
||
int gl_renderer_has_bezel(void) { return s_bezel_tex != 0; }
|
||
|
||
/* Cover the drawable with the bezel before the game quad. */
|
||
static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh) {
|
||
if (!s_bezel_tex || ww <= 0 || wh <= 0) return;
|
||
if (lx <= 0 && ly <= 0 && lw >= ww && lh >= wh) return;
|
||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
glDisable(GL_BLEND);
|
||
glViewport(0, 0, ww, wh);
|
||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||
glBindTexture(GL_TEXTURE_2D, s_bezel_tex);
|
||
p_glUseProgram(s_present_prog);
|
||
p_glUniform1i(s_present_uTex, 0);
|
||
present_set_gamma(s_present_uGamma, 0);
|
||
p_glUniform4f(s_present_uUvRect, 0.0f, 0.0f, 1.0f, 1.0f);
|
||
PRESENT_SCANLINE(0, 0, 0); /* bezel art is not scanlined */
|
||
p_glBindVertexArray(s_present_vao);
|
||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||
p_glBindVertexArray(0);
|
||
p_glUseProgram(0);
|
||
}
|
||
|
||
int gl_renderer_present_hold_last(void) {
|
||
int ww = 0, wh = 0;
|
||
int lx, ly, lw, lh;
|
||
if (!s_ctx || !s_win || s_hold_kind == HOLD_NONE || !s_hold_tex)
|
||
return 0;
|
||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||
if (ww < 1 || wh < 1)
|
||
return 0;
|
||
glDisable(GL_SCISSOR_TEST);
|
||
glViewport(0, 0, ww, wh);
|
||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||
glClear(GL_COLOR_BUFFER_BIT);
|
||
if (s_hold_kind == HOLD_DRAWABLE) {
|
||
/* Captured image already includes letterbox bars. Stretch 1:1, or
|
||
* letterbox to preserve aspect if the drawable resized mid-resim. */
|
||
lx = 0;
|
||
ly = 0;
|
||
lw = ww;
|
||
lh = wh;
|
||
if (s_hold_tw > 0 && s_hold_th > 0 &&
|
||
((int64_t)s_hold_tw * wh != (int64_t)s_hold_th * ww)) {
|
||
if ((int64_t)ww * s_hold_th > (int64_t)wh * s_hold_tw) {
|
||
lw = (int)(((int64_t)wh * s_hold_tw) / s_hold_th);
|
||
if (lw < 1) lw = 1;
|
||
lx = (ww - lw) / 2;
|
||
lh = wh;
|
||
ly = 0;
|
||
} else {
|
||
lh = (int)(((int64_t)ww * s_hold_th) / s_hold_tw);
|
||
if (lh < 1) lh = 1;
|
||
ly = (wh - lh) / 2;
|
||
lw = ww;
|
||
lx = 0;
|
||
}
|
||
}
|
||
/* v_flip=0: drawable capture is already screen-oriented. */
|
||
present_target_quad(s_hold_tex, s_hold_tw, s_hold_th,
|
||
0, 0, s_hold_tw, s_hold_th, 0, lx, ly, lw, lh, 0, 0, 1);
|
||
} else {
|
||
if (s_hold_force_4_3)
|
||
letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh);
|
||
else
|
||
letterbox_rect(ww, wh, &lx, &ly, &lw, &lh);
|
||
present_target_quad(s_hold_tex, s_hold_tw, s_hold_th,
|
||
0, 0, s_hold_tw, s_hold_th, s_hold_linear,
|
||
lx, ly, lw, lh, 1, 1, 1);
|
||
/* Upgrade to drawable after letterboxing once. Live+interp leaves
|
||
* HOLD_NATIVE (no Swap on main); drawable path is the soak-proven
|
||
* full-window image (GL without interp). */
|
||
hold_capture_drawable();
|
||
}
|
||
gpu_timeline_note(GTL_PRESENT, GTL_PATH_HOLD, 0);
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
gl_swap_with_osd();
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
s_probe_swap++;
|
||
return 1;
|
||
}
|
||
|
||
void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
|
||
int force_4_3) {
|
||
if (!s_ctx || !s_raster_ok) return;
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
flush_cpu_upload();
|
||
if (stereo_present(w, h)) return;
|
||
if (!s_native_surface_enabled && s_force_present_remaining <= 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 &&
|
||
!present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1) &&
|
||
!host_osd_needs_present() &&
|
||
!psx_present_vsync_owns_cadence() &&
|
||
!gl_renderer_interpolation_owns_cadence()) {
|
||
s_probe_skip++;
|
||
gpu_timeline_note(GTL_PRESENT_SKIP, GTL_PATH_VRAM_FBO,
|
||
(uint32_t)disp_x | ((uint32_t)disp_y << 16));
|
||
gl_perf_present_enter();
|
||
gl_perf_present_exit(0);
|
||
return;
|
||
}
|
||
gl_perf_present_enter(); /* per-frame backdrop-phase reset + dbg snapshot live in here */
|
||
int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||
int lx, ly, lw, lh;
|
||
if (force_4_3)
|
||
letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh);
|
||
else
|
||
letterbox_rect(ww, wh, &lx, &ly, &lw, &lh);
|
||
/* No short-band adjustment on the 15-bit FBO path: a game's native short
|
||
* display mode (e.g. 216/224-line menus) must fill the rect as before.
|
||
* The FMV band fix lives in the depth24/CPU present paths only. */
|
||
|
||
/* Present source: the hr surface, or in windowed high-resolution mode the
|
||
* window holding this display (grown to it on first sight). A display the
|
||
* window cannot hold presents from the 1x surface without temporal
|
||
* blending (whose captures assume the S-scaled source). */
|
||
GLuint src_fbo = s_hr_fbo, src_tex = s_hr_tex, interp_fbo = s_hr_fbo;
|
||
int src_tw = VRAM_W, src_x = disp_x, src_scale = s_out_scale;
|
||
if (s_hiw) {
|
||
const HiwTile *T = hiw_ensure(disp_x, disp_x + w);
|
||
s_hiw_last_x0 = disp_x; s_hiw_last_x1 = disp_x + w;
|
||
if (T) {
|
||
src_fbo = interp_fbo = T->fbo; src_tex = T->tex;
|
||
src_tw = T->x1 - T->x0; src_x = disp_x - T->x0;
|
||
} else {
|
||
src_scale = s_hr_scale;
|
||
interp_fbo = 0;
|
||
}
|
||
}
|
||
|
||
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(
|
||
interp_fbo, src_x, disp_y, w, h, linear, force_4_3, GL_PRES_VRAM,
|
||
disp_x, disp_y,
|
||
present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1));
|
||
if (interp_pair) {
|
||
/* Temporal blending owns this stock frame interval. Capture hold-last
|
||
* before pacing; every blend and Swap remains on this context/thread. */
|
||
hold_capture_native_fbo(src_fbo, src_x, disp_y, w, h, force_4_3, linear);
|
||
interp_present_source_interval();
|
||
gl_perf_present_exit(0);
|
||
present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0);
|
||
present_force_consumed();
|
||
s_last_present_path = GL_PRES_VRAM;
|
||
s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = w; s_last_dh = h;
|
||
return;
|
||
}
|
||
present_bezel(ww, wh, lx, ly, lw, lh);
|
||
present_target_quad(src_tex, src_tw, VRAM_H,
|
||
src_x, disp_y, w, h, linear, lx, ly, lw, lh, 1, 1, src_scale);
|
||
pres_record(GL_PRES_VRAM, disp_x, disp_y, w, h, lx, ly, lw, lh);
|
||
gpu_timeline_note(GTL_PRESENT, GTL_PATH_VRAM_FBO,
|
||
(uint32_t)disp_x | ((uint32_t)disp_y << 16));
|
||
hold_capture_drawable();
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
gl_swap_with_osd();
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
s_probe_swap++;
|
||
gl_perf_present_exit(0);
|
||
present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0);
|
||
present_force_consumed();
|
||
s_last_present_path = GL_PRES_VRAM;
|
||
s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = w; s_last_dh = h;
|
||
coh_record(GL_COH_PRESENT, disp_x, disp_y, disp_x + w - 1, disp_y + h - 1);
|
||
}
|
||
|
||
/* Native-wide fast path: authoritatively copy the canonical 4:3 framebuffer
|
||
* into the wide surface's CENTRE columns [g_wide_off, g_wide_off+native_w) for
|
||
* the displayed Y band, so the per-prim mirror could skip every centre-only
|
||
* prim (the dominant native-wide GPU saving). The reveal margins were already
|
||
* produced by the mirror; this leaves them untouched. One FBO->FBO blit,
|
||
* x-translated by the reveal offset. No-op when s_wide_fast is off (then the
|
||
* mirror drew the full surface, as before). Shared by both present paths. */
|
||
static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h) {
|
||
if (!wide_center_is_canonical() || g_wide_w <= 0) return;
|
||
int native_w = g_wide_w - 2 * g_wide_off;
|
||
if (native_w <= 0) return;
|
||
int S = s_out_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. */
|
||
if (s_hiw) {
|
||
/* Windowed high-resolution mode: the centre comes from a tile at S
|
||
* (grown to it on first sight). Columns no tile holds are the 1x
|
||
* surface, upscaled; tiles overwrite the columns they hold. */
|
||
int cx0 = base_x, cx1 = base_x + native_w;
|
||
const HiwTile *in = hiw_ensure(cx0, cx1);
|
||
int R = s_hr_scale;
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, wide_fbo);
|
||
if (!in) {
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo);
|
||
p_glBlitFramebuffer(cx0 * R, 0, cx1 * R, VRAM_H * R,
|
||
g_wide_off * S, 0, (g_wide_off + native_w) * S, VRAM_H * S,
|
||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||
}
|
||
for (int t = 0; t < s_hiw_n; t++) {
|
||
const HiwTile *T = in ? in : &s_hiw_t[t];
|
||
int h0 = cx0 > T->x0 ? cx0 : T->x0, h1 = cx1 < T->x1 ? cx1 : T->x1;
|
||
if (h1 > h0) {
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, T->fbo);
|
||
p_glBlitFramebuffer((h0 - T->x0) * S, 0, (h1 - T->x0) * S, VRAM_H * S,
|
||
(g_wide_off + h0 - cx0) * S, 0,
|
||
(g_wide_off + h1 - cx0) * S, VRAM_H * S,
|
||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||
}
|
||
if (in) break;
|
||
}
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||
return;
|
||
}
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, wide_fbo);
|
||
glDisable(GL_SCISSOR_TEST);
|
||
p_glBlitFramebuffer(base_x * S, 0,
|
||
(base_x + native_w) * S, VRAM_H * S,
|
||
g_wide_off * S, 0,
|
||
(g_wide_off + native_w) * S, VRAM_H * S,
|
||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||
}
|
||
|
||
/* GPU-direct native-wide present: blit the displayed buffer's wide FBO straight
|
||
* to the window (no glReadPixels / glFinish CPU round-trip). The wide surface is
|
||
* g_wide_w wide × VRAM_H tall (at scale S); present its [0,g_wide_w] × [disp_y,
|
||
* disp_y+disp_h] region into the letterbox, V-flipped like present_vram (FBO y
|
||
* bottom-origin → window top). Returns 0 if there's no wide surface for base_x
|
||
* (caller falls back). disp_x is the displayed buffer base (the wide-surface key). */
|
||
int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear) {
|
||
if (!s_ctx || !s_raster_ok || g_wide_w <= 0) return 0;
|
||
GLuint fbo = 0, tex = 0;
|
||
for (int i = 0; i < WIDE_MAX_SURF; i++)
|
||
if (s_wide_fbo[i] && s_wide_base[i] == disp_x) {
|
||
fbo = s_wide_fbo[i]; tex = s_wide_tex[i]; break;
|
||
}
|
||
if (!fbo) return 0;
|
||
flush_flat_batch();
|
||
flush_tex_batch();
|
||
flush_cpu_upload();
|
||
hiw_flush_queue(); /* windowed: queued wide mirrors */
|
||
if (!s_native_surface_enabled && s_force_present_remaining <= 0 &&
|
||
s_last_present_path == GL_PRES_WIDE &&
|
||
s_last_dx == disp_x && s_last_dy == disp_y &&
|
||
s_last_dw == g_wide_w && s_last_dh == disp_h &&
|
||
!present_dirty_test(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1) &&
|
||
!host_osd_needs_present() &&
|
||
!psx_present_vsync_owns_cadence() &&
|
||
!gl_renderer_interpolation_owns_cadence()) {
|
||
s_probe_skip++;
|
||
gpu_timeline_note(GTL_PRESENT_SKIP, GTL_PATH_WIDE_FBO,
|
||
(uint32_t)disp_x | ((uint32_t)disp_y << 16));
|
||
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,
|
||
disp_x, disp_y,
|
||
present_dirty_test(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1));
|
||
if (interp_pair) {
|
||
hold_capture_native_fbo(fbo, 0, disp_y, g_wide_w, disp_h, 0, linear);
|
||
interp_present_source_interval();
|
||
gl_perf_present_exit(1);
|
||
present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0);
|
||
present_force_consumed();
|
||
s_last_present_path = GL_PRES_WIDE;
|
||
s_last_dx = disp_x; s_last_dy = disp_y;
|
||
s_last_dw = g_wide_w; s_last_dh = disp_h;
|
||
return 1;
|
||
}
|
||
present_target_quad(tex, g_wide_w, VRAM_H,
|
||
0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh, 1, 1, s_out_scale);
|
||
pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh);
|
||
gpu_timeline_note(GTL_PRESENT, GTL_PATH_WIDE_FBO,
|
||
(uint32_t)disp_x | ((uint32_t)disp_y << 16));
|
||
hold_capture_drawable();
|
||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||
gl_swap_with_osd();
|
||
latency_ring_mark(LAT_SWAP_END);
|
||
s_probe_swap++;
|
||
gl_perf_present_exit(1);
|
||
present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0);
|
||
present_force_consumed();
|
||
s_last_present_path = GL_PRES_WIDE;
|
||
s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = g_wide_w; s_last_dh = disp_h;
|
||
coh_record(GL_COH_PRESENT, 0, disp_y, g_wide_w - 1, disp_y + disp_h - 1);
|
||
return 1;
|
||
}
|
||
|
||
static const GpuRenderBackend GL_BACKEND = {
|
||
.name = "opengl",
|
||
.init = glb_init, .set_scale = glb_set_scale, .scale = glb_scale,
|
||
.set_texture_filter = glb_set_texture_filter, .texture_filter = glb_texture_filter,
|
||
.set_semi_transparency = glb_set_semi_transparency, .set_mask_bits = glb_set_mask_bits,
|
||
.set_texture_window = glb_set_texture_window, .set_color_modulation = glb_set_color_modulation,
|
||
.set_precise_triangle = glb_set_precise_triangle,
|
||
.set_perspective_triangle = glb_set_perspective_triangle,
|
||
.fill_rect = glb_fill_rect, .copy_rect = glb_copy_rect,
|
||
.draw_flat_triangle = glb_draw_flat_triangle, .draw_gouraud_triangle = glb_draw_gouraud_triangle,
|
||
.draw_textured_triangle = glb_draw_textured_triangle,
|
||
.draw_shaded_textured_triangle = glb_draw_shaded_textured_triangle,
|
||
.draw_flat_rect = glb_draw_flat_rect, .draw_textured_rect = glb_draw_textured_rect,
|
||
.draw_textured_rect_scaled = glb_draw_textured_rect_scaled,
|
||
.draw_line = glb_draw_line, .draw_shaded_line = glb_draw_shaded_line,
|
||
.render_display = glb_render_display, .render_display_hires = glb_render_display_hires,
|
||
.vram_write = glb_vram_write, .vram_read = glb_vram_read,
|
||
.vram_transfer_in = glb_vram_transfer_in, .vram_transfer_out = glb_vram_transfer_out,
|
||
.set_draw_area = glb_set_draw_area, .get_draw_area = glb_get_draw_area,
|
||
.set_draw_offset = glb_set_draw_offset,
|
||
.wide_configure = glb_wide_configure,
|
||
.wide_set_view = glb_wide_set_view,
|
||
.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; }
|