mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Support native-wide HUD and tiled backgrounds with guarded culling
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<std::string>(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}",
|
||||
|
||||
@@ -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<WidescreenSignedBoundSite> ws_signed_x_bound_sites;
|
||||
// [widescreen] offer — whether the launcher OFFERS its EXPERIMENTAL
|
||||
// Widescreen toggle for this title. Default true. Set false while a
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
#include "code_generator.h"
|
||||
#include "config_loader.h"
|
||||
#include "control_flow.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<uint8_t>& bytes, uint32_t word) {
|
||||
bytes.push_back(static_cast<uint8_t>(word));
|
||||
bytes.push_back(static_cast<uint8_t>(word >> 8));
|
||||
bytes.push_back(static_cast<uint8_t>(word >> 16));
|
||||
bytes.push_back(static_cast<uint8_t>(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<PSXRecomp::GeneratedFunction> 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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+13
-1
@@ -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);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#ifndef PSXRECOMP_WS_HUD_ANCHOR_H
|
||||
#define PSXRECOMP_WS_HUD_ANCHOR_H
|
||||
|
||||
#include "ws_prepass_guard.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,186 @@
|
||||
#ifndef PSXRECOMP_WS_REPEAT_RECT_H
|
||||
#define PSXRECOMP_WS_REPEAT_RECT_H
|
||||
|
||||
#include "ws_prepass_guard.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "ws_hud_anchor.h"
|
||||
|
||||
#ifdef NDEBUG
|
||||
#undef NDEBUG
|
||||
#endif
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
#include "ws_repeat_rect.h"
|
||||
|
||||
#ifdef NDEBUG
|
||||
#undef NDEBUG
|
||||
#endif
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 <stdint.h>\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 <stdio.h>
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user