// GTA Vice City Stories (PSP, ULUS-10160) - VCSNative draw-distance patch // Target: VCSRecomp Stage 40 / load base 0x08804000 // // INI (VCSNative.ini): // // [DrawDistance] // Enabled = 1 // Objects = 1.50 // Vehicles = 1.50 // NPCs = 1.50 // // 1.0 = original PSP distance. Values below 1.0 are clamped to 1.0. // Objects/World/Vehicles/NPCs are allowed up to 8.0 for explicit testing. // For mission compatibility, keep Vehicles/NPCs <= 2.0 unless tested. // // Integration (no header required): // 1) Add this .cpp to VCSNative's target sources. // 2) In host/main.cpp, after register_generated_functions(runtime), declare/call: // namespace vcs { // void install_draw_distance_patch(psprecomp::Runtime &, // const std::filesystem::path &); // } // vcs::install_draw_distance_patch(runtime, configuration.source_path); // // The install call MUST be after register_generated_functions(runtime), because this // file intentionally replaces a few AOT entry labels in Runtime's function table. #include "vcs_draw_distance_patch.hpp" #include "psprecomp/runtime.hpp" #include "psprecomp/common.hpp" // hex32, para o relatorio de hooks vivos/mortos #include "vcs_runtime_log.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace vcs { DrawDistanceRuntimeScales g_draw_distance_runtime_scales{}; DrawDistanceRuntimeTelemetry g_draw_distance_runtime_telemetry{}; namespace { struct DrawDistanceConfig { bool enabled{false}; float world{1.0f}; float vehicles{1.0f}; float npcs{1.0f}; }; DrawDistanceConfig g_config{}; // ULUS-10160 / VCSNative AOT guest addresses. // VCS globals, addressed from $gp (r28). constexpr std::uint32_t kGpFarClipOffset = 7796u; // CDraw::ms_fFarClipZ constexpr std::uint32_t kGpIdeCountOffset = 7656u; // IDE/model-info slot count constexpr std::uint32_t kGpIdeTableOffset = 24u; // IDE/model-info pointer table // VCS CBaseModelInfo-like layout exposed by the IDE table. constexpr std::uint32_t kModelHashOffset = 0x08u; constexpr std::uint32_t kModelTypeOffset = 0x10u; constexpr std::uint32_t kDrawDist1Offset = 0x2Cu; constexpr std::uint32_t kDrawDist2Offset = 0x30u; constexpr std::uint32_t kDrawDist3Offset = 0x34u; constexpr std::uint8_t kIdeTypeObject = 1u; constexpr std::uint8_t kIdeTypeTimedObj = 3u; struct OriginalWorldModel { std::uint32_t hash{}; std::uint8_t type{}; float d1{}; float d2{}; float d3{}; }; std::unordered_map g_original_world_models; std::string trim_copy(std::string value) { auto is_space = [](unsigned char c) { return std::isspace(c) != 0; }; while (!value.empty() && is_space(static_cast(value.front()))) value.erase(value.begin()); while (!value.empty() && is_space(static_cast(value.back()))) value.pop_back(); return value; } std::string lower_copy(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return value; } std::string strip_comment(std::string value) { bool quoted = false; char quote = '\0'; for (std::size_t i = 0; i < value.size(); ++i) { const char c = value[i]; if (c == '\'' || c == '"') { if (!quoted) { quoted = true; quote = c; } else if (quote == c) { quoted = false; } } else if (!quoted && (c == ';' || c == '#')) { value.resize(i); break; } } return trim_copy(std::move(value)); } bool parse_bool(std::string value, bool &out) { value = lower_copy(trim_copy(std::move(value))); if (value == "1" || value == "true" || value == "yes" || value == "on") { out = true; return true; } if (value == "0" || value == "false" || value == "no" || value == "off") { out = false; return true; } return false; } bool parse_float(std::string value, float &out) { value = trim_copy(std::move(value)); if (value.empty()) return false; char *end = nullptr; const float v = std::strtof(value.c_str(), &end); if (end == value.c_str() || *end != '\0' || !std::isfinite(v)) return false; out = v; return true; } DrawDistanceConfig load_config(const std::filesystem::path &path) { DrawDistanceConfig cfg{}; std::ifstream in(path); if (!in) return cfg; bool in_section = false; std::string line; while (std::getline(in, line)) { line = strip_comment(std::move(line)); if (line.empty()) continue; if (line.front() == '[' && line.back() == ']') { const std::string section = lower_copy(trim_copy(line.substr(1, line.size() - 2))); in_section = (section == "drawdistance" || section == "draw distance"); continue; } if (!in_section) continue; const std::size_t eq = line.find('='); if (eq == std::string::npos) continue; const std::string key = lower_copy(trim_copy(line.substr(0, eq))); const std::string value = trim_copy(line.substr(eq + 1)); if (key == "enabled") { parse_bool(value, cfg.enabled); } else if (key == "world" || key == "worldmultiplier" || key == "objects" || key == "buildings" || key == "objectmultiplier" || key == "buildingmultiplier") { parse_float(value, cfg.world); } else if (key == "vehicles" || key == "cars" || key == "vehiclemultiplier") { parse_float(value, cfg.vehicles); } else if (key == "npcs" || key == "peds" || key == "npcmultiplier") { parse_float(value, cfg.npcs); } } cfg.world = std::clamp(cfg.world, 1.0f, 8.0f); cfg.vehicles = std::clamp(cfg.vehicles, 1.0f, 8.0f); cfg.npcs = std::clamp(cfg.npcs, 1.0f, 8.0f); return cfg; } float load_float(psprecomp::Runtime &runtime, std::uint32_t address) { return std::bit_cast(runtime.memory().load32(address)); } void store_float(psprecomp::Runtime &runtime, std::uint32_t address, float value) { runtime.memory().store32(address, std::bit_cast(value)); } bool sane_draw_distance(float value) { // Zero/negative values can be sentinels in model-info. Leave them untouched. return std::isfinite(value) && value > 0.0f && value < 100000.0f; } bool nearly_equal(float a, float b) { const float scale = std::max({1.0f, std::fabs(a), std::fabs(b)}); return std::fabs(a - b) <= scale * 1.0e-4f; } float scaled_or_original(float original, float multiplier) { return sane_draw_distance(original) ? original * multiplier : original; } // Returns true once it has actually written entries, so the caller can stop // retrying. Called every frame from SetFarClipZ until it succeeds. bool patch_world_model_table(psprecomp::Runtime &runtime, std::uint32_t gp) { if (g_config.world <= 1.0f || gp == 0u) return false; const std::uint32_t count = runtime.memory().load32(gp + kGpIdeCountOffset); const std::uint32_t table = runtime.memory().load32(gp + kGpIdeTableOffset); // Corrupt/uninitialized globals must never turn this optional patch into a crash. const bool bad = count == 0u || count > 32768u || table == 0u || !runtime.memory().contains(table, static_cast(count) * 4u); // The guard sits before the "entries patched" banner, so a wrong GP offset // used to look exactly like the hook never running. Report the values once. static bool guard_logged = false; if (!guard_logged) { guard_logged = true; std::cerr << "[draw-distance] ide_init gp=" << psprecomp::hex32(gp) << " count@" << kGpIdeCountOffset << "=" << count << " table@" << kGpIdeTableOffset << "=" << psprecomp::hex32(table) << (bad ? " -> GUARD REJEITOU" : " -> ok") << "\n"; } if (bad) return false; std::uint32_t patched = 0u; for (std::uint32_t i = 0; i < count; ++i) { const std::uint32_t info = runtime.memory().load32(table + i * 4u); if (info == 0u || !runtime.memory().contains(info, 0x38u)) continue; const std::uint8_t type = runtime.memory().load8(info + kModelTypeOffset); if (type != kIdeTypeObject && type != kIdeTypeTimedObj) continue; const std::uint32_t hash = runtime.memory().load32(info + kModelHashOffset); const float cur1 = load_float(runtime, info + kDrawDist1Offset); const float cur2 = load_float(runtime, info + kDrawDist2Offset); const float cur3 = load_float(runtime, info + kDrawDist3Offset); auto found = g_original_world_models.find(info); if (found == g_original_world_models.end() || found->second.hash != hash || found->second.type != type) { found = g_original_world_models.emplace( info, OriginalWorldModel{hash, type, cur1, cur2, cur3}).first; } else { // If the game reloaded/rebuilt the same model-info address, refresh the baseline // instead of multiplying an already multiplied value again. const OriginalWorldModel &old = found->second; const bool still_patched = nearly_equal(cur1, scaled_or_original(old.d1, g_config.world)) && nearly_equal(cur2, scaled_or_original(old.d2, g_config.world)) && nearly_equal(cur3, scaled_or_original(old.d3, g_config.world)); const bool still_original = nearly_equal(cur1, old.d1) && nearly_equal(cur2, old.d2) && nearly_equal(cur3, old.d3); if (!still_patched && !still_original) { found->second = OriginalWorldModel{hash, type, cur1, cur2, cur3}; } } const OriginalWorldModel &base = found->second; store_float(runtime, info + kDrawDist1Offset, scaled_or_original(base.d1, g_config.world)); store_float(runtime, info + kDrawDist2Offset, scaled_or_original(base.d2, g_config.world)); store_float(runtime, info + kDrawDist3Offset, scaled_or_original(base.d3, g_config.world)); ++patched; } static bool logged = false; if (!logged && patched != 0u) { std::cerr << "[draw-distance] world model-info entries patched=" << patched << " multiplier=" << g_config.world << "\n"; // "entries patched" only proves the writes happened. Print a couple of // real before/after pairs so the magnitude is visible, and print the far // clip alongside -- if the values grow but nothing appears on screen, the // limit is fog or the renderer reads other fields, not this table. std::uint32_t shown = 0u; for (std::uint32_t i = 0; i < count && shown < 3u; ++i) { const std::uint32_t info = runtime.memory().load32(table + i * 4u); if (info == 0u || !runtime.memory().contains(info, 0x38u)) continue; const auto found = g_original_world_models.find(info); if (found == g_original_world_models.end()) continue; std::cerr << " [dd-amostra] info=" << psprecomp::hex32(info) << " d1 " << found->second.d1 << " -> " << load_float(runtime, info + kDrawDist1Offset) << " | d2 " << found->second.d2 << " -> " << load_float(runtime, info + kDrawDist2Offset) << "\n"; ++shown; } std::cerr << " [dd-amostra] far_clip@" << kGpFarClipOffset << "=" << load_float(runtime, gp + kGpFarClipOffset) << "\n"; logged = true; } // Re-check on a later pass whether our values survived. If the game // repopulates the table, they will have gone back to the originals. static std::uint32_t verifications = 0u; if (logged && patched != 0u && verifications < 3u) { ++verifications; std::uint32_t intact = 0u, reverted = 0u; for (const auto &[info, base] : g_original_world_models) { const float now = load_float(runtime, info + kDrawDist1Offset); if (nearly_equal(now, scaled_or_original(base.d1, g_config.world))) ++intact; else if (nearly_equal(now, base.d1)) ++reverted; } std::cerr << " [dd-verifica] passada=" << verifications << " intactos=" << intact << " revertidos=" << reverted << "\n"; } if (patched != 0u) { ++g_draw_distance_runtime_telemetry.world_table_patch_hits; g_draw_distance_runtime_telemetry.world_models_patched += patched; } return patched != 0u; } } // namespace void install_draw_distance_patch(psprecomp::Runtime &runtime, const std::filesystem::path &ini_path) { g_config = load_config(ini_path); g_original_world_models.clear(); g_draw_distance_runtime_scales = {}; g_draw_distance_runtime_telemetry = {}; if (g_config.enabled) { g_draw_distance_runtime_scales.world = g_config.world; g_draw_distance_runtime_scales.entity = std::max({g_config.world, g_config.vehicles, g_config.npcs}); g_draw_distance_runtime_scales.vehicles = g_config.vehicles; g_draw_distance_runtime_scales.npcs = g_config.npcs; } if (!g_config.enabled) { std::cerr << "[draw-distance] disabled\n"; return; } // Far clip and entity/vehicle/NPC constants are patched directly at // their generated local labels, because gameplay reaches // those labels with intra-unit gotos that cannot be intercepted by the runtime // function registry. if (g_config.world > 1.0f) { // Far clip is scaled at the exact gp+7796 setter. Model-info LODs are // patched at IDE completion and refreshed at vblank if streaming rebuilds them. std::cerr << "[draw-distance] world path=exact far-clip setter + IDE model table\n"; } if (g_config.vehicles > 1.0f || g_config.npcs > 1.0f) { // Entity/vehicle/NPC labels are also reached by local gotos inside their // generated units. Runtime::register_function cannot intercept those // paths. The generated corpus now reads g_draw_distance_runtime_scales // at the exact labels instead, so do not install misleading dead hooks. std::cerr << "[draw-distance] entity path=verified WidescreenFix semantic + vehicle/NPC local constants" << "\n"; } std::cerr << "[draw-distance] enabled" << " world=" << g_config.world << " vehicles=" << g_config.vehicles << " npcs=" << g_config.npcs << " entity_lod=" << std::max({g_config.world, g_config.vehicles, g_config.npcs}) << "\n"; { std::ostringstream line; line << "draw-distance config enabled=1" << " world=" << g_config.world << " vehicles=" << g_config.vehicles << " npcs=" << g_config.npcs << " common_lod=" << std::max({g_config.world, g_config.vehicles, g_config.npcs}); runtime_log_line(line.str()); } } void draw_distance_world_table_ready(psprecomp::Runtime &runtime, std::uint32_t guest_gp) noexcept { if (!g_config.enabled || g_config.world <= 1.0f || guest_gp == 0u) return; try { if (patch_world_model_table(runtime, guest_gp)) { std::cerr << "[draw-distance] world table patched at authoritative IDE init" << " gp=" << psprecomp::hex32(guest_gp) << "\n"; } } catch (const std::exception &e) { std::cerr << "[draw-distance] world IDE init patch skipped: " << e.what() << "\n"; } catch (...) { std::cerr << "[draw-distance] world IDE init patch skipped: unknown exception\n"; } } void draw_distance_vblank_tick(psprecomp::Runtime &runtime, std::uint32_t guest_gp, std::uint64_t vblank_index) noexcept { if (!g_config.enabled || guest_gp == 0u) return; try { // Far clip is now multiplied at the actual CDraw::SetFarClipZ AOT // setter (0x08A1AD6C), so vblank must never multiply it a second time. // World model-info is still refreshed here because streaming may rebuild // entries after the original IDE load. if (g_config.world > 1.0f && (vblank_index < 240u || (vblank_index % 120u) == 0u)) (void)patch_world_model_table(runtime, guest_gp); if ((vblank_index % 120u) == 0u) { const std::uint32_t count = runtime.memory().contains( guest_gp + kGpIdeCountOffset, sizeof(std::uint32_t)) ? runtime.memory().load32(guest_gp + kGpIdeCountOffset) : 0u; const std::uint32_t table = runtime.memory().contains( guest_gp + kGpIdeTableOffset, sizeof(std::uint32_t)) ? runtime.memory().load32(guest_gp + kGpIdeTableOffset) : 0u; float far_clip = 0.0f; if (runtime.memory().contains(guest_gp + kGpFarClipOffset, sizeof(std::uint32_t))) far_clip = load_float(runtime, guest_gp + kGpFarClipOffset); std::ostringstream line; line << "draw-distance telemetry vblank=" << vblank_index << " gp=" << psprecomp::hex32(guest_gp) << " far_clip=" << far_clip << " model_count=" << count << " model_table=" << psprecomp::hex32(table) << " far_set_hits=" << g_draw_distance_runtime_telemetry.far_clip_set_hits << " actor_hits=" << g_draw_distance_runtime_telemetry.actor_lod_hits << " npc_hits=" << g_draw_distance_runtime_telemetry.npc_constant_hits << " veh_dynamic_hits=" << g_draw_distance_runtime_telemetry.vehicle_dynamic_hits << " veh_fallback_hits=" << g_draw_distance_runtime_telemetry.vehicle_fallback_hits << " table_patch_hits=" << g_draw_distance_runtime_telemetry.world_table_patch_hits << " world_models_written=" << g_draw_distance_runtime_telemetry.world_models_patched; runtime_log_line(line.str()); } } catch (const std::exception &error) { static bool logged = false; if (!logged) { logged = true; std::cerr << "[draw-distance] vblank maintenance disabled after error: " << error.what() << "\n"; runtime_log_error("draw-distance telemetry", error.what()); } } } } // namespace vcs