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/ diff --git a/GL_RENDERER_HANDOFF.md b/GL_RENDERER_HANDOFF.md new file mode 100644 index 00000000..96e2cc30 --- /dev/null +++ b/GL_RENDERER_HANDOFF.md @@ -0,0 +1,147 @@ +# OpenGL Renderer — Handoff + +Branch: `feat/gl-renderer` (both psxrecomp and TombaRecomp). +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`. + +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 (v2 — "GPU-authoritative VRAM") + +- **Facade + vtable.** `gpu.c` calls `gr_*` (gpu_render.h/.c); a + `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. + +## Verified (2026-06-12 session) + +- 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) + +- `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. + +## Open / deferred + +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). + +## Audio fade (shipped with this branch, main.cpp) + +`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` + (from `TombaRecomp/`; GL source is in `psxrecomp/runtime/`). +- Run: `./build-stable/psx-runtime.exe --game game.toml` (BIOS path baked). +- 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). 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_gl_renderer.h b/runtime/include/gpu_gl_renderer.h new file mode 100644 index 00000000..ceeb0536 --- /dev/null +++ b/runtime/include/gpu_gl_renderer.h @@ -0,0 +1,79 @@ +#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 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); + +/* 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); + +/* 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 + +#endif /* PSX_GPU_GL_RENDERER_H */ 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..349784ac 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 @@ -225,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/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 406704dc..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 @@ -5544,14 +5552,121 @@ 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 * 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) { @@ -5563,50 +5678,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); @@ -5637,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; @@ -8222,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 }, diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index de23bf97..531043e5 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; @@ -303,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; @@ -542,8 +546,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 +560,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 +577,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 +610,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 +633,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 +664,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 +712,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 +760,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 +797,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 +817,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 +831,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 +846,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 +869,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 +879,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 +898,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 +908,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 +927,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 +952,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 +982,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 +992,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 +1003,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 +1014,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 +1024,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 +1454,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 +1470,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 +1496,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; } @@ -1560,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) { @@ -1580,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) @@ -1589,7 +1596,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 +1627,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 +1664,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 +1705,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..2085723b --- /dev/null +++ b/runtime/src/gpu_gl_renderer.c @@ -0,0 +1,1467 @@ +/* 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_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/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); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index afd47637..19b27951 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -15,6 +15,8 @@ #include "autocompile.h" #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" @@ -100,6 +102,12 @@ 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 (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 @@ -543,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; @@ -558,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, @@ -1010,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); @@ -1024,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 @@ -1097,9 +1174,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; @@ -1107,6 +1188,20 @@ static void sdl_vblank_present(void) { disabled_frame_presented = false; w = di.width; h = di.height; + /* 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_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). */ @@ -1114,7 +1209,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 { @@ -1133,6 +1228,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))); @@ -1164,6 +1265,7 @@ static void sdl_vblank_present(void) { } } } + } #endif } @@ -1240,6 +1342,9 @@ 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; + { 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]; @@ -1300,15 +1405,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", @@ -1383,17 +1492,30 @@ 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); + } + /* 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. * * History (TombaRecomp/ISSUES.md #6): @@ -1421,6 +1543,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 @@ -1439,6 +1562,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). */ @@ -1449,6 +1573,7 @@ int main(int argc, char** argv) { return 1; } + if (!g_gl_active) { sdl_texture = SDL_CreateTexture( sdl_renderer, SDL_PIXELFORMAT_ARGB8888, @@ -1461,6 +1586,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); @@ -1597,8 +1723,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(); 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"