diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33ffc99e..9240f5b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -273,6 +273,11 @@ By contributing, you agree your contribution is licensed under the same license the project. Do not submit code you don't have the right to contribute, and do not copy from incompatible licenses or proprietary sources. +Runtime comments cite hardware facts to PSX-SPX, and emulators are used only as +black-box references whose outputs we compare. See +[`docs/PROVENANCE.md`](docs/PROVENANCE.md) for the rules and for the passages +still to be replaced. + ### Upstream-derived work and credit When a change adapts code or behavior from another branch or repository, add a diff --git a/docs/PROVENANCE.md b/docs/PROVENANCE.md new file mode 100644 index 00000000..4b0bac5d --- /dev/null +++ b/docs/PROVENANCE.md @@ -0,0 +1,76 @@ +# Where runtime code and its facts come from + +This note is for anyone who writes or reviews runtime code (`runtime/src`, +`runtime/include`). It explains which outside sources we use and how, and lists +the places that still need work. Issue #443 tracks the series of changes that +removes the remaining passages. + +## The rules + +1. **Hardware facts are cited to documentation.** When a comment states how the + PlayStation behaves (a register bit, a table, a timing, a quirk), it names + PSX-SPX and the page and section where the fact is written. PSX-SPX is + published at . A page is cited by its name + in the address, for example `graphicsprocessingunitgpu` for + , followed by + the section heading in quotes. +2. **Emulators are black-box references.** We run Beetle PSX, DuckStation, + Mednafen or PCSX-Redux and compare what they output with what we output. + That is how the oracle tools (`psx-beetle`, the trace and diff commands) are + used, and comments may say "the Beetle reference gives the same result". +3. **We do not copy from emulator source.** Do not copy code, tables, constants, + identifiers or the structure of a routine from an emulator's source, and do + not quote emulator source in a comment. Their licences (GPL for Beetle, + Mednafen and PCSX-Redux, CC BY-NC-ND for DuckStation) differ from this + project's, and [`CONTRIBUTING.md`](../CONTRIBUTING.md) already rules out + copying from incompatible licences. +4. **When documentation is silent, measure.** If PSX-SPX does not state a fact, + write a small test program, run it on hardware or on the references, and + record what it shows. Say in the comment that the value was measured and + where the measurement lives. +5. **When PSX-SPX and the code disagree, say so.** Write both in the comment and + leave the behaviour as it is. Changing it is a separate change with its own + evidence. + +## What the first change of the series did + +It changed comments and documentation only; no code changed. + +- Comments that named an emulator's source file, function or line as the + authority for a hardware fact now cite PSX-SPX, where PSX-SPX states the + fact. Each new citation was checked against the PSX-SPX text. +- Two comments quoted emulator source: the I-cache store rule in + `psx_icache.c` and the interrupt line rule in `interrupts.c`. The quotes are + removed. The comments now describe the behaviour and cite PSX-SPX. +- Where PSX-SPX says something different from the code, the comment now says + so (GP0 commands 03h and FFh in `gpu.c`, the I-cache tag value in + `psx_icache.c`). + +## What is still to do + +These passages say that code follows an emulator's source, or they name a +source file, function or line. Rewording would hide that, so they stay as they +are until the code itself is replaced. The order is the one agreed on #443. + +| Change | Files | What the comments say today | +|---|---|---| +| 2. spu | `spu.c`, `spu_gauss.h` | The envelope rate decoder is "ported verbatim" from Beetle's `CalcVCDelta`; envelope steps follow `RunEnvelope`; several Beetle `spu.cpp` line references; the Gaussian table names DuckStation's `spu.cpp`. | +| 3. cpu/icache and memory/cycles | `psx_icache.c`, `psx_icache.h`, `psx_instr_cost.h`, `psx_cyc.h`, `psx_cycles.c`, `memory.c`, `cpu_state.h`, `dirty_ram_interp.c`, `main.cpp`, `docs/internal/CYCLE_MODEL_BEETLE.md` | The fetch, load-timing and I-cache models are "transcribed" from Beetle `cpu.cpp`; MULT/DIV and GTE latencies are transcribed from Beetle; `ReadAbsorb`/`LDWhich` names; the MTC0 software-interrupt check is labelled with a PCSX-Redux function name. | +| 4. mdec | `mdec.c` | The zig-zag table, IDCT matrix load and passes, dequantisation and RGB packing follow Beetle `mdec.cpp` with line references. | +| 5. cdrom | `cdrom.c`, `cdrom.h` | Command latencies (ID, ReadTOC, Pause, Init) and the data-ready notification follow Beetle `PS_CDC` functions; DuckStation `cdrom.cpp` handlers are cited for command cancellation and read restarts. | + +Passages found in areas the agreed series does not cover yet: + +| Area | Files | What the comments say today | +|---|---|---| +| GPU rasteriser | `gpu.c` | The triangle edge stepping cites DuckStation `gpu_sw_rasterizer.inl` (`makefp_xy`, `makestep_xy`). | +| GPU display crop | `gpu.c` | The vertical crop follows DuckStation `GetFullDisplayResolution`. | +| Texture sampling | `gpu_uv.h`, `gpu_sw_renderer.c`, `gpu_gl_renderer.c` | The UV offset model and bilinear filter are described as the Beetle / parallel-psx formulation (`Calc_UVOffsets_Adjust_Verts`). | +| SPU DMA cost | `dma.c` | The 48-cycle per-word cost cites Beetle `dma.cpp` lines. | +| PGXP cache | `gte.cpp` | The vertex cache layout names Beetle `pgxp_gpu.c` and DuckStation `cpu_pgxp.cpp`. | + +Comparisons with the references (what the oracle showed, which emulator agrees), +the oracle tools themselves, and the DuckStation texture-pack file format +(`duckstation_texture_pack.*`, `docs/DUCKSTATION_TEXTURE_FORMAT.md`) are not on +these lists: they compare outputs or read a published file format, which the +rules allow. diff --git a/docs/config_schema.md b/docs/config_schema.md index cdce0fcc..b2989de5 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -702,16 +702,23 @@ player's `settings.toml`, and a game may ship a default the same way: [video] internal_resolution = "4k" # native | 720p | 1080p | 1440p | 4k | 5k | 8k | display, or a number of lines resolution_reference_lines = 240 # game.toml only: the title's usual display height (120..1024) +supersample_disable_at_lines = 0 # game.toml only: supersample 1.0 at/above this display height (0 = off, 240..8640) match_display_max_lines = 0 # game.toml only: cap on the display height Match display uses (0 = none, 240..8640) ``` `match_display_max_lines` caps the monitor height that Match display, its dynamic-resolution floor (`display`) and graphics-preset detection resolve -against. With no cap, an 8K panel asks 18x (27x with `supersample = 1.5`), which is +against. It also caps the final Match display target after supersampling, +so a larger saved factor cannot exceed it. Fixed presets keep their own target. +With no cap, an 8K panel asks 18x (27x with `supersample = 1.5`), which is past the full-VRAM surface. The high-resolution window then engages, and dynamic resolution cannot step there. A title whose GPU budget is set for 4K lines sets 2160. +`supersample_disable_at_lines` uses the physical monitor height before the cap. +At or above its threshold the effective factor is 1.0; the saved factor is +retained for smaller monitors. Zero preserves the default behaviour. + A preset is a target height. The runtime renders at the integer scale `S = ceil(target / resolution_reference_lines)`: at the default 240 lines, 720p is 3x, 1080p is 5x (1200 lines, area-resolved to 1080), 1440p 6x, 4K 9x, diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index e8134784..70cb65a7 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -75a8d9834efb1c994169a939fbe043073b1b4b133bfe7b0235b80338af7f2f08 +61bf4f6d29680623d215fbe407958408cab0a1e08dbda3c62baa25e607d63b7b diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 46161cf4..768350b7 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -780,6 +780,13 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& } rt.video_match_display_max_lines = static_cast(n); } + if (video.contains("supersample_disable_at_lines")) { + const auto n = toml::find(video, "supersample_disable_at_lines"); + if (n != 0 && (n < 240 || n > 8640)) + throw std::runtime_error(fmt::format( + "[video] supersample_disable_at_lines must be 0 or 240..8640: {}", n)); + rt.video_supersample_disable_at_lines = static_cast(n); + } if (video.contains("window_width")) { const auto n = toml::find(video, "window_width"); if (n < 640 || n > 7680) { diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 7fb4375b..1cca1185 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -472,6 +472,8 @@ struct RuntimeConfig { /* [video] match_display_max_lines: cap on the display height Match * display (and its dynamic-resolution floor) resolves against; 0 = none. */ int video_match_display_max_lines = 0; + /* Disable target supersampling on displays at least this tall (0 = off). */ + int video_supersample_disable_at_lines = 0; // Optional initial window width declared by the title profile. Zero keeps // the historical fit-to-display behavior; player settings may override it. diff --git a/recompiler/tests/video_enhancement_settings_test.cpp b/recompiler/tests/video_enhancement_settings_test.cpp index 002bbd8f..cdc5b83a 100644 --- a/recompiler/tests/video_enhancement_settings_test.cpp +++ b/recompiler/tests/video_enhancement_settings_test.cpp @@ -226,14 +226,17 @@ static void test_internal_resolution_game_toml() { "resolution_reference_lines defaults to 240"); check(gc.runtime.video_match_display_max_lines == 0, "match_display_max_lines defaults to 0 (no cap)"); + check(gc.runtime.video_supersample_disable_at_lines == 0, "display SS policy defaults off"); fs::remove(p); p = write_game_toml("psxrecomp_ir_match_cap.toml", "[video]\n" "internal_resolution = \"display\"\n" - "match_display_max_lines = 2160\n"); + "match_display_max_lines = 2160\n" + "supersample_disable_at_lines = 2160\n"); gc = PSXRecompV4::load_game_config(p); check(gc.runtime.video_match_display_max_lines == 2160, "match_display_max_lines parses"); + check(gc.runtime.video_supersample_disable_at_lines == 2160, "display SS policy parses"); fs::remove(p); p = write_game_toml("psxrecomp_ir_4k.toml", @@ -257,6 +260,7 @@ static void test_internal_resolution_game_toml() { "internal_resolution = 1\n", "resolution_reference_lines = 50\n", "match_display_max_lines = 100\n", + "supersample_disable_at_lines = 100\n", "supersampling = 33\n" }) { p = write_game_toml("psxrecomp_ir_bad.toml", std::string("[video]\n") + bad); bool rejected = false; diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index b0347d69..5badda9f 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -26,6 +26,10 @@ if(BUILD_TESTING) # Windows UTF-8 text boundary (issue #371): the helpers legacy Windows # relies on, and — with the shipped manifest — a UTF-8 process code page. if(WIN32) + add_executable(host_time_threads_test tests/test_host_time_threads.c src/host_time.c) + target_include_directories(host_time_threads_test PRIVATE include) + add_test(NAME host_time_threads_test COMMAND host_time_threads_test) + set_tests_properties(host_time_threads_test PROPERTIES TIMEOUT 15) add_executable(win_utf8_helpers_test tests/test_win_utf8.cpp) target_include_directories(win_utf8_helpers_test PRIVATE include) add_test(NAME win_utf8_helpers_test COMMAND win_utf8_helpers_test) diff --git a/runtime/include/internal_resolution.h b/runtime/include/internal_resolution.h index 50e61016..c37dc32a 100644 --- a/runtime/include/internal_resolution.h +++ b/runtime/include/internal_resolution.h @@ -143,6 +143,13 @@ static inline int psx_ss_parse_milli(const char *s) { return (int)m; } +/* Opt-in display policy; keep the saved factor intact for smaller displays. */ +static inline int psx_ir_display_supersample(int ss_milli, int display_px_h, + int disable_at_lines) { + return disable_at_lines > 0 && display_px_h >= disable_at_lines + ? PSX_SS_MILLI_MIN : ss_milli; +} + /* psx_resolve_internal_scale with the supersample factor on the target. */ static inline int psx_resolve_internal_scale_ss(int value, int ref_lines, int display_px_h, int s_max, @@ -160,6 +167,18 @@ static inline int psx_resolve_internal_scale_ss(int value, int ref_lines, return psx_resolve_internal_scale(value, ref_lines, display_px_h, s_max); } +/* A Match display cap bounds the final target, including supersampling. + * Fixed presets and the legacy integer scale retain their own ceilings. */ +static inline int psx_resolve_internal_scale_ss_cap(int value, int ref_lines, + int display_px_h, int s_max, int ss_milli, int max_display_lines) { + int s = psx_resolve_internal_scale_ss(value, ref_lines, display_px_h, s_max, ss_milli); + if (value == PSX_IR_DISPLAY && max_display_lines > 0) { + int cap = psx_resolve_internal_scale(max_display_lines, ref_lines, 0, s_max); + if (s > cap) s = cap; + } + return s; +} + /* A legacy [video] supersampling factor as a value the launcher can show: * the preset that resolves to the same scale (1 -> Native, 3 -> 720p, * 5 -> 1080p, 6 -> 1440p, 9 -> 4K, 12 -> 5K, 18 -> 8K at 240 lines), diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 258e7b97..d77acc18 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -655,7 +655,7 @@ int psx_mod_render_pass(struct CPUState* cpu, const PSXModRenderPass* pass, * transaction. Draws two mattes of the same slice, the first into eye 0 of the * stage surface and the second into eye 1, so both planes of a frame cost one * readback instead of two. Never publishes an eye pair or changes presentation. - * Returns native-sized top-to-bottom RGBA8 pixels (alpha remains the PSX mask + * Returns native-sized bottom-to-top RGBA8 pixels (alpha remains the PSX mask * bit, not coverage). Call outside another pass. rgba_bytes must cover * 2*rect->w*rect->h*4; the first matte arrives in the buffer's first half and * the second in its second half. */ diff --git a/runtime/include/pgxp.h b/runtime/include/pgxp.h index af6bebcc..680703ba 100644 --- a/runtime/include/pgxp.h +++ b/runtime/include/pgxp.h @@ -62,7 +62,8 @@ int pgxp_position_fallback(void); * near geometry. Guest-visible SXY, MAC0 and FLAG are unchanged either way. * Because that position can then differ from the guest integer by more than * the fraction, truncation agreement becomes a window (PGXP_PPP_AGREE_*) and - * a vertex at the GTE's saturation limit must still agree exactly. One + * unclamped IR projections need an intact, value-validated GTE word; halves + * and cache fallbacks keep the strict saturation checks. One * consumer is not visual: [widescreen] precise_nclip takes its branch sign * from these shadows, so in a title that sets both, that (already * non-faithful) widescreen sign follows the exact projection too. */ @@ -91,6 +92,7 @@ int pgxp_preserve_projection(void); * when none). Returns 1 with the position in x16/y16 when the vertex * qualifies (sf=1, IR1/IR2 not saturated, SZ3 not saturated and nonzero, and * no divide overflow: H < 2*SZ3), else 0 and the caller keeps the IR path. */ +/* Return 2 for an opt-in IR-unclamped projection, 1 for ordinary precision. */ int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3, int shift, int32_t ir1, int32_t ir2, uint32_t sz3, uint32_t h, int32_t ofx, int32_t ofy, int64_t x_num, int64_t x_den, @@ -164,6 +166,11 @@ void pgxp_suppress_end(void); /* Called at the RTPS/RTPT projection with the pre-truncation 16.16 screen * coordinates, the projected depth (SZ3), and the packed SXY word the guest * sees. Shifts the shadow FIFO exactly like push_sxy (regs 12..15). */ +/* Tag only immediately after push_sxy of a qualified unclamped projection. */ +void pgxp_gte_mark_unclamped(uint32_t depth); +/* Full positive MAC3 depth of an intact, validated exact-projection word; + * zero when unavailable. The legacy SZ APIs keep their hardware u16 value. */ +float pgxp_get_unclamped_depth(uint32_t addr, uint32_t word); void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, uint32_t packed); /* Host-only homogeneous screen projection, before SZ/SXY/divider saturation. diff --git a/runtime/include/psx_icache.h b/runtime/include/psx_icache.h index e45b9381..96692f95 100644 --- a/runtime/include/psx_icache.h +++ b/runtime/include/psx_icache.h @@ -14,13 +14,13 @@ void psx_icache_reset(void); void psx_icache_fetch(CPUState *cpu, uint32_t addr); void psx_icache_fetch_miss(CPUState *cpu, uint32_t addr); -/* BIU / cache control (0xFFFE0130) bits the cache model reads; names and values - * as Beetle's cpu.cpp BIU_* defines. */ -#define PSX_BIU_ICACHE_ENABLE 0x800u /* BIU_ENABLE_ICACHE_S1 */ -#define PSX_BIU_DCACHE_ENABLE 0x080u /* BIU_ENABLE_DCACHE */ -#define PSX_BIU_TAG_TEST 0x004u /* BIU_TAG_TEST_MODE */ -#define PSX_BIU_INVALIDATE 0x002u /* BIU_INVALIDATE_MODE */ -#define PSX_BIU_LOCK 0x001u /* BIU_LOCK_MODE */ +/* BIU / cache control (0xFFFE0130) bits the cache model reads (PSX-SPX + * memorycontrol, "FFFE0130h - BCC, BIU/Cache Configuration Register"). */ +#define PSX_BIU_ICACHE_ENABLE 0x800u /* bit 11, IS1 */ +#define PSX_BIU_DCACHE_ENABLE 0x080u /* bit 7, DS */ +#define PSX_BIU_TAG_TEST 0x004u /* bit 2, TAG */ +#define PSX_BIU_INVALIDATE 0x002u /* bit 1, INV */ +#define PSX_BIU_LOCK 0x001u /* bit 0, LOCK */ /* A CPU store made while SR.IsC is set, applied to the I-cache model: the * I-cache half of Beetle PS_CPU::WriteMemory's IsC branch. With the I-cache diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index d667ba3f..1d104243 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -1220,8 +1220,9 @@ static void xa_reset_decode(void) { } /* CD-audio volume matrix ([data_source][output_port], 0x80 = unity) applied - * to every decoded XA/CD-DA sample, exactly as the CD controller does on - * real hardware (Beetle PS_CDC::ApplyVolume, cdc.cpp:524). Written via the + * to every decoded XA/CD-DA sample, as the CD controller does on real + * hardware (PSX-SPX cdromdrive, the ATV0..ATV3 volume registers and + * ADPCTL). Written via the * index-2/3 register banks and latched by the "apply changes" bit; games * drive it CONSTANTLY — X5 fades music between scenes with it (measured * 0x7E steady, ramping to 0x00 at transitions on the Beetle oracle). Not @@ -2902,8 +2903,9 @@ uint32_t cdrom_read(uint32_t addr) { case 0x1F801800: { uint8_t s = index_reg & 0x03; /* Bit 2 is ADPBUSY (XA-ADPCM playback in progress), NOT "ADPCM - * empty" — it must idle at 0. The oracle (beetle cdc.cpp, Read - * A==0) never raises it. Raising it permanently made every BIOS + * empty" — it must idle at 0 (PSX-SPX cdromdrive, HSTS: "ADPBUSY + * ADPCM busy (R, 1=playing XA-ADPCM)"; the Beetle reference never + * raises it either). Raising it permanently made every BIOS * status poll see "XA busy" and steered the kernel CD driver * init down a different branch from real hardware. */ if (param_count == 0) s |= (1 << 3); @@ -2981,9 +2983,9 @@ void cdrom_write(uint32_t addr, uint32_t value) { if (index_reg == 2) { cd_pending_vol[0][1] = val; /* Left-CD -> Right-SPU */ } else if (index_reg == 3) { - /* Apply-changes latch (bit 5) copies pending -> live, exactly - * like Beetle cdc.cpp case 0x0B. (ADPMute bit 0 is not modeled - * by the Beetle oracle either.) */ + /* Apply-changes latch (bit 5) copies pending -> live (PSX-SPX + * cdromdrive, ADPCTL bit 5 CHNGATV). (ADPMUTE, bit 0, is not + * modeled; the Beetle reference does not model it either.) */ if (val & 0x20) { memcpy(cd_decode_vol, cd_pending_vol, sizeof(cd_decode_vol)); } diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 913b8a93..77e5e99d 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -9231,6 +9231,12 @@ static void handle_openxr_pose_override(int id,const char *json) { int x=json_get_int(json,"x_mm",0),y=json_get_int(json,"y_mm",0),z=json_get_int(json,"z_mm",0); double pitch_deg=(double)json_get_int(json,"pitch_deg",0) + (double)json_get_int(json,"pitch_mdeg",0)/1000.0; + /* Validate the complete absolute request before changing either override. */ + if(x < -10000 || x > 10000 || y < -10000 || y > 10000 || + z < -10000 || z > 10000 || pitch_deg < -90.0 || pitch_deg > 90.0) { + send_err(id,"pose must be within +/-10 m and pitch within +/-90 degrees");return; + } + if(!psx_openxr_pose_override((double)x/1000.0,(double)y/1000.0,(double)z/1000.0)){ send_err(id,"pose offset must be finite and within +/-10 m");return; } diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index f7c85b4c..0d8856fa 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -2277,8 +2277,9 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, if (rd == 13) { /* Cause register: only the software-interrupt pending bits * [9:8] are writable; hardware IP [15:10], ExcCode [6:2] and - * BD [31] are read-only from software (psx-spx, matches - * PCSX-Redux/Beetle MTC0). */ + * BD [31] are read-only from software (PSX-SPX + * cpuspecifications, "cop0r13 - CAUSE - (Read-only, except, + * Bit8-9 are R/W)"). */ cpu->cop0[13] = (cpu->cop0[13] & ~0x0300u) | (val & 0x0300u); } else { cpu->cop0[rd] = val; diff --git a/runtime/src/dma.c b/runtime/src/dma.c index b60d8add..5bf713a3 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -9,7 +9,7 @@ * request-mode transfers are advanced from the guest cycle clock so games * which synchronize video decode through DMA busy/request state see realistic * backpressure. - * Reference: nocash PSX specs, DuckStation src/core/dma.cpp + * Reference: PSX-SPX, page dmachannels (DMA Channels). */ #include "dma.h" diff --git a/runtime/src/duckstation_texture_pack.cpp b/runtime/src/duckstation_texture_pack.cpp index 12c5e730..e577638f 100644 --- a/runtime/src/duckstation_texture_pack.cpp +++ b/runtime/src/duckstation_texture_pack.cpp @@ -589,14 +589,16 @@ bool finalize_source(DuckTexturePack& p, Source& source, bool keep_observations } return success; } -bool has_residency(const DuckTexturePack& p, const Source* source) { - for (const auto& upload : p.uploads) if (upload.source.get() == source) return true; - for (const auto& page : p.pages) if (page.source.get() == source) return true; - return false; -} void retire_unused(DuckTexturePack& p) { + /* One pass over the residency records, then one over the sources: this + * runs on every VRAM write, and a per-source scan of up to kMaxUploads + * records each cost ~140 ms a frame after a course load filled them. */ + std::unordered_set resident; + resident.reserve(p.uploads.size() + p.pages.size()); + for (const auto& upload : p.uploads) resident.insert(upload.source.get()); + for (const auto& page : p.pages) resident.insert(page.source.get()); for (auto it = p.sources.begin(); it != p.sources.end();) { - if (has_residency(p,it->get())) { ++it; continue; } + if (resident.count(it->get())) { ++it; continue; } if (!finalize_source(p,**it)) { /* Keep failed pending records bounded and visible to the caller. */ p.lifecycle_error = p.lifecycle_error.empty() ? "texture dump could not be finalized" : p.lifecycle_error; @@ -826,11 +828,21 @@ void duck_texture_pack_invalidate(DuckTexturePack* p, uint16_t x, uint16_t y, ui const std::array regions{{{px,py,w0,h0},{0,py,unsigned(w)-w0,h0}, {px,0,w0,unsigned(h)-h0},{0,0,unsigned(w)-w0,unsigned(h)-h0}}}; ++p->revision; + bool released = false; /* a residency record dropped or split */ for (const auto& r : regions) if (r.w && r.h) { + /* Most writes (render targets, CLUT animation) touch no tracked + * upload: skip rebuilding the list then. */ + const bool upload_hit = std::any_of(p->uploads.begin(), p->uploads.end(), + [&](const Upload& u) { const Rect c = intersection(u.active, r); return c.w && c.h; }); + if (upload_hit) { std::vector survivors; survivors.reserve(p->uploads.size()); for (const auto& upload : p->uploads) { const Rect cut = intersection(upload.active,r); - if (!cut.w || !cut.h) { if (survivors.size() < kMaxUploads) survivors.push_back(upload); continue; } + if (!cut.w || !cut.h) { + if (survivors.size() < kMaxUploads) survivors.push_back(upload); else released = true; + continue; + } + released = true; if (upload.splits >= p->config.max_vram_write_splits || contains(r,upload.active)) continue; const Rect a = upload.active; const Rect fragments[] = { @@ -842,12 +854,15 @@ void duck_texture_pack_invalidate(DuckTexturePack* p, uint16_t x, uint16_t y, ui survivors.push_back({upload.source,fragment,upload.splits+1}); } p->uploads.swap(survivors); + } + const size_t pages_before = p->pages.size(); p->pages.erase(std::remove_if(p->pages.begin(),p->pages.end(),[&](const PageSource& page) { if (intersects(page.source->rect,r)) return true; for (const auto& record : page.source->records) if (record.depth < 2 && intersects({record.clut_x,record.clut_y,record.palette_size,1},r)) return true; return false; }),p->pages.end()); + released = released || p->pages.size() != pages_before; p->rect_cache.erase(std::remove_if(p->rect_cache.begin(), p->rect_cache.end(), [&](const RectHash& v){ return intersects(v.rect,r); }), p->rect_cache.end()); p->match_cache.erase(std::remove_if(p->match_cache.begin(), p->match_cache.end(), [&](const MatchCache& c) { const auto& q = c.query; @@ -856,7 +871,8 @@ void duck_texture_pack_invalidate(DuckTexturePack* p, uint16_t x, uint16_t y, ui return intersects(page,r) || (q.depth < 2 && intersects(clut,r)); }), p->match_cache.end()); } - retire_unused(*p); + /* Only a dropped record can leave a source unreferenced. */ + if (released) retire_unused(*p); } catch (...) { /* Allocation/finalization failure must never preserve stale residency * after a native write. Pending immutable observations remain owned. */ diff --git a/runtime/src/frame_trace.c b/runtime/src/frame_trace.c index 7dba7663..4567fbff 100644 --- a/runtime/src/frame_trace.c +++ b/runtime/src/frame_trace.c @@ -18,6 +18,8 @@ #define PSX_FRAME_TRACE_DEFAULT 2048 static PSXFrameTraceRecord s_ring[PSX_FRAME_TRACE_MAX]; +/* An in-progress frame never overwrites the retained committed window. */ +static PSXFrameTraceRecord s_pending; static int s_configured; static int s_enabled; @@ -129,7 +131,7 @@ void psx_frame_trace_reset(void) { } static PSXFrameTraceRecord *open_record(void) { - return &s_ring[(s_opened - 1u) & (uint64_t)(s_cap - 1u)]; + return &s_pending; } static uint64_t stamp_now(void) { @@ -145,7 +147,7 @@ int psx_frame_trace_open(void) { ensure_configured(); if (!s_enabled || s_have_open) return 0; uint64_t seq = s_opened + 1u; - PSXFrameTraceRecord *r = &s_ring[(seq - 1u) & (uint64_t)(s_cap - 1u)]; + PSXFrameTraceRecord *r = &s_pending; memset(r, 0, sizeof *r); r->seq = seq; r->eye_failed = -1; @@ -283,6 +285,7 @@ void psx_frame_trace_commit(void) { } else if (s_have_publish) { r->pair_age_ms = r->open_ms - s_last_publish_ms; } + s_ring[(r->seq - 1u) & (uint64_t)(s_cap - 1u)] = *r; s_committed++; s_have_open = 0; } @@ -331,14 +334,15 @@ static const char *jstr(const char *s, char *tmp, int cap) { int o = 0; if (cap <= 0) return ""; tmp[0] = '\0'; - for (const char *p = s; p && *p && o < cap - 2; ++p) { - unsigned char c = (unsigned char)*p; + for (const unsigned char *p = (const unsigned char *)s; p && *p; ++p) { + const unsigned char c = *p; + const int need = c == '"' || c == '\\' || c == '\n' ? 2 : c < 0x20 ? 6 : 1; + if (need > cap - 1 - o) break; if (c == '"' || c == '\\') { tmp[o++] = '\\'; tmp[o++] = (char)c; } - else if (c == '\n') { tmp[o++] = '\\'; if (o < cap - 1) tmp[o++] = 'n'; } - else if (c < 0x20) { o += snprintf(tmp + o, (size_t)(cap - o), "\\u%04x", c); } + else if (c == '\n') { tmp[o++] = '\\'; tmp[o++] = 'n'; } + else if (c < 0x20) o += snprintf(tmp + o, (size_t)(cap - o), "\\u%04x", c); else tmp[o++] = (char)c; } - if (o > cap - 1) o = cap - 1; tmp[o] = '\0'; return tmp; } diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 478af8be..0e50a7cd 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -7,7 +7,7 @@ * - GP0 command write — ABORTS (not yet implemented) * - GPUREAD — returns last latched value * - * Reference: nocash PSX specs, DuckStation src/core/gpu.cpp + * Reference: PSX-SPX, page graphicsprocessingunitgpu (Graphics Processing Unit). */ #include "gpu.h" @@ -4485,7 +4485,8 @@ void gpu_get_display_info(GpuDisplayInfo* out) { out->depth24 = (int)(display_depth & 1u); out->disabled = (int)display_disabled; - /* Dot-clock divider from GP1(08h) hres (psx-spx / DuckStation). */ + /* Dot-clock divider from GP1(08h) hres (PSX-SPX graphicsprocessingunitgpu, + * "Horizontal Timings", Dotclocks: /10, /8, /7, /5, /4). */ uint32_t cycles; uint32_t mode_w; if (hres2) { @@ -4790,7 +4791,7 @@ static void prepare_precise_triangle(int i0, int i1, int i2, } const int idx[3] = { i0, i1, i2 }; int32_t fx[3], fy[3]; - uint16_t vz[3] = {0, 0, 0}; + float vz[3] = {0, 0, 0}; int any_precise = 0, n_precise = 0; PGXPTriRecord rec; memset(&rec, 0, sizeof rec); @@ -4816,6 +4817,10 @@ static void prepare_precise_triangle(int i0, int i1, int i2, /* PGXP depth (G1.14): only a value-validated dataflow shadow carries * SZ, also when the seam clamp kept the native position. */ vz[i] = sz; + if (sz == 65535u) { + float full = pgxp_get_unclamped_depth(addr, word); + if (full > 0.0f) vz[i] = full; + } /* Native-wide edge recovery is not PGXP: it does not count in n_precise. */ if (native_wide_projection_x(addr, word, raw_x, raw_y, &px)) any_precise = 1; @@ -4828,7 +4833,7 @@ static void prepare_precise_triangle(int i0, int i1, int i2, rec.word[i] = word; rec.x16[i] = fx[i]; rec.y16[i] = fy[i]; - rec.z[i] = vz[i]; + rec.z[i] = sz; /* receipt retains the hardware SZ, not the visual depth */ } if (geometry) { pgxp_note_triangle(n_precise); /* G1.1 crack exposure: mixed share */ @@ -4916,6 +4921,7 @@ static void prepare_texture_triangle(int i0, int i1, int i2) { if (gp0_cmd_source_addr == 0xFFFFFFFFu) { s_texcorr.no_source++; return; } int indices[3] = { i0, i1, i2 }; uint16_t z[3]; + float depth[3]; for (int i = 0; i < 3; i++) { uint32_t addr = GPU_RAM_KEY((gp0_cmd_source_addr + (uint32_t)indices[i] * 4u)); if (!gte_precision_load_word(addr, gp0_cmd_buf[indices[i]], NULL, NULL, &z[i]) || @@ -4923,8 +4929,13 @@ static void prepare_texture_triangle(int i0, int i1, int i2) { s_texcorr.no_depth++; return; } + depth[i] = z[i]; + if (z[i] == 65535u) { + float full = pgxp_get_unclamped_depth(addr, gp0_cmd_buf[indices[i]]); + if (full > 0.0f) depth[i] = full; + } } - float q[3] = { 1.0f / (float)z[0], 1.0f / (float)z[1], 1.0f / (float)z[2] }; + float q[3] = { 1.0f / depth[0], 1.0f / depth[1], 1.0f / depth[2] }; float qmax = q[0]; if (q[1] > qmax) qmax = q[1]; if (q[2] > qmax) qmax = q[2]; @@ -5345,7 +5356,8 @@ static uint16_t current_texpage(void) { * (GPUSTAT bits 0-8) exactly like GP0(E1) bits 0-8 — the poly's own word, not * the last E1, decides its semi-transparency mode, and later rectangle/sprite * prims (which carry no texpage word) consume the state the poly left behind. - * Beetle: SetTPage(CB[4 + ((cc>>4)&1)] >> 16) on every textured poly. + * PSX-SPX graphicsprocessingunitgpu, "TPage Attribute (Parameter for textured polygon + * commands)": bits 0-8 and 11 are the same as GP0(E1h), bits 9-10 unused. * Bits 9-10 (dither / draw-to-display) exist only in E1; bit 11 (texture * disable) only latches when GP1(09h) allowed it, which polys can't grant. */ static void set_tpage_from_poly(uint16_t tpage_word) { @@ -6442,7 +6454,10 @@ static int gp0_command_word_count(uint8_t opcode) { case 0xE6: return 1; /* mask bits */ default: - /* 0x03-0x1E, 0xE7-0xEF, 0xFF: NOP (1 word) per DuckStation */ + /* 0x03-0x1E, 0xE7-0xEF, 0xFF: NOP (1 word). PSX-SPX graphicsprocessingunitgpu, + * "GP0(04h..1Eh,E0h,E7h..EFh) - Mirrors of GP0(00h) - NOP (?)", + * lists 04h-1Eh and E7h-EFh; it calls 03h unknown and does not + * list FFh. This code also treats 03h and FFh as NOPs. */ if ((opcode >= 0x03 && opcode <= 0x1E) || (opcode >= 0xE7 && opcode <= 0xEF) || opcode == 0xFF) { @@ -7580,8 +7595,9 @@ static void gpu_write_gp0_body(uint32_t val) { return; } - /* Polyline terminator rule (Beetle mednafen/psx/gpu.c INCMD_PLINE, - * DuckStation gpu.cpp HandleRenderPolyLineCommand/DrawingPolyLine): + /* Polyline terminator rule (PSX-SPX graphicsprocessingunitgpu, "GPU Render Line + * Commands": at least two vertices; the terminator is tested on the + * first word of a vertex, the colour word when shaded): * * 1. A polyline always has at least two vertices. The words of the first * two vertices are consumed unconditionally — mono [V0][V1], shaded @@ -7845,8 +7861,10 @@ static void gp1_display_mode(uint32_t val) { static void gp1_get_info(uint32_t val) { /* GP1(10h): Get GPU info — writes result to GPUREAD latch. - * Mednafen-psx masks the subcommand to 4 bits (val & 0x0F) and - * services cases 2..5, 7, 8. Tomba's ResetGraph() uses param 7 to + * PSX-SPX graphicsprocessingunitgpu, "GP1(10h) - Read GPU internal + * register" (v2 GPU): indices 10h and up mirror 00h..0Fh, so the + * subcommand is val & 0x0F; 02h..05h, 07h and 08h return data. + * Tomba's ResetGraph() uses param 7 to * read the GPU version (must be 2) to pick its video-mode path — * the wrong value here lands the game on a no-draw branch. */ uint32_t which = val & 0x0F; @@ -7864,10 +7882,10 @@ static void gp1_get_info(uint32_t val) { gpuread_latch = ((uint32_t)draw_offset_x & 0x7FFu) | (((uint32_t)draw_offset_y & 0x7FFu) << 11); break; - case 7: /* GPU version (real-hw + mednafen return 2) */ + case 7: /* GPU version: PSX-SPX gives 1 or 2; this models 2 */ gpuread_latch = 2; break; - case 8: /* unknown info index, real hw / mednafen return 0 */ + case 8: /* unknown info index, returns 0 (PSX-SPX) */ gpuread_latch = 0; break; default: diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 87394aa5..8f69bc9c 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -1816,12 +1816,15 @@ static const char *GEO_VS = " float a = a_col.a, code = floor(a * 0.5), zn = 0.0, w = 1.0;\n" " float m = a - 2.0 * code;\n" " /* + 2*131072: the primitive is dither-eligible (GPU_GEO_DITHER_CODE). */\n" - " float dth = (a >= 0.0 && code >= 131072.0) ? 1.0 : 0.0; code -= 131072.0 * dth; v_dith = int(dth);\n" + " float dth = (a >= 0.0 && code >= 131072.0) ? 1.0 : 0.0; code -= 131072.0 * dth;\n" " float cp = code >= 65536.0 ? 1.0 : 0.0, sz = code - 65536.0 * cp;\n" - " if (a < 0.0) { m = 0.0; zn = 0.9999; sz = 0.0; cp = 0.0; } /* inside the far plane: never clipped */\n" + " bool far_code = a < -1.5;\n" + " if (far_code) { uint bits = floatBitsToUint(-a); m = float(bits & 1u); cp = float((bits >> 1u) & 1u); dth = float((bits >> 2u) & 1u); sz = uintBitsToFloat(bits & 0xfffffff8u) - 2.0; }\n" + " v_dith = int(dth);\n" + " if (a < 0.0 && !far_code) { m = 0.0; zn = 1.0; sz = 0.0; cp = 0.0; }\n" " else if (sz > 0.5) { zn = 1.0 - 512.0 / (max(sz * (1.0 - u_zbias) - (u_zbias > 0.0 ? 48.0 : 0.0), 1.0) + 256.0); if (cp > 0.5) w = sz / 1024.0; }\n" " v_col = vec4(a_col.rgb, m); v_col_p = a_col.rgb; v_cp = int(cp);\n" - " v_b3d = (a >= 0.0 && sz > 0.5) ? 1.0 : 0.0;\n" + " v_b3d = ((a >= 0.0 || far_code) && sz > 0.5) ? 1.0 : 0.0;\n" " float xb = a_pos.x;\n" " if (u_xscale < 0.0) {\n" " float s = -u_xscale; float h = u_xhalf / s;\n" @@ -1918,6 +1921,7 @@ static const char *TEX_VS = " * the vertex stays TEXV floats for every title. */\n" " float pz = a_col.a < 0.0 ? -a_col.a : 0.0;\n" " float cp = pz >= 65536.0 ? 1.0 : 0.0, sz = pz - 65536.0 * cp, zn = 0.0;\n" + " if (a_col.a > 1.5) { uint bits = floatBitsToUint(a_col.a); cp = float((bits >> 1u) & 1u); sz = uintBitsToFloat(bits & 0xfffffff8u) - 2.0; }\n" " if (sz > 0.5) { zn = 1.0 - 512.0 / (max(sz * (1.0 - u_zbias) - (u_zbias > 0.0 ? 48.0 : 0.0), 1.0) + 256.0); if (a_q <= 0.0 && cp > 0.5) w = sz / 1024.0; }\n" " v_cp = int(cp); v_b3d = sz > 0.5 ? 1.0 : 0.0;\n" " vec2 ndc = vec2((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0);\n" @@ -4404,6 +4408,21 @@ static float pgxp_vertex_code(float sz) { return sz + (s_pgxp_cpersp ? 65536.0f : 0.0f); } +/* Depth beyond the hardware SZ range cannot share its old numeric flag + * offsets. Keep that format byte-identical for normal vertices. Far vertices + * carry a finite positive float (depth+2), with three low mantissa bits for + * mask/colour/dither. Integer depths through 2^20 retain every depth bit; + * larger depths lose at most seven float ULPs. Negative GEO / positive TEX + * alpha distinguishes this format from legacy draws and depth-clear (-1). */ +static float pgxp_far_vertex_code(float sz, int mask, int dither) { + float value = sz + 2.0f; + uint32_t bits; + memcpy(&bits, &value, sizeof bits); + bits = (bits & ~7u) | (mask ? 1u : 0u) | (s_pgxp_cpersp ? 2u : 0u) | (dither ? 4u : 0u); + memcpy(&value, &bits, sizeof value); + return value; +} + /* The depth buffer is cleared (beyond every SZ, over the drawing area and * the native-wide margins) by a colourless depth-only draw through the flat * batch, so every surface a draw reaches (the hr surface, the high-resolution @@ -4523,13 +4542,14 @@ static void seam_q(const float B[3][3], float q[3]) { for (int k = 0; k < 3; k++) q[k] = o[k] > 1e-9f ? o[k] : q[k]; } static void seam_sz(const float B[3][3], float z[3]) { + float limit = fmaxf(z[0], fmaxf(z[1], z[2])) > 65535.0f ? 2147483648.0f : 65535.0f; float o[3]; for (int k = 0; k < 3; k++) { float iz = 0.0f; for (int i = 0; i < 3; i++) iz += B[k][i] / z[i]; o[k] = iz > 1e-9f ? 1.0f / iz : z[k]; if (o[k] < 1.0f) o[k] = 1.0f; - if (o[k] > 65535.0f) o[k] = 65535.0f; + if (o[k] > limit) o[k] = limit; } for (int k = 0; k < 3; k++) z[k] = o[k]; } @@ -4741,7 +4761,9 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys, } } for (int i = 0; i < 3; i++) - v0[i * 6 + 5] = mask_a + 2.0f * pgxp_vertex_code(z[i]); + v0[i * 6 + 5] = z[i] > 65535.0f + ? -pgxp_far_vertex_code(z[i], ((int)mask_a & 1), mask_a >= GPU_GEO_DITHER_CODE) + : mask_a + 2.0f * pgxp_vertex_code(z[i]); } if (n == 3) pgxp_tri_log_tri('G', dmode, semi, v0, 6); } @@ -4924,7 +4946,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys, } for (int i = 0; i < 3; i++) { const float code = pgxp_vertex_code(z[i]); - if (code > 0.0f) t0[i * TEXV + 7] = -code; /* a_col.a (TEX_VS) */ + if (z[i] > 65535.0f) t0[i * TEXV + 7] = pgxp_far_vertex_code(z[i], 0, 0); + else if (code > 0.0f) t0[i * TEXV + 7] = -code; /* a_col.a (TEX_VS) */ } } pgxp_tri_log_tri('T', tdmode, semi, &s_tb[s_tb_n * TEXV], TEXV); diff --git a/runtime/src/gpu_vk_renderer.c b/runtime/src/gpu_vk_renderer.c index 6ad261f3..0faa63c6 100644 --- a/runtime/src/gpu_vk_renderer.c +++ b/runtime/src/gpu_vk_renderer.c @@ -688,6 +688,20 @@ static void img_barrier(VkCommandBuffer cb, VkImage img, p_vkCmdPipelineBarrier(cb, src_stage, dst_stage, 0, 0, NULL, 0, NULL, 1, &b); } +/* Clear a swapchain image (TRANSFER_DST_OPTIMAL) to opaque black, then order + * that clear before the next transfer write to the same image. Validation + * reported SYNC-HAZARD-WRITE-AFTER-WRITE for clear -> blit without a barrier; + * on RDNA3 the clear overlapped the blit and tiles of the frame (24-bit FMV + * via vk_renderer_present_cpu) came out black. */ +static void sc_clear_black(VkCommandBuffer cb, VkImage sc) { + VkClearColorValue black = {{0, 0, 0, 1}}; + VkImageSubresourceRange rng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + p_vkCmdClearColorImage(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &rng); + img_barrier(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); +} + /* Map a layout to its (access mask, pipeline stage) for barriers. */ static void layout_access(VkImageLayout l, VkAccessFlags *acc, VkPipelineStageFlags *stage) { switch (l) { @@ -1935,11 +1949,9 @@ int vk_renderer_present_vram(int disp_x, int disp_y, int w, int h, 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); + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); /* clear the swapchain (letterbox bars) to black */ - VkClearColorValue black = {{0,0,0,1}}; - VkImageSubresourceRange rng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - p_vkCmdClearColorImage(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &rng); + sc_clear_black(cb, sc); VkImageBlit blit = {0}; blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; @@ -1968,10 +1980,8 @@ void vk_renderer_present_blank(void) { if (!acquire_present(&sc, &cb, &idx, &fr)) return; 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); - VkClearColorValue black = {{0,0,0,1}}; - VkImageSubresourceRange rng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - p_vkCmdClearColorImage(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &rng); + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + sc_clear_black(cb, sc); finish_present(cb, sc, idx, fr); perf_snapshot_present(); } @@ -2042,10 +2052,8 @@ void vk_renderer_present_cpu(const uint32_t *pixels, int src_w, int src_h, 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); - VkClearColorValue black = {{0,0,0,1}}; - VkImageSubresourceRange rng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - p_vkCmdClearColorImage(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &rng); + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + sc_clear_black(cb, sc); VkOffset3D dst[2]; letterbox((int)s_sc_extent.width, (int)s_sc_extent.height, force_4_3 ? 4 : s_aspect_num, force_4_3 ? 3 : s_aspect_den, dst); @@ -2111,10 +2119,8 @@ int vk_renderer_present_wide(int disp_x, int disp_y, int disp_h, int linear) { 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); - VkClearColorValue black = {{0, 0, 0, 1}}; - VkImageSubresourceRange rng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - p_vkCmdClearColorImage(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &rng); + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + sc_clear_black(cb, sc); img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 7bf9998e..7c9c36ce 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -34,8 +34,8 @@ namespace GTE { // // The real PS1 GTE does NOT compute an exact H*0x20000/SZ3. It uses an // Unsigned Newton-Raphson (UNR) reciprocal approximation driven by a 257-entry -// seed table (documented in PSX-SPX "GTE Division Inaccuracy"; identical to the -// mednafen/Beetle oracle in beetle-psx/mednafen/psx/gte.cpp). Exact division +// seed table (documented in PSX-SPX geometrytransformationenginegte, "GTE Division +// Inaccuracy"; the Beetle reference gives the same results). Exact division // diverges from hardware by +/-1 (occasionally up to a few units) on ~25% of // inputs, which is enough to flip games' distance/intensity threshold branches // (e.g. Ape Escape's additive-glow CLUT semi-transparency bit). This is the @@ -1082,7 +1082,7 @@ static int32_t gte_h_scaled(const GTEState* gte) { // --------------------------------------------------------------------------- // RTPS — Perspective Transformation (internal, operates on given vertex V) // -// Matches DuckStation/Beetle (psx-spx): +// PSX-SPX geometrytransformationenginegte, "COP2 0180001h - 15 Cycles - RTPS": // MAC1/2/3 = (TR*1000h + RT*V) SAR (sf*12) // IR1/IR2 = limB(MAC, lm) // IR3 FLAG = limB(MAC3_unshifted SAR 12, lm=0); stored IR3 = limB(MAC3, lm) @@ -1140,7 +1140,7 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr) gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm); gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm); - // IR3 quirk (psx-spx / DuckStation): FLAG.22 from (mac3>>12) as if lm=0; + // IR3 quirk (PSX-SPX, RTPS note): FLAG.22 from (mac3>>12) as if lm=0; // stored IR3 clamps MAC3 with the real lm bit and does not touch FLAG again. (void)gte->saturate_ir(static_cast(mac3 >> 12), 3, false); { @@ -1256,6 +1256,7 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr) int64_t cx16 = sx16 < -kLim ? -kLim : (sx16 > kLim - 1 ? kLim - 1 : sx16); int64_t cy16 = sy16 < -kLim ? -kLim : (sy16 > kLim - 1 ? kLim - 1 : sy16); int32_t px16 = (int32_t)cx16, py16 = (int32_t)cy16; + bool unclamped_projection = false; /* Preserve projection precision (G1.11): shadow the exact projection * instead when the vertex qualifies and it lies inside the GPU's * truncation-agreement window; outside it the IR path's shadow is @@ -1265,15 +1266,18 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr) * passes), where the shadow would be dropped anyway. */ if (!proj_override && pgxp_preserve_projection() && pgxp_active()) { int32_t ex16, ey16; - if (pgxp_project_precise(mac1, mac2, mac3, shift, gte->IR1, gte->IR2, - gte->SZ[3], gte_h_scaled(gte), gte->OFX, gte->OFY, - x_num, x_den, &ex16, &ey16) && - pgxp_ppp_accept(ex16, ey16, (uint32_t)gte->SXY[2])) { + const int precise = pgxp_project_precise(mac1, mac2, mac3, shift, + gte->IR1, gte->IR2, gte->SZ[3], gte_h_scaled(gte), + gte->OFX, gte->OFY, x_num, x_den, &ex16, &ey16); + if (precise == 2 || (precise == 1 && + pgxp_ppp_accept(ex16, ey16, (uint32_t)gte->SXY[2]))) { + unclamped_projection = precise == 2; px16 = ex16; py16 = ey16; } } pgxp_gte_push_sxy(px16, py16, gte->SZ[3], (uint32_t)gte->SXY[2]); + if (unclamped_projection) pgxp_gte_mark_unclamped((uint32_t)(mac3 >> 12)); if (!proj_override && pgxp_projection_tracking() && shift == 12 && !lm) { const double z = (double)(mac3 >> 12); const double h_proj = (double)gte_h_scaled(gte); @@ -1566,7 +1570,7 @@ void gte_nclip(GTEState* gte, uint32_t instr) { // --------------------------------------------------------------------------- // AVSZ3 (0x2D) — Average Z (3 points) // --------------------------------------------------------------------------- -// Hardware (psx-spx / DuckStation): MAC0 keeps ZSF3*(SZ1+SZ2+SZ3); only OTZ +// Hardware (PSX-SPX, "AVSZ3"): MAC0 keeps ZSF3*(SZ1+SZ2+SZ3); only OTZ // gets MAC0/1000h. CTR NdIntroWoodEmitHelper indexes the batch OT with // MAC0>>17 (== OTZ>>5). Shifting MAC0 here collapsed every face into slot 0 // (arms/trophy/chest z-fight + missing banner under later same-bucket paint). @@ -1793,8 +1797,10 @@ void gte_mvmva(GTEState* gte, uint32_t instr) { case 1: std::memcpy(M, gte->L, sizeof(M)); break; case 2: std::memcpy(M, gte->LC, sizeof(M)); break; default: { - /* Undocumented matrix selector wiring, verified by hardware - * register vectors (also used by Beetle/DuckStation). */ + /* Matrix selector 3: PSX-SPX geometrytransformationenginegte, + * "MVMVA": "Setting Mx=3 selects a garbage matrix" (-R*10h, + * +R*10h, IR0, RT13 x3, RT22 x3), verified by hardware + * register vectors. */ const int16_t color = static_cast( static_cast(gte->RGBC & 0xFFu) << 4); M[0][0] = static_cast(-color); diff --git a/runtime/src/host_time.c b/runtime/src/host_time.c index c70cd94b..4fb5f435 100644 --- a/runtime/src/host_time.c +++ b/runtime/src/host_time.c @@ -21,31 +21,49 @@ #endif static LARGE_INTEGER s_qpc_freq; -static int s_qpc_freq_init; -static HANDLE s_sleep_timer; -static int s_sleep_timer_init; +static INIT_ONCE s_time_once = INIT_ONCE_STATIC_INIT; +static DWORD s_sleep_timer_slot = FLS_OUT_OF_INDEXES; -static void sleep_timer_ensure(void) +static void CALLBACK sleep_timer_close(void *timer) { - if (s_sleep_timer_init) - return; - s_sleep_timer_init = 1; - s_sleep_timer = + if (timer) CloseHandle((HANDLE)timer); +} + +static BOOL CALLBACK time_initialize(PINIT_ONCE once, PVOID parameter, PVOID *context) +{ + (void)once; (void)parameter; (void)context; + if (!QueryPerformanceFrequency(&s_qpc_freq)) s_qpc_freq.QuadPart = 0; + /* An auto-reset timer has one pending deadline and wakes one waiter. + * Sharing it lets a render/present worker cancel the guest pacer's wait. + * FLS also releases the handle when its thread/guest fiber is destroyed. */ + s_sleep_timer_slot = FlsAlloc(sleep_timer_close); + return TRUE; +} + +static HANDLE sleep_timer_get(void) +{ + HANDLE timer; + if (!InitOnceExecuteOnce(&s_time_once, time_initialize, NULL, NULL) || + s_sleep_timer_slot == FLS_OUT_OF_INDEXES) + return NULL; + timer = (HANDLE)FlsGetValue(s_sleep_timer_slot); + if (timer) return timer; + timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); - if (!s_sleep_timer) - s_sleep_timer = CreateWaitableTimerW(NULL, FALSE, NULL); + if (!timer) timer = CreateWaitableTimerW(NULL, FALSE, NULL); + if (timer && !FlsSetValue(s_sleep_timer_slot, timer)) { + CloseHandle(timer); + return NULL; + } + return timer; } uint64_t psx_host_mono_ms(void) { LARGE_INTEGER counter; - if (!s_qpc_freq_init) { - if (!QueryPerformanceFrequency(&s_qpc_freq)) - s_qpc_freq.QuadPart = 0; - s_qpc_freq_init = 1; - } + InitOnceExecuteOnce(&s_time_once, time_initialize, NULL, NULL); if (s_qpc_freq.QuadPart == 0) return (uint64_t)GetTickCount64(); if (!QueryPerformanceCounter(&counter)) @@ -57,18 +75,21 @@ void psx_host_sleep_micros(unsigned usec) { LARGE_INTEGER due; LONGLONG hundred_ns; + HANDLE timer; if (usec == 0u) return; - sleep_timer_ensure(); - if (s_sleep_timer) { + timer = sleep_timer_get(); + if (timer) { hundred_ns = -((LONGLONG)usec * 10LL); if (hundred_ns >= 0) hundred_ns = -1LL; due.QuadPart = hundred_ns; - if (SetWaitableTimer(s_sleep_timer, &due, 0, NULL, NULL, FALSE)) { - (void)WaitForSingleObject(s_sleep_timer, INFINITE); + /* Bounded: a missed signal costs at most the sleep plus 2 ms, never + * an indefinitely parked thread. */ + if (SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE)) { + (void)WaitForSingleObject(timer, (DWORD)(usec / 1000u) + 2u); return; } } diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index 275c9a02..c229733b 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -145,14 +145,13 @@ extern uint32_t i_mask; * again sees a pending interrupt that no longer exists and can spin in its * event scan forever. * - * Verified against the independent Beetle oracle rather than asserted: - * beetle-psx/mednafen/psx/irq.cpp defines - * #define Recalc() PSX_CPU->AssertIRQ(0, (bool)(Status & Mask)) - * and calls it from IRQ_Assert (raise), from IRQ_Write for BOTH the Status ack - * and the Mask write, and at power-on; cpu.cpp's AssertIRQ clears bit (10+n) - * unconditionally and re-sets it only when the level is asserted. So the line - * is recomputed at every point (I_STAT & I_MASK) can change, which is exactly - * the set of call sites below. + * PSX-SPX interrupts, "PSX specific COP0 Notes": cop0r13.bit10 "is NOT a + * latch, ie. it gets automatically cleared as soon as (I_STAT AND + * I_MASK)=zero", and "Interrupt Request / Execution": it is set while + * (I_STAT AND I_MASK) is nonzero. So the bit is recomputed at every point + * (I_STAT & I_MASK) can change: a raise, a Status ack, a Mask write and + * power-on, which is exactly the set of call sites below. The Beetle + * reference behaves the same. * * Ownership: this function is the ONLY writer of CAUSE bit 10. The delivery * path no longer ORs it in separately — one owner, no divergence. @@ -173,7 +172,7 @@ void psx_irq_refresh_cause_ip2(void) void psx_irq_set_cause_ptr(uint32_t *p) { s_cause_ptr = p; - /* Power-on recompute, mirroring Beetle's IRQ_Power() -> Recalc(). Without + /* Power-on recompute (the Beetle reference also recomputes here). Without * this the first mirror only happens at the first raise/ack, so a CAUSE * read before any interrupt activity would show a stale bit. */ psx_irq_refresh_cause_ip2(); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 484a98f8..6bf5eea9 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1272,6 +1272,11 @@ static int g_video_ss_milli = 1000; /* [video] supersample / PSX_SUPER * full-VRAM surface, where the high-resolution window engages and dynamic * resolution cannot step; a title can cap it at, say, 4K lines. */ static int g_video_match_max_lines = 0; +static int g_video_ss_disable_at_lines = 0; +static int display_supersample(SDL_Window *window) { + return psx_ir_display_supersample(g_video_ss_milli, + psx_sdl_display_pixel_height(window), g_video_ss_disable_at_lines); +} static int match_display_px_h(SDL_Window *window) { const int h = psx_sdl_display_pixel_height(window); return (g_video_match_max_lines > 0 && h > g_video_match_max_lines) @@ -1454,9 +1459,8 @@ static int effective_internal_resolution(void) { static void apply_internal_resolution(int display_px_h) { const int preset = effective_internal_resolution(); if (preset == PSX_IR_UNSET) return; - g_video_scale = psx_resolve_internal_scale_ss(preset, g_video_ref_lines, - display_px_h, video_scale_ceiling(), - g_video_ss_milli); + g_video_scale = psx_resolve_internal_scale_ss_cap(preset, g_video_ref_lines, + display_px_h, video_scale_ceiling(), display_supersample(nullptr), g_video_match_max_lines); } /* The value the launcher row starts on: the preset, or the legacy factor shown @@ -17860,6 +17864,7 @@ int main(int argc, char** argv) { g_video_internal_res = gc.runtime.video_internal_resolution; g_video_ref_lines = gc.runtime.video_resolution_reference_lines; g_video_match_max_lines = gc.runtime.video_match_display_max_lines; + g_video_ss_disable_at_lines = gc.runtime.video_supersample_disable_at_lines; g_video_dynres = gc.runtime.video_dynamic_resolution ? 1 : 0; g_video_dynres_min = gc.runtime.video_dynamic_resolution_min; if (gc.runtime.video_window_width > 0) { @@ -20353,9 +20358,10 @@ session_reboot: else if (!std::strcmp(e, "fxaa_hq")) g_video_post_aa = GL_POST_AA_FXAA_HQ; else std::fprintf(stdout, "psxrecomp: PSX_AA_MODE=%s not understood (off, fxaa, fxaa_hq)\n", e); } - if (g_video_ss_milli != 1000) + const int effective_ss = display_supersample(nullptr); + if (!g_video_ss_disable_at_lines && effective_ss != 1000) std::fprintf(stdout, "psxrecomp: supersample %.3gx on the internal-resolution target\n", - g_video_ss_milli / 1000.0); + effective_ss / 1000.0); apply_internal_resolution(match_display_px_h(nullptr)); if (g_video_scale < 1) g_video_scale = 1; if (const char* e = std::getenv("PSX_DYNRES")) @@ -20996,17 +21002,22 @@ session_reboot: /* Match display: the monitor's pixel height, now that the window says * which monitor. glb_set_scale only records the request; the hr * surface is allocated at context init below. */ - if (effective_internal_resolution() == PSX_IR_DISPLAY && g_video_scale_applies) { + const int ir = effective_internal_resolution(); + if (g_video_scale_applies && ir != PSX_IR_UNSET && + (ir == PSX_IR_DISPLAY || g_video_ss_disable_at_lines > 0)) { const int dh = match_display_px_h(sdl_window); - const int s = psx_resolve_internal_scale_ss(PSX_IR_DISPLAY, g_video_ref_lines, - dh, GL_MAX_INTERNAL_SCALE, - g_video_ss_milli); + const int ss = display_supersample(sdl_window); + const int s = psx_resolve_internal_scale_ss_cap(ir, g_video_ref_lines, + dh, GL_MAX_INTERNAL_SCALE, ss, g_video_match_max_lines); gr_set_scale(s); g_video_requested_scale = s; - std::fprintf(stdout, "psxrecomp: internal resolution Match display: " - "%d px -> %dx requested\n", dh, s); + const char *label = psx_ir_label_for(ir); + std::fprintf(stdout, "psxrecomp: internal resolution %s: " + "%d px -> %dx requested (supersample %.3gx)\n", + label ? label : "custom", dh, s, ss / 1000.0); } gl_renderer_set_swap_interval(present_effective_swap_interval()); /* applied at context init */ + /* Dynamic resolution: the surfaces are allocated at the scale above * (the ceiling) and the level steps under it (dynres_setup). */ #ifndef PSX_NO_DEBUG_TOOLS diff --git a/runtime/src/memcard.c b/runtime/src/memcard.c index 8ff08baa..d3ba4b45 100644 --- a/runtime/src/memcard.c +++ b/runtime/src/memcard.c @@ -173,7 +173,9 @@ static uint8_t frame_checksum(const uint8_t *frame) { } /* Format a 128KB card image to match a real PS1 blank card. - * Layout verified against DuckStation's MemoryCardImage::Format(). */ + * Layout per PSX-SPX controllersandmemorycards, "Memory Card Data Format": + * header frame, directory frames (free A0h, next FFFFh), broken sector + * list (FFFFFFFFh = none) and the write test frame 63. */ static void memcard_format(uint8_t *data) { memset(data, 0xFF, MEMCARD_SIZE); diff --git a/runtime/src/memory.c b/runtime/src/memory.c index f994d3cc..53b3c460 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -1689,8 +1689,8 @@ uint32_t psx_read_word(uint32_t addr) { } /* Physical address of a CPU/DMA main-RAM access. Fold KUSEG/KSEG0/KSEG1 first * (0x1FFFFFFF), then fold the 2nd-4th main-RAM mirrors: real hardware mirrors the - * 2 MB DRAM across the WHOLE 0..0x7FFFFF physical window (Beetle libretro.cpp:874 - * `A < 0x00800000` routes to main RAM; psx-spx "2048K RAM ... mirrored 4x"). A game + * 2 MB DRAM across the WHOLE 0..0x7FFFFF physical window (PSX-SPX memorymap, + * "Memory Mirrors": "2MB RAM can be mirrored to the first 8MB"). A game * may legitimately place its stack at the top of that window — Tsumu Light computes * sp = (ramtop-8)|0x80000000 with its ramtop constant 0x00800000, giving sp=0x807FFFF8 * (top of the 4th mirror). Without this fold those accesses miss DRAM (`< RAM_SIZE` @@ -1701,9 +1701,9 @@ static uint32_t psx_read_word_raw(uint32_t addr) { /* KSEG2 cache control — before physical translation. */ if (addr == 0xFFFE0130u) return cache_ctrl; /* KSEG2 (0xC0000000+): only cache control (0xFFFE0130, above where - * applicable) exists there. Real hardware maps NOTHING else — Beetle - * (cpu.cpp addr_mask[6..7]=0xFFFFFFFF) leaves KSEG2 addresses unmasked - * so they fall to unmapped space and the access is a no-op. Our flat + * applicable) exists there. PSX-SPX memorymap ("Memory Map") lists only + * the cache control registers at FFFE0000h in KSEG2; every other KSEG2 + * access is unmapped and is a no-op here, as on the Beetle reference. Our flat * 0x1FFFFFFF masking routed KSEG2 garbage onto LIVE registers: Tomba2's * attract runs a BIOS bzero over a wild 0xDF80xxxx pointer, which zeroed * the SIO/memctrl I/O block byte-by-byte and then hit the unmapped-MMIO diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index b5ea3d29..6eb0abeb 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -63,6 +63,7 @@ enum { * precise value may sit up to one pixel below or two above it. */ PGXP_F_DX = 1u << 4, PGXP_F_DY = 1u << 5, + PGXP_F_UNCLAMPED = 1u << 6, /* exact GTE projection; intact word only */ PGXP_F_VXY = PGXP_F_VX | PGXP_F_VY, }; @@ -574,7 +575,7 @@ static inline int pv_live(const PGXPValue *pv) { static inline void pv_validate(PGXPValue *pv, uint32_t actual) { if (pv->gen != s_gen) return; uint32_t diff = pv->value ^ actual; - if (diff) pv->flags &= (uint16_t)~PGXP_F_PROJECTION; + if (diff) pv->flags &= (uint16_t)~(PGXP_F_PROJECTION | PGXP_F_UNCLAMPED); if ((pv->flags & PGXP_F_VX) && (diff & 0x0000FFFFu)) pv->flags &= (uint16_t)~(PGXP_F_VX | PGXP_F_DX | PGXP_F_VZ); if ((pv->flags & PGXP_F_VY) && (diff & 0xFFFF0000u)) @@ -719,7 +720,7 @@ extern "C" void psx_pgxp_load(struct CPUState *cpu, uint32_t instr, * cpu-mode keeps working. */ dst->y16 = ((int32_t)value >> 16) << 16; dst->flags = PGXP_F_VY; - if (src && src->gen == s_gen) { + if (src && src->gen == s_gen && !(src->flags & PGXP_F_UNCLAMPED)) { uint32_t actual_half = (value & 0xFFFFu) << (hi_half ? 16 : 0); uint32_t mask = hi_half ? 0xFFFF0000u : 0x0000FFFFu; uint16_t want = hi_half ? PGXP_F_VY : PGXP_F_VX; @@ -791,7 +792,7 @@ extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr, if (hi_half) dst->value = (dst->value & 0x0000FFFFu) | (half << 16); else dst->value = (dst->value & 0xFFFF0000u) | half; dst->flags &= (uint16_t)~((hi_half ? (PGXP_F_VY | PGXP_F_DY) : (PGXP_F_VX | PGXP_F_DX)) | - PGXP_F_VZ | PGXP_F_PROJECTION); + PGXP_F_VZ | PGXP_F_PROJECTION | PGXP_F_UNCLAMPED); dst->z = 0; int ok = 0, derived = 0; int32_t v16 = 0; @@ -805,7 +806,8 @@ extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr, * coordinate half here. */ if (hi_half) { dst->y16 = v16; dst->flags |= (uint16_t)(PGXP_F_VY | (derived ? PGXP_F_DY : 0)); } else { dst->x16 = v16; dst->flags |= (uint16_t)(PGXP_F_VX | (derived ? PGXP_F_DX : 0)); } - } else if (src && src->gen == s_gen && (src->flags & PGXP_F_VX) && + } else if (src && src->gen == s_gen && !(src->flags & PGXP_F_UNCLAMPED) && + (src->flags & PGXP_F_VX) && ((src->value ^ value) & 0xFFFFu) == 0) { const uint16_t d = (src->flags & PGXP_F_DX) ? (hi_half ? PGXP_F_DY : PGXP_F_DX) : 0; if (hi_half) { dst->y16 = src->x16; dst->flags |= (uint16_t)(PGXP_F_VY | d); } @@ -820,7 +822,7 @@ extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr, dst->value = (dst->value & ~(0xFFu << shift)) | ((value & 0xFFu) << shift); dst->flags &= (uint16_t)~(((addr & 2u) ? (PGXP_F_VY | PGXP_F_DY) : (PGXP_F_VX | PGXP_F_DX)) | - PGXP_F_VZ | PGXP_F_PROJECTION); + PGXP_F_VZ | PGXP_F_PROJECTION | PGXP_F_UNCLAMPED); dst->z = 0; return; } @@ -960,7 +962,7 @@ extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr, * sub-pixel value; an untracked half contributes its exact integer value * (adding an exact offset to a tracked coordinate keeps the fraction). */ static inline int32_t comp16(const PGXPValue *pv, uint32_t value, int hi_half) { - if (pv && pv->gen == s_gen) { + if (pv && pv->gen == s_gen && !(pv->flags & PGXP_F_UNCLAMPED)) { if (!hi_half && (pv->flags & PGXP_F_VX) && ((pv->value ^ value) & 0x0000FFFFu) == 0) return pv->x16; @@ -974,7 +976,7 @@ static inline int32_t comp16(const PGXPValue *pv, uint32_t value, int hi_half) { /* The derived-half marks (PGXP_F_DX / DY) an operand contributes through * comp16: a tracked derived half keeps its rounding window in the sum. */ static inline uint16_t derived_bits(const PGXPValue *pv, uint32_t value) { - if (!pv || pv->gen != s_gen) return 0; + if (!pv || pv->gen != s_gen || (pv->flags & PGXP_F_UNCLAMPED)) return 0; uint16_t d = 0; if ((pv->flags & PGXP_F_DX) && (pv->flags & PGXP_F_VX) && ((pv->value ^ value) & 0x0000FFFFu) == 0) d |= PGXP_F_DX; @@ -984,7 +986,7 @@ static inline uint16_t derived_bits(const PGXPValue *pv, uint32_t value) { } static inline int comp_tracked(const PGXPValue *pv, uint32_t value, int hi_half) { - if (!pv || pv->gen != s_gen) return 0; + if (!pv || pv->gen != s_gen || (pv->flags & PGXP_F_UNCLAMPED)) return 0; if (!hi_half) return (pv->flags & PGXP_F_VX) && ((pv->value ^ value) & 0x0000FFFFu) == 0; @@ -1527,13 +1529,26 @@ extern "C" int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3, int64_t x_num, int64_t x_den, int32_t *x16, int32_t *y16) { /* Qualify exactly the vertices whose guest projection is the plain - * IR * (H / SZ3) of the continuous one: sf=1, no IR1/IR2 clamp (the - * stored IR equals MAC >> 12), SZ3 unclamped and nonzero, and no UNR + * IR * (H / SZ3) of the continuous one: sf=1, verified IR1/IR2 conversion, SZ3 unclamped and nonzero, and no UNR * divide overflow (the GTE saturates the quotient when H >= 2*SZ3). */ if (shift != 12 || x_den <= 0) return 0; - if ((mac1 >> 12) != (int64_t)ir1 || (mac2 >> 12) != (int64_t)ir2) return 0; + const int64_t ix = mac1 >> 12, iy = mac2 >> 12; const int64_t z_int = mac3 >> 12; - if (z_int <= 0 || z_int > 0xFFFF || (uint32_t)z_int != sz3) return 0; + if (z_int <= 0) return 0; + const bool xy_clamped = ix != ir1 || iy != ir2; + const bool z_clamped = z_int > 0xFFFF; + const bool unclamped = xy_clamped || z_clamped; + if (unclamped) { + /* Only the opt-in exact projection may bypass IR saturation. Reject + * wrapped accumulators, lm=1 sign clamps and every other mismatch. */ + const int64_t lo = -(INT64_C(1) << 43), hi = (INT64_C(1) << 43) - 1; + const int64_t cx = ix < -32768 ? -32768 : ix > 32767 ? 32767 : ix; + const int64_t cy = iy < -32768 ? -32768 : iy > 32767 ? 32767 : iy; + if (!s_preserve_projection || mac1 < lo || mac1 > hi || + mac2 < lo || mac2 > hi || mac3 < lo || mac3 > hi || + cx != ir1 || cy != ir2) return 0; + } + if ((uint32_t)(z_clamped ? 0xFFFF : z_int) != sz3) return 0; if ((int64_t)h >= 2 * z_int) return 0; /* (MAC/4096) * H / (MAC3/4096): the 4096s cancel. Double keeps ~52 bits, * far below the 1/65536 px the shadow carries. Host-only and visual-only, @@ -1551,7 +1566,7 @@ extern "C" int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3, if (fy > lim - 1.0) fy = lim - 1.0; *x16 = (int32_t)std::floor(fx); *y16 = (int32_t)std::floor(fy); - return 1; + return unclamped ? 2 : 1; } /* Defined below with the GPU consumer. */ @@ -1585,6 +1600,21 @@ extern "C" void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, s_gte[15] = *pv; /* SXYP mirrors SXY2 */ } +extern "C" void pgxp_gte_mark_unclamped(uint32_t depth) { + if (!g_pgxp_active) return; + s_gte[14].flags |= PGXP_F_UNCLAMPED; + s_gte[14].projection.z = (float)depth; + s_gte[15] = s_gte[14]; +} + +extern "C" float pgxp_get_unclamped_depth(uint32_t addr, uint32_t word) { + if (!g_pgxp_active || !s_preserve_projection) return 0.0f; + const PGXPValue *pv = pgxp_ptr(addr); + if (!pv || pv->gen != s_gen || pv->value != word || + !(pv->flags & PGXP_F_UNCLAMPED) || !(pv->flags & PGXP_F_VZ)) return 0.0f; + return pv->projection.z; +} + extern "C" int pgxp_get_gte_sxy(uint32_t index, int32_t *x16, int32_t *y16) { return pgxp_get_gte_sxy_checked(index, 0u, 0, x16, y16); } @@ -1718,8 +1748,12 @@ static inline int pgxp_accept(int32_t px, int32_t py, int32_t int_x, int32_t int_y, uint32_t word, uint16_t flags) { const int dx_derived = (flags & PGXP_F_DX) != 0; const int dy_derived = (flags & PGXP_F_DY) != 0; - if (!pgxp_agrees(px, int_x, (int16_t)(word & 0xFFFFu), dx_derived) || - !pgxp_agrees(py, int_y, (int16_t)(word >> 16), dy_derived)) + /* This tag can only ride an intact, value-validated GTE word. Arithmetic + * constructs new flags and loses it; the position-cache tier never has it. + * Clamped IR is an integer hardware limit, not the enhanced visual vertex. */ + if (!(s_preserve_projection && (flags & PGXP_F_UNCLAMPED)) && + (!pgxp_agrees(px, int_x, (int16_t)(word & 0xFFFFu), dx_derived) || + !pgxp_agrees(py, int_y, (int16_t)(word >> 16), dy_derived))) return 1; if (s_tolerance >= 0.0f) { if (pgxp_moved(px, int_x, dx_derived) > s_tolerance || diff --git a/runtime/src/psx_icache.c b/runtime/src/psx_icache.c index 60749aa4..1aa40992 100644 --- a/runtime/src/psx_icache.c +++ b/runtime/src/psx_icache.c @@ -108,23 +108,19 @@ void psx_icache_fetch_miss(CPUState* cpu, uint32_t addr) { #endif } -/* IsC store -> I-cache, transcribed from Beetle PS_CPU::WriteMemory - * (mednafen/psx/cpu.cpp:484-501): +/* IsC store -> I-cache. This function was transcribed from Beetle's + * PS_CPU::WriteMemory; it is to be replaced by the cpu/icache change + * tracked in issue #443 (docs/PROVENANCE.md). * - * if(BIU & BIU_ENABLE_ICACHE_S1) { - * if(BIU & (BIU_TAG_TEST_MODE | BIU_INVALIDATE_MODE | BIU_LOCK_MODE)) { - * valid_bits = (BIU & BIU_TAG_TEST_MODE) ? ((value << ((address & 0x3) * 8)) & 0x0F) : 0x00; - * ICI = &ICache[(address & 0xFF0) >> 2]; - * for(i = 0; i < 4; i++) - * ICI[i].TV = ((valid_bits & (1U << i)) ? 0x00 : 0x02) | (address & 0xFFFFFFF0) | (i << 2); - * } else - * ICache[(address & 0xFFC) >> 2].Data = value << ((address & 0x3) * 8); - * } - * - * The else arm writes an instruction word and leaves TV alone. This model keeps - * no instruction words (compiled code supplies them), so it has nothing to do. - * Beetle does not model SR.SwC (swapped caches), so neither does this. The - * store keeps the full virtual address as the tag, as Beetle's refill does. */ + * With SR.IsC set and the I-cache enabled (BCC bit 11, IS1), a store made + * with BCC TAG, INV or LOCK set rewrites the tag and valid bits of the four + * words of its line (PSX-SPX memorycontrol, "FFFE0130h - BCC"); that is + * how FlushCache (A 44h) invalidates the cache. A store with none of those + * bits writes an instruction word; this model keeps no instruction words + * (compiled code supplies them), so it does nothing. SR.SwC is not + * modeled. PSX-SPX gives the stored tag as (data AND 0Fh) OR (offset AND + * FFFFF000h); this model stores the line address with its own valid-bit + * encoding, and the replacement is to be checked against that text. */ void psx_icache_isc_store(uint32_t biu, uint32_t addr, uint32_t value) { #ifdef PSX_ENABLE_BLOCK_CYCLES /* Same gate as a refill: lockstep replay must not move the shared tags; diff --git a/runtime/src/sio.c b/runtime/src/sio.c index 2d82ca85..e85f8b46 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -69,8 +69,9 @@ static uint8_t pad_analog_device[PSX_MAX_PLAYERS] = { /* DualShock command 0x4D maps the six writable bytes in a 0x42 poll onto the * two motors: 0x00 = small/high-frequency, 0x01 = large/low-frequency, - * 0xFF = unused. The map powers up unassigned and is echoed back while a new - * map is latched, matching the physical pad/Mednafen protocol. */ + * 0xFF = unused (PSX-SPX controllersandmemorycards, "Config Mode - Command 4Dh"). + * The map powers up unassigned and is echoed back while a new map is + * latched, as on the Mednafen reference. */ static uint8_t pad_rumble_map[PSX_MAX_PLAYERS][6] = { PSX_PAD_INIT({ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }) }; @@ -274,8 +275,9 @@ static uint8_t pad_protocol_id(int logical) { } /* NeGcon (NPC-101) poll payload: buttons, twist, then analog I, II and L - * (Mednafen psx/input/negcon.cpp). The device has no config mode and no - * motors; every byte is fixed by the current host sample. */ + * (PSX-SPX controllersandmemorycards, "neGcon Racing Controller"). + * The device has no config mode and no motors; every byte is fixed by the + * current host sample. */ static void pad_fill_negcon_poll(int logical, uint8_t out[8]) { out[0] = 0x23; out[1] = 0x5A; @@ -1412,7 +1414,7 @@ static void pad_process_byte(uint8_t tx_byte) { if (pad_analog[lp] == SIO_PAD_NEGCON) { /* NeGcon answers only the 0x42 poll. Its ID byte goes out with any * command, but for anything else it does not ACK and the - * transaction ends (Mednafen negcon.cpp phase 1). */ + * transaction ends, as on the Mednafen reference. */ pad_fill_negcon_poll(lp, pad_response); sio_rx_data = pad_response[0]; if (tx_byte == 0x42) { @@ -1817,8 +1819,9 @@ static void mc_process_byte(uint8_t tx_byte) { } else { mc_data_idx = 0; mc_state = MC_WRITE_LSB_ECHO; - /* Hardware/Beetle echo the high address byte on the address-LSB - * transfer (zero for all 1Mbit card sectors), then echo the low + /* Hardware echoes the high address byte on the address-LSB + * transfer (PSX-SPX controllersandmemorycards, "Writing Data to Memory + * Card": reply "pre"; zero for all 1Mbit card sectors), then echo the low * address byte while receiving the first payload byte. Some BIOS * card-write paths validate this handshake before they schedule the * follow-up directory reads. */ diff --git a/runtime/tests/gpu_exec_stubs.inc b/runtime/tests/gpu_exec_stubs.inc index 72c8b16a..76716d1f 100644 --- a/runtime/tests/gpu_exec_stubs.inc +++ b/runtime/tests/gpu_exec_stubs.inc @@ -60,6 +60,10 @@ int pgxp_load_projection(uint32_t addr, uint32_t word, PGXPProjection *out) { } return 0; } +float pgxp_get_unclamped_depth(uint32_t addr, uint32_t word) { + PGXPProjection projection; + return pgxp_load_projection(addr, word, &projection) ? projection.z : 0.0f; +} static int fixture_projection_tracking; int pgxp_projection_tracking(void) { return fixture_projection_tracking; } void pgxp_set_projection_tracking(int enabled) { fixture_projection_tracking=!!enabled; } diff --git a/runtime/tests/test_frame_trace.c b/runtime/tests/test_frame_trace.c index 3f887fe0..8650980c 100644 --- a/runtime/tests/test_frame_trace.c +++ b/runtime/tests/test_frame_trace.c @@ -194,6 +194,21 @@ int main(void) { CHECK(one[0].seq == extra); } + /* Opening the next frame must not retire a committed slot before commit. */ + { + static PSXFrameTraceRecord retained[4096]; + uint32_t capacity = psx_frame_trace_capacity(); + uint64_t committed = psx_frame_trace_committed(); + CHECK(psx_frame_trace_open() == 1); + psx_frame_trace_note("not committed"); + CHECK(psx_frame_trace_read(retained, capacity) == capacity); + CHECK(retained[0].seq == committed - capacity + 1); + CHECK(retained[capacity - 1].seq == committed); + psx_frame_trace_commit(); + CHECK(psx_frame_trace_read(retained, capacity) == capacity); + CHECK(retained[capacity - 1].seq == committed + 1); + } + /* ---- exports: valid, NULL-terminated, truncation-safe ---- */ { static char big[1 << 20]; @@ -239,6 +254,19 @@ int main(void) { CHECK(strstr(meta, "\"target_period_us\":0") != NULL); } + /* Fixed scratch buffers may shorten a string, never split a JSON escape. */ + { + char controls[128]; static char json[1 << 16]; + memset(controls, 1, sizeof controls - 1); controls[127] = 0; + psx_frame_trace_session(controls, 0); + CHECK(psx_frame_trace_to_json(json, sizeof json, 1) > 0); + const char *value = strstr(json, "\"runtime\":\""); + CHECK(value != NULL); value += strlen("\"runtime\":\""); + const char *end = strchr(value, '\"'); + CHECK(end && (end - value) % 6 == 0); + for (; value < end; value += 6) CHECK(!memcmp(value, "\\u0001", 6)); + } + /* ---- XR cadence: the runtime's display-time delta and repeat marking ---- * One presented pair per granted display period is the pacing target, so a * frame that carried no new pair must be distinguishable from one that did diff --git a/runtime/tests/test_gpu_gp0_history_guard.py b/runtime/tests/test_gpu_gp0_history_guard.py index fa7b3629..2bacef54 100644 --- a/runtime/tests/test_gpu_gp0_history_guard.py +++ b/runtime/tests/test_gpu_gp0_history_guard.py @@ -297,6 +297,8 @@ void pgxp_projective_scope_end(PGXPProjectiveScope *scope) { (void)scope; abort(); } int pgxp_load_projection(uint32_t addr, uint32_t packed, PGXPProjection *out) { (void)addr; (void)packed; (void)out; abort(); } +float pgxp_get_unclamped_depth(uint32_t addr, uint32_t packed) +{ (void)addr; (void)packed; abort(); } int gl_renderer_projective_supported(void) { abort(); } void gl_renderer_draw_projected_triangle(const PSXProjectedVertex vertices[3], uint16_t texpage, uint16_t cx, uint16_t cy, int raw, int semi, int perspective) diff --git a/runtime/tests/test_host_time_threads.c b/runtime/tests/test_host_time_threads.c new file mode 100644 index 00000000..a8eae238 --- /dev/null +++ b/runtime/tests/test_host_time_threads.c @@ -0,0 +1,57 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include "host_time.h" + +static HANDLE start_event; +static volatile LONG errors; +static DWORD WINAPI sleeper(void *arg) { + (void)arg; + WaitForSingleObject(start_event, INFINITE); + uint64_t previous = psx_host_mono_ms(); + for (unsigned i=0; i<250; ++i) { + psx_host_sleep_micros(1000); + uint64_t now = psx_host_mono_ms(); + if (now < previous) InterlockedIncrement(&errors); + previous = now; + } + return 0; +} +static int run_wave(void) { + HANDLE threads[8]; + start_event = CreateEventW(NULL, TRUE, FALSE, NULL); + if (!start_event) return 2; + for (unsigned i=0; i<8; ++i) { + threads[i] = CreateThread(NULL, 0, sleeper, NULL, 0, NULL); + if (!threads[i]) return 2; + } + SetEvent(start_event); + DWORD result = WaitForMultipleObjects(8, threads, TRUE, 10000); + if (result != WAIT_OBJECT_0) { + puts("FAIL: concurrent sleepers lost a wakeup"); + return 1; + } + for (unsigned i=0; i<8; ++i) CloseHandle(threads[i]); + CloseHandle(start_event); + return errors ? 1 : 0; +} +int main(void) { + /* The cold wave still tests concurrent InitOnce initialization. Measure + * handle growth only after process/CRT/Windows lazy initialization, then + * repeat whole thread lifetimes: a leaked caller timer grows every wave. */ + int result = run_wave(); + if (result) return result; + DWORD before, after; + if (!GetProcessHandleCount(GetCurrentProcess(), &before)) return 2; + for (int wave = 0; wave < 3; ++wave) { + result = run_wave(); + if (result) return result; + } + if (!GetProcessHandleCount(GetCurrentProcess(), &after) || after > before) { + printf("FAIL: sleeper timer handles leaked after thread exit: before=%lu after=%lu\n", before, after); + return 1; + } + if (errors) return 1; + puts("host time concurrent sleepers: PASS"); + return 0; +} diff --git a/runtime/tests/test_internal_resolution.cpp b/runtime/tests/test_internal_resolution.cpp index 25b04cdd..597c271c 100644 --- a/runtime/tests/test_internal_resolution.cpp +++ b/runtime/tests/test_internal_resolution.cpp @@ -222,6 +222,21 @@ int main() { expect("ss clamps to the ceiling", psx_resolve_internal_scale_ss(4320, 240, 0, 32, 4000), 32); expect("ss unknown display stays 1", psx_resolve_internal_scale_ss(PSX_IR_DISPLAY, 240, 0, 32, 2000), 1); + expect("ss policy opt-out", psx_ir_display_supersample(1500, 4320, 0), 1500); + expect("ss below threshold", psx_ir_display_supersample(1500, 2159, 2160), 1500); + expect("ss at threshold", psx_ir_display_supersample(1500, 2160, 2160), 1000); + expect("ss on 8K", psx_ir_display_supersample(1500, 4320, 2160), 1000); + expect("ss unknown display", psx_ir_display_supersample(1500, 0, 2160), 1500); + expect("cap includes SS below threshold", psx_resolve_internal_scale_ss_cap( + PSX_IR_DISPLAY, 240, 1440, 32, 4000, 4320), 18); + expect("cap below 8K", psx_resolve_internal_scale_ss_cap( + PSX_IR_DISPLAY, 240, 1080, 32, 1500, 4320), 7); + expect("8K capped after SS", psx_resolve_internal_scale_ss_cap( + PSX_IR_DISPLAY, 240, 8640, 32, 4000, 4320), 18); + expect("fixed preset keeps own ceiling", psx_resolve_internal_scale_ss_cap( + 4320, 240, 8640, 32, 4000, 4320), 32); + expect("cap opt-out", psx_resolve_internal_scale_ss_cap( + PSX_IR_DISPLAY, 240, 4320, 32, 1500, 0), 27); launcher_round_trips(); if (g_failures) return 1; diff --git a/runtime/tests/test_pgxp.cpp b/runtime/tests/test_pgxp.cpp index 7150f6fb..b2e40931 100644 --- a/runtime/tests/test_pgxp.cpp +++ b/runtime/tests/test_pgxp.cpp @@ -640,6 +640,52 @@ int main(void) { &x, &y) == 1); } + /* Far-world projection: exact dataflow may recover IR saturation, while + * stale words, lm sign clamps and half writes must stay rejected. */ + { + const int saved_preserve = pgxp_preserve_projection(); + pgxp_set_preserve_projection(1); + pgxp_set_tolerance(-1.0f); + int32_t x, y; + CHECK(pgxp_project_precise(INT64_C(40000)*4096, INT64_C(-35000)*4096, + INT64_C(60000)*4096, 12, 32767, -32768, 60000, 300, + 160<<16, 120<<16, 1, 1, &x, &y) == 2); + /* Fused/non-fused host arithmetic may straddle the final floor by + * one 16.16 quantum (less than 0.00002 pixel). */ + CHECK(std::abs(x - 360*65536) <= 1 && std::abs(y + 55*65536) <= 1); + const uint32_t packed = (uint16_t)323 | ((uint32_t)(uint16_t)-44 << 16); + pgxp_gte_push_sxy(x, y, 60000, packed); + pgxp_gte_mark_unclamped(60000); + pgxp_store_gte_reg(0x80005000, 14); + int32_t px, py; uint16_t z; + CHECK(pgxp_get_precise_vertex(0x80005000, packed, 323, -44, &px, &py, &z)); + CHECK(px == x && py == y && z == 60000); + CHECK(pgxp_get_unclamped_depth(0x80005000, packed) == 60000.0f); + CHECK(!pgxp_get_precise_vertex(0x80005000, packed^1u, 322, -44, &px, &py, &z)); + psx_pgxp_cop2(nullptr, MFC2(2, 14), packed, 0); + psx_pgxp_store(nullptr, SH(1, 2), 0x80005000, packed); + CHECK(!pgxp_get_precise_vertex(0x80005000, packed, 323, -44, &px, &py, &z)); + CHECK(pgxp_project_precise(-4096, 0, INT64_C(60000)*4096, 12, 0, 0, + 60000, 300, 0, 0, 1, 1, &x, &y) == 0); + CHECK(pgxp_project_precise(INT64_C(40000)*4096, INT64_C(20000)*4096, + INT64_C(100000)*4096, 12, 32767, 20000, 65535, 300, + 160<<16, 120<<16, 1, 1, &x, &y) == 2); + CHECK(x == 280*65536 && y == 180*65536); + const uint32_t far_word = 310u | (211u << 16); + pgxp_gte_push_sxy(x, y, 65535, far_word); + pgxp_gte_mark_unclamped(100000); + pgxp_store_gte_reg(0x80005004, 14); + CHECK(pgxp_get_precise_vertex(0x80005004, far_word, 310, 211, &px, &py, &z)); + CHECK(px == x && py == y && z == 65535); + CHECK(pgxp_get_unclamped_depth(0x80005004, far_word) == 100000.0f); + CHECK(pgxp_get_unclamped_depth(0x80005004, far_word^1) == 0.0f); + pgxp_set_preserve_projection(0); + CHECK(pgxp_get_unclamped_depth(0x80005004, far_word) == 0.0f); + CHECK(pgxp_project_precise(INT64_C(40000)*4096, 0, INT64_C(60000)*4096, + 12, 32767, 0, 60000, 300, 0, 0, 1, 1, &x, &y) == 0); + pgxp_set_preserve_projection(saved_preserve); + } + /* --- probe: the same decision, not counted --- */ { produce_at(ADDR_A);