Support native-wide HUD and tiled backgrounds with guarded culling

This commit is contained in:
Matthew Stanley
2026-09-07 22:40:23 -07:00
parent d3e91e07b5
commit 912c44c75e
19 changed files with 1265 additions and 21 deletions
+16
View File
@@ -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:
+26
View File
@@ -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
)
+23 -4
View File
@@ -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";
+20 -2
View File
@@ -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}",
+4 -4
View File
@@ -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;
}
+10
View File
@@ -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
View File
@@ -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);
+100
View File
@@ -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
+186
View File
@@ -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
+30 -2
View File
@@ -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 */
+162
View File
@@ -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,
+1
View File
@@ -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()
+70
View File
@@ -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;
}
+196
View File
@@ -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;
}
+26 -8
View File
@@ -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