Merge pull request #468 from RetroPortingToolKit/feat/max-detail

Add opt-in draw-distance clamps ([[draw_distance.clamp]])
This commit is contained in:
Matt Stanley
2026-10-02 19:30:17 -07:00
committed by GitHub
21 changed files with 1160 additions and 3 deletions
+14
View File
@@ -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).
+7
View File
@@ -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
+48
View File
@@ -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]
```
@@ -378,6 +379,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)
+1 -1
View File
@@ -1 +1 @@
6f777c349081f0b31bbaed39224269698219bdc9ea85fbaf83f924698a4fb4e9
ad77e3f69dede5ce4005c19ff385ea402a30637e0428cbe73e89df93a6f35de6
+1 -1
View File
@@ -1 +1 @@
852297730a86157bff30c08100f3decc395261935385288564d4eb0926952f67
2cb77c18b5fb1e2c2eaf88faf9a712170fe379ee612e10abe2d890839b8c024a
+55
View File
@@ -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).
+8
View File
@@ -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);
+47
View File
@@ -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";
}
+66
View File
@@ -891,6 +891,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.
@@ -2367,6 +2431,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;
}
+20
View File
@@ -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.
@@ -1243,6 +1260,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.
+5
View File
@@ -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;
+2 -1
View File
@@ -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;
}
+71
View File
@@ -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 */
+11
View File
@@ -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,
+1
View File
@@ -369,6 +369,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
+17
View File
@@ -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>
@@ -1527,6 +1528,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) {
+74
View File
@@ -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;
}
+13
View File
@@ -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"
@@ -1551,6 +1552,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;
@@ -13775,6 +13777,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;
+322
View File
@@ -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;",