diff --git a/docs/config_schema.md b/docs/config_schema.md index 0454d4e1..5a268708 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -304,6 +304,22 @@ queue_guard = false # this lower-level predicate appends to no fixed queue The debug server’s `ws_aspect_cone_site` command accepts an `address` string and reports exact-site identity/keep/reject counters. +Signed horizontal bounds can be widened at the constant-load site: + +```toml +[[widescreen.signed_x_bound]] +address = "0x800BD290" +expected = "0x2402FF00" # addiu v0,zero,-256 +``` + +- `LUI rt,imm` sites are signed Q16 gameplay bounds and use the gameplay-field + scaler. +- `ADDIU rt,zero,imm` and `ORI rt,zero,imm` sites are screen-pixel bounds and + move by the live horizontal margin. ADDIU sign-extends the constant; ORI + zero-extends it. The destination and immediate must both be nonzero. +- Site identity is the normalized physical address plus the complete + instruction word. The helper is identity at 4:3. + Explicit `bias_sites` / `range_sites` may opt into an additional resident object lead without widening terrain or render queues: diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 742878f4..01b85a34 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -416,6 +416,32 @@ if(BUILD_TESTING) target_link_libraries(full_function_emitter_test PRIVATE fmt rabbitizer) add_test(NAME full_function_emitter_test COMMAND full_function_emitter_test) + add_executable(signed_x_bound_codegen_test + tests/signed_x_bound_codegen_test.cpp + src/ps1_exe_parser.cpp + src/mips_decoder.cpp + src/control_flow.cpp + src/function_analysis.cpp + src/code_generator.cpp + src/pgxp_hook_emitter.cpp + src/annotations.cpp + src/bios_address_model.cpp + src/config_loader.cpp + src/recompiler_patch.cpp + ) + target_include_directories(signed_x_bound_codegen_test PRIVATE + include + src + lib/rabbitizer/include + lib/rabbitizer/cplusplus/include + lib/fmt/include + lib/toml11 + lib/ELFIO + ) + target_link_libraries(signed_x_bound_codegen_test PRIVATE fmt rabbitizer) + add_test(NAME signed_x_bound_codegen_test + COMMAND signed_x_bound_codegen_test) + add_executable(recompiler_patch_cxx17_header_test tests/recompiler_patch_cxx17_header_test.cpp ) diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index e9e9a946..e2c0adae 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -883,10 +883,28 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) site.expected, addr, instr); std::exit(1); } - const int32_t vanilla = (int32_t)((instr & 0xFFFFu) << 16); - return fmt::format("{} = (uint32_t)psx_ws_player_x_bound((int32_t)0x{:08X});" - " /* typed native-wide signed X bound */{}", - reg_name(get_rt(instr)), (uint32_t)vanilla, comment); + const uint32_t bound_opcode = instr >> 26; + if (bound_opcode == 0x0Fu) { // LUI rt,imm: signed Q16 gameplay bound + const int32_t vanilla = (int32_t)((instr & 0xFFFFu) << 16); + return fmt::format("{} = (uint32_t)psx_ws_player_x_bound((int32_t)0x{:08X});" + " /* typed native-wide signed X bound */{}", + reg_name(get_rt(instr)), (uint32_t)vanilla, comment); + } + if ((bound_opcode == 0x09u || bound_opcode == 0x0Du) && + get_rs(instr) == 0u && get_rt(instr) != 0u) { + // ADDIU sign-extends, ORI zero-extends its screen-pixel constant. + const int32_t vanilla = bound_opcode == 0x09u + ? (int16_t)(instr & 0xFFFFu) : (int32_t)(instr & 0xFFFFu); + return fmt::format("{} = (uint32_t)psx_ws_screen_x_bound({});" + " /* typed native-wide signed screen-X bound */{}", + reg_name(get_rt(instr)), vanilla, comment); + } + if (!config_.overlay_mode) { + fmt::print(stderr, + "ERROR: signed_x_bound site 0x{:08X} is not LUI or ADDIU/ORI rt,zero,nonzero-imm (0x{:08X})\n", + addr, instr); + std::exit(1); + } } // Full-word-guarded, camera-horizontal model-participation cones. The @@ -3273,6 +3291,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const { ss << "extern void psx_ws_mmx6_bg_stage_init(void); /* ws 2D stage reveal invalidation (gpu.c) */\n"; ss << "extern int psx_ws_x_margin(void); /* widescreen cull-margin term (gpu.c) */\n"; ss << "extern int32_t psx_ws_player_x_bound(int32_t vanilla); /* typed gameplay X bound */\n"; + ss << "extern int32_t psx_ws_screen_x_bound(int32_t vanilla); /* typed screen-pixel X bound */\n"; ss << "extern int psx_ws_cull_sltiu(uint32_t sx, uint32_t imm); /* ws auto screen-x cull (gpu.c) */\n"; ss << "extern int psx_ws_cull_slti(uint32_t sx, uint32_t imm); /* ws cull signed right edge (gpu.c) */\n"; ss << "extern int psx_ws_cull_slti_lower(uint32_t sx, uint32_t imm); /* ws cull signed lower edge (gpu.c) */\n"; diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 6e7e21ec..db8975fe 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -1648,10 +1648,28 @@ GameConfig load_game_config(const fs::path& config_path_in) { "widescreen.signed_x_bound.address"); site.expected = parse_hex(toml::find(item, "expected"), "widescreen.signed_x_bound.expected"); - if ((site.expected >> 26) != 0x0Fu) + const uint32_t opcode = site.expected >> 26; + if (opcode != 0x0Fu && opcode != 0x09u && opcode != 0x0Du) throw std::runtime_error(fmt::format( - "{}: [[widescreen.signed_x_bound]] expected must be LUI", + "{}: [[widescreen.signed_x_bound]] expected must be LUI or ADDIU/ORI", config_path.string())); + if (opcode == 0x09u || opcode == 0x0Du) { + const uint32_t rs = (site.expected >> 21) & 0x1Fu; + const uint32_t rt = (site.expected >> 16) & 0x1Fu; + const uint32_t imm = site.expected & 0xFFFFu; + if (rs != 0u) + throw std::runtime_error(fmt::format( + "{}: [[widescreen.signed_x_bound]] ADDIU/ORI expected must use rs=$zero", + config_path.string())); + if (rt == 0u) + throw std::runtime_error(fmt::format( + "{}: [[widescreen.signed_x_bound]] ADDIU/ORI expected must not write $zero", + config_path.string())); + if (imm == 0u) + throw std::runtime_error(fmt::format( + "{}: [[widescreen.signed_x_bound]] ADDIU/ORI expected must have a non-zero signed screen edge", + config_path.string())); + } if (!seen.insert(site.address & 0x1FFFFFFFu).second) throw std::runtime_error(fmt::format( "{}: duplicate [[widescreen.signed_x_bound]] address 0x{:08X}", diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 766fcb20..9457279f 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -87,7 +87,7 @@ inline bool video_fmv_filter_parse(const std::string& s, int* out) { struct WidescreenSignedBoundSite { uint32_t address = 0; - uint32_t expected = 0; // guarded LUI instruction + uint32_t expected = 0; // guarded LUI or ADDIU rt,zero,imm instruction }; // One exact compare whose verdict is forced while a widescreen reveal is @@ -1101,9 +1101,9 @@ struct GameConfig { // transformed in the mirror. bool ws_nw_full_mirror = false; - // [[widescreen.signed_x_bound]] guarded LUI sites whose signed Q16 - // constants scale with the active native-wide field and remain identity in - // 4:3/menus/FMV. Shared by static codegen, overlay JIT, and interpreter. + // [[widescreen.signed_x_bound]] guarded LUI signed-Q16 bounds or ADDIU/ORI + // rt,zero,imm screen-pixel bounds. Both remain identity in 4:3/menus/FMV. + // Shared by static codegen, overlay JIT, and interpreter. std::vector ws_signed_x_bound_sites; // [widescreen] offer — whether the launcher OFFERS its EXPERIMENTAL // Widescreen toggle for this title. Default true. Set false while a diff --git a/recompiler/tests/recompiler_patch_test.cpp b/recompiler/tests/recompiler_patch_test.cpp index ab644eae..da713103 100644 --- a/recompiler/tests/recompiler_patch_test.cpp +++ b/recompiler/tests/recompiler_patch_test.cpp @@ -311,6 +311,28 @@ result = 0 keep_config.ws_cull_keep_sites[1].result == 0u, "parser preserves full-word-guarded maximal-participation sites"); + const auto signed_bound = write_config(root, "signed-x-bound", R"toml( +[[widescreen.signed_x_bound]] +address = "0x8002D290" +expected = "0x2402FF00" + +[[widescreen.signed_x_bound]] +address = "0x8002D298" +expected = "0x3C02FFF0" +)toml"); + const auto signed_bound_config = + PSXRecompV4::load_game_config(signed_bound); + check(signed_bound_config.ws_signed_x_bound_sites.size() == 2 && + signed_bound_config.ws_signed_x_bound_sites[0].address == + 0x8002D290u && + signed_bound_config.ws_signed_x_bound_sites[0].expected == + 0x2402FF00u && + signed_bound_config.ws_signed_x_bound_sites[1].address == + 0x8002D298u && + signed_bound_config.ws_signed_x_bound_sites[1].expected == + 0x3C02FFF0u, + "parser preserves guarded signed pixel and Q16 X bounds"); + const auto bad_keep = write_config(root, "cull-keep-bad", R"toml( [[widescreen.cull.keep]] address = "0x8002B310" @@ -700,6 +722,30 @@ void codegen_tests() { check(depth_overlay_mismatch.find("ws cull depth") == std::string::npos, "overlay nonmatching depth variant remains unchanged"); + PSXRecomp::CodeGenConfig signed_bound_config; + signed_bound_config.ws_signed_x_bound_sites.push_back( + {0x80010000u, 0x2402FF00u}); // addiu v0,zero,-256 + const std::string signed_pixel_bound = generate_first_instruction( + 0x2402FF00u, {}, false, signed_bound_config); + check(signed_pixel_bound.find("psx_ws_screen_x_bound(-256)") != + std::string::npos, + "codegen emits guarded signed screen-pixel X bound"); + + signed_bound_config.ws_signed_x_bound_sites[0] = + {0x80010000u, 0x3C02FFF0u}; // lui v0,0xfff0 + const std::string signed_q16_bound = generate_first_instruction( + 0x3C02FFF0u, {}, false, signed_bound_config); + check(signed_q16_bound.find( + "psx_ws_player_x_bound((int32_t)0xFFF00000)") != + std::string::npos, + "codegen keeps guarded LUI X bounds on the Q16 helper"); + + const std::string signed_bound_overlay_mismatch = generate_first_instruction( + 0x24020078u, {}, true, signed_bound_config); + check(signed_bound_overlay_mismatch.find("typed native-wide signed") == + std::string::npos, + "overlay nonmatching signed-bound variant remains unchanged"); + PSXRecomp::CodeGenConfig range_config; range_config.ws_cull_range_sites.insert(0x80010000u); const std::string range = generate_first_instruction( diff --git a/recompiler/tests/signed_x_bound_codegen_test.cpp b/recompiler/tests/signed_x_bound_codegen_test.cpp new file mode 100644 index 00000000..9a2b7572 --- /dev/null +++ b/recompiler/tests/signed_x_bound_codegen_test.cpp @@ -0,0 +1,209 @@ +#include "code_generator.h" +#include "config_loader.h" +#include "control_flow.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +namespace { + +constexpr uint32_t kBase = 0x80010000u; +int failures = 0; + +void check(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "FAIL: %s\n", message); + ++failures; + } +} + +void append_word(std::vector& bytes, uint32_t word) { + bytes.push_back(static_cast(word)); + bytes.push_back(static_cast(word >> 8)); + bytes.push_back(static_cast(word >> 16)); + bytes.push_back(static_cast(word >> 24)); +} + +fs::path write_temp_config(const char* stem, const std::string& body) { + const auto nonce = std::chrono::high_resolution_clock::now() + .time_since_epoch().count(); + fs::path path = fs::temp_directory_path() / + (std::string(stem) + "-" + std::to_string(nonce) + ".toml"); + std::ofstream out(path, std::ios::binary | std::ios::trunc); + out << body; + return path; +} + +std::string base_config() { + return R"toml([game] +name = "Signed Bound Test" +id = "TEST-00000" +exe = "TEST.EXE" +load_address = "0x80010000" +entry_pc = "0x80010000" +text_size = "0x1000" +stack_base = "0x801FFFF0" + +[recompiler] +seeds = "seeds.txt" +out_dir = "generated" +)toml"; +} + +bool load_throws_with(const std::string& body, const char* needle) { + fs::path path = write_temp_config("signed-x-bound", body); + bool matched = false; + try { + (void)PSXRecompV4::load_game_config(path); + } catch (const std::exception& e) { + matched = std::string(e.what()).find(needle) != std::string::npos; + } + fs::remove(path); + return matched; +} + +PSXRecomp::GeneratedFunction generate_first_instruction( + uint32_t first_word, const PSXRecomp::CodeGenConfig& config) { + PSXRecomp::PS1Executable exe{}; + exe.header.load_address = kBase; + exe.header.initial_pc = kBase; + exe.header.file_size = 20; + append_word(exe.code_data, first_word); + append_word(exe.code_data, 0x00000000u); + append_word(exe.code_data, 0x24030001u); + append_word(exe.code_data, 0x03E00008u); + append_word(exe.code_data, 0x00000000u); + + PSXRecomp::Function function{}; + function.start_addr = kBase; + function.end_addr = kBase + 20u; + function.size = 20u; + function.name = "signed_bound_test"; + + PSXRecomp::ControlFlowAnalyzer analyzer(exe); + const auto cfg = analyzer.analyze_function(function); + PSXRecomp::CodeGenerator generator(exe, config); + return generator.generate_function(function, cfg); +} + +void loader_accepts_lui_and_addiu() { + fs::path path = write_temp_config("signed-x-bound-valid", base_config() + R"toml( +[[widescreen.signed_x_bound]] +address = "0x80010000" +expected = "0x3C020001" + +[[widescreen.signed_x_bound]] +address = "0x80010004" +expected = "0x2402FF00" +)toml"); + auto config = PSXRecompV4::load_game_config(path); + check(config.ws_signed_x_bound_sites.size() == 2, + "loader accepts LUI plus ADDIU signed_x_bound entries"); + fs::remove(path); +} + +void loader_rejects_bad_addiu_shapes() { + check(load_throws_with(base_config() + R"toml( +[[widescreen.signed_x_bound]] +address = "0x80010000" +expected = "0x30020100" +)toml", "expected must be LUI or ADDIU"), + "loader rejects unsupported signed_x_bound opcode"); + + check(load_throws_with(base_config() + R"toml( +[[widescreen.signed_x_bound]] +address = "0x80010000" +expected = "0x20020100" +)toml", "expected must be LUI or ADDIU"), + "loader rejects ADDI without a matching codegen/interpreter route"); + + check(load_throws_with(base_config() + R"toml( +[[widescreen.signed_x_bound]] +address = "0x80010000" +expected = "0x2482FF00" +)toml", "must use rs=$zero"), + "loader rejects ADDIU signed_x_bound with nonzero rs"); + + check(load_throws_with(base_config() + R"toml( +[[widescreen.signed_x_bound]] +address = "0x80010000" +expected = "0x2400FF00" +)toml", "must not write $zero"), + "loader rejects ADDIU signed_x_bound writing zero"); + + check(load_throws_with(base_config() + R"toml( +[[widescreen.signed_x_bound]] +address = "0x80010000" +expected = "0x24020000" +)toml", "non-zero signed screen edge"), + "loader rejects ADDIU signed_x_bound with zero immediate"); +} + +void codegen_routes_bound_kinds() { + PSXRecomp::CodeGenConfig config{}; + config.emit_comments = true; + config.ws_signed_x_bound_sites.push_back( + PSXRecompV4::WidescreenSignedBoundSite{kBase, 0x2402FF00u}); + auto addiu_neg = generate_first_instruction(0x2402FF00u, config).full_code; + check(addiu_neg.find( + "cpu->gpr[2] = (uint32_t)psx_ws_screen_x_bound(-256);") != + std::string::npos, + "codegen routes ADDIU negative screen bound to screen helper"); + + config.ws_signed_x_bound_sites[0] = + PSXRecompV4::WidescreenSignedBoundSite{kBase, 0x24020100u}; + auto addiu_pos = generate_first_instruction(0x24020100u, config).full_code; + check(addiu_pos.find( + "cpu->gpr[2] = (uint32_t)psx_ws_screen_x_bound(256);") != + std::string::npos, + "codegen routes ADDIU positive screen bound to screen helper"); + + config.ws_signed_x_bound_sites[0] = + PSXRecompV4::WidescreenSignedBoundSite{kBase, 0x3402FF00u}; + auto ori = generate_first_instruction(0x3402FF00u, config).full_code; + check(ori.find("psx_ws_screen_x_bound(65280)") != std::string::npos, + "ORI bound zero-extends, unlike the ADDIU negative bound"); + + config.ws_signed_x_bound_sites[0] = + PSXRecompV4::WidescreenSignedBoundSite{kBase, 0x3C020001u}; + auto lui = generate_first_instruction(0x3C020001u, config).full_code; + check(lui.find( + "cpu->gpr[2] = (uint32_t)psx_ws_player_x_bound((int32_t)0x00010000);") != + std::string::npos, + "codegen preserves LUI signed-Q16 gameplay helper"); +} + +void shared_decls_include_screen_helper() { + PSXRecomp::PS1Executable exe{}; + exe.header.load_address = kBase; + PSXRecomp::CodeGenerator generator(exe); + std::vector functions; + const auto decls = generator.build_shared_decls_header(functions); + check(decls.find("psx_ws_screen_x_bound") != std::string::npos, + "shared declarations include psx_ws_screen_x_bound"); +} + +} // namespace + +int main() { + loader_accepts_lui_and_addiu(); + loader_rejects_bad_addiu_shapes(); + codegen_routes_bound_kinds(); + shared_decls_include_screen_helper(); + + if (failures != 0) { + std::fprintf(stderr, "signed_x_bound_codegen_test: %d failure(s)\n", + failures); + return 1; + } + std::puts("PASS: signed_x_bound codegen"); + return 0; +} diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 82903b69..c1ddbad1 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -30,6 +30,8 @@ if(BUILD_TESTING) set(PSX_GL_READBACK_COMPILER_BIN "${_gl_test_default_bin}" CACHE PATH "GCC/MinGW bin for GL fixture") find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) + add_test(NAME interp_mod_entry_guards_test COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_interp_mod_entry_guards.py") add_test(NAME gl_readback_runner_test COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_readback_runner.py") endif() @@ -204,6 +206,14 @@ if(BUILD_TESTING) target_include_directories(ws_prepass_guard_test PRIVATE include) add_test(NAME ws_prepass_guard_test COMMAND ws_prepass_guard_test) + add_executable(ws_hud_anchor_test + tests/test_ws_hud_anchor.c) + target_include_directories(ws_hud_anchor_test PRIVATE include) + add_test(NAME ws_hud_anchor_test COMMAND ws_hud_anchor_test) + add_executable(ws_repeat_rect_test tests/test_ws_repeat_rect.c) + target_include_directories(ws_repeat_rect_test PRIVATE include) + add_test(NAME ws_repeat_rect_test COMMAND ws_repeat_rect_test) + add_executable(mod_gpu_dma_aperture_test tests/test_mod_gpu_dma_aperture.c) target_include_directories(mod_gpu_dma_aperture_test PRIVATE include) diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index c831f207..3f5c1b76 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -218,7 +218,6 @@ int psx_ws_mmx6_bg_stream_left(int x); int psx_ws_mmx6_bg_stream_right(int x); struct CPUState; void psx_ws_sprite_tag(struct CPUState* cpu); - /* Native-wide (mode 2) on a game frame. ws_nw_extra() is the total width the * frame grows by, in display pixels (the present path widens the display read * by this; 0 when native-wide is inactive). */ @@ -298,6 +297,8 @@ int32_t psx_ws_player_x_bound(int32_t vanilla); void gpu_ws_set_signed_x_bound_sites(const uint32_t *addresses, const uint32_t *expected, int count); int psx_ws_is_signed_x_bound_site(uint32_t pc, uint32_t instr); +/* Widen a signed screen-pixel edge loaded by an explicitly guarded site. */ +int32_t psx_ws_screen_x_bound(int32_t vanilla); /* Shared render-funnel screen-X cull widening ([widescreen.cull] auto_screen_x): * the gcc emit and the interpreter both route a flagged @@ -348,6 +349,17 @@ void gpu_ws_set_gameplay_state_gate(uint32_t addr, * outer-third screen-space HUD primitives out to the true wide-frame corners * (they otherwise sit inset by the reveal). Runtime-only. Off by default. */ void gpu_ws_set_nw_hud_corners(int on); +/* Explicit native-wide HUD packet anchor from a trusted title plugin. + * `prim` is the address of the PsyQ P_TAG word; the drawn command starts at + * prim+4. anchor: -1 = left, 0 = center, +1 = right. */ +void gpu_ws_tag_hud_prim(uint32_t prim, int anchor); +/* Clear synthetic margins to black over an opaque 0x64/65 rectangle's Y band + * immediately before it executes. Packet-guarded; canonical VRAM is untouched. */ +void gpu_ws_tag_black_reveal_rect(uint32_t prim); +/* Repeat an opaque, already-clipped textured rectangle into native-wide + * reveal margins only. The caller verifies the composite's source period. + * Original UVs, texel density, and canonical VRAM writes are unchanged. */ +void gpu_ws_tag_repeat_rect(uint32_t prim, int32_t period); /* Targeted alternative for sprite-heavy 2D games: corner-anchor only primitives * whose ordering-table packet lives in the configured half-open RAM range. */ void gpu_ws_set_nw_left_hud_packet_range(uint32_t lo, uint32_t hi); diff --git a/runtime/include/ws_hud_anchor.h b/runtime/include/ws_hud_anchor.h new file mode 100644 index 00000000..9c201e79 --- /dev/null +++ b/runtime/include/ws_hud_anchor.h @@ -0,0 +1,100 @@ +#ifndef PSXRECOMP_WS_HUD_ANCHOR_H +#define PSXRECOMP_WS_HUD_ANCHOR_H + +#include "ws_prepass_guard.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define WS_HUD_ANCHOR_TABLE_SIZE 256u +#define WS_HUD_ANCHOR_PROBES 8u +#define WS_HUD_ANCHOR_FRESH_FRAMES 2u + +typedef struct { + uint32_t command_addr; + uint32_t frame; + int8_t anchor; + uint8_t used; + WsPrepassPacketGuard guard; +} WsHudAnchorTag; + +static inline int ws_hud_anchor_clamp(int anchor) { + if (anchor < 0) return -1; + if (anchor > 0) return 1; + return 0; +} + +static inline int32_t ws_hud_anchor_native_delta(int native_active, + int32_t offset, + int anchor) { + if (!native_active || offset <= 0) return 0; + return (int32_t)ws_hud_anchor_clamp(anchor) * offset; +} + +static inline uint32_t ws_hud_anchor_slot(uint32_t command_addr) { + return (command_addr >> 2) & (WS_HUD_ANCHOR_TABLE_SIZE - 1u); +} + +static inline void ws_hud_anchor_clear(WsHudAnchorTag *tags, + uint32_t count) { + if (!tags) return; + for (uint32_t i = 0; i < count; i++) + tags[i].used = 0; +} + +static inline void ws_hud_anchor_insert(WsHudAnchorTag *tags, uint32_t count, + uint32_t command_addr, int anchor, + const WsPrepassPacketGuard *guard, + uint32_t frame) { + if (!tags || !count || !guard) return; + uint32_t idx = ws_hud_anchor_slot(command_addr) & (count - 1u); + uint32_t victim = idx; + for (uint32_t i = 0; i < WS_HUD_ANCHOR_PROBES && i < count; i++) { + uint32_t j = (idx + i) & (count - 1u); + WsHudAnchorTag *tag = &tags[j]; + if (!tag->used || tag->command_addr == command_addr) { + victim = j; + break; + } + if (frame - tag->frame > WS_HUD_ANCHOR_FRESH_FRAMES) + victim = j; + } + tags[victim].used = 1; + tags[victim].command_addr = command_addr; + tags[victim].frame = frame; + tags[victim].anchor = (int8_t)ws_hud_anchor_clamp(anchor); + tags[victim].guard = *guard; +} + +static inline int ws_hud_anchor_lookup(const WsHudAnchorTag *tags, + uint32_t count, + uint32_t command_addr, + const uint32_t *words, + uint32_t word_count, + uint32_t frame, + int *out_anchor) { + if (!tags || !count || !words) return 0; + uint32_t idx = ws_hud_anchor_slot(command_addr) & (count - 1u); + for (uint32_t i = 0; i < WS_HUD_ANCHOR_PROBES && i < count; i++) { + const WsHudAnchorTag *tag = &tags[(idx + i) & (count - 1u)]; + if (!tag->used || tag->command_addr != command_addr) + continue; + if (tag->frame > frame || + frame - tag->frame > WS_HUD_ANCHOR_FRESH_FRAMES) + return 0; + if (!ws_prepass_packet_matches(&tag->guard, words, word_count)) + return 0; + if (out_anchor) *out_anchor = tag->anchor; + return 1; + } + return 0; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/runtime/include/ws_repeat_rect.h b/runtime/include/ws_repeat_rect.h new file mode 100644 index 00000000..01883e1e --- /dev/null +++ b/runtime/include/ws_repeat_rect.h @@ -0,0 +1,186 @@ +#ifndef PSXRECOMP_WS_REPEAT_RECT_H +#define PSXRECOMP_WS_REPEAT_RECT_H + +#include "ws_prepass_guard.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define WS_REPEAT_RECT_TAG_TABLE_SIZE 256u +#define WS_REPEAT_RECT_TAG_PROBES 8u +#define WS_REPEAT_RECT_TAG_FRESH_FRAMES 2u +#define WS_REPEAT_RECT_MAX_SCAN 128 + +typedef struct { + int32_t x; + int32_t u; + int32_t w; +} WsRepeatSpan; + +typedef struct { + uint32_t command_addr; + uint32_t frame; + int32_t period; + uint8_t used; + WsPrepassPacketGuard guard; +} WsRepeatRectTag; + +static inline int ws_repeat_rect_valid_inputs(int32_t x, int32_t u, int32_t w, + int32_t period, + int32_t canonical_left, + int32_t canonical_width, + int32_t margin, int capacity) { + return x >= -8192 && x <= 8192 && + u >= 0 && u <= 255 && + w >= 1 && w <= 256 && + u + w <= 256 && + period >= 1 && period <= 4096 && + canonical_left >= 0 && canonical_left <= 1023 && + canonical_width >= 1 && canonical_width <= 1024 && + margin >= 0 && margin <= 2048 && + capacity >= 0; +} + +static inline int64_t ws_repeat_rect_floor_div(int64_t num, int64_t den) { + int64_t q = num / den; + int64_t r = num % den; + return (r != 0 && ((r < 0) != (den < 0))) ? q - 1 : q; +} + +static inline int64_t ws_repeat_rect_ceil_div(int64_t num, int64_t den) { + return -ws_repeat_rect_floor_div(-num, den); +} + +static inline int ws_repeat_rect_append_span(WsRepeatSpan *out, int capacity, + int write, int count, + int64_t rect_x, int32_t rect_u, + int32_t rect_w, + int64_t band_left, + int64_t band_right) { + int64_t rect_left = rect_x; + int64_t rect_right = rect_x + rect_w; + int64_t clip_left = rect_left > band_left ? rect_left : band_left; + int64_t clip_right = rect_right < band_right ? rect_right : band_right; + if (clip_left >= clip_right) + return count; + + if (write && count < capacity) { + out[count].x = (int32_t)clip_left; + out[count].u = rect_u + (int32_t)(clip_left - rect_left); + out[count].w = (int32_t)(clip_right - clip_left); + } + return count + 1; +} + +static inline int ws_repeat_rect_spans(int32_t x, int32_t u, int32_t w, + int32_t period, + int32_t canonical_left, + int32_t canonical_width, + int32_t margin, WsRepeatSpan *out, + int capacity) { + if (!ws_repeat_rect_valid_inputs(x, u, w, period, canonical_left, + canonical_width, margin, capacity)) + return 0; + if (margin == 0) + return 0; + if (!out) + return 0; + + int64_t left = canonical_left; + int64_t right = left + canonical_width; + int64_t scan_left = left - margin; + int64_t scan_right = right + margin; + int64_t n_min = ws_repeat_rect_ceil_div(scan_left - w + 1 - x, period); + int64_t n_max = ws_repeat_rect_floor_div(scan_right - 1 - x, period); + if (n_max < n_min) + return 0; + if (n_max - n_min + 1 > WS_REPEAT_RECT_MAX_SCAN) + return 0; + + int count = 0; + for (int pass = 0; pass < 2; pass++) { + int write = pass != 0; + count = 0; + for (int64_t n = n_min; n <= n_max; n++) { + int64_t rect_x = (int64_t)x + n * (int64_t)period; + count = ws_repeat_rect_append_span(out, capacity, write, count, + rect_x, u, w, scan_left, left); + count = ws_repeat_rect_append_span(out, capacity, write, count, + rect_x, u, w, right, + scan_right); + if (!write && count > capacity) + return 0; + } + } + + return count; +} + +static inline uint32_t ws_repeat_rect_tag_slot(uint32_t command_addr) { + return (command_addr >> 2) & (WS_REPEAT_RECT_TAG_TABLE_SIZE - 1u); +} + +static inline void ws_repeat_rect_tag_clear(WsRepeatRectTag *tags) { + if (!tags) return; + for (uint32_t i = 0; i < WS_REPEAT_RECT_TAG_TABLE_SIZE; i++) + tags[i].used = 0; +} + +static inline void ws_repeat_rect_tag_insert(WsRepeatRectTag *tags, + uint32_t command_addr, + int32_t period, + const WsPrepassPacketGuard *guard, + uint32_t frame) { + if (!tags || !guard || period <= 0) + return; + uint32_t idx = ws_repeat_rect_tag_slot(command_addr); + uint32_t victim = idx; + for (uint32_t i = 0; i < WS_REPEAT_RECT_TAG_PROBES; i++) { + uint32_t j = (idx + i) & (WS_REPEAT_RECT_TAG_TABLE_SIZE - 1u); + WsRepeatRectTag *tag = &tags[j]; + if (!tag->used || tag->command_addr == command_addr) { + victim = j; + break; + } + if (frame - tag->frame > WS_REPEAT_RECT_TAG_FRESH_FRAMES) + victim = j; + } + + tags[victim].used = 1; + tags[victim].command_addr = command_addr; + tags[victim].frame = frame; + tags[victim].period = period; + tags[victim].guard = *guard; +} + +static inline int32_t ws_repeat_rect_tag_lookup(const WsRepeatRectTag *tags, + uint32_t command_addr, + const uint32_t *words, + uint32_t word_count, + uint32_t frame) { + if (!tags || !words) + return 0; + uint32_t idx = ws_repeat_rect_tag_slot(command_addr); + for (uint32_t i = 0; i < WS_REPEAT_RECT_TAG_PROBES; i++) { + const WsRepeatRectTag *tag = + &tags[(idx + i) & (WS_REPEAT_RECT_TAG_TABLE_SIZE - 1u)]; + if (!tag->used || tag->command_addr != command_addr) + continue; + if (tag->frame > frame || + frame - tag->frame > WS_REPEAT_RECT_TAG_FRESH_FRAMES) + return 0; + if (!ws_prepass_packet_matches(&tag->guard, words, word_count)) + return 0; + return tag->period; + } + return 0; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index d8d218a4..dbdfdb72 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -1983,6 +1983,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t a = cpu->gpr[rs]; if (psx_ws_angle_site(pc, insn, &widened)) cpu->gpr[rt] = widened; + else if (rs == 0 && rt != 0 && psx_ws_is_signed_x_bound_site(pc, insn)) + cpu->gpr[rt] = (uint32_t)psx_ws_screen_x_bound(simm); else cpu->gpr[rt] = a + (uint32_t)simm + (psx_ws_is_cull_bias_site(pc) @@ -2050,7 +2052,10 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, return 0; case 0x0D: { /* ORI */ uint32_t a = cpu->gpr[rs]; - cpu->gpr[rt] = a | imm; + if (rs == 0 && rt != 0 && psx_ws_is_signed_x_bound_site(pc, insn)) + cpu->gpr[rt] = (uint32_t)psx_ws_screen_x_bound((int32_t)imm); + else + cpu->gpr[rt] = a | imm; psx_pgxp_alu(cpu, insn, cpu->gpr[rt], a, imm); cpu->gpr[0] = 0; return 0; @@ -2793,7 +2798,11 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ #ifdef PSX_HAS_OVERLAY_DISPATCH { extern int psx_overlay_dispatch(CPUState *cpu, uint32_t addr); - if (psx_overlay_dispatch(cpu, addr)) return 1; + int previous_phase = g_exec_phase; + g_exec_phase = 3; + int handled = psx_overlay_dispatch(cpu, addr); + g_exec_phase = previous_phase; + if (handled) return 1; } #endif @@ -3197,6 +3206,19 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ target != stop_addr && phys_is_overlay_flow_region(target_phys) && dirty_ram_is_dirty(target_phys)) { +#ifdef PSX_HAS_OVERLAY_DISPATCH + /* A save restore or an uncompiled continuation can enter the + * interpreter in an otherwise static overlay. Surface an exact + * resident match to the outer dispatcher, never nest tail flow. */ + extern int psx_overlay_static_can_dispatch(uint32_t addr); + if (psx_overlay_static_can_dispatch(target)) { + g_dirty_ram_native_handoffs++; + g_dirty_ram_blocks_run++; + if (pc_entry) pc_entry->insns += (uint64_t)insns_executed; + g_dirty_interp_chain_target = target; + OV_FPLOG_RET1(); + } +#endif /* A runtime overlay may start executing while its final code * bytes are still being installed. Entry-time native validation * must reject that partial image, but local dirty flow used to @@ -3247,6 +3269,12 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ e->frame = (uint32_t)s_frame_count; } #endif + /* Local transfers bypass dispatch, but must retain the same + * function-entry hooks as a surfaced interpreter entry. */ + { + extern void psx_mod_function_entry(CPUState *, uint32_t); + psx_mod_function_entry(cpu, target); + } pc = target; current_page = target_phys >> 12; current_page_dirty = 1; /* is_local_dirty_target proved it */ diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 8d91f79a..690a91d0 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -30,6 +30,8 @@ #include "ws_aspect_cone_math.h" #include "ws_ui_group.h" #include "ws_prepass_guard.h" +#include "ws_hud_anchor.h" +#include "ws_repeat_rect.h" #include #include #include @@ -90,6 +92,7 @@ static int ws_hud_sprt = 0; /* edge-anchor untagged HUD SPRTs * static int ws_auto_ui_squash; static int ws_auto_ui_dense; static int ws_active(void); +static int gp0_command_word_count(uint8_t opcode); static uint64_t ws_auto_ui_candidate_count; static uint64_t ws_auto_ui_transform_count; #define WS_UI_PREPASS_MAX 2048u @@ -163,6 +166,9 @@ static void ws_nw_sync_target(void); #define WS_FMV_HYSTERESIS 30 /* frames a colour MDEC decode pins 4:3 */ typedef struct { uint32_t key; uint32_t stamp; int32_t anchor_x; } WsTag; static WsTag ws_tags[WS_TAG_BUCKETS]; +static WsHudAnchorTag ws_hud_anchor_tags[WS_HUD_ANCHOR_TABLE_SIZE]; +static WsHudAnchorTag ws_reveal_clear_tags[WS_HUD_ANCHOR_TABLE_SIZE]; +static WsRepeatRectTag ws_repeat_rect_tags[WS_REPEAT_RECT_TAG_TABLE_SIZE]; static uint32_t ws_last_tag_stamp = (uint32_t)-1000; /* frame of newest tag */ static uint32_t ws_last_3d_stamp = (uint32_t)-1000; /* frame of newest shaded prim (diagnostic) */ extern uint64_t s_frame_count; /* defined in debug_server.c */ @@ -1407,6 +1413,12 @@ int gpu_ws_mmx6_validate(int *bad_out) { return total; } +/* Guarded signed pixel bounds expand by one live margin, with identity at 4:3. */ +int32_t psx_ws_screen_x_bound(int32_t vanilla) { + int32_t margin = psx_ws_x_margin(); + return vanilla < 0 ? vanilla - margin : vanilla > 0 ? vanilla + margin : 0; +} + /* Shared render-funnel screen-X cull widening ([widescreen.cull] auto_screen_x), * called identically by the gcc emit and the interpreter so every * overlay execution path widens the same way. sx = the GTE screen-X as loaded by @@ -1828,6 +1840,91 @@ void psx_ws_sprite_tag(CPUState* cpu) { ws_last_tag_stamp = now; } +static int ws_hud_command_words(uint32_t command_addr, uint32_t *words, + uint32_t *out_count) { + if (!words || !out_count) return 0; + if ((command_addr & 3u) != 0) return 0; + uint32_t phys = command_addr & 0x1FFFFFFFu; + if (phys > 0x00200000u - 4u) return 0; + + words[0] = psx_read_word(command_addr); + uint8_t op = (uint8_t)(words[0] >> 24); + if (op < 0x20u || op > 0x7Fu) return 0; + int count = gp0_command_word_count(op); + if (count <= 0 || count > 12) return 0; + uint32_t bytes = (uint32_t)count * 4u; + if (phys > 0x00200000u - bytes) return 0; + + for (int i = 1; i < count; i++) + words[i] = psx_read_word(command_addr + (uint32_t)i * 4u); + *out_count = (uint32_t)count; + return 1; +} + +void gpu_ws_tag_hud_prim(uint32_t prim, int anchor) { + if (!ws_native_wide_configured()) return; + if ((prim & 3u) != 0 || prim > UINT32_MAX - 4u) return; + uint32_t command_addr = prim + 4u; + uint32_t words[12]; + uint32_t word_count = 0; + if (!ws_hud_command_words(command_addr, words, &word_count)) + return; + + WsPrepassPacketGuard guard = + ws_prepass_packet_guard(words, word_count); + ws_hud_anchor_insert(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE, + command_addr & 0x1FFFFCu, anchor, &guard, + (uint32_t)s_frame_count); +} + +void gpu_ws_tag_black_reveal_rect(uint32_t prim) { + if (!ws_native_wide_configured()) return; + if ((prim & 3u) != 0 || prim > UINT32_MAX - 4u) return; + uint32_t words[12], count = 0; + if (!ws_hud_command_words(prim + 4u, words, &count)) return; + /* Only opaque variable-size textured rectangles define this clear band. */ + if ((words[0] >> 24) != 0x64u && (words[0] >> 24) != 0x65u) return; + WsPrepassPacketGuard guard = ws_prepass_packet_guard(words, count); + ws_hud_anchor_insert(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE, + (prim + 4u) & 0x1FFFFCu, 0, &guard, + (uint32_t)s_frame_count); +} + +void gpu_ws_tag_repeat_rect(uint32_t prim, int32_t period) { + if (!ws_native_wide_configured() || period <= 0 || period > 4096 || + (prim & 3u) || prim > UINT32_MAX - 4u) return; + uint32_t words[12], count = 0; + if (!ws_hud_command_words(prim + 4u, words, &count)) return; + if ((words[0] >> 24) != 0x64u && (words[0] >> 24) != 0x65u) return; + uint32_t w = words[3] & 0xFFFFu; + uint32_t h = words[3] >> 16; + if (!w || w > 256u || (words[2] & 255u) + w > 256u || !h || h > 511u) + return; + WsPrepassPacketGuard guard = ws_prepass_packet_guard(words, count); + ws_repeat_rect_tag_insert(ws_repeat_rect_tags, (prim + 4u) & 0x1FFFFCu, + period, &guard, (uint32_t)s_frame_count); +} + +static int ws_nw_explicit_hud_delta(int32_t *out_delta) { + if (out_delta) *out_delta = 0; + if (!ws_native_wide_active() || gp0_cmd_source_addr == 0xFFFFFFFFu) + return 0; + if (gp0_words_needed <= 0 || gp0_words_needed > 12) + return 0; + uint32_t command_addr = gp0_cmd_source_addr & 0x1FFFFFFFu; + if (command_addr >= 0x00200000u || (command_addr & 3u)) return 0; + int anchor = 0; + if (!ws_hud_anchor_lookup(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE, + command_addr, gp0_cmd_buf, + (uint32_t)gp0_words_needed, + (uint32_t)s_frame_count, &anchor)) + return 0; + int32_t off = ws_nw_offset(); + if (out_delta) + *out_delta = ws_hud_anchor_native_delta(1, off, anchor); + return 1; +} + /* Look up the executing GP0 command's prim in the tag table. The command's * first word lives at prim+4 (the PsyQ P_TAG header precedes it), but accept * a direct hit too in case a tag site passes the colour-word address. */ @@ -2215,6 +2312,8 @@ static int32_t ws_nw_hud_shift(int32_t x, int32_t w) { if (!ws_native_wide_active()) return 0; int32_t off = ws_nw_offset(); if (off <= 0) return 0; + int32_t explicit_delta = 0; + if (ws_nw_explicit_hud_delta(&explicit_delta)) return explicit_delta; if (!ws_nw_left_hud_packet() && !ws_nw_hud_corners) return 0; /* Sprite-tag titles (anchor configured): HUD ≡ UNTAGGED rect-family prims * — the same discriminator the squash path's hud_sprt_squash used. Tagged @@ -2238,6 +2337,12 @@ static int32_t ws_nw_hud_shift(int32_t x, int32_t w) { * the game's dedicated HUD packet arena reaches here, never world polygons. */ static void ws_nw_hud_shift_vertices(int32_t *vx, int count) { if (count <= 0) return; + int32_t explicit_delta = 0; + if (ws_nw_explicit_hud_delta(&explicit_delta)) { + if (explicit_delta) + for (int i = 0; i < count; i++) vx[i] += explicit_delta; + return; + } /* Sprite-tag titles: polygon/line prims are the GTE world and the tagged * character billboards, never HUD — only the rect-family sites (which * call ws_nw_hud_shift directly, with the untagged filter) re-anchor. */ @@ -2569,6 +2674,28 @@ static void ws_clear_all_reveal_margins(void) { gr_wide_clear_margins((int)ws_fb_base[i], 0, 512, 0, 3); } +static void ws_clear_tagged_rect_reveal(int y, int h) { + if (!ws_native_wide_active() || h <= 0 || gp0_cmd_source_addr == UINT32_MAX) + return; + uint32_t address = gp0_cmd_source_addr & 0x1FFFFFFFu; + if (address >= 0x00200000u || (address & 3u) || + !ws_hud_anchor_lookup(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE, + address, gp0_cmd_buf, (uint32_t)gp0_words_needed, + (uint32_t)s_frame_count, NULL)) return; + int base = 0; + if (!ws_local_viewport_draw_target(&base)) { + if (ws_local_viewport_cfg) return; + if (!ws_display_viewport_draw_target(&base)) { + if (!ws_is_fb_base(draw_area_left)) return; + base = (int)draw_area_left; + } + } + int bottom = y + h; + if (y < (int)draw_area_top) y = (int)draw_area_top; + if (bottom > (int)draw_area_bottom + 1) bottom = (int)draw_area_bottom + 1; + if (bottom > y) gr_wide_clear_margins(base, y, bottom - y, 0, 3); +} + /* Stage-init already clears both synthetic margins once. Do not keep clearing * a guessed finite-map side here: MMX6's authored layers enter the reveal at * different times, so the side guess produced a moving black trim over valid @@ -2657,6 +2784,9 @@ static void gpu_reset_state(int clear_vram) { memset(gp0_cmd_buf, 0, sizeof(gp0_cmd_buf)); gp0_next_source_addr = 0xFFFFFFFFu; gp0_cmd_source_addr = 0xFFFFFFFFu; + ws_hud_anchor_clear(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE); + ws_hud_anchor_clear(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE); + ws_repeat_rect_tag_clear(ws_repeat_rect_tags); polyline_color = 0; polyline_prev_x = polyline_prev_y = 0; polyline_prev_c = 0; @@ -4134,6 +4264,32 @@ static void gp0_exec_mono_rect(void) { gr_draw_flat_rect(x0, y0, w, h, color); } +static void ws_repeat_textured_rect_reveal(int x, int y, int w, int h, + int u, int v, uint16_t clut_x, + uint16_t clut_y) { + int margin = draw_area_wide_x_margin(); + int width = (int)ws_disp_w(); + if (margin <= 0 || gp0_cmd_source_addr == UINT32_MAX || + draw_area_right < draw_area_left || + draw_area_right - draw_area_left + 1u != (uint32_t)width) + return; + uint32_t address = gp0_cmd_source_addr & 0x1FFFFFFFu; + if (address >= 0x00200000u || (address & 3u)) return; + int32_t period = ws_repeat_rect_tag_lookup(ws_repeat_rect_tags, address, + gp0_cmd_buf, (uint32_t)gp0_words_needed, (uint32_t)s_frame_count); + if (!period) return; + WsRepeatSpan spans[128]; + int count = ws_repeat_rect_spans(x, u, w, period, (int)draw_area_left, + width, margin, spans, 128); + for (int i = 0; i < count; ++i) { + if (draw_area_out_rect(spans[i].x, y, spans[i].w, h)) continue; + /* Each span is outside the canonical draw area. Backend scissoring + * discards canonical writes while the native-wide mirror draws it. */ + gr_draw_textured_rect(spans[i].x, y, spans[i].w, h, spans[i].u, v, + clut_x, clut_y, current_texpage()); + } +} + /* Execute textured rectangle (GP0 0x64-0x67) */ static void gp0_exec_textured_rect(void) { uint32_t color24 = gp0_cmd_buf[0] & 0xFFFFFFu; @@ -4150,6 +4306,7 @@ static void gp0_exec_textured_rect(void) { int h = (gp0_cmd_buf[3] >> 16) & 0x1FF; if (w > 1023) w = 1023; if (h > 511) h = 511; + ws_clear_tagged_rect_reveal(y0 + draw_offset_y, h); /* Widescreen: tagged sprite parts squash around their projected anchor; * untagged SPRTs are screen-space 2D (HUD/menus) and squash around the @@ -4183,6 +4340,7 @@ static void gp0_exec_textured_rect(void) { clut_x, clut_y, current_texpage()); else gr_draw_textured_rect(x0, y0, w, h, u0, v0, clut_x, clut_y, current_texpage()); + ws_repeat_textured_rect_reveal(x0, y0, w, h, u0, v0, clut_x, clut_y); } /* Execute 1x1 dot (GP0 0x68-0x6B) */ @@ -5278,6 +5436,7 @@ static void gp0_execute_command(void) { parse_vertex(gp0_cmd_buf[1], &x0, &y0); (void)ws_sprt_fixed_transform(&x0, y0, 1); /* position only; 1px stays 1px */ x0 += draw_offset_x; y0 += draw_offset_y; + x0 = ws_nw_hud_shift(x0, 1); int u0 = gp0_cmd_buf[2] & 0xFF; int v0 = (gp0_cmd_buf[2] >> 8) & 0xFF; uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16); @@ -5937,6 +6096,9 @@ int gpu_snapshot_read(const uint8_t *p, uint32_t len) { if (len != gpu_snapshot_bytes()) return 0; pst_r_init(&r, p, len); if (!gpu_snap_parse(&r)) return 0; + ws_hud_anchor_clear(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE); + ws_hud_anchor_clear(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE); + ws_repeat_rect_tag_clear(ws_repeat_rect_tags); /* Sync renderer clip/scissor to restored GP0(E3/E4); vars alone leave GL * on a stale draw area after savestate load. */ gr_set_draw_area((int)draw_area_left, (int)draw_area_top, diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 0fa3232a..b85cd551 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -3464,6 +3464,7 @@ static void glb_wide_clear(int base_x, int y, int h, uint16_t color) { static void glb_wide_clear_margins(int base_x, int y, int h, uint16_t color, int sides) { if (!s_raster_ok || s_ws_ablate == 1 || g_wide_off <= 0) return; double t0 = cw_ms(); s_cw_wide_clears++; + flush_flat_batch(); flush_tex_batch(); GLuint fbo = wide_fbo_for(base_x); if (!fbo) { s_cw_wide_ms += cw_ms() - t0; return; } diff --git a/runtime/tests/test_interp_mod_entry_guards.py b/runtime/tests/test_interp_mod_entry_guards.py new file mode 100644 index 00000000..7562d5da --- /dev/null +++ b/runtime/tests/test_interp_mod_entry_guards.py @@ -0,0 +1,33 @@ +"""Keep local interpreted control flow from bypassing mod entry callbacks.""" + +from pathlib import Path + + +def main(): + source = (Path(__file__).resolve().parents[1] / + 'src/dirty_ram_interp.c').read_text(encoding='utf-8') + start = source.index('static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {') + source = source[start:] + static_dispatch = source[source.index('/* B-2: statically-compiled'):] + static_dispatch = static_dispatch[:static_dispatch.index('/* A-1:')] + assert static_dispatch.index('g_exec_phase = 3;') < static_dispatch.index( + 'psx_overlay_dispatch(cpu, addr);') < static_dispatch.index( + 'g_exec_phase = previous_phase;') < static_dispatch.index('if (handled) return 1;') + assert source.count('psx_mod_function_entry(cpu, addr);') == 1 + local_start = source.index('if (allow_local_dirty_flow && target != 0 &&') + local_end = source.index('current_page = target_phys >> 12;', local_start) + local = source[local_start:local_end] + probe_start = local.index('if (psx_overlay_static_can_dispatch(target))') + probe_end = local.index('#endif', probe_start) + probe = local[probe_start:probe_end] + assert 'g_dirty_interp_chain_target = target;' in probe + assert 'OV_FPLOG_RET1();' in probe + assert 'psx_overlay_dispatch(cpu' not in probe + assert local.count('psx_mod_function_entry(cpu, target);') == 1 + assert local.index('overlay_loader_dispatch(cpu, target)') < local.index( + 'psx_mod_function_entry(cpu, target);') < local.index('pc = target;') + print('interpreter mod entry guards: PASS') + + +if __name__ == '__main__': + main() diff --git a/runtime/tests/test_ws_hud_anchor.c b/runtime/tests/test_ws_hud_anchor.c new file mode 100644 index 00000000..7c27fc9d --- /dev/null +++ b/runtime/tests/test_ws_hud_anchor.c @@ -0,0 +1,70 @@ +#include "ws_hud_anchor.h" + +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include + +static void make_guard(WsPrepassPacketGuard *guard, + const uint32_t *words, uint32_t count) { + *guard = ws_prepass_packet_guard(words, count); +} + +int main(void) { + WsHudAnchorTag tags[WS_HUD_ANCHOR_TABLE_SIZE]; + uint32_t packet[] = { + 0x64010203u, 0x00100020u, 0x00040008u, 0x00100010u + }; + WsPrepassPacketGuard guard; + int anchor = 99; + + ws_hud_anchor_clear(tags, WS_HUD_ANCHOR_TABLE_SIZE); + make_guard(&guard, packet, 4u); + + assert(ws_hud_anchor_native_delta(0, 53, -1) == 0); + assert(ws_hud_anchor_native_delta(1, 0, 1) == 0); + assert(ws_hud_anchor_native_delta(1, 53, -1) == -53); + assert(ws_hud_anchor_native_delta(1, 53, 0) == 0); + assert(ws_hud_anchor_native_delta(1, 53, 1) == 53); + + ws_hud_anchor_insert(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, 9, &guard, 10u); + assert(ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, packet, 4u, 10u, &anchor)); + assert(anchor == 1); + + anchor = 99; + assert(ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, packet, 4u, 12u, &anchor)); + assert(anchor == 1); + + assert(!ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, packet, 4u, 9u, &anchor)); + + assert(!ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, packet, 4u, 13u, &anchor)); + + packet[3] = 0x00200010u; + assert(!ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, packet, 4u, 11u, &anchor)); + + packet[3] = 0x00100010u; + ws_hud_anchor_insert(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, -7, &guard, 20u); + anchor = 99; + assert(ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, packet, 4u, 20u, &anchor)); + assert(anchor == -1); + + assert(!ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010008u, packet, 4u, 20u, &anchor)); + + ws_hud_anchor_clear(tags, WS_HUD_ANCHOR_TABLE_SIZE); + assert(!ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE, + 0x80010004u, packet, 4u, 20u, &anchor)); + + puts("ws_hud_anchor_test: PASS"); + return 0; +} diff --git a/runtime/tests/test_ws_repeat_rect.c b/runtime/tests/test_ws_repeat_rect.c new file mode 100644 index 00000000..61b37e3e --- /dev/null +++ b/runtime/tests/test_ws_repeat_rect.c @@ -0,0 +1,196 @@ +#include "ws_repeat_rect.h" + +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include + +static void assert_span(const WsRepeatSpan *span, int32_t x, int32_t u, + int32_t w) { + assert(span->x == x); + assert(span->u == u); + assert(span->w == w); +} + +static int emit_spans(int32_t x, int32_t u, int32_t w, int32_t period, + int32_t left, int32_t width, int32_t margin, + WsRepeatSpan *out, int capacity) { + for (int i = 0; i < capacity; i++) { + out[i].x = 0x12345678; + out[i].u = 0x12345678; + out[i].w = 0x12345678; + } + return ws_repeat_rect_spans(x, u, w, period, left, width, margin, out, + capacity); +} + +static void test_margin_zero(void) { + WsRepeatSpan spans[4]; + assert(emit_spans(0, 0, 128, 512, 0, 512, 0, spans, 4) == 0); + assert(spans[0].x == 0x12345678); +} + +static void test_both_sides_and_u_offsets(void) { + WsRepeatSpan spans[4]; + int count = emit_spans(-10, 0, 20, 512, 0, 512, 20, spans, 4); + assert(count == 2); + assert_span(&spans[0], -10, 0, 10); + assert_span(&spans[1], 512, 10, 10); +} + +static void test_xoffset_framebuffer(void) { + WsRepeatSpan spans[4]; + int count = emit_spans(502, 12, 20, 512, 512, 512, 20, spans, 4); + assert(count == 2); + assert_span(&spans[0], 502, 12, 10); + assert_span(&spans[1], 1024, 22, 10); +} + +static void test_negative_x_partial_strip(void) { + WsRepeatSpan spans[4]; + int count = emit_spans(-30, 10, 60, 512, 0, 512, 64, spans, 4); + assert(count == 2); + assert_span(&spans[0], -30, 10, 30); + assert_span(&spans[1], 512, 40, 30); + + count = emit_spans(500, 4, 20, 512, 0, 512, 16, spans, 4); + assert(count == 2); + assert_span(&spans[0], -12, 4, 12); + assert_span(&spans[1], 512, 16, 8); +} + +static void mark_span_coverage(const WsRepeatSpan *span, int *left_cov, + int *right_cov) { + for (int32_t x = span->x; x < span->x + span->w; x++) { + if (x >= -256 && x < 0) + left_cov[x + 256]++; + else if (x >= 512 && x < 768) + right_cov[x - 512]++; + else + assert(0 && "span overlapped canonical area or outer margin"); + } +} + +static void test_full_512_composite_all_phases(void) { + for (int phase = 0; phase < 256; phase++) { + int left_cov[256] = {0}; + int right_cov[256] = {0}; + WsRepeatSpan spans[4]; + int total = 0; + + if (phase == 0) { + const int32_t pieces[2][3] = { + {0, 0, 256}, + {256, 0, 256}, + }; + for (int i = 0; i < 2; i++) { + int count = emit_spans(pieces[i][0], pieces[i][1], + pieces[i][2], 512, 0, 512, 256, + spans, 4); + total += count; + for (int j = 0; j < count; j++) + mark_span_coverage(&spans[j], left_cov, right_cov); + } + } else { + const int32_t pieces[4][3] = { + {512 - phase, 0, phase}, + {256, phase, 256 - phase}, + {256 - phase, 0, phase}, + {0, phase, 256 - phase}, + }; + for (int i = 0; i < 4; i++) { + int count = emit_spans(pieces[i][0], pieces[i][1], + pieces[i][2], 512, 0, 512, 256, + spans, 4); + total += count; + for (int j = 0; j < count; j++) + mark_span_coverage(&spans[j], left_cov, right_cov); + } + } + + assert(total == (phase == 0 ? 2 : 4)); + for (int i = 0; i < 256; i++) { + assert(left_cov[i] == 1); + assert(right_cov[i] == 1); + } + } +} + +static void test_bad_inputs_and_capacity(void) { + WsRepeatSpan spans[8]; + + assert(emit_spans(0, 0, 20, 0, 0, 512, 32, spans, 8) == 0); + assert(emit_spans(0, 0, 0, 512, 0, 512, 32, spans, 8) == 0); + assert(emit_spans(0, 0, 257, 512, 0, 512, 32, spans, 8) == 0); + assert(emit_spans(0, -1, 20, 512, 0, 512, 32, spans, 8) == 0); + assert(emit_spans(0, 240, 20, 512, 0, 512, 32, spans, 8) == 0); + assert(emit_spans(9000, 0, 20, 512, 0, 512, 32, spans, 8) == 0); + assert(emit_spans(0, 0, 20, 512, -1, 512, 32, spans, 8) == 0); + assert(emit_spans(0, 0, 20, 512, 0, 0, 32, spans, 8) == 0); + assert(emit_spans(0, 0, 20, 512, 0, 512, -1, spans, 8) == 0); + + assert(emit_spans(-10, 0, 20, 512, 0, 512, 20, spans, 1) == 0); + assert(spans[0].x == 0x12345678); + + assert(emit_spans(0, 0, 1, 1, 0, 512, 2048, spans, 8) == 0); +} + +static void test_tag_table(void) { + WsRepeatRectTag tags[WS_REPEAT_RECT_TAG_TABLE_SIZE]; + uint32_t packet[] = { + 0x64010203u, 0x00100020u, 0x00040008u, 0x00100010u + }; + WsPrepassPacketGuard guard = ws_prepass_packet_guard(packet, 4u); + + ws_repeat_rect_tag_clear(tags); + ws_repeat_rect_tag_insert(tags, 0x80010004u, 512, &guard, 100u); + + assert(ws_repeat_rect_tag_lookup(tags, 0x80010004u, packet, 4u, 100u) == + 512); + assert(ws_repeat_rect_tag_lookup(tags, 0x80010004u, packet, 4u, 102u) == + 512); + assert(ws_repeat_rect_tag_lookup(tags, 0x80010004u, packet, 4u, 99u) == + 0); + assert(ws_repeat_rect_tag_lookup(tags, 0x80010004u, packet, 4u, 103u) == + 0); + assert(ws_repeat_rect_tag_lookup(tags, 0x80010008u, packet, 4u, 100u) == + 0); + assert(ws_repeat_rect_tag_lookup(tags, 0x80010004u, packet, 3u, 100u) == + 0); + + packet[3] = 0x00200010u; + assert(ws_repeat_rect_tag_lookup(tags, 0x80010004u, packet, 4u, 100u) == + 0); + packet[3] = 0x00100010u; + + ws_repeat_rect_tag_insert(tags, 0x80010004u, 256, &guard, 104u); + assert(ws_repeat_rect_tag_lookup(tags, 0x80010004u, packet, 4u, 104u) == + 256); + + ws_repeat_rect_tag_insert(tags, 0x80010404u, 128, &guard, 104u); + assert(ws_repeat_rect_tag_lookup(tags, 0x80010404u, packet, 4u, 104u) == + 128); + + ws_repeat_rect_tag_insert(tags, 0x80010804u, 0, &guard, 104u); + assert(ws_repeat_rect_tag_lookup(tags, 0x80010804u, packet, 4u, 104u) == + 0); + + ws_repeat_rect_tag_clear(tags); + assert(ws_repeat_rect_tag_lookup(tags, 0x80010004u, packet, 4u, 104u) == + 0); +} + +int main(void) { + test_margin_zero(); + test_both_sides_and_u_offsets(); + test_xoffset_framebuffer(); + test_negative_x_partial_strip(); + test_full_512_composite_all_phases(); + test_bad_inputs_and_capacity(); + test_tag_table(); + + puts("ws_repeat_rect_test: PASS"); + return 0; +} diff --git a/tools/compile_overlays.py b/tools/compile_overlays.py index 19e70488..8891fd54 100644 --- a/tools/compile_overlays.py +++ b/tools/compile_overlays.py @@ -2624,17 +2624,27 @@ def generate_overlay_dispatch(variants: list) -> str: ' if (address_misses) *address_misses = psx_ov_static_address_misses;', '}', '', - 'int psx_overlay_dispatch(CPUState *cpu, uint32_t addr) {', + 'static const PsxOvVariant *psx_overlay_static_find_variant(uint32_t addr) {', ' const uint32_t key = (addr & 0x1FFFFFFFu) | 0x80000000u;', ] if not flat_variants: lines += [ - ' (void)cpu; (void)key;', + ' (void)key;', ' psx_ov_static_address_misses++;', ' return 0;', '}', '', + 'int psx_overlay_static_can_dispatch(uint32_t addr) {', + ' return psx_overlay_static_find_variant(addr) != 0;', + '}', + '', + 'int psx_overlay_dispatch(CPUState *cpu, uint32_t addr) {', + ' (void)cpu;', + ' if (psx_overlay_static_find_variant(addr) == 0) return 0;', + ' return 0;', + '}', + '', ] return '\n'.join(lines) @@ -2668,9 +2678,7 @@ def generate_overlay_dispatch(variants: list) -> str: ' psx_ov_static_checks++;', ' if (psx_overlay_static_code_matches(v->ranges, v->count,', ' v->crc)) {', - ' psx_ov_static_hits++;', - ' v->fn(cpu);', - ' return 1;', + ' return v;', ' }', ' psx_ov_static_variant_misses++;', ' }', @@ -2680,10 +2688,8 @@ def generate_overlay_dispatch(variants: list) -> str: ' psx_ov_static_checks++;', ' if (psx_overlay_static_code_matches(v->ranges, v->count,', ' v->crc)) {', - ' psx_ov_static_hits++;', ' psx_ov_last_hit[ei] = (uint16_t)i;', - ' v->fn(cpu);', - ' return 1;', + ' return v;', ' }', ' psx_ov_static_variant_misses++;', ' }', @@ -2692,6 +2698,18 @@ def generate_overlay_dispatch(variants: list) -> str: ' return 0;', '}', '', + 'int psx_overlay_static_can_dispatch(uint32_t addr) {', + ' return psx_overlay_static_find_variant(addr) != 0;', + '}', + '', + 'int psx_overlay_dispatch(CPUState *cpu, uint32_t addr) {', + ' const PsxOvVariant *v = psx_overlay_static_find_variant(addr);', + ' if (!v) return 0;', + ' psx_ov_static_hits++;', + ' v->fn(cpu);', + ' return 1;', + '}', + '', ] return '\n'.join(lines) diff --git a/tools/test_compile_overlays_static_split.py b/tools/test_compile_overlays_static_split.py index 4e880e81..0d9f8c3f 100644 --- a/tools/test_compile_overlays_static_split.py +++ b/tools/test_compile_overlays_static_split.py @@ -4,8 +4,11 @@ """ import os import pickle +import shutil +import subprocess import sys import tempfile +import textwrap import time import unittest from pathlib import Path @@ -109,6 +112,97 @@ class StaticSplitTests(unittest.TestCase): 'overlays_static_0001.c']) +class StaticDispatchApiTests(unittest.TestCase): + def test_empty_dispatcher_exports_can_dispatch_api(self): + dispatch = compile_overlays.generate_overlay_dispatch([]) + self.assertIn('int psx_overlay_static_can_dispatch(uint32_t addr)', dispatch) + self.assertIn('int psx_overlay_dispatch(CPUState *cpu, uint32_t addr)', dispatch) + self.assertIn('psx_overlay_static_find_variant(addr)', dispatch) + + def test_can_dispatch_uses_crc_lookup_and_does_not_invoke_cpu(self): + cc = shutil.which('gcc') or shutil.which('clang') + if cc is None: + self.skipTest('no C compiler available') + + variants = [ + {'addr': 0x80100000, 'symbol': 'ov_func_a', 'crc': 0x11111111, + 'ranges': ((0x00100000, 8),)}, + {'addr': 0x80100000, 'symbol': 'ov_func_b', 'crc': 0x22222222, + 'ranges': ((0x00100000, 8),)}, + ] + dispatch = compile_overlays.generate_overlay_dispatch(variants) + with tempfile.TemporaryDirectory() as tmp: + Path(tmp, 'psx_runtime.h').write_text( + '#include \n' + 'typedef struct CPUState { uint32_t pc; } CPUState;\n', + encoding='utf-8') + exe = os.path.join(tmp, 'dispatch_api.exe' if os.name == 'nt' + else 'dispatch_api') + src = os.path.join(tmp, 'dispatch_api.c') + Path(src).write_text(textwrap.dedent(f''' + #include "psx_runtime.h" + #include + + void ov_func_a(CPUState *cpu); + void ov_func_b(CPUState *cpu); + + {dispatch} + + static uint32_t resident_crc; + static int match_checks; + static int calls_a; + static int calls_b; + + int psx_overlay_static_code_matches( + const uint32_t *lo_len_pairs, uint32_t count, + uint32_t expected_crc) + {{ + (void)lo_len_pairs; + (void)count; + match_checks++; + return expected_crc == resident_crc; + }} + + void ov_func_a(CPUState *cpu) {{ (void)cpu; calls_a++; }} + void ov_func_b(CPUState *cpu) {{ (void)cpu; calls_b++; }} + + int main(void) + {{ + CPUState cpu = {{0}}; + uint64_t checks = 0, hits = 0, vm = 0, am = 0; + + resident_crc = 0x22222222u; + if (!psx_overlay_static_can_dispatch(0x80100000u)) return 1; + if (calls_a != 0 || calls_b != 0) return 2; + + psx_overlay_static_get_stats(&checks, &hits, &vm, &am); + if (checks != 2 || hits != 0 || vm != 1 || am != 0) {{ + fprintf(stderr, "stats after can: %llu %llu %llu %llu\\n", + (unsigned long long)checks, + (unsigned long long)hits, + (unsigned long long)vm, + (unsigned long long)am); + return 3; + }} + + if (!psx_overlay_dispatch(&cpu, 0x80100000u)) return 4; + if (calls_a != 0 || calls_b != 1) return 5; + if (match_checks != 3) return 6; + + resident_crc = 0x33333333u; + if (psx_overlay_static_can_dispatch(0x80100000u)) return 7; + if (calls_a != 0 || calls_b != 1) return 8; + + if (psx_overlay_static_can_dispatch(0x80100004u)) return 9; + if (calls_a != 0 || calls_b != 1) return 10; + return 0; + }} + '''), encoding='utf-8') + subprocess.run([cc, src, '-I', tmp, '-std=c99', '-Wall', '-Werror', + '-o', exe], check=True) + subprocess.run([exe], check=True) + + class StaticWorkerContractTests(unittest.TestCase): def test_worker_is_picklable_for_a_process_pool(self): # ProcessPoolExecutor pickles the callable by qualified name; a nested