From 72799bda9397eeb4f88366233954451e549f04a6 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 11:14:06 -0700 Subject: [PATCH 01/18] gl-renderer phase 1: renderer facade + [video] renderer config option Foundation for a selectable hardware (OpenGL) renderer alongside the software rasterizer. No behavior change: software remains the default and routes through the facade to the exact same sw_* functions. - config: [video] renderer = "software" (default) | "opengl" (config_loader). - gpu_render.h/.c: gr_* facade + GpuRenderBackend vtable. gpu.c (50 call sites) and main.cpp (present/scale/filter) now call gr_* instead of sw_* directly. The software backend table points straight at sw_*, so the SW path is byte-identical. - gpu_gl_renderer.c: Phase-1 stub. gl_backend_get() returns NULL, so requesting renderer="opengl" logs a notice and falls back to software. - main.cpp: selects the backend (gr_set_backend) before gpu_init(). Phase 2 (next) builds the real GL backend: SDL GL context, VRAM-as-texture/FBO, shader rasterization at internal resolution, present. Phase 3: coherency, transparency/dither/mask, then default-with-fallback. Co-Authored-By: Claude Opus 4.8 (1M context) --- recompiler/src/config_loader.cpp | 7 ++ recompiler/src/config_loader.h | 5 + runtime/include/gpu_render.h | 155 +++++++++++++++++++++++++++++++ runtime/runtime.cmake | 2 + runtime/src/gpu.c | 101 ++++++++++---------- runtime/src/gpu_gl_renderer.c | 24 +++++ runtime/src/gpu_render.c | 132 ++++++++++++++++++++++++++ runtime/src/main.cpp | 15 ++- 8 files changed, 387 insertions(+), 54 deletions(-) create mode 100644 runtime/include/gpu_render.h create mode 100644 runtime/src/gpu_gl_renderer.c create mode 100644 runtime/src/gpu_render.c diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 1a45f30e..37049df7 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -104,6 +104,13 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& else throw std::runtime_error(fmt::format( "[video] texture_filtering must be \"nearest\" or \"bilinear\": {}", mode)); } + if (video.contains("renderer")) { + const auto mode = toml::find(video, "renderer"); + if (mode == "software") rt.video_renderer = 0; + else if (mode == "opengl") rt.video_renderer = 1; + else throw std::runtime_error(fmt::format( + "[video] renderer must be \"software\" or \"opengl\": {}", mode)); + } } return rt; diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 7ee59acf..bea49ba3 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -93,6 +93,11 @@ struct RuntimeConfig { // texture_filtering: "nearest" (default, native PSX look) | "bilinear" // (smooths textures and 2D backgrounds). Stored as 0/1. int video_texture_filter = 0; + + // renderer: "software" (default) | "opengl". Selects the rasterizer/present + // backend. The OpenGL backend is a hardware-accelerated alternative; the + // software rasterizer remains the fallback. Stored as 0=software, 1=opengl. + int video_renderer = 0; }; // One entry from [[recompiler.bios_vectors]]. diff --git a/runtime/include/gpu_render.h b/runtime/include/gpu_render.h new file mode 100644 index 00000000..7dd4ca8e --- /dev/null +++ b/runtime/include/gpu_render.h @@ -0,0 +1,155 @@ +#ifndef PSX_GPU_RENDER_H +#define PSX_GPU_RENDER_H + +/* Renderer facade. gpu.c (command processing) and main.cpp (present) call + * these gr_* entry points instead of a specific backend. The facade + * dispatches to a selected backend: the software rasterizer (gpu_sw_renderer.c, + * the default + fallback) or a hardware OpenGL backend (gpu_gl_renderer.c). + * + * The gr_* signatures mirror the software renderer's interface exactly, so a + * backend is just a table of the same functions. Select the backend with + * gr_set_backend() BEFORE gr_init(); gr_backend() reports the EFFECTIVE backend + * (a requested backend that fails to initialize falls back to software). */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + GR_BACKEND_SOFTWARE = 0, + GR_BACKEND_OPENGL = 1 +} GrBackend; + +void gr_set_backend(GrBackend backend); /* call before gr_init() */ +GrBackend gr_backend(void); /* effective backend after init */ + +/* Lifecycle / global state */ +void gr_init(uint16_t *vram); +void gr_set_scale(int scale); +int gr_scale(void); +void gr_set_texture_filter(int bilinear); +int gr_texture_filter(void); + +/* Per-primitive draw state */ +void gr_set_semi_transparency(int enabled, int mode); +void gr_set_mask_bits(int set_bit, int check_bit); +void gr_set_texture_window(uint32_t raw); +void gr_set_color_modulation(int r, int g, int b, int raw_texture); + +/* Primitives */ +void gr_fill_rect(int x, int y, int w, int h, uint16_t color); +void gr_copy_rect(int src_x, int src_y, int dst_x, int dst_y, int w, int h); +void gr_draw_flat_triangle(int x0, int y0, int x1, int y1, int x2, int y2, + uint16_t color); +void gr_draw_gouraud_triangle(int x0, int y0, uint16_t c0, + int x1, int y1, uint16_t c1, + int x2, int y2, uint16_t c2); +void gr_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 clut_x, uint16_t clut_y, + uint16_t texpage); +void gr_draw_shaded_textured_triangle(int x0, int y0, int u0, int v0, + uint32_t color0, + int x1, int y1, int u1, int v1, + uint32_t color1, + int x2, int y2, int u2, int v2, + uint32_t color2, + uint16_t clut_x, uint16_t clut_y, + uint16_t texpage, int raw_texture); +void gr_draw_flat_rect(int x, int y, int w, int h, uint16_t color); +void gr_draw_textured_rect(int x, int y, int w, int h, + int u, int v, + uint16_t clut_x, uint16_t clut_y, + uint16_t texpage); +void gr_draw_textured_rect_scaled(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 texpage); +void gr_draw_line(int x0, int y0, int x1, int y1, uint16_t color); +void gr_draw_shaded_line(int x0, int y0, uint16_t c0, + int x1, int y1, uint16_t c1); + +/* Display readout (present path) */ +int gr_render_display(uint32_t *out_pixels, int out_pitch, + int disp_x, int disp_y, int disp_w, int disp_h); +int gr_render_display_hires(uint32_t *out_pixels, int out_pitch, + int disp_x, int disp_y, int disp_w, int disp_h); + +/* VRAM transfers */ +void gr_vram_write(int x, int y, uint16_t pixel); +uint16_t gr_vram_read(int x, int y); +void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data); +void gr_vram_transfer_out(int x, int y, int w, int h, uint16_t *data); + +/* Draw area / offset */ +void gr_set_draw_area(int x1, int y1, int x2, int y2); +void gr_get_draw_area(int *x1, int *y1, int *x2, int *y2); +void gr_set_draw_offset(int x, int y); + +/* ---- Backend vtable ----------------------------------------------------- + * A backend supplies the same set of functions. gpu_gl_renderer.c (Phase 2) + * provides gl_backend_get(); until it does, requesting OpenGL falls back to + * software. Members mirror the gr_* / sw_* signatures 1:1. */ +typedef struct GpuRenderBackend { + const char *name; + void (*init)(uint16_t *vram); + void (*set_scale)(int scale); + int (*scale)(void); + void (*set_texture_filter)(int bilinear); + int (*texture_filter)(void); + void (*set_semi_transparency)(int enabled, int mode); + void (*set_mask_bits)(int set_bit, int check_bit); + void (*set_texture_window)(uint32_t raw); + void (*set_color_modulation)(int r, int g, int b, int raw_texture); + void (*fill_rect)(int x, int y, int w, int h, uint16_t color); + void (*copy_rect)(int src_x, int src_y, int dst_x, int dst_y, int w, int h); + void (*draw_flat_triangle)(int x0, int y0, int x1, int y1, int x2, int y2, + uint16_t color); + void (*draw_gouraud_triangle)(int x0, int y0, uint16_t c0, + int x1, int y1, uint16_t c1, + int x2, int y2, uint16_t c2); + void (*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 clut_x, uint16_t clut_y, + uint16_t texpage); + void (*draw_shaded_textured_triangle)(int x0, int y0, int u0, int v0, + uint32_t color0, + int x1, int y1, int u1, int v1, + uint32_t color1, + int x2, int y2, int u2, int v2, + uint32_t color2, + uint16_t clut_x, uint16_t clut_y, + uint16_t texpage, int raw_texture); + void (*draw_flat_rect)(int x, int y, int w, int h, uint16_t color); + void (*draw_textured_rect)(int x, int y, int w, int h, int u, int v, + uint16_t clut_x, uint16_t clut_y, + uint16_t texpage); + void (*draw_textured_rect_scaled)(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 texpage); + void (*draw_line)(int x0, int y0, int x1, int y1, uint16_t color); + void (*draw_shaded_line)(int x0, int y0, uint16_t c0, + int x1, int y1, uint16_t c1); + int (*render_display)(uint32_t *out, int pitch, + int dx, int dy, int dw, int dh); + int (*render_display_hires)(uint32_t *out, int pitch, + int dx, int dy, int dw, int dh); + void (*vram_write)(int x, int y, uint16_t pixel); + uint16_t (*vram_read)(int x, int y); + void (*vram_transfer_in)(int x, int y, int w, int h, const uint16_t *data); + void (*vram_transfer_out)(int x, int y, int w, int h, uint16_t *data); + void (*set_draw_area)(int x1, int y1, int x2, int y2); + void (*get_draw_area)(int *x1, int *y1, int *x2, int *y2); + void (*set_draw_offset)(int x, int y); +} GpuRenderBackend; + +#ifdef __cplusplus +} +#endif + +#endif /* PSX_GPU_RENDER_H */ diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 2bb63f87..2460c629 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -68,6 +68,8 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/memory.c ${PSXRECOMP_ROOT}/runtime/src/gpu.c ${PSXRECOMP_ROOT}/runtime/src/gpu_sw_renderer.c + ${PSXRECOMP_ROOT}/runtime/src/gpu_render.c + ${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c ${PSXRECOMP_ROOT}/runtime/src/dma.c ${PSXRECOMP_ROOT}/runtime/src/mdec.c ${PSXRECOMP_ROOT}/runtime/src/timers.c diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index de23bf97..afd67387 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -12,6 +12,7 @@ #include "gpu.h" #include "gpu_sw_renderer.h" +#include "gpu_render.h" #include "crash_trace.h" #include "debug_server.h" #include "cpu_state.h" @@ -166,7 +167,7 @@ static uint32_t gpustat_poll_count; void gpu_init(void) { memset(vram, 0, sizeof(vram)); - sw_renderer_init(vram); + gr_init(vram); /* Reset GP0 state machine */ gp0_state = GP0_IDLE; @@ -542,8 +543,8 @@ static void gp0_exec_mono_tri(void) { vx[i] += draw_offset_x; vy[i] += draw_offset_y; } - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); } /* Execute mono quad (GP0 0x28-0x2B) — two triangles: (0,1,2) and (2,1,3) */ @@ -556,9 +557,9 @@ static void gp0_exec_mono_quad(void) { vx[i] += draw_offset_x; vy[i] += draw_offset_y; } - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); - sw_draw_flat_triangle(vx[2], vy[2], vx[1], vy[1], vx[3], vy[3], color); + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); + gr_draw_flat_triangle(vx[2], vy[2], vx[1], vy[1], vx[3], vy[3], color); } /* Execute shaded triangle (GP0 0x30-0x33) — Gouraud shaded */ @@ -573,8 +574,8 @@ static void gp0_exec_shaded_tri(void) { vx[i] += draw_offset_x; vy[i] += draw_offset_y; } - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_gouraud_triangle(vx[0], vy[0], c[0], + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_gouraud_triangle(vx[0], vy[0], c[0], vx[1], vy[1], c[1], vx[2], vy[2], c[2]); } @@ -606,11 +607,11 @@ static void gp0_exec_shaded_quad(void) { e->color[i] = gp0_cmd_buf[i * 2] & 0xFFFFFFu; } } - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_gouraud_triangle(vx[0], vy[0], c[0], + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_gouraud_triangle(vx[0], vy[0], c[0], vx[1], vy[1], c[1], vx[2], vy[2], c[2]); - sw_draw_gouraud_triangle(vx[2], vy[2], c[2], + gr_draw_gouraud_triangle(vx[2], vy[2], c[2], vx[1], vy[1], c[1], vx[3], vy[3], c[3]); } @@ -629,8 +630,8 @@ static void setup_textured_draw(uint32_t color24, int semi_trans, int raw_textur uint32_t r = (color24 >> 0) & 0xFF; uint32_t g = (color24 >> 8) & 0xFF; uint32_t b = (color24 >> 16) & 0xFF; - sw_set_color_modulation((int)r, (int)g, (int)b, raw_texture); - sw_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_set_color_modulation((int)r, (int)g, (int)b, raw_texture); + gr_set_semi_transparency(semi_trans, (int)semi_transparency); } /* Execute textured triangle (GP0 0x24-0x27) */ @@ -660,7 +661,7 @@ static void gp0_exec_textured_tri(void) { } setup_textured_draw(color24, semi_trans, raw_texture); - sw_draw_textured_triangle(vx[0], vy[0], u[0], v[0], + gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0], vx[1], vy[1], u[1], v[1], vx[2], vy[2], u[2], v[2], clut_x, clut_y, tpage); @@ -708,22 +709,22 @@ static void gp0_exec_textured_quad(void) { int bot_v = vy[0] < vy[2] ? v[2] : v[0]; if (w > 0 && h > 0) { if (right_u - left_u == w && bot_v - top_v == h) { - sw_draw_textured_rect(x, y, w, h, left_u, top_v, + gr_draw_textured_rect(x, y, w, h, left_u, top_v, clut_x, clut_y, tpage); return; } - sw_draw_textured_rect_scaled(x, y, w, h, left_u, top_v, + gr_draw_textured_rect_scaled(x, y, w, h, left_u, top_v, right_u, bot_v, clut_x, clut_y, tpage); return; } } - sw_draw_textured_triangle(vx[0], vy[0], u[0], v[0], + gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0], vx[1], vy[1], u[1], v[1], vx[2], vy[2], u[2], v[2], clut_x, clut_y, tpage); - sw_draw_textured_triangle(vx[2], vy[2], u[2], v[2], + gr_draw_textured_triangle(vx[2], vy[2], u[2], v[2], vx[1], vy[1], u[1], v[1], vx[3], vy[3], u[3], v[3], clut_x, clut_y, tpage); @@ -756,8 +757,8 @@ static void gp0_exec_shaded_textured_tri(void) { vy[i] += draw_offset_y; } - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0], + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0], vx[1], vy[1], u[1], v[1], c[1], vx[2], vy[2], u[2], v[2], c[2], clut_x, clut_y, tpage, raw_texture); @@ -793,12 +794,12 @@ static void gp0_exec_shaded_textured_quad(void) { vy[i] += draw_offset_y; } - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0], + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0], vx[1], vy[1], u[1], v[1], c[1], vx[2], vy[2], u[2], v[2], c[2], clut_x, clut_y, tpage, raw_texture); - sw_draw_shaded_textured_triangle(vx[2], vy[2], u[2], v[2], c[2], + gr_draw_shaded_textured_triangle(vx[2], vy[2], u[2], v[2], c[2], vx[1], vy[1], u[1], v[1], c[1], vx[3], vy[3], u[3], v[3], c[3], clut_x, clut_y, tpage, raw_texture); @@ -813,8 +814,8 @@ static void gp0_exec_mono_line(void) { parse_vertex(gp0_cmd_buf[2], &x1, &y1); x0 += draw_offset_x; y0 += draw_offset_y; x1 += draw_offset_x; y1 += draw_offset_y; - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_line(x0, y0, x1, y1, color); + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_line(x0, y0, x1, y1, color); } /* Execute shaded line (GP0 0x50-0x57) */ @@ -827,8 +828,8 @@ static void gp0_exec_shaded_line(void) { parse_vertex(gp0_cmd_buf[3], &x1, &y1); x0 += draw_offset_x; y0 += draw_offset_y; x1 += draw_offset_x; y1 += draw_offset_y; - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_shaded_line(x0, y0, c0, x1, y1, c1); + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_shaded_line(x0, y0, c0, x1, y1, c1); } /* Execute mono rectangle (GP0 0x60-0x63) */ @@ -842,8 +843,8 @@ static void gp0_exec_mono_rect(void) { int h = (gp0_cmd_buf[2] >> 16) & 0xFFFFu; if (w > 1023) w = 1023; if (h > 511) h = 511; - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_flat_rect(x0, y0, w, h, color); + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_flat_rect(x0, y0, w, h, color); } /* Execute textured rectangle (GP0 0x64-0x67) */ @@ -865,7 +866,7 @@ static void gp0_exec_textured_rect(void) { if (h > 511) h = 511; setup_textured_draw(color24, semi_trans, raw_texture); - sw_draw_textured_rect(x0, y0, w, h, u0, v0, clut_x, clut_y, current_texpage()); + gr_draw_textured_rect(x0, y0, w, h, u0, v0, clut_x, clut_y, current_texpage()); } /* Execute 1x1 dot (GP0 0x68-0x6B) */ @@ -875,8 +876,8 @@ static void gp0_exec_mono_dot(void) { int32_t x, y; parse_vertex(gp0_cmd_buf[1], &x, &y); x += draw_offset_x; y += draw_offset_y; - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_flat_rect(x, y, 1, 1, color); + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_flat_rect(x, y, 1, 1, color); } /* Execute 8x8 textured sprite (GP0 0x74-0x77) */ @@ -894,7 +895,7 @@ static void gp0_exec_textured_8x8(void) { uint16_t clut_y = (clut >> 6) & 0x1FF; setup_textured_draw(color24, semi_trans, raw_texture); - sw_draw_textured_rect(x0, y0, 8, 8, u0, v0, clut_x, clut_y, current_texpage()); + gr_draw_textured_rect(x0, y0, 8, 8, u0, v0, clut_x, clut_y, current_texpage()); } /* Execute 8x8 sprite (GP0 0x70-0x73) */ @@ -904,8 +905,8 @@ static void gp0_exec_mono_8x8(void) { int32_t x0, y0; parse_vertex(gp0_cmd_buf[1], &x0, &y0); x0 += draw_offset_x; y0 += draw_offset_y; - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_flat_rect(x0, y0, 8, 8, color); + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_flat_rect(x0, y0, 8, 8, color); } /* Execute 16x16 textured sprite (GP0 0x7C-0x7F) */ @@ -923,7 +924,7 @@ static void gp0_exec_textured_16x16(void) { uint16_t clut_y = (clut >> 6) & 0x1FF; setup_textured_draw(color24, semi_trans, raw_texture); - sw_draw_textured_rect(x0, y0, 16, 16, u0, v0, clut_x, clut_y, current_texpage()); + gr_draw_textured_rect(x0, y0, 16, 16, u0, v0, clut_x, clut_y, current_texpage()); } /* ---- GP0 command execution ---- */ @@ -948,7 +949,7 @@ static void gp0_exec_fill_rect(void) { /* Fill ignores draw area, mask bits, and draw offset — writes directly to * VRAM. Routed through the renderer so it also fills the hi-res * supersampling mirror (no-op cost when supersampling is off). */ - sw_fill_rect((int)dst_x, (int)dst_y, (int)width, (int)height, color16); + gr_fill_rect((int)dst_x, (int)dst_y, (int)width, (int)height, color16); } static void gp0_exec_draw_mode(void) { @@ -978,7 +979,7 @@ static void gp0_exec_texture_window(void) { * Bits 10-14: offset X * Bits 15-19: offset Y */ texture_window_value = gp0_cmd_buf[0] & 0x000FFFFFu; - sw_set_texture_window(texture_window_value); + gr_set_texture_window(texture_window_value); } static void gp0_exec_draw_area_tl(void) { @@ -988,7 +989,7 @@ static void gp0_exec_draw_area_tl(void) { uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu; draw_area_left = param & 0x3FF; draw_area_top = (param >> 10) & 0x3FF; - sw_set_draw_area((int)draw_area_left, (int)draw_area_top, + gr_set_draw_area((int)draw_area_left, (int)draw_area_top, (int)draw_area_right, (int)draw_area_bottom); } @@ -999,7 +1000,7 @@ static void gp0_exec_draw_area_br(void) { uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu; draw_area_right = param & 0x3FF; draw_area_bottom = (param >> 10) & 0x3FF; - sw_set_draw_area((int)draw_area_left, (int)draw_area_top, + gr_set_draw_area((int)draw_area_left, (int)draw_area_top, (int)draw_area_right, (int)draw_area_bottom); } @@ -1010,7 +1011,7 @@ static void gp0_exec_draw_offset(void) { uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu; draw_offset_x = sign_extend(param & 0x7FFu, 11); draw_offset_y = sign_extend((param >> 11) & 0x7FFu, 11); - sw_set_draw_offset(draw_offset_x, draw_offset_y); + gr_set_draw_offset(draw_offset_x, draw_offset_y); } static void gp0_exec_mask_bit(void) { @@ -1020,7 +1021,7 @@ static void gp0_exec_mask_bit(void) { uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu; set_mask_bit = param & 1; check_mask_bit = (param >> 1) & 1; - sw_set_mask_bits((int)set_mask_bit, (int)check_mask_bit); + gr_set_mask_bits((int)set_mask_bit, (int)check_mask_bit); } /* A0 upload history for debug inspection */ @@ -1450,7 +1451,7 @@ static void gp0_execute_command(void) { uint16_t clut_x = (clut & 0x3F) * 16; uint16_t clut_y = (clut >> 6) & 0x1FF; setup_textured_draw(color24, semi_trans, raw_texture); - sw_draw_textured_rect(x0, y0, 1, 1, u0, v0, clut_x, clut_y, current_texpage()); + gr_draw_textured_rect(x0, y0, 1, 1, u0, v0, clut_x, clut_y, current_texpage()); break; } case 0x70: case 0x71: case 0x72: case 0x73: @@ -1466,8 +1467,8 @@ static void gp0_execute_command(void) { int32_t x0, y0; parse_vertex(gp0_cmd_buf[1], &x0, &y0); x0 += draw_offset_x; y0 += draw_offset_y; - sw_set_semi_transparency(semi_trans, (int)semi_transparency); - sw_draw_flat_rect(x0, y0, 16, 16, color); + gr_set_semi_transparency(semi_trans, (int)semi_transparency); + gr_draw_flat_rect(x0, y0, 16, 16, color); break; } case 0x7C: case 0x7D: case 0x7E: case 0x7F: @@ -1492,7 +1493,7 @@ static void gp0_execute_command(void) { int h = (gp0_cmd_buf[3] >> 16) & 0x1FF; if (w == 0) w = 0x400; if (h == 0) h = 0x200; - sw_copy_rect(src_x, src_y, dst_x, dst_y, w, h); + gr_copy_rect(src_x, src_y, dst_x, dst_y, w, h); break; } @@ -1589,7 +1590,7 @@ void gpu_write_gp0(uint32_t val) { /* Route through the renderer so the hi-res supersampling mirror * is kept coherent (writes native VRAM identically when off). */ - sw_vram_write((int)wx, (int)wy, pixel); + gr_vram_write((int)wx, (int)wy, pixel); next_pixel: if (++vram_write_col == vram_write_w) { @@ -1620,7 +1621,7 @@ void gpu_write_gp0(uint32_t val) { parse_vertex(val, &x, &y); x += draw_offset_x; y += draw_offset_y; if (polyline_has_prev) { - sw_draw_line(polyline_prev_x, polyline_prev_y, x, y, polyline_color); + gr_draw_line(polyline_prev_x, polyline_prev_y, x, y, polyline_color); } polyline_prev_x = x; polyline_prev_y = y; polyline_has_prev = 1; @@ -1657,7 +1658,7 @@ void gpu_write_gp0(uint32_t val) { int32_t x, y; parse_vertex(val, &x, &y); x += draw_offset_x; y += draw_offset_y; - sw_draw_shaded_line(polyline_prev_x, polyline_prev_y, polyline_prev_c, + gr_draw_shaded_line(polyline_prev_x, polyline_prev_y, polyline_prev_c, x, y, polyline_color); polyline_prev_x = x; polyline_prev_y = y; polyline_prev_c = polyline_color; @@ -1698,7 +1699,7 @@ void gpu_write_gp0(uint32_t val) { polyline_color = rgb888_to_rgb555(val & 0xFFFFFFu); polyline_prev_c = polyline_color; polyline_has_prev = 0; - sw_set_semi_transparency(polyline_semi_trans, (int)semi_transparency); + gr_set_semi_transparency(polyline_semi_trans, (int)semi_transparency); gp0_state = shaded ? GP0_POLYLINE_SHADED : GP0_POLYLINE_MONO; gp0_draw_count++; /* Record polyline header (variable-length body not captured; diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c new file mode 100644 index 00000000..40f00b00 --- /dev/null +++ b/runtime/src/gpu_gl_renderer.c @@ -0,0 +1,24 @@ +/* gpu_gl_renderer.c — hardware OpenGL renderer backend. + * + * PHASE 1 (current): stub. gl_backend_get() returns NULL, so requesting + * [video] renderer = "opengl" logs a notice and falls back to software (see + * gpu_render.c). This keeps the facade + config + build wiring in place and + * exercised while the real backend is built up. + * + * PHASE 2+: this file will create an SDL OpenGL context, hold VRAM as a GPU + * texture/FBO, rasterize PS1 primitives via shaders at the configured internal + * resolution, and present. It will then return a populated GpuRenderBackend + * table here. See gpu_render.h for the backend interface. + * + * The PS1 GPU model the backend must reproduce: 1024x512 16-bit (RGB555+mask) + * VRAM; textured polys sample 4/8/15-bit texels via CLUT from VRAM; four + * semi-transparency blend modes; texture windows; dithering; mask bit; 15/24 + * bit display readout. Coherency (game reads VRAM back) is the hard part — + * handled via on-demand GPU->CPU download in vram_transfer_out / vram_read. */ + +#include "gpu_render.h" + +const GpuRenderBackend *gl_backend_get(void) { + /* Phase 1: not yet implemented. */ + return 0; +} diff --git a/runtime/src/gpu_render.c b/runtime/src/gpu_render.c new file mode 100644 index 00000000..d38f9842 --- /dev/null +++ b/runtime/src/gpu_render.c @@ -0,0 +1,132 @@ +/* gpu_render.c — renderer facade / backend dispatch. + * + * gpu.c and main.cpp call the gr_* functions; this file forwards each to the + * selected backend's vtable. The software rasterizer is the default and the + * fallback. The OpenGL backend (gpu_gl_renderer.c) is selected via + * gr_set_backend(GR_BACKEND_OPENGL) and supplies its table through + * gl_backend_get(); if that returns NULL (GL unavailable / init failed) we + * stay on software so a misconfigured [video] renderer never bricks boot. + * + * The software backend table points straight at the existing sw_* functions, + * so the software path is byte-for-byte unchanged. */ + +#include "gpu_render.h" +#include "gpu_sw_renderer.h" +#include + +static const GpuRenderBackend SW_BACKEND = { + .name = "software", + .init = sw_renderer_init, + .set_scale = sw_renderer_set_scale, + .scale = sw_renderer_scale, + .set_texture_filter = sw_set_texture_filter, + .texture_filter = sw_texture_filter, + .set_semi_transparency = sw_set_semi_transparency, + .set_mask_bits = sw_set_mask_bits, + .set_texture_window = sw_set_texture_window, + .set_color_modulation = sw_set_color_modulation, + .fill_rect = sw_fill_rect, + .copy_rect = sw_copy_rect, + .draw_flat_triangle = sw_draw_flat_triangle, + .draw_gouraud_triangle = sw_draw_gouraud_triangle, + .draw_textured_triangle = sw_draw_textured_triangle, + .draw_shaded_textured_triangle = sw_draw_shaded_textured_triangle, + .draw_flat_rect = sw_draw_flat_rect, + .draw_textured_rect = sw_draw_textured_rect, + .draw_textured_rect_scaled = sw_draw_textured_rect_scaled, + .draw_line = sw_draw_line, + .draw_shaded_line = sw_draw_shaded_line, + .render_display = sw_render_display, + .render_display_hires = sw_render_display_hires, + .vram_write = sw_vram_write, + .vram_read = sw_vram_read, + .vram_transfer_in = sw_vram_transfer_in, + .vram_transfer_out = sw_vram_transfer_out, + .set_draw_area = sw_set_draw_area, + .get_draw_area = sw_get_draw_area, + .set_draw_offset = sw_set_draw_offset, +}; + +/* Supplied by gpu_gl_renderer.c; returns NULL until the GL backend is ready. */ +extern const GpuRenderBackend *gl_backend_get(void); + +static const GpuRenderBackend *g_b = &SW_BACKEND; +static GrBackend g_effective = GR_BACKEND_SOFTWARE; + +void gr_set_backend(GrBackend backend) { + if (backend == GR_BACKEND_OPENGL) { + const GpuRenderBackend *gl = gl_backend_get(); + if (gl) { + g_b = gl; + g_effective = GR_BACKEND_OPENGL; + fprintf(stdout, "psxrecomp: renderer = opengl (%s)\n", gl->name); + return; + } + fprintf(stdout, "psxrecomp: renderer = opengl requested but unavailable " + "— falling back to software\n"); + } + g_b = &SW_BACKEND; + g_effective = GR_BACKEND_SOFTWARE; +} + +GrBackend gr_backend(void) { return g_effective; } + +/* ---- Dispatch wrappers (one line each; forward to the active backend) ---- */ +void gr_init(uint16_t *vram) { g_b->init(vram); } +void gr_set_scale(int scale) { g_b->set_scale(scale); } +int gr_scale(void) { return g_b->scale(); } +void gr_set_texture_filter(int bilinear) { g_b->set_texture_filter(bilinear); } +int gr_texture_filter(void) { return g_b->texture_filter(); } +void gr_set_semi_transparency(int e, int m) { g_b->set_semi_transparency(e, m); } +void gr_set_mask_bits(int s, int c) { g_b->set_mask_bits(s, c); } +void gr_set_texture_window(uint32_t raw) { g_b->set_texture_window(raw); } +void gr_set_color_modulation(int r, int g, int b, int raw) { g_b->set_color_modulation(r, g, b, raw); } +void gr_fill_rect(int x, int y, int w, int h, uint16_t c) { g_b->fill_rect(x, y, w, h, c); } +void gr_copy_rect(int sx, int sy, int dx, int dy, int w, int h) { g_b->copy_rect(sx, sy, dx, dy, w, h); } +void gr_draw_flat_triangle(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t c) { + g_b->draw_flat_triangle(x0, y0, x1, y1, x2, y2, c); +} +void gr_draw_gouraud_triangle(int x0, int y0, uint16_t c0, int x1, int y1, uint16_t c1, + int x2, int y2, uint16_t c2) { + g_b->draw_gouraud_triangle(x0, y0, c0, x1, y1, c1, x2, y2, c2); +} +void gr_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 clut_x, uint16_t clut_y, uint16_t texpage) { + g_b->draw_textured_triangle(x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2, + clut_x, clut_y, texpage); +} +void gr_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 clut_x, uint16_t clut_y, + uint16_t texpage, int raw) { + g_b->draw_shaded_textured_triangle(x0, y0, u0, v0, c0, x1, y1, u1, v1, c1, + x2, y2, u2, v2, c2, clut_x, clut_y, texpage, raw); +} +void gr_draw_flat_rect(int x, int y, int w, int h, uint16_t c) { g_b->draw_flat_rect(x, y, w, h, c); } +void gr_draw_textured_rect(int x, int y, int w, int h, int u, int v, + uint16_t clut_x, uint16_t clut_y, uint16_t texpage) { + g_b->draw_textured_rect(x, y, w, h, u, v, clut_x, clut_y, texpage); +} +void gr_draw_textured_rect_scaled(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 texpage) { + g_b->draw_textured_rect_scaled(x, y, w, h, u0, v0, u1, v1, clut_x, clut_y, texpage); +} +void gr_draw_line(int x0, int y0, int x1, int y1, uint16_t c) { g_b->draw_line(x0, y0, x1, y1, c); } +void gr_draw_shaded_line(int x0, int y0, uint16_t c0, int x1, int y1, uint16_t c1) { + g_b->draw_shaded_line(x0, y0, c0, x1, y1, c1); +} +int gr_render_display(uint32_t *o, int p, int dx, int dy, int dw, int dh) { + return g_b->render_display(o, p, dx, dy, dw, dh); +} +int gr_render_display_hires(uint32_t *o, int p, int dx, int dy, int dw, int dh) { + return g_b->render_display_hires(o, p, dx, dy, dw, dh); +} +void gr_vram_write(int x, int y, uint16_t pixel) { g_b->vram_write(x, y, pixel); } +uint16_t gr_vram_read(int x, int y) { return g_b->vram_read(x, y); } +void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *d) { g_b->vram_transfer_in(x, y, w, h, d); } +void gr_vram_transfer_out(int x, int y, int w, int h, uint16_t *d) { g_b->vram_transfer_out(x, y, w, h, d); } +void gr_set_draw_area(int x1, int y1, int x2, int y2){ g_b->set_draw_area(x1, y1, x2, y2); } +void gr_get_draw_area(int *x1, int *y1, int *x2, int *y2) { g_b->get_draw_area(x1, y1, x2, y2); } +void gr_set_draw_offset(int x, int y) { g_b->set_draw_offset(x, y); } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index afd47637..44e0aa11 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -15,6 +15,7 @@ #include "autocompile.h" #include "gpu.h" #include "gpu_sw_renderer.h" +#include "gpu_render.h" #include "frame_pacing.h" #include "sio.h" #include "spu.h" @@ -100,6 +101,7 @@ static int s_fast_boot_active = 0; /* cleared when game entry PC fire static int g_video_scale = 1; /* internal-resolution SSAA factor */ static bool g_video_aa = true; /* linear present filtering */ static int g_video_texfilter = 0; /* 0=nearest, 1=bilinear */ +static int g_video_renderer = 0; /* 0=software, 1=opengl */ /* Vsync self-heal state (see SDL_RenderPresent wrapper in * sdl_vblank_present). C linkage: freeze_heartbeat.c includes both in @@ -1114,7 +1116,7 @@ static void sdl_vblank_present(void) { if (active_scale > 1) { int sw = (int)w * active_scale; - sw_render_display_hires(sdl_pixel_buf, (int)(sw * sizeof(uint32_t)), + gr_render_display_hires(sdl_pixel_buf, (int)(sw * sizeof(uint32_t)), (int)di.display_x, (int)di.display_y, (int)w, (int)h); } else { @@ -1240,6 +1242,7 @@ int main(int argc, char** argv) { g_video_scale = gc.runtime.video_supersampling; g_video_aa = gc.runtime.video_antialiasing; g_video_texfilter = gc.runtime.video_texture_filter; + g_video_renderer = gc.runtime.video_renderer; game_entry_pc = gc.entry_pc; fast_boot = gc.runtime.fast_boot; /* Overlay DLL cache (Layer A). Off unless enabled in [runtime]; @@ -1300,15 +1303,19 @@ int main(int argc, char** argv) { std::fprintf(stdout, "psxrecomp runtime: loading BIOS from %s\n", bios_path_str.c_str()); memory_init(bios_path_str.c_str()); + /* Select the renderer backend BEFORE gpu_init() (which runs gr_init -> + * the backend's init on the VRAM buffer). Software is the default and the + * fallback; an unavailable OpenGL backend reverts to software. */ + gr_set_backend(g_video_renderer == 1 ? GR_BACKEND_OPENGL : GR_BACKEND_SOFTWARE); gpu_init(); /* Internal-resolution supersampling (SSAA). Must follow gpu_init (which * runs sw_renderer_init). scale==1 is a no-op; >1 allocates the hi-res * VRAM mirror. */ if (g_video_scale < 1) g_video_scale = 1; if (g_video_scale > SW_MAX_INTERNAL_SCALE) g_video_scale = SW_MAX_INTERNAL_SCALE; - sw_renderer_set_scale(g_video_scale); - g_video_scale = sw_renderer_scale(); /* reflect any clamp / alloc fallback */ - sw_set_texture_filter(g_video_texfilter); + gr_set_scale(g_video_scale); + g_video_scale = gr_scale(); /* reflect any clamp / alloc fallback */ + gr_set_texture_filter(g_video_texfilter); if (g_video_scale > 1 || g_video_texfilter) std::fprintf(stdout, "psxrecomp: supersampling %dx (antialiasing %s, texture filter %s)\n", From 1eaee11299d710f7db418f087362456f326a3c18 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 11:34:46 -0700 Subject: [PATCH 02/18] gl-renderer phase 2a: real GL context + VRAM-texture present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The opengl backend now creates a genuine OpenGL context and presents through it. Verified: Tomba boots and renders gameplay correctly via GL (context "4.6.0 NVIDIA"), pixel-identical to software. - gpu_gl_renderer.c: gl_renderer_init_context (SDL_GL context on a SDL_WINDOW_OPENGL window), gl_renderer_present (upload ARGB image -> GL texture -> full-screen quad -> SDL_GL_SwapWindow), present_blank, shutdown. Legacy GL 1.x (immediate mode) so no GL loader needed yet (-lopengl32). Backend vtable still delegates rasterization to sw_* this phase, so the GL path is pixel-identical — this isolates/validates the context+present. - main.cpp: window gains SDL_WINDOW_OPENGL when renderer=opengl; GL context created with graceful fallback to the SDL_Renderer path on failure; present + disabled-frame + cleanup branch on g_gl_active. SW path untouched. - runtime.cmake: link opengl32 (Win) / OpenGL::GL (other). Phase 2b (next): move rasterization onto the GPU — VRAM as a GL texture/FBO, modern-GL loader + shaders, primitives at internal resolution (the actual scaling-without-perf-loss win). Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/include/gpu_gl_renderer.h | 34 ++++++ runtime/runtime.cmake | 9 +- runtime/src/gpu_gl_renderer.c | 172 +++++++++++++++++++++++++++--- runtime/src/main.cpp | 42 ++++++-- 4 files changed, 233 insertions(+), 24 deletions(-) create mode 100644 runtime/include/gpu_gl_renderer.h diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h new file mode 100644 index 00000000..9dc9ff12 --- /dev/null +++ b/runtime/include/gpu_gl_renderer.h @@ -0,0 +1,34 @@ +#ifndef PSX_GPU_GL_RENDERER_H +#define PSX_GPU_GL_RENDERER_H + +/* GL backend context + present entry points, called from main.cpp's window + * setup and present path when [video] renderer = "opengl". The backend's + * rasterization vtable is obtained separately via gl_backend_get() + * (gpu_render.h). SDL_Window is forward-declared (SDL typedefs it from this + * same struct tag) so this header needs no SDL include. */ + +#include + +struct SDL_Window; + +#ifdef __cplusplus +extern "C" { +#endif + +/* Create the GL context on a window made with SDL_WINDOW_OPENGL. + * Returns 1 on success, 0 to fall back to the SDL_Renderer present path. */ +int gl_renderer_init_context(struct SDL_Window *win); + +/* Present an ARGB8888 image (BGRA byte order) as a full-screen quad + swap. */ +void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear); + +/* Clear to black + swap (display-disabled frame). */ +void gl_renderer_present_blank(void); + +void gl_renderer_shutdown(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PSX_GPU_GL_RENDERER_H */ diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 2460c629..349784ac 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -227,7 +227,14 @@ function(psxrecomp_add_runtime_target target) endif() if(WIN32 OR MINGW) - target_link_libraries(${target} PRIVATE ws2_32 dbghelp comdlg32) + # opengl32: GL backend (gpu_gl_renderer.c). GL 1.x is exported directly + # by opengl32; Phase 2b will load modern GL via SDL_GL_GetProcAddress. + target_link_libraries(${target} PRIVATE ws2_32 dbghelp comdlg32 opengl32) + else() + find_package(OpenGL) + if(OpenGL_FOUND) + target_link_libraries(${target} PRIVATE OpenGL::GL) + endif() endif() if(MINGW) diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 40f00b00..d2d716af 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -1,24 +1,164 @@ /* gpu_gl_renderer.c — hardware OpenGL renderer backend. * - * PHASE 1 (current): stub. gl_backend_get() returns NULL, so requesting - * [video] renderer = "opengl" logs a notice and falls back to software (see - * gpu_render.c). This keeps the facade + config + build wiring in place and - * exercised while the real backend is built up. + * PHASE 2a (current): GL context + present. The window gets a real OpenGL + * context (created by main.cpp via gl_renderer_init_context) and the final + * image is uploaded to a GL texture and drawn as a full-screen quad with + * SDL_GL_SwapWindow — replacing the SDL_Renderer 2D blit for the opengl path. * - * PHASE 2+: this file will create an SDL OpenGL context, hold VRAM as a GPU - * texture/FBO, rasterize PS1 primitives via shaders at the configured internal - * resolution, and present. It will then return a populated GpuRenderBackend - * table here. See gpu_render.h for the backend interface. + * Rasterization in this phase still runs through the software rasterizer: the + * backend vtable delegates every draw/transfer call to the existing sw_* + * functions, so the GL path produces a pixel-identical image to software. This + * validates the GL window/context/present plumbing in isolation. * - * The PS1 GPU model the backend must reproduce: 1024x512 16-bit (RGB555+mask) - * VRAM; textured polys sample 4/8/15-bit texels via CLUT from VRAM; four - * semi-transparency blend modes; texture windows; dithering; mask bit; 15/24 - * bit display readout. Coherency (game reads VRAM back) is the hard part — - * handled via on-demand GPU->CPU download in vram_transfer_out / vram_read. */ + * PHASE 2b (next): replace the delegating draw_* members with real GPU + * rasterization — VRAM as a GL texture/FBO, shaders that sample 4/8/15-bit + * texels via CLUT, primitives drawn at internal resolution. Only the vtable + * members change; the context/present scaffolding here stays. + * + * Uses legacy GL 1.x (immediate mode, no shader/FBO) for the present quad so + * Phase 2a needs no GL function loader — just -lopengl32. Phase 2b adds a + * modern-GL loader via SDL_GL_GetProcAddress. */ #include "gpu_render.h" +#include "gpu_sw_renderer.h" -const GpuRenderBackend *gl_backend_get(void) { - /* Phase 1: not yet implemented. */ - return 0; +#include +#include +#include + +/* GL 1.2 enums MinGW's may not declare (it ships GL 1.1 prototypes). */ +#ifndef GL_BGRA +#define GL_BGRA 0x80E1 +#endif +#ifndef GL_CLAMP_TO_EDGE +#define GL_CLAMP_TO_EDGE 0x812F +#endif + +/* ---- GL context + present state ---------------------------------------- */ +static SDL_Window *s_win = NULL; +static SDL_GLContext s_ctx = NULL; +static GLuint s_tex = 0; +static int s_tex_w = 0, s_tex_h = 0; /* current texture alloc */ + +/* Create the GL context on an already-created SDL window (must have been + * created with SDL_WINDOW_OPENGL). Returns 1 on success, 0 to fall back to + * the software/SDL_Renderer present path. Called from main.cpp. */ +int gl_renderer_init_context(SDL_Window *win) { + s_win = win; + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + s_ctx = SDL_GL_CreateContext(win); + if (!s_ctx) { + fprintf(stdout, "psxrecomp: GL context creation failed (%s) — " + "falling back to software present\n", SDL_GetError()); + return 0; + } + if (SDL_GL_MakeCurrent(win, s_ctx) != 0) { + fprintf(stdout, "psxrecomp: SDL_GL_MakeCurrent failed (%s)\n", SDL_GetError()); + SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; + return 0; + } + SDL_GL_SetSwapInterval(1); /* vsync; the wall-clock pacer still rules timing */ + + glGenTextures(1, &s_tex); + glBindTexture(GL_TEXTURE_2D, s_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); + 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 : "?"); + return 1; } + +void gl_renderer_shutdown(void) { + if (s_ctx) { + if (s_tex) { glDeleteTextures(1, &s_tex); s_tex = 0; } + SDL_GL_DeleteContext(s_ctx); + s_ctx = NULL; + } +} + +/* Upload an ARGB8888 (little-endian: BGRA bytes) image and draw it as a + * full-screen quad. `linear` selects the upscale filter (antialiasing). */ +void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear) { + if (!s_ctx) return; + int win_w = 0, win_h = 0; + SDL_GL_GetDrawableSize(s_win, &win_w, &win_h); + glViewport(0, 0, win_w, win_h); + glClearColor(0.f, 0.f, 0.f, 1.f); + glClear(GL_COLOR_BUFFER_BIT); + + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, s_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)allocate the texture only when the source size changes; otherwise + * sub-update for speed. ARGB8888 in memory is BGRA byte order. */ + if (src_w != s_tex_w || src_h != s_tex_h) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, src_w, src_h, 0, + GL_BGRA, GL_UNSIGNED_BYTE, pixels); + s_tex_w = src_w; s_tex_h = src_h; + } else { + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, src_w, src_h, + GL_BGRA, GL_UNSIGNED_BYTE, pixels); + } + + /* Full-screen quad. GL clip space: (-1,-1) bottom-left. The source image is + * top-down, so flip V (top row -> +1). */ + glBegin(GL_QUADS); + glTexCoord2f(0.f, 0.f); glVertex2f(-1.f, 1.f); + glTexCoord2f(1.f, 0.f); glVertex2f( 1.f, 1.f); + glTexCoord2f(1.f, 1.f); glVertex2f( 1.f, -1.f); + glTexCoord2f(0.f, 1.f); glVertex2f(-1.f, -1.f); + glEnd(); + + SDL_GL_SwapWindow(s_win); +} + +void gl_renderer_present_blank(void) { + if (!s_ctx) return; + int win_w = 0, win_h = 0; + SDL_GL_GetDrawableSize(s_win, &win_w, &win_h); + glViewport(0, 0, win_w, win_h); + glClearColor(0.f, 0.f, 0.f, 1.f); + glClear(GL_COLOR_BUFFER_BIT); + SDL_GL_SwapWindow(s_win); +} + +/* ---- Backend vtable (Phase 2a: delegate rasterization to software) ------ */ +static const GpuRenderBackend GL_BACKEND = { + .name = "opengl", + .init = sw_renderer_init, + .set_scale = sw_renderer_set_scale, + .scale = sw_renderer_scale, + .set_texture_filter = sw_set_texture_filter, + .texture_filter = sw_texture_filter, + .set_semi_transparency = sw_set_semi_transparency, + .set_mask_bits = sw_set_mask_bits, + .set_texture_window = sw_set_texture_window, + .set_color_modulation = sw_set_color_modulation, + .fill_rect = sw_fill_rect, + .copy_rect = sw_copy_rect, + .draw_flat_triangle = sw_draw_flat_triangle, + .draw_gouraud_triangle = sw_draw_gouraud_triangle, + .draw_textured_triangle = sw_draw_textured_triangle, + .draw_shaded_textured_triangle = sw_draw_shaded_textured_triangle, + .draw_flat_rect = sw_draw_flat_rect, + .draw_textured_rect = sw_draw_textured_rect, + .draw_textured_rect_scaled = sw_draw_textured_rect_scaled, + .draw_line = sw_draw_line, + .draw_shaded_line = sw_draw_shaded_line, + .render_display = sw_render_display, + .render_display_hires = sw_render_display_hires, + .vram_write = sw_vram_write, + .vram_read = sw_vram_read, + .vram_transfer_in = sw_vram_transfer_in, + .vram_transfer_out = sw_vram_transfer_out, + .set_draw_area = sw_set_draw_area, + .get_draw_area = sw_get_draw_area, + .set_draw_offset = sw_set_draw_offset, +}; + +const GpuRenderBackend *gl_backend_get(void) { return &GL_BACKEND; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 44e0aa11..c26149e0 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -16,6 +16,7 @@ #include "gpu.h" #include "gpu_sw_renderer.h" #include "gpu_render.h" +#include "gpu_gl_renderer.h" #include "frame_pacing.h" #include "sio.h" #include "spu.h" @@ -101,7 +102,8 @@ static int s_fast_boot_active = 0; /* cleared when game entry PC fire static int g_video_scale = 1; /* internal-resolution SSAA factor */ static bool g_video_aa = true; /* linear present filtering */ static int g_video_texfilter = 0; /* 0=nearest, 1=bilinear */ -static int g_video_renderer = 0; /* 0=software, 1=opengl */ +static int g_video_renderer = 0; /* 0=software, 1=opengl (requested) */ +static bool g_gl_active = false; /* GL context live -> GL present path */ /* Vsync self-heal state (see SDL_RenderPresent wrapper in * sdl_vblank_present). C linkage: freeze_heartbeat.c includes both in @@ -1099,9 +1101,13 @@ static void sdl_vblank_present(void) { #ifndef PSX_SDL_NO_RENDER if (!disabled_frame_presented) { disabled_frame_presented = true; - SDL_SetRenderDrawColor(sdl_renderer, 0, 0, 0, 255); - SDL_RenderClear(sdl_renderer); - SDL_RenderPresent(sdl_renderer); + if (g_gl_active) { + gl_renderer_present_blank(); + } else { + SDL_SetRenderDrawColor(sdl_renderer, 0, 0, 0, 255); + SDL_RenderClear(sdl_renderer); + SDL_RenderPresent(sdl_renderer); + } } #endif return; @@ -1135,6 +1141,12 @@ static void sdl_vblank_present(void) { #ifndef PSX_SDL_NO_RENDER int src_w = (int)w * active_scale; int src_h = (int)h * active_scale; + if (g_gl_active) { + /* OpenGL present: upload the active display rect and draw a full-screen + * quad. SDL_GL_SwapWindow handles vsync; the wall-clock pacer above + * still owns timing. */ + gl_renderer_present(sdl_pixel_buf, src_w, src_h, g_video_aa ? 1 : 0); + } else { SDL_Rect src = { 0, 0, src_w, src_h }; SDL_UpdateTexture(sdl_texture, &src, sdl_pixel_buf, (int)(src_w * sizeof(uint32_t))); @@ -1166,6 +1178,7 @@ static void sdl_vblank_present(void) { } } } + } #endif } @@ -1390,17 +1403,27 @@ int main(int argc, char** argv) { } #endif + Uint32 win_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE; + if (g_video_renderer == 1) win_flags |= SDL_WINDOW_OPENGL; sdl_window = SDL_CreateWindow( window_title.c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, - SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE + win_flags ); if (!sdl_window) { std::fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError()); return 1; } + /* OpenGL backend: create the GL context now. On failure, relabel the + * facade back to software (rasterization already runs through software in + * this phase) and fall through to the SDL_Renderer present path below. */ + if (g_video_renderer == 1) { + g_gl_active = (gl_renderer_init_context(sdl_window) != 0); + if (!g_gl_active) gr_set_backend(GR_BACKEND_SOFTWARE); + } + /* Force OpenGL renderer. * * History (TombaRecomp/ISSUES.md #6): @@ -1428,6 +1451,7 @@ int main(int argc, char** argv) { * presentation hang; macOS/Linux have no GDI path, so let SDL choose its * native backend (Metal on Apple Silicon). PRESENTVSYNC removes tearing; * fall back progressively if a driver can't provide vsync/accel. */ + if (!g_gl_active) { #ifdef _WIN32 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl"); #endif @@ -1446,6 +1470,7 @@ int main(int argc, char** argv) { * default window when supersampling is off, so native rendering is * unchanged. */ SDL_RenderSetLogicalSize(sdl_renderer, 640 * g_video_scale, 480 * g_video_scale); + } /* Staging buffer + backing texture are sized for the internal resolution * (640x512 native, times the supersampling factor). */ @@ -1456,6 +1481,7 @@ int main(int argc, char** argv) { return 1; } + if (!g_gl_active) { sdl_texture = SDL_CreateTexture( sdl_renderer, SDL_PIXELFORMAT_ARGB8888, @@ -1468,6 +1494,7 @@ int main(int argc, char** argv) { } SDL_SetTextureScaleMode(sdl_texture, g_video_aa ? SDL_ScaleModeLinear : SDL_ScaleModeNearest); + } /* Register vblank presentation callback. */ gpu_set_vblank_callback(sdl_vblank_present); @@ -1604,8 +1631,9 @@ int main(int argc, char** argv) { std::fprintf(stdout, "psxrecomp runtime: execution completed, PC=0x%08X\n", cpu.pc); shutdown_runtime(); - SDL_DestroyTexture(sdl_texture); - SDL_DestroyRenderer(sdl_renderer); + if (g_gl_active) gl_renderer_shutdown(); + SDL_DestroyTexture(sdl_texture); /* NULL-safe in GL mode */ + SDL_DestroyRenderer(sdl_renderer); /* NULL-safe in GL mode */ SDL_DestroyWindow(sdl_window); SDL_Quit(); From 25fc85b2e58f9cce1d36e70c9e3d702498440497 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 11:39:18 -0700 Subject: [PATCH 03/18] gl-renderer: show active renderer ([OpenGL]/[Software]) in the window title So it's unambiguous at a glance which backend is live during play/testing. Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/src/main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index c26149e0..f4ca4ee5 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1423,6 +1423,9 @@ int main(int argc, char** argv) { g_gl_active = (gl_renderer_init_context(sdl_window) != 0); if (!g_gl_active) gr_set_backend(GR_BACKEND_SOFTWARE); } + /* Make the active renderer visible in the title bar (no guessing). */ + SDL_SetWindowTitle(sdl_window, + (window_title + (g_gl_active ? " [OpenGL]" : " [Software]")).c_str()); /* Force OpenGL renderer. * From b509e8ffa77557d388dced853339a0f188426deb Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 11:50:22 -0700 Subject: [PATCH 04/18] gl-renderer phase 2b step 1: modern-GL loader + shader present pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the legacy immediate-mode present quad with a modern-GL pipeline: SDL_GL_GetProcAddress loader for the GL 2.0+/3.0 entry points, a compiled vertex+fragment present program, and a fullscreen-triangle draw via an empty VAO. Verified: Tomba renders correctly via the shader path (log "GL present path = shader", GL 4.6 NVIDIA). On any loader/compile/link failure it degrades to the immediate-mode quad, so present never black-screens. Rasterization still delegates to software (vtable unchanged) — this isolates and validates the shader/VAO plumbing that the GPU rasterizer builds on next: VRAM as a GL texture + an FBO at internal resolution, primitive shaders. Avoids GLchar/GLsizeiptr (absent in MinGW GL 1.1 headers) by spelling loaded prototypes with char/ptrdiff_t (ABI-identical). Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/src/gpu_gl_renderer.c | 236 +++++++++++++++++++++++++++------- 1 file changed, 191 insertions(+), 45 deletions(-) diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index d2d716af..62184f98 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -1,48 +1,171 @@ /* gpu_gl_renderer.c — hardware OpenGL renderer backend. * - * PHASE 2a (current): GL context + present. The window gets a real OpenGL - * context (created by main.cpp via gl_renderer_init_context) and the final - * image is uploaded to a GL texture and drawn as a full-screen quad with - * SDL_GL_SwapWindow — replacing the SDL_Renderer 2D blit for the opengl path. + * PHASE 2b step 1 (current): modern-GL substrate. A GL function loader + * (SDL_GL_GetProcAddress) plus a shader + VAO present pipeline replaces the + * legacy immediate-mode quad. The display image is uploaded to a GL texture + * and drawn with a fullscreen-triangle shader. If modern-GL init fails for any + * reason, present degrades to the legacy immediate-mode path (never a black + * screen). Rasterization still runs through software via the backend vtable, + * so output stays pixel-identical — this isolates and validates the shader + * plumbing that GPU rasterization (next steps) is built on. * - * Rasterization in this phase still runs through the software rasterizer: the - * backend vtable delegates every draw/transfer call to the existing sw_* - * functions, so the GL path produces a pixel-identical image to software. This - * validates the GL window/context/present plumbing in isolation. + * PHASE 2b next: VRAM as a GL texture + an FBO at internal resolution; + * primitive shaders rasterize geometry into the FBO; present samples the FBO. * - * PHASE 2b (next): replace the delegating draw_* members with real GPU - * rasterization — VRAM as a GL texture/FBO, shaders that sample 4/8/15-bit - * texels via CLUT, primitives drawn at internal resolution. Only the vtable - * members change; the context/present scaffolding here stays. - * - * Uses legacy GL 1.x (immediate mode, no shader/FBO) for the present quad so - * Phase 2a needs no GL function loader — just -lopengl32. Phase 2b adds a - * modern-GL loader via SDL_GL_GetProcAddress. */ + * GL usage: GL 1.x (textures, immediate-mode fallback) comes from opengl32; + * GL 2.0+/3.0+ entry points (shaders, VAO) are loaded at runtime. We avoid the + * GLchar/GLsizeiptr typedefs (absent from MinGW's ) by spelling the + * loaded prototypes with char/ptrdiff_t, which are ABI-identical. */ #include "gpu_render.h" #include "gpu_sw_renderer.h" #include #include +#include #include -/* GL 1.2 enums MinGW's may not declare (it ships GL 1.1 prototypes). */ #ifndef GL_BGRA #define GL_BGRA 0x80E1 #endif #ifndef GL_CLAMP_TO_EDGE #define GL_CLAMP_TO_EDGE 0x812F #endif +/* GL 2.0+ enums (not in MinGW 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 + +#ifndef APIENTRY +#define APIENTRY +#endif + +/* ---- 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_glGenVertexArrays)(GLsizei, GLuint *); +typedef void (APIENTRY *PFN_glBindVertexArray)(GLuint); +typedef void (APIENTRY *PFN_glActiveTexture)(GLenum); + +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_glGenVertexArrays p_glGenVertexArrays; +static PFN_glBindVertexArray p_glBindVertexArray; +static PFN_glActiveTexture p_glActiveTexture; + +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_glGenVertexArrays, "glGenVertexArrays"); + LOAD(p_glBindVertexArray, "glBindVertexArray"); + LOAD(p_glActiveTexture, "glActiveTexture"); +#undef LOAD + return ok; +} /* ---- GL context + present state ---------------------------------------- */ static SDL_Window *s_win = NULL; static SDL_GLContext s_ctx = NULL; static GLuint s_tex = 0; -static int s_tex_w = 0, s_tex_h = 0; /* current texture alloc */ +static int s_tex_w = 0, s_tex_h = 0; +static int s_modern_ok = 0; /* shader pipeline available */ +static GLuint s_prog = 0; /* present shader program */ +static GLuint s_vao = 0; /* empty VAO for the fullscreen triangle */ +static GLint s_uTex = -1; + +static const char *PRESENT_VS = + "#version 330\n" + "out vec2 v_uv;\n" + "void main(){\n" + " vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);\n" /* (0,0)(2,0)(0,2) */ + " v_uv = vec2(p.x, 1.0 - p.y);\n" /* flip V: src is top-down */ + " gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n" + "}\n"; + +static const char *PRESENT_FS = + "#version 330\n" + "in vec2 v_uv;\n" + "uniform sampler2D u_tex;\n" + "out vec4 frag;\n" + "void main(){ frag = texture(u_tex, v_uv); }\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_src, const char *fs_src) { + GLuint vs = compile_shader(PSXGL_VERTEX_SHADER, vs_src); + GLuint fs = compile_shader(PSXGL_FRAGMENT_SHADER, fs_src); + if (!vs || !fs) return 0; + GLuint prog = p_glCreateProgram(); + p_glAttachShader(prog, vs); + p_glAttachShader(prog, fs); + p_glLinkProgram(prog); + p_glDeleteShader(vs); + p_glDeleteShader(fs); + GLint ok = 0; + p_glGetProgramiv(prog, PSXGL_LINK_STATUS, &ok); + if (!ok) { + char log[1024]; log[0] = 0; + p_glGetProgramInfoLog(prog, sizeof log, NULL, log); + fprintf(stdout, "psxrecomp: GL program link failed: %s\n", log); + return 0; + } + return prog; +} -/* Create the GL context on an already-created SDL window (must have been - * created with SDL_WINDOW_OPENGL). Returns 1 on success, 0 to fall back to - * the software/SDL_Renderer present path. Called from main.cpp. */ int gl_renderer_init_context(SDL_Window *win) { s_win = win; SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); @@ -57,7 +180,7 @@ int gl_renderer_init_context(SDL_Window *win) { SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; return 0; } - SDL_GL_SetSwapInterval(1); /* vsync; the wall-clock pacer still rules timing */ + SDL_GL_SetSwapInterval(1); glGenTextures(1, &s_tex); glBindTexture(GL_TEXTURE_2D, s_tex); @@ -68,6 +191,20 @@ int gl_renderer_init_context(SDL_Window *win) { const char *ver = (const char *)glGetString(GL_VERSION); fprintf(stdout, "psxrecomp: OpenGL context created (%s)\n", ver ? ver : "?"); + + /* Modern-GL present pipeline (optional; falls back to immediate mode). */ + s_modern_ok = load_modern_gl(); + if (s_modern_ok) { + s_prog = build_program(PRESENT_VS, PRESENT_FS); + if (s_prog) { + p_glGenVertexArrays(1, &s_vao); + s_uTex = p_glGetUniformLocation(s_prog, "u_tex"); + } else { + s_modern_ok = 0; + } + } + fprintf(stdout, "psxrecomp: GL present path = %s\n", + s_modern_ok ? "shader" : "immediate(legacy)"); return 1; } @@ -79,23 +216,11 @@ void gl_renderer_shutdown(void) { } } -/* Upload an ARGB8888 (little-endian: BGRA bytes) image and draw it as a - * full-screen quad. `linear` selects the upscale filter (antialiasing). */ -void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear) { - if (!s_ctx) return; - int win_w = 0, win_h = 0; - SDL_GL_GetDrawableSize(s_win, &win_w, &win_h); - glViewport(0, 0, win_w, win_h); - glClearColor(0.f, 0.f, 0.f, 1.f); - glClear(GL_COLOR_BUFFER_BIT); - - glEnable(GL_TEXTURE_2D); +/* Upload the ARGB8888 (BGRA byte order) image into s_tex (alloc on size change). */ +static void upload_present_tex(const uint32_t *pixels, int src_w, int src_h, int linear) { glBindTexture(GL_TEXTURE_2D, s_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)allocate the texture only when the source size changes; otherwise - * sub-update for speed. ARGB8888 in memory is BGRA byte order. */ if (src_w != s_tex_w || src_h != s_tex_h) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, src_w, src_h, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels); @@ -104,15 +229,36 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, src_w, src_h, GL_BGRA, GL_UNSIGNED_BYTE, pixels); } +} - /* Full-screen quad. GL clip space: (-1,-1) bottom-left. The source image is - * top-down, so flip V (top row -> +1). */ - glBegin(GL_QUADS); - glTexCoord2f(0.f, 0.f); glVertex2f(-1.f, 1.f); - glTexCoord2f(1.f, 0.f); glVertex2f( 1.f, 1.f); - glTexCoord2f(1.f, 1.f); glVertex2f( 1.f, -1.f); - glTexCoord2f(0.f, 1.f); glVertex2f(-1.f, -1.f); - glEnd(); +void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear) { + if (!s_ctx) return; + int win_w = 0, win_h = 0; + SDL_GL_GetDrawableSize(s_win, &win_w, &win_h); + glViewport(0, 0, win_w, win_h); + glClearColor(0.f, 0.f, 0.f, 1.f); + glClear(GL_COLOR_BUFFER_BIT); + + p_glActiveTexture(PSXGL_TEXTURE0); + upload_present_tex(pixels, src_w, src_h, linear); + + if (s_modern_ok) { + p_glUseProgram(s_prog); + p_glUniform1i(s_uTex, 0); + p_glBindVertexArray(s_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); /* fullscreen triangle */ + p_glBindVertexArray(0); + p_glUseProgram(0); + } else { + /* Legacy fallback: textured fullscreen quad (V already flipped here). */ + glEnable(GL_TEXTURE_2D); + glBegin(GL_QUADS); + glTexCoord2f(0.f, 0.f); glVertex2f(-1.f, 1.f); + glTexCoord2f(1.f, 0.f); glVertex2f( 1.f, 1.f); + glTexCoord2f(1.f, 1.f); glVertex2f( 1.f, -1.f); + glTexCoord2f(0.f, 1.f); glVertex2f(-1.f, -1.f); + glEnd(); + } SDL_GL_SwapWindow(s_win); } @@ -127,7 +273,7 @@ void gl_renderer_present_blank(void) { SDL_GL_SwapWindow(s_win); } -/* ---- Backend vtable (Phase 2a: delegate rasterization to software) ------ */ +/* ---- Backend vtable (rasterization still delegated to software) --------- */ static const GpuRenderBackend GL_BACKEND = { .name = "opengl", .init = sw_renderer_init, From d7b430cf4f298bae6d63cfa2484e05de62f28696 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 12:08:16 -0700 Subject: [PATCH 05/18] gl-renderer phase 2b step 2: GPU rasterization of opaque flat/gouraud polys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First real GPU rasterization. VRAM is mirrored as an RGBA8 GL texture bound to an FBO (the render target); opaque flat and gouraud triangles are rasterized on the GPU into that FBO via a geometry shader. Everything else (textured polys, rects/sprites, lines, fills, semi-transparent polys, VRAM transfers/copies) stays on the software rasterizer over CPU VRAM. Coherency: two dirty flags keep CPU VRAM and the FBO in sync — a GPU draw uploads CPU VRAM if the CPU side is ahead; a software op or the display readout reads the FBO back (glReadPixels) if the GPU side is ahead. So both paths interleave correctly in PS1 draw order. The sync is entirely inside the backend vtable, so main.cpp/present are unchanged. Verified on Tomba: GPU raster path enabled (FBO complete), renders correctly and matches software A/B (Whoopee Camp logo intro), no corruption/crash. Known/expected: the per-op full-VRAM sync is simple, not fast — with most drawing still software it thrashes and runs slow. That disappears as textured polys move to the GPU (next steps): textured-poly CLUT sampling shader, then semi-transparency/dither/mask, then internal-res scaling, then drop the readback and keep VRAM GPU-side. Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/src/gpu_gl_renderer.c | 496 ++++++++++++++++++++++------------ 1 file changed, 317 insertions(+), 179 deletions(-) diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 62184f98..f5056eb5 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -1,21 +1,25 @@ /* gpu_gl_renderer.c — hardware OpenGL renderer backend. * - * PHASE 2b step 1 (current): modern-GL substrate. A GL function loader - * (SDL_GL_GetProcAddress) plus a shader + VAO present pipeline replaces the - * legacy immediate-mode quad. The display image is uploaded to a GL texture - * and drawn with a fullscreen-triangle shader. If modern-GL init fails for any - * reason, present degrades to the legacy immediate-mode path (never a black - * screen). Rasterization still runs through software via the backend vtable, - * so output stays pixel-identical — this isolates and validates the shader - * plumbing that GPU rasterization (next steps) is built on. + * PHASE 2b step 2 (current): first real GPU rasterization. VRAM is mirrored as + * a GL texture bound to an FBO (the render target); OPAQUE flat and gouraud + * triangles are rasterized on the GPU into that FBO. Everything else — + * textured polys, rects/sprites, lines, fills, semi-transparent polys, VRAM + * transfers/copies — still runs on the software rasterizer over CPU VRAM. * - * PHASE 2b next: VRAM as a GL texture + an FBO at internal resolution; - * primitive shaders rasterize geometry into the FBO; present samples the FBO. + * Coherency: CPU VRAM and the FBO are kept in sync with two dirty flags. A GPU + * draw first uploads CPU VRAM if the CPU side is ahead; a software op (or the + * display readout) first reads the FBO back if the GPU side is ahead. So the + * two paths interleave correctly in PS1 draw order. The sync lives entirely in + * the backend vtable — main.cpp/present are unchanged (they still read the + * software display via render_display, which syncs the FBO down first). * - * GL usage: GL 1.x (textures, immediate-mode fallback) comes from opengl32; - * GL 2.0+/3.0+ entry points (shaders, VAO) are loaded at runtime. We avoid the - * GLchar/GLsizeiptr typedefs (absent from MinGW's ) by spelling the - * loaded prototypes with char/ptrdiff_t, which are ABI-identical. */ + * This per-op full-VRAM sync is intentionally simple, not fast: with most of + * Tomba's drawing still software (textured), it thrashes and runs slow. That's + * expected for this step — the win arrives once textured polys move to the GPU + * too (next steps), at which point the sync largely disappears. + * + * Present pipeline + GL loader: see the modern-GL section below (shared with + * step 1). Legacy immediate-mode present remains the fallback. */ #include "gpu_render.h" #include "gpu_sw_renderer.h" @@ -24,6 +28,8 @@ #include #include #include +#include +#include #ifndef GL_BGRA #define GL_BGRA 0x80E1 @@ -31,17 +37,28 @@ #ifndef GL_CLAMP_TO_EDGE #define GL_CLAMP_TO_EDGE 0x812F #endif -/* GL 2.0+ enums (not in MinGW 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 +#ifndef GL_RGBA8 +#define GL_RGBA8 0x8058 +#endif +/* GL 2.0+/3.0 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_COLOR_ATTACHMENT0 0x8CE0 +#define PSXGL_FRAMEBUFFER_COMPLETE 0x8CD5 #ifndef APIENTRY #define APIENTRY #endif +#define VRAM_W 1024 +#define VRAM_H 512 + /* ---- Loaded modern-GL entry points ------------------------------------- */ typedef GLuint (APIENTRY *PFN_glCreateShader)(GLenum); typedef void (APIENTRY *PFN_glShaderSource)(GLuint, GLsizei, const char *const *, const GLint *); @@ -60,6 +77,15 @@ typedef void (APIENTRY *PFN_glUniform1i)(GLint, GLint); 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); static PFN_glCreateShader p_glCreateShader; static PFN_glShaderSource p_glShaderSource; @@ -78,233 +104,345 @@ static PFN_glUniform1i p_glUniform1i; 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 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_glGenVertexArrays, "glGenVertexArrays"); - LOAD(p_glBindVertexArray, "glBindVertexArray"); - LOAD(p_glActiveTexture, "glActiveTexture"); + 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_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"); #undef LOAD return ok; } -/* ---- GL context + present state ---------------------------------------- */ +/* ---- state ------------------------------------------------------------- */ static SDL_Window *s_win = NULL; static SDL_GLContext s_ctx = NULL; -static GLuint s_tex = 0; -static int s_tex_w = 0, s_tex_h = 0; -static int s_modern_ok = 0; /* shader pipeline available */ -static GLuint s_prog = 0; /* present shader program */ -static GLuint s_vao = 0; /* empty VAO for the fullscreen triangle */ -static GLint s_uTex = -1; +static uint16_t *s_vram = NULL; /* CPU VRAM (authoritative mirror) */ +static GLuint s_present_tex = 0; +static int s_present_w = 0, s_present_h = 0; +static int s_modern_ok = 0; +static GLuint s_present_prog = 0, s_present_vao = 0; +static GLint s_present_uTex = -1; + +/* GPU rasterization target: VRAM as an RGBA8 texture + FBO. */ +static int s_raster_ok = 0; /* GPU geometry path available */ +static GLuint s_vram_tex = 0; +static GLuint s_fbo = 0; +static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0; +static int s_cpu_dirty = 0; /* CPU VRAM has changes not in FBO */ +static int s_gpu_dirty = 0; /* FBO has changes not in CPU VRAM */ +static uint32_t *s_conv = NULL; /* 1024*512 RGBA8 staging for sync */ + +/* draw state mirrored from the vtable set_* calls (for GPU prims) */ +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; + +/* ---- shaders ----------------------------------------------------------- */ static const char *PRESENT_VS = "#version 330\n" "out vec2 v_uv;\n" - "void main(){\n" - " vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);\n" /* (0,0)(2,0)(0,2) */ - " v_uv = vec2(p.x, 1.0 - p.y);\n" /* flip V: src is top-down */ - " gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n" - "}\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;\n" - "uniform sampler2D u_tex;\n" - "out vec4 frag;\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), color in + * 0..1. Transform to clip space over the 1024x512 VRAM; no Y flip (VRAM y=0 -> + * clip.y=-1 -> FBO row 0 -> glReadPixels row 0 -> CPU VRAM row 0). */ +static const char *GEO_VS = + "#version 330\n" + "layout(location=0) in vec2 a_pos;\n" + "layout(location=1) in vec4 a_col;\n" + "out vec4 v_col;\n" + "void main(){ v_col = a_col;\n" + " float cx = a_pos.x/512.0 - 1.0;\n" + " float cy = a_pos.y/256.0 - 1.0;\n" + " gl_Position = vec4(cx, cy, 0.0, 1.0); }\n"; +static const char *GEO_FS = + "#version 330\n" + "in vec4 v_col; out vec4 frag;\n" + "void main(){ frag = v_col; }\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); + 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; - } + 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; +} -static GLuint build_program(const char *vs_src, const char *fs_src) { - GLuint vs = compile_shader(PSXGL_VERTEX_SHADER, vs_src); - GLuint fs = compile_shader(PSXGL_FRAGMENT_SHADER, fs_src); - if (!vs || !fs) return 0; - GLuint prog = p_glCreateProgram(); - p_glAttachShader(prog, vs); - p_glAttachShader(prog, fs); - p_glLinkProgram(prog); - p_glDeleteShader(vs); - p_glDeleteShader(fs); - GLint ok = 0; - p_glGetProgramiv(prog, PSXGL_LINK_STATUS, &ok); - if (!ok) { - char log[1024]; log[0] = 0; - p_glGetProgramInfoLog(prog, sizeof log, NULL, log); - fprintf(stdout, "psxrecomp: GL program link failed: %s\n", log); - return 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 */ +} +static inline uint16_t conv_rgba8_to_1555(uint32_t c) { + uint16_t r = (c & 0xFF) >> 3, g = ((c >> 8) & 0xFF) >> 3, b = ((c >> 16) & 0xFF) >> 3; + uint16_t m = ((c >> 24) & 0x80) ? 0x8000 : 0; + return (uint16_t)(r | (g << 5) | (b << 10) | m); +} + +/* ---- coherency --------------------------------------------------------- */ +static void ensure_gpu(void) { /* make the FBO reflect CPU VRAM */ + if (!s_raster_ok || !s_cpu_dirty) return; + for (int i = 0; i < VRAM_W * VRAM_H; i++) s_conv[i] = conv_1555_to_rgba8(s_vram[i]); + glBindTexture(GL_TEXTURE_2D, s_vram_tex); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VRAM_W, VRAM_H, GL_RGBA, GL_UNSIGNED_BYTE, s_conv); + s_cpu_dirty = 0; +} +static void ensure_cpu(void) { /* make CPU VRAM reflect the FBO */ + if (!s_raster_ok || !s_gpu_dirty) return; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); + glReadPixels(0, 0, VRAM_W, VRAM_H, GL_RGBA, GL_UNSIGNED_BYTE, s_conv); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + for (int i = 0; i < VRAM_W * VRAM_H; i++) s_vram[i] = conv_rgba8_to_1555(s_conv[i]); + s_gpu_dirty = 0; +} + +/* Rasterize one triangle (3 verts: x,y in raw coords; colors 1555) on the GPU. */ +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) { + ensure_gpu(); + float verts[3*6]; + int xs[3] = {x0+s_off_x, x1+s_off_x, x2+s_off_x}; + int ys[3] = {y0+s_off_y, y1+s_off_y, y2+s_off_y}; + uint16_t cs[3] = {c0,c1,c2}; + for (int i = 0; i < 3; 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] = 1.0f; } - return prog; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); + glViewport(0, 0, VRAM_W, VRAM_H); + glEnable(GL_SCISSOR_TEST); + int sx = s_area_x1, sy = s_area_y1; + 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(sx, sy, sw, sh); /* FBO y matches VRAM y (no flip) */ + glDisable(GL_BLEND); /* opaque only in this step */ + p_glUseProgram(s_geo_prog); + 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, 3); + p_glBindVertexArray(0); + p_glUseProgram(0); + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + s_gpu_dirty = 1; +} + +/* ---- backend vtable wrappers ------------------------------------------- */ +static void glb_init(uint16_t *vram) { s_vram = vram; sw_renderer_init(vram); } +static void glb_set_scale(int s) { sw_renderer_set_scale(s); } +static int glb_scale(void) { return sw_renderer_scale(); } +static void glb_set_texture_filter(int b) { sw_set_texture_filter(b); } +static int glb_texture_filter(void) { return sw_texture_filter(); } + +static void glb_set_semi_transparency(int e, int m) { s_semi_en = e; s_semi_mode = m; sw_set_semi_transparency(e, m); } +static void glb_set_mask_bits(int s, int c) { sw_set_mask_bits(s, c); } +static void glb_set_texture_window(uint32_t r) { sw_set_texture_window(r); } +static void glb_set_color_modulation(int r,int g,int b,int 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); } + +/* GPU-rasterized: opaque flat / gouraud triangles. Semi-transparent -> SW. */ +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 && !s_semi_en) { gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col); return; } + ensure_cpu(); sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col); s_cpu_dirty = 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 && !s_semi_en) { gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2); return; } + ensure_cpu(); sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2); s_cpu_dirty = 1; +} + +/* Everything else: software, with the FBO read back first if it's ahead. */ +static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){ ensure_cpu(); sw_fill_rect(x,y,w,h,c); s_cpu_dirty=1; } +static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){ ensure_cpu(); sw_copy_rect(sx,sy,dx,dy,w,h); s_cpu_dirty=1; } +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){ ensure_cpu(); sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp); s_cpu_dirty=1; } +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){ ensure_cpu(); sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); s_cpu_dirty=1; } +static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){ ensure_cpu(); sw_draw_flat_rect(x,y,w,h,c); s_cpu_dirty=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){ ensure_cpu(); sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp); s_cpu_dirty=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){ ensure_cpu(); sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp); s_cpu_dirty=1; } +static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){ ensure_cpu(); sw_draw_line(x0,y0,x1,y1,c); s_cpu_dirty=1; } +static void glb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1){ ensure_cpu(); sw_draw_shaded_line(x0,y0,c0,x1,y1,c1); s_cpu_dirty=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){ ensure_cpu(); sw_vram_write(x,y,px); s_cpu_dirty=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){ ensure_cpu(); sw_vram_transfer_in(x,y,w,h,d); s_cpu_dirty=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); + } +} + +static void init_gpu_raster(void) { + s_geo_prog = build_program(GEO_VS, GEO_FS); + if (!s_geo_prog) { fprintf(stdout, "psxrecomp: GL geometry program failed; GPU raster off\n"); return; } + s_conv = (uint32_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint32_t)); + if (!s_conv) return; + glGenTextures(1, &s_vram_tex); + glBindTexture(GL_TEXTURE_2D, s_vram_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, VRAM_W, VRAM_H, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + p_glGenFramebuffers(1, &s_fbo); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); + p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, s_vram_tex, 0); + 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; } + 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_glBindVertexArray(0); + s_cpu_dirty = 1; /* FBO empty; first GPU draw uploads current VRAM */ + s_raster_ok = 1; + fprintf(stdout, "psxrecomp: GPU rasterization enabled (opaque flat/gouraud polys)\n"); } int gl_renderer_init_context(SDL_Window *win) { s_win = win; SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); s_ctx = SDL_GL_CreateContext(win); - if (!s_ctx) { - fprintf(stdout, "psxrecomp: GL context creation failed (%s) — " - "falling back to software present\n", SDL_GetError()); - return 0; - } - if (SDL_GL_MakeCurrent(win, s_ctx) != 0) { - fprintf(stdout, "psxrecomp: SDL_GL_MakeCurrent failed (%s)\n", SDL_GetError()); - SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; - return 0; - } + 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); - - glGenTextures(1, &s_tex); - glBindTexture(GL_TEXTURE_2D, s_tex); + 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); - glDisable(GL_DEPTH_TEST); - glDisable(GL_CULL_FACE); - + 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 : "?"); - /* Modern-GL present pipeline (optional; falls back to immediate mode). */ s_modern_ok = load_modern_gl(); if (s_modern_ok) { - s_prog = build_program(PRESENT_VS, PRESENT_FS); - if (s_prog) { - p_glGenVertexArrays(1, &s_vao); - s_uTex = p_glGetUniformLocation(s_prog, "u_tex"); - } else { - s_modern_ok = 0; - } + 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 s_modern_ok = 0; } - fprintf(stdout, "psxrecomp: GL present path = %s\n", - s_modern_ok ? "shader" : "immediate(legacy)"); + fprintf(stdout, "psxrecomp: GL present path = %s\n", s_modern_ok ? "shader" : "immediate(legacy)"); + if (s_modern_ok) init_gpu_raster(); return 1; } void gl_renderer_shutdown(void) { if (s_ctx) { - if (s_tex) { glDeleteTextures(1, &s_tex); s_tex = 0; } - SDL_GL_DeleteContext(s_ctx); - s_ctx = NULL; - } -} - -/* Upload the ARGB8888 (BGRA byte order) image into s_tex (alloc on size change). */ -static void upload_present_tex(const uint32_t *pixels, int src_w, int src_h, int linear) { - glBindTexture(GL_TEXTURE_2D, s_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 (src_w != s_tex_w || src_h != s_tex_h) { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, src_w, src_h, 0, - GL_BGRA, GL_UNSIGNED_BYTE, pixels); - s_tex_w = src_w; s_tex_h = src_h; - } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, src_w, src_h, - GL_BGRA, GL_UNSIGNED_BYTE, pixels); + ensure_cpu(); + if (s_present_tex) glDeleteTextures(1, &s_present_tex); + if (s_vram_tex) glDeleteTextures(1, &s_vram_tex); + SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; } + free(s_conv); s_conv = NULL; } void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear) { if (!s_ctx) return; - int win_w = 0, win_h = 0; - SDL_GL_GetDrawableSize(s_win, &win_w, &win_h); - glViewport(0, 0, win_w, win_h); - glClearColor(0.f, 0.f, 0.f, 1.f); - glClear(GL_COLOR_BUFFER_BIT); - + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + glViewport(0, 0, ww, wh); + glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); p_glActiveTexture(PSXGL_TEXTURE0); upload_present_tex(pixels, src_w, src_h, linear); - if (s_modern_ok) { - p_glUseProgram(s_prog); - p_glUniform1i(s_uTex, 0); - p_glBindVertexArray(s_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); /* fullscreen triangle */ - p_glBindVertexArray(0); - p_glUseProgram(0); + 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); } else { - /* Legacy fallback: textured fullscreen quad (V already flipped here). */ - glEnable(GL_TEXTURE_2D); + glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, s_present_tex); glBegin(GL_QUADS); - glTexCoord2f(0.f, 0.f); glVertex2f(-1.f, 1.f); - glTexCoord2f(1.f, 0.f); glVertex2f( 1.f, 1.f); - glTexCoord2f(1.f, 1.f); glVertex2f( 1.f, -1.f); - glTexCoord2f(0.f, 1.f); glVertex2f(-1.f, -1.f); + glTexCoord2f(0,0); glVertex2f(-1, 1); glTexCoord2f(1,0); glVertex2f(1, 1); + glTexCoord2f(1,1); glVertex2f(1,-1); glTexCoord2f(0,1); glVertex2f(-1,-1); glEnd(); } - SDL_GL_SwapWindow(s_win); } void gl_renderer_present_blank(void) { if (!s_ctx) return; - int win_w = 0, win_h = 0; - SDL_GL_GetDrawableSize(s_win, &win_w, &win_h); - glViewport(0, 0, win_w, win_h); - glClearColor(0.f, 0.f, 0.f, 1.f); - glClear(GL_COLOR_BUFFER_BIT); + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + glViewport(0, 0, ww, wh); glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); SDL_GL_SwapWindow(s_win); } -/* ---- Backend vtable (rasterization still delegated to software) --------- */ static const GpuRenderBackend GL_BACKEND = { - .name = "opengl", - .init = sw_renderer_init, - .set_scale = sw_renderer_set_scale, - .scale = sw_renderer_scale, - .set_texture_filter = sw_set_texture_filter, - .texture_filter = sw_texture_filter, - .set_semi_transparency = sw_set_semi_transparency, - .set_mask_bits = sw_set_mask_bits, - .set_texture_window = sw_set_texture_window, - .set_color_modulation = sw_set_color_modulation, - .fill_rect = sw_fill_rect, - .copy_rect = sw_copy_rect, - .draw_flat_triangle = sw_draw_flat_triangle, - .draw_gouraud_triangle = sw_draw_gouraud_triangle, - .draw_textured_triangle = sw_draw_textured_triangle, - .draw_shaded_textured_triangle = sw_draw_shaded_textured_triangle, - .draw_flat_rect = sw_draw_flat_rect, - .draw_textured_rect = sw_draw_textured_rect, - .draw_textured_rect_scaled = sw_draw_textured_rect_scaled, - .draw_line = sw_draw_line, - .draw_shaded_line = sw_draw_shaded_line, - .render_display = sw_render_display, - .render_display_hires = sw_render_display_hires, - .vram_write = sw_vram_write, - .vram_read = sw_vram_read, - .vram_transfer_in = sw_vram_transfer_in, - .vram_transfer_out = sw_vram_transfer_out, - .set_draw_area = sw_set_draw_area, - .get_draw_area = sw_get_draw_area, - .set_draw_offset = sw_set_draw_offset, + .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; } From c2b2959af4de0509fe7c736b3b38bae8614c9560 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 14:58:05 -0700 Subject: [PATCH 06/18] gl-renderer phase 2b step 3: GPU rasterization of textured polys Route opaque textured/shaded-textured triangles to the GPU via a raw 16-bit VRAM integer sampler (GL_R16UI) + a CLUT-decoding fragment shader (4/8/15-bit depths, texel-0 discard, PS1 *2-around-0x80 modulation). The raw VRAM texture mirrors CPU VRAM in ensure_gpu; render-to-texture within a single GPU batch is a known later-step limitation. Semi-transparent textured polys still fall back to software. Two play-test fixes folded in: - Present at native res under the GL backend (glb_set_scale/glb_scale return 1). GPU draws write the FBO only, not the software SSAA hi-res mirror g_hr, so presenting via g_hr dropped every GPU layer ("entire layers missing"). GPU-side hi-res scaling is a later step. - noperspective on GEO/TEX varyings for PS1 affine (non-perspective) parity ("logos look horrible" -> clean). Verified: PlayStation BIOS logo + Tomba face cutscene render correctly via the GPU path. Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/src/gpu_gl_renderer.c | 176 ++++++++++++++++++++++++++++++++-- 1 file changed, 168 insertions(+), 8 deletions(-) diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index f5056eb5..ef55fc6d 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -51,6 +51,9 @@ #define PSXGL_FRAMEBUFFER 0x8D40 #define PSXGL_COLOR_ATTACHMENT0 0x8CE0 #define PSXGL_FRAMEBUFFER_COMPLETE 0x8CD5 +#define PSXGL_R16UI 0x8234 +#define PSXGL_RED_INTEGER 0x8D94 +#define PSXGL_TEXTURE1 0x84C1 #ifndef APIENTRY #define APIENTRY @@ -74,6 +77,7 @@ typedef void (APIENTRY *PFN_glGetProgramInfoLog)(GLuint, GLsizei, GLsizei *, c 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_glUniform2i)(GLint, GLint, GLint); typedef void (APIENTRY *PFN_glGenVertexArrays)(GLsizei, GLuint *); typedef void (APIENTRY *PFN_glBindVertexArray)(GLuint); typedef void (APIENTRY *PFN_glActiveTexture)(GLenum); @@ -101,6 +105,7 @@ static PFN_glGetProgramInfoLog p_glGetProgramInfoLog; static PFN_glUseProgram p_glUseProgram; static PFN_glGetUniformLocation p_glGetUniformLocation; static PFN_glUniform1i p_glUniform1i; +static PFN_glUniform2i p_glUniform2i; static PFN_glGenVertexArrays p_glGenVertexArrays; static PFN_glBindVertexArray p_glBindVertexArray; static PFN_glActiveTexture p_glActiveTexture; @@ -124,6 +129,7 @@ static int load_modern_gl(void) { 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_glUniform2i, "glUniform2i"); LOAD(p_glGenVertexArrays, "glGenVertexArrays"); LOAD(p_glBindVertexArray, "glBindVertexArray"); LOAD(p_glActiveTexture, "glActiveTexture"); LOAD(p_glGenBuffers, "glGenBuffers"); LOAD(p_glBindBuffer, "glBindBuffer"); LOAD(p_glBufferData, "glBufferData"); @@ -152,6 +158,14 @@ static int s_raster_ok = 0; /* GPU geometry path available */ static GLuint s_vram_tex = 0; static GLuint s_fbo = 0; static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0; +/* Textured polys: raw 16-bit VRAM as an integer sampler + a CLUT-decoding + * fragment shader. The raw texture mirrors CPU VRAM (synced in ensure_gpu); + * sampling freshly-GPU-drawn pixels (render-to-texture within a GPU batch) is a + * known limitation handled later — uploaded textures (the common case) work. */ +static GLuint s_vram_raw_tex = 0; +static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0; +static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1, s_uRaw = -1; +static uint16_t *s_raw = NULL; /* 1024*512 raw-uint16 staging */ static int s_cpu_dirty = 0; /* CPU VRAM has changes not in FBO */ static int s_gpu_dirty = 0; /* FBO has changes not in CPU VRAM */ static uint32_t *s_conv = NULL; /* 1024*512 RGBA8 staging for sync */ @@ -160,6 +174,7 @@ static uint32_t *s_conv = NULL; /* 1024*512 RGBA8 staging for sync */ 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; /* ---- shaders ----------------------------------------------------------- */ static const char *PRESENT_VS = @@ -179,16 +194,57 @@ static const char *GEO_VS = "#version 330\n" "layout(location=0) in vec2 a_pos;\n" "layout(location=1) in vec4 a_col;\n" - "out vec4 v_col;\n" + "noperspective out vec4 v_col;\n" "void main(){ v_col = a_col;\n" " float cx = a_pos.x/512.0 - 1.0;\n" " float cy = a_pos.y/256.0 - 1.0;\n" " gl_Position = vec4(cx, cy, 0.0, 1.0); }\n"; static const char *GEO_FS = "#version 330\n" - "in vec4 v_col; out vec4 frag;\n" + "noperspective in vec4 v_col; out vec4 frag;\n" "void main(){ frag = v_col; }\n"; +/* Textured polys: pos in VRAM px (offset applied), uv = texel coords, color = + * modulation. Fragment samples raw VRAM (integer), decodes per color depth + * with CLUT lookup, discards texel 0 (PS1 transparent), applies modulation + * (PS1: texel*color/128, i.e. *2 around a 0x80 identity). */ +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" + "noperspective out vec2 v_uv; noperspective out vec4 v_col;\n" + "void main(){ v_uv = a_uv; v_col = a_col;\n" + " gl_Position = vec4(a_pos.x/512.0 - 1.0, a_pos.y/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, in VRAM pixels */\n" + "uniform ivec2 u_clut; /* CLUT base, in VRAM pixels */\n" + "uniform int u_depth; /* 0=4bit 1=8bit 2=15bit */\n" + "uniform int u_raw; /* 1 = no color modulation */\n" + "int vram(int x,int y){ return int(texelFetch(u_vram, ivec2(x,y), 0).r); }\n" + "void main(){\n" + " int u = int(v_uv.x + 0.5); int v = int(v_uv.y + 0.5);\n" + " int raw;\n" + " if (u_depth == 0) {\n" + " int px = vram(u_tpage.x + (u>>2), u_tpage.y + v);\n" + " int idx = (px >> ((u & 3)*4)) & 0xF;\n" + " raw = vram(u_clut.x + idx, u_clut.y);\n" + " } else if (u_depth == 1) {\n" + " int px = vram(u_tpage.x + (u>>1), u_tpage.y + v);\n" + " int idx = (px >> ((u & 1)*8)) & 0xFF;\n" + " raw = vram(u_clut.x + idx, u_clut.y);\n" + " } else {\n" + " raw = vram(u_tpage.x + u, u_tpage.y + v);\n" + " }\n" + " if (raw == 0) discard;\n" + " vec3 t = vec3(float(raw&31), float((raw>>5)&31), float((raw>>10)&31)) / 31.0;\n" + " if (u_raw == 0) t = clamp(t * v_col.rgb * 2.0, 0.0, 1.0);\n" + " frag = vec4(t, 1.0);\n" + "}\n"; + static GLuint compile_shader(GLenum type, const char *src) { GLuint s = p_glCreateShader(type); p_glShaderSource(s, 1, &src, NULL); @@ -229,6 +285,10 @@ static void ensure_gpu(void) { /* make the FBO reflect CPU VRAM */ for (int i = 0; i < VRAM_W * VRAM_H; i++) s_conv[i] = conv_1555_to_rgba8(s_vram[i]); glBindTexture(GL_TEXTURE_2D, s_vram_tex); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VRAM_W, VRAM_H, GL_RGBA, GL_UNSIGNED_BYTE, s_conv); + /* raw 16-bit VRAM for texture sampling (upload CPU VRAM directly) */ + glBindTexture(GL_TEXTURE_2D, s_vram_raw_tex); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VRAM_W, VRAM_H, + PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, s_vram); s_cpu_dirty = 0; } static void ensure_cpu(void) { /* make CPU VRAM reflect the FBO */ @@ -276,17 +336,69 @@ static void gpu_triangle(int x0,int y0,uint16_t c0, int x1,int y1,uint16_t c1, s_gpu_dirty = 1; } +/* Rasterize one textured triangle on the GPU. Coords raw (offset applied here), + * uv = texel coords, col = per-vertex modulation RGB (0..1, 3 verts × 3). */ +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) { + ensure_gpu(); + float verts[3 * 8]; + for (int i = 0; i < 3; i++) { + verts[i*8+0] = (float)(xs[i] + s_off_x); + verts[i*8+1] = (float)(ys[i] + s_off_y); + 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; + } + int base_x = (texpage & 0xF) * 64; + int base_y = ((texpage >> 4) & 1) * 256; + int depth = (texpage >> 7) & 3; if (depth > 2) depth = 2; + + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); + glViewport(0, 0, VRAM_W, VRAM_H); + glEnable(GL_SCISSOR_TEST); + 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_area_y1, sw, sh); + glDisable(GL_BLEND); + p_glUseProgram(s_tex_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_vram_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_glBindVertexArray(s_tex_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); + p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); + glDrawArrays(GL_TRIANGLES, 0, 3); + p_glBindVertexArray(0); + p_glUseProgram(0); + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + s_gpu_dirty = 1; +} + /* ---- backend vtable wrappers ------------------------------------------- */ static void glb_init(uint16_t *vram) { s_vram = vram; sw_renderer_init(vram); } -static void glb_set_scale(int s) { sw_renderer_set_scale(s); } -static int glb_scale(void) { return sw_renderer_scale(); } +/* GPU draws write the FBO (native res), not the software SSAA hi-res mirror, so + * presenting via the hi-res mirror would drop every GPU-rendered layer. Force + * native-res present under the GL backend; GPU-side high-res scaling is a later + * step (the FBO will simply be allocated larger). */ +static void glb_set_scale(int s) { (void)s; sw_renderer_set_scale(1); } +static int glb_scale(void) { return 1; } static void glb_set_texture_filter(int b) { sw_set_texture_filter(b); } static int glb_texture_filter(void) { return sw_texture_filter(); } static void glb_set_semi_transparency(int e, int m) { s_semi_en = e; s_semi_mode = m; sw_set_semi_transparency(e, m); } static void glb_set_mask_bits(int s, int c) { sw_set_mask_bits(s, c); } static void glb_set_texture_window(uint32_t r) { sw_set_texture_window(r); } -static void glb_set_color_modulation(int r,int g,int b,int raw) { sw_set_color_modulation(r,g,b,raw); } +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); } @@ -304,8 +416,24 @@ static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,ui /* Everything else: software, with the FBO read back first if it's ahead. */ static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){ ensure_cpu(); sw_fill_rect(x,y,w,h,c); s_cpu_dirty=1; } static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){ ensure_cpu(); sw_copy_rect(sx,sy,dx,dy,w,h); s_cpu_dirty=1; } -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){ ensure_cpu(); sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp); s_cpu_dirty=1; } -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){ ensure_cpu(); sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); s_cpu_dirty=1; } +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 && s_tex_prog && !s_semi_en) { + 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); return; + } + ensure_cpu(); sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp); s_cpu_dirty=1; +} +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 && s_tex_prog && !s_semi_en) { + 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); return; + } + ensure_cpu(); sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); s_cpu_dirty=1; +} static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){ ensure_cpu(); sw_draw_flat_rect(x,y,w,h,c); s_cpu_dirty=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){ ensure_cpu(); sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp); s_cpu_dirty=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){ ensure_cpu(); sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp); s_cpu_dirty=1; } @@ -356,9 +484,39 @@ static void init_gpu_raster(void) { p_glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(2 * sizeof(float))); p_glEnableVertexAttribArray(1); p_glBindVertexArray(0); + + /* Textured path: raw 16-bit VRAM integer sampler + CLUT-decode program. */ + s_tex_prog = build_program(TEX_VS, TEX_FS); + if (s_tex_prog) { + s_raw = (uint16_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint16_t)); + glGenTextures(1, &s_vram_raw_tex); + glBindTexture(GL_TEXTURE_2D, s_vram_raw_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexImage2D(GL_TEXTURE_2D, 0, PSXGL_R16UI, VRAM_W, VRAM_H, 0, + PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, NULL); + 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"); + 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_glBindVertexArray(0); + } + s_cpu_dirty = 1; /* FBO empty; first GPU draw uploads current VRAM */ s_raster_ok = 1; - fprintf(stdout, "psxrecomp: GPU rasterization enabled (opaque flat/gouraud polys)\n"); + fprintf(stdout, "psxrecomp: GPU rasterization enabled (opaque flat/gouraud%s polys)\n", + s_tex_prog ? " + textured" : ""); } int gl_renderer_init_context(SDL_Window *win) { @@ -392,9 +550,11 @@ void gl_renderer_shutdown(void) { ensure_cpu(); if (s_present_tex) glDeleteTextures(1, &s_present_tex); if (s_vram_tex) glDeleteTextures(1, &s_vram_tex); + if (s_vram_raw_tex) glDeleteTextures(1, &s_vram_raw_tex); SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; } free(s_conv); s_conv = NULL; + free(s_raw); s_raw = NULL; } void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear) { From ee0a454a56b26e335448afe85980721506bb4c89 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 15:06:39 -0700 Subject: [PATCH 07/18] debug server: screenshot writes a real PNG instead of a .png-named BMP handle_screenshot_file emitted a 24-bit BMP regardless of extension, so "screenshot_file path=foo.png" produced a file that PNG-only consumers (and the agent Read tool) rejected. Replace with a self-contained PNG encoder: 8-bit truecolor, IDAT carried as a zlib stream of stored (uncompressed) DEFLATE blocks with correct CRC32 + Adler32. No new link dependency, so the static self-contained runtime stays self-contained. Default name is now psx_screenshot.png. Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/src/debug_server.c | 145 ++++++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 35 deletions(-) diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 406704dc..91dacd21 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -5544,8 +5544,107 @@ static void handle_get_snapshots(int id, const char *json) s_snapshot_addrs[3], s_snapshot_active[3]); } -/* Unified screenshot: writes a 24-bit BMP of the current display to "path" - * (default psx_screenshot.bmp in the runtime cwd) and answers with a single +/* ---- Minimal self-contained PNG writer ------------------------------------ + * Emits a valid 8-bit truecolor (RGB) PNG with zero external dependencies: the + * IDAT payload is a zlib stream whose DEFLATE body is "stored" (uncompressed) + * blocks. Bigger on disk than a compressed PNG, but a real PNG that every + * viewer (and the harness Read tool) accepts, and nothing new to link against + * — which keeps the self-contained static runtime self-contained. */ +static uint32_t s_png_crc_tbl[256]; +static int s_png_crc_init = 0; +static void png_crc_build(void) { + for (uint32_t n = 0; n < 256; n++) { + uint32_t c = n; + for (int k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1); + s_png_crc_tbl[n] = c; + } + s_png_crc_init = 1; +} +static uint32_t png_crc_update(uint32_t crc, const uint8_t *p, size_t n) { + if (!s_png_crc_init) png_crc_build(); + for (size_t i = 0; i < n; i++) crc = s_png_crc_tbl[(crc ^ p[i]) & 0xFF] ^ (crc >> 8); + return crc; +} +static uint32_t png_adler32(const uint8_t *p, size_t n) { + uint32_t a = 1, b = 0; + /* process in blocks so the 16-bit sums never overflow before the mod */ + while (n) { + size_t k = n < 5552 ? n : 5552; + for (size_t i = 0; i < k; i++) { a += p[i]; b += a; } + a %= 65521; b %= 65521; p += k; n -= k; + } + return (b << 16) | a; +} +static void png_put_be32(FILE *f, uint32_t v) { + uint8_t b[4] = { (uint8_t)(v >> 24), (uint8_t)(v >> 16), + (uint8_t)(v >> 8), (uint8_t)v }; + fwrite(b, 1, 4, f); +} +static void png_chunk(FILE *f, const char *type, const uint8_t *data, size_t len) { + png_put_be32(f, (uint32_t)len); + uint32_t crc = 0xFFFFFFFFu; + crc = png_crc_update(crc, (const uint8_t *)type, 4); + crc = png_crc_update(crc, data, len); + fwrite(type, 1, 4, f); + if (len) fwrite(data, 1, len, f); + png_put_be32(f, crc ^ 0xFFFFFFFFu); +} +/* Write an RGB (3 bytes/pixel, top-down) buffer as a PNG. Returns 1 on success. */ +static int png_write_rgb(FILE *f, const uint8_t *rgb, uint32_t w, uint32_t h) { + static const uint8_t sig[8] = { 137,80,78,71,13,10,26,10 }; + fwrite(sig, 1, 8, f); + + uint8_t ihdr[13]; + ihdr[0]=(uint8_t)(w>>24); ihdr[1]=(uint8_t)(w>>16); ihdr[2]=(uint8_t)(w>>8); ihdr[3]=(uint8_t)w; + ihdr[4]=(uint8_t)(h>>24); ihdr[5]=(uint8_t)(h>>16); ihdr[6]=(uint8_t)(h>>8); ihdr[7]=(uint8_t)h; + ihdr[8]=8; /* bit depth */ + ihdr[9]=2; /* color type 2 = truecolor RGB */ + ihdr[10]=0; /* compression */ + ihdr[11]=0; /* filter */ + ihdr[12]=0; /* interlace */ + png_chunk(f, "IHDR", ihdr, sizeof ihdr); + + /* Filtered raw scanlines: each row prefixed with filter byte 0 (None). */ + size_t raw_len = (size_t)h * (1 + (size_t)w * 3); + uint8_t *raw = (uint8_t *)malloc(raw_len); + if (!raw) return 0; + for (uint32_t y = 0; y < h; y++) { + uint8_t *row = raw + (size_t)y * (1 + (size_t)w * 3); + row[0] = 0; + memcpy(row + 1, rgb + (size_t)y * w * 3, (size_t)w * 3); + } + + /* zlib stream: 2-byte header + stored DEFLATE blocks + 4-byte Adler32. */ + size_t nblocks = (raw_len + 65534) / 65535; if (nblocks == 0) nblocks = 1; + size_t z_len = 2 + nblocks * 5 + raw_len + 4; + uint8_t *z = (uint8_t *)malloc(z_len); + if (!z) { free(raw); return 0; } + size_t zi = 0; + z[zi++] = 0x78; /* CMF: 32K window, deflate */ + z[zi++] = 0x01; /* FLG: check bits make 0x7801 a multiple of 31 */ + size_t off = 0; + while (off < raw_len) { + size_t n = raw_len - off; if (n > 65535) n = 65535; + int final = (off + n >= raw_len); + z[zi++] = (uint8_t)(final ? 1 : 0); /* BFINAL | BTYPE=00 */ + z[zi++] = (uint8_t)(n & 0xFF); z[zi++] = (uint8_t)(n >> 8); + uint16_t nl = (uint16_t)~n; + z[zi++] = (uint8_t)(nl & 0xFF); z[zi++] = (uint8_t)(nl >> 8); + memcpy(z + zi, raw + off, n); zi += n; off += n; + } + uint32_t ad = png_adler32(raw, raw_len); + z[zi++] = (uint8_t)(ad >> 24); z[zi++] = (uint8_t)(ad >> 16); + z[zi++] = (uint8_t)(ad >> 8); z[zi++] = (uint8_t)ad; + free(raw); + + png_chunk(f, "IDAT", z, zi); + free(z); + png_chunk(f, "IEND", NULL, 0); + return 1; +} + +/* Unified screenshot: writes an 8-bit RGB PNG of the current display to "path" + * (default psx_screenshot.png in the runtime cwd) and answers with a single * metadata line. Registered as both "screenshot" and "screenshot_file"; * the old "screenshot" inline-hex-row variant streamed h+1 response lines * per request, which violated the one-request/one-response protocol and @@ -5563,50 +5662,26 @@ static void handle_screenshot_file(int id, const char *json) char path[512]; if (!json_get_str(json, "path", path, sizeof(path))) - strncpy(path, "psx_screenshot.bmp", sizeof(path) - 1); + strncpy(path, "psx_screenshot.png", sizeof(path) - 1); path[sizeof(path) - 1] = '\0'; FILE *f = fopen(path, "wb"); if (!f) { send_err(id, "cannot open file"); return; } - /* BMP row stride: 3 bytes/pixel, padded to 4-byte boundary */ - uint32_t row_stride = (w * 3 + 3) & ~3u; - uint32_t pixel_size = row_stride * h; - uint32_t file_size = 14 + 40 + pixel_size; - - /* BITMAPFILEHEADER (14 bytes) */ - uint8_t bfh[14] = {0}; - bfh[0] = 'B'; bfh[1] = 'M'; - bfh[2] = file_size & 0xFF; bfh[3] = (file_size >> 8) & 0xFF; - bfh[4] = (file_size >> 16) & 0xFF; bfh[5] = (file_size >> 24) & 0xFF; - bfh[10] = 54; /* offset to pixel data */ - fwrite(bfh, 1, 14, f); - - /* BITMAPINFOHEADER (40 bytes) */ - uint8_t bih[40] = {0}; - bih[0] = 40; /* header size */ - bih[4] = w & 0xFF; bih[5] = (w >> 8) & 0xFF; - /* Height negative = top-down */ - int32_t neg_h = -(int32_t)h; - memcpy(bih + 8, &neg_h, 4); - bih[12] = 1; /* planes */ - bih[14] = 24; /* bits per pixel */ - fwrite(bih, 1, 40, f); - - /* Pixel data: top-down, BGR */ - uint8_t *row = (uint8_t *)malloc(row_stride); + /* Gather pixels top-down, RGB. */ + uint8_t *rgb = (uint8_t *)malloc((size_t)w * h * 3); + if (!rgb) { fclose(f); send_err(id, "alloc failed"); return; } for (uint32_t y = 0; y < h; y++) { - memset(row, 0, row_stride); for (uint32_t x = 0; x < w; x++) { uint8_t r, g, b; gpu_display_pixel_rgb(&di, x, y, &r, &g, &b); - row[x * 3 + 0] = b; - row[x * 3 + 1] = g; - row[x * 3 + 2] = r; + uint8_t *p = rgb + ((size_t)y * w + x) * 3; + p[0] = r; p[1] = g; p[2] = b; } - fwrite(row, 1, row_stride, f); } - free(row); + int ok = png_write_rgb(f, rgb, w, h); + free(rgb); fclose(f); + if (!ok) { send_err(id, "png encode failed"); return; } send_fmt("{\"id\":%d,\"ok\":true,\"path\":\"%s\",\"width\":%u,\"height\":%u}", id, path, w, h); From 6c30b65b3dee9669d7373df7fbc7b783ef7c2633 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 15:06:39 -0700 Subject: [PATCH 08/18] gl-renderer phase 2b step 4: GPU semi-transparency (4 PS1 blend modes) Route semi-transparent flat/gouraud/textured polys to the GPU instead of falling back to software (which forced a full FBO readback per prim). The four PS1 modes (B/2+F/2, B+F, B-F, B+F/4) map to fixed-function blending: FUNC_ADD/REVERSE_SUBTRACT with ONE/CONSTANT_ALPHA factors, the fragment as source and the FBO as dest; framebuffer clamp matches the PS1 saturate. glBlendEquation/glBlendColor loaded via the modern-GL loader. Textured semi-transparency honors the per-texel STP bit via a two-pass split (new u_semipass uniform): pass 1 lays opaque texels with blend off, pass 2 blends only STP texels. Untextured semi prims blend every pixel in one pass. Opaque prims are unchanged (semi = -1, blend disabled). Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/src/gpu_gl_renderer.c | 88 +++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index ef55fc6d..5350264f 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -54,6 +54,9 @@ #define PSXGL_R16UI 0x8234 #define PSXGL_RED_INTEGER 0x8D94 #define PSXGL_TEXTURE1 0x84C1 +#define PSXGL_FUNC_ADD 0x8006 +#define PSXGL_FUNC_REVERSE_SUBTRACT 0x800B +#define PSXGL_CONSTANT_ALPHA 0x8003 #ifndef APIENTRY #define APIENTRY @@ -78,6 +81,8 @@ 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_glUniform2i)(GLint, GLint, GLint); +typedef void (APIENTRY *PFN_glBlendEquation)(GLenum); +typedef void (APIENTRY *PFN_glBlendColor)(GLfloat, GLfloat, GLfloat, GLfloat); typedef void (APIENTRY *PFN_glGenVertexArrays)(GLsizei, GLuint *); typedef void (APIENTRY *PFN_glBindVertexArray)(GLuint); typedef void (APIENTRY *PFN_glActiveTexture)(GLenum); @@ -106,6 +111,8 @@ static PFN_glUseProgram p_glUseProgram; static PFN_glGetUniformLocation p_glGetUniformLocation; static PFN_glUniform1i p_glUniform1i; static PFN_glUniform2i p_glUniform2i; +static PFN_glBlendEquation p_glBlendEquation; +static PFN_glBlendColor p_glBlendColor; static PFN_glGenVertexArrays p_glGenVertexArrays; static PFN_glBindVertexArray p_glBindVertexArray; static PFN_glActiveTexture p_glActiveTexture; @@ -130,6 +137,7 @@ static int load_modern_gl(void) { LOAD(p_glGetProgramInfoLog, "glGetProgramInfoLog"); LOAD(p_glUseProgram, "glUseProgram"); LOAD(p_glGetUniformLocation, "glGetUniformLocation"); LOAD(p_glUniform1i, "glUniform1i"); LOAD(p_glUniform2i, "glUniform2i"); + LOAD(p_glBlendEquation, "glBlendEquation"); LOAD(p_glBlendColor, "glBlendColor"); LOAD(p_glGenVertexArrays, "glGenVertexArrays"); LOAD(p_glBindVertexArray, "glBindVertexArray"); LOAD(p_glActiveTexture, "glActiveTexture"); LOAD(p_glGenBuffers, "glGenBuffers"); LOAD(p_glBindBuffer, "glBindBuffer"); LOAD(p_glBufferData, "glBufferData"); @@ -164,7 +172,7 @@ static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0; * known limitation handled later — uploaded textures (the common case) work. */ static GLuint s_vram_raw_tex = 0; static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0; -static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1, s_uRaw = -1; +static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1, s_uRaw = -1, s_uSemipass = -1; static uint16_t *s_raw = NULL; /* 1024*512 raw-uint16 staging */ static int s_cpu_dirty = 0; /* CPU VRAM has changes not in FBO */ static int s_gpu_dirty = 0; /* FBO has changes not in CPU VRAM */ @@ -224,6 +232,7 @@ static const char *TEX_FS = "uniform ivec2 u_clut; /* CLUT base, in VRAM pixels */\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=draw all, 1=opaque texels only, 2=semi texels only */\n" "int vram(int x,int y){ return int(texelFetch(u_vram, ivec2(x,y), 0).r); }\n" "void main(){\n" " int u = int(v_uv.x + 0.5); int v = int(v_uv.y + 0.5);\n" @@ -240,6 +249,9 @@ static const char *TEX_FS = " raw = vram(u_tpage.x + u, u_tpage.y + v);\n" " }\n" " if (raw == 0) discard;\n" + " int stp = (raw >> 15) & 1; /* per-texel semi-transparency bit */\n" + " if (u_semipass == 1 && stp == 1) discard; /* opaque pass skips semi texels */\n" + " if (u_semipass == 2 && stp == 0) discard; /* semi pass skips opaque texels */\n" " vec3 t = vec3(float(raw&31), float((raw>>5)&31), float((raw>>10)&31)) / 31.0;\n" " if (u_raw == 0) t = clamp(t * v_col.rgb * 2.0, 0.0, 1.0);\n" " frag = vec4(t, 1.0);\n" @@ -300,9 +312,39 @@ static void ensure_cpu(void) { /* make CPU VRAM reflect the FBO */ s_gpu_dirty = 0; } -/* Rasterize one triangle (3 verts: x,y in raw coords; colors 1555) on the GPU. */ +/* Configure fixed-function blending for a PS1 semi-transparency mode, with the + * incoming fragment as source (F = front) and the FBO as dest (B = back): + * 0: B/2 + F/2 1: B + F 2: B - F 3: B + F/4 + * Modes are exact in 5-bit; GL blends in normalized [0,1] which is the same + * proportional math, and the framebuffer clamp matches the PS1 saturate. */ +static void apply_psx_blend(int mode) { + glEnable(GL_BLEND); + switch (mode & 3) { + case 0: /* B/2 + F/2 */ + p_glBlendColor(0.5f, 0.5f, 0.5f, 0.5f); + p_glBlendEquation(PSXGL_FUNC_ADD); + glBlendFunc(PSXGL_CONSTANT_ALPHA, PSXGL_CONSTANT_ALPHA); + break; + case 1: /* B + F */ + p_glBlendEquation(PSXGL_FUNC_ADD); + glBlendFunc(GL_ONE, GL_ONE); + break; + case 2: /* B - F (dst*1 - src*1) */ + p_glBlendEquation(PSXGL_FUNC_REVERSE_SUBTRACT); + glBlendFunc(GL_ONE, GL_ONE); + break; + case 3: /* B + F/4 */ + p_glBlendColor(0.25f, 0.25f, 0.25f, 0.25f); + p_glBlendEquation(PSXGL_FUNC_ADD); + glBlendFunc(PSXGL_CONSTANT_ALPHA, GL_ONE); + break; + } +} + +/* Rasterize one triangle (3 verts: x,y in raw coords; colors 1555) on the GPU. + * semi<0 = opaque; semi 0..3 = blend with that PS1 semi-transparency mode. */ 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 x2,int y2,uint16_t c2, int semi) { ensure_gpu(); float verts[3*6]; int xs[3] = {x0+s_off_x, x1+s_off_x, x2+s_off_x}; @@ -323,7 +365,7 @@ static void gpu_triangle(int x0,int y0,uint16_t c0, int x1,int y1,uint16_t c1, 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(sx, sy, sw, sh); /* FBO y matches VRAM y (no flip) */ - glDisable(GL_BLEND); /* opaque only in this step */ + if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); p_glUseProgram(s_geo_prog); p_glBindVertexArray(s_geo_vao); p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); @@ -331,6 +373,7 @@ static void gpu_triangle(int x0,int y0,uint16_t c0, int x1,int y1,uint16_t c1, glDrawArrays(GL_TRIANGLES, 0, 3); p_glBindVertexArray(0); p_glUseProgram(0); + glDisable(GL_BLEND); glDisable(GL_SCISSOR_TEST); p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); s_gpu_dirty = 1; @@ -341,7 +384,8 @@ static void gpu_triangle(int x0,int y0,uint16_t c0, int x1,int y1,uint16_t c1, 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) { + uint16_t clut_x, uint16_t clut_y, int rawtex, + int semi) { ensure_gpu(); float verts[3 * 8]; for (int i = 0; i < 3; i++) { @@ -364,7 +408,6 @@ static void gpu_textured_triangle(const int *xs, const int *ys, 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_area_y1, sw, sh); - glDisable(GL_BLEND); p_glUseProgram(s_tex_prog); p_glActiveTexture(PSXGL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, s_vram_raw_tex); @@ -376,9 +419,25 @@ static void gpu_textured_triangle(const int *xs, const int *ys, p_glBindVertexArray(s_tex_vao); p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); - glDrawArrays(GL_TRIANGLES, 0, 3); + if (semi < 0) { + /* opaque primitive: every texel drawn, no blend */ + glDisable(GL_BLEND); + p_glUniform1i(s_uSemipass, 0); + glDrawArrays(GL_TRIANGLES, 0, 3); + } else { + /* semi-transparent primitive: the per-texel STP bit decides whether a + * texel is opaque or blended. Pass 1 lays down the opaque texels with + * blend off; pass 2 blends the semi texels in the requested mode. */ + glDisable(GL_BLEND); + p_glUniform1i(s_uSemipass, 1); + glDrawArrays(GL_TRIANGLES, 0, 3); + apply_psx_blend(semi); + p_glUniform1i(s_uSemipass, 2); + glDrawArrays(GL_TRIANGLES, 0, 3); + } p_glBindVertexArray(0); p_glUseProgram(0); + glDisable(GL_BLEND); glDisable(GL_SCISSOR_TEST); p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); s_gpu_dirty = 1; @@ -403,13 +462,13 @@ static void glb_set_draw_area(int x1,int y1,int x2,int y2) { s_area_x1=x1; s_are 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); } -/* GPU-rasterized: opaque flat / gouraud triangles. Semi-transparent -> SW. */ +/* GPU-rasterized: flat / gouraud triangles, opaque and semi-transparent. */ 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 && !s_semi_en) { gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col); return; } + if (s_raster_ok) { gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1); return; } ensure_cpu(); sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col); s_cpu_dirty = 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 && !s_semi_en) { gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2); return; } + if (s_raster_ok) { gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1); return; } ensure_cpu(); sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2); s_cpu_dirty = 1; } @@ -417,20 +476,20 @@ static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,ui static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){ ensure_cpu(); sw_fill_rect(x,y,w,h,c); s_cpu_dirty=1; } static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){ ensure_cpu(); sw_copy_rect(sx,sy,dx,dy,w,h); s_cpu_dirty=1; } 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 && s_tex_prog && !s_semi_en) { + if (s_raster_ok && s_tex_prog) { 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); return; + gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1); return; } ensure_cpu(); sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp); s_cpu_dirty=1; } 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 && s_tex_prog && !s_semi_en) { + if (s_raster_ok && s_tex_prog) { 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); return; + gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1); return; } ensure_cpu(); sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); s_cpu_dirty=1; } @@ -500,6 +559,7 @@ static void init_gpu_raster(void) { 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"); p_glGenVertexArrays(1, &s_tex_vao); p_glBindVertexArray(s_tex_vao); p_glGenBuffers(1, &s_tex_vbo); From 846e402b9812e2f45f13fa905e98e09b895e47f2 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 15:17:22 -0700 Subject: [PATCH 09/18] gl-renderer: fix double-applied draw offset dropping terrain layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gpu.c applies draw_offset_x/y to every primitive's coordinates before calling the gr_* facade (see gp0_exec_* — triangles, rects, sprites, lines all pre-offset), and the software raster consumes them directly (g_offset_x is never read during rasterization). The GPU triangle paths were adding s_off again, double-applying the offset. At the logos the offset is 0 so it was invisible, but in the scrolling overworld it displaced every GPU-rendered triangle by 2x the scroll, pushing the background/foreground terrain quads off-screen while sprites (drawn as rects -> software, single offset) stayed put. Net effect: "the entire overworld is gone" — only sprites survived. Drop the s_off addition in gpu_triangle/gpu_textured_triangle. Verified: the attract-demo overworld now renders full terrain (grass, mushrooms, cliffs, signpost, coins) instead of sprites on an empty gradient. Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/src/gpu_gl_renderer.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 5350264f..7e7dea5a 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -347,8 +347,10 @@ 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) { ensure_gpu(); float verts[3*6]; - int xs[3] = {x0+s_off_x, x1+s_off_x, x2+s_off_x}; - int ys[3] = {y0+s_off_y, y1+s_off_y, y2+s_off_y}; + /* gpu.c already applied the draw offset before calling the gr_* facade + * (see gp0_exec_* in gpu.c); coords arrive in final VRAM space. */ + int xs[3] = {x0, x1, x2}; + int ys[3] = {y0, y1, y2}; uint16_t cs[3] = {c0,c1,c2}; for (int i = 0; i < 3; i++) { verts[i*6+0] = (float)xs[i]; @@ -389,8 +391,9 @@ static void gpu_textured_triangle(const int *xs, const int *ys, ensure_gpu(); float verts[3 * 8]; for (int i = 0; i < 3; i++) { - verts[i*8+0] = (float)(xs[i] + s_off_x); - verts[i*8+1] = (float)(ys[i] + s_off_y); + /* coords already offset by gpu.c — do not re-apply s_off here */ + 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]; From 2140c139458f360ac94cd1938f537e426fd146ec Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 15:28:10 -0700 Subject: [PATCH 10/18] gitignore: ignore _wip/ scratch dir (local WIP patches) Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 11390af2..7d56eacf 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ __pycache__/ # DuckStation oracle (local-only; private fork at git@github.com:mstan/duckstation.git) /duckstation/ +_wip/ From af49fb02d6bab901934789679be0fd1df1f16243 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 15:39:51 -0700 Subject: [PATCH 11/18] gl-renderer: GPU rects/fills/lines + present straight from the FBO Move the remaining 2D primitives (flat/textured rects, fill, lines) onto the GPU, and present directly from the FBO color texture cropped to the display region instead of reading the whole 1 MB FBO back to CPU VRAM every frame. Why both at once: the native present read the `vram` array directly and relied on an incidental software op each frame (the HUD/sprites, drawn last) to trigger the FBO->CPU sync. Moving every primitive to the GPU removed that trigger, so a fully-GPU frame left `vram` stale -> black present (the earlier FMV->black regression). The fix is to present from the FBO when the GPU holds the freshest frame (gl_renderer_have_gpu_frame) and to sync explicitly (gl_renderer_sync_cpu) on the software/24-bit path. Result: the per-frame readback is gone for GPU scenes (the forest-lag source), software/FMV frames keep the CPU present path (verified: the MDEC intro renders correctly), and loads are faster. KNOWN-FOLLOWUP cosmetic artifacts to clean up: the GPU textured shader doesn't yet implement the texture window (wrap/mask), so tiled/wrapped sprites show seams; bilinear texture_filtering isn't honored on the GPU path; and the debug screenshot still reads CPU VRAM (stale under the FBO-present path) so captures can lag the on-screen image. Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/include/gpu_gl_renderer.h | 12 ++ runtime/src/gpu_gl_renderer.c | 175 ++++++++++++++++++++++++++++-- runtime/src/main.cpp | 13 +++ 3 files changed, 192 insertions(+), 8 deletions(-) diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 9dc9ff12..f32c4fd9 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -25,6 +25,18 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea /* Clear to black + swap (display-disabled frame). */ void gl_renderer_present_blank(void); +/* True when the GPU FBO holds the freshest VRAM (a GPU draw happened since the + * last CPU sync). The present path reads from the FBO directly when true. */ +int gl_renderer_have_gpu_frame(void); + +/* Sync the FBO down to CPU VRAM if the GPU side is ahead (else a no-op). The + * software / 24-bit (FMV) present path calls this before reading CPU VRAM. */ +void gl_renderer_sync_cpu(void); + +/* Present straight from the FBO, cropped to the display region. No readback; + * fast path for GPU-rendered 15-bit frames. linear = filter on upscale. */ +void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear); + void gl_renderer_shutdown(void); #ifdef __cplusplus diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 7e7dea5a..978a6706 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -81,6 +81,7 @@ 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_glUniform2i)(GLint, GLint, GLint); +typedef void (APIENTRY *PFN_glUniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat); typedef void (APIENTRY *PFN_glBlendEquation)(GLenum); typedef void (APIENTRY *PFN_glBlendColor)(GLfloat, GLfloat, GLfloat, GLfloat); typedef void (APIENTRY *PFN_glGenVertexArrays)(GLsizei, GLuint *); @@ -111,6 +112,7 @@ static PFN_glUseProgram p_glUseProgram; static PFN_glGetUniformLocation p_glGetUniformLocation; static PFN_glUniform1i p_glUniform1i; static PFN_glUniform2i p_glUniform2i; +static PFN_glUniform4f p_glUniform4f; static PFN_glBlendEquation p_glBlendEquation; static PFN_glBlendColor p_glBlendColor; static PFN_glGenVertexArrays p_glGenVertexArrays; @@ -136,7 +138,7 @@ static int load_modern_gl(void) { 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_glUniform2i, "glUniform2i"); + LOAD(p_glUniform2i, "glUniform2i"); LOAD(p_glUniform4f, "glUniform4f"); LOAD(p_glBlendEquation, "glBlendEquation"); LOAD(p_glBlendColor, "glBlendColor"); LOAD(p_glGenVertexArrays, "glGenVertexArrays"); LOAD(p_glBindVertexArray, "glBindVertexArray"); LOAD(p_glActiveTexture, "glActiveTexture"); LOAD(p_glGenBuffers, "glGenBuffers"); @@ -160,6 +162,8 @@ static int s_present_w = 0, s_present_h = 0; static int s_modern_ok = 0; static GLuint s_present_prog = 0, s_present_vao = 0; static GLint s_present_uTex = -1; +static GLuint s_present_crop_prog = 0; +static GLint s_present_crop_uTex = -1, s_present_crop_uRect = -1; /* GPU rasterization target: VRAM as an RGBA8 texture + FBO. */ static int s_raster_ok = 0; /* GPU geometry path available */ @@ -195,6 +199,17 @@ static const char *PRESENT_FS = "in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n" "void main(){ frag = texture(u_tex, v_uv); }\n"; +/* Present straight from the FBO color texture (the live VRAM render target), + * cropped to the display region. u_rect = (u0, v0_top, uw, vh) in 0..1 texture + * space; screen top maps to the display's top VRAM row (v0_top). */ +static const char *PRESENT_CROP_VS = + "#version 330\n" + "uniform vec4 u_rect;\n" + "out vec2 v_uv;\n" + "void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n" + " v_uv = vec2(u_rect.x + p.x*u_rect.z, u_rect.y + (1.0-p.y)*u_rect.w);\n" + " gl_Position = vec4(p*2.0-1.0, 0.0, 1.0); }\n"; + /* Geometry: position in VRAM pixels (draw offset already applied), color in * 0..1. Transform to clip space over the 1024x512 VRAM; no Y flip (VRAM y=0 -> * clip.y=-1 -> FBO row 0 -> glReadPixels row 0 -> CPU VRAM row 0). */ @@ -446,6 +461,84 @@ static void gpu_textured_triangle(const int *xs, const int *ys, s_gpu_dirty = 1; } +/* A flat/gouraud rectangle is two triangles; reuse the geometry path so it + * shares scissor/blend/offset behavior. Coords already offset by gpu.c. */ +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); +} + +/* A textured rectangle is two textured triangles with the UV rectangle mapped + * to its corners. u0,v0 = top-left texel; u1,v1 = bottom-right texel. */ +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}; + 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); + 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); +} + +/* GP0(02h) fill: write a solid color straight into VRAM, ignoring draw area, + * mask, semi-transparency and offset. A scissored glClear is exactly this. */ +static void gpu_fill(int x,int y,int w,int h,uint16_t c) { + if (w <= 0 || h <= 0) return; + ensure_gpu(); + float r=(c&0x1F)/31.0f, g=((c>>5)&0x1F)/31.0f, b=((c>>10)&0x1F)/31.0f; + int sx=x, sy=y, sw=w, sh=h; + if (sx<0){ sw+=sx; sx=0; } if (sy<0){ sh+=sy; sy=0; } + if (sw<0) sw=0; if (sh<0) sh=0; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); + glViewport(0,0,VRAM_W,VRAM_H); + glEnable(GL_SCISSOR_TEST); + glScissor(sx, sy, sw, sh); + glDisable(GL_BLEND); + glClearColor(r,g,b,1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + s_gpu_dirty = 1; +} + +/* 1px line via GL_LINES; coords already offset, clipped to the draw area. */ +static void gpu_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int semi) { + ensure_gpu(); + float verts[2*6]; + int xs[2]={x0,x1}, ys[2]={y0,y1}; uint16_t cs[2]={c0,c1}; + for (int i=0;i<2;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]=1.0f; + } + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); + glViewport(0,0,VRAM_W,VRAM_H); + glEnable(GL_SCISSOR_TEST); + 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_area_y1, sw, sh); + if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); + p_glUseProgram(s_geo_prog); + 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_LINES, 0, 2); + p_glBindVertexArray(0); + p_glUseProgram(0); + glDisable(GL_BLEND); + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + s_gpu_dirty = 1; +} + /* ---- backend vtable wrappers ------------------------------------------- */ static void glb_init(uint16_t *vram) { s_vram = vram; sw_renderer_init(vram); } /* GPU draws write the FBO (native res), not the software SSAA hi-res mirror, so @@ -475,8 +568,13 @@ static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,ui ensure_cpu(); sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2); s_cpu_dirty = 1; } -/* Everything else: software, with the FBO read back first if it's ahead. */ -static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){ ensure_cpu(); sw_fill_rect(x,y,w,h,c); s_cpu_dirty=1; } +/* GPU-rasterized fill. copy_rect (VRAM->VRAM blit) stays on the software path + * for now: an in-place FBO self-blit with arbitrary overlap is the one case GL + * can't express with a single glBlitFramebuffer. */ +static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){ + if (s_raster_ok) { gpu_fill(x,y,w,h,c); return; } + ensure_cpu(); sw_fill_rect(x,y,w,h,c); s_cpu_dirty=1; +} static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){ ensure_cpu(); sw_copy_rect(sx,sy,dx,dy,w,h); s_cpu_dirty=1; } 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 && s_tex_prog) { @@ -496,11 +594,26 @@ static void glb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32 } ensure_cpu(); sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); s_cpu_dirty=1; } -static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){ ensure_cpu(); sw_draw_flat_rect(x,y,w,h,c); s_cpu_dirty=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){ ensure_cpu(); sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp); s_cpu_dirty=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){ ensure_cpu(); sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp); s_cpu_dirty=1; } -static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){ ensure_cpu(); sw_draw_line(x0,y0,x1,y1,c); s_cpu_dirty=1; } -static void glb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1){ ensure_cpu(); sw_draw_shaded_line(x0,y0,c0,x1,y1,c1); s_cpu_dirty=1; } +static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){ + if (s_raster_ok) { gpu_flat_rect(x,y,w,h,c, s_semi_en?s_semi_mode:-1); return; } + ensure_cpu(); sw_draw_flat_rect(x,y,w,h,c); s_cpu_dirty=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 && s_tex_prog) { gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1); return; } + ensure_cpu(); sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp); s_cpu_dirty=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 && s_tex_prog) { gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1); return; } + ensure_cpu(); sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp); s_cpu_dirty=1; +} +static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){ + if (s_raster_ok) { gpu_line(x0,y0,c, x1,y1,c, s_semi_en?s_semi_mode:-1); return; } + ensure_cpu(); sw_draw_line(x0,y0,x1,y1,c); s_cpu_dirty=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) { gpu_line(x0,y0,c0, x1,y1,c1, s_semi_en?s_semi_mode:-1); return; } + ensure_cpu(); sw_draw_shaded_line(x0,y0,c0,x1,y1,c1); s_cpu_dirty=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){ ensure_cpu(); sw_vram_write(x,y,px); s_cpu_dirty=1; } @@ -602,6 +715,11 @@ int gl_renderer_init_context(SDL_Window *win) { 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 s_modern_ok = 0; + s_present_crop_prog = build_program(PRESENT_CROP_VS, PRESENT_FS); + if (s_present_crop_prog) { + s_present_crop_uTex = p_glGetUniformLocation(s_present_crop_prog, "u_tex"); + s_present_crop_uRect = p_glGetUniformLocation(s_present_crop_prog, "u_rect"); + } } fprintf(stdout, "psxrecomp: GL present path = %s\n", s_modern_ok ? "shader" : "immediate(legacy)"); if (s_modern_ok) init_gpu_raster(); @@ -648,6 +766,47 @@ void gl_renderer_present_blank(void) { SDL_GL_SwapWindow(s_win); } +/* True when the FBO holds the freshest VRAM (a GPU draw happened since the last + * CPU sync). The present path uses this to read straight from the FBO instead + * of forcing a full readback to CPU VRAM. */ +int gl_renderer_have_gpu_frame(void) { + return s_raster_ok && s_gpu_dirty; +} + +/* Sync the FBO down into CPU VRAM (no-op if the CPU side is already current). + * Called by the software/24-bit present path before it reads CPU VRAM. */ +void gl_renderer_sync_cpu(void) { + ensure_cpu(); +} + +/* Present straight from the FBO color texture (the live VRAM render target), + * cropped to the display region [disp_x,disp_x+w) x [disp_y,disp_y+h). No + * readback — this is the fast path for GPU-rendered (15-bit) frames. */ +void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear) { + if (!s_ctx || !s_present_crop_prog || !s_vram_tex) return; + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + glViewport(0, 0, ww, wh); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); + glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_vram_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); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + p_glUseProgram(s_present_crop_prog); + p_glUniform1i(s_present_crop_uTex, 0); + p_glUniform4f(s_present_crop_uRect, + (float)disp_x / (float)VRAM_W, (float)disp_y / (float)VRAM_H, + (float)w / (float)VRAM_W, (float)h / (float)VRAM_H); + p_glBindVertexArray(s_present_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); + p_glBindVertexArray(0); + p_glUseProgram(0); + SDL_GL_SwapWindow(s_win); +} + static const GpuRenderBackend GL_BACKEND = { .name = "opengl", .init = glb_init, .set_scale = glb_set_scale, .scale = glb_scale, diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index f4ca4ee5..93a67768 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1115,6 +1115,19 @@ static void sdl_vblank_present(void) { disabled_frame_presented = false; w = di.width; h = di.height; + /* OpenGL fast path: when the GPU FBO holds the freshest VRAM (a GPU + * draw happened this frame) and the display is 15-bit, present straight + * from the FBO — no 1 MB readback. Software / 24-bit (FMV) frames fall + * through to the CPU readout below, after syncing the FBO down. */ +#ifndef PSX_SDL_NO_RENDER + if (g_gl_active && !di.depth24 && gl_renderer_have_gpu_frame()) { + gl_renderer_present_vram((int)di.display_x, (int)di.display_y, + (int)w, (int)h, g_video_aa ? 1 : 0); + return; + } + if (g_gl_active) gl_renderer_sync_cpu(); +#endif + /* The hi-res mirror is a 15-bit copy of VRAM; 24-bit display (FMV) * reads packed bytes the mirror can't represent, so fall back to the * native path for those frames (the present filter still upscales). */ From b592447136a83107eaea548979a338a4cdba1eb4 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 15:49:31 -0700 Subject: [PATCH 12/18] gl-renderer: present via glBlitFramebuffer; screenshot syncs FBO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FBO-present crop shader sampled a sub-region of the 1024x512 VRAM texture with vertex-interpolated UVs; sub-texel precision over the fullscreen triangle produced a vertical seam and text smearing on screen (the GPU render itself was correct — forcing the software present path showed a clean image, which localized the bug to the present sampling). Replace the crop shader with glBlitFramebuffer: an exact integer-rect copy of the display region from the FBO to the window, with the vertical flip folded into the dst rect. No per-fragment UV math, no sub-texel error. linear=GL_LINEAR (AA) else GL_NEAREST. Also: - screenshot now calls gl_renderer_sync_cpu() first, so captures reflect the on-screen frame under the FBO-present path (was reading stale CPU VRAM). Safe: the debug server is pumped on the main GL-context thread. - PSX_GL_FORCE_CPU_PRESENT=1 forces the software readout present path — a fallback/diagnostic that also keeps CPU VRAM current every frame. Co-Authored-By: Claude Opus 4.8 (1M context) --- runtime/src/debug_server.c | 8 ++++++++ runtime/src/gpu_gl_renderer.c | 37 ++++++++++++++++++----------------- runtime/src/main.cpp | 8 +++++++- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 91dacd21..a5127cd3 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -5651,6 +5651,14 @@ static int png_write_rgb(FILE *f, const uint8_t *rgb, uint32_t w, uint32_t h) { * poisoned every client connection that used it. */ static void handle_screenshot_file(int id, const char *json) { + /* Under the OpenGL FBO-present path, CPU VRAM can be stale (the FBO holds + * the freshest frame and is presented without a readback). Sync it down so + * the capture reflects what's on screen. No-op for the software backend or + * when no GPU frame is pending. Safe here: the debug server is pumped on + * the main (GL-context) thread. */ + extern void gl_renderer_sync_cpu(void); + gl_renderer_sync_cpu(); + GpuDisplayInfo di; gpu_get_display_info(&di); if (di.disabled || di.width == 0 || di.height == 0) { diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 978a6706..96f7fbfa 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -57,6 +57,8 @@ #define PSXGL_FUNC_ADD 0x8006 #define PSXGL_FUNC_REVERSE_SUBTRACT 0x800B #define PSXGL_CONSTANT_ALPHA 0x8003 +#define PSXGL_READ_FRAMEBUFFER 0x8CA8 +#define PSXGL_DRAW_FRAMEBUFFER 0x8CA9 #ifndef APIENTRY #define APIENTRY @@ -96,6 +98,7 @@ 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); static PFN_glCreateShader p_glCreateShader; static PFN_glShaderSource p_glShaderSource; @@ -127,6 +130,7 @@ 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 int load_modern_gl(void) { int ok = 1; @@ -148,6 +152,7 @@ static int load_modern_gl(void) { LOAD(p_glGenFramebuffers, "glGenFramebuffers"); LOAD(p_glBindFramebuffer, "glBindFramebuffer"); LOAD(p_glFramebufferTexture2D, "glFramebufferTexture2D"); LOAD(p_glCheckFramebufferStatus, "glCheckFramebufferStatus"); + LOAD(p_glBlitFramebuffer, "glBlitFramebuffer"); #undef LOAD return ok; } @@ -783,27 +788,23 @@ void gl_renderer_sync_cpu(void) { * cropped to the display region [disp_x,disp_x+w) x [disp_y,disp_y+h). No * readback — this is the fast path for GPU-rendered (15-bit) frames. */ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear) { - if (!s_ctx || !s_present_crop_prog || !s_vram_tex) return; + if (!s_ctx || !s_fbo || !p_glBlitFramebuffer) return; int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - glViewport(0, 0, ww, wh); + + /* Exact integer-rect copy of the display region from the FBO to the window + * (no shader, no sub-texel UV sampling). The FBO's GL y-axis equals VRAM y + * (GEO_VS maps VRAM y=0 -> clip y=-1 -> framebuffer row 0); the default + * framebuffer has y=0 at the bottom, so we flip vertically by mapping src + * y [disp_y, disp_y+h] to dst y [wh, 0] — display top lands at window top. */ + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); glDisable(GL_SCISSOR_TEST); - glDisable(GL_BLEND); + glViewport(0, 0, ww, wh); glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_vram_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); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - p_glUseProgram(s_present_crop_prog); - p_glUniform1i(s_present_crop_uTex, 0); - p_glUniform4f(s_present_crop_uRect, - (float)disp_x / (float)VRAM_W, (float)disp_y / (float)VRAM_H, - (float)w / (float)VRAM_W, (float)h / (float)VRAM_H); - p_glBindVertexArray(s_present_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); - p_glBindVertexArray(0); - p_glUseProgram(0); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_fbo); + p_glBlitFramebuffer(disp_x, disp_y, disp_x + w, disp_y + h, + 0, wh, ww, 0, + GL_COLOR_BUFFER_BIT, linear ? GL_LINEAR : GL_NEAREST); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); SDL_GL_SwapWindow(s_win); } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 93a67768..aec3e610 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -104,6 +104,10 @@ static bool g_video_aa = true; /* linear present filtering */ static int g_video_texfilter = 0; /* 0=nearest, 1=bilinear */ static int g_video_renderer = 0; /* 0=software, 1=opengl (requested) */ static bool g_gl_active = false; /* GL context live -> GL present path */ +/* Present straight from the FBO (fast, no readback). Set PSX_GL_FORCE_CPU_PRESENT=1 + * to force the software readout path instead — a diagnostic/fallback that also + * keeps CPU VRAM current every frame (so screenshots reflect the screen). */ +static int g_gl_fbo_present = 1; /* Vsync self-heal state (see SDL_RenderPresent wrapper in * sdl_vblank_present). C linkage: freeze_heartbeat.c includes both in @@ -1120,7 +1124,7 @@ static void sdl_vblank_present(void) { * from the FBO — no 1 MB readback. Software / 24-bit (FMV) frames fall * through to the CPU readout below, after syncing the FBO down. */ #ifndef PSX_SDL_NO_RENDER - if (g_gl_active && !di.depth24 && gl_renderer_have_gpu_frame()) { + if (g_gl_active && g_gl_fbo_present && !di.depth24 && gl_renderer_have_gpu_frame()) { gl_renderer_present_vram((int)di.display_x, (int)di.display_y, (int)w, (int)h, g_video_aa ? 1 : 0); return; @@ -1269,6 +1273,8 @@ int main(int argc, char** argv) { g_video_aa = gc.runtime.video_antialiasing; g_video_texfilter = gc.runtime.video_texture_filter; g_video_renderer = gc.runtime.video_renderer; + { const char *e = std::getenv("PSX_GL_FORCE_CPU_PRESENT"); + if (e && e[0] && e[0] != '0') g_gl_fbo_present = 0; } game_entry_pc = gc.entry_pc; fast_boot = gc.runtime.fast_boot; /* Overlay DLL cache (Layer A). Off unless enabled in [runtime]; From 88b20c566659b6aa7e18c4abe44edc9174eebfa6 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 11 Jun 2026 15:55:36 -0700 Subject: [PATCH 13/18] =?UTF-8?q?docs:=20OpenGL=20renderer=20handoff=20?= =?UTF-8?q?=E2=80=94=20architecture,=20open=20issues,=20next=20tasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-off for the next session (Fable): current GL renderer architecture and verified state, the open visual issues with the diagnostic evidence gathered (menu jitter/seam = present-path alternation, not the GPU render; garbled text = missing texture window; fullscreen stretch = no aspect/letterbox; copy_rect still software), and the user-approved audio fade-in/out task for turbo transition windows. Co-Authored-By: Claude Opus 4.8 (1M context) --- GL_RENDERER_HANDOFF.md | 185 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 GL_RENDERER_HANDOFF.md diff --git a/GL_RENDERER_HANDOFF.md b/GL_RENDERER_HANDOFF.md new file mode 100644 index 00000000..f59b9743 --- /dev/null +++ b/GL_RENDERER_HANDOFF.md @@ -0,0 +1,185 @@ +# OpenGL Renderer — Handoff (for Fable) + +Branch: `feat/gl-renderer` (both psxrecomp and TombaRecomp). +Last commit this session: `b592447` (psxrecomp). +Status: **experimental, opt-in.** Software renderer is still the committed +default; OpenGL is selected per-game via `[video] renderer = "opengl"`. +TombaRecomp `game.toml` has a LOCAL (uncommitted) `renderer = "opengl"` + +`texture_filtering = "bilinear"` override for testing — do NOT commit those; +committed defaults are `software` / `nearest`. + +The goal of this work: add a hardware (OpenGL) renderer as a *selectable +option* alongside software, to enable higher internal-resolution scaling +without the software renderer's per-pixel CPU cost. Make it default-with- +software-fallback only once it renders cleanly. + +--- + +## Architecture (as built) + +- **Facade + vtable.** `gpu.c` calls `gr_*` (gpu_render.h/.c); a + `GpuRenderBackend` vtable dispatches to the software (`sw_*`) or OpenGL + (`glb_*`) backend. `gr_set_backend()` picks at startup from the config. +- **GL backend** lives in `runtime/src/gpu_gl_renderer.c` + (+ `runtime/include/gpu_gl_renderer.h`). VRAM is mirrored as a GL texture + (`s_vram_tex`, RGBA8) bound to an FBO (`s_fbo`) — the render target. A + second raw-16-bit integer texture (`s_vram_raw_tex`, GL_R16UI) is the + texture-sampling source for textured polys (CLUT decode in-shader). +- **What's on the GPU now:** flat/gouraud triangles; textured triangles + (4/8/15-bit CLUT, texel-0 discard, PS1 *2-around-0x80 modulation); + flat/textured rects; fills (scissored glClear); lines (GL_LINES); + semi-transparency (4 PS1 blend modes via fixed-function blend; textured + STP via a 2-pass opaque/semi split). +- **Still software** (route through CPU VRAM with FBO sync): `copy_rect` + (VRAM→VRAM blit), VRAM transfers in/out, single vram read/write. +- **Coherency:** two dirty flags (`s_cpu_dirty`/`s_gpu_dirty`). A GPU draw + uploads CPU→FBO first if CPU is ahead (`ensure_gpu`); a software op reads + FBO→CPU first if GPU is ahead (`ensure_cpu`). Both are **full 1 MB** + syncs today. +- **Present (main.cpp ~line 1118):** + - If the GPU drew this frame (`gl_renderer_have_gpu_frame()`) and the + display is 15-bit → `gl_renderer_present_vram()` blits the display + region from the FBO straight to the window (`glBlitFramebuffer`, no + readback). This is the fast path. + - Otherwise (software frame / 24-bit FMV) → `gl_renderer_sync_cpu()` then + the software readout path (`gpu_display_pixel_argb` → `gl_renderer_present`). + - `PSX_GL_FORCE_CPU_PRESENT=1` forces the software present path always + (diagnostic/fallback; also keeps CPU VRAM current every frame). +- Present forces **native resolution** under GL (`glb_scale`/`glb_set_scale` + return 1) — GPU draws write the FBO at native res only, not the software + SSAA hi-res mirror `g_hr`. GPU-side hi-res scaling is not implemented yet. + +## What works / is verified + +- Boots; renderer selectable; `[OpenGL]`/`[Software]` shown in window title. +- BIOS/Sony logos, FMV (MDEC 24-bit intro), and gameplay terrain all render. +- **Double-offset bug fixed** (`846e402`): `gpu.c` pre-applies `draw_offset` + to every primitive; the GPU triangle paths were adding it again, which + displaced all GPU geometry off-screen once the camera scrolled (symptom: + "the entire overworld is gone — only sprites left"). Fixed by not + re-applying `s_off` in `gpu_triangle`/`gpu_textured_triangle`. +- Loads are faster (user-confirmed) thanks to present-from-FBO removing the + per-frame readback for GPU scenes. +- Screenshot writes a real PNG (`ee0a454`) and now syncs the FBO down first + so captures reflect the on-screen frame under FBO-present (`b592447`). + +--- + +## OPEN ISSUES (what looks bad — fix these) + +### 1. Menu: vertical line splits the screen; the two halves JITTER badly +- Repro: skip to the LOAD memory-card screen (pink "TOMBA!" tiled bg). A + vertical seam runs top-to-bottom near center; the two halves shimmer/jitter + frame-to-frame. "Looks awful." +- **Key evidence:** with `PSX_GL_FORCE_CPU_PRESENT=1` the menu is CLEAN. So + the artifact is in the *present path*, NOT the GPU render of the frame. +- Switching the FBO present from a crop-shader to `glBlitFramebuffer` + (`b592447`) did **not** fix it — so it's almost certainly NOT sub-texel + sampling. +- **Leading hypothesis: the present path ALTERNATES frame-to-frame.** The + menu interleaves GPU draws (text/sprites) with software `copy_rect` (the + card-shell UI). Whether a frame ends on a GPU op (`s_gpu_dirty` → FBO/blit + present) or a software op (`s_cpu_dirty` → software present) flips between + frames. The two present paths likely differ in scale/position/seam → + jitter. The vertical seam may be where a `copy_rect` region meets GPU + content within one path. +- **Suggested fix:** make the FBO the single authoritative surface and use + ONE present path deterministically. The clean way is to move `copy_rect` + onto the GPU (FBO self-blit) so the software round-trip — and the path + flip — disappears. See issue 6. + +### 2. Gameplay: persistent horizontal dotted line under Tomba +- A dashed/dotted horizontal line sits at about mid-body height across the + open area. Persists in the user's view. +- NOT yet isolated to FBO-vs-software present (the attract demo didn't reach + this scene during the CPU-forced test). First step: reproduce with + `PSX_GL_FORCE_CPU_PRESENT=1` to see if it's render-side or present-side. +- Candidate causes: a textured-rect/sprite strip seam; missing texture + window (issue 3); or the same present-path alternation as issue 1. + +### 3. Garbled / fuzzy / misaligned text (e.g. the AP counter, top-right) +- Glyphs look smeared, doubled, and misaligned. +- **Leading cause: the GPU textured shader does NOT implement the PS1 + TEXTURE WINDOW** (mask/offset UV wrap). The software path applies it in + `gpu_sw_renderer.c` `texel_fetch` (`g_tw_mask_x/y`, `g_tw_off_x/y`, the + `u = (u & ~(mask*8)) | ((off & mask)*8)` masking). `glb_set_texture_window` + just forwards to SW and is ignored by the GPU shader. Sprites/text that + rely on UV wrapping render wrong. +- Also: `texture_filtering = "bilinear"` is honored only by the software + path; the GPU textured shader is nearest-only (issue 5). With bilinear + requested, the SW-vs-GPU mismatch can compound the fuzzy look. +- **Suggested fix:** pass the texture-window mask/offset as uniforms to the + textured fragment shader and apply the same masking before the texel + fetch. Then add an optional bilinear texel fetch (issue 5). + +### 4. Fullscreen does not preserve aspect ratio (stretches, no black bars) +- Going fullscreen stretches the image to fill the window instead of + letterboxing to the PSX display aspect. +- Both present paths map the source to the FULL drawable: + `gl_renderer_present` (quad over whole window) and + `gl_renderer_present_vram` (blit dst = `0,wh,ww,0`). +- **Suggested fix:** compute a 4:3 (or correct PSX display aspect) dst rect + centered in the drawable, clear the whole window to black first, then + present into the inset rect. Apply to BOTH present functions. + +### 5. Bilinear texture filtering not honored on the GPU path +- GPU textured shader does nearest texel fetch only. Software path supports + `texture_filtering = "bilinear"`. Add a bilinear path in the textured + fragment shader (manual 4-tap on the integer VRAM texture + CLUT, since + raw sampling is GL_R16UI/nearest). + +### 6. `copy_rect` still software → forces FBO↔CPU round-trips +- This is the structural driver of issue 1 (present-path alternation) and of + residual lag. Implementing GPU `copy_rect` as an FBO-region self-copy + (careful with overlap; may need a scratch texture or `glBlitFramebuffer` + with a temp) lets the FBO stay authoritative and removes the software + round-trip for menus and scrolling buffers. + +### 7. Coherency is full-1 MB sync per transition +- `ensure_gpu`/`ensure_cpu` upload/read the entire 1024×512 VRAM. Once + copy_rect is on the GPU and the FBO is authoritative, most syncs vanish; + what remains (texture/CLUT uploads via VRAM transfer) could be reduced to + dirty sub-rectangles. + +### 8. (Deferred goal) GPU-side internal-resolution scaling +- The original ask: "higher scaling without performance loss." Present + currently forces native res. Once the above render correctly, allocate the + FBO at N× native, scale GPU draw coordinates, and downsample/blit on + present. This is the payoff; don't start it until 1–6 are clean. + +--- + +## TASK FOR NEXT SESSION (user-approved, separate from GL) + +### Audio fade-in/fade-out at turbo transition windows — APPROVED, implement +- Context: `turbo_loads` runs load screens faster than real time. Tomba's + load BGM is *sequenced* (CPU writes SPU regs per game-frame), so under + turbo it speeds up into garble. We MUTE during turbo + (`main.cpp` ~line 1024–1032: `if (!turbo_loads_active) sdl_audio_pump();`). +- Decoupling audio to "normal pace" during turbo is **NOT feasible** — the + music's tempo is bound to the frame rate that turbo is compressing + (analysis in session). The mute stays. +- The mute's hard cut/resume causes awkward gaps. **User approved a quick + fade-in/fade-out** to smooth the edges (and a small hysteresis so very + short loads don't flicker the mute on/off). +- Implementation sketch: apply a gain ramp (~30–60 ms) to the SPU output + when entering/leaving `turbo_loads_active` — ramp down before muting, ramp + up on resume — rather than hard start/stop of `sdl_audio_pump()`. Apply the + gain in `spu_render` output or scale the queued `int16_t` samples. Add a + small minimum-turbo-duration / debounce so brief loads don't audibly dip. + +--- + +## Useful commands / paths + +- Build: `PATH=/c/msys64/mingw64/bin:$PATH cmake --build build-stable --target psx-runtime` + (run from `TombaRecomp/`; the GL source is in `psxrecomp/runtime/`). +- Run: `./build-stable/psx-runtime.exe --game game.toml` (BIOS path baked). +- Debug TCP 4470: `python ../psxrecomp/tools/debug_client.py --port 4470 ping` + and `... screenshot_file path=foo.png` (now a real PNG, FBO-synced). +- Force software present (diagnostic/fallback): set env + `PSX_GL_FORCE_CPU_PRESENT=1` before launch. +- Reusable WIP patches live in `psxrecomp/_wip/` (gitignored). +- Attract demo auto-plays gameplay on a cold boot (AP is always 97400 — it's + the canned demo). Tapping START a few times skips the long intro FMV to the + title/LOAD menu. From 06315384078b4def8a521ce7379a025c448b4875 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 12 Jun 2026 05:19:01 -0700 Subject: [PATCH 14/18] gl-renderer v2: GPU-authoritative VRAM FBO + Beetle-style filtered sampling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- runtime/include/gpu_gl_renderer.h | 51 +- runtime/src/gpu.c | 16 +- runtime/src/gpu_gl_renderer.c | 2298 ++++++++++++++++++----------- runtime/src/gpu_sw_renderer.c | 140 +- 4 files changed, 1628 insertions(+), 877 deletions(-) diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index f32c4fd9..ceeb0536 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -19,26 +19,59 @@ extern "C" { * Returns 1 on success, 0 to fall back to the SDL_Renderer present path. */ int gl_renderer_init_context(struct SDL_Window *win); -/* Present an ARGB8888 image (BGRA byte order) as a full-screen quad + swap. */ +/* Present an ARGB8888 image (BGRA byte order) as a letterboxed quad + swap. + * Used for 24-bit (FMV) frames and the PSX_GL_FORCE_CPU_PRESENT diagnostic. */ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear); /* Clear to black + swap (display-disabled frame). */ void gl_renderer_present_blank(void); -/* True when the GPU FBO holds the freshest VRAM (a GPU draw happened since the - * last CPU sync). The present path reads from the FBO directly when true. */ -int gl_renderer_have_gpu_frame(void); - -/* Sync the FBO down to CPU VRAM if the GPU side is ahead (else a no-op). The - * software / 24-bit (FMV) present path calls this before reading CPU VRAM. */ +/* Sync the authoritative FBO down to CPU VRAM if the GPU side is ahead (else + * a no-op). The 24-bit (FMV) present path, screenshots, and the debug server + * call this before reading CPU VRAM. */ void gl_renderer_sync_cpu(void); -/* Present straight from the FBO, cropped to the display region. No readback; - * fast path for GPU-rendered 15-bit frames. linear = filter on upscale. */ +/* THE present path for 15-bit frames: blit the display region straight from + * the authoritative VRAM FBO into a 4:3 letterboxed rect (no readback). + * Deterministic — used for every 15-bit frame. linear = filter on scale. */ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear); void gl_renderer_shutdown(void); +/* Diagnostics (debug server): read GPU-side VRAM without touching the CPU + * array; report coherency flags + dirty rects. fbo_peek returns 0 when the + * GL pipeline is inactive. */ +int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out); +void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]); + +/* Always-on coherency event ring (debug server "gl_coh_ring"): every upload + * flush, fill, copy, draw bbox, pack, full readback, present, and probe + * perturbation, with rect + frame. An op that flushes internally records its + * own event AFTER the FLUSH it caused (the event after a FLUSH = trigger). */ +enum { + GL_COH_FLUSH = 1, /* CPU->FBO upload flush (pending box) */ + GL_COH_FILL = 2, /* GP0(02) fill rect */ + GL_COH_COPY_SRC = 3, /* GP0(80) copy, source rect */ + GL_COH_COPY = 4, /* GP0(80) copy, dest rect */ + GL_COH_DRAW = 5, /* drawn prim bbox (clipped to draw area) */ + GL_COH_PACK = 6, /* hr FBO -> raw mirror pack (dirty box) */ + GL_COH_ENSURE = 7, /* full FBO -> CPU VRAM readback */ + GL_COH_PRESENT = 8, /* 15-bit present blit (display rect) */ + GL_COH_UPLOAD = 9, /* bulk CPU->VRAM transfer_in dest rect */ + GL_COH_PEEK = 10, /* gl_fbo_peek probe (perturbs: flushes) */ + GL_COH_DIFF = 11, /* gl_vram_diff probe (perturbs: flushes) */ +}; + +typedef struct { + uint32_t frame; + uint8_t kind; + int16_t x0, y0, x1, y1; /* native VRAM coords, inclusive */ +} GlCohEvent; + +uint64_t gl_renderer_coh_total(void); +/* Fetch event by absolute sequence number; 0 if evicted or out of range. */ +int gl_renderer_coh_get(uint64_t seq, GlCohEvent *out); + #ifdef __cplusplus } #endif diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index afd67387..531043e5 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -304,12 +304,15 @@ uint32_t gpu_read_gpuread(void) { if (!vram_read_active) return gpuread_latch; - /* Read two 16-bit pixels from VRAM and pack into one 32-bit word */ + /* Read two 16-bit pixels from VRAM and pack into one 32-bit word. + * Routed through the renderer facade: under the GL backend the GPU-side + * framebuffer is authoritative and must be synced down before the CPU + * array is read (no-op cost on the software backend). */ uint32_t value = 0; for (int i = 0; i < 2; i++) { uint16_t rx = (vram_read_x + vram_read_col) % 1024; uint16_t ry = (vram_read_y + vram_read_row) % 512; - value |= (uint32_t)vram[ry * 1024 + rx] << (i * 16); + value |= (uint32_t)gr_vram_read((int)rx, (int)ry) << (i * 16); if (++vram_read_col == vram_read_w) { vram_read_col = 0; @@ -1561,7 +1564,8 @@ void gpu_get_draw_area(GpuDrawArea* out) { uint16_t gpu_vram_peek(int x, int y) { if (x < 0 || x >= 1024 || y < 0 || y >= 512) return 0; - return vram[y * 1024 + x]; + /* Through the facade so the GL backend syncs its FBO down first. */ + return gr_vram_read(x, y); } void gpu_write_gp0(uint32_t val) { @@ -1581,8 +1585,10 @@ void gpu_write_gp0(uint32_t val) { uint16_t wx = (vram_write_x + vram_write_col) % 1024; uint16_t wy = (vram_write_y + vram_write_row) % 512; - /* Respect mask bit settings */ - if (check_mask_bit && (vram[wy * 1024 + wx] & 0x8000)) + /* Respect mask bit settings. The check reads through the facade: + * under the GL backend GPU-drawn mask bits live in the FBO, not + * the CPU array (one sync per burst; free when check is off). */ + if (check_mask_bit && (gr_vram_read((int)wx, (int)wy) & 0x8000)) goto next_pixel; if (set_mask_bit) diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 96f7fbfa..2085723b 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -1,831 +1,1467 @@ -/* gpu_gl_renderer.c — hardware OpenGL renderer backend. - * - * PHASE 2b step 2 (current): first real GPU rasterization. VRAM is mirrored as - * a GL texture bound to an FBO (the render target); OPAQUE flat and gouraud - * triangles are rasterized on the GPU into that FBO. Everything else — - * textured polys, rects/sprites, lines, fills, semi-transparent polys, VRAM - * transfers/copies — still runs on the software rasterizer over CPU VRAM. - * - * Coherency: CPU VRAM and the FBO are kept in sync with two dirty flags. A GPU - * draw first uploads CPU VRAM if the CPU side is ahead; a software op (or the - * display readout) first reads the FBO back if the GPU side is ahead. So the - * two paths interleave correctly in PS1 draw order. The sync lives entirely in - * the backend vtable — main.cpp/present are unchanged (they still read the - * software display via render_display, which syncs the FBO down first). - * - * This per-op full-VRAM sync is intentionally simple, not fast: with most of - * Tomba's drawing still software (textured), it thrashes and runs slow. That's - * expected for this step — the win arrives once textured polys move to the GPU - * too (next steps), at which point the sync largely disappears. - * - * Present pipeline + GL loader: see the modern-GL section below (shared with - * step 1). Legacy immediate-mode present remains the fallback. */ - -#include "gpu_render.h" -#include "gpu_sw_renderer.h" - -#include -#include -#include -#include -#include -#include - -#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 2.0+/3.0 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_COLOR_ATTACHMENT0 0x8CE0 -#define PSXGL_FRAMEBUFFER_COMPLETE 0x8CD5 -#define PSXGL_R16UI 0x8234 -#define PSXGL_RED_INTEGER 0x8D94 -#define PSXGL_TEXTURE1 0x84C1 -#define PSXGL_FUNC_ADD 0x8006 -#define PSXGL_FUNC_REVERSE_SUBTRACT 0x800B -#define PSXGL_CONSTANT_ALPHA 0x8003 -#define PSXGL_READ_FRAMEBUFFER 0x8CA8 -#define PSXGL_DRAW_FRAMEBUFFER 0x8CA9 - -#ifndef APIENTRY -#define APIENTRY -#endif - -#define VRAM_W 1024 -#define VRAM_H 512 - -/* ---- 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_glUniform2i)(GLint, GLint, GLint); -typedef void (APIENTRY *PFN_glUniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat); -typedef void (APIENTRY *PFN_glBlendEquation)(GLenum); -typedef void (APIENTRY *PFN_glBlendColor)(GLfloat, GLfloat, GLfloat, GLfloat); -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); - -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_glUniform2i p_glUniform2i; -static PFN_glUniform4f p_glUniform4f; -static PFN_glBlendEquation p_glBlendEquation; -static PFN_glBlendColor p_glBlendColor; -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 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_glUniform2i, "glUniform2i"); LOAD(p_glUniform4f, "glUniform4f"); - LOAD(p_glBlendEquation, "glBlendEquation"); LOAD(p_glBlendColor, "glBlendColor"); - 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"); -#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 (authoritative mirror) */ - -static GLuint s_present_tex = 0; -static int s_present_w = 0, s_present_h = 0; -static int s_modern_ok = 0; -static GLuint s_present_prog = 0, s_present_vao = 0; -static GLint s_present_uTex = -1; -static GLuint s_present_crop_prog = 0; -static GLint s_present_crop_uTex = -1, s_present_crop_uRect = -1; - -/* GPU rasterization target: VRAM as an RGBA8 texture + FBO. */ -static int s_raster_ok = 0; /* GPU geometry path available */ -static GLuint s_vram_tex = 0; -static GLuint s_fbo = 0; -static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0; -/* Textured polys: raw 16-bit VRAM as an integer sampler + a CLUT-decoding - * fragment shader. The raw texture mirrors CPU VRAM (synced in ensure_gpu); - * sampling freshly-GPU-drawn pixels (render-to-texture within a GPU batch) is a - * known limitation handled later — uploaded textures (the common case) work. */ -static GLuint s_vram_raw_tex = 0; -static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0; -static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1, s_uRaw = -1, s_uSemipass = -1; -static uint16_t *s_raw = NULL; /* 1024*512 raw-uint16 staging */ -static int s_cpu_dirty = 0; /* CPU VRAM has changes not in FBO */ -static int s_gpu_dirty = 0; /* FBO has changes not in CPU VRAM */ -static uint32_t *s_conv = NULL; /* 1024*512 RGBA8 staging for sync */ - -/* draw state mirrored from the vtable set_* calls (for GPU prims) */ -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; - -/* ---- 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"; - -/* Present straight from the FBO color texture (the live VRAM render target), - * cropped to the display region. u_rect = (u0, v0_top, uw, vh) in 0..1 texture - * space; screen top maps to the display's top VRAM row (v0_top). */ -static const char *PRESENT_CROP_VS = - "#version 330\n" - "uniform vec4 u_rect;\n" - "out vec2 v_uv;\n" - "void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n" - " v_uv = vec2(u_rect.x + p.x*u_rect.z, u_rect.y + (1.0-p.y)*u_rect.w);\n" - " gl_Position = vec4(p*2.0-1.0, 0.0, 1.0); }\n"; - -/* Geometry: position in VRAM pixels (draw offset already applied), color in - * 0..1. Transform to clip space over the 1024x512 VRAM; no Y flip (VRAM y=0 -> - * clip.y=-1 -> FBO row 0 -> glReadPixels row 0 -> CPU VRAM row 0). */ -static const char *GEO_VS = - "#version 330\n" - "layout(location=0) in vec2 a_pos;\n" - "layout(location=1) in vec4 a_col;\n" - "noperspective out vec4 v_col;\n" - "void main(){ v_col = a_col;\n" - " float cx = a_pos.x/512.0 - 1.0;\n" - " float cy = a_pos.y/256.0 - 1.0;\n" - " gl_Position = vec4(cx, cy, 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 polys: pos in VRAM px (offset applied), uv = texel coords, color = - * modulation. Fragment samples raw VRAM (integer), decodes per color depth - * with CLUT lookup, discards texel 0 (PS1 transparent), applies modulation - * (PS1: texel*color/128, i.e. *2 around a 0x80 identity). */ -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" - "noperspective out vec2 v_uv; noperspective out vec4 v_col;\n" - "void main(){ v_uv = a_uv; v_col = a_col;\n" - " gl_Position = vec4(a_pos.x/512.0 - 1.0, a_pos.y/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, in VRAM pixels */\n" - "uniform ivec2 u_clut; /* CLUT base, in VRAM pixels */\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=draw all, 1=opaque texels only, 2=semi texels only */\n" - "int vram(int x,int y){ return int(texelFetch(u_vram, ivec2(x,y), 0).r); }\n" - "void main(){\n" - " int u = int(v_uv.x + 0.5); int v = int(v_uv.y + 0.5);\n" - " int raw;\n" - " if (u_depth == 0) {\n" - " int px = vram(u_tpage.x + (u>>2), u_tpage.y + v);\n" - " int idx = (px >> ((u & 3)*4)) & 0xF;\n" - " raw = vram(u_clut.x + idx, u_clut.y);\n" - " } else if (u_depth == 1) {\n" - " int px = vram(u_tpage.x + (u>>1), u_tpage.y + v);\n" - " int idx = (px >> ((u & 1)*8)) & 0xFF;\n" - " raw = vram(u_clut.x + idx, u_clut.y);\n" - " } else {\n" - " raw = vram(u_tpage.x + u, u_tpage.y + v);\n" - " }\n" - " if (raw == 0) discard;\n" - " int stp = (raw >> 15) & 1; /* per-texel semi-transparency bit */\n" - " if (u_semipass == 1 && stp == 1) discard; /* opaque pass skips semi texels */\n" - " if (u_semipass == 2 && stp == 0) discard; /* semi pass skips opaque texels */\n" - " vec3 t = vec3(float(raw&31), float((raw>>5)&31), float((raw>>10)&31)) / 31.0;\n" - " if (u_raw == 0) t = clamp(t * v_col.rgb * 2.0, 0.0, 1.0);\n" - " frag = vec4(t, 1.0);\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 */ -} -static inline uint16_t conv_rgba8_to_1555(uint32_t c) { - uint16_t r = (c & 0xFF) >> 3, g = ((c >> 8) & 0xFF) >> 3, b = ((c >> 16) & 0xFF) >> 3; - uint16_t m = ((c >> 24) & 0x80) ? 0x8000 : 0; - return (uint16_t)(r | (g << 5) | (b << 10) | m); -} - -/* ---- coherency --------------------------------------------------------- */ -static void ensure_gpu(void) { /* make the FBO reflect CPU VRAM */ - if (!s_raster_ok || !s_cpu_dirty) return; - for (int i = 0; i < VRAM_W * VRAM_H; i++) s_conv[i] = conv_1555_to_rgba8(s_vram[i]); - glBindTexture(GL_TEXTURE_2D, s_vram_tex); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VRAM_W, VRAM_H, GL_RGBA, GL_UNSIGNED_BYTE, s_conv); - /* raw 16-bit VRAM for texture sampling (upload CPU VRAM directly) */ - glBindTexture(GL_TEXTURE_2D, s_vram_raw_tex); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VRAM_W, VRAM_H, - PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, s_vram); - s_cpu_dirty = 0; -} -static void ensure_cpu(void) { /* make CPU VRAM reflect the FBO */ - if (!s_raster_ok || !s_gpu_dirty) return; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); - glReadPixels(0, 0, VRAM_W, VRAM_H, GL_RGBA, GL_UNSIGNED_BYTE, s_conv); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - for (int i = 0; i < VRAM_W * VRAM_H; i++) s_vram[i] = conv_rgba8_to_1555(s_conv[i]); - s_gpu_dirty = 0; -} - -/* Configure fixed-function blending for a PS1 semi-transparency mode, with the - * incoming fragment as source (F = front) and the FBO as dest (B = back): - * 0: B/2 + F/2 1: B + F 2: B - F 3: B + F/4 - * Modes are exact in 5-bit; GL blends in normalized [0,1] which is the same - * proportional math, and the framebuffer clamp matches the PS1 saturate. */ -static void apply_psx_blend(int mode) { - glEnable(GL_BLEND); - switch (mode & 3) { - case 0: /* B/2 + F/2 */ - p_glBlendColor(0.5f, 0.5f, 0.5f, 0.5f); - p_glBlendEquation(PSXGL_FUNC_ADD); - glBlendFunc(PSXGL_CONSTANT_ALPHA, PSXGL_CONSTANT_ALPHA); - break; - case 1: /* B + F */ - p_glBlendEquation(PSXGL_FUNC_ADD); - glBlendFunc(GL_ONE, GL_ONE); - break; - case 2: /* B - F (dst*1 - src*1) */ - p_glBlendEquation(PSXGL_FUNC_REVERSE_SUBTRACT); - glBlendFunc(GL_ONE, GL_ONE); - break; - case 3: /* B + F/4 */ - p_glBlendColor(0.25f, 0.25f, 0.25f, 0.25f); - p_glBlendEquation(PSXGL_FUNC_ADD); - glBlendFunc(PSXGL_CONSTANT_ALPHA, GL_ONE); - break; - } -} - -/* Rasterize one triangle (3 verts: x,y in raw coords; colors 1555) on the GPU. - * semi<0 = opaque; semi 0..3 = blend with that PS1 semi-transparency mode. */ -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) { - ensure_gpu(); - float verts[3*6]; - /* gpu.c already applied the draw offset before calling the gr_* facade - * (see gp0_exec_* in gpu.c); coords arrive in final VRAM space. */ - int xs[3] = {x0, x1, x2}; - int ys[3] = {y0, y1, y2}; - uint16_t cs[3] = {c0,c1,c2}; - for (int i = 0; i < 3; 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] = 1.0f; - } - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); - glViewport(0, 0, VRAM_W, VRAM_H); - glEnable(GL_SCISSOR_TEST); - int sx = s_area_x1, sy = s_area_y1; - 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(sx, sy, sw, sh); /* FBO y matches VRAM y (no flip) */ - if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); - p_glUseProgram(s_geo_prog); - 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, 3); - p_glBindVertexArray(0); - p_glUseProgram(0); - glDisable(GL_BLEND); - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - s_gpu_dirty = 1; -} - -/* Rasterize one textured triangle on the GPU. Coords raw (offset applied here), - * uv = texel coords, col = per-vertex modulation RGB (0..1, 3 verts × 3). */ -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) { - ensure_gpu(); - float verts[3 * 8]; - for (int i = 0; i < 3; i++) { - /* coords already offset by gpu.c — do not re-apply s_off here */ - 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; - } - int base_x = (texpage & 0xF) * 64; - int base_y = ((texpage >> 4) & 1) * 256; - int depth = (texpage >> 7) & 3; if (depth > 2) depth = 2; - - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); - glViewport(0, 0, VRAM_W, VRAM_H); - glEnable(GL_SCISSOR_TEST); - 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_area_y1, sw, sh); - p_glUseProgram(s_tex_prog); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_vram_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_glBindVertexArray(s_tex_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); - p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); - if (semi < 0) { - /* opaque primitive: every texel drawn, no blend */ - glDisable(GL_BLEND); - p_glUniform1i(s_uSemipass, 0); - glDrawArrays(GL_TRIANGLES, 0, 3); - } else { - /* semi-transparent primitive: the per-texel STP bit decides whether a - * texel is opaque or blended. Pass 1 lays down the opaque texels with - * blend off; pass 2 blends the semi texels in the requested mode. */ - glDisable(GL_BLEND); - p_glUniform1i(s_uSemipass, 1); - glDrawArrays(GL_TRIANGLES, 0, 3); - apply_psx_blend(semi); - p_glUniform1i(s_uSemipass, 2); - glDrawArrays(GL_TRIANGLES, 0, 3); - } - p_glBindVertexArray(0); - p_glUseProgram(0); - glDisable(GL_BLEND); - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - s_gpu_dirty = 1; -} - -/* A flat/gouraud rectangle is two triangles; reuse the geometry path so it - * shares scissor/blend/offset behavior. Coords already offset by gpu.c. */ -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); -} - -/* A textured rectangle is two textured triangles with the UV rectangle mapped - * to its corners. u0,v0 = top-left texel; u1,v1 = bottom-right texel. */ -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}; - 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); - 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); -} - -/* GP0(02h) fill: write a solid color straight into VRAM, ignoring draw area, - * mask, semi-transparency and offset. A scissored glClear is exactly this. */ -static void gpu_fill(int x,int y,int w,int h,uint16_t c) { - if (w <= 0 || h <= 0) return; - ensure_gpu(); - float r=(c&0x1F)/31.0f, g=((c>>5)&0x1F)/31.0f, b=((c>>10)&0x1F)/31.0f; - int sx=x, sy=y, sw=w, sh=h; - if (sx<0){ sw+=sx; sx=0; } if (sy<0){ sh+=sy; sy=0; } - if (sw<0) sw=0; if (sh<0) sh=0; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); - glViewport(0,0,VRAM_W,VRAM_H); - glEnable(GL_SCISSOR_TEST); - glScissor(sx, sy, sw, sh); - glDisable(GL_BLEND); - glClearColor(r,g,b,1.0f); - glClear(GL_COLOR_BUFFER_BIT); - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - s_gpu_dirty = 1; -} - -/* 1px line via GL_LINES; coords already offset, clipped to the draw area. */ -static void gpu_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int semi) { - ensure_gpu(); - float verts[2*6]; - int xs[2]={x0,x1}, ys[2]={y0,y1}; uint16_t cs[2]={c0,c1}; - for (int i=0;i<2;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]=1.0f; - } - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); - glViewport(0,0,VRAM_W,VRAM_H); - glEnable(GL_SCISSOR_TEST); - 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_area_y1, sw, sh); - if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); - p_glUseProgram(s_geo_prog); - 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_LINES, 0, 2); - p_glBindVertexArray(0); - p_glUseProgram(0); - glDisable(GL_BLEND); - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - s_gpu_dirty = 1; -} - -/* ---- backend vtable wrappers ------------------------------------------- */ -static void glb_init(uint16_t *vram) { s_vram = vram; sw_renderer_init(vram); } -/* GPU draws write the FBO (native res), not the software SSAA hi-res mirror, so - * presenting via the hi-res mirror would drop every GPU-rendered layer. Force - * native-res present under the GL backend; GPU-side high-res scaling is a later - * step (the FBO will simply be allocated larger). */ -static void glb_set_scale(int s) { (void)s; sw_renderer_set_scale(1); } -static int glb_scale(void) { return 1; } -static void glb_set_texture_filter(int b) { sw_set_texture_filter(b); } -static int glb_texture_filter(void) { return sw_texture_filter(); } - -static void glb_set_semi_transparency(int e, int m) { s_semi_en = e; s_semi_mode = m; sw_set_semi_transparency(e, m); } -static void glb_set_mask_bits(int s, int c) { sw_set_mask_bits(s, c); } -static void glb_set_texture_window(uint32_t r) { 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); } - -/* GPU-rasterized: flat / gouraud triangles, opaque and semi-transparent. */ -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) { gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1); return; } - ensure_cpu(); sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col); s_cpu_dirty = 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) { gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1); return; } - ensure_cpu(); sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2); s_cpu_dirty = 1; -} - -/* GPU-rasterized fill. copy_rect (VRAM->VRAM blit) stays on the software path - * for now: an in-place FBO self-blit with arbitrary overlap is the one case GL - * can't express with a single glBlitFramebuffer. */ -static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){ - if (s_raster_ok) { gpu_fill(x,y,w,h,c); return; } - ensure_cpu(); sw_fill_rect(x,y,w,h,c); s_cpu_dirty=1; -} -static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){ ensure_cpu(); sw_copy_rect(sx,sy,dx,dy,w,h); s_cpu_dirty=1; } -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 && s_tex_prog) { - 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); return; - } - ensure_cpu(); sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp); s_cpu_dirty=1; -} -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 && s_tex_prog) { - 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); return; - } - ensure_cpu(); sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); s_cpu_dirty=1; -} -static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){ - if (s_raster_ok) { gpu_flat_rect(x,y,w,h,c, s_semi_en?s_semi_mode:-1); return; } - ensure_cpu(); sw_draw_flat_rect(x,y,w,h,c); s_cpu_dirty=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 && s_tex_prog) { gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1); return; } - ensure_cpu(); sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp); s_cpu_dirty=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 && s_tex_prog) { gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1); return; } - ensure_cpu(); sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp); s_cpu_dirty=1; -} -static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){ - if (s_raster_ok) { gpu_line(x0,y0,c, x1,y1,c, s_semi_en?s_semi_mode:-1); return; } - ensure_cpu(); sw_draw_line(x0,y0,x1,y1,c); s_cpu_dirty=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) { gpu_line(x0,y0,c0, x1,y1,c1, s_semi_en?s_semi_mode:-1); return; } - ensure_cpu(); sw_draw_shaded_line(x0,y0,c0,x1,y1,c1); s_cpu_dirty=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){ ensure_cpu(); sw_vram_write(x,y,px); s_cpu_dirty=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){ ensure_cpu(); sw_vram_transfer_in(x,y,w,h,d); s_cpu_dirty=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); - } -} - -static void init_gpu_raster(void) { - s_geo_prog = build_program(GEO_VS, GEO_FS); - if (!s_geo_prog) { fprintf(stdout, "psxrecomp: GL geometry program failed; GPU raster off\n"); return; } - s_conv = (uint32_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint32_t)); - if (!s_conv) return; - glGenTextures(1, &s_vram_tex); - glBindTexture(GL_TEXTURE_2D, s_vram_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, VRAM_W, VRAM_H, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); - p_glGenFramebuffers(1, &s_fbo); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_fbo); - p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, s_vram_tex, 0); - 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; } - 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_glBindVertexArray(0); - - /* Textured path: raw 16-bit VRAM integer sampler + CLUT-decode program. */ - s_tex_prog = build_program(TEX_VS, TEX_FS); - if (s_tex_prog) { - s_raw = (uint16_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint16_t)); - glGenTextures(1, &s_vram_raw_tex); - glBindTexture(GL_TEXTURE_2D, s_vram_raw_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexImage2D(GL_TEXTURE_2D, 0, PSXGL_R16UI, VRAM_W, VRAM_H, 0, - PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, NULL); - 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"); - 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_glBindVertexArray(0); - } - - s_cpu_dirty = 1; /* FBO empty; first GPU draw uploads current VRAM */ - s_raster_ok = 1; - fprintf(stdout, "psxrecomp: GPU rasterization enabled (opaque flat/gouraud%s polys)\n", - s_tex_prog ? " + textured" : ""); -} - -int gl_renderer_init_context(SDL_Window *win) { - s_win = win; - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - 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); - 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); - 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 : "?"); - - s_modern_ok = load_modern_gl(); - if (s_modern_ok) { - 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 s_modern_ok = 0; - s_present_crop_prog = build_program(PRESENT_CROP_VS, PRESENT_FS); - if (s_present_crop_prog) { - s_present_crop_uTex = p_glGetUniformLocation(s_present_crop_prog, "u_tex"); - s_present_crop_uRect = p_glGetUniformLocation(s_present_crop_prog, "u_rect"); - } - } - fprintf(stdout, "psxrecomp: GL present path = %s\n", s_modern_ok ? "shader" : "immediate(legacy)"); - if (s_modern_ok) init_gpu_raster(); - return 1; -} - -void gl_renderer_shutdown(void) { - if (s_ctx) { - ensure_cpu(); - if (s_present_tex) glDeleteTextures(1, &s_present_tex); - if (s_vram_tex) glDeleteTextures(1, &s_vram_tex); - if (s_vram_raw_tex) glDeleteTextures(1, &s_vram_raw_tex); - SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; - } - free(s_conv); s_conv = NULL; - free(s_raw); s_raw = NULL; -} - -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); - glViewport(0, 0, ww, wh); - glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); - p_glActiveTexture(PSXGL_TEXTURE0); - upload_present_tex(pixels, src_w, src_h, linear); - if (s_modern_ok) { - 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); - } else { - glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, s_present_tex); - glBegin(GL_QUADS); - glTexCoord2f(0,0); glVertex2f(-1, 1); glTexCoord2f(1,0); glVertex2f(1, 1); - glTexCoord2f(1,1); glVertex2f(1,-1); glTexCoord2f(0,1); glVertex2f(-1,-1); - glEnd(); - } - 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); - glViewport(0, 0, ww, wh); glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); - SDL_GL_SwapWindow(s_win); -} - -/* True when the FBO holds the freshest VRAM (a GPU draw happened since the last - * CPU sync). The present path uses this to read straight from the FBO instead - * of forcing a full readback to CPU VRAM. */ -int gl_renderer_have_gpu_frame(void) { - return s_raster_ok && s_gpu_dirty; -} - -/* Sync the FBO down into CPU VRAM (no-op if the CPU side is already current). - * Called by the software/24-bit present path before it reads CPU VRAM. */ -void gl_renderer_sync_cpu(void) { - ensure_cpu(); -} - -/* Present straight from the FBO color texture (the live VRAM render target), - * cropped to the display region [disp_x,disp_x+w) x [disp_y,disp_y+h). No - * readback — this is the fast path for GPU-rendered (15-bit) frames. */ -void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear) { - if (!s_ctx || !s_fbo || !p_glBlitFramebuffer) return; - int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - - /* Exact integer-rect copy of the display region from the FBO to the window - * (no shader, no sub-texel UV sampling). The FBO's GL y-axis equals VRAM y - * (GEO_VS maps VRAM y=0 -> clip y=-1 -> framebuffer row 0); the default - * framebuffer has y=0 at the bottom, so we flip vertically by mapping src - * y [disp_y, disp_y+h] to dst y [wh, 0] — display top lands at window top. */ - 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_fbo); - p_glBlitFramebuffer(disp_x, disp_y, disp_x + w, disp_y + h, - 0, wh, ww, 0, - GL_COLOR_BUFFER_BIT, linear ? GL_LINEAR : GL_NEAREST); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - SDL_GL_SwapWindow(s_win); -} - -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; } +/* 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 +#include +#include +#include +#include +#include + +#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; } diff --git a/runtime/src/gpu_sw_renderer.c b/runtime/src/gpu_sw_renderer.c index 3afb874e..1c081f3f 100644 --- a/runtime/src/gpu_sw_renderer.c +++ b/runtime/src/gpu_sw_renderer.c @@ -281,48 +281,110 @@ static uint16_t texel_fetch(int u, int v, uint16_t texpage, } } +/* Per-prim uv sampling bounds (inclusive), Beetle-PSX model: bilinear + * neighbours clamp to the prim's own texture rect so they never blend in + * texels from a neighbouring sprite/tile or empty VRAM. Set by every + * textured prim entry point; ignored while a texture window is active + * (texel_fetch applies the window instead, which wraps by design). */ +static int g_uv_lim[4] = { 0, 0, 255, 255 }; + +static uint16_t bl_fetch(int u, int v, uint16_t texpage, + uint16_t clut_x, uint16_t clut_y) { + u &= 0xFF; v &= 0xFF; + if (!(g_tw_mask_x | g_tw_mask_y)) { + if (u < g_uv_lim[0]) u = g_uv_lim[0]; else if (u > g_uv_lim[2]) u = g_uv_lim[2]; + if (v < g_uv_lim[1]) v = g_uv_lim[1]; else if (v > g_uv_lim[3]) v = g_uv_lim[3]; + } + return texel_fetch(u, v, texpage, clut_x, clut_y); +} + /* Bilinear texel sample, in RGB space (after the CLUT lookup — never * interpolate palette indices). fu/fv are texel-space coordinates. * - * Transparency-aware: returns 0x0000 (transparent) when the nearest texel is - * transparent, so sprite cutouts are preserved; transparent *neighbours* are - * replaced by the nearest texel's colour so edges don't bleed toward black. - * Neighbours wrap within the 256-texel page (matches the nearest path's &0xFF) - * which keeps the blend inside one texture instead of bleeding across pages. */ + * Beetle-PSX formulation: the NEAREST texel is the base (cutout + STP + * authority), the neighbours lie toward the sub-texel offset and clamp to + * g_uv_lim, and each texel's weight is gated by its opacity with the colour + * renormalised — so prim edges and cutout borders keep their colour instead + * of dissolving into the transparent (black) neighbour and dropping whole + * edge columns (the v1 "-0.5 then floor" base sampled one texel OUTSIDE the + * prim on its top/left edges). Matches the GL TEX shader's bilinear path. */ static uint16_t texel_fetch_bilinear(float fu, float fv, uint16_t texpage, uint16_t clut_x, uint16_t clut_y) { - float su = fu - 0.5f, sv = fv - 0.5f; - int iu = (int)floorf(su); - int iv = (int)floorf(sv); - int fx = (int)((su - (float)iu) * 256.0f); - int fy = (int)((sv - (float)iv) * 256.0f); - if (fx < 0) fx = 0; else if (fx > 256) fx = 256; - if (fy < 0) fy = 0; else if (fy > 256) fy = 256; + int iu = (int)floorf(fu); + int iv = (int)floorf(fv); + int fx = (int)((fu - (float)iu) * 256.0f) - 128; + int fy = (int)((fv - (float)iv) * 256.0f) - 128; + int su = fx < 0 ? -1 : 1, sv = fy < 0 ? -1 : 1; + if (fx < 0) fx = -fx; + if (fy < 0) fy = -fy; + if (fx > 128) fx = 128; + if (fy > 128) fy = 128; - uint16_t c00 = texel_fetch(iu & 0xFF, iv & 0xFF, texpage, clut_x, clut_y); - if (c00 == 0x0000) return 0x0000; /* preserve cutout */ - uint16_t c10 = texel_fetch((iu + 1) & 0xFF, iv & 0xFF, texpage, clut_x, clut_y); - uint16_t c01 = texel_fetch(iu & 0xFF, (iv + 1) & 0xFF, texpage, clut_x, clut_y); - uint16_t c11 = texel_fetch((iu + 1) & 0xFF, (iv + 1) & 0xFF, texpage, clut_x, clut_y); - if (c10 == 0x0000) c10 = c00; - if (c01 == 0x0000) c01 = c00; - if (c11 == 0x0000) c11 = c00; + uint16_t c00 = bl_fetch(iu, iv, texpage, clut_x, clut_y); + uint16_t c10 = bl_fetch(iu + su, iv, texpage, clut_x, clut_y); + uint16_t c01 = bl_fetch(iu, iv + sv, texpage, clut_x, clut_y); + uint16_t c11 = bl_fetch(iu + su, iv + sv, texpage, clut_x, clut_y); - int r00 = c00 & 0x1F, g00 = (c00 >> 5) & 0x1F, b00 = (c00 >> 10) & 0x1F; - int r10 = c10 & 0x1F, g10 = (c10 >> 5) & 0x1F, b10 = (c10 >> 10) & 0x1F; - int r01 = c01 & 0x1F, g01 = (c01 >> 5) & 0x1F, b01 = (c01 >> 10) & 0x1F; - int r11 = c11 & 0x1F, g11 = (c11 >> 5) & 0x1F, b11 = (c11 >> 10) & 0x1F; + int w00 = (c00 ? 1 : 0) * (256 - fx) * (256 - fy); + int w10 = (c10 ? 1 : 0) * fx * (256 - fy); + int w01 = (c01 ? 1 : 0) * (256 - fx) * fy; + int w11 = (c11 ? 1 : 0) * fx * fy; + int opac = w00 + w10 + w01 + w11; + if (opac < 32768) return 0x0000; /* opacity < 0.5: transparent */ - int w00 = (256 - fx) * (256 - fy); - int w10 = fx * (256 - fy); - int w01 = (256 - fx) * fy; - int w11 = fx * fy; + int r = ((c00 & 0x1F) * w00 + (c10 & 0x1F) * w10 + + (c01 & 0x1F) * w01 + (c11 & 0x1F) * w11) / opac; + int g = (((c00 >> 5) & 0x1F) * w00 + ((c10 >> 5) & 0x1F) * w10 + + ((c01 >> 5) & 0x1F) * w01 + ((c11 >> 5) & 0x1F) * w11) / opac; + int b = (((c00 >> 10) & 0x1F) * w00 + ((c10 >> 10) & 0x1F) * w10 + + ((c01 >> 10) & 0x1F) * w01 + ((c11 >> 10) & 0x1F) * w11) / opac; + int stp = (((c00 >> 15) & 1) * w00 + ((c10 >> 15) & 1) * w10 + + ((c01 >> 15) & 1) * w01 + ((c11 >> 15) & 1) * w11) * 2 >= opac; - int r = (r00 * w00 + r10 * w10 + r01 * w01 + r11 * w11) >> 16; - int g = (g00 * w00 + g10 * w10 + g01 * w01 + g11 * w11) >> 16; - int b = (b00 * w00 + b10 * w10 + b01 * w01 + b11 * w11) >> 16; + return (uint16_t)(r | (g << 5) | (b << 10) | (stp ? 0x8000 : 0)); +} - return (uint16_t)(r | (g << 5) | (b << 10) | (c00 & 0x8000)); +/* uv sampling bounds for a textured triangle (see g_uv_lim): min/max of the + * vertex uvs; for axis-aligned (2D) mappings — any zero uv derivative — the + * max-uv vertex is an exclusive edge whose texel the DDA never samples, so + * back it off by one. Crossing a 256 boundary means page wrap: widen to the + * full page (clamp disabled). */ +static void sw_tri_uv_limits(const int *xs, const int *ys, + const int *us, const int *vs) { + 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; } + g_uv_lim[0] = lo_u; g_uv_lim[1] = lo_v; g_uv_lim[2] = hi_u; g_uv_lim[3] = hi_v; +} + +/* uv sampling bounds for rect prims: forward mappings sample [u0, u1-1] + * (u1 is the exclusive edge); mirrored ones keep the full inclusive range. */ +static void sw_rect_uv_limits(int u0, int v0, int u1, int v1) { + 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; } + g_uv_lim[0] = lim[0]; g_uv_lim[1] = lim[1]; + g_uv_lim[2] = lim[2]; g_uv_lim[3] = lim[3]; } /* ------------------------------------------------------------------ */ @@ -802,6 +864,12 @@ void sw_draw_textured_triangle(int x0, int y0, int u0, int v0, - (int64_t)(x2 - x0) * (int64_t)(y1 - y0); if (area2 == 0) return; + if (g_texture_filter) { + int xs[3] = {x0,x1,x2}, ys[3] = {y0,y1,y2}; + int us[3] = {u0,u1,u2}, vs[3] = {v0,v1,v2}; + sw_tri_uv_limits(xs, ys, us, vs); + } + RTarget n = rt_native(); raster_textured_triangle(&n, x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2, clut_x, clut_y, texpage); @@ -941,6 +1009,12 @@ void sw_draw_shaded_textured_triangle(int x0, int y0, int u0, int v0, - (int64_t)(x2 - x0) * (int64_t)(y1 - y0); if (area2 == 0) return; + if (g_texture_filter) { + int xs[3] = {x0,x1,x2}, ys[3] = {y0,y1,y2}; + int us[3] = {u0,u1,u2}, vs[3] = {v0,v1,v2}; + sw_tri_uv_limits(xs, ys, us, vs); + } + int r0, g0, b0, r1, g1, b1, r2, g2, b2; color24_to_mod(color0, &r0, &g0, &b0); color24_to_mod(color1, &r1, &g1, &b1); @@ -1032,6 +1106,7 @@ void sw_draw_textured_rect(int x, int y, int w, int h, int u, int v, uint16_t clut_x, uint16_t clut_y, uint16_t texpage) { + if (g_texture_filter) sw_rect_uv_limits(u, v, u + w, v + h); RTarget n = rt_native(); raster_textured_rect(&n, x, y, w, h, u, v, clut_x, clut_y, texpage); if (g_hr) { @@ -1079,6 +1154,7 @@ void sw_draw_textured_rect_scaled(int x, int y, int w, int h, uint16_t clut_x, uint16_t clut_y, uint16_t texpage) { if (w <= 0 || h <= 0) return; + if (g_texture_filter) sw_rect_uv_limits(u0, v0, u1, v1); RTarget n = rt_native(); raster_textured_rect_scaled(&n, x, y, w, h, u0, v0, u1, v1, clut_x, clut_y, texpage); From b228c6270ef90d0cdd32a374e14be1177b059df7 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 12 Jun 2026 05:19:17 -0700 Subject: [PATCH 15/18] runtime: GL present wiring + turbo-loads audio fade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Present path: 15-bit frames always blit the display rect straight from the GL VRAM FBO (4:3 letterboxed); 24-bit FMV frames sync the FBO down and present the CPU conversion. One deterministic path per depth — the old per-frame FBO-vs-CPU alternation (menu jitter) is structurally gone. Audio: sdl_audio_update() ramps gain over 40 ms at turbo-loads mute/unmute edges with an 8-frame unmute hangover to debounce burst gaps. The fade tail is sized to the ramp (1764 frames) and clamped to the buffer — the first version rendered 2205 frames into the 2048-frame sdl_audio_buf and corrupted adjacent statics. Co-Authored-By: Claude Fable 5 --- runtime/src/main.cpp | 96 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 13 deletions(-) diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index aec3e610..19b27951 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -551,6 +551,24 @@ static void shutdown_runtime(void) { debug_server_shutdown(); } +/* Linear gain ramp g0 -> g1 across a block of interleaved stereo frames. */ +static void sdl_audio_gain_ramp(int16_t* buf, int frames, float g0, float g1) { + if (frames <= 0) return; + const float step = (g1 - g0) / (float)frames; + float g = g0; + for (int f = 0; f < frames; f++, g += step) { + buf[f * 2 + 0] = (int16_t)((float)buf[f * 2 + 0] * g); + buf[f * 2 + 1] = (int16_t)((float)buf[f * 2 + 1] * g); + } +} + +/* Fade-in state: samples of rising ramp still to apply after an unmute. + * Consumed by sdl_audio_pump across however many pump calls it spans. + * MUST fit sdl_audio_buf (2048 frames): the fade-out tail renders this many + * frames in one spu_render call. 1764 frames = 40 ms. */ +static const int sdl_audio_fade_samples = 44100 * 40 / 1000; /* 40 ms */ +static int sdl_audio_fadein_left = 0; + static void sdl_audio_pump(void) { if (!sdl_audio_device) return; @@ -566,10 +584,65 @@ static void sdl_audio_pump(void) { if (frames > 2048) frames = 2048; spu_render(sdl_audio_buf, frames); + if (sdl_audio_fadein_left > 0) { + const float g0 = 1.0f - (float)sdl_audio_fadein_left + / (float)sdl_audio_fade_samples; + int ramp = sdl_audio_fadein_left < frames ? sdl_audio_fadein_left : frames; + const float g1 = 1.0f - (float)(sdl_audio_fadein_left - ramp) + / (float)sdl_audio_fade_samples; + sdl_audio_gain_ramp(sdl_audio_buf, ramp, g0, g1); + sdl_audio_fadein_left -= ramp; + } SDL_QueueAudio(sdl_audio_device, sdl_audio_buf, (uint32_t)frames * bytes_per_frame); } +/* Audio gating across turbo-loads transitions. + * + * The mute model stays: during turbo the guest runs at host speed, so + * rendered SPU audio is time-compressed garble — we stop pumping, the queue + * drains, voice positions freeze, and music resumes in place afterward. + * What changes is the EDGES: + * - entering turbo: render one short tail of the current voice state, + * ramp it to silence, and queue it — the drain ends in a fade instead + * of a hard cut; + * - leaving turbo: hold the mute for a short hangover first (loads often + * re-trigger within a few frames; without the debounce the mute would + * flicker audibly), then resume pumping with a rising ramp applied + * across the first ~50 ms of samples (sdl_audio_pump above). */ +static void sdl_audio_update(int turbo_active) { + if (!sdl_audio_device) return; + const int HANGOVER_FRAMES = 8; /* ~133 ms at 60 fps */ + static int muted = 0; + static int hangover = 0; + + if (turbo_active) { + if (!muted) { + int tail = sdl_audio_fade_samples; + const int buf_cap = (int)(sizeof(sdl_audio_buf) / (2 * sizeof(int16_t))); + if (tail > buf_cap) tail = buf_cap; + /* An unmute ramp may still be in progress; start the down-ramp + * from its current gain so the edge stays continuous. */ + const float g0 = 1.0f - (float)sdl_audio_fadein_left + / (float)sdl_audio_fade_samples; + sdl_audio_fadein_left = 0; + spu_render(sdl_audio_buf, tail); + sdl_audio_gain_ramp(sdl_audio_buf, tail, g0, 0.0f); + SDL_QueueAudio(sdl_audio_device, sdl_audio_buf, + (uint32_t)tail * sizeof(int16_t) * 2u); + muted = 1; + } + hangover = HANGOVER_FRAMES; + return; + } + if (muted) { + if (hangover > 0) { hangover--; return; } + muted = 0; + sdl_audio_fadein_left = sdl_audio_fade_samples; + } + sdl_audio_pump(); +} + /* PS1 digital pad button bits (active-low: 0=pressed, 1=released). * Bit 0 = SELECT, Bit 3 = START, Bit 4 = UP, Bit 5 = RIGHT, * Bit 6 = DOWN, Bit 7 = LEFT, Bit 8 = L2, Bit 9 = R2, @@ -1018,13 +1091,9 @@ static void sdl_vblank_present(void) { } sio_set_pad_state(pad_buttons_this_frame); - /* Turbo-active test, shared by the audio mute here and the pacing/ - * present gate below. During turbo the guest runs at host speed, so - * rendered SPU audio is time-compressed garble — queueing it is what - * players hear as load-time stutter. Muting = stop queueing: the SDL - * device drains the last real-time samples and underruns to silence; - * SPU voice positions freeze with the pump, so music resumes from - * where it left off when pacing returns. */ + /* Turbo-active test, shared by the audio gate here and the pacing/ + * present gate below. sdl_audio_update owns the mute + fade-in/out + + * debounce across turbo transitions (see its comment). */ int turbo_loads_active = 0; if (g_turbo_loads_enabled) { extern int fntrace_is_game_started(void); @@ -1032,7 +1101,7 @@ static void sdl_vblank_present(void) { turbo_loads_active = 1; } #ifndef PSX_SDL_NO_AUDIO - if (!turbo_loads_active) sdl_audio_pump(); + sdl_audio_update(turbo_loads_active); #endif /* TCP turbo is for automated validation and trace capture. It keeps the @@ -1119,12 +1188,13 @@ static void sdl_vblank_present(void) { disabled_frame_presented = false; w = di.width; h = di.height; - /* OpenGL fast path: when the GPU FBO holds the freshest VRAM (a GPU - * draw happened this frame) and the display is 15-bit, present straight - * from the FBO — no 1 MB readback. Software / 24-bit (FMV) frames fall - * through to the CPU readout below, after syncing the FBO down. */ + /* OpenGL: 15-bit frames ALWAYS present straight from the authoritative + * VRAM FBO — one deterministic path (the old per-frame FBO-vs-CPU + * alternation caused the menu seam/jitter). 24-bit (FMV) frames and + * the PSX_GL_FORCE_CPU_PRESENT diagnostic sync the FBO down and use + * the CPU readout below. */ #ifndef PSX_SDL_NO_RENDER - if (g_gl_active && g_gl_fbo_present && !di.depth24 && gl_renderer_have_gpu_frame()) { + if (g_gl_active && g_gl_fbo_present && !di.depth24) { gl_renderer_present_vram((int)di.display_x, (int)di.display_y, (int)w, (int)h, g_video_aa ? 1 : 0); return; From ca6f423edbaaee2ec80f705945c33891ecfbdca3 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 12 Jun 2026 05:19:17 -0700 Subject: [PATCH 16/18] debug: gl_coh_ring/gl_fbo_peek/gl_vram_diff commands; richer SEH dumps New debug server commands for the GL backend: gl_fbo_peek reads a rect of GPU-side VRAM (via the pack pass) without touching the CPU array, gl_vram_diff reports full-VRAM FBO-vs-CPU mismatch count/bbox/samples, and gl_coh_ring dumps the always-on coherency event ring (last n events, optional frame_min filter). crash_trace SEH dumps now report the faulting module + base + offset and scan the stack for executable return addresses, so dumps stay addr2line-able despite ASLR. Co-Authored-By: Claude Fable 5 --- runtime/src/crash_trace.c | 53 +++++++++++++++- runtime/src/debug_server.c | 124 +++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 2 deletions(-) diff --git a/runtime/src/crash_trace.c b/runtime/src/crash_trace.c index 66038af9..fc9a062f 100644 --- a/runtime/src/crash_trace.c +++ b/runtime/src/crash_trace.c @@ -149,9 +149,58 @@ void psx_crash_trace_dump(const char *reason, void *seh_info) { " \"code\": \"0x%08lX\",\n" " \"address\": \"%p\",\n" " \"access\": \"%s\",\n" - " \"fault_addr\": \"0x%p\"\n" - " },\n", + " \"fault_addr\": \"0x%p\",\n", code, addr, kind, (void *)fault_addr); + + /* Module-relative location of the faulting instruction, so the + * address survives ASLR and feeds straight into addr2line. */ + { + MEMORY_BASIC_INFORMATION mbi; + char mod_name[MAX_PATH] = "?"; + void *mod_base = NULL; + if (VirtualQuery(addr, &mbi, sizeof(mbi))) { + mod_base = mbi.AllocationBase; + GetModuleFileNameA((HMODULE)mod_base, mod_name, sizeof(mod_name)); + } + append_fmt(buf, sizeof(buf), &pos, + " \"module\": \"%s\",\n" + " \"module_base\": \"%p\",\n" + " \"module_offset\": \"0x%llX\",\n", + mod_name, mod_base, + (unsigned long long)((char *)addr - (char *)mod_base)); + } + + /* Poor-man's backtrace: scan the faulting thread's stack for values + * that point into executable module regions and report them + * module-relative. Noisy but enough to pin the faulting call path. */ + append_str(buf, sizeof(buf), &pos, " \"stack_scan\": ["); + { + CONTEXT *ctx = info->ContextRecord; + ULONG_PTR *sp = (ULONG_PTR *)ctx->Rsp; + int emitted = 0; + for (int i = 0; i < 512 && emitted < 24; i++) { + ULONG_PTR v; + MEMORY_BASIC_INFORMATION mbi; + if (!VirtualQuery(&sp[i], &mbi, sizeof(mbi)) || + !(mbi.State & MEM_COMMIT)) break; + v = sp[i]; + if (v < 0x10000) continue; + if (!VirtualQuery((void *)v, &mbi, sizeof(mbi))) continue; + if (mbi.Type != MEM_IMAGE) continue; + if (!(mbi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | + PAGE_EXECUTE_READWRITE))) continue; + char mod_name[MAX_PATH] = "?"; + GetModuleFileNameA((HMODULE)mbi.AllocationBase, mod_name, + sizeof(mod_name)); + const char *base = strrchr(mod_name, '\\'); + append_fmt(buf, sizeof(buf), &pos, + "%s\n {\"module\": \"%s\", \"offset\": \"0x%llX\"}", + emitted ? "," : "", base ? base + 1 : mod_name, + (unsigned long long)(v - (ULONG_PTR)mbi.AllocationBase)); + emitted++; + } + } + append_str(buf, sizeof(buf), &pos, "\n ]\n },\n"); } #else (void)seh_info; diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index a5127cd3..360be2f7 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -7,6 +7,12 @@ * Same function names and protocol as nesrecomp/snesrecomp versions * so TCP.md and DEBUG.md are reusable across projects. */ +/* Expose POSIX clock_gettime()/CLOCK_MONOTONIC (used by monotonic_ms) on + * glibc — must precede any system header. Harmless on Windows/macOS. */ +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 200809L +#endif +#include #include "debug_server.h" #include "overlay_loader.h" #include "overlay_capture.h" @@ -25,6 +31,7 @@ #include "card_read_summary.h" #include "card_data_writes.h" #include "crash_trace.h" +#include "gpu_gl_renderer.h" #include #include @@ -48,6 +55,7 @@ #else # include # include +# include /* struct timeval — socket send/recv timeouts */ # include # include # include @@ -5720,6 +5728,119 @@ static void handle_vram_peek(int id, const char *json) free(hex); } +/* GL-backend diagnostic: peek the GPU-side (FBO) VRAM for a rect, plus the + * coherency flags/rects — lets probes diff FBO truth against CPU truth. */ +extern int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out); +extern void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]); + +static void handle_gl_fbo_peek(int id, const char *json) +{ + int x = json_get_int(json, "x", 0); + int y = json_get_int(json, "y", 0); + int w = json_get_int(json, "w", 8); + int h = json_get_int(json, "h", 1); + if (w < 1) w = 1; + if (h < 1) h = 1; + if (w > 128) w = 128; + if (h > 128) h = 128; + uint16_t px[128 * 128]; + if (!gl_renderer_fbo_peek(x, y, w, h, px)) { + send_err(id, "GL pipeline inactive or rect out of range"); return; + } + int gpu_dirty = 0, pend[5] = {0}, pack[5] = {0}; + gl_renderer_diag(&gpu_dirty, pend, pack); + size_t hex_len = (size_t)w * h * 4 + 1; + char *hex = (char *)malloc(hex_len); + if (!hex) { send_err(id, "alloc failed"); return; } + int pos = 0; + for (int i = 0; i < w * h; i++) + pos += snprintf(hex + pos, hex_len - pos, "%04x", px[i]); + send_fmt("{\"id\":%d,\"ok\":true,\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d," + "\"gpu_dirty\":%d," + "\"pending\":[%d,%d,%d,%d,%d],\"pack\":[%d,%d,%d,%d,%d]," + "\"hex\":\"%s\"}", + id, x, y, w, h, gpu_dirty, + pend[0], pend[1], pend[2], pend[3], pend[4], + pack[0], pack[1], pack[2], pack[3], pack[4], hex); + free(hex); +} + +extern int gl_renderer_vram_diff(uint32_t *count, int bbox[4], + int samples[8][2], uint16_t samples_px[8][2]); + +static void handle_gl_vram_diff(int id, const char *json) +{ + (void)json; + uint32_t n = 0; + int bbox[4] = {0}, samples[8][2] = {{0}}; + uint16_t spx[8][2] = {{0}}; + int r = gl_renderer_vram_diff(&n, bbox, samples, spx); + if (!r) { send_err(id, "GL pipeline inactive"); return; } + int ns = r - 1; + int gpu_dirty = 0, pend[5] = {0}, pack[5] = {0}; + gl_renderer_diag(&gpu_dirty, pend, pack); + char smp[512]; int pos = 0; smp[0] = 0; + for (int i = 0; i < ns; i++) + pos += snprintf(smp + pos, sizeof(smp) - pos, + "%s[%d,%d,\"0x%04X\",\"0x%04X\"]", i ? "," : "", + samples[i][0], samples[i][1], spx[i][0], spx[i][1]); + send_fmt("{\"id\":%d,\"ok\":true,\"mismatches\":%u," + "\"bbox\":[%d,%d,%d,%d],\"gpu_dirty\":%d," + "\"samples_xy_fbo_cpu\":[%s]}", + id, n, bbox[0], bbox[1], bbox[2], bbox[3], gpu_dirty, smp); +} + +/* GL-backend coherency event ring: dump the last n events (default 200), + * optionally only events from frame >= frame_min. Always-on capture; this + * just reads a window. */ +static const char *gl_coh_kind_name(int k) +{ + switch (k) { + case GL_COH_FLUSH: return "flush"; + case GL_COH_FILL: return "fill"; + case GL_COH_COPY_SRC: return "copy_src"; + case GL_COH_COPY: return "copy"; + case GL_COH_DRAW: return "draw"; + case GL_COH_PACK: return "pack"; + case GL_COH_ENSURE: return "ensure"; + case GL_COH_PRESENT: return "present"; + case GL_COH_UPLOAD: return "upload"; + case GL_COH_PEEK: return "peek"; + case GL_COH_DIFF: return "diff"; + default: return "?"; + } +} + +static void handle_gl_coh_ring(int id, const char *json) +{ + int n = json_get_int(json, "n", 200); + long frame_min = json_get_int(json, "frame_min", -1); + if (n < 1) n = 1; + if (n > 8192) n = 8192; + uint64_t total = gl_renderer_coh_total(); + uint64_t start = total > (uint64_t)n ? total - (uint64_t)n : 0; + int bufsz = 64 + n * 64; + char *buf = (char *)malloc((size_t)bufsz); + if (!buf) { send_err(id, "alloc failed"); return; } + int pos = snprintf(buf, bufsz, + "{\"id\":%d,\"ok\":true,\"total\":%llu,\"events\":[", + id, (unsigned long long)total); + int first = 1; + for (uint64_t s = start; s < total && pos < bufsz - 128; s++) { + GlCohEvent e; + if (!gl_renderer_coh_get(s, &e)) continue; + if (frame_min >= 0 && e.frame < (uint32_t)frame_min) continue; + pos += snprintf(buf + pos, bufsz - pos, + "%s[%llu,%u,\"%s\",%d,%d,%d,%d]", + first ? "" : ",", (unsigned long long)s, e.frame, + gl_coh_kind_name(e.kind), e.x0, e.y0, e.x1, e.y1); + first = 0; + } + pos += snprintf(buf + pos, bufsz - pos, "]}"); + send_fmt("%s", buf); + free(buf); +} + /* ---- Write trace: hook + handlers (Tier 1 reverse debugger) ---- */ extern CPUState *debug_cpu_ptr; @@ -8305,6 +8426,9 @@ static const CmdEntry s_commands[] = { { "gpu_state", handle_gpu_state }, { "mem_words", handle_mem_words }, { "vram_peek", handle_vram_peek }, + { "gl_coh_ring", handle_gl_coh_ring }, + { "gl_fbo_peek", handle_gl_fbo_peek }, + { "gl_vram_diff", handle_gl_vram_diff }, { "irq_state", handle_irq_state }, { "timers_state", handle_timers_state }, { "cdrom_state", handle_cdrom_state }, From 45cc5c3887edb5f9b2a8d6d487f560a46a7d4acc Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 12 Jun 2026 05:19:17 -0700 Subject: [PATCH 17/18] psx_fiber: SIGSTKSZ compile-time fallback for glibc >= 2.34 Co-Authored-By: Claude Fable 5 --- runtime/src/psx_fiber.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/runtime/src/psx_fiber.c b/runtime/src/psx_fiber.c index 59367d6d..a379eca0 100644 --- a/runtime/src/psx_fiber.c +++ b/runtime/src/psx_fiber.c @@ -47,6 +47,13 @@ void psx_fiber_destroy(psx_fiber_t fiber) { if (fiber) DeleteFiber((LPVOID)fiber #include #include #include +#include /* SIGSTKSZ — minimum fiber stack floor */ + +/* glibc >= 2.34 no longer exposes SIGSTKSZ as a compile-time constant under + * _XOPEN_SOURCE; provide a portable fallback (used only as a stack floor). */ +#ifndef SIGSTKSZ +# define SIGSTKSZ 16384 +#endif #if defined(__clang__) || defined(__GNUC__) # pragma GCC diagnostic ignored "-Wdeprecated-declarations" From bba973da12caa4602fe580183ed4e4464e9982be Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 12 Jun 2026 05:20:34 -0700 Subject: [PATCH 18/18] docs: GL renderer handoff updated to v2 architecture + seam root cause Co-Authored-By: Claude Fable 5 --- GL_RENDERER_HANDOFF.md | 290 ++++++++++++++++++----------------------- 1 file changed, 126 insertions(+), 164 deletions(-) diff --git a/GL_RENDERER_HANDOFF.md b/GL_RENDERER_HANDOFF.md index f59b9743..96e2cc30 100644 --- a/GL_RENDERER_HANDOFF.md +++ b/GL_RENDERER_HANDOFF.md @@ -1,185 +1,147 @@ -# OpenGL Renderer — Handoff (for Fable) +# OpenGL Renderer — Handoff Branch: `feat/gl-renderer` (both psxrecomp and TombaRecomp). -Last commit this session: `b592447` (psxrecomp). -Status: **experimental, opt-in.** Software renderer is still the committed -default; OpenGL is selected per-game via `[video] renderer = "opengl"`. -TombaRecomp `game.toml` has a LOCAL (uncommitted) `renderer = "opengl"` + -`texture_filtering = "bilinear"` override for testing — do NOT commit those; -committed defaults are `software` / `nearest`. +Last commits this session: `0631538`..`45cc5c3` (psxrecomp). +Status: **v2 architecture, opt-in, rendering cleanly on every screen +tested.** Software renderer is still the committed default; OpenGL is +selected per-game via `[video] renderer = "opengl"`. TombaRecomp +`game.toml` has LOCAL (uncommitted) overrides (`renderer = "opengl"`, +`texture_filtering = "bilinear"`, `supersampling = 2`) for testing — do +NOT commit those; committed defaults are `software` / `nearest`. -The goal of this work: add a hardware (OpenGL) renderer as a *selectable -option* alongside software, to enable higher internal-resolution scaling -without the software renderer's per-pixel CPU cost. Make it default-with- -software-fallback only once it renders cleanly. +Goal: a hardware (OpenGL) renderer as a *selectable option* alongside +software, enabling higher internal-resolution scaling without the software +renderer's per-pixel CPU cost. Make it default-with-software-fallback only +after broad visual validation. --- -## Architecture (as built) +## Architecture (v2 — "GPU-authoritative VRAM") - **Facade + vtable.** `gpu.c` calls `gr_*` (gpu_render.h/.c); a - `GpuRenderBackend` vtable dispatches to the software (`sw_*`) or OpenGL - (`glb_*`) backend. `gr_set_backend()` picks at startup from the config. -- **GL backend** lives in `runtime/src/gpu_gl_renderer.c` - (+ `runtime/include/gpu_gl_renderer.h`). VRAM is mirrored as a GL texture - (`s_vram_tex`, RGBA8) bound to an FBO (`s_fbo`) — the render target. A - second raw-16-bit integer texture (`s_vram_raw_tex`, GL_R16UI) is the - texture-sampling source for textured polys (CLUT decode in-shader). -- **What's on the GPU now:** flat/gouraud triangles; textured triangles - (4/8/15-bit CLUT, texel-0 discard, PS1 *2-around-0x80 modulation); - flat/textured rects; fills (scissored glClear); lines (GL_LINES); - semi-transparency (4 PS1 blend modes via fixed-function blend; textured - STP via a 2-pass opaque/semi split). -- **Still software** (route through CPU VRAM with FBO sync): `copy_rect` - (VRAM→VRAM blit), VRAM transfers in/out, single vram read/write. -- **Coherency:** two dirty flags (`s_cpu_dirty`/`s_gpu_dirty`). A GPU draw - uploads CPU→FBO first if CPU is ahead (`ensure_gpu`); a software op reads - FBO→CPU first if GPU is ahead (`ensure_cpu`). Both are **full 1 MB** - syncs today. -- **Present (main.cpp ~line 1118):** - - If the GPU drew this frame (`gl_renderer_have_gpu_frame()`) and the - display is 15-bit → `gl_renderer_present_vram()` blits the display - region from the FBO straight to the window (`glBlitFramebuffer`, no - readback). This is the fast path. - - Otherwise (software frame / 24-bit FMV) → `gl_renderer_sync_cpu()` then - the software readout path (`gpu_display_pixel_argb` → `gl_renderer_present`). - - `PSX_GL_FORCE_CPU_PRESENT=1` forces the software present path always - (diagnostic/fallback; also keeps CPU VRAM current every frame). -- Present forces **native resolution** under GL (`glb_scale`/`glb_set_scale` - return 1) — GPU draws write the FBO at native res only, not the software - SSAA hi-res mirror `g_hr`. GPU-side hi-res scaling is not implemented yet. + `GpuRenderBackend` vtable dispatches to software (`sw_*`) or OpenGL + (`glb_*`). `gr_set_backend()` picks at startup from the config. +- **GL backend** in `runtime/src/gpu_gl_renderer.c` + (+ `runtime/include/gpu_gl_renderer.h`). The single authoritative VRAM is + the **hr FBO** (`s_hr_tex`, RGBA8, 1024·S × 512·S, + depth24/stencil8); + internal scale S comes from `[video] supersampling` (GL-side; the SW + hi-res mirror is forced off under GL). +- **Mask bit exact:** FBO alpha carries bit15; the stencil buffer mirrors it + (bit0). `check_mask` = stencil test EQUAL 0; writing 1 under check uses + GL_INVERT. Textured prims/copies/uploads draw in two passes split by the + texel STP bit so each pass writes one known stencil value. +- **Coherency model** (all state in gpu_gl_renderer.c): + - `s_up_pending`: CPU→VRAM writes land in the CPU array immediately and + accumulate a pending rect; `flush_cpu_upload()` quad-blits the rect into + the hr FBO (and subimages the raw mirror directly) before the next GPU + op, present, or readback. + - `s_pack_dirty`: hr-FBO content not yet re-encoded into the native R16UI + raw mirror (`s_raw_tex`) — the texture-sampling source and readback + surface. `pack_flush()` runs a scissored PACK pass over the union before + any textured draw that samples a dirty page/CLUT. + - `s_gpu_dirty`: CPU array stale; `ensure_cpu()` = flush + pack + full + readback. Used by GPUREAD, A0 mask-check reads, vram peek, screenshots, + 24-bit present. +- **Texture sampling (Beetle-PSX model, structure studied from + beetle-psx-libretro `rsx/`):** + - Per-primitive **uv sampling bounds** (`u_limits`), 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. Rect prims pass exact bounds ([u0, u1-1] forward); + triangles compute min/max with the exclusive max edge backed off for + axis-aligned (2D) uv mappings; uv ranges crossing a 256 wrap boundary + widen to the full page. + - **Bilinear** (`texture_filtering = "bilinear"`): nearest texel is the + base (cutout + STP authority), neighbours toward the sub-texel offset, + per-texel opacity weights with renormalised colour, discard iff + opacity < 0.5. The SW renderer's bilinear uses the same formulation + (GL/SW parity); SW nearest is untouched. + - **Sample-grid alignment:** all draw shaders shift positions by + `u_shift = 0.5/S − 1/64` so GL's center-sample grid matches the PS1 + integer DDA (fixes striped/squished fonts). Beetle instead uses a + `+0.001` uv epsilon and truncation — coarser; we keep u_shift. +- **GPU ops:** fills (scissored clear, wrap-split 4 ways), copy_rect + (hr→scratch blit + masked quad back), all triangle/rect/line prims, + batched CPU→VRAM uploads. Blending via glBlendFuncSeparate — alpha (mask + bit) is never blended. +- **Present:** one deterministic path per depth. 15-bit always blits the + display rect from the hr FBO (4:3 letterboxed); 24-bit FMV syncs and + presents the CPU conversion (letterboxed). The old per-frame FBO-vs-CPU + alternation (menu jitter) is structurally gone. +- **Init is all-or-nothing:** any shader/FBO failure → clean fallback to + pure software. -## What works / is verified +## Verified (2026-06-12 session) -- Boots; renderer selectable; `[OpenGL]`/`[Software]` shown in window title. -- BIOS/Sony logos, FMV (MDEC 24-bit intro), and gameplay terrain all render. -- **Double-offset bug fixed** (`846e402`): `gpu.c` pre-applies `draw_offset` - to every primitive; the GPU triangle paths were adding it again, which - displaced all GPU geometry off-screen once the camera scrolled (symptom: - "the entire overworld is gone — only sprites left"). Fixed by not - re-applying `s_off` in `gpu_triangle`/`gpu_textured_triangle`. -- Loads are faster (user-confirmed) thanks to present-from-FBO removing the - per-frame readback for GPU scenes. -- Screenshot writes a real PNG (`ee0a454`) and now syncs the FBO down first - so captures reflect the on-screen frame under FBO-present (`b592447`). +- BIOS → intro FMV (24-bit) → title → memcard slot screen → save-block list + all render **clean** under GL at S=1 and S=2 with bilinear on: + - Title vertical line at VRAM x=640 — GONE. + - Memory-card screens' cross seams (tile edges at 704/864/256/368) — GONE. + - Menu text fully readable under bilinear. +- Root cause of all the black lines: the v1 bilinear base texel was + `floor(uv − 0.5)` — one texel OUTSIDE the prim on top/left edges. With + u0=v0=0 tiles (card wallpaper, title bg page seam) the neighbour wrapped + to empty VRAM → texel 0 → whole edge column/row discarded. They were + never coherency holes; both the FBO and (post-sync) CPU VRAM agreed. +- gl_vram_diff at a quiesced (static-screen) moment: **0 mismatches**. +- Audio fade (below) built and boots clean; user feel-test still pending. ---- +## Diagnostics (debug TCP 4470, dev builds) -## OPEN ISSUES (what looks bad — fix these) +- `gl_fbo_peek x= y= w= h=` — read GPU-side VRAM rect (via pack) without + writing CPU VRAM; returns pending/pack rects + gpu_dirty. +- `gl_vram_diff` — full-VRAM FBO-vs-CPU mismatch count/bbox/samples. Forces + a full pack first so it reads FBO truth even if the raw-mirror invariant + broke. **Run it BEFORE any screenshot** — screenshots ensure_cpu (CPU := + FBO) and make the diff trivially clean. +- `gl_coh_ring n= [frame_min=]` — always-on coherency event ring: flushes, + fills, copy src/dst, draw bboxes, packs, readbacks, presents, probe + perturbations, each with rect + frame. The event AFTER a "flush" names + the op that triggered it. Keep n ≤ ~2000 per query (TCP send budget). +- Caveats: peeks/diffs themselves flush pending uploads mid-frame (they're + recorded in the ring as peek/diff events). Animated screens legitimately + diverge mid-frame (gpu_dirty=1); only quiesced screens give clean diffs. -### 1. Menu: vertical line splits the screen; the two halves JITTER badly -- Repro: skip to the LOAD memory-card screen (pink "TOMBA!" tiled bg). A - vertical seam runs top-to-bottom near center; the two halves shimmer/jitter - frame-to-frame. "Looks awful." -- **Key evidence:** with `PSX_GL_FORCE_CPU_PRESENT=1` the menu is CLEAN. So - the artifact is in the *present path*, NOT the GPU render of the frame. -- Switching the FBO present from a crop-shader to `glBlitFramebuffer` - (`b592447`) did **not** fix it — so it's almost certainly NOT sub-texel - sampling. -- **Leading hypothesis: the present path ALTERNATES frame-to-frame.** The - menu interleaves GPU draws (text/sprites) with software `copy_rect` (the - card-shell UI). Whether a frame ends on a GPU op (`s_gpu_dirty` → FBO/blit - present) or a software op (`s_cpu_dirty` → software present) flips between - frames. The two present paths likely differ in scale/position/seam → - jitter. The vertical seam may be where a `copy_rect` region meets GPU - content within one path. -- **Suggested fix:** make the FBO the single authoritative surface and use - ONE present path deterministically. The clean way is to move `copy_rect` - onto the GPU (FBO self-blit) so the software round-trip — and the path - flip — disappears. See issue 6. +## Open / deferred -### 2. Gameplay: persistent horizontal dotted line under Tomba -- A dashed/dotted horizontal line sits at about mid-body height across the - open area. Persists in the user's view. -- NOT yet isolated to FBO-vs-software present (the attract demo didn't reach - this scene during the CPU-forced test). First step: reproduce with - `PSX_GL_FORCE_CPU_PRESENT=1` to see if it's render-side or present-side. -- Candidate causes: a textured-rect/sprite strip seam; missing texture - window (issue 3); or the same present-path alternation as issue 1. +1. **User visual validation** of the whole pass (incl. fullscreen letterbox + + audio fade feel) — everything above was screenshot-verified only. +2. Gameplay smoke under GL this build (last session verified gameplay + renders; this session's shader changes were validated on title/menu + screens which exercise the same textured paths). +3. Mirrored-texture mappings: bilinear/limits keep the full inclusive range + for mirrored rects (no exclusive-edge backoff); ±1 texel at + exact-integer uv remains the documented 1/64-bias tradeoff. Beetle + handles mirrors with a +1 uv offset (`off_u/off_v` in + gpu_polygon_sub.cpp) — adopt if a mirrored sprite ever shows it. +4. Dirty-sub-rect readbacks (ensure_cpu is a full 1 MB read), dither (SW + lacks it too), GL_LINES vs Bresenham edge cases. +5. S=4 unvalidated (S=2 verified this session). -### 3. Garbled / fuzzy / misaligned text (e.g. the AP counter, top-right) -- Glyphs look smeared, doubled, and misaligned. -- **Leading cause: the GPU textured shader does NOT implement the PS1 - TEXTURE WINDOW** (mask/offset UV wrap). The software path applies it in - `gpu_sw_renderer.c` `texel_fetch` (`g_tw_mask_x/y`, `g_tw_off_x/y`, the - `u = (u & ~(mask*8)) | ((off & mask)*8)` masking). `glb_set_texture_window` - just forwards to SW and is ignored by the GPU shader. Sprites/text that - rely on UV wrapping render wrong. -- Also: `texture_filtering = "bilinear"` is honored only by the software - path; the GPU textured shader is nearest-only (issue 5). With bilinear - requested, the SW-vs-GPU mismatch can compound the fuzzy look. -- **Suggested fix:** pass the texture-window mask/offset as uniforms to the - textured fragment shader and apply the same masking before the texel - fetch. Then add an optional bilinear texel fetch (issue 5). +## Audio fade (shipped with this branch, main.cpp) -### 4. Fullscreen does not preserve aspect ratio (stretches, no black bars) -- Going fullscreen stretches the image to fill the window instead of - letterboxing to the PSX display aspect. -- Both present paths map the source to the FULL drawable: - `gl_renderer_present` (quad over whole window) and - `gl_renderer_present_vram` (blit dst = `0,wh,ww,0`). -- **Suggested fix:** compute a 4:3 (or correct PSX display aspect) dst rect - centered in the drawable, clear the whole window to black first, then - present into the inset rect. Apply to BOTH present functions. - -### 5. Bilinear texture filtering not honored on the GPU path -- GPU textured shader does nearest texel fetch only. Software path supports - `texture_filtering = "bilinear"`. Add a bilinear path in the textured - fragment shader (manual 4-tap on the integer VRAM texture + CLUT, since - raw sampling is GL_R16UI/nearest). - -### 6. `copy_rect` still software → forces FBO↔CPU round-trips -- This is the structural driver of issue 1 (present-path alternation) and of - residual lag. Implementing GPU `copy_rect` as an FBO-region self-copy - (careful with overlap; may need a scratch texture or `glBlitFramebuffer` - with a temp) lets the FBO stay authoritative and removes the software - round-trip for menus and scrolling buffers. - -### 7. Coherency is full-1 MB sync per transition -- `ensure_gpu`/`ensure_cpu` upload/read the entire 1024×512 VRAM. Once - copy_rect is on the GPU and the FBO is authoritative, most syncs vanish; - what remains (texture/CLUT uploads via VRAM transfer) could be reduced to - dirty sub-rectangles. - -### 8. (Deferred goal) GPU-side internal-resolution scaling -- The original ask: "higher scaling without performance loss." Present - currently forces native res. Once the above render correctly, allocate the - FBO at N× native, scale GPU draw coordinates, and downsample/blit on - present. This is the payoff; don't start it until 1–6 are clean. - ---- - -## TASK FOR NEXT SESSION (user-approved, separate from GL) - -### Audio fade-in/fade-out at turbo transition windows — APPROVED, implement -- Context: `turbo_loads` runs load screens faster than real time. Tomba's - load BGM is *sequenced* (CPU writes SPU regs per game-frame), so under - turbo it speeds up into garble. We MUTE during turbo - (`main.cpp` ~line 1024–1032: `if (!turbo_loads_active) sdl_audio_pump();`). -- Decoupling audio to "normal pace" during turbo is **NOT feasible** — the - music's tempo is bound to the frame rate that turbo is compressing - (analysis in session). The mute stays. -- The mute's hard cut/resume causes awkward gaps. **User approved a quick - fade-in/fade-out** to smooth the edges (and a small hysteresis so very - short loads don't flicker the mute on/off). -- Implementation sketch: apply a gain ramp (~30–60 ms) to the SPU output - when entering/leaving `turbo_loads_active` — ramp down before muting, ramp - up on resume — rather than hard start/stop of `sdl_audio_pump()`. Apply the - gain in `spu_render` output or scale the queued `int16_t` samples. Add a - small minimum-turbo-duration / debounce so brief loads don't audibly dip. - ---- +`turbo_loads` mutes sequenced load BGM (tempo is frame-bound; can't +decouple). `sdl_audio_update()` ramps gain over 40 ms at mute/unmute edges +with an 8-frame unmute hangover to debounce burst gaps. The fade tail is +sized to the ramp and clamped to `sdl_audio_buf` (the first version +overflowed the 2048-frame buffer and corrupted adjacent statics — wedged +the boot before display enable). ## Useful commands / paths - Build: `PATH=/c/msys64/mingw64/bin:$PATH cmake --build build-stable --target psx-runtime` - (run from `TombaRecomp/`; the GL source is in `psxrecomp/runtime/`). + (from `TombaRecomp/`; GL source is in `psxrecomp/runtime/`). - Run: `./build-stable/psx-runtime.exe --game game.toml` (BIOS path baked). -- Debug TCP 4470: `python ../psxrecomp/tools/debug_client.py --port 4470 ping` - and `... screenshot_file path=foo.png` (now a real PNG, FBO-synced). -- Force software present (diagnostic/fallback): set env - `PSX_GL_FORCE_CPU_PRESENT=1` before launch. -- Reusable WIP patches live in `psxrecomp/_wip/` (gitignored). -- Attract demo auto-plays gameplay on a cold boot (AP is always 97400 — it's - the canned demo). Tapping START a few times skips the long intro FMV to the - title/LOAD menu. +- Debug: `python ../psxrecomp/tools/debug_client.py --port 4470 `; + `screenshot_file path=...png` (CPU-side render after FBO sync — native + res, shows FBO truth; the window shows the S× blit). +- Input injection: `press buttons=N frames=M` is consumed faster than some + screens poll — prefer `set_input buttons=0xNNNN`, sleep ~1–2 s, then + `clear_input`. Circle (0x2000) = confirm in Tomba menus; Start (0x0008) + skips the intro FMV. +- A/B vs software: `sed -i 's/renderer = "opengl"/renderer = "software"/' game.toml`. +- Beetle PSX GL reference (GPL — structure only, never copy code): + `psxrecomp/beetle-psx/rsx/rsx_lib_gl.cpp`, `rsx/shaders_gl/*.glsl.h`, + `mednafen/psx/gpu_polygon_sub.cpp` (uv limits, may_be_2d, mirror offsets). +- GP0 ring decode: `python TombaRecomp/_shots/decode_frame.py `. +- Attract demo auto-plays on cold boot (AP 97400 = canned demo).