mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 10:38:46 -04:00
0631538407
Full rewrite of the GL backend around a single authoritative hr VRAM FBO (RGBA8 + stencil mask-bit mirror, internal scale from [video] supersampling): GPU fills/copies/draws rasterize into it, CPU uploads batch through a pending-rect quad blit, a PACK pass re-encodes dirty regions into the native R16UI raw mirror for texture sampling and readback, and gpu.c routes GPUREAD/mask-check/peek reads through the facade so the CPU array syncs before any CPU-side read. Texture sampling adopts the Beetle-PSX model (structure only, studied from beetle-psx-libretro): - Per-primitive uv sampling bounds, clamped in the fragment shader when no texture window is active. Filtered neighbours and S>1 interpolation overshoot can never read outside the prim's own texture rect. Bounds back off the exclusive max edge for axis-aligned (2D) uv mappings and widen to the full page when the uv range crosses a 256 wrap boundary. - Bilinear takes the NEAREST texel as base (cutout + STP authority), neighbours toward the sub-texel offset, with per-texel opacity weights and renormalised colour. The old "-0.5 then floor" base sampled one texel OUTSIDE the prim on top/left edges; where that neighbour was empty VRAM (e.g. tiled menu backgrounds with u0=0) the whole edge column/row discarded — the vertical line on Tomba's title and the cross seams on the memory-card screens. The software renderer's bilinear gets the same reformulation so GL and SW render filtered output identically; the SW nearest path (shipping default) is untouched. Also: always-on coherency event ring (flushes, fills, copies, draw bboxes, packs, readbacks, presents, probe perturbations, with rects + frame numbers) queryable via the debug server, and gl_vram_diff now forces a full pack so it reads FBO truth even where the raw-mirror invariant is broken. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1468 lines
71 KiB
C
1468 lines
71 KiB
C
/* gpu_gl_renderer.c — hardware OpenGL renderer backend.
|
|
*
|
|
* ARCHITECTURE (v2 — GPU-authoritative VRAM)
|
|
* -------------------------------------------
|
|
* The FBO color texture (`s_hr_tex`, RGBA8, 1024*S x 512*S where S is the
|
|
* internal-resolution scale from [video] supersampling) is the single
|
|
* authoritative copy of VRAM. EVERY mutation goes through the GPU:
|
|
*
|
|
* - polys / rects / lines -> rasterized into the hr FBO
|
|
* - GP0(02h) fills -> scissored glClear (color + stencil)
|
|
* - VRAM->VRAM copies -> hr FBO -> scratch texture blit, then a
|
|
* masked quad draw back into the hr FBO
|
|
* - CPU->VRAM transfers -> written to the CPU mirror immediately and
|
|
* accumulated into a pending-upload rect; the
|
|
* rect is flushed (staging texture + quad into
|
|
* the hr FBO, plus a direct R16UI subimage)
|
|
* before the next GPU op, preserving op order
|
|
*
|
|
* PS1 MASK BIT (bit15). The FBO alpha channel carries bit15 exactly
|
|
* (1.0 = set), and a stencil buffer mirrors it (stencil bit0 == bit15):
|
|
* - "set mask" -> fragment alpha + stencil write value
|
|
* - "check mask"-> stencil test (pass iff stored == 0). The stencil write
|
|
* value is coupled to the test reference in GL, so when checking we use
|
|
* GL_INVERT on pass to write a 1 (stored is known 0 when the test
|
|
* passes) and GL_KEEP to write a 0.
|
|
* - textured prims (and copies/uploads, whose pixels carry per-texel STP
|
|
* bits) are drawn in TWO passes split by the STP bit via discard, so
|
|
* each pass writes a single known stencil value. The same split already
|
|
* existed for semi-transparent texture blending.
|
|
* - blending uses glBlendFuncSeparate so the alpha (mask) channel is
|
|
* always REPLACED by the source fragment's mask bit, never blended.
|
|
*
|
|
* TEXTURE SAMPLING / RENDER-TO-TEXTURE. Textured prims sample a native-res
|
|
* R16UI mirror (`s_raw_tex`) holding raw 1555 VRAM values (CLUT decode +
|
|
* texture window + optional bilinear in the fragment shader). GPU draws
|
|
* mark a native-coords dirty union; before any textured draw whose texture
|
|
* page or CLUT intersects the union, a PACK pass re-encodes the dirty
|
|
* region of the hr FBO into the raw mirror (point-sampled at native
|
|
* coords). CPU->VRAM uploads update the raw mirror directly. So content
|
|
* rendered by the GPU is immediately valid as a texture source.
|
|
*
|
|
* CPU READBACKS (VRAM->CPU transfers, GPUREAD, screenshots, 24-bit FMV
|
|
* display) flush uploads + pack, then glReadPixels the raw mirror straight
|
|
* into the CPU VRAM array (raw 1555, no conversion loop).
|
|
*
|
|
* PRESENT is deterministic: 15-bit frames always blit the display region
|
|
* from the hr FBO into a 4:3 letterboxed rect (single path — no more
|
|
* frame-to-frame alternation between FBO and CPU presents). 24-bit (FMV)
|
|
* frames sync to CPU and use the quad-present path, also letterboxed.
|
|
* PSX_GL_FORCE_CPU_PRESENT=1 (read by main.cpp) forces the CPU path as a
|
|
* diagnostic.
|
|
*
|
|
* Known divergences from the software rasterizer (accepted, documented):
|
|
* - GL triangle/line coverage rules differ from the PS1 DDA by ±1px on
|
|
* edges; lines use GL_LINES (width S) instead of Bresenham.
|
|
* - No dithering (the software path doesn't dither either).
|
|
* - Gouraud interpolation happens at 8-bit precision instead of 5-bit
|
|
* (smoother gradients; readback re-quantizes to 5-bit).
|
|
* - VRAM-wrapping draws are clamped, except GP0(02h) fills which split
|
|
* into wrapped segments. Wrapping copies/draws are unused by real SDKs.
|
|
*
|
|
* Init is all-or-nothing: if any shader/FBO fails, gl_renderer_init_context
|
|
* returns 0 and the runtime falls back to the pure software renderer — no
|
|
* half-GL hybrid. */
|
|
|
|
#include "gpu_render.h"
|
|
#include "gpu_sw_renderer.h"
|
|
#include "gpu_gl_renderer.h"
|
|
|
|
#include <SDL.h>
|
|
#include <SDL_opengl.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifndef GL_BGRA
|
|
#define GL_BGRA 0x80E1
|
|
#endif
|
|
#ifndef GL_CLAMP_TO_EDGE
|
|
#define GL_CLAMP_TO_EDGE 0x812F
|
|
#endif
|
|
#ifndef GL_RGBA8
|
|
#define GL_RGBA8 0x8058
|
|
#endif
|
|
/* GL 1.4+ enums absent from MinGW's GL 1.1 headers. */
|
|
#define PSXGL_FRAGMENT_SHADER 0x8B30
|
|
#define PSXGL_VERTEX_SHADER 0x8B31
|
|
#define PSXGL_COMPILE_STATUS 0x8B81
|
|
#define PSXGL_LINK_STATUS 0x8B82
|
|
#define PSXGL_TEXTURE0 0x84C0
|
|
#define PSXGL_ARRAY_BUFFER 0x8892
|
|
#define PSXGL_STREAM_DRAW 0x88E0
|
|
#define PSXGL_FRAMEBUFFER 0x8D40
|
|
#define PSXGL_READ_FRAMEBUFFER 0x8CA8
|
|
#define PSXGL_DRAW_FRAMEBUFFER 0x8CA9
|
|
#define PSXGL_COLOR_ATTACHMENT0 0x8CE0
|
|
#define PSXGL_DEPTH_STENCIL_ATTACHMENT 0x821A
|
|
#define PSXGL_FRAMEBUFFER_COMPLETE 0x8CD5
|
|
#define PSXGL_RENDERBUFFER 0x8D41
|
|
#define PSXGL_DEPTH24_STENCIL8 0x88F0
|
|
#define PSXGL_R16UI 0x8234
|
|
#define PSXGL_RED_INTEGER 0x8D94
|
|
#define PSXGL_FUNC_ADD 0x8006
|
|
#define PSXGL_FUNC_REVERSE_SUBTRACT 0x800B
|
|
#define PSXGL_CONSTANT_ALPHA 0x8003
|
|
#define PSXGL_UNPACK_ROW_LENGTH 0x0CF2
|
|
|
|
#ifndef APIENTRY
|
|
#define APIENTRY
|
|
#endif
|
|
|
|
#define VRAM_W 1024
|
|
#define VRAM_H 512
|
|
#define GL_MAX_INTERNAL_SCALE 4
|
|
|
|
/* ---- Loaded modern-GL entry points ------------------------------------- */
|
|
typedef GLuint (APIENTRY *PFN_glCreateShader)(GLenum);
|
|
typedef void (APIENTRY *PFN_glShaderSource)(GLuint, GLsizei, const char *const *, const GLint *);
|
|
typedef void (APIENTRY *PFN_glCompileShader)(GLuint);
|
|
typedef void (APIENTRY *PFN_glGetShaderiv)(GLuint, GLenum, GLint *);
|
|
typedef void (APIENTRY *PFN_glGetShaderInfoLog)(GLuint, GLsizei, GLsizei *, char *);
|
|
typedef void (APIENTRY *PFN_glDeleteShader)(GLuint);
|
|
typedef GLuint (APIENTRY *PFN_glCreateProgram)(void);
|
|
typedef void (APIENTRY *PFN_glAttachShader)(GLuint, GLuint);
|
|
typedef void (APIENTRY *PFN_glLinkProgram)(GLuint);
|
|
typedef void (APIENTRY *PFN_glGetProgramiv)(GLuint, GLenum, GLint *);
|
|
typedef void (APIENTRY *PFN_glGetProgramInfoLog)(GLuint, GLsizei, GLsizei *, char *);
|
|
typedef void (APIENTRY *PFN_glUseProgram)(GLuint);
|
|
typedef GLint (APIENTRY *PFN_glGetUniformLocation)(GLuint, const char *);
|
|
typedef void (APIENTRY *PFN_glUniform1i)(GLint, GLint);
|
|
typedef void (APIENTRY *PFN_glUniform1f)(GLint, GLfloat);
|
|
typedef void (APIENTRY *PFN_glUniform2i)(GLint, GLint, GLint);
|
|
typedef void (APIENTRY *PFN_glUniform4i)(GLint, GLint, GLint, GLint, GLint);
|
|
typedef void (APIENTRY *PFN_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_glGenFramebuffers)(GLsizei, 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_glBindRenderbuffer)(GLenum, GLuint);
|
|
typedef void (APIENTRY *PFN_glRenderbufferStorage)(GLenum, GLenum, GLsizei, GLsizei);
|
|
typedef void (APIENTRY *PFN_glFramebufferRenderbuffer)(GLenum, GLenum, GLenum, GLuint);
|
|
|
|
static PFN_glCreateShader p_glCreateShader;
|
|
static PFN_glShaderSource p_glShaderSource;
|
|
static PFN_glCompileShader p_glCompileShader;
|
|
static PFN_glGetShaderiv p_glGetShaderiv;
|
|
static PFN_glGetShaderInfoLog p_glGetShaderInfoLog;
|
|
static PFN_glDeleteShader p_glDeleteShader;
|
|
static PFN_glCreateProgram p_glCreateProgram;
|
|
static PFN_glAttachShader p_glAttachShader;
|
|
static PFN_glLinkProgram p_glLinkProgram;
|
|
static PFN_glGetProgramiv p_glGetProgramiv;
|
|
static PFN_glGetProgramInfoLog p_glGetProgramInfoLog;
|
|
static PFN_glUseProgram p_glUseProgram;
|
|
static PFN_glGetUniformLocation p_glGetUniformLocation;
|
|
static PFN_glUniform1i p_glUniform1i;
|
|
static PFN_glUniform1f p_glUniform1f;
|
|
static PFN_glUniform2i p_glUniform2i;
|
|
static PFN_glUniform4i p_glUniform4i;
|
|
static PFN_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_glGenFramebuffers p_glGenFramebuffers;
|
|
static PFN_glBindFramebuffer p_glBindFramebuffer;
|
|
static PFN_glFramebufferTexture2D p_glFramebufferTexture2D;
|
|
static PFN_glCheckFramebufferStatus p_glCheckFramebufferStatus;
|
|
static PFN_glBlitFramebuffer p_glBlitFramebuffer;
|
|
static PFN_glGenRenderbuffers p_glGenRenderbuffers;
|
|
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_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_glGenFramebuffers, "glGenFramebuffers"); LOAD(p_glBindFramebuffer, "glBindFramebuffer");
|
|
LOAD(p_glFramebufferTexture2D, "glFramebufferTexture2D");
|
|
LOAD(p_glCheckFramebufferStatus, "glCheckFramebufferStatus");
|
|
LOAD(p_glBlitFramebuffer, "glBlitFramebuffer");
|
|
LOAD(p_glGenRenderbuffers, "glGenRenderbuffers");
|
|
LOAD(p_glBindRenderbuffer, "glBindRenderbuffer");
|
|
LOAD(p_glRenderbufferStorage, "glRenderbufferStorage");
|
|
LOAD(p_glFramebufferRenderbuffer, "glFramebufferRenderbuffer");
|
|
#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_scale = 1; /* internal-res scale (hr FBO) */
|
|
static int s_req_scale = 1; /* requested before context init */
|
|
|
|
static GLuint s_present_tex = 0; /* CPU-readout present path (24bpp) */
|
|
static int s_present_w = 0, s_present_h = 0;
|
|
static GLuint s_present_prog = 0, s_present_vao = 0;
|
|
static GLint s_present_uTex = -1;
|
|
|
|
static int s_raster_ok = 0; /* full GPU pipeline available */
|
|
|
|
/* Authoritative VRAM: hr color texture + stencil (mask bit) FBO. */
|
|
static GLuint s_hr_tex = 0, s_hr_fbo = 0, s_hr_rb = 0;
|
|
/* Native raw-1555 sampling mirror + readback source. */
|
|
static GLuint s_raw_tex = 0, s_raw_fbo = 0;
|
|
/* CPU->VRAM upload staging (native RGBA8). */
|
|
static GLuint s_up_tex = 0;
|
|
/* copy_rect staging (hr-sized RGBA8). */
|
|
static GLuint s_scratch_tex = 0, s_scratch_fbo = 0;
|
|
|
|
/* Programs. */
|
|
static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0;
|
|
static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0;
|
|
static GLuint s_blit_prog = 0, s_blit_vao = 0, s_blit_vbo = 0;
|
|
static GLuint s_pack_prog = 0, s_empty_vao = 0;
|
|
|
|
/* TEX program uniforms. */
|
|
static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1;
|
|
static GLint s_uRaw = -1, s_uSemipass = -1, s_uTwin = -1, s_uMaskset = -1, s_uFilter = -1;
|
|
static GLint s_uLimits = -1;
|
|
/* 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 uint32_t *s_conv = NULL; /* RGBA8 staging for uploads */
|
|
static int s_gpu_dirty = 0; /* CPU VRAM array may be stale */
|
|
|
|
/* Dirty-rect unions, native VRAM coords, inclusive bounds. */
|
|
typedef struct { int x0, y0, x1, y1, set; } DirtyRect;
|
|
static DirtyRect s_pack_dirty; /* hr FBO content not in raw mirror */
|
|
static DirtyRect s_up_pending; /* CPU writes not yet in the FBO */
|
|
|
|
/* 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;
|
|
|
|
/* ---- 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);
|
|
}
|
|
|
|
/* ---- 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;
|
|
|
|
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++;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/* ---- shaders ------------------------------------------------------------ */
|
|
static const char *PRESENT_VS =
|
|
"#version 330\n"
|
|
"out vec2 v_uv;\n"
|
|
"void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n"
|
|
" v_uv = vec2(p.x, 1.0 - p.y); gl_Position = vec4(p*2.0-1.0,0.0,1.0); }\n";
|
|
static const char *PRESENT_FS =
|
|
"#version 330\n"
|
|
"in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n"
|
|
"void main(){ frag = texture(u_tex, v_uv); }\n";
|
|
|
|
/* 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"
|
|
"noperspective out vec4 v_col;\n"
|
|
"void main(){ v_col = a_col;\n"
|
|
" gl_Position = vec4((a_pos.x+u_shift)/512.0 - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n";
|
|
static const char *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). */
|
|
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"
|
|
"uniform float u_shift;\n"
|
|
"noperspective out vec2 v_uv; noperspective out vec4 v_col;\n"
|
|
"void main(){ v_uv = a_uv; v_col = a_col;\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"
|
|
" gl_Position = vec4((a_pos.x+u_shift)/512.0 - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n";
|
|
static const char *TEX_FS =
|
|
"#version 330\n"
|
|
"noperspective in vec2 v_uv; noperspective in vec4 v_col; out vec4 frag;\n"
|
|
"uniform usampler2D u_vram;\n"
|
|
"uniform ivec2 u_tpage; /* texture page base, VRAM px */\n"
|
|
"uniform ivec2 u_clut; /* CLUT base, VRAM px */\n"
|
|
"uniform int u_depth; /* 0=4bit 1=8bit 2=15bit */\n"
|
|
"uniform int u_raw; /* 1 = no color modulation */\n"
|
|
"uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n"
|
|
"uniform ivec4 u_twin; /* texture window: mask_x, mask_y, off_x, off_y */\n"
|
|
"uniform int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n"
|
|
"uniform int u_filter; /* 1 = bilinear */\n"
|
|
"uniform ivec4 u_limits; /* prim uv sampling bounds (inclusive, post-wrap\n"
|
|
" space): filtered neighbours and S>1 overshoot\n"
|
|
" clamp here so a sample never reads outside the\n"
|
|
" prim's own texture rect (Beetle-PSX model).\n"
|
|
" Ignored while a texture window is active. */\n"
|
|
"int vram_at(int x, int y){\n"
|
|
" return int(texelFetch(u_vram, ivec2(x & 1023, y & 511), 0).r);\n"
|
|
"}\n"
|
|
"int fetch_texel(int u, int v){\n"
|
|
" u &= 255; v &= 255;\n"
|
|
" if ((u_twin.x | u_twin.y) != 0) {\n"
|
|
" u = (u & ~(u_twin.x * 8)) | ((u_twin.z & u_twin.x) * 8);\n"
|
|
" v = (v & ~(u_twin.y * 8)) | ((u_twin.w & u_twin.y) * 8);\n"
|
|
" } else {\n"
|
|
" u = clamp(u, u_limits.x, u_limits.z);\n"
|
|
" v = clamp(v, u_limits.y, u_limits.w);\n"
|
|
" }\n"
|
|
" if (u_depth == 0) {\n"
|
|
" int px = vram_at(u_tpage.x + (u >> 2), u_tpage.y + v);\n"
|
|
" return vram_at(u_clut.x + ((px >> ((u & 3) * 4)) & 0xF), u_clut.y);\n"
|
|
" } else if (u_depth == 1) {\n"
|
|
" int px = vram_at(u_tpage.x + (u >> 1), u_tpage.y + v);\n"
|
|
" return vram_at(u_clut.x + ((px >> ((u & 1) * 8)) & 0xFF), u_clut.y);\n"
|
|
" }\n"
|
|
" return vram_at(u_tpage.x + u, u_tpage.y + v);\n"
|
|
"}\n"
|
|
"vec3 col5(int raw){\n"
|
|
" return vec3(float(raw & 31), float((raw >> 5) & 31), float((raw >> 10) & 31)) / 31.0;\n"
|
|
"}\n"
|
|
"void main(){\n"
|
|
" int stp; vec3 rgb;\n"
|
|
" if (u_filter == 0) {\n"
|
|
" int raw = fetch_texel(int(floor(v_uv.x)), int(floor(v_uv.y)));\n"
|
|
" if (raw == 0) discard;\n"
|
|
" rgb = col5(raw);\n"
|
|
" stp = (raw >> 15) & 1;\n"
|
|
" } else {\n"
|
|
" /* Bilinear, Beetle-PSX formulation: the NEAREST texel is the base\n"
|
|
" * (cutout + STP authority), the neighbours lie toward the sub-texel\n"
|
|
" * offset and clamp to u_limits, and each texel's weight is gated by\n"
|
|
" * its opacity with the colour renormalised — so prim edges and\n"
|
|
" * cutout borders keep their colour instead of dissolving into the\n"
|
|
" * transparent (black) neighbour and discarding whole edge columns. */\n"
|
|
" int iu = int(floor(v_uv.x)), iv = int(floor(v_uv.y));\n"
|
|
" float fx = v_uv.x - float(iu) - 0.5, fy = v_uv.y - float(iv) - 0.5;\n"
|
|
" int sx = fx < 0.0 ? -1 : 1, sy = fy < 0.0 ? -1 : 1;\n"
|
|
" fx = abs(fx); fy = abs(fy);\n"
|
|
" int c00 = fetch_texel(iu, iv);\n"
|
|
" int c10 = fetch_texel(iu + sx, iv);\n"
|
|
" int c01 = fetch_texel(iu, iv + sy);\n"
|
|
" int c11 = fetch_texel(iu + sx, iv + sy);\n"
|
|
" float w00 = (c00 == 0 ? 0.0 : 1.0) * (1.0 - fx) * (1.0 - fy);\n"
|
|
" float w10 = (c10 == 0 ? 0.0 : 1.0) * fx * (1.0 - fy);\n"
|
|
" float w01 = (c01 == 0 ? 0.0 : 1.0) * (1.0 - fx) * fy;\n"
|
|
" float w11 = (c11 == 0 ? 0.0 : 1.0) * fx * fy;\n"
|
|
" float opac = w00 + w10 + w01 + w11;\n"
|
|
" if (opac < 0.5) discard;\n"
|
|
" rgb = (col5(c00)*w00 + col5(c10)*w10 + col5(c01)*w01 + col5(c11)*w11) / opac;\n"
|
|
" float stpf = (float((c00 >> 15) & 1) * w00 + float((c10 >> 15) & 1) * w10\n"
|
|
" + float((c01 >> 15) & 1) * w01 + float((c11 >> 15) & 1) * w11) / opac;\n"
|
|
" stp = stpf >= 0.5 ? 1 : 0;\n"
|
|
" }\n"
|
|
" if (u_semipass == 1 && stp == 1) discard;\n"
|
|
" if (u_semipass == 2 && stp == 0) discard;\n"
|
|
" if (u_raw == 0) rgb = clamp(rgb * v_col.rgb * 2.0, 0.0, 1.0);\n"
|
|
" frag = vec4(rgb, (stp == 1 || u_maskset == 1) ? 1.0 : 0.0);\n"
|
|
"}\n";
|
|
|
|
/* Quad blit: used for CPU->VRAM upload flushes and VRAM->VRAM copies.
|
|
* Samples an RGBA8 source (alpha = bit15), splits by STP for the stencil
|
|
* write, optionally ORs set-mask into the output alpha. */
|
|
static const char *BLIT_VS =
|
|
"#version 330\n"
|
|
"layout(location=0) in vec2 a_pos; /* native VRAM px */\n"
|
|
"uniform float u_shift;\n"
|
|
"void main(){\n"
|
|
" gl_Position = vec4((a_pos.x+u_shift)/512.0 - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n";
|
|
static const char *BLIT_FS =
|
|
"#version 330\n"
|
|
"out vec4 frag;\n"
|
|
"uniform sampler2D u_src;\n"
|
|
"uniform int u_stp_pass; /* 0=all, 1=bit15=0 only, 2=bit15=1 only */\n"
|
|
"uniform int u_maskset;\n"
|
|
"uniform int u_src_div; /* fragcoord -> src texel divisor (S for native-res\n"
|
|
" sources, 1 for hr-res sources) */\n"
|
|
"uniform ivec2 u_src_off; /* added after the divide, in src texel units */\n"
|
|
"void main(){\n"
|
|
" /* Exact integer source fetch — no normalized-uv edge precision. */\n"
|
|
" ivec2 p = ivec2(gl_FragCoord.xy);\n"
|
|
" vec4 c = texelFetch(u_src, p / u_src_div + u_src_off, 0);\n"
|
|
" bool stp = c.a >= 0.5;\n"
|
|
" if (u_stp_pass == 1 && stp) discard;\n"
|
|
" if (u_stp_pass == 2 && !stp) discard;\n"
|
|
" frag = vec4(c.rgb, (stp || u_maskset != 0) ? 1.0 : 0.0);\n"
|
|
"}\n";
|
|
|
|
/* Pack: re-encode the hr FBO into the native R16UI raw mirror. Runs over a
|
|
* native-res viewport with scissor = dirty rect; each native pixel takes the
|
|
* top-left sample of its S*S block (the sample at the exact native coord). */
|
|
static const char *PACK_VS =
|
|
"#version 330\n"
|
|
"void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n"
|
|
" gl_Position = vec4(p*2.0-1.0, 0.0, 1.0); }\n";
|
|
static const char *PACK_FS =
|
|
"#version 330\n"
|
|
"uniform sampler2D u_hr;\n"
|
|
"uniform int u_scale;\n"
|
|
"out uint o_pix;\n"
|
|
"void main(){\n"
|
|
" ivec2 p = ivec2(gl_FragCoord.xy);\n"
|
|
" vec4 c = texelFetch(u_hr, p * u_scale, 0);\n"
|
|
" uint r = uint(c.r * 255.0 + 0.5) >> 3;\n"
|
|
" uint g = uint(c.g * 255.0 + 0.5) >> 3;\n"
|
|
" uint b = uint(c.b * 255.0 + 0.5) >> 3;\n"
|
|
" o_pix = r | (g << 5) | (b << 10) | (c.a >= 0.5 ? 0x8000u : 0u);\n"
|
|
"}\n";
|
|
|
|
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(const char *vs, const char *fs) {
|
|
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); 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;
|
|
}
|
|
|
|
/* ---- pixel conversion (PS1 1555: bit15=mask, B[14:10] G[9:5] R[4:0]) ---- */
|
|
static inline uint32_t conv_1555_to_rgba8(uint16_t p) {
|
|
uint32_t r = (p & 0x1F) << 3, g = ((p >> 5) & 0x1F) << 3, b = ((p >> 10) & 0x1F) << 3;
|
|
uint32_t a = (p >> 15) & 1 ? 0xFF : 0;
|
|
return r | (g << 8) | (b << 16) | (a << 24); /* RGBA8 little-endian */
|
|
}
|
|
|
|
/* ---- mask-bit stencil --------------------------------------------------- *
|
|
* Stencil bit0 mirrors bit15 of every pixel.
|
|
* check off: test ALWAYS, op REPLACE with ref = write value.
|
|
* check on: test EQUAL 0 (pass iff dest unmasked). GL couples the REPLACE
|
|
* value to the test reference, so write a 1 via INVERT (the
|
|
* stored value is known to be 0 when the test passed) and a 0
|
|
* via KEEP. */
|
|
static void mask_stencil(int write_val) {
|
|
glEnable(GL_STENCIL_TEST);
|
|
glStencilMask(0x01);
|
|
if (s_mask_check) {
|
|
glStencilFunc(GL_EQUAL, 0, 0x01);
|
|
glStencilOp(GL_KEEP, GL_KEEP, write_val ? GL_INVERT : GL_KEEP);
|
|
} else {
|
|
glStencilFunc(GL_ALWAYS, write_val ? 1 : 0, 0x01);
|
|
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
|
}
|
|
}
|
|
/* Like mask_stencil but never checks (uploads: gpu.c already applied mask). */
|
|
static void plain_stencil(int write_val) {
|
|
glEnable(GL_STENCIL_TEST);
|
|
glStencilMask(0x01);
|
|
glStencilFunc(GL_ALWAYS, write_val ? 1 : 0, 0x01);
|
|
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
|
}
|
|
|
|
/* PS1 semi-transparency as fixed-function blending, RGB only — the alpha
|
|
* channel (mask bit) is always replaced by the source fragment's alpha.
|
|
* 0: B/2 + F/2 1: B + F 2: B - F 3: B + F/4 */
|
|
static void apply_psx_blend(int mode) {
|
|
glEnable(GL_BLEND);
|
|
p_glBlendEquationSeparate((mode & 3) == 2 ? PSXGL_FUNC_REVERSE_SUBTRACT
|
|
: PSXGL_FUNC_ADD,
|
|
PSXGL_FUNC_ADD);
|
|
switch (mode & 3) {
|
|
case 0:
|
|
p_glBlendColor(0.5f, 0.5f, 0.5f, 0.5f);
|
|
p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ZERO);
|
|
break;
|
|
case 1:
|
|
case 2:
|
|
p_glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ZERO);
|
|
break;
|
|
case 3:
|
|
p_glBlendColor(0.25f, 0.25f, 0.25f, 0.25f);
|
|
p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ONE, GL_ZERO);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ---- hr FBO render-state bracket ---------------------------------------- */
|
|
static void hr_begin(int clip_to_draw_area) {
|
|
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo);
|
|
glViewport(0, 0, VRAM_W * s_scale, VRAM_H * s_scale);
|
|
glEnable(GL_SCISSOR_TEST);
|
|
if (clip_to_draw_area) {
|
|
int sw = s_area_x2 - s_area_x1 + 1, sh = s_area_y2 - s_area_y1 + 1;
|
|
if (sw < 0) sw = 0; if (sh < 0) sh = 0;
|
|
glScissor(s_area_x1 * s_scale, s_area_y1 * s_scale,
|
|
sw * s_scale, sh * s_scale);
|
|
}
|
|
}
|
|
static void hr_end(void) {
|
|
glDisable(GL_BLEND);
|
|
glDisable(GL_STENCIL_TEST);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
p_glBindVertexArray(0);
|
|
p_glUseProgram(0);
|
|
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
/* ---- coherency: CPU -> GPU upload flush --------------------------------- */
|
|
/* CPU-side VRAM writes (GP0 A0 transfers, DMA, single pixel pokes) land in
|
|
* the CPU array immediately and accumulate s_up_pending. 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_pending.set) return;
|
|
int x = s_up_pending.x0, y = s_up_pending.y0;
|
|
int w = s_up_pending.x1 - s_up_pending.x0 + 1;
|
|
int h = s_up_pending.y1 - s_up_pending.y0 + 1;
|
|
rect_clear(&s_up_pending);
|
|
coh_record(GL_COH_FLUSH, x, y, x + w - 1, y + h - 1);
|
|
|
|
/* RGBA8 staging for the hr quad draw. */
|
|
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]);
|
|
}
|
|
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);
|
|
|
|
/* Quad 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. */
|
|
hr_begin(0);
|
|
glScissor(x * s_scale, y * s_scale, w * s_scale, h * s_scale);
|
|
glDisable(GL_BLEND);
|
|
p_glUseProgram(s_blit_prog);
|
|
p_glActiveTexture(PSXGL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D, s_up_tex);
|
|
p_glUniform1i(s_uBlitSrc, 0);
|
|
p_glUniform1i(s_uBlitMaskset, 0);
|
|
p_glUniform1i(s_uBlitSrcDiv, s_scale);
|
|
p_glUniform2i(s_uBlitSrcOff, 0, 0);
|
|
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_glBindVertexArray(s_blit_vao);
|
|
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo);
|
|
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();
|
|
}
|
|
|
|
/* ---- coherency: hr FBO -> raw mirror (pack) ------------------------------ */
|
|
static void pack_flush(void) {
|
|
if (!s_raster_ok || !s_pack_dirty.set) return;
|
|
int x = s_pack_dirty.x0, y = s_pack_dirty.y0;
|
|
int w = s_pack_dirty.x1 - s_pack_dirty.x0 + 1;
|
|
int h = s_pack_dirty.y1 - s_pack_dirty.y0 + 1;
|
|
rect_clear(&s_pack_dirty);
|
|
coh_record(GL_COH_PACK, x, y, x + w - 1, y + h - 1);
|
|
|
|
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo);
|
|
glViewport(0, 0, VRAM_W, VRAM_H);
|
|
glEnable(GL_SCISSOR_TEST);
|
|
glScissor(x, y, w, h);
|
|
glDisable(GL_BLEND);
|
|
glDisable(GL_STENCIL_TEST);
|
|
p_glUseProgram(s_pack_prog);
|
|
p_glActiveTexture(PSXGL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D, s_hr_tex);
|
|
p_glUniform1i(s_uPackHr, 0);
|
|
p_glUniform1i(s_uPackScale, s_scale);
|
|
p_glBindVertexArray(s_empty_vao);
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
p_glBindVertexArray(0);
|
|
p_glUseProgram(0);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
/* Make sure the raw mirror is current for a textured draw that samples the
|
|
* given texture page / CLUT. */
|
|
static void flush_pack_if_sampling(int tpage_x, int tpage_y, int depth,
|
|
int clut_x, int clut_y) {
|
|
if (!s_pack_dirty.set) return;
|
|
int page_w = depth == 0 ? 64 : depth == 1 ? 128 : 256; /* VRAM columns */
|
|
if (rect_intersects(&s_pack_dirty, tpage_x, tpage_y,
|
|
tpage_x + page_w - 1, tpage_y + 255)) {
|
|
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))
|
|
pack_flush();
|
|
}
|
|
}
|
|
|
|
/* ---- coherency: GPU -> CPU readback -------------------------------------- */
|
|
static void ensure_cpu(void) {
|
|
if (!s_raster_ok || !s_gpu_dirty) return;
|
|
flush_cpu_upload();
|
|
pack_flush();
|
|
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo);
|
|
glReadPixels(0, 0, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, s_vram);
|
|
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
|
s_gpu_dirty = 0;
|
|
coh_record(GL_COH_ENSURE, 0, 0, VRAM_W - 1, VRAM_H - 1);
|
|
}
|
|
|
|
/* ---- GPU primitives ------------------------------------------------------ */
|
|
|
|
static void mark_prim_dirty(const int *xs, const int *ys, int n) {
|
|
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];
|
|
}
|
|
if (x0 < s_area_x1) x0 = s_area_x1;
|
|
if (y0 < s_area_y1) y0 = s_area_y1;
|
|
if (x1 > s_area_x2) x1 = s_area_x2;
|
|
if (y1 > s_area_y2) y1 = s_area_y2;
|
|
rect_add(&s_pack_dirty, x0, y0, x1, y1);
|
|
s_gpu_dirty = 1;
|
|
coh_record(GL_COH_DRAW, x0, y0, x1, y1);
|
|
}
|
|
|
|
/* Flat / gouraud triangles and lines share the GEO program. mode: GL_TRIANGLES
|
|
* or GL_LINES; verts are (x, y, r, g, b) tuples with colors as 1555. */
|
|
static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
|
|
const uint16_t *cs, int n, int semi) {
|
|
flush_cpu_upload();
|
|
mark_prim_dirty(xs, ys, n);
|
|
float verts[3 * 6];
|
|
float mask_a = s_mask_set ? 1.0f : 0.0f;
|
|
for (int i = 0; i < n; i++) {
|
|
verts[i*6+0] = (float)xs[i];
|
|
verts[i*6+1] = (float)ys[i];
|
|
verts[i*6+2] = ((cs[i] & 0x1F) << 3) / 255.0f;
|
|
verts[i*6+3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f;
|
|
verts[i*6+4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f;
|
|
verts[i*6+5] = mask_a;
|
|
}
|
|
hr_begin(1);
|
|
if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND);
|
|
mask_stencil(s_mask_set);
|
|
if (mode == GL_LINES) glLineWidth((float)s_scale);
|
|
p_glUseProgram(s_geo_prog);
|
|
p_glBindVertexArray(s_geo_vao);
|
|
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo);
|
|
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(n * 6 * sizeof(float)), verts, PSXGL_STREAM_DRAW);
|
|
glDrawArrays(mode, 0, n);
|
|
hr_end();
|
|
}
|
|
|
|
static void gpu_triangle(int x0,int y0,uint16_t c0, int x1,int y1,uint16_t c1,
|
|
int x2,int y2,uint16_t c2, int semi) {
|
|
int xs[3] = {x0, x1, x2}, ys[3] = {y0, y1, y2};
|
|
uint16_t cs[3] = {c0, c1, c2};
|
|
gpu_geometry(GL_TRIANGLES, xs, ys, cs, 3, semi);
|
|
}
|
|
|
|
static void gpu_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int semi) {
|
|
int xs[2] = {x0, x1}, ys[2] = {y0, y1};
|
|
uint16_t cs[2] = {c0, c1};
|
|
gpu_geometry(GL_LINES, xs, ys, cs, 2, semi);
|
|
}
|
|
|
|
/* Per-prim uv sampling bounds (inclusive), Beetle-PSX model: filtered
|
|
* neighbours (and S>1 interpolation overshoot) clamp to these so a sample
|
|
* never reads outside the prim's own texture rect. For axis-aligned (2D)
|
|
* uv mappings — any zero uv derivative — the max-uv vertex is an exclusive
|
|
* edge whose texel the PS1 DDA never samples, so back it off by one. If the
|
|
* uv range crosses a 256 wrap boundary the prim relies on page wrapping and
|
|
* the bounds widen to the full page (clamp disabled). */
|
|
static void tri_uv_limits(const int *xs, const int *ys,
|
|
const int *us, const int *vs, int lim[4]) {
|
|
int lo_u = us[0], hi_u = us[0], lo_v = vs[0], hi_v = vs[0];
|
|
for (int i = 1; i < 3; i++) {
|
|
if (us[i] < lo_u) lo_u = us[i]; if (us[i] > hi_u) hi_u = us[i];
|
|
if (vs[i] < lo_v) lo_v = vs[i]; if (vs[i] > hi_v) hi_v = vs[i];
|
|
}
|
|
long dudx = -(long)(ys[1]-ys[0])*us[2] - (long)(ys[2]-ys[1])*us[0] - (long)(ys[0]-ys[2])*us[1];
|
|
long dvdx = -(long)(ys[1]-ys[0])*vs[2] - (long)(ys[2]-ys[1])*vs[0] - (long)(ys[0]-ys[2])*vs[1];
|
|
long dudy = (long)(xs[1]-xs[0])*us[2] + (long)(xs[2]-xs[1])*us[0] + (long)(xs[0]-xs[2])*us[1];
|
|
long dvdy = (long)(xs[1]-xs[0])*vs[2] + (long)(xs[2]-xs[1])*vs[0] + (long)(xs[0]-xs[2])*vs[1];
|
|
if (dudx == 0 || dudy == 0 || dvdx == 0 || dvdy == 0) {
|
|
if (hi_u > lo_u) hi_u--;
|
|
if (hi_v > lo_v) hi_v--;
|
|
}
|
|
if ((lo_u >> 8) == (hi_u >> 8)) { lo_u &= 255; hi_u &= 255; }
|
|
else { lo_u = 0; hi_u = 255; }
|
|
if ((lo_v >> 8) == (hi_v >> 8)) { lo_v &= 255; hi_v &= 255; }
|
|
else { lo_v = 0; hi_v = 255; }
|
|
lim[0] = lo_u; lim[1] = lo_v; lim[2] = hi_u; lim[3] = hi_v;
|
|
}
|
|
|
|
/* Textured triangle. Always two passes split by the per-texel STP bit so the
|
|
* stencil (mask) write value is constant within each pass; the semi pass is
|
|
* also where PS1 blending applies. lim = uv sampling bounds (see
|
|
* tri_uv_limits); NULL computes them from the vertices. */
|
|
static void gpu_textured_triangle(const int *xs, const int *ys,
|
|
const int *us, const int *vs,
|
|
const float *col, uint16_t texpage,
|
|
uint16_t clut_x, uint16_t clut_y, int rawtex,
|
|
int semi, const int *lim) {
|
|
int lim_buf[4];
|
|
if (!lim) { tri_uv_limits(xs, ys, us, vs, lim_buf); lim = lim_buf; }
|
|
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();
|
|
flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y);
|
|
mark_prim_dirty(xs, ys, 3);
|
|
|
|
float verts[3 * 8];
|
|
for (int i = 0; i < 3; i++) {
|
|
verts[i*8+0] = (float)xs[i];
|
|
verts[i*8+1] = (float)ys[i];
|
|
verts[i*8+2] = (float)us[i];
|
|
verts[i*8+3] = (float)vs[i];
|
|
verts[i*8+4] = col[i*3+0];
|
|
verts[i*8+5] = col[i*3+1];
|
|
verts[i*8+6] = col[i*3+2];
|
|
verts[i*8+7] = 1.0f;
|
|
}
|
|
|
|
hr_begin(1);
|
|
p_glUseProgram(s_tex_prog);
|
|
p_glActiveTexture(PSXGL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D, s_raw_tex);
|
|
p_glUniform1i(s_uVram, 0);
|
|
p_glUniform2i(s_uTpage, base_x, base_y);
|
|
p_glUniform2i(s_uClut, clut_x, clut_y);
|
|
p_glUniform1i(s_uDepth, depth);
|
|
p_glUniform1i(s_uRaw, rawtex);
|
|
p_glUniform4i(s_uTwin, s_tw_mask_x, s_tw_mask_y, s_tw_off_x, s_tw_off_y);
|
|
p_glUniform1i(s_uMaskset, s_mask_set);
|
|
p_glUniform1i(s_uFilter, s_tex_filter);
|
|
p_glUniform4i(s_uLimits, lim[0], lim[1], lim[2], lim[3]);
|
|
p_glBindVertexArray(s_tex_vao);
|
|
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo);
|
|
p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW);
|
|
|
|
/* Pass 1: STP=0 texels — never blended; written bit15 = set_mask. */
|
|
glDisable(GL_BLEND);
|
|
mask_stencil(s_mask_set);
|
|
p_glUniform1i(s_uSemipass, 1);
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
/* Pass 2: STP=1 texels — blended iff the prim is semi; bit15 = 1. */
|
|
if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND);
|
|
mask_stencil(1);
|
|
p_glUniform1i(s_uSemipass, 2);
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
hr_end();
|
|
}
|
|
|
|
static void gpu_flat_rect(int x,int y,int w,int h,uint16_t c,int semi) {
|
|
if (w <= 0 || h <= 0) return;
|
|
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);
|
|
}
|
|
|
|
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};
|
|
/* uv sampling bounds: forward mappings sample [u0, u1-1] (u1 is the
|
|
* exclusive edge); mirrored ones keep the full inclusive range. Crossing
|
|
* a 256 boundary means page wrap — widen to the full page. */
|
|
int lim[4];
|
|
lim[0] = u0 < u1 ? u0 : u1; lim[2] = u0 < u1 ? u1 - 1 : u0;
|
|
lim[1] = v0 < v1 ? v0 : v1; lim[3] = v0 < v1 ? v1 - 1 : v0;
|
|
if (lim[2] < lim[0]) lim[2] = lim[0];
|
|
if (lim[3] < lim[1]) lim[3] = lim[1];
|
|
if ((lim[0] >> 8) == (lim[2] >> 8)) { lim[0] &= 255; lim[2] &= 255; }
|
|
else { lim[0] = 0; lim[2] = 255; }
|
|
if ((lim[1] >> 8) == (lim[3] >> 8)) { lim[1] &= 255; lim[3] &= 255; }
|
|
else { lim[1] = 0; lim[3] = 255; }
|
|
int xs1[3]={x, x+w, x}, ys1[3]={y, y, y+h};
|
|
int us1[3]={u0,u1,u0}, vs1[3]={v0,v0,v1};
|
|
gpu_textured_triangle(xs1,ys1,us1,vs1,col,tp,clut_x,clut_y,s_mod_raw,semi,lim);
|
|
int xs2[3]={x+w, x, x+w}, ys2[3]={y, y+h, y+h};
|
|
int us2[3]={u1,u0,u1}, vs2[3]={v0,v1,v1};
|
|
gpu_textured_triangle(xs2,ys2,us2,vs2,col,tp,clut_x,clut_y,s_mod_raw,semi,lim);
|
|
}
|
|
|
|
/* GP0(02h) fill: writes color with bit15=0, ignoring draw area, mask and
|
|
* offset; coordinates wrap. A scissored clear (color + stencil) per wrapped
|
|
* segment is exactly this. */
|
|
static void fill_segment(int x, int y, int w, int h, float r, float g, float b) {
|
|
if (w <= 0 || h <= 0) return;
|
|
glScissor(x * s_scale, y * s_scale, w * s_scale, h * s_scale);
|
|
glClearColor(r, g, b, 0.0f);
|
|
glClearStencil(0);
|
|
glStencilMask(0xFF);
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
|
rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1);
|
|
}
|
|
|
|
static void gpu_fill(int x,int y,int w,int h,uint16_t c) {
|
|
if (w <= 0 || h <= 0) return;
|
|
flush_cpu_upload();
|
|
float r=(c&0x1F)/31.0f, g=((c>>5)&0x1F)/31.0f, b=((c>>10)&0x1F)/31.0f;
|
|
x &= VRAM_W - 1; y &= VRAM_H - 1;
|
|
if (w > VRAM_W) w = VRAM_W;
|
|
if (h > VRAM_H) h = VRAM_H;
|
|
int w1 = w, w2 = 0, h1 = h, h2 = 0;
|
|
if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; }
|
|
if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; }
|
|
|
|
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo);
|
|
glViewport(0, 0, VRAM_W * s_scale, VRAM_H * s_scale);
|
|
glEnable(GL_SCISSOR_TEST);
|
|
fill_segment(x, y, w1, h1, r, g, b);
|
|
if (w2) fill_segment(0, y, w2, h1, r, g, b);
|
|
if (h2) fill_segment(x, 0, w1, h2, r, g, b);
|
|
if (w2 && h2) fill_segment(0, 0, w2, h2, r, g, b);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
|
s_gpu_dirty = 1;
|
|
coh_record(GL_COH_FILL, x, y, x + w - 1, y + h - 1);
|
|
}
|
|
|
|
/* VRAM->VRAM copy: blit the source region to the scratch texture (resolves
|
|
* overlap), then draw it back at the destination through the BLIT program so
|
|
* mask set/check and the stencil mirror apply, exactly like sw_copy_rect. */
|
|
static void gpu_copy_rect(int sx,int sy,int dx,int dy,int w,int h) {
|
|
if (w <= 0 || h <= 0) return;
|
|
flush_cpu_upload();
|
|
/* Clamp to bounds (the software path wraps; wrapping copies are unused
|
|
* in practice — see the file header). */
|
|
if (sx < 0) sx = 0; if (sy < 0) sy = 0;
|
|
if (dx < 0) dx = 0; if (dy < 0) dy = 0;
|
|
if (sx + w > VRAM_W) w = VRAM_W - sx;
|
|
if (dx + w > VRAM_W) w = VRAM_W - dx;
|
|
if (sy + h > VRAM_H) h = VRAM_H - sy;
|
|
if (dy + h > VRAM_H) h = VRAM_H - dy;
|
|
if (w <= 0 || h <= 0) return;
|
|
|
|
int S = s_scale;
|
|
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo);
|
|
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
p_glBlitFramebuffer(sx*S, sy*S, (sx+w)*S, (sy+h)*S,
|
|
sx*S, sy*S, (sx+w)*S, (sy+h)*S,
|
|
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
|
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
|
|
|
hr_begin(0); /* copies ignore the draw area */
|
|
glScissor(dx*S, dy*S, w*S, h*S);
|
|
glDisable(GL_BLEND);
|
|
p_glUseProgram(s_blit_prog);
|
|
p_glActiveTexture(PSXGL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D, s_scratch_tex);
|
|
p_glUniform1i(s_uBlitSrc, 0);
|
|
p_glUniform1i(s_uBlitMaskset, s_mask_set);
|
|
/* Scratch holds the source at its own hr coords: texel = frag + (src-dst)*S. */
|
|
p_glUniform1i(s_uBlitSrcDiv, 1);
|
|
p_glUniform2i(s_uBlitSrcOff, (sx - dx) * S, (sy - dy) * S);
|
|
float fx0 = (float)dx, fy0 = (float)dy, fx1 = (float)(dx + w), fy1 = (float)(dy + h);
|
|
float verts[6 * 2] = {
|
|
fx0, fy0, fx1, fy0, fx0, fy1,
|
|
fx1, fy0, fx0, fy1, fx1, fy1,
|
|
};
|
|
p_glBindVertexArray(s_blit_vao);
|
|
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo);
|
|
p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW);
|
|
/* Two passes split by source bit15 so stencil tracks the copied mask. */
|
|
mask_stencil(s_mask_set); p_glUniform1i(s_uBlitPass, 1); glDrawArrays(GL_TRIANGLES, 0, 6);
|
|
mask_stencil(1); p_glUniform1i(s_uBlitPass, 2); glDrawArrays(GL_TRIANGLES, 0, 6);
|
|
hr_end();
|
|
|
|
rect_add(&s_pack_dirty, dx, dy, dx + w - 1, dy + h - 1);
|
|
s_gpu_dirty = 1;
|
|
coh_record(GL_COH_COPY_SRC, sx, sy, sx + w - 1, sy + h - 1);
|
|
coh_record(GL_COH_COPY, dx, dy, dx + w - 1, dy + h - 1);
|
|
}
|
|
|
|
/* ---- backend vtable wrappers ------------------------------------------- */
|
|
static void glb_init(uint16_t *vram) { s_vram = vram; sw_renderer_init(vram); }
|
|
|
|
/* Under GL the internal-resolution scale lives in the hr FBO; the CPU-side
|
|
* (software mirror, readbacks, screenshots) stays native, so the reported
|
|
* scale is 1 and the software hi-res mirror stays off. */
|
|
static void glb_set_scale(int s) {
|
|
if (s < 1) s = 1;
|
|
if (s > GL_MAX_INTERNAL_SCALE) s = GL_MAX_INTERNAL_SCALE;
|
|
s_req_scale = s;
|
|
sw_renderer_set_scale(1);
|
|
}
|
|
static int glb_scale(void) { return 1; }
|
|
static void glb_set_texture_filter(int b) { s_tex_filter = b ? 1 : 0; sw_set_texture_filter(b); }
|
|
static int glb_texture_filter(void) { return s_tex_filter; }
|
|
|
|
static void glb_set_semi_transparency(int e, int m) { s_semi_en = e; s_semi_mode = m & 3; sw_set_semi_transparency(e, m); }
|
|
static void glb_set_mask_bits(int s, int c) { s_mask_set = s ? 1 : 0; s_mask_check = c ? 1 : 0; sw_set_mask_bits(s, c); }
|
|
static void glb_set_texture_window(uint32_t r) {
|
|
s_tw_mask_x = (int)(r & 0x1F);
|
|
s_tw_mask_y = (int)((r >> 5) & 0x1F);
|
|
s_tw_off_x = (int)((r >> 10) & 0x1F);
|
|
s_tw_off_y = (int)((r >> 15) & 0x1F);
|
|
sw_set_texture_window(r);
|
|
}
|
|
static void glb_set_color_modulation(int r,int g,int b,int raw) { s_mod_r=r; s_mod_g=g; s_mod_b=b; s_mod_raw=raw; sw_set_color_modulation(r,g,b,raw); }
|
|
static void glb_set_draw_area(int x1,int y1,int x2,int y2) { 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) { s_off_x=x; s_off_y=y; sw_set_draw_offset(x,y); }
|
|
|
|
/* Pre-context draws (s_raster_ok == 0) fall back to the software rasterizer
|
|
* over CPU VRAM; the initial full-VRAM upload at context init folds them in.
|
|
* Post-init, the GPU pipeline is all-or-nothing — no per-prim fallback. */
|
|
static void glb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col) {
|
|
if (!s_raster_ok) { sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col); return; }
|
|
gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1);
|
|
}
|
|
static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2) {
|
|
if (!s_raster_ok) { sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2); return; }
|
|
gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1);
|
|
}
|
|
static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){
|
|
if (!s_raster_ok) { sw_fill_rect(x,y,w,h,c); return; }
|
|
gpu_fill(x,y,w,h,c);
|
|
}
|
|
static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){
|
|
if (!s_raster_ok) { sw_copy_rect(sx,sy,dx,dy,w,h); return; }
|
|
gpu_copy_rect(sx,sy,dx,dy,w,h);
|
|
}
|
|
static void glb_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,int x2,int y2,int u2,int v2,uint16_t cx,uint16_t cy,uint16_t tp){
|
|
if (!s_raster_ok) { sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp); return; }
|
|
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
|
float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f;
|
|
float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb};
|
|
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL);
|
|
}
|
|
static void glb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,int x1,int y1,int u1,int v1,uint32_t c1,int x2,int y2,int u2,int v2,uint32_t c2,uint16_t cx,uint16_t cy,uint16_t tp,int raw){
|
|
if (!s_raster_ok) { sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); return; }
|
|
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
|
uint32_t cc[3]={c0,c1,c2}; float col[9];
|
|
for (int i=0;i<3;i++){ col[i*3+0]=(cc[i]&0xFF)/255.0f; col[i*3+1]=((cc[i]>>8)&0xFF)/255.0f; col[i*3+2]=((cc[i]>>16)&0xFF)/255.0f; }
|
|
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL);
|
|
}
|
|
static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){
|
|
if (!s_raster_ok) { sw_draw_flat_rect(x,y,w,h,c); return; }
|
|
gpu_flat_rect(x,y,w,h,c, s_semi_en?s_semi_mode:-1);
|
|
}
|
|
static void glb_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t cx,uint16_t cy,uint16_t tp){
|
|
if (!s_raster_ok) { sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp); return; }
|
|
gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
|
}
|
|
static void glb_draw_textured_rect_scaled(int x,int y,int w,int h,int u0,int v0,int u1,int v1,uint16_t cx,uint16_t cy,uint16_t tp){
|
|
if (!s_raster_ok) { sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp); return; }
|
|
gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
|
}
|
|
static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){
|
|
if (!s_raster_ok) { sw_draw_line(x0,y0,x1,y1,c); return; }
|
|
gpu_line(x0,y0,c, x1,y1,c, s_semi_en?s_semi_mode:-1);
|
|
}
|
|
static void glb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1){
|
|
if (!s_raster_ok) { sw_draw_shaded_line(x0,y0,c0,x1,y1,c1); return; }
|
|
gpu_line(x0,y0,c0, x1,y1,c1, s_semi_en?s_semi_mode:-1);
|
|
}
|
|
static int glb_render_display(uint32_t *o,int p,int dx,int dy,int dw,int dh){ ensure_cpu(); return sw_render_display(o,p,dx,dy,dw,dh); }
|
|
static int glb_render_display_hires(uint32_t *o,int p,int dx,int dy,int dw,int dh){ ensure_cpu(); return sw_render_display_hires(o,p,dx,dy,dw,dh); }
|
|
static void glb_vram_write(int x,int y,uint16_t px){
|
|
sw_vram_write(x,y,px);
|
|
rect_add(&s_up_pending, x & (VRAM_W-1), y & (VRAM_H-1), x & (VRAM_W-1), y & (VRAM_H-1));
|
|
}
|
|
static uint16_t glb_vram_read(int x,int y){ ensure_cpu(); return sw_vram_read(x,y); }
|
|
static void glb_vram_transfer_in(int x,int y,int w,int h,const uint16_t *d){
|
|
sw_vram_transfer_in(x,y,w,h,d);
|
|
if (x + w > VRAM_W || y + h > VRAM_H)
|
|
rect_add(&s_up_pending, 0, 0, VRAM_W-1, VRAM_H-1); /* wrapped: take all */
|
|
else
|
|
rect_add(&s_up_pending, x, y, x+w-1, y+h-1);
|
|
coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1);
|
|
}
|
|
static void glb_vram_transfer_out(int x,int y,int w,int h,uint16_t *d){ ensure_cpu(); sw_vram_transfer_out(x,y,w,h,d); }
|
|
|
|
/* ---- context init / present -------------------------------------------- */
|
|
static void upload_present_tex(const uint32_t *pixels, int w, int h, int linear) {
|
|
glBindTexture(GL_TEXTURE_2D, s_present_tex);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST);
|
|
if (w != s_present_w || h != s_present_h) {
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels);
|
|
s_present_w = w; s_present_h = h;
|
|
} else {
|
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_BGRA, GL_UNSIGNED_BYTE, pixels);
|
|
}
|
|
}
|
|
|
|
/* 4:3 letterbox: largest 4:3 rect centered in the drawable. */
|
|
static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h) {
|
|
int dw = ww, dh = (ww * 3) / 4;
|
|
if (dh > wh) { dh = wh; dw = (wh * 4) / 3; }
|
|
*x = (ww - dw) / 2;
|
|
*y = (wh - dh) / 2;
|
|
*w = dw; *h = dh;
|
|
}
|
|
|
|
static GLuint make_tex(GLenum internal, int w, int h, GLenum fmt, GLenum type) {
|
|
GLuint t = 0;
|
|
glGenTextures(1, &t);
|
|
glBindTexture(GL_TEXTURE_2D, t);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, (GLint)internal, w, h, 0, fmt, type, NULL);
|
|
return t;
|
|
}
|
|
|
|
static int make_fbo(GLuint *out_fbo, GLuint color_tex, GLuint stencil_rb) {
|
|
p_glGenFramebuffers(1, out_fbo);
|
|
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, *out_fbo);
|
|
p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_tex, 0);
|
|
if (stencil_rb)
|
|
p_glFramebufferRenderbuffer(PSXGL_FRAMEBUFFER, PSXGL_DEPTH_STENCIL_ATTACHMENT,
|
|
PSXGL_RENDERBUFFER, stencil_rb);
|
|
GLenum st = p_glCheckFramebufferStatus(PSXGL_FRAMEBUFFER);
|
|
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
|
if (st != PSXGL_FRAMEBUFFER_COMPLETE) {
|
|
fprintf(stdout, "psxrecomp: GL FBO incomplete (0x%X)\n", st);
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int init_gpu_raster(void) {
|
|
s_scale = s_req_scale;
|
|
|
|
s_geo_prog = build_program(GEO_VS, GEO_FS);
|
|
s_tex_prog = build_program(TEX_VS, TEX_FS);
|
|
s_blit_prog = build_program(BLIT_VS, BLIT_FS);
|
|
s_pack_prog = build_program(PACK_VS, PACK_FS);
|
|
if (!s_geo_prog || !s_tex_prog || !s_blit_prog || !s_pack_prog) return 0;
|
|
|
|
s_conv = (uint32_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint32_t));
|
|
if (!s_conv) return 0;
|
|
|
|
int hw = VRAM_W * s_scale, hh = VRAM_H * s_scale;
|
|
s_hr_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE);
|
|
s_scratch_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE);
|
|
s_up_tex = make_tex(GL_RGBA8, VRAM_W, VRAM_H, GL_RGBA, GL_UNSIGNED_BYTE);
|
|
s_raw_tex = make_tex(PSXGL_R16UI, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT);
|
|
|
|
p_glGenRenderbuffers(1, &s_hr_rb);
|
|
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_hr_rb);
|
|
p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, hw, hh);
|
|
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, 0);
|
|
|
|
if (!make_fbo(&s_hr_fbo, s_hr_tex, s_hr_rb)) return 0;
|
|
if (!make_fbo(&s_raw_fbo, s_raw_tex, 0)) return 0;
|
|
if (!make_fbo(&s_scratch_fbo, s_scratch_tex, 0)) return 0;
|
|
|
|
s_uVram = p_glGetUniformLocation(s_tex_prog, "u_vram");
|
|
s_uTpage = p_glGetUniformLocation(s_tex_prog, "u_tpage");
|
|
s_uClut = p_glGetUniformLocation(s_tex_prog, "u_clut");
|
|
s_uDepth = p_glGetUniformLocation(s_tex_prog, "u_depth");
|
|
s_uRaw = p_glGetUniformLocation(s_tex_prog, "u_raw");
|
|
s_uSemipass = p_glGetUniformLocation(s_tex_prog, "u_semipass");
|
|
s_uTwin = p_glGetUniformLocation(s_tex_prog, "u_twin");
|
|
s_uMaskset = p_glGetUniformLocation(s_tex_prog, "u_maskset");
|
|
s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter");
|
|
s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits");
|
|
s_uBlitSrc = p_glGetUniformLocation(s_blit_prog, "u_src");
|
|
s_uBlitPass = p_glGetUniformLocation(s_blit_prog, "u_stp_pass");
|
|
s_uBlitMaskset = p_glGetUniformLocation(s_blit_prog, "u_maskset");
|
|
s_uBlitSrcDiv = p_glGetUniformLocation(s_blit_prog, "u_src_div");
|
|
s_uBlitSrcOff = p_glGetUniformLocation(s_blit_prog, "u_src_off");
|
|
s_uPackHr = p_glGetUniformLocation(s_pack_prog, "u_hr");
|
|
s_uPackScale = p_glGetUniformLocation(s_pack_prog, "u_scale");
|
|
|
|
/* Sample-grid alignment shift: half an HR pixel, set once (S is fixed
|
|
* for the lifetime of the pipeline). Backed off by 1/64 native px so
|
|
* primitive edges never land EXACTLY on sample centers — that float tie
|
|
* dropped 1px columns at quad seams (e.g. the 256px texture-page seam in
|
|
* Tomba's title background). The 1/64 bias keeps floor(uv) on the exact
|
|
* PS1 texel for |uv slope| < 64; mirrored (negative-slope) mappings can
|
|
* be off by one texel at exact-integer uv — accepted. */
|
|
{
|
|
float shift = 0.5f / (float)s_scale - 1.0f / 64.0f;
|
|
p_glUseProgram(s_geo_prog);
|
|
p_glUniform1f(p_glGetUniformLocation(s_geo_prog, "u_shift"), shift);
|
|
p_glUseProgram(s_tex_prog);
|
|
p_glUniform1f(p_glGetUniformLocation(s_tex_prog, "u_shift"), shift);
|
|
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);
|
|
p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)0);
|
|
p_glEnableVertexAttribArray(0);
|
|
p_glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)(2*sizeof(float)));
|
|
p_glEnableVertexAttribArray(1);
|
|
p_glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)(4*sizeof(float)));
|
|
p_glEnableVertexAttribArray(2);
|
|
|
|
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_up_pending);
|
|
rect_add(&s_up_pending, 0, 0, VRAM_W - 1, VRAM_H - 1);
|
|
s_gpu_dirty = 0;
|
|
|
|
s_raster_ok = 1;
|
|
fprintf(stdout, "psxrecomp: GL GPU pipeline ready (internal scale %dx, "
|
|
"mask-bit stencil, texture window, GPU copy/upload)\n", s_scale);
|
|
return 1;
|
|
}
|
|
|
|
int gl_renderer_init_context(SDL_Window *win) {
|
|
s_win = win;
|
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
|
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
|
s_ctx = SDL_GL_CreateContext(win);
|
|
if (!s_ctx) { fprintf(stdout, "psxrecomp: GL context creation failed (%s)\n", SDL_GetError()); return 0; }
|
|
if (SDL_GL_MakeCurrent(win, s_ctx) != 0) { fprintf(stdout, "psxrecomp: MakeCurrent failed (%s)\n", SDL_GetError()); SDL_GL_DeleteContext(s_ctx); s_ctx=NULL; return 0; }
|
|
SDL_GL_SetSwapInterval(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);
|
|
if (s_present_prog) {
|
|
p_glGenVertexArrays(1, &s_present_vao);
|
|
s_present_uTex = p_glGetUniformLocation(s_present_prog, "u_tex");
|
|
} 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;
|
|
}
|
|
|
|
void gl_renderer_shutdown(void) {
|
|
if (s_ctx) {
|
|
ensure_cpu();
|
|
SDL_GL_DeleteContext(s_ctx); s_ctx = NULL;
|
|
}
|
|
free(s_conv); s_conv = NULL;
|
|
s_raster_ok = 0;
|
|
}
|
|
|
|
/* CPU-readout present (24-bit FMV frames and the PSX_GL_FORCE_CPU_PRESENT
|
|
* diagnostic): full-window clear, then a quad into the 4:3 letterbox rect. */
|
|
void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear) {
|
|
if (!s_ctx) return;
|
|
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;
|
|
letterbox_rect(ww, wh, &lx, &ly, &lw, &lh);
|
|
glViewport(lx, ly, lw, lh);
|
|
p_glActiveTexture(PSXGL_TEXTURE0);
|
|
upload_present_tex(pixels, src_w, src_h, linear);
|
|
p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0);
|
|
p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
p_glBindVertexArray(0); p_glUseProgram(0);
|
|
SDL_GL_SwapWindow(s_win);
|
|
}
|
|
|
|
void gl_renderer_present_blank(void) {
|
|
if (!s_ctx) return;
|
|
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);
|
|
SDL_GL_SwapWindow(s_win);
|
|
}
|
|
|
|
/* Sync the authoritative FBO down into CPU VRAM (no-op when current).
|
|
* Called by the 24-bit present path, screenshots, and the debug server. */
|
|
void gl_renderer_sync_cpu(void) {
|
|
ensure_cpu();
|
|
}
|
|
|
|
/* Diagnostic (debug server "gl_fbo_peek"): read a rect of the GPU-side
|
|
* authoritative VRAM (via the pack pass + raw mirror) WITHOUT writing CPU
|
|
* VRAM — lets a probe diff FBO truth against CPU truth. Returns 0 when the
|
|
* GL pipeline is inactive (software backend). */
|
|
int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out) {
|
|
if (!s_raster_ok || !s_ctx) return 0;
|
|
if (x < 0 || y < 0 || w < 1 || h < 1 ||
|
|
x + w > VRAM_W || y + h > VRAM_H) return 0;
|
|
flush_cpu_upload();
|
|
rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1);
|
|
pack_flush();
|
|
coh_record(GL_COH_PEEK, x, y, x + w - 1, y + h - 1);
|
|
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo);
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 2);
|
|
glReadPixels(x, y, w, h, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, out);
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
|
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
|
return 1;
|
|
}
|
|
|
|
/* Diagnostic (debug server "gl_vram_diff"): full-VRAM comparison of the
|
|
* GPU-side truth (FBO via pack) against the CPU array, WITHOUT writing
|
|
* either. Reports mismatch count + bounding box + a few sample coords.
|
|
* Divergence is expected where the GPU is legitimately ahead (gpu_dirty);
|
|
* at upload-only scenes the two must match exactly. */
|
|
int gl_renderer_vram_diff(uint32_t *count, int bbox[4],
|
|
int samples[8][2], uint16_t samples_px[8][2]) {
|
|
if (!s_raster_ok || !s_ctx) return 0;
|
|
uint16_t *tmp = (uint16_t *)malloc((size_t)VRAM_W * VRAM_H * 2);
|
|
if (!tmp) return 0;
|
|
flush_cpu_upload();
|
|
/* Force a full pack: the diff must read FBO truth even where the
|
|
* raw-mirror invariant (raw == FBO outside s_pack_dirty) is broken —
|
|
* a broken invariant is exactly what this tool hunts. */
|
|
rect_add(&s_pack_dirty, 0, 0, VRAM_W - 1, VRAM_H - 1);
|
|
pack_flush();
|
|
coh_record(GL_COH_DIFF, 0, 0, VRAM_W - 1, VRAM_H - 1);
|
|
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo);
|
|
glReadPixels(0, 0, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, tmp);
|
|
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
|
uint32_t n = 0;
|
|
int x0 = VRAM_W, y0 = VRAM_H, x1 = -1, y1 = -1, ns = 0;
|
|
for (int y = 0; y < VRAM_H; y++) {
|
|
for (int x = 0; x < VRAM_W; x++) {
|
|
uint16_t f = tmp[y * VRAM_W + x], c = s_vram[y * VRAM_W + x];
|
|
if (f == c) continue;
|
|
n++;
|
|
if (x < x0) x0 = x; if (x > x1) x1 = x;
|
|
if (y < y0) y0 = y; if (y > y1) y1 = y;
|
|
if (ns < 8 && (n % 977) == 1) { /* spread samples */
|
|
samples[ns][0] = x; samples[ns][1] = y;
|
|
samples_px[ns][0] = f; samples_px[ns][1] = c;
|
|
ns++;
|
|
}
|
|
}
|
|
}
|
|
free(tmp);
|
|
*count = n;
|
|
bbox[0] = x0; bbox[1] = y0; bbox[2] = x1; bbox[3] = y1;
|
|
return 1 + ns; /* >=1 means valid; ns = samples filled */
|
|
}
|
|
|
|
/* Diagnostic state for the debug server: coherency flags + dirty rects. */
|
|
void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]) {
|
|
if (gpu_dirty) *gpu_dirty = s_gpu_dirty;
|
|
if (pending) {
|
|
pending[0] = s_up_pending.set;
|
|
pending[1] = s_up_pending.x0; pending[2] = s_up_pending.y0;
|
|
pending[3] = s_up_pending.x1; pending[4] = s_up_pending.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;
|
|
}
|
|
}
|
|
|
|
/* THE present path for 15-bit frames: blit the display region from the
|
|
* authoritative hr FBO into a 4:3 letterboxed rect. Deterministic — runs
|
|
* every 15-bit frame regardless of what mix of ops produced it. */
|
|
void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear) {
|
|
if (!s_ctx || !s_raster_ok) return;
|
|
flush_cpu_upload();
|
|
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);
|
|
|
|
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);
|
|
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo);
|
|
int S = s_scale;
|
|
/* FBO y matches VRAM y; the window's y=0 is at the bottom, so flip
|
|
* vertically: display top row lands at the letterbox rect's top. */
|
|
p_glBlitFramebuffer(disp_x * S, disp_y * S, (disp_x + w) * S, (disp_y + h) * S,
|
|
lx, ly + lh, lx + lw, ly,
|
|
GL_COLOR_BUFFER_BIT, linear ? GL_LINEAR : GL_NEAREST);
|
|
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
|
SDL_GL_SwapWindow(s_win);
|
|
coh_record(GL_COH_PRESENT, disp_x, disp_y, disp_x + w - 1, disp_y + h - 1);
|
|
}
|
|
|
|
static const GpuRenderBackend GL_BACKEND = {
|
|
.name = "opengl",
|
|
.init = glb_init, .set_scale = glb_set_scale, .scale = glb_scale,
|
|
.set_texture_filter = glb_set_texture_filter, .texture_filter = glb_texture_filter,
|
|
.set_semi_transparency = glb_set_semi_transparency, .set_mask_bits = glb_set_mask_bits,
|
|
.set_texture_window = glb_set_texture_window, .set_color_modulation = glb_set_color_modulation,
|
|
.fill_rect = glb_fill_rect, .copy_rect = glb_copy_rect,
|
|
.draw_flat_triangle = glb_draw_flat_triangle, .draw_gouraud_triangle = glb_draw_gouraud_triangle,
|
|
.draw_textured_triangle = glb_draw_textured_triangle,
|
|
.draw_shaded_textured_triangle = glb_draw_shaded_textured_triangle,
|
|
.draw_flat_rect = glb_draw_flat_rect, .draw_textured_rect = glb_draw_textured_rect,
|
|
.draw_textured_rect_scaled = glb_draw_textured_rect_scaled,
|
|
.draw_line = glb_draw_line, .draw_shaded_line = glb_draw_shaded_line,
|
|
.render_display = glb_render_display, .render_display_hires = glb_render_display_hires,
|
|
.vram_write = glb_vram_write, .vram_read = glb_vram_read,
|
|
.vram_transfer_in = glb_vram_transfer_in, .vram_transfer_out = glb_vram_transfer_out,
|
|
.set_draw_area = glb_set_draw_area, .get_draw_area = glb_get_draw_area,
|
|
.set_draw_offset = glb_set_draw_offset,
|
|
};
|
|
|
|
const GpuRenderBackend *gl_backend_get(void) { return &GL_BACKEND; }
|