From df4cc5c70ab1e5d0fd2a8f817dd346905d4d0eeb Mon Sep 17 00:00:00 2001 From: Martin Penkava Date: Sun, 12 Jul 2026 16:53:38 +0200 Subject: [PATCH 1/7] build: MSVC portability for runtime and generated code - psx_cyc.h / psx_cycles.c: _BitScanForward/_BitScanReverse shims for __builtin_ctz/__builtin_clz under _MSC_VER - full_function_emitter.cpp / main_psx.cpp: emit a portable .CRT$XCU init-pointer instead of GCC-only __attribute__((constructor)) in generated dispatch/game code - main.cpp: declare cross-language globals extern "C" so MSVC links them against their C definitions (no-op for GCC/Clang) Co-Authored-By: Claude Fable 5 --- recompiler/src/full_function_emitter.cpp | 10 +++++++++- recompiler/src/main_psx.cpp | 10 +++++++++- runtime/include/psx_cyc.h | 9 +++++++++ runtime/src/main.cpp | 15 +++++++++++++++ runtime/src/psx_cycles.c | 9 +++++++++ 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp index aed4c4fd8..ed10a7134 100644 --- a/recompiler/src/full_function_emitter.cpp +++ b/recompiler/src/full_function_emitter.cpp @@ -1656,9 +1656,17 @@ void FullFunctionEmitter::emit_dispatch( // RECURSION_BUG.md §25 — mark CPS mode at startup for runtime code that // must emit the CPS contract (the overlay sljit JIT, overlay_sljit.c). out += "\n/* CPS runtime-mode marker (overlay sljit JIT reads g_psx_cps_mode). */\n"; - out += "__attribute__((constructor)) static void psx_cps_mark_bios(void) {\n"; + out += "static void psx_cps_mark_bios(void) {\n"; out += " extern int g_psx_cps_mode; g_psx_cps_mode = 1;\n"; out += "}\n"; + // Run psx_cps_mark_bios before main(). __attribute__((constructor)) is + // GCC/Clang-only; MSVC uses a static initializer pointer in .CRT$XCU. + out += "#if defined(_MSC_VER)\n"; + out += "#pragma section(\".CRT$XCU\", read)\n"; + out += "__declspec(allocate(\".CRT$XCU\")) static void (*psx_cps_mark_bios_ctor)(void) = psx_cps_mark_bios;\n"; + out += "#else\n"; + out += "__attribute__((constructor)) static void psx_cps_mark_bios_ctor(void) { psx_cps_mark_bios(); }\n"; + out += "#endif\n"; } } diff --git a/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index f4055aac5..a80f3e0d2 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -1006,9 +1006,17 @@ int main(int argc, char** argv) { // JIT (overlay_sljit.c) emits the CPS contract. Static ctor: no clash // with the BIOS dispatch's marker. ds << "\n/* CPS runtime-mode marker (overlay sljit JIT reads g_psx_cps_mode). */\n"; - ds << "__attribute__((constructor)) static void psx_cps_mark_game(void) {\n"; + ds << "static void psx_cps_mark_game(void) {\n"; ds << " extern int g_psx_cps_mode; g_psx_cps_mode = 1;\n"; ds << "}\n"; + // Run psx_cps_mark_game before main(). __attribute__((constructor)) is + // GCC/Clang-only; MSVC uses a static initializer pointer in .CRT$XCU. + ds << "#if defined(_MSC_VER)\n"; + ds << "#pragma section(\".CRT$XCU\", read)\n"; + ds << "__declspec(allocate(\".CRT$XCU\")) static void (*psx_cps_mark_game_ctor)(void) = psx_cps_mark_game;\n"; + ds << "#else\n"; + ds << "__attribute__((constructor)) static void psx_cps_mark_game_ctor(void) { psx_cps_mark_game(); }\n"; + ds << "#endif\n"; } std::ofstream dispatch_file(dispatch_filename); diff --git a/runtime/include/psx_cyc.h b/runtime/include/psx_cyc.h index 0166d5513..ce093ed5a 100644 --- a/runtime/include/psx_cyc.h +++ b/runtime/include/psx_cyc.h @@ -28,6 +28,9 @@ #define PSX_CYC_H #include +#if defined(_MSC_VER) +#include /* MSVC intrinsics: _BitScanForward (no __builtin_ctz) */ +#endif #include "cpu_state.h" /* CPUState (guard-safe: cpu_state.h includes us last) */ #ifdef __cplusplus @@ -49,7 +52,13 @@ static inline void psx_cyc_base(CPUState* cpu) { static inline void psx_cyc_deps(CPUState* cpu, uint32_t reg_mask) { reg_mask &= 0xFFFFFFFEu; /* never touch ReadAbsorb[0] */ while (reg_mask) { +#if defined(_MSC_VER) + unsigned long _psx_ctz_idx; + _BitScanForward(&_psx_ctz_idx, reg_mask); + unsigned n = (unsigned)_psx_ctz_idx; +#else unsigned n = (unsigned)__builtin_ctz(reg_mask); +#endif cpu->read_absorb[n] = 0u; reg_mask &= reg_mask - 1u; } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index d7e710867..6c2d0659b 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -93,6 +93,21 @@ extern "C" uint64_t gte_get_exec_count(void); +/* Cross-language globals defined in C translation units. Declared extern "C" at + * file scope so MSVC gives them C linkage (matching the C definitions); without + * this MSVC name-mangles the C++ references and they fail to link. GCC/Clang do + * not mangle namespace-scope variables, so this is a no-op there. The existing + * block-scope `extern` redeclarations inside functions inherit this C linkage. */ +extern "C" { + extern uint64_t psx_cycle_count; + extern uint64_t s_frame_count; + extern uint32_t g_overlay_region_floor; + extern int g_psx_cps_mode; + extern uint64_t g_slice_fired, g_slice_irq_taken, g_dirty_ram_insns_run; + extern uint32_t g_slice_exit_pc, g_slice_exit_reason, g_slice_exit_iter; + extern uint32_t g_slice_exit_dispatchable, g_slice_exit_dirty, g_slice_exit_in_text, g_slice_exit_want; +} + /* memory.c */ extern "C" void memory_init(const char* bios_path); extern "C" void memory_set_sr_ptr(const uint32_t *p); diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index 1d92a7e5e..1610d522c 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -3,6 +3,9 @@ #include "psx_cycles.h" #include "cpu_state.h" #include +#if defined(_MSC_VER) +#include /* MSVC intrinsics: _BitScanReverse (no __builtin_clz) */ +#endif #include "cdrom.h" #include "dma.h" #include "interrupts.h" @@ -451,7 +454,13 @@ static const uint8_t PSX_MULT_TAB24[24] = { static inline uint32_t psx_clz32(uint32_t v) { /* v is never 0 here (callers OR in 0x400). */ +#if defined(_MSC_VER) + unsigned long _idx; + _BitScanReverse(&_idx, v); /* index of highest set bit */ + return (uint32_t)(31u - _idx); +#else return (uint32_t)__builtin_clz(v); +#endif } uint32_t psx_mult_latency_s(uint32_t rs) { /* MULT (signed): sign-fold magnitude */ From e6809ccf7d778a4c2f32d9e27c0ec31a44cbd2ba Mon Sep 17 00:00:00 2001 From: Martin Penkava Date: Sun, 12 Jul 2026 16:54:02 +0200 Subject: [PATCH 2/7] runtime: fix boot freeze from overlay-loader init race + discovery gap Two deterministic boot hangs (WWF SmackDown! 2, SLUS-01234): 1. overlay_loader_dispatch()/is_candidate()/call_native() ran during early BIOS kernel init (0xBFC0DB10 memcard polling loop) before overlay_loader_init() built the range-index structures, walking uninitialized lists until a PC=0 trap (froze at cycle 208195 every boot). Guard all three entry points on s_active. 2. Code at 0x800D1F28 (boot-text populated by bulk host transfer that bypassed the RAM write-marking hooks) was neither compiled, dirty, nor overlay -> unknown-dispatch fail at frame 1340. Add a fallback: an address above overlay_region_floor whose word decodes as valid MIPS is marked executable and handed to the dirty-RAM interpreter. Validated 5/5 reproducible boots, 2000+ frame sustained runs. Co-Authored-By: Claude Fable 5 --- runtime/src/dirty_ram_interp.c | 15 ++++++++++++++- runtime/src/overlay_loader.c | 5 ++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index b52072bfb..bfebac42e 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -279,6 +279,7 @@ extern void psx_dispatch_call(CPUState* cpu, uint32_t addr, uint32_t return_addr /* Forward decls from memory.c — used to read instruction bytes. */ extern uint8_t *memory_get_ram_ptr(void); +extern void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len); /* MIPS instruction field decoders. */ static inline uint32_t op_field (uint32_t i) { return (i >> 26) & 0x3Fu; } @@ -2210,7 +2211,19 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ } #define OV_FPLOG_RET1() do { if (_ovfp) overlay_fp_log(addr, _in_regs, cpu, 0); return 1; } while (0) - if (!dirty_ram_is_dirty(phys) && !clean_game_text_miss) return 0; + if (!dirty_ram_is_dirty(phys) && !clean_game_text_miss) { + /* Some games populate post-EXE overlays through bulk host transfers that + * do not pass through the normal RAM write hooks. A real control transfer + * to decodable code above the configured boot-EXE text end is sufficient + * evidence that the page is executable; mark it so local overlay flow can + * remain in the interpreter. Invalid/data targets still fail dispatch. */ + if (phys < (2u * 1024u * 1024u) && phys >= g_overlay_region_floor && + dirty_ram_word_looks_decodable(fetch_word(phys))) { + dirty_ram_mark_executable_range(phys, 4u); + } else { + return 0; + } + } /* Interp-pressure signal for variant-capture automation (step 2.8): * counts dispatches the interpreter actually handles inside a capture diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 4c674bae5..485049c66 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -2298,6 +2298,7 @@ static int overlay_find_by_range(uint32_t phys) { int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) { uint32_t phys = addr & 0x1FFFFFFFu; + if (!s_active) return 0; int lazy_loaded = 0; retry_candidates: int head = idx_head(phys); @@ -2791,6 +2792,7 @@ static FpEnt s_fp[FP_CAP]; static uint64_t s_fp_seq = 0; int overlay_loader_is_candidate(uint32_t phys) { + if (!s_active) return 0; phys &= 0x1FFFFFFFu; return idx_head(phys) >= 0 || lazy_has_exact_entry(phys); } @@ -3137,7 +3139,8 @@ void overlay_fp_log(uint32_t addr, const uint32_t *in_regs, * leaks (root cause of the dwarf->overworld native blue screen). * Returns 1 iff a native candidate ran. */ int overlay_loader_call_native(CPUState *cpu, uint32_t addr) { - if (!s_native_exec) return 0; /* interp mode: keep the legacy inline path */ + if (!s_active || !s_native_exec) + return 0; /* inactive/interp mode: keep the legacy inline path */ uint32_t phys = addr & 0x1FFFFFFFu; if (idx_head(phys) < 0 && !lazy_has_exact_entry(phys)) return 0; /* neither a registered nor an exact cached entry */ From e069c8a531b14c2ec4566d6a3dd5b1816a5600e6 Mon Sep 17 00:00:00 2001 From: Martin Penkava Date: Sun, 12 Jul 2026 16:54:02 +0200 Subject: [PATCH 3/7] spu: hardware Gaussian interpolation, reverb, HQ shadow gating - Replace nearest-sample voice pitch resampling with the PS1 hardware 4-tap Gaussian (No$ table, new spu_gauss.h; formula (g[0xFF-i]*s[-3] + g[0x1FF-i]*s[-2] + g[0x100+i]*s[-1] + g[i]*s[0])>>15 with i=(phase>>4)&0xFF). Nearest-sample put a flat aliasing shelf at ~-31 dB across 8-22 kHz (measured); Gaussian drops it 18 dB and restores natural spectral decay. Adds prev[3] cross-block history. - Beetle-model reverb engine on the SPU mix. - Gate the float-shadow substitution off while any voice has a reverb send (the shadow models only the dry mix). Co-Authored-By: Claude Fable 5 --- runtime/include/spu_gauss.h | 75 ++++++++++++++ runtime/src/spu.c | 198 +++++++++++++++++++++++++++++++++++- 2 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 runtime/include/spu_gauss.h diff --git a/runtime/include/spu_gauss.h b/runtime/include/spu_gauss.h new file mode 100644 index 000000000..4e1807991 --- /dev/null +++ b/runtime/include/spu_gauss.h @@ -0,0 +1,75 @@ +/* spu_gauss.h - PS1 SPU hardware Gaussian interpolation table (512 entries). + * Values from No$PSX docs / DuckStation core/spu.cpp (SPU::Voice::Interpolate). + * Usage, with i = (phase >> 4) & 0xFF and s[0]=current, s[-1..-3]=history: + * out = (g[0x0FF-i]*s[-3] + g[0x1FF-i]*s[-2] + g[0x100+i]*s[-1] + g[i]*s[0]) >> 15 + */ +#ifndef PSX_SPU_GAUSS_H +#define PSX_SPU_GAUSS_H +#include +static const int16_t spu_gauss_table[512] = { + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 2, 2, 2, 3, 3, + 3, 4, 4, 5, 5, 6, 7, 7, + 8, 9, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 21, 22, 24, + 25, 27, 28, 30, 32, 33, 35, 37, + 39, 41, 44, 46, 48, 51, 53, 56, + 58, 61, 64, 67, 70, 73, 77, 80, + 84, 87, 91, 95, 99, 103, 107, 111, + 116, 120, 125, 130, 135, 140, 145, 150, + 156, 161, 167, 173, 179, 186, 192, 199, + 205, 212, 219, 227, 234, 242, 250, 257, + 266, 274, 283, 291, 300, 309, 319, 328, + 338, 348, 358, 369, 379, 390, 401, 412, + 424, 436, 448, 460, 473, 485, 498, 512, + 525, 539, 553, 567, 582, 597, 612, 627, + 643, 659, 675, 692, 708, 726, 743, 761, + 779, 797, 816, 835, 854, 874, 894, 914, + 935, 956, 977, 999, 1020, 1043, 1066, 1089, + 1112, 1136, 1160, 1184, 1209, 1234, 1260, 1286, + 1312, 1339, 1366, 1394, 1422, 1450, 1479, 1508, + 1537, 1567, 1598, 1628, 1660, 1691, 1723, 1756, + 1789, 1822, 1856, 1890, 1924, 1959, 1995, 2031, + 2067, 2104, 2141, 2179, 2217, 2256, 2295, 2334, + 2374, 2415, 2456, 2497, 2539, 2582, 2624, 2668, + 2712, 2756, 2801, 2846, 2892, 2938, 2985, 3032, + 3079, 3128, 3176, 3225, 3275, 3325, 3376, 3427, + 3479, 3531, 3584, 3637, 3691, 3745, 3799, 3855, + 3910, 3967, 4023, 4081, 4138, 4197, 4255, 4315, + 4374, 4435, 4495, 4557, 4619, 4681, 4744, 4807, + 4871, 4935, 5000, 5065, 5131, 5197, 5264, 5332, + 5399, 5468, 5536, 5606, 5676, 5746, 5817, 5888, + 5959, 6032, 6104, 6177, 6251, 6325, 6400, 6475, + 6550, 6626, 6702, 6779, 6856, 6934, 7012, 7091, + 7170, 7249, 7329, 7409, 7490, 7571, 7653, 7735, + 7817, 7900, 7983, 8066, 8150, 8234, 8319, 8404, + 8489, 8575, 8661, 8748, 8834, 8922, 9009, 9097, + 9185, 9273, 9362, 9451, 9541, 9630, 9720, 9811, + 9901, 9992, 10083, 10174, 10266, 10358, 10450, 10542, + 10635, 10727, 10820, 10913, 11007, 11100, 11194, 11288, + 11382, 11476, 11571, 11665, 11760, 11855, 11950, 12045, + 12140, 12236, 12331, 12427, 12522, 12618, 12714, 12809, + 12905, 13001, 13097, 13193, 13289, 13385, 13481, 13577, + 13673, 13769, 13865, 13961, 14056, 14152, 14248, 14343, + 14439, 14534, 14630, 14725, 14820, 14915, 15010, 15104, + 15199, 15293, 15387, 15481, 15575, 15669, 15762, 15855, + 15948, 16041, 16133, 16226, 16317, 16409, 16500, 16592, + 16682, 16773, 16863, 16953, 17042, 17131, 17220, 17308, + 17396, 17484, 17571, 17658, 17744, 17830, 17916, 18001, + 18086, 18170, 18254, 18337, 18420, 18502, 18584, 18665, + 18746, 18826, 18905, 18985, 19063, 19141, 19219, 19295, + 19372, 19447, 19522, 19597, 19671, 19744, 19816, 19888, + 19959, 20030, 20100, 20169, 20238, 20306, 20373, 20439, + 20505, 20570, 20634, 20698, 20760, 20822, 20884, 20944, + 21004, 21063, 21121, 21178, 21235, 21290, 21345, 21399, + 21452, 21505, 21556, 21607, 21657, 21706, 21754, 21801, + 21848, 21893, 21938, 21982, 22025, 22066, 22107, 22148, + 22187, 22225, 22262, 22299, 22334, 22369, 22402, 22435, + 22467, 22498, 22527, 22556, 22584, 22611, 22637, 22662, + 22686, 22709, 22731, 22752, 22772, 22791, 22809, 22826, + 22842, 22857, 22872, 22885, 22897, 22908, 22918, 22927, + 22935, 22942, 22948, 22953, 22957, 22960, 22962, 22963, +}; +#endif /* PSX_SPU_GAUSS_H */ diff --git a/runtime/src/spu.c b/runtime/src/spu.c index 6dd8af662..202c3e87f 100644 --- a/runtime/src/spu.c +++ b/runtime/src/spu.c @@ -4,10 +4,11 @@ * This is intentionally still a compact hardware model: it accepts SPU * register reads/writes, DMA4 transfers into 512KB SPU RAM, mixes the * 24 direct ADPCM voices, and accepts decoded CD/XA audio on the SPU CD - * input bus. Reverb, noise, sweep volumes, and IRQ timing are not modeled yet. + * input bus. Noise, sweep volumes, and IRQ timing are not modeled yet. */ #include "spu.h" +#include "spu_gauss.h" #include "spu_shadow.h" #include "audio_trace.h" #include "psx_cycles.h" @@ -39,6 +40,137 @@ static uint32_t endx_latch; static uint32_t kon_latch; static uint32_t koff_latch; +static inline int16_t clamp16(int32_t v); +static inline uint32_t reg_index(uint32_t addr); + +/* PS1 reverb engine. Register indices 0xE0..0xFF correspond to + * 0x1F801DC0..0x1F801DFF in spu_regs. The implementation follows Beetle's + * integer work-area and 44.1<->22.05 kHz resampling model. */ +static uint32_t reverb_wa; +static uint32_t reverb_cur; +static int16_t reverb_down[2][128]; +static int16_t reverb_up[2][64]; +static uint32_t reverb_res_pos; + +enum { + RV_FB_SRC_A, RV_FB_SRC_B, RV_IIR_ALPHA, RV_ACC_COEF_A, + RV_ACC_COEF_B, RV_ACC_COEF_C, RV_ACC_COEF_D, RV_IIR_COEF, + RV_FB_ALPHA, RV_FB_X, RV_IIR_DEST_A0, RV_IIR_DEST_A1, + RV_ACC_SRC_A0, RV_ACC_SRC_A1, RV_ACC_SRC_B0, RV_ACC_SRC_B1, + RV_IIR_SRC_A0, RV_IIR_SRC_A1, RV_IIR_DEST_B0, RV_IIR_DEST_B1, + RV_ACC_SRC_C0, RV_ACC_SRC_C1, RV_ACC_SRC_D0, RV_ACC_SRC_D1, + RV_IIR_SRC_B0, RV_IIR_SRC_B1, RV_MIX_DEST_A0, RV_MIX_DEST_A1, + RV_MIX_DEST_B0, RV_MIX_DEST_B1, RV_IN_COEF_L, RV_IN_COEF_R +}; + +static inline uint16_t rv_u(int r) { return spu_regs[0xE0u + (uint32_t)r]; } +static inline int16_t rv_s(int r) { return (int16_t)rv_u(r); } + +static inline int16_t reverb_sat(int32_t v) { return clamp16(v); } + +static uint32_t reverb_offset(uint32_t offset) { + uint32_t out = reverb_cur + (offset & 0x3FFFFu); + if (out & 0x40000u) out += reverb_wa; + return out & 0x3FFFFu; +} + +static int16_t reverb_ram_read(uint16_t raw, int32_t extra) { + uint32_t word = reverb_offset(((uint32_t)raw << 2) + (uint32_t)extra); + uint32_t byte = word << 1; + return (int16_t)((uint16_t)spu_ram[byte] | + ((uint16_t)spu_ram[byte + 1u] << 8)); +} + +static void reverb_ram_write(uint16_t raw, int16_t sample) { + uint16_t ctrl = spu_regs[reg_index(0x1F801DAAu)]; + if (!(ctrl & 0x0080u)) return; + uint32_t byte = reverb_offset((uint32_t)raw << 2) << 1; + spu_ram[byte] = (uint8_t)sample; + spu_ram[byte + 1u] = (uint8_t)((uint16_t)sample >> 8); +} + +static int32_t reverb_iiasm(int16_t alpha, int16_t sample) { + if (alpha == (int16_t)0x8000) + return sample == (int16_t)0x8000 ? 0 : (int32_t)sample * -65536; + return (int32_t)sample * (32768 - alpha); +} + +static const int16_t reverb_resamp[20] = { + -1, 2, -10, 35, -103, 266, -616, 1332, -2960, 10246, + 10246, -2960, 1332, -616, 266, -103, 35, -10, 2, -1 +}; + +static int32_t reverb_4422(const int16_t *src) { + int32_t out = 0; + for (int i = 0; i < 20; i++) out += reverb_resamp[i] * src[i * 2]; + out += 0x4000 * src[19]; + return reverb_sat(out >> 15); +} + +static int32_t reverb_2244(const int16_t *src) { + int32_t out = 0; + for (int i = 0; i < 20; i++) out += reverb_resamp[i] * src[i]; + return reverb_sat(out >> 14); +} + +static int16_t reverb_neg(int16_t v) { + return v == (int16_t)0x8000 ? 0x7FFF : (int16_t)-v; +} + +static void reverb_run_channel(unsigned lr, int32_t input) { + int a = (int)lr; + int b = a ^ 1; + int16_t in_coef = rv_s(lr ? RV_IN_COEF_R : RV_IN_COEF_L); + int16_t iir_coef = rv_s(RV_IIR_COEF); + int16_t alpha = rv_s(RV_IIR_ALPHA); + int16_t iir_in_a = reverb_sat((((reverb_ram_read(rv_u(RV_IIR_SRC_A0 + a), 0) * iir_coef) >> 14) + + ((input * in_coef) >> 14)) >> 1); + int16_t iir_in_b = reverb_sat((((reverb_ram_read(rv_u(RV_IIR_SRC_B0 + b), 0) * iir_coef) >> 14) + + ((input * in_coef) >> 14)) >> 1); + int16_t iir_a = reverb_sat((((iir_in_a * alpha) >> 14) + + (reverb_iiasm(alpha, reverb_ram_read(rv_u(RV_IIR_DEST_A0 + a), -1)) >> 14)) >> 1); + int16_t iir_b = reverb_sat((((iir_in_b * alpha) >> 14) + + (reverb_iiasm(alpha, reverb_ram_read(rv_u(RV_IIR_DEST_B0 + a), -1)) >> 14)) >> 1); + reverb_ram_write(rv_u(RV_IIR_DEST_A0 + a), iir_a); + reverb_ram_write(rv_u(RV_IIR_DEST_B0 + a), iir_b); + + int32_t acc = ((reverb_ram_read(rv_u(RV_ACC_SRC_A0 + a), 0) * rv_s(RV_ACC_COEF_A)) >> 14) + + ((reverb_ram_read(rv_u(RV_ACC_SRC_B0 + a), 0) * rv_s(RV_ACC_COEF_B)) >> 14) + + ((reverb_ram_read(rv_u(RV_ACC_SRC_C0 + a), 0) * rv_s(RV_ACC_COEF_C)) >> 14) + + ((reverb_ram_read(rv_u(RV_ACC_SRC_D0 + a), 0) * rv_s(RV_ACC_COEF_D)) >> 14); + int16_t fb_a = reverb_ram_read((uint16_t)(rv_u(RV_MIX_DEST_A0 + a) - rv_u(RV_FB_SRC_A)), 0); + int16_t fb_b = reverb_ram_read((uint16_t)(rv_u(RV_MIX_DEST_B0 + a) - rv_u(RV_FB_SRC_B)), 0); + int16_t fb_alpha = rv_s(RV_FB_ALPHA); + int16_t fb_x = rv_s(RV_FB_X); + int16_t mda = reverb_sat((acc + ((fb_a * reverb_neg(fb_alpha)) >> 14)) >> 1); + int16_t mdb = reverb_sat(fb_a + ((((mda * fb_alpha) >> 14) + + ((fb_b * reverb_neg(fb_x)) >> 14)) >> 1)); + int16_t ivb = reverb_sat(fb_b + ((mdb * fb_x) >> 15)); + reverb_ram_write(rv_u(RV_MIX_DEST_A0 + a), mda); + reverb_ram_write(rv_u(RV_MIX_DEST_B0 + a), mdb); + reverb_up[a][(reverb_res_pos >> 1) | 0x20u] = ivb; + reverb_up[a][reverb_res_pos >> 1] = ivb; +} + +static void reverb_run(const int32_t in[2], int32_t out[2]) { + for (int lr = 0; lr < 2; lr++) { + reverb_down[lr][reverb_res_pos] = (int16_t)in[lr]; + reverb_down[lr][reverb_res_pos | 0x40u] = (int16_t)in[lr]; + } + if (reverb_res_pos & 1u) { + reverb_run_channel(0, reverb_4422(&reverb_down[0][(reverb_res_pos - 38u) & 0x3Fu])); + } else { + reverb_run_channel(1, reverb_4422(&reverb_down[1][(reverb_res_pos - 38u) & 0x3Fu])); + reverb_cur = (reverb_cur + 1u) & 0x3FFFFu; + if (!reverb_cur) reverb_cur = reverb_wa; + } + const int16_t *l = &reverb_up[0][((reverb_res_pos >> 1) - 19u) & 0x1Fu]; + const int16_t *r = &reverb_up[1][((reverb_res_pos >> 1) - 19u) & 0x1Fu]; + if (reverb_res_pos & 1u) { out[0] = reverb_2244(l); out[1] = r[9]; } + else { out[0] = l[9]; out[1] = reverb_2244(r); } + reverb_res_pos = (reverb_res_pos + 1u) & 0x3Fu; +} + /* External vblank counter (debug_server.c) used as event timestamp. */ extern uint64_t s_frame_count; @@ -72,6 +204,8 @@ typedef struct { uint32_t cur_addr; uint32_t repeat_addr; int16_t samples[SPU_BLOCK_SAMPLES]; + int16_t prev[3]; /* last 3 decoded samples of the previous block: + prev[0]=s[-3] prev[1]=s[-2] prev[2]=s[-1] */ int sample_idx; uint32_t phase; int16_t hist1; @@ -312,6 +446,12 @@ static void decode_block(SpuVoice *v) { uint32_t addr = v->cur_addr & (SPU_RAM_SIZE - 1u); if (addr + 16u > SPU_RAM_SIZE) addr = 0; + /* Carry the tail of the outgoing block so Gaussian interpolation has + * continuous history across the block boundary (hw keeps 3 samples). */ + v->prev[0] = v->samples[SPU_BLOCK_SAMPLES - 3]; + v->prev[1] = v->samples[SPU_BLOCK_SAMPLES - 2]; + v->prev[2] = v->samples[SPU_BLOCK_SAMPLES - 1]; + uint8_t header = spu_ram[addr + 0u]; uint8_t flags = spu_ram[addr + 1u]; int shift = header & 0x0F; @@ -445,7 +585,24 @@ static int16_t voice_next_sample(int idx) { decode_block(v); } - int16_t raw_s = v->samples[v->sample_idx]; + /* Hardware 4-tap Gaussian interpolation (No$ formula, spu_gauss.h). + * s[-3..-1] come from prev[] when the tap window crosses the block + * boundary backwards. Replaces the old nearest-sample pick, whose + * aliasing images put a flat noise shelf across 8-22 kHz. */ + int gi = (int)((v->phase >> 4) & 0xFFu); + int si = v->sample_idx; + int32_t acc = 0; + static const int gtap[4] = { 3, 2, 1, 0 }; + for (int k = 0; k < 4; k++) { + int off = si - gtap[k]; + int16_t sv = (off >= 0) ? v->samples[off] : v->prev[3 + off]; + int gidx = (k == 0) ? (0x0FF - gi) + : (k == 1) ? (0x1FF - gi) + : (k == 2) ? (0x100 + gi) + : gi; + acc += (int32_t)spu_gauss_table[gidx] * (int32_t)sv; + } + int16_t raw_s = (int16_t)(acc >> 15); /* Apply envelope (0..0x7FFF as a 15-bit gain). */ int32_t shaped = ((int32_t)raw_s * (int32_t)v->env_level) >> 15; if (shaped > 32767) shaped = 32767; @@ -540,6 +697,11 @@ void spu_init(void) { endx_latch = 0; kon_latch = 0; koff_latch = 0; + reverb_wa = 0; + reverb_cur = 0; + reverb_res_pos = 0; + memset(reverb_down, 0, sizeof(reverb_down)); + memset(reverb_up, 0, sizeof(reverb_up)); s_event_idx = 0; s_event_seq = 0; spu_cd_audio_reset(); @@ -557,11 +719,15 @@ void spu_render(int16_t* out_stereo, int frames) { int16_t main_r = direct_volume(spu_regs[reg_index(0x1F801D82u)]); int16_t cd_vol_l = cd_input_volume(spu_regs[reg_index(0x1F801DB0u)]); int16_t cd_vol_r = cd_input_volume(spu_regs[reg_index(0x1F801DB2u)]); + uint32_t reverb_mode = (uint32_t)spu_regs[reg_index(0x1F801D98u)] | + ((uint32_t)spu_regs[reg_index(0x1F801D9Au)] << 16); /* Shadow tap: arm recording for this block if the float SPU shadow is on. * Off by default => s_shadow_tap_on stays 0 and the mix loop is unchanged * and byte-identical to upstream. */ - s_shadow_tap_on = spu_shadow_enabled() ? 1 : 0; + /* The enhancement shadow currently models only the dry voice mix. Never + * let it replace the canonical output while any voice has a reverb send. */ + s_shadow_tap_on = (spu_shadow_enabled() && reverb_mode == 0u) ? 1 : 0; s_shadow_tap_frame = 0; if (s_shadow_tap_on) { int cap = frames < SPU_SHADOW_TAP_FRAMES ? frames : SPU_SHADOW_TAP_FRAMES; @@ -572,6 +738,8 @@ void spu_render(int16_t* out_stereo, int frames) { for (int f = 0; f < frames; f++) { int32_t mix_l = 0; int32_t mix_r = 0; + int32_t reverb_in[2] = {0, 0}; + int32_t reverb_out[2] = {0, 0}; if (enabled) { for (int v = 0; v < SPU_VOICE_COUNT; v++) { @@ -587,6 +755,10 @@ void spu_render(int16_t* out_stereo, int frames) { if (!s) continue; mix_l += ((int32_t)s * vl) >> 14; mix_r += ((int32_t)s * vr) >> 14; + if (reverb_mode & (1u << v)) { + reverb_in[0] += ((int32_t)s * vl) >> 14; + reverb_in[1] += ((int32_t)s * vr) >> 14; + } } { /* T0 tap: voice sum only (pre CD mix, pre main volume) so the * final mix can be decomposed source-by-source offline. */ @@ -599,12 +771,23 @@ void spu_render(int16_t* out_stereo, int frames) { int16_t cd_l = 0; int16_t cd_r = 0; if (cd_audio_pop(&cd_l, &cd_r)) { - mix_l += ((int32_t)cd_l * cd_vol_l) >> 15; - mix_r += ((int32_t)cd_r * cd_vol_r) >> 15; + int32_t cd_mix_l = ((int32_t)cd_l * cd_vol_l) >> 15; + int32_t cd_mix_r = ((int32_t)cd_r * cd_vol_r) >> 15; + mix_l += cd_mix_l; + mix_r += cd_mix_r; + if (ctrl & 0x0004u) { + reverb_in[0] += cd_mix_l; + reverb_in[1] += cd_mix_r; + } } else if (cd_push_frames != 0) { cd_underflow_frames++; } } + reverb_in[0] = reverb_sat(reverb_in[0]); + reverb_in[1] = reverb_sat(reverb_in[1]); + reverb_run(reverb_in, reverb_out); + mix_l += (reverb_out[0] * (int16_t)spu_regs[reg_index(0x1F801D84u)]) >> 15; + mix_r += (reverb_out[1] * (int16_t)spu_regs[reg_index(0x1F801D86u)]) >> 15; mix_l = (mix_l * main_l) >> 14; mix_r = (mix_r * main_r) >> 14; } @@ -746,6 +929,11 @@ void spu_write(uint32_t addr, uint32_t value) { if (transfer_addr >= SPU_RAM_SIZE) transfer_addr = 0; } + if (addr == 0x1F801DA2u) { + reverb_wa = ((uint32_t)(uint16_t)value << 2) & 0x3FFFFu; + reverb_cur = reverb_wa; + } + if (addr == 0x1F801DA8u) { if (transfer_addr + 1 < SPU_RAM_SIZE) { spu_ram[transfer_addr] = (uint8_t)(value & 0xFF); From cfc5d0eab94c7ddfd30ed23c4d3beecdc737c835 Mon Sep 17 00:00:00 2001 From: Martin Penkava Date: Sun, 12 Jul 2026 16:54:31 +0200 Subject: [PATCH 4/7] gpu: implement hardware primitive size-reject (1023x511 rule) The PS1 GPU does not render a polygon or line when the distance between any two vertices exceeds 1023 horizontally or 511 vertically (No$ docs; Beetle/DuckStation enforce it per rendered triangle). Games rely on the cull during zoom transitions and close camera cuts: their 11-bit-wrapped vertices span the whole coordinate space and, without the reject, get rasterized as giant flat-color triangles covering the frame (SmackDown 2 rendered its menu star wipe and in-match crowd cuts as a full black cover in both the software and GL backends). Applied per-triangle in all polygon execs (quads reject each half independently; textured variants still latch the texpage word first, matching hardware), plus lines and both polyline continuations. Co-Authored-By: Claude Fable 5 --- runtime/src/gpu.c | 121 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 31 deletions(-) diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index f9bcacaca..803a01f3d 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -2041,6 +2041,37 @@ static void parse_vertex(uint32_t word, int32_t* x, int32_t* y) { *y = sign_extend((word >> 16) & 0x7FFu, 11); } +/* Hardware primitive size reject (No$ GPU docs; Beetle/DuckStation mirror it + * per rendered triangle): a polygon/line is NOT rendered when the distance + * between any two of its vertices exceeds 1023 horizontally or 511 + * vertically. Games rely on this cull for zoom transitions and close camera + * cuts — their 11-bit-wrapped vertices otherwise span the whole coordinate + * space and splat giant flat-color triangles across the frame (SmackDown 2 + * menu star wipe / in-match crowd cuts rendered as a full black cover). + * Checked on the parsed pre-widescreen, pre-draw-offset coordinates: offsets + * don't change vertex distances and the ws hacks may legitimately widen. */ +static int tri_oversize(const int32_t* vx, const int32_t* vy, + int a, int b, int c) { + int32_t minx = vx[a], maxx = vx[a]; + if (vx[b] < minx) minx = vx[b]; + if (vx[b] > maxx) maxx = vx[b]; + if (vx[c] < minx) minx = vx[c]; + if (vx[c] > maxx) maxx = vx[c]; + if (maxx - minx > 1023) return 1; + int32_t miny = vy[a], maxy = vy[a]; + if (vy[b] < miny) miny = vy[b]; + if (vy[b] > maxy) maxy = vy[b]; + if (vy[c] < miny) miny = vy[c]; + if (vy[c] > maxy) maxy = vy[c]; + return maxy - miny > 511; +} + +static int line_oversize(int32_t x0, int32_t y0, int32_t x1, int32_t y1) { + int32_t dx = x0 > x1 ? x0 - x1 : x1 - x0; + int32_t dy = y0 > y1 ? y0 - y1 : y1 - y0; + return dx > 1023 || dy > 511; +} + /* Write a single pixel to VRAM with draw area clipping and mask bit handling */ static void raster_pixel(int32_t x, int32_t y, uint16_t color) { if (x < (int32_t)draw_area_left || x > (int32_t)draw_area_right) return; @@ -2142,6 +2173,7 @@ static void gp0_exec_mono_tri(void) { for (int i = 0; i < 3; i++) { parse_vertex(gp0_cmd_buf[1 + i], &vx[i], &vy[i]); } + if (tri_oversize(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { vx[i] += draw_offset_x; @@ -2158,6 +2190,9 @@ static void gp0_exec_mono_quad(void) { int32_t vx[4], vy[4]; for (int i = 0; i < 4; i++) parse_vertex(gp0_cmd_buf[1 + i], &vx[i], &vy[i]); + int rej_a = tri_oversize(vx, vy, 0, 1, 2); + int rej_b = tri_oversize(vx, vy, 2, 1, 3); + if (rej_a && rej_b) return; /* Full-screen filters are commonly encoded as an axis-aligned quad. Drawing * a semi-transparent quad as two independent triangles blends their shared @@ -2187,8 +2222,10 @@ static void gp0_exec_mono_quad(void) { vy[i] += draw_offset_y; } gr_set_semi_transparency(semi_trans, (int)semi_transparency); - gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); - gr_draw_flat_triangle(vx[2], vy[2], vx[1], vy[1], vx[3], vy[3], color); + if (!rej_a) + gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); + if (!rej_b) + gr_draw_flat_triangle(vx[2], vy[2], vx[1], vy[1], vx[3], vy[3], color); } /* Execute shaded triangle (GP0 0x30-0x33) — Gouraud shaded */ @@ -2201,6 +2238,7 @@ static void gp0_exec_shaded_tri(void) { c[i] = rgb888_to_rgb555(gp0_cmd_buf[i * 2] & 0xFFFFFFu); parse_vertex(gp0_cmd_buf[1 + i * 2], &vx[i], &vy[i]); } + if (tri_oversize(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { vx[i] += draw_offset_x; @@ -2229,6 +2267,9 @@ static void gp0_exec_shaded_quad(void) { c[i] = rgb888_to_rgb555(gp0_cmd_buf[i * 2] & 0xFFFFFFu); parse_vertex(gp0_cmd_buf[1 + i * 2], &vx[i], &vy[i]); } + int rej_a = tri_oversize(vx, vy, 0, 1, 2); + int rej_b = tri_oversize(vx, vy, 2, 1, 3); + if (rej_a && rej_b) return; ws_nw_backdrop_stretch_quad(vx, vy); /* full-frame 2D backdrop stretch (sky gradient; no-op else) */ ws_nw_hud_shift_vertices(vx, 4); for (int i = 0; i < 4; i++) { @@ -2244,12 +2285,14 @@ static void gp0_exec_shaded_quad(void) { } } gr_set_semi_transparency(semi_trans, (int)semi_transparency); - gr_draw_gouraud_triangle(vx[0], vy[0], c[0], - vx[1], vy[1], c[1], - vx[2], vy[2], c[2]); - gr_draw_gouraud_triangle(vx[2], vy[2], c[2], - vx[1], vy[1], c[1], - vx[3], vy[3], c[3]); + if (!rej_a) + gr_draw_gouraud_triangle(vx[0], vy[0], c[0], + vx[1], vy[1], c[1], + vx[2], vy[2], c[2]); + if (!rej_b) + gr_draw_gouraud_triangle(vx[2], vy[2], c[2], + vx[1], vy[1], c[1], + vx[3], vy[3], c[3]); } /* Helper: build texpage word from GPU state for SW renderer. @@ -2305,7 +2348,8 @@ static void gp0_exec_textured_tri(void) { /* Texpage from word 4 bits 16-31 */ uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[4] >> 16); uint16_t tpage = tpage_word & 0x1FF; - set_tpage_from_poly(tpage_word); + set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ + if (tri_oversize(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { @@ -2341,7 +2385,10 @@ static void gp0_exec_textured_quad(void) { uint16_t clut_y = (clut >> 6) & 0x1FF; uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[4] >> 16); uint16_t tpage = tpage_word & 0x1FF; - set_tpage_from_poly(tpage_word); + set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ + int rej_a = tri_oversize(vx, vy, 0, 1, 2); + int rej_b = tri_oversize(vx, vy, 2, 1, 3); + if (rej_a && rej_b) return; /* Widescreen: tagged billboard quads carry CPU-computed pixel offsets the * GTE squash never saw — re-squash every X around the prim's anchor. */ @@ -2385,14 +2432,16 @@ static void gp0_exec_textured_quad(void) { } } - gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0], - vx[1], vy[1], u[1], v[1], - vx[2], vy[2], u[2], v[2], - clut_x, clut_y, tpage); - gr_draw_textured_triangle(vx[2], vy[2], u[2], v[2], - vx[1], vy[1], u[1], v[1], - vx[3], vy[3], u[3], v[3], - clut_x, clut_y, tpage); + if (!rej_a) + gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0], + vx[1], vy[1], u[1], v[1], + vx[2], vy[2], u[2], v[2], + clut_x, clut_y, tpage); + if (!rej_b) + gr_draw_textured_triangle(vx[2], vy[2], u[2], v[2], + vx[1], vy[1], u[1], v[1], + vx[3], vy[3], u[3], v[3], + clut_x, clut_y, tpage); } /* Execute shaded textured triangle (GP0 0x34-0x37) */ @@ -2417,7 +2466,8 @@ static void gp0_exec_shaded_textured_tri(void) { uint16_t clut_y = (clut >> 6) & 0x1FF; uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[5] >> 16); uint16_t tpage = tpage_word & 0x1FF; - set_tpage_from_poly(tpage_word); + set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ + if (tri_oversize(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { @@ -2457,7 +2507,10 @@ static void gp0_exec_shaded_textured_quad(void) { uint16_t clut_y = (clut >> 6) & 0x1FF; uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[5] >> 16); uint16_t tpage = tpage_word & 0x1FF; - set_tpage_from_poly(tpage_word); + set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ + int rej_a = tri_oversize(vx, vy, 0, 1, 2); + int rej_b = tri_oversize(vx, vy, 2, 1, 3); + if (rej_a && rej_b) return; ws_nw_hud_shift_vertices(vx, 4); for (int i = 0; i < 4; i++) { @@ -2466,14 +2519,16 @@ static void gp0_exec_shaded_textured_quad(void) { } gr_set_semi_transparency(semi_trans, (int)semi_transparency); - gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0], - vx[1], vy[1], u[1], v[1], c[1], - vx[2], vy[2], u[2], v[2], c[2], - clut_x, clut_y, tpage, raw_texture); - gr_draw_shaded_textured_triangle(vx[2], vy[2], u[2], v[2], c[2], - vx[1], vy[1], u[1], v[1], c[1], - vx[3], vy[3], u[3], v[3], c[3], - clut_x, clut_y, tpage, raw_texture); + if (!rej_a) + gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0], + vx[1], vy[1], u[1], v[1], c[1], + vx[2], vy[2], u[2], v[2], c[2], + clut_x, clut_y, tpage, raw_texture); + if (!rej_b) + gr_draw_shaded_textured_triangle(vx[2], vy[2], u[2], v[2], c[2], + vx[1], vy[1], u[1], v[1], c[1], + vx[3], vy[3], u[3], v[3], c[3], + clut_x, clut_y, tpage, raw_texture); } /* Execute mono line (GP0 0x40-0x47) — Bresenham */ @@ -2483,6 +2538,7 @@ static void gp0_exec_mono_line(void) { int32_t x0, y0, x1, y1; parse_vertex(gp0_cmd_buf[1], &x0, &y0); parse_vertex(gp0_cmd_buf[2], &x1, &y1); + if (line_oversize(x0, y0, x1, y1)) return; int32_t vx[2] = { x0, x1 }; ws_nw_hud_shift_vertices(vx, 2); x0 = vx[0]; x1 = vx[1]; @@ -2500,6 +2556,7 @@ static void gp0_exec_shaded_line(void) { int32_t x0, y0, x1, y1; parse_vertex(gp0_cmd_buf[1], &x0, &y0); parse_vertex(gp0_cmd_buf[3], &x1, &y1); + if (line_oversize(x0, y0, x1, y1)) return; int32_t vx[2] = { x0, x1 }; ws_nw_hud_shift_vertices(vx, 2); x0 = vx[0]; x1 = vx[1]; @@ -3574,7 +3631,8 @@ static void gpu_write_gp0_body(uint32_t val) { int32_t x, y; parse_vertex(val, &x, &y); x += draw_offset_x; y += draw_offset_y; - if (polyline_has_prev) { + if (polyline_has_prev && + !line_oversize(polyline_prev_x, polyline_prev_y, x, y)) { gr_draw_line(polyline_prev_x, polyline_prev_y, x, y, polyline_color); } polyline_prev_x = x; polyline_prev_y = y; @@ -3613,8 +3671,9 @@ static void gpu_write_gp0_body(uint32_t val) { int32_t x, y; parse_vertex(val, &x, &y); x += draw_offset_x; y += draw_offset_y; - gr_draw_shaded_line(polyline_prev_x, polyline_prev_y, polyline_prev_c, - x, y, polyline_color); + if (!line_oversize(polyline_prev_x, polyline_prev_y, x, y)) + gr_draw_shaded_line(polyline_prev_x, polyline_prev_y, + polyline_prev_c, x, y, polyline_color); polyline_prev_x = x; polyline_prev_y = y; polyline_prev_c = polyline_color; polyline_has_prev = 1; From 4517bd5a3ed0901a2402ff7b93bebe3de2cf9afd Mon Sep 17 00:00:00 2001 From: Martin Penkava Date: Sun, 12 Jul 2026 16:54:31 +0200 Subject: [PATCH 5/7] vulkan: WIP AMD fixes + diagnostics for swapchain corruption Fixes found chasing block corruption on AMD RDNA3 (RX 7900 XT) that the NVIDIA driver this backend was developed on tolerates: - flush_cpu_upload: keep every packed rect's VkBufferImageCopy bufferOffset 4-byte aligned (an odd w*h rect misaligned every following rect's R16 offset to 2 mod 4) - begin_geo_pass + wide passes: color-aspect self-barrier mirroring the existing stencil self-barrier (img_to emits no barrier when the layout is unchanged, and the render pass declares no EXTERNAL dependency, so back-to-back passes had no memory dependency on the color attachment) - submit_present: wait the acquire semaphore at TRANSFER (all swapchain writes here are transfer ops); waiting at COLOR_ATTACHMENT_OUTPUT left them unordered against presentation-engine release Still NOT fixed on AMD: black block confetti on BIOS/FMV screens. VRAM-state dumps prove the hr image content is bit-perfect at present time, so remaining corruption is in the present chain. Env-gated diagnostics (tagged [DEBUG-vk01]) included for the ongoing hunt: PSX_VK_VERIFY / PSX_VK_DUMP / PSX_VK_ONEPASS / PSX_VK_EAGER_UPLOAD / PSX_VK_NO_MERGE. Co-Authored-By: Claude Fable 5 --- runtime/src/gpu_vk_renderer.c | 195 +++++++++++++++++++++++++++++++--- 1 file changed, 182 insertions(+), 13 deletions(-) diff --git a/runtime/src/gpu_vk_renderer.c b/runtime/src/gpu_vk_renderer.c index b3287ee07..0b9c92b07 100644 --- a/runtime/src/gpu_vk_renderer.c +++ b/runtime/src/gpu_vk_renderer.c @@ -363,6 +363,7 @@ static int s_tb_twin[4] = {0,0,0,0}; static void flush_geometry(void); /* commit pending untextured batch */ static void flush_tex_batch(void); /* commit pending textured batch */ static void flush_cpu_upload(void); /* pending CPU writes -> GPU images */ +static int vk_eager_upload(void); /* [DEBUG-vk01] env diagnostic gate */ static void pack_flush(void); /* hr -> raw mirror (dirty rect) */ static void flush_pack_if_sampling(int tpx, int tpy, int depth, int clx, int cly); static void vram_upload_block(int x, int y, int w, int h, const uint16_t *data); @@ -429,7 +430,14 @@ static void up_add(int x0, int y0, int x1, int y1) { 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++) { + /* [DEBUG-vk01] PSX_VK_NO_MERGE=1: append raw rects, no merging — isolates + * the merge rule from the batching machinery at full speed. */ + static int no_merge = -1; + if (no_merge < 0) { + const char *e = getenv("PSX_VK_NO_MERGE"); + no_merge = (e && e[0] == '1') ? 1 : 0; + } + for (int i = 0; !no_merge && 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 */ @@ -1399,7 +1407,15 @@ static int acquire_present(VkImage *out_sc, VkCommandBuffer *out_cb, static void submit_present(VkCommandBuffer cb, uint32_t img_idx, uint32_t fr) { p_vkEndCommandBuffer(cb); - VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + /* The present cb writes the swapchain image with TRANSFER ops (layout + * barrier + clear + blit), not color-attachment output. Waiting the + * acquire semaphore at COLOR_ATTACHMENT_OUTPUT leaves those transfer + * writes UNORDERED against the acquire — the GPU may write the image + * before the presentation engine releases it (visible garbage on AMD; + * the NVIDIA driver this was developed on happened to tolerate it). + * Wait at every stage that touches the image. */ + VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_TRANSFER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.waitSemaphoreCount = 1; si.pWaitSemaphores = &s_sem_acquire[fr]; si.pWaitDstStageMask = &wait_stage; @@ -1425,11 +1441,102 @@ static void letterbox(int sw, int sh, int aw, int ah, VkOffset3D off[2]) { off[1].x = x + tw; off[1].y = y + th; off[1].z = 1; } +/* [DEBUG-vk01] PSX_VK_VERIFY=1: every 30th present, read the GPU raw mirror + * back into a temp buffer (full hr->raw pack first, like ensure_cpu) and diff + * it against the CPU s_vram mirror over the displayed rect. CPU-uploaded + * content (MDEC/FMV stills) must match exactly; a mismatch bbox localizes + * which pixels the upload chain lost or stomped. */ +static void vk_verify_probe(int disp_x, int disp_y, int w, int h) { + static int en = -1, calls = 0; + if (en < 0) { const char *e = getenv("PSX_VK_VERIFY"); en = (e && e[0]=='1') ? 1 : 0; } + if (!en || !s_ready || !s_vram) return; + if ((calls++ % 30) != 0) return; + rect_add(&s_pack_dirty, 0, 0, VRAM_W - 1, VRAM_H - 1); + pack_flush(); + VkBuffer buf; VkDeviceMemory mem; void *map; + if (!make_staging((VkDeviceSize)VRAM_W * VRAM_H * 2, &buf, &mem, &map)) return; + VkCommandBuffer cb = begin_oneshot(); + img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + VkBufferImageCopy rc = {0}; + rc.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + rc.imageSubresource.layerCount = 1; + rc.imageExtent.width = VRAM_W; rc.imageExtent.height = VRAM_H; rc.imageExtent.depth = 1; + p_vkCmdCopyImageToBuffer(cb, s_raw_img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buf, 1, &rc); + img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + end_oneshot(cb); + gpu_sync(); + const uint16_t *g = (const uint16_t *)map; + int mism = 0, bx0 = 1 << 30, by0 = 1 << 30, bx1 = -1, by1 = -1, logged = 0; + for (int row = 0; row < h; row++) { + int y = disp_y + row; if (y >= VRAM_H) break; + for (int col = 0; col < w; col++) { + int x = disp_x + col; if (x >= VRAM_W) break; + uint16_t a = g[y * VRAM_W + x] & 0x7FFF; + uint16_t b = s_vram[y * VRAM_W + x] & 0x7FFF; + if (a != b) { + mism++; + if (x < bx0) bx0 = x; if (x > bx1) bx1 = x; + if (y < by0) by0 = y; if (y > by1) by1 = y; + if (logged < 4 && (mism == 1 || (mism % 5000) == 0)) { + fprintf(stdout, "[DEBUG-vk01] mism@(%d,%d) gpu=%04x cpu=%04x\n", + x, y, a, b); + logged++; + } + } + } + } + fprintf(stdout, "[DEBUG-vk01] verify present#%d disp=(%d,%d %dx%d) mism=%d bbox=(%d,%d)-(%d,%d)\n", + calls - 1, disp_x, disp_y, w, h, mism, bx0, by0, bx1, by1); + fflush(stdout); + free_staging(buf, mem); +} + +/* [DEBUG-vk01] PSX_VK_DUMP=1: at presents 60/120/180 dump three VRAM states + * as raw 1024x512 u16 files into the CWD: cpu (s_vram mirror), rawpre (GPU + * raw mirror as-is, no pack), rawpost (raw after a full hr->raw pack = hr + * content). Comparing them offline localizes which surface holds the black. */ +static void vk_dump_probe(void) { + static int en = -1, calls = 0; + if (en < 0) { const char *e = getenv("PSX_VK_DUMP"); en = (e && e[0]=='1') ? 1 : 0; } + if (!en || !s_ready || !s_vram) return; + int c = calls++; + if (c != 60 && c != 120 && c != 180) return; + char name[64]; + FILE *f; + snprintf(name, sizeof name, "vkdump_%d_cpu.bin", c); + if ((f = fopen(name, "wb"))) { fwrite(s_vram, 2, VRAM_W * VRAM_H, f); fclose(f); } + VkBuffer buf; VkDeviceMemory mem; void *map; + if (!make_staging((VkDeviceSize)VRAM_W * VRAM_H * 2, &buf, &mem, &map)) return; + for (int pass = 0; pass < 2; pass++) { + if (pass == 1) { /* full hr -> raw pack, then read hr content */ + rect_add(&s_pack_dirty, 0, 0, VRAM_W - 1, VRAM_H - 1); + pack_flush(); + } + VkCommandBuffer cb = begin_oneshot(); + img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + VkBufferImageCopy rc = {0}; + rc.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + rc.imageSubresource.layerCount = 1; + rc.imageExtent.width = VRAM_W; rc.imageExtent.height = VRAM_H; rc.imageExtent.depth = 1; + p_vkCmdCopyImageToBuffer(cb, s_raw_img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buf, 1, &rc); + img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + end_oneshot(cb); + gpu_sync(); + snprintf(name, sizeof name, "vkdump_%d_%s.bin", c, pass ? "rawpost" : "rawpre"); + if ((f = fopen(name, "wb"))) { fwrite(map, 2, VRAM_W * VRAM_H, f); fclose(f); } + } + free_staging(buf, mem); + fprintf(stdout, "[DEBUG-vk01] dumped VRAM states at present %d\n", c); + fflush(stdout); +} + int vk_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear, int force_4_3) { if (!s_ctx_ok) return 0; flush_cpu_upload(); /* displayed VRAM may include pending CPU writes */ flush_tex_batch(); flush_geometry(); gpu_sync(); /* drain all draws; VRAM in TRANSFER_SRC */ + vk_verify_probe(disp_x, disp_y, w, h); /* [DEBUG-vk01] */ + vk_dump_probe(); /* [DEBUG-vk01] */ VkImage sc; VkCommandBuffer cb; uint32_t idx, fr; if (!acquire_present(&sc, &cb, &idx, &fr)) return 1; /* frame skipped/recreated */ @@ -1589,6 +1696,8 @@ int vk_renderer_present_wide(int disp_x, int disp_y, int disp_h, int linear) { if (i < 0) return 0; flush_cpu_upload(); flush_tex_batch(); flush_geometry(); gpu_sync(); + vk_verify_probe(disp_x, disp_y, 320, disp_h); /* [DEBUG-vk01] */ + vk_dump_probe(); /* [DEBUG-vk01] */ VkImage sc; VkCommandBuffer cb; uint32_t idx, fr; if (!acquire_present(&sc, &cb, &idx, &fr)) return 1; /* frame skipped/recreated */ @@ -1679,6 +1788,32 @@ static void set_scissor_px(VkCommandBuffer cb, int x, int y, int w, int h) { VkRect2D sc = { { x * s_scale, y * s_scale }, { (uint32_t)(w * s_scale), (uint32_t)(h * s_scale) } }; p_vkCmdSetScissor(cb, 0, 1, &sc); } +/* Same hazard as the stencil self-barrier below, COLOR aspect: when the target + * image is already in COLOR_ATTACHMENT layout, img_to()/vram_to() emit NO + * barrier, so back-to-back geo/blit passes (loadOp=LOAD, render pass declares + * no EXTERNAL subpass dependency) have no memory dependency on the color + * attachment. The NVIDIA driver this backend was developed on tolerates that; + * AMD (RDNA3 DCC) returns compression-block garbage — the growing black block + * confetti on the BIOS/FMV screens. Order prior color writes before this + * pass's load/blend reads and writes. */ +static void color_self_barrier(VkCommandBuffer cb, VkImage img) { + VkImageMemoryBarrier cbar = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + cbar.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + cbar.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + cbar.srcQueueFamilyIndex = cbar.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + cbar.image = img; + cbar.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + cbar.subresourceRange.levelCount = 1; + cbar.subresourceRange.layerCount = 1; + cbar.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + cbar.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + p_vkCmdPipelineBarrier(cb, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, 0, NULL, 0, NULL, 1, &cbar); +} + static void begin_geo_pass(VkCommandBuffer cb) { /* Explicit stencil ordering across one-shot submits: this pass both TESTS * and WRITES the stencil (PSX mask bits) that a PRIOR submit's pass wrote. @@ -1703,6 +1838,7 @@ static void begin_geo_pass(VkCommandBuffer cb) { VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, 0, 0, NULL, 0, NULL, 1, &db); + color_self_barrier(cb, s_vram_img); VkRenderPassBeginInfo rp = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; rp.renderPass = s_rpass; rp.framebuffer = s_fbo; rp.renderArea.extent.width = VRAM_W * s_scale; @@ -1905,8 +2041,8 @@ static void flush_cpu_upload(void) { * ONE RGBA8 staging, addressed via VkBufferImageCopy bufferOffset. */ size_t total = 0; for (int i = 0; i < nrects; i++) - total += (size_t)(rects[i].x1 - rects[i].x0 + 1) * - (size_t)(rects[i].y1 - rects[i].y0 + 1); + total += ((size_t)(rects[i].x1 - rects[i].x0 + 1) * + (size_t)(rects[i].y1 - rects[i].y0 + 1) + 1) & ~(size_t)1; if (total == 0) return; VkBuffer rbuf; VkDeviceMemory rmem; void *rmap; @@ -1942,7 +2078,10 @@ static void flush_cpu_upload(void) { rcopies[i].imageExtent.depth = 1; ucopies[i] = rcopies[i]; ucopies[i].bufferOffset = (VkDeviceSize)texoff * 4; - texoff += (size_t)w * h; + /* Keep every rect's bufferOffset 4-byte aligned (VUID: bufferOffset + * must be a multiple of 4/texel size): an odd w*h rect would misalign + * the R16 offset (texoff*2 ≡ 2 mod 4) for every following rect. */ + texoff += ((size_t)w * h + 1) & ~(size_t)1; } p_vkUnmapMemory(s_dev, rmem); p_vkUnmapMemory(s_dev, umem); @@ -1988,14 +2127,28 @@ static void flush_cpu_upload(void) { bp.shift = px_shift(); bp.maskset = 0; bp.src_div = s_scale; bp.src_off[0] = 0; bp.src_off[1] = 0; bp.rect[0] = x; bp.rect[1] = y; bp.rect[2] = x + w; bp.rect[3] = y + h; - bind_masked(cb, 2, 0, 0, 0, 0); - bp.stp_pass = 1; - p_vkCmdPushConstants(cb, s_pl_blit, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof bp, &bp); - p_vkCmdDraw(cb, 6, 1, 0, 0); - bind_masked(cb, 2, 0, 0, 0, 1); - bp.stp_pass = 2; - p_vkCmdPushConstants(cb, s_pl_blit, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof bp, &bp); - p_vkCmdDraw(cb, 6, 1, 0, 0); + /* [DEBUG-vk01] PSX_VK_ONEPASS=1: single non-discarding blit pass + * (stencil mirror not updated) to isolate the two-pass STP split. */ + static int onepass = -1; + if (onepass < 0) { + const char *e = getenv("PSX_VK_ONEPASS"); + onepass = (e && e[0] == '1') ? 1 : 0; + } + if (onepass) { + bind_masked(cb, 2, 0, 0, 0, 0); + bp.stp_pass = 0; + p_vkCmdPushConstants(cb, s_pl_blit, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof bp, &bp); + p_vkCmdDraw(cb, 6, 1, 0, 0); + } else { + bind_masked(cb, 2, 0, 0, 0, 0); + bp.stp_pass = 1; + p_vkCmdPushConstants(cb, s_pl_blit, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof bp, &bp); + p_vkCmdDraw(cb, 6, 1, 0, 0); + bind_masked(cb, 2, 0, 0, 0, 1); + bp.stp_pass = 2; + p_vkCmdPushConstants(cb, s_pl_blit, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof bp, &bp); + p_vkCmdDraw(cb, 6, 1, 0, 0); + } } p_vkCmdEndRenderPass(cb); vram_to(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); @@ -2068,12 +2221,25 @@ static void vkb_fill_rect(int x, int y, int w, int h, uint16_t color) { if (x + w > VRAM_W) w = VRAM_W - x; if (y + h > VRAM_H) h = VRAM_H - y; if (w <= 0 || h <= 0) return; up_add(x, y, x + w - 1, y + h - 1); + if (vk_eager_upload()) flush_cpu_upload(); /* [DEBUG-vk01] */ +} + +/* [DEBUG-vk01] diagnostic: PSX_VK_EAGER_UPLOAD=1 flushes after every CPU + * write path so the rect batching/merge is out of the loop (slow; test only). */ +static int vk_eager_upload(void) { + static int v = -1; + if (v < 0) { + const char *e = getenv("PSX_VK_EAGER_UPLOAD"); + v = (e && e[0] == '1') ? 1 : 0; + } + return v; } static void vkb_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data) { sw_vram_transfer_in(x, y, w, h, data); if (!s_ctx_ok) return; up_add_transfer(x, y, w, h); /* exact touched rects, incl. per-pixel wrap */ + if (vk_eager_upload()) flush_cpu_upload(); } static void vkb_vram_transfer_out(int x, int y, int w, int h, uint16_t *data) { ensure_cpu(); /* sync GPU-rendered content down to the CPU mirror first */ @@ -2226,6 +2392,7 @@ static void wide_pass_begin(VkCommandBuffer cb) { VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, 0, 0, NULL, 0, NULL, 1, &db); + color_self_barrier(cb, s_wide_img[i]); VkRenderPassBeginInfo rp = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; rp.renderPass = s_rpass; rp.framebuffer = s_wide_fb[i]; rp.renderArea.extent.width = (uint32_t)(s_wide_w * S); @@ -2719,6 +2886,7 @@ static void vkb_wide_clear(int base_x, int y, int h, uint16_t color) { if (y1 <= y0) return; VkCommandBuffer cb = begin_oneshot(); img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + color_self_barrier(cb, s_wide_img[i]); VkRenderPassBeginInfo rp = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; rp.renderPass = s_rpass; rp.framebuffer = s_wide_fb[i]; rp.renderArea.extent.width = (uint32_t)(s_wide_w * S); @@ -2756,6 +2924,7 @@ static void vkb_wide_clear_margins(int base_x, int y, int h, uint16_t color, int if (y1 <= y0 || margin * 2 >= W) return; VkCommandBuffer cb = begin_oneshot(); img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + color_self_barrier(cb, s_wide_img[i]); VkRenderPassBeginInfo rp = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; rp.renderPass = s_rpass; rp.framebuffer = s_wide_fb[i]; rp.renderArea.extent.width = (uint32_t)W; From 9601aab330abd232dc6d20db56b49e76349ae497 Mon Sep 17 00:00:00 2001 From: Martin Penkava Date: Sun, 12 Jul 2026 16:54:32 +0200 Subject: [PATCH 6/7] build: Windows helper scripts for framework build and regeneration Co-Authored-By: Claude Fable 5 --- build_framework.bat | 25 +++++++++++++++++++++++++ regen_all.bat | 24 ++++++++++++++++++++++++ regen_game_build.bat | 19 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 build_framework.bat create mode 100644 regen_all.bat create mode 100644 regen_game_build.bat diff --git a/build_framework.bat b/build_framework.bat new file mode 100644 index 000000000..3924c3461 --- /dev/null +++ b/build_framework.bat @@ -0,0 +1,25 @@ +@echo off +REM Build the psxrecomp FRAMEWORK (recompiler tool + BIOS-only runtime). +REM Uses MSVC from VS Build Tools 2026 and its bundled CMake + Ninja (no PATH setup required). + +set "VSBT=C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools" +call "%VSBT%\VC\Auxiliary\Build\vcvars64.bat" || exit /b 1 +set "PATH=%VSBT%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin;%VSBT%\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja;%PATH%" + +cd /d D:\tools\psx-recomp || exit /b 1 + +echo ===== [1/4] Configure recompiler ===== +cmake -S recompiler -B recompiler/build -G Ninja -DCMAKE_BUILD_TYPE=Release || exit /b 1 +echo ===== [2/4] Build recompiler ===== +cmake --build recompiler/build || exit /b 1 +echo ===== [3/4] Configure runtime ===== +REM MSVC portability flags: +REM /DNOMINMAX - stop windows.h min/max macros clobbering std::min/std::max +REM /EHsc - C++ exception unwind semantics (restored after CXX_FLAGS override) +REM /std:c11 - the runtime's C files use (C11 atomics) +REM /experimental:c11atomics - MSVC gates behind this opt-in +cmake -S runtime -B runtime/build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="/DNOMINMAX /EHsc" -DCMAKE_C_FLAGS="/DNOMINMAX /std:c11 /experimental:c11atomics" || exit /b 1 +echo ===== [4/4] Build runtime (psx-runtime) ===== +cmake --build runtime/build --target psx-runtime || exit /b 1 + +echo === BUILD OK === diff --git a/regen_all.bat b/regen_all.bat new file mode 100644 index 000000000..ff78f8007 --- /dev/null +++ b/regen_all.bat @@ -0,0 +1,24 @@ +@echo off +REM Rebuild recompiler with the portable-constructor emitter fix, regenerate the +REM BIOS + game C, and rebuild the framework runtime. Run after editing the emitter. + +set "VSBT=C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools" +call "%VSBT%\VC\Auxiliary\Build\vcvars64.bat" || exit /b 1 +set "PATH=%VSBT%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin;%VSBT%\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja;D:\DEV_TOOLS\Git\bin;%PATH%" + +cd /d D:\tools\psx-recomp || exit /b 1 +echo ===== [1/4] Rebuild recompiler (emitter fix) ===== +cmake --build recompiler/build || exit /b 1 + +echo ===== [2/4] Regenerate BIOS C ===== +bash tools/regen_bios.sh || exit /b 1 + +echo ===== [3/4] Regenerate SmackDown game C ===== +cd /d D:\tools\SmackDown2Recomp || exit /b 1 +"D:\tools\psx-recomp\recompiler\build\psxrecomp-game.exe" --config game.toml || exit /b 1 + +echo ===== [4/4] Rebuild framework runtime (BIOS) ===== +cd /d D:\tools\psx-recomp || exit /b 1 +cmake --build runtime/build --target psx-runtime || exit /b 1 + +echo === REGEN + RUNTIME OK === diff --git a/regen_game_build.bat b/regen_game_build.bat new file mode 100644 index 000000000..c54a6f115 --- /dev/null +++ b/regen_game_build.bat @@ -0,0 +1,19 @@ +@echo off +REM Rebuild recompiler (game emitter fix), regenerate SmackDown game C, rebuild game. + +set "VSBT=C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools" +call "%VSBT%\VC\Auxiliary\Build\vcvars64.bat" || exit /b 1 +set "PATH=%VSBT%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin;%VSBT%\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja;%PATH%" + +cd /d D:\tools\psx-recomp || exit /b 1 +echo ===== [1/3] Rebuild recompiler (game emitter fix) ===== +cmake --build recompiler/build || exit /b 1 + +echo ===== [2/3] Regenerate SmackDown game C ===== +cd /d D:\tools\SmackDown2Recomp || exit /b 1 +"D:\tools\psx-recomp\recompiler\build\psxrecomp-game.exe" --config game.toml || exit /b 1 + +echo ===== [3/3] Build SmackDown2Recomp ===== +cmake --build build --target psx-runtime || exit /b 1 + +echo === GAME BUILD OK === From d05d14bb11caafad2963abe824f249249a80cb11 Mon Sep 17 00:00:00 2001 From: Martin Penkava Date: Sun, 12 Jul 2026 19:16:19 +0200 Subject: [PATCH 7/7] vulkan: optimize frame delivery and add CRT presentation --- runtime/include/gpu_vk_renderer.h | 12 +- runtime/launcher/launcher.cpp | 6 +- runtime/shaders/crt.frag | 82 +++++++ runtime/shaders/crt.vert | 9 + runtime/src/gpu.c | 2 + runtime/src/gpu_vk_renderer.c | 384 +++++++++++++++++++++++++++--- 6 files changed, 452 insertions(+), 43 deletions(-) create mode 100644 runtime/shaders/crt.frag create mode 100644 runtime/shaders/crt.vert diff --git a/runtime/include/gpu_vk_renderer.h b/runtime/include/gpu_vk_renderer.h index ea4b18a2e..72a79dbb0 100644 --- a/runtime/include/gpu_vk_renderer.h +++ b/runtime/include/gpu_vk_renderer.h @@ -45,11 +45,17 @@ void vk_renderer_present_blank(void); * server, 24-bit present). No-op when the Vulkan path is inactive. */ void vk_renderer_sync_cpu(void); -/* Set the present mode: 1=FIFO (vsync), 0=IMMEDIATE (lowest latency, may tear), - * -1=MAILBOX (low-latency, tear-free). Applied on the next swapchain (re)build; - * unsupported modes fall back to FIFO (always available). */ +/* Set the present policy: 1=tear-free, 0=IMMEDIATE (lowest latency, may tear), + * -1=MAILBOX. Tear-free prefers MAILBOX because the frontend already paces + * frames; unsupported modes fall back to FIFO (always available). */ void vk_renderer_set_present_mode(int mode); +/* Screen simulation for the Vulkan present path (ScreenKind values from + * color_lut.h: 0 raw, 1 crt, 2 composite, 3 trinitron). Raw keeps the exact + * present blit; the others route the 15-bit present through a CRT shader + * pass (PSX_SCREEN env overrides, same as the software path). */ +void vk_renderer_set_screen_kind(int kind); + #ifdef __cplusplus } #endif diff --git a/runtime/launcher/launcher.cpp b/runtime/launcher/launcher.cpp index 35021f2a9..32711ce11 100644 --- a/runtime/launcher/launcher.cpp +++ b/runtime/launcher/launcher.cpp @@ -96,7 +96,7 @@ class LauncherSystemInterface : public SystemInterface_SDL { // Mirror of the user-tunable settings, in the value shapes the RML binds to. struct LauncherModel { - int renderer = 0; // 0=software, 1=opengl + int renderer = 0; // 0=software, 1=opengl, 2=vulkan int supersampling = 1; // 1..4 bool antialiasing = true; int texture_filter = 0; // 0=nearest, 1=bilinear @@ -381,7 +381,7 @@ int lang_index_for(const std::vector& langs, return 0; // unknown/first — the game's declared default sits at [0] by convention } -const char* renderer_name(int v) { return v == 1 ? "OpenGL" : "Software"; } +const char* renderer_name(int v) { return v == 2 ? "Vulkan" : v == 1 ? "OpenGL" : "Software"; } const char* texfilter_name(int v) { return v == 1 ? "Bilinear" : "Nearest"; } const char* crt_name(int v) { switch (v) { @@ -948,7 +948,7 @@ Result run(SDL_Window* window, void* gl_context, c.BindEventCallback("cycle_renderer", [&m, handle](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) mutable { - m.renderer ^= 1; + m.renderer = (m.renderer + 1) % 3; refresh_labels(m); handle.DirtyVariable("renderer_label"); handle.DirtyVariable("opengl_renderer"); diff --git a/runtime/shaders/crt.frag b/runtime/shaders/crt.frag new file mode 100644 index 000000000..b7ac48ac4 --- /dev/null +++ b/runtime/shaders/crt.frag @@ -0,0 +1,82 @@ +#version 450 +/* CRT present shader (CRT-Royale-inspired, single pass). Replaces the plain + * present blit when screen kind != raw. Sampled in linear filtering. + * + * Kinds (must match ScreenKind in color_lut.h): + * 1 crt shadow-mask triads + gaussian scanline beam + * 2 composite strong horizontal blur + chroma bleed + soft scanlines + * 3 trinitron aperture-grille stripes + gaussian scanline beam + * + * Royale signature bits kept: gamma-correct pipeline, energy-conserving + * gaussian beam whose width grows with brightness (bright lines bloom), + * phosphor mask applied in linear light with gain compensation, and the + * scanline effect fading out when the output has too few pixels per line + * to resolve it (instead of aliasing into moire). */ +layout(location = 0) in vec2 v_uv; +layout(location = 0) out vec4 o_col; +layout(set = 0, binding = 0) uniform sampler2D u_src; +layout(push_constant) uniform PC { + vec4 u_src_rect; /* displayed region in normalized src tex coords: x0,y0,x1,y1 */ + vec2 u_out_size; /* viewport (letterbox rect) size in px */ + vec2 u_native; /* native source resolution (w = px per line, h = scanlines) */ + int u_kind; +} pc; + +vec3 fetch(vec2 uv) { + uv = clamp(uv, vec2(0.0), vec2(1.0)); + return texture(u_src, mix(pc.u_src_rect.xy, pc.u_src_rect.zw, uv)).rgb; +} +vec3 to_lin(vec3 c) { return pow(max(c, 0.0), vec3(2.4)); } +vec3 to_gam(vec3 c) { return pow(max(c, 0.0), vec3(1.0 / 2.2)); } + +/* Horizontally pre-filtered, gamma-decoded source sample at scanline y. */ +vec3 line_sample(float x, float y) { + float hx = (pc.u_kind == 2) ? 1.1 : 0.4; /* blur radius, native px */ + vec2 d = vec2(hx / pc.u_native.x, 0.0); + vec2 uv = vec2(x, y); + vec3 c = fetch(uv) * 0.5 + (fetch(uv - d) + fetch(uv + d)) * 0.25; + if (pc.u_kind == 2) { /* composite chroma bleed */ + c.r = mix(c.r, fetch(uv - 2.0 * d).r, 0.4); + c.b = mix(c.b, fetch(uv + 2.0 * d).b, 0.4); + } + return to_lin(c); +} + +void main() { + float lines = max(pc.u_native.y, 1.0); + float ly = v_uv.y * lines; /* position in scanline space */ + float lc = floor(ly - 0.5) + 0.5; /* nearest line centre */ + + /* Energy-conserving gaussian beam over the 3 nearest scanlines. */ + vec3 beam = vec3(0.0); + const float INV_SQRT_2PI = 0.3989423; + for (int i = -1; i <= 1; i++) { + float yc = lc + float(i); + vec3 c = line_sample(v_uv.x, yc / lines); + float lum = clamp(dot(c, vec3(0.299, 0.587, 0.114)), 0.0, 1.0); + float sigma = mix(0.30, 0.55, lum); /* bright lines bloom wider */ + float d = ly - yc; + beam += c * exp(-0.5 * d * d / (sigma * sigma)) * (INV_SQRT_2PI / sigma); + } + + /* Too few output px per scanline -> fade the beam into a flat sample. */ + float px_per_line = pc.u_out_size.y / lines; + float fade = clamp(px_per_line * 0.5 - 0.5, 0.0, 1.0); + vec3 col = mix(line_sample(v_uv.x, v_uv.y), beam, fade); + + /* Phosphor mask (output-pixel space), gain-compensated in linear light. */ + float mstr = (pc.u_kind == 2) ? 0.0 : 0.5; + mstr *= fade; /* tiny windows: skip mask too */ + if (mstr > 0.0) { + float x = gl_FragCoord.x; + if (pc.u_kind == 1) /* shadow mask: triads, half- + * period shift every 2 rows */ + x += (mod(floor(gl_FragCoord.y * 0.5), 2.0) < 1.0) ? 0.0 : 1.5; + int m = int(mod(x, 3.0)); + vec3 triad = vec3(m == 0 ? 1.0 : 0.0, m == 1 ? 1.0 : 0.0, m == 2 ? 1.0 : 0.0); + col *= mix(vec3(1.0), triad, mstr); + col /= 1.0 - mstr * (2.0 / 3.0); + } + + o_col = vec4(to_gam(min(col, 1.0)), 1.0); +} diff --git a/runtime/shaders/crt.vert b/runtime/shaders/crt.vert new file mode 100644 index 000000000..1195f703f --- /dev/null +++ b/runtime/shaders/crt.vert @@ -0,0 +1,9 @@ +#version 450 +/* Fullscreen triangle for the CRT present pass. The viewport is set to the + * letterbox rect, so v_uv [0,1] spans exactly the displayed picture. */ +layout(location = 0) out vec2 v_uv; +void main() { + vec2 p = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); + v_uv = p; + gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 803a01f3d..9089b959c 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -19,6 +19,7 @@ #include "cpu_state.h" #include "event_ring.h" #include "color_lut.h" +#include "gpu_vk_renderer.h" #include "ws_cull_detect.h" #include #include @@ -1927,6 +1928,7 @@ static int s_screen_kind_cfg = SCREEN_RAW; /* config/launcher-set; env ov void gpu_set_screen_kind(int kind) { if (kind < SCREEN_RAW || kind > SCREEN_TRINITRON) kind = SCREEN_RAW; + vk_renderer_set_screen_kind(kind); /* Vulkan present applies it as a shader */ if (kind == s_screen_kind_cfg) return; s_screen_kind_cfg = kind; s_screen_lut_init = 0; /* rebuild on next scanout */ diff --git a/runtime/src/gpu_vk_renderer.c b/runtime/src/gpu_vk_renderer.c index 0b9c92b07..c01690905 100644 --- a/runtime/src/gpu_vk_renderer.c +++ b/runtime/src/gpu_vk_renderer.c @@ -38,6 +38,7 @@ void vk_renderer_present_cpu(const uint32_t*p,int w,int h,int l,int f){(void)p;( void vk_renderer_present_blank(void){} void vk_renderer_sync_cpu(void){} void vk_renderer_set_present_mode(int m){(void)m;} +void vk_renderer_set_screen_kind(int k){(void)k;} int vk_perf_json(char *out,int cap,int count){(void)count; return cap>2?snprintf(out,cap,"[]"):0;} const GpuRenderBackend *vk_backend_get(void) { return 0; } @@ -47,6 +48,7 @@ const GpuRenderBackend *vk_backend_get(void) { return 0; } #include #define VK_NO_PROTOTYPES #include +#include "color_lut.h" /* ScreenKind + screen_kind_from_name (CRT present) */ #include "vk_shaders_spv.h" /* generated: spv_geo_vert/frag, spv_geo_tex_vert/frag, * spv_pack_comp, spv_blit_vert/frag */ @@ -182,12 +184,48 @@ static VkBuffer s_pending_buf[PENDING_STAGING_MAX]; static VkDeviceMemory s_pending_mem[PENDING_STAGING_MAX]; static int s_pending_n; +/* Persistently mapped host-visible staging buffers. CPU-heavy menu screens can + * upload nearly all of VRAM every frame; creating, allocating, mapping and + * destroying the same ~1 MiB + ~2 MiB buffers at 60 Hz saturated one CPU core + * and limited Vulkan to ~57 fps. gpu_sync already drains the queue before a + * deferred buffer is released, so released entries are safe to reuse. */ +#define STAGING_CACHE_MAX 16 +typedef struct { + VkBuffer buf; + VkDeviceMemory mem; + VkDeviceSize size; + void *map; + int busy; +} StagingCacheEntry; +static StagingCacheEntry s_staging_cache[STAGING_CACHE_MAX]; + +static void staging_release(VkBuffer buf, VkDeviceMemory mem) { + for (int i = 0; i < STAGING_CACHE_MAX; i++) { + if (s_staging_cache[i].buf == buf && s_staging_cache[i].mem == mem) { + s_staging_cache[i].busy = 0; + return; + } + } + p_vkDestroyBuffer(s_dev, buf, NULL); + p_vkFreeMemory(s_dev, mem, NULL); +} + static VkSwapchainKHR s_swapchain; static VkFormat s_sc_format; static VkExtent2D s_sc_extent; static uint32_t s_sc_count; static VkImage s_sc_images[8]; -static int s_present_mode_req = 1; /* 1 FIFO, 0 IMMEDIATE, -1 MAILBOX */ +static int s_present_mode_req = 1; /* 1 tear-free, 0 IMMEDIATE, -1 MAILBOX */ + +/* CRT present pass (screen kind != raw): a fullscreen fragment-shader pass + * into the swapchain replaces the plain present blit. Render pass, per-image + * views/framebuffers and the pipeline live with the swapchain (rebuilt on + * resize / format change). */ +static int s_crt_kind; /* effective ScreenKind (0 = raw = off) */ +static VkRenderPass s_rp_present; +static VkImageView s_sc_views[8]; +static VkFramebuffer s_sc_fbs[8]; +static VkPipeline s_pipe_crt; /* lazy; needs s_rp_present + modules */ /* Per-frame sync (double-buffered command recording). */ #define VK_FRAMES 2 @@ -290,6 +328,8 @@ static int s_ds_blit_idx; * a frame that jumps from ~10 to ~2000 allocs is the smoking gun). Always on. */ typedef struct { uint32_t present_idx; + uint64_t present_qpc; /* [DEBUG-vkpresent] QueuePresent return timestamp */ + uint32_t wait_us, acquire_us, present_us; /* [DEBUG-vkpresent] blocking boundaries */ uint32_t allocs, alloc_kb, oneshots, submits, syncs, pack_flushes, blits, upload_blocks, copy_rects, geo_flushes, tex_flushes, wide_passes, wide_clears; @@ -300,8 +340,15 @@ static uint32_t s_perf_head; /* number of frames recorded (monotonic) * static VkPerf s_perf_cur; /* current (in-progress) frame */ static uint32_t s_present_idx; +static uint32_t perf_elapsed_us(uint64_t start) { + uint64_t freq = SDL_GetPerformanceFrequency(); + uint64_t ticks = SDL_GetPerformanceCounter() - start; + return freq ? (uint32_t)(ticks * 1000000u / freq) : 0; +} + static void perf_snapshot_present(void) { s_perf_cur.present_idx = s_present_idx++; + s_perf_cur.present_qpc = SDL_GetPerformanceCounter(); s_perf_ring[s_perf_head % VK_PERF_RING] = s_perf_cur; s_perf_head++; memset(&s_perf_cur, 0, sizeof s_perf_cur); @@ -327,6 +374,10 @@ static VkPipeline s_pipe_pack; /* compute */ #define PIPE_CACHE_N (PIPE_PROGS * PIPE_TOPOS * PIPE_BLENDS * PIPE_STENCILS * PIPE_CMASKS) static VkPipeline s_pipe_cache[PIPE_CACHE_N]; static VkShaderModule s_mod_geo_v, s_mod_geo_f, s_mod_tex_v, s_mod_tex_f, s_mod_blit_v, s_mod_blit_f; +static VkShaderModule s_mod_crt_v, s_mod_crt_f; +static VkSampler s_samp_lin; /* linear; CRT pass source sampling */ +static VkPipelineLayout s_pl_crt; /* s_dsl_blit + fragment push block */ +static VkDescriptorSet s_ds_crt[VK_FRAMES]; /* per-frame: present cbs stay in flight */ /* Untextured batch (flat/gouraud triangles, lines-as-quads, flat rects). */ typedef struct { float x, y, r, g, b, a; } Vert; /* GEO vertex, stride 24 */ @@ -514,10 +565,18 @@ static uint32_t find_mem_type(uint32_t type_bits, VkMemoryPropertyFlags want) { static void vk_gpu_sync_internal(void); /* drain queue + reclaim work pool/staging */ -/* Begin a one-shot work command buffer (allocated from s_work_pool, which is - * bulk-reset at gpu_sync). */ +/* Coalesced work command buffer. Per-op begin/end used to allocate + submit a + * fresh CB per op; in-ring gameplay hits 250-650 tex-batch flushes per frame + * and the per-submit driver overhead alone blew the 33ms frame budget (~58ms + * mean, 110ms p95 measured). Ops now record into ONE open CB; flush_work() + * ends + submits it exactly once, at gpu_sync (present / readback / ring + * drain). Single-CB recording order is a strict superset of the old queue + * submission order, so the per-op image-layout barriers keep working as-is. */ +static VkCommandBuffer s_work_cb; /* open coalesced work CB (NULL if none) */ + static VkCommandBuffer begin_oneshot(void) { s_perf_cur.oneshots++; + if (s_work_cb) return s_work_cb; VkCommandBufferAllocateInfo ai = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; ai.commandPool = s_work_pool; ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; @@ -527,21 +586,27 @@ static VkCommandBuffer begin_oneshot(void) { VkCommandBufferBeginInfo bi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; p_vkBeginCommandBuffer(cb, &bi); + s_work_cb = cb; return cb; } -/* Submit the work CB WITHOUT waiting. Cross-submit ordering is provided by the - * per-op image-layout barriers (img_to): a barrier's first scope covers all - * commands earlier in queue submission order, so each op's transitions wait for - * prior ops' writes. The queue is drained only at gpu_sync (present / readback). */ +/* The op's commands stay in the open work CB; nothing is submitted here. */ static void end_oneshot(VkCommandBuffer cb) { - p_vkEndCommandBuffer(cb); + (void)cb; + s_work_pending = 1; +} + +/* End + submit the open work CB (no wait). Must run before any host-side wait + * on the work and before a present submit consumes its results. */ +static void flush_work(void) { + if (!s_work_cb) return; + p_vkEndCommandBuffer(s_work_cb); VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; - si.pCommandBuffers = &cb; + si.pCommandBuffers = &s_work_cb; p_vkQueueSubmit(s_queue, 1, &si, VK_NULL_HANDLE); s_perf_cur.submits++; - s_work_pending = 1; + s_work_cb = VK_NULL_HANDLE; } /* Defer a staging buffer's destruction until the queue is next idle (its @@ -556,8 +621,11 @@ static void defer_staging(VkBuffer buf, VkDeviceMemory mem) { * The single sync point for the deferred-submit model. */ static void vk_gpu_sync_internal(void) { s_perf_cur.syncs++; + flush_work(); /* submit the open coalesced work CB before waiting */ if (s_work_pending) { + uint64_t wait_start = SDL_GetPerformanceCounter(); p_vkQueueWaitIdle(s_queue); + s_perf_cur.wait_us += perf_elapsed_us(wait_start); /* RELEASE_RESOURCES so the per-op command buffers allocated since the * last sync are actually freed (a plain reset only recycles them, and * begin_oneshot always allocates fresh -> unbounded growth). */ @@ -565,8 +633,7 @@ static void vk_gpu_sync_internal(void) { s_work_pending = 0; } for (int i = 0; i < s_pending_n; i++) { - p_vkDestroyBuffer(s_dev, s_pending_buf[i], NULL); - p_vkFreeMemory(s_dev, s_pending_mem[i], NULL); + staging_release(s_pending_buf[i], s_pending_mem[i]); } s_pending_n = 0; s_ds_blit_idx = 0; @@ -739,9 +806,14 @@ static VkPresentModeKHR choose_present_mode(void) { if (n > 8) n = 8; VkPresentModeKHR modes[8]; p_vkGetPhysicalDeviceSurfacePresentModesKHR(s_phys, s_surface, &n, modes); + /* The frontend's wall-clock pacer already owns the 59.94 Hz cadence. + * FIFO adds a second blocking pacer and produces a beat-frequency tail + * (measured on SmackDown 2: 17.88 ms p50 / 19.30 ms p95). MAILBOX stays + * tear-free without making acquire/present another metronome (16.68 ms + * p50 / 16.70 ms p95 on the identical savestate). Prefer it for both + * tear-free modes; FIFO remains the required fallback. */ VkPresentModeKHR want = (s_present_mode_req == 0) ? VK_PRESENT_MODE_IMMEDIATE_KHR - : (s_present_mode_req < 0) ? VK_PRESENT_MODE_MAILBOX_KHR - : VK_PRESENT_MODE_FIFO_KHR; + : VK_PRESENT_MODE_MAILBOX_KHR; for (uint32_t i = 0; i < n; i++) if (modes[i] == want) return want; return VK_PRESENT_MODE_FIFO_KHR; /* always supported */ } @@ -792,10 +864,63 @@ static int create_swapchain(void) { p_vkGetSwapchainImagesKHR(s_dev, s_swapchain, &s_sc_count, NULL); if (s_sc_count > 8) s_sc_count = 8; p_vkGetSwapchainImagesKHR(s_dev, s_swapchain, &s_sc_count, s_sc_images); + + /* CRT present pass objects (render pass, per-image view+framebuffer). A + * failure here just leaves s_rp_present NULL: present falls back to the + * plain blit, it is not fatal. */ + { + VkAttachmentDescription att = {0}; + att.format = s_sc_format; att.samples = VK_SAMPLE_COUNT_1_BIT; + att.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; /* black letterbox bars */ + att.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + att.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + att.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + att.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + att.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + VkAttachmentReference cref = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkSubpassDescription sub = {0}; + sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + sub.colorAttachmentCount = 1; sub.pColorAttachments = &cref; + VkSubpassDependency dep = {0}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.srcAccessMask = 0; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpi = { VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; + rpi.attachmentCount = 1; rpi.pAttachments = &att; + rpi.subpassCount = 1; rpi.pSubpasses = ⊂ + rpi.dependencyCount = 1; rpi.pDependencies = &dep; + if (p_vkCreateRenderPass(s_dev, &rpi, NULL, &s_rp_present) != VK_SUCCESS) + s_rp_present = VK_NULL_HANDLE; + for (uint32_t i = 0; s_rp_present && i < s_sc_count; i++) { + VkImageViewCreateInfo vi = { VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vi.image = s_sc_images[i]; vi.viewType = VK_IMAGE_VIEW_TYPE_2D; + vi.format = s_sc_format; + vi.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + vi.subresourceRange.levelCount = 1; vi.subresourceRange.layerCount = 1; + if (p_vkCreateImageView(s_dev, &vi, NULL, &s_sc_views[i]) != VK_SUCCESS) { + s_sc_views[i] = VK_NULL_HANDLE; break; + } + VkFramebufferCreateInfo fi = { VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fi.renderPass = s_rp_present; fi.attachmentCount = 1; + fi.pAttachments = &s_sc_views[i]; + fi.width = s_sc_extent.width; fi.height = s_sc_extent.height; fi.layers = 1; + if (p_vkCreateFramebuffer(s_dev, &fi, NULL, &s_sc_fbs[i]) != VK_SUCCESS) { + s_sc_fbs[i] = VK_NULL_HANDLE; break; + } + } + } return 1; } static void destroy_swapchain(void) { + for (int i = 0; i < 8; i++) { + if (s_sc_fbs[i]) { p_vkDestroyFramebuffer(s_dev, s_sc_fbs[i], NULL); s_sc_fbs[i] = VK_NULL_HANDLE; } + if (s_sc_views[i]) { p_vkDestroyImageView(s_dev, s_sc_views[i], NULL); s_sc_views[i] = VK_NULL_HANDLE; } + } + if (s_pipe_crt) { p_vkDestroyPipeline(s_dev, s_pipe_crt, NULL); s_pipe_crt = VK_NULL_HANDLE; } + if (s_rp_present) { p_vkDestroyRenderPass(s_dev, s_rp_present, NULL); s_rp_present = VK_NULL_HANDLE; } if (s_swapchain) { p_vkDestroySwapchainKHR(s_dev, s_swapchain, NULL); s_swapchain = VK_NULL_HANDLE; } } @@ -885,6 +1010,12 @@ typedef struct { int rect[4]; /* 32..47: x0,y0,x1,y1 native px */ } BlitPush; /* 48 bytes */ typedef struct { int scale, off_x, off_y; } PackPush; /* 12 bytes */ +typedef struct { + float src_rect[4]; /* displayed region, normalized src uv */ + float out_size[2]; /* letterbox rect px */ + float native[2]; /* native source w, h (scanline count) */ + int kind; /* ScreenKind 1..3 */ +} CrtPush; /* 36 bytes; must match crt.frag PC */ /* Create a device-local image + view (color aspect by default). */ static int make_image(VkFormat fmt, int w, int h, VkImageUsageFlags usage, @@ -1040,6 +1171,10 @@ static int create_render_targets(void) { sci.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; if (p_vkCreateSampler(s_dev, &sci, NULL, &s_samp) != VK_SUCCESS) return vk_die("sampler failed"); + /* Linear sampler for the CRT present pass (smooth horizontal sampling). */ + sci.magFilter = sci.minFilter = VK_FILTER_LINEAR; + if (p_vkCreateSampler(s_dev, &sci, NULL, &s_samp_lin) != VK_SUCCESS) return vk_die("linear sampler failed"); + /* Descriptor set layouts. */ { VkDescriptorSetLayoutBinding b = { 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, @@ -1058,10 +1193,10 @@ static int create_render_targets(void) { /* Descriptor pool + sets: ds_tex (1) + ds_pack (1) + blit ring (BLIT_DESC_RING). */ { VkDescriptorPoolSize sizes[2] = { - { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, BLIT_DESC_RING + 2 }, + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, BLIT_DESC_RING + 2 + VK_FRAMES }, { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1 } }; VkDescriptorPoolCreateInfo pci = { VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; - pci.maxSets = BLIT_DESC_RING + 2; pci.poolSizeCount = 2; pci.pPoolSizes = sizes; + pci.maxSets = BLIT_DESC_RING + 2 + VK_FRAMES; pci.poolSizeCount = 2; pci.pPoolSizes = sizes; if (p_vkCreateDescriptorPool(s_dev, &pci, NULL, &s_dpool) != VK_SUCCESS) return vk_die("desc pool"); VkDescriptorSetLayout layouts[2] = { s_dsl_tex, s_dsl_pack }; VkDescriptorSet *sets[2] = { &s_ds_tex, &s_ds_pack }; @@ -1075,6 +1210,14 @@ static int create_render_targets(void) { ai.descriptorPool = s_dpool; ai.descriptorSetCount = 1; ai.pSetLayouts = &s_dsl_blit; if (p_vkAllocateDescriptorSets(s_dev, &ai, &s_ds_blit_ring[i]) != VK_SUCCESS) return vk_die("blit set alloc"); } + /* CRT present sets: one per in-flight frame (the frame fence in + * acquire_present guarantees the set is idle before it is rewritten; + * the blit ring resets at gpu_sync and can't give that guarantee). */ + for (int i = 0; i < VK_FRAMES; i++) { + VkDescriptorSetAllocateInfo ai = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; + ai.descriptorPool = s_dpool; ai.descriptorSetCount = 1; ai.pSetLayouts = &s_dsl_blit; + if (p_vkAllocateDescriptorSets(s_dev, &ai, &s_ds_crt[i]) != VK_SUCCESS) return vk_die("crt set alloc"); + } /* ds_tex: raw mirror sampled. ds_pack: hr sampled + raw storage. */ VkDescriptorImageInfo raw_smp = { s_samp, s_raw_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; VkDescriptorImageInfo hr_smp = { s_samp, s_vram_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; @@ -1103,6 +1246,8 @@ static int create_render_targets(void) { if (p_vkCreatePipelineLayout(s_dev, &li, NULL, &s_pl_blit) != VK_SUCCESS) return vk_die("pl blit"); pr.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; pr.size = sizeof(PackPush); li.pSetLayouts = &s_dsl_pack; if (p_vkCreatePipelineLayout(s_dev, &li, NULL, &s_pl_pack) != VK_SUCCESS) return vk_die("pl pack"); + pr.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; pr.size = sizeof(CrtPush); li.pSetLayouts = &s_dsl_blit; + if (p_vkCreatePipelineLayout(s_dev, &li, NULL, &s_pl_crt) != VK_SUCCESS) return vk_die("pl crt"); } /* Shader modules (kept for lazy graphics-pipeline creation). */ @@ -1112,7 +1257,10 @@ static int create_render_targets(void) { s_mod_tex_f = make_module(spv_geo_tex_frag, spv_geo_tex_frag_size); s_mod_blit_v = make_module(spv_blit_vert, spv_blit_vert_size); s_mod_blit_f = make_module(spv_blit_frag, spv_blit_frag_size); - if (!s_mod_geo_v || !s_mod_geo_f || !s_mod_tex_v || !s_mod_tex_f || !s_mod_blit_v || !s_mod_blit_f) + s_mod_crt_v = make_module(spv_crt_vert, spv_crt_vert_size); + s_mod_crt_f = make_module(spv_crt_frag, spv_crt_frag_size); + if (!s_mod_geo_v || !s_mod_geo_f || !s_mod_tex_v || !s_mod_tex_f || !s_mod_blit_v || !s_mod_blit_f || + !s_mod_crt_v || !s_mod_crt_f) return vk_die("shader module failed"); /* Pack compute pipeline. */ @@ -1319,6 +1467,14 @@ void vk_renderer_shutdown(void) { p_vkDeviceWaitIdle(s_dev); vk_gpu_sync_internal(); /* reclaim deferred staging before tearing down */ cpres_cache_free(); /* FMV CPU-present cached image + staging */ + for (int i = 0; i < STAGING_CACHE_MAX; i++) { + StagingCacheEntry *e = &s_staging_cache[i]; + if (!e->buf) continue; + if (e->map) p_vkUnmapMemory(s_dev, e->mem); + p_vkDestroyBuffer(s_dev, e->buf, NULL); + p_vkFreeMemory(s_dev, e->mem, NULL); + memset(e, 0, sizeof *e); + } wide_free_all(); /* native-wide surfaces (color + DS + framebuffers) */ for (int i = 0; i < PIPE_CACHE_N; i++) if (s_pipe_cache[i]) p_vkDestroyPipeline(s_dev, s_pipe_cache[i], NULL); @@ -1329,15 +1485,19 @@ void vk_renderer_shutdown(void) { if (s_mod_tex_f) p_vkDestroyShaderModule(s_dev, s_mod_tex_f, NULL); if (s_mod_blit_v) p_vkDestroyShaderModule(s_dev, s_mod_blit_v, NULL); if (s_mod_blit_f) p_vkDestroyShaderModule(s_dev, s_mod_blit_f, NULL); + if (s_mod_crt_v) p_vkDestroyShaderModule(s_dev, s_mod_crt_v, NULL); + if (s_mod_crt_f) p_vkDestroyShaderModule(s_dev, s_mod_crt_f, NULL); if (s_pl_geo) p_vkDestroyPipelineLayout(s_dev, s_pl_geo, NULL); if (s_pl_tex) p_vkDestroyPipelineLayout(s_dev, s_pl_tex, NULL); if (s_pl_blit) p_vkDestroyPipelineLayout(s_dev, s_pl_blit, NULL); if (s_pl_pack) p_vkDestroyPipelineLayout(s_dev, s_pl_pack, NULL); + if (s_pl_crt) p_vkDestroyPipelineLayout(s_dev, s_pl_crt, NULL); if (s_dpool) p_vkDestroyDescriptorPool(s_dev, s_dpool, NULL); if (s_dsl_tex) p_vkDestroyDescriptorSetLayout(s_dev, s_dsl_tex, NULL); if (s_dsl_pack) p_vkDestroyDescriptorSetLayout(s_dev, s_dsl_pack, NULL); if (s_dsl_blit) p_vkDestroyDescriptorSetLayout(s_dev, s_dsl_blit, NULL); if (s_samp) p_vkDestroySampler(s_dev, s_samp, NULL); + if (s_samp_lin) p_vkDestroySampler(s_dev, s_samp_lin, NULL); if (s_fbo) p_vkDestroyFramebuffer(s_dev, s_fbo, NULL); if (s_rpass) p_vkDestroyRenderPass(s_dev, s_rpass, NULL); if (s_vram_view) p_vkDestroyImageView(s_dev, s_vram_view, NULL); @@ -1375,6 +1535,95 @@ void vk_renderer_shutdown(void) { void vk_renderer_set_present_mode(int mode) { s_present_mode_req = mode; } +void vk_renderer_set_screen_kind(int kind) { + const char *e = getenv("PSX_SCREEN"); /* debug override, like the SW path */ + ScreenKind envk; + if (e && screen_kind_from_name(e, &envk)) kind = (int)envk; + s_crt_kind = (kind >= SCREEN_RAW && kind <= SCREEN_TRINITRON) ? kind : SCREEN_RAW; +} + +/* ---- CRT present pass --------------------------------------------------- */ +/* Lazily build the CRT graphics pipeline (needs s_rp_present, which lives + * with the swapchain, and the modules/layout from create_render_targets). */ +static int crt_pipeline_ensure(void) { + if (s_pipe_crt) return 1; + if (!s_rp_present || !s_mod_crt_v || !s_mod_crt_f || !s_pl_crt) return 0; + VkPipelineShaderStageCreateInfo stages[2] = { + { VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO }, + { VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO } }; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = s_mod_crt_v; stages[0].pName = "main"; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = s_mod_crt_f; stages[1].pName = "main"; + VkPipelineVertexInputStateCreateInfo vin = { VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; + VkPipelineInputAssemblyStateCreateInfo ia = { VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; + ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkPipelineViewportStateCreateInfo vp = { VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; + vp.viewportCount = 1; vp.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo rs = { VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; + rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo ms = { VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; + ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState ba = {0}; + ba.colorWriteMask = 0xF; + VkPipelineColorBlendStateCreateInfo cb = { VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; + cb.attachmentCount = 1; cb.pAttachments = &ba; + VkDynamicState dyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; + VkPipelineDynamicStateCreateInfo dy = { VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; + dy.dynamicStateCount = 2; dy.pDynamicStates = dyn; + VkGraphicsPipelineCreateInfo ci = { VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; + ci.stageCount = 2; ci.pStages = stages; + ci.pVertexInputState = &vin; ci.pInputAssemblyState = &ia; + ci.pViewportState = &vp; ci.pRasterizationState = &rs; + ci.pMultisampleState = &ms; ci.pColorBlendState = &cb; ci.pDynamicState = &dy; + ci.layout = s_pl_crt; ci.renderPass = s_rp_present; + if (p_vkCreateGraphicsPipelines(s_dev, VK_NULL_HANDLE, 1, &ci, NULL, &s_pipe_crt) != VK_SUCCESS) { + s_pipe_crt = VK_NULL_HANDLE; + return 0; + } + return 1; +} + +/* Record the CRT pass into the present cb: clears the whole swapchain image + * black (letterbox bars) and draws the shader-filtered picture into the dst + * rect. src_view must already be in SHADER_READ_ONLY_OPTIMAL; the swapchain + * image must be untouched this frame (pass takes it UNDEFINED -> PRESENT). + * Returns 0 if the pass is unavailable (caller falls back to the blit). */ +static int crt_present_draw(VkCommandBuffer cb, uint32_t img_idx, uint32_t fr, + VkImageView src_view, const VkOffset3D dst[2], + float u0, float v0, float u1, float v1, + float native_w, float native_h) { + if (img_idx >= 8 || !s_sc_fbs[img_idx] || !crt_pipeline_ensure()) return 0; + + VkDescriptorImageInfo ii = { s_samp_lin, src_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; + VkWriteDescriptorSet w = { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; + w.dstSet = s_ds_crt[fr]; w.dstBinding = 0; w.descriptorCount = 1; + w.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w.pImageInfo = ⅈ + p_vkUpdateDescriptorSets(s_dev, 1, &w, 0, NULL); + + VkClearValue clear = {0}; + clear.color.float32[3] = 1.0f; + VkRenderPassBeginInfo bi = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + bi.renderPass = s_rp_present; bi.framebuffer = s_sc_fbs[img_idx]; + bi.renderArea.extent = s_sc_extent; + bi.clearValueCount = 1; bi.pClearValues = &clear; + p_vkCmdBeginRenderPass(cb, &bi, VK_SUBPASS_CONTENTS_INLINE); + + VkViewport view = { (float)dst[0].x, (float)dst[0].y, + (float)(dst[1].x - dst[0].x), (float)(dst[1].y - dst[0].y), + 0.0f, 1.0f }; + VkRect2D sci = { { dst[0].x, dst[0].y }, + { (uint32_t)(dst[1].x - dst[0].x), (uint32_t)(dst[1].y - dst[0].y) } }; + p_vkCmdSetViewport(cb, 0, 1, &view); + p_vkCmdSetScissor(cb, 0, 1, &sci); + p_vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, s_pipe_crt); + p_vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, s_pl_crt, 0, 1, &s_ds_crt[fr], 0, NULL); + CrtPush pc = { { u0, v0, u1, v1 }, { view.width, view.height }, + { native_w, native_h }, s_crt_kind }; + p_vkCmdPushConstants(cb, s_pl_crt, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof pc, &pc); + p_vkCmdDraw(cb, 3, 1, 0, 0); + p_vkCmdEndRenderPass(cb); + return 1; +} + /* ---- present ----------------------------------------------------------- */ /* Acquire a swapchain image, run `record` (which leaves it in * PRESENT_SRC_KHR-ready state), submit, present. record may be NULL for a @@ -1384,8 +1633,10 @@ static int acquire_present(VkImage *out_sc, VkCommandBuffer *out_cb, uint32_t fr = s_frame_idx % VK_FRAMES; p_vkWaitForFences(s_dev, 1, &s_fence[fr], VK_TRUE, UINT64_MAX); uint32_t img_idx = 0; + uint64_t acquire_start = SDL_GetPerformanceCounter(); VkResult r = p_vkAcquireNextImageKHR(s_dev, s_swapchain, UINT64_MAX, s_sem_acquire[fr], VK_NULL_HANDLE, &img_idx); + s_perf_cur.acquire_us += perf_elapsed_us(acquire_start); if (r == VK_ERROR_OUT_OF_DATE_KHR) { p_vkDeviceWaitIdle(s_dev); destroy_swapchain(); @@ -1427,7 +1678,9 @@ static void submit_present(VkCommandBuffer cb, uint32_t img_idx, uint32_t fr) { pi.waitSemaphoreCount = 1; pi.pWaitSemaphores = &s_sem_render[fr]; pi.swapchainCount = 1; pi.pSwapchains = &s_swapchain; pi.pImageIndices = &img_idx; + uint64_t present_start = SDL_GetPerformanceCounter(); p_vkQueuePresentKHR(s_queue, &pi); + s_perf_cur.present_us += perf_elapsed_us(present_start); s_frame_idx++; } @@ -1541,6 +1794,26 @@ int vk_renderer_present_vram(int disp_x, int disp_y, int w, int h, if (!acquire_present(&sc, &cb, &idx, &fr)) return 1; /* frame skipped/recreated */ int S = s_scale; + VkOffset3D dst[2]; + letterbox((int)s_sc_extent.width, (int)s_sc_extent.height, + force_4_3 ? 4 : 4, force_4_3 ? 3 : 3, dst); + + /* Screen simulation: route the present through the CRT shader pass + * instead of the blit. Falls through to the blit if unavailable. */ + if (s_crt_kind != SCREEN_RAW && s_ready) { + vram_to(cb, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + int ok = crt_present_draw(cb, idx, fr, s_vram_view, dst, + disp_x / (float)VRAM_W, disp_y / (float)VRAM_H, + (disp_x + w) / (float)VRAM_W, (disp_y + h) / (float)VRAM_H, + (float)w, (float)h); + vram_to(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); /* re-park */ + if (ok) { + submit_present(cb, idx, fr); + perf_snapshot_present(); + return 1; + } + } + img_barrier(cb, sc, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); @@ -1549,10 +1822,6 @@ int vk_renderer_present_vram(int disp_x, int disp_y, int w, int h, VkImageSubresourceRange rng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; p_vkCmdClearColorImage(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &rng); - VkOffset3D dst[2]; - letterbox((int)s_sc_extent.width, (int)s_sc_extent.height, - force_4_3 ? 4 : 4, force_4_3 ? 3 : 3, dst); - VkImageBlit blit = {0}; blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; blit.srcSubresource.layerCount = 1; @@ -1702,6 +1971,28 @@ int vk_renderer_present_wide(int disp_x, int disp_y, int disp_h, int linear) { if (!acquire_present(&sc, &cb, &idx, &fr)) return 1; /* frame skipped/recreated */ int S = s_scale; + int native_w = s_wide_w - 2 * s_wide_offset; + if (native_w <= 0) native_w = s_wide_w; + VkOffset3D dst[2]; + letterbox((int)s_sc_extent.width, (int)s_sc_extent.height, + 4 * s_wide_w, 3 * native_w, dst); + + /* Screen simulation: CRT shader pass instead of the blit (see + * vk_renderer_present_vram). */ + if (s_crt_kind != SCREEN_RAW) { + img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + int ok = crt_present_draw(cb, idx, fr, s_wide_view[i], dst, + 0.0f, disp_y / (float)VRAM_H, + 1.0f, (disp_y + disp_h) / (float)VRAM_H, + (float)s_wide_w, (float)disp_h); + img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + if (ok) { + submit_present(cb, idx, fr); + perf_snapshot_present(); + return 1; + } + } + img_barrier(cb, sc, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); @@ -1711,12 +2002,6 @@ int vk_renderer_present_wide(int disp_x, int disp_y, int disp_h, int linear) { img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); - int native_w = s_wide_w - 2 * s_wide_offset; - if (native_w <= 0) native_w = s_wide_w; - VkOffset3D dst[2]; - letterbox((int)s_sc_extent.width, (int)s_sc_extent.height, - 4 * s_wide_w, 3 * native_w, dst); - int sy0 = disp_y * S, sy1 = (disp_y + disp_h) * S; if (sy0 < 0) sy0 = 0; if (sy1 > VRAM_H * S) sy1 = VRAM_H * S; @@ -1759,11 +2044,14 @@ int vk_perf_json(char *out, int cap, int count) { uint32_t k = total - n + i; /* frame index, oldest..newest */ VkPerf *p = &s_perf_ring[k % VK_PERF_RING]; o += snprintf(out + o, cap - o, - "%s{\"f\":%u,\"alloc\":%u,\"alloc_kb\":%u,\"oneshot\":%u,\"submit\":%u," + "%s{\"f\":%u,\"qpc\":%llu,\"wait_us\":%u,\"acquire_us\":%u,\"present_us\":%u," + "\"alloc\":%u,\"alloc_kb\":%u,\"oneshot\":%u,\"submit\":%u," "\"sync\":%u,\"pack\":%u,\"blit\":%u,\"upload\":%u,\"copy\":%u," "\"geo\":%u,\"tex\":%u,\"wide\":%u,\"wclr\":%u}", i ? "," : "", - p->present_idx, p->allocs, p->alloc_kb, p->oneshots, p->submits, + p->present_idx, (unsigned long long)p->present_qpc, + p->wait_us, p->acquire_us, p->present_us, + p->allocs, p->alloc_kb, p->oneshots, p->submits, p->syncs, p->pack_flushes, p->blits, p->upload_blocks, p->copy_rects, p->geo_flushes, p->tex_flushes, p->wide_passes, p->wide_clears); if (o >= cap - 256) break; @@ -1864,6 +2152,20 @@ static void bind_masked_stencil_only(VkCommandBuffer cb, int prog, int topo, int /* Host-visible staging buffer (TRANSFER_SRC). */ static int make_staging(VkDeviceSize bytes, VkBuffer *buf, VkDeviceMemory *mem, void **map) { + int best = -1; + for (int i = 0; i < STAGING_CACHE_MAX; i++) { + if (!s_staging_cache[i].busy && s_staging_cache[i].buf && + s_staging_cache[i].size >= bytes && + (best < 0 || s_staging_cache[i].size < s_staging_cache[best].size)) + best = i; + } + if (best >= 0) { + StagingCacheEntry *e = &s_staging_cache[best]; + e->busy = 1; + *buf = e->buf; *mem = e->mem; *map = e->map; + return 1; + } + VkBufferCreateInfo bci = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; bci.size = bytes; bci.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -1877,13 +2179,26 @@ static int make_staging(VkDeviceSize bytes, VkBuffer *buf, VkDeviceMemory *mem, p_vkDestroyBuffer(s_dev, *buf, NULL); return 0; } p_vkBindBufferMemory(s_dev, *buf, *mem, 0); - p_vkMapMemory(s_dev, *mem, 0, bytes, 0, map); + if (p_vkMapMemory(s_dev, *mem, 0, bytes, 0, map) != VK_SUCCESS) { + p_vkDestroyBuffer(s_dev, *buf, NULL); p_vkFreeMemory(s_dev, *mem, NULL); + return 0; + } + for (int i = 0; i < STAGING_CACHE_MAX; i++) { + if (!s_staging_cache[i].buf) { + s_staging_cache[i].buf = *buf; + s_staging_cache[i].mem = *mem; + s_staging_cache[i].size = bytes; + s_staging_cache[i].map = *map; + s_staging_cache[i].busy = 1; + break; + } + } s_perf_cur.allocs++; s_perf_cur.alloc_kb += (uint32_t)(bytes / 1024); return 1; } static void free_staging(VkBuffer buf, VkDeviceMemory mem) { - p_vkDestroyBuffer(s_dev, buf, NULL); p_vkFreeMemory(s_dev, mem, NULL); + staging_release(buf, mem); } /* hr -> raw mirror: pack the dirty rect via the compute pass (top-left sample of @@ -1982,7 +2297,6 @@ static void vram_upload_block(int x, int y, int w, int h, const uint16_t *data) VkBuffer rbuf; VkDeviceMemory rmem; void *rmap; if (make_staging((VkDeviceSize)w * h * 2, &rbuf, &rmem, &rmap)) { memcpy(rmap, data, (size_t)w * h * 2); - p_vkUnmapMemory(s_dev, rmem); VkCommandBuffer cb = begin_oneshot(); img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); VkBufferImageCopy rc = {0}; @@ -2000,7 +2314,6 @@ static void vram_upload_block(int x, int y, int w, int h, const uint16_t *data) if (make_staging((VkDeviceSize)w * h * 4, &ubuf, &umem, &umap)) { uint8_t *m = (uint8_t*)umap; for (int i = 0; i < w * h; i++) rgb555_to_rgba8(data[i], m + (size_t)i * 4); - p_vkUnmapMemory(s_dev, umem); VkCommandBuffer cb = begin_oneshot(); img_to(cb, s_up_img, &s_up_layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); VkBufferImageCopy uc = {0}; @@ -2049,7 +2362,6 @@ static void flush_cpu_upload(void) { if (!make_staging((VkDeviceSize)total * 2, &rbuf, &rmem, &rmap)) return; VkBuffer ubuf; VkDeviceMemory umem; void *umap; if (!make_staging((VkDeviceSize)total * 4, &ubuf, &umem, &umap)) { - p_vkUnmapMemory(s_dev, rmem); defer_staging(rbuf, rmem); return; } @@ -2083,8 +2395,6 @@ static void flush_cpu_upload(void) { * the R16 offset (texoff*2 ≡ 2 mod 4) for every following rect. */ texoff += ((size_t)w * h + 1) & ~(size_t)1; } - p_vkUnmapMemory(s_dev, rmem); - p_vkUnmapMemory(s_dev, umem); /* Submit 1: both image copy sets. */ VkCommandBuffer cb = begin_oneshot(); @@ -2322,7 +2632,7 @@ static int wide_surf_for(int base_x) { int S = s_scale, w = s_wide_w * S, h = VRAM_H * S; if (!make_image(VK_FORMAT_R8G8B8A8_UNORM, w, h, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | - VK_IMAGE_USAGE_TRANSFER_DST_BIT, + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT, &s_wide_img[i], &s_wide_mem[i], &s_wide_view[i])) return -1; if (!make_image(s_ds_format, w, h,