mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
merge: feat/gl-renderer — GPU-authoritative OpenGL renderer (v2) into master
This commit is contained in:
@@ -73,3 +73,4 @@ __pycache__/
|
||||
|
||||
# DuckStation oracle (local-only; private fork at git@github.com:mstan/duckstation.git)
|
||||
/duckstation/
|
||||
_wip/
|
||||
|
||||
@@ -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).
|
||||
@@ -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;
|
||||
|
||||
@@ -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]].
|
||||
|
||||
@@ -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 */
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user