diff --git a/TCP_COMMANDS.md b/TCP_COMMANDS.md index 6d1caef20..99f8a29aa 100644 --- a/TCP_COMMANDS.md +++ b/TCP_COMMANDS.md @@ -1,99 +1,100 @@ -# TCP Debug Server Commands - -Protocol: **JSON over newline**, one object per line, responses on same connection. - -- Request shape: `{"id": N, "cmd": "", ...params}` -- Success: `{"id": N, "ok": true, ...data}` -- Failure: `{"id": N, "ok": false, "error": ""}` - -There are **two live** servers, both implementing this protocol with overlapping command sets: - -| Server | Port | Source | -|---|---|---| -| **Native** (our recompiled runtime) | `4370` | `runtime/src/debug_server.c` | -| **Beetle PSX** (oracle) | `4380` | `runtime/src/beetle_debug_server.c` | - -> **DuckStation (port 4371) was retired as the oracle on 2026-05-05** and is no -> longer built from this repository — there is no `duckstation` entry in -> `.gitmodules`. The **D** column in the curated inventory below is historical -> and reflects DuckStation, not Beetle. For authoritative per-command -> native/Beetle coverage, use the generated -> [Complete command index](#complete-command-index-generated) at the bottom of -> this file, which is derived from the two servers' command tables. - -The `debug_client.py` CLI can target either, or `compare` two at once to diff state live — that's how divergence hunts work. - -```bash -python tools/debug_client.py [args] # native (port 4370) -python tools/debug_client.py --port 4380 # psx-beetle -python tools/debug_client.py --ds [args] # duckstation (port 4371) -python tools/debug_client.py compare # run on both, diff results -``` - -Commands without a bespoke CLI mapping pass through generically: extra -args of the form `key=value` become JSON fields (ints when numeric, else -strings), so every server command is reachable, e.g. -`debug_client.py --port 4370 gpu_frame_dump frame=14528 count=65536`. - ---- - -## Command inventory - -Columns: **N** = native, **D** = DuckStation oracle. - -| Command | N | D | Params | Description | -|---|---|---|---|---| -| `ping` / `frame` | ✓ | ✓ | — | Heartbeat + current frame number | -| `get_registers` (`regs`) | ✓ | ✓ | — | All 32 GPRs + PC + HI + LO (native also: COP0 SR/Cause/EPC, I_STAT, I_MASK) | -| `read_ram` | ✓ | ✓ | `addr`, `len` | Read bytes from PS1 address space as hex string — up to the full 2 MB in ONE response line. `dump_ram` is an alias (the old chunked multi-line variant is gone: it broke the one-request/one-response protocol and wedged the server) | -| `write_ram` | ✓ | ✓ | `addr`, `val` | Write **one byte** to PS1 address space. Note the parameter is `val` (not `hex`), and the write is a single byte per call — this row previously documented both incorrectly | -| `read_scratch` | | ✓ | `addr`, `len` | Read PS1 scratchpad (0x1F800000 region) | -| `read_vram` / `vram_peek` | ✓¹ | ✓ | `x`, `y`, `w`, `h` | Read 16-bit VRAM pixels (max 128×128) | +# TCP Debug Server Commands + +Protocol: **JSON over newline**, one object per line, responses on same connection. + +- Request shape: `{"id": N, "cmd": "", ...params}` +- Success: `{"id": N, "ok": true, ...data}` +- Failure: `{"id": N, "ok": false, "error": ""}` + +There are **two live** servers, both implementing this protocol with overlapping command sets: + +| Server | Port | Source | +|---|---|---| +| **Native** (our recompiled runtime) | `4370` | `runtime/src/debug_server.c` | +| **Beetle PSX** (oracle) | `4380` | `runtime/src/beetle_debug_server.c` | + +> **DuckStation (port 4371) was retired as the oracle on 2026-05-05** and is no +> longer built from this repository — there is no `duckstation` entry in +> `.gitmodules`. The **D** column in the curated inventory below is historical +> and reflects DuckStation, not Beetle. For authoritative per-command +> native/Beetle coverage, use the generated +> [Complete command index](#complete-command-index-generated) at the bottom of +> this file, which is derived from the two servers' command tables. + +The `debug_client.py` CLI can target either, or `compare` two at once to diff state live — that's how divergence hunts work. + +```bash +python tools/debug_client.py [args] # native (port 4370) +python tools/debug_client.py --port 4380 # psx-beetle +python tools/debug_client.py --ds [args] # duckstation (port 4371) +python tools/debug_client.py compare # run on both, diff results +``` + +Commands without a bespoke CLI mapping pass through generically: extra +args of the form `key=value` become JSON fields (ints when numeric, else +strings), so every server command is reachable, e.g. +`debug_client.py --port 4370 gpu_frame_dump frame=14528 count=65536`. + +--- + +## Command inventory + +Columns: **N** = native, **D** = DuckStation oracle. + +| Command | N | D | Params | Description | +|---|---|---|---|---| +| `ping` / `frame` | ✓ | ✓ | — | Heartbeat + current frame number | +| `get_registers` (`regs`) | ✓ | ✓ | — | All 32 GPRs + PC + HI + LO (native also: COP0 SR/Cause/EPC, I_STAT, I_MASK) | +| `read_ram` | ✓ | ✓ | `addr`, `len` | Read bytes from PS1 address space as hex string — up to the full 2 MB in ONE response line. `dump_ram` is an alias (the old chunked multi-line variant is gone: it broke the one-request/one-response protocol and wedged the server) | +| `write_ram` | ✓ | ✓ | `addr`, `val` | Write **one byte** to PS1 address space. Note the parameter is `val` (not `hex`), and the write is a single byte per call — this row previously documented both incorrectly | +| `read_scratch` | | ✓ | `addr`, `len` | Read PS1 scratchpad (0x1F800000 region) | +| `read_vram` / `vram_peek` | ✓¹ | ✓ | `x`, `y`, `w`, `h` | Read 16-bit VRAM pixels (max 128×128) | | `gpu_state` | ✓ | ✓ | — | Display area, display depth, `screen_offset_y`, draw offset, GPUSTAT, clip rect, xfer state. A positive screen offset moves 24-bit scanout down from the PAL or NTSC broadcast centre | | `screenshot_hires` | ✓ | ✓ | `path` | PNG of the **supersampled** surface (the present path the window uses), at `display × gr_scale()`. ⚠ `screenshot`/`screenshot_file` capture native 15-bit VRAM and are **blind to anything that only exists in the hi-res mirror** — geometry correction, SSAA edges, perspective UVs — so they show a clean frame while the player sees a broken one. Use this one to verify those. Falls back to the native resolve (and reports `scale: 1`) when no hi-res surface exists | | `present_shot` | ✓ | | `path` | PNG of the **composed present surface** — the frame after the backend fits the display buffer to the window, so it carries the presented aspect. ⚠ every other capture resolves the display buffer *before* that fit: on a 508×256 display in a 4:3 window they answer 508×256 while the player sees 640×480. Use this one for anything aspect-shaped (widescreen, letterbox), where a pre-fit buffer would hide the very stage the change touches. Staged and fulfilled on the next present, so the ack means *queued* — poll `present_shot_seq`. Unavailable headless and on the Vulkan backend (its swapchain has no readback hook) | | `present_shot_seq` | ✓ | | — | Completion counter for `present_shot`, plus `wrote` (1 = that completion produced a PNG). Sample before staging, poll until `seq` moves. Advances on success *and* failure, so the poll always terminates | -| `geom_correction` | | ✓ | — | `[video] geometry_correction` / `perspective_texturing` engagement: enable flag plus free-running `geometry_vertex_hits` and `perspective_triangles` totals. Both enhancements silently fall back to the faithful path on anything they cannot prove is projected geometry, so a zero counter with the flag on means the title never qualifies — sample twice and diff for a rate | -| `sio_state` | ✓ | ✓ | — | SIO registers + (native only) pad/memcard protocol + TX/RX history | -| `irq_state` | ✓ | ✓ | — | `I_STAT`, `I_MASK` (both), plus chain state on native | -| `dma_state` | ✓ | ✓ | — | DPCR, DICR, all 7 channel states (madr/bcr/chcr) | -| `event_state` | | ✓ | — | EvCB table summary (stub on DS — events are BIOS-level) | -| `overlay_state` | | ✓ | — | Current overlay info | -| `cdrom_sector_dump` | ✓ | | `offset`, `len` | Dump bytes from the last CD-ROM sector observed by the controller, including LBA/mode metadata | -| `cdrom_sector_history` | ✓ | | `count`, optional `lba` | Dump newest CD-ROM sector history entries, including raw XA subheader fields, CPU/audio delivery flags, and the first 128 bytes | -| `cdrom_sector_history_clear` | ✓ | | — | Reset the CD-ROM sector history ring | -| `watch` | ✓ | ✓ | `addr` | Set byte-level memory watchpoint (fires per-frame on change) | -| `unwatch` | ✓ | ✓ | `addr` | Remove memory watchpoint | -| `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry` | Override pad1 buttons and optional analog axes (PS1 inverted bitmask, 0 = pressed; axes 0-255). Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames | -| `clear_input` | ✓ | ✓ | — | Remove input and analog axis overrides | -| `turbo` | ✓ | | `enabled` | Enable/disable TCP-controlled frontend turbo for fast-forward validation | -| `turbo_state` | ✓ | | — | Query TCP-controlled turbo state | -| `pause` | ✓ | | — | **REMOVED** — still registered, but always returns an error. Query a ring buffer (`fn_entry_tail`, `wtrace_dump`, `gpu_frame_dump`) instead of synthesizing a snapshot | -| `continue` (`c`) | ✓ | | — | **REMOVED** — nothing to resume, since `pause` is gone | -| `step` | ✓ | | — | **REMOVED** — query a ring buffer over the window of interest instead of advancing N frames synchronously | -| `run_to_frame` | ✓ | | — | **REMOVED** — use `frame_range` / `read_frame_ram` against the live frame ring instead | -| `history` | ✓ | ✓ | — | Ring buffer stats (frames available) | -| `get_frame` | ✓ | ✓ | `frame` | Full frame record from ring buffer | -| `frame_range` | ✓ | ✓ | `start`, `end` | Range query, max 200 frames | -| `frame_timeseries` | ✓ | ✓ | `start`, `end` | Compact timeseries, max 200 frames | -| `set_snapshot` | ✓ | ✓ | `slot`, `addr`, `size` | Configure per-frame RAM snapshot region (slots 0-3) | -| `get_snapshots` | ✓ | ✓ | — | Show snapshot config | -| `screenshot` | ✓ | ✓ | `path` (optional) | Write a **PNG** of the current display to `path` (default `psx_screenshot.png` in the runtime cwd); single metadata response `{path,width,height}`. `screenshot_file` is an alias; the old inline-hex-row `screenshot` is gone (it streamed h+1 response lines per request and poisoned the connection) | -| `first_failure` | ✓ | | — | Find first divergence point between runs (native-side tracking) | -| `read_frame_ram` | ✓ | | `addr`, `len`, `frame` | Read RAM **as of a specific frame** (from ring buffer) | -| `wtrace_range` | ✓ | | `lo`, `hi` | Set RAM-write trace range (ring of 262 144 writes with RA — `WRITE_TRACE_CAP`, `1 << 18`) | -| `wtrace_dump` | ✓ | beetle | optional `addr_lo`, `addr_hi`, `count`, `newest` | Dump RAM-write trace entries as JSON. The address filter is applied server-side over the FULL ring before the emit cap — always pass it when hunting a specific buffer, otherwise you only see the oldest `count` entries of the whole ring | -| `wtrace_clear` | ✓ | | — | Reset the trace ring | -| `mmio_dump` | ✓ | | optional `addr`, `count`, `newest` | Dump the always-on MMIO write ring (256K entries, ALL 0x1F801xxx writes — SPU/DMA traffic rolls it in well under a minute of gameplay; for display history use `gp1_dump`) | -| `mmio_clear` | ✓ | | — | Reset the MMIO write ring | -| `gp1_dump` | ✓ | | optional `frame_lo`, `frame_hi`, `count`, `newest` | Dump the dedicated ALWAYS-ON GP1 (0x1F801814 display control) ring — 512K entries ≈ 15 min of gameplay (Tomba writes ~10 GP1/frame), survives the general MMIO ring's eviction. Frame filter is server-side over the full ring. Each entry: val + func/pc/cpu_pc/ra/sp/a0/a1/sr/epc/frame | -| `pc_break` | | ✓² | `addr` | DS execute breakpoint, state captured on hit (via `pc_hit_last`) | -| `pc_unbreak` | | ✓² | `addr` | Remove an execute breakpoint | -| `pc_break_list` | | ✓² | — | List active execute breakpoints | -| `pc_hit_last` | | ✓² | — | Captured state (PC, $ra, all GPRs, COP0) from most recent PC break hit | -| `pc_hit_clear` | | ✓² | — | Clear the last-hit record | -| `quit` | ✓ | | — | Shutdown native runtime | - +| `geom_correction` | | ✓ | — | `[video] geometry_correction` / `perspective_texturing` engagement: enable flag plus free-running `geometry_vertex_hits` and `perspective_triangles` totals. Both enhancements silently fall back to the faithful path on anything they cannot prove is projected geometry, so a zero counter with the flag on means the title never qualifies — sample twice and diff for a rate | +| `sio_state` | ✓ | ✓ | — | SIO registers + (native only) pad/memcard protocol + TX/RX history | +| `irq_state` | ✓ | ✓ | — | `I_STAT`, `I_MASK` (both), plus chain state on native | +| `dma_state` | ✓ | ✓ | — | DPCR, DICR, all 7 channel states (madr/bcr/chcr) | +| `event_state` | | ✓ | — | EvCB table summary (stub on DS — events are BIOS-level) | +| `overlay_state` | | ✓ | — | Current overlay info | +| `cdrom_sector_dump` | ✓ | | `offset`, `len` | Dump bytes from the last CD-ROM sector observed by the controller, including LBA/mode metadata | +| `cdrom_sector_history` | ✓ | | `count`, optional `lba` | Dump newest CD-ROM sector history entries, including raw XA subheader fields, CPU/audio delivery flags, and the first 128 bytes | +| `cdrom_sector_history_clear` | ✓ | | — | Reset the CD-ROM sector history ring | +| `watch` | ✓ | ✓ | `addr` | Set byte-level memory watchpoint (fires per-frame on change) | +| `unwatch` | ✓ | ✓ | `addr` | Remove memory watchpoint | +| `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry` | Override pad1 buttons and optional analog axes (PS1 inverted bitmask, 0 = pressed; axes 0-255). Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames | +| `clear_input` | ✓ | ✓ | — | Remove input and analog axis overrides | +| `turbo` | ✓ | | `enabled` | Enable/disable TCP-controlled frontend turbo for fast-forward validation | +| `turbo_state` | ✓ | | — | Query TCP-controlled turbo state | +| `pause` | ✓ | | — | **REMOVED** — still registered, but always returns an error. Query a ring buffer (`fn_entry_tail`, `wtrace_dump`, `gpu_frame_dump`) instead of synthesizing a snapshot | +| `continue` (`c`) | ✓ | | — | **REMOVED** — nothing to resume, since `pause` is gone | +| `step` | ✓ | | — | **REMOVED** — query a ring buffer over the window of interest instead of advancing N frames synchronously | +| `run_to_frame` | ✓ | | — | **REMOVED** — use `frame_range` / `read_frame_ram` against the live frame ring instead | +| `history` | ✓ | ✓ | — | Ring buffer stats (frames available) | +| `get_frame` | ✓ | ✓ | `frame` | Full frame record from ring buffer | +| `frame_range` | ✓ | ✓ | `start`, `end` | Range query, max 200 frames | +| `frame_timeseries` | ✓ | ✓ | `start`, `end` | Compact timeseries, max 200 frames | +| `set_snapshot` | ✓ | ✓ | `slot`, `addr`, `size` | Configure per-frame RAM snapshot region (slots 0-3) | +| `get_snapshots` | ✓ | ✓ | — | Show snapshot config | +| `screenshot` | ✓ | ✓ | `path` (optional) | Write a **PNG** of the current display to `path` (default `psx_screenshot.png` in the runtime cwd); single metadata response `{path,width,height}`. `screenshot_file` is an alias; the old inline-hex-row `screenshot` is gone (it streamed h+1 response lines per request and poisoned the connection) | +| `scanline` | ✓ | | optional `on` (0/1), optional `pct` (0..100) | Live A/B toggle for the present-time scanline post-process. Omit `on` to keep the current toggle, omit `pct` to keep the current strength; reports `{scanlines, strength_pct}`. Same state the F6 hotkey and `[video] scanlines` drive | +| `first_failure` | ✓ | | — | Find first divergence point between runs (native-side tracking) | +| `read_frame_ram` | ✓ | | `addr`, `len`, `frame` | Read RAM **as of a specific frame** (from ring buffer) | +| `wtrace_range` | ✓ | | `lo`, `hi` | Set RAM-write trace range (ring of 262 144 writes with RA — `WRITE_TRACE_CAP`, `1 << 18`) | +| `wtrace_dump` | ✓ | beetle | optional `addr_lo`, `addr_hi`, `count`, `newest` | Dump RAM-write trace entries as JSON. The address filter is applied server-side over the FULL ring before the emit cap — always pass it when hunting a specific buffer, otherwise you only see the oldest `count` entries of the whole ring | +| `wtrace_clear` | ✓ | | — | Reset the trace ring | +| `mmio_dump` | ✓ | | optional `addr`, `count`, `newest` | Dump the always-on MMIO write ring (256K entries, ALL 0x1F801xxx writes — SPU/DMA traffic rolls it in well under a minute of gameplay; for display history use `gp1_dump`) | +| `mmio_clear` | ✓ | | — | Reset the MMIO write ring | +| `gp1_dump` | ✓ | | optional `frame_lo`, `frame_hi`, `count`, `newest` | Dump the dedicated ALWAYS-ON GP1 (0x1F801814 display control) ring — 512K entries ≈ 15 min of gameplay (Tomba writes ~10 GP1/frame), survives the general MMIO ring's eviction. Frame filter is server-side over the full ring. Each entry: val + func/pc/cpu_pc/ra/sp/a0/a1/sr/epc/frame | +| `pc_break` | | ✓² | `addr` | DS execute breakpoint, state captured on hit (via `pc_hit_last`) | +| `pc_unbreak` | | ✓² | `addr` | Remove an execute breakpoint | +| `pc_break_list` | | ✓² | — | List active execute breakpoints | +| `pc_hit_last` | | ✓² | — | Captured state (PC, $ra, all GPRs, COP0) from most recent PC break hit | +| `pc_hit_clear` | | ✓² | — | Clear the last-hit record | +| `quit` | ✓ | | — | Shutdown native runtime | + ¹ Native `vram_peek` is the legacy name; DS calls it `read_vram`. Same semantics. ² The `pc_*` family is specific to the DS oracle: DuckStation's CPU core honours `CPU::AddBreakpointWithCallback`, while our native runtime dispatches whole recompiled functions (no mid-function PC breaks). @@ -118,466 +119,467 @@ and DMA channel. The option is ignored in builds made with debug tools disabled. --- ## Divergence-hunt workflow - -When a recompiled-BIOS bug is suspected, the two servers let you find the **first** divergence instead of chasing symptoms. Standard procedure (inherited from v3's `DEBUG.md`): - -1. **Sync state via PC + registers, not frame number.** Frames drift after even a single timing glitch. Pause both servers; compare `get_registers` until they match. -2. **Dump both sides fully.** Compare `get_frame`, `gpu_state`, `irq_state`, `dma_state` (DS), `dump_ram` over the same regions. -3. **Byte-level comparison.** Tiny mismatches usually point at one subsystem. Use `debug_client.py compare ` for automatic diff. -4. **Find the earliest mismatch**, not a later symptom. Ring-buffer queries (`frame_range`, `read_frame_ram`) help locate which frame went wrong. -5. **Trace the write.** Use `watch` to catch the divergent store, or DS's `pc_break` on the suspect function entry. Look at `$ra` in `pc_hit_last` to identify the caller chain. -6. **Classify.** codegen (recompiler generates wrong instruction), runtime (MMIO or kernel simulation wrong), timing (IRQ cadence), or BIOS (real-hardware quirk we didn't model). -7. **Minimal fix** in the correct subsystem. Never hand-deliver state to hide the symptom (see CLAUDE.md §0). - ---- - -## Server send budget + serve-stall telemetry - -The server is pumped on the **main thread**: every millisecond it spends -sending a response is a millisecond the emulator does not run. Inline -responses are bounded — 2 s per zero-progress chunk and **15 s total per -response**; a client that exceeds the budget is disconnected (the runtime -never stalls indefinitely). Responses bigger than the budget allows must -use the `*_dump_file` variants, which write to disk instead of the socket. - -Cumulative serve-stall is exported as `tcp_send_stall_ms` / -`tcp_clients_dropped` in `psx_freeze_heartbeat.json` (plus per-tick -`tcp_ms` in its ring) and in every wedge/fatal dump header. **Check these -first when diagnosing slow or stalled frames** — on 2026-06-10 two -"attract-idle degradations" turned out to be a TCP client trickle-draining -mega-dumps, throttling the main loop to 6 fps (all 8 watchdog stack -samples inside `WS2_32!send`). A slow-frames wedge with a large -`tcp_send_stall_ms` delta over the same window is observer interference, -not a guest bug. - -## Call-contract (bail) telemetry - -The dispatch call contract (Bug D family fix, 2026-06-10) guards every -generated continuation: it may only run if the guest actually returned to -the call site ($ra == site return address, $sp == caller's sp at the -call). Violations begin a "bail" unwind that abandons stale C frames and -re-dispatches the guest's true target. Counters in -`psx_freeze_heartbeat.json` and dump headers: - -- `bail_first` — contract violations detected (wild returns). Nonzero - during gameplay means the game executed a wild control transfer (e.g. - Tomba's dead jumptable case `jal 0x80120B3C`, the chest freeze). A - small count with the game continuing normally is the fix working. -- `bail_resolved` — unwinds that resolved at an enclosing call site whose - contract matched (multi-level return). -- `bail_flattened` — unwinds that reached the outermost dispatch loop and - re-dispatched the wild target on a clean host stack. -- `bail_anomaly` — bail flag observed at exception entry (must stay 0; - anything else is a runtime bug). - ---- - -## `bios_info` — linked recompiled-BIOS identity (native only) - -Reports which BIOS image this build's recompiled C was generated from -(`psx_bios_image`, emitted into the generated dispatch from the BIOS profile) -and whether the loaded ROM matches it: `image_id`, `sha256`, `crc32`, `size`, -`bundled` (redistributable image shipped with the game), the kernel-bless -window, the HLE anchors (`shell_entry_phys` / `deliver_event_ret`; 0 = -structurally unavailable on this BIOS), and `image_wordsum` vs -`loaded_wordsum` with `match`. With the launch identity gate a running -process always reports `match:1`. - -- `{"cmd":"bios_info"}` - -## `s3_smear_watch` — callee-saved-register smear tripwire (native only) - -Latches the first interpreted instruction in a PC window whose execution -changes `$s3` (`runtime/src/dirty_ram_interp.c`). A `jalr`'s exec_one spans -the entire nested native callee, so the latch names the callee that returned -with a clobbered callee-saved register; the insn ring is frozen at the latch. - -- `{"cmd":"s3_smear_watch","lo":"","hi":""}` — arm (each arming - fully re-specifies the watch). Optional `"excl":""`: exact - encoding to ignore, so a watched loop's own `$s3` advance (e.g. an - `addi s3,s3,8` list walk) doesn't trip the latch. -- `{"cmd":"s3_smear_watch"}` — report the latch: `valid`, `pc`, `insn`, - `s3_old`/`s3_new`, `call_target` (rs at the call site for jr/jalr), - `frame`. -- `{"cmd":"s3_smear_watch","lo":"0"}` — disarm. - -## `callret_watch` — interp JALR call-resolution ring (native only) - -64-entry ring (`runtime/src/dirty_ram_interp.c`) recording, for every -interpreted JALR whose call PC lies in a window, which resolution tier ran -the callee and the full post-call outcome — the complement of -`s3_smear_watch`: the tripwire names the callee that came back smeared, this -ring names the return path that let it come back. - -- `{"cmd":"callret_watch","lo":"","hi":""}` — arm (resets the ring). -- `{"cmd":"callret_watch"}` — dump (newest last): per entry `cyc`, `f` - (frame), `pc`, `tgt`, `path` (`CRES_*` tier code, see the enum in - dirty_ram_interp.c; `|0x100` = finish() escaped), pre-call - `sp_b`/`ra_b`/`s0_b`/`s3_b`, post-call `pc_a`/`ra_a`/`sp_a`/`s0_a`/ - `s3_a`/`v0_a`, `bail`/`rfe`/`esc`/`in_exc` flags, `dstatic`/`dblocks`/ - `dexc` engine-attribution deltas across the call, `last_func`. + +When a recompiled-BIOS bug is suspected, the two servers let you find the **first** divergence instead of chasing symptoms. Standard procedure (inherited from v3's `DEBUG.md`): + +1. **Sync state via PC + registers, not frame number.** Frames drift after even a single timing glitch. Pause both servers; compare `get_registers` until they match. +2. **Dump both sides fully.** Compare `get_frame`, `gpu_state`, `irq_state`, `dma_state` (DS), `dump_ram` over the same regions. +3. **Byte-level comparison.** Tiny mismatches usually point at one subsystem. Use `debug_client.py compare ` for automatic diff. +4. **Find the earliest mismatch**, not a later symptom. Ring-buffer queries (`frame_range`, `read_frame_ram`) help locate which frame went wrong. +5. **Trace the write.** Use `watch` to catch the divergent store, or DS's `pc_break` on the suspect function entry. Look at `$ra` in `pc_hit_last` to identify the caller chain. +6. **Classify.** codegen (recompiler generates wrong instruction), runtime (MMIO or kernel simulation wrong), timing (IRQ cadence), or BIOS (real-hardware quirk we didn't model). +7. **Minimal fix** in the correct subsystem. Never hand-deliver state to hide the symptom (see CLAUDE.md §0). + +--- + +## Server send budget + serve-stall telemetry + +The server is pumped on the **main thread**: every millisecond it spends +sending a response is a millisecond the emulator does not run. Inline +responses are bounded — 2 s per zero-progress chunk and **15 s total per +response**; a client that exceeds the budget is disconnected (the runtime +never stalls indefinitely). Responses bigger than the budget allows must +use the `*_dump_file` variants, which write to disk instead of the socket. + +Cumulative serve-stall is exported as `tcp_send_stall_ms` / +`tcp_clients_dropped` in `psx_freeze_heartbeat.json` (plus per-tick +`tcp_ms` in its ring) and in every wedge/fatal dump header. **Check these +first when diagnosing slow or stalled frames** — on 2026-06-10 two +"attract-idle degradations" turned out to be a TCP client trickle-draining +mega-dumps, throttling the main loop to 6 fps (all 8 watchdog stack +samples inside `WS2_32!send`). A slow-frames wedge with a large +`tcp_send_stall_ms` delta over the same window is observer interference, +not a guest bug. + +## Call-contract (bail) telemetry + +The dispatch call contract (Bug D family fix, 2026-06-10) guards every +generated continuation: it may only run if the guest actually returned to +the call site ($ra == site return address, $sp == caller's sp at the +call). Violations begin a "bail" unwind that abandons stale C frames and +re-dispatches the guest's true target. Counters in +`psx_freeze_heartbeat.json` and dump headers: + +- `bail_first` — contract violations detected (wild returns). Nonzero + during gameplay means the game executed a wild control transfer (e.g. + Tomba's dead jumptable case `jal 0x80120B3C`, the chest freeze). A + small count with the game continuing normally is the fix working. +- `bail_resolved` — unwinds that resolved at an enclosing call site whose + contract matched (multi-level return). +- `bail_flattened` — unwinds that reached the outermost dispatch loop and + re-dispatched the wild target on a clean host stack. +- `bail_anomaly` — bail flag observed at exception entry (must stay 0; + anything else is a runtime bug). + +--- + +## `bios_info` — linked recompiled-BIOS identity (native only) + +Reports which BIOS image this build's recompiled C was generated from +(`psx_bios_image`, emitted into the generated dispatch from the BIOS profile) +and whether the loaded ROM matches it: `image_id`, `sha256`, `crc32`, `size`, +`bundled` (redistributable image shipped with the game), the kernel-bless +window, the HLE anchors (`shell_entry_phys` / `deliver_event_ret`; 0 = +structurally unavailable on this BIOS), and `image_wordsum` vs +`loaded_wordsum` with `match`. With the launch identity gate a running +process always reports `match:1`. + +- `{"cmd":"bios_info"}` + +## `s3_smear_watch` — callee-saved-register smear tripwire (native only) + +Latches the first interpreted instruction in a PC window whose execution +changes `$s3` (`runtime/src/dirty_ram_interp.c`). A `jalr`'s exec_one spans +the entire nested native callee, so the latch names the callee that returned +with a clobbered callee-saved register; the insn ring is frozen at the latch. + +- `{"cmd":"s3_smear_watch","lo":"","hi":""}` — arm (each arming + fully re-specifies the watch). Optional `"excl":""`: exact + encoding to ignore, so a watched loop's own `$s3` advance (e.g. an + `addi s3,s3,8` list walk) doesn't trip the latch. +- `{"cmd":"s3_smear_watch"}` — report the latch: `valid`, `pc`, `insn`, + `s3_old`/`s3_new`, `call_target` (rs at the call site for jr/jalr), + `frame`. +- `{"cmd":"s3_smear_watch","lo":"0"}` — disarm. + +## `callret_watch` — interp JALR call-resolution ring (native only) + +64-entry ring (`runtime/src/dirty_ram_interp.c`) recording, for every +interpreted JALR whose call PC lies in a window, which resolution tier ran +the callee and the full post-call outcome — the complement of +`s3_smear_watch`: the tripwire names the callee that came back smeared, this +ring names the return path that let it come back. + +- `{"cmd":"callret_watch","lo":"","hi":""}` — arm (resets the ring). +- `{"cmd":"callret_watch"}` — dump (newest last): per entry `cyc`, `f` + (frame), `pc`, `tgt`, `path` (`CRES_*` tier code, see the enum in + dirty_ram_interp.c; `|0x100` = finish() escaped), pre-call + `sp_b`/`ra_b`/`s0_b`/`s3_b`, post-call `pc_a`/`ra_a`/`sp_a`/`s0_a`/ + `s3_a`/`v0_a`, `bail`/`rfe`/`esc`/`in_exc` flags, `dstatic`/`dblocks`/ + `dexc` engine-attribution deltas across the call, `last_func`. - `{"cmd":"callret_watch","lo":"0"}` — disarm. - -## `hle_dump` — BIOS-HLE tier call ring (native only) - -Always-on ring (`runtime/src/bios_hle.c`, 16K entries) recording every -A0/B0/C0 kernel-vector dispatch the HLE tier's hook observes, plus the boot -shell-skip event. - -The hook is installed when EITHER axis is on (`bios_hle_plan.h`), so a run with -the boot-skip on but kernel calls left to LLE — the default on the bundled -OpenBIOS, which exports no `deliver_event_ret` — reports -`backend: "LLE (recompiled BIOS)"` and still fills the ring with `route: 0` -vector observations plus the one `route: 2` boot entry. Only a run with BOTH axes -off installs no hook and leaves the ring empty; use `bioscall_dump` for LLE-side -vector observation there. - -- `{"cmd":"hle_dump"}` — status: `backend` (`HLE (LLE fallback)` / - `LLE (recompiled BIOS)`), `boot_skip`, `boot_turbo_active`, `total`. -- `{"cmd":"hle_dump","tail":N}` — last N entries: `seq`, `cycle` (guest - cycle), `vec` (0xA0/0xB0/0xC0, or 0x30000 for the boot skip), `fn` ($t1 - function number), `a0..a3`, `ra`, `v0` (result when HLE-serviced), `route` - (0 = fell through to LLE, 1 = serviced in HLE, 2 = boot shell-skip). -- Filters: `"fn":N`, `"route":0|1|2`. - ---- - -## Rule when the server can't answer your question - -If an inspection need isn't covered by the existing commands, **do not fall back to printf or log files**. Instead: - -1. Add a handler in `runtime/src/debug_server.c` (native) -2. Add the matching handler in `runtime/src/beetle_debug_server.c` (Beetle oracle) - when the question needs a cross-check against hardware behaviour -3. Keep field names parallel between the two -4. Run `python tools/gen_tcp_commands.py` to refresh the generated index, and add - a row to the curated inventory above if the command needs explaining - -> Step 4 used to read "Update this file", and that did not survive contact with -> reality: this document described 47 commands while the servers registered 292. -> The index is now generated from the command tables, and -> `python tools/gen_tcp_commands.py --check` fails when it drifts. Prose in the -> curated inventory is still hand-written and still worth adding. - -The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` is absolute: **no `fprintf(stderr, …)` in source code, ever, for any reason**. - - - -## Complete command index (generated) - + +## `hle_dump` — BIOS-HLE tier call ring (native only) + +Always-on ring (`runtime/src/bios_hle.c`, 16K entries) recording every +A0/B0/C0 kernel-vector dispatch the HLE tier's hook observes, plus the boot +shell-skip event. + +The hook is installed when EITHER axis is on (`bios_hle_plan.h`), so a run with +the boot-skip on but kernel calls left to LLE — the default on the bundled +OpenBIOS, which exports no `deliver_event_ret` — reports +`backend: "LLE (recompiled BIOS)"` and still fills the ring with `route: 0` +vector observations plus the one `route: 2` boot entry. Only a run with BOTH axes +off installs no hook and leaves the ring empty; use `bioscall_dump` for LLE-side +vector observation there. + +- `{"cmd":"hle_dump"}` — status: `backend` (`HLE (LLE fallback)` / + `LLE (recompiled BIOS)`), `boot_skip`, `boot_turbo_active`, `total`. +- `{"cmd":"hle_dump","tail":N}` — last N entries: `seq`, `cycle` (guest + cycle), `vec` (0xA0/0xB0/0xC0, or 0x30000 for the boot skip), `fn` ($t1 + function number), `a0..a3`, `ra`, `v0` (result when HLE-serviced), `route` + (0 = fell through to LLE, 1 = serviced in HLE, 2 = boot shell-skip). +- Filters: `"fn":N`, `"route":0|1|2`. + +--- + +## Rule when the server can't answer your question + +If an inspection need isn't covered by the existing commands, **do not fall back to printf or log files**. Instead: + +1. Add a handler in `runtime/src/debug_server.c` (native) +2. Add the matching handler in `runtime/src/beetle_debug_server.c` (Beetle oracle) + when the question needs a cross-check against hardware behaviour +3. Keep field names parallel between the two +4. Run `python tools/gen_tcp_commands.py` to refresh the generated index, and add + a row to the curated inventory above if the command needs explaining + +> Step 4 used to read "Update this file", and that did not survive contact with +> reality: this document described 47 commands while the servers registered 292. +> The index is now generated from the command tables, and +> `python tools/gen_tcp_commands.py --check` fails when it drifts. Prose in the +> curated inventory is still hand-written and still worth adding. + +The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` is absolute: **no `fprintf(stderr, …)` in source code, ever, for any reason**. + + + +## Complete command index (generated) + **306 commands registered** — 293 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). 51 of 306 have prose above; **255 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. - -Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code. - -| Command | Native | Beetle | Described above | -|---|:--:|:--:|:--:| -| `a0_history` | ✓ | | | -| `audio_events` | ✓ | ✓ | | -| `audio_stats` | ✓ | ✓ | | -| `audio_wav` | ✓ | ✓ | | -| `autocompile_run` | ✓ | | | -| `autocompile_status` | ✓ | | | -| `bios_info` | ✓ | | ✓ | -| `bioscall_dump` | ✓ | | | -| `c0_history` | ✓ | | | -| `call_focus_dump` | ✓ | | | -| `call_focus_reset` | ✓ | | | -| `call_focus_stats` | ✓ | | | -| `callret_watch` | ✓ | | ✓ | -| `capture_freeze` | ✓ | | | -| `capture_quads` | ✓ | | | -| `card_buffer_dump` | ✓ | | | -| `card_data_writes` | ✓ | | | -| `card_data_writes_reset` | ✓ | | | -| `card_handoff` | ✓ | | | -| `card_mgr_clear` | ✓ | | | -| `card_mgr_trace` | ✓ | | | -| `card_read_summary` | ✓ | | | -| `card_read_summary_reset` | ✓ | | | -| `card_trace_dump` | ✓ | | | -| `card_txn_dump` | ✓ | | | -| `cd_overwrite` | ✓ | | | -| `cd_read_log` | ✓ | | | -| `cdc_volume` | | ✓ | | -| `cdrom_bursts` | ✓ | | | -| `cdrom_cmd_dump` | | ✓ | | -| `cdrom_cmd_reset` | | ✓ | | -| `cdrom_command_history` | ✓ | | | -| `cdrom_command_history_clear` | ✓ | | | -| `cdrom_instant_rate` | ✓ | | | -| `cdrom_sector_dump` | ✓ | | ✓ | -| `cdrom_sector_history` | ✓ | | ✓ | -| `cdrom_sector_history_clear` | ✓ | | ✓ | -| `cdrom_state` | ✓ | | | -| `cdrom_timing` | ✓ | | | -| `cdrom_trace_clear` | ✓ | | | -| `cdrom_trace_dump` | ✓ | | | -| `ce_profile` | ✓ | | | -| `chain_trace` | ✓ | | | -| `clear_input` | ✓ | ✓ | ✓ | -| `continue` | ✓ | | ✓ | -| `cyc_watch` | ✓ | ✓ | | -| `cyc_watch_clear` | ✓ | ✓ | | -| `cyc_watch_dump` | ✓ | ✓ | | -| `cycles_to_next_event` | ✓ | | | -| `d44_ring` | ✓ | | | -| `data_shards` | ✓ | | | -| `devtrace_ctl` | ✓ | ✓ | | -| `devtrace_dump` | ✓ | ✓ | | -| `dirty_block_dump_file` | ✓ | | | -| `dirty_block_log` | ✓ | | | -| `dirty_break_clear` | ✓ | | | -| `dirty_break_range` | ✓ | | | -| `dirty_break_state` | ✓ | | | -| `dirty_flow_log` | ✓ | | | -| `dirty_insn_dump_file` | ✓ | | | -| `dirty_insn_gate` | ✓ | | | -| `dirty_insn_log` | ✓ | | | -| `dirty_ram_stats` | ✓ | | | -| `dirty_ram_unsupported` | ✓ | | | -| `disp_ring` | ✓ | | | -| `dispatch_check` | ✓ | | | -| `dispatch_stats` | ✓ | | | -| `dispatch_tail` | ✓ | | | -| `display_ring_aux` | ✓ | | | -| `display_ring_get` | ✓ | | | -| `display_ring_stats` | ✓ | | | -| `dma_cdrom_history` | ✓ | | | -| `dma_state` | ✓ | | ✓ | -| `dma_trace_clear` | ✓ | | | -| `dma_trace_dump` | ✓ | | | -| `dump_buffer` | ✓ | | | -| `dump_ram` | ✓ | ✓ | ✓ | -| `evcb_snapshot` | ✓ | | | -| `evcb_walk_dump` | ✓ | | | -| `evcb_walk_stats` | ✓ | | | -| `event_ring_clear` | ✓ | | | -| `event_ring_dump` | ✓ | | | -| `event_ring_tail` | ✓ | | | -| `exc_ring` | | ✓ | | -| `first_failure` | ✓ | ✓ | ✓ | -| `fmv_state` | ✓ | | | -| `fn_clear` | ✓ | | | -| `fn_disable` | ✓ | | | -| `fn_entry_dump` | ✓ | | | -| `fn_entry_tail` | ✓ | | ✓ | -| `fn_exit_dump` | ✓ | | | -| `fn_filter` | ✓ | | | -| `fn_stats` | ✓ | | | -| `fntrace_arm` | ✓ | ✓ | | -| `fntrace_arm_clear` | ✓ | | | -| `fntrace_armed` | ✓ | | | -| `fntrace_arms` | | ✓ | | -| `fntrace_clear` | ✓ | | | -| `fntrace_disarm` | | ✓ | | -| `fntrace_dump` | ✓ | ✓ | | -| `fntrace_reset` | | ✓ | | -| `fntrace_unfiltered` | | ✓ | | -| `frame` | ✓ | | ✓ | -| `frame_fingerprint` | ✓ | | | -| `frame_perf` | ✓ | | | -| `frame_range` | ✓ | ✓ | ✓ | -| `frame_timeseries` | ✓ | ✓ | ✓ | -| `freeze_check` | ✓ | | | -| `game_options` | ✓ | | | -| `geom_correction` | ✓ | | ✓ | -| `get_frame` | ✓ | ✓ | ✓ | -| `get_quads` | ✓ | | | -| `get_registers` | ✓ | ✓ | ✓ | -| `get_snapshots` | ✓ | ✓ | ✓ | -| `gl_coh_ring` | ✓ | | | -| `gl_fbo_peek` | ✓ | | | -| `gl_interp` | ✓ | | | -| `gl_present_ring` | ✓ | | | -| `gl_vram_diff` | ✓ | | | -| `gl_wide_fast` | ✓ | | | -| `gl_ws_ablate` | ✓ | | | -| `gp1_dump` | ✓ | | ✓ | -| `gpu_frame_dump` | ✓ | | ✓ | -| `gpu_opcodes` | ✓ | | | -| `gpu_ring_stats` | ✓ | | | -| `gpu_state` | ✓ | | ✓ | -| `gte_frame_stats` | ✓ | | | -| `gte_intpl_dump` | ✓ | | | -| `gte_latch_dump` | ✓ | | | -| `gte_ring_dump` | ✓ | | | -| `gte_state` | ✓ | | | -| `history` | ✓ | ✓ | ✓ | -| `hle_dump` | ✓ | | ✓ | -| `idle_skip` | ✓ | | | -| `imask_trace` | ✓ | | | -| `input_route_append` | ✓ | | | -| `input_route_clear` | ✓ | | | -| `input_route_start` | ✓ | | | -| `input_route_status` | ✓ | | | -| `input_route_stop` | ✓ | | | -| `insn_freeze` | ✓ | | | -| `insn_freeze_snapshot` | ✓ | | | -| `insn_freeze_status` | ✓ | | | -| `insn_freeze_target` | ✓ | | | -| `irq_state` | ✓ | | ✓ | -| `irqctx_ring` | ✓ | | | -| `kernel_bless` | ✓ | | | -| `latency` | ✓ | | | -| `load_transitions` | ✓ | | | -| `lockstep` | ✓ | | | -| `lockstep_func` | ✓ | | | -| `mc_status` | ✓ | | | -| `mdec_state` | ✓ | | | -| `mdec_trace` | ✓ | | | -| `mdec_trace_clear` | ✓ | | | -| `mem_words` | ✓ | | | -| `mmio_clear` | ✓ | | ✓ | -| `mmio_dump` | ✓ | | ✓ | -| `mmx6_freshfix` | ✓ | | | -| `overlay_candidates` | ✓ | | | -| `overlay_capture_dump` | ✓ | | | -| `overlay_cps_probe` | ✓ | | | -| `overlay_diff_off` | ✓ | | | -| `overlay_diff_on` | ✓ | | | -| `overlay_dump` | ✓ | | | -| `overlay_fp_dump` | ✓ | | | -| `overlay_irq_ratelimit` | ✓ | | | -| `overlay_irq_suppress_off` | ✓ | | | -| `overlay_irq_suppress_on` | ✓ | | | -| `overlay_loader_status` | ✓ | | | -| `overlay_native_block` | ✓ | | | -| `overlay_native_event_granularity` | ✓ | | | -| `overlay_native_off` | ✓ | | | -| `overlay_native_on` | ✓ | | | -| `overlay_native_ring` | ✓ | | | -| `overlay_rescan` | ✓ | | | -| `overlay_shadow_detail` | ✓ | | | -| `overlay_shadow_dump` | ✓ | | | -| `pace_state` | ✓ | | | -| `pad_status` | ✓ | ✓ | | -| `parity_ctl` | ✓ | ✓ | | -| `parity_dump` | ✓ | ✓ | | -| `pause` | ✓ | | ✓ | -| `pc_probe_arm` | ✓ | | | -| `pc_probe_clear` | ✓ | | | -| `pc_probe_dump` | ✓ | | | -| `pgxp` | ✓ | | | -| `phase_hot` | ✓ | | | -| `phase_profile` | ✓ | | | -| `ping` | ✓ | ✓ | ✓ | -| `present_ring` | ✓ | | | + +Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code. + +| Command | Native | Beetle | Described above | +|---|:--:|:--:|:--:| +| `a0_history` | ✓ | | | +| `audio_events` | ✓ | ✓ | | +| `audio_stats` | ✓ | ✓ | | +| `audio_wav` | ✓ | ✓ | | +| `autocompile_run` | ✓ | | | +| `autocompile_status` | ✓ | | | +| `bios_info` | ✓ | | ✓ | +| `bioscall_dump` | ✓ | | | +| `c0_history` | ✓ | | | +| `call_focus_dump` | ✓ | | | +| `call_focus_reset` | ✓ | | | +| `call_focus_stats` | ✓ | | | +| `callret_watch` | ✓ | | ✓ | +| `capture_freeze` | ✓ | | | +| `capture_quads` | ✓ | | | +| `card_buffer_dump` | ✓ | | | +| `card_data_writes` | ✓ | | | +| `card_data_writes_reset` | ✓ | | | +| `card_handoff` | ✓ | | | +| `card_mgr_clear` | ✓ | | | +| `card_mgr_trace` | ✓ | | | +| `card_read_summary` | ✓ | | | +| `card_read_summary_reset` | ✓ | | | +| `card_trace_dump` | ✓ | | | +| `card_txn_dump` | ✓ | | | +| `cd_overwrite` | ✓ | | | +| `cd_read_log` | ✓ | | | +| `cdc_volume` | | ✓ | | +| `cdrom_bursts` | ✓ | | | +| `cdrom_cmd_dump` | | ✓ | | +| `cdrom_cmd_reset` | | ✓ | | +| `cdrom_command_history` | ✓ | | | +| `cdrom_command_history_clear` | ✓ | | | +| `cdrom_instant_rate` | ✓ | | | +| `cdrom_sector_dump` | ✓ | | ✓ | +| `cdrom_sector_history` | ✓ | | ✓ | +| `cdrom_sector_history_clear` | ✓ | | ✓ | +| `cdrom_state` | ✓ | | | +| `cdrom_timing` | ✓ | | | +| `cdrom_trace_clear` | ✓ | | | +| `cdrom_trace_dump` | ✓ | | | +| `ce_profile` | ✓ | | | +| `chain_trace` | ✓ | | | +| `clear_input` | ✓ | ✓ | ✓ | +| `continue` | ✓ | | ✓ | +| `cyc_watch` | ✓ | ✓ | | +| `cyc_watch_clear` | ✓ | ✓ | | +| `cyc_watch_dump` | ✓ | ✓ | | +| `cycles_to_next_event` | ✓ | | | +| `d44_ring` | ✓ | | | +| `data_shards` | ✓ | | | +| `devtrace_ctl` | ✓ | ✓ | | +| `devtrace_dump` | ✓ | ✓ | | +| `dirty_block_dump_file` | ✓ | | | +| `dirty_block_log` | ✓ | | | +| `dirty_break_clear` | ✓ | | | +| `dirty_break_range` | ✓ | | | +| `dirty_break_state` | ✓ | | | +| `dirty_flow_log` | ✓ | | | +| `dirty_insn_dump_file` | ✓ | | | +| `dirty_insn_gate` | ✓ | | | +| `dirty_insn_log` | ✓ | | | +| `dirty_ram_stats` | ✓ | | | +| `dirty_ram_unsupported` | ✓ | | | +| `disp_ring` | ✓ | | | +| `dispatch_check` | ✓ | | | +| `dispatch_stats` | ✓ | | | +| `dispatch_tail` | ✓ | | | +| `display_ring_aux` | ✓ | | | +| `display_ring_get` | ✓ | | | +| `display_ring_stats` | ✓ | | | +| `dma_cdrom_history` | ✓ | | | +| `dma_state` | ✓ | | ✓ | +| `dma_trace_clear` | ✓ | | | +| `dma_trace_dump` | ✓ | | | +| `dump_buffer` | ✓ | | | +| `dump_ram` | ✓ | ✓ | ✓ | +| `evcb_snapshot` | ✓ | | | +| `evcb_walk_dump` | ✓ | | | +| `evcb_walk_stats` | ✓ | | | +| `event_ring_clear` | ✓ | | | +| `event_ring_dump` | ✓ | | | +| `event_ring_tail` | ✓ | | | +| `exc_ring` | | ✓ | | +| `first_failure` | ✓ | ✓ | ✓ | +| `fmv_state` | ✓ | | | +| `fn_clear` | ✓ | | | +| `fn_disable` | ✓ | | | +| `fn_entry_dump` | ✓ | | | +| `fn_entry_tail` | ✓ | | ✓ | +| `fn_exit_dump` | ✓ | | | +| `fn_filter` | ✓ | | | +| `fn_stats` | ✓ | | | +| `fntrace_arm` | ✓ | ✓ | | +| `fntrace_arm_clear` | ✓ | | | +| `fntrace_armed` | ✓ | | | +| `fntrace_arms` | | ✓ | | +| `fntrace_clear` | ✓ | | | +| `fntrace_disarm` | | ✓ | | +| `fntrace_dump` | ✓ | ✓ | | +| `fntrace_reset` | | ✓ | | +| `fntrace_unfiltered` | | ✓ | | +| `frame` | ✓ | | ✓ | +| `frame_fingerprint` | ✓ | | | +| `frame_perf` | ✓ | | | +| `frame_range` | ✓ | ✓ | ✓ | +| `frame_timeseries` | ✓ | ✓ | ✓ | +| `freeze_check` | ✓ | | | +| `game_options` | ✓ | | | +| `geom_correction` | ✓ | | ✓ | +| `get_frame` | ✓ | ✓ | ✓ | +| `get_quads` | ✓ | | | +| `get_registers` | ✓ | ✓ | ✓ | +| `get_snapshots` | ✓ | ✓ | ✓ | +| `gl_coh_ring` | ✓ | | | +| `gl_fbo_peek` | ✓ | | | +| `gl_interp` | ✓ | | | +| `gl_present_ring` | ✓ | | | +| `gl_vram_diff` | ✓ | | | +| `gl_wide_fast` | ✓ | | | +| `gl_ws_ablate` | ✓ | | | +| `gp1_dump` | ✓ | | ✓ | +| `gpu_frame_dump` | ✓ | | ✓ | +| `gpu_opcodes` | ✓ | | | +| `gpu_ring_stats` | ✓ | | | +| `gpu_state` | ✓ | | ✓ | +| `gte_frame_stats` | ✓ | | | +| `gte_intpl_dump` | ✓ | | | +| `gte_latch_dump` | ✓ | | | +| `gte_ring_dump` | ✓ | | | +| `gte_state` | ✓ | | | +| `history` | ✓ | ✓ | ✓ | +| `hle_dump` | ✓ | | ✓ | +| `idle_skip` | ✓ | | | +| `imask_trace` | ✓ | | | +| `input_route_append` | ✓ | | | +| `input_route_clear` | ✓ | | | +| `input_route_start` | ✓ | | | +| `input_route_status` | ✓ | | | +| `input_route_stop` | ✓ | | | +| `insn_freeze` | ✓ | | | +| `insn_freeze_snapshot` | ✓ | | | +| `insn_freeze_status` | ✓ | | | +| `insn_freeze_target` | ✓ | | | +| `irq_state` | ✓ | | ✓ | +| `irqctx_ring` | ✓ | | | +| `kernel_bless` | ✓ | | | +| `latency` | ✓ | | | +| `load_transitions` | ✓ | | | +| `lockstep` | ✓ | | | +| `lockstep_func` | ✓ | | | +| `mc_status` | ✓ | | | +| `mdec_state` | ✓ | | | +| `mdec_trace` | ✓ | | | +| `mdec_trace_clear` | ✓ | | | +| `mem_words` | ✓ | | | +| `mmio_clear` | ✓ | | ✓ | +| `mmio_dump` | ✓ | | ✓ | +| `mmx6_freshfix` | ✓ | | | +| `overlay_candidates` | ✓ | | | +| `overlay_capture_dump` | ✓ | | | +| `overlay_cps_probe` | ✓ | | | +| `overlay_diff_off` | ✓ | | | +| `overlay_diff_on` | ✓ | | | +| `overlay_dump` | ✓ | | | +| `overlay_fp_dump` | ✓ | | | +| `overlay_irq_ratelimit` | ✓ | | | +| `overlay_irq_suppress_off` | ✓ | | | +| `overlay_irq_suppress_on` | ✓ | | | +| `overlay_loader_status` | ✓ | | | +| `overlay_native_block` | ✓ | | | +| `overlay_native_event_granularity` | ✓ | | | +| `overlay_native_off` | ✓ | | | +| `overlay_native_on` | ✓ | | | +| `overlay_native_ring` | ✓ | | | +| `overlay_rescan` | ✓ | | | +| `overlay_shadow_detail` | ✓ | | | +| `overlay_shadow_dump` | ✓ | | | +| `pace_state` | ✓ | | | +| `pad_status` | ✓ | ✓ | | +| `parity_ctl` | ✓ | ✓ | | +| `parity_dump` | ✓ | ✓ | | +| `pause` | ✓ | | ✓ | +| `pc_probe_arm` | ✓ | | | +| `pc_probe_clear` | ✓ | | | +| `pc_probe_dump` | ✓ | | | +| `pgxp` | ✓ | | | +| `phase_hot` | ✓ | | | +| `phase_profile` | ✓ | | | +| `ping` | ✓ | ✓ | ✓ | +| `present_ring` | ✓ | | | | `present_shot` | ✓ | | ✓ | | `present_shot_seq` | ✓ | | ✓ | -| `press` | ✓ | ✓ | | -| `probe_clear` | ✓ | | | -| `probe_trace` | ✓ | | | -| `quit` | ✓ | | ✓ | -| `ra_load_watch` | ✓ | | | -| `read_frame_ram` | ✓ | ✓ | ✓ | -| `read_ram` | ✓ | ✓ | ✓ | -| `record_frame` | ✓ | | | -| `record_frame_dump` | ✓ | | | -| `record_reads_dump` | ✓ | | | -| `restore_trace` | ✓ | | | -| `restore_trace_clear` | ✓ | | | -| `restore_trace_window` | ✓ | | | -| `rtrace_arm` | ✓ | ✓ | | -| `rtrace_clear` | ✓ | | | -| `rtrace_disarm` | | ✓ | | -| `rtrace_disarm_all` | | ✓ | | -| `rtrace_dump` | ✓ | ✓ | | -| `rtrace_ranges` | ✓ | ✓ | | -| `rtrace_reset` | | ✓ | | -| `rtrace_stats` | ✓ | ✓ | | -| `run_to_frame` | ✓ | | ✓ | -| `s3_smear_watch` | ✓ | | ✓ | -| `savestate` | ✓ | | | -| `screenshot` | ✓ | ✓ | ✓ | -| `screenshot_file` | ✓ | ✓ | ✓ | -| `screenshot_hires` | ✓ | | ✓ | -| `set_input` | ✓ | ✓ | ✓ | -| `set_snapshot` | ✓ | ✓ | ✓ | -| `sio_arm_audit` | ✓ | | | -| `sio_burst_stats` | ✓ | | | -| `sio_ctrl_reg_clear` | ✓ | | | -| `sio_ctrl_reg_trace` | ✓ | | | -| `sio_ctrl_reg_window` | ✓ | | | -| `sio_irq_dump` | ✓ | | | -| `sio_irq_window` | ✓ | | | -| `sio_pc_trace` | ✓ | | | -| `sio_pc_window` | ✓ | | | -| `sio_state` | ✓ | | ✓ | -| `sio_trace` | ✓ | ✓ | | -| `sio_trace_reset` | | ✓ | | -| `sio_trace_window` | ✓ | | | -| `sio_write_window` | | ✓ | | -| `sp_ring` | ✓ | | | -| `spu_events` | ✓ | ✓ | | -| `spu_events_reset` | ✓ | | | -| `spu_ram` | ✓ | | | -| `spu_status` | ✓ | | | -| `spu_voices` | ✓ | ✓ | | -| `sreg_trace_clear` | ✓ | | | -| `sreg_trace_dump` | ✓ | | | -| `sreg_trace_find` | ✓ | | | -| `sreg_trace_stats` | ✓ | | | -| `stack_profile` | ✓ | | | -| `starv_ring` | ✓ | | | -| `step` | ✓ | | ✓ | -| `synth_recurse` | ✓ | | | -| `thread_ctx_ring` | ✓ | | | -| `thread_trace` | ✓ | | | -| `thread_trace_clear` | ✓ | | | -| `timers_state` | ✓ | | | -| `turbo` | ✓ | | ✓ | -| `turbo_audio_sink` | ✓ | | | -| `turbo_loads` | ✓ | | | -| `turbo_state` | ✓ | | ✓ | -| `unknown_dispatch_log` | ✓ | | | -| `unwatch` | ✓ | | ✓ | -| `vblank_rate` | ✓ | | | -| `vk_perf` | ✓ | | | -| `vram_peek` | ✓ | ✓ | ✓ | -| `vsync_query_hle` | ✓ | | | -| `warm_cd_route` | ✓ | | | -| `watch` | ✓ | | ✓ | -| `wide_full` | ✓ | | | -| `wide_shot` | ✓ | | | -| `write_ram` | ✓ | | ✓ | -| `ws_aspect` | ✓ | | | -| `ws_aspect_cone_site` | ✓ | | | -| `ws_backdrop_margin` | ✓ | | | -| `ws_backdrop_ring` | ✓ | | | -| `ws_backdrop_stretch` | ✓ | | | -| `ws_census` | ✓ | | | -| `ws_dbg_stretch` | ✓ | | | -| `ws_dome` | ✓ | | | -| `ws_dome_probe` | ✓ | | | -| `ws_far_threshold` | ✓ | | | -| `ws_hud_mode` | ✓ | | | -| `ws_margin` | ✓ | | | -| `ws_nw` | ✓ | | | -| `wtrace_add` | ✓ | | | -| `wtrace_all_dump` | ✓ | ✓ | | -| `wtrace_all_reset` | ✓ | ✓ | | -| `wtrace_all_stats` | ✓ | ✓ | | -| `wtrace_arm` | ✓ | ✓ | | -| `wtrace_boot_dump` | ✓ | | | -| `wtrace_boot_reset` | ✓ | | | -| `wtrace_boot_stats` | ✓ | | | -| `wtrace_boot_summary` | ✓ | | | -| `wtrace_clear` | ✓ | | ✓ | -| `wtrace_del` | ✓ | | | -| `wtrace_disarm` | ✓ | ✓ | | -| `wtrace_disarm_all` | ✓ | ✓ | | -| `wtrace_dump` | ✓ | ✓ | ✓ | -| `wtrace_range` | ✓ | | ✓ | -| `wtrace_ranges` | ✓ | ✓ | | -| `wtrace_reset` | ✓ | ✓ | | -| `wtrace_stats` | ✓ | ✓ | | -| `wtrace_trans_dump` | ✓ | | | -| `wtrace_trans_reset` | ✓ | | | -| `wtrace_trans_stats` | ✓ | | | -| `xlate` | ✓ | | | -| `xprobe` | ✓ | | | -| `xprobe_arm` | ✓ | | | - - +| `press` | ✓ | ✓ | | +| `probe_clear` | ✓ | | | +| `probe_trace` | ✓ | | | +| `quit` | ✓ | | ✓ | +| `ra_load_watch` | ✓ | | | +| `read_frame_ram` | ✓ | ✓ | ✓ | +| `read_ram` | ✓ | ✓ | ✓ | +| `record_frame` | ✓ | | | +| `record_frame_dump` | ✓ | | | +| `record_reads_dump` | ✓ | | | +| `restore_trace` | ✓ | | | +| `restore_trace_clear` | ✓ | | | +| `restore_trace_window` | ✓ | | | +| `rtrace_arm` | ✓ | ✓ | | +| `rtrace_clear` | ✓ | | | +| `rtrace_disarm` | | ✓ | | +| `rtrace_disarm_all` | | ✓ | | +| `rtrace_dump` | ✓ | ✓ | | +| `rtrace_ranges` | ✓ | ✓ | | +| `rtrace_reset` | | ✓ | | +| `rtrace_stats` | ✓ | ✓ | | +| `run_to_frame` | ✓ | | ✓ | +| `s3_smear_watch` | ✓ | | ✓ | +| `savestate` | ✓ | | | +| `scanline` | ✓ | | | +| `screenshot` | ✓ | ✓ | ✓ | +| `screenshot_file` | ✓ | ✓ | ✓ | +| `screenshot_hires` | ✓ | | ✓ | +| `set_input` | ✓ | ✓ | ✓ | +| `set_snapshot` | ✓ | ✓ | ✓ | +| `sio_arm_audit` | ✓ | | | +| `sio_burst_stats` | ✓ | | | +| `sio_ctrl_reg_clear` | ✓ | | | +| `sio_ctrl_reg_trace` | ✓ | | | +| `sio_ctrl_reg_window` | ✓ | | | +| `sio_irq_dump` | ✓ | | | +| `sio_irq_window` | ✓ | | | +| `sio_pc_trace` | ✓ | | | +| `sio_pc_window` | ✓ | | | +| `sio_state` | ✓ | | ✓ | +| `sio_trace` | ✓ | ✓ | | +| `sio_trace_reset` | | ✓ | | +| `sio_trace_window` | ✓ | | | +| `sio_write_window` | | ✓ | | +| `sp_ring` | ✓ | | | +| `spu_events` | ✓ | ✓ | | +| `spu_events_reset` | ✓ | | | +| `spu_ram` | ✓ | | | +| `spu_status` | ✓ | | | +| `spu_voices` | ✓ | ✓ | | +| `sreg_trace_clear` | ✓ | | | +| `sreg_trace_dump` | ✓ | | | +| `sreg_trace_find` | ✓ | | | +| `sreg_trace_stats` | ✓ | | | +| `stack_profile` | ✓ | | | +| `starv_ring` | ✓ | | | +| `step` | ✓ | | ✓ | +| `synth_recurse` | ✓ | | | +| `thread_ctx_ring` | ✓ | | | +| `thread_trace` | ✓ | | | +| `thread_trace_clear` | ✓ | | | +| `timers_state` | ✓ | | | +| `turbo` | ✓ | | ✓ | +| `turbo_audio_sink` | ✓ | | | +| `turbo_loads` | ✓ | | | +| `turbo_state` | ✓ | | ✓ | +| `unknown_dispatch_log` | ✓ | | | +| `unwatch` | ✓ | | ✓ | +| `vblank_rate` | ✓ | | | +| `vk_perf` | ✓ | | | +| `vram_peek` | ✓ | ✓ | ✓ | +| `vsync_query_hle` | ✓ | | | +| `warm_cd_route` | ✓ | | | +| `watch` | ✓ | | ✓ | +| `wide_full` | ✓ | | | +| `wide_shot` | ✓ | | | +| `write_ram` | ✓ | | ✓ | +| `ws_aspect` | ✓ | | | +| `ws_aspect_cone_site` | ✓ | | | +| `ws_backdrop_margin` | ✓ | | | +| `ws_backdrop_ring` | ✓ | | | +| `ws_backdrop_stretch` | ✓ | | | +| `ws_census` | ✓ | | | +| `ws_dbg_stretch` | ✓ | | | +| `ws_dome` | ✓ | | | +| `ws_dome_probe` | ✓ | | | +| `ws_far_threshold` | ✓ | | | +| `ws_hud_mode` | ✓ | | | +| `ws_margin` | ✓ | | | +| `ws_nw` | ✓ | | | +| `wtrace_add` | ✓ | | | +| `wtrace_all_dump` | ✓ | ✓ | | +| `wtrace_all_reset` | ✓ | ✓ | | +| `wtrace_all_stats` | ✓ | ✓ | | +| `wtrace_arm` | ✓ | ✓ | | +| `wtrace_boot_dump` | ✓ | | | +| `wtrace_boot_reset` | ✓ | | | +| `wtrace_boot_stats` | ✓ | | | +| `wtrace_boot_summary` | ✓ | | | +| `wtrace_clear` | ✓ | | ✓ | +| `wtrace_del` | ✓ | | | +| `wtrace_disarm` | ✓ | ✓ | | +| `wtrace_disarm_all` | ✓ | ✓ | | +| `wtrace_dump` | ✓ | ✓ | ✓ | +| `wtrace_range` | ✓ | | ✓ | +| `wtrace_ranges` | ✓ | ✓ | | +| `wtrace_reset` | ✓ | ✓ | | +| `wtrace_stats` | ✓ | ✓ | | +| `wtrace_trans_dump` | ✓ | | | +| `wtrace_trans_reset` | ✓ | | | +| `wtrace_trans_stats` | ✓ | | | +| `xlate` | ✓ | | | +| `xprobe` | ✓ | | | +| `xprobe_arm` | ✓ | | | + + diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index ce4dc10eb..6ddffea9a 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -635,6 +635,15 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& "[video] crt_filter must be \"raw\"|\"crt\"|\"composite\"|\"trinitron\": {}", mode)); } + if (video.contains("scanlines")) { + rt.video_scanlines = toml::find(video, "scanlines"); + } + if (video.contains("scanline_strength")) { + double s = toml::find(video, "scanline_strength"); + if (s < 0.0) s = 0.0; + if (s > 1.0) s = 1.0; + rt.video_scanline_strength = s; + } if (video.contains("auto_skip_fmv")) { rt.video_auto_skip_fmv = toml::find(video, "auto_skip_fmv"); } @@ -2296,6 +2305,15 @@ UserSettings load_user_settings(const fs::path& path) { else if (m == "composite") { s.screen_kind = 2; s.has_screen_kind = true; } else if (m == "trinitron") { s.screen_kind = 3; s.has_screen_kind = true; } }); + if (v.contains("scanlines")) try_get([&]{ + s.scanlines = toml::find(v, "scanlines"); s.has_scanlines = true; + }); + if (v.contains("scanline_strength")) try_get([&]{ + double d = toml::find(v, "scanline_strength"); + if (d < 0.0) d = 0.0; + if (d > 1.0) d = 1.0; + s.scanline_strength = d; s.has_scanline_strength = true; + }); if (v.contains("auto_skip_fmv")) try_get([&]{ s.auto_skip_fmv = toml::find(v, "auto_skip_fmv"); s.has_auto_skip_fmv = true; }); @@ -2615,6 +2633,10 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) { : s.screen_kind == 3 ? "trinitron" : "raw"; f << "crt_filter = \"" << k << "\"\n"; } + if (s.has_scanlines) + f << "scanlines = " << (s.scanlines ? "true" : "false") << "\n"; + if (s.has_scanline_strength) + f << "scanline_strength = " << s.scanline_strength << "\n"; if (s.has_auto_skip_fmv) f << "auto_skip_fmv = " << (s.auto_skip_fmv ? "true" : "false") << "\n"; /* turbo_loads is deliberately NOT written back: it is deprecated and no diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 41724162b..d979d59d3 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -488,6 +488,13 @@ struct RuntimeConfig { // PSX_SCREEN env var overrides this at runtime (debug path). int video_screen_kind = 0; + // scanlines: present-time horizontal scanline darkening (host enhancement, + // orthogonal to crt_filter's colour LUT — the two compose). Off by default; + // scanline_strength 0..1 is the dark-gap depth. PSX_SCANLINES / + // PSX_SCANLINE_STRENGTH override at runtime (debug path). + bool video_scanlines = false; + double video_scanline_strength = 0.5; + // auto_skip_fmv: when true, full-motion videos (streaming XA audio + MDEC // video) are skipped the instant they're detected — presentation + pacing are // suppressed and audio muted for the duration, so an FMV ends in a fraction of @@ -1196,6 +1203,10 @@ struct UserSettings { bool has_geometry_correction = false; bool geometry_correction = false; bool has_perspective_texturing = false; bool perspective_texturing = false; bool has_screen_kind = false; int screen_kind = 0; // 0..3 (ScreenKind) + // Scanline post-process (see RuntimeConfig::video_scanlines). Strength stored + // 0..1; the launcher ABI carries it as an integer percent. + bool has_scanlines = false; bool scanlines = false; + bool has_scanline_strength = false; double scanline_strength = 0.5; bool has_auto_skip_fmv = false; bool auto_skip_fmv = false; // skip FMVs // [video] turbo_loads: DEPRECATED AND IGNORED — the legacy home of the // generic Turbo loads switch, back when the launcher drew a row for it. diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index 9f64b4aba..26aa54170 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -122,6 +122,14 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear) * for the widescreen field-of-view hack. */ void gl_renderer_set_display_aspect(int num, int den); +/* Scanline post-process (host display setting). on toggles the effect; strength + * (0..1) is the depth of the dark gap between PS1 scanlines. Applied at the + * native display-line pitch in the present/interpolation shaders, and faded in + * with output scale so it never shimmers on a sub-2x window. gl_renderer_get_ + * scanlines returns the on flag and (via out-param) the current strength. */ +void gl_renderer_set_scanlines(int on, float strength); +int gl_renderer_get_scanlines(float *strength); + /* Select full native-wide mirror rendering instead of the centre-splice fast * path. Textured edge expansion needs the complete mirror surface. */ void gl_renderer_set_wide_fast(int on); diff --git a/runtime/include/host_keymap.h b/runtime/include/host_keymap.h index dd077093e..c11862d2e 100644 --- a/runtime/include/host_keymap.h +++ b/runtime/include/host_keymap.h @@ -21,6 +21,7 @@ typedef enum HostKeymapAction { HOST_KEYMAP_DISPLAY_PERF, HOST_KEYMAP_REWIND, /* default F8 */ HOST_KEYMAP_SAVE_STATE_MENU, /* default F7 */ + HOST_KEYMAP_SCANLINES, /* default F6 */ HOST_KEYMAP_ACTION_COUNT } HostKeymapAction; diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 5c2278f09..0f5fa5d2f 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -7846,6 +7846,25 @@ static void handle_ws_nw(int id, const char *json) id, psx_ws_get_native_wide(), ws.mode, ws.nw_extra); } +/* Live scanline post-process toggle (A/B): `scanline on=<0|1> pct=<0..100>`. + * Either field is optional — omit `on` to keep the current toggle, omit `pct` + * to keep the current strength. Reports the resulting state. */ +extern void psx_video_set_scanlines(int on, float strength); +extern int psx_video_get_scanlines(float *strength); +static void handle_scanline(int id, const char *json) +{ + int on = json_get_int(json, "on", -1); + int pct = json_get_int(json, "pct", -1); + float cur = 0.f; + int cur_on = psx_video_get_scanlines(&cur); + int new_on = (on >= 0) ? (on ? 1 : 0) : cur_on; + float new_s = (pct >= 0 && pct <= 100) ? (float)pct / 100.f : cur; + psx_video_set_scanlines(new_on, new_s); + cur_on = psx_video_get_scanlines(&cur); + send_fmt("{\"id\":%d,\"ok\":true,\"scanlines\":%d,\"strength_pct\":%d}", + id, cur_on, (int)(cur * 100.f + 0.5f)); +} + /* ws_backdrop_ring: dump the always-on auto_backdrop rewrite ring (which windows * fire, live extent/camera/DL-count, orig vs final bound). Read-only; small * heap envelope so the per-byte stall of a giant read is never in play. */ @@ -13531,6 +13550,7 @@ static const CmdEntry s_commands[] = { { "kernel_bless", handle_kernel_bless }, { "ws_aspect", handle_ws_aspect }, { "ws_nw", handle_ws_nw }, + { "scanline", handle_scanline }, { "ws_backdrop_ring", handle_ws_backdrop_ring }, { "ws_ui_groups", handle_ws_ui_groups }, { "ws_backdrop_margin", handle_ws_backdrop_margin }, diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 31c8346ec..3312551e8 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -1,334 +1,344 @@ -/* gpu_gl_renderer.c — hardware OpenGL renderer backend. - * - * ARCHITECTURE (v2 — GPU-authoritative VRAM) - * ------------------------------------------- - * The FBO color texture (`s_hr_tex`, RGBA8, 1024*S x 512*S where S is the - * internal-resolution scale from [video] supersampling) is the single - * authoritative copy of VRAM. EVERY mutation goes through the GPU: - * - * - polys / rects / lines -> rasterized into the hr FBO - * - GP0(02h) fills -> scissored glClear (color + stencil) - * - VRAM->VRAM copies -> hr FBO -> scratch texture blit, then a - * masked quad draw back into the hr FBO - * - CPU->VRAM transfers -> written to the CPU mirror immediately and - * accumulated into a pending-upload rect; the - * rect is flushed (staging texture + quad into - * the hr FBO, plus a direct R16UI subimage) - * before the next GPU op, preserving op order - * - * PS1 MASK BIT (bit15). The FBO alpha channel carries bit15 exactly - * (1.0 = set), and a stencil buffer mirrors it (stencil bit0 == bit15): - * - "set mask" -> fragment alpha + stencil write value - * - "check mask"-> stencil test (pass iff stored == 0). The stencil write - * value is coupled to the test reference in GL, so when checking we use - * GL_INVERT on pass to write a 1 (stored is known 0 when the test - * passes) and GL_KEEP to write a 0. - * - textured prims (and copies/uploads, whose pixels carry per-texel STP - * bits) are drawn in TWO passes split by the STP bit via discard, so - * each pass writes a single known stencil value. The same split already - * existed for semi-transparent texture blending. - * - blending uses glBlendFuncSeparate so the alpha (mask) channel is - * always REPLACED by the source fragment's mask bit, never blended. - * - * TEXTURE SAMPLING / RENDER-TO-TEXTURE. Textured prims sample a native-res - * R16UI mirror (`s_raw_tex`) holding raw 1555 VRAM values (CLUT decode + - * texture window + optional bilinear in the fragment shader). GPU draws - * mark a native-coords dirty union; before any textured draw whose texture - * page or CLUT intersects the union, a PACK pass re-encodes the dirty - * region of the hr FBO into the raw mirror (point-sampled at native - * coords). CPU->VRAM uploads update the raw mirror directly. So content - * rendered by the GPU is immediately valid as a texture source. - * - * CPU READBACKS (VRAM->CPU transfers, GPUREAD, screenshots, 24-bit FMV - * display) flush uploads + pack, then glReadPixels the raw mirror straight - * into the CPU VRAM array (raw 1555, no conversion loop). - * - * PRESENT is deterministic: 15-bit frames always blit the display region - * from the hr FBO into a 4:3 letterboxed rect (single path — no more - * frame-to-frame alternation between FBO and CPU presents). 24-bit (FMV) - * frames sync to CPU and use the quad-present path, also letterboxed. - * PSX_GL_FORCE_CPU_PRESENT=1 (read by main.cpp) forces the CPU path as a - * diagnostic. - * - * Known divergences from the software rasterizer (accepted, documented): - * - GL triangle/line coverage rules differ from the PS1 DDA by ±1px on - * edges; lines use GL_LINES (width S) instead of Bresenham. - * - No dithering (the software path doesn't dither either). - * - Gouraud interpolation happens at 8-bit precision instead of 5-bit - * (smoother gradients; readback re-quantizes to 5-bit). - * - VRAM-wrapping draws are clamped, except GP0(02h) fills which split - * into wrapped segments. Wrapping copies/draws are unused by real SDKs. - * - * Init is all-or-nothing: if any shader/FBO fails, gl_renderer_init_context - * returns 0 and the runtime falls back to the pure software renderer — no - * half-GL hybrid. */ - -#include "gpu.h" -#include "gpu_render.h" +/* gpu_gl_renderer.c — hardware OpenGL renderer backend. + * + * ARCHITECTURE (v2 — GPU-authoritative VRAM) + * ------------------------------------------- + * The FBO color texture (`s_hr_tex`, RGBA8, 1024*S x 512*S where S is the + * internal-resolution scale from [video] supersampling) is the single + * authoritative copy of VRAM. EVERY mutation goes through the GPU: + * + * - polys / rects / lines -> rasterized into the hr FBO + * - GP0(02h) fills -> scissored glClear (color + stencil) + * - VRAM->VRAM copies -> hr FBO -> scratch texture blit, then a + * masked quad draw back into the hr FBO + * - CPU->VRAM transfers -> written to the CPU mirror immediately and + * accumulated into a pending-upload rect; the + * rect is flushed (staging texture + quad into + * the hr FBO, plus a direct R16UI subimage) + * before the next GPU op, preserving op order + * + * PS1 MASK BIT (bit15). The FBO alpha channel carries bit15 exactly + * (1.0 = set), and a stencil buffer mirrors it (stencil bit0 == bit15): + * - "set mask" -> fragment alpha + stencil write value + * - "check mask"-> stencil test (pass iff stored == 0). The stencil write + * value is coupled to the test reference in GL, so when checking we use + * GL_INVERT on pass to write a 1 (stored is known 0 when the test + * passes) and GL_KEEP to write a 0. + * - textured prims (and copies/uploads, whose pixels carry per-texel STP + * bits) are drawn in TWO passes split by the STP bit via discard, so + * each pass writes a single known stencil value. The same split already + * existed for semi-transparent texture blending. + * - blending uses glBlendFuncSeparate so the alpha (mask) channel is + * always REPLACED by the source fragment's mask bit, never blended. + * + * TEXTURE SAMPLING / RENDER-TO-TEXTURE. Textured prims sample a native-res + * R16UI mirror (`s_raw_tex`) holding raw 1555 VRAM values (CLUT decode + + * texture window + optional bilinear in the fragment shader). GPU draws + * mark a native-coords dirty union; before any textured draw whose texture + * page or CLUT intersects the union, a PACK pass re-encodes the dirty + * region of the hr FBO into the raw mirror (point-sampled at native + * coords). CPU->VRAM uploads update the raw mirror directly. So content + * rendered by the GPU is immediately valid as a texture source. + * + * CPU READBACKS (VRAM->CPU transfers, GPUREAD, screenshots, 24-bit FMV + * display) flush uploads + pack, then glReadPixels the raw mirror straight + * into the CPU VRAM array (raw 1555, no conversion loop). + * + * PRESENT is deterministic: 15-bit frames always blit the display region + * from the hr FBO into a 4:3 letterboxed rect (single path — no more + * frame-to-frame alternation between FBO and CPU presents). 24-bit (FMV) + * frames sync to CPU and use the quad-present path, also letterboxed. + * PSX_GL_FORCE_CPU_PRESENT=1 (read by main.cpp) forces the CPU path as a + * diagnostic. + * + * Known divergences from the software rasterizer (accepted, documented): + * - GL triangle/line coverage rules differ from the PS1 DDA by ±1px on + * edges; lines use GL_LINES (width S) instead of Bresenham. + * - No dithering (the software path doesn't dither either). + * - Gouraud interpolation happens at 8-bit precision instead of 5-bit + * (smoother gradients; readback re-quantizes to 5-bit). + * - VRAM-wrapping draws are clamped, except GP0(02h) fills which split + * into wrapped segments. Wrapping copies/draws are unused by real SDKs. + * + * Init is all-or-nothing: if any shader/FBO fails, gl_renderer_init_context + * returns 0 and the runtime falls back to the pure software renderer — no + * half-GL hybrid. */ + +#include "gpu.h" +#include "gpu_render.h" #include "gpu_sw_renderer.h" #include "gpu_gl_renderer.h" #include "frame_interpolation.h" #include "host_osd.h" #include "psx_savestate_menu.h" -#include "host_time.h" -#include "latency_ring.h" -#include "frame_pacing.h" -#include "psx_rewind.h" - -#include "psx_sdl.h" -#if defined(PSX_SDL3) -#include -#else -#include -#endif -#include -#include -#include -#include -#include +#include "host_time.h" +#include "latency_ring.h" +#include "frame_pacing.h" +#include "psx_rewind.h" + +#include "psx_sdl.h" +#if defined(PSX_SDL3) +#include +#else +#include +#endif +#include +#include +#include +#include +#include #include "png_write.h" /* png_write_rgb — present_shot readback */ - -#ifndef GL_BGRA -#define GL_BGRA 0x80E1 -#endif -#ifndef GL_CLAMP_TO_EDGE -#define GL_CLAMP_TO_EDGE 0x812F -#endif -#ifndef GL_RGBA8 -#define GL_RGBA8 0x8058 -#endif -/* GL 1.4+ enums absent from MinGW's GL 1.1 headers. */ -#define PSXGL_FRAGMENT_SHADER 0x8B30 -#define PSXGL_VERTEX_SHADER 0x8B31 -#define PSXGL_COMPILE_STATUS 0x8B81 -#define PSXGL_LINK_STATUS 0x8B82 -#define PSXGL_TEXTURE0 0x84C0 -#define PSXGL_ARRAY_BUFFER 0x8892 -#define PSXGL_STREAM_DRAW 0x88E0 -#define PSXGL_FRAMEBUFFER 0x8D40 -#define PSXGL_READ_FRAMEBUFFER 0x8CA8 -#define PSXGL_DRAW_FRAMEBUFFER 0x8CA9 -#define PSXGL_COLOR_ATTACHMENT0 0x8CE0 -#define PSXGL_DEPTH_STENCIL_ATTACHMENT 0x821A -#define PSXGL_FRAMEBUFFER_COMPLETE 0x8CD5 -#define PSXGL_RENDERBUFFER 0x8D41 -#define PSXGL_DEPTH24_STENCIL8 0x88F0 -#define PSXGL_R16UI 0x8234 -#define PSXGL_RED_INTEGER 0x8D94 -#define PSXGL_FUNC_ADD 0x8006 -#define PSXGL_FUNC_REVERSE_SUBTRACT 0x800B -#define PSXGL_CONSTANT_ALPHA 0x8003 -#define PSXGL_UNPACK_ROW_LENGTH 0x0CF2 -#define PSXGL_SRC1_ALPHA 0x8589 - -#ifndef APIENTRY -#define APIENTRY -#endif - -#define VRAM_W 1024 -#define VRAM_H 512 -#define GL_MAX_INTERNAL_SCALE 4 - -/* ---- Loaded modern-GL entry points ------------------------------------- */ -typedef GLuint (APIENTRY *PFN_glCreateShader)(GLenum); -typedef void (APIENTRY *PFN_glShaderSource)(GLuint, GLsizei, const char *const *, const GLint *); -typedef void (APIENTRY *PFN_glCompileShader)(GLuint); -typedef void (APIENTRY *PFN_glGetShaderiv)(GLuint, GLenum, GLint *); -typedef void (APIENTRY *PFN_glGetShaderInfoLog)(GLuint, GLsizei, GLsizei *, char *); -typedef void (APIENTRY *PFN_glDeleteShader)(GLuint); -typedef GLuint (APIENTRY *PFN_glCreateProgram)(void); -typedef void (APIENTRY *PFN_glAttachShader)(GLuint, GLuint); -typedef void (APIENTRY *PFN_glLinkProgram)(GLuint); -typedef void (APIENTRY *PFN_glGetProgramiv)(GLuint, GLenum, GLint *); -typedef void (APIENTRY *PFN_glGetProgramInfoLog)(GLuint, GLsizei, GLsizei *, char *); -typedef void (APIENTRY *PFN_glUseProgram)(GLuint); -typedef GLint (APIENTRY *PFN_glGetUniformLocation)(GLuint, const char *); -typedef void (APIENTRY *PFN_glUniform1i)(GLint, GLint); -typedef void (APIENTRY *PFN_glUniform1f)(GLint, GLfloat); -typedef void (APIENTRY *PFN_glUniform2i)(GLint, GLint, GLint); -typedef void (APIENTRY *PFN_glUniform4i)(GLint, GLint, GLint, GLint, GLint); + +#ifndef GL_BGRA +#define GL_BGRA 0x80E1 +#endif +#ifndef GL_CLAMP_TO_EDGE +#define GL_CLAMP_TO_EDGE 0x812F +#endif +#ifndef GL_RGBA8 +#define GL_RGBA8 0x8058 +#endif +/* GL 1.4+ enums absent from MinGW's GL 1.1 headers. */ +#define PSXGL_FRAGMENT_SHADER 0x8B30 +#define PSXGL_VERTEX_SHADER 0x8B31 +#define PSXGL_COMPILE_STATUS 0x8B81 +#define PSXGL_LINK_STATUS 0x8B82 +#define PSXGL_TEXTURE0 0x84C0 +#define PSXGL_ARRAY_BUFFER 0x8892 +#define PSXGL_STREAM_DRAW 0x88E0 +#define PSXGL_FRAMEBUFFER 0x8D40 +#define PSXGL_READ_FRAMEBUFFER 0x8CA8 +#define PSXGL_DRAW_FRAMEBUFFER 0x8CA9 +#define PSXGL_COLOR_ATTACHMENT0 0x8CE0 +#define PSXGL_DEPTH_STENCIL_ATTACHMENT 0x821A +#define PSXGL_FRAMEBUFFER_COMPLETE 0x8CD5 +#define PSXGL_RENDERBUFFER 0x8D41 +#define PSXGL_DEPTH24_STENCIL8 0x88F0 +#define PSXGL_R16UI 0x8234 +#define PSXGL_RED_INTEGER 0x8D94 +#define PSXGL_FUNC_ADD 0x8006 +#define PSXGL_FUNC_REVERSE_SUBTRACT 0x800B +#define PSXGL_CONSTANT_ALPHA 0x8003 +#define PSXGL_UNPACK_ROW_LENGTH 0x0CF2 +#define PSXGL_SRC1_ALPHA 0x8589 + +#ifndef APIENTRY +#define APIENTRY +#endif + +#define VRAM_W 1024 +#define VRAM_H 512 +#define GL_MAX_INTERNAL_SCALE 4 + +/* ---- Loaded modern-GL entry points ------------------------------------- */ +typedef GLuint (APIENTRY *PFN_glCreateShader)(GLenum); +typedef void (APIENTRY *PFN_glShaderSource)(GLuint, GLsizei, const char *const *, const GLint *); +typedef void (APIENTRY *PFN_glCompileShader)(GLuint); +typedef void (APIENTRY *PFN_glGetShaderiv)(GLuint, GLenum, GLint *); +typedef void (APIENTRY *PFN_glGetShaderInfoLog)(GLuint, GLsizei, GLsizei *, char *); +typedef void (APIENTRY *PFN_glDeleteShader)(GLuint); +typedef GLuint (APIENTRY *PFN_glCreateProgram)(void); +typedef void (APIENTRY *PFN_glAttachShader)(GLuint, GLuint); +typedef void (APIENTRY *PFN_glLinkProgram)(GLuint); +typedef void (APIENTRY *PFN_glGetProgramiv)(GLuint, GLenum, GLint *); +typedef void (APIENTRY *PFN_glGetProgramInfoLog)(GLuint, GLsizei, GLsizei *, char *); +typedef void (APIENTRY *PFN_glUseProgram)(GLuint); +typedef GLint (APIENTRY *PFN_glGetUniformLocation)(GLuint, const char *); +typedef void (APIENTRY *PFN_glUniform1i)(GLint, GLint); +typedef void (APIENTRY *PFN_glUniform1f)(GLint, GLfloat); +typedef void (APIENTRY *PFN_glUniform2i)(GLint, GLint, GLint); +typedef void (APIENTRY *PFN_glUniform4i)(GLint, GLint, GLint, GLint, GLint); typedef void (APIENTRY *PFN_glUniform2f)(GLint, GLfloat, GLfloat); -typedef void (APIENTRY *PFN_glUniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat); -typedef void (APIENTRY *PFN_glBlendColor)(GLfloat, GLfloat, GLfloat, GLfloat); -typedef void (APIENTRY *PFN_glBlendFuncSeparate)(GLenum, GLenum, GLenum, GLenum); -typedef void (APIENTRY *PFN_glBlendEquationSeparate)(GLenum, GLenum); -typedef void (APIENTRY *PFN_glGenVertexArrays)(GLsizei, GLuint *); -typedef void (APIENTRY *PFN_glBindVertexArray)(GLuint); -typedef void (APIENTRY *PFN_glActiveTexture)(GLenum); -typedef void (APIENTRY *PFN_glGenBuffers)(GLsizei, GLuint *); -typedef void (APIENTRY *PFN_glBindBuffer)(GLenum, GLuint); -typedef void (APIENTRY *PFN_glBufferData)(GLenum, ptrdiff_t, const void *, GLenum); -typedef void (APIENTRY *PFN_glVertexAttribPointer)(GLuint, GLint, GLenum, GLboolean, GLsizei, const void *); -typedef void (APIENTRY *PFN_glEnableVertexAttribArray)(GLuint); -typedef void (APIENTRY *PFN_glBindFragDataLocationIndexed)(GLuint, GLuint, GLuint, const char *); -typedef void (APIENTRY *PFN_glGenFramebuffers)(GLsizei, GLuint *); -typedef void (APIENTRY *PFN_glDeleteFramebuffers)(GLsizei, const GLuint *); -typedef void (APIENTRY *PFN_glBindFramebuffer)(GLenum, GLuint); -typedef void (APIENTRY *PFN_glFramebufferTexture2D)(GLenum, GLenum, GLenum, GLuint, GLint); -typedef GLenum (APIENTRY *PFN_glCheckFramebufferStatus)(GLenum); -typedef void (APIENTRY *PFN_glBlitFramebuffer)(GLint,GLint,GLint,GLint,GLint,GLint,GLint,GLint,GLbitfield,GLenum); -typedef void (APIENTRY *PFN_glGenRenderbuffers)(GLsizei, GLuint *); -typedef void (APIENTRY *PFN_glDeleteRenderbuffers)(GLsizei, const GLuint *); -typedef void (APIENTRY *PFN_glBindRenderbuffer)(GLenum, GLuint); -typedef void (APIENTRY *PFN_glRenderbufferStorage)(GLenum, GLenum, GLsizei, GLsizei); -typedef void (APIENTRY *PFN_glFramebufferRenderbuffer)(GLenum, GLenum, GLenum, GLuint); -/* GPU timer queries (ARB_timer_query / core GL 3.3) — frame_perf instrumentation. */ -typedef void (APIENTRY *PFN_glGenQueries)(GLsizei, GLuint *); -typedef void (APIENTRY *PFN_glDeleteQueries)(GLsizei, const GLuint *); -typedef void (APIENTRY *PFN_glBeginQuery)(GLenum, GLuint); -typedef void (APIENTRY *PFN_glEndQuery)(GLenum); -typedef void (APIENTRY *PFN_glGetQueryObjectui64v)(GLuint, GLenum, GLuint64 *); -typedef void (APIENTRY *PFN_glGetQueryObjectiv)(GLuint, GLenum, GLint *); -typedef void (APIENTRY *PFN_glQueryCounter)(GLuint, GLenum); -#ifndef GL_TIME_ELAPSED -#define GL_TIME_ELAPSED 0x88BF -#endif -#ifndef GL_TIMESTAMP -#define GL_TIMESTAMP 0x8E28 -#endif -#ifndef GL_QUERY_RESULT -#define GL_QUERY_RESULT 0x8866 -#endif -#ifndef GL_QUERY_RESULT_AVAILABLE -#define GL_QUERY_RESULT_AVAILABLE 0x8867 -#endif - -static PFN_glCreateShader p_glCreateShader; -static PFN_glShaderSource p_glShaderSource; -static PFN_glCompileShader p_glCompileShader; -static PFN_glGetShaderiv p_glGetShaderiv; -static PFN_glGetShaderInfoLog p_glGetShaderInfoLog; -static PFN_glDeleteShader p_glDeleteShader; -static PFN_glCreateProgram p_glCreateProgram; -static PFN_glAttachShader p_glAttachShader; -static PFN_glLinkProgram p_glLinkProgram; -static PFN_glGetProgramiv p_glGetProgramiv; -static PFN_glGetProgramInfoLog p_glGetProgramInfoLog; -static PFN_glUseProgram p_glUseProgram; -static PFN_glGetUniformLocation p_glGetUniformLocation; -static PFN_glUniform1i p_glUniform1i; -static PFN_glUniform1f p_glUniform1f; -static PFN_glUniform2i p_glUniform2i; -static PFN_glUniform4i p_glUniform4i; +typedef void (APIENTRY *PFN_glUniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat); +typedef void (APIENTRY *PFN_glBlendColor)(GLfloat, GLfloat, GLfloat, GLfloat); +typedef void (APIENTRY *PFN_glBlendFuncSeparate)(GLenum, GLenum, GLenum, GLenum); +typedef void (APIENTRY *PFN_glBlendEquationSeparate)(GLenum, GLenum); +typedef void (APIENTRY *PFN_glGenVertexArrays)(GLsizei, GLuint *); +typedef void (APIENTRY *PFN_glBindVertexArray)(GLuint); +typedef void (APIENTRY *PFN_glActiveTexture)(GLenum); +typedef void (APIENTRY *PFN_glGenBuffers)(GLsizei, GLuint *); +typedef void (APIENTRY *PFN_glBindBuffer)(GLenum, GLuint); +typedef void (APIENTRY *PFN_glBufferData)(GLenum, ptrdiff_t, const void *, GLenum); +typedef void (APIENTRY *PFN_glVertexAttribPointer)(GLuint, GLint, GLenum, GLboolean, GLsizei, const void *); +typedef void (APIENTRY *PFN_glEnableVertexAttribArray)(GLuint); +typedef void (APIENTRY *PFN_glBindFragDataLocationIndexed)(GLuint, GLuint, GLuint, const char *); +typedef void (APIENTRY *PFN_glGenFramebuffers)(GLsizei, GLuint *); +typedef void (APIENTRY *PFN_glDeleteFramebuffers)(GLsizei, const GLuint *); +typedef void (APIENTRY *PFN_glBindFramebuffer)(GLenum, GLuint); +typedef void (APIENTRY *PFN_glFramebufferTexture2D)(GLenum, GLenum, GLenum, GLuint, GLint); +typedef GLenum (APIENTRY *PFN_glCheckFramebufferStatus)(GLenum); +typedef void (APIENTRY *PFN_glBlitFramebuffer)(GLint,GLint,GLint,GLint,GLint,GLint,GLint,GLint,GLbitfield,GLenum); +typedef void (APIENTRY *PFN_glGenRenderbuffers)(GLsizei, GLuint *); +typedef void (APIENTRY *PFN_glDeleteRenderbuffers)(GLsizei, const GLuint *); +typedef void (APIENTRY *PFN_glBindRenderbuffer)(GLenum, GLuint); +typedef void (APIENTRY *PFN_glRenderbufferStorage)(GLenum, GLenum, GLsizei, GLsizei); +typedef void (APIENTRY *PFN_glFramebufferRenderbuffer)(GLenum, GLenum, GLenum, GLuint); +/* GPU timer queries (ARB_timer_query / core GL 3.3) — frame_perf instrumentation. */ +typedef void (APIENTRY *PFN_glGenQueries)(GLsizei, GLuint *); +typedef void (APIENTRY *PFN_glDeleteQueries)(GLsizei, const GLuint *); +typedef void (APIENTRY *PFN_glBeginQuery)(GLenum, GLuint); +typedef void (APIENTRY *PFN_glEndQuery)(GLenum); +typedef void (APIENTRY *PFN_glGetQueryObjectui64v)(GLuint, GLenum, GLuint64 *); +typedef void (APIENTRY *PFN_glGetQueryObjectiv)(GLuint, GLenum, GLint *); +typedef void (APIENTRY *PFN_glQueryCounter)(GLuint, GLenum); +#ifndef GL_TIME_ELAPSED +#define GL_TIME_ELAPSED 0x88BF +#endif +#ifndef GL_TIMESTAMP +#define GL_TIMESTAMP 0x8E28 +#endif +#ifndef GL_QUERY_RESULT +#define GL_QUERY_RESULT 0x8866 +#endif +#ifndef GL_QUERY_RESULT_AVAILABLE +#define GL_QUERY_RESULT_AVAILABLE 0x8867 +#endif + +static PFN_glCreateShader p_glCreateShader; +static PFN_glShaderSource p_glShaderSource; +static PFN_glCompileShader p_glCompileShader; +static PFN_glGetShaderiv p_glGetShaderiv; +static PFN_glGetShaderInfoLog p_glGetShaderInfoLog; +static PFN_glDeleteShader p_glDeleteShader; +static PFN_glCreateProgram p_glCreateProgram; +static PFN_glAttachShader p_glAttachShader; +static PFN_glLinkProgram p_glLinkProgram; +static PFN_glGetProgramiv p_glGetProgramiv; +static PFN_glGetProgramInfoLog p_glGetProgramInfoLog; +static PFN_glUseProgram p_glUseProgram; +static PFN_glGetUniformLocation p_glGetUniformLocation; +static PFN_glUniform1i p_glUniform1i; +static PFN_glUniform1f p_glUniform1f; +static PFN_glUniform2i p_glUniform2i; +static PFN_glUniform4i p_glUniform4i; static PFN_glUniform2f p_glUniform2f; -static PFN_glUniform4f p_glUniform4f; -static PFN_glBlendColor p_glBlendColor; -static PFN_glBlendFuncSeparate p_glBlendFuncSeparate; -static PFN_glBlendEquationSeparate p_glBlendEquationSeparate; -static PFN_glGenVertexArrays p_glGenVertexArrays; -static PFN_glBindVertexArray p_glBindVertexArray; -static PFN_glActiveTexture p_glActiveTexture; -static PFN_glGenBuffers p_glGenBuffers; -static PFN_glBindBuffer p_glBindBuffer; -static PFN_glBufferData p_glBufferData; -static PFN_glVertexAttribPointer p_glVertexAttribPointer; -static PFN_glEnableVertexAttribArray p_glEnableVertexAttribArray; -static PFN_glBindFragDataLocationIndexed p_glBindFragDataLocationIndexed; -static PFN_glGenFramebuffers p_glGenFramebuffers; -static PFN_glDeleteFramebuffers p_glDeleteFramebuffers; -static PFN_glBindFramebuffer p_glBindFramebuffer; -static PFN_glFramebufferTexture2D p_glFramebufferTexture2D; -static PFN_glCheckFramebufferStatus p_glCheckFramebufferStatus; -static PFN_glBlitFramebuffer p_glBlitFramebuffer; -static PFN_glGenQueries p_glGenQueries; -static PFN_glDeleteQueries p_glDeleteQueries; -static PFN_glBeginQuery p_glBeginQuery; -static PFN_glEndQuery p_glEndQuery; -static PFN_glGetQueryObjectui64v p_glGetQueryObjectui64v; -static PFN_glGetQueryObjectiv p_glGetQueryObjectiv; -static PFN_glQueryCounter p_glQueryCounter; -static void gl_perf_init(void); /* frame_perf — defined below, called from init_gpu_raster */ -static void gl_perf_mirror_begin(void); /* frame_perf: GPU-time bracket around ONE native-wide */ -static void gl_perf_mirror_end(void); /* mirror pass (timestamp pair; splits scene canonical/mirror) */ -/* Native-wide mirror ABLATION (perf attribution, debug cmd gl_ws_ablate): - * 0 = normal, 1 = skip the whole mirror pass, 2 = full mirror state churn but no - * draw calls, 3 = mirror draws land in the hr FBO (no per-pass FBO rebind; wide - * margins go stale + hr gets garbage — perf probe only). */ -static int s_ws_ablate = 0; -static void flush_tex_batch(void); /* textured-prim batch — defined below, flushed from coherency points */ -static void flush_flat_batch(void); /* flat/gouraud GEO batch (MotK starfield 0x68 dots) */ -static PFN_glGenRenderbuffers p_glGenRenderbuffers; -static PFN_glDeleteRenderbuffers p_glDeleteRenderbuffers; -static PFN_glBindRenderbuffer p_glBindRenderbuffer; -static PFN_glRenderbufferStorage p_glRenderbufferStorage; -static PFN_glFramebufferRenderbuffer p_glFramebufferRenderbuffer; - -static int load_modern_gl(void) { - int ok = 1; -#define LOAD(p, n) do { p = (void *)SDL_GL_GetProcAddress(n); if (!p) ok = 0; } while (0) - LOAD(p_glCreateShader, "glCreateShader"); LOAD(p_glShaderSource, "glShaderSource"); - LOAD(p_glCompileShader, "glCompileShader"); LOAD(p_glGetShaderiv, "glGetShaderiv"); - LOAD(p_glGetShaderInfoLog, "glGetShaderInfoLog"); LOAD(p_glDeleteShader, "glDeleteShader"); - LOAD(p_glCreateProgram, "glCreateProgram"); LOAD(p_glAttachShader, "glAttachShader"); - LOAD(p_glLinkProgram, "glLinkProgram"); LOAD(p_glGetProgramiv, "glGetProgramiv"); - LOAD(p_glGetProgramInfoLog, "glGetProgramInfoLog"); LOAD(p_glUseProgram, "glUseProgram"); - LOAD(p_glGetUniformLocation, "glGetUniformLocation"); LOAD(p_glUniform1i, "glUniform1i"); - LOAD(p_glUniform1f, "glUniform1f"); - LOAD(p_glUniform2i, "glUniform2i"); LOAD(p_glUniform4i, "glUniform4i"); +static PFN_glUniform4f p_glUniform4f; +static PFN_glBlendColor p_glBlendColor; +static PFN_glBlendFuncSeparate p_glBlendFuncSeparate; +static PFN_glBlendEquationSeparate p_glBlendEquationSeparate; +static PFN_glGenVertexArrays p_glGenVertexArrays; +static PFN_glBindVertexArray p_glBindVertexArray; +static PFN_glActiveTexture p_glActiveTexture; +static PFN_glGenBuffers p_glGenBuffers; +static PFN_glBindBuffer p_glBindBuffer; +static PFN_glBufferData p_glBufferData; +static PFN_glVertexAttribPointer p_glVertexAttribPointer; +static PFN_glEnableVertexAttribArray p_glEnableVertexAttribArray; +static PFN_glBindFragDataLocationIndexed p_glBindFragDataLocationIndexed; +static PFN_glGenFramebuffers p_glGenFramebuffers; +static PFN_glDeleteFramebuffers p_glDeleteFramebuffers; +static PFN_glBindFramebuffer p_glBindFramebuffer; +static PFN_glFramebufferTexture2D p_glFramebufferTexture2D; +static PFN_glCheckFramebufferStatus p_glCheckFramebufferStatus; +static PFN_glBlitFramebuffer p_glBlitFramebuffer; +static PFN_glGenQueries p_glGenQueries; +static PFN_glDeleteQueries p_glDeleteQueries; +static PFN_glBeginQuery p_glBeginQuery; +static PFN_glEndQuery p_glEndQuery; +static PFN_glGetQueryObjectui64v p_glGetQueryObjectui64v; +static PFN_glGetQueryObjectiv p_glGetQueryObjectiv; +static PFN_glQueryCounter p_glQueryCounter; +static void gl_perf_init(void); /* frame_perf — defined below, called from init_gpu_raster */ +static void gl_perf_mirror_begin(void); /* frame_perf: GPU-time bracket around ONE native-wide */ +static void gl_perf_mirror_end(void); /* mirror pass (timestamp pair; splits scene canonical/mirror) */ +/* Native-wide mirror ABLATION (perf attribution, debug cmd gl_ws_ablate): + * 0 = normal, 1 = skip the whole mirror pass, 2 = full mirror state churn but no + * draw calls, 3 = mirror draws land in the hr FBO (no per-pass FBO rebind; wide + * margins go stale + hr gets garbage — perf probe only). */ +static int s_ws_ablate = 0; +static void flush_tex_batch(void); /* textured-prim batch — defined below, flushed from coherency points */ +static void flush_flat_batch(void); /* flat/gouraud GEO batch (MotK starfield 0x68 dots) */ +static PFN_glGenRenderbuffers p_glGenRenderbuffers; +static PFN_glDeleteRenderbuffers p_glDeleteRenderbuffers; +static PFN_glBindRenderbuffer p_glBindRenderbuffer; +static PFN_glRenderbufferStorage p_glRenderbufferStorage; +static PFN_glFramebufferRenderbuffer p_glFramebufferRenderbuffer; + +static int load_modern_gl(void) { + int ok = 1; +#define LOAD(p, n) do { p = (void *)SDL_GL_GetProcAddress(n); if (!p) ok = 0; } while (0) + LOAD(p_glCreateShader, "glCreateShader"); LOAD(p_glShaderSource, "glShaderSource"); + LOAD(p_glCompileShader, "glCompileShader"); LOAD(p_glGetShaderiv, "glGetShaderiv"); + LOAD(p_glGetShaderInfoLog, "glGetShaderInfoLog"); LOAD(p_glDeleteShader, "glDeleteShader"); + LOAD(p_glCreateProgram, "glCreateProgram"); LOAD(p_glAttachShader, "glAttachShader"); + LOAD(p_glLinkProgram, "glLinkProgram"); LOAD(p_glGetProgramiv, "glGetProgramiv"); + LOAD(p_glGetProgramInfoLog, "glGetProgramInfoLog"); LOAD(p_glUseProgram, "glUseProgram"); + LOAD(p_glGetUniformLocation, "glGetUniformLocation"); LOAD(p_glUniform1i, "glUniform1i"); + LOAD(p_glUniform1f, "glUniform1f"); + LOAD(p_glUniform2i, "glUniform2i"); LOAD(p_glUniform4i, "glUniform4i"); LOAD(p_glUniform2f, "glUniform2f"); - LOAD(p_glUniform4f, "glUniform4f"); - LOAD(p_glBlendColor, "glBlendColor"); - LOAD(p_glBlendFuncSeparate, "glBlendFuncSeparate"); - LOAD(p_glBlendEquationSeparate, "glBlendEquationSeparate"); - LOAD(p_glGenVertexArrays, "glGenVertexArrays"); LOAD(p_glBindVertexArray, "glBindVertexArray"); - LOAD(p_glActiveTexture, "glActiveTexture"); LOAD(p_glGenBuffers, "glGenBuffers"); - LOAD(p_glBindBuffer, "glBindBuffer"); LOAD(p_glBufferData, "glBufferData"); - LOAD(p_glVertexAttribPointer, "glVertexAttribPointer"); - LOAD(p_glEnableVertexAttribArray, "glEnableVertexAttribArray"); - LOAD(p_glBindFragDataLocationIndexed, "glBindFragDataLocationIndexed"); - LOAD(p_glGenFramebuffers, "glGenFramebuffers"); LOAD(p_glBindFramebuffer, "glBindFramebuffer"); - LOAD(p_glDeleteFramebuffers, "glDeleteFramebuffers"); - LOAD(p_glFramebufferTexture2D, "glFramebufferTexture2D"); - LOAD(p_glCheckFramebufferStatus, "glCheckFramebufferStatus"); - LOAD(p_glBlitFramebuffer, "glBlitFramebuffer"); - LOAD(p_glGenRenderbuffers, "glGenRenderbuffers"); - LOAD(p_glDeleteRenderbuffers, "glDeleteRenderbuffers"); - LOAD(p_glBindRenderbuffer, "glBindRenderbuffer"); - LOAD(p_glRenderbufferStorage, "glRenderbufferStorage"); - LOAD(p_glFramebufferRenderbuffer, "glFramebufferRenderbuffer"); - /* GPU timer queries — optional (frame_perf). Don't fail the renderer if - * absent; gl_perf just stays disabled. */ - p_glGenQueries = (void *)SDL_GL_GetProcAddress("glGenQueries"); - p_glDeleteQueries = (void *)SDL_GL_GetProcAddress("glDeleteQueries"); - p_glBeginQuery = (void *)SDL_GL_GetProcAddress("glBeginQuery"); - p_glEndQuery = (void *)SDL_GL_GetProcAddress("glEndQuery"); - p_glGetQueryObjectui64v = (void *)SDL_GL_GetProcAddress("glGetQueryObjectui64v"); - p_glGetQueryObjectiv = (void *)SDL_GL_GetProcAddress("glGetQueryObjectiv"); - p_glQueryCounter = (void *)SDL_GL_GetProcAddress("glQueryCounter"); -#undef LOAD - return ok; -} - -/* ---- state ------------------------------------------------------------- */ -static SDL_Window *s_win = NULL; -static SDL_GLContext s_ctx = NULL; -static uint16_t *s_vram = NULL; /* CPU VRAM array (gpu.c's storage) */ -static int s_swap_interval = 1; /* SDL_GL swap interval (vsync mode) */ - -static int s_scale = 1; /* internal-res scale (hr FBO) */ -/* Netplay: SW@1× + GPU@s_scale dual write; CPU VRAM always authoritative. */ -static int s_cpu_auth_dual = 0; -static int s_req_scale = 1; /* requested before context init */ - -static GLuint s_present_tex = 0; /* CPU-readout present path (24bpp) */ -static int s_present_w = 0, s_present_h = 0; -static GLuint s_osd_tex = 0; -static int s_osd_tw = 0, s_osd_th = 0; -static GLuint s_present_prog = 0, s_present_vao = 0; -static void gl_swap_with_osd(void); -static GLint s_present_uTex = -1, s_present_uUvRect = -1; + LOAD(p_glUniform4f, "glUniform4f"); + LOAD(p_glBlendColor, "glBlendColor"); + LOAD(p_glBlendFuncSeparate, "glBlendFuncSeparate"); + LOAD(p_glBlendEquationSeparate, "glBlendEquationSeparate"); + LOAD(p_glGenVertexArrays, "glGenVertexArrays"); LOAD(p_glBindVertexArray, "glBindVertexArray"); + LOAD(p_glActiveTexture, "glActiveTexture"); LOAD(p_glGenBuffers, "glGenBuffers"); + LOAD(p_glBindBuffer, "glBindBuffer"); LOAD(p_glBufferData, "glBufferData"); + LOAD(p_glVertexAttribPointer, "glVertexAttribPointer"); + LOAD(p_glEnableVertexAttribArray, "glEnableVertexAttribArray"); + LOAD(p_glBindFragDataLocationIndexed, "glBindFragDataLocationIndexed"); + LOAD(p_glGenFramebuffers, "glGenFramebuffers"); LOAD(p_glBindFramebuffer, "glBindFramebuffer"); + LOAD(p_glDeleteFramebuffers, "glDeleteFramebuffers"); + LOAD(p_glFramebufferTexture2D, "glFramebufferTexture2D"); + LOAD(p_glCheckFramebufferStatus, "glCheckFramebufferStatus"); + LOAD(p_glBlitFramebuffer, "glBlitFramebuffer"); + LOAD(p_glGenRenderbuffers, "glGenRenderbuffers"); + LOAD(p_glDeleteRenderbuffers, "glDeleteRenderbuffers"); + LOAD(p_glBindRenderbuffer, "glBindRenderbuffer"); + LOAD(p_glRenderbufferStorage, "glRenderbufferStorage"); + LOAD(p_glFramebufferRenderbuffer, "glFramebufferRenderbuffer"); + /* GPU timer queries — optional (frame_perf). Don't fail the renderer if + * absent; gl_perf just stays disabled. */ + p_glGenQueries = (void *)SDL_GL_GetProcAddress("glGenQueries"); + p_glDeleteQueries = (void *)SDL_GL_GetProcAddress("glDeleteQueries"); + p_glBeginQuery = (void *)SDL_GL_GetProcAddress("glBeginQuery"); + p_glEndQuery = (void *)SDL_GL_GetProcAddress("glEndQuery"); + p_glGetQueryObjectui64v = (void *)SDL_GL_GetProcAddress("glGetQueryObjectui64v"); + p_glGetQueryObjectiv = (void *)SDL_GL_GetProcAddress("glGetQueryObjectiv"); + p_glQueryCounter = (void *)SDL_GL_GetProcAddress("glQueryCounter"); +#undef LOAD + return ok; +} + +/* ---- state ------------------------------------------------------------- */ +static SDL_Window *s_win = NULL; +static SDL_GLContext s_ctx = NULL; +static uint16_t *s_vram = NULL; /* CPU VRAM array (gpu.c's storage) */ +static int s_swap_interval = 1; /* SDL_GL swap interval (vsync mode) */ + +static int s_scale = 1; /* internal-res scale (hr FBO) */ +/* Netplay: SW@1× + GPU@s_scale dual write; CPU VRAM always authoritative. */ +static int s_cpu_auth_dual = 0; +static int s_req_scale = 1; /* requested before context init */ + +static GLuint s_present_tex = 0; /* CPU-readout present path (24bpp) */ +static int s_present_w = 0, s_present_h = 0; +static GLuint s_osd_tex = 0; +static int s_osd_tw = 0, s_osd_th = 0; +static GLuint s_present_prog = 0, s_present_vao = 0; +static void gl_swap_with_osd(void); +static GLint s_present_uTex = -1, s_present_uUvRect = -1; static GLint s_present_uTexSize = -1, s_present_uSharpScale = -1; static GLint s_present_uSharp = -1; +/* Scanline post-process state (host display setting; see gl_renderer_set_scanlines). + * s_scanline_on/strength are pushed to whichever program draws game content; + * OSD/bezel and the already-composed hold-last re-present force it off so the + * effect is applied exactly once, at native-line pitch. */ +static int s_scanline_on = 0; +static float s_scanline_strength = 0.5f; +static GLint s_present_uScanline = -1, s_present_uScanStrength = -1; +static GLint s_present_uScanLines = -1, s_present_uScanScale = -1; static GLuint s_interp_prog = 0, s_interp_tex[3]; -static GLint s_interp_uPrev = -1, s_interp_uCurr = -1; -static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1; -static GLint s_interp_uBlendMode = -1; -static int s_interp_enabled = 0, s_interp_valid = 0; -static int s_interp_suspended = 0; -static int s_interp_blend_mode = 0; -static int s_interp_prev = 0, s_interp_cur = 0; -static int s_interp_w = 0, s_interp_h = 0, s_interp_linear = 0; -static int s_interp_force_4_3 = 0, s_interp_source_path = -1; +static GLint s_interp_uPrev = -1, s_interp_uCurr = -1; +static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1; +static GLint s_interp_uBlendMode = -1; +static GLint s_interp_uScanline = -1, s_interp_uScanStrength = -1; +static GLint s_interp_uScanLines = -1, s_interp_uScanScale = -1; +static int s_interp_enabled = 0, s_interp_valid = 0; +static int s_interp_suspended = 0; +static int s_interp_blend_mode = 0; +static int s_interp_prev = 0, s_interp_cur = 0; +static int s_interp_w = 0, s_interp_h = 0, s_interp_linear = 0; +static int s_interp_force_4_3 = 0, s_interp_source_path = -1; static uint64_t s_interp_swaps = 0; static uint64_t s_interp_captures = 0; static int s_interp_diag = 0; @@ -341,523 +351,523 @@ static int interp_present(float alpha); static void interp_present_source_interval(void); static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh); static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh); - -static int s_raster_ok = 0; /* full GPU pipeline available */ - -/* Authoritative VRAM: hr color texture + stencil (mask bit) FBO. */ -static GLuint s_hr_tex = 0, s_hr_fbo = 0, s_hr_rb = 0; -/* Native raw-1555 sampling mirror + readback source. */ -static GLuint s_raw_tex = 0, s_raw_fbo = 0; -/* CPU->VRAM upload staging (native RGBA8). */ -static GLuint s_up_tex = 0; -/* copy_rect staging (hr-sized RGBA8). */ -static GLuint s_scratch_tex = 0, s_scratch_fbo = 0; - -/* Programs. */ -static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0; -static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0; -/* Textured vertex: pos(2) uv(2) col(4) tpage(2) clut(2) depth(1) raw(1) limits(4) - * semi(1) q(1) - * — per-prim texture state in flat attributes so prims batch (see flush_tex_batch). - * q is the perspective weight ([video] perspective_texturing): 0 = affine, the - * PS1-faithful default, which makes the vertex shader's w exactly 1.0 and the - * fragment shader read the noperspective varying — i.e. bit-identical to the - * pre-feature pipeline. */ -#define TEXV 20 -static GLuint s_blit_prog = 0, s_blit_vao = 0, s_blit_vbo = 0; -static GLuint s_pack_prog = 0, s_stencil_prog = 0, s_empty_vao = 0; - -/* Sub-pixel vertex override + perspective UV weights for the next triangle - * ([video] geometry_correction / perspective_texturing; see gpu_render.h). - * gpu.c sets these immediately before the matching gr_draw_*_triangle and they - * are consumed by it. All-zero == the faithful integer/affine path. */ -static int s_pc_valid = 0; /* sub-pixel positions present */ -static float s_pc_x[3], s_pc_y[3]; /* native VRAM px, fractional */ -static int s_pq_valid = 0; /* perspective weights present */ -static float s_pq[3]; - -/* TEX program uniforms. */ -static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1; -static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1; -static GLint s_uTwin = -1, s_uMaskset = -1, s_uFilter = -1; -static GLint s_uLimits = -1; -/* Native-wide x-projection uniforms (per program). u_xoff = x translation in - * native px (0 canonical), u_xhalf = x clip half-extent in native px (512 - * canonical). When wide is off these stay 0 / 512 so the canonical pass is - * bit-identical to the pre-native-wide projection. */ -static GLint s_geo_uXoff = -1, s_geo_uXhalf = -1; -static GLint s_tex_uXoff = -1, s_tex_uXhalf = -1; -/* Native-wide 2D-backdrop x-stretch uniforms. The far 2D backdrop layer is a - * ~4:3-width set of static prims scrolled by the draw offset; native-wide widens - * 3D via the GTE but never these, so they leave a void in the 16:9 margins. The - * wide mirror scales them about screen centre (u_xscale, u_xcenter) to fill it. - * Applied only to "backdrop-phase" prims (drawn before the first clearly-wide - * prim each frame). 1.0 / 0.0 => no-op, so the canonical pass stays identical. */ -static GLint s_geo_uXscale = -1, s_geo_uXcenter = -1; -static GLint s_tex_uXscale = -1, s_tex_uXcenter = -1; -/* Runtime controls (ws_backdrop_stretch debug command). */ -int g_ws_bd_stretch_on = 1; /* feature on (gated by native-wide + per-prim gate) */ -int g_ws_bd_stretch_pct = 0; /* 0 = auto (g_wide_w/native_w); else pct/100 */ -int g_ws_bd_phase_thresh = 24; /* "narrow" margin (px): a prim overhanging the 4:3 - * region by more than this is treated as already- - * widened GTE geometry and NOT stretched */ -int g_ws_bd_phase_mode = 1; /* (retained for the debug command; unused since the - * gate is now per-prim !tagged && narrow) */ -/* Per-prim 2D-backdrop gate (replaces the old draw-order phase): a prim is - * stretched iff native-wide + feature on + NOT sprite-tagged (foreground chars/ - * HUD are tagged) + NARROW (the GTE far-parallax/3D extend into the margins). - * s_bd_gate is what wide_set_bd_scale reads; s_tb_gate is the open textured - * batch's gate (batch flushes when a prim's gate differs, so a batch is uniform). */ -static int s_bd_gate = 0; -static int s_tb_gate = 0; -/* ws_backdrop_stretch diagnostics: per-frame snapshot reported by the command. */ -int g_bdg_applied = 0, g_bdg_prims = 0, g_bdg_clearx = -999999; -int g_bdg_cur = 0, g_bdg_base = 0, g_bdg_w = 0, g_bdg_off = 0; -static int s_bdg_applied = 0, s_bdg_prims = 0, s_bdg_clearx = -999999; -/* per-prim draw-order trace (ws_backdrop_trace): x-extent + textured flag, in - * draw order, for the last frame -- so we can SEE where the background sits. */ -typedef struct { short x0, x1, y0, y1; unsigned char tex; } PrimRec; -#define PTRACE_CAP 200 -static PrimRec s_ptrace[PTRACE_CAP]; static int s_ptrace_n = 0; -PrimRec g_ptrace[PTRACE_CAP]; int g_ptrace_n = 0; /* snapshot (extern) */ -/* BLIT program uniforms. */ -static GLint s_uBlitSrc = -1, s_uBlitPass = -1, s_uBlitMaskset = -1; -static GLint s_uBlitSrcDiv = -1, s_uBlitSrcOff = -1; -/* PACK program uniforms. */ -static GLint s_uPackHr = -1, s_uPackScale = -1; -static GLint s_uStencilSrc = -1; - -static uint32_t *s_conv = NULL; /* RGBA8 staging for uploads */ -static int s_gpu_dirty = 0; /* CPU VRAM array may be stale */ - -/* Dirty-rect unions, native VRAM coords, inclusive bounds. */ -typedef struct { int x0, y0, x1, y1, set; } DirtyRect; -static DirtyRect s_pack_dirty; /* hr FBO content not in raw mirror */ - -/* CPU writes not yet in the FBO — an EXACT rect list, NOT a single union. - * - * THE FLICKER CLASS BUG (MMX6 GL black-frame flicker, ISSUES.md #7): the old - * single-union s_up_pending merged DISJOINT uploads (e.g. a sprite column at - * x>=320 and a tile row at y>=480) into one bounding box that covered the - * framebuffers in between; flush_cpu_upload then painted that whole box from - * the CPU VRAM array — which is STALE under GL (the FBO is authoritative, - * ensure_cpu only syncs on demand) — stomping freshly-rendered framebuffer - * content with stale (typically black) pixels for 1-2 presents until the game - * redrew each buffer. Fix: track the exact uploaded rects and flush each one; - * only pixels the CPU actually wrote are ever painted. Merging is allowed only - * when it adds NO uncovered pixels (containment / same-band extension). On - * overflow the pending set is flushed and the new rect starts a fresh list — - * order-preserving and still batched for the common poke patterns. - * (Pre-context, s_raster_ok == 0, the CPU array is fully authoritative — the - * software rasterizer mirrors every draw — so union-merging is harmless and - * used as the overflow strategy there.) */ -#define UP_RECTS_MAX 16 -static DirtyRect s_up_rects[UP_RECTS_MAX]; -static int s_up_nrects = 0; -static uint64_t s_rt_up_diag[6]; - -static int runtime_upload_diag_enabled(void) { - static int enabled = -1; - if (enabled < 0) { - const char *e = getenv("PSX_RUNTIME_PERF_DIAG"); - enabled = e && e[0] && e[0] != '0'; - } - return enabled; -} - -void gl_renderer_runtime_diag(uint64_t out[6]) { - for (int i = 0; i < 6; i++) out[i] = s_rt_up_diag[i]; -} - -/* Draw state mirrored from the vtable set_* calls. */ -static int s_off_x = 0, s_off_y = 0; -static int s_area_x1 = 0, s_area_y1 = 0, s_area_x2 = VRAM_W - 1, s_area_y2 = VRAM_H - 1; -static int s_semi_en = 0, s_semi_mode = 0; -static int s_mod_r = 128, s_mod_g = 128, s_mod_b = 128, s_mod_raw = 0; -static int s_mask_set = 0, s_mask_check = 0; -static int s_tw_mask_x = 0, s_tw_mask_y = 0, s_tw_off_x = 0, s_tw_off_y = 0; -static int s_tex_filter = 0; -/* Opaque textured draws carry the exact mask bit in FBO alpha. Keeping the - * duplicate stencil copy current is deferred until mask checking is requested. */ -static int s_stencil_valid = 1; - -/* ---- native-wide compositor (mirrors gpu_sw_renderer.c g_wide_*) ---------- - * Canonical VRAM (the hr FBO) stays 100% faithful. Native-wide lives in - * SEPARATE wide FBOs keyed by framebuffer base_x; each framebuffer-targeting - * primitive is mirrored into the active wide surface at local x = vram_x - - * base_x + OFFSET. Present reads the displayed buffer's wide surface. Textures - * always sample canonical VRAM (s_raw_tex). 4:3 / non-opted games never call - * wide_configure, so g_wide_cur stays 0 and the wide pass never runs. */ -#define WIDE_MAX_SURF 4 -static GLuint s_wide_tex[WIDE_MAX_SURF]; /* color tex per surface (0 = free) */ -static GLuint s_wide_fbo[WIDE_MAX_SURF]; /* FBO per surface */ -static GLuint s_wide_rb[WIDE_MAX_SURF]; /* depth-stencil RB per surface (mask - * mirror, same as hr). PERF-CRITICAL: - * a stencil-less wide FBO made every - * stencil-enabled mirror draw cost - * ~0.6ms of driver work (Tomba2 16:9 - * collapsed to 12fps); with the RB - * attached the pass costs ~2us. */ -static int s_wide_base[WIDE_MAX_SURF]; /* base_x per surface (-1 = free) */ -static int g_wide_w = 0; /* wide width (native px); 0 = disabled */ -static int g_wide_off = 0; /* centering OFFSET (native px) */ -static GLuint g_wide_cur = 0; /* active mirror FBO (0 = no mirror) */ -static int g_wide_cur_base = 0; /* base_x of g_wide_cur */ -/* Set by gpu_flat_rect for the full-screen-overlay case so the generic - * gpu_geometry wide mirror is skipped and the flat path emits its own - * full-wide-width pass instead (mirrors sw_draw_flat_rect). */ -static int s_wide_suppress = 0; - -/* X-translation (native px) from canonical VRAM space into the active wide - * surface: local_x = vram_x - base_x + OFFSET. Same as SW wide_dx(). */ -static inline int wide_dx(void) { return g_wide_off - g_wide_cur_base; } - -/* ---- dirty-rect helpers ------------------------------------------------- */ -static void rect_clear(DirtyRect *r) { r->set = 0; } -static void rect_add(DirtyRect *r, int x0, int y0, int x1, int y1) { - if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; - if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; - if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; - if (x0 > x1 || y0 > y1) return; - if (!r->set) { r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; r->set = 1; return; } - if (x0 < r->x0) r->x0 = x0; - if (y0 < r->y0) r->y0 = y0; - if (x1 > r->x1) r->x1 = x1; - if (y1 > r->y1) r->y1 = y1; -} -static int rect_intersects(const DirtyRect *r, int x0, int y0, int x1, int y1) { - if (!r->set) return 0; - return !(x1 < r->x0 || x0 > r->x1 || y1 < r->y0 || y0 > r->y1); -} - -/* ---- exact pending-upload rect list (see s_up_rects comment) ------------- */ -static void flush_cpu_upload(void); /* fwd: overflow flushes then re-adds */ - -/* Add an uploaded rect. Merges ONLY when the merge adds no uncovered pixels: - * containment either way, or an extension within the same row-band / column- - * band (equal y-range with touching/overlapping x-ranges, or equal x-range - * with touching/overlapping y-ranges — the row-scan / column-scan poke - * patterns). Never unions disjoint rects post-init (that was the flicker bug). */ -static void up_add(int x0, int y0, int x1, int y1) { - if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; - if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; - if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; - if (x0 > x1 || y0 > y1) return; - for (int i = 0; i < s_up_nrects; i++) { - DirtyRect *r = &s_up_rects[i]; - if (x0 >= r->x0 && x1 <= r->x1 && y0 >= r->y0 && y1 <= r->y1) - return; /* contained */ - if (x0 <= r->x0 && x1 >= r->x1 && y0 <= r->y0 && y1 >= r->y1) { - r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; /* contains */ - return; - } - if (y0 == r->y0 && y1 == r->y1 && - x0 <= r->x1 + 1 && x1 >= r->x0 - 1) { /* same row-band extension */ - if (x0 < r->x0) r->x0 = x0; - if (x1 > r->x1) r->x1 = x1; - return; - } - if (x0 == r->x0 && x1 == r->x1 && - y0 <= r->y1 + 1 && y1 >= r->y0 - 1) { /* same column-band extension */ - if (y0 < r->y0) r->y0 = y0; - if (y1 > r->y1) r->y1 = y1; - return; - } - } - if (s_up_nrects >= UP_RECTS_MAX) { - if (s_raster_ok) { - flush_cpu_upload(); /* order-preserving: land the old ones */ - } else { - /* Pre-context: the CPU array is fully authoritative (software - * rasterizer mirrors every op), so a union is harmless. */ - DirtyRect *r = &s_up_rects[0]; - for (int i = 1; i < s_up_nrects; i++) { - if (s_up_rects[i].x0 < r->x0) r->x0 = s_up_rects[i].x0; - if (s_up_rects[i].y0 < r->y0) r->y0 = s_up_rects[i].y0; - if (s_up_rects[i].x1 > r->x1) r->x1 = s_up_rects[i].x1; - if (s_up_rects[i].y1 > r->y1) r->y1 = s_up_rects[i].y1; - } - if (x0 < r->x0) r->x0 = x0; - if (y0 < r->y0) r->y0 = y0; - if (x1 > r->x1) r->x1 = x1; - if (y1 > r->y1) r->y1 = y1; - s_up_nrects = 1; - return; - } - } - DirtyRect *r = &s_up_rects[s_up_nrects++]; - r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; r->set = 1; -} - -/* Add a GP0(A0) transfer's exact touched region. The software reference wraps - * per pixel (px = (x+col) & 1023, py = (y+row) & 511), so a wrapping transfer - * touches up to four exact rects — NOT all of VRAM (the old "wrapped: take - * all" full-VRAM union painted stale CPU content over the framebuffers). */ -static void up_add_transfer(int x, int y, int w, int h) { - x &= VRAM_W - 1; y &= VRAM_H - 1; - if (w > VRAM_W) w = VRAM_W; - if (h > VRAM_H) h = VRAM_H; - if (w <= 0 || h <= 0) return; - int w1 = w, w2 = 0, h1 = h, h2 = 0; - if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; } - if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; } - up_add(x, y, x + w1 - 1, y + h1 - 1); - if (w2) up_add(0, y, w2 - 1, y + h1 - 1); - if (h2) up_add(x, 0, x + w1 - 1, h2 - 1); - if (w2 && h2) up_add(0, 0, w2 - 1, h2 - 1); -} - -/* ---- coherency event ring (always-on, debug server "gl_coh_ring") -------- */ -/* Every coherency-relevant operation — upload flushes, fills, copies, draw - * bboxes, packs, full readbacks, presents, and probe perturbations — is - * recorded with its rect and frame number. Per CLAUDE.md ring-buffer rule: - * capture is continuous, observers query a window after the fact. Trigger - * attribution convention: an op that flushes internally (fill/copy/draw/ - * present/peek) records its own event AFTER the FLUSH event it caused, so - * the event following a FLUSH names the trigger. 16 B * 64 Ki = 1 MB. */ -extern uint64_t s_frame_count; /* defined in debug_server.c */ - -#define GL_COH_RING_CAP (1u << 16) -static GlCohEvent s_coh_ring[GL_COH_RING_CAP]; -static uint64_t s_coh_seq = 0; - -/* 16x16 native-pixel tiles changed since their last on-screen present. This - * lets a double-buffered 30 Hz game avoid swapping the unchanged front buffer - * on the intervening 60 Hz vblank without guessing from game identity. - * - * INVARIANT: eliding a swap also elides whatever that swap was blocking on. - * The frontend calls present once per guest vblank, so when the driver's swap - * block owns the guest cadence (psx_present_vsync_owns_cadence(), the XOR - * partner of the wall-clock pacer) an elided frame is an unthrottled frame: - * a 30 Hz-presenting game then advances two vblanks per block and runs at 2x. - * Every skip site below is therefore gated on that query. */ -#define PRES_TILE 16 -#define PRES_ROWS (VRAM_H / PRES_TILE) -static uint64_t s_present_dirty[PRES_ROWS]; -static int s_last_present_path = -1; -static int s_last_dx, s_last_dy, s_last_dw, s_last_dh; -/* After savestate restore: keep swapping for a few presents even when the - * display rect is byte-identical to the last swap. Double/triple-buffered - * windows otherwise can leave a stale back buffer on screen while vblanks - * (and FPS) keep advancing — especially on a 2nd+ load of the same slot. */ -static int s_force_present_remaining = 0; - -/* §33 rollback resim hold-last: sticky copy of the last Live present so - * mid-resim ticks can Swap a frozen frame without reading mutating VRAM. - * HOLD_DRAWABLE = full backbuffer copy taken just before SwapWindow. - * HOLD_NATIVE = GPU blit of a display band (used when interp owns Swap). */ -#define HOLD_NONE 0 -#define HOLD_DRAWABLE 1 -#define HOLD_NATIVE 2 -static int s_hold_kind = HOLD_NONE; -static GLuint s_hold_tex = 0; -static GLuint s_hold_fbo = 0; -static int s_hold_tw = 0, s_hold_th = 0; /* texture size in texels */ -static int s_hold_force_4_3 = 0; -static int s_hold_linear = 0; - -/* Post-load freeze probe (main.cpp): accumulate skip/swap/dirty marks. */ -static uint64_t s_probe_skip = 0; -static uint64_t s_probe_swap = 0; -static uint64_t s_probe_dirty_marks = 0; - -static void present_dirty_rect(int x0, int y0, int x1, int y1, int set) { - if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; - if (x1 >= VRAM_W) x1 = VRAM_W - 1; if (y1 >= VRAM_H) y1 = VRAM_H - 1; - if (x0 > x1 || y0 > y1) return; - int tx0 = x0 / PRES_TILE, tx1 = x1 / PRES_TILE; - uint64_t mask = (~0ull << tx0) & (~0ull >> (63 - tx1)); - for (int ty = y0 / PRES_TILE; ty <= y1 / PRES_TILE; ty++) { - if (set) s_present_dirty[ty] |= mask; else s_present_dirty[ty] &= ~mask; - } - if (set) s_probe_dirty_marks++; -} - -static int present_dirty_test(int x0, int y0, int x1, int y1) { - if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; - if (x1 >= VRAM_W) x1 = VRAM_W - 1; if (y1 >= VRAM_H) y1 = VRAM_H - 1; - if (x0 > x1 || y0 > y1) return 0; - int tx0 = x0 / PRES_TILE, tx1 = x1 / PRES_TILE; - uint64_t mask = (~0ull << tx0) & (~0ull >> (63 - tx1)); - for (int ty = y0 / PRES_TILE; ty <= y1 / PRES_TILE; ty++) - if (s_present_dirty[ty] & mask) return 1; - return 0; -} - -static void present_force_consumed(void) { - if (s_force_present_remaining > 0) - s_force_present_remaining--; -} - -static void hold_ensure_tex(int w, int h) { - if (w < 1) w = 1; - if (h < 1) h = 1; - if (!s_hold_tex) { - glGenTextures(1, &s_hold_tex); - if (!s_hold_fbo) - p_glGenFramebuffers(1, &s_hold_fbo); - } - if (s_hold_tw == w && s_hold_th == h) - return; - glBindTexture(GL_TEXTURE_2D, s_hold_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, - NULL); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hold_fbo); - p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, - GL_TEXTURE_2D, s_hold_tex, 0); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - s_hold_tw = w; - s_hold_th = h; -} - -/* Snapshot the just-drawn default backbuffer (letterbox + content) before Swap. - * Hold-last can then redraw this exact image without touching guest VRAM. */ -static void hold_capture_drawable(void) { - int ww = 0, wh = 0; - if (!s_ctx || !s_win) - return; - SDL_GL_GetDrawableSize(s_win, &ww, &wh); - if (ww < 1 || wh < 1) - return; - hold_ensure_tex(ww, wh); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - glBindTexture(GL_TEXTURE_2D, s_hold_tex); - glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, ww, wh); - glBindTexture(GL_TEXTURE_2D, 0); - s_hold_kind = HOLD_DRAWABLE; - s_hold_force_4_3 = 0; - s_hold_linear = 0; -} - -/* Snapshot a native display band from an FBO when the main thread will not - * Swap (interpolation owns the cadence). Scale-aware blit into hold tex. */ -static void hold_capture_native_fbo(GLuint src_fbo, int dx, int dy, int dw, int dh, - int force_4_3, int linear) { - int S = s_scale > 0 ? s_scale : 1; - if (!s_ctx || !src_fbo || dw < 1 || dh < 1) - return; - hold_ensure_tex(dw * S, dh * S); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src_fbo); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_hold_fbo); - glDisable(GL_SCISSOR_TEST); - p_glBlitFramebuffer(dx * S, dy * S, (dx + dw) * S, (dy + dh) * S, - 0, 0, dw * S, dh * S, - GL_COLOR_BUFFER_BIT, GL_NEAREST); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); - s_hold_kind = HOLD_NATIVE; - s_hold_force_4_3 = force_4_3 ? 1 : 0; - s_hold_linear = linear ? 1 : 0; -} - -static void hold_invalidate(void) { - s_hold_kind = HOLD_NONE; -} - -/* Defined after present_target_quad / letterbox helpers. */ -static void letterbox_rect_aspect(int ww, int wh, int num, int den, - int *x, int *y, int *w, int *h); -static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h); -static void present_target_quad(GLuint tex, int tex_w, int tex_h, - int x, int y, int w, int h, int linear, - int lx, int ly, int lw, int lh, int v_flip); - -static void coh_record(int kind, int x0, int y0, int x1, int y1) { - GlCohEvent *e = &s_coh_ring[s_coh_seq % GL_COH_RING_CAP]; - e->frame = (uint32_t)s_frame_count; - e->kind = (uint8_t)kind; - e->x0 = (int16_t)x0; e->y0 = (int16_t)y0; - e->x1 = (int16_t)x1; e->y1 = (int16_t)y1; - s_coh_seq++; - if (kind == GL_COH_FLUSH || kind == GL_COH_FILL || - kind == GL_COH_COPY || kind == GL_COH_DRAW) - present_dirty_rect(x0, y0, x1, y1, 1); -} - -uint64_t gl_renderer_coh_total(void) { return s_coh_seq; } -int gl_renderer_coh_get(uint64_t seq, GlCohEvent *out) { - if (seq >= s_coh_seq) return 0; - if (s_coh_seq - seq > GL_COH_RING_CAP) return 0; /* evicted */ - *out = s_coh_ring[seq % GL_COH_RING_CAP]; - return 1; -} - -/* ---- present ring (always-on, debug server "present_ring") --------------- */ -/* Records every SwapWindow (vram/wide/cpu/blank/interpolated) and its - * source + letterbox rects. PSX_GL_PRESENT_PROBE=1 additionally drains - * glGetError and samples one backbuffer pixel before the swap; that synchronous - * diagnostic is intentionally opt-in. Observers query a window after the fact. */ -#define GL_PRES_RING_CAP 4096 -static GlPresEvent s_pres_ring[GL_PRES_RING_CAP]; -static uint64_t s_pres_seq = 0; - -static void pres_record(int path, int dx, int dy, int w, int h, - int lx, int ly, int lw, int lh) { - /* The ring metadata stays always-on, but pixel probing must not: each - * glReadPixels synchronously drains queued GPU work. Two probes per frame - * were enough to make Tomba 2 miss its frame budget. */ - static int probe_pixels = -1; - if (probe_pixels < 0) { - const char *cfg = getenv("PSX_GL_PRESENT_PROBE"); - probe_pixels = (cfg && cfg[0] == '1') ? 1 : 0; - } - GlPresEvent *e = &s_pres_ring[s_pres_seq % GL_PRES_RING_CAP]; - e->frame = (uint32_t)s_frame_count; - e->t_ms = (uint32_t)SDL_GetTicks(); - e->path = (uint8_t)path; - e->glerr = probe_pixels ? (uint16_t)glGetError() : 0; - e->dx = (int16_t)dx; e->dy = (int16_t)dy; - e->w = (int16_t)w; e->h = (int16_t)h; - e->lx = (int16_t)lx; e->ly = (int16_t)ly; - e->lw = (int16_t)lw; e->lh = (int16_t)lh; - /* Backbuffer sample at the letterbox centre (GL bottom-origin; the rects - * we pass in are already bottom-origin GL window coords). */ - uint8_t px[3] = { 0, 0, 0 }; - if (probe_pixels && lw > 0 && lh > 0) { - glReadBuffer(GL_BACK); - glPixelStorei(GL_PACK_ALIGNMENT, 1); - glReadPixels(lx + lw / 2, ly + lh / 2, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, px); - glPixelStorei(GL_PACK_ALIGNMENT, 4); - } - e->px_r = px[0]; e->px_g = px[1]; e->px_b = px[2]; - /* Blit-source sample: the hr FBO pixel at the display-rect centre. Splits - * "FBO content was black" from "the blit malfunctioned". */ - uint8_t sp[3] = { 0, 0, 0 }; - e->src_valid = 0; - if (probe_pixels && (path == GL_PRES_VRAM) && w > 0 && h > 0 && s_hr_fbo) { - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); - glPixelStorei(GL_PACK_ALIGNMENT, 1); - glReadPixels((dx + w / 2) * s_scale, (dy + h / 2) * s_scale, - 1, 1, GL_RGB, GL_UNSIGNED_BYTE, sp); - glPixelStorei(GL_PACK_ALIGNMENT, 4); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - e->src_valid = 1; - } - e->src_r = sp[0]; e->src_g = sp[1]; e->src_b = sp[2]; - s_pres_seq++; -} - -uint64_t gl_renderer_pres_total(void) { return s_pres_seq; } -int gl_renderer_pres_get(uint64_t seq, GlPresEvent *out) { - if (seq >= s_pres_seq) return 0; - if (s_pres_seq - seq > GL_PRES_RING_CAP) return 0; /* evicted */ - *out = s_pres_ring[seq % GL_PRES_RING_CAP]; - return 1; -} - -/* ---- shaders ------------------------------------------------------------ */ -static const char *PRESENT_VS = - "#version 330\n" - "out vec2 v_uv;\n" - "uniform vec4 u_uv_rect;\n" - "void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n" - " v_uv = vec2(mix(u_uv_rect.x,u_uv_rect.z,p.x),\n" - " mix(u_uv_rect.y,u_uv_rect.w,1.0-p.y));\n" - " gl_Position = vec4(p*2.0-1.0,0.0,1.0); }\n"; + +static int s_raster_ok = 0; /* full GPU pipeline available */ + +/* Authoritative VRAM: hr color texture + stencil (mask bit) FBO. */ +static GLuint s_hr_tex = 0, s_hr_fbo = 0, s_hr_rb = 0; +/* Native raw-1555 sampling mirror + readback source. */ +static GLuint s_raw_tex = 0, s_raw_fbo = 0; +/* CPU->VRAM upload staging (native RGBA8). */ +static GLuint s_up_tex = 0; +/* copy_rect staging (hr-sized RGBA8). */ +static GLuint s_scratch_tex = 0, s_scratch_fbo = 0; + +/* Programs. */ +static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0; +static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0; +/* Textured vertex: pos(2) uv(2) col(4) tpage(2) clut(2) depth(1) raw(1) limits(4) + * semi(1) q(1) + * — per-prim texture state in flat attributes so prims batch (see flush_tex_batch). + * q is the perspective weight ([video] perspective_texturing): 0 = affine, the + * PS1-faithful default, which makes the vertex shader's w exactly 1.0 and the + * fragment shader read the noperspective varying — i.e. bit-identical to the + * pre-feature pipeline. */ +#define TEXV 20 +static GLuint s_blit_prog = 0, s_blit_vao = 0, s_blit_vbo = 0; +static GLuint s_pack_prog = 0, s_stencil_prog = 0, s_empty_vao = 0; + +/* Sub-pixel vertex override + perspective UV weights for the next triangle + * ([video] geometry_correction / perspective_texturing; see gpu_render.h). + * gpu.c sets these immediately before the matching gr_draw_*_triangle and they + * are consumed by it. All-zero == the faithful integer/affine path. */ +static int s_pc_valid = 0; /* sub-pixel positions present */ +static float s_pc_x[3], s_pc_y[3]; /* native VRAM px, fractional */ +static int s_pq_valid = 0; /* perspective weights present */ +static float s_pq[3]; + +/* TEX program uniforms. */ +static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1; +static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1; +static GLint s_uTwin = -1, s_uMaskset = -1, s_uFilter = -1; +static GLint s_uLimits = -1; +/* Native-wide x-projection uniforms (per program). u_xoff = x translation in + * native px (0 canonical), u_xhalf = x clip half-extent in native px (512 + * canonical). When wide is off these stay 0 / 512 so the canonical pass is + * bit-identical to the pre-native-wide projection. */ +static GLint s_geo_uXoff = -1, s_geo_uXhalf = -1; +static GLint s_tex_uXoff = -1, s_tex_uXhalf = -1; +/* Native-wide 2D-backdrop x-stretch uniforms. The far 2D backdrop layer is a + * ~4:3-width set of static prims scrolled by the draw offset; native-wide widens + * 3D via the GTE but never these, so they leave a void in the 16:9 margins. The + * wide mirror scales them about screen centre (u_xscale, u_xcenter) to fill it. + * Applied only to "backdrop-phase" prims (drawn before the first clearly-wide + * prim each frame). 1.0 / 0.0 => no-op, so the canonical pass stays identical. */ +static GLint s_geo_uXscale = -1, s_geo_uXcenter = -1; +static GLint s_tex_uXscale = -1, s_tex_uXcenter = -1; +/* Runtime controls (ws_backdrop_stretch debug command). */ +int g_ws_bd_stretch_on = 1; /* feature on (gated by native-wide + per-prim gate) */ +int g_ws_bd_stretch_pct = 0; /* 0 = auto (g_wide_w/native_w); else pct/100 */ +int g_ws_bd_phase_thresh = 24; /* "narrow" margin (px): a prim overhanging the 4:3 + * region by more than this is treated as already- + * widened GTE geometry and NOT stretched */ +int g_ws_bd_phase_mode = 1; /* (retained for the debug command; unused since the + * gate is now per-prim !tagged && narrow) */ +/* Per-prim 2D-backdrop gate (replaces the old draw-order phase): a prim is + * stretched iff native-wide + feature on + NOT sprite-tagged (foreground chars/ + * HUD are tagged) + NARROW (the GTE far-parallax/3D extend into the margins). + * s_bd_gate is what wide_set_bd_scale reads; s_tb_gate is the open textured + * batch's gate (batch flushes when a prim's gate differs, so a batch is uniform). */ +static int s_bd_gate = 0; +static int s_tb_gate = 0; +/* ws_backdrop_stretch diagnostics: per-frame snapshot reported by the command. */ +int g_bdg_applied = 0, g_bdg_prims = 0, g_bdg_clearx = -999999; +int g_bdg_cur = 0, g_bdg_base = 0, g_bdg_w = 0, g_bdg_off = 0; +static int s_bdg_applied = 0, s_bdg_prims = 0, s_bdg_clearx = -999999; +/* per-prim draw-order trace (ws_backdrop_trace): x-extent + textured flag, in + * draw order, for the last frame -- so we can SEE where the background sits. */ +typedef struct { short x0, x1, y0, y1; unsigned char tex; } PrimRec; +#define PTRACE_CAP 200 +static PrimRec s_ptrace[PTRACE_CAP]; static int s_ptrace_n = 0; +PrimRec g_ptrace[PTRACE_CAP]; int g_ptrace_n = 0; /* snapshot (extern) */ +/* BLIT program uniforms. */ +static GLint s_uBlitSrc = -1, s_uBlitPass = -1, s_uBlitMaskset = -1; +static GLint s_uBlitSrcDiv = -1, s_uBlitSrcOff = -1; +/* PACK program uniforms. */ +static GLint s_uPackHr = -1, s_uPackScale = -1; +static GLint s_uStencilSrc = -1; + +static uint32_t *s_conv = NULL; /* RGBA8 staging for uploads */ +static int s_gpu_dirty = 0; /* CPU VRAM array may be stale */ + +/* Dirty-rect unions, native VRAM coords, inclusive bounds. */ +typedef struct { int x0, y0, x1, y1, set; } DirtyRect; +static DirtyRect s_pack_dirty; /* hr FBO content not in raw mirror */ + +/* CPU writes not yet in the FBO — an EXACT rect list, NOT a single union. + * + * THE FLICKER CLASS BUG (MMX6 GL black-frame flicker, ISSUES.md #7): the old + * single-union s_up_pending merged DISJOINT uploads (e.g. a sprite column at + * x>=320 and a tile row at y>=480) into one bounding box that covered the + * framebuffers in between; flush_cpu_upload then painted that whole box from + * the CPU VRAM array — which is STALE under GL (the FBO is authoritative, + * ensure_cpu only syncs on demand) — stomping freshly-rendered framebuffer + * content with stale (typically black) pixels for 1-2 presents until the game + * redrew each buffer. Fix: track the exact uploaded rects and flush each one; + * only pixels the CPU actually wrote are ever painted. Merging is allowed only + * when it adds NO uncovered pixels (containment / same-band extension). On + * overflow the pending set is flushed and the new rect starts a fresh list — + * order-preserving and still batched for the common poke patterns. + * (Pre-context, s_raster_ok == 0, the CPU array is fully authoritative — the + * software rasterizer mirrors every draw — so union-merging is harmless and + * used as the overflow strategy there.) */ +#define UP_RECTS_MAX 16 +static DirtyRect s_up_rects[UP_RECTS_MAX]; +static int s_up_nrects = 0; +static uint64_t s_rt_up_diag[6]; + +static int runtime_upload_diag_enabled(void) { + static int enabled = -1; + if (enabled < 0) { + const char *e = getenv("PSX_RUNTIME_PERF_DIAG"); + enabled = e && e[0] && e[0] != '0'; + } + return enabled; +} + +void gl_renderer_runtime_diag(uint64_t out[6]) { + for (int i = 0; i < 6; i++) out[i] = s_rt_up_diag[i]; +} + +/* Draw state mirrored from the vtable set_* calls. */ +static int s_off_x = 0, s_off_y = 0; +static int s_area_x1 = 0, s_area_y1 = 0, s_area_x2 = VRAM_W - 1, s_area_y2 = VRAM_H - 1; +static int s_semi_en = 0, s_semi_mode = 0; +static int s_mod_r = 128, s_mod_g = 128, s_mod_b = 128, s_mod_raw = 0; +static int s_mask_set = 0, s_mask_check = 0; +static int s_tw_mask_x = 0, s_tw_mask_y = 0, s_tw_off_x = 0, s_tw_off_y = 0; +static int s_tex_filter = 0; +/* Opaque textured draws carry the exact mask bit in FBO alpha. Keeping the + * duplicate stencil copy current is deferred until mask checking is requested. */ +static int s_stencil_valid = 1; + +/* ---- native-wide compositor (mirrors gpu_sw_renderer.c g_wide_*) ---------- + * Canonical VRAM (the hr FBO) stays 100% faithful. Native-wide lives in + * SEPARATE wide FBOs keyed by framebuffer base_x; each framebuffer-targeting + * primitive is mirrored into the active wide surface at local x = vram_x - + * base_x + OFFSET. Present reads the displayed buffer's wide surface. Textures + * always sample canonical VRAM (s_raw_tex). 4:3 / non-opted games never call + * wide_configure, so g_wide_cur stays 0 and the wide pass never runs. */ +#define WIDE_MAX_SURF 4 +static GLuint s_wide_tex[WIDE_MAX_SURF]; /* color tex per surface (0 = free) */ +static GLuint s_wide_fbo[WIDE_MAX_SURF]; /* FBO per surface */ +static GLuint s_wide_rb[WIDE_MAX_SURF]; /* depth-stencil RB per surface (mask + * mirror, same as hr). PERF-CRITICAL: + * a stencil-less wide FBO made every + * stencil-enabled mirror draw cost + * ~0.6ms of driver work (Tomba2 16:9 + * collapsed to 12fps); with the RB + * attached the pass costs ~2us. */ +static int s_wide_base[WIDE_MAX_SURF]; /* base_x per surface (-1 = free) */ +static int g_wide_w = 0; /* wide width (native px); 0 = disabled */ +static int g_wide_off = 0; /* centering OFFSET (native px) */ +static GLuint g_wide_cur = 0; /* active mirror FBO (0 = no mirror) */ +static int g_wide_cur_base = 0; /* base_x of g_wide_cur */ +/* Set by gpu_flat_rect for the full-screen-overlay case so the generic + * gpu_geometry wide mirror is skipped and the flat path emits its own + * full-wide-width pass instead (mirrors sw_draw_flat_rect). */ +static int s_wide_suppress = 0; + +/* X-translation (native px) from canonical VRAM space into the active wide + * surface: local_x = vram_x - base_x + OFFSET. Same as SW wide_dx(). */ +static inline int wide_dx(void) { return g_wide_off - g_wide_cur_base; } + +/* ---- dirty-rect helpers ------------------------------------------------- */ +static void rect_clear(DirtyRect *r) { r->set = 0; } +static void rect_add(DirtyRect *r, int x0, int y0, int x1, int y1) { + if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; + if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; + if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; + if (x0 > x1 || y0 > y1) return; + if (!r->set) { r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; r->set = 1; return; } + if (x0 < r->x0) r->x0 = x0; + if (y0 < r->y0) r->y0 = y0; + if (x1 > r->x1) r->x1 = x1; + if (y1 > r->y1) r->y1 = y1; +} +static int rect_intersects(const DirtyRect *r, int x0, int y0, int x1, int y1) { + if (!r->set) return 0; + return !(x1 < r->x0 || x0 > r->x1 || y1 < r->y0 || y0 > r->y1); +} + +/* ---- exact pending-upload rect list (see s_up_rects comment) ------------- */ +static void flush_cpu_upload(void); /* fwd: overflow flushes then re-adds */ + +/* Add an uploaded rect. Merges ONLY when the merge adds no uncovered pixels: + * containment either way, or an extension within the same row-band / column- + * band (equal y-range with touching/overlapping x-ranges, or equal x-range + * with touching/overlapping y-ranges — the row-scan / column-scan poke + * patterns). Never unions disjoint rects post-init (that was the flicker bug). */ +static void up_add(int x0, int y0, int x1, int y1) { + if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; + if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; + if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; + if (x0 > x1 || y0 > y1) return; + for (int i = 0; i < s_up_nrects; i++) { + DirtyRect *r = &s_up_rects[i]; + if (x0 >= r->x0 && x1 <= r->x1 && y0 >= r->y0 && y1 <= r->y1) + return; /* contained */ + if (x0 <= r->x0 && x1 >= r->x1 && y0 <= r->y0 && y1 >= r->y1) { + r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; /* contains */ + return; + } + if (y0 == r->y0 && y1 == r->y1 && + x0 <= r->x1 + 1 && x1 >= r->x0 - 1) { /* same row-band extension */ + if (x0 < r->x0) r->x0 = x0; + if (x1 > r->x1) r->x1 = x1; + return; + } + if (x0 == r->x0 && x1 == r->x1 && + y0 <= r->y1 + 1 && y1 >= r->y0 - 1) { /* same column-band extension */ + if (y0 < r->y0) r->y0 = y0; + if (y1 > r->y1) r->y1 = y1; + return; + } + } + if (s_up_nrects >= UP_RECTS_MAX) { + if (s_raster_ok) { + flush_cpu_upload(); /* order-preserving: land the old ones */ + } else { + /* Pre-context: the CPU array is fully authoritative (software + * rasterizer mirrors every op), so a union is harmless. */ + DirtyRect *r = &s_up_rects[0]; + for (int i = 1; i < s_up_nrects; i++) { + if (s_up_rects[i].x0 < r->x0) r->x0 = s_up_rects[i].x0; + if (s_up_rects[i].y0 < r->y0) r->y0 = s_up_rects[i].y0; + if (s_up_rects[i].x1 > r->x1) r->x1 = s_up_rects[i].x1; + if (s_up_rects[i].y1 > r->y1) r->y1 = s_up_rects[i].y1; + } + if (x0 < r->x0) r->x0 = x0; + if (y0 < r->y0) r->y0 = y0; + if (x1 > r->x1) r->x1 = x1; + if (y1 > r->y1) r->y1 = y1; + s_up_nrects = 1; + return; + } + } + DirtyRect *r = &s_up_rects[s_up_nrects++]; + r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; r->set = 1; +} + +/* Add a GP0(A0) transfer's exact touched region. The software reference wraps + * per pixel (px = (x+col) & 1023, py = (y+row) & 511), so a wrapping transfer + * touches up to four exact rects — NOT all of VRAM (the old "wrapped: take + * all" full-VRAM union painted stale CPU content over the framebuffers). */ +static void up_add_transfer(int x, int y, int w, int h) { + x &= VRAM_W - 1; y &= VRAM_H - 1; + if (w > VRAM_W) w = VRAM_W; + if (h > VRAM_H) h = VRAM_H; + if (w <= 0 || h <= 0) return; + int w1 = w, w2 = 0, h1 = h, h2 = 0; + if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; } + if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; } + up_add(x, y, x + w1 - 1, y + h1 - 1); + if (w2) up_add(0, y, w2 - 1, y + h1 - 1); + if (h2) up_add(x, 0, x + w1 - 1, h2 - 1); + if (w2 && h2) up_add(0, 0, w2 - 1, h2 - 1); +} + +/* ---- coherency event ring (always-on, debug server "gl_coh_ring") -------- */ +/* Every coherency-relevant operation — upload flushes, fills, copies, draw + * bboxes, packs, full readbacks, presents, and probe perturbations — is + * recorded with its rect and frame number. Per CLAUDE.md ring-buffer rule: + * capture is continuous, observers query a window after the fact. Trigger + * attribution convention: an op that flushes internally (fill/copy/draw/ + * present/peek) records its own event AFTER the FLUSH event it caused, so + * the event following a FLUSH names the trigger. 16 B * 64 Ki = 1 MB. */ +extern uint64_t s_frame_count; /* defined in debug_server.c */ + +#define GL_COH_RING_CAP (1u << 16) +static GlCohEvent s_coh_ring[GL_COH_RING_CAP]; +static uint64_t s_coh_seq = 0; + +/* 16x16 native-pixel tiles changed since their last on-screen present. This + * lets a double-buffered 30 Hz game avoid swapping the unchanged front buffer + * on the intervening 60 Hz vblank without guessing from game identity. + * + * INVARIANT: eliding a swap also elides whatever that swap was blocking on. + * The frontend calls present once per guest vblank, so when the driver's swap + * block owns the guest cadence (psx_present_vsync_owns_cadence(), the XOR + * partner of the wall-clock pacer) an elided frame is an unthrottled frame: + * a 30 Hz-presenting game then advances two vblanks per block and runs at 2x. + * Every skip site below is therefore gated on that query. */ +#define PRES_TILE 16 +#define PRES_ROWS (VRAM_H / PRES_TILE) +static uint64_t s_present_dirty[PRES_ROWS]; +static int s_last_present_path = -1; +static int s_last_dx, s_last_dy, s_last_dw, s_last_dh; +/* After savestate restore: keep swapping for a few presents even when the + * display rect is byte-identical to the last swap. Double/triple-buffered + * windows otherwise can leave a stale back buffer on screen while vblanks + * (and FPS) keep advancing — especially on a 2nd+ load of the same slot. */ +static int s_force_present_remaining = 0; + +/* §33 rollback resim hold-last: sticky copy of the last Live present so + * mid-resim ticks can Swap a frozen frame without reading mutating VRAM. + * HOLD_DRAWABLE = full backbuffer copy taken just before SwapWindow. + * HOLD_NATIVE = GPU blit of a display band (used when interp owns Swap). */ +#define HOLD_NONE 0 +#define HOLD_DRAWABLE 1 +#define HOLD_NATIVE 2 +static int s_hold_kind = HOLD_NONE; +static GLuint s_hold_tex = 0; +static GLuint s_hold_fbo = 0; +static int s_hold_tw = 0, s_hold_th = 0; /* texture size in texels */ +static int s_hold_force_4_3 = 0; +static int s_hold_linear = 0; + +/* Post-load freeze probe (main.cpp): accumulate skip/swap/dirty marks. */ +static uint64_t s_probe_skip = 0; +static uint64_t s_probe_swap = 0; +static uint64_t s_probe_dirty_marks = 0; + +static void present_dirty_rect(int x0, int y0, int x1, int y1, int set) { + if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; + if (x1 >= VRAM_W) x1 = VRAM_W - 1; if (y1 >= VRAM_H) y1 = VRAM_H - 1; + if (x0 > x1 || y0 > y1) return; + int tx0 = x0 / PRES_TILE, tx1 = x1 / PRES_TILE; + uint64_t mask = (~0ull << tx0) & (~0ull >> (63 - tx1)); + for (int ty = y0 / PRES_TILE; ty <= y1 / PRES_TILE; ty++) { + if (set) s_present_dirty[ty] |= mask; else s_present_dirty[ty] &= ~mask; + } + if (set) s_probe_dirty_marks++; +} + +static int present_dirty_test(int x0, int y0, int x1, int y1) { + if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; + if (x1 >= VRAM_W) x1 = VRAM_W - 1; if (y1 >= VRAM_H) y1 = VRAM_H - 1; + if (x0 > x1 || y0 > y1) return 0; + int tx0 = x0 / PRES_TILE, tx1 = x1 / PRES_TILE; + uint64_t mask = (~0ull << tx0) & (~0ull >> (63 - tx1)); + for (int ty = y0 / PRES_TILE; ty <= y1 / PRES_TILE; ty++) + if (s_present_dirty[ty] & mask) return 1; + return 0; +} + +static void present_force_consumed(void) { + if (s_force_present_remaining > 0) + s_force_present_remaining--; +} + +static void hold_ensure_tex(int w, int h) { + if (w < 1) w = 1; + if (h < 1) h = 1; + if (!s_hold_tex) { + glGenTextures(1, &s_hold_tex); + if (!s_hold_fbo) + p_glGenFramebuffers(1, &s_hold_fbo); + } + if (s_hold_tw == w && s_hold_th == h) + return; + glBindTexture(GL_TEXTURE_2D, s_hold_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, + NULL); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hold_fbo); + p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, s_hold_tex, 0); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + s_hold_tw = w; + s_hold_th = h; +} + +/* Snapshot the just-drawn default backbuffer (letterbox + content) before Swap. + * Hold-last can then redraw this exact image without touching guest VRAM. */ +static void hold_capture_drawable(void) { + int ww = 0, wh = 0; + if (!s_ctx || !s_win) + return; + SDL_GL_GetDrawableSize(s_win, &ww, &wh); + if (ww < 1 || wh < 1) + return; + hold_ensure_tex(ww, wh); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + glBindTexture(GL_TEXTURE_2D, s_hold_tex); + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, ww, wh); + glBindTexture(GL_TEXTURE_2D, 0); + s_hold_kind = HOLD_DRAWABLE; + s_hold_force_4_3 = 0; + s_hold_linear = 0; +} + +/* Snapshot a native display band from an FBO when the main thread will not + * Swap (interpolation owns the cadence). Scale-aware blit into hold tex. */ +static void hold_capture_native_fbo(GLuint src_fbo, int dx, int dy, int dw, int dh, + int force_4_3, int linear) { + int S = s_scale > 0 ? s_scale : 1; + if (!s_ctx || !src_fbo || dw < 1 || dh < 1) + return; + hold_ensure_tex(dw * S, dh * S); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src_fbo); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_hold_fbo); + glDisable(GL_SCISSOR_TEST); + p_glBlitFramebuffer(dx * S, dy * S, (dx + dw) * S, (dy + dh) * S, + 0, 0, dw * S, dh * S, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); + s_hold_kind = HOLD_NATIVE; + s_hold_force_4_3 = force_4_3 ? 1 : 0; + s_hold_linear = linear ? 1 : 0; +} + +static void hold_invalidate(void) { + s_hold_kind = HOLD_NONE; +} + +/* Defined after present_target_quad / letterbox helpers. */ +static void letterbox_rect_aspect(int ww, int wh, int num, int den, + int *x, int *y, int *w, int *h); +static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h); +static void present_target_quad(GLuint tex, int tex_w, int tex_h, + int x, int y, int w, int h, int linear, + int lx, int ly, int lw, int lh, int v_flip); + +static void coh_record(int kind, int x0, int y0, int x1, int y1) { + GlCohEvent *e = &s_coh_ring[s_coh_seq % GL_COH_RING_CAP]; + e->frame = (uint32_t)s_frame_count; + e->kind = (uint8_t)kind; + e->x0 = (int16_t)x0; e->y0 = (int16_t)y0; + e->x1 = (int16_t)x1; e->y1 = (int16_t)y1; + s_coh_seq++; + if (kind == GL_COH_FLUSH || kind == GL_COH_FILL || + kind == GL_COH_COPY || kind == GL_COH_DRAW) + present_dirty_rect(x0, y0, x1, y1, 1); +} + +uint64_t gl_renderer_coh_total(void) { return s_coh_seq; } +int gl_renderer_coh_get(uint64_t seq, GlCohEvent *out) { + if (seq >= s_coh_seq) return 0; + if (s_coh_seq - seq > GL_COH_RING_CAP) return 0; /* evicted */ + *out = s_coh_ring[seq % GL_COH_RING_CAP]; + return 1; +} + +/* ---- present ring (always-on, debug server "present_ring") --------------- */ +/* Records every SwapWindow (vram/wide/cpu/blank/interpolated) and its + * source + letterbox rects. PSX_GL_PRESENT_PROBE=1 additionally drains + * glGetError and samples one backbuffer pixel before the swap; that synchronous + * diagnostic is intentionally opt-in. Observers query a window after the fact. */ +#define GL_PRES_RING_CAP 4096 +static GlPresEvent s_pres_ring[GL_PRES_RING_CAP]; +static uint64_t s_pres_seq = 0; + +static void pres_record(int path, int dx, int dy, int w, int h, + int lx, int ly, int lw, int lh) { + /* The ring metadata stays always-on, but pixel probing must not: each + * glReadPixels synchronously drains queued GPU work. Two probes per frame + * were enough to make Tomba 2 miss its frame budget. */ + static int probe_pixels = -1; + if (probe_pixels < 0) { + const char *cfg = getenv("PSX_GL_PRESENT_PROBE"); + probe_pixels = (cfg && cfg[0] == '1') ? 1 : 0; + } + GlPresEvent *e = &s_pres_ring[s_pres_seq % GL_PRES_RING_CAP]; + e->frame = (uint32_t)s_frame_count; + e->t_ms = (uint32_t)SDL_GetTicks(); + e->path = (uint8_t)path; + e->glerr = probe_pixels ? (uint16_t)glGetError() : 0; + e->dx = (int16_t)dx; e->dy = (int16_t)dy; + e->w = (int16_t)w; e->h = (int16_t)h; + e->lx = (int16_t)lx; e->ly = (int16_t)ly; + e->lw = (int16_t)lw; e->lh = (int16_t)lh; + /* Backbuffer sample at the letterbox centre (GL bottom-origin; the rects + * we pass in are already bottom-origin GL window coords). */ + uint8_t px[3] = { 0, 0, 0 }; + if (probe_pixels && lw > 0 && lh > 0) { + glReadBuffer(GL_BACK); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(lx + lw / 2, ly + lh / 2, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, px); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + } + e->px_r = px[0]; e->px_g = px[1]; e->px_b = px[2]; + /* Blit-source sample: the hr FBO pixel at the display-rect centre. Splits + * "FBO content was black" from "the blit malfunctioned". */ + uint8_t sp[3] = { 0, 0, 0 }; + e->src_valid = 0; + if (probe_pixels && (path == GL_PRES_VRAM) && w > 0 && h > 0 && s_hr_fbo) { + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels((dx + w / 2) * s_scale, (dy + h / 2) * s_scale, + 1, 1, GL_RGB, GL_UNSIGNED_BYTE, sp); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + e->src_valid = 1; + } + e->src_r = sp[0]; e->src_g = sp[1]; e->src_b = sp[2]; + s_pres_seq++; +} + +uint64_t gl_renderer_pres_total(void) { return s_pres_seq; } +int gl_renderer_pres_get(uint64_t seq, GlPresEvent *out) { + if (seq >= s_pres_seq) return 0; + if (s_pres_seq - seq > GL_PRES_RING_CAP) return 0; /* evicted */ + *out = s_pres_ring[seq % GL_PRES_RING_CAP]; + return 1; +} + +/* ---- shaders ------------------------------------------------------------ */ +static const char *PRESENT_VS = + "#version 330\n" + "out vec2 v_uv;\n" + "uniform vec4 u_uv_rect;\n" + "void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n" + " v_uv = vec2(mix(u_uv_rect.x,u_uv_rect.z,p.x),\n" + " mix(u_uv_rect.y,u_uv_rect.w,1.0-p.y));\n" + " gl_Position = vec4(p*2.0-1.0,0.0,1.0); }\n"; /* Present sampling. u_sharp==0 is the historical behaviour: sample straight at * v_uv, so the texture's own filter (NEAREST or LINEAR) decides everything. * @@ -874,13 +884,38 @@ static const char *PRESENT_VS = * there. The result is clamped to u_uv_rect so the half-texel edge inset the * caller applied still holds — that inset is what keeps LINEAR from bleeding * the border texel into the image (the old reason FMV was pinned to NEAREST). */ -static const char *PRESENT_FS = - "#version 330\n" - "in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n" +/* Scanline post-process, shared by the present and interpolation shaders. + * Darkens toward the gap between PS1 scanlines with a soft sinusoidal beam + * centred on each line. The pitch is u_scanline_lines (the number of native + * display lines across the source rect), so it tracks the PS1 line grid, not + * the internal SSAA scale or the window size. u_scanline_scale is output pixels + * per PS1 line; the gate fades the effect in from 1x to 2x so it never shimmers + * on a window too small to resolve one line as two rows (Nyquist). Computed + * against v_uv.y (position across the display region) so it is orientation- and + * scale-agnostic. */ +#define PSX_SCANLINE_UNIFORMS \ + "uniform int u_scanline;\n" \ + "uniform float u_scanline_strength;\n" \ + "uniform float u_scanline_lines;\n" \ + "uniform float u_scanline_scale;\n" +#define PSX_SCANLINE_FUNC \ + "vec3 psx_scanline(vec3 rgb, float vy){\n" \ + " if (u_scanline == 0) return rgb;\n" \ + " float gate = clamp(u_scanline_scale - 1.0, 0.0, 1.0);\n" \ + " if (gate <= 0.0) return rgb;\n" \ + " float p = fract(vy * u_scanline_lines);\n" \ + " float beam = sin(3.14159265 * p);\n" \ + " float mult = 1.0 - (u_scanline_strength * gate) * (1.0 - beam);\n" \ + " return rgb * mult;\n" \ + "}\n" +static const char *PRESENT_FS = + "#version 330\n" + "in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n" "uniform vec4 u_uv_rect;\n" "uniform vec2 u_tex_size;\n" "uniform vec2 u_sharp_scale;\n" "uniform int u_sharp;\n" + PSX_SCANLINE_UNIFORMS /* Catmull-Rom bicubic via 9 bilinear taps. Sharper than plain bilinear at * the same smoothness, with mild overshoot that reads as edge definition. * The present texture holds exactly the source rect and wraps CLAMP_TO_EDGE, @@ -909,1582 +944,1592 @@ static const char *PRESENT_FS = " r += texture(u_tex, vec2(t3.x , t3.y )) * (w3.x * w3.y );\n" " return r;\n" "}\n" + PSX_SCANLINE_FUNC "void main(){\n" " vec2 uv = v_uv;\n" - " if (u_sharp == 2) { frag = bicubic(uv); return; }\n" - " if (u_sharp == 1) {\n" - " vec2 scale = max(u_sharp_scale, vec2(1.0));\n" - " vec2 texel = uv * u_tex_size;\n" - " vec2 tf = floor(texel);\n" - " vec2 cd = (texel - tf) - 0.5;\n" - " vec2 band = 0.5 - 0.5 / scale;\n" - " vec2 f = (cd - clamp(cd, -band, band)) * scale + 0.5;\n" - " uv = (tf + f) / u_tex_size;\n" - " vec2 lo = min(u_uv_rect.xy, u_uv_rect.zw);\n" - " vec2 hi = max(u_uv_rect.xy, u_uv_rect.zw);\n" - " uv = clamp(uv, lo, hi);\n" + " vec4 c;\n" + " if (u_sharp == 2) { c = bicubic(uv); }\n" + " else {\n" + " if (u_sharp == 1) {\n" + " vec2 scale = max(u_sharp_scale, vec2(1.0));\n" + " vec2 texel = uv * u_tex_size;\n" + " vec2 tf = floor(texel);\n" + " vec2 cd = (texel - tf) - 0.5;\n" + " vec2 band = 0.5 - 0.5 / scale;\n" + " vec2 f = (cd - clamp(cd, -band, band)) * scale + 0.5;\n" + " uv = (tf + f) / u_tex_size;\n" + " vec2 lo = min(u_uv_rect.xy, u_uv_rect.zw);\n" + " vec2 hi = max(u_uv_rect.xy, u_uv_rect.zw);\n" + " uv = clamp(uv, lo, hi);\n" + " }\n" + " c = texture(u_tex, uv);\n" " }\n" - " frag = texture(u_tex, uv);\n" + " c.rgb = psx_scanline(c.rgb, v_uv.y);\n" + " frag = c;\n" "}\n"; -static const char *INTERP_FS = - "#version 330\n" - "in vec2 v_uv; uniform sampler2D u_prev; uniform sampler2D u_curr;\n" - "uniform float u_alpha; uniform int u_blend_mode; out vec4 frag;\n" - "void main(){\n" - " vec4 prev=texture(u_prev,v_uv), curr=texture(u_curr,v_uv);\n" - " float alpha=u_alpha;\n" - " if(u_blend_mode==1){\n" - " vec3 d=abs(prev.rgb-curr.rgb);\n" - " float change=max(max(d.r,d.g),d.b);\n" - " float safe_blend=1.0-smoothstep(0.08,0.20,change);\n" - " alpha=mix(step(0.5,u_alpha),u_alpha,safe_blend);\n" - " }\n" - " frag=mix(prev,curr,alpha);\n" - "}\n"; - -/* Geometry: position in VRAM pixels (draw offset already applied by gpu.c), - * color rgb in 0..1, color a = mask bit (0/1). The clip transform is in - * native VRAM space; the viewport at S* the size scales rasterization. - * - * ALL drawn prims shift positions by u_shift = half an HR pixel (0.5/S in - * native units): GL samples coverage/attributes at pixel CENTERS, the PS1 - * DDA at INTEGER coords. The shift aligns GL's sample grid with the PS1 - * grid — without it, any texture mapping with slope != 1 (scaled sprites, - * squished menu fonts) samples one texel off per row/column (striped - * glyphs, seam lines). Half an HR pixel (not half a native pixel!) keeps - * rect coverage exactly [x*S, (x+w)*S) at every scale AND makes the - * top-left subpixel of each S*S block sample the exact PS1 value (which is - * what the PACK pass reads back). */ -static const char *GEO_VS = - "#version 330\n" - "layout(location=0) in vec2 a_pos;\n" - "layout(location=1) in vec4 a_col;\n" - "uniform float u_shift;\n" - "uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n" - "uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n" - "uniform float u_xscale; /* native-wide 2D-backdrop x-stretch; 1 canonical */\n" - "uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n" - "noperspective out vec4 v_col;\n" - "void main(){ v_col = a_col;\n" - " float xb = a_pos.x;\n" - " if (u_xscale < 0.0) {\n" - " float s = -u_xscale; float h = u_xhalf / s;\n" - " float l = u_xcenter - h, r = u_xcenter + h;\n" - " if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n" - " } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n" - " gl_Position = vec4((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n"; -static const char *GEO_FS = - "#version 330\n" - "noperspective in vec4 v_col; out vec4 frag;\n" - "void main(){ frag = v_col; }\n"; - -/* Textured prims: sample raw 1555 VRAM (integer), CLUT decode per depth, - * texture window, optional bilinear, texel-0 discard, STP-split discard, - * PS1 *2-around-0x80 modulation. Output alpha = bit15 of the written pixel. - * Texel coords use floor() to match the software rasterizer's truncation - * (rounding shifted sampling +1 texel half the time: smeared text). */ -/* Textured program. Per-prim texture state (texpage, clut, depth, raw, uv - * limits) is carried in FLAT vertex attributes — constant across a prim's - * vertices — instead of uniforms, so consecutive textured prims with the same - * blend/mask/texture-window state batch into one draw (see flush_tex_batch). - * The remaining uniforms (u_twin/u_maskset/u_filter/u_semipass) are the batch - * keys + per-pass state. */ -static const char *TEX_VS = - "#version 330\n" - "layout(location=0) in vec2 a_pos;\n" - "layout(location=1) in vec2 a_uv;\n" - "layout(location=2) in vec4 a_col;\n" - "layout(location=3) in vec2 a_tpage;\n" - "layout(location=4) in vec2 a_clut;\n" - "layout(location=5) in float a_depth;\n" - "layout(location=6) in float a_raw;\n" - "layout(location=7) in vec4 a_limits;\n" - "layout(location=8) in float a_semi;\n" - "layout(location=9) in float a_q; /* persp weight; 0 = affine (default) */\n" - "uniform float u_shift;\n" - "uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n" - "uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n" - "uniform float u_xscale; /* native-wide 2D-backdrop x-stretch; 1 canonical */\n" - "uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n" - "noperspective out vec2 v_uv; noperspective out vec4 v_col;\n" - "smooth out vec2 v_uv_p; /* perspective-correct UV (used when v_persp!=0) */\n" - "flat out int v_persp;\n" - "flat out ivec2 v_tpage; flat out ivec2 v_clut; flat out int v_depth;\n" - "flat out int v_raw; flat out ivec4 v_limits; flat out int v_semi;\n" - "void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col;\n" - " v_persp = (a_q > 0.0) ? 1 : 0;\n" - " v_tpage = ivec2(a_tpage + 0.5); v_clut = ivec2(a_clut + 0.5);\n" - " v_depth = int(a_depth + 0.5); v_raw = int(a_raw + 0.5);\n" - " v_semi = int(a_semi + 0.5);\n" - " v_limits = ivec4(floor(a_limits + 0.5));\n" - " /* u_shift: align GL's center-sample grid with the PS1 integer grid (see\n" - " * GEO_VS) so interpolated uv at a fragment equals the PS1 DDA value. */\n" - " float xb = a_pos.x;\n" - " if (u_xscale < 0.0) {\n" - " float s = -u_xscale; float h = u_xhalf / s;\n" - " float l = u_xcenter - h, r = u_xcenter + h;\n" - " if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n" - " } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n" - " /* Perspective-correct UV rides the standard w divide: emit clip coords\n" - " * pre-multiplied by w = 1/q so the post-divide NDC is unchanged while\n" - " * the rasterizer interpolates the smooth varying hyperbolically. With\n" - " * a_q == 0 (feature off) w is exactly 1.0 and this is the old expression. */\n" - " float w = (a_q > 0.0) ? (1.0 / a_q) : 1.0;\n" - " vec2 ndc = vec2((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0);\n" - " gl_Position = vec4(ndc * w, 0.0, w); }\n"; -static const char *TEX_FS = - "#version 330\n" - "noperspective in vec2 v_uv; noperspective in vec4 v_col;\n" - "smooth in vec2 v_uv_p; flat in int v_persp;\n" - "out vec4 frag; out vec4 blend_factor;\n" - "flat in ivec2 v_tpage; /* texture page base, VRAM px */\n" - "flat in ivec2 v_clut; /* CLUT base, VRAM px */\n" - "flat in int v_depth; /* 0=4bit 1=8bit 2=15bit */\n" - "flat in int v_raw; /* 1 = no color modulation */\n" - "flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n" - "flat in int v_semi; /* GP0 command has semi-transparency enabled */\n" - "uniform usampler2D u_vram;\n" - "uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n" - "uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n" +static const char *INTERP_FS = + "#version 330\n" + "in vec2 v_uv; uniform sampler2D u_prev; uniform sampler2D u_curr;\n" + "uniform float u_alpha; uniform int u_blend_mode; out vec4 frag;\n" + PSX_SCANLINE_UNIFORMS + PSX_SCANLINE_FUNC + "void main(){\n" + " vec4 prev=texture(u_prev,v_uv), curr=texture(u_curr,v_uv);\n" + " float alpha=u_alpha;\n" + " if(u_blend_mode==1){\n" + " vec3 d=abs(prev.rgb-curr.rgb);\n" + " float change=max(max(d.r,d.g),d.b);\n" + " float safe_blend=1.0-smoothstep(0.08,0.20,change);\n" + " alpha=mix(step(0.5,u_alpha),u_alpha,safe_blend);\n" + " }\n" + " vec4 c=mix(prev,curr,alpha);\n" + " c.rgb=psx_scanline(c.rgb, v_uv.y);\n" + " frag=c;\n" + "}\n"; + +/* Geometry: position in VRAM pixels (draw offset already applied by gpu.c), + * color rgb in 0..1, color a = mask bit (0/1). The clip transform is in + * native VRAM space; the viewport at S* the size scales rasterization. + * + * ALL drawn prims shift positions by u_shift = half an HR pixel (0.5/S in + * native units): GL samples coverage/attributes at pixel CENTERS, the PS1 + * DDA at INTEGER coords. The shift aligns GL's sample grid with the PS1 + * grid — without it, any texture mapping with slope != 1 (scaled sprites, + * squished menu fonts) samples one texel off per row/column (striped + * glyphs, seam lines). Half an HR pixel (not half a native pixel!) keeps + * rect coverage exactly [x*S, (x+w)*S) at every scale AND makes the + * top-left subpixel of each S*S block sample the exact PS1 value (which is + * what the PACK pass reads back). */ +static const char *GEO_VS = + "#version 330\n" + "layout(location=0) in vec2 a_pos;\n" + "layout(location=1) in vec4 a_col;\n" + "uniform float u_shift;\n" + "uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n" + "uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n" + "uniform float u_xscale; /* native-wide 2D-backdrop x-stretch; 1 canonical */\n" + "uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n" + "noperspective out vec4 v_col;\n" + "void main(){ v_col = a_col;\n" + " float xb = a_pos.x;\n" + " if (u_xscale < 0.0) {\n" + " float s = -u_xscale; float h = u_xhalf / s;\n" + " float l = u_xcenter - h, r = u_xcenter + h;\n" + " if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n" + " } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n" + " gl_Position = vec4((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n"; +static const char *GEO_FS = + "#version 330\n" + "noperspective in vec4 v_col; out vec4 frag;\n" + "void main(){ frag = v_col; }\n"; + +/* Textured prims: sample raw 1555 VRAM (integer), CLUT decode per depth, + * texture window, optional bilinear, texel-0 discard, STP-split discard, + * PS1 *2-around-0x80 modulation. Output alpha = bit15 of the written pixel. + * Texel coords use floor() to match the software rasterizer's truncation + * (rounding shifted sampling +1 texel half the time: smeared text). */ +/* Textured program. Per-prim texture state (texpage, clut, depth, raw, uv + * limits) is carried in FLAT vertex attributes — constant across a prim's + * vertices — instead of uniforms, so consecutive textured prims with the same + * blend/mask/texture-window state batch into one draw (see flush_tex_batch). + * The remaining uniforms (u_twin/u_maskset/u_filter/u_semipass) are the batch + * keys + per-pass state. */ +static const char *TEX_VS = + "#version 330\n" + "layout(location=0) in vec2 a_pos;\n" + "layout(location=1) in vec2 a_uv;\n" + "layout(location=2) in vec4 a_col;\n" + "layout(location=3) in vec2 a_tpage;\n" + "layout(location=4) in vec2 a_clut;\n" + "layout(location=5) in float a_depth;\n" + "layout(location=6) in float a_raw;\n" + "layout(location=7) in vec4 a_limits;\n" + "layout(location=8) in float a_semi;\n" + "layout(location=9) in float a_q; /* persp weight; 0 = affine (default) */\n" + "uniform float u_shift;\n" + "uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n" + "uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n" + "uniform float u_xscale; /* native-wide 2D-backdrop x-stretch; 1 canonical */\n" + "uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n" + "noperspective out vec2 v_uv; noperspective out vec4 v_col;\n" + "smooth out vec2 v_uv_p; /* perspective-correct UV (used when v_persp!=0) */\n" + "flat out int v_persp;\n" + "flat out ivec2 v_tpage; flat out ivec2 v_clut; flat out int v_depth;\n" + "flat out int v_raw; flat out ivec4 v_limits; flat out int v_semi;\n" + "void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col;\n" + " v_persp = (a_q > 0.0) ? 1 : 0;\n" + " v_tpage = ivec2(a_tpage + 0.5); v_clut = ivec2(a_clut + 0.5);\n" + " v_depth = int(a_depth + 0.5); v_raw = int(a_raw + 0.5);\n" + " v_semi = int(a_semi + 0.5);\n" + " v_limits = ivec4(floor(a_limits + 0.5));\n" + " /* u_shift: align GL's center-sample grid with the PS1 integer grid (see\n" + " * GEO_VS) so interpolated uv at a fragment equals the PS1 DDA value. */\n" + " float xb = a_pos.x;\n" + " if (u_xscale < 0.0) {\n" + " float s = -u_xscale; float h = u_xhalf / s;\n" + " float l = u_xcenter - h, r = u_xcenter + h;\n" + " if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n" + " } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n" + " /* Perspective-correct UV rides the standard w divide: emit clip coords\n" + " * pre-multiplied by w = 1/q so the post-divide NDC is unchanged while\n" + " * the rasterizer interpolates the smooth varying hyperbolically. With\n" + " * a_q == 0 (feature off) w is exactly 1.0 and this is the old expression. */\n" + " float w = (a_q > 0.0) ? (1.0 / a_q) : 1.0;\n" + " vec2 ndc = vec2((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0);\n" + " gl_Position = vec4(ndc * w, 0.0, w); }\n"; +static const char *TEX_FS = + "#version 330\n" + "noperspective in vec2 v_uv; noperspective in vec4 v_col;\n" + "smooth in vec2 v_uv_p; flat in int v_persp;\n" + "out vec4 frag; out vec4 blend_factor;\n" + "flat in ivec2 v_tpage; /* texture page base, VRAM px */\n" + "flat in ivec2 v_clut; /* CLUT base, VRAM px */\n" + "flat in int v_depth; /* 0=4bit 1=8bit 2=15bit */\n" + "flat in int v_raw; /* 1 = no color modulation */\n" + "flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n" + "flat in int v_semi; /* GP0 command has semi-transparency enabled */\n" + "uniform usampler2D u_vram;\n" + "uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n" + "uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n" "uniform ivec4 u_twin; /* texture window: mask_x, mask_y, off_x, off_y */\n" "uniform int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n" "uniform int u_filter; /* 1 = bilinear */\n" "uniform float u_shift;\n" "int vram_at(int x, int y){\n" - " return int(texelFetch(u_vram, ivec2(x & 1023, y & 511), 0).r);\n" - "}\n" - "int fetch_texel(int u, int v){\n" - " u &= 255; v &= 255;\n" - " if ((u_twin.x | u_twin.y) != 0) {\n" - " u = (u & ~(u_twin.x * 8)) | ((u_twin.z & u_twin.x) * 8);\n" - " v = (v & ~(u_twin.y * 8)) | ((u_twin.w & u_twin.y) * 8);\n" - " } else {\n" - " u = clamp(u, v_limits.x, v_limits.z);\n" - " v = clamp(v, v_limits.y, v_limits.w);\n" - " }\n" - " if (v_depth == 0) {\n" - " int px = vram_at(v_tpage.x + (u >> 2), v_tpage.y + v);\n" - " return vram_at(v_clut.x + ((px >> ((u & 3) * 4)) & 0xF), v_clut.y);\n" - " } else if (v_depth == 1) {\n" - " int px = vram_at(v_tpage.x + (u >> 1), v_tpage.y + v);\n" - " return vram_at(v_clut.x + ((px >> ((u & 1) * 8)) & 0xFF), v_clut.y);\n" - " }\n" - " return vram_at(v_tpage.x + u, v_tpage.y + v);\n" - "}\n" - "vec3 col5(int raw){\n" - " return vec3(float(raw & 31), float((raw >> 5) & 31), float((raw >> 10) & 31)) / 31.0;\n" - "}\n" - "void main(){\n" - " int stp; vec3 rgb;\n" - " /* v_persp is 0 for every prim unless [video] perspective_texturing is on\n" - " * AND this prim's packet carried full GTE projection provenance, so the\n" - " * default is the PS1's affine (noperspective) mapping. */\n" - " vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;\n" - " if (u_filter == 0) {\n" - " int raw = fetch_texel(int(floor(uv.x)), int(floor(uv.y)));\n" - " if (raw == 0) discard;\n" - " rgb = col5(raw);\n" - " stp = (raw >> 15) & 1;\n" - " } else {\n" - " /* Bilinear, Beetle-PSX formulation: the NEAREST texel is the base\n" - " * (cutout + STP authority), the neighbours lie toward the sub-texel\n" - " * offset and clamp to u_limits, and each texel's weight is gated by\n" - " * its opacity with the colour renormalised — so prim edges and\n" - " * cutout borders keep their colour instead of dissolving into the\n" - " * transparent (black) neighbour and discarding whole edge columns.\n" - " * Recenter from PS1 top-left point sampling to the bilinear footprint. */\n" - " uv += vec2(u_shift);\n" - " int iu = int(floor(uv.x)), iv = int(floor(uv.y));\n" - " float fx = uv.x - float(iu) - 0.5, fy = uv.y - float(iv) - 0.5;\n" - " int sx = fx < 0.0 ? -1 : 1, sy = fy < 0.0 ? -1 : 1;\n" - " fx = abs(fx); fy = abs(fy);\n" - " int c00 = fetch_texel(iu, iv);\n" - " if (c00 == 0) discard;\n" - " int c10 = fetch_texel(iu + sx, iv);\n" - " int c01 = fetch_texel(iu, iv + sy);\n" - " int c11 = fetch_texel(iu + sx, iv + sy);\n" - " float w00 = (c00 == 0 ? 0.0 : 1.0) * (1.0 - fx) * (1.0 - fy);\n" - " float w10 = (c10 == 0 ? 0.0 : 1.0) * fx * (1.0 - fy);\n" - " float w01 = (c01 == 0 ? 0.0 : 1.0) * (1.0 - fx) * fy;\n" - " float w11 = (c11 == 0 ? 0.0 : 1.0) * fx * fy;\n" - " float opac = w00 + w10 + w01 + w11;\n" - " rgb = (col5(c00)*w00 + col5(c10)*w10 + col5(c01)*w01 + col5(c11)*w11) / opac;\n" - " float stpf = (float((c00 >> 15) & 1) * w00 + float((c10 >> 15) & 1) * w10\n" - " + float((c01 >> 15) & 1) * w01 + float((c11 >> 15) & 1) * w11) / opac;\n" - " stp = stpf >= 0.5 ? 1 : 0;\n" - " }\n" - " if (u_semipass == 1 && stp == 1) discard;\n" - " if (u_semipass == 2 && stp == 0) discard;\n" - " if (v_raw == 0) rgb = clamp(rgb * v_col.rgb * 2.0, 0.0, 1.0);\n" - " float dst_factor = 0.0;\n" - " if (u_semimode == 4 && v_semi != 0 && stp != 0) {\n" - " dst_factor = v_semi == 1 ? 0.5 : 1.0;\n" - " if (v_semi == 1) rgb *= 0.5; else if (v_semi == 4) rgb *= 0.25;\n" - " }\n" - " frag = vec4(rgb, (stp == 1 || u_maskset == 1) ? 1.0 : 0.0);\n" - " blend_factor = vec4(0.0, 0.0, 0.0, dst_factor);\n" - "}\n"; - -/* Quad blit: used for CPU->VRAM upload flushes and VRAM->VRAM copies. - * Samples an RGBA8 source (alpha = bit15), splits by STP for the stencil - * write, optionally ORs set-mask into the output alpha. */ -static const char *BLIT_VS = - "#version 330\n" - "layout(location=0) in vec2 a_pos; /* native VRAM px */\n" - "uniform float u_shift;\n" - "void main(){\n" - " gl_Position = vec4((a_pos.x+u_shift)/512.0 - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n"; -static const char *BLIT_FS = - "#version 330\n" - "out vec4 frag;\n" - "uniform sampler2D u_src;\n" - "uniform int u_stp_pass; /* 0=all, 1=bit15=0 only, 2=bit15=1 only */\n" - "uniform int u_maskset;\n" - "uniform int u_src_div; /* fragcoord -> src texel divisor (S for native-res\n" - " sources, 1 for hr-res sources) */\n" - "uniform ivec2 u_src_off; /* added after the divide, in src texel units */\n" - "void main(){\n" - " /* Exact integer source fetch — no normalized-uv edge precision. */\n" - " ivec2 p = ivec2(gl_FragCoord.xy);\n" - " vec4 c = texelFetch(u_src, p / u_src_div + u_src_off, 0);\n" - " bool stp = c.a >= 0.5;\n" - " if (u_stp_pass == 1 && stp) discard;\n" - " if (u_stp_pass == 2 && !stp) discard;\n" - " frag = vec4(c.rgb, (stp || u_maskset != 0) ? 1.0 : 0.0);\n" - "}\n"; - -/* Pack: re-encode the hr FBO into the native R16UI raw mirror. Runs over a - * native-res viewport with scissor = dirty rect; each native pixel takes the - * top-left sample of its S*S block (the sample at the exact native coord). */ -static const char *PACK_VS = - "#version 330\n" - "void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n" - " gl_Position = vec4(p*2.0-1.0, 0.0, 1.0); }\n"; -static const char *PACK_FS = - "#version 330\n" - "uniform sampler2D u_hr;\n" - "uniform int u_scale;\n" - "out uint o_pix;\n" - "void main(){\n" - " ivec2 p = ivec2(gl_FragCoord.xy);\n" - " vec4 c = texelFetch(u_hr, p * u_scale, 0);\n" - " uint r = uint(c.r * 255.0 + 0.5) >> 3;\n" - " uint g = uint(c.g * 255.0 + 0.5) >> 3;\n" - " uint b = uint(c.b * 255.0 + 0.5) >> 3;\n" - " o_pix = r | (g << 5) | (b << 10) | (c.a >= 0.5 ? 0x8000u : 0u);\n" - "}\n"; - -/* Rebuild stencil bit 0 from a copied RGBA target's alpha. Color writes are - * disabled while this shader runs; only alpha>=0.5 fragments replace stencil. */ -static const char *STENCIL_FS = - "#version 330\n" - "uniform sampler2D u_src; out vec4 frag;\n" - "void main(){ vec4 c=texelFetch(u_src,ivec2(gl_FragCoord.xy),0);\n" - " if(c.a<0.5) discard; frag=vec4(0.0); }\n"; - -static GLuint compile_shader(GLenum type, const char *src) { - GLuint s = p_glCreateShader(type); - p_glShaderSource(s, 1, &src, NULL); - p_glCompileShader(s); - GLint ok = 0; p_glGetShaderiv(s, PSXGL_COMPILE_STATUS, &ok); - if (!ok) { char log[1024]; log[0]=0; p_glGetShaderInfoLog(s, sizeof log, NULL, log); - fprintf(stdout, "psxrecomp: GL shader compile failed: %s\n", log); - p_glDeleteShader(s); return 0; } - return s; -} -static GLuint build_program_ex(const char *vs, const char *fs, int dual_source) { - GLuint v = compile_shader(PSXGL_VERTEX_SHADER, vs), f = compile_shader(PSXGL_FRAGMENT_SHADER, fs); - if (!v || !f) return 0; - GLuint p = p_glCreateProgram(); - p_glAttachShader(p, v); p_glAttachShader(p, f); - if (dual_source) { - p_glBindFragDataLocationIndexed(p, 0, 0, "frag"); - p_glBindFragDataLocationIndexed(p, 0, 1, "blend_factor"); - } - p_glLinkProgram(p); - p_glDeleteShader(v); p_glDeleteShader(f); - GLint ok = 0; p_glGetProgramiv(p, PSXGL_LINK_STATUS, &ok); - if (!ok) { char log[1024]; log[0]=0; p_glGetProgramInfoLog(p, sizeof log, NULL, log); - fprintf(stdout, "psxrecomp: GL program link failed: %s\n", log); return 0; } - return p; -} -static GLuint build_program(const char *vs, const char *fs) { - return build_program_ex(vs, fs, 0); -} - -/* ---- pixel conversion (PS1 1555: bit15=mask, B[14:10] G[9:5] R[4:0]) ---- */ -static inline uint32_t conv_1555_to_rgba8(uint16_t p) { - uint32_t r = (p & 0x1F) << 3, g = ((p >> 5) & 0x1F) << 3, b = ((p >> 10) & 0x1F) << 3; - uint32_t a = (p >> 15) & 1 ? 0xFF : 0; - return r | (g << 8) | (b << 16) | (a << 24); /* RGBA8 little-endian */ -} - -/* ---- mask-bit stencil --------------------------------------------------- * - * Stencil bit0 mirrors bit15 of every pixel. - * check off: test ALWAYS, op REPLACE with ref = write value. - * check on: test EQUAL 0 (pass iff dest unmasked). GL couples the REPLACE - * value to the test reference, so write a 1 via INVERT (the - * stored value is known to be 0 when the test passed) and a 0 - * via KEEP. */ -static void mask_stencil(int write_val) { - glEnable(GL_STENCIL_TEST); - glStencilMask(0x01); - if (s_mask_check) { - glStencilFunc(GL_EQUAL, 0, 0x01); - glStencilOp(GL_KEEP, GL_KEEP, write_val ? GL_INVERT : GL_KEEP); - } else { - glStencilFunc(GL_ALWAYS, write_val ? 1 : 0, 0x01); - glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); - } -} -/* Like mask_stencil but never checks (uploads: gpu.c already applied mask). */ -static void plain_stencil(int write_val) { - glEnable(GL_STENCIL_TEST); - glStencilMask(0x01); - glStencilFunc(GL_ALWAYS, write_val ? 1 : 0, 0x01); - glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); -} - -/* PS1 semi-transparency as fixed-function blending, RGB only — the alpha - * channel (mask bit) is always replaced by the source fragment's alpha. - * 0: B/2 + F/2 1: B + F 2: B - F 3: B + F/4 */ -static void apply_psx_blend(int mode) { - glEnable(GL_BLEND); - p_glBlendEquationSeparate((mode & 3) == 2 ? PSXGL_FUNC_REVERSE_SUBTRACT - : PSXGL_FUNC_ADD, - PSXGL_FUNC_ADD); - switch (mode & 3) { - case 0: - p_glBlendColor(0.5f, 0.5f, 0.5f, 0.5f); - p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ZERO); - break; - case 1: - case 2: - p_glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ZERO); - break; - case 3: - p_glBlendColor(0.25f, 0.25f, 0.25f, 0.25f); - p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ONE, GL_ZERO); - break; - } -} - -/* ---- hr FBO render-state bracket ---------------------------------------- */ -static void hr_begin(int clip_to_draw_area) { - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); - glViewport(0, 0, VRAM_W * s_scale, VRAM_H * s_scale); - glEnable(GL_SCISSOR_TEST); - if (clip_to_draw_area) { - int sw = s_area_x2 - s_area_x1 + 1, sh = s_area_y2 - s_area_y1 + 1; - if (sw < 0) sw = 0; if (sh < 0) sh = 0; - glScissor(s_area_x1 * s_scale, s_area_y1 * s_scale, - sw * s_scale, sh * s_scale); - } -} -static void hr_end(void) { - glDisable(GL_BLEND); - /* apply_psx_blend mode 2 leaves REVERSE_SUBTRACT armed; reset so later - * host draws (OSD) that re-enable blend do not inherit B-F math. */ - if (p_glBlendEquationSeparate) - p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); - glDisable(GL_STENCIL_TEST); - glDisable(GL_SCISSOR_TEST); - p_glBindVertexArray(0); - p_glUseProgram(0); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); -} - -/* ---- coherency: CPU -> GPU upload flush --------------------------------- */ -/* CPU-side VRAM writes (GP0 A0 transfers, DMA, single pixel pokes) land in - * the CPU array immediately and accumulate s_up_rects. Flushing before the - * next GPU op (or readback/present) preserves PS1 command order. */ -static void flush_cpu_upload(void) { - if (!s_raster_ok || s_up_nrects == 0) return; - const int diag = runtime_upload_diag_enabled(); - if (diag) { s_rt_up_diag[0]++; s_rt_up_diag[1] += (uint64_t)s_up_nrects; } - flush_flat_batch(); /* queued flat GEO before upload mutates VRAM */ - flush_tex_batch(); /* queued textured draws before this upload writes VRAM */ - /* Snapshot + clear first (re-entrancy safe; up_add overflow calls back in). */ - DirtyRect rects[UP_RECTS_MAX]; - int nrects = s_up_nrects; - memcpy(rects, s_up_rects, (size_t)nrects * sizeof(DirtyRect)); - s_up_nrects = 0; - - /* Stage every rect's CPU data first (texture uploads outside the FBO - * bracket), then draw all the quads in one bracket. Only the exact - * uploaded rects are painted — never the union bounding box (stale-CPU - * flicker class bug, see s_up_rects). */ - for (int i = 0; i < nrects; i++) { - int x = rects[i].x0, y = rects[i].y0; - int w = rects[i].x1 - rects[i].x0 + 1; - int h = rects[i].y1 - rects[i].y0 + 1; - if (diag) s_rt_up_diag[2] += (uint64_t)w * (uint64_t)h; - coh_record(GL_COH_FLUSH, x, y, x + w - 1, y + h - 1); - - /* RGBA8 staging for the hr quad draw. */ - uint64_t t0 = diag ? SDL_GetPerformanceCounter() : 0; - for (int row = 0; row < h; row++) { - const uint16_t *src = s_vram + (size_t)(y + row) * VRAM_W + x; - uint32_t *dst = s_conv + (size_t)row * w; - for (int col = 0; col < w; col++) dst[col] = conv_1555_to_rgba8(src[col]); - } - if (diag) s_rt_up_diag[3] += SDL_GetPerformanceCounter() - t0; - t0 = diag ? SDL_GetPerformanceCounter() : 0; - glBindTexture(GL_TEXTURE_2D, s_up_tex); - glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, s_conv); - - /* Raw mirror takes the CPU data directly — current for this rect, so no - * pack is needed for uploaded content. */ - glBindTexture(GL_TEXTURE_2D, s_raw_tex); - glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, VRAM_W); - glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, - PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, - s_vram + (size_t)y * VRAM_W + x); - glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0); - if (diag) s_rt_up_diag[4] += SDL_GetPerformanceCounter() - t0; - } - - /* Quads into the hr FBO; two passes split by bit15 so the stencil mirror - * stays exact. gpu.c applied mask set/check per pixel already — no check - * here, the data is final. up_tex is VRAM-aligned: src texel = frag/S. */ - uint64_t draw_t0 = diag ? SDL_GetPerformanceCounter() : 0; - hr_begin(0); - glDisable(GL_BLEND); - p_glUseProgram(s_blit_prog); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_up_tex); - p_glUniform1i(s_uBlitSrc, 0); - p_glUniform1i(s_uBlitMaskset, 0); - p_glUniform1i(s_uBlitSrcDiv, s_scale); - p_glUniform2i(s_uBlitSrcOff, 0, 0); - p_glBindVertexArray(s_blit_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); - for (int i = 0; i < nrects; i++) { - int x = rects[i].x0, y = rects[i].y0; - int w = rects[i].x1 - rects[i].x0 + 1; - int h = rects[i].y1 - rects[i].y0 + 1; - glScissor(x * s_scale, y * s_scale, w * s_scale, h * s_scale); - float fx0 = (float)x, fy0 = (float)y, fx1 = (float)(x + w), fy1 = (float)(y + h); - float verts[6 * 2] = { - fx0, fy0, fx1, fy0, fx0, fy1, - fx1, fy0, fx0, fy1, fx1, fy1, - }; - p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); - plain_stencil(0); p_glUniform1i(s_uBlitPass, 1); glDrawArrays(GL_TRIANGLES, 0, 6); - plain_stencil(1); p_glUniform1i(s_uBlitPass, 2); glDrawArrays(GL_TRIANGLES, 0, 6); - } - hr_end(); - if (diag) s_rt_up_diag[5] += SDL_GetPerformanceCounter() - draw_t0; -} - -/* Recreate one target's stencil mask from its authoritative alpha channel. - * Sampling an attached render target is undefined, so copy color to the shared - * scratch texture first. Wide targets never exceed the 1024-pixel VRAM width. */ -static void rebuild_target_stencil(GLuint target_fbo, int target_w, int target_h) { - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, target_fbo); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo); - p_glBlitFramebuffer(0, 0, target_w, target_h, 0, 0, target_w, target_h, - GL_COLOR_BUFFER_BIT, GL_NEAREST); - - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, target_fbo); - glViewport(0, 0, target_w, target_h); - glDisable(GL_SCISSOR_TEST); - glDisable(GL_BLEND); - glEnable(GL_STENCIL_TEST); - glStencilMask(0x01); - glClearStencil(0); - glClear(GL_STENCIL_BUFFER_BIT); - glStencilFunc(GL_ALWAYS, 1, 0x01); - glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); - glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); - p_glUseProgram(s_stencil_prog); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_scratch_tex); - p_glUniform1i(s_uStencilSrc, 0); - p_glBindVertexArray(s_empty_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); - glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - glDisable(GL_STENCIL_TEST); - p_glBindVertexArray(0); - p_glUseProgram(0); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); -} - -static void rebuild_mask_stencils(void) { - if (s_stencil_valid || !s_raster_ok) return; - int hw = VRAM_W * s_scale, hh = VRAM_H * s_scale; - rebuild_target_stencil(s_hr_fbo, hw, hh); - for (int i = 0; i < WIDE_MAX_SURF; i++) { - if (s_wide_fbo[i]) - rebuild_target_stencil(s_wide_fbo[i], g_wide_w * s_scale, hh); - } - s_stencil_valid = 1; -} - -/* ---- coherency: hr FBO -> raw mirror (pack) ------------------------------ */ -static void pack_flush(void) { - if (!s_raster_ok || !s_pack_dirty.set) return; - int x = s_pack_dirty.x0, y = s_pack_dirty.y0; - int w = s_pack_dirty.x1 - s_pack_dirty.x0 + 1; - int h = s_pack_dirty.y1 - s_pack_dirty.y0 + 1; - rect_clear(&s_pack_dirty); - coh_record(GL_COH_PACK, x, y, x + w - 1, y + h - 1); - - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo); - glViewport(0, 0, VRAM_W, VRAM_H); - glEnable(GL_SCISSOR_TEST); - glScissor(x, y, w, h); - glDisable(GL_BLEND); - glDisable(GL_STENCIL_TEST); - p_glUseProgram(s_pack_prog); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_hr_tex); - p_glUniform1i(s_uPackHr, 0); - p_glUniform1i(s_uPackScale, s_scale); - p_glBindVertexArray(s_empty_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); - p_glBindVertexArray(0); - p_glUseProgram(0); - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); -} - -/* Make sure the raw mirror is current for a textured draw that samples the - * given texture page / CLUT. */ -static void flush_pack_if_sampling(int tpage_x, int tpage_y, int depth, - int clut_x, int clut_y) { - if (!s_pack_dirty.set) return; - int page_w = depth == 0 ? 64 : depth == 1 ? 128 : 256; /* VRAM columns */ - if (rect_intersects(&s_pack_dirty, tpage_x, tpage_y, - tpage_x + page_w - 1, tpage_y + 255)) { - flush_flat_batch(); - flush_tex_batch(); /* queued draws are part of s_pack_dirty — realise them before packing */ - pack_flush(); return; - } - if (depth <= 1) { - int n = depth == 0 ? 16 : 256; - if (rect_intersects(&s_pack_dirty, clut_x, clut_y, clut_x + n - 1, clut_y)) { - flush_flat_batch(); - flush_tex_batch(); - pack_flush(); - } - } -} - + " return int(texelFetch(u_vram, ivec2(x & 1023, y & 511), 0).r);\n" + "}\n" + "int fetch_texel(int u, int v){\n" + " u &= 255; v &= 255;\n" + " if ((u_twin.x | u_twin.y) != 0) {\n" + " u = (u & ~(u_twin.x * 8)) | ((u_twin.z & u_twin.x) * 8);\n" + " v = (v & ~(u_twin.y * 8)) | ((u_twin.w & u_twin.y) * 8);\n" + " } else {\n" + " u = clamp(u, v_limits.x, v_limits.z);\n" + " v = clamp(v, v_limits.y, v_limits.w);\n" + " }\n" + " if (v_depth == 0) {\n" + " int px = vram_at(v_tpage.x + (u >> 2), v_tpage.y + v);\n" + " return vram_at(v_clut.x + ((px >> ((u & 3) * 4)) & 0xF), v_clut.y);\n" + " } else if (v_depth == 1) {\n" + " int px = vram_at(v_tpage.x + (u >> 1), v_tpage.y + v);\n" + " return vram_at(v_clut.x + ((px >> ((u & 1) * 8)) & 0xFF), v_clut.y);\n" + " }\n" + " return vram_at(v_tpage.x + u, v_tpage.y + v);\n" + "}\n" + "vec3 col5(int raw){\n" + " return vec3(float(raw & 31), float((raw >> 5) & 31), float((raw >> 10) & 31)) / 31.0;\n" + "}\n" + "void main(){\n" + " int stp; vec3 rgb;\n" + " /* v_persp is 0 for every prim unless [video] perspective_texturing is on\n" + " * AND this prim's packet carried full GTE projection provenance, so the\n" + " * default is the PS1's affine (noperspective) mapping. */\n" + " vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;\n" + " if (u_filter == 0) {\n" + " int raw = fetch_texel(int(floor(uv.x)), int(floor(uv.y)));\n" + " if (raw == 0) discard;\n" + " rgb = col5(raw);\n" + " stp = (raw >> 15) & 1;\n" + " } else {\n" + " /* Bilinear, Beetle-PSX formulation: the NEAREST texel is the base\n" + " * (cutout + STP authority), the neighbours lie toward the sub-texel\n" + " * offset and clamp to u_limits, and each texel's weight is gated by\n" + " * its opacity with the colour renormalised — so prim edges and\n" + " * cutout borders keep their colour instead of dissolving into the\n" + " * transparent (black) neighbour and discarding whole edge columns.\n" + " * Recenter from PS1 top-left point sampling to the bilinear footprint. */\n" + " uv += vec2(u_shift);\n" + " int iu = int(floor(uv.x)), iv = int(floor(uv.y));\n" + " float fx = uv.x - float(iu) - 0.5, fy = uv.y - float(iv) - 0.5;\n" + " int sx = fx < 0.0 ? -1 : 1, sy = fy < 0.0 ? -1 : 1;\n" + " fx = abs(fx); fy = abs(fy);\n" + " int c00 = fetch_texel(iu, iv);\n" + " if (c00 == 0) discard;\n" + " int c10 = fetch_texel(iu + sx, iv);\n" + " int c01 = fetch_texel(iu, iv + sy);\n" + " int c11 = fetch_texel(iu + sx, iv + sy);\n" + " float w00 = (c00 == 0 ? 0.0 : 1.0) * (1.0 - fx) * (1.0 - fy);\n" + " float w10 = (c10 == 0 ? 0.0 : 1.0) * fx * (1.0 - fy);\n" + " float w01 = (c01 == 0 ? 0.0 : 1.0) * (1.0 - fx) * fy;\n" + " float w11 = (c11 == 0 ? 0.0 : 1.0) * fx * fy;\n" + " float opac = w00 + w10 + w01 + w11;\n" + " rgb = (col5(c00)*w00 + col5(c10)*w10 + col5(c01)*w01 + col5(c11)*w11) / opac;\n" + " float stpf = (float((c00 >> 15) & 1) * w00 + float((c10 >> 15) & 1) * w10\n" + " + float((c01 >> 15) & 1) * w01 + float((c11 >> 15) & 1) * w11) / opac;\n" + " stp = stpf >= 0.5 ? 1 : 0;\n" + " }\n" + " if (u_semipass == 1 && stp == 1) discard;\n" + " if (u_semipass == 2 && stp == 0) discard;\n" + " if (v_raw == 0) rgb = clamp(rgb * v_col.rgb * 2.0, 0.0, 1.0);\n" + " float dst_factor = 0.0;\n" + " if (u_semimode == 4 && v_semi != 0 && stp != 0) {\n" + " dst_factor = v_semi == 1 ? 0.5 : 1.0;\n" + " if (v_semi == 1) rgb *= 0.5; else if (v_semi == 4) rgb *= 0.25;\n" + " }\n" + " frag = vec4(rgb, (stp == 1 || u_maskset == 1) ? 1.0 : 0.0);\n" + " blend_factor = vec4(0.0, 0.0, 0.0, dst_factor);\n" + "}\n"; + +/* Quad blit: used for CPU->VRAM upload flushes and VRAM->VRAM copies. + * Samples an RGBA8 source (alpha = bit15), splits by STP for the stencil + * write, optionally ORs set-mask into the output alpha. */ +static const char *BLIT_VS = + "#version 330\n" + "layout(location=0) in vec2 a_pos; /* native VRAM px */\n" + "uniform float u_shift;\n" + "void main(){\n" + " gl_Position = vec4((a_pos.x+u_shift)/512.0 - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n"; +static const char *BLIT_FS = + "#version 330\n" + "out vec4 frag;\n" + "uniform sampler2D u_src;\n" + "uniform int u_stp_pass; /* 0=all, 1=bit15=0 only, 2=bit15=1 only */\n" + "uniform int u_maskset;\n" + "uniform int u_src_div; /* fragcoord -> src texel divisor (S for native-res\n" + " sources, 1 for hr-res sources) */\n" + "uniform ivec2 u_src_off; /* added after the divide, in src texel units */\n" + "void main(){\n" + " /* Exact integer source fetch — no normalized-uv edge precision. */\n" + " ivec2 p = ivec2(gl_FragCoord.xy);\n" + " vec4 c = texelFetch(u_src, p / u_src_div + u_src_off, 0);\n" + " bool stp = c.a >= 0.5;\n" + " if (u_stp_pass == 1 && stp) discard;\n" + " if (u_stp_pass == 2 && !stp) discard;\n" + " frag = vec4(c.rgb, (stp || u_maskset != 0) ? 1.0 : 0.0);\n" + "}\n"; + +/* Pack: re-encode the hr FBO into the native R16UI raw mirror. Runs over a + * native-res viewport with scissor = dirty rect; each native pixel takes the + * top-left sample of its S*S block (the sample at the exact native coord). */ +static const char *PACK_VS = + "#version 330\n" + "void main(){ vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);\n" + " gl_Position = vec4(p*2.0-1.0, 0.0, 1.0); }\n"; +static const char *PACK_FS = + "#version 330\n" + "uniform sampler2D u_hr;\n" + "uniform int u_scale;\n" + "out uint o_pix;\n" + "void main(){\n" + " ivec2 p = ivec2(gl_FragCoord.xy);\n" + " vec4 c = texelFetch(u_hr, p * u_scale, 0);\n" + " uint r = uint(c.r * 255.0 + 0.5) >> 3;\n" + " uint g = uint(c.g * 255.0 + 0.5) >> 3;\n" + " uint b = uint(c.b * 255.0 + 0.5) >> 3;\n" + " o_pix = r | (g << 5) | (b << 10) | (c.a >= 0.5 ? 0x8000u : 0u);\n" + "}\n"; + +/* Rebuild stencil bit 0 from a copied RGBA target's alpha. Color writes are + * disabled while this shader runs; only alpha>=0.5 fragments replace stencil. */ +static const char *STENCIL_FS = + "#version 330\n" + "uniform sampler2D u_src; out vec4 frag;\n" + "void main(){ vec4 c=texelFetch(u_src,ivec2(gl_FragCoord.xy),0);\n" + " if(c.a<0.5) discard; frag=vec4(0.0); }\n"; + +static GLuint compile_shader(GLenum type, const char *src) { + GLuint s = p_glCreateShader(type); + p_glShaderSource(s, 1, &src, NULL); + p_glCompileShader(s); + GLint ok = 0; p_glGetShaderiv(s, PSXGL_COMPILE_STATUS, &ok); + if (!ok) { char log[1024]; log[0]=0; p_glGetShaderInfoLog(s, sizeof log, NULL, log); + fprintf(stdout, "psxrecomp: GL shader compile failed: %s\n", log); + p_glDeleteShader(s); return 0; } + return s; +} +static GLuint build_program_ex(const char *vs, const char *fs, int dual_source) { + GLuint v = compile_shader(PSXGL_VERTEX_SHADER, vs), f = compile_shader(PSXGL_FRAGMENT_SHADER, fs); + if (!v || !f) return 0; + GLuint p = p_glCreateProgram(); + p_glAttachShader(p, v); p_glAttachShader(p, f); + if (dual_source) { + p_glBindFragDataLocationIndexed(p, 0, 0, "frag"); + p_glBindFragDataLocationIndexed(p, 0, 1, "blend_factor"); + } + p_glLinkProgram(p); + p_glDeleteShader(v); p_glDeleteShader(f); + GLint ok = 0; p_glGetProgramiv(p, PSXGL_LINK_STATUS, &ok); + if (!ok) { char log[1024]; log[0]=0; p_glGetProgramInfoLog(p, sizeof log, NULL, log); + fprintf(stdout, "psxrecomp: GL program link failed: %s\n", log); return 0; } + return p; +} +static GLuint build_program(const char *vs, const char *fs) { + return build_program_ex(vs, fs, 0); +} + +/* ---- pixel conversion (PS1 1555: bit15=mask, B[14:10] G[9:5] R[4:0]) ---- */ +static inline uint32_t conv_1555_to_rgba8(uint16_t p) { + uint32_t r = (p & 0x1F) << 3, g = ((p >> 5) & 0x1F) << 3, b = ((p >> 10) & 0x1F) << 3; + uint32_t a = (p >> 15) & 1 ? 0xFF : 0; + return r | (g << 8) | (b << 16) | (a << 24); /* RGBA8 little-endian */ +} + +/* ---- mask-bit stencil --------------------------------------------------- * + * Stencil bit0 mirrors bit15 of every pixel. + * check off: test ALWAYS, op REPLACE with ref = write value. + * check on: test EQUAL 0 (pass iff dest unmasked). GL couples the REPLACE + * value to the test reference, so write a 1 via INVERT (the + * stored value is known to be 0 when the test passed) and a 0 + * via KEEP. */ +static void mask_stencil(int write_val) { + glEnable(GL_STENCIL_TEST); + glStencilMask(0x01); + if (s_mask_check) { + glStencilFunc(GL_EQUAL, 0, 0x01); + glStencilOp(GL_KEEP, GL_KEEP, write_val ? GL_INVERT : GL_KEEP); + } else { + glStencilFunc(GL_ALWAYS, write_val ? 1 : 0, 0x01); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + } +} +/* Like mask_stencil but never checks (uploads: gpu.c already applied mask). */ +static void plain_stencil(int write_val) { + glEnable(GL_STENCIL_TEST); + glStencilMask(0x01); + glStencilFunc(GL_ALWAYS, write_val ? 1 : 0, 0x01); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); +} + +/* PS1 semi-transparency as fixed-function blending, RGB only — the alpha + * channel (mask bit) is always replaced by the source fragment's alpha. + * 0: B/2 + F/2 1: B + F 2: B - F 3: B + F/4 */ +static void apply_psx_blend(int mode) { + glEnable(GL_BLEND); + p_glBlendEquationSeparate((mode & 3) == 2 ? PSXGL_FUNC_REVERSE_SUBTRACT + : PSXGL_FUNC_ADD, + PSXGL_FUNC_ADD); + switch (mode & 3) { + case 0: + p_glBlendColor(0.5f, 0.5f, 0.5f, 0.5f); + p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ZERO); + break; + case 1: + case 2: + p_glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ZERO); + break; + case 3: + p_glBlendColor(0.25f, 0.25f, 0.25f, 0.25f); + p_glBlendFuncSeparate(PSXGL_CONSTANT_ALPHA, GL_ONE, GL_ONE, GL_ZERO); + break; + } +} + +/* ---- hr FBO render-state bracket ---------------------------------------- */ +static void hr_begin(int clip_to_draw_area) { + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); + glViewport(0, 0, VRAM_W * s_scale, VRAM_H * s_scale); + glEnable(GL_SCISSOR_TEST); + if (clip_to_draw_area) { + int sw = s_area_x2 - s_area_x1 + 1, sh = s_area_y2 - s_area_y1 + 1; + if (sw < 0) sw = 0; if (sh < 0) sh = 0; + glScissor(s_area_x1 * s_scale, s_area_y1 * s_scale, + sw * s_scale, sh * s_scale); + } +} +static void hr_end(void) { + glDisable(GL_BLEND); + /* apply_psx_blend mode 2 leaves REVERSE_SUBTRACT armed; reset so later + * host draws (OSD) that re-enable blend do not inherit B-F math. */ + if (p_glBlendEquationSeparate) + p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); + glDisable(GL_STENCIL_TEST); + glDisable(GL_SCISSOR_TEST); + p_glBindVertexArray(0); + p_glUseProgram(0); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); +} + +/* ---- coherency: CPU -> GPU upload flush --------------------------------- */ +/* CPU-side VRAM writes (GP0 A0 transfers, DMA, single pixel pokes) land in + * the CPU array immediately and accumulate s_up_rects. Flushing before the + * next GPU op (or readback/present) preserves PS1 command order. */ +static void flush_cpu_upload(void) { + if (!s_raster_ok || s_up_nrects == 0) return; + const int diag = runtime_upload_diag_enabled(); + if (diag) { s_rt_up_diag[0]++; s_rt_up_diag[1] += (uint64_t)s_up_nrects; } + flush_flat_batch(); /* queued flat GEO before upload mutates VRAM */ + flush_tex_batch(); /* queued textured draws before this upload writes VRAM */ + /* Snapshot + clear first (re-entrancy safe; up_add overflow calls back in). */ + DirtyRect rects[UP_RECTS_MAX]; + int nrects = s_up_nrects; + memcpy(rects, s_up_rects, (size_t)nrects * sizeof(DirtyRect)); + s_up_nrects = 0; + + /* Stage every rect's CPU data first (texture uploads outside the FBO + * bracket), then draw all the quads in one bracket. Only the exact + * uploaded rects are painted — never the union bounding box (stale-CPU + * flicker class bug, see s_up_rects). */ + for (int i = 0; i < nrects; i++) { + int x = rects[i].x0, y = rects[i].y0; + int w = rects[i].x1 - rects[i].x0 + 1; + int h = rects[i].y1 - rects[i].y0 + 1; + if (diag) s_rt_up_diag[2] += (uint64_t)w * (uint64_t)h; + coh_record(GL_COH_FLUSH, x, y, x + w - 1, y + h - 1); + + /* RGBA8 staging for the hr quad draw. */ + uint64_t t0 = diag ? SDL_GetPerformanceCounter() : 0; + for (int row = 0; row < h; row++) { + const uint16_t *src = s_vram + (size_t)(y + row) * VRAM_W + x; + uint32_t *dst = s_conv + (size_t)row * w; + for (int col = 0; col < w; col++) dst[col] = conv_1555_to_rgba8(src[col]); + } + if (diag) s_rt_up_diag[3] += SDL_GetPerformanceCounter() - t0; + t0 = diag ? SDL_GetPerformanceCounter() : 0; + glBindTexture(GL_TEXTURE_2D, s_up_tex); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, s_conv); + + /* Raw mirror takes the CPU data directly — current for this rect, so no + * pack is needed for uploaded content. */ + glBindTexture(GL_TEXTURE_2D, s_raw_tex); + glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, VRAM_W); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, + PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, + s_vram + (size_t)y * VRAM_W + x); + glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0); + if (diag) s_rt_up_diag[4] += SDL_GetPerformanceCounter() - t0; + } + + /* Quads into the hr FBO; two passes split by bit15 so the stencil mirror + * stays exact. gpu.c applied mask set/check per pixel already — no check + * here, the data is final. up_tex is VRAM-aligned: src texel = frag/S. */ + uint64_t draw_t0 = diag ? SDL_GetPerformanceCounter() : 0; + hr_begin(0); + glDisable(GL_BLEND); + p_glUseProgram(s_blit_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_up_tex); + p_glUniform1i(s_uBlitSrc, 0); + p_glUniform1i(s_uBlitMaskset, 0); + p_glUniform1i(s_uBlitSrcDiv, s_scale); + p_glUniform2i(s_uBlitSrcOff, 0, 0); + p_glBindVertexArray(s_blit_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); + for (int i = 0; i < nrects; i++) { + int x = rects[i].x0, y = rects[i].y0; + int w = rects[i].x1 - rects[i].x0 + 1; + int h = rects[i].y1 - rects[i].y0 + 1; + glScissor(x * s_scale, y * s_scale, w * s_scale, h * s_scale); + float fx0 = (float)x, fy0 = (float)y, fx1 = (float)(x + w), fy1 = (float)(y + h); + float verts[6 * 2] = { + fx0, fy0, fx1, fy0, fx0, fy1, + fx1, fy0, fx0, fy1, fx1, fy1, + }; + p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); + plain_stencil(0); p_glUniform1i(s_uBlitPass, 1); glDrawArrays(GL_TRIANGLES, 0, 6); + plain_stencil(1); p_glUniform1i(s_uBlitPass, 2); glDrawArrays(GL_TRIANGLES, 0, 6); + } + hr_end(); + if (diag) s_rt_up_diag[5] += SDL_GetPerformanceCounter() - draw_t0; +} + +/* Recreate one target's stencil mask from its authoritative alpha channel. + * Sampling an attached render target is undefined, so copy color to the shared + * scratch texture first. Wide targets never exceed the 1024-pixel VRAM width. */ +static void rebuild_target_stencil(GLuint target_fbo, int target_w, int target_h) { + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, target_fbo); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo); + p_glBlitFramebuffer(0, 0, target_w, target_h, 0, 0, target_w, target_h, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, target_fbo); + glViewport(0, 0, target_w, target_h); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); + glEnable(GL_STENCIL_TEST); + glStencilMask(0x01); + glClearStencil(0); + glClear(GL_STENCIL_BUFFER_BIT); + glStencilFunc(GL_ALWAYS, 1, 0x01); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + p_glUseProgram(s_stencil_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_scratch_tex); + p_glUniform1i(s_uStencilSrc, 0); + p_glBindVertexArray(s_empty_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDisable(GL_STENCIL_TEST); + p_glBindVertexArray(0); + p_glUseProgram(0); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); +} + +static void rebuild_mask_stencils(void) { + if (s_stencil_valid || !s_raster_ok) return; + int hw = VRAM_W * s_scale, hh = VRAM_H * s_scale; + rebuild_target_stencil(s_hr_fbo, hw, hh); + for (int i = 0; i < WIDE_MAX_SURF; i++) { + if (s_wide_fbo[i]) + rebuild_target_stencil(s_wide_fbo[i], g_wide_w * s_scale, hh); + } + s_stencil_valid = 1; +} + +/* ---- coherency: hr FBO -> raw mirror (pack) ------------------------------ */ +static void pack_flush(void) { + if (!s_raster_ok || !s_pack_dirty.set) return; + int x = s_pack_dirty.x0, y = s_pack_dirty.y0; + int w = s_pack_dirty.x1 - s_pack_dirty.x0 + 1; + int h = s_pack_dirty.y1 - s_pack_dirty.y0 + 1; + rect_clear(&s_pack_dirty); + coh_record(GL_COH_PACK, x, y, x + w - 1, y + h - 1); + + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo); + glViewport(0, 0, VRAM_W, VRAM_H); + glEnable(GL_SCISSOR_TEST); + glScissor(x, y, w, h); + glDisable(GL_BLEND); + glDisable(GL_STENCIL_TEST); + p_glUseProgram(s_pack_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_hr_tex); + p_glUniform1i(s_uPackHr, 0); + p_glUniform1i(s_uPackScale, s_scale); + p_glBindVertexArray(s_empty_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); + p_glBindVertexArray(0); + p_glUseProgram(0); + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); +} + +/* Make sure the raw mirror is current for a textured draw that samples the + * given texture page / CLUT. */ +static void flush_pack_if_sampling(int tpage_x, int tpage_y, int depth, + int clut_x, int clut_y) { + if (!s_pack_dirty.set) return; + int page_w = depth == 0 ? 64 : depth == 1 ? 128 : 256; /* VRAM columns */ + if (rect_intersects(&s_pack_dirty, tpage_x, tpage_y, + tpage_x + page_w - 1, tpage_y + 255)) { + flush_flat_batch(); + flush_tex_batch(); /* queued draws are part of s_pack_dirty — realise them before packing */ + pack_flush(); return; + } + if (depth <= 1) { + int n = depth == 0 ? 16 : 256; + if (rect_intersects(&s_pack_dirty, clut_x, clut_y, clut_x + n - 1, clut_y)) { + flush_flat_batch(); + flush_tex_batch(); + pack_flush(); + } + } +} + /* ---- coherency: GPU -> CPU readback -------------------------------------- */ /* Defined with the depth24 policy below; needed here for the readback guard. */ static int s_depth24_skip_up = 0; static DirtyRect s_d24_skip_fb; /* union of skipped MDEC FB rects (VRAM halfwords) */ static void ensure_cpu(void) { - extern int psx_netplay_active(void); - if (!s_raster_ok || !s_gpu_dirty) return; - /* Dual-raster / netplay: CPU VRAM is written on every GP0 (or pure SW). - * Never glReadPixels — that forked peer snaps/resim. */ - if (s_cpu_auth_dual || psx_netplay_active()) { - s_gpu_dirty = 0; - return; - } - flush_flat_batch(); - flush_tex_batch(); /* realise queued textured draws before reading the FBO back */ - flush_cpu_upload(); - pack_flush(); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); - glReadPixels(0, 0, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, s_vram); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - s_gpu_dirty = 0; - coh_record(GL_COH_ENSURE, 0, 0, VRAM_W - 1, VRAM_H - 1); -} - -/* ---- GPU primitives ------------------------------------------------------ */ - -static uint64_t s_scene_prims = 0; /* frame_perf: scene primitives submitted (pre double-draw) */ -static uint64_t s_scene_prims_tex = 0; /* frame_perf: of which textured (vs flat geometry) */ -static void flush_tex_batch(void); /* fwd: drained at the backdrop-phase boundary below */ -static void mark_prim_dirty(const int *xs, const int *ys, int n, int textured) { - s_scene_prims++; - int x0 = xs[0], x1 = xs[0], y0 = ys[0], y1 = ys[0]; - for (int i = 1; i < n; i++) { - if (xs[i] < x0) x0 = xs[i]; if (xs[i] > x1) x1 = xs[i]; - if (ys[i] < y0) y0 = ys[i]; if (ys[i] > y1) y1 = ys[i]; - } - /* A sub-pixel-corrected vertex lies in [int, int+1) of the integer position - * it was rounded from, so the corrected prim can touch one more pixel on - * each max edge than the integer bbox covers. Widen before clipping so the - * pack/readback dirty rect never trails the drawn area. */ - if (s_pc_valid) { x1 += 1; y1 += 1; } - s_bdg_prims++; /* dbg: prims seen this frame (gate is now per-prim, see bd_prim_gate) */ - if (s_ptrace_n < PTRACE_CAP) { - PrimRec *p = &s_ptrace[s_ptrace_n++]; - p->x0 = (short)x0; p->x1 = (short)x1; p->y0 = (short)y0; p->y1 = (short)y1; - p->tex = (unsigned char)textured; - } - if (x0 < s_area_x1) x0 = s_area_x1; - if (y0 < s_area_y1) y0 = s_area_y1; - if (x1 > s_area_x2) x1 = s_area_x2; - if (y1 > s_area_y2) y1 = s_area_y2; - rect_add(&s_pack_dirty, x0, y0, x1, y1); - /* Dual-raster keeps CPU current via SW writes — do not mark GPU-ahead. */ - if (!s_cpu_auth_dual) - s_gpu_dirty = 1; - coh_record(GL_COH_DRAW, x0, y0, x1, y1); -} - -/* ---- native-wide mirror pass plumbing ----------------------------------- * - * Re-issue an already-set-up draw (program/VAO/VBO/blend/stencil bound) into - * the active wide surface. The geometry positions are identical on the host - * side; the x translation (wide_dx) and the wider clip are applied entirely in - * the vertex shader via u_xoff / u_xhalf, so the SAME glDrawArrays produces the - * shifted copy. wide_target_begin binds the wide FBO + viewport + scissor and - * sets the projection uniforms; wide_target_end restores u_xoff=0/u_xhalf=512 - * on the active program so the next canonical pass is bit-identical. The - * caller stays inside hr_begin/hr_end; hr_end unbinds and the next hr_begin - * resets the viewport, so no viewport restore is needed mid-function. - * - * The scissor X is the FULL wide surface (NOT the draw area): the SW reference - * (rt_wide()) deliberately lets the shifted geometry fill the revealed 16:9 - * margins that lie OUTSIDE the game's 4:3 draw-area x-range; scissoring X to - * the (translated) draw area would crop exactly the margin content native-wide - * exists to reveal. The scissor Y stays clamped to the DRAW AREA, exactly like - * rt_wide() (t.cy1/cy2 = g_clip_y1/y2): native-wide only widens X. A full-height - * Y scissor let draws that canonically clip at a vertical double-buffer band - * boundary (MMX6: draw area alternates y=0/y=240, both bands in ONE wide - * surface) bleed into the OTHER band's rows — presented one frame later as - * top/bottom edge flicker (16:9 GL only). */ -static void wide_target_begin(int dx, GLint uXoff, GLint uXhalf) { - if (s_ws_ablate != 3) /* ablate 3: no FBO rebind (draws land in hr — perf probe) */ - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, g_wide_cur); - glViewport(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); - glEnable(GL_SCISSOR_TEST); - { - int sy = s_area_y1, sh = s_area_y2 - s_area_y1 + 1; - if (sy < 0) { sh += sy; sy = 0; } - if (sy + sh > VRAM_H) sh = VRAM_H - sy; - if (sh < 0) sh = 0; - glScissor(0, sy * s_scale, g_wide_w * s_scale, sh * s_scale); - } - p_glUniform1f(uXoff, (float)dx); - p_glUniform1f(uXhalf, (float)g_wide_w / 2.0f); -} -static void wide_target_end(GLint uXoff, GLint uXhalf) { - p_glUniform1f(uXoff, 0.0f); - p_glUniform1f(uXhalf, 512.0f); - if (s_ws_ablate != 3) - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); -} - -extern int psx_ws_prim_is_tagged(void); /* gpu.c: is the current GP0 prim sprite-tagged? */ -extern int psx_ws_prim_in_backdrop(void); /* gpu.c: is its source addr in the flower-field struct? */ -extern int gpu_ws_nw_flat_backdrop_enabled(void); /* gpu.c: per-title flat backdrop opt-in */ - -/* Per-prim gate: stretch this prim iff native-wide + feature on AND the prim's - * source address is inside the flower-field backdrop data structure (precise — - * excludes the 3D rock/foreground, which is untagged AND has narrow prims so the - * earlier tag/narrow heuristic tore it). mode!=0 falls back to the old - * tag+narrow heuristic for A/B. */ -static int bd_prim_gate(const int *xs, int n, int textured) { - if (g_wide_w <= 0 || !g_ws_bd_stretch_on) return 0; - /* Some games draw their authored 4:3 sky/water as flat-colour polygons. - * Stretch those only in the native-wide mirror: the canonical framebuffer - * remains byte-for-byte 4:3, while the flat backdrop reaches the reveal - * margins. Opt-in because flat foreground geometry is title-dependent. */ - if (!textured && gpu_ws_nw_flat_backdrop_enabled()) return 1; - if (g_ws_bd_phase_mode != 0) return psx_ws_prim_in_backdrop(); /* default: precise address gate */ - /* mode 0: legacy tag+narrow heuristic (kept for comparison) */ - int native_w = g_wide_w - 2 * g_wide_off; - if (native_w <= 0) return 0; - if (psx_ws_prim_is_tagged()) return 0; - int base = g_wide_cur_base, lo = xs[0], hi = xs[0]; - for (int i = 1; i < n; i++) { if (xs[i] < lo) lo = xs[i]; if (xs[i] > hi) hi = xs[i]; } - if (lo < base - g_ws_bd_phase_thresh) return 0; /* into left margin -> GTE-wide */ - if (hi > base + native_w + g_ws_bd_phase_thresh) return 0; /* into right margin -> GTE-wide */ - return 1; -} - -/* ---- native-wide FAST path (skip redundant center mirror) ----------------- * - * The wide surface's CENTRE columns [g_wide_off, g_wide_off+native_w) are, by - * construction, identical to the canonical 4:3 framebuffer. So instead of - * re-rasterizing every primitive into the wide surface (the "mirror" pass — the - * dominant native-wide GPU cost, ~2x scene fill), we copy the canonical centre - * into the wide surface once at present (blit_wide_center_from_canonical), and - * the per-prim mirror only needs to produce the reveal MARGINS. Any prim/batch - * whose x-range is fully inside the 4:3 frame contributes nothing to the margins, - * so its mirror is skipped entirely. Correctness does not depend on the skip - * being precise: the centre is authoritatively overwritten by the blit, so the - * ONLY requirement is that a margin-reaching prim is NOT skipped — hence the - * conservative strict-inside test. 4:3 never runs any of this (g_wide_cur == 0). - * Toggle via gl_wide_fast for A/B; default ON. */ -static int s_wide_fast = 1; -void gl_renderer_set_wide_fast(int on) { s_wide_fast = on ? 1 : 0; } -int gl_renderer_get_wide_fast(void) { return s_wide_fast; } -static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h); /* def below */ -/* True if [lo,hi] (canonical draw-x) lies strictly inside the 4:3 frame, so the - * prim adds nothing to either reveal margin and its mirror can be skipped. */ -static int mirror_x_center_only(int lo, int hi) { - if (!s_wide_fast) return 0; - int base = g_wide_cur_base, native_w = g_wide_w - 2 * g_wide_off; - if (native_w <= 0) return 0; - return (lo >= base) && (hi < base + native_w); -} -static int mirror_geo_center_only(const int *xs, int n) { - if (!s_wide_fast) return 0; - int lo = xs[0], hi = xs[0]; - for (int i = 1; i < n; i++) { if (xs[i] < lo) lo = xs[i]; if (xs[i] > hi) hi = xs[i]; } - return mirror_x_center_only(lo, hi); -} -/* mirror_batch_center_only (textured-batch variant) is defined after s_tb below. */ - -/* Set / clear the 2D-backdrop x-stretch for a wide-mirror draw, per the current - * gate (s_bd_gate, set by the caller from bd_prim_gate / the batch gate). */ -static void wide_set_bd_scale(GLint uScale, GLint uCenter) { - extern int g_ws_tex_edge_pct; - float scale = 1.0f, center = 0.0f; - if (s_bd_gate && g_ws_bd_stretch_on && g_wide_w > 0) { - int native_w = g_wide_w - 2 * g_wide_off; - if (native_w > 0) { - scale = g_ws_bd_stretch_pct > 0 ? (float)g_ws_bd_stretch_pct / 100.0f - : (float)g_wide_w / (float)native_w; - if (s_bd_gate == 2) { - scale = g_ws_tex_edge_pct > 0 - ? (float)g_ws_tex_edge_pct / 100.0f : scale; - scale = -scale; /* shader: expand only beyond canonical edges */ - } - center = (float)g_wide_cur_base + (float)native_w / 2.0f; - } - } - if (scale != 1.0f) s_bdg_applied++; - p_glUniform1f(uScale, scale); - p_glUniform1f(uCenter, center); -} -static void wide_clear_bd_scale(GLint uScale, GLint uCenter) { - p_glUniform1f(uScale, 1.0f); - p_glUniform1f(uCenter, 0.0f); -} - -/* ---- textured-prim batching -------------------------------------------- * - * Consecutive textured prims sharing blend/mask/texwindow/filter coalesce into - * one draw. Per-prim texture state (texpage/clut/depth/raw/uv-limits) rides in - * the vertex (TEXV flat attributes), so only `semi` (blend) and the global - * mask/twin/filter are batch keys. flush_tex_batch() draws the queued verts; it - * is called before any op that reads VRAM, writes it outside the batch, or - * changes batch state (see callers: flush_cpu_upload, flush_pack_if_sampling, - * every non-textured glb_ wrapper, and the present path). Drawing reads only the - * already-coherent texture (per-prim coherency was ensured at append time), so - * flush_tex_batch never re-enters those helpers. */ -#define TEXBATCH_MAXV 8190 /* multiple of 3; ~2730 tris */ -static float s_tb[TEXBATCH_MAXV * TEXV]; -static int s_tb_n = 0; /* verts queued */ -static int s_tb_semi = -2; -static int s_tb_mask = 0, s_tb_filter = 0; -static int s_tb_twin[4] = {0, 0, 0, 0}; -static uint64_t s_batch_total = 0, s_batch_reason[7]; - -void gl_renderer_batch_diag(uint64_t out[8]) { - out[0] = s_batch_total; - for (int i = 0; i < 7; i++) out[i + 1] = s_batch_reason[i]; -} - -/* Draw the queued textured batch with correct PSX mask-bit handling AND correct - * painter's order. The mask bit lives in both the colour-attachment alpha - * (frag.a) and the stencil; STP=1 texels must always set it. - * - * OPAQUE batch (semi < 0): the STP bit does NOT gate COLOUR (every texel is - * opaque), so colour is drawn in ONE ordered pass over all texels — frag.a still - * carries the per-texel mask bit, so the alpha mask is correct — followed by a - * COLOUR-MASKED pass that only fixes the STENCIL for STP=1 texels. The old code - * split colour into STP=0 (pass 1) then STP=1 (pass 2) across the WHOLE batch, - * which let a behind prim's STP=1 texels overwrite a front prim's STP=0 colour - * (the Tomba character drew behind an AP-block's letters / a save post on GL - * only). Same draw count, order preserved, mask preserved. - * - * SEMI batch (semi >= 0): genuine two-pass — STP=0 texels opaque, STP=1 texels - * blended per the PSX mode. Cross-prim order is kept by isolating semi prims to - * one per batch (see gpu_textured_triangle), so this batch holds a single prim - * whose two passes do not self-overlap. */ -static void tex_batch_draw_passes(int nverts, int semi) { - p_glUniform1i(s_uSemimode, semi < 0 ? 0 : semi); - if (semi < 0) { - glDisable(GL_BLEND); - mask_stencil(s_tb_mask); - p_glUniform1i(s_uSemipass, 0); /* all texels, one ordered colour pass */ - glDrawArrays(GL_TRIANGLES, 0, nverts); - if (s_mask_check) { - glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); /* stencil-only fixup */ - mask_stencil(1); - p_glUniform1i(s_uSemipass, 2); /* STP=1 texels set the mask bit */ - glDrawArrays(GL_TRIANGLES, 0, nverts); - glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - } else if (!s_tb_mask) { - /* Alpha is already exact; defer its duplicate stencil encoding - * until a later GP0(E6h) actually enables destination masking. */ - s_stencil_valid = 0; - } - } else if (!s_mask_check && semi == 4) { - /* Modes 0/1/3 can select opaque-vs-semi behavior per fragment with - * dual-source factors, so the whole painter-ordered batch is one draw. - * Mode 2 needs a different blend equation and stays on the conservative - * isolated path below. */ - glEnable(GL_BLEND); - p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); - p_glBlendFuncSeparate(GL_ONE, PSXGL_SRC1_ALPHA, GL_ONE, GL_ZERO); - if (s_tb_mask) mask_stencil(1); else glDisable(GL_STENCIL_TEST); - p_glUniform1i(s_uSemipass, 0); - glDrawArrays(GL_TRIANGLES, 0, nverts); - if (!s_tb_mask) s_stencil_valid = 0; - } else { - glDisable(GL_BLEND); /* Pass 1: STP=0 texels (opaque) */ - mask_stencil(s_tb_mask); - p_glUniform1i(s_uSemipass, 1); - glDrawArrays(GL_TRIANGLES, 0, nverts); - apply_psx_blend(semi); /* Pass 2: STP=1 texels (blended) */ - mask_stencil(1); - p_glUniform1i(s_uSemipass, 2); - glDrawArrays(GL_TRIANGLES, 0, nverts); - } -} - -/* ---- frame_perf CPU attribution (native-wide wedge hunt) ----------------- * - * Per-frame CPU wall time spent inside the GL submission paths (driver CPU - * cost surfaces INSIDE our gl* calls) + counters for the wide plumbing, so a - * CPU-bound wide frame (emu_cpu >> scene_gpu) can be attributed without a - * sampling profiler. Reset at present enter; reported by frame_perf. */ -static double cw_ms(void) { - static double freq = 0.0; - if (freq == 0.0) { - uint64_t f = SDL_GetPerformanceFrequency(); - freq = f ? (double)f : 1.0; - } - return (double)SDL_GetPerformanceCounter() * 1000.0 / freq; -} -static double s_cw_flush_ms = 0.0; /* CPU wall inside flush_tex_batch */ -static double s_cw_wide_ms = 0.0; /* CPU wall inside glb_wide_* entry points */ -static int s_cw_batches = 0, s_cw_wide_sets = 0, s_cw_wide_cfgs = 0, - s_cw_wide_clears = 0, s_cw_fbo_creates = 0, s_cw_flush_depth = 0; - -/* Textured-batch variant of mirror_x_center_only: scan the queued verts' x - * (attr 0, stride TEXV). Defined here so s_tb / TEXV are in scope. */ -static int mirror_batch_center_only(int nverts) { - if (!s_wide_fast || nverts <= 0) return 0; - float flo = s_tb[0], fhi = s_tb[0]; - for (int i = 1; i < nverts; i++) { - float x = s_tb[i * TEXV]; - if (x < flo) flo = x; if (x > fhi) fhi = x; - } - /* floor/ceil, not a truncating cast: with geometry_correction these are - * fractional, and (int) rounds toward zero — which WIDENS a negative x - * toward the centre and could wrongly call a margin-touching batch - * centre-only, dropping its wide-mirror draw. Whole values are unchanged. */ - return mirror_x_center_only((int)floorf(flo), (int)ceilf(fhi)); -} - -static void flush_tex_batch(void) { - if (s_tb_n == 0) return; - int nverts = s_tb_n, semi = s_tb_semi; - s_tb_n = 0; /* clear first: re-entrancy safe */ - double cw_t0 = cw_ms(); - s_cw_batches++; s_batch_total++; s_cw_flush_depth++; - - hr_begin(1); - p_glUseProgram(s_tex_prog); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_raw_tex); - p_glUniform1i(s_uVram, 0); - p_glUniform4i(s_uTwin, s_tb_twin[0], s_tb_twin[1], s_tb_twin[2], s_tb_twin[3]); - p_glUniform1i(s_uMaskset, s_tb_mask); - p_glUniform1i(s_uFilter, s_tb_filter); - p_glBindVertexArray(s_tex_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); - p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(nverts * TEXV * sizeof(float)), s_tb, PSXGL_STREAM_DRAW); - - tex_batch_draw_passes(nverts, semi); - - /* Native-wide mirror — skipped for a batch fully inside the 4:3 frame (its - * centre content comes from the present-time canonical blit; nothing to add - * to the margins). A backdrop-stretched batch (s_tb_gate) widens past the - * frame, so it is never treated as centre-only. */ - if (g_wide_cur && s_ws_ablate != 1 && - !(s_tb_gate == 0 && mirror_batch_center_only(nverts))) { /* native-wide mirror */ - int dx = wide_dx(); - s_bd_gate = s_tb_gate; /* this batch is uniform-gate (flushed on change) */ - gl_perf_mirror_begin(); - wide_target_begin(dx, s_tex_uXoff, s_tex_uXhalf); - wide_set_bd_scale(s_tex_uXscale, s_tex_uXcenter); - if (s_ws_ablate != 2) tex_batch_draw_passes(nverts, semi); - wide_clear_bd_scale(s_tex_uXscale, s_tex_uXcenter); - wide_target_end(s_tex_uXoff, s_tex_uXhalf); - gl_perf_mirror_end(); - } - hr_end(); - if (--s_cw_flush_depth == 0) s_cw_flush_ms += cw_ms() - cw_t0; -} - -/* Flat / gouraud GEO batch — MotK title/char-select starfields issue ~30k/s - * GP0(68h) 1x1 dots; each was two immediate gpu_triangle draws (BufferData + - * DrawArrays each). Coalesce opaque/semi-uniform tris into one draw. */ -#define FLATBATCH_MAXV 8190 /* multiple of 3 */ -static float s_fb[FLATBATCH_MAXV * 6]; -static int s_fb_n = 0; -static int s_fb_semi = -2; -static int s_fb_mask = -1; - -static int mirror_flat_batch_center_only(int nverts) { - if (!s_wide_fast || nverts <= 0) return 0; - float flo = s_fb[0], fhi = s_fb[0]; - for (int i = 1; i < nverts; i++) { - float x = s_fb[i * 6]; - if (x < flo) flo = x; if (x > fhi) fhi = x; - } - /* floor/ceil rather than a truncating cast — see mirror_batch_center_only. */ - return mirror_x_center_only((int)floorf(flo), (int)ceilf(fhi)); -} - -static void flush_flat_batch(void) { - if (s_fb_n == 0) return; - int nverts = s_fb_n, semi = s_fb_semi, mask = s_fb_mask; - s_fb_n = 0; - - hr_begin(1); - if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); - mask_stencil(mask); - p_glUseProgram(s_geo_prog); - p_glBindVertexArray(s_geo_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); - p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(nverts * 6 * sizeof(float)), - s_fb, PSXGL_STREAM_DRAW); - glDrawArrays(GL_TRIANGLES, 0, nverts); - - if (g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 && - !(!g_ws_bd_stretch_on && mirror_flat_batch_center_only(nverts))) { - int dx = wide_dx(); - /* Batch may span many prims; use stretch gate off (flat dots/UI). */ - s_bd_gate = 0; - gl_perf_mirror_begin(); - wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf); - wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter); - if (s_ws_ablate != 2) glDrawArrays(GL_TRIANGLES, 0, nverts); - wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter); - wide_target_end(s_geo_uXoff, s_geo_uXhalf); - gl_perf_mirror_end(); - } - hr_end(); -} - -/* Flat / gouraud triangles and lines share the GEO program. mode: GL_TRIANGLES - * or GL_LINES; verts are (x, y, r, g, b, a) tuples with colors as 1555. */ -static void gpu_geometry(GLenum mode, const int *xs, const int *ys, - const uint16_t *cs, int n, int semi) { - flush_tex_batch(); /* flat prim: drain textured draws first (order + program) */ - flush_cpu_upload(); /* also drains flat batch if an upload was pending */ - mark_prim_dirty(xs, ys, n, 0 /* flat */); - /* Sub-pixel positions only describe a 3-vertex projected triangle. */ - const int precise = s_pc_valid && mode == GL_TRIANGLES && n == 3; - - /* Lines stay immediate (rare); tris batch for MotK 0x68 starfields. */ - if (mode != GL_TRIANGLES || n < 3) { - flush_flat_batch(); - float verts[3 * 6]; - float mask_a = s_mask_set ? 1.0f : 0.0f; - for (int i = 0; i < n; i++) { - verts[i*6+0] = precise ? s_pc_x[i] : (float)xs[i]; - verts[i*6+1] = precise ? s_pc_y[i] : (float)ys[i]; - verts[i*6+2] = ((cs[i] & 0x1F) << 3) / 255.0f; - verts[i*6+3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f; - verts[i*6+4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f; - verts[i*6+5] = mask_a; - } - hr_begin(1); - if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); - mask_stencil(s_mask_set); - if (mode == GL_LINES) glLineWidth((float)s_scale); - p_glUseProgram(s_geo_prog); - p_glBindVertexArray(s_geo_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); - p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(n * 6 * sizeof(float)), - verts, PSXGL_STREAM_DRAW); - glDrawArrays(mode, 0, n); - if (g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 && - !(!g_ws_bd_stretch_on && mirror_geo_center_only(xs, n))) { - int dx = wide_dx(); - s_bd_gate = bd_prim_gate(xs, n, 0); - gl_perf_mirror_begin(); - wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf); - wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter); - if (s_ws_ablate != 2) glDrawArrays(mode, 0, n); - wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter); - wide_target_end(s_geo_uXoff, s_geo_uXhalf); - gl_perf_mirror_end(); - } - hr_end(); - return; - } - - if (s_fb_n > 0 && (s_fb_semi != semi || s_fb_mask != (int)s_mask_set)) - flush_flat_batch(); - if (s_fb_n + n > FLATBATCH_MAXV) - flush_flat_batch(); - s_fb_semi = semi; - s_fb_mask = (int)s_mask_set; - - float mask_a = s_mask_set ? 1.0f : 0.0f; - for (int i = 0; i < n; i++) { - float *v = &s_fb[s_fb_n * 6]; - v[0] = precise ? s_pc_x[i] : (float)xs[i]; - v[1] = precise ? s_pc_y[i] : (float)ys[i]; - v[2] = ((cs[i] & 0x1F) << 3) / 255.0f; - v[3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f; - v[4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f; - v[5] = mask_a; - s_fb_n++; - } -} - -static void gpu_triangle(int x0,int y0,uint16_t c0, int x1,int y1,uint16_t c1, - int x2,int y2,uint16_t c2, int semi) { - int xs[3] = {x0, x1, x2}, ys[3] = {y0, y1, y2}; - uint16_t cs[3] = {c0, c1, c2}; - gpu_geometry(GL_TRIANGLES, xs, ys, cs, 3, semi); -} - -static void gpu_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int semi) { - int xs[2] = {x0, x1}, ys[2] = {y0, y1}; - uint16_t cs[2] = {c0, c1}; - gpu_geometry(GL_LINES, xs, ys, cs, 2, semi); -} - -/* Shared PS1 uv-sampling model (limits + mirrored-2D compensation) — one - * implementation for GL/VK/SW, see gpu_uv.h. */ -#include "gpu_uv.h" - -/* Textured triangle. Always two passes split by the per-texel STP bit so the - * stencil (mask) write value is constant within each pass; the semi pass is - * also where PS1 blending applies. lim = uv sampling bounds (see - * tri_uv_limits); NULL computes them from the vertices. */ -static void gpu_textured_triangle(const int *xs, const int *ys, - const int *us, const int *vs, - const float *col, uint16_t texpage, - uint16_t clut_x, uint16_t clut_y, int rawtex, - int semi, const int *lim) { - int lim_buf[4]; - int uv_buf[6]; - if (!lim) { - /* Poly path: exact sampled bounds from the ORIGINAL uvs, then the - * center-sampling mirror compensation (rect prims arrive with their - * own precomputed lim and pre-bumped uvs). */ - int *mu = uv_buf, *mv = uv_buf + 3; - for (int i = 0; i < 3; i++) { mu[i] = us[i]; mv[i] = vs[i]; } - psx_uv_tri_limits(xs, ys, mu, mv, lim_buf); - psx_uv_tri_mirror_offset(xs, ys, mu, mv); - us = mu; vs = mv; - lim = lim_buf; - } - s_scene_prims_tex++; - int base_x = (texpage & 0xF) * 64; - int base_y = ((texpage >> 4) & 1) * 256; - int depth = (texpage >> 7) & 3; if (depth > 2) depth = 2; - - flush_cpu_upload(); /* if a CPU->VRAM upload is pending it flushes the batch first */ - flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y); /* flushes batch iff it must pack */ - mark_prim_dirty(xs, ys, 3, 1 /* textured */); - - /* Append to the textured batch. Flush first if this prim's blend/mask/twin/ - * filter differ from the open batch, or the buffer is full. Per-prim texture - * state goes in the vertex; only these keys force a new draw. */ - { - flush_flat_batch(); /* painter order: flat GEO before textured */ - int twx = s_tw_mask_x, twy = s_tw_mask_y, tox = s_tw_off_x, toy = s_tw_off_y; - int gate = bd_prim_gate(xs, 3, 1); /* backdrop-stretch gate is also a batch key */ - /* Batch key: keep opaque as -1. Dual-source (4) is only for semi modes - * 0/1/3 when mask-check is off — never coalesce opaque into that key. - * Mixing opaque+semi under u_semimode==4 made additive particle/glow - * batches (CTR Naughty Dog intro binary funnel) paint over later - * opaque crate flaps whenever submission order and STP bits disagreed - * with the dual-source path's assumptions. */ - int batch_semi; - if (semi < 0) - batch_semi = -1; - else if (!s_mask_check && semi != 2) - batch_semi = 4; - else - batch_semi = semi; - /* STP draw-ORDER correctness. flush_tex_batch's conservative two-pass - * path draws pass 1 = every prim's STP=0 texels then pass 2 = every - * prim's STP=1 texels — a behind prim's semi texels then overwrite a - * front prim's opaque texels (Tomba AP-block / CTR intro flaps). The - * dual-source single-pass path avoids that WITHIN one prim, but - * batching many overlapping semi quads (digit particles + glow) still - * mis-orders against neighbouring opaque geometry. Isolate EVERY - * semi-transparent textured prim: drain the open batch, draw this - * prim alone (composited fully before the next), let opaque prims - * keep batching. Cost is one draw per semi prim. */ - int isolate = (semi >= 0); - int reason = -1; - if (s_tb_n > 0) { - if (isolate) reason = 0; - else if (batch_semi != s_tb_semi) reason = 1; - else if (s_mask_set != s_tb_mask) reason = 2; - else if (s_tex_filter != s_tb_filter) reason = 3; - else if (gate != s_tb_gate) reason = 4; - else if (twx != s_tb_twin[0] || twy != s_tb_twin[1] || - tox != s_tb_twin[2] || toy != s_tb_twin[3]) reason = 5; - } - if (reason >= 0) { - s_batch_reason[reason]++; - flush_tex_batch(); - } - if (s_tb_n + 3 > TEXBATCH_MAXV) { s_batch_reason[6]++; flush_tex_batch(); } - if (s_tb_n == 0) { /* opening a batch: capture its keyed state */ - s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = s_tex_filter; s_tb_gate = gate; - s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy; - } - float *vp = &s_tb[s_tb_n * TEXV]; - for (int i = 0; i < 3; i++, vp += TEXV) { - vp[0] = s_pc_valid ? s_pc_x[i] : (float)xs[i]; - vp[1] = s_pc_valid ? s_pc_y[i] : (float)ys[i]; - vp[2] = (float)us[i]; vp[3] = (float)vs[i]; - vp[4] = col[i*3+0]; vp[5] = col[i*3+1]; vp[6] = col[i*3+2]; vp[7] = 1.0f; - vp[8] = (float)base_x; vp[9] = (float)base_y; /* a_tpage */ - vp[10] = (float)clut_x; vp[11] = (float)clut_y; /* a_clut */ - vp[12] = (float)depth; vp[13] = (float)rawtex; /* a_depth, a_raw */ - vp[14] = (float)lim[0]; vp[15] = (float)lim[1]; /* a_limits */ - vp[16] = (float)lim[2]; vp[17] = (float)lim[3]; - vp[18] = semi >= 0 ? (float)(semi + 1) : 0.0f; /* a_semi code */ - vp[19] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */ - } - s_tb_n += 3; - if (isolate) flush_tex_batch(); /* draw this semi prim alone, in submission order */ - } -} - -/* Draw a flat-colored rect (GEO program) DIRECTLY into the active wide surface - * at wide-space coords [wx, wx+ww) × [y, y+h). Used only by the full-screen- - * overlay path; positions are already in wide space so u_xoff stays 0. Mirrors - * raster_flat_rect(&wt, ...) in sw_draw_flat_rect. Caller stays inside - * hr_begin/hr_end. */ -static void wide_flat_rect_direct(int wx, int y, int ww, int h, uint16_t c, int semi) { - if (ww <= 0 || h <= 0) return; - float r = ((c & 0x1F) << 3) / 255.0f; - float g = (((c >> 5) & 0x1F) << 3) / 255.0f; - float b = (((c >> 10) & 0x1F) << 3) / 255.0f; - float a = s_mask_set ? 1.0f : 0.0f; - float fx0 = (float)wx, fy0 = (float)y, fx1 = (float)(wx + ww), fy1 = (float)(y + h); - float verts[6 * 6] = { - fx0,fy0,r,g,b,a, fx1,fy0,r,g,b,a, fx0,fy1,r,g,b,a, - fx1,fy0,r,g,b,a, fx0,fy1,r,g,b,a, fx1,fy1,r,g,b,a, - }; - /* Wide target: positions already wide-space so u_xoff = 0; full-width - * scissor; u_xhalf = g_wide_w/2. */ - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, g_wide_cur); - glViewport(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); - glEnable(GL_SCISSOR_TEST); - glScissor(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); /* full surface (rt_wide) */ - if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); - mask_stencil(s_mask_set); - p_glUseProgram(s_geo_prog); - p_glUniform1f(s_geo_uXoff, 0.0f); - p_glUniform1f(s_geo_uXhalf, (float)g_wide_w / 2.0f); - p_glBindVertexArray(s_geo_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); - p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); - glDrawArrays(GL_TRIANGLES, 0, 6); - p_glUniform1f(s_geo_uXoff, 0.0f); - p_glUniform1f(s_geo_uXhalf, 512.0f); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); -} - -static void gpu_flat_rect(int x,int y,int w,int h,uint16_t c,int semi) { - if (w <= 0 || h <= 0) return; - /* Full-screen 2D overlay (pause gray-filter / load fade): a flat rect - * spanning the whole 4:3 framebuffer must cover the whole wide surface too, - * else the revealed 16:9 margins are left undimmed/unfaded. Same detection - * as sw_draw_flat_rect: native_w = the 4:3 framebuffer width (g_wide_w less - * the per-side reveal on both sides); the rect's native screen-X span - * (x - base) must cover [0, native_w). When it does, the two canonical - * triangles are drawn WITHOUT the per-triangle wide mirror (suppressed) and - * a single full-width rect is drawn into the wide surface instead; every - * other rect mirrors 1:1 via the generic gpu_geometry path. Only runs in - * native-wide (g_wide_cur != 0), so 4:3 is unaffected. */ - int overlay = 0; - if (g_wide_cur) { - int native_w = g_wide_w - 2 * g_wide_off; - int lx = x - g_wide_cur_base, rx = x + w - g_wide_cur_base; - overlay = (native_w > 0 && lx <= 0 && rx >= native_w); - } - if (overlay) s_wide_suppress = 1; - gpu_triangle(x, y, c, x+w, y, c, x, y+h, c, semi); - gpu_triangle(x+w, y, c, x, y+h, c, x+w, y+h, c, semi); - if (overlay) { - s_wide_suppress = 0; - /* The two canonical triangles drew into the hr FBO already (each - * gpu_triangle ran its own hr_begin/hr_end). Re-open the bracket just - * for the full-width wide pass so blend/scissor/program state is - * clean. */ - if (s_ws_ablate != 1) { - hr_begin(0); - gl_perf_mirror_begin(); - wide_flat_rect_direct(0, y, g_wide_w, h, c, semi); - gl_perf_mirror_end(); - hr_end(); - } - } -} - -static void gpu_textured_rect(int x,int y,int w,int h, - int u0,int v0,int u1,int v1, - uint16_t clut_x,uint16_t clut_y,uint16_t tp,int semi) { - if (w <= 0 || h <= 0) return; - float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f; - float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb}; - /* gpu.c routes axis-aligned MIRRORED quads (X/Y-flipped 2D sprites, - * e.g. right-facing MMX entities) through THIS path as scaled rects - * with u0>u1 / v0>v1 — they never reach the poly path. Exact bounds - * from the original corners, then the mirror bump (see gpu_uv.h). */ - int lim[4]; - psx_uv_rect_limits(u0, v0, u1, v1, lim); - psx_uv_rect_mirror_offset(&u0, &v0, &u1, &v1); - int xs1[3]={x, x+w, x}, ys1[3]={y, y, y+h}; - int us1[3]={u0,u1,u0}, vs1[3]={v0,v0,v1}; - gpu_textured_triangle(xs1,ys1,us1,vs1,col,tp,clut_x,clut_y,s_mod_raw,semi,lim); - int xs2[3]={x+w, x, x+w}, ys2[3]={y, y+h, y+h}; - int us2[3]={u1,u0,u1}, vs2[3]={v0,v1,v1}; - gpu_textured_triangle(xs2,ys2,us2,vs2,col,tp,clut_x,clut_y,s_mod_raw,semi,lim); -} - -/* GP0(02h) fill: writes color with bit15=0, ignoring draw area, mask and - * offset; coordinates wrap. A scissored clear (color + stencil) per wrapped - * segment is exactly this. */ -static void fill_segment(int x, int y, int w, int h, float r, float g, float b) { - if (w <= 0 || h <= 0) return; - glScissor(x * s_scale, y * s_scale, w * s_scale, h * s_scale); - glClearColor(r, g, b, 0.0f); - glClearStencil(0); - glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1); -} - -static void gpu_fill(int x,int y,int w,int h,uint16_t c) { - if (w <= 0 || h <= 0) return; - flush_flat_batch(); - flush_tex_batch(); - flush_cpu_upload(); - float r=(c&0x1F)/31.0f, g=((c>>5)&0x1F)/31.0f, b=((c>>10)&0x1F)/31.0f; - x &= VRAM_W - 1; y &= VRAM_H - 1; - if (w > VRAM_W) w = VRAM_W; - if (h > VRAM_H) h = VRAM_H; - int w1 = w, w2 = 0, h1 = h, h2 = 0; - if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; } - if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; } - - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); - glViewport(0, 0, VRAM_W * s_scale, VRAM_H * s_scale); - glEnable(GL_SCISSOR_TEST); - fill_segment(x, y, w1, h1, r, g, b); - if (w2) fill_segment(0, y, w2, h1, r, g, b); - if (h2) fill_segment(x, 0, w1, h2, r, g, b); - if (w2 && h2) fill_segment(0, 0, w2, h2, r, g, b); - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - if (!s_cpu_auth_dual) - s_gpu_dirty = 1; - coh_record(GL_COH_FILL, x, y, x + w - 1, y + h - 1); -} - -/* VRAM->VRAM copy: blit the source region to the scratch texture (resolves - * overlap), then draw it back at the destination through the BLIT program so - * mask set/check and the stencil mirror apply, exactly like sw_copy_rect. */ -static void gpu_copy_rect(int sx,int sy,int dx,int dy,int w,int h) { - if (w <= 0 || h <= 0) return; - flush_flat_batch(); - flush_tex_batch(); - flush_cpu_upload(); - /* Clamp to bounds (the software path wraps; wrapping copies are unused - * in practice — see the file header). */ - if (sx < 0) sx = 0; if (sy < 0) sy = 0; - if (dx < 0) dx = 0; if (dy < 0) dy = 0; - if (sx + w > VRAM_W) w = VRAM_W - sx; - if (dx + w > VRAM_W) w = VRAM_W - dx; - if (sy + h > VRAM_H) h = VRAM_H - sy; - if (dy + h > VRAM_H) h = VRAM_H - dy; - if (w <= 0 || h <= 0) return; - - int S = s_scale; - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo); - glDisable(GL_SCISSOR_TEST); - p_glBlitFramebuffer(sx*S, sy*S, (sx+w)*S, (sy+h)*S, - sx*S, sy*S, (sx+w)*S, (sy+h)*S, - GL_COLOR_BUFFER_BIT, GL_NEAREST); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); - - hr_begin(0); /* copies ignore the draw area */ - glScissor(dx*S, dy*S, w*S, h*S); - glDisable(GL_BLEND); - p_glUseProgram(s_blit_prog); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_scratch_tex); - p_glUniform1i(s_uBlitSrc, 0); - p_glUniform1i(s_uBlitMaskset, s_mask_set); - /* Scratch holds the source at its own hr coords: texel = frag + (src-dst)*S. */ - p_glUniform1i(s_uBlitSrcDiv, 1); - p_glUniform2i(s_uBlitSrcOff, (sx - dx) * S, (sy - dy) * S); - float fx0 = (float)dx, fy0 = (float)dy, fx1 = (float)(dx + w), fy1 = (float)(dy + h); - float verts[6 * 2] = { - fx0, fy0, fx1, fy0, fx0, fy1, - fx1, fy0, fx0, fy1, fx1, fy1, - }; - p_glBindVertexArray(s_blit_vao); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); - p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); - /* Two passes split by source bit15 so stencil tracks the copied mask. */ - mask_stencil(s_mask_set); p_glUniform1i(s_uBlitPass, 1); glDrawArrays(GL_TRIANGLES, 0, 6); - mask_stencil(1); p_glUniform1i(s_uBlitPass, 2); glDrawArrays(GL_TRIANGLES, 0, 6); - hr_end(); - - rect_add(&s_pack_dirty, dx, dy, dx + w - 1, dy + h - 1); - if (!s_cpu_auth_dual) - s_gpu_dirty = 1; - coh_record(GL_COH_COPY_SRC, sx, sy, sx + w - 1, sy + h - 1); - coh_record(GL_COH_COPY, dx, dy, dx + w - 1, dy + h - 1); -} - -/* ---- backend vtable wrappers ------------------------------------------- */ -static void glb_init(uint16_t *vram) { s_vram = vram; sw_renderer_init(vram); } - -/* Under GL the internal-resolution scale lives in the hr FBO; the CPU-side - * (software mirror, readbacks, screenshots) stays native, so the reported - * scale is 1 and the software hi-res mirror stays off. */ -static void glb_set_scale(int s) { - if (s < 1) s = 1; - if (s > GL_MAX_INTERNAL_SCALE) s = GL_MAX_INTERNAL_SCALE; - s_req_scale = s; - sw_renderer_set_scale(1); -} -static int glb_scale(void) { return s_scale; } /* real internal SSAA scale (was a stub 1; the - native-wide CPU present path + gr_scale() callers - need the true scale — the FBO-direct present is - unaffected since it never reads gr_scale()) */ -static void glb_set_texture_filter(int b) { s_tex_filter = b ? 1 : 0; sw_set_texture_filter(b); } -static int glb_texture_filter(void) { return s_tex_filter; } - -static void glb_set_semi_transparency(int e, int m) { s_semi_en = e; s_semi_mode = m & 3; sw_set_semi_transparency(e, m); } -static void glb_set_mask_bits(int s, int c) { - int next_check = c ? 1 : 0; - if (next_check && !s_mask_check) { - /* Land all alpha-authoritative work before deriving stencil from it. */ - flush_tex_batch(); - flush_cpu_upload(); - rebuild_mask_stencils(); - } - s_mask_set = s ? 1 : 0; - s_mask_check = next_check; - sw_set_mask_bits(s, c); -} -static void glb_set_texture_window(uint32_t r) { - s_tw_mask_x = (int)(r & 0x1F); - s_tw_mask_y = (int)((r >> 5) & 0x1F); - s_tw_off_x = (int)((r >> 10) & 0x1F); - s_tw_off_y = (int)((r >> 15) & 0x1F); - sw_set_texture_window(r); -} -static void glb_set_color_modulation(int r,int g,int b,int raw) { s_mod_r=r; s_mod_g=g; s_mod_b=b; s_mod_raw=raw; sw_set_color_modulation(r,g,b,raw); } -/* Sub-pixel / perspective overrides for the next triangle. Forwarded to the - * software rasterizer too: pre-context draws (s_raster_ok == 0) still go there, - * and the GL backend delegates whole prim classes to it. */ -static void glb_set_precise_triangle(int enabled, - int32_t x0,int32_t y0, int32_t x1,int32_t y1, - int32_t x2,int32_t y2) { - s_pc_valid = enabled ? 1 : 0; - if (s_pc_valid) { - /* 16.16 native VRAM px -> float. The GL pipeline is float end-to-end, - * so the fraction survives to the rasterizer at any internal scale. */ - s_pc_x[0] = (float)x0 / 65536.0f; s_pc_y[0] = (float)y0 / 65536.0f; - s_pc_x[1] = (float)x1 / 65536.0f; s_pc_y[1] = (float)y1 / 65536.0f; - s_pc_x[2] = (float)x2 / 65536.0f; s_pc_y[2] = (float)y2 / 65536.0f; - } - sw_set_precise_triangle(enabled, x0,y0, x1,y1, x2,y2); -} -static void glb_set_perspective_triangle(int enabled, float q0, float q1, float q2) { - s_pq_valid = (enabled && q0 > 0.0f && q1 > 0.0f && q2 > 0.0f) ? 1 : 0; - s_pq[0] = q0; s_pq[1] = q1; s_pq[2] = q2; - sw_set_perspective_triangle(enabled, q0, q1, q2); -} -static void glb_set_draw_area(int x1,int y1,int x2,int y2) { flush_flat_batch(); flush_tex_batch(); s_area_x1=x1; s_area_y1=y1; s_area_x2=x2; s_area_y2=y2; sw_set_draw_area(x1,y1,x2,y2); } -static void glb_get_draw_area(int *x1,int *y1,int *x2,int *y2) { sw_get_draw_area(x1,y1,x2,y2); } -static void glb_set_draw_offset(int x,int y) { flush_flat_batch(); flush_tex_batch(); s_off_x=x; s_off_y=y; sw_set_draw_offset(x,y); } - -/* Pre-context draws (s_raster_ok == 0) fall back to the software rasterizer - * over CPU VRAM; the initial full-VRAM upload at context init folds them in. - * Offline post-init is GPU-only (FBO-auth). Netplay dual-raster always writes - * SW @ 1× for authority, then GPU @ s_scale for present quality. */ -/* The sub-pixel / perspective override describes exactly one triangle; drop it - * once that triangle has been submitted so a later prim can never inherit it. */ -static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; } - -static void glb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col) { - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col); - if (!s_raster_ok) return; - gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1); - precise_consumed(); -} -static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2) { - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2); - if (!s_raster_ok) return; - gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1); - precise_consumed(); -} -static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_fill_rect(x,y,w,h,c); - if (!s_raster_ok) return; - gpu_fill(x,y,w,h,c); -} -static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_copy_rect(sx,sy,dx,dy,w,h); - if (!s_raster_ok) return; - gpu_copy_rect(sx,sy,dx,dy,w,h); -} -static void glb_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,int x2,int y2,int u2,int v2,uint16_t cx,uint16_t cy,uint16_t tp){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp); - if (!s_raster_ok) return; - int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2}; - float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f; - float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb}; - gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL); - precise_consumed(); -} -static void glb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,int x1,int y1,int u1,int v1,uint32_t c1,int x2,int y2,int u2,int v2,uint32_t c2,uint16_t cx,uint16_t cy,uint16_t tp,int raw){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); - if (!s_raster_ok) return; - int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2}; - uint32_t cc[3]={c0,c1,c2}; float col[9]; - for (int i=0;i<3;i++){ col[i*3+0]=(cc[i]&0xFF)/255.0f; col[i*3+1]=((cc[i]>>8)&0xFF)/255.0f; col[i*3+2]=((cc[i]>>16)&0xFF)/255.0f; } - gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL); - precise_consumed(); -} -static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_flat_rect(x,y,w,h,c); - if (!s_raster_ok) return; - gpu_flat_rect(x,y,w,h,c, s_semi_en?s_semi_mode:-1); -} -static void glb_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t cx,uint16_t cy,uint16_t tp){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp); - if (!s_raster_ok) return; - gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1); -} -static void glb_draw_textured_rect_scaled(int x,int y,int w,int h,int u0,int v0,int u1,int v1,uint16_t cx,uint16_t cy,uint16_t tp){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp); - if (!s_raster_ok) return; - gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1); -} -static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_line(x0,y0,x1,y1,c); - if (!s_raster_ok) return; - gpu_line(x0,y0,c, x1,y1,c, s_semi_en?s_semi_mode:-1); -} -static void glb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1){ - if (s_cpu_auth_dual || !s_raster_ok) - sw_draw_shaded_line(x0,y0,c0,x1,y1,c1); - if (!s_raster_ok) return; - gpu_line(x0,y0,c0, x1,y1,c1, s_semi_en?s_semi_mode:-1); -} -static int glb_render_display(uint32_t *o,int p,int dx,int dy,int dw,int dh){ ensure_cpu(); return sw_render_display(o,p,dx,dy,dw,dh); } -static int glb_render_display_hires(uint32_t *o,int p,int dx,int dy,int dw,int dh){ ensure_cpu(); return sw_render_display_hires(o,p,dx,dy,dw,dh); } -/* While GP1 depth24 is on, packed RGB888 lives in the CPU mirror and is - * presented via gl_renderer_present — never as 1555 FBO texels. Queuing those - * MDEC A0 rects hits UP_RECTS_MAX (16) and force-flushes mid-movie (MotK intro - * ~50→~30 FPS). Skip ONLY framebuffer-sized transfers (RGB888); still upload - * smaller texture A0s so post-FMV menus keep VRAM pages coherent. - * On leave: clear the skipped FB union in the FBO — do NOT restage CPU RGB888 - * as 1555 (that painted MotK title rainbow/static). */ + extern int psx_netplay_active(void); + if (!s_raster_ok || !s_gpu_dirty) return; + /* Dual-raster / netplay: CPU VRAM is written on every GP0 (or pure SW). + * Never glReadPixels — that forked peer snaps/resim. */ + if (s_cpu_auth_dual || psx_netplay_active()) { + s_gpu_dirty = 0; + return; + } + flush_flat_batch(); + flush_tex_batch(); /* realise queued textured draws before reading the FBO back */ + flush_cpu_upload(); + pack_flush(); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); + glReadPixels(0, 0, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, s_vram); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + s_gpu_dirty = 0; + coh_record(GL_COH_ENSURE, 0, 0, VRAM_W - 1, VRAM_H - 1); +} + +/* ---- GPU primitives ------------------------------------------------------ */ + +static uint64_t s_scene_prims = 0; /* frame_perf: scene primitives submitted (pre double-draw) */ +static uint64_t s_scene_prims_tex = 0; /* frame_perf: of which textured (vs flat geometry) */ +static void flush_tex_batch(void); /* fwd: drained at the backdrop-phase boundary below */ +static void mark_prim_dirty(const int *xs, const int *ys, int n, int textured) { + s_scene_prims++; + int x0 = xs[0], x1 = xs[0], y0 = ys[0], y1 = ys[0]; + for (int i = 1; i < n; i++) { + if (xs[i] < x0) x0 = xs[i]; if (xs[i] > x1) x1 = xs[i]; + if (ys[i] < y0) y0 = ys[i]; if (ys[i] > y1) y1 = ys[i]; + } + /* A sub-pixel-corrected vertex lies in [int, int+1) of the integer position + * it was rounded from, so the corrected prim can touch one more pixel on + * each max edge than the integer bbox covers. Widen before clipping so the + * pack/readback dirty rect never trails the drawn area. */ + if (s_pc_valid) { x1 += 1; y1 += 1; } + s_bdg_prims++; /* dbg: prims seen this frame (gate is now per-prim, see bd_prim_gate) */ + if (s_ptrace_n < PTRACE_CAP) { + PrimRec *p = &s_ptrace[s_ptrace_n++]; + p->x0 = (short)x0; p->x1 = (short)x1; p->y0 = (short)y0; p->y1 = (short)y1; + p->tex = (unsigned char)textured; + } + if (x0 < s_area_x1) x0 = s_area_x1; + if (y0 < s_area_y1) y0 = s_area_y1; + if (x1 > s_area_x2) x1 = s_area_x2; + if (y1 > s_area_y2) y1 = s_area_y2; + rect_add(&s_pack_dirty, x0, y0, x1, y1); + /* Dual-raster keeps CPU current via SW writes — do not mark GPU-ahead. */ + if (!s_cpu_auth_dual) + s_gpu_dirty = 1; + coh_record(GL_COH_DRAW, x0, y0, x1, y1); +} + +/* ---- native-wide mirror pass plumbing ----------------------------------- * + * Re-issue an already-set-up draw (program/VAO/VBO/blend/stencil bound) into + * the active wide surface. The geometry positions are identical on the host + * side; the x translation (wide_dx) and the wider clip are applied entirely in + * the vertex shader via u_xoff / u_xhalf, so the SAME glDrawArrays produces the + * shifted copy. wide_target_begin binds the wide FBO + viewport + scissor and + * sets the projection uniforms; wide_target_end restores u_xoff=0/u_xhalf=512 + * on the active program so the next canonical pass is bit-identical. The + * caller stays inside hr_begin/hr_end; hr_end unbinds and the next hr_begin + * resets the viewport, so no viewport restore is needed mid-function. + * + * The scissor X is the FULL wide surface (NOT the draw area): the SW reference + * (rt_wide()) deliberately lets the shifted geometry fill the revealed 16:9 + * margins that lie OUTSIDE the game's 4:3 draw-area x-range; scissoring X to + * the (translated) draw area would crop exactly the margin content native-wide + * exists to reveal. The scissor Y stays clamped to the DRAW AREA, exactly like + * rt_wide() (t.cy1/cy2 = g_clip_y1/y2): native-wide only widens X. A full-height + * Y scissor let draws that canonically clip at a vertical double-buffer band + * boundary (MMX6: draw area alternates y=0/y=240, both bands in ONE wide + * surface) bleed into the OTHER band's rows — presented one frame later as + * top/bottom edge flicker (16:9 GL only). */ +static void wide_target_begin(int dx, GLint uXoff, GLint uXhalf) { + if (s_ws_ablate != 3) /* ablate 3: no FBO rebind (draws land in hr — perf probe) */ + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, g_wide_cur); + glViewport(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); + glEnable(GL_SCISSOR_TEST); + { + int sy = s_area_y1, sh = s_area_y2 - s_area_y1 + 1; + if (sy < 0) { sh += sy; sy = 0; } + if (sy + sh > VRAM_H) sh = VRAM_H - sy; + if (sh < 0) sh = 0; + glScissor(0, sy * s_scale, g_wide_w * s_scale, sh * s_scale); + } + p_glUniform1f(uXoff, (float)dx); + p_glUniform1f(uXhalf, (float)g_wide_w / 2.0f); +} +static void wide_target_end(GLint uXoff, GLint uXhalf) { + p_glUniform1f(uXoff, 0.0f); + p_glUniform1f(uXhalf, 512.0f); + if (s_ws_ablate != 3) + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); +} + +extern int psx_ws_prim_is_tagged(void); /* gpu.c: is the current GP0 prim sprite-tagged? */ +extern int psx_ws_prim_in_backdrop(void); /* gpu.c: is its source addr in the flower-field struct? */ +extern int gpu_ws_nw_flat_backdrop_enabled(void); /* gpu.c: per-title flat backdrop opt-in */ + +/* Per-prim gate: stretch this prim iff native-wide + feature on AND the prim's + * source address is inside the flower-field backdrop data structure (precise — + * excludes the 3D rock/foreground, which is untagged AND has narrow prims so the + * earlier tag/narrow heuristic tore it). mode!=0 falls back to the old + * tag+narrow heuristic for A/B. */ +static int bd_prim_gate(const int *xs, int n, int textured) { + if (g_wide_w <= 0 || !g_ws_bd_stretch_on) return 0; + /* Some games draw their authored 4:3 sky/water as flat-colour polygons. + * Stretch those only in the native-wide mirror: the canonical framebuffer + * remains byte-for-byte 4:3, while the flat backdrop reaches the reveal + * margins. Opt-in because flat foreground geometry is title-dependent. */ + if (!textured && gpu_ws_nw_flat_backdrop_enabled()) return 1; + if (g_ws_bd_phase_mode != 0) return psx_ws_prim_in_backdrop(); /* default: precise address gate */ + /* mode 0: legacy tag+narrow heuristic (kept for comparison) */ + int native_w = g_wide_w - 2 * g_wide_off; + if (native_w <= 0) return 0; + if (psx_ws_prim_is_tagged()) return 0; + int base = g_wide_cur_base, lo = xs[0], hi = xs[0]; + for (int i = 1; i < n; i++) { if (xs[i] < lo) lo = xs[i]; if (xs[i] > hi) hi = xs[i]; } + if (lo < base - g_ws_bd_phase_thresh) return 0; /* into left margin -> GTE-wide */ + if (hi > base + native_w + g_ws_bd_phase_thresh) return 0; /* into right margin -> GTE-wide */ + return 1; +} + +/* ---- native-wide FAST path (skip redundant center mirror) ----------------- * + * The wide surface's CENTRE columns [g_wide_off, g_wide_off+native_w) are, by + * construction, identical to the canonical 4:3 framebuffer. So instead of + * re-rasterizing every primitive into the wide surface (the "mirror" pass — the + * dominant native-wide GPU cost, ~2x scene fill), we copy the canonical centre + * into the wide surface once at present (blit_wide_center_from_canonical), and + * the per-prim mirror only needs to produce the reveal MARGINS. Any prim/batch + * whose x-range is fully inside the 4:3 frame contributes nothing to the margins, + * so its mirror is skipped entirely. Correctness does not depend on the skip + * being precise: the centre is authoritatively overwritten by the blit, so the + * ONLY requirement is that a margin-reaching prim is NOT skipped — hence the + * conservative strict-inside test. 4:3 never runs any of this (g_wide_cur == 0). + * Toggle via gl_wide_fast for A/B; default ON. */ +static int s_wide_fast = 1; +void gl_renderer_set_wide_fast(int on) { s_wide_fast = on ? 1 : 0; } +int gl_renderer_get_wide_fast(void) { return s_wide_fast; } +static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h); /* def below */ +/* True if [lo,hi] (canonical draw-x) lies strictly inside the 4:3 frame, so the + * prim adds nothing to either reveal margin and its mirror can be skipped. */ +static int mirror_x_center_only(int lo, int hi) { + if (!s_wide_fast) return 0; + int base = g_wide_cur_base, native_w = g_wide_w - 2 * g_wide_off; + if (native_w <= 0) return 0; + return (lo >= base) && (hi < base + native_w); +} +static int mirror_geo_center_only(const int *xs, int n) { + if (!s_wide_fast) return 0; + int lo = xs[0], hi = xs[0]; + for (int i = 1; i < n; i++) { if (xs[i] < lo) lo = xs[i]; if (xs[i] > hi) hi = xs[i]; } + return mirror_x_center_only(lo, hi); +} +/* mirror_batch_center_only (textured-batch variant) is defined after s_tb below. */ + +/* Set / clear the 2D-backdrop x-stretch for a wide-mirror draw, per the current + * gate (s_bd_gate, set by the caller from bd_prim_gate / the batch gate). */ +static void wide_set_bd_scale(GLint uScale, GLint uCenter) { + extern int g_ws_tex_edge_pct; + float scale = 1.0f, center = 0.0f; + if (s_bd_gate && g_ws_bd_stretch_on && g_wide_w > 0) { + int native_w = g_wide_w - 2 * g_wide_off; + if (native_w > 0) { + scale = g_ws_bd_stretch_pct > 0 ? (float)g_ws_bd_stretch_pct / 100.0f + : (float)g_wide_w / (float)native_w; + if (s_bd_gate == 2) { + scale = g_ws_tex_edge_pct > 0 + ? (float)g_ws_tex_edge_pct / 100.0f : scale; + scale = -scale; /* shader: expand only beyond canonical edges */ + } + center = (float)g_wide_cur_base + (float)native_w / 2.0f; + } + } + if (scale != 1.0f) s_bdg_applied++; + p_glUniform1f(uScale, scale); + p_glUniform1f(uCenter, center); +} +static void wide_clear_bd_scale(GLint uScale, GLint uCenter) { + p_glUniform1f(uScale, 1.0f); + p_glUniform1f(uCenter, 0.0f); +} + +/* ---- textured-prim batching -------------------------------------------- * + * Consecutive textured prims sharing blend/mask/texwindow/filter coalesce into + * one draw. Per-prim texture state (texpage/clut/depth/raw/uv-limits) rides in + * the vertex (TEXV flat attributes), so only `semi` (blend) and the global + * mask/twin/filter are batch keys. flush_tex_batch() draws the queued verts; it + * is called before any op that reads VRAM, writes it outside the batch, or + * changes batch state (see callers: flush_cpu_upload, flush_pack_if_sampling, + * every non-textured glb_ wrapper, and the present path). Drawing reads only the + * already-coherent texture (per-prim coherency was ensured at append time), so + * flush_tex_batch never re-enters those helpers. */ +#define TEXBATCH_MAXV 8190 /* multiple of 3; ~2730 tris */ +static float s_tb[TEXBATCH_MAXV * TEXV]; +static int s_tb_n = 0; /* verts queued */ +static int s_tb_semi = -2; +static int s_tb_mask = 0, s_tb_filter = 0; +static int s_tb_twin[4] = {0, 0, 0, 0}; +static uint64_t s_batch_total = 0, s_batch_reason[7]; + +void gl_renderer_batch_diag(uint64_t out[8]) { + out[0] = s_batch_total; + for (int i = 0; i < 7; i++) out[i + 1] = s_batch_reason[i]; +} + +/* Draw the queued textured batch with correct PSX mask-bit handling AND correct + * painter's order. The mask bit lives in both the colour-attachment alpha + * (frag.a) and the stencil; STP=1 texels must always set it. + * + * OPAQUE batch (semi < 0): the STP bit does NOT gate COLOUR (every texel is + * opaque), so colour is drawn in ONE ordered pass over all texels — frag.a still + * carries the per-texel mask bit, so the alpha mask is correct — followed by a + * COLOUR-MASKED pass that only fixes the STENCIL for STP=1 texels. The old code + * split colour into STP=0 (pass 1) then STP=1 (pass 2) across the WHOLE batch, + * which let a behind prim's STP=1 texels overwrite a front prim's STP=0 colour + * (the Tomba character drew behind an AP-block's letters / a save post on GL + * only). Same draw count, order preserved, mask preserved. + * + * SEMI batch (semi >= 0): genuine two-pass — STP=0 texels opaque, STP=1 texels + * blended per the PSX mode. Cross-prim order is kept by isolating semi prims to + * one per batch (see gpu_textured_triangle), so this batch holds a single prim + * whose two passes do not self-overlap. */ +static void tex_batch_draw_passes(int nverts, int semi) { + p_glUniform1i(s_uSemimode, semi < 0 ? 0 : semi); + if (semi < 0) { + glDisable(GL_BLEND); + mask_stencil(s_tb_mask); + p_glUniform1i(s_uSemipass, 0); /* all texels, one ordered colour pass */ + glDrawArrays(GL_TRIANGLES, 0, nverts); + if (s_mask_check) { + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); /* stencil-only fixup */ + mask_stencil(1); + p_glUniform1i(s_uSemipass, 2); /* STP=1 texels set the mask bit */ + glDrawArrays(GL_TRIANGLES, 0, nverts); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + } else if (!s_tb_mask) { + /* Alpha is already exact; defer its duplicate stencil encoding + * until a later GP0(E6h) actually enables destination masking. */ + s_stencil_valid = 0; + } + } else if (!s_mask_check && semi == 4) { + /* Modes 0/1/3 can select opaque-vs-semi behavior per fragment with + * dual-source factors, so the whole painter-ordered batch is one draw. + * Mode 2 needs a different blend equation and stays on the conservative + * isolated path below. */ + glEnable(GL_BLEND); + p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); + p_glBlendFuncSeparate(GL_ONE, PSXGL_SRC1_ALPHA, GL_ONE, GL_ZERO); + if (s_tb_mask) mask_stencil(1); else glDisable(GL_STENCIL_TEST); + p_glUniform1i(s_uSemipass, 0); + glDrawArrays(GL_TRIANGLES, 0, nverts); + if (!s_tb_mask) s_stencil_valid = 0; + } else { + glDisable(GL_BLEND); /* Pass 1: STP=0 texels (opaque) */ + mask_stencil(s_tb_mask); + p_glUniform1i(s_uSemipass, 1); + glDrawArrays(GL_TRIANGLES, 0, nverts); + apply_psx_blend(semi); /* Pass 2: STP=1 texels (blended) */ + mask_stencil(1); + p_glUniform1i(s_uSemipass, 2); + glDrawArrays(GL_TRIANGLES, 0, nverts); + } +} + +/* ---- frame_perf CPU attribution (native-wide wedge hunt) ----------------- * + * Per-frame CPU wall time spent inside the GL submission paths (driver CPU + * cost surfaces INSIDE our gl* calls) + counters for the wide plumbing, so a + * CPU-bound wide frame (emu_cpu >> scene_gpu) can be attributed without a + * sampling profiler. Reset at present enter; reported by frame_perf. */ +static double cw_ms(void) { + static double freq = 0.0; + if (freq == 0.0) { + uint64_t f = SDL_GetPerformanceFrequency(); + freq = f ? (double)f : 1.0; + } + return (double)SDL_GetPerformanceCounter() * 1000.0 / freq; +} +static double s_cw_flush_ms = 0.0; /* CPU wall inside flush_tex_batch */ +static double s_cw_wide_ms = 0.0; /* CPU wall inside glb_wide_* entry points */ +static int s_cw_batches = 0, s_cw_wide_sets = 0, s_cw_wide_cfgs = 0, + s_cw_wide_clears = 0, s_cw_fbo_creates = 0, s_cw_flush_depth = 0; + +/* Textured-batch variant of mirror_x_center_only: scan the queued verts' x + * (attr 0, stride TEXV). Defined here so s_tb / TEXV are in scope. */ +static int mirror_batch_center_only(int nverts) { + if (!s_wide_fast || nverts <= 0) return 0; + float flo = s_tb[0], fhi = s_tb[0]; + for (int i = 1; i < nverts; i++) { + float x = s_tb[i * TEXV]; + if (x < flo) flo = x; if (x > fhi) fhi = x; + } + /* floor/ceil, not a truncating cast: with geometry_correction these are + * fractional, and (int) rounds toward zero — which WIDENS a negative x + * toward the centre and could wrongly call a margin-touching batch + * centre-only, dropping its wide-mirror draw. Whole values are unchanged. */ + return mirror_x_center_only((int)floorf(flo), (int)ceilf(fhi)); +} + +static void flush_tex_batch(void) { + if (s_tb_n == 0) return; + int nverts = s_tb_n, semi = s_tb_semi; + s_tb_n = 0; /* clear first: re-entrancy safe */ + double cw_t0 = cw_ms(); + s_cw_batches++; s_batch_total++; s_cw_flush_depth++; + + hr_begin(1); + p_glUseProgram(s_tex_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_raw_tex); + p_glUniform1i(s_uVram, 0); + p_glUniform4i(s_uTwin, s_tb_twin[0], s_tb_twin[1], s_tb_twin[2], s_tb_twin[3]); + p_glUniform1i(s_uMaskset, s_tb_mask); + p_glUniform1i(s_uFilter, s_tb_filter); + p_glBindVertexArray(s_tex_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); + p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(nverts * TEXV * sizeof(float)), s_tb, PSXGL_STREAM_DRAW); + + tex_batch_draw_passes(nverts, semi); + + /* Native-wide mirror — skipped for a batch fully inside the 4:3 frame (its + * centre content comes from the present-time canonical blit; nothing to add + * to the margins). A backdrop-stretched batch (s_tb_gate) widens past the + * frame, so it is never treated as centre-only. */ + if (g_wide_cur && s_ws_ablate != 1 && + !(s_tb_gate == 0 && mirror_batch_center_only(nverts))) { /* native-wide mirror */ + int dx = wide_dx(); + s_bd_gate = s_tb_gate; /* this batch is uniform-gate (flushed on change) */ + gl_perf_mirror_begin(); + wide_target_begin(dx, s_tex_uXoff, s_tex_uXhalf); + wide_set_bd_scale(s_tex_uXscale, s_tex_uXcenter); + if (s_ws_ablate != 2) tex_batch_draw_passes(nverts, semi); + wide_clear_bd_scale(s_tex_uXscale, s_tex_uXcenter); + wide_target_end(s_tex_uXoff, s_tex_uXhalf); + gl_perf_mirror_end(); + } + hr_end(); + if (--s_cw_flush_depth == 0) s_cw_flush_ms += cw_ms() - cw_t0; +} + +/* Flat / gouraud GEO batch — MotK title/char-select starfields issue ~30k/s + * GP0(68h) 1x1 dots; each was two immediate gpu_triangle draws (BufferData + + * DrawArrays each). Coalesce opaque/semi-uniform tris into one draw. */ +#define FLATBATCH_MAXV 8190 /* multiple of 3 */ +static float s_fb[FLATBATCH_MAXV * 6]; +static int s_fb_n = 0; +static int s_fb_semi = -2; +static int s_fb_mask = -1; + +static int mirror_flat_batch_center_only(int nverts) { + if (!s_wide_fast || nverts <= 0) return 0; + float flo = s_fb[0], fhi = s_fb[0]; + for (int i = 1; i < nverts; i++) { + float x = s_fb[i * 6]; + if (x < flo) flo = x; if (x > fhi) fhi = x; + } + /* floor/ceil rather than a truncating cast — see mirror_batch_center_only. */ + return mirror_x_center_only((int)floorf(flo), (int)ceilf(fhi)); +} + +static void flush_flat_batch(void) { + if (s_fb_n == 0) return; + int nverts = s_fb_n, semi = s_fb_semi, mask = s_fb_mask; + s_fb_n = 0; + + hr_begin(1); + if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); + mask_stencil(mask); + p_glUseProgram(s_geo_prog); + p_glBindVertexArray(s_geo_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); + p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(nverts * 6 * sizeof(float)), + s_fb, PSXGL_STREAM_DRAW); + glDrawArrays(GL_TRIANGLES, 0, nverts); + + if (g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 && + !(!g_ws_bd_stretch_on && mirror_flat_batch_center_only(nverts))) { + int dx = wide_dx(); + /* Batch may span many prims; use stretch gate off (flat dots/UI). */ + s_bd_gate = 0; + gl_perf_mirror_begin(); + wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf); + wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter); + if (s_ws_ablate != 2) glDrawArrays(GL_TRIANGLES, 0, nverts); + wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter); + wide_target_end(s_geo_uXoff, s_geo_uXhalf); + gl_perf_mirror_end(); + } + hr_end(); +} + +/* Flat / gouraud triangles and lines share the GEO program. mode: GL_TRIANGLES + * or GL_LINES; verts are (x, y, r, g, b, a) tuples with colors as 1555. */ +static void gpu_geometry(GLenum mode, const int *xs, const int *ys, + const uint16_t *cs, int n, int semi) { + flush_tex_batch(); /* flat prim: drain textured draws first (order + program) */ + flush_cpu_upload(); /* also drains flat batch if an upload was pending */ + mark_prim_dirty(xs, ys, n, 0 /* flat */); + /* Sub-pixel positions only describe a 3-vertex projected triangle. */ + const int precise = s_pc_valid && mode == GL_TRIANGLES && n == 3; + + /* Lines stay immediate (rare); tris batch for MotK 0x68 starfields. */ + if (mode != GL_TRIANGLES || n < 3) { + flush_flat_batch(); + float verts[3 * 6]; + float mask_a = s_mask_set ? 1.0f : 0.0f; + for (int i = 0; i < n; i++) { + verts[i*6+0] = precise ? s_pc_x[i] : (float)xs[i]; + verts[i*6+1] = precise ? s_pc_y[i] : (float)ys[i]; + verts[i*6+2] = ((cs[i] & 0x1F) << 3) / 255.0f; + verts[i*6+3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f; + verts[i*6+4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f; + verts[i*6+5] = mask_a; + } + hr_begin(1); + if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); + mask_stencil(s_mask_set); + if (mode == GL_LINES) glLineWidth((float)s_scale); + p_glUseProgram(s_geo_prog); + p_glBindVertexArray(s_geo_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); + p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)(n * 6 * sizeof(float)), + verts, PSXGL_STREAM_DRAW); + glDrawArrays(mode, 0, n); + if (g_wide_cur && !s_wide_suppress && s_ws_ablate != 1 && + !(!g_ws_bd_stretch_on && mirror_geo_center_only(xs, n))) { + int dx = wide_dx(); + s_bd_gate = bd_prim_gate(xs, n, 0); + gl_perf_mirror_begin(); + wide_target_begin(dx, s_geo_uXoff, s_geo_uXhalf); + wide_set_bd_scale(s_geo_uXscale, s_geo_uXcenter); + if (s_ws_ablate != 2) glDrawArrays(mode, 0, n); + wide_clear_bd_scale(s_geo_uXscale, s_geo_uXcenter); + wide_target_end(s_geo_uXoff, s_geo_uXhalf); + gl_perf_mirror_end(); + } + hr_end(); + return; + } + + if (s_fb_n > 0 && (s_fb_semi != semi || s_fb_mask != (int)s_mask_set)) + flush_flat_batch(); + if (s_fb_n + n > FLATBATCH_MAXV) + flush_flat_batch(); + s_fb_semi = semi; + s_fb_mask = (int)s_mask_set; + + float mask_a = s_mask_set ? 1.0f : 0.0f; + for (int i = 0; i < n; i++) { + float *v = &s_fb[s_fb_n * 6]; + v[0] = precise ? s_pc_x[i] : (float)xs[i]; + v[1] = precise ? s_pc_y[i] : (float)ys[i]; + v[2] = ((cs[i] & 0x1F) << 3) / 255.0f; + v[3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f; + v[4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f; + v[5] = mask_a; + s_fb_n++; + } +} + +static void gpu_triangle(int x0,int y0,uint16_t c0, int x1,int y1,uint16_t c1, + int x2,int y2,uint16_t c2, int semi) { + int xs[3] = {x0, x1, x2}, ys[3] = {y0, y1, y2}; + uint16_t cs[3] = {c0, c1, c2}; + gpu_geometry(GL_TRIANGLES, xs, ys, cs, 3, semi); +} + +static void gpu_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int semi) { + int xs[2] = {x0, x1}, ys[2] = {y0, y1}; + uint16_t cs[2] = {c0, c1}; + gpu_geometry(GL_LINES, xs, ys, cs, 2, semi); +} + +/* Shared PS1 uv-sampling model (limits + mirrored-2D compensation) — one + * implementation for GL/VK/SW, see gpu_uv.h. */ +#include "gpu_uv.h" + +/* Textured triangle. Always two passes split by the per-texel STP bit so the + * stencil (mask) write value is constant within each pass; the semi pass is + * also where PS1 blending applies. lim = uv sampling bounds (see + * tri_uv_limits); NULL computes them from the vertices. */ +static void gpu_textured_triangle(const int *xs, const int *ys, + const int *us, const int *vs, + const float *col, uint16_t texpage, + uint16_t clut_x, uint16_t clut_y, int rawtex, + int semi, const int *lim) { + int lim_buf[4]; + int uv_buf[6]; + if (!lim) { + /* Poly path: exact sampled bounds from the ORIGINAL uvs, then the + * center-sampling mirror compensation (rect prims arrive with their + * own precomputed lim and pre-bumped uvs). */ + int *mu = uv_buf, *mv = uv_buf + 3; + for (int i = 0; i < 3; i++) { mu[i] = us[i]; mv[i] = vs[i]; } + psx_uv_tri_limits(xs, ys, mu, mv, lim_buf); + psx_uv_tri_mirror_offset(xs, ys, mu, mv); + us = mu; vs = mv; + lim = lim_buf; + } + s_scene_prims_tex++; + int base_x = (texpage & 0xF) * 64; + int base_y = ((texpage >> 4) & 1) * 256; + int depth = (texpage >> 7) & 3; if (depth > 2) depth = 2; + + flush_cpu_upload(); /* if a CPU->VRAM upload is pending it flushes the batch first */ + flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y); /* flushes batch iff it must pack */ + mark_prim_dirty(xs, ys, 3, 1 /* textured */); + + /* Append to the textured batch. Flush first if this prim's blend/mask/twin/ + * filter differ from the open batch, or the buffer is full. Per-prim texture + * state goes in the vertex; only these keys force a new draw. */ + { + flush_flat_batch(); /* painter order: flat GEO before textured */ + int twx = s_tw_mask_x, twy = s_tw_mask_y, tox = s_tw_off_x, toy = s_tw_off_y; + int gate = bd_prim_gate(xs, 3, 1); /* backdrop-stretch gate is also a batch key */ + /* Batch key: keep opaque as -1. Dual-source (4) is only for semi modes + * 0/1/3 when mask-check is off — never coalesce opaque into that key. + * Mixing opaque+semi under u_semimode==4 made additive particle/glow + * batches (CTR Naughty Dog intro binary funnel) paint over later + * opaque crate flaps whenever submission order and STP bits disagreed + * with the dual-source path's assumptions. */ + int batch_semi; + if (semi < 0) + batch_semi = -1; + else if (!s_mask_check && semi != 2) + batch_semi = 4; + else + batch_semi = semi; + /* STP draw-ORDER correctness. flush_tex_batch's conservative two-pass + * path draws pass 1 = every prim's STP=0 texels then pass 2 = every + * prim's STP=1 texels — a behind prim's semi texels then overwrite a + * front prim's opaque texels (Tomba AP-block / CTR intro flaps). The + * dual-source single-pass path avoids that WITHIN one prim, but + * batching many overlapping semi quads (digit particles + glow) still + * mis-orders against neighbouring opaque geometry. Isolate EVERY + * semi-transparent textured prim: drain the open batch, draw this + * prim alone (composited fully before the next), let opaque prims + * keep batching. Cost is one draw per semi prim. */ + int isolate = (semi >= 0); + int reason = -1; + if (s_tb_n > 0) { + if (isolate) reason = 0; + else if (batch_semi != s_tb_semi) reason = 1; + else if (s_mask_set != s_tb_mask) reason = 2; + else if (s_tex_filter != s_tb_filter) reason = 3; + else if (gate != s_tb_gate) reason = 4; + else if (twx != s_tb_twin[0] || twy != s_tb_twin[1] || + tox != s_tb_twin[2] || toy != s_tb_twin[3]) reason = 5; + } + if (reason >= 0) { + s_batch_reason[reason]++; + flush_tex_batch(); + } + if (s_tb_n + 3 > TEXBATCH_MAXV) { s_batch_reason[6]++; flush_tex_batch(); } + if (s_tb_n == 0) { /* opening a batch: capture its keyed state */ + s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = s_tex_filter; s_tb_gate = gate; + s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy; + } + float *vp = &s_tb[s_tb_n * TEXV]; + for (int i = 0; i < 3; i++, vp += TEXV) { + vp[0] = s_pc_valid ? s_pc_x[i] : (float)xs[i]; + vp[1] = s_pc_valid ? s_pc_y[i] : (float)ys[i]; + vp[2] = (float)us[i]; vp[3] = (float)vs[i]; + vp[4] = col[i*3+0]; vp[5] = col[i*3+1]; vp[6] = col[i*3+2]; vp[7] = 1.0f; + vp[8] = (float)base_x; vp[9] = (float)base_y; /* a_tpage */ + vp[10] = (float)clut_x; vp[11] = (float)clut_y; /* a_clut */ + vp[12] = (float)depth; vp[13] = (float)rawtex; /* a_depth, a_raw */ + vp[14] = (float)lim[0]; vp[15] = (float)lim[1]; /* a_limits */ + vp[16] = (float)lim[2]; vp[17] = (float)lim[3]; + vp[18] = semi >= 0 ? (float)(semi + 1) : 0.0f; /* a_semi code */ + vp[19] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */ + } + s_tb_n += 3; + if (isolate) flush_tex_batch(); /* draw this semi prim alone, in submission order */ + } +} + +/* Draw a flat-colored rect (GEO program) DIRECTLY into the active wide surface + * at wide-space coords [wx, wx+ww) × [y, y+h). Used only by the full-screen- + * overlay path; positions are already in wide space so u_xoff stays 0. Mirrors + * raster_flat_rect(&wt, ...) in sw_draw_flat_rect. Caller stays inside + * hr_begin/hr_end. */ +static void wide_flat_rect_direct(int wx, int y, int ww, int h, uint16_t c, int semi) { + if (ww <= 0 || h <= 0) return; + float r = ((c & 0x1F) << 3) / 255.0f; + float g = (((c >> 5) & 0x1F) << 3) / 255.0f; + float b = (((c >> 10) & 0x1F) << 3) / 255.0f; + float a = s_mask_set ? 1.0f : 0.0f; + float fx0 = (float)wx, fy0 = (float)y, fx1 = (float)(wx + ww), fy1 = (float)(y + h); + float verts[6 * 6] = { + fx0,fy0,r,g,b,a, fx1,fy0,r,g,b,a, fx0,fy1,r,g,b,a, + fx1,fy0,r,g,b,a, fx0,fy1,r,g,b,a, fx1,fy1,r,g,b,a, + }; + /* Wide target: positions already wide-space so u_xoff = 0; full-width + * scissor; u_xhalf = g_wide_w/2. */ + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, g_wide_cur); + glViewport(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); + glEnable(GL_SCISSOR_TEST); + glScissor(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); /* full surface (rt_wide) */ + if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND); + mask_stencil(s_mask_set); + p_glUseProgram(s_geo_prog); + p_glUniform1f(s_geo_uXoff, 0.0f); + p_glUniform1f(s_geo_uXhalf, (float)g_wide_w / 2.0f); + p_glBindVertexArray(s_geo_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); + p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); + glDrawArrays(GL_TRIANGLES, 0, 6); + p_glUniform1f(s_geo_uXoff, 0.0f); + p_glUniform1f(s_geo_uXhalf, 512.0f); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); +} + +static void gpu_flat_rect(int x,int y,int w,int h,uint16_t c,int semi) { + if (w <= 0 || h <= 0) return; + /* Full-screen 2D overlay (pause gray-filter / load fade): a flat rect + * spanning the whole 4:3 framebuffer must cover the whole wide surface too, + * else the revealed 16:9 margins are left undimmed/unfaded. Same detection + * as sw_draw_flat_rect: native_w = the 4:3 framebuffer width (g_wide_w less + * the per-side reveal on both sides); the rect's native screen-X span + * (x - base) must cover [0, native_w). When it does, the two canonical + * triangles are drawn WITHOUT the per-triangle wide mirror (suppressed) and + * a single full-width rect is drawn into the wide surface instead; every + * other rect mirrors 1:1 via the generic gpu_geometry path. Only runs in + * native-wide (g_wide_cur != 0), so 4:3 is unaffected. */ + int overlay = 0; + if (g_wide_cur) { + int native_w = g_wide_w - 2 * g_wide_off; + int lx = x - g_wide_cur_base, rx = x + w - g_wide_cur_base; + overlay = (native_w > 0 && lx <= 0 && rx >= native_w); + } + if (overlay) s_wide_suppress = 1; + gpu_triangle(x, y, c, x+w, y, c, x, y+h, c, semi); + gpu_triangle(x+w, y, c, x, y+h, c, x+w, y+h, c, semi); + if (overlay) { + s_wide_suppress = 0; + /* The two canonical triangles drew into the hr FBO already (each + * gpu_triangle ran its own hr_begin/hr_end). Re-open the bracket just + * for the full-width wide pass so blend/scissor/program state is + * clean. */ + if (s_ws_ablate != 1) { + hr_begin(0); + gl_perf_mirror_begin(); + wide_flat_rect_direct(0, y, g_wide_w, h, c, semi); + gl_perf_mirror_end(); + hr_end(); + } + } +} + +static void gpu_textured_rect(int x,int y,int w,int h, + int u0,int v0,int u1,int v1, + uint16_t clut_x,uint16_t clut_y,uint16_t tp,int semi) { + if (w <= 0 || h <= 0) return; + float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f; + float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb}; + /* gpu.c routes axis-aligned MIRRORED quads (X/Y-flipped 2D sprites, + * e.g. right-facing MMX entities) through THIS path as scaled rects + * with u0>u1 / v0>v1 — they never reach the poly path. Exact bounds + * from the original corners, then the mirror bump (see gpu_uv.h). */ + int lim[4]; + psx_uv_rect_limits(u0, v0, u1, v1, lim); + psx_uv_rect_mirror_offset(&u0, &v0, &u1, &v1); + int xs1[3]={x, x+w, x}, ys1[3]={y, y, y+h}; + int us1[3]={u0,u1,u0}, vs1[3]={v0,v0,v1}; + gpu_textured_triangle(xs1,ys1,us1,vs1,col,tp,clut_x,clut_y,s_mod_raw,semi,lim); + int xs2[3]={x+w, x, x+w}, ys2[3]={y, y+h, y+h}; + int us2[3]={u1,u0,u1}, vs2[3]={v0,v1,v1}; + gpu_textured_triangle(xs2,ys2,us2,vs2,col,tp,clut_x,clut_y,s_mod_raw,semi,lim); +} + +/* GP0(02h) fill: writes color with bit15=0, ignoring draw area, mask and + * offset; coordinates wrap. A scissored clear (color + stencil) per wrapped + * segment is exactly this. */ +static void fill_segment(int x, int y, int w, int h, float r, float g, float b) { + if (w <= 0 || h <= 0) return; + glScissor(x * s_scale, y * s_scale, w * s_scale, h * s_scale); + glClearColor(r, g, b, 0.0f); + glClearStencil(0); + glStencilMask(0xFF); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1); +} + +static void gpu_fill(int x,int y,int w,int h,uint16_t c) { + if (w <= 0 || h <= 0) return; + flush_flat_batch(); + flush_tex_batch(); + flush_cpu_upload(); + float r=(c&0x1F)/31.0f, g=((c>>5)&0x1F)/31.0f, b=((c>>10)&0x1F)/31.0f; + x &= VRAM_W - 1; y &= VRAM_H - 1; + if (w > VRAM_W) w = VRAM_W; + if (h > VRAM_H) h = VRAM_H; + int w1 = w, w2 = 0, h1 = h, h2 = 0; + if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; } + if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; } + + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); + glViewport(0, 0, VRAM_W * s_scale, VRAM_H * s_scale); + glEnable(GL_SCISSOR_TEST); + fill_segment(x, y, w1, h1, r, g, b); + if (w2) fill_segment(0, y, w2, h1, r, g, b); + if (h2) fill_segment(x, 0, w1, h2, r, g, b); + if (w2 && h2) fill_segment(0, 0, w2, h2, r, g, b); + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + if (!s_cpu_auth_dual) + s_gpu_dirty = 1; + coh_record(GL_COH_FILL, x, y, x + w - 1, y + h - 1); +} + +/* VRAM->VRAM copy: blit the source region to the scratch texture (resolves + * overlap), then draw it back at the destination through the BLIT program so + * mask set/check and the stencil mirror apply, exactly like sw_copy_rect. */ +static void gpu_copy_rect(int sx,int sy,int dx,int dy,int w,int h) { + if (w <= 0 || h <= 0) return; + flush_flat_batch(); + flush_tex_batch(); + flush_cpu_upload(); + /* Clamp to bounds (the software path wraps; wrapping copies are unused + * in practice — see the file header). */ + if (sx < 0) sx = 0; if (sy < 0) sy = 0; + if (dx < 0) dx = 0; if (dy < 0) dy = 0; + if (sx + w > VRAM_W) w = VRAM_W - sx; + if (dx + w > VRAM_W) w = VRAM_W - dx; + if (sy + h > VRAM_H) h = VRAM_H - sy; + if (dy + h > VRAM_H) h = VRAM_H - dy; + if (w <= 0 || h <= 0) return; + + int S = s_scale; + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_scratch_fbo); + glDisable(GL_SCISSOR_TEST); + p_glBlitFramebuffer(sx*S, sy*S, (sx+w)*S, (sy+h)*S, + sx*S, sy*S, (sx+w)*S, (sy+h)*S, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); + + hr_begin(0); /* copies ignore the draw area */ + glScissor(dx*S, dy*S, w*S, h*S); + glDisable(GL_BLEND); + p_glUseProgram(s_blit_prog); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_scratch_tex); + p_glUniform1i(s_uBlitSrc, 0); + p_glUniform1i(s_uBlitMaskset, s_mask_set); + /* Scratch holds the source at its own hr coords: texel = frag + (src-dst)*S. */ + p_glUniform1i(s_uBlitSrcDiv, 1); + p_glUniform2i(s_uBlitSrcOff, (sx - dx) * S, (sy - dy) * S); + float fx0 = (float)dx, fy0 = (float)dy, fx1 = (float)(dx + w), fy1 = (float)(dy + h); + float verts[6 * 2] = { + fx0, fy0, fx1, fy0, fx0, fy1, + fx1, fy0, fx0, fy1, fx1, fy1, + }; + p_glBindVertexArray(s_blit_vao); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); + p_glBufferData(PSXGL_ARRAY_BUFFER, sizeof verts, verts, PSXGL_STREAM_DRAW); + /* Two passes split by source bit15 so stencil tracks the copied mask. */ + mask_stencil(s_mask_set); p_glUniform1i(s_uBlitPass, 1); glDrawArrays(GL_TRIANGLES, 0, 6); + mask_stencil(1); p_glUniform1i(s_uBlitPass, 2); glDrawArrays(GL_TRIANGLES, 0, 6); + hr_end(); + + rect_add(&s_pack_dirty, dx, dy, dx + w - 1, dy + h - 1); + if (!s_cpu_auth_dual) + s_gpu_dirty = 1; + coh_record(GL_COH_COPY_SRC, sx, sy, sx + w - 1, sy + h - 1); + coh_record(GL_COH_COPY, dx, dy, dx + w - 1, dy + h - 1); +} + +/* ---- backend vtable wrappers ------------------------------------------- */ +static void glb_init(uint16_t *vram) { s_vram = vram; sw_renderer_init(vram); } + +/* Under GL the internal-resolution scale lives in the hr FBO; the CPU-side + * (software mirror, readbacks, screenshots) stays native, so the reported + * scale is 1 and the software hi-res mirror stays off. */ +static void glb_set_scale(int s) { + if (s < 1) s = 1; + if (s > GL_MAX_INTERNAL_SCALE) s = GL_MAX_INTERNAL_SCALE; + s_req_scale = s; + sw_renderer_set_scale(1); +} +static int glb_scale(void) { return s_scale; } /* real internal SSAA scale (was a stub 1; the + native-wide CPU present path + gr_scale() callers + need the true scale — the FBO-direct present is + unaffected since it never reads gr_scale()) */ +static void glb_set_texture_filter(int b) { s_tex_filter = b ? 1 : 0; sw_set_texture_filter(b); } +static int glb_texture_filter(void) { return s_tex_filter; } + +static void glb_set_semi_transparency(int e, int m) { s_semi_en = e; s_semi_mode = m & 3; sw_set_semi_transparency(e, m); } +static void glb_set_mask_bits(int s, int c) { + int next_check = c ? 1 : 0; + if (next_check && !s_mask_check) { + /* Land all alpha-authoritative work before deriving stencil from it. */ + flush_tex_batch(); + flush_cpu_upload(); + rebuild_mask_stencils(); + } + s_mask_set = s ? 1 : 0; + s_mask_check = next_check; + sw_set_mask_bits(s, c); +} +static void glb_set_texture_window(uint32_t r) { + s_tw_mask_x = (int)(r & 0x1F); + s_tw_mask_y = (int)((r >> 5) & 0x1F); + s_tw_off_x = (int)((r >> 10) & 0x1F); + s_tw_off_y = (int)((r >> 15) & 0x1F); + sw_set_texture_window(r); +} +static void glb_set_color_modulation(int r,int g,int b,int raw) { s_mod_r=r; s_mod_g=g; s_mod_b=b; s_mod_raw=raw; sw_set_color_modulation(r,g,b,raw); } +/* Sub-pixel / perspective overrides for the next triangle. Forwarded to the + * software rasterizer too: pre-context draws (s_raster_ok == 0) still go there, + * and the GL backend delegates whole prim classes to it. */ +static void glb_set_precise_triangle(int enabled, + int32_t x0,int32_t y0, int32_t x1,int32_t y1, + int32_t x2,int32_t y2) { + s_pc_valid = enabled ? 1 : 0; + if (s_pc_valid) { + /* 16.16 native VRAM px -> float. The GL pipeline is float end-to-end, + * so the fraction survives to the rasterizer at any internal scale. */ + s_pc_x[0] = (float)x0 / 65536.0f; s_pc_y[0] = (float)y0 / 65536.0f; + s_pc_x[1] = (float)x1 / 65536.0f; s_pc_y[1] = (float)y1 / 65536.0f; + s_pc_x[2] = (float)x2 / 65536.0f; s_pc_y[2] = (float)y2 / 65536.0f; + } + sw_set_precise_triangle(enabled, x0,y0, x1,y1, x2,y2); +} +static void glb_set_perspective_triangle(int enabled, float q0, float q1, float q2) { + s_pq_valid = (enabled && q0 > 0.0f && q1 > 0.0f && q2 > 0.0f) ? 1 : 0; + s_pq[0] = q0; s_pq[1] = q1; s_pq[2] = q2; + sw_set_perspective_triangle(enabled, q0, q1, q2); +} +static void glb_set_draw_area(int x1,int y1,int x2,int y2) { flush_flat_batch(); flush_tex_batch(); s_area_x1=x1; s_area_y1=y1; s_area_x2=x2; s_area_y2=y2; sw_set_draw_area(x1,y1,x2,y2); } +static void glb_get_draw_area(int *x1,int *y1,int *x2,int *y2) { sw_get_draw_area(x1,y1,x2,y2); } +static void glb_set_draw_offset(int x,int y) { flush_flat_batch(); flush_tex_batch(); s_off_x=x; s_off_y=y; sw_set_draw_offset(x,y); } + +/* Pre-context draws (s_raster_ok == 0) fall back to the software rasterizer + * over CPU VRAM; the initial full-VRAM upload at context init folds them in. + * Offline post-init is GPU-only (FBO-auth). Netplay dual-raster always writes + * SW @ 1× for authority, then GPU @ s_scale for present quality. */ +/* The sub-pixel / perspective override describes exactly one triangle; drop it + * once that triangle has been submitted so a later prim can never inherit it. */ +static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; } + +static void glb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col) { + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col); + if (!s_raster_ok) return; + gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1); + precise_consumed(); +} +static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2) { + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2); + if (!s_raster_ok) return; + gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1); + precise_consumed(); +} +static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_fill_rect(x,y,w,h,c); + if (!s_raster_ok) return; + gpu_fill(x,y,w,h,c); +} +static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_copy_rect(sx,sy,dx,dy,w,h); + if (!s_raster_ok) return; + gpu_copy_rect(sx,sy,dx,dy,w,h); +} +static void glb_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,int x2,int y2,int u2,int v2,uint16_t cx,uint16_t cy,uint16_t tp){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp); + if (!s_raster_ok) return; + int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2}; + float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f; + float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb}; + gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL); + precise_consumed(); +} +static void glb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,int x1,int y1,int u1,int v1,uint32_t c1,int x2,int y2,int u2,int v2,uint32_t c2,uint16_t cx,uint16_t cy,uint16_t tp,int raw){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); + if (!s_raster_ok) return; + int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2}; + uint32_t cc[3]={c0,c1,c2}; float col[9]; + for (int i=0;i<3;i++){ col[i*3+0]=(cc[i]&0xFF)/255.0f; col[i*3+1]=((cc[i]>>8)&0xFF)/255.0f; col[i*3+2]=((cc[i]>>16)&0xFF)/255.0f; } + gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL); + precise_consumed(); +} +static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_flat_rect(x,y,w,h,c); + if (!s_raster_ok) return; + gpu_flat_rect(x,y,w,h,c, s_semi_en?s_semi_mode:-1); +} +static void glb_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t cx,uint16_t cy,uint16_t tp){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp); + if (!s_raster_ok) return; + gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1); +} +static void glb_draw_textured_rect_scaled(int x,int y,int w,int h,int u0,int v0,int u1,int v1,uint16_t cx,uint16_t cy,uint16_t tp){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp); + if (!s_raster_ok) return; + gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1); +} +static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_line(x0,y0,x1,y1,c); + if (!s_raster_ok) return; + gpu_line(x0,y0,c, x1,y1,c, s_semi_en?s_semi_mode:-1); +} +static void glb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1){ + if (s_cpu_auth_dual || !s_raster_ok) + sw_draw_shaded_line(x0,y0,c0,x1,y1,c1); + if (!s_raster_ok) return; + gpu_line(x0,y0,c0, x1,y1,c1, s_semi_en?s_semi_mode:-1); +} +static int glb_render_display(uint32_t *o,int p,int dx,int dy,int dw,int dh){ ensure_cpu(); return sw_render_display(o,p,dx,dy,dw,dh); } +static int glb_render_display_hires(uint32_t *o,int p,int dx,int dy,int dw,int dh){ ensure_cpu(); return sw_render_display_hires(o,p,dx,dy,dw,dh); } +/* While GP1 depth24 is on, packed RGB888 lives in the CPU mirror and is + * presented via gl_renderer_present — never as 1555 FBO texels. Queuing those + * MDEC A0 rects hits UP_RECTS_MAX (16) and force-flushes mid-movie (MotK intro + * ~50→~30 FPS). Skip ONLY framebuffer-sized transfers (RGB888); still upload + * smaller texture A0s so post-FMV menus keep VRAM pages coherent. + * On leave: clear the skipped FB union in the FBO — do NOT restage CPU RGB888 + * as 1555 (that painted MotK title rainbow/static). */ static int depth24_is_fb_transfer(int x, int y, int w, int h) { - if (!gpu_display_is_depth24() || w <= 0 || h <= 0) return 0; - GpuDisplayInfo di; - gpu_get_display_info(&di); - int fb_w = (int)((di.width * 3u + 1u) / 2u); /* RGB W → halfwords */ - int fb_h = (int)di.height; - if (fb_w < 8) fb_w = 8; - if (fb_h < 1) fb_h = 1; - /* Classic VRAM texture pages. The area heuristic below treats 256×256 as - * "half of a 320-wide RGB888 FB" (480×240/2) and would skip staging them - * during FMV — TM4 post-intro loading text then samples an empty FBO. */ - if (w <= 256 && h <= 256) return 0; - /* Must target the CRTC scanout band (halfword coords). */ - { - int dx = (int)(di.display_x & 1023u); - int dy = (int)(di.display_y & 511u); - int x0 = x & (VRAM_W - 1), y0 = y & (VRAM_H - 1); - if (x0 + w <= dx || x0 >= dx + fb_w || y0 + h <= dy || y0 >= dy + fb_h) - return 0; - } - /* MotK: 768×128 class blits; allow slack. */ - if (h >= fb_h - 8 && h <= fb_h + 16 && w >= (fb_w * 3) / 4) return 1; - if ((int64_t)w * (int64_t)h >= ((int64_t)fb_w * fb_h) / 2) return 1; - return 0; -} - -/* Remember only the depth24 scanout band for clear-on-leave. Used after a - * full-VRAM savestate restage so texture pages stay in the FBO while the - * RGB888 movie band is still wiped when GP1 leaves 24-bit (avoids MotK - * rainbow/static from treating packed RGB as 1555). */ -static void depth24_mark_scanout_band(void) { - GpuDisplayInfo di; - int fb_w, fb_h, x0, y0, x1, y1; - if (!gpu_display_is_depth24()) return; - gpu_get_display_info(&di); - fb_w = (int)((di.width * 3u + 1u) / 2u); - fb_h = (int)di.height; - if (fb_w < 8) fb_w = 8; - if (fb_h < 1) fb_h = 1; - x0 = (int)(di.display_x & 1023u); - y0 = (int)(di.display_y & 511u); - x1 = x0 + fb_w - 1; - y1 = y0 + fb_h - 1; - if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; - if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; - rect_add(&s_d24_skip_fb, x0, y0, x1, y1); -} - -static void depth24_clear_skipped_fb(void) { - if (!s_raster_ok || !s_d24_skip_fb.set) return; - flush_flat_batch(); - flush_tex_batch(); - int x0 = s_d24_skip_fb.x0, y0 = s_d24_skip_fb.y0; - int x1 = s_d24_skip_fb.x1, y1 = s_d24_skip_fb.y1; - int rw = x1 - x0 + 1, rh = y1 - y0 + 1; - int S = s_scale; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); - glDisable(GL_BLEND); - glDisable(GL_STENCIL_TEST); - glEnable(GL_SCISSOR_TEST); - glViewport(0, 0, VRAM_W * S, VRAM_H * S); - glScissor(x0 * S, y0 * S, rw * S, rh * S); - glClearColor(0.f, 0.f, 0.f, 0.f); - glClearStencil(0); - glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - if (s_raw_fbo) { - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo); - glViewport(0, 0, VRAM_W, VRAM_H); - glScissor(x0, y0, rw, rh); - glClear(GL_COLOR_BUFFER_BIT); - } - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - rect_add(&s_pack_dirty, x0, y0, x1, y1); - present_dirty_rect(x0, y0, x1, y1, 1); - rect_clear(&s_d24_skip_fb); -} - + if (!gpu_display_is_depth24() || w <= 0 || h <= 0) return 0; + GpuDisplayInfo di; + gpu_get_display_info(&di); + int fb_w = (int)((di.width * 3u + 1u) / 2u); /* RGB W → halfwords */ + int fb_h = (int)di.height; + if (fb_w < 8) fb_w = 8; + if (fb_h < 1) fb_h = 1; + /* Classic VRAM texture pages. The area heuristic below treats 256×256 as + * "half of a 320-wide RGB888 FB" (480×240/2) and would skip staging them + * during FMV — TM4 post-intro loading text then samples an empty FBO. */ + if (w <= 256 && h <= 256) return 0; + /* Must target the CRTC scanout band (halfword coords). */ + { + int dx = (int)(di.display_x & 1023u); + int dy = (int)(di.display_y & 511u); + int x0 = x & (VRAM_W - 1), y0 = y & (VRAM_H - 1); + if (x0 + w <= dx || x0 >= dx + fb_w || y0 + h <= dy || y0 >= dy + fb_h) + return 0; + } + /* MotK: 768×128 class blits; allow slack. */ + if (h >= fb_h - 8 && h <= fb_h + 16 && w >= (fb_w * 3) / 4) return 1; + if ((int64_t)w * (int64_t)h >= ((int64_t)fb_w * fb_h) / 2) return 1; + return 0; +} + +/* Remember only the depth24 scanout band for clear-on-leave. Used after a + * full-VRAM savestate restage so texture pages stay in the FBO while the + * RGB888 movie band is still wiped when GP1 leaves 24-bit (avoids MotK + * rainbow/static from treating packed RGB as 1555). */ +static void depth24_mark_scanout_band(void) { + GpuDisplayInfo di; + int fb_w, fb_h, x0, y0, x1, y1; + if (!gpu_display_is_depth24()) return; + gpu_get_display_info(&di); + fb_w = (int)((di.width * 3u + 1u) / 2u); + fb_h = (int)di.height; + if (fb_w < 8) fb_w = 8; + if (fb_h < 1) fb_h = 1; + x0 = (int)(di.display_x & 1023u); + y0 = (int)(di.display_y & 511u); + x1 = x0 + fb_w - 1; + y1 = y0 + fb_h - 1; + if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; + if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; + rect_add(&s_d24_skip_fb, x0, y0, x1, y1); +} + +static void depth24_clear_skipped_fb(void) { + if (!s_raster_ok || !s_d24_skip_fb.set) return; + flush_flat_batch(); + flush_tex_batch(); + int x0 = s_d24_skip_fb.x0, y0 = s_d24_skip_fb.y0; + int x1 = s_d24_skip_fb.x1, y1 = s_d24_skip_fb.y1; + int rw = x1 - x0 + 1, rh = y1 - y0 + 1; + int S = s_scale; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); + glDisable(GL_BLEND); + glDisable(GL_STENCIL_TEST); + glEnable(GL_SCISSOR_TEST); + glViewport(0, 0, VRAM_W * S, VRAM_H * S); + glScissor(x0 * S, y0 * S, rw * S, rh * S); + glClearColor(0.f, 0.f, 0.f, 0.f); + glClearStencil(0); + glStencilMask(0xFF); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + if (s_raw_fbo) { + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo); + glViewport(0, 0, VRAM_W, VRAM_H); + glScissor(x0, y0, rw, rh); + glClear(GL_COLOR_BUFFER_BIT); + } + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + rect_add(&s_pack_dirty, x0, y0, x1, y1); + present_dirty_rect(x0, y0, x1, y1, 1); + rect_clear(&s_d24_skip_fb); +} + static void depth24_upload_policy(void) { int d24 = gpu_display_is_depth24(); if (d24 && !s_depth24_skip_up) { @@ -2511,465 +2556,531 @@ static void depth24_upload_policy(void) { depth24_clear_skipped_fb(); flush_cpu_upload(); gpu_depth24_upload_span_reset(); - /* Force a fresh present after FMV→15-bit so menus/loading screens - * are not held as a stale depth24 frame. */ - gl_renderer_invalidate_present(); - } - s_depth24_skip_up = d24; -} - -static void glb_vram_write(int x,int y,uint16_t px){ - sw_vram_write(x,y,px); - depth24_upload_policy(); - /* Point pokes are never MDEC frames — always stage to FBO. */ - up_add(x & (VRAM_W-1), y & (VRAM_H-1), x & (VRAM_W-1), y & (VRAM_H-1)); -} -static uint16_t glb_vram_read(int x,int y){ ensure_cpu(); return sw_vram_read(x,y); } -static void glb_vram_transfer_in(int x,int y,int w,int h,const uint16_t *d){ - sw_vram_transfer_in(x,y,w,h,d); - depth24_upload_policy(); - if (s_depth24_skip_up && depth24_is_fb_transfer(x, y, w, h)) { - /* Full-VRAM restore (boot_state): must stage into the FBO or every - * texture page outside the movie band is missing after FMV→menus. - * Only the scanout band is remembered for clear-on-leave — do not - * union every skipped rect (misclassified texture pages would be - * wiped from the FBO when leaving depth24). */ - if (w >= VRAM_W && h >= VRAM_H) { - up_add_transfer(x, y, w, h); - rect_clear(&s_d24_skip_fb); - depth24_mark_scanout_band(); - coh_record(GL_COH_UPLOAD, x, y, x + w - 1, y + h - 1); - return; - } - depth24_mark_scanout_band(); - coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1); - return; - } - up_add_transfer(x, y, w, h); /* exact touched rects, incl. per-pixel wrap */ - coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1); -} -static void glb_vram_transfer_out(int x,int y,int w,int h,uint16_t *d){ ensure_cpu(); sw_vram_transfer_out(x,y,w,h,d); } - -/* ---- context init / present -------------------------------------------- */ -static void upload_present_tex(const uint32_t *pixels, int w, int h, int linear) { - glBindTexture(GL_TEXTURE_2D, s_present_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); - /* Re-assert clamp every upload: a stale REPEAT wrap samples past the - * right edge into garbage (MotK FMV right-strip flicker). */ - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - if (w != s_present_w || h != s_present_h) { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels); - s_present_w = w; s_present_h = h; - } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_BGRA, GL_UNSIGNED_BYTE, pixels); - } + /* Force a fresh present after FMV→15-bit so menus/loading screens + * are not held as a stale depth24 frame. */ + gl_renderer_invalidate_present(); + } + s_depth24_skip_up = d24; +} + +static void glb_vram_write(int x,int y,uint16_t px){ + sw_vram_write(x,y,px); + depth24_upload_policy(); + /* Point pokes are never MDEC frames — always stage to FBO. */ + up_add(x & (VRAM_W-1), y & (VRAM_H-1), x & (VRAM_W-1), y & (VRAM_H-1)); +} +static uint16_t glb_vram_read(int x,int y){ ensure_cpu(); return sw_vram_read(x,y); } +static void glb_vram_transfer_in(int x,int y,int w,int h,const uint16_t *d){ + sw_vram_transfer_in(x,y,w,h,d); + depth24_upload_policy(); + if (s_depth24_skip_up && depth24_is_fb_transfer(x, y, w, h)) { + /* Full-VRAM restore (boot_state): must stage into the FBO or every + * texture page outside the movie band is missing after FMV→menus. + * Only the scanout band is remembered for clear-on-leave — do not + * union every skipped rect (misclassified texture pages would be + * wiped from the FBO when leaving depth24). */ + if (w >= VRAM_W && h >= VRAM_H) { + up_add_transfer(x, y, w, h); + rect_clear(&s_d24_skip_fb); + depth24_mark_scanout_band(); + coh_record(GL_COH_UPLOAD, x, y, x + w - 1, y + h - 1); + return; + } + depth24_mark_scanout_band(); + coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1); + return; + } + up_add_transfer(x, y, w, h); /* exact touched rects, incl. per-pixel wrap */ + coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1); +} +static void glb_vram_transfer_out(int x,int y,int w,int h,uint16_t *d){ ensure_cpu(); sw_vram_transfer_out(x,y,w,h,d); } + +/* ---- context init / present -------------------------------------------- */ +static void upload_present_tex(const uint32_t *pixels, int w, int h, int linear) { + glBindTexture(GL_TEXTURE_2D, s_present_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); + /* Re-assert clamp every upload: a stale REPEAT wrap samples past the + * right edge into garbage (MotK FMV right-strip flicker). */ + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + if (w != s_present_w || h != s_present_h) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels); + s_present_w = w; s_present_h = h; + } else { + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_BGRA, GL_UNSIGNED_BYTE, pixels); + } } -static int s_fmv_filter_cfg = 0; /* VIDEO_FMV_FILTER_NEAREST */ +static int s_fmv_filter_cfg = 0; /* VIDEO_FMV_FILTER_NEAREST */ + +void gl_renderer_set_fmv_filter(int cfg_value) { + if (cfg_value >= 0 && cfg_value <= 3) s_fmv_filter_cfg = cfg_value; +} + +static int fmv_filter_mode(void) { + return s_fmv_filter_cfg - 1; +} + +/* Select the present-program sampling mode. s_present_prog is shared by the CPU + * present and both VRAM/FBO quad paths, and program uniforms persist, so every + * user states its choice rather than inheriting the last one's. sharp=0 keeps + * the historical straight sample. */ +static void present_set_sharp(int mode, int tex_w, int tex_h, + int out_w, int out_h) { + if (s_present_uSharp < 0) return; + if (mode <= 0 || tex_w <= 0 || tex_h <= 0) { + p_glUniform1i(s_present_uSharp, 0); + return; + } + p_glUniform1i(s_present_uSharp, mode); + if (s_present_uTexSize >= 0) + p_glUniform2f(s_present_uTexSize, (float)tex_w, (float)tex_h); + if (s_present_uSharpScale >= 0) + p_glUniform2f(s_present_uSharpScale, + (float)out_w / (float)tex_w, + (float)out_h / (float)tex_h); +} + +/* Push scanline uniforms into the currently-bound present or interpolation + * program. + * pitch_lines = the height of the TEXTURE that v_uv is normalized against + * (v_uv.y * pitch_lines is the texel-row coordinate, and one + * source texel row is one PS1 scanline). For the VRAM/FBO quad + * that is VRAM_H (v_uv spans only the display sub-range of the + * 512-row texture); for the CPU/interp textures, which hold + * exactly the display rect, it equals the display height. + * disp_lines = PS1 display lines actually shown, for the output-scale gate. + * out_h = letterbox height in window px. + * Pass pitch_lines/disp_lines <= 0 (OSD, bezel, or the already-composed + * hold-last drawable) to force the effect off — program uniforms persist, so + * every content draw must state its own choice. */ +static void present_set_scanline(GLint uOn, GLint uStr, GLint uLines, + GLint uScale, int pitch_lines, int disp_lines, + int out_h) { + if (uOn < 0) return; + int on = (s_scanline_on && pitch_lines > 0 && disp_lines > 0 && out_h > 0) + ? 1 : 0; + p_glUniform1i(uOn, on); + if (!on) return; + if (uStr >= 0) p_glUniform1f(uStr, s_scanline_strength); + if (uLines >= 0) p_glUniform1f(uLines, (float)pitch_lines); + if (uScale >= 0) p_glUniform1f(uScale, (float)out_h / (float)disp_lines); +} +#define PRESENT_SCANLINE(pitch, disp, oh) \ + present_set_scanline(s_present_uScanline, s_present_uScanStrength, \ + s_present_uScanLines, s_present_uScanScale, \ + (pitch), (disp), (oh)) +#define INTERP_SCANLINE(pitch, disp, oh) \ + present_set_scanline(s_interp_uScanline, s_interp_uScanStrength, \ + s_interp_uScanLines, s_interp_uScanScale, \ + (pitch), (disp), (oh)) + +/* Display aspect for the present letterbox. Default 4:3 (native). When a wide + * aspect is configured the 4:3 frame is stretched into it — paired with the + * GTE X-squash (gte_set_display_aspect) this nets a wider field of view. */ +static int s_aspect_num = 4, s_aspect_den = 3; + +void gl_renderer_set_display_aspect(int num, int den) { + if (num <= 0 || den <= 0) { num = 4; den = 3; } + s_aspect_num = num; s_aspect_den = den; +} + +/* Scanline post-process toggle (host display setting). strength is the depth of + * the dark gap between lines, 0..1 (0 = off-looking, 1 = fully black gap). The + * actual darkening is applied per-draw in the present/interpolation shaders and + * fades in with output scale — see PSX_SCANLINE_FUNC. */ +void gl_renderer_set_scanlines(int on, float strength) { + s_scanline_on = on ? 1 : 0; + if (strength < 0.f) strength = 0.f; + if (strength > 1.f) strength = 1.f; + s_scanline_strength = strength; +} + +int gl_renderer_get_scanlines(float *strength) { + if (strength) *strength = s_scanline_strength; + return s_scanline_on; +} + +/* Letterbox: largest num:den rect centered in the drawable. */ +static void letterbox_rect_aspect(int ww, int wh, int num, int den, + int *x, int *y, int *w, int *h) { + int dw = ww, dh = (ww * den) / num; + if (dh > wh) { dh = wh; dw = (wh * num) / den; } + *x = (ww - dw) / 2; + *y = (wh - dh) / 2; + *w = dw; *h = dh; +} +static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h) { + letterbox_rect_aspect(ww, wh, s_aspect_num, s_aspect_den, x, y, w, h); +} + +static GLuint make_tex(GLenum internal, int w, int h, GLenum fmt, GLenum type) { + GLuint t = 0; + glGenTextures(1, &t); + glBindTexture(GL_TEXTURE_2D, t); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, (GLint)internal, w, h, 0, fmt, type, NULL); + return t; +} + +static int make_fbo(GLuint *out_fbo, GLuint color_tex, GLuint stencil_rb) { + p_glGenFramebuffers(1, out_fbo); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, *out_fbo); + p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_tex, 0); + if (stencil_rb) + p_glFramebufferRenderbuffer(PSXGL_FRAMEBUFFER, PSXGL_DEPTH_STENCIL_ATTACHMENT, + PSXGL_RENDERBUFFER, stencil_rb); + GLenum st = p_glCheckFramebufferStatus(PSXGL_FRAMEBUFFER); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + if (st != PSXGL_FRAMEBUFFER_COMPLETE) { + fprintf(stdout, "psxrecomp: GL FBO incomplete (0x%X)\n", st); + return 0; + } + return 1; +} + +static int init_gpu_raster(void) { + s_scale = s_req_scale; + + s_geo_prog = build_program(GEO_VS, GEO_FS); + s_tex_prog = build_program_ex(TEX_VS, TEX_FS, 1); + s_blit_prog = build_program(BLIT_VS, BLIT_FS); + s_pack_prog = build_program(PACK_VS, PACK_FS); + s_stencil_prog = build_program(PACK_VS, STENCIL_FS); + if (!s_geo_prog || !s_tex_prog || !s_blit_prog || !s_pack_prog || !s_stencil_prog) return 0; + + s_conv = (uint32_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint32_t)); + if (!s_conv) return 0; + + int hw = VRAM_W * s_scale, hh = VRAM_H * s_scale; + s_hr_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE); + s_scratch_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE); + s_up_tex = make_tex(GL_RGBA8, VRAM_W, VRAM_H, GL_RGBA, GL_UNSIGNED_BYTE); + s_raw_tex = make_tex(PSXGL_R16UI, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT); + /* Force the driver's first texture-upload allocation while the renderer is + * initializing. NVIDIA otherwise defers it until the first MDEC frame, + * producing a measured ~33 ms glTexSubImage hitch and an audible underrun. */ + { + const uint32_t zero_rgba = 0; + const uint16_t zero_raw = 0; + glBindTexture(GL_TEXTURE_2D, s_up_tex); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, + GL_RGBA, GL_UNSIGNED_BYTE, &zero_rgba); + glBindTexture(GL_TEXTURE_2D, s_raw_tex); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, + PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, &zero_raw); + glFinish(); + } + + p_glGenRenderbuffers(1, &s_hr_rb); + p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_hr_rb); + p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, hw, hh); + p_glBindRenderbuffer(PSXGL_RENDERBUFFER, 0); + + if (!make_fbo(&s_hr_fbo, s_hr_tex, s_hr_rb)) return 0; + if (!make_fbo(&s_raw_fbo, s_raw_tex, 0)) return 0; + if (!make_fbo(&s_scratch_fbo, s_scratch_tex, 0)) return 0; + + s_uVram = p_glGetUniformLocation(s_tex_prog, "u_vram"); + s_uTpage = p_glGetUniformLocation(s_tex_prog, "u_tpage"); + s_uClut = p_glGetUniformLocation(s_tex_prog, "u_clut"); + s_uDepth = p_glGetUniformLocation(s_tex_prog, "u_depth"); + s_uRaw = p_glGetUniformLocation(s_tex_prog, "u_raw"); + s_uSemipass = p_glGetUniformLocation(s_tex_prog, "u_semipass"); + s_uSemimode = p_glGetUniformLocation(s_tex_prog, "u_semimode"); + s_uTwin = p_glGetUniformLocation(s_tex_prog, "u_twin"); + s_uMaskset = p_glGetUniformLocation(s_tex_prog, "u_maskset"); + s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter"); + s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits"); + s_uBlitSrc = p_glGetUniformLocation(s_blit_prog, "u_src"); + s_uBlitPass = p_glGetUniformLocation(s_blit_prog, "u_stp_pass"); + s_uBlitMaskset = p_glGetUniformLocation(s_blit_prog, "u_maskset"); + s_uBlitSrcDiv = p_glGetUniformLocation(s_blit_prog, "u_src_div"); + s_uBlitSrcOff = p_glGetUniformLocation(s_blit_prog, "u_src_off"); + s_uPackHr = p_glGetUniformLocation(s_pack_prog, "u_hr"); + s_uPackScale = p_glGetUniformLocation(s_pack_prog, "u_scale"); + s_uStencilSrc = p_glGetUniformLocation(s_stencil_prog, "u_src"); + s_geo_uXoff = p_glGetUniformLocation(s_geo_prog, "u_xoff"); + s_geo_uXhalf = p_glGetUniformLocation(s_geo_prog, "u_xhalf"); + s_tex_uXoff = p_glGetUniformLocation(s_tex_prog, "u_xoff"); + s_tex_uXhalf = p_glGetUniformLocation(s_tex_prog, "u_xhalf"); + s_geo_uXscale = p_glGetUniformLocation(s_geo_prog, "u_xscale"); + s_geo_uXcenter = p_glGetUniformLocation(s_geo_prog, "u_xcenter"); + s_tex_uXscale = p_glGetUniformLocation(s_tex_prog, "u_xscale"); + s_tex_uXcenter = p_glGetUniformLocation(s_tex_prog, "u_xcenter"); + /* Default the new uniforms to the no-op (1.0 scale, 0 centre) -- GLSL would + * otherwise zero them, collapsing all x to 0. */ + p_glUseProgram(s_geo_prog); + p_glUniform1f(s_geo_uXscale, 1.0f); p_glUniform1f(s_geo_uXcenter, 0.0f); + p_glUseProgram(s_tex_prog); + p_glUniform1f(s_tex_uXscale, 1.0f); p_glUniform1f(s_tex_uXcenter, 0.0f); + + /* Sample-grid alignment shift: half an HR pixel, set once (S is fixed + * for the lifetime of the pipeline). Backed off by 1/64 native px so + * primitive edges never land EXACTLY on sample centers — that float tie + * dropped 1px columns at quad seams (e.g. the 256px texture-page seam in + * Tomba's title background). The 1/64 bias keeps floor(uv) on the exact + * PS1 texel for |uv slope| < 64; mirrored (negative-slope) mappings can + * be off by one texel at exact-integer uv — accepted. */ + { + float shift = 0.5f / (float)s_scale - 1.0f / 64.0f; + p_glUseProgram(s_geo_prog); + p_glUniform1f(p_glGetUniformLocation(s_geo_prog, "u_shift"), shift); + /* Native-wide projection defaults: x translation 0, clip half-extent + * 512 — so the GEO_VS x term reduces to (x+u_shift)/512-1, bit-identical + * to the pre-native-wide projection. The wide passes set these, then + * restore these defaults. */ + p_glUniform1f(s_geo_uXoff, 0.0f); + p_glUniform1f(s_geo_uXhalf, 512.0f); + p_glUseProgram(s_tex_prog); + p_glUniform1f(p_glGetUniformLocation(s_tex_prog, "u_shift"), shift); + p_glUniform1f(s_tex_uXoff, 0.0f); + p_glUniform1f(s_tex_uXhalf, 512.0f); + p_glUseProgram(s_blit_prog); + p_glUniform1f(p_glGetUniformLocation(s_blit_prog, "u_shift"), shift); + p_glUseProgram(0); + } + + p_glGenVertexArrays(1, &s_geo_vao); + p_glBindVertexArray(s_geo_vao); + p_glGenBuffers(1, &s_geo_vbo); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); + p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0); + p_glEnableVertexAttribArray(0); + p_glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(2 * sizeof(float))); + p_glEnableVertexAttribArray(1); -void gl_renderer_set_fmv_filter(int cfg_value) { - if (cfg_value >= 0 && cfg_value <= 3) s_fmv_filter_cfg = cfg_value; -} + p_glGenVertexArrays(1, &s_tex_vao); + p_glBindVertexArray(s_tex_vao); + p_glGenBuffers(1, &s_tex_vbo); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); + { + GLsizei st = TEXV * sizeof(float); + p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, st, (void*)0); p_glEnableVertexAttribArray(0); /* pos */ + p_glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, st, (void*)(2*sizeof(float))); p_glEnableVertexAttribArray(1); /* uv */ + p_glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, st, (void*)(4*sizeof(float))); p_glEnableVertexAttribArray(2); /* col */ + p_glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, st, (void*)(8*sizeof(float))); p_glEnableVertexAttribArray(3); /* tpage */ + p_glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, st, (void*)(10*sizeof(float))); p_glEnableVertexAttribArray(4); /* clut */ + p_glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, st, (void*)(12*sizeof(float))); p_glEnableVertexAttribArray(5); /* depth */ + p_glVertexAttribPointer(6, 1, GL_FLOAT, GL_FALSE, st, (void*)(13*sizeof(float))); p_glEnableVertexAttribArray(6); /* raw */ + p_glVertexAttribPointer(7, 4, GL_FLOAT, GL_FALSE, st, (void*)(14*sizeof(float))); p_glEnableVertexAttribArray(7); /* limits */ + p_glVertexAttribPointer(8, 1, GL_FLOAT, GL_FALSE, st, (void*)(18*sizeof(float))); p_glEnableVertexAttribArray(8); /* semi */ + p_glVertexAttribPointer(9, 1, GL_FLOAT, GL_FALSE, st, (void*)(19*sizeof(float))); p_glEnableVertexAttribArray(9); /* q */ + } -static int fmv_filter_mode(void) { - return s_fmv_filter_cfg - 1; + p_glGenVertexArrays(1, &s_blit_vao); + p_glBindVertexArray(s_blit_vao); + p_glGenBuffers(1, &s_blit_vbo); + p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); + p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); + p_glEnableVertexAttribArray(0); + + p_glGenVertexArrays(1, &s_empty_vao); + p_glBindVertexArray(0); + + /* Clear the authoritative surface (color + stencil) and queue a full + * upload of whatever the CPU VRAM already holds (pre-context software + * draws, BIOS logo state, ...). */ + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); + glDisable(GL_SCISSOR_TEST); + glClearColor(0, 0, 0, 0); + glClearStencil(0); + glStencilMask(0xFF); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + rect_clear(&s_pack_dirty); + s_up_nrects = 0; + up_add(0, 0, VRAM_W - 1, VRAM_H - 1); + s_gpu_dirty = 0; + s_stencil_valid = 1; + for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull; + s_last_present_path = -1; + + /* Native-wide compositor surfaces start unallocated (lazily created when a + * widescreen game calls wide_configure + wide_set_target). */ + for (int i = 0; i < WIDE_MAX_SURF; i++) { + s_wide_tex[i] = 0; s_wide_fbo[i] = 0; s_wide_base[i] = -1; + } + g_wide_w = 0; g_wide_off = 0; g_wide_cur = 0; g_wide_cur_base = 0; + + s_raster_ok = 1; + gl_perf_init(); /* frame_perf GPU/CPU phase timing (no-op if queries absent) */ + if (s_cpu_auth_dual) { + fprintf(stdout, "psxrecomp: GL GPU pipeline ready (dual-raster, " + "internal scale %dx, SW@1x authority, no FBO readback)\n", + s_scale); + } else { + fprintf(stdout, "psxrecomp: GL GPU pipeline ready (internal scale %dx, " + "mask-bit stencil, texture window, GPU copy/upload)\n", s_scale); + } + return 1; } -/* Select the present-program sampling mode. s_present_prog is shared by the CPU - * present and both VRAM/FBO quad paths, and program uniforms persist, so every - * user states its choice rather than inheriting the last one's. sharp=0 keeps - * the historical straight sample. */ -static void present_set_sharp(int mode, int tex_w, int tex_h, - int out_w, int out_h) { - if (s_present_uSharp < 0) return; - if (mode <= 0 || tex_w <= 0 || tex_h <= 0) { - p_glUniform1i(s_present_uSharp, 0); - return; +int gl_renderer_init_context(SDL_Window *win) { + s_win = win; + s_present_w = 0; + s_present_h = 0; + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + s_ctx = SDL_GL_CreateContext(win); + if (!s_ctx) { fprintf(stdout, "psxrecomp: GL context creation failed (%s)\n", SDL_GetError()); return 0; } + if (SDL_GL_MakeCurrent(win, s_ctx) != 0) { fprintf(stdout, "psxrecomp: MakeCurrent failed (%s)\n", SDL_GetError()); SDL_GL_DeleteContext(s_ctx); s_ctx=NULL; return 0; } + /* Swap interval: 1=vsync (tear-free, default), 0=immediate (lowest display + * latency, may tear; our wall-clock pacer still holds 59.94Hz), -1=adaptive. + * Adaptive falls back to vsync if the driver rejects it. */ + if (SDL_GL_SetSwapInterval(s_swap_interval) != 0 && s_swap_interval < 0) { + SDL_GL_SetSwapInterval(1); + s_swap_interval = 1; } - p_glUniform1i(s_present_uSharp, mode); - if (s_present_uTexSize >= 0) - p_glUniform2f(s_present_uTexSize, (float)tex_w, (float)tex_h); - if (s_present_uSharpScale >= 0) - p_glUniform2f(s_present_uSharpScale, - (float)out_w / (float)tex_w, - (float)out_h / (float)tex_h); -} - -/* Display aspect for the present letterbox. Default 4:3 (native). When a wide - * aspect is configured the 4:3 frame is stretched into it — paired with the - * GTE X-squash (gte_set_display_aspect) this nets a wider field of view. */ -static int s_aspect_num = 4, s_aspect_den = 3; - -void gl_renderer_set_display_aspect(int num, int den) { - if (num <= 0 || den <= 0) { num = 4; den = 3; } - s_aspect_num = num; s_aspect_den = den; -} - -/* Letterbox: largest num:den rect centered in the drawable. */ -static void letterbox_rect_aspect(int ww, int wh, int num, int den, - int *x, int *y, int *w, int *h) { - int dw = ww, dh = (ww * den) / num; - if (dh > wh) { dh = wh; dw = (wh * num) / den; } - *x = (ww - dw) / 2; - *y = (wh - dh) / 2; - *w = dw; *h = dh; -} -static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h) { - letterbox_rect_aspect(ww, wh, s_aspect_num, s_aspect_den, x, y, w, h); -} - -static GLuint make_tex(GLenum internal, int w, int h, GLenum fmt, GLenum type) { - GLuint t = 0; - glGenTextures(1, &t); - glBindTexture(GL_TEXTURE_2D, t); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, (GLint)internal, w, h, 0, fmt, type, NULL); - return t; -} - -static int make_fbo(GLuint *out_fbo, GLuint color_tex, GLuint stencil_rb) { - p_glGenFramebuffers(1, out_fbo); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, *out_fbo); - p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_tex, 0); - if (stencil_rb) - p_glFramebufferRenderbuffer(PSXGL_FRAMEBUFFER, PSXGL_DEPTH_STENCIL_ATTACHMENT, - PSXGL_RENDERBUFFER, stencil_rb); - GLenum st = p_glCheckFramebufferStatus(PSXGL_FRAMEBUFFER); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - if (st != PSXGL_FRAMEBUFFER_COMPLETE) { - fprintf(stdout, "psxrecomp: GL FBO incomplete (0x%X)\n", st); - return 0; - } - return 1; -} - -static int init_gpu_raster(void) { - s_scale = s_req_scale; - - s_geo_prog = build_program(GEO_VS, GEO_FS); - s_tex_prog = build_program_ex(TEX_VS, TEX_FS, 1); - s_blit_prog = build_program(BLIT_VS, BLIT_FS); - s_pack_prog = build_program(PACK_VS, PACK_FS); - s_stencil_prog = build_program(PACK_VS, STENCIL_FS); - if (!s_geo_prog || !s_tex_prog || !s_blit_prog || !s_pack_prog || !s_stencil_prog) return 0; - - s_conv = (uint32_t *)malloc((size_t)VRAM_W * VRAM_H * sizeof(uint32_t)); - if (!s_conv) return 0; - - int hw = VRAM_W * s_scale, hh = VRAM_H * s_scale; - s_hr_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE); - s_scratch_tex = make_tex(GL_RGBA8, hw, hh, GL_RGBA, GL_UNSIGNED_BYTE); - s_up_tex = make_tex(GL_RGBA8, VRAM_W, VRAM_H, GL_RGBA, GL_UNSIGNED_BYTE); - s_raw_tex = make_tex(PSXGL_R16UI, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT); - /* Force the driver's first texture-upload allocation while the renderer is - * initializing. NVIDIA otherwise defers it until the first MDEC frame, - * producing a measured ~33 ms glTexSubImage hitch and an audible underrun. */ - { - const uint32_t zero_rgba = 0; - const uint16_t zero_raw = 0; - glBindTexture(GL_TEXTURE_2D, s_up_tex); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, - GL_RGBA, GL_UNSIGNED_BYTE, &zero_rgba); - glBindTexture(GL_TEXTURE_2D, s_raw_tex); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, - PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, &zero_raw); - glFinish(); - } - - p_glGenRenderbuffers(1, &s_hr_rb); - p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_hr_rb); - p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, hw, hh); - p_glBindRenderbuffer(PSXGL_RENDERBUFFER, 0); - - if (!make_fbo(&s_hr_fbo, s_hr_tex, s_hr_rb)) return 0; - if (!make_fbo(&s_raw_fbo, s_raw_tex, 0)) return 0; - if (!make_fbo(&s_scratch_fbo, s_scratch_tex, 0)) return 0; - - s_uVram = p_glGetUniformLocation(s_tex_prog, "u_vram"); - s_uTpage = p_glGetUniformLocation(s_tex_prog, "u_tpage"); - s_uClut = p_glGetUniformLocation(s_tex_prog, "u_clut"); - s_uDepth = p_glGetUniformLocation(s_tex_prog, "u_depth"); - s_uRaw = p_glGetUniformLocation(s_tex_prog, "u_raw"); - s_uSemipass = p_glGetUniformLocation(s_tex_prog, "u_semipass"); - s_uSemimode = p_glGetUniformLocation(s_tex_prog, "u_semimode"); - s_uTwin = p_glGetUniformLocation(s_tex_prog, "u_twin"); - s_uMaskset = p_glGetUniformLocation(s_tex_prog, "u_maskset"); - s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter"); - s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits"); - s_uBlitSrc = p_glGetUniformLocation(s_blit_prog, "u_src"); - s_uBlitPass = p_glGetUniformLocation(s_blit_prog, "u_stp_pass"); - s_uBlitMaskset = p_glGetUniformLocation(s_blit_prog, "u_maskset"); - s_uBlitSrcDiv = p_glGetUniformLocation(s_blit_prog, "u_src_div"); - s_uBlitSrcOff = p_glGetUniformLocation(s_blit_prog, "u_src_off"); - s_uPackHr = p_glGetUniformLocation(s_pack_prog, "u_hr"); - s_uPackScale = p_glGetUniformLocation(s_pack_prog, "u_scale"); - s_uStencilSrc = p_glGetUniformLocation(s_stencil_prog, "u_src"); - s_geo_uXoff = p_glGetUniformLocation(s_geo_prog, "u_xoff"); - s_geo_uXhalf = p_glGetUniformLocation(s_geo_prog, "u_xhalf"); - s_tex_uXoff = p_glGetUniformLocation(s_tex_prog, "u_xoff"); - s_tex_uXhalf = p_glGetUniformLocation(s_tex_prog, "u_xhalf"); - s_geo_uXscale = p_glGetUniformLocation(s_geo_prog, "u_xscale"); - s_geo_uXcenter = p_glGetUniformLocation(s_geo_prog, "u_xcenter"); - s_tex_uXscale = p_glGetUniformLocation(s_tex_prog, "u_xscale"); - s_tex_uXcenter = p_glGetUniformLocation(s_tex_prog, "u_xcenter"); - /* Default the new uniforms to the no-op (1.0 scale, 0 centre) -- GLSL would - * otherwise zero them, collapsing all x to 0. */ - p_glUseProgram(s_geo_prog); - p_glUniform1f(s_geo_uXscale, 1.0f); p_glUniform1f(s_geo_uXcenter, 0.0f); - p_glUseProgram(s_tex_prog); - p_glUniform1f(s_tex_uXscale, 1.0f); p_glUniform1f(s_tex_uXcenter, 0.0f); - - /* Sample-grid alignment shift: half an HR pixel, set once (S is fixed - * for the lifetime of the pipeline). Backed off by 1/64 native px so - * primitive edges never land EXACTLY on sample centers — that float tie - * dropped 1px columns at quad seams (e.g. the 256px texture-page seam in - * Tomba's title background). The 1/64 bias keeps floor(uv) on the exact - * PS1 texel for |uv slope| < 64; mirrored (negative-slope) mappings can - * be off by one texel at exact-integer uv — accepted. */ - { - float shift = 0.5f / (float)s_scale - 1.0f / 64.0f; - p_glUseProgram(s_geo_prog); - p_glUniform1f(p_glGetUniformLocation(s_geo_prog, "u_shift"), shift); - /* Native-wide projection defaults: x translation 0, clip half-extent - * 512 — so the GEO_VS x term reduces to (x+u_shift)/512-1, bit-identical - * to the pre-native-wide projection. The wide passes set these, then - * restore these defaults. */ - p_glUniform1f(s_geo_uXoff, 0.0f); - p_glUniform1f(s_geo_uXhalf, 512.0f); - p_glUseProgram(s_tex_prog); - p_glUniform1f(p_glGetUniformLocation(s_tex_prog, "u_shift"), shift); - p_glUniform1f(s_tex_uXoff, 0.0f); - p_glUniform1f(s_tex_uXhalf, 512.0f); - p_glUseProgram(s_blit_prog); - p_glUniform1f(p_glGetUniformLocation(s_blit_prog, "u_shift"), shift); - p_glUseProgram(0); - } - - p_glGenVertexArrays(1, &s_geo_vao); - p_glBindVertexArray(s_geo_vao); - p_glGenBuffers(1, &s_geo_vbo); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_geo_vbo); - p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0); - p_glEnableVertexAttribArray(0); - p_glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(2 * sizeof(float))); - p_glEnableVertexAttribArray(1); - - p_glGenVertexArrays(1, &s_tex_vao); - p_glBindVertexArray(s_tex_vao); - p_glGenBuffers(1, &s_tex_vbo); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo); - { - GLsizei st = TEXV * sizeof(float); - p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, st, (void*)0); p_glEnableVertexAttribArray(0); /* pos */ - p_glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, st, (void*)(2*sizeof(float))); p_glEnableVertexAttribArray(1); /* uv */ - p_glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, st, (void*)(4*sizeof(float))); p_glEnableVertexAttribArray(2); /* col */ - p_glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, st, (void*)(8*sizeof(float))); p_glEnableVertexAttribArray(3); /* tpage */ - p_glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, st, (void*)(10*sizeof(float))); p_glEnableVertexAttribArray(4); /* clut */ - p_glVertexAttribPointer(5, 1, GL_FLOAT, GL_FALSE, st, (void*)(12*sizeof(float))); p_glEnableVertexAttribArray(5); /* depth */ - p_glVertexAttribPointer(6, 1, GL_FLOAT, GL_FALSE, st, (void*)(13*sizeof(float))); p_glEnableVertexAttribArray(6); /* raw */ - p_glVertexAttribPointer(7, 4, GL_FLOAT, GL_FALSE, st, (void*)(14*sizeof(float))); p_glEnableVertexAttribArray(7); /* limits */ - p_glVertexAttribPointer(8, 1, GL_FLOAT, GL_FALSE, st, (void*)(18*sizeof(float))); p_glEnableVertexAttribArray(8); /* semi */ - p_glVertexAttribPointer(9, 1, GL_FLOAT, GL_FALSE, st, (void*)(19*sizeof(float))); p_glEnableVertexAttribArray(9); /* q */ - } - - p_glGenVertexArrays(1, &s_blit_vao); - p_glBindVertexArray(s_blit_vao); - p_glGenBuffers(1, &s_blit_vbo); - p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_blit_vbo); - p_glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0); - p_glEnableVertexAttribArray(0); - - p_glGenVertexArrays(1, &s_empty_vao); - p_glBindVertexArray(0); - - /* Clear the authoritative surface (color + stencil) and queue a full - * upload of whatever the CPU VRAM already holds (pre-context software - * draws, BIOS logo state, ...). */ - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hr_fbo); - glDisable(GL_SCISSOR_TEST); - glClearColor(0, 0, 0, 0); - glClearStencil(0); - glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - rect_clear(&s_pack_dirty); - s_up_nrects = 0; - up_add(0, 0, VRAM_W - 1, VRAM_H - 1); - s_gpu_dirty = 0; - s_stencil_valid = 1; - for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull; - s_last_present_path = -1; - - /* Native-wide compositor surfaces start unallocated (lazily created when a - * widescreen game calls wide_configure + wide_set_target). */ - for (int i = 0; i < WIDE_MAX_SURF; i++) { - s_wide_tex[i] = 0; s_wide_fbo[i] = 0; s_wide_base[i] = -1; - } - g_wide_w = 0; g_wide_off = 0; g_wide_cur = 0; g_wide_cur_base = 0; - - s_raster_ok = 1; - gl_perf_init(); /* frame_perf GPU/CPU phase timing (no-op if queries absent) */ - if (s_cpu_auth_dual) { - fprintf(stdout, "psxrecomp: GL GPU pipeline ready (dual-raster, " - "internal scale %dx, SW@1x authority, no FBO readback)\n", - s_scale); - } else { - fprintf(stdout, "psxrecomp: GL GPU pipeline ready (internal scale %dx, " - "mask-bit stencil, texture window, GPU copy/upload)\n", s_scale); - } - return 1; -} - -int gl_renderer_init_context(SDL_Window *win) { - s_win = win; - s_present_w = 0; - s_present_h = 0; - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); - s_ctx = SDL_GL_CreateContext(win); - if (!s_ctx) { fprintf(stdout, "psxrecomp: GL context creation failed (%s)\n", SDL_GetError()); return 0; } - if (SDL_GL_MakeCurrent(win, s_ctx) != 0) { fprintf(stdout, "psxrecomp: MakeCurrent failed (%s)\n", SDL_GetError()); SDL_GL_DeleteContext(s_ctx); s_ctx=NULL; return 0; } - /* Swap interval: 1=vsync (tear-free, default), 0=immediate (lowest display - * latency, may tear; our wall-clock pacer still holds 59.94Hz), -1=adaptive. - * Adaptive falls back to vsync if the driver rejects it. */ - if (SDL_GL_SetSwapInterval(s_swap_interval) != 0 && s_swap_interval < 0) { - SDL_GL_SetSwapInterval(1); - s_swap_interval = 1; - } - glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); - const char *ver = (const char *)glGetString(GL_VERSION); - fprintf(stdout, "psxrecomp: OpenGL context created (%s)\n", ver ? ver : "?"); - - /* All-or-nothing: any missing entry point / failed shader / bad FBO means - * the whole GL renderer is unavailable and the runtime stays on the pure - * software path — no half-GL hybrid (that mixed mode is what produced - * the alternating-present menu jitter). */ - int ok = load_modern_gl(); - if (ok) { - glGenTextures(1, &s_present_tex); - glBindTexture(GL_TEXTURE_2D, s_present_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - s_present_prog = build_program(PRESENT_VS, PRESENT_FS); - s_interp_prog = build_program(PRESENT_VS, INTERP_FS); - if (s_present_prog && s_interp_prog) { - p_glGenVertexArrays(1, &s_present_vao); - s_present_uTex = p_glGetUniformLocation(s_present_prog, "u_tex"); - s_present_uUvRect = p_glGetUniformLocation(s_present_prog, "u_uv_rect"); + glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); + const char *ver = (const char *)glGetString(GL_VERSION); + fprintf(stdout, "psxrecomp: OpenGL context created (%s)\n", ver ? ver : "?"); + + /* All-or-nothing: any missing entry point / failed shader / bad FBO means + * the whole GL renderer is unavailable and the runtime stays on the pure + * software path — no half-GL hybrid (that mixed mode is what produced + * the alternating-present menu jitter). */ + int ok = load_modern_gl(); + if (ok) { + glGenTextures(1, &s_present_tex); + glBindTexture(GL_TEXTURE_2D, s_present_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + s_present_prog = build_program(PRESENT_VS, PRESENT_FS); + s_interp_prog = build_program(PRESENT_VS, INTERP_FS); + if (s_present_prog && s_interp_prog) { + p_glGenVertexArrays(1, &s_present_vao); + s_present_uTex = p_glGetUniformLocation(s_present_prog, "u_tex"); + s_present_uUvRect = p_glGetUniformLocation(s_present_prog, "u_uv_rect"); s_present_uTexSize = p_glGetUniformLocation(s_present_prog, "u_tex_size"); s_present_uSharpScale = p_glGetUniformLocation(s_present_prog, "u_sharp_scale"); s_present_uSharp = p_glGetUniformLocation(s_present_prog, "u_sharp"); - s_interp_uPrev = p_glGetUniformLocation(s_interp_prog, "u_prev"); - s_interp_uCurr = p_glGetUniformLocation(s_interp_prog, "u_curr"); - s_interp_uAlpha = p_glGetUniformLocation(s_interp_prog, "u_alpha"); - s_interp_uUvRect = p_glGetUniformLocation(s_interp_prog, "u_uv_rect"); - s_interp_uBlendMode = - p_glGetUniformLocation(s_interp_prog, "u_blend_mode"); - glGenTextures(3, s_interp_tex); - for (int i = 0; i < 3; i++) { - glBindTexture(GL_TEXTURE_2D, s_interp_tex[i]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } - } else ok = 0; - } - if (ok) ok = init_gpu_raster(); - if (!ok) { - fprintf(stdout, "psxrecomp: GL pipeline init failed — falling back to software renderer\n"); - SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; - s_raster_ok = 0; - return 0; - } - return 1; -} - -/* Set the GL swap interval (vsync mode): 1=vsync, 0=immediate, -1=adaptive. - * Safe to call before or after context creation; applies live when a context - * exists. Adaptive falls back to vsync if unsupported. */ -void gl_renderer_set_swap_interval(int interval) { - s_swap_interval = interval; - if (s_ctx) { - if (SDL_GL_SetSwapInterval(interval) != 0 && interval < 0) { - SDL_GL_SetSwapInterval(1); - s_swap_interval = 1; - } - } -} - + s_present_uScanline = + p_glGetUniformLocation(s_present_prog, "u_scanline"); + s_present_uScanStrength = + p_glGetUniformLocation(s_present_prog, "u_scanline_strength"); + s_present_uScanLines = + p_glGetUniformLocation(s_present_prog, "u_scanline_lines"); + s_present_uScanScale = + p_glGetUniformLocation(s_present_prog, "u_scanline_scale"); + s_interp_uScanline = + p_glGetUniformLocation(s_interp_prog, "u_scanline"); + s_interp_uScanStrength = + p_glGetUniformLocation(s_interp_prog, "u_scanline_strength"); + s_interp_uScanLines = + p_glGetUniformLocation(s_interp_prog, "u_scanline_lines"); + s_interp_uScanScale = + p_glGetUniformLocation(s_interp_prog, "u_scanline_scale"); + s_interp_uPrev = p_glGetUniformLocation(s_interp_prog, "u_prev"); + s_interp_uCurr = p_glGetUniformLocation(s_interp_prog, "u_curr"); + s_interp_uAlpha = p_glGetUniformLocation(s_interp_prog, "u_alpha"); + s_interp_uUvRect = p_glGetUniformLocation(s_interp_prog, "u_uv_rect"); + s_interp_uBlendMode = + p_glGetUniformLocation(s_interp_prog, "u_blend_mode"); + glGenTextures(3, s_interp_tex); + for (int i = 0; i < 3; i++) { + glBindTexture(GL_TEXTURE_2D, s_interp_tex[i]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + } else ok = 0; + } + if (ok) ok = init_gpu_raster(); + if (!ok) { + fprintf(stdout, "psxrecomp: GL pipeline init failed — falling back to software renderer\n"); + SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; + s_raster_ok = 0; + return 0; + } + return 1; +} + +/* Set the GL swap interval (vsync mode): 1=vsync, 0=immediate, -1=adaptive. + * Safe to call before or after context creation; applies live when a context + * exists. Adaptive falls back to vsync if unsupported. */ +void gl_renderer_set_swap_interval(int interval) { + s_swap_interval = interval; + if (s_ctx) { + if (SDL_GL_SetSwapInterval(interval) != 0 && interval < 0) { + SDL_GL_SetSwapInterval(1); + s_swap_interval = 1; + } + } +} + void gl_renderer_shutdown(void) { if (s_ctx) { ensure_cpu(); SDL_GL_DeleteContext(s_ctx); s_ctx = NULL; } - free(s_conv); s_conv = NULL; - s_raster_ok = 0; - /* New context regenerates s_present_tex empty; a stale size makes - * upload_present_tex take glTexSubImage2D into an unallocated texture - * (rematch 24-bit FMV → black picture, audio still runs). */ - s_present_w = 0; - s_present_h = 0; - s_osd_tex = 0; - s_osd_tw = 0; - s_osd_th = 0; - s_depth24_skip_up = 0; - rect_clear(&s_d24_skip_fb); - hold_invalidate(); - s_hold_tex = 0; - s_hold_fbo = 0; - s_hold_tw = 0; - s_hold_th = 0; -} - -/* CPU-readout present (24-bit FMV frames and the PSX_GL_FORCE_CPU_PRESENT - * diagnostic): full-window clear, then a quad into the letterbox rect. - * force_4_3 pins the rect to native 4:3 regardless of the display aspect — - * FMVs are authored 4:3 and have no GTE squash to compensate a stretch, so - * widescreen presents them pillarboxed instead of distorted. */ -void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear, - int force_4_3, int content_w) { - if (!s_ctx) return; - interp_reset_history(); - int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); - glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); - int lx, ly, lw, lh; - if (force_4_3) - letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); - else - letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); - /* Short GP1(07h) bands (MotK FMV is 128 lines) only fill a fraction of - * NTSC active height on hardware. Stretching them to the full letterbox - * doubles vertical scale vs horizontal and makes the frame look too wide - * with the right edge clipped. Letterbox within the present rect instead. - * Apply whenever the source is short — not only when force_4_3 — so a - * misclassified FMV frame still keeps correct pixel aspect. */ - /* Genuinely windowed video bands only (<80% of the 240-line field, e.g. - * MotK's 128-line FMV). A game's native short display mode (216/224) - * fills the rect as on hardware. */ - if (src_h > 0 && src_h < 192) { - int content_h = (lh * src_h) / 240; - if (content_h < 1) content_h = 1; - ly += (lh - content_h) / 2; - lh = content_h; - } - /* Optional trailing-column crop (depth24 margin): shrink the draw width - * left-aligned so cleared black remains on the right — never stretch. */ - float uv_x1 = 1.f; - int crop = (content_w > 0 && content_w < src_w && src_w > 0); - if (crop) { - uv_x1 = (float)content_w / (float)src_w; - lw = (lw * content_w) / src_w; - if (lw < 1) lw = 1; - } + free(s_conv); s_conv = NULL; + s_raster_ok = 0; + /* New context regenerates s_present_tex empty; a stale size makes + * upload_present_tex take glTexSubImage2D into an unallocated texture + * (rematch 24-bit FMV → black picture, audio still runs). */ + s_present_w = 0; + s_present_h = 0; + s_osd_tex = 0; + s_osd_tw = 0; + s_osd_th = 0; + s_depth24_skip_up = 0; + rect_clear(&s_d24_skip_fb); + hold_invalidate(); + s_hold_tex = 0; + s_hold_fbo = 0; + s_hold_tw = 0; + s_hold_th = 0; +} + +/* CPU-readout present (24-bit FMV frames and the PSX_GL_FORCE_CPU_PRESENT + * diagnostic): full-window clear, then a quad into the letterbox rect. + * force_4_3 pins the rect to native 4:3 regardless of the display aspect — + * FMVs are authored 4:3 and have no GTE squash to compensate a stretch, so + * widescreen presents them pillarboxed instead of distorted. */ +void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear, + int force_4_3, int content_w) { + if (!s_ctx) return; + interp_reset_history(); + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); + glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); + int lx, ly, lw, lh; + if (force_4_3) + letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); + else + letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); + /* Short GP1(07h) bands (MotK FMV is 128 lines) only fill a fraction of + * NTSC active height on hardware. Stretching them to the full letterbox + * doubles vertical scale vs horizontal and makes the frame look too wide + * with the right edge clipped. Letterbox within the present rect instead. + * Apply whenever the source is short — not only when force_4_3 — so a + * misclassified FMV frame still keeps correct pixel aspect. */ + /* Genuinely windowed video bands only (<80% of the 240-line field, e.g. + * MotK's 128-line FMV). A game's native short display mode (216/224) + * fills the rect as on hardware. */ + if (src_h > 0 && src_h < 192) { + int content_h = (lh * src_h) / 240; + if (content_h < 1) content_h = 1; + ly += (lh - content_h) / 2; + lh = content_h; + } + /* Optional trailing-column crop (depth24 margin): shrink the draw width + * left-aligned so cleared black remains on the right — never stretch. */ + float uv_x1 = 1.f; + int crop = (content_w > 0 && content_w < src_w && src_w > 0); + if (crop) { + uv_x1 = (float)content_w / (float)src_w; + lw = (lw * content_w) / src_w; + if (lw < 1) lw = 1; + } /* This is the low-res source path (24-bit FMV, and the forced-CPU present * diagnostic): a 320x192-class image blown up to fill the window, so how it * is reconstructed is very visible. `linear` (the video AA setting) allows @@ -2990,717 +3101,720 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea int filt_mode = linear ? fmv_filter_mode() : -1; /* AA off: nearest, no shader work */ glViewport(lx, ly, lw, lh); - p_glActiveTexture(PSXGL_TEXTURE0); + p_glActiveTexture(PSXGL_TEXTURE0); upload_present_tex(pixels, src_w, src_h, filt_mode >= 0 ? 1 : 0); - p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); + p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); present_set_sharp(filt_mode, src_w, src_h, lw, lh); - if (crop) { - /* Cropped present keeps left-aligned content; still inset so linear - * AA does not blend the cut column with undefined border texels. */ - float u0 = (src_w > 0) ? (0.5f / (float)src_w) : 0.f; - float v0 = (src_h > 0) ? (0.5f / (float)src_h) : 0.f; - p_glUniform4f(s_present_uUvRect, u0, v0, uv_x1 - u0, 1.f - v0); - } else if (src_w > 0 && src_h > 0) { - /* Half-texel UV inset for both nearest and linear. Corner-mapped - * UV=1.0 grazes past the last texel (driver-dependent border sample); - * with GL_LINEAR that also blends an edge stripe into the image. - * Matches present_target_quad / MotK present UV edge-bleed fix. */ - float u0 = 0.5f / (float)src_w, v0 = 0.5f / (float)src_h; - p_glUniform4f(s_present_uUvRect, u0, v0, 1.f - u0, 1.f - v0); - } else { - p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f); - } - p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3); - p_glBindVertexArray(0); p_glUseProgram(0); + /* CPU present texture holds exactly the display rect, so v_uv spans it and + * pitch == display height == src_h. */ + PRESENT_SCANLINE(src_h, src_h, lh); + if (crop) { + /* Cropped present keeps left-aligned content; still inset so linear + * AA does not blend the cut column with undefined border texels. */ + float u0 = (src_w > 0) ? (0.5f / (float)src_w) : 0.f; + float v0 = (src_h > 0) ? (0.5f / (float)src_h) : 0.f; + p_glUniform4f(s_present_uUvRect, u0, v0, uv_x1 - u0, 1.f - v0); + } else if (src_w > 0 && src_h > 0) { + /* Half-texel UV inset for both nearest and linear. Corner-mapped + * UV=1.0 grazes past the last texel (driver-dependent border sample); + * with GL_LINEAR that also blends an edge stripe into the image. + * Matches present_target_quad / MotK present UV edge-bleed fix. */ + float u0 = 0.5f / (float)src_w, v0 = 0.5f / (float)src_h; + p_glUniform4f(s_present_uUvRect, u0, v0, 1.f - u0, 1.f - v0); + } else { + p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f); + } + p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3); + p_glBindVertexArray(0); p_glUseProgram(0); pres_record(GL_PRES_CPU, 0, 0, src_w, src_h, lx, ly, lw, lh); - hold_capture_drawable(); - latency_ring_mark(LAT_SWAP_BEGIN); - gl_swap_with_osd(); - latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; - present_force_consumed(); - s_last_present_path = GL_PRES_CPU; -} - -void gl_renderer_present_blank(void) { - if (!s_ctx) return; - interp_reset_history(); - int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); - pres_record(GL_PRES_BLANK, 0, 0, 0, 0, 0, 0, ww, wh); - hold_capture_drawable(); - latency_ring_mark(LAT_SWAP_BEGIN); - gl_swap_with_osd(); - latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; - present_force_consumed(); - s_last_present_path = GL_PRES_BLANK; -} - -/* Sync the authoritative FBO down into CPU VRAM (no-op when current). - * Screenshots / debug server. Not for 24-bit FMV scanout (see flush). */ -void gl_renderer_sync_cpu(void) { - ensure_cpu(); -} - -void gl_renderer_invalidate_present(void) { - for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull; - s_last_present_path = -1; - s_force_present_remaining = 8; - hold_invalidate(); - interp_reset_history(); -} - -void gl_renderer_restage_vram_after_savestate(void) { - if (!s_raster_ok || !s_vram) return; - /* Belt-and-suspenders after boot_state VRAM apply: force CPU mirror → FBO - * even if a depth24 skip swallowed the restore, then re-arm scanout-band - * clear so leaving FMV does not keep RGB888-as-1555 junk. */ - s_up_nrects = 0; - rect_clear(&s_d24_skip_fb); - s_depth24_skip_up = 0; - up_add_transfer(0, 0, VRAM_W, VRAM_H); - flush_cpu_upload(); - if (gpu_display_is_depth24()) { - s_depth24_skip_up = 1; - depth24_mark_scanout_band(); - } - /* Dual-raster: CPU is authority after snap; FBO is cosmetics only. */ - if (s_cpu_auth_dual) - s_gpu_dirty = 0; -} - -void gl_renderer_set_cpu_auth_dual(int on) { - s_cpu_auth_dual = on ? 1 : 0; - if (s_cpu_auth_dual) - s_gpu_dirty = 0; -} - -int gl_renderer_cpu_auth_dual(void) { - return s_cpu_auth_dual; -} - -void gl_renderer_present_probe_reset(void) { - s_probe_skip = 0; - s_probe_swap = 0; - s_probe_dirty_marks = 0; -} - -void gl_renderer_present_probe_take(uint64_t *skip_delta, uint64_t *swap_delta, - uint64_t *dirty_mark_delta, - int *force_remaining) { - if (skip_delta) { *skip_delta = s_probe_skip; s_probe_skip = 0; } - if (swap_delta) { *swap_delta = s_probe_swap; s_probe_swap = 0; } - if (dirty_mark_delta) { - *dirty_mark_delta = s_probe_dirty_marks; - s_probe_dirty_marks = 0; - } - if (force_remaining) *force_remaining = s_force_present_remaining; -} - -int gl_renderer_present_rect_dirty(int disp_x, int disp_y, int w, int h) { - if (!s_raster_ok || w <= 0 || h <= 0) return 0; - return present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1); -} - -void gl_renderer_flush_cpu_uploads(void) { - if (!s_raster_ok) return; - flush_flat_batch(); - flush_tex_batch(); - flush_cpu_upload(); -} - -/* Diagnostic (debug server "gl_fbo_peek"): read a rect of the GPU-side - * authoritative VRAM (via the pack pass + raw mirror) WITHOUT writing CPU - * VRAM — lets a probe diff FBO truth against CPU truth. Returns 0 when the - * GL pipeline is inactive (software backend). */ -int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out) { - if (!s_raster_ok || !s_ctx) return 0; - if (x < 0 || y < 0 || w < 1 || h < 1 || - x + w > VRAM_W || y + h > VRAM_H) return 0; - flush_cpu_upload(); - rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1); - pack_flush(); - coh_record(GL_COH_PEEK, x, y, x + w - 1, y + h - 1); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); - glPixelStorei(GL_PACK_ALIGNMENT, 2); - glReadPixels(x, y, w, h, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, out); - glPixelStorei(GL_PACK_ALIGNMENT, 4); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - return 1; -} - -/* Diagnostic (debug server "gl_vram_diff"): full-VRAM comparison of the - * GPU-side truth (FBO via pack) against the CPU array, WITHOUT writing - * either. Reports mismatch count + bounding box + a few sample coords. - * Divergence is expected where the GPU is legitimately ahead (gpu_dirty); - * at upload-only scenes the two must match exactly. */ -int gl_renderer_vram_diff(uint32_t *count, int bbox[4], - int samples[8][2], uint16_t samples_px[8][2]) { - if (!s_raster_ok || !s_ctx) return 0; - uint16_t *tmp = (uint16_t *)malloc((size_t)VRAM_W * VRAM_H * 2); - if (!tmp) return 0; - flush_cpu_upload(); - /* Force a full pack: the diff must read FBO truth even where the - * raw-mirror invariant (raw == FBO outside s_pack_dirty) is broken — - * a broken invariant is exactly what this tool hunts. */ - rect_add(&s_pack_dirty, 0, 0, VRAM_W - 1, VRAM_H - 1); - pack_flush(); - coh_record(GL_COH_DIFF, 0, 0, VRAM_W - 1, VRAM_H - 1); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); - glReadPixels(0, 0, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, tmp); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - uint32_t n = 0; - int x0 = VRAM_W, y0 = VRAM_H, x1 = -1, y1 = -1, ns = 0; - for (int y = 0; y < VRAM_H; y++) { - for (int x = 0; x < VRAM_W; x++) { - uint16_t f = tmp[y * VRAM_W + x], c = s_vram[y * VRAM_W + x]; - if (f == c) continue; - n++; - if (x < x0) x0 = x; if (x > x1) x1 = x; - if (y < y0) y0 = y; if (y > y1) y1 = y; - if (ns < 8 && (n % 977) == 1) { /* spread samples */ - samples[ns][0] = x; samples[ns][1] = y; - samples_px[ns][0] = f; samples_px[ns][1] = c; - ns++; - } - } - } - free(tmp); - *count = n; - bbox[0] = x0; bbox[1] = y0; bbox[2] = x1; bbox[3] = y1; - return 1 + ns; /* >=1 means valid; ns = samples filled */ -} - -/* Diagnostic state for the debug server: coherency flags + dirty rects. */ -void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]) { - if (gpu_dirty) *gpu_dirty = s_gpu_dirty; - if (pending) { - /* [0] = pending rect count; [1..4] = union bbox (diagnostic only — - * the flush itself paints the exact rects, never this union). */ - pending[0] = s_up_nrects; - pending[1] = pending[2] = pending[3] = pending[4] = 0; - for (int i = 0; i < s_up_nrects; i++) { - if (i == 0) { - pending[1] = s_up_rects[i].x0; pending[2] = s_up_rects[i].y0; - pending[3] = s_up_rects[i].x1; pending[4] = s_up_rects[i].y1; - } else { - if (s_up_rects[i].x0 < pending[1]) pending[1] = s_up_rects[i].x0; - if (s_up_rects[i].y0 < pending[2]) pending[2] = s_up_rects[i].y0; - if (s_up_rects[i].x1 > pending[3]) pending[3] = s_up_rects[i].x1; - if (s_up_rects[i].y1 > pending[4]) pending[4] = s_up_rects[i].y1; - } - } - } - if (pack) { - pack[0] = s_pack_dirty.set; - pack[1] = s_pack_dirty.x0; pack[2] = s_pack_dirty.y0; - pack[3] = s_pack_dirty.x1; pack[4] = s_pack_dirty.y1; - } -} - -/* ------------------------------------------------------------------------- * - * Native-wide compositor (GL). Mirrors gpu_sw_renderer.c's wide functions: - * canonical VRAM (the hr FBO) is untouched; framebuffer draws are also mirrored - * into per-base_x wide FBOs (see the draw funcs' wide passes). Present reads the - * displayed buffer's wide FBO via glReadPixels into the CPU present buffer, then - * the existing CPU present path uploads/letterboxes it (Option B: reuse the CPU - * present path). Self-gates on the GL pipeline being live; never runs for - * 4:3 / non-opted games (gpu.c never calls wide_configure for those). - * ------------------------------------------------------------------------- */ - -static void wide_free_all(void) { - for (int i = 0; i < WIDE_MAX_SURF; i++) { - if (s_wide_fbo[i]) { p_glDeleteFramebuffers(1, &s_wide_fbo[i]); s_wide_fbo[i] = 0; } - if (s_wide_tex[i]) { glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0; } - if (s_wide_rb[i]) { p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0; } - s_wide_base[i] = -1; - } - g_wide_cur = 0; -} - -/* Lazily allocate (or find) the wide FBO+tex for base_x. Returns the FBO id, or - * 0 on failure / more distinct buffers than WIDE_MAX_SURF. */ -static GLuint wide_fbo_for(int base_x) { - if (g_wide_w <= 0) return 0; - for (int i = 0; i < WIDE_MAX_SURF; i++) - if (s_wide_fbo[i] && s_wide_base[i] == base_x) return s_wide_fbo[i]; - for (int i = 0; i < WIDE_MAX_SURF; i++) { - if (!s_wide_fbo[i]) { - int w = g_wide_w * s_scale, h = VRAM_H * s_scale; - s_cw_fbo_creates++; - s_wide_tex[i] = make_tex(GL_RGBA8, w, h, GL_RGBA, GL_UNSIGNED_BYTE); - /* Depth-stencil RB, same as the hr FBO: the stencil carries the - * PSX mask-bit mirror for the wide surface, and (the hard lesson) - * a stencil-less FBO turns every stencil-enabled mirror draw into - * ~0.6ms of driver-side work — the 16:9 GL perf collapse. */ - p_glGenRenderbuffers(1, &s_wide_rb[i]); - p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_wide_rb[i]); - p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, w, h); - if (!make_fbo(&s_wide_fbo[i], s_wide_tex[i], s_wide_rb[i])) { - glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0; - p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0; - return 0; - } - /* Clear to black so unwritten margins are clean (not stale). */ - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_wide_fbo[i]); - glDisable(GL_SCISSOR_TEST); - glClearColor(0, 0, 0, 0); - glClearStencil(0); - glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - s_wide_base[i] = base_x; - return s_wide_fbo[i]; - } - } - return 0; /* more distinct buffers than WIDE_MAX_SURF — shouldn't happen */ -} - -/* Enable native-wide with a wide width + centering offset (native px), or - * disable (wide_w <= 0). Re-allocates if the width changed. Mirrors - * sw_wide_configure. */ -static void glb_wide_configure(int wide_w, int offset) { - if (!s_raster_ok) return; - double t0 = cw_ms(); s_cw_wide_cfgs++; - flush_tex_batch(); /* a queued batch's wide mirror targets the CURRENT surfaces */ - if (wide_w <= 0) { wide_free_all(); g_wide_w = 0; g_wide_off = 0; s_cw_wide_ms += cw_ms() - t0; return; } - if (wide_w != g_wide_w) wide_free_all(); - g_wide_w = wide_w; - g_wide_off = offset; - s_cw_wide_ms += cw_ms() - t0; -} - -/* Select the wide surface to mirror into for the back buffer at base_x. */ -static void glb_wide_set_target(int base_x) { - if (!s_raster_ok) { g_wide_cur = 0; return; } - double t0 = cw_ms(); s_cw_wide_sets++; - flush_tex_batch(); /* drain into the OLD target before switching */ - g_wide_cur = wide_fbo_for(base_x); - g_wide_cur_base = base_x; - s_cw_wide_ms += cw_ms() - t0; -} - -/* Stop mirroring (offscreen draws that don't target a framebuffer). */ -static void glb_wide_disable_target(void) { flush_tex_batch(); g_wide_cur = 0; } - -/* Mirror a framebuffer clear: fill the full wide width over [y, y+h) of the - * surface for base_x, so the revealed margins are clean. Mirrors sw_wide_clear: - * a scissored glClear with the 1555 color converted to RGBA8 (alpha = bit15). */ -static void glb_wide_clear(int base_x, int y, int h, uint16_t color) { - if (!s_raster_ok || s_ws_ablate == 1) return; - double t0 = cw_ms(); s_cw_wide_clears++; - flush_tex_batch(); - GLuint fbo = wide_fbo_for(base_x); - if (!fbo) { s_cw_wide_ms += cw_ms() - t0; return; } - gl_perf_mirror_begin(); - int H = VRAM_H * s_scale; - int y0 = y * s_scale, y1 = (y + h) * s_scale; - if (y0 < 0) y0 = 0; - if (y1 > H) y1 = H; - if (y1 <= y0) return; - float r = (color & 0x1F) / 31.0f; - float g = ((color >> 5) & 0x1F) / 31.0f; - float b = ((color >> 10) & 0x1F) / 31.0f; - float a = (color >> 15) & 1 ? 1.0f : 0.0f; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo); - glViewport(0, 0, g_wide_w * s_scale, H); - glEnable(GL_SCISSOR_TEST); - glScissor(0, y0, g_wide_w * s_scale, y1 - y0); - glClearColor(r, g, b, a); - glClearStencil((color >> 15) & 1); /* stencil mirrors bit15, like the color alpha */ - glStencilMask(0xFF); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - gl_perf_mirror_end(); - s_cw_wide_ms += cw_ms() - t0; -} - -/* Clear only the two synthetic reveal strips, preserving the centred canonical - * framebuffer. This is an opt-in transition cleanup driven by gpu.c. */ -static void glb_wide_clear_margins(int base_x, int y, int h, uint16_t color, int sides) { - if (!s_raster_ok || s_ws_ablate == 1 || g_wide_off <= 0) return; - double t0 = cw_ms(); s_cw_wide_clears++; - flush_tex_batch(); - GLuint fbo = wide_fbo_for(base_x); - if (!fbo) { s_cw_wide_ms += cw_ms() - t0; return; } - gl_perf_mirror_begin(); - int H = VRAM_H * s_scale; - int W = g_wide_w * s_scale; - int margin = g_wide_off * s_scale; - int y0 = y * s_scale, y1 = (y + h) * s_scale; - if (y0 < 0) y0 = 0; - if (y1 > H) y1 = H; - if (y1 <= y0 || margin * 2 >= W) { - gl_perf_mirror_end(); - s_cw_wide_ms += cw_ms() - t0; - return; - } - float r = (color & 0x1F) / 31.0f; - float g = ((color >> 5) & 0x1F) / 31.0f; - float b = ((color >> 10) & 0x1F) / 31.0f; - float a = (color >> 15) & 1 ? 1.0f : 0.0f; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo); - glViewport(0, 0, W, H); - glEnable(GL_SCISSOR_TEST); - glClearColor(r, g, b, a); - glClearStencil((color >> 15) & 1); - glStencilMask(0xFF); - if (sides & 1) { - glScissor(0, y0, margin, y1 - y0); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - } - if (sides & 2) { - glScissor(W - margin, y0, margin, y1 - y0); - glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - } - glDisable(GL_SCISSOR_TEST); - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - gl_perf_mirror_end(); - s_cw_wide_ms += cw_ms() - t0; -} - -/* Present source: read the wide FBO for the displayed buffer (base_x) into the - * CPU present buffer as ARGB8888, byte-identical to sw_render_wide_display's - * output so the shared CPU present path consumes it the same way. Output is - * (g_wide_w*scale) wide × (disp_h*scale) tall. Returns pixel count (>0), or 0 - * if no surface exists for base_x (caller falls back to the canonical present). - * - * PIXEL FORMAT: glReadPixels(GL_BGRA, GL_UNSIGNED_BYTE) yields, per pixel, the - * little-endian uint32 0xAARRGGBB == ARGB8888 — exactly what rgb555_to_argb - * produces and what upload_present_tex feeds to glTexImage2D(GL_BGRA,...). The - * SW path forces alpha to 0xFF; we OR it in to match (present ignores alpha, but - * we keep the two paths bit-identical). GL's read origin is bottom-left, so the - * block is read bottom-to-top and copied into `out` reversed (out row 0 = the - * display's top scanline, as the SW path and the PRESENT_VS V-flip expect). */ -static int glb_render_wide_display(uint32_t *out, int pitch, int base_x, - int disp_y, int disp_h) { - if (!s_raster_ok || !s_ctx || g_wide_w <= 0) return 0; - GLuint fbo = 0; - for (int i = 0; i < WIDE_MAX_SURF; i++) - if (s_wide_fbo[i] && s_wide_base[i] == base_x) { fbo = s_wide_fbo[i]; break; } - if (!fbo) return 0; - - /* Fold any pending CPU->VRAM uploads into the canonical FBO first (uploads - * are never mirrored to wide, but draws after them are; keep op order) and - * make sure all wide-FBO draws have completed before the readback. */ - flush_tex_batch(); - flush_cpu_upload(); - wide_blit_center(fbo, base_x, disp_y, disp_h); /* fast-path: authoritative centre before readback */ - glFinish(); - - int W = g_wide_w * s_scale; - int H = VRAM_H * s_scale; - int out_h = disp_h * s_scale; - int ry0 = disp_y * s_scale; - if (ry0 < 0) ry0 = 0; - if (ry0 + out_h > H) out_h = H - ry0; - if (out_h <= 0) return 0; - - uint32_t *tmp = (uint32_t *)malloc((size_t)W * out_h * sizeof(uint32_t)); - if (!tmp) return 0; - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo); - glPixelStorei(GL_PACK_ALIGNMENT, 4); - glReadPixels(0, ry0, W, out_h, GL_BGRA, GL_UNSIGNED_BYTE, tmp); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - - /* Orientation: the wide FBO stores PS1 y inverted (geo shader maps vram_y=0 - * to NDC y=-1 = FBO BOTTOM), and glReadPixels reads bottom-up — the two - * inversions CANCEL, so glReadPixels row 0 already = PS1 top scanline. Copy - * straight (NO flip) so `out` is top-down, matching sw_render_wide_display - * (which the shared CPU present path + its PRESENT_VS V-flip expect). Force - * alpha = 0xFF (match SW). [An earlier reversal here made the frame - * upside-down.] */ - int count = 0; - for (int row = 0; row < out_h; row++) { - const uint32_t *src = tmp + (size_t)row * W; - uint32_t *dst = (uint32_t *)((uint8_t *)out + (size_t)row * pitch); - for (int col = 0; col < W; col++) { dst[col] = src[col] | 0xFF000000u; count++; } - } - free(tmp); - return count; -} - -/* Dump the ENTIRE wide compositor surface for base_x (all double-buffer bands + - * both reveal margins), g_wide_w x VRAM_H at scale. Debug/inspection tool (TCP - * wide_full) — the GL analog of sw_wide_dump_full, so native-wide can be - * inspected without touching the game window. Runs the same authoritative - * centre blit first so the dump matches what present shows. Top-down, alpha=FF - * (matches sw_wide_dump_full / render_wide_display orientation). */ -static int glb_wide_dump_full(uint32_t *out, int cap_pixels, int *ow, int *oh, - int base_x) { - if (!s_raster_ok || !s_ctx || g_wide_w <= 0) return 0; - GLuint fbo = 0; - for (int i = 0; i < WIDE_MAX_SURF; i++) - if (s_wide_fbo[i] && s_wide_base[i] == base_x) { fbo = s_wide_fbo[i]; break; } - if (!fbo) return 0; - flush_tex_batch(); - flush_cpu_upload(); - wide_blit_center(fbo, base_x, 0, VRAM_H); /* authoritative centre (full height) */ - glFinish(); - int W = g_wide_w * s_scale; - int H = VRAM_H * s_scale; - if (cap_pixels > 0 && (long)W * H > cap_pixels) { H = cap_pixels / W; if (H <= 0) return 0; } - uint32_t *tmp = (uint32_t *)malloc((size_t)W * H * sizeof(uint32_t)); - if (!tmp) return 0; - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo); - glPixelStorei(GL_PACK_ALIGNMENT, 4); - glReadPixels(0, 0, W, H, GL_BGRA, GL_UNSIGNED_BYTE, tmp); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - /* Same orientation reasoning as glb_render_wide_display: glReadPixels row 0 = - * PS1 top scanline, so copy straight (top-down). */ - int count = 0; - for (int i = 0; i < W * H; i++) { out[i] = tmp[i] | 0xFF000000u; count++; } - free(tmp); - if (ow) *ow = W; - if (oh) *oh = H; - return count; -} - -/* THE present path for 15-bit frames: blit the display region from the - * authoritative hr FBO into a letterboxed rect. Deterministic — runs - * every 15-bit frame regardless of what mix of ops produced it. - * force_4_3 pins to native 4:3 (15-bit MDEC FMV frames on a wide aspect). */ -/* ===================== frame_perf: per-frame GPU/CPU phase timing ============ - * Developer builds use two GL_TIME_ELAPSED queries per frame to bracket (a) the - * scene draws (all GP0 raster issued between two presents) and (b) the present - * clear+blit, giving TRUE GPU time per phase independent of CPU/GPU overlap - * (glFinish would only catch the non-overlapped tail and mislead). CPU wall time - * (present-to-present total, and the present call) comes from SDL perf counters. - * Results are read back GLPERF_NBUF frames late (no pipeline stall) into a ring - * the debug server's frame_perf command aggregates. Release builds compile out - * the debug server, so they also leave this instrumentation disabled: a native- - * wide frame can otherwise issue hundreds of unused mirror timestamp queries. - * One question this answers in a diagnostics build: - * where does a 16:9 frame go vs 4:3 — scene fill, wide composite, or CPU. */ -#define GLPERF_NBUF 4 -#define GLPERF_RING 256 -typedef struct { - double total_ms; /* present-entry to present-entry (full frame) */ - double present_wall_ms; /* CPU wall time inside the present call */ - double scene_gpu_ms; /* GPU: all scene draws this frame */ - double present_gpu_ms; /* GPU: the present clear+blit */ - double mirror_gpu_ms; /* GPU: of scene_gpu, the native-wide mirror passes */ - double prims; /* scene primitives submitted this frame */ - double mirror_passes; /* mirror passes this frame (measured + overflow) */ - double cw_flush_ms; /* CPU wall inside flush_tex_batch this frame */ - double cw_wide_ms; /* CPU wall inside glb_wide_* this frame */ - double batches; /* flush_tex_batch draws this frame */ - double wide_sets; /* glb_wide_set_target calls this frame */ - double fbo_creates; /* wide FBO+tex creations this frame */ - int wide; /* 1 = native-wide (16:9) present, 0 = 4:3 */ - uint64_t frame; -} GlPerfSample; - -/* Native-wide mirror-pass GPU attribution: the scene TIME_ELAPSED query spans - * the whole frame (queries of one target cannot nest), so each mirror pass is - * bracketed with a GL_TIMESTAMP pair instead (glQueryCounter does not conflict - * with an active TIME_ELAPSED query). Pairs are pooled per buffered frame and - * summed at readback, splitting scene_gpu into canonical vs mirror cost. */ -#define GLPERF_MIRQ 1024 /* measured mirror passes per frame */ -static GLuint s_mq_q[GLPERF_NBUF][GLPERF_MIRQ * 2]; -static int s_mq_n[GLPERF_NBUF]; /* pairs recorded this frame */ -static int s_mq_over[GLPERF_NBUF]; /* passes beyond the pool (counted, untimed) */ -static int s_mq_open = 0; /* begin issued, end pending */ -static int s_mq_ok = 0; /* glQueryCounter available */ - -static int s_pf_on = 0; -static GLuint s_pf_scene_q[GLPERF_NBUF]; -static GLuint s_pf_present_q[GLPERF_NBUF]; -static int s_pf_b = 0; /* buffer for the CURRENT frame */ -static int s_pf_scene_active = 0; -static uint64_t s_pf_count = 0; -static uint64_t s_pf_freq = 1; -static uint64_t s_pf_last_enter = 0; -static uint64_t s_pf_enter = 0; -static double s_pf_total_pending = 0.0; -static double s_pf_buf_total[GLPERF_NBUF]; -static double s_pf_buf_pwall[GLPERF_NBUF]; -static double s_pf_buf_prims[GLPERF_NBUF]; -static int s_pf_buf_wide[GLPERF_NBUF]; -static uint64_t s_pf_buf_frame[GLPERF_NBUF]; -static uint64_t s_pf_prims_last = 0; -static double s_pf_prims_pending = 0.0; -static double s_pf_cw_pending[5]; /* flush_ms, wide_ms, batches, wide_sets, fbo_creates */ -static double s_pf_buf_cw[GLPERF_NBUF][5]; -static GlPerfSample s_pf_ring[GLPERF_RING]; -static uint64_t s_pf_ring_seq = 0; - -static void gl_perf_init(void) { -#ifdef PSX_NO_DEBUG_TOOLS - return; -#else - /* Timer queries are intentionally available in diagnostics builds, but a - * driver may serialize command submission while collecting them. Keep an - * escape hatch so frame cadence can be A/B tested without rebuilding or - * losing the rest of the debug server telemetry. */ - { - const char *enabled = getenv("PSX_GL_PERF"); - if (enabled && enabled[0] == '0') return; - } - if (!p_glGenQueries || !p_glBeginQuery || !p_glEndQuery || !p_glGetQueryObjectui64v) return; - p_glGenQueries(GLPERF_NBUF, s_pf_scene_q); - p_glGenQueries(GLPERF_NBUF, s_pf_present_q); - s_mq_ok = (p_glQueryCounter != NULL); - if (s_mq_ok) - for (int i = 0; i < GLPERF_NBUF; i++) { - p_glGenQueries(GLPERF_MIRQ * 2, s_mq_q[i]); - s_mq_n[i] = 0; s_mq_over[i] = 0; - } - s_mq_open = 0; - s_pf_freq = SDL_GetPerformanceFrequency(); - if (!s_pf_freq) s_pf_freq = 1; - s_pf_b = 0; s_pf_scene_active = 0; s_pf_count = 0; s_pf_ring_seq = 0; - s_pf_last_enter = 0; - s_pf_on = 1; -#endif -} - -/* Bracket ONE native-wide mirror pass (called from the wide-mirror draw sites). - * Timestamp pairs, not TIME_ELAPSED — see the pool comment above. */ -static void gl_perf_mirror_begin(void) { - if (!s_pf_on || !s_mq_ok) return; - int b = s_pf_b; - if (s_mq_n[b] >= GLPERF_MIRQ) { s_mq_over[b]++; return; } - p_glQueryCounter(s_mq_q[b][s_mq_n[b] * 2], GL_TIMESTAMP); - s_mq_open = 1; -} -static void gl_perf_mirror_end(void) { - if (!s_pf_on || !s_mq_ok || !s_mq_open) return; - int b = s_pf_b; - p_glQueryCounter(s_mq_q[b][s_mq_n[b] * 2 + 1], GL_TIMESTAMP); - s_mq_n[b]++; - s_mq_open = 0; -} - -/* Top of present (after flush_cpu_upload, before clear/blit). */ -static void gl_perf_present_enter(void) { - /* Per-frame boundary for the 2D-backdrop stretch — runs from BOTH present - * paths (4:3 present_vram AND native-wide present_wide_fbo), before the - * perf-on gate so native-wide frames reset too. Snapshot this frame's - * backdrop-stretch diagnostics, then reset the per-frame counters. (The gate - * is per-prim now — no draw-order phase to reset.) Final batch already flushed - * by the caller. */ - g_bdg_applied = s_bdg_applied; g_bdg_prims = s_bdg_prims; g_bdg_clearx = s_bdg_clearx; - g_bdg_cur = (g_wide_cur != 0); g_bdg_base = g_wide_cur_base; g_bdg_w = g_wide_w; g_bdg_off = g_wide_off; - s_bdg_applied = 0; s_bdg_prims = 0; s_bdg_clearx = -999999; - { extern void psx_ws_dbg_gate_frame_snapshot(void); psx_ws_dbg_gate_frame_snapshot(); } - if (!s_pf_on) return; - uint64_t now = SDL_GetPerformanceCounter(); - s_pf_enter = now; - s_pf_total_pending = s_pf_last_enter - ? (double)(now - s_pf_last_enter) * 1000.0 / (double)s_pf_freq : 0.0; - s_pf_last_enter = now; - s_pf_prims_pending = (double)(s_scene_prims - s_pf_prims_last); /* prims drawn this frame */ - s_pf_prims_last = s_scene_prims; - s_pf_cw_pending[0] = s_cw_flush_ms; s_pf_cw_pending[1] = s_cw_wide_ms; - s_pf_cw_pending[2] = (double)s_cw_batches; - s_pf_cw_pending[3] = (double)s_cw_wide_sets; - s_pf_cw_pending[4] = (double)s_cw_fbo_creates; - s_cw_flush_ms = 0.0; s_cw_wide_ms = 0.0; - s_cw_batches = 0; s_cw_wide_sets = 0; s_cw_wide_cfgs = 0; - s_cw_wide_clears = 0; s_cw_fbo_creates = 0; - if (s_pf_scene_active) { p_glEndQuery(GL_TIME_ELAPSED); s_pf_scene_active = 0; } /* end frame b's scene draws */ - p_glBeginQuery(GL_TIME_ELAPSED, s_pf_present_q[s_pf_b]); /* time frame b's present */ -} - -/* End of present (after SwapWindow). wide = native-wide path. */ -static void gl_perf_present_exit(int wide) { - if (!s_pf_on) return; - uint64_t now = SDL_GetPerformanceCounter(); - p_glEndQuery(GL_TIME_ELAPSED); /* end present_q[b] */ - s_pf_buf_total[s_pf_b] = s_pf_total_pending; - s_pf_buf_pwall[s_pf_b] = (double)(now - s_pf_enter) * 1000.0 / (double)s_pf_freq; - s_pf_buf_prims[s_pf_b] = s_pf_prims_pending; - for (int ci = 0; ci < 5; ci++) s_pf_buf_cw[s_pf_b][ci] = s_pf_cw_pending[ci]; - s_pf_buf_wide[s_pf_b] = wide; - s_pf_buf_frame[s_pf_b] = s_pf_count; - int rd = (s_pf_b + 1) % GLPERF_NBUF; /* oldest buffer (frame count+1-NBUF), now done */ - if (s_pf_count >= (uint64_t)GLPERF_NBUF) { - GLuint64 sc = 0, pr = 0; - p_glGetQueryObjectui64v(s_pf_scene_q[rd], GL_QUERY_RESULT, &sc); - p_glGetQueryObjectui64v(s_pf_present_q[rd], GL_QUERY_RESULT, &pr); - double mir = 0.0; - for (int i = 0; i < s_mq_n[rd]; i++) { /* sum that frame's mirror pairs */ - GLuint64 t0 = 0, t1 = 0; - p_glGetQueryObjectui64v(s_mq_q[rd][i * 2], GL_QUERY_RESULT, &t0); - p_glGetQueryObjectui64v(s_mq_q[rd][i * 2 + 1], GL_QUERY_RESULT, &t1); - if (t1 > t0) mir += (double)(t1 - t0); - } - GlPerfSample *s = &s_pf_ring[s_pf_ring_seq % GLPERF_RING]; - s->total_ms = s_pf_buf_total[rd]; - s->present_wall_ms = s_pf_buf_pwall[rd]; - s->scene_gpu_ms = (double)sc / 1.0e6; - s->present_gpu_ms = (double)pr / 1.0e6; - s->mirror_gpu_ms = mir / 1.0e6; - s->prims = s_pf_buf_prims[rd]; - s->mirror_passes = (double)(s_mq_n[rd] + s_mq_over[rd]); - s->cw_flush_ms = s_pf_buf_cw[rd][0]; - s->cw_wide_ms = s_pf_buf_cw[rd][1]; - s->batches = s_pf_buf_cw[rd][2]; - s->wide_sets = s_pf_buf_cw[rd][3]; - s->fbo_creates = s_pf_buf_cw[rd][4]; - s->wide = s_pf_buf_wide[rd]; - s->frame = s_pf_buf_frame[rd]; - s_pf_ring_seq++; - } - s_mq_n[rd] = 0; s_mq_over[rd] = 0; /* rd becomes the next frame's buffer */ - s_pf_count++; - s_pf_b = rd; /* reuse oldest for next frame */ - p_glBeginQuery(GL_TIME_ELAPSED, s_pf_scene_q[s_pf_b]); /* open next frame's scene draws */ - s_pf_scene_active = 1; -} - -/* Aggregate the ring for the debug server. wide_filter: -1 all, 0 = 4:3, 1 = wide. - * out[0]=count, [1]=total_avg, [2]=total_max, [3]=emu_cpu_avg (total-present_wall), - * [4]=present_wall_avg, [5]=scene_gpu_avg, [6]=scene_gpu_max, [7]=present_gpu_avg, - * [8]=present_gpu_max. Returns the sample count. */ -/* Cumulative textured fraction of scene prims (decides flat vs textured batching - * priority). out_tex_frac = textured/total since boot; returns total prim count. */ -uint64_t gl_renderer_perf_prim_split(double *out_tex_frac) { - if (out_tex_frac) *out_tex_frac = s_scene_prims ? (double)s_scene_prims_tex / (double)s_scene_prims : 0.0; - return s_scene_prims; -} - -int gl_renderer_perf_aggregate(int wide_filter, double out[18]) { - for (int i = 0; i < 18; i++) out[i] = 0.0; - if (!s_pf_on) return 0; - int navail = (int)(s_pf_ring_seq < (uint64_t)GLPERF_RING ? s_pf_ring_seq : GLPERF_RING); - uint64_t start = s_pf_ring_seq - (uint64_t)navail; - int n = 0; - for (int i = 0; i < navail; i++) { - const GlPerfSample *s = &s_pf_ring[(start + i) % GLPERF_RING]; - if (wide_filter >= 0 && s->wide != wide_filter) continue; - double emu = s->total_ms - s->present_wall_ms; if (emu < 0) emu = 0; - out[1] += s->total_ms; if (s->total_ms > out[2]) out[2] = s->total_ms; - out[3] += emu; - out[4] += s->present_wall_ms; - out[5] += s->scene_gpu_ms; if (s->scene_gpu_ms > out[6]) out[6] = s->scene_gpu_ms; - out[7] += s->present_gpu_ms; if (s->present_gpu_ms > out[8]) out[8] = s->present_gpu_ms; - out[9] += s->prims; - out[10] += s->mirror_gpu_ms; if (s->mirror_gpu_ms > out[11]) out[11] = s->mirror_gpu_ms; - out[12] += s->mirror_passes; - out[13] += s->cw_flush_ms; - out[14] += s->cw_wide_ms; - out[15] += s->batches; - out[16] += s->wide_sets; - out[17] += s->fbo_creates; - n++; - } - if (n) { - out[1]/=n; out[3]/=n; out[4]/=n; out[5]/=n; out[7]/=n; out[9]/=n; out[10]/=n; out[12]/=n; - out[13]/=n; out[14]/=n; out[15]/=n; out[16]/=n; out[17]/=n; - } - out[0] = (double)n; - return n; -} - -/* Native-wide mirror ablation (perf attribution): see s_ws_ablate. */ -void gl_renderer_set_ws_ablate(int mode) { s_ws_ablate = (mode >= 0 && mode <= 3) ? mode : 0; } -int gl_renderer_get_ws_ablate(void) { return s_ws_ablate; } - + hold_capture_drawable(); + latency_ring_mark(LAT_SWAP_BEGIN); + gl_swap_with_osd(); + latency_ring_mark(LAT_SWAP_END); + s_probe_swap++; + present_force_consumed(); + s_last_present_path = GL_PRES_CPU; +} + +void gl_renderer_present_blank(void) { + if (!s_ctx) return; + interp_reset_history(); + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); + pres_record(GL_PRES_BLANK, 0, 0, 0, 0, 0, 0, ww, wh); + hold_capture_drawable(); + latency_ring_mark(LAT_SWAP_BEGIN); + gl_swap_with_osd(); + latency_ring_mark(LAT_SWAP_END); + s_probe_swap++; + present_force_consumed(); + s_last_present_path = GL_PRES_BLANK; +} + +/* Sync the authoritative FBO down into CPU VRAM (no-op when current). + * Screenshots / debug server. Not for 24-bit FMV scanout (see flush). */ +void gl_renderer_sync_cpu(void) { + ensure_cpu(); +} + +void gl_renderer_invalidate_present(void) { + for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull; + s_last_present_path = -1; + s_force_present_remaining = 8; + hold_invalidate(); + interp_reset_history(); +} + +void gl_renderer_restage_vram_after_savestate(void) { + if (!s_raster_ok || !s_vram) return; + /* Belt-and-suspenders after boot_state VRAM apply: force CPU mirror → FBO + * even if a depth24 skip swallowed the restore, then re-arm scanout-band + * clear so leaving FMV does not keep RGB888-as-1555 junk. */ + s_up_nrects = 0; + rect_clear(&s_d24_skip_fb); + s_depth24_skip_up = 0; + up_add_transfer(0, 0, VRAM_W, VRAM_H); + flush_cpu_upload(); + if (gpu_display_is_depth24()) { + s_depth24_skip_up = 1; + depth24_mark_scanout_band(); + } + /* Dual-raster: CPU is authority after snap; FBO is cosmetics only. */ + if (s_cpu_auth_dual) + s_gpu_dirty = 0; +} + +void gl_renderer_set_cpu_auth_dual(int on) { + s_cpu_auth_dual = on ? 1 : 0; + if (s_cpu_auth_dual) + s_gpu_dirty = 0; +} + +int gl_renderer_cpu_auth_dual(void) { + return s_cpu_auth_dual; +} + +void gl_renderer_present_probe_reset(void) { + s_probe_skip = 0; + s_probe_swap = 0; + s_probe_dirty_marks = 0; +} + +void gl_renderer_present_probe_take(uint64_t *skip_delta, uint64_t *swap_delta, + uint64_t *dirty_mark_delta, + int *force_remaining) { + if (skip_delta) { *skip_delta = s_probe_skip; s_probe_skip = 0; } + if (swap_delta) { *swap_delta = s_probe_swap; s_probe_swap = 0; } + if (dirty_mark_delta) { + *dirty_mark_delta = s_probe_dirty_marks; + s_probe_dirty_marks = 0; + } + if (force_remaining) *force_remaining = s_force_present_remaining; +} + +int gl_renderer_present_rect_dirty(int disp_x, int disp_y, int w, int h) { + if (!s_raster_ok || w <= 0 || h <= 0) return 0; + return present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1); +} + +void gl_renderer_flush_cpu_uploads(void) { + if (!s_raster_ok) return; + flush_flat_batch(); + flush_tex_batch(); + flush_cpu_upload(); +} + +/* Diagnostic (debug server "gl_fbo_peek"): read a rect of the GPU-side + * authoritative VRAM (via the pack pass + raw mirror) WITHOUT writing CPU + * VRAM — lets a probe diff FBO truth against CPU truth. Returns 0 when the + * GL pipeline is inactive (software backend). */ +int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out) { + if (!s_raster_ok || !s_ctx) return 0; + if (x < 0 || y < 0 || w < 1 || h < 1 || + x + w > VRAM_W || y + h > VRAM_H) return 0; + flush_cpu_upload(); + rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1); + pack_flush(); + coh_record(GL_COH_PEEK, x, y, x + w - 1, y + h - 1); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); + glPixelStorei(GL_PACK_ALIGNMENT, 2); + glReadPixels(x, y, w, h, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, out); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + return 1; +} + +/* Diagnostic (debug server "gl_vram_diff"): full-VRAM comparison of the + * GPU-side truth (FBO via pack) against the CPU array, WITHOUT writing + * either. Reports mismatch count + bounding box + a few sample coords. + * Divergence is expected where the GPU is legitimately ahead (gpu_dirty); + * at upload-only scenes the two must match exactly. */ +int gl_renderer_vram_diff(uint32_t *count, int bbox[4], + int samples[8][2], uint16_t samples_px[8][2]) { + if (!s_raster_ok || !s_ctx) return 0; + uint16_t *tmp = (uint16_t *)malloc((size_t)VRAM_W * VRAM_H * 2); + if (!tmp) return 0; + flush_cpu_upload(); + /* Force a full pack: the diff must read FBO truth even where the + * raw-mirror invariant (raw == FBO outside s_pack_dirty) is broken — + * a broken invariant is exactly what this tool hunts. */ + rect_add(&s_pack_dirty, 0, 0, VRAM_W - 1, VRAM_H - 1); + pack_flush(); + coh_record(GL_COH_DIFF, 0, 0, VRAM_W - 1, VRAM_H - 1); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_raw_fbo); + glReadPixels(0, 0, VRAM_W, VRAM_H, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, tmp); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + uint32_t n = 0; + int x0 = VRAM_W, y0 = VRAM_H, x1 = -1, y1 = -1, ns = 0; + for (int y = 0; y < VRAM_H; y++) { + for (int x = 0; x < VRAM_W; x++) { + uint16_t f = tmp[y * VRAM_W + x], c = s_vram[y * VRAM_W + x]; + if (f == c) continue; + n++; + if (x < x0) x0 = x; if (x > x1) x1 = x; + if (y < y0) y0 = y; if (y > y1) y1 = y; + if (ns < 8 && (n % 977) == 1) { /* spread samples */ + samples[ns][0] = x; samples[ns][1] = y; + samples_px[ns][0] = f; samples_px[ns][1] = c; + ns++; + } + } + } + free(tmp); + *count = n; + bbox[0] = x0; bbox[1] = y0; bbox[2] = x1; bbox[3] = y1; + return 1 + ns; /* >=1 means valid; ns = samples filled */ +} + +/* Diagnostic state for the debug server: coherency flags + dirty rects. */ +void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]) { + if (gpu_dirty) *gpu_dirty = s_gpu_dirty; + if (pending) { + /* [0] = pending rect count; [1..4] = union bbox (diagnostic only — + * the flush itself paints the exact rects, never this union). */ + pending[0] = s_up_nrects; + pending[1] = pending[2] = pending[3] = pending[4] = 0; + for (int i = 0; i < s_up_nrects; i++) { + if (i == 0) { + pending[1] = s_up_rects[i].x0; pending[2] = s_up_rects[i].y0; + pending[3] = s_up_rects[i].x1; pending[4] = s_up_rects[i].y1; + } else { + if (s_up_rects[i].x0 < pending[1]) pending[1] = s_up_rects[i].x0; + if (s_up_rects[i].y0 < pending[2]) pending[2] = s_up_rects[i].y0; + if (s_up_rects[i].x1 > pending[3]) pending[3] = s_up_rects[i].x1; + if (s_up_rects[i].y1 > pending[4]) pending[4] = s_up_rects[i].y1; + } + } + } + if (pack) { + pack[0] = s_pack_dirty.set; + pack[1] = s_pack_dirty.x0; pack[2] = s_pack_dirty.y0; + pack[3] = s_pack_dirty.x1; pack[4] = s_pack_dirty.y1; + } +} + +/* ------------------------------------------------------------------------- * + * Native-wide compositor (GL). Mirrors gpu_sw_renderer.c's wide functions: + * canonical VRAM (the hr FBO) is untouched; framebuffer draws are also mirrored + * into per-base_x wide FBOs (see the draw funcs' wide passes). Present reads the + * displayed buffer's wide FBO via glReadPixels into the CPU present buffer, then + * the existing CPU present path uploads/letterboxes it (Option B: reuse the CPU + * present path). Self-gates on the GL pipeline being live; never runs for + * 4:3 / non-opted games (gpu.c never calls wide_configure for those). + * ------------------------------------------------------------------------- */ + +static void wide_free_all(void) { + for (int i = 0; i < WIDE_MAX_SURF; i++) { + if (s_wide_fbo[i]) { p_glDeleteFramebuffers(1, &s_wide_fbo[i]); s_wide_fbo[i] = 0; } + if (s_wide_tex[i]) { glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0; } + if (s_wide_rb[i]) { p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0; } + s_wide_base[i] = -1; + } + g_wide_cur = 0; +} + +/* Lazily allocate (or find) the wide FBO+tex for base_x. Returns the FBO id, or + * 0 on failure / more distinct buffers than WIDE_MAX_SURF. */ +static GLuint wide_fbo_for(int base_x) { + if (g_wide_w <= 0) return 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + if (s_wide_fbo[i] && s_wide_base[i] == base_x) return s_wide_fbo[i]; + for (int i = 0; i < WIDE_MAX_SURF; i++) { + if (!s_wide_fbo[i]) { + int w = g_wide_w * s_scale, h = VRAM_H * s_scale; + s_cw_fbo_creates++; + s_wide_tex[i] = make_tex(GL_RGBA8, w, h, GL_RGBA, GL_UNSIGNED_BYTE); + /* Depth-stencil RB, same as the hr FBO: the stencil carries the + * PSX mask-bit mirror for the wide surface, and (the hard lesson) + * a stencil-less FBO turns every stencil-enabled mirror draw into + * ~0.6ms of driver-side work — the 16:9 GL perf collapse. */ + p_glGenRenderbuffers(1, &s_wide_rb[i]); + p_glBindRenderbuffer(PSXGL_RENDERBUFFER, s_wide_rb[i]); + p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, w, h); + if (!make_fbo(&s_wide_fbo[i], s_wide_tex[i], s_wide_rb[i])) { + glDeleteTextures(1, &s_wide_tex[i]); s_wide_tex[i] = 0; + p_glDeleteRenderbuffers(1, &s_wide_rb[i]); s_wide_rb[i] = 0; + return 0; + } + /* Clear to black so unwritten margins are clean (not stale). */ + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_wide_fbo[i]); + glDisable(GL_SCISSOR_TEST); + glClearColor(0, 0, 0, 0); + glClearStencil(0); + glStencilMask(0xFF); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + s_wide_base[i] = base_x; + return s_wide_fbo[i]; + } + } + return 0; /* more distinct buffers than WIDE_MAX_SURF — shouldn't happen */ +} + +/* Enable native-wide with a wide width + centering offset (native px), or + * disable (wide_w <= 0). Re-allocates if the width changed. Mirrors + * sw_wide_configure. */ +static void glb_wide_configure(int wide_w, int offset) { + if (!s_raster_ok) return; + double t0 = cw_ms(); s_cw_wide_cfgs++; + flush_tex_batch(); /* a queued batch's wide mirror targets the CURRENT surfaces */ + if (wide_w <= 0) { wide_free_all(); g_wide_w = 0; g_wide_off = 0; s_cw_wide_ms += cw_ms() - t0; return; } + if (wide_w != g_wide_w) wide_free_all(); + g_wide_w = wide_w; + g_wide_off = offset; + s_cw_wide_ms += cw_ms() - t0; +} + +/* Select the wide surface to mirror into for the back buffer at base_x. */ +static void glb_wide_set_target(int base_x) { + if (!s_raster_ok) { g_wide_cur = 0; return; } + double t0 = cw_ms(); s_cw_wide_sets++; + flush_tex_batch(); /* drain into the OLD target before switching */ + g_wide_cur = wide_fbo_for(base_x); + g_wide_cur_base = base_x; + s_cw_wide_ms += cw_ms() - t0; +} + +/* Stop mirroring (offscreen draws that don't target a framebuffer). */ +static void glb_wide_disable_target(void) { flush_tex_batch(); g_wide_cur = 0; } + +/* Mirror a framebuffer clear: fill the full wide width over [y, y+h) of the + * surface for base_x, so the revealed margins are clean. Mirrors sw_wide_clear: + * a scissored glClear with the 1555 color converted to RGBA8 (alpha = bit15). */ +static void glb_wide_clear(int base_x, int y, int h, uint16_t color) { + if (!s_raster_ok || s_ws_ablate == 1) return; + double t0 = cw_ms(); s_cw_wide_clears++; + flush_tex_batch(); + GLuint fbo = wide_fbo_for(base_x); + if (!fbo) { s_cw_wide_ms += cw_ms() - t0; return; } + gl_perf_mirror_begin(); + int H = VRAM_H * s_scale; + int y0 = y * s_scale, y1 = (y + h) * s_scale; + if (y0 < 0) y0 = 0; + if (y1 > H) y1 = H; + if (y1 <= y0) return; + float r = (color & 0x1F) / 31.0f; + float g = ((color >> 5) & 0x1F) / 31.0f; + float b = ((color >> 10) & 0x1F) / 31.0f; + float a = (color >> 15) & 1 ? 1.0f : 0.0f; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo); + glViewport(0, 0, g_wide_w * s_scale, H); + glEnable(GL_SCISSOR_TEST); + glScissor(0, y0, g_wide_w * s_scale, y1 - y0); + glClearColor(r, g, b, a); + glClearStencil((color >> 15) & 1); /* stencil mirrors bit15, like the color alpha */ + glStencilMask(0xFF); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + gl_perf_mirror_end(); + s_cw_wide_ms += cw_ms() - t0; +} + +/* Clear only the two synthetic reveal strips, preserving the centred canonical + * framebuffer. This is an opt-in transition cleanup driven by gpu.c. */ +static void glb_wide_clear_margins(int base_x, int y, int h, uint16_t color, int sides) { + if (!s_raster_ok || s_ws_ablate == 1 || g_wide_off <= 0) return; + double t0 = cw_ms(); s_cw_wide_clears++; + flush_tex_batch(); + GLuint fbo = wide_fbo_for(base_x); + if (!fbo) { s_cw_wide_ms += cw_ms() - t0; return; } + gl_perf_mirror_begin(); + int H = VRAM_H * s_scale; + int W = g_wide_w * s_scale; + int margin = g_wide_off * s_scale; + int y0 = y * s_scale, y1 = (y + h) * s_scale; + if (y0 < 0) y0 = 0; + if (y1 > H) y1 = H; + if (y1 <= y0 || margin * 2 >= W) { + gl_perf_mirror_end(); + s_cw_wide_ms += cw_ms() - t0; + return; + } + float r = (color & 0x1F) / 31.0f; + float g = ((color >> 5) & 0x1F) / 31.0f; + float b = ((color >> 10) & 0x1F) / 31.0f; + float a = (color >> 15) & 1 ? 1.0f : 0.0f; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, fbo); + glViewport(0, 0, W, H); + glEnable(GL_SCISSOR_TEST); + glClearColor(r, g, b, a); + glClearStencil((color >> 15) & 1); + glStencilMask(0xFF); + if (sides & 1) { + glScissor(0, y0, margin, y1 - y0); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + } + if (sides & 2) { + glScissor(W - margin, y0, margin, y1 - y0); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + } + glDisable(GL_SCISSOR_TEST); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + gl_perf_mirror_end(); + s_cw_wide_ms += cw_ms() - t0; +} + +/* Present source: read the wide FBO for the displayed buffer (base_x) into the + * CPU present buffer as ARGB8888, byte-identical to sw_render_wide_display's + * output so the shared CPU present path consumes it the same way. Output is + * (g_wide_w*scale) wide × (disp_h*scale) tall. Returns pixel count (>0), or 0 + * if no surface exists for base_x (caller falls back to the canonical present). + * + * PIXEL FORMAT: glReadPixels(GL_BGRA, GL_UNSIGNED_BYTE) yields, per pixel, the + * little-endian uint32 0xAARRGGBB == ARGB8888 — exactly what rgb555_to_argb + * produces and what upload_present_tex feeds to glTexImage2D(GL_BGRA,...). The + * SW path forces alpha to 0xFF; we OR it in to match (present ignores alpha, but + * we keep the two paths bit-identical). GL's read origin is bottom-left, so the + * block is read bottom-to-top and copied into `out` reversed (out row 0 = the + * display's top scanline, as the SW path and the PRESENT_VS V-flip expect). */ +static int glb_render_wide_display(uint32_t *out, int pitch, int base_x, + int disp_y, int disp_h) { + if (!s_raster_ok || !s_ctx || g_wide_w <= 0) return 0; + GLuint fbo = 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + if (s_wide_fbo[i] && s_wide_base[i] == base_x) { fbo = s_wide_fbo[i]; break; } + if (!fbo) return 0; + + /* Fold any pending CPU->VRAM uploads into the canonical FBO first (uploads + * are never mirrored to wide, but draws after them are; keep op order) and + * make sure all wide-FBO draws have completed before the readback. */ + flush_tex_batch(); + flush_cpu_upload(); + wide_blit_center(fbo, base_x, disp_y, disp_h); /* fast-path: authoritative centre before readback */ + glFinish(); + + int W = g_wide_w * s_scale; + int H = VRAM_H * s_scale; + int out_h = disp_h * s_scale; + int ry0 = disp_y * s_scale; + if (ry0 < 0) ry0 = 0; + if (ry0 + out_h > H) out_h = H - ry0; + if (out_h <= 0) return 0; + + uint32_t *tmp = (uint32_t *)malloc((size_t)W * out_h * sizeof(uint32_t)); + if (!tmp) return 0; + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glReadPixels(0, ry0, W, out_h, GL_BGRA, GL_UNSIGNED_BYTE, tmp); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + + /* Orientation: the wide FBO stores PS1 y inverted (geo shader maps vram_y=0 + * to NDC y=-1 = FBO BOTTOM), and glReadPixels reads bottom-up — the two + * inversions CANCEL, so glReadPixels row 0 already = PS1 top scanline. Copy + * straight (NO flip) so `out` is top-down, matching sw_render_wide_display + * (which the shared CPU present path + its PRESENT_VS V-flip expect). Force + * alpha = 0xFF (match SW). [An earlier reversal here made the frame + * upside-down.] */ + int count = 0; + for (int row = 0; row < out_h; row++) { + const uint32_t *src = tmp + (size_t)row * W; + uint32_t *dst = (uint32_t *)((uint8_t *)out + (size_t)row * pitch); + for (int col = 0; col < W; col++) { dst[col] = src[col] | 0xFF000000u; count++; } + } + free(tmp); + return count; +} + +/* Dump the ENTIRE wide compositor surface for base_x (all double-buffer bands + + * both reveal margins), g_wide_w x VRAM_H at scale. Debug/inspection tool (TCP + * wide_full) — the GL analog of sw_wide_dump_full, so native-wide can be + * inspected without touching the game window. Runs the same authoritative + * centre blit first so the dump matches what present shows. Top-down, alpha=FF + * (matches sw_wide_dump_full / render_wide_display orientation). */ +static int glb_wide_dump_full(uint32_t *out, int cap_pixels, int *ow, int *oh, + int base_x) { + if (!s_raster_ok || !s_ctx || g_wide_w <= 0) return 0; + GLuint fbo = 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + if (s_wide_fbo[i] && s_wide_base[i] == base_x) { fbo = s_wide_fbo[i]; break; } + if (!fbo) return 0; + flush_tex_batch(); + flush_cpu_upload(); + wide_blit_center(fbo, base_x, 0, VRAM_H); /* authoritative centre (full height) */ + glFinish(); + int W = g_wide_w * s_scale; + int H = VRAM_H * s_scale; + if (cap_pixels > 0 && (long)W * H > cap_pixels) { H = cap_pixels / W; if (H <= 0) return 0; } + uint32_t *tmp = (uint32_t *)malloc((size_t)W * H * sizeof(uint32_t)); + if (!tmp) return 0; + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glReadPixels(0, 0, W, H, GL_BGRA, GL_UNSIGNED_BYTE, tmp); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + /* Same orientation reasoning as glb_render_wide_display: glReadPixels row 0 = + * PS1 top scanline, so copy straight (top-down). */ + int count = 0; + for (int i = 0; i < W * H; i++) { out[i] = tmp[i] | 0xFF000000u; count++; } + free(tmp); + if (ow) *ow = W; + if (oh) *oh = H; + return count; +} + +/* THE present path for 15-bit frames: blit the display region from the + * authoritative hr FBO into a letterboxed rect. Deterministic — runs + * every 15-bit frame regardless of what mix of ops produced it. + * force_4_3 pins to native 4:3 (15-bit MDEC FMV frames on a wide aspect). */ +/* ===================== frame_perf: per-frame GPU/CPU phase timing ============ + * Developer builds use two GL_TIME_ELAPSED queries per frame to bracket (a) the + * scene draws (all GP0 raster issued between two presents) and (b) the present + * clear+blit, giving TRUE GPU time per phase independent of CPU/GPU overlap + * (glFinish would only catch the non-overlapped tail and mislead). CPU wall time + * (present-to-present total, and the present call) comes from SDL perf counters. + * Results are read back GLPERF_NBUF frames late (no pipeline stall) into a ring + * the debug server's frame_perf command aggregates. Release builds compile out + * the debug server, so they also leave this instrumentation disabled: a native- + * wide frame can otherwise issue hundreds of unused mirror timestamp queries. + * One question this answers in a diagnostics build: + * where does a 16:9 frame go vs 4:3 — scene fill, wide composite, or CPU. */ +#define GLPERF_NBUF 4 +#define GLPERF_RING 256 +typedef struct { + double total_ms; /* present-entry to present-entry (full frame) */ + double present_wall_ms; /* CPU wall time inside the present call */ + double scene_gpu_ms; /* GPU: all scene draws this frame */ + double present_gpu_ms; /* GPU: the present clear+blit */ + double mirror_gpu_ms; /* GPU: of scene_gpu, the native-wide mirror passes */ + double prims; /* scene primitives submitted this frame */ + double mirror_passes; /* mirror passes this frame (measured + overflow) */ + double cw_flush_ms; /* CPU wall inside flush_tex_batch this frame */ + double cw_wide_ms; /* CPU wall inside glb_wide_* this frame */ + double batches; /* flush_tex_batch draws this frame */ + double wide_sets; /* glb_wide_set_target calls this frame */ + double fbo_creates; /* wide FBO+tex creations this frame */ + int wide; /* 1 = native-wide (16:9) present, 0 = 4:3 */ + uint64_t frame; +} GlPerfSample; + +/* Native-wide mirror-pass GPU attribution: the scene TIME_ELAPSED query spans + * the whole frame (queries of one target cannot nest), so each mirror pass is + * bracketed with a GL_TIMESTAMP pair instead (glQueryCounter does not conflict + * with an active TIME_ELAPSED query). Pairs are pooled per buffered frame and + * summed at readback, splitting scene_gpu into canonical vs mirror cost. */ +#define GLPERF_MIRQ 1024 /* measured mirror passes per frame */ +static GLuint s_mq_q[GLPERF_NBUF][GLPERF_MIRQ * 2]; +static int s_mq_n[GLPERF_NBUF]; /* pairs recorded this frame */ +static int s_mq_over[GLPERF_NBUF]; /* passes beyond the pool (counted, untimed) */ +static int s_mq_open = 0; /* begin issued, end pending */ +static int s_mq_ok = 0; /* glQueryCounter available */ + +static int s_pf_on = 0; +static GLuint s_pf_scene_q[GLPERF_NBUF]; +static GLuint s_pf_present_q[GLPERF_NBUF]; +static int s_pf_b = 0; /* buffer for the CURRENT frame */ +static int s_pf_scene_active = 0; +static uint64_t s_pf_count = 0; +static uint64_t s_pf_freq = 1; +static uint64_t s_pf_last_enter = 0; +static uint64_t s_pf_enter = 0; +static double s_pf_total_pending = 0.0; +static double s_pf_buf_total[GLPERF_NBUF]; +static double s_pf_buf_pwall[GLPERF_NBUF]; +static double s_pf_buf_prims[GLPERF_NBUF]; +static int s_pf_buf_wide[GLPERF_NBUF]; +static uint64_t s_pf_buf_frame[GLPERF_NBUF]; +static uint64_t s_pf_prims_last = 0; +static double s_pf_prims_pending = 0.0; +static double s_pf_cw_pending[5]; /* flush_ms, wide_ms, batches, wide_sets, fbo_creates */ +static double s_pf_buf_cw[GLPERF_NBUF][5]; +static GlPerfSample s_pf_ring[GLPERF_RING]; +static uint64_t s_pf_ring_seq = 0; + +static void gl_perf_init(void) { +#ifdef PSX_NO_DEBUG_TOOLS + return; +#else + /* Timer queries are intentionally available in diagnostics builds, but a + * driver may serialize command submission while collecting them. Keep an + * escape hatch so frame cadence can be A/B tested without rebuilding or + * losing the rest of the debug server telemetry. */ + { + const char *enabled = getenv("PSX_GL_PERF"); + if (enabled && enabled[0] == '0') return; + } + if (!p_glGenQueries || !p_glBeginQuery || !p_glEndQuery || !p_glGetQueryObjectui64v) return; + p_glGenQueries(GLPERF_NBUF, s_pf_scene_q); + p_glGenQueries(GLPERF_NBUF, s_pf_present_q); + s_mq_ok = (p_glQueryCounter != NULL); + if (s_mq_ok) + for (int i = 0; i < GLPERF_NBUF; i++) { + p_glGenQueries(GLPERF_MIRQ * 2, s_mq_q[i]); + s_mq_n[i] = 0; s_mq_over[i] = 0; + } + s_mq_open = 0; + s_pf_freq = SDL_GetPerformanceFrequency(); + if (!s_pf_freq) s_pf_freq = 1; + s_pf_b = 0; s_pf_scene_active = 0; s_pf_count = 0; s_pf_ring_seq = 0; + s_pf_last_enter = 0; + s_pf_on = 1; +#endif +} + +/* Bracket ONE native-wide mirror pass (called from the wide-mirror draw sites). + * Timestamp pairs, not TIME_ELAPSED — see the pool comment above. */ +static void gl_perf_mirror_begin(void) { + if (!s_pf_on || !s_mq_ok) return; + int b = s_pf_b; + if (s_mq_n[b] >= GLPERF_MIRQ) { s_mq_over[b]++; return; } + p_glQueryCounter(s_mq_q[b][s_mq_n[b] * 2], GL_TIMESTAMP); + s_mq_open = 1; +} +static void gl_perf_mirror_end(void) { + if (!s_pf_on || !s_mq_ok || !s_mq_open) return; + int b = s_pf_b; + p_glQueryCounter(s_mq_q[b][s_mq_n[b] * 2 + 1], GL_TIMESTAMP); + s_mq_n[b]++; + s_mq_open = 0; +} + +/* Top of present (after flush_cpu_upload, before clear/blit). */ +static void gl_perf_present_enter(void) { + /* Per-frame boundary for the 2D-backdrop stretch — runs from BOTH present + * paths (4:3 present_vram AND native-wide present_wide_fbo), before the + * perf-on gate so native-wide frames reset too. Snapshot this frame's + * backdrop-stretch diagnostics, then reset the per-frame counters. (The gate + * is per-prim now — no draw-order phase to reset.) Final batch already flushed + * by the caller. */ + g_bdg_applied = s_bdg_applied; g_bdg_prims = s_bdg_prims; g_bdg_clearx = s_bdg_clearx; + g_bdg_cur = (g_wide_cur != 0); g_bdg_base = g_wide_cur_base; g_bdg_w = g_wide_w; g_bdg_off = g_wide_off; + s_bdg_applied = 0; s_bdg_prims = 0; s_bdg_clearx = -999999; + { extern void psx_ws_dbg_gate_frame_snapshot(void); psx_ws_dbg_gate_frame_snapshot(); } + if (!s_pf_on) return; + uint64_t now = SDL_GetPerformanceCounter(); + s_pf_enter = now; + s_pf_total_pending = s_pf_last_enter + ? (double)(now - s_pf_last_enter) * 1000.0 / (double)s_pf_freq : 0.0; + s_pf_last_enter = now; + s_pf_prims_pending = (double)(s_scene_prims - s_pf_prims_last); /* prims drawn this frame */ + s_pf_prims_last = s_scene_prims; + s_pf_cw_pending[0] = s_cw_flush_ms; s_pf_cw_pending[1] = s_cw_wide_ms; + s_pf_cw_pending[2] = (double)s_cw_batches; + s_pf_cw_pending[3] = (double)s_cw_wide_sets; + s_pf_cw_pending[4] = (double)s_cw_fbo_creates; + s_cw_flush_ms = 0.0; s_cw_wide_ms = 0.0; + s_cw_batches = 0; s_cw_wide_sets = 0; s_cw_wide_cfgs = 0; + s_cw_wide_clears = 0; s_cw_fbo_creates = 0; + if (s_pf_scene_active) { p_glEndQuery(GL_TIME_ELAPSED); s_pf_scene_active = 0; } /* end frame b's scene draws */ + p_glBeginQuery(GL_TIME_ELAPSED, s_pf_present_q[s_pf_b]); /* time frame b's present */ +} + +/* End of present (after SwapWindow). wide = native-wide path. */ +static void gl_perf_present_exit(int wide) { + if (!s_pf_on) return; + uint64_t now = SDL_GetPerformanceCounter(); + p_glEndQuery(GL_TIME_ELAPSED); /* end present_q[b] */ + s_pf_buf_total[s_pf_b] = s_pf_total_pending; + s_pf_buf_pwall[s_pf_b] = (double)(now - s_pf_enter) * 1000.0 / (double)s_pf_freq; + s_pf_buf_prims[s_pf_b] = s_pf_prims_pending; + for (int ci = 0; ci < 5; ci++) s_pf_buf_cw[s_pf_b][ci] = s_pf_cw_pending[ci]; + s_pf_buf_wide[s_pf_b] = wide; + s_pf_buf_frame[s_pf_b] = s_pf_count; + int rd = (s_pf_b + 1) % GLPERF_NBUF; /* oldest buffer (frame count+1-NBUF), now done */ + if (s_pf_count >= (uint64_t)GLPERF_NBUF) { + GLuint64 sc = 0, pr = 0; + p_glGetQueryObjectui64v(s_pf_scene_q[rd], GL_QUERY_RESULT, &sc); + p_glGetQueryObjectui64v(s_pf_present_q[rd], GL_QUERY_RESULT, &pr); + double mir = 0.0; + for (int i = 0; i < s_mq_n[rd]; i++) { /* sum that frame's mirror pairs */ + GLuint64 t0 = 0, t1 = 0; + p_glGetQueryObjectui64v(s_mq_q[rd][i * 2], GL_QUERY_RESULT, &t0); + p_glGetQueryObjectui64v(s_mq_q[rd][i * 2 + 1], GL_QUERY_RESULT, &t1); + if (t1 > t0) mir += (double)(t1 - t0); + } + GlPerfSample *s = &s_pf_ring[s_pf_ring_seq % GLPERF_RING]; + s->total_ms = s_pf_buf_total[rd]; + s->present_wall_ms = s_pf_buf_pwall[rd]; + s->scene_gpu_ms = (double)sc / 1.0e6; + s->present_gpu_ms = (double)pr / 1.0e6; + s->mirror_gpu_ms = mir / 1.0e6; + s->prims = s_pf_buf_prims[rd]; + s->mirror_passes = (double)(s_mq_n[rd] + s_mq_over[rd]); + s->cw_flush_ms = s_pf_buf_cw[rd][0]; + s->cw_wide_ms = s_pf_buf_cw[rd][1]; + s->batches = s_pf_buf_cw[rd][2]; + s->wide_sets = s_pf_buf_cw[rd][3]; + s->fbo_creates = s_pf_buf_cw[rd][4]; + s->wide = s_pf_buf_wide[rd]; + s->frame = s_pf_buf_frame[rd]; + s_pf_ring_seq++; + } + s_mq_n[rd] = 0; s_mq_over[rd] = 0; /* rd becomes the next frame's buffer */ + s_pf_count++; + s_pf_b = rd; /* reuse oldest for next frame */ + p_glBeginQuery(GL_TIME_ELAPSED, s_pf_scene_q[s_pf_b]); /* open next frame's scene draws */ + s_pf_scene_active = 1; +} + +/* Aggregate the ring for the debug server. wide_filter: -1 all, 0 = 4:3, 1 = wide. + * out[0]=count, [1]=total_avg, [2]=total_max, [3]=emu_cpu_avg (total-present_wall), + * [4]=present_wall_avg, [5]=scene_gpu_avg, [6]=scene_gpu_max, [7]=present_gpu_avg, + * [8]=present_gpu_max. Returns the sample count. */ +/* Cumulative textured fraction of scene prims (decides flat vs textured batching + * priority). out_tex_frac = textured/total since boot; returns total prim count. */ +uint64_t gl_renderer_perf_prim_split(double *out_tex_frac) { + if (out_tex_frac) *out_tex_frac = s_scene_prims ? (double)s_scene_prims_tex / (double)s_scene_prims : 0.0; + return s_scene_prims; +} + +int gl_renderer_perf_aggregate(int wide_filter, double out[18]) { + for (int i = 0; i < 18; i++) out[i] = 0.0; + if (!s_pf_on) return 0; + int navail = (int)(s_pf_ring_seq < (uint64_t)GLPERF_RING ? s_pf_ring_seq : GLPERF_RING); + uint64_t start = s_pf_ring_seq - (uint64_t)navail; + int n = 0; + for (int i = 0; i < navail; i++) { + const GlPerfSample *s = &s_pf_ring[(start + i) % GLPERF_RING]; + if (wide_filter >= 0 && s->wide != wide_filter) continue; + double emu = s->total_ms - s->present_wall_ms; if (emu < 0) emu = 0; + out[1] += s->total_ms; if (s->total_ms > out[2]) out[2] = s->total_ms; + out[3] += emu; + out[4] += s->present_wall_ms; + out[5] += s->scene_gpu_ms; if (s->scene_gpu_ms > out[6]) out[6] = s->scene_gpu_ms; + out[7] += s->present_gpu_ms; if (s->present_gpu_ms > out[8]) out[8] = s->present_gpu_ms; + out[9] += s->prims; + out[10] += s->mirror_gpu_ms; if (s->mirror_gpu_ms > out[11]) out[11] = s->mirror_gpu_ms; + out[12] += s->mirror_passes; + out[13] += s->cw_flush_ms; + out[14] += s->cw_wide_ms; + out[15] += s->batches; + out[16] += s->wide_sets; + out[17] += s->fbo_creates; + n++; + } + if (n) { + out[1]/=n; out[3]/=n; out[4]/=n; out[5]/=n; out[7]/=n; out[9]/=n; out[10]/=n; out[12]/=n; + out[13]/=n; out[14]/=n; out[15]/=n; out[16]/=n; out[17]/=n; + } + out[0] = (double)n; + return n; +} + +/* Native-wide mirror ablation (perf attribution): see s_ws_ablate. */ +void gl_renderer_set_ws_ablate(int mode) { s_ws_ablate = (mode >= 0 && mode <= 3) ? mode : 0; } +int gl_renderer_get_ws_ablate(void) { return s_ws_ablate; } + static void interp_reset_history_unlocked(void) { s_interp_valid = 0; s_interp_w = s_interp_h = 0; @@ -3748,55 +3862,55 @@ void gl_renderer_set_interpolation_suspended(int suspended) { int gl_renderer_interpolation_owns_cadence(void) { return s_ctx && s_interp_enabled && !s_interp_suspended; } - -void gl_renderer_interpolation_diag(int *enabled, int *suspended, - int *history_frames, - double *host_hz, double *target_hz, - uint64_t *swaps) { + +void gl_renderer_interpolation_diag(int *enabled, int *suspended, + int *history_frames, + double *host_hz, double *target_hz, + uint64_t *swaps) { if (enabled) *enabled = s_interp_enabled; - if (suspended) *suspended = s_interp_suspended; - if (history_frames) *history_frames = s_interp_valid; - if (host_hz) *host_hz = s_interp_host_hz; + if (suspended) *suspended = s_interp_suspended; + if (history_frames) *history_frames = s_interp_valid; + if (host_hz) *host_hz = s_interp_host_hz; if (target_hz) *target_hz = s_interp_target_hz; if (swaps) *swaps = s_interp_swaps; } - + /* Copy a stable display image out of the mutable VRAM/wide render target. * Returns true when temporal blending owns this source-frame interval. */ -static int interp_capture(GLuint fbo, int x, int y, int w, int h, - int linear, int force_4_3, int source_path) { +static int interp_capture(GLuint fbo, int x, int y, int w, int h, + int linear, int force_4_3, int source_path) { if (!s_interp_enabled || s_interp_suspended || !fbo || w <= 0 || h <= 0) return 0; - int pw = w * s_scale, ph = h * s_scale; - if (pw != s_interp_w || ph != s_interp_h || - source_path != s_interp_source_path || force_4_3 != s_interp_force_4_3) { - s_interp_valid = 0; - s_interp_w = pw; s_interp_h = ph; - s_interp_prev = s_interp_cur = 0; - for (int i = 0; i < 3; i++) { - glBindTexture(GL_TEXTURE_2D, s_interp_tex[i]); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, pw, ph, 0, - GL_RGBA, GL_UNSIGNED_BYTE, NULL); - } - } - - int dst = 0; - if (s_interp_valid == 1) dst = s_interp_cur == 0 ? 1 : 0; - else if (s_interp_valid >= 2) dst = s_interp_prev; + int pw = w * s_scale, ph = h * s_scale; + if (pw != s_interp_w || ph != s_interp_h || + source_path != s_interp_source_path || force_4_3 != s_interp_force_4_3) { + s_interp_valid = 0; + s_interp_w = pw; s_interp_h = ph; + s_interp_prev = s_interp_cur = 0; + for (int i = 0; i < 3; i++) { + glBindTexture(GL_TEXTURE_2D, s_interp_tex[i]); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, pw, ph, 0, + GL_RGBA, GL_UNSIGNED_BYTE, NULL); + } + } + + int dst = 0; + if (s_interp_valid == 1) dst = s_interp_cur == 0 ? 1 : 0; + else if (s_interp_valid >= 2) dst = s_interp_prev; p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo); - glBindTexture(GL_TEXTURE_2D, s_interp_tex[dst]); - glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, - x * s_scale, y * s_scale, pw, ph); + glBindTexture(GL_TEXTURE_2D, s_interp_tex[dst]); + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, + x * s_scale, y * s_scale, pw, ph); p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - if (s_interp_valid == 0) { - s_interp_cur = dst; - s_interp_valid = 1; - } else { - s_interp_prev = s_interp_cur; - s_interp_cur = dst; - s_interp_valid = 2; - } - s_interp_linear = linear; - s_interp_force_4_3 = force_4_3; + if (s_interp_valid == 0) { + s_interp_cur = dst; + s_interp_valid = 1; + } else { + s_interp_prev = s_interp_cur; + s_interp_cur = dst; + s_interp_valid = 2; + } + s_interp_linear = linear; + s_interp_force_4_3 = force_4_3; s_interp_source_path = source_path; s_interp_captures++; return 1; @@ -3804,51 +3918,54 @@ static int interp_capture(GLuint fbo, int x, int y, int w, int h, static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh) { int prev = s_interp_prev, curr = s_interp_cur; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - glViewport(lx, ly, lw, lh); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_interp_tex[prev]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); - p_glActiveTexture(PSXGL_TEXTURE0 + 1); - glBindTexture(GL_TEXTURE_2D, s_interp_tex[curr]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); - p_glUseProgram(s_interp_prog); - p_glUniform1i(s_interp_uPrev, 0); - p_glUniform1i(s_interp_uCurr, 1); - p_glUniform1f(s_interp_uAlpha, alpha); - p_glUniform1i(s_interp_uBlendMode, s_interp_blend_mode); - p_glUniform4f(s_interp_uUvRect, 0.f, 0.f, 1.f, 1.f); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + glViewport(lx, ly, lw, lh); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_interp_tex[prev]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); + p_glActiveTexture(PSXGL_TEXTURE0 + 1); + glBindTexture(GL_TEXTURE_2D, s_interp_tex[curr]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); + p_glUseProgram(s_interp_prog); + p_glUniform1i(s_interp_uPrev, 0); + p_glUniform1i(s_interp_uCurr, 1); + p_glUniform1f(s_interp_uAlpha, alpha); + p_glUniform1i(s_interp_uBlendMode, s_interp_blend_mode); + p_glUniform4f(s_interp_uUvRect, 0.f, 0.f, 1.f, 1.f); + /* Interp textures hold exactly the display rect (uv_rect is 0..1), so pitch + * == display height == s_interp_h. */ + INTERP_SCANLINE(s_interp_h, s_interp_h, lh); p_glBindVertexArray(s_present_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); - p_glBindVertexArray(0); - p_glUseProgram(0); - p_glActiveTexture(PSXGL_TEXTURE0); -} - + glDrawArrays(GL_TRIANGLES, 0, 3); + p_glBindVertexArray(0); + p_glUseProgram(0); + p_glActiveTexture(PSXGL_TEXTURE0); +} + static int interp_present(float alpha) { if (!s_ctx || !s_interp_enabled || s_interp_suspended || s_interp_valid < 1) return 0; - int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - int lx, ly, lw, lh; - if (s_interp_force_4_3) - letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); - else - letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + int lx, ly, lw, lh; + if (s_interp_force_4_3) + letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); + else + letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); if (lx != 0 || ly != 0 || lw != ww || lh != wh) { glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT); } present_bezel(ww, wh, lx, ly, lw, lh); interp_draw_quad(alpha, lx, ly, lw, lh); - pres_record(GL_PRES_INTERP, 0, 0, s_interp_w, s_interp_h, - lx, ly, lw, lh); + pres_record(GL_PRES_INTERP, 0, 0, s_interp_w, s_interp_h, + lx, ly, lw, lh); gl_swap_with_osd(); - s_interp_swaps++; - return 1; + s_interp_swaps++; + return 1; } static void interp_wait_until(uint64_t deadline, uint64_t frequency) { @@ -3904,97 +4021,98 @@ static void interp_present_source_interval(void) { diag_swaps = s_interp_swaps; } } - -/* Draw one host OSD ARGB image into the default framebuffer at (vx,vy) - * in top-left window coordinates (y down). Bitmap is ow×oh; viewport is - * dw×dh (may upscale for HiDPI / large windows). */ -static void gl_draw_osd_image(const uint32_t *px, int ow, int oh, - int dw, int dh, int vx, int vy, int ww, int wh) { - if (!px || ow <= 0 || oh <= 0 || dw <= 0 || dh <= 0 || - !s_present_prog || ww <= 0 || wh <= 0) - return; - if (!s_osd_tex) glGenTextures(1, &s_osd_tex); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, s_osd_tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - if (s_osd_tw != ow || s_osd_th != oh) { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, ow, oh, 0, - GL_BGRA, GL_UNSIGNED_BYTE, px); - s_osd_tw = ow; - s_osd_th = oh; - } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, ow, oh, - GL_BGRA, GL_UNSIGNED_BYTE, px); - } - if (vx + dw > ww) dw = ww - vx; - if (vy + dh > wh) dh = wh - vy; - if (dw < 1 || dh < 1) return; - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); - glDisable(GL_SCISSOR_TEST); - glDisable(GL_STENCIL_TEST); - glDisable(GL_DEPTH_TEST); - /* host_osd bakes opaque panels (A=0xFF). Do not blend — PSX mode-2 - * REVERSE_SUBTRACT left armed across FMV present made toasts solid black. */ - glDisable(GL_BLEND); - if (p_glBlendEquationSeparate) - p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); - /* GL viewport origin is bottom-left. */ - glViewport(vx, wh - vy - dh, dw, dh); - p_glUseProgram(s_present_prog); - p_glUniform1i(s_present_uTex, 0); + +/* Draw one host OSD ARGB image into the default framebuffer at (vx,vy) + * in top-left window coordinates (y down). Bitmap is ow×oh; viewport is + * dw×dh (may upscale for HiDPI / large windows). */ +static void gl_draw_osd_image(const uint32_t *px, int ow, int oh, + int dw, int dh, int vx, int vy, int ww, int wh) { + if (!px || ow <= 0 || oh <= 0 || dw <= 0 || dh <= 0 || + !s_present_prog || ww <= 0 || wh <= 0) + return; + if (!s_osd_tex) glGenTextures(1, &s_osd_tex); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, s_osd_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + if (s_osd_tw != ow || s_osd_th != oh) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, ow, oh, 0, + GL_BGRA, GL_UNSIGNED_BYTE, px); + s_osd_tw = ow; + s_osd_th = oh; + } else { + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, ow, oh, + GL_BGRA, GL_UNSIGNED_BYTE, px); + } + if (vx + dw > ww) dw = ww - vx; + if (vy + dh > wh) dh = wh - vy; + if (dw < 1 || dh < 1) return; + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_STENCIL_TEST); + glDisable(GL_DEPTH_TEST); + /* host_osd bakes opaque panels (A=0xFF). Do not blend — PSX mode-2 + * REVERSE_SUBTRACT left armed across FMV present made toasts solid black. */ + glDisable(GL_BLEND); + if (p_glBlendEquationSeparate) + p_glBlendEquationSeparate(PSXGL_FUNC_ADD, PSXGL_FUNC_ADD); + /* GL viewport origin is bottom-left. */ + glViewport(vx, wh - vy - dh, dw, dh); + p_glUseProgram(s_present_prog); + p_glUniform1i(s_present_uTex, 0); present_set_sharp(0, 0, 0, 0, 0); /* OSD is authored at output res */ - /* Host OSD bitmaps are top-down (row 0 = top), same as guest CPU - * present with v_flip=1: uv (0,0)-(1,1). (0,1)-(1,0) was the hold-last - * cancel for already-oriented captures and made toasts upside-down. */ - p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f); - { + PRESENT_SCANLINE(0, 0, 0); /* never scanline the host OSD */ + /* Host OSD bitmaps are top-down (row 0 = top), same as guest CPU + * present with v_flip=1: uv (0,0)-(1,1). (0,1)-(1,0) was the hold-last + * cancel for already-oriented captures and made toasts upside-down. */ + p_glUniform4f(s_present_uUvRect, 0.f, 0.f, 1.f, 1.f); + { p_glBindVertexArray(s_present_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); - p_glBindVertexArray(0); - } - p_glUseProgram(0); -} - -/* Composite host toast + volume bar into the default framebuffer, then swap. */ -static void gl_swap_with_osd(void) { - if (s_present_prog && s_ctx) { - int ww = 0, wh = 0; - SDL_GL_GetDrawableSize(s_win, &ww, &wh); - if (ww > 0 && wh > 0) { - const uint32_t *px = NULL; - int ow = 0, oh = 0; - /* OSD authored for ~480-tall present; grow with drawable height. */ - int ui = wh / 480; - int margin; - if (ui < 1) ui = 1; - if (ui > 8) ui = 8; - margin = 8 * ui; - if (host_osd_image(&px, &ow, &oh) && px) - gl_draw_osd_image(px, ow, oh, ow * ui, oh * ui, - margin, margin, ww, wh); - if (host_osd_volume_image(&px, &ow, &oh) && px) { - const int dw = ow * ui, dh = oh * ui; - int vx = (ww > dw + margin) ? (ww - dw - margin) : margin; - int vy = (wh > dh) ? ((wh - dh) / 2) : margin; - gl_draw_osd_image(px, ow, oh, dw, dh, vx, vy, ww, wh); - } + glDrawArrays(GL_TRIANGLES, 0, 3); + p_glBindVertexArray(0); + } + p_glUseProgram(0); +} + +/* Composite host toast + volume bar into the default framebuffer, then swap. */ +static void gl_swap_with_osd(void) { + if (s_present_prog && s_ctx) { + int ww = 0, wh = 0; + SDL_GL_GetDrawableSize(s_win, &ww, &wh); + if (ww > 0 && wh > 0) { + const uint32_t *px = NULL; + int ow = 0, oh = 0; + /* OSD authored for ~480-tall present; grow with drawable height. */ + int ui = wh / 480; + int margin; + if (ui < 1) ui = 1; + if (ui > 8) ui = 8; + margin = 8 * ui; + if (host_osd_image(&px, &ow, &oh) && px) + gl_draw_osd_image(px, ow, oh, ow * ui, oh * ui, + margin, margin, ww, wh); + if (host_osd_volume_image(&px, &ow, &oh) && px) { + const int dw = ow * ui, dh = oh * ui; + int vx = (ww > dw + margin) ? (ww - dw - margin) : margin; + int vy = (wh > dh) ? ((wh - dh) / 2) : margin; + gl_draw_osd_image(px, ow, oh, dw, dh, vx, vy, ww, wh); + } if (psx_rewind_overlay_image(&px, &ow, &oh) && px) { float slide = psx_rewind_slide(); int dw = ww; - int dh = (wh * oh) / 480; - int vy; - if (dh < 8) dh = oh; - vy = wh - (int)((float)dh * slide + 0.5f); + int dh = (wh * oh) / 480; + int vy; + if (dh < 8) dh = oh; + vy = wh - (int)((float)dh * slide + 0.5f); gl_draw_osd_image(px, ow, oh, dw, dh, 0, vy, ww, wh); } if (psx_savestate_menu_overlay_image(&px, &ow, &oh) && px) gl_draw_osd_image(px, ow, oh, ww, wh, 0, 0, ww, wh); } } - host_osd_present_done(); + host_osd_present_done(); /* present_shot (GL backend): the default framebuffer now holds the composed * frame — display quad fitted to the window, plus OSD — so this is the only * capture that carries the presented aspect. Buffer-level captures resolve @@ -4031,45 +4149,51 @@ static void gl_swap_with_osd(void) { present_shot_done(wrote); } } - SDL_GL_SwapWindow(s_win); -} - + SDL_GL_SwapWindow(s_win); +} + static void present_target_quad(GLuint tex, int tex_w, int tex_h, int x, int y, int w, int h, int linear, int lx, int ly, int lw, int lh, int v_flip) { - float u0, v0, u1, v1; - p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); - glViewport(lx, ly, lw, lh); - p_glActiveTexture(PSXGL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, tex); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); - p_glUseProgram(s_present_prog); - p_glUniform1i(s_present_uTex, 0); + float u0, v0, u1, v1; + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); + glViewport(lx, ly, lw, lh); + p_glActiveTexture(PSXGL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); + p_glUseProgram(s_present_prog); + p_glUniform1i(s_present_uTex, 0); /* The rasterized path already renders at the internal scale, so it has no * low-res source to reconstruct — keep the plain sample. */ present_set_sharp(0, 0, 0, 0, 0); - /* Half-texel inset: with GL_LINEAR, corner-mapped UVs make the outermost - * dest pixels blend the border texel with VRAM outside the content rect - * (visible edge stripe with AA on). Center-mapped UVs keep edge samples - * inside the rect; interior sampling is unchanged. */ - u0 = ((float)x + 0.5f) / (float)tex_w; - v0 = ((float)y + 0.5f) / (float)tex_h; - u1 = ((float)(x + w) - 0.5f) / (float)tex_w; - v1 = ((float)(y + h) - 0.5f) / (float)tex_h; - /* PRESENT_VS always samples with mix(v0,v1,1-p.y). For CPU/FBO guest - * bands that is the correct PSX top-down → GL mapping (v_flip=1). For a - * glCopyTexSubImage2D of the already-presented drawable, the texture - * already matches screen orientation — swapping v ends cancels the - * shader flip so hold-last is not upside-down for one frame. */ - if (!v_flip) { - float t = v0; - v0 = v1; - v1 = t; - } - p_glUniform4f(s_present_uUvRect, u0, v0, u1, v1); - p_glBindVertexArray(s_present_vao); - glDrawArrays(GL_TRIANGLES, 0, 3); + /* Scanline at the native line grid. v_uv is normalized against tex_h (the + * full VRAM/FBO texture), and one texel row is one PS1 scanline, so tex_h is + * the phase pitch; h is the displayed line count for the output-scale gate. + * v_flip=0 is the already-composed hold-last drawable (scanlines, if any, + * are already baked) — skip it. */ + PRESENT_SCANLINE(v_flip ? tex_h : 0, v_flip ? h : 0, lh); + /* Half-texel inset: with GL_LINEAR, corner-mapped UVs make the outermost + * dest pixels blend the border texel with VRAM outside the content rect + * (visible edge stripe with AA on). Center-mapped UVs keep edge samples + * inside the rect; interior sampling is unchanged. */ + u0 = ((float)x + 0.5f) / (float)tex_w; + v0 = ((float)y + 0.5f) / (float)tex_h; + u1 = ((float)(x + w) - 0.5f) / (float)tex_w; + v1 = ((float)(y + h) - 0.5f) / (float)tex_h; + /* PRESENT_VS always samples with mix(v0,v1,1-p.y). For CPU/FBO guest + * bands that is the correct PSX top-down → GL mapping (v_flip=1). For a + * glCopyTexSubImage2D of the already-presented drawable, the texture + * already matches screen orientation — swapping v ends cancels the + * shader flip so hold-last is not upside-down for one frame. */ + if (!v_flip) { + float t = v0; + v0 = v1; + v1 = t; + } + p_glUniform4f(s_present_uUvRect, u0, v0, u1, v1); + p_glBindVertexArray(s_present_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); p_glBindVertexArray(0); p_glUseProgram(0); } @@ -4113,6 +4237,7 @@ static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh) { p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); p_glUniform4f(s_present_uUvRect, 0.0f, 0.0f, 1.0f, 1.0f); + PRESENT_SCANLINE(0, 0, 0); /* bezel art is not scanlined */ p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3); p_glBindVertexArray(0); @@ -4120,69 +4245,69 @@ static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh) { } int gl_renderer_present_hold_last(void) { - int ww = 0, wh = 0; - int lx, ly, lw, lh; - if (!s_ctx || !s_win || s_hold_kind == HOLD_NONE || !s_hold_tex) - return 0; - SDL_GL_GetDrawableSize(s_win, &ww, &wh); - if (ww < 1 || wh < 1) - return 0; - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); - glClearColor(0.f, 0.f, 0.f, 1.f); - glClear(GL_COLOR_BUFFER_BIT); - if (s_hold_kind == HOLD_DRAWABLE) { - /* Captured image already includes letterbox bars. Stretch 1:1, or - * letterbox to preserve aspect if the drawable resized mid-resim. */ - lx = 0; - ly = 0; - lw = ww; - lh = wh; - if (s_hold_tw > 0 && s_hold_th > 0 && - ((int64_t)s_hold_tw * wh != (int64_t)s_hold_th * ww)) { - if ((int64_t)ww * s_hold_th > (int64_t)wh * s_hold_tw) { - lw = (int)(((int64_t)wh * s_hold_tw) / s_hold_th); - if (lw < 1) lw = 1; - lx = (ww - lw) / 2; - lh = wh; - ly = 0; - } else { - lh = (int)(((int64_t)ww * s_hold_th) / s_hold_tw); - if (lh < 1) lh = 1; - ly = (wh - lh) / 2; - lw = ww; - lx = 0; - } - } - /* v_flip=0: drawable capture is already screen-oriented. */ - present_target_quad(s_hold_tex, s_hold_tw, s_hold_th, - 0, 0, s_hold_tw, s_hold_th, 0, lx, ly, lw, lh, 0); - } else { - if (s_hold_force_4_3) - letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); - else - letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); - present_target_quad(s_hold_tex, s_hold_tw, s_hold_th, - 0, 0, s_hold_tw, s_hold_th, s_hold_linear, - lx, ly, lw, lh, 1); - /* Upgrade to drawable after letterboxing once. Live+interp leaves - * HOLD_NATIVE (no Swap on main); drawable path is the soak-proven - * full-window image (GL without interp). */ - hold_capture_drawable(); - } - latency_ring_mark(LAT_SWAP_BEGIN); - gl_swap_with_osd(); - latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; - return 1; -} - -void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear, - int force_4_3) { - if (!s_ctx || !s_raster_ok) return; - flush_flat_batch(); - flush_tex_batch(); - flush_cpu_upload(); + int ww = 0, wh = 0; + int lx, ly, lw, lh; + if (!s_ctx || !s_win || s_hold_kind == HOLD_NONE || !s_hold_tex) + return 0; + SDL_GL_GetDrawableSize(s_win, &ww, &wh); + if (ww < 1 || wh < 1) + return 0; + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); + glClearColor(0.f, 0.f, 0.f, 1.f); + glClear(GL_COLOR_BUFFER_BIT); + if (s_hold_kind == HOLD_DRAWABLE) { + /* Captured image already includes letterbox bars. Stretch 1:1, or + * letterbox to preserve aspect if the drawable resized mid-resim. */ + lx = 0; + ly = 0; + lw = ww; + lh = wh; + if (s_hold_tw > 0 && s_hold_th > 0 && + ((int64_t)s_hold_tw * wh != (int64_t)s_hold_th * ww)) { + if ((int64_t)ww * s_hold_th > (int64_t)wh * s_hold_tw) { + lw = (int)(((int64_t)wh * s_hold_tw) / s_hold_th); + if (lw < 1) lw = 1; + lx = (ww - lw) / 2; + lh = wh; + ly = 0; + } else { + lh = (int)(((int64_t)ww * s_hold_th) / s_hold_tw); + if (lh < 1) lh = 1; + ly = (wh - lh) / 2; + lw = ww; + lx = 0; + } + } + /* v_flip=0: drawable capture is already screen-oriented. */ + present_target_quad(s_hold_tex, s_hold_tw, s_hold_th, + 0, 0, s_hold_tw, s_hold_th, 0, lx, ly, lw, lh, 0); + } else { + if (s_hold_force_4_3) + letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); + else + letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); + present_target_quad(s_hold_tex, s_hold_tw, s_hold_th, + 0, 0, s_hold_tw, s_hold_th, s_hold_linear, + lx, ly, lw, lh, 1); + /* Upgrade to drawable after letterboxing once. Live+interp leaves + * HOLD_NATIVE (no Swap on main); drawable path is the soak-proven + * full-window image (GL without interp). */ + hold_capture_drawable(); + } + latency_ring_mark(LAT_SWAP_BEGIN); + gl_swap_with_osd(); + latency_ring_mark(LAT_SWAP_END); + s_probe_swap++; + return 1; +} + +void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear, + int force_4_3) { + if (!s_ctx || !s_raster_ok) return; + flush_flat_batch(); + flush_tex_batch(); + flush_cpu_upload(); if (s_force_present_remaining <= 0 && s_last_present_path == GL_PRES_VRAM && s_last_dx == disp_x && s_last_dy == disp_y && @@ -4191,191 +4316,191 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear, !host_osd_needs_present() && !psx_present_vsync_owns_cadence() && !gl_renderer_interpolation_owns_cadence()) { - s_probe_skip++; - gl_perf_present_enter(); - gl_perf_present_exit(0); - return; - } - gl_perf_present_enter(); /* per-frame backdrop-phase reset + dbg snapshot live in here */ - int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - int lx, ly, lw, lh; - if (force_4_3) - letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); - else - letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); - /* No short-band adjustment on the 15-bit FBO path: a game's native short - * display mode (e.g. 216/224-line menus) must fill the rect as before. - * The FMV band fix lives in the depth24/CPU present paths only. */ - - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); - if (lx != 0 || ly != 0 || lw != ww || lh != wh) { - glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); - } - int interp_pair = interp_capture(s_hr_fbo, disp_x, disp_y, w, h, - linear, force_4_3, GL_PRES_VRAM); + s_probe_skip++; + gl_perf_present_enter(); + gl_perf_present_exit(0); + return; + } + gl_perf_present_enter(); /* per-frame backdrop-phase reset + dbg snapshot live in here */ + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + int lx, ly, lw, lh; + if (force_4_3) + letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh); + else + letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); + /* No short-band adjustment on the 15-bit FBO path: a game's native short + * display mode (e.g. 216/224-line menus) must fill the rect as before. + * The FMV band fix lives in the depth24/CPU present paths only. */ + + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); + if (lx != 0 || ly != 0 || lw != ww || lh != wh) { + glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT); + } + int interp_pair = interp_capture(s_hr_fbo, disp_x, disp_y, w, h, + linear, force_4_3, GL_PRES_VRAM); if (interp_pair) { /* Temporal blending owns this stock frame interval. Capture hold-last * before pacing; every blend and Swap remains on this context/thread. */ hold_capture_native_fbo(s_hr_fbo, disp_x, disp_y, w, h, force_4_3, linear); interp_present_source_interval(); - gl_perf_present_exit(0); - present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0); - present_force_consumed(); - s_last_present_path = GL_PRES_VRAM; - s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = w; s_last_dh = h; - return; - } + gl_perf_present_exit(0); + present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0); + present_force_consumed(); + s_last_present_path = GL_PRES_VRAM; + s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = w; s_last_dh = h; + return; + } present_bezel(ww, wh, lx, ly, lw, lh); present_target_quad(s_hr_tex, VRAM_W, VRAM_H, disp_x, disp_y, w, h, linear, lx, ly, lw, lh, 1); - pres_record(GL_PRES_VRAM, disp_x, disp_y, w, h, lx, ly, lw, lh); - hold_capture_drawable(); - latency_ring_mark(LAT_SWAP_BEGIN); - gl_swap_with_osd(); - latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; - gl_perf_present_exit(0); - present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0); - present_force_consumed(); - s_last_present_path = GL_PRES_VRAM; - s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = w; s_last_dh = h; - coh_record(GL_COH_PRESENT, disp_x, disp_y, disp_x + w - 1, disp_y + h - 1); -} - -/* Native-wide fast path: authoritatively copy the canonical 4:3 framebuffer - * into the wide surface's CENTRE columns [g_wide_off, g_wide_off+native_w) for - * the displayed Y band, so the per-prim mirror could skip every centre-only - * prim (the dominant native-wide GPU saving). The reveal margins were already - * produced by the mirror; this leaves them untouched. One FBO->FBO blit, - * x-translated by the reveal offset. No-op when s_wide_fast is off (then the - * mirror drew the full surface, as before). Shared by both present paths. */ -static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h) { - if (!s_wide_fast || g_wide_w <= 0) return; - int native_w = g_wide_w - 2 * g_wide_off; - if (native_w <= 0) return; - int S = s_scale; - (void)disp_y; (void)disp_h; - /* Copy the canonical framebuffer column into the wide surface CENTRE over the - * FULL VRAM height, not just the current display band [disp_y, disp_y+disp_h]. - * The wide surface holds BOTH vertical double-buffer bands (Ape flips - * display_y 0<->256), and the game's draw area / display band can differ per - * scene (the cityscape intro exposed rows outside disp_h). Copying the whole - * column makes the wide CENTRE bit-identical to what the full mirror would - * have drawn there for every band, so nothing the present reads is ever left - * black. The margins (x outside the centre) are untouched. */ - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, wide_fbo); - glDisable(GL_SCISSOR_TEST); - p_glBlitFramebuffer(base_x * S, 0, - (base_x + native_w) * S, VRAM_H * S, - g_wide_off * S, 0, - (g_wide_off + native_w) * S, VRAM_H * S, - GL_COLOR_BUFFER_BIT, GL_NEAREST); - p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); -} - -/* GPU-direct native-wide present: blit the displayed buffer's wide FBO straight - * to the window (no glReadPixels / glFinish CPU round-trip). The wide surface is - * g_wide_w wide × VRAM_H tall (at scale S); present its [0,g_wide_w] × [disp_y, - * disp_y+disp_h] region into the letterbox, V-flipped like present_vram (FBO y - * bottom-origin → window top). Returns 0 if there's no wide surface for base_x - * (caller falls back). disp_x is the displayed buffer base (the wide-surface key). */ -int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear) { - if (!s_ctx || !s_raster_ok || g_wide_w <= 0) return 0; - GLuint fbo = 0, tex = 0; - for (int i = 0; i < WIDE_MAX_SURF; i++) - if (s_wide_fbo[i] && s_wide_base[i] == disp_x) { - fbo = s_wide_fbo[i]; tex = s_wide_tex[i]; break; - } - if (!fbo) return 0; - flush_flat_batch(); - flush_tex_batch(); - flush_cpu_upload(); + pres_record(GL_PRES_VRAM, disp_x, disp_y, w, h, lx, ly, lw, lh); + hold_capture_drawable(); + latency_ring_mark(LAT_SWAP_BEGIN); + gl_swap_with_osd(); + latency_ring_mark(LAT_SWAP_END); + s_probe_swap++; + gl_perf_present_exit(0); + present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0); + present_force_consumed(); + s_last_present_path = GL_PRES_VRAM; + s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = w; s_last_dh = h; + coh_record(GL_COH_PRESENT, disp_x, disp_y, disp_x + w - 1, disp_y + h - 1); +} + +/* Native-wide fast path: authoritatively copy the canonical 4:3 framebuffer + * into the wide surface's CENTRE columns [g_wide_off, g_wide_off+native_w) for + * the displayed Y band, so the per-prim mirror could skip every centre-only + * prim (the dominant native-wide GPU saving). The reveal margins were already + * produced by the mirror; this leaves them untouched. One FBO->FBO blit, + * x-translated by the reveal offset. No-op when s_wide_fast is off (then the + * mirror drew the full surface, as before). Shared by both present paths. */ +static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h) { + if (!s_wide_fast || g_wide_w <= 0) return; + int native_w = g_wide_w - 2 * g_wide_off; + if (native_w <= 0) return; + int S = s_scale; + (void)disp_y; (void)disp_h; + /* Copy the canonical framebuffer column into the wide surface CENTRE over the + * FULL VRAM height, not just the current display band [disp_y, disp_y+disp_h]. + * The wide surface holds BOTH vertical double-buffer bands (Ape flips + * display_y 0<->256), and the game's draw area / display band can differ per + * scene (the cityscape intro exposed rows outside disp_h). Copying the whole + * column makes the wide CENTRE bit-identical to what the full mirror would + * have drawn there for every band, so nothing the present reads is ever left + * black. The margins (x outside the centre) are untouched. */ + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_hr_fbo); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, wide_fbo); + glDisable(GL_SCISSOR_TEST); + p_glBlitFramebuffer(base_x * S, 0, + (base_x + native_w) * S, VRAM_H * S, + g_wide_off * S, 0, + (g_wide_off + native_w) * S, VRAM_H * S, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0); + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); +} + +/* GPU-direct native-wide present: blit the displayed buffer's wide FBO straight + * to the window (no glReadPixels / glFinish CPU round-trip). The wide surface is + * g_wide_w wide × VRAM_H tall (at scale S); present its [0,g_wide_w] × [disp_y, + * disp_y+disp_h] region into the letterbox, V-flipped like present_vram (FBO y + * bottom-origin → window top). Returns 0 if there's no wide surface for base_x + * (caller falls back). disp_x is the displayed buffer base (the wide-surface key). */ +int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear) { + if (!s_ctx || !s_raster_ok || g_wide_w <= 0) return 0; + GLuint fbo = 0, tex = 0; + for (int i = 0; i < WIDE_MAX_SURF; i++) + if (s_wide_fbo[i] && s_wide_base[i] == disp_x) { + fbo = s_wide_fbo[i]; tex = s_wide_tex[i]; break; + } + if (!fbo) return 0; + flush_flat_batch(); + flush_tex_batch(); + flush_cpu_upload(); if (s_force_present_remaining <= 0 && s_last_present_path == GL_PRES_WIDE && - s_last_dx == disp_x && s_last_dy == disp_y && - s_last_dw == g_wide_w && s_last_dh == disp_h && - !present_dirty_test(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1) && + s_last_dx == disp_x && s_last_dy == disp_y && + s_last_dw == g_wide_w && s_last_dh == disp_h && + !present_dirty_test(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1) && !host_osd_needs_present() && !psx_present_vsync_owns_cadence() && !gl_renderer_interpolation_owns_cadence()) { - s_probe_skip++; - gl_perf_present_enter(); - gl_perf_present_exit(1); - return 1; - } - gl_perf_present_enter(); - int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); - int lx, ly, lw, lh; - letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); - wide_blit_center(fbo, disp_x, disp_y, disp_h); /* fast-path: authoritative centre */ - - p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); - glDisable(GL_SCISSOR_TEST); - glViewport(0, 0, ww, wh); - if (lx != 0 || ly != 0 || lw != ww || lh != wh) { - glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT); - } - int interp_pair = interp_capture(fbo, 0, disp_y, g_wide_w, disp_h, - linear, 0, GL_PRES_WIDE); + s_probe_skip++; + gl_perf_present_enter(); + gl_perf_present_exit(1); + return 1; + } + gl_perf_present_enter(); + int ww = 0, wh = 0; SDL_GL_GetDrawableSize(s_win, &ww, &wh); + int lx, ly, lw, lh; + letterbox_rect(ww, wh, &lx, &ly, &lw, &lh); + wide_blit_center(fbo, disp_x, disp_y, disp_h); /* fast-path: authoritative centre */ + + p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0); + glDisable(GL_SCISSOR_TEST); + glViewport(0, 0, ww, wh); + if (lx != 0 || ly != 0 || lw != ww || lh != wh) { + glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT); + } + int interp_pair = interp_capture(fbo, 0, disp_y, g_wide_w, disp_h, + linear, 0, GL_PRES_WIDE); if (interp_pair) { hold_capture_native_fbo(fbo, 0, disp_y, g_wide_w, disp_h, 0, linear); interp_present_source_interval(); - gl_perf_present_exit(1); - present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0); - present_force_consumed(); - s_last_present_path = GL_PRES_WIDE; - s_last_dx = disp_x; s_last_dy = disp_y; - s_last_dw = g_wide_w; s_last_dh = disp_h; - return 1; - } - present_target_quad(tex, g_wide_w, VRAM_H, - 0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh, 1); - pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh); - hold_capture_drawable(); - latency_ring_mark(LAT_SWAP_BEGIN); - gl_swap_with_osd(); - latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; - gl_perf_present_exit(1); - present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0); - present_force_consumed(); - s_last_present_path = GL_PRES_WIDE; - s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = g_wide_w; s_last_dh = disp_h; - coh_record(GL_COH_PRESENT, 0, disp_y, g_wide_w - 1, disp_y + disp_h - 1); - return 1; -} - -static const GpuRenderBackend GL_BACKEND = { - .name = "opengl", - .init = glb_init, .set_scale = glb_set_scale, .scale = glb_scale, - .set_texture_filter = glb_set_texture_filter, .texture_filter = glb_texture_filter, - .set_semi_transparency = glb_set_semi_transparency, .set_mask_bits = glb_set_mask_bits, - .set_texture_window = glb_set_texture_window, .set_color_modulation = glb_set_color_modulation, - .set_precise_triangle = glb_set_precise_triangle, - .set_perspective_triangle = glb_set_perspective_triangle, - .fill_rect = glb_fill_rect, .copy_rect = glb_copy_rect, - .draw_flat_triangle = glb_draw_flat_triangle, .draw_gouraud_triangle = glb_draw_gouraud_triangle, - .draw_textured_triangle = glb_draw_textured_triangle, - .draw_shaded_textured_triangle = glb_draw_shaded_textured_triangle, - .draw_flat_rect = glb_draw_flat_rect, .draw_textured_rect = glb_draw_textured_rect, - .draw_textured_rect_scaled = glb_draw_textured_rect_scaled, - .draw_line = glb_draw_line, .draw_shaded_line = glb_draw_shaded_line, - .render_display = glb_render_display, .render_display_hires = glb_render_display_hires, - .vram_write = glb_vram_write, .vram_read = glb_vram_read, - .vram_transfer_in = glb_vram_transfer_in, .vram_transfer_out = glb_vram_transfer_out, - .set_draw_area = glb_set_draw_area, .get_draw_area = glb_get_draw_area, - .set_draw_offset = glb_set_draw_offset, - .wide_configure = glb_wide_configure, - .wide_set_target = glb_wide_set_target, - .wide_disable_target = glb_wide_disable_target, - .wide_clear = glb_wide_clear, - .wide_clear_margins = glb_wide_clear_margins, - .render_wide_display = glb_render_wide_display, - .wide_dump_full = glb_wide_dump_full, -}; - -const GpuRenderBackend *gl_backend_get(void) { return &GL_BACKEND; } + gl_perf_present_exit(1); + present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0); + present_force_consumed(); + s_last_present_path = GL_PRES_WIDE; + s_last_dx = disp_x; s_last_dy = disp_y; + s_last_dw = g_wide_w; s_last_dh = disp_h; + return 1; + } + present_target_quad(tex, g_wide_w, VRAM_H, + 0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh, 1); + pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh); + hold_capture_drawable(); + latency_ring_mark(LAT_SWAP_BEGIN); + gl_swap_with_osd(); + latency_ring_mark(LAT_SWAP_END); + s_probe_swap++; + gl_perf_present_exit(1); + present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0); + present_force_consumed(); + s_last_present_path = GL_PRES_WIDE; + s_last_dx = disp_x; s_last_dy = disp_y; s_last_dw = g_wide_w; s_last_dh = disp_h; + coh_record(GL_COH_PRESENT, 0, disp_y, g_wide_w - 1, disp_y + disp_h - 1); + return 1; +} + +static const GpuRenderBackend GL_BACKEND = { + .name = "opengl", + .init = glb_init, .set_scale = glb_set_scale, .scale = glb_scale, + .set_texture_filter = glb_set_texture_filter, .texture_filter = glb_texture_filter, + .set_semi_transparency = glb_set_semi_transparency, .set_mask_bits = glb_set_mask_bits, + .set_texture_window = glb_set_texture_window, .set_color_modulation = glb_set_color_modulation, + .set_precise_triangle = glb_set_precise_triangle, + .set_perspective_triangle = glb_set_perspective_triangle, + .fill_rect = glb_fill_rect, .copy_rect = glb_copy_rect, + .draw_flat_triangle = glb_draw_flat_triangle, .draw_gouraud_triangle = glb_draw_gouraud_triangle, + .draw_textured_triangle = glb_draw_textured_triangle, + .draw_shaded_textured_triangle = glb_draw_shaded_textured_triangle, + .draw_flat_rect = glb_draw_flat_rect, .draw_textured_rect = glb_draw_textured_rect, + .draw_textured_rect_scaled = glb_draw_textured_rect_scaled, + .draw_line = glb_draw_line, .draw_shaded_line = glb_draw_shaded_line, + .render_display = glb_render_display, .render_display_hires = glb_render_display_hires, + .vram_write = glb_vram_write, .vram_read = glb_vram_read, + .vram_transfer_in = glb_vram_transfer_in, .vram_transfer_out = glb_vram_transfer_out, + .set_draw_area = glb_set_draw_area, .get_draw_area = glb_get_draw_area, + .set_draw_offset = glb_set_draw_offset, + .wide_configure = glb_wide_configure, + .wide_set_target = glb_wide_set_target, + .wide_disable_target = glb_wide_disable_target, + .wide_clear = glb_wide_clear, + .wide_clear_margins = glb_wide_clear_margins, + .render_wide_display = glb_render_wide_display, + .wide_dump_full = glb_wide_dump_full, +}; + +const GpuRenderBackend *gl_backend_get(void) { return &GL_BACKEND; } diff --git a/runtime/src/host_keymap.c b/runtime/src/host_keymap.c index 42263a760..848309e20 100644 --- a/runtime/src/host_keymap.c +++ b/runtime/src/host_keymap.c @@ -97,6 +97,8 @@ static void apply_defaults(void) { #endif if (s_actions[HOST_KEYMAP_SAVE_STATE_MENU].count == 0) add_bind(HOST_KEYMAP_SAVE_STATE_MENU, (int)SDLK_F7, (int)SDL_SCANCODE_F7, 0); + if (s_actions[HOST_KEYMAP_SCANLINES].count == 0) + add_bind(HOST_KEYMAP_SCANLINES, (int)SDLK_F6, (int)SDL_SCANCODE_F6, 0); } /* Parse one "Ctrl+Alt+PageUp" token into key+mods. */ @@ -154,6 +156,7 @@ static HostKeymapAction action_for_key(const char *name) { if (ieq(name, "DisplayPerf")) return HOST_KEYMAP_DISPLAY_PERF; if (ieq(name, "Rewind")) return HOST_KEYMAP_REWIND; if (ieq(name, "SaveStateMenu")) return HOST_KEYMAP_SAVE_STATE_MENU; + if (ieq(name, "Scanlines")) return HOST_KEYMAP_SCANLINES; return HOST_KEYMAP_ACTION_COUNT; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 7663ea7e8..47d351734 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1157,6 +1157,27 @@ static bool g_video_aa = true; /* linear present filtering */ /* FMV present reconstruction (VIDEO_FMV_FILTER_*), pushed to the GL renderer * once the config is resolved. Only consulted while g_video_aa is on. */ static int g_video_fmv_filter = PSXRecompV4::VIDEO_FMV_FILTER_DEFAULT; +/* Scanline post-process (host display enhancement). Off by default; toggled by + * the launcher Display card, the PSX_SCANLINES env override, the F6 hotkey, or + * the `scanline` TCP command. Strength 0..1 is the dark-gap depth. Pushed to the + * GL renderer each present alongside the FMV filter. */ +static bool g_video_scanlines = false; +static float g_video_scanline_strength = 0.5f; + +/* Single point that changes scanline state: keeps the g_video_* mirror (used by + * the hotkey and startup banner) in lockstep with the GL renderer, so the F6 + * hotkey, the PSX_SCANLINES env override, and the `scanline` TCP command can be + * mixed without drifting. Declared extern "C" so debug_server.c can call it. */ +extern "C" void psx_video_set_scanlines(int on, float strength) { + g_video_scanlines = on ? true : false; + if (strength >= 0.f && strength <= 1.f) g_video_scanline_strength = strength; + gl_renderer_set_scanlines(g_video_scanlines ? 1 : 0, + g_video_scanline_strength); +} +extern "C" int psx_video_get_scanlines(float *strength) { + if (strength) *strength = g_video_scanline_strength; + return g_video_scanlines ? 1 : 0; +} /* recomp-ui stores this 1-based so a zero-initialized (older) host reads as * "unset" rather than pinning nearest; the config enum is 0-based. Convert at @@ -6589,6 +6610,17 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { (int)mod)) { fps_telemetry_toggle(); } + else if (!key_repeat && + host_keymap_match_event(HOST_KEYMAP_SCANLINES, + (int)key, (int)scancode, + (int)mod)) { + psx_video_set_scanlines(g_video_scanlines ? 0 : 1, + g_video_scanline_strength); + char msg[48]; + std::snprintf(msg, sizeof(msg), "Scanlines %s", + g_video_scanlines ? "on" : "off"); + host_osd_push(msg, 1200); + } /* Host volume: config.ini [KeyMap] VolumeUp/VolumeDown * (defaults: keypad +/-). 5% steps; shows right-side bar. */ else if (host_keymap_match_event(HOST_KEYMAP_VOLUME_UP, @@ -11322,6 +11354,9 @@ int main(int argc, char** argv) { g_video_pgxp_tolerance = (float)gc.runtime.video_pgxp_tolerance; g_video_renderer = gc.runtime.video_renderer; g_video_screen = gc.runtime.video_screen_kind; + g_video_scanlines = gc.runtime.video_scanlines; + g_video_scanline_strength = + (float)gc.runtime.video_scanline_strength; g_video_aspect_num = gc.runtime.video_aspect_num; g_video_aspect_den = gc.runtime.video_aspect_den; g_low_latency_input = gc.runtime.video_low_latency_input ? 1 : 0; @@ -11711,6 +11746,9 @@ int main(int argc, char** argv) { if (us.has_perspective_texturing) g_video_perspective_texturing = us.perspective_texturing ? 1 : 0; if (us.has_screen_kind) g_video_screen = us.screen_kind; + if (us.has_scanlines) g_video_scanlines = us.scanlines; + if (us.has_scanline_strength) + g_video_scanline_strength = (float)us.scanline_strength; if (us.has_auto_skip_fmv) g_auto_skip_fmv = us.auto_skip_fmv ? 1 : 0; /* turbo_loads is deliberately NOT restored from settings.toml. It is a * write-only latch: the launcher stopped drawing a Turbo loads row when @@ -12209,6 +12247,9 @@ int main(int argc, char** argv) { seed.perspective_texturing = (g_video_perspective_texturing != 0); seed.has_perspective_texturing = true; seed.screen_kind = g_video_screen; seed.has_screen_kind = true; + seed.scanlines = g_video_scanlines; seed.has_scanlines = true; + seed.scanline_strength = g_video_scanline_strength; + seed.has_scanline_strength = true; seed.auto_skip_fmv = (g_auto_skip_fmv != 0); seed.has_auto_skip_fmv = skip_fmv_offered; seed.turbo_loads = (g_turbo_loads_enabled != 0); @@ -12394,6 +12435,11 @@ int main(int argc, char** argv) { ls.geometry_correction = seed.geometry_correction ? 1 : 0; ls.perspective_texturing = seed.perspective_texturing ? 1 : 0; ls.screen_kind = seed.screen_kind; +#if defined(RECOMP_LAUNCHER_HAS_SCANLINES) + ls.scanlines = seed.scanlines ? 1 : 0; + ls.scanline_strength_pct = seed.has_scanline_strength + ? (int)(seed.scanline_strength * 100.0 + 0.5) : 50; +#endif ls.frame_interp = seed.frame_interpolation ? 1 : 0; ls.frame_interp_fps = seed.frame_interpolation_fps; ls.spu_hq = seed.spu_hq ? 1 : 0; @@ -12670,6 +12716,13 @@ int main(int argc, char** argv) { seed.fmv_filter = launcher_fmv_filter_to_cfg(ls.fmv_filter); seed.has_fmv_filter = true; seed.screen_kind = ls.screen_kind; seed.has_screen_kind = true; +#if defined(RECOMP_LAUNCHER_HAS_SCANLINES) + seed.scanlines = ls.scanlines != 0; seed.has_scanlines = true; + if (ls.scanline_strength_pct >= 0) { + seed.scanline_strength = ls.scanline_strength_pct / 100.0; + seed.has_scanline_strength = true; + } +#endif seed.frame_interpolation = ls.frame_interp != 0; seed.has_frame_interpolation = true; seed.frame_interpolation_fps = ls.frame_interp_fps; seed.has_frame_interpolation_fps = true; seed.audio_freq = ls.audio_freq; seed.has_audio_freq = true; @@ -12877,6 +12930,11 @@ int main(int argc, char** argv) { g_video_geometry_correction = seed.geometry_correction ? 1 : 0; g_video_perspective_texturing = seed.perspective_texturing ? 1 : 0; g_video_screen = seed.screen_kind; + if (seed.has_scanlines) g_video_scanlines = seed.scanlines; + if (seed.has_scanline_strength) + g_video_scanline_strength = (float)seed.scanline_strength; + gl_renderer_set_scanlines(g_video_scanlines ? 1 : 0, + g_video_scanline_strength); g_auto_skip_fmv = skip_fmv_offered && seed.auto_skip_fmv ? 1 : 0; g_turbo_loads_enabled = turbo_loads_offered && seed.turbo_loads ? 1 : 0; @@ -13242,6 +13300,18 @@ int main(int argc, char** argv) { gpu_texture_correction_set(g_video_perspective_texturing); pgxp_set_cpu_mode(g_video_pgxp_cpu_mode); pgxp_set_tolerance(g_video_pgxp_tolerance); + /* Scanlines: env override wins over config, same as the corrections above, + * so a headless/free-run boot can be captured with the effect armed from the + * first present. PSX_SCANLINES=0/1; PSX_SCANLINE_STRENGTH=0..1. Pushed to the + * GL renderer, which holds the state and applies it per-draw. */ + if (const char* e = std::getenv("PSX_SCANLINES")) + g_video_scanlines = (*e && *e != '0'); + if (const char* e = std::getenv("PSX_SCANLINE_STRENGTH")) { + float s = (float)atof(e); + if (s >= 0.f && s <= 1.f) g_video_scanline_strength = s; + } + gl_renderer_set_scanlines(g_video_scanlines ? 1 : 0, + g_video_scanline_strength); if (g_video_geometry_correction || g_video_perspective_texturing) { std::fprintf(stdout, "psxrecomp: geometry correction %s, perspective texturing %s%s\n", @@ -14354,6 +14424,10 @@ int main(int argc, char** argv) { ls.geometry_correction = g_video_geometry_correction ? 1 : 0; ls.perspective_texturing = g_video_perspective_texturing ? 1 : 0; ls.screen_kind = g_video_screen; +#if defined(RECOMP_LAUNCHER_HAS_SCANLINES) + ls.scanlines = g_video_scanlines ? 1 : 0; + ls.scanline_strength_pct = (int)(g_video_scanline_strength * 100.0f + 0.5f); +#endif ls.frame_interp = g_frame_interpolation ? 1 : 0; ls.frame_interp_fps = g_frame_interpolation_fps; ls.spu_hq = g_audio_spu_hq ? 1 : 0; @@ -14633,6 +14707,14 @@ int main(int argc, char** argv) { us.has_perspective_texturing = true; us.screen_kind = ls.screen_kind; us.has_screen_kind = true; +#if defined(RECOMP_LAUNCHER_HAS_SCANLINES) + us.scanlines = ls.scanlines != 0; + us.has_scanlines = true; + if (ls.scanline_strength_pct >= 0) { + us.scanline_strength = ls.scanline_strength_pct / 100.0; + us.has_scanline_strength = true; + } +#endif us.frame_interpolation = ls.frame_interp != 0; us.has_frame_interpolation = true; us.frame_interpolation_fps = ls.frame_interp_fps; @@ -14688,6 +14770,13 @@ int main(int argc, char** argv) { g_video_geometry_correction = ls.geometry_correction ? 1 : 0; g_video_perspective_texturing = ls.perspective_texturing ? 1 : 0; g_video_screen = ls.screen_kind; +#if defined(RECOMP_LAUNCHER_HAS_SCANLINES) + g_video_scanlines = ls.scanlines != 0; + if (ls.scanline_strength_pct >= 0) + g_video_scanline_strength = ls.scanline_strength_pct / 100.0f; + gl_renderer_set_scanlines(g_video_scanlines ? 1 : 0, + g_video_scanline_strength); +#endif /* Load acceleration and FMV skipping are mod-owned on PSX, and the * launcher struct these come from was snapshotted BEFORE * mod_runtime_activate_plugins() ran. Applying them here would