mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Add opt-in draw-distance clamps ([[draw_distance.clamp]])
Many PS1 renderers drop a primitive whose depth index falls past the end of their ordering table. Raising that limit can write past the table, so a title can now list such guards in game.toml with the register that carries the index and the last safe value. While a trusted mod has called psx_mod_set_draw_distance_clamp(1), generated main-EXE code clamps that register before the guard runs, so far geometry is kept in the farthest slot instead of popping in later. - Off by default and reset at every session start, so netplay and every title that lists no sites run unchanged; with no sites the generated code and declarations are byte-identical. - Main executable only: the full instruction word is the guard, generation fails on a mismatch or a load-delay hazard, captured overlay code keeps its own code (the sites are not part of the overlay config identity), and the dirty-RAM interpreter applies the clamp where the game's text image holds the listed word. - Compatibility: code_generator.cpp is a codegen-hash input, so the codegen hash and the overlay-cache tag change for every title, sites or not. Savestates from earlier builds are refused, and overlay shards rebuild once on first visit. Generated code and behaviour are unchanged for a title without sites or with the switch off. - Tests: draw_distance_codegen_test (config rules, emission, guards, declarations, overlay identity) and draw_distance_interp_test (store, switch, interpreted guards); the session-reset guard covers the switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -979,3 +979,17 @@ surface, including copies between the buffers and 1000-column copies staged
|
||||
in chunks; at 18x both buffers must read back at S with two tiles. In R4 on an M4, the 8K preset reports
|
||||
`effective_scale 18, internal_lines 4320, hr_scale 1, hires_fbo 5760x9216`, and
|
||||
`screenshot_hires` in a race is 5760×4320 with the rear-view mirror present.
|
||||
|
||||
## DD1 — Opt-in draw-distance clamps (2026-10-01)
|
||||
|
||||
`[[draw_distance.clamp]]` (docs/config_schema.md) lists a title's
|
||||
ordering-table range guards. While a trusted mod has called
|
||||
`psx_mod_set_draw_distance_clamp(1)`, the guard's depth index is clamped to
|
||||
the last safe slot before the guard runs, so far geometry the game would drop
|
||||
stays in the farthest bucket (drawn first, under everything nearer) instead
|
||||
of popping in later. Raising the limit instead can push a biased primitive
|
||||
past the end of the table. Off by default and at every session start;
|
||||
identity when off; main executable only (overlay code keeps its own code);
|
||||
native code and the dirty-RAM interpreter agree
|
||||
(`draw_distance_codegen_test`, `draw_distance_interp_test`). First user: R4's
|
||||
course renderers (RidgeRacerType4Recomp, Max Detail).
|
||||
|
||||
@@ -548,6 +548,7 @@ defensive, for the state listed here.
|
||||
| World-scene predicate | `psx_mod_set_world_scene_predicate` | NULL | every session |
|
||||
| Retained-scene predicate | `psx_mod_set_retained_scene_predicate` | NULL | every session |
|
||||
| Adaptive backdrop preload | `psx_mod_set_adaptive_backdrop_preload` | 0 | every session |
|
||||
| Draw-distance clamps | `psx_mod_set_draw_distance_clamp` | off | every session |
|
||||
| Bezel artwork | `psx_mod_set_bezel_artwork` | none (see below) | every session |
|
||||
| Frame-interpolation blend mode | `psx_mod_set_frame_interpolation_blend` | default | every session |
|
||||
| Native VBlank pacing and its rate | `psx_mod_set_native_vblank_rate` | off, 0 | every session |
|
||||
@@ -621,6 +622,12 @@ selects uncapped host speed. A zero-frame release stops acceleration as soon as
|
||||
the sustained-load predicate clears, which is appropriate for timing-sensitive
|
||||
or speedrun-oriented packages.
|
||||
|
||||
`psx_mod_set_draw_distance_clamp(enabled)` switches the title's
|
||||
`[[draw_distance.clamp]]` sites (docs/config_schema.md) on for the session: a
|
||||
far primitive the game would drop past the end of its ordering table is kept
|
||||
in the farthest slot. It returns 0 when the title lists no sites. More
|
||||
primitives mean more guest work, so it belongs to an opt-in feature.
|
||||
|
||||
`psx_mod_set_disc_speed(divisor, instant_max_per_frame)` is the guest-visible
|
||||
alternative. Divisors 2 and 4 shorten emulated CD deadlines; zero selects the
|
||||
bounded instant scheduler. Because this changes interrupt timing, packages
|
||||
|
||||
@@ -56,6 +56,7 @@ How the two configs relate:
|
||||
[prepare_disc] # optional; data-track digests for prepare/verify
|
||||
[netplay] # optional; TOC / cue policy for online
|
||||
[recompiler]
|
||||
[draw_distance] # optional; opt-in far-geometry clamps (below)
|
||||
[runtime]
|
||||
[audit]
|
||||
```
|
||||
@@ -372,6 +373,53 @@ vertical scan X bounds, and any associated respawn-reset interval together.
|
||||
Keep authored placement flags and vertical bounds intact. Empty is inert;
|
||||
configured sites require regeneration.
|
||||
|
||||
## Draw-distance clamps (`[[draw_distance.clamp]]`)
|
||||
|
||||
Many PS1 renderers drop a primitive whose depth index falls past the end of
|
||||
their ordering table (`sltiu t, z, N; beqz t, reject`). The limit protects the
|
||||
OT, so raising it can write past the table. A title can list such guards and
|
||||
let a trusted mod keep the far primitive in the farthest slot instead:
|
||||
|
||||
```toml
|
||||
[[draw_distance.clamp]]
|
||||
address = "0x80061230"
|
||||
expected = "0x2C4A01C0" # sltiu t2, v0, 0x1C0
|
||||
reg = 2 # v0: the depth index the guard tests
|
||||
max = 0x1BF # last safe index
|
||||
|
||||
[[draw_distance.clamp]]
|
||||
address = "0x80066FF8"
|
||||
expected = "0x2441FFFF" # addiu at, v0, -1 (then sltiu at, at, 0x1BF)
|
||||
reg = 2
|
||||
max = 0x1BF
|
||||
```
|
||||
|
||||
- While a mod has called `psx_mod_set_draw_distance_clamp(1)`, `reg` is
|
||||
clamped to `max` (signed: `if ((int32_t)reg > max) reg = max`) immediately
|
||||
before the instruction at `address` runs. The original guard then keeps
|
||||
the primitive and the code that follows indexes the table with the clamped
|
||||
value. A negative (wrapped) value is left alone, so a guard that also
|
||||
rejects too-near primitives keeps doing so.
|
||||
- Off, the default and the state at every session start, the sites run the
|
||||
original code. Netplay never activates a mod, so it stays vanilla.
|
||||
- `expected` is the complete instruction word and must be an ALU
|
||||
instruction (`ADDI`/`ADDIU`/`SLTI`/`SLTIU`/`ANDI`/`ORI`/`XORI`, or a
|
||||
SPECIAL shift/arithmetic/logic/`SLT`/`SLTU`) that reads `reg`. `reg` is
|
||||
1..31; `max` is a signed 32-bit integer. Addresses are unique by physical
|
||||
address.
|
||||
- Main executable only. Generation fails when the listed word is not
|
||||
`expected`, or when the previous instruction loads `reg` (its value would
|
||||
still be in the load-delay slot). Captured overlay code at a listed address
|
||||
keeps its own code, so the sites do not enter the overlay-cache identity.
|
||||
The dirty-RAM interpreter applies a clamp where the game's text image holds
|
||||
the listed word.
|
||||
- Pick `max` so the clamped index reaches no further than the farthest index
|
||||
the unclamped code can already produce. Choose the register the following
|
||||
code really indexes with: in the `addiu` form above, the guard tests `at`
|
||||
but the slot is computed from `v0`.
|
||||
- Regenerate after changing the list. With no sites the generated code is
|
||||
unchanged.
|
||||
|
||||
## Runtime block
|
||||
|
||||
Consumed by the cmake macro `psxrecomp_v4_add_runtime_target` (eventually)
|
||||
|
||||
@@ -531,6 +531,34 @@ if(BUILD_TESTING)
|
||||
add_test(NAME ws_cull_edge_codegen_test
|
||||
COMMAND ws_cull_edge_codegen_test)
|
||||
|
||||
# [[draw_distance.clamp]]: config rules, the emitted main-EXE clamp and
|
||||
# its guards, the shared declaration and the unchanged overlay identity.
|
||||
add_executable(draw_distance_codegen_test
|
||||
tests/draw_distance_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(draw_distance_codegen_test PRIVATE
|
||||
include
|
||||
src
|
||||
lib/rabbitizer/include
|
||||
lib/rabbitizer/cplusplus/include
|
||||
lib/fmt/include
|
||||
lib/toml11
|
||||
lib/ELFIO
|
||||
)
|
||||
target_link_libraries(draw_distance_codegen_test PRIVATE fmt rabbitizer)
|
||||
add_test(NAME draw_distance_codegen_test
|
||||
COMMAND draw_distance_codegen_test)
|
||||
|
||||
# The same explicit cull kinds in the dirty-RAM interpreter: gpu.c's
|
||||
# sorted, capped site store and real interpreted bgez/bltz/branch-keep/
|
||||
# clip-edge instructions at margin 0 and at a forced margin. The test
|
||||
@@ -560,6 +588,33 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_BINARY_DIR}/ws_cull_edge_interp_stderr.txt)
|
||||
endif()
|
||||
|
||||
# [[draw_distance.clamp]] in the dirty-RAM interpreter: draw_distance.c's
|
||||
# store and switch and real interpreted guard instructions. The test
|
||||
# #includes draw_distance.c and dirty_ram_interp.c (as above).
|
||||
if(NOT MSVC)
|
||||
add_executable(draw_distance_interp_test
|
||||
../runtime/tests/test_draw_distance_interp.c
|
||||
../runtime/src/ws_aspect_cone_math.c)
|
||||
target_include_directories(draw_distance_interp_test PRIVATE
|
||||
../runtime/include
|
||||
../runtime/src)
|
||||
target_compile_options(draw_distance_interp_test PRIVATE
|
||||
-O2 -flto -ffunction-sections -fdata-sections)
|
||||
if(APPLE)
|
||||
target_link_options(draw_distance_interp_test PRIVATE
|
||||
-flto -Wl,-dead_strip)
|
||||
else()
|
||||
target_link_options(draw_distance_interp_test PRIVATE
|
||||
-flto -Wl,--gc-sections)
|
||||
endif()
|
||||
if(NOT WIN32)
|
||||
target_link_libraries(draw_distance_interp_test PRIVATE m)
|
||||
endif()
|
||||
add_test(NAME draw_distance_interp_test
|
||||
COMMAND draw_distance_interp_test
|
||||
${CMAKE_CURRENT_BINARY_DIR}/draw_distance_interp_stderr.txt)
|
||||
endif()
|
||||
|
||||
# Uncached (KSEG1) code charges a fetch before every instruction; cached
|
||||
# code keeps the line-leader rule (test_uncached_fetch_charge.py checks
|
||||
# the BIOS emitter on OpenBIOS and the cycle model).
|
||||
|
||||
@@ -177,6 +177,11 @@ struct CodeGenConfig {
|
||||
// scales tan(angle) by the current horizontal reveal factor.
|
||||
std::vector<PSXRecompV4::WidescreenAngleSite> ws_cull_angle_sites;
|
||||
|
||||
// [[draw_distance.clamp]]: main-EXE ordering-table range guards. While
|
||||
// g_psx_draw_distance_clamp is set the site's register is clamped to its
|
||||
// max before the instruction runs; overlay code keeps its own code.
|
||||
std::vector<PSXRecompV4::DrawDistanceClampSite> draw_distance_clamp_sites;
|
||||
|
||||
// Exact model-participation cosine compares that gain a camera-horizontal
|
||||
// aspect envelope while preserving the vanilla vertical cone.
|
||||
PSXRecompV4::WidescreenAspectConeConfig ws_aspect_cone;
|
||||
@@ -481,6 +486,9 @@ private:
|
||||
|
||||
// Instruction translation
|
||||
std::string translate_instruction(uint32_t addr, uint32_t instr);
|
||||
std::string translate_instruction_core(uint32_t addr, uint32_t instr);
|
||||
// The [[draw_distance.clamp]] statement for (addr, instr), or "".
|
||||
std::string draw_distance_clamp_prefix(uint32_t addr, uint32_t instr) const;
|
||||
|
||||
// Register name mapping
|
||||
static std::string reg_name(int reg_num);
|
||||
|
||||
@@ -870,7 +870,52 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
|
||||
return keep_branch_if_wide("0 /* unknown branch condition: defaults to not-taken */");
|
||||
}
|
||||
|
||||
// [[draw_distance.clamp]]: a main-EXE ordering-table range guard. While a
|
||||
// mod has the clamps on, the site's register is clamped to `max` before the
|
||||
// instruction runs, so the guard keeps a far primitive in the last OT slot
|
||||
// instead of dropping it. Identity while off. Captured overlays may hold
|
||||
// unrelated code at a listed address and keep their own code.
|
||||
std::string CodeGenerator::draw_distance_clamp_prefix(uint32_t addr,
|
||||
uint32_t instr) const {
|
||||
if (config_.overlay_mode || config_.draw_distance_clamp_sites.empty())
|
||||
return {};
|
||||
for (const auto& site : config_.draw_distance_clamp_sites) {
|
||||
if ((site.address & 0x1FFFFFFFu) != (addr & 0x1FFFFFFFu)) continue;
|
||||
if (instr != site.expected) {
|
||||
fmt::print(stderr,
|
||||
"ERROR: [[draw_distance.clamp]] expected 0x{:08X} at "
|
||||
"0x{:08X}, found 0x{:08X}\n",
|
||||
site.expected, addr, instr);
|
||||
std::exit(1);
|
||||
}
|
||||
// A load into the register in the previous slot is still in flight
|
||||
// (MIPS-I load delay): the clamp would act on the stale value.
|
||||
if (const auto prev = exe_.read_word(addr - 4u)) {
|
||||
const uint32_t pop = *prev >> 26;
|
||||
if (pop >= 0x20u && pop <= 0x26u && get_rt(*prev) == site.reg) {
|
||||
fmt::print(stderr,
|
||||
"ERROR: [[draw_distance.clamp]] site 0x{:08X} is "
|
||||
"the load-delay slot of a load into reg {}\n",
|
||||
addr, site.reg);
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
return fmt::format(
|
||||
"if (g_psx_draw_distance_clamp && (int32_t){0} > {1}) {0} = "
|
||||
"(uint32_t)({1}); /* draw-distance clamp: keep far geometry */",
|
||||
reg_name((int)site.reg), site.max);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) {
|
||||
std::string clamp = draw_distance_clamp_prefix(addr, instr);
|
||||
if (clamp.empty()) return translate_instruction_core(addr, instr);
|
||||
// The core translation carries its own indent; give the clamp the same.
|
||||
return config_.indent + clamp + "\n" + translate_instruction_core(addr, instr);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t instr) {
|
||||
uint32_t opcode = (instr >> 26) & 0x3F;
|
||||
uint32_t funct = instr & 0x3F;
|
||||
|
||||
@@ -3471,6 +3516,8 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
|
||||
ss << "extern int psx_game_option_store(uint32_t addr, int val); /* persisted OPTION restore-at-init (game_options.c) */\n";
|
||||
ss << "extern uint32_t psx_ws_backdrop_value(uint32_t orig, int is_end, int window_cols); /* ws backdrop preload (gpu.c) */\n";
|
||||
ss << "extern void gte_ws_set_suppress(int on); /* widescreen far-backdrop un-squash (gte.cpp) */\n";
|
||||
if (!config_.overlay_mode && !config_.draw_distance_clamp_sites.empty())
|
||||
ss << "extern uint32_t g_psx_draw_distance_clamp; /* [[draw_distance.clamp]] switch (draw_distance.c) */\n";
|
||||
ss << "extern uint32_t g_debug_last_store_pc; /* exact PC of the executing SW/SH/SB — wtrace/readtrace producer attribution (debug_server.c) */\n\n";
|
||||
}
|
||||
|
||||
|
||||
@@ -887,6 +887,70 @@ fs::path find_project_root(const fs::path& config_path) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
bool draw_distance_clamp_reads(uint32_t instr, uint32_t reg) {
|
||||
if (reg == 0u || reg > 31u) return false;
|
||||
const uint32_t op = instr >> 26;
|
||||
const uint32_t rs = (instr >> 21) & 31u;
|
||||
const uint32_t rt = (instr >> 16) & 31u;
|
||||
// ADDI, ADDIU, SLTI, SLTIU, ANDI, ORI, XORI read rs (LUI reads nothing).
|
||||
if (op >= 0x08u && op <= 0x0Eu) return rs == reg;
|
||||
if (op != 0x00u) return false;
|
||||
const uint32_t funct = instr & 0x3Fu;
|
||||
// SLL, SRL, SRA: rt. SLLV, SRLV, SRAV: rt and rs.
|
||||
if (funct == 0x00u || funct == 0x02u || funct == 0x03u) return rt == reg;
|
||||
if (funct == 0x04u || funct == 0x06u || funct == 0x07u)
|
||||
return rs == reg || rt == reg;
|
||||
// ADD, ADDU, SUB, SUBU, AND, OR, XOR, NOR, SLT, SLTU.
|
||||
if (funct >= 0x20u && funct <= 0x2Bu && funct != 0x28u && funct != 0x29u)
|
||||
return rs == reg || rt == reg;
|
||||
return false;
|
||||
}
|
||||
|
||||
// [[draw_distance.clamp]] (docs/config_schema.md "Draw-distance clamps").
|
||||
static std::vector<DrawDistanceClampSite> parse_draw_distance_clamps(
|
||||
const toml::value& cfg, const fs::path& config_path) {
|
||||
std::vector<DrawDistanceClampSite> sites;
|
||||
if (!cfg.contains("draw_distance")) return sites;
|
||||
const toml::value& dd = toml::find(cfg, "draw_distance");
|
||||
if (!dd.contains("clamp")) return sites;
|
||||
const auto& items = toml::find<toml::array>(dd, "clamp");
|
||||
std::set<uint32_t> seen;
|
||||
for (const auto& item : items) {
|
||||
DrawDistanceClampSite site;
|
||||
site.address = parse_hex(toml::find<std::string>(item, "address"),
|
||||
"draw_distance.clamp.address");
|
||||
site.expected = parse_hex(toml::find<std::string>(item, "expected"),
|
||||
"draw_distance.clamp.expected");
|
||||
const int64_t reg = toml::find<int64_t>(item, "reg");
|
||||
const int64_t max = toml::find<int64_t>(item, "max");
|
||||
if ((site.address & 3u) != 0u)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: draw-distance clamp address 0x{:08X} is not "
|
||||
"instruction-aligned", config_path.string(), site.address));
|
||||
if (!seen.insert(site.address & 0x1FFFFFFFu).second)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: duplicate draw-distance clamp address 0x{:08X}",
|
||||
config_path.string(), site.address));
|
||||
if (reg < 1 || reg > 31)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: draw-distance clamp at 0x{:08X}: reg must be 1..31",
|
||||
config_path.string(), site.address));
|
||||
if (max < INT32_MIN || max > INT32_MAX)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: draw-distance clamp at 0x{:08X}: max is not a signed "
|
||||
"32-bit value", config_path.string(), site.address));
|
||||
site.reg = (uint32_t)reg;
|
||||
site.max = (int32_t)max;
|
||||
if (!draw_distance_clamp_reads(site.expected, site.reg))
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: draw-distance clamp at 0x{:08X}: expected 0x{:08X} must "
|
||||
"be an ALU instruction that reads reg {}",
|
||||
config_path.string(), site.address, site.expected, site.reg));
|
||||
sites.push_back(site);
|
||||
}
|
||||
return sites;
|
||||
}
|
||||
|
||||
// Derive the output filename stem from a rom basename. Mirrors the Python
|
||||
// audit_config.py logic: strip a trailing .BIN/.EXE (case-insensitive) but
|
||||
// preserve dotted names like "SCUS_942.36" unchanged.
|
||||
@@ -2306,6 +2370,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
loaded.ws_cull_clip_edge_x_load_sites =
|
||||
std::move(ws_cull_clip_edge_x_load_sites);
|
||||
loaded.ws_cull_clip_edge_width = ws_cull_clip_edge_width;
|
||||
loaded.draw_distance_clamp_sites =
|
||||
parse_draw_distance_clamps(cfg, config_path);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +108,23 @@ struct WidescreenAngleSite {
|
||||
uint32_t expected = 0; // guarded ADDI/ADDIU with rs == zero
|
||||
};
|
||||
|
||||
// [[draw_distance.clamp]]: an ordering-table range guard that a trusted mod
|
||||
// may turn from "drop the far primitive" into "keep it in the last slot".
|
||||
// While the runtime switch is on (psx_mod_set_draw_distance_clamp), `reg` is
|
||||
// clamped to `max` (signed) immediately before the instruction at `address`
|
||||
// runs. Main EXE only; the complete instruction word is the guard.
|
||||
struct DrawDistanceClampSite {
|
||||
uint32_t address = 0;
|
||||
uint32_t expected = 0; // the instruction; `reg` must be one of its sources
|
||||
uint32_t reg = 0; // 1..31
|
||||
int32_t max = 0;
|
||||
};
|
||||
|
||||
// True when `instr` is an ALU instruction (I-type or SPECIAL R-type, not a
|
||||
// load, store, branch or jump) that reads GPR `reg`. These are the only
|
||||
// instructions a draw-distance clamp may precede.
|
||||
bool draw_distance_clamp_reads(uint32_t instr, uint32_t reg);
|
||||
|
||||
// Aspect-aware horizontal participation cone. The exact compare sites are
|
||||
// full-word guarded because overlay variants can reuse a virtual address for
|
||||
// unrelated code. Registers are MIPS GPR indices captured at each comparison.
|
||||
@@ -1217,6 +1234,9 @@ struct GameConfig {
|
||||
// [widescreen.cull] clip_edge_width -- the screen width a right-edge clip
|
||||
// bound equals. 0 = the first screen_w_imms entry (0x140 by default).
|
||||
uint32_t ws_cull_clip_edge_width = 0;
|
||||
// [[draw_distance.clamp]] -- opt-in "keep far geometry" clamps. Empty by
|
||||
// default; inert until a mod switches them on; regen required.
|
||||
std::vector<DrawDistanceClampSite> draw_distance_clamp_sites;
|
||||
};
|
||||
|
||||
// Effective clip_edge_width: explicit value, else screen_w_imms[0], else 320.
|
||||
|
||||
@@ -242,6 +242,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
uint32_t ws_cull_clip_edge_width = 0; // [widescreen.cull] clip_edge_width
|
||||
std::vector<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep;
|
||||
std::vector<PSXRecompV4::WidescreenAngleSite> ws_cull_angle;
|
||||
std::vector<PSXRecompV4::DrawDistanceClampSite> draw_distance_clamps; // [[draw_distance.clamp]]
|
||||
PSXRecompV4::WidescreenAspectConeConfig ws_aspect_cone;
|
||||
int ws_cull_activation_guard_pixels = 0;
|
||||
std::vector<uint32_t> ws_cull_w_imms = { 0x140, 0x141 }; // [widescreen.cull] screen_w_imms
|
||||
@@ -316,6 +317,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
ws_cull_clip_edge_width = PSXRecompV4::ws_cull_clip_edge_width(cfg);
|
||||
ws_cull_keep = cfg.ws_cull_keep_sites;
|
||||
ws_cull_angle = cfg.ws_cull_angle_sites;
|
||||
draw_distance_clamps = cfg.draw_distance_clamp_sites;
|
||||
ws_aspect_cone = cfg.ws_aspect_cone;
|
||||
ws_cull_activation_guard_pixels =
|
||||
cfg.ws_cull_activation_guard_pixels;
|
||||
@@ -635,6 +637,8 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
check_set("widescreen.backdrop.x_sites", ws_backdrop_x);
|
||||
check_set("widescreen.backdrop.unsquash_funcs", ws_backdrop_unsquash);
|
||||
check_set("persist_options.init_store_pc", persist_init_sites);
|
||||
for (const auto& site : draw_distance_clamps)
|
||||
check("draw_distance.clamp.address", site.address);
|
||||
check("widescreen.bg2d.count_site", ws_bg2d_count_site);
|
||||
check("widescreen.bg2d.startcol_site", ws_bg2d_startcol_site);
|
||||
check("widescreen.bg2d.startx_site", ws_bg2d_startx_site);
|
||||
@@ -1486,6 +1490,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
codegen_config.ws_cull_clip_edge_width = ws_cull_w_imms.front();
|
||||
codegen_config.ws_cull_keep_sites = ws_cull_keep;
|
||||
codegen_config.ws_cull_angle_sites = ws_cull_angle;
|
||||
codegen_config.draw_distance_clamp_sites = draw_distance_clamps;
|
||||
codegen_config.ws_aspect_cone = ws_aspect_cone;
|
||||
codegen_config.ws_cull_w_imms = ws_cull_w_imms;
|
||||
codegen_config.ws_cull_h_imms = ws_cull_h_imms;
|
||||
|
||||
@@ -190,7 +190,8 @@ CodeGenConfig move_exact_config_sites(const CodeGenConfig& cfg, At at) {
|
||||
v.ws_bg2d_bufbase_site = at(v.ws_bg2d_bufbase_site);
|
||||
v.ws_bg2d_cap_site = at(v.ws_bg2d_cap_site);
|
||||
// Physically matched, kept as written: ws_signed_x_bound_sites,
|
||||
// ws_cull_keep_sites, ws_cull_angle_sites, ws_aspect_cone.sites.
|
||||
// ws_cull_keep_sites, ws_cull_angle_sites, ws_aspect_cone.sites,
|
||||
// draw_distance_clamp_sites.
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
// [[draw_distance.clamp]]: config parsing and validation, the emitted
|
||||
// main-EXE clamp (and its absence everywhere else), the main-EXE guards, the
|
||||
// shared declaration, and the overlay-cache identity (unchanged: overlays
|
||||
// keep their own code at a listed address).
|
||||
#include "code_generator.h"
|
||||
#include "config_loader.h"
|
||||
#include "control_flow.h"
|
||||
#include "../../runtime/include/draw_distance.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <cstdlib>
|
||||
#include <process.h>
|
||||
#else
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
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 = "Draw Distance 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";
|
||||
}
|
||||
|
||||
std::string clamp(const char* address, const char* expected, int reg,
|
||||
const char* max) {
|
||||
return std::string("\n[[draw_distance.clamp]]\naddress = \"") + address +
|
||||
"\"\nexpected = \"" + expected + "\"\nreg = " + std::to_string(reg) +
|
||||
"\nmax = " + max + "\n";
|
||||
}
|
||||
|
||||
PSXRecompV4::GameConfig load(const std::string& body) {
|
||||
fs::path path = write_temp_config("draw-distance", body);
|
||||
auto config = PSXRecompV4::load_game_config(path);
|
||||
fs::remove(path);
|
||||
return config;
|
||||
}
|
||||
|
||||
bool load_throws_with(const std::string& body, const char* needle) {
|
||||
fs::path path = write_temp_config("draw-distance-bad", 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;
|
||||
if (!matched) std::fprintf(stderr, " (threw: %s)\n", e.what());
|
||||
}
|
||||
fs::remove(path);
|
||||
return matched;
|
||||
}
|
||||
|
||||
// A function made of `words` followed by `jr ra ; nop`.
|
||||
PSXRecomp::GeneratedFunction generate(std::initializer_list<uint32_t> words,
|
||||
const PSXRecomp::CodeGenConfig& config) {
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = kBase;
|
||||
exe.header.initial_pc = kBase;
|
||||
for (uint32_t w : words) append_word(exe.code_data, w);
|
||||
append_word(exe.code_data, 0x03E00008u);
|
||||
append_word(exe.code_data, 0x00000000u);
|
||||
const uint32_t size = static_cast<uint32_t>(exe.code_data.size());
|
||||
exe.header.file_size = size;
|
||||
|
||||
PSXRecomp::Function function{};
|
||||
function.start_addr = kBase;
|
||||
function.end_addr = kBase + size;
|
||||
function.size = size;
|
||||
function.name = "clamp_site_test";
|
||||
|
||||
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
|
||||
const auto cfg = analyzer.analyze_function(function);
|
||||
PSXRecomp::CodeGenerator generator(exe, config);
|
||||
return generator.generate_function(function, cfg);
|
||||
}
|
||||
|
||||
// Runs `body` in a child and reports whether it exited with a failure status
|
||||
// (the main-EXE guards call std::exit(1)). Windows has no fork: the Nth call
|
||||
// re-runs this binary as `--exit-probe N`, which runs only the Nth body.
|
||||
const char* g_self = nullptr;
|
||||
int g_exit_probe = -1;
|
||||
int g_probe_seq = 0;
|
||||
|
||||
template <class F>
|
||||
bool exits_with_failure(F&& body) {
|
||||
#if defined(_WIN32)
|
||||
const int seq = g_probe_seq++;
|
||||
if (g_exit_probe >= 0) {
|
||||
if (seq != g_exit_probe) return false;
|
||||
body();
|
||||
std::fflush(nullptr);
|
||||
std::_Exit(0);
|
||||
}
|
||||
std::fflush(nullptr);
|
||||
const std::string quoted = std::string("\"") + g_self + "\"";
|
||||
const std::string n = std::to_string(seq);
|
||||
const intptr_t rc = _spawnl(_P_WAIT, g_self, quoted.c_str(), "--exit-probe",
|
||||
n.c_str(), nullptr);
|
||||
return rc > 0;
|
||||
#else
|
||||
(void)g_probe_seq;
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
if (!std::freopen("/dev/null", "w", stderr)) _exit(3);
|
||||
body();
|
||||
_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (pid < 0 || waitpid(pid, &status, 0) != pid) return false;
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr uint32_t kSltiuT2V0 = 0x2C4A01C0u; // sltiu t2, v0, 0x1C0
|
||||
constexpr uint32_t kAddiuAtV0 = 0x2441FFFFu; // addiu at, v0, -1
|
||||
constexpr uint32_t kSravV0 = 0x00561007u; // srav v0, s6, v0
|
||||
constexpr uint32_t kLwV0 = 0x8C820060u; // lw v0, 0x60(a0)
|
||||
constexpr uint32_t kLwV1 = 0x8C830060u; // lw v1, 0x60(a0)
|
||||
constexpr uint32_t kBeqZero = 0x10000002u; // b +2 (beq zero, zero)
|
||||
constexpr uint32_t kNop = 0x00000000u;
|
||||
|
||||
const char* kClampV0 =
|
||||
"if (g_psx_draw_distance_clamp && (int32_t)cpu->gpr[2] > 447) "
|
||||
"cpu->gpr[2] = (uint32_t)(447);";
|
||||
|
||||
PSXRecomp::CodeGenConfig with_site(uint32_t address, uint32_t expected,
|
||||
uint32_t reg, int32_t max) {
|
||||
PSXRecomp::CodeGenConfig config{};
|
||||
PSXRecompV4::DrawDistanceClampSite site;
|
||||
site.address = address;
|
||||
site.expected = expected;
|
||||
site.reg = reg;
|
||||
site.max = max;
|
||||
config.draw_distance_clamp_sites.push_back(site);
|
||||
return config;
|
||||
}
|
||||
|
||||
void reads_classifier() {
|
||||
using PSXRecompV4::draw_distance_clamp_reads;
|
||||
check(draw_distance_clamp_reads(kSltiuT2V0, 2), "sltiu reads rs");
|
||||
check(!draw_distance_clamp_reads(kSltiuT2V0, 10), "sltiu does not read rt");
|
||||
check(draw_distance_clamp_reads(kAddiuAtV0, 2), "addiu reads rs");
|
||||
check(draw_distance_clamp_reads(kSravV0, 2) &&
|
||||
draw_distance_clamp_reads(kSravV0, 22),
|
||||
"srav reads rs and rt");
|
||||
check(draw_distance_clamp_reads(0x00021080u, 2), "sll reads rt"); // sll v0,v0,2
|
||||
check(draw_distance_clamp_reads(0x0062102Bu, 3), "sltu reads rs"); // sltu v0,v1,v0
|
||||
check(!draw_distance_clamp_reads(0x3C02800Bu, 2), "lui reads nothing");
|
||||
check(!draw_distance_clamp_reads(kLwV0, 4), "a load is not a clamp site");
|
||||
check(!draw_distance_clamp_reads(0x10400002u, 2), "a branch is not a clamp site");
|
||||
check(!draw_distance_clamp_reads(0x00430018u, 2), "mult is not a clamp site");
|
||||
check(!draw_distance_clamp_reads(0x03E00008u, 31), "jr is not a clamp site");
|
||||
check(!draw_distance_clamp_reads(kSltiuT2V0, 0), "reg 0 is refused");
|
||||
}
|
||||
|
||||
void loader() {
|
||||
auto none = load(base_config());
|
||||
check(none.draw_distance_clamp_sites.empty(), "no clamps by default");
|
||||
|
||||
auto two = load(base_config() +
|
||||
clamp("0x80061230", "0x2C4A01C0", 2, "0x1BF") +
|
||||
clamp("0x80066FF8", "0x2441FFFF", 2, "447"));
|
||||
check(two.draw_distance_clamp_sites.size() == 2, "two clamps parsed");
|
||||
if (two.draw_distance_clamp_sites.size() == 2) {
|
||||
const auto& a = two.draw_distance_clamp_sites[0];
|
||||
const auto& b = two.draw_distance_clamp_sites[1];
|
||||
check(a.address == 0x80061230u && a.expected == kSltiuT2V0 &&
|
||||
a.reg == 2u && a.max == 0x1BF,
|
||||
"first clamp fields");
|
||||
check(b.address == 0x80066FF8u && b.expected == kAddiuAtV0 &&
|
||||
b.max == 447,
|
||||
"second clamp fields (decimal max)");
|
||||
}
|
||||
auto neg = load(base_config() + clamp("0x80061230", "0x2C4A01C0", 2, "-5"));
|
||||
check(neg.draw_distance_clamp_sites.size() == 1 &&
|
||||
neg.draw_distance_clamp_sites[0].max == -5,
|
||||
"a negative max is accepted");
|
||||
|
||||
check(load_throws_with(base_config() + clamp("0x80061232", "0x2C4A01C0", 2, "1"),
|
||||
"not instruction-aligned"),
|
||||
"unaligned address refused");
|
||||
check(load_throws_with(base_config() +
|
||||
clamp("0x80061230", "0x2C4A01C0", 2, "1") +
|
||||
clamp("0xA0061230", "0x2C4A01C0", 2, "1"),
|
||||
"duplicate draw-distance clamp"),
|
||||
"duplicate physical address refused");
|
||||
check(load_throws_with(base_config() + clamp("0x80061230", "0x2C4A01C0", 0, "1"),
|
||||
"reg must be 1..31"),
|
||||
"reg 0 refused");
|
||||
check(load_throws_with(base_config() + clamp("0x80061230", "0x2C4A01C0", 32, "1"),
|
||||
"reg must be 1..31"),
|
||||
"reg 32 refused");
|
||||
check(load_throws_with(base_config() + clamp("0x80061230", "0x2C4A01C0", 3, "1"),
|
||||
"must be an ALU instruction that reads reg 3"),
|
||||
"a reg the instruction does not read is refused");
|
||||
check(load_throws_with(base_config() + clamp("0x80061230", "0x8C820060", 4, "1"),
|
||||
"must be an ALU instruction"),
|
||||
"a load site is refused");
|
||||
check(load_throws_with(base_config() +
|
||||
clamp("0x80061230", "0x2C4A01C0", 2, "0x100000000"),
|
||||
"not a signed 32-bit value"),
|
||||
"max outside int32 refused");
|
||||
}
|
||||
|
||||
void emission() {
|
||||
// The clamp runs immediately before the guard it protects.
|
||||
const auto cfg = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
|
||||
const auto out = generate({kSravV0, kSltiuT2V0, kNop}, cfg).full_code;
|
||||
const size_t at = out.find(kClampV0);
|
||||
const size_t guard = out.find("0x80010004:");
|
||||
check(at != std::string::npos, "main-EXE site emits the clamp");
|
||||
check(at != std::string::npos && guard != std::string::npos && at < guard,
|
||||
"the clamp precedes the guard instruction");
|
||||
check(at != std::string::npos &&
|
||||
out.find(kClampV0, at + 1) == std::string::npos,
|
||||
"exactly one clamp emitted");
|
||||
check(at != std::string::npos && at >= 5 &&
|
||||
out.compare(at - 5, 5, "\n ") == 0 &&
|
||||
out.find("\n cpu->gpr[10] =", at) != std::string::npos,
|
||||
"the clamp and the guard share the statement indent");
|
||||
|
||||
// The addiu form clamps the source the slot index is computed from.
|
||||
const auto addiu = generate({kSravV0, kAddiuAtV0, kNop},
|
||||
with_site(kBase + 4u, kAddiuAtV0, 2, 447)).full_code;
|
||||
check(addiu.find(kClampV0) != std::string::npos,
|
||||
"addiu site emits the clamp on its source");
|
||||
|
||||
// A site in a branch delay slot is clamped inside the slot.
|
||||
const auto ds = generate({kBeqZero, kSltiuT2V0, kNop, kNop},
|
||||
with_site(kBase + 4u, kSltiuT2V0, 2, 447)).full_code;
|
||||
check(ds.find(kClampV0) != std::string::npos, "delay-slot site is clamped");
|
||||
|
||||
// No sites: nothing about the clamp anywhere.
|
||||
PSXRecomp::CodeGenConfig plain{};
|
||||
const auto vanilla = generate({kSravV0, kSltiuT2V0, kNop}, plain).full_code;
|
||||
check(vanilla.find("draw_distance") == std::string::npos &&
|
||||
vanilla.find("draw-distance") == std::string::npos,
|
||||
"no sites: vanilla code");
|
||||
|
||||
// A site elsewhere leaves this instruction alone.
|
||||
const auto other = generate({kSravV0, kSltiuT2V0, kNop},
|
||||
with_site(kBase + 0x100u, kSltiuT2V0, 2, 447)).full_code;
|
||||
check(other.find("draw-distance") == std::string::npos,
|
||||
"unlisted address: vanilla code");
|
||||
|
||||
// Captured overlays keep their own code at a listed address, matching
|
||||
// word or not.
|
||||
auto overlay = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
|
||||
overlay.overlay_mode = true;
|
||||
check(generate({kSravV0, kSltiuT2V0, kNop}, overlay).full_code.find(
|
||||
"draw-distance") == std::string::npos,
|
||||
"overlay variant with the listed word stays vanilla");
|
||||
check(!exits_with_failure([&] { generate({kSravV0, kAddiuAtV0, kNop}, overlay); }),
|
||||
"overlay variant with another word generates");
|
||||
}
|
||||
|
||||
void guards() {
|
||||
const auto cfg = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
|
||||
check(!exits_with_failure([&] { generate({kSravV0, kSltiuT2V0, kNop}, cfg); }),
|
||||
"matching word generates");
|
||||
check(exits_with_failure([&] { generate({kSravV0, kAddiuAtV0, kNop}, cfg); }),
|
||||
"a main-EXE word mismatch fails generation");
|
||||
check(exits_with_failure([&] { generate({kLwV0, kSltiuT2V0, kNop}, cfg); }),
|
||||
"a site in the load-delay slot of its register fails generation");
|
||||
check(!exits_with_failure([&] { generate({kLwV1, kSltiuT2V0, kNop}, cfg); }),
|
||||
"a load into another register before the site is fine");
|
||||
}
|
||||
|
||||
void shared_decls() {
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = kBase;
|
||||
std::vector<PSXRecomp::GeneratedFunction> functions;
|
||||
{
|
||||
PSXRecomp::CodeGenerator plain(exe);
|
||||
check(plain.build_shared_decls_header(functions).find(
|
||||
"g_psx_draw_distance_clamp") == std::string::npos,
|
||||
"no sites: the declarations do not change");
|
||||
}
|
||||
{
|
||||
const auto cfg = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
|
||||
PSXRecomp::CodeGenerator gen(exe, cfg);
|
||||
check(gen.build_shared_decls_header(functions).find(
|
||||
"extern uint32_t g_psx_draw_distance_clamp;") != std::string::npos,
|
||||
"sites: the switch is declared");
|
||||
}
|
||||
}
|
||||
|
||||
void overlay_identity_unchanged() {
|
||||
auto none = load(base_config());
|
||||
auto some = load(base_config() + clamp("0x80061230", "0x2C4A01C0", 2, "0x1BF"));
|
||||
check(PSXRecompV4::overlay_codegen_config_hash(none) ==
|
||||
PSXRecompV4::overlay_codegen_config_hash(some),
|
||||
"clamps do not enter the overlay-cache identity");
|
||||
}
|
||||
|
||||
void runtime_value() {
|
||||
check(psx_draw_distance_clamp_value(0x1C0u, 0x1BF) == 0x1BFu, "clamps past max");
|
||||
check(psx_draw_distance_clamp_value(0x7FFu, 0x1BF) == 0x1BFu, "clamps far past max");
|
||||
check(psx_draw_distance_clamp_value(0x1BFu, 0x1BF) == 0x1BFu, "max is kept");
|
||||
check(psx_draw_distance_clamp_value(5u, 0x1BF) == 5u, "near values unchanged");
|
||||
check(psx_draw_distance_clamp_value(0u, 0x1BF) == 0u, "zero unchanged");
|
||||
check(psx_draw_distance_clamp_value(0xFFFFFFFFu, 0x1BE) == 0xFFFFFFFFu,
|
||||
"a negative value is left for the original predicate");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_self = argv[0];
|
||||
if (argc >= 3 && std::string(argv[1]) == "--exit-probe")
|
||||
g_exit_probe = std::atoi(argv[2]);
|
||||
reads_classifier();
|
||||
loader();
|
||||
emission();
|
||||
guards();
|
||||
shared_decls();
|
||||
overlay_identity_unchanged();
|
||||
runtime_value();
|
||||
if (failures) {
|
||||
std::fprintf(stderr, "draw_distance_codegen_test: %d failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
std::printf("draw_distance_codegen_test: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/* draw_distance.h - opt-in draw-distance clamps ([[draw_distance.clamp]]).
|
||||
*
|
||||
* Many PS1 renderers drop a primitive whose depth falls past the end of their
|
||||
* ordering table: `sltiu t, z, N; beqz t, reject`. The limit protects the OT,
|
||||
* so simply raising it can write past the table. A title lists such
|
||||
* predicates in game.toml together with the register that carries the depth
|
||||
* index and the last safe value:
|
||||
*
|
||||
* [[draw_distance.clamp]]
|
||||
* address = "0x80061230"
|
||||
* expected = "0x2C4A01C0" # sltiu t2, v0, 0x1C0
|
||||
* reg = 2 # v0
|
||||
* max = 0x1BF
|
||||
*
|
||||
* While a trusted mod has switched the clamps on, the listed register is
|
||||
* clamped to `max` (signed) immediately before the listed instruction runs,
|
||||
* so a far primitive is kept in the farthest OT slot instead of being
|
||||
* dropped. Off (the default, reset at every session start) the sites run the
|
||||
* original code. Main executable only: generated main-EXE code applies the
|
||||
* clamp, captured overlay code at the same address keeps its own code, and
|
||||
* the dirty-RAM interpreter applies it where the game's text image still
|
||||
* holds the listed instruction.
|
||||
*
|
||||
* docs/config_schema.md "Draw-distance clamps" has the config rules.
|
||||
*/
|
||||
#ifndef PSX_DRAW_DISTANCE_H
|
||||
#define PSX_DRAW_DISTANCE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct PSXDrawDistanceClampSite {
|
||||
uint32_t address; /* any segment; stored physical */
|
||||
uint32_t expected; /* the complete instruction word at address */
|
||||
uint32_t reg; /* 1..31: a source register of `expected` */
|
||||
int32_t max; /* signed upper bound */
|
||||
} PSXDrawDistanceClampSite;
|
||||
|
||||
#define PSX_DRAW_DISTANCE_CLAMP_SITES_MAX 256
|
||||
|
||||
/* Nonzero while the clamps are on. Generated main-EXE code reads it at every
|
||||
* configured site. */
|
||||
extern uint32_t g_psx_draw_distance_clamp;
|
||||
/* On and at least one site configured: the interpreter's per-instruction
|
||||
* gate. */
|
||||
extern int g_psx_draw_distance_clamp_live;
|
||||
|
||||
/* The title's sites (game config). Copied, sorted, capped at
|
||||
* PSX_DRAW_DISTANCE_CLAMP_SITES_MAX (a longer list is logged). Returns the
|
||||
* number kept. */
|
||||
int psx_draw_distance_set_clamp_sites(const PSXDrawDistanceClampSite *sites,
|
||||
int count);
|
||||
int psx_draw_distance_clamp_site_count(void);
|
||||
/* The site at `pc` whose instruction is `insn`, or NULL. */
|
||||
const PSXDrawDistanceClampSite *psx_draw_distance_clamp_find(uint32_t pc,
|
||||
uint32_t insn);
|
||||
|
||||
/* The value the clamp leaves in the register. */
|
||||
static inline uint32_t psx_draw_distance_clamp_value(uint32_t value,
|
||||
int32_t max) {
|
||||
return (int32_t)value > max ? (uint32_t)max : value;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PSX_DRAW_DISTANCE_H */
|
||||
@@ -394,6 +394,17 @@ int psx_mod_set_load_acceleration(uint32_t wall_clock_multiplier,
|
||||
int psx_mod_set_disc_speed(uint32_t divisor,
|
||||
uint32_t instant_max_per_frame);
|
||||
|
||||
/*
|
||||
* Turn the title's [[draw_distance.clamp]] sites on or off for this session
|
||||
* (draw_distance.h): a far primitive the game would drop past the end of its
|
||||
* ordering table is kept in the farthest slot instead. Returns 1 when the
|
||||
* title configured sites, 0 when it has none (the call then changes nothing
|
||||
* the guest sees). Off by default and reset to off at every session start;
|
||||
* it adds guest work, so call it from activation, never in netplay.
|
||||
*/
|
||||
int psx_mod_set_draw_distance_clamp(int enabled);
|
||||
int psx_mod_draw_distance_clamp_enabled(void);
|
||||
|
||||
/* Controller presentation values exposed to trusted game-owned plugins. */
|
||||
enum {
|
||||
PSX_MOD_CONTROLLER_ANALOG = 1,
|
||||
|
||||
@@ -360,6 +360,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/debug_server.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/debug_trace_ranges.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/dirty_ram_interp.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/draw_distance.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/game_dispatch_compat.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_segment_miss.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/fntrace.c
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "starvation_ring.h"
|
||||
#include "fntrace.h" /* fntrace_is_game_started / fntrace_mark_game_started */
|
||||
#include "psx_segment_miss.h" /* segment misses in static game code (§5.5) */
|
||||
#include "draw_distance.h" /* [[draw_distance.clamp]] sites */
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
@@ -1514,6 +1515,22 @@ static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
(ld_rt != 0u);
|
||||
const uint32_t ld_before = is_ld ? cpu->gpr[ld_rt] : 0u;
|
||||
|
||||
/* [[draw_distance.clamp]] (draw_distance.h): while a mod has the clamps
|
||||
* on, a listed main-EXE site clamps its register before it runs, exactly
|
||||
* as the generated code does. Captured overlay code keeps its own code,
|
||||
* so only the game's text image qualifies. One load when off. */
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
if (g_psx_draw_distance_clamp_live) {
|
||||
const PSXDrawDistanceClampSite *dd =
|
||||
psx_draw_distance_clamp_find(pc, insn);
|
||||
if (dd && psx_game_address_in_text(pc)) {
|
||||
cpu->gpr[dd->reg] =
|
||||
psx_draw_distance_clamp_value(cpu->gpr[dd->reg], dd->max);
|
||||
cpu->gpr[0] = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const int rv = exec_one_fetched_inner(cpu, pc, insn, next_pc_out);
|
||||
|
||||
if (is_ld) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/* draw_distance.c - the [[draw_distance.clamp]] site store and switch
|
||||
* (draw_distance.h). */
|
||||
#include "draw_distance.h"
|
||||
#include "mod_plugins.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
uint32_t g_psx_draw_distance_clamp = 0;
|
||||
int g_psx_draw_distance_clamp_live = 0;
|
||||
|
||||
static PSXDrawDistanceClampSite s_sites[PSX_DRAW_DISTANCE_CLAMP_SITES_MAX];
|
||||
static int s_site_count = 0;
|
||||
|
||||
static void update_live(void) {
|
||||
g_psx_draw_distance_clamp_live =
|
||||
(g_psx_draw_distance_clamp != 0u && s_site_count > 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
static int site_cmp(const void *a, const void *b) {
|
||||
const uint32_t x = ((const PSXDrawDistanceClampSite *)a)->address;
|
||||
const uint32_t y = ((const PSXDrawDistanceClampSite *)b)->address;
|
||||
return (x > y) - (x < y);
|
||||
}
|
||||
|
||||
int psx_draw_distance_set_clamp_sites(const PSXDrawDistanceClampSite *sites,
|
||||
int count) {
|
||||
if (!sites || count < 0) count = 0;
|
||||
if (count > PSX_DRAW_DISTANCE_CLAMP_SITES_MAX) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: [[draw_distance.clamp]] lists %d sites; keeping "
|
||||
"the first %d\n",
|
||||
count, PSX_DRAW_DISTANCE_CLAMP_SITES_MAX);
|
||||
count = PSX_DRAW_DISTANCE_CLAMP_SITES_MAX;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
s_sites[i] = sites[i];
|
||||
s_sites[i].address &= 0x1FFFFFFFu;
|
||||
}
|
||||
if (count > 1) qsort(s_sites, (size_t)count, sizeof s_sites[0], site_cmp);
|
||||
s_site_count = count;
|
||||
update_live();
|
||||
return count;
|
||||
}
|
||||
|
||||
int psx_draw_distance_clamp_site_count(void) { return s_site_count; }
|
||||
|
||||
const PSXDrawDistanceClampSite *psx_draw_distance_clamp_find(uint32_t pc,
|
||||
uint32_t insn) {
|
||||
const uint32_t phys = pc & 0x1FFFFFFFu;
|
||||
int lo = 0, hi = s_site_count;
|
||||
while (lo < hi) {
|
||||
const int mid = lo + (hi - lo) / 2;
|
||||
if (s_sites[mid].address < phys) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
if (lo < s_site_count && s_sites[lo].address == phys &&
|
||||
s_sites[lo].expected == insn)
|
||||
return &s_sites[lo];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* mod_plugins.h. Process-wide host state: the session start resets it to off
|
||||
* (main.cpp reset_mod_owned_presentation), so only a session whose plan
|
||||
* activates a plugin that asks for it, never netplay, runs with it on. */
|
||||
int psx_mod_set_draw_distance_clamp(int enabled) {
|
||||
g_psx_draw_distance_clamp = enabled ? 1u : 0u;
|
||||
update_live();
|
||||
return s_site_count > 0;
|
||||
}
|
||||
|
||||
int psx_mod_draw_distance_clamp_enabled(void) {
|
||||
return g_psx_draw_distance_clamp != 0u;
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#include "overlay_backend.h"
|
||||
#include "gpu.h"
|
||||
#include "draw_distance.h"
|
||||
#include "display_scanout.h"
|
||||
#include "pgxp.h"
|
||||
#include "interrupts.h"
|
||||
@@ -1546,6 +1547,7 @@ static void reset_mod_owned_presentation(void) {
|
||||
psx_mod_set_world_scene_predicate(nullptr);
|
||||
psx_mod_set_retained_scene_predicate(nullptr);
|
||||
psx_mod_set_adaptive_backdrop_preload(0);
|
||||
(void)psx_mod_set_draw_distance_clamp(0);
|
||||
g_bezel_path.clear();
|
||||
g_frame_interpolation_blend = g_frame_interpolation_blend_default;
|
||||
g_frame_interpolation_source = PSX_MOD_FRAME_SOURCE_VBLANK;
|
||||
@@ -13527,6 +13529,17 @@ int main(int argc, char** argv) {
|
||||
gpu_ws_set_angle_sites(
|
||||
addresses.data(), expected.data(), (int)addresses.size());
|
||||
}
|
||||
{
|
||||
/* [[draw_distance.clamp]]: stored for the interpreter; the
|
||||
* switch stays off until a mod's activation turns it on. */
|
||||
std::vector<PSXDrawDistanceClampSite> sites;
|
||||
sites.reserve(gc.draw_distance_clamp_sites.size());
|
||||
for (const auto& site : gc.draw_distance_clamp_sites)
|
||||
sites.push_back({site.address, site.expected, site.reg,
|
||||
site.max});
|
||||
(void)psx_draw_distance_set_clamp_sites(sites.data(),
|
||||
(int)sites.size());
|
||||
}
|
||||
{
|
||||
std::vector<uint32_t> addresses, expected, thresholds;
|
||||
std::vector<uint32_t> object_regs, x_regs, z_regs, y_regs;
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
/* [[draw_distance.clamp]] in the dirty-RAM interpreter.
|
||||
*
|
||||
* Generated main-EXE code clamps a listed site's register before the guard
|
||||
* runs (draw_distance_codegen_test covers the emit). Main-EXE code that runs
|
||||
* from the interpreter goes through dirty_ram_interp.c instead, which looks
|
||||
* the PC up in draw_distance.c's sorted store. This test runs the production
|
||||
* code of both files:
|
||||
*
|
||||
* - the store: physical addresses, sort, word-guarded lookup, the cap and
|
||||
* its log line, empty and NULL lists;
|
||||
* - the switch: psx_mod_set_draw_distance_clamp's return value and the
|
||||
* interpreter's live gate;
|
||||
* - real interpreted instructions (exec_one_fetched) with the switch off
|
||||
* and on: the sltiu and addiu guard forms, an unlisted twin, a
|
||||
* mismatched word, a site outside the game's text image, and a negative
|
||||
* index that the original guard still rejects.
|
||||
*
|
||||
* The sources are #included (gpu.c for the widescreen site stores the
|
||||
* interpreter also consults) so the test can call the interpreter's static
|
||||
* entry point; unreferenced code is dropped at link time (LTO plus section
|
||||
* GC / dead_strip), so only the stubs below are needed. */
|
||||
#define PSX_HAS_GAME_DISPATCH 1
|
||||
#include "../src/draw_distance.c"
|
||||
#include "../src/gpu.c"
|
||||
#include "../src/dirty_ram_interp.c"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#if defined(_WIN32)
|
||||
#include <io.h>
|
||||
#define dup _dup
|
||||
#define dup2 _dup2
|
||||
#define fileno _fileno
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
/* ---- link stubs for code reachable from exec_one_fetched ------------------ */
|
||||
uint64_t s_frame_count;
|
||||
uint32_t g_debug_current_func_addr;
|
||||
uint32_t g_debug_last_store_pc;
|
||||
uint32_t g_dirty_ram_code_gen;
|
||||
uint64_t g_dispatch_static_hits;
|
||||
int g_rfe_escape_pending;
|
||||
int g_exc_escape_reason;
|
||||
int g_psx_call_bail;
|
||||
uint64_t g_psx_bail_first;
|
||||
uint64_t g_psx_bail_resolved;
|
||||
uint32_t i_stat;
|
||||
uint32_t i_mask;
|
||||
uint64_t psx_cycle_count;
|
||||
uint32_t g_psx_mod_function_entry_hooks;
|
||||
uint32_t g_psx_ram_size = PSX_MAIN_RAM_RETAIL_BYTES;
|
||||
uint32_t g_psx_ram_mask = PSX_MAIN_RAM_RETAIL_BYTES - 1u;
|
||||
|
||||
static uint8_t test_ram[0x00200000u];
|
||||
|
||||
uint8_t *memory_get_ram_ptr(void) { return test_ram; }
|
||||
uint8_t psx_read_byte(uint32_t a) { return test_ram[a & 0x1FFFFFu]; }
|
||||
uint16_t psx_read_half(uint32_t a) {
|
||||
uint16_t v; memcpy(&v, test_ram + (a & 0x1FFFFEu), sizeof v); return v;
|
||||
}
|
||||
uint32_t psx_read_word(uint32_t a) {
|
||||
uint32_t v; memcpy(&v, test_ram + (a & 0x1FFFFCu), sizeof v); return v;
|
||||
}
|
||||
uint8_t psx_cyc_load_byte(CPUState *cpu, uint32_t a, uint32_t rt, uint32_t m) {
|
||||
(void)cpu; (void)rt; (void)m; return psx_read_byte(a);
|
||||
}
|
||||
uint32_t psx_cyc_lwc2_read(CPUState *cpu, uint32_t a) { (void)cpu; return psx_read_word(a); }
|
||||
int dirty_ram_is_dirty(uint32_t phys) { (void)phys; return 1; }
|
||||
int mdec_recently_active(uint32_t f) { (void)f; return 0; }
|
||||
void gte_execute(CPUState *cpu, uint32_t cmd) { (void)cpu; (void)cmd; }
|
||||
void gte_precision_store_word(uint32_t a, uint8_t r) { (void)a; (void)r; }
|
||||
uint32_t gte_read_ctrl(CPUState *cpu, uint8_t r) { (void)cpu; (void)r; return 0; }
|
||||
uint32_t gte_read_data(CPUState *cpu, uint8_t r) { (void)cpu; (void)r; return 0; }
|
||||
void gte_write_ctrl(CPUState *cpu, uint8_t r, uint32_t v) { (void)cpu; (void)r; (void)v; }
|
||||
void gte_write_data(CPUState *cpu, uint8_t r, uint32_t v) { (void)cpu; (void)r; (void)v; }
|
||||
int overlay_loader_call_native(CPUState *cpu, uint32_t a) { (void)cpu; (void)a; return 0; }
|
||||
void psx_bail_record(uint32_t ra, uint32_t sp, uint32_t pc, uint32_t gsp) {
|
||||
(void)ra; (void)sp; (void)pc; (void)gsp;
|
||||
}
|
||||
void psx_break(CPUState *cpu, uint32_t code, uint32_t pc) { (void)cpu; (void)code; (void)pc; }
|
||||
void psx_check_interrupts(struct CPUState *cpu) { (void)cpu; }
|
||||
void psx_dispatch_call(CPUState *cpu, uint32_t t, uint32_t r) { (void)cpu; (void)t; (void)r; }
|
||||
void psx_fatal_halt(const char *reason) { fprintf(stderr, "fatal: %s\n", reason); exit(2); }
|
||||
void psx_get_freeze_diag(uint64_t *a, uint32_t *b, int *c, int *d, uint64_t *e,
|
||||
uint64_t *f) {
|
||||
(void)a; (void)b; (void)c; (void)d; (void)e; (void)f;
|
||||
}
|
||||
int psx_get_in_exception(void) { return 0; }
|
||||
int psx_interrupts_checked_at_current_cycle(uint32_t pc) { (void)pc; return 1; }
|
||||
void psx_pgxp_alu(struct CPUState *c, uint32_t i, uint32_t r, uint32_t a, uint32_t b) {
|
||||
(void)c; (void)i; (void)r; (void)a; (void)b;
|
||||
}
|
||||
void psx_pgxp_cop2(struct CPUState *c, uint32_t i, uint32_t v, uint32_t a) {
|
||||
(void)c; (void)i; (void)v; (void)a;
|
||||
}
|
||||
void psx_pgxp_load(struct CPUState *c, uint32_t i, uint32_t a, uint32_t v) {
|
||||
(void)c; (void)i; (void)a; (void)v;
|
||||
}
|
||||
void psx_pgxp_muldiv(struct CPUState *c, uint32_t i, uint32_t h, uint32_t l,
|
||||
uint32_t a, uint32_t b) {
|
||||
(void)c; (void)i; (void)h; (void)l; (void)a; (void)b;
|
||||
}
|
||||
void psx_pgxp_store(struct CPUState *c, uint32_t i, uint32_t a, uint32_t v) {
|
||||
(void)c; (void)i; (void)a; (void)v;
|
||||
}
|
||||
void psx_rfe_mark_escape(void) {}
|
||||
int psx_syscall(CPUState *cpu, uint32_t code) { (void)cpu; (void)code; return 0; }
|
||||
int psx_dispatch_game_compiled(CPUState *cpu, uint32_t a) { (void)cpu; (void)a; return 0; }
|
||||
int psx_game_is_function_entry(uint32_t a) { (void)a; return 0; }
|
||||
int psx_game_text_native_ok(uint32_t a) { (void)a; return 0; }
|
||||
int psx_game_text_native_ok_full(uint32_t a) { (void)a; return 0; }
|
||||
int psx_mod_function_entry(struct CPUState *cpu, uint32_t a) { (void)cpu; (void)a; return 0; }
|
||||
|
||||
/* The game's text image for this test: [TEXT_LO, TEXT_HI). */
|
||||
#define TEXT_LO 0x80020000u
|
||||
#define TEXT_HI 0x80030000u
|
||||
int psx_game_address_in_text(uint32_t addr) {
|
||||
const uint32_t phys = addr & 0x1FFFFFFFu;
|
||||
return phys >= (TEXT_LO & 0x1FFFFFFFu) && phys < (TEXT_HI & 0x1FFFFFFFu);
|
||||
}
|
||||
|
||||
/* ---- harness --------------------------------------------------------------- */
|
||||
static int failures;
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
|
||||
fprintf(stderr, __VA_ARGS__); \
|
||||
fputc('\n', stderr); \
|
||||
++failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
enum { AT = 1, V0 = 2, T2 = 10 };
|
||||
#define SLTIU_T2_V0 0x2C4A01C0u /* sltiu t2, v0, 0x1C0 */
|
||||
#define ADDIU_AT_V0 0x2441FFFFu /* addiu at, v0, -1 */
|
||||
#define SLTIU_AT_AT 0x2C2101BFu /* sltiu at, at, 0x1BF */
|
||||
|
||||
static void step(CPUState *cpu, uint32_t pc, uint32_t insn) {
|
||||
uint32_t next = 0;
|
||||
cpu->pc = pc;
|
||||
(void)exec_one_fetched(cpu, pc, insn, &next);
|
||||
}
|
||||
|
||||
static void put_word(uint32_t addr, uint32_t v) { memcpy(test_ram + (addr & 0x1FFFFFu), &v, 4); }
|
||||
|
||||
static const char *s_capture_path = "draw_distance_interp_stderr.txt";
|
||||
|
||||
static void capture_stderr(void (*fn)(void), char *buf, size_t cap) {
|
||||
FILE *tmp = fopen(s_capture_path, "w+b");
|
||||
buf[0] = '\0';
|
||||
if (!tmp) {
|
||||
CHECK(0, "cannot open the stderr capture file %s", s_capture_path);
|
||||
fn();
|
||||
return;
|
||||
}
|
||||
fflush(stderr);
|
||||
int saved = dup(fileno(stderr));
|
||||
dup2(fileno(tmp), fileno(stderr));
|
||||
fn();
|
||||
fflush(stderr);
|
||||
dup2(saved, fileno(stderr));
|
||||
close(saved);
|
||||
rewind(tmp);
|
||||
size_t n = fread(buf, 1, cap - 1, tmp);
|
||||
buf[n] = '\0';
|
||||
fclose(tmp);
|
||||
remove(s_capture_path);
|
||||
}
|
||||
|
||||
/* ---- store ----------------------------------------------------------------- */
|
||||
static PSXDrawDistanceClampSite big[300];
|
||||
static void set_300(void) { (void)psx_draw_distance_set_clamp_sites(big, 300); }
|
||||
|
||||
static void test_store(void) {
|
||||
const PSXDrawDistanceClampSite sites[] = {
|
||||
{ 0x80020010u, SLTIU_T2_V0, V0, 0x1BF },
|
||||
{ 0xA0020000u, ADDIU_AT_V0, V0, 0x1BF }, /* KSEG1 spelling */
|
||||
{ 0x00020008u, SLTIU_T2_V0, V0, 0x100 }, /* KUSEG spelling */
|
||||
};
|
||||
CHECK(psx_draw_distance_set_clamp_sites(sites, 3) == 3, "three stored");
|
||||
CHECK(psx_draw_distance_clamp_site_count() == 3, "count 3");
|
||||
CHECK(s_sites[0].address == 0x00020000u && s_sites[1].address == 0x00020008u &&
|
||||
s_sites[2].address == 0x00020010u,
|
||||
"stored sorted and physical");
|
||||
const PSXDrawDistanceClampSite *s = psx_draw_distance_clamp_find(0x80020000u, ADDIU_AT_V0);
|
||||
CHECK(s && s->reg == V0 && s->max == 0x1BF, "KSEG0 lookup of a KSEG1 entry");
|
||||
CHECK(psx_draw_distance_clamp_find(0x00020008u, SLTIU_T2_V0) != NULL,
|
||||
"KUSEG lookup");
|
||||
CHECK(psx_draw_distance_clamp_find(0x80020010u, ADDIU_AT_V0) == NULL,
|
||||
"another word at a listed address misses");
|
||||
CHECK(psx_draw_distance_clamp_find(0x80020004u, SLTIU_T2_V0) == NULL,
|
||||
"between entries misses");
|
||||
CHECK(psx_draw_distance_clamp_find(0x80020014u, SLTIU_T2_V0) == NULL,
|
||||
"above the last entry misses");
|
||||
CHECK(psx_draw_distance_clamp_find(0x8001FFFCu, SLTIU_T2_V0) == NULL,
|
||||
"below the first entry misses");
|
||||
|
||||
CHECK(psx_draw_distance_set_clamp_sites(NULL, 5) == 0, "NULL list stores none");
|
||||
CHECK(psx_draw_distance_set_clamp_sites(sites, -1) == 0, "negative count stores none");
|
||||
CHECK(psx_draw_distance_clamp_find(0x80020010u, SLTIU_T2_V0) == NULL, "empty store misses");
|
||||
|
||||
for (int i = 0; i < 300; i++) {
|
||||
big[i].address = 0x80040000u + 4u * (uint32_t)(299 - i);
|
||||
big[i].expected = SLTIU_T2_V0;
|
||||
big[i].reg = V0;
|
||||
big[i].max = 0x1BF;
|
||||
}
|
||||
char log[512];
|
||||
capture_stderr(set_300, log, sizeof log);
|
||||
CHECK(psx_draw_distance_clamp_site_count() == PSX_DRAW_DISTANCE_CLAMP_SITES_MAX,
|
||||
"over-long list stored %d", psx_draw_distance_clamp_site_count());
|
||||
CHECK(strstr(log, "lists 300 sites; keeping the first 256") != NULL,
|
||||
"overflow not logged (got \"%s\")", log);
|
||||
int hits = 0;
|
||||
for (int i = 0; i < 300; i++)
|
||||
hits += psx_draw_distance_clamp_find(big[i].address, SLTIU_T2_V0) != NULL;
|
||||
CHECK(hits == 256, "%d of the first 256 kept", hits);
|
||||
}
|
||||
|
||||
/* ---- switch ---------------------------------------------------------------- */
|
||||
static void test_switch(void) {
|
||||
(void)psx_draw_distance_set_clamp_sites(NULL, 0);
|
||||
CHECK(psx_mod_set_draw_distance_clamp(1) == 0, "no sites: the setter says so");
|
||||
CHECK(psx_mod_draw_distance_clamp_enabled(), "the switch is still recorded");
|
||||
CHECK(!g_psx_draw_distance_clamp_live, "no sites: interpreter gate stays off");
|
||||
const PSXDrawDistanceClampSite one = { 0x80020000u, SLTIU_T2_V0, V0, 0x1BF };
|
||||
(void)psx_draw_distance_set_clamp_sites(&one, 1);
|
||||
CHECK(g_psx_draw_distance_clamp_live, "sites after the switch: gate on");
|
||||
CHECK(psx_mod_set_draw_distance_clamp(0) == 1, "sites: the setter says so");
|
||||
CHECK(!g_psx_draw_distance_clamp_live && !g_psx_draw_distance_clamp,
|
||||
"off clears both");
|
||||
CHECK(psx_mod_set_draw_distance_clamp(7) == 1 && g_psx_draw_distance_clamp == 1u,
|
||||
"any nonzero is on");
|
||||
(void)psx_mod_set_draw_distance_clamp(0);
|
||||
}
|
||||
|
||||
/* ---- interpreted instructions ---------------------------------------------- */
|
||||
static void test_interpreter(void) {
|
||||
CPUState cpu;
|
||||
memset(&cpu, 0, sizeof cpu);
|
||||
const uint32_t site = TEXT_LO + 0x100u, twin = TEXT_LO + 0x200u;
|
||||
const uint32_t a_site = TEXT_LO + 0x300u, a_guard = a_site + 4u;
|
||||
const uint32_t outside = TEXT_HI + 0x100u;
|
||||
put_word(site, SLTIU_T2_V0);
|
||||
put_word(twin, SLTIU_T2_V0);
|
||||
put_word(a_site, ADDIU_AT_V0);
|
||||
put_word(a_guard, SLTIU_AT_AT);
|
||||
put_word(outside, SLTIU_T2_V0);
|
||||
const PSXDrawDistanceClampSite sites[] = {
|
||||
{ site, SLTIU_T2_V0, V0, 0x1BF },
|
||||
{ a_site, ADDIU_AT_V0, V0, 0x1BF },
|
||||
{ outside, SLTIU_T2_V0, V0, 0x1BF },
|
||||
};
|
||||
(void)psx_draw_distance_set_clamp_sites(sites, 3);
|
||||
|
||||
/* Off: the original guard rejects the far index and keeps the register. */
|
||||
(void)psx_mod_set_draw_distance_clamp(0);
|
||||
cpu.gpr[V0] = 0x300u;
|
||||
step(&cpu, site, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[T2] == 0u && cpu.gpr[V0] == 0x300u, "off: vanilla reject");
|
||||
|
||||
(void)psx_mod_set_draw_distance_clamp(1);
|
||||
/* On: kept in the last slot. */
|
||||
cpu.gpr[V0] = 0x300u;
|
||||
step(&cpu, site, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[V0] == 0x1BFu && cpu.gpr[T2] == 1u, "on: clamped and kept (v0=%X t2=%u)",
|
||||
cpu.gpr[V0], cpu.gpr[T2]);
|
||||
/* Near indices are untouched. */
|
||||
cpu.gpr[V0] = 0x40u;
|
||||
step(&cpu, site, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[V0] == 0x40u && cpu.gpr[T2] == 1u, "on: near index unchanged");
|
||||
/* The same instruction at an unlisted address stays vanilla. */
|
||||
cpu.gpr[V0] = 0x300u;
|
||||
step(&cpu, twin, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[V0] == 0x300u && cpu.gpr[T2] == 0u, "on: unlisted twin vanilla");
|
||||
/* Another word at a listed address stays vanilla. */
|
||||
cpu.gpr[V0] = 0x300u;
|
||||
step(&cpu, site, ADDIU_AT_V0);
|
||||
CHECK(cpu.gpr[V0] == 0x300u && cpu.gpr[AT] == 0x2FFu,
|
||||
"on: mismatched word vanilla");
|
||||
/* Outside the game's text image (captured overlay code): vanilla. */
|
||||
cpu.gpr[V0] = 0x300u;
|
||||
step(&cpu, outside, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[V0] == 0x300u && cpu.gpr[T2] == 0u, "on: outside text vanilla");
|
||||
|
||||
/* The addiu form: `addiu at,v0,-1; sltiu at,at,0x1BF` rejects 0 (too
|
||||
* near) and >= 0x1C0 (too far). The clamp keeps far, never near. */
|
||||
cpu.gpr[V0] = 0x900u;
|
||||
step(&cpu, a_site, ADDIU_AT_V0);
|
||||
step(&cpu, a_guard, SLTIU_AT_AT);
|
||||
CHECK(cpu.gpr[V0] == 0x1BFu && cpu.gpr[AT] == 1u, "on: addiu form keeps far");
|
||||
cpu.gpr[V0] = 0u;
|
||||
step(&cpu, a_site, ADDIU_AT_V0);
|
||||
step(&cpu, a_guard, SLTIU_AT_AT);
|
||||
CHECK(cpu.gpr[V0] == 0u && cpu.gpr[AT] == 0u, "on: addiu form still rejects near");
|
||||
/* A negative (wrapped) index is left for the original guard. */
|
||||
cpu.gpr[V0] = 0xFFFFFFF0u;
|
||||
step(&cpu, site, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[V0] == 0xFFFFFFF0u && cpu.gpr[T2] == 0u, "on: negative index vanilla");
|
||||
|
||||
(void)psx_mod_set_draw_distance_clamp(0);
|
||||
cpu.gpr[V0] = 0x900u;
|
||||
step(&cpu, a_site, ADDIU_AT_V0);
|
||||
step(&cpu, a_guard, SLTIU_AT_AT);
|
||||
CHECK(cpu.gpr[V0] == 0x900u && cpu.gpr[AT] == 0u, "off again: vanilla");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc > 1) s_capture_path = argv[1];
|
||||
test_store();
|
||||
test_switch();
|
||||
test_interpreter();
|
||||
if (failures) {
|
||||
fprintf(stderr, "%d check(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("PASS draw-distance clamp store, switch and interpreted sites");
|
||||
return 0;
|
||||
}
|
||||
@@ -124,6 +124,7 @@ for reset in (
|
||||
"psx_mod_set_world_scene_predicate(nullptr);",
|
||||
"psx_mod_set_retained_scene_predicate(nullptr);",
|
||||
"psx_mod_set_adaptive_backdrop_preload(0);",
|
||||
"(void)psx_mod_set_draw_distance_clamp(0);",
|
||||
"g_bezel_path.clear();",
|
||||
"g_frame_interpolation_blend = g_frame_interpolation_blend_default;",
|
||||
"g_frame_interpolation_source = PSX_MOD_FRAME_SOURCE_VBLANK;",
|
||||
|
||||
Reference in New Issue
Block a user