merge: feat/gl-renderer — GPU-authoritative OpenGL renderer (v2) into master

This commit is contained in:
Matthew Stanley
2026-06-12 11:15:39 -07:00
15 changed files with 2618 additions and 143 deletions
+1
View File
@@ -73,3 +73,4 @@ __pycache__/
# DuckStation oracle (local-only; private fork at git@github.com:mstan/duckstation.git)
/duckstation/
_wip/
+147
View File
@@ -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 <cmd>`;
`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 <frame>`.
- Attract demo auto-plays on cold boot (AP 97400 = canned demo).
+7
View File
@@ -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<std::string>(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;
+5
View File
@@ -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]].
+79
View File
@@ -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 <stdint.h>
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 */
+155
View File
@@ -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 <stdint.h>
#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 */
+10 -1
View File
@@ -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)
+51 -2
View File
@@ -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;
+242 -35
View File
@@ -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 <time.h>
#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 <stdio.h>
#include <stdlib.h>
@@ -48,6 +55,7 @@
#else
# include <sys/socket.h>
# include <netinet/in.h>
# include <sys/time.h> /* struct timeval — socket send/recv timeouts */
# include <unistd.h>
# include <fcntl.h>
# include <errno.h>
@@ -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 },
+62 -55
View File
@@ -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;
File diff suppressed because it is too large Load Diff
+132
View File
@@ -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 <stdio.h>
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); }
+108 -32
View File
@@ -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);
+145 -18
View File
@@ -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();
+7
View File
@@ -47,6 +47,13 @@ void psx_fiber_destroy(psx_fiber_t fiber) { if (fiber) DeleteFiber((LPVOID)fiber
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <signal.h> /* 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"