temp stuff, might not good

temp stuff, might not good
This commit is contained in:
Jessica_Natalia
2026-08-15 11:12:49 -03:00
parent f6e7d415c7
commit 4a6b83aede
19 changed files with 1571 additions and 189 deletions
@@ -0,0 +1,35 @@
--- /mnt/data/savedata_windows_compile_fix_work/savedata_dialog.orig.cpp 2026-08-15 02:10:03.548449495 +0000
+++ /mnt/data/savedata_windows_compile_fix_work/savedata_dialog.cpp 2026-08-15 02:10:03.548765745 +0000
@@ -81,7 +81,7 @@
WS_CHILD | WS_VISIBLE, 16, 14, 470, 22, window, nullptr, nullptr, nullptr);
state->list = CreateWindowExW(WS_EX_CLIENTEDGE, L"LISTBOX", L"",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | LBS_NOTIFY | WS_VSCROLL,
- 16, 42, 470, 224, window, reinterpret_cast<HMENU>(kListId), nullptr, nullptr);
+ 16, 42, 470, 224, window, reinterpret_cast<HMENU>(static_cast<INT_PTR>(kListId)), nullptr, nullptr);
for (std::size_t i = 0; i < state->slots->size(); ++i) {
const auto &slot = (*state->slots)[i];
std::wstring line = L"Slot " + std::to_wstring(i + 1u) + L" - " + utf8_to_wide(slot.save_name);
@@ -90,10 +90,10 @@
}
CreateWindowExW(0, L"BUTTON", state->saving ? L"Save" : L"Load",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON,
- 310, 280, 82, 30, window, reinterpret_cast<HMENU>(kAcceptId), nullptr, nullptr);
+ 310, 280, 82, 30, window, reinterpret_cast<HMENU>(static_cast<INT_PTR>(kAcceptId)), nullptr, nullptr);
CreateWindowExW(0, L"BUTTON", L"Cancel",
WS_CHILD | WS_VISIBLE | WS_TABSTOP,
- 404, 280, 82, 30, window, reinterpret_cast<HMENU>(kCancelId), nullptr, nullptr);
+ 404, 280, 82, 30, window, reinterpret_cast<HMENU>(static_cast<INT_PTR>(kCancelId)), nullptr, nullptr);
return 0;
}
case WM_COMMAND:
@@ -134,8 +134,8 @@
WNDCLASSW cls{};
cls.lpfnWndProc = savedata_window_proc;
cls.hInstance = instance;
- cls.hCursor = LoadCursorW(nullptr, IDC_ARROW);
- cls.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
+ cls.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512));
+ cls.hbrBackground = reinterpret_cast<HBRUSH>(static_cast<INT_PTR>(COLOR_WINDOW + 1));
cls.lpszClassName = kClassName;
registered = RegisterClassW(&cls) != 0 || GetLastError() == ERROR_CLASS_ALREADY_EXISTS;
}
+3
View File
@@ -0,0 +1,3 @@
@echo off
call "%~dp0scripts\build_release_ninja.bat"
exit /b %errorlevel%
+1
View File
@@ -124,6 +124,7 @@ set(VCS_HOST_SOURCES
host/vcs_native_fast_paths.cpp
host/framebuffer_capture.cpp
host/display_window.cpp
host/savedata_dialog.cpp
host/audio_output.cpp
host/vcs_config.cpp
host/vcs_camera_input.cpp
+40
View File
@@ -0,0 +1,40 @@
@echo off
setlocal EnableExtensions
for %%I in ("%~dp0..\..") do set "REPO=%%~fI"
echo ================================================================
echo PSPRecomp - FIX FUTURE FILE TIMESTAMPS
echo Repository:
echo %REPO%
echo ================================================================
echo.
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command ^
"$repo=[IO.Path]::GetFullPath('%REPO%');" ^
"$now=Get-Date;" ^
"$safe=$now.AddSeconds(-5);" ^
"$skip=@('\out\','\.git\','\.vs\','\build\','\CMakeFiles\');" ^
"$future=Get-ChildItem -LiteralPath $repo -Recurse -File -Force -ErrorAction SilentlyContinue | Where-Object {" ^
" $p=$_.FullName;" ^
" $blocked=$false;" ^
" foreach($s in $skip){if($p.IndexOf($s,[StringComparison]::OrdinalIgnoreCase)-ge 0){$blocked=$true;break}};" ^
" (-not $blocked) -and $_.LastWriteTime -gt $now.AddSeconds(2)" ^
"};" ^
"Write-Host ('Current local time: ' + $now.ToString('yyyy-MM-dd HH:mm:ss'));" ^
"Write-Host ('Future-dated source files: ' + $future.Count);" ^
"foreach($f in $future){" ^
" Write-Host (' fixing: ' + $f.FullName + ' [' + $f.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss') + ']');" ^
" $f.LastWriteTime=$safe" ^
"};" ^
"if($future.Count -gt 0){Write-Host ('Normalized to: ' + $safe.ToString('yyyy-MM-dd HH:mm:ss'))} else {Write-Host 'No future timestamps found.'}"
if errorlevel 1 (
echo.
echo ERROR: PowerShell timestamp normalization failed.
pause
exit /b 1
)
echo.
echo Timestamp check complete.
exit /b 0
+6 -3
View File
@@ -71,10 +71,13 @@ PerformanceLog=false
Enabled=false
[DrawDistance]
; Correctness 0.1: implementation is complete but remains opt-in so normal
; gameplay/performance can be compared directly against the clean GitHub baseline.
; Change only Enabled=true to test the prefilled 1.50x extension.
Enabled=false
World=1.00
Vehicles=1.00
NPCs=1.00
World=1.50
Vehicles=1.50
NPCs=1.50
[Controls]
CameraStick=true
@@ -8791,6 +8791,21 @@ L_089CB33C:
goto L_089CB38C;
}
L_089CB38C:
if (vcs::g_draw_distance_runtime_scales.npcs > 1.0f) {
const float dd_scale = vcs::g_draw_distance_runtime_scales.npcs;
ctx.gpr[19] = ctx.gpr[29] + static_cast<std::uint32_t>(64);
ctx.gpr[30] = ctx.gpr[29] + static_cast<std::uint32_t>(16);
ctx.gpr[23] = ctx.gpr[29] + static_cast<std::uint32_t>(32);
ctx.fpr[22] = 120.0f;
ctx.fpr[28] = 51.0f * dd_scale;
ctx.fpr[26] = 25.0f * dd_scale;
ctx.fpr[24] = 80.0f * dd_scale;
ctx.gpr[4] = ctx.gpr[18] << 5u;
ctx.gpr[20] = ctx.gpr[4];
ctx.gpr[4] <<= 4u;
ctx.gpr[20] += ctx.gpr[4];
goto L_089CB3C8;
}
ctx.gpr[19] = (ctx.gpr[29] + static_cast<std::uint32_t>(64));
ctx.gpr[30] = (ctx.gpr[29] + static_cast<std::uint32_t>(16));
ctx.gpr[23] = (ctx.gpr[29] + static_cast<std::uint32_t>(32));
@@ -1092,6 +1092,14 @@ L_08A24120:
if (rt.invoke_chained_direct<&recomp_unit_0133_entry, 133u, 449u, 0x08A1AD6Cu>(ctx, &aot_mem) && ctx.pc == 0x08A24128u) goto L_08A24128;
return;
L_08A24128:
if (vcs::g_draw_distance_runtime_scales.entity > 1.0f) {
const float dd_base = std::bit_cast<float>(aot_mem.aot_load32(ctx.gpr[16] + static_cast<std::uint32_t>(1960)));
const float dd_scale = vcs::g_draw_distance_runtime_scales.entity;
aot_mem.aot_store32(ctx.gpr[16] + static_cast<std::uint32_t>(1952),
std::bit_cast<std::uint32_t>(dd_base * dd_scale));
ctx.fpr[0] = dd_scale;
goto L_08A24138;
}
ctx.fpr[12] = std::bit_cast<float>(aot_mem.aot_load32(ctx.gpr[16] + static_cast<std::uint32_t>(1960)));
aot_mem.aot_store32(ctx.gpr[16] + static_cast<std::uint32_t>(1952), std::bit_cast<std::uint32_t>(ctx.fpr[12]));
ctx.gpr[31] = (0x08A24138u);
@@ -4568,6 +4568,10 @@ L_08B45AA4:
goto L_08B45AC0;
}
L_08B45AC0:
if (vcs::g_draw_distance_runtime_scales.vehicles > 1.0f) {
ctx.fpr[12] = 60.0f * vcs::g_draw_distance_runtime_scales.vehicles;
goto L_08B45AC8;
}
ctx.gpr[4] = (17008u << 16u);
ctx.fpr[12] = std::bit_cast<float>(ctx.gpr[4]);
goto L_08B45AC8;
@@ -2,6 +2,7 @@
#include <cstdint>
#include "psprecomp/guest_memory.hpp"
#include "vcs_draw_distance_patch.hpp"
namespace psprecomp {
class Runtime;
+259 -43
View File
@@ -14,6 +14,7 @@
#include <cstdlib>
#include <limits>
#include <iterator>
#include <iostream>
#include <span>
#include <sstream>
#include <unordered_map>
@@ -715,7 +716,23 @@ D3D12_COMPARISON_FUNC depth_compare(std::uint32_t function) noexcept {
return D3D12_COMPARISON_FUNC_ALWAYS;
}
struct Dx12BlendPlan {
bool enabled{};
bool exact{true};
bool uses_constant{};
std::uint32_t constant_rgb{};
D3D12_BLEND src{D3D12_BLEND_ONE};
D3D12_BLEND dst{D3D12_BLEND_ZERO};
D3D12_BLEND src_alpha{D3D12_BLEND_ONE};
D3D12_BLEND dst_alpha{D3D12_BLEND_ZERO};
D3D12_BLEND_OP op{D3D12_BLEND_OP_ADD};
D3D12_BLEND_OP op_alpha{D3D12_BLEND_OP_ADD};
};
std::size_t blend_variant(const GeGpuDrawDescriptor &draw) noexcept {
// Retained only for the legacy per-mode counters in GeGpuBackendReport.
// Pipeline selection itself uses dx12_blend_plan(), because VCS uses more
// PSP GE blend states than the old five hard-coded variants.
if (!draw.blend_enabled || draw.clear_mode) return 0u;
const std::uint32_t eq = draw.blend_equation & 7u;
const std::uint32_t src = draw.blend_source_factor & 0xFu;
@@ -736,6 +753,206 @@ std::size_t blend_variant(const GeGpuDrawDescriptor &draw) noexcept {
return 0u;
}
bool fixed_rgb_is_complement(std::uint32_t left, std::uint32_t right) noexcept {
left &= 0x00FFFFFFu;
right &= 0x00FFFFFFu;
for (std::uint32_t shift = 0u; shift < 24u; shift += 8u) {
if ((((left >> shift) & 0xFFu) + ((right >> shift) & 0xFFu)) != 0xFFu)
return false;
}
return true;
}
bool blend_source_factor(Dx12BlendPlan &plan, std::uint32_t factor,
std::uint32_t fixed_rgb) noexcept {
switch (factor & 0xFu) {
case 0u:
plan.src = D3D12_BLEND_DEST_COLOR;
plan.src_alpha = D3D12_BLEND_DEST_ALPHA;
return true;
case 1u:
plan.src = D3D12_BLEND_INV_DEST_COLOR;
plan.src_alpha = D3D12_BLEND_INV_DEST_ALPHA;
return true;
case 2u:
plan.src = D3D12_BLEND_SRC_ALPHA;
plan.src_alpha = D3D12_BLEND_SRC_ALPHA;
return true;
case 3u:
plan.src = D3D12_BLEND_INV_SRC_ALPHA;
plan.src_alpha = D3D12_BLEND_INV_SRC_ALPHA;
return true;
case 4u:
plan.src = D3D12_BLEND_DEST_ALPHA;
plan.src_alpha = D3D12_BLEND_DEST_ALPHA;
return true;
case 5u:
plan.src = D3D12_BLEND_INV_DEST_ALPHA;
plan.src_alpha = D3D12_BLEND_INV_DEST_ALPHA;
return true;
case 10u: {
const std::uint32_t fixed = fixed_rgb & 0x00FFFFFFu;
if (fixed == 0u) {
plan.src = D3D12_BLEND_ZERO;
plan.src_alpha = D3D12_BLEND_ZERO;
} else if (fixed == 0x00FFFFFFu) {
plan.src = D3D12_BLEND_ONE;
plan.src_alpha = D3D12_BLEND_ONE;
} else {
plan.src = D3D12_BLEND_BLEND_FACTOR;
// PSP FIX alpha is effectively 1.0: the GE register contains RGB.
plan.src_alpha = D3D12_BLEND_ONE;
plan.uses_constant = true;
plan.constant_rgb = fixed;
}
return true;
}
default:
// PSP factors 6..9 are 2x-alpha forms. D3D12 fixed-function blending
// has no bit-exact equivalent, so those states take the safe fallback.
return false;
}
}
bool blend_dest_factor(Dx12BlendPlan &plan, std::uint32_t factor,
std::uint32_t fixed_rgb) noexcept {
switch (factor & 0xFu) {
case 0u:
plan.dst = D3D12_BLEND_SRC_COLOR;
plan.dst_alpha = D3D12_BLEND_SRC_ALPHA;
return true;
case 1u:
plan.dst = D3D12_BLEND_INV_SRC_COLOR;
plan.dst_alpha = D3D12_BLEND_INV_SRC_ALPHA;
return true;
case 2u:
plan.dst = D3D12_BLEND_SRC_ALPHA;
plan.dst_alpha = D3D12_BLEND_SRC_ALPHA;
return true;
case 3u:
plan.dst = D3D12_BLEND_INV_SRC_ALPHA;
plan.dst_alpha = D3D12_BLEND_INV_SRC_ALPHA;
return true;
case 4u:
plan.dst = D3D12_BLEND_DEST_ALPHA;
plan.dst_alpha = D3D12_BLEND_DEST_ALPHA;
return true;
case 5u:
plan.dst = D3D12_BLEND_INV_DEST_ALPHA;
plan.dst_alpha = D3D12_BLEND_INV_DEST_ALPHA;
return true;
case 10u: {
const std::uint32_t fixed = fixed_rgb & 0x00FFFFFFu;
if (fixed == 0u) {
plan.dst = D3D12_BLEND_ZERO;
plan.dst_alpha = D3D12_BLEND_ZERO;
} else if (fixed == 0x00FFFFFFu) {
plan.dst = D3D12_BLEND_ONE;
plan.dst_alpha = D3D12_BLEND_ONE;
} else {
plan.dst = D3D12_BLEND_BLEND_FACTOR;
plan.dst_alpha = D3D12_BLEND_ONE;
plan.uses_constant = true;
plan.constant_rgb = fixed;
}
return true;
}
default:
return false;
}
}
Dx12BlendPlan dx12_blend_plan(const GeGpuDrawDescriptor &draw) noexcept {
Dx12BlendPlan plan{};
if (!draw.blend_enabled || draw.clear_mode) return plan;
plan.enabled = true;
const std::uint32_t equation = draw.blend_equation & 7u;
const std::uint32_t src_factor = draw.blend_source_factor & 0xFu;
const std::uint32_t dst_factor = draw.blend_dest_factor & 0xFu;
const std::uint32_t src_fix = draw.blend_fix_source & 0x00FFFFFFu;
const std::uint32_t dst_fix = draw.blend_fix_dest & 0x00FFFFFFu;
// MIN/MAX in the PSP software reference operate on the unweighted source
// and destination colors. D3D12 MIN/MAX do exactly that with ONE/ONE.
if (equation == 3u || equation == 4u) {
plan.src = plan.dst = D3D12_BLEND_ONE;
plan.src_alpha = plan.dst_alpha = D3D12_BLEND_ONE;
plan.op = plan.op_alpha = equation == 3u ? D3D12_BLEND_OP_MIN : D3D12_BLEND_OP_MAX;
return plan;
}
switch (equation) {
case 0u: plan.op = plan.op_alpha = D3D12_BLEND_OP_ADD; break;
case 1u: plan.op = plan.op_alpha = D3D12_BLEND_OP_SUBTRACT; break;
case 2u: plan.op = plan.op_alpha = D3D12_BLEND_OP_REV_SUBTRACT; break;
default:
// ABS_DIFF and unknown equations cannot be expressed by the D3D12
// fixed-function blend unit.
plan.exact = false;
break;
}
if (plan.exact && src_factor == 10u && dst_factor == 10u &&
src_fix != 0u && src_fix != 0x00FFFFFFu &&
dst_fix != 0u && dst_fix != 0x00FFFFFFu) {
// D3D12 exposes one dynamic blend constant. Two arbitrary PSP FIX/FIX
// colors are representable only when they are identical or complementary.
if (src_fix == dst_fix) {
plan.uses_constant = true;
plan.constant_rgb = src_fix;
plan.src = plan.dst = D3D12_BLEND_BLEND_FACTOR;
plan.src_alpha = plan.dst_alpha = D3D12_BLEND_ONE;
} else if (fixed_rgb_is_complement(src_fix, dst_fix)) {
plan.uses_constant = true;
plan.constant_rgb = src_fix;
plan.src = D3D12_BLEND_BLEND_FACTOR;
plan.dst = D3D12_BLEND_INV_BLEND_FACTOR;
plan.src_alpha = D3D12_BLEND_ONE;
plan.dst_alpha = D3D12_BLEND_ZERO;
} else {
plan.exact = false;
}
} else if (plan.exact) {
const bool src_ok = blend_source_factor(plan, src_factor, src_fix);
const bool src_uses_constant = plan.uses_constant;
const std::uint32_t src_constant = plan.constant_rgb;
// Evaluate the destination independently so an arbitrary FIX on each
// side cannot silently overwrite the one D3D12 blend constant.
Dx12BlendPlan dst_plan{};
const bool dst_ok = blend_dest_factor(dst_plan, dst_factor, dst_fix);
if (!src_ok || !dst_ok ||
(src_uses_constant && dst_plan.uses_constant && src_constant != dst_plan.constant_rgb)) {
plan.exact = false;
} else {
plan.dst = dst_plan.dst;
plan.dst_alpha = dst_plan.dst_alpha;
if (dst_plan.uses_constant) {
plan.uses_constant = true;
plan.constant_rgb = dst_plan.constant_rgb;
}
}
}
if (!plan.exact) {
// Safety fallback. The old DX12 path returned blend variant 0 here,
// disabling blending and turning translucent sun/lighting overlays into
// opaque full-screen washes. Keep the draw translucent instead. Exact
// parity for 2x-alpha/ABS_DIFF/non-complementary FIX/FIX still requires
// a shader/RMW path, and is reported as unsupported below.
plan.enabled = true;
plan.uses_constant = false;
plan.constant_rgb = 0u;
plan.src = D3D12_BLEND_SRC_ALPHA;
plan.dst = D3D12_BLEND_INV_SRC_ALPHA;
plan.src_alpha = D3D12_BLEND_ONE;
plan.dst_alpha = D3D12_BLEND_INV_SRC_ALPHA;
plan.op = plan.op_alpha = D3D12_BLEND_OP_ADD;
}
return plan;
}
std::uint8_t color_write_mask(const GeGpuDrawDescriptor &draw) noexcept {
std::uint8_t mask = 0u;
for (std::uint32_t channel = 0u; channel < 4u; ++channel) {
@@ -746,11 +963,20 @@ std::uint8_t color_write_mask(const GeGpuDrawDescriptor &draw) noexcept {
}
std::uint64_t pipeline_key(const GeGpuDrawDescriptor &draw) noexcept {
const Dx12BlendPlan blend = dx12_blend_plan(draw);
std::uint64_t key = static_cast<std::uint64_t>(draw.depth_test_enabled ? 1u : 0u);
key |= static_cast<std::uint64_t>(draw.depth_write_enabled ? 1u : 0u) << 1u;
key |= static_cast<std::uint64_t>(draw.depth_function & 7u) << 2u;
key |= static_cast<std::uint64_t>(blend_variant(draw) & 7u) << 5u;
key |= static_cast<std::uint64_t>(color_write_mask(draw) & 0xFu) << 8u;
key |= static_cast<std::uint64_t>(blend.enabled ? 1u : 0u) << 5u;
key |= static_cast<std::uint64_t>(blend.exact ? 0u : 1u) << 6u;
key |= static_cast<std::uint64_t>(static_cast<std::uint32_t>(blend.src) & 0x1Fu) << 7u;
key |= static_cast<std::uint64_t>(static_cast<std::uint32_t>(blend.dst) & 0x1Fu) << 12u;
key |= static_cast<std::uint64_t>(static_cast<std::uint32_t>(blend.src_alpha) & 0x1Fu) << 17u;
key |= static_cast<std::uint64_t>(static_cast<std::uint32_t>(blend.dst_alpha) & 0x1Fu) << 22u;
key |= static_cast<std::uint64_t>(static_cast<std::uint32_t>(blend.op) & 0x7u) << 27u;
key |= static_cast<std::uint64_t>(static_cast<std::uint32_t>(blend.op_alpha) & 0x7u) << 30u;
key |= static_cast<std::uint64_t>(color_write_mask(draw) & 0xFu) << 33u;
key |= static_cast<std::uint64_t>(blend.uses_constant ? 1u : 0u) << 37u;
return key;
}
@@ -805,9 +1031,9 @@ bool adjacent_batch_merge_compatible(const Dx12Batch &a, const Dx12Batch &b) noe
if (a.draw.scissor_x0 != b.draw.scissor_x0 || a.draw.scissor_y0 != b.draw.scissor_y0 ||
a.draw.scissor_x1 != b.draw.scissor_x1 || a.draw.scissor_y1 != b.draw.scissor_y1)
return false;
if (blend_variant(a.draw) == 4u &&
(a.draw.blend_fix_source & 0x00FFFFFFu) !=
(b.draw.blend_fix_source & 0x00FFFFFFu)) return false;
const Dx12BlendPlan blend_a = dx12_blend_plan(a.draw);
const Dx12BlendPlan blend_b = dx12_blend_plan(b.draw);
if (blend_a.uses_constant && blend_a.constant_rgb != blend_b.constant_rgb) return false;
// Pixel state moved out of the vertex stream in 45.4, so merged draws must
// share the exact root-constant state rather than merely a compatible PSO.
const Dx12PixelConstants pa = make_pixel_constants(a.draw, a.draw.texture_enabled);
@@ -1598,42 +1824,14 @@ ComPtr<ID3D12PipelineState> create_pipeline(Dx12GeState &s,
pso.BlendState.IndependentBlendEnable = FALSE;
D3D12_RENDER_TARGET_BLEND_DESC blend{};
blend.RenderTargetWriteMask = color_write_mask(draw);
const std::size_t variant = blend_variant(draw);
if (variant != 0u && variant != 2u) blend.BlendEnable = TRUE;
blend.SrcBlend = D3D12_BLEND_ONE;
blend.DestBlend = D3D12_BLEND_ZERO;
blend.BlendOp = D3D12_BLEND_OP_ADD;
blend.SrcBlendAlpha = D3D12_BLEND_ONE;
blend.DestBlendAlpha = D3D12_BLEND_ZERO;
blend.BlendOpAlpha = D3D12_BLEND_OP_ADD;
switch (variant) {
case 1u:
blend.SrcBlend = D3D12_BLEND_SRC_ALPHA;
blend.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
blend.SrcBlendAlpha = D3D12_BLEND_ONE;
blend.DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA;
break;
case 3u:
blend.SrcBlend = D3D12_BLEND_ONE;
blend.DestBlend = D3D12_BLEND_ONE;
blend.SrcBlendAlpha = D3D12_BLEND_ONE;
blend.DestBlendAlpha = D3D12_BLEND_ONE;
break;
case 4u:
blend.SrcBlend = D3D12_BLEND_BLEND_FACTOR;
blend.DestBlend = D3D12_BLEND_INV_BLEND_FACTOR;
blend.SrcBlendAlpha = D3D12_BLEND_ONE;
blend.DestBlendAlpha = D3D12_BLEND_ZERO;
break;
case 5u:
blend.SrcBlend = D3D12_BLEND_SRC_ALPHA;
blend.DestBlend = D3D12_BLEND_ONE;
blend.SrcBlendAlpha = D3D12_BLEND_ONE;
blend.DestBlendAlpha = D3D12_BLEND_ONE;
break;
default:
break;
}
const Dx12BlendPlan blend_plan = dx12_blend_plan(draw);
blend.BlendEnable = blend_plan.enabled ? TRUE : FALSE;
blend.SrcBlend = blend_plan.src;
blend.DestBlend = blend_plan.dst;
blend.BlendOp = blend_plan.op;
blend.SrcBlendAlpha = blend_plan.src_alpha;
blend.DestBlendAlpha = blend_plan.dst_alpha;
blend.BlendOpAlpha = blend_plan.op_alpha;
pso.BlendState.RenderTarget[0] = blend;
pso.DepthStencilState.DepthEnable = draw.depth_test_enabled ? TRUE : FALSE;
pso.DepthStencilState.DepthWriteMask = draw.depth_write_enabled
@@ -3986,8 +4184,9 @@ bool ge_gpu_backend_finish_color_frame(std::uint64_t vblank) noexcept {
active_scissor = scissor;
active_scissor_valid = true;
}
if (blend_variant(batch.draw) == 4u) {
const std::uint32_t fix = batch.draw.blend_fix_source & 0x00FFFFFFu;
const Dx12BlendPlan blend_plan = dx12_blend_plan(batch.draw);
if (blend_plan.uses_constant) {
const std::uint32_t fix = blend_plan.constant_rgb;
if (fix != active_blend_fix) {
const float factors[4]{
static_cast<float>(fix & 0xFFu) / 255.0f,
@@ -4014,6 +4213,23 @@ bool ge_gpu_backend_finish_color_frame(std::uint64_t vblank) noexcept {
case 3u: s.report.additive_blended_game_draw_calls += batch.logical_draw_count; break;
default: break;
}
if (batch.draw.blend_enabled && !batch.draw.clear_mode && !blend_plan.exact) {
s.report.unsupported_blend_game_draw_calls += batch.logical_draw_count;
static std::uint32_t diagnostic_count = 0u;
if (std::getenv("PSPRECOMP_DX12_BLEND_DIAG") != nullptr && diagnostic_count < 32u) {
std::ostringstream line;
line << "DX12 unsupported PSP blend fallback #" << (diagnostic_count + 1u)
<< ": eq=" << (batch.draw.blend_equation & 7u)
<< " src=" << (batch.draw.blend_source_factor & 0xFu)
<< " dst=" << (batch.draw.blend_dest_factor & 0xFu)
<< " fixS=0x" << std::hex << (batch.draw.blend_fix_source & 0x00FFFFFFu)
<< " fixD=0x" << (batch.draw.blend_fix_dest & 0x00FFFFFFu) << std::dec;
const std::string message = line.str();
std::cerr << "[blend] " << message << "\n";
runtime_log_error("blend", message);
++diagnostic_count;
}
}
if (batch.draw.fog_enabled) s.report.fogged_game_draw_calls += batch.logical_draw_count;
if (srv_index != 0u) {
const std::uint32_t submitted_vertices = batch.indexed ? batch.index_count : batch.vertex_count;
+549
View File
@@ -0,0 +1,549 @@
#include "savedata_dialog.hpp"
#include "display_window.hpp"
#include <algorithm>
#include <cstdlib>
#include <string>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
namespace vcs {
namespace {
SavedataDialogChoice deterministic_choice(const std::vector<SavedataSlotEntry> &slots,
bool saving,
std::string_view current) {
if (!current.empty()) {
const auto found = std::find_if(slots.begin(), slots.end(), [&](const auto &slot) {
return slot.save_name == current && (saving || slot.exists);
});
if (found != slots.end()) return {true, found->save_name};
}
const auto found = std::find_if(slots.begin(), slots.end(), [&](const auto &slot) {
return saving || slot.exists;
});
if (found != slots.end()) return {true, found->save_name};
return {};
}
#ifdef _WIN32
std::wstring utf8_to_wide(std::string_view text) {
if (text.empty()) return {};
const int needed = MultiByteToWideChar(CP_UTF8, 0, text.data(),
static_cast<int>(text.size()), nullptr, 0);
if (needed <= 0) return std::wstring(text.begin(), text.end());
std::wstring result(static_cast<std::size_t>(needed), L'\0');
MultiByteToWideChar(CP_UTF8, 0, text.data(), static_cast<int>(text.size()),
result.data(), needed);
return result;
}
constexpr int kListId = 1001;
constexpr int kAcceptId = 1002;
constexpr int kCancelId = 1003;
constexpr COLORREF kBgTop = RGB(139, 121, 195);
constexpr COLORREF kBgBottom = RGB(125, 163, 218);
constexpr COLORREF kHorizon = RGB(82, 123, 187);
constexpr COLORREF kPanelFill = RGB(16, 28, 47);
constexpr COLORREF kPanelBorder = RGB(220, 228, 244);
constexpr COLORREF kListFill = RGB(238, 244, 251);
constexpr COLORREF kListEmptyText = RGB(70, 80, 104);
constexpr COLORREF kListSavedText = RGB(20, 41, 76);
constexpr COLORREF kSelection = RGB(44, 126, 218);
constexpr COLORREF kButtonPrimary = RGB(255, 165, 77);
constexpr COLORREF kButtonPrimaryHover = RGB(255, 183, 101);
constexpr COLORREF kButtonSecondary = RGB(226, 231, 241);
constexpr COLORREF kButtonSecondaryHover = RGB(240, 244, 250);
constexpr COLORREF kButtonTextDark = RGB(26, 33, 53);
constexpr COLORREF kButtonTextLight = RGB(255, 255, 255);
constexpr COLORREF kWhite = RGB(255, 255, 255);
constexpr COLORREF kShadow = RGB(28, 33, 56);
struct DialogState {
const std::vector<SavedataSlotEntry> *slots{};
bool saving{};
int selected{-1};
bool done{};
bool confirmed{};
HWND list{};
HWND accept_button{};
HWND cancel_button{};
HFONT title_font{};
HFONT subtitle_font{};
HFONT list_font{};
HFONT button_font{};
HBRUSH list_brush{};
HBRUSH dialog_brush{};
};
std::wstring slot_label(const SavedataSlotEntry &slot, std::size_t index) {
std::wstring line = L"SLOT " + std::to_wstring(index + 1u) + L" " + utf8_to_wide(slot.save_name);
line += slot.exists ? L" SAVED" : L" EMPTY";
return line;
}
void create_dialog_fonts(DialogState &state) {
state.title_font = CreateFontW(-26, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
VARIABLE_PITCH, L"Trebuchet MS");
state.subtitle_font = CreateFontW(-16, 0, 0, 0, FW_SEMIBOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
VARIABLE_PITCH, L"Segoe UI");
state.list_font = CreateFontW(-18, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
VARIABLE_PITCH, L"Trebuchet MS");
state.button_font = CreateFontW(-17, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
VARIABLE_PITCH, L"Segoe UI");
}
void destroy_dialog_resources(DialogState &state) {
if (state.title_font) DeleteObject(state.title_font);
if (state.subtitle_font) DeleteObject(state.subtitle_font);
if (state.list_font) DeleteObject(state.list_font);
if (state.button_font) DeleteObject(state.button_font);
if (state.list_brush) DeleteObject(state.list_brush);
if (state.dialog_brush) DeleteObject(state.dialog_brush);
state.title_font = nullptr;
state.subtitle_font = nullptr;
state.list_font = nullptr;
state.button_font = nullptr;
state.list_brush = nullptr;
state.dialog_brush = nullptr;
}
COLORREF lerp_color(COLORREF a, COLORREF b, float t) {
const auto mix = [&](int x, int y) {
return static_cast<int>(static_cast<float>(x) + (static_cast<float>(y - x) * t));
};
return RGB(mix(GetRValue(a), GetRValue(b)),
mix(GetGValue(a), GetGValue(b)),
mix(GetBValue(a), GetBValue(b)));
}
void fill_vertical_gradient(HDC hdc, const RECT &rect, COLORREF top, COLORREF bottom) {
const int raw_height = rect.bottom - rect.top;
const int height = raw_height > 1 ? raw_height : 1;
for (int y = 0; y < height; ++y) {
const float t = static_cast<float>(y) / static_cast<float>(height - 1 <= 0 ? 1 : height - 1);
const HBRUSH brush = CreateSolidBrush(lerp_color(top, bottom, t));
RECT band{rect.left, rect.top + y, rect.right, rect.top + y + 1};
FillRect(hdc, &band, brush);
DeleteObject(brush);
}
}
void draw_skyline(HDC hdc, const RECT &client) {
const int width = client.right - client.left;
const int height = client.bottom - client.top;
const int horizon_y = client.top + static_cast<int>(height * 0.67f);
RECT horizon{client.left, horizon_y, client.right, client.bottom};
fill_vertical_gradient(hdc, horizon, RGB(64, 111, 183), RGB(106, 174, 225));
HPEN line_pen = CreatePen(PS_SOLID, 1, RGB(91, 143, 202));
HPEN old_pen = static_cast<HPEN>(SelectObject(hdc, line_pen));
MoveToEx(hdc, client.left, horizon_y, nullptr);
LineTo(hdc, client.right, horizon_y);
SelectObject(hdc, old_pen);
DeleteObject(line_pen);
const RECT water_reflection{client.left, horizon_y + 8, client.right, client.bottom};
for (int y = water_reflection.top; y < water_reflection.bottom; y += 4) {
const int alpha_band = (y - water_reflection.top) / 4;
const COLORREF c = alpha_band % 2 == 0 ? RGB(105, 165, 215) : RGB(95, 152, 205);
HBRUSH brush = CreateSolidBrush(c);
const int band_bottom = (y + 2) < water_reflection.bottom ? (y + 2) : water_reflection.bottom;
RECT band{water_reflection.left, y, water_reflection.right, band_bottom};
FillRect(hdc, &band, brush);
DeleteObject(brush);
}
HBRUSH skyline_brush = CreateSolidBrush(kHorizon);
HBRUSH old_brush = static_cast<HBRUSH>(SelectObject(hdc, skyline_brush));
HPEN skyline_pen = CreatePen(PS_SOLID, 1, kHorizon);
old_pen = static_cast<HPEN>(SelectObject(hdc, skyline_pen));
auto tower = [&](int x, int w, int h) {
Rectangle(hdc, x, horizon_y - h, x + w, horizon_y);
};
tower(client.left + width * 6 / 100, width * 4 / 100, height * 9 / 100);
tower(client.left + width * 13 / 100, width * 6 / 100, height * 7 / 100);
tower(client.left + width * 24 / 100, width * 5 / 100, height * 10 / 100);
tower(client.left + width * 32 / 100, width * 8 / 100, height * 11 / 100);
tower(client.left + width * 44 / 100, width * 5 / 100, height * 8 / 100);
tower(client.left + width * 53 / 100, width * 4 / 100, height * 6 / 100);
tower(client.left + width * 66 / 100, width * 6 / 100, height * 13 / 100);
tower(client.left + width * 79 / 100, width * 5 / 100, height * 8 / 100);
tower(client.left + width * 87 / 100, width * 4 / 100, height * 18 / 100);
tower(client.left + width * 91 / 100, width * 3 / 100, height * 12 / 100);
auto palm = [&](int x, int trunk_h, int lean) {
MoveToEx(hdc, x, horizon_y, nullptr);
LineTo(hdc, x + lean, horizon_y - trunk_h);
const int top_x = x + lean;
const int top_y = horizon_y - trunk_h;
MoveToEx(hdc, top_x, top_y, nullptr);
LineTo(hdc, top_x - 16, top_y - 7);
MoveToEx(hdc, top_x, top_y, nullptr);
LineTo(hdc, top_x + 17, top_y - 5);
MoveToEx(hdc, top_x, top_y, nullptr);
LineTo(hdc, top_x - 14, top_y + 6);
MoveToEx(hdc, top_x, top_y, nullptr);
LineTo(hdc, top_x + 13, top_y + 5);
};
palm(client.left + width * 12 / 100, height * 10 / 100, -8);
palm(client.left + width * 20 / 100, height * 12 / 100, 6);
palm(client.left + width * 48 / 100, height * 10 / 100, -4);
palm(client.left + width * 59 / 100, height * 13 / 100, 8);
SelectObject(hdc, old_pen);
SelectObject(hdc, old_brush);
DeleteObject(skyline_pen);
DeleteObject(skyline_brush);
}
void paint_background(HDC hdc, HWND window, const DialogState &state) {
RECT client{};
GetClientRect(window, &client);
fill_vertical_gradient(hdc, client, kBgTop, kBgBottom);
draw_skyline(hdc, client);
RECT shadow{58, 48, client.right - 54, client.bottom - 44};
OffsetRect(&shadow, 4, 5);
HBRUSH shadow_brush = CreateSolidBrush(kShadow);
FillRect(hdc, &shadow, shadow_brush);
DeleteObject(shadow_brush);
RECT panel{56, 44, client.right - 56, client.bottom - 48};
HBRUSH panel_brush = CreateSolidBrush(kPanelFill);
FillRect(hdc, &panel, panel_brush);
DeleteObject(panel_brush);
HPEN border_pen = CreatePen(PS_SOLID, 2, kPanelBorder);
HPEN old_pen = static_cast<HPEN>(SelectObject(hdc, border_pen));
HBRUSH hollow = static_cast<HBRUSH>(GetStockObject(HOLLOW_BRUSH));
HBRUSH old_brush = static_cast<HBRUSH>(SelectObject(hdc, hollow));
Rectangle(hdc, panel.left, panel.top, panel.right, panel.bottom);
SelectObject(hdc, old_pen);
SelectObject(hdc, old_brush);
DeleteObject(border_pen);
SetBkMode(hdc, TRANSPARENT);
RECT title{86, 62, client.right - 86, 96};
HFONT old_font = static_cast<HFONT>(SelectObject(hdc, state.title_font));
SetTextColor(hdc, kWhite);
DrawTextW(hdc, state.saving ? L"SAVE GAME" : L"LOAD GAME", -1, &title,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
RECT subtitle{88, 100, client.right - 88, 126};
SelectObject(hdc, state.subtitle_font);
SetTextColor(hdc, RGB(232, 237, 245));
DrawTextW(hdc,
state.saving ? L"Choose a slot to save your progress." : L"Choose a saved game to continue.",
-1, &subtitle, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
RECT tag{client.right - 230, 70, client.right - 88, 98};
SetTextColor(hdc, RGB(255, 196, 125));
DrawTextW(hdc, L"VICE CITY STORIES", -1, &tag,
DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
RECT hint{88, client.bottom - 92, client.right - 88, client.bottom - 72};
SetTextColor(hdc, RGB(220, 228, 244));
DrawTextW(hdc, L"Double-click a slot or press Confirm.", -1, &hint,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
SelectObject(hdc, old_font);
}
void accept_selection(HWND window, DialogState &state) {
const LRESULT selected = SendMessageW(state.list, LB_GETCURSEL, 0, 0);
if (selected == LB_ERR) return;
const int index = static_cast<int>(selected);
if (index < 0 || static_cast<std::size_t>(index) >= state.slots->size()) return;
if (!state.saving && !(*state.slots)[static_cast<std::size_t>(index)].exists) return;
state.selected = index;
state.confirmed = true;
state.done = true;
DestroyWindow(window);
}
void draw_button(const DRAWITEMSTRUCT &dis, bool primary) {
HDC hdc = dis.hDC;
RECT rc = dis.rcItem;
const bool pressed = (dis.itemState & ODS_SELECTED) != 0;
const bool disabled = (dis.itemState & ODS_DISABLED) != 0;
const COLORREF fill = primary
? (pressed ? kButtonPrimaryHover : kButtonPrimary)
: (pressed ? kButtonSecondaryHover : kButtonSecondary);
const COLORREF text = primary ? kButtonTextDark : kButtonTextDark;
HBRUSH brush = CreateSolidBrush(fill);
HPEN pen = CreatePen(PS_SOLID, 1, primary ? RGB(255, 212, 166) : RGB(212, 220, 234));
HBRUSH old_brush = static_cast<HBRUSH>(SelectObject(hdc, brush));
HPEN old_pen = static_cast<HPEN>(SelectObject(hdc, pen));
RoundRect(hdc, rc.left, rc.top, rc.right, rc.bottom, 10, 10);
SelectObject(hdc, old_brush);
SelectObject(hdc, old_pen);
DeleteObject(brush);
DeleteObject(pen);
SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, disabled ? RGB(132, 138, 151) : text);
wchar_t buffer[64]{};
GetWindowTextW(dis.hwndItem, buffer, 64);
DrawTextW(hdc, buffer, -1, &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
if (dis.itemState & ODS_FOCUS) {
RECT focus = rc;
InflateRect(&focus, -4, -4);
DrawFocusRect(hdc, &focus);
}
}
void draw_list_item(const DRAWITEMSTRUCT &dis, const DialogState &state) {
if (dis.itemID == static_cast<unsigned int>(-1) || !state.slots) return;
const auto &slot = (*state.slots)[dis.itemID];
const bool selected = (dis.itemState & ODS_SELECTED) != 0;
const bool exists = slot.exists;
COLORREF back = exists ? kListFill : RGB(244, 246, 251);
COLORREF fore = exists ? kListSavedText : kListEmptyText;
if (selected) {
back = kSelection;
fore = kWhite;
}
HBRUSH back_brush = CreateSolidBrush(back);
FillRect(dis.hDC, &dis.rcItem, back_brush);
DeleteObject(back_brush);
RECT text_rc = dis.rcItem;
InflateRect(&text_rc, -12, 0);
SetBkMode(dis.hDC, TRANSPARENT);
SetTextColor(dis.hDC, fore);
std::wstring line = slot_label(slot, dis.itemID);
DrawTextW(dis.hDC, line.c_str(), -1, &text_rc, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
if (dis.itemState & ODS_FOCUS) {
RECT focus = dis.rcItem;
InflateRect(&focus, -2, -2);
DrawFocusRect(dis.hDC, &focus);
}
}
LRESULT CALLBACK savedata_window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) {
DialogState *state = reinterpret_cast<DialogState *>(GetWindowLongPtrW(window, GWLP_USERDATA));
if (message == WM_NCCREATE) {
const auto *create = reinterpret_cast<const CREATESTRUCTW *>(lparam);
state = static_cast<DialogState *>(create->lpCreateParams);
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(state));
}
switch (message) {
case WM_CREATE: {
if (!state) return -1;
create_dialog_fonts(*state);
state->list_brush = CreateSolidBrush(kListFill);
state->dialog_brush = CreateSolidBrush(kPanelFill);
state->list = CreateWindowExW(0, L"LISTBOX", L"",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_OWNERDRAWFIXED | LBS_NOINTEGRALHEIGHT,
86, 136, 588, 240, window,
reinterpret_cast<HMENU>(static_cast<INT_PTR>(kListId)), nullptr, nullptr);
if (!state->list) return -1;
SendMessageW(state->list, WM_SETFONT, reinterpret_cast<WPARAM>(state->list_font), TRUE);
for (std::size_t i = 0; i < state->slots->size(); ++i) {
std::wstring line = slot_label((*state->slots)[i], i);
SendMessageW(state->list, LB_ADDSTRING, 0, reinterpret_cast<LPARAM>(line.c_str()));
}
state->accept_button = CreateWindowExW(0, L"BUTTON", state->saving ? L"CONFIRM" : L"LOAD",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_OWNERDRAW,
468, 404, 98, 34, window,
reinterpret_cast<HMENU>(static_cast<INT_PTR>(kAcceptId)), nullptr, nullptr);
state->cancel_button = CreateWindowExW(0, L"BUTTON", L"CANCEL",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_OWNERDRAW,
576, 404, 98, 34, window,
reinterpret_cast<HMENU>(static_cast<INT_PTR>(kCancelId)), nullptr, nullptr);
SendMessageW(state->accept_button, WM_SETFONT, reinterpret_cast<WPARAM>(state->button_font), TRUE);
SendMessageW(state->cancel_button, WM_SETFONT, reinterpret_cast<WPARAM>(state->button_font), TRUE);
return 0;
}
case WM_ERASEBKGND:
return 1;
case WM_PAINT: {
PAINTSTRUCT ps{};
HDC hdc = BeginPaint(window, &ps);
if (hdc && state) paint_background(hdc, window, *state);
EndPaint(window, &ps);
return 0;
}
case WM_MEASUREITEM: {
auto *measure = reinterpret_cast<MEASUREITEMSTRUCT *>(lparam);
if (measure && measure->CtlID == kListId) {
measure->itemHeight = 28;
return TRUE;
}
break;
}
case WM_DRAWITEM: {
auto *draw = reinterpret_cast<DRAWITEMSTRUCT *>(lparam);
if (!draw || !state) break;
if (draw->CtlID == kListId) {
draw_list_item(*draw, *state);
return TRUE;
}
if (draw->CtlID == kAcceptId) {
SelectObject(draw->hDC, state->button_font);
draw_button(*draw, true);
return TRUE;
}
if (draw->CtlID == kCancelId) {
SelectObject(draw->hDC, state->button_font);
draw_button(*draw, false);
return TRUE;
}
break;
}
case WM_CTLCOLORLISTBOX:
if (state && reinterpret_cast<HWND>(lparam) == state->list) {
HDC hdc = reinterpret_cast<HDC>(wparam);
SetBkColor(hdc, kListFill);
SetTextColor(hdc, kListSavedText);
return reinterpret_cast<INT_PTR>(state->list_brush);
}
break;
case WM_COMMAND:
if (!state) break;
if (LOWORD(wparam) == kAcceptId ||
(LOWORD(wparam) == kListId && HIWORD(wparam) == LBN_DBLCLK)) {
accept_selection(window, *state);
return 0;
}
if (LOWORD(wparam) == kCancelId) {
state->done = true;
state->confirmed = false;
DestroyWindow(window);
return 0;
}
break;
case WM_KEYDOWN:
if (state && wparam == VK_RETURN) {
accept_selection(window, *state);
return 0;
}
if (state && wparam == VK_ESCAPE) {
state->done = true;
state->confirmed = false;
DestroyWindow(window);
return 0;
}
break;
case WM_CLOSE:
if (state) {
state->done = true;
state->confirmed = false;
}
DestroyWindow(window);
return 0;
case WM_DESTROY:
if (state) {
state->done = true;
destroy_dialog_resources(*state);
}
return 0;
}
return DefWindowProcW(window, message, wparam, lparam);
}
SavedataDialogChoice native_choice(const std::vector<SavedataSlotEntry> &slots,
bool saving,
std::string_view current) {
static const wchar_t *kClassName = L"VCSNativeSavedataSlotDialog";
static bool registered = false;
HINSTANCE instance = GetModuleHandleW(nullptr);
if (!registered) {
WNDCLASSW cls{};
cls.lpfnWndProc = savedata_window_proc;
cls.hInstance = instance;
cls.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512));
cls.hbrBackground = reinterpret_cast<HBRUSH>(static_cast<INT_PTR>(COLOR_WINDOW + 1));
cls.lpszClassName = kClassName;
registered = RegisterClassW(&cls) != 0 || GetLastError() == ERROR_CLASS_ALREADY_EXISTS;
}
if (!registered) return deterministic_choice(slots, saving, current);
DialogState state{&slots, saving};
HWND owner = static_cast<HWND>(display_window_surface().window);
HWND window = CreateWindowExW(WS_EX_DLGMODALFRAME | WS_EX_TOPMOST,
kClassName, saving ? L"GTA Vice City Stories - Save Game" : L"GTA Vice City Stories - Load Game",
WS_CAPTION | WS_SYSMENU,
CW_USEDEFAULT, CW_USEDEFAULT, 760, 500,
owner, nullptr, instance, &state);
if (!window) return deterministic_choice(slots, saving, current);
int initial = -1;
for (std::size_t i = 0; i < slots.size(); ++i) {
if (slots[i].save_name == current && (saving || slots[i].exists)) {
initial = static_cast<int>(i);
break;
}
}
if (initial < 0) {
for (std::size_t i = 0; i < slots.size(); ++i) {
if (saving || slots[i].exists) { initial = static_cast<int>(i); break; }
}
}
if (initial >= 0) {
SendMessageW(state.list, LB_SETCURSEL, static_cast<WPARAM>(initial), 0);
SetFocus(state.list);
}
RECT rect{};
GetWindowRect(window, &rect);
const int width = rect.right - rect.left;
const int height = rect.bottom - rect.top;
const int screen_w = GetSystemMetrics(SM_CXSCREEN);
const int screen_h = GetSystemMetrics(SM_CYSCREEN);
SetWindowPos(window, HWND_TOPMOST, (screen_w - width) / 2, (screen_h - height) / 2,
0, 0, SWP_NOSIZE | SWP_SHOWWINDOW);
MSG message{};
while (!state.done && GetMessageW(&message, nullptr, 0, 0) > 0) {
if (!IsDialogMessageW(window, &message)) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
}
if (state.confirmed && state.selected >= 0 &&
static_cast<std::size_t>(state.selected) < slots.size()) {
return {true, slots[static_cast<std::size_t>(state.selected)].save_name};
}
return {};
}
#endif
} // namespace
SavedataDialogChoice choose_savedata_slot(const std::vector<SavedataSlotEntry> &slots,
bool saving,
std::string_view current_save_name) {
if (slots.empty()) return {};
const char *disable = std::getenv("PSPRECOMP_SAVEDATA_NATIVE_DIALOG");
const bool native_enabled = disable == nullptr ||
(*disable != '\0' && std::string_view(disable) != "0" &&
std::string_view(disable) != "false" && std::string_view(disable) != "off");
#ifdef _WIN32
if (native_enabled) return native_choice(slots, saving, current_save_name);
#else
(void)native_enabled;
#endif
return deterministic_choice(slots, saving, current_save_name);
}
} // namespace vcs
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <string>
#include <string_view>
#include <vector>
namespace vcs {
struct SavedataSlotEntry {
std::string save_name;
bool exists{};
};
struct SavedataDialogChoice {
bool confirmed{};
std::string save_name;
};
// Native replacement for the PSP LISTLOAD/LISTSAVE system utility screen.
// On Windows a small host dialog is shown. On non-Windows/headless validation
// the choice is deterministic so automated tests never block on UI.
[[nodiscard]] SavedataDialogChoice choose_savedata_slot(
const std::vector<SavedataSlotEntry> &slots,
bool saving,
std::string_view current_save_name);
} // namespace vcs
+71 -138
View File
@@ -25,6 +25,7 @@
// 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
@@ -43,6 +44,9 @@
#include <unordered_map>
namespace vcs {
DrawDistanceRuntimeScales g_draw_distance_runtime_scales{};
namespace {
struct DrawDistanceConfig {
@@ -55,24 +59,12 @@ struct DrawDistanceConfig {
DrawDistanceConfig g_config{};
// ULUS-10160 / VCSNative AOT guest addresses.
constexpr std::uint32_t kFarClipSetter = 0x08A1AD6Cu;
constexpr std::uint32_t kEntityLodSetup = 0x08A24128u;
constexpr std::uint32_t kEntityLodSetupContinue = 0x08A24138u;
constexpr std::uint32_t kNpcRangeSetup = 0x089CB38Cu;
constexpr std::uint32_t kNpcRangeContinue = 0x089CB3C8u;
constexpr std::uint32_t kVehicleRangeSetup = 0x08B45AC0u;
constexpr std::uint32_t kVehicleRangeContinue = 0x08B45AC8u;
constexpr std::uint32_t kIdeInitEpilogue = 0x08AEC918u;
// 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
// Runtime entity fields used by the original VCS WidescreenFix LOD patch.
constexpr std::uint32_t kEntityLodDistance = 0x7A0u;
constexpr std::uint32_t kEntityBaseLodDistance = 0x7A8u;
// VCS CBaseModelInfo-like layout exposed by the IDE table.
constexpr std::uint32_t kModelHashOffset = 0x08u;
constexpr std::uint32_t kModelTypeOffset = 0x10u;
@@ -91,6 +83,9 @@ struct OriginalWorldModel {
};
std::unordered_map<std::uint32_t, OriginalWorldModel> g_original_world_models;
bool g_far_clip_seen{};
float g_far_clip_last_raw{};
float g_far_clip_last_scaled{};
std::string trim_copy(std::string value) {
auto is_space = [](unsigned char c) { return std::isspace(c) != 0; };
@@ -215,10 +210,9 @@ float scaled_or_original(float original, float multiplier) {
// 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, const psprecomp::AllegrexContext &ctx) {
if (g_config.world <= 1.0f) return false;
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 gp = ctx.gpr[28];
const std::uint32_t count = runtime.memory().load32(gp + kGpIdeCountOffset);
const std::uint32_t table = runtime.memory().load32(gp + kGpIdeTableOffset);
@@ -318,147 +312,47 @@ bool patch_world_model_table(psprecomp::Runtime &runtime, const psprecomp::Alleg
return patched != 0u;
}
// CDraw::SetFarClipZ(float): keep the game's dynamic far-clip selection, but scale
// every value written through the real setter. This avoids a static far-clip hack.
void far_clip_setter_patch(psprecomp::Runtime &runtime, psprecomp::AllegrexContext &ctx) {
const float far_clip = ctx.fpr[12] * g_config.world;
store_float(runtime, ctx.gpr[28] + kGpFarClipOffset, far_clip);
// The model-info table is patched from here, not from the IDE epilogue.
//
// 0x08AEC918 IS a registered AOT entry point -- has_function() confirms it --
// but it is never dispatched: control reaches it as a local label inside its
// own generated unit, and a `goto L_xxxx` does not consult the runtime's
// function table, so the replacement is bypassed. Same mechanism the fast
// path at 0x088B1554 documents. Measured: the hook installs, reports OK, and
// its body never runs.
//
// SetFarClipZ runs every frame and already carries gp in $28, which is all
// patch_world_model_table needs. Retry until the table is populated -- at the
// first calls the globals are still zero -- then stop, and refresh
// periodically so a streamed-in reload does not stay unpatched.
static std::uint64_t calls = 0u;
static bool table_done = false;
++calls;
if (!table_done || (calls % 600u) == 0u) {
if (patch_world_model_table(runtime, ctx)) table_done = true;
}
ctx.pc = ctx.gpr[31];
}
// Equivalent to ThirteenAG's VCS WidescreenFix entity LOD hook, adapted to the
// static AOT recomp. The original PSP patch uses one LOD multiplier for cars+peds;
// use max(Vehicles,NPCs) here, while their actual despawn/culling ranges remain
// independently controlled below.
void entity_lod_setup_patch(psprecomp::Runtime &runtime, psprecomp::AllegrexContext &ctx) {
const float multiplier = std::max(g_config.vehicles, g_config.npcs);
const std::uint32_t entity = ctx.gpr[16]; // s0 in this VCS call site
if (entity != 0u && runtime.memory().contains(entity + kEntityLodDistance, 12u)) {
const float base = load_float(runtime, entity + kEntityBaseLodDistance);
store_float(runtime, entity + kEntityLodDistance, base * multiplier);
}
ctx.fpr[0] = multiplier; // helper's return value; next block scales +0x7A8
ctx.set_gpr(31, kEntityLodSetupContinue); // preserve JAL-visible RA semantics
ctx.pc = kEntityLodSetupContinue;
}
// VCS vehicle off-screen despawn/culling constant: original 60.0f.
void vehicle_range_patch(psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) {
ctx.fpr[12] = 60.0f * g_config.vehicles;
ctx.pc = kVehicleRangeContinue;
}
// VCS population/ped range block. Re-emulates the whole original AOT label,
// changing only 51/25/80; the original 120 constant and integer setup are preserved.
void npc_range_patch(psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) {
ctx.set_gpr(19, ctx.gpr[29] + 64u);
ctx.set_gpr(30, ctx.gpr[29] + 16u);
ctx.set_gpr(23, ctx.gpr[29] + 32u);
ctx.fpr[22] = 120.0f;
ctx.fpr[28] = 51.0f * g_config.npcs;
ctx.fpr[26] = 25.0f * g_config.npcs;
ctx.fpr[24] = 80.0f * g_config.npcs;
ctx.set_gpr(4, ctx.gpr[18] << 5u);
ctx.set_gpr(20, ctx.gpr[4]);
ctx.set_gpr(4, ctx.gpr[4] << 4u);
ctx.set_gpr(20, ctx.gpr[20] + ctx.gpr[4]);
ctx.pc = kNpcRangeContinue;
}
// Runs at the real IDE initialization epilogue, once the game's pointer table has
// been installed. Patch only map objects (OBJ=1, TOBJ=3), not vehicle/ped model-info.
void ide_init_epilogue_patch(psprecomp::Runtime &runtime, psprecomp::AllegrexContext &ctx) {
patch_world_model_table(runtime, ctx);
// Exact original epilogue for 0x08AEC918.
ctx.set_gpr(16, runtime.memory().load32(ctx.gpr[29] + 0u));
ctx.set_gpr(17, runtime.memory().load32(ctx.gpr[29] + 4u));
ctx.set_gpr(18, runtime.memory().load32(ctx.gpr[29] + 8u));
ctx.set_gpr(31, runtime.memory().load32(ctx.gpr[29] + 12u));
const std::uint32_t return_pc = ctx.gpr[31];
ctx.set_gpr(29, ctx.gpr[29] + 16u);
ctx.pc = return_pc;
}
} // 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_far_clip_seen = false;
g_far_clip_last_raw = 0.0f;
g_far_clip_last_scaled = 0.0f;
g_draw_distance_runtime_scales = {};
if (g_config.enabled) {
g_draw_distance_runtime_scales.entity = std::max(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;
}
// These replacements are intentionally registered AFTER generated functions.
// Runtime::register_function overwrites the existing address in both the hash
// registry and direct dispatch table.
//
// But it only has any effect when the address already IS an AOT entry point.
// Registering an address that lands mid-block succeeds silently and is never
// dispatched -- vcs_project2dfx.cpp hit exactly this with the heli-height
// kit's continuation address. The header of this file says it targets
// "Stage 40 / load base 0x08804000", i.e. a different recompilation, so every
// address here is a candidate. Report which ones are live instead of leaving
// a dead hook looking installed.
std::uint32_t live = 0u;
std::uint32_t dead = 0u;
const auto hook = [&](std::uint32_t address, psprecomp::Runtime::RecompiledFunction function,
std::string name, const char *what) {
const bool exists = runtime.has_function(address);
if (exists) {
runtime.register_function(address, function, std::move(name));
++live;
} else {
++dead;
}
std::cerr << "[draw-distance] hook " << what << " em " << psprecomp::hex32(address)
<< (exists ? " OK" : " MORTO (nao e ponto de entrada AOT nesta recompilacao)")
<< "\n";
};
// World is maintained at vblank. 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) {
hook(kFarClipSetter, &far_clip_setter_patch, "vcs_draw_distance_far_clip", "far_clip");
hook(kIdeInitEpilogue, &ide_init_epilogue_patch, "vcs_draw_distance_world_ide", "ide_init");
// World distance is maintained from the display-vblank boundary. Both
// historical entry hooks can be reached as local labels inside an AOT
// unit and therefore bypass Runtime::register_function entirely. Keeping
// them registered also poisons direct chaining for no reliable benefit.
std::cerr << "[draw-distance] world path=vblank (no fragile far_clip/IDE AOT hooks)\n";
}
if (g_config.vehicles > 1.0f || g_config.npcs > 1.0f) {
hook(kEntityLodSetup, &entity_lod_setup_patch, "vcs_draw_distance_entity_lod", "entity_lod");
// 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/vehicle/NPC path=generated-local-label" << "\n";
}
if (g_config.vehicles > 1.0f) {
hook(kVehicleRangeSetup, &vehicle_range_patch, "vcs_draw_distance_vehicle_range", "vehicle_range");
}
if (g_config.npcs > 1.0f) {
hook(kNpcRangeSetup, &npc_range_patch, "vcs_draw_distance_npc_range", "npc_range");
}
std::cerr << "[draw-distance] hooks vivos=" << live << " mortos=" << dead << "\n";
std::cerr << "[draw-distance] enabled"
<< " world=" << g_config.world
<< " vehicles=" << g_config.vehicles
@@ -467,4 +361,43 @@ void install_draw_distance_patch(psprecomp::Runtime &runtime,
<< "\n";
}
void draw_distance_vblank_tick(psprecomp::Runtime &runtime,
std::uint32_t guest_gp,
std::uint64_t vblank_index) noexcept {
if (!g_config.enabled || g_config.world <= 1.0f || guest_gp == 0u) return;
try {
const std::uint32_t far_address = guest_gp + kGpFarClipOffset;
if (runtime.memory().contains(far_address, sizeof(std::uint32_t))) {
const float current = load_float(runtime, far_address);
if (std::isfinite(current) && current > 0.0f) {
// Never multiply our own value again. If the game writes a new
// dynamic far clip (weather/interior/etc.), treat that new value
// as the next raw baseline and scale it exactly once.
if (!g_far_clip_seen ||
(!nearly_equal(current, g_far_clip_last_scaled) &&
!nearly_equal(current, g_far_clip_last_raw))) {
g_far_clip_seen = true;
g_far_clip_last_raw = current;
g_far_clip_last_scaled = current * g_config.world;
}
if (g_far_clip_seen && !nearly_equal(current, g_far_clip_last_scaled))
store_float(runtime, far_address, g_far_clip_last_scaled);
}
}
// The model-info table can appear after the first gameplay vblanks and
// can be repopulated by streaming. Retry aggressively at startup, then
// refresh twice a second-ish without touching generated guest code.
if (vblank_index < 240u || (vblank_index % 120u) == 0u)
(void)patch_world_model_table(runtime, guest_gp);
} 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";
}
}
}
} // namespace vcs
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include <filesystem>
namespace psprecomp { class Runtime; }
namespace vcs {
struct DrawDistanceRuntimeScales {
float entity{1.0f};
float vehicles{1.0f};
float npcs{1.0f};
};
// Read-only after profile installation. Generated AOT code reads these three
// values directly at the exact local labels that the old Runtime hook could
// not intercept.
extern DrawDistanceRuntimeScales g_draw_distance_runtime_scales;
void install_draw_distance_patch(psprecomp::Runtime &runtime,
const std::filesystem::path &ini_path);
// Authoritative world-distance maintenance point. Called once per display
// vblank so it cannot be bypassed by generated AOT local gotos.
void draw_distance_vblank_tick(psprecomp::Runtime &runtime,
std::uint32_t guest_gp,
std::uint64_t vblank_index) noexcept;
} // namespace vcs
+170 -3
View File
@@ -10,6 +10,8 @@
#include "ge_renderer.hpp"
#include "ge_gpu_backend.hpp"
#include "vcs_project2dfx.hpp"
#include "vcs_draw_distance_patch.hpp"
#include "savedata_dialog.hpp"
#include "psprecomp/common.hpp"
#include "psprecomp/deflate.hpp"
@@ -1747,7 +1749,16 @@ std::uint32_t sas_step_envelope(SasVoiceState &voice) noexcept {
}
break;
case SasEnvelopePhase::Release:
height = sas_walk_envelope_curve(height, voice.adsr_modes[3], voice.adsr_rates[3]);
// A zero release rate is legal in the compact PSP ADSR encoding, but
// treating it as an actual delta of zero makes a KeyOff voice immortal.
// That is catastrophic for looped vehicle/horn VAGs: the source keeps
// wrapping forever after the game has explicitly keyed it off. Hardware
// still reaches the off state; use the same short de-click ramp we use
// for an unconfigured envelope when the decoded release cannot advance.
if (voice.adsr_rates[3] <= 0)
height -= kSasFallbackReleaseStep;
else
height = sas_walk_envelope_curve(height, voice.adsr_modes[3], voice.adsr_rates[3]);
if (height <= 0) {
height = 0;
voice.envelope_phase = SasEnvelopePhase::Off;
@@ -2161,6 +2172,7 @@ struct SavedataUtilityState {
UtilityStatus status{UtilityStatus::None};
std::uint32_t parameter_address{};
bool operation_complete{};
bool slot_selection_complete{};
};
SavedataUtilityState savedata_utility{};
@@ -2169,6 +2181,7 @@ constexpr std::uint32_t kUtilityCommonResultOffset = 0x1Cu;
constexpr std::uint32_t kSavedataModeOffset = 0x30u;
constexpr std::uint32_t kSavedataGameNameOffset = 0x3Cu;
constexpr std::uint32_t kSavedataSaveNameOffset = 0x4Cu;
constexpr std::uint32_t kSavedataSaveNameListOffset = 0x60u;
constexpr std::uint32_t kSavedataFileNameOffset = 0x64u;
constexpr std::uint32_t kSavedataDataBufferOffset = 0x74u;
constexpr std::uint32_t kSavedataDataBufferSizeOffset = 0x78u;
@@ -2177,6 +2190,7 @@ constexpr std::uint32_t kSavedataIcon0Offset = 0x584u;
constexpr std::uint32_t kSavedataIcon1Offset = 0x594u;
constexpr std::uint32_t kSavedataPic1Offset = 0x5A4u;
constexpr std::uint32_t kSavedataSnd0Offset = 0x5B4u;
constexpr std::uint32_t kSavedataAbortStatusOffset = 0x5CCu;
constexpr std::uint32_t kSavedataIdListOffset = 0x5F4u;
constexpr std::uint32_t kSavedataFileListOffset = 0x5F8u;
constexpr std::uint32_t kSavedataSizeInfoOffset = 0x5FCu;
@@ -2220,6 +2234,95 @@ std::filesystem::path savedata_directory(const psprecomp::Runtime &runtime, std:
return savedata_root(runtime) / (game + save);
}
void write_fixed_string(psprecomp::GuestMemory &memory, std::uint32_t address,
std::size_t capacity, std::string_view value) {
if (capacity == 0u) return;
memory.zero(address, capacity);
const std::size_t count = std::min<std::size_t>(capacity - 1u, value.size());
for (std::size_t i = 0; i < count; ++i)
memory.store8(address + static_cast<std::uint32_t>(i),
static_cast<std::uint8_t>(value[i]));
}
std::vector<SavedataSlotEntry> savedata_slot_entries(psprecomp::Runtime &runtime,
std::uint32_t parameter_address,
bool saving) {
std::vector<SavedataSlotEntry> slots;
const std::string game = safe_savedata_component(read_fixed_string(
runtime.memory(), parameter_address + kSavedataGameNameOffset, 13u));
const auto root = savedata_root(runtime);
const std::uint32_t list = runtime.memory().load32(parameter_address + kSavedataSaveNameListOffset);
if (list != 0u) {
// SceUtilitySavedataParam::saveNameList is char (*)[20]. The list is
// terminated by an empty entry. Keep a hard cap so malformed guest data
// cannot turn a host system dialog into an unbounded memory walk.
for (std::uint32_t i = 0u; i < 128u; ++i) {
const std::uint32_t entry = list + i * 20u;
if (!runtime.memory().contains(entry, 20u)) break;
std::string name = safe_savedata_component(read_fixed_string(runtime.memory(), entry, 20u));
if (name.empty()) break;
const bool exists = std::filesystem::is_directory(root / (game + name));
if (saving || exists) slots.push_back({std::move(name), exists});
}
}
// Some titles leave saveNameList null and rely on the utility to discover
// matching directories. Reconstruct that list from our host SAVEDATA root.
if (slots.empty()) {
std::error_code error;
if (std::filesystem::is_directory(root, error)) {
for (std::filesystem::directory_iterator it(root, error), end;
it != end && !error; it.increment(error)) {
if (!it->is_directory(error)) continue;
const std::string full = it->path().filename().string();
if (full.size() < game.size() || full.compare(0u, game.size(), game) != 0) continue;
std::string suffix = full.substr(game.size());
if (suffix.empty() || suffix.size() >= 20u) continue;
slots.push_back({std::move(suffix), true});
}
std::sort(slots.begin(), slots.end(), [](const auto &a, const auto &b) {
return a.save_name < b.save_name;
});
}
}
const std::string current = safe_savedata_component(read_fixed_string(
runtime.memory(), parameter_address + kSavedataSaveNameOffset, 20u));
if (!current.empty() && std::none_of(slots.begin(), slots.end(), [&](const auto &slot) {
return slot.save_name == current;
})) {
const bool exists = std::filesystem::is_directory(root / (game + current));
if (saving || exists) slots.push_back({current, exists});
}
return slots;
}
enum class SavedataSlotPreparation { Ready, Cancelled, NoSlots };
SavedataSlotPreparation prepare_savedata_list_selection(psprecomp::Runtime &runtime,
std::uint32_t parameter_address,
std::uint32_t mode) {
if (mode != 4u && mode != 5u && mode != 6u) return SavedataSlotPreparation::Ready;
const bool saving = mode == 5u;
auto slots = savedata_slot_entries(runtime, parameter_address, saving);
if (slots.empty()) return SavedataSlotPreparation::NoSlots;
const std::string current = safe_savedata_component(read_fixed_string(
runtime.memory(), parameter_address + kSavedataSaveNameOffset, 20u));
const SavedataDialogChoice choice = choose_savedata_slot(slots, saving, current);
if (!choice.confirmed) {
// PSP exposes cancellation separately from base.result for the savedata
// list utility. Leave base.result successful and flag abortStatus so the
// guest can return from its fade/system-dialog state normally.
runtime.memory().store32(parameter_address + kSavedataAbortStatusOffset, 1u);
return SavedataSlotPreparation::Cancelled;
}
runtime.memory().store32(parameter_address + kSavedataAbortStatusOffset, 0u);
write_fixed_string(runtime.memory(), parameter_address + kSavedataSaveNameOffset,
20u, choice.save_name);
return SavedataSlotPreparation::Ready;
}
bool write_guest_file(psprecomp::Runtime &runtime, const std::filesystem::path &path,
std::uint32_t buffer, std::uint32_t size) {
if (size == 0u) return true;
@@ -2411,6 +2514,7 @@ std::uint32_t execute_savedata_operation(psprecomp::Runtime &runtime, std::uint3
case 3u: // SAVE
case 5u: // LISTSAVE
return save_savedata_file(runtime, parameter_address, data_path, false);
case 6u: // LISTDELETE (slot was selected by the utility UI)
case 9u: // AUTODELETE
case 10u: // DELETE
if (!std::filesystem::exists(directory)) return 0x80110347u;
@@ -2447,7 +2551,8 @@ std::uint64_t system_time_microseconds() {
std::uint32_t audio_remaining_samples(const AudioChannelState &channel) {
if (!channel.reserved || channel.busy_until_us <= virtual_time_us) return 0u;
const std::uint64_t remaining_us = channel.busy_until_us - virtual_time_us;
const std::uint64_t samples = (remaining_us * 44100u + 999999u) / 1000000u;
const std::uint64_t rate = channel.frequency == 0u ? 44100u : channel.frequency;
const std::uint64_t samples = (remaining_us * rate + 999999u) / 1000000u;
return static_cast<std::uint32_t>(std::min<std::uint64_t>(samples, channel.sample_count));
}
@@ -7046,6 +7151,9 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
ge_gpu_backend_set_display_framebuffer(display_state.frame_buffer);
project2dfx_render_frame(
rt.memory(), ctx.gpr[28], display_vblank_index, display_state.frame_buffer);
// Draw-distance world/far-clip maintenance is tied to a real vblank
// rather than fragile AOT entry hooks, which local generated gotos can bypass.
draw_distance_vblank_tick(rt, ctx.gpr[28], display_vblank_index);
// A movie frame is a finished 480x272 picture with no more image at the
// sides, so widening it can only stretch it. Present it black-barred at
// its own shape instead; gameplay keeps the widescreen treatment.
@@ -7105,6 +7213,10 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
if (frame_time_diag_enabled())
frame_time_stats.present_time += std::chrono::steady_clock::now() - present_entry;
limit_frame_rate();
// limit_frame_rate() may advance virtual_time_us when the host misses the
// target. Seal the audio timeline immediately at that corrected guest
// time instead of leaving waveOut one vblank behind during heavy frames.
vcs::audio_output_advance(virtual_time_us);
if (display_window_close_requested()) {
ctx.set_gpr(2, 0u);
rt.stop("Display window closed by the user");
@@ -7176,7 +7288,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
ctx.set_gpr(2, 0x80110004u);
return;
}
savedata_utility = SavedataUtilityState{UtilityStatus::Init, parameter, false};
savedata_utility = SavedataUtilityState{UtilityStatus::Init, parameter, false, false};
rt.memory().store32(parameter + kUtilityCommonResultOffset, 0u);
if (std::getenv("PSPRECOMP_TRACE") != nullptr) {
std::cerr << "[hle] savedata init mode=" << rt.memory().load32(parameter + kSavedataModeOffset)
@@ -7196,6 +7308,21 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
if (savedata_utility.status == UtilityStatus::Init) {
savedata_utility.status = UtilityStatus::Visible;
} else if (savedata_utility.status == UtilityStatus::Visible && !savedata_utility.operation_complete) {
const std::uint32_t mode = rt.memory().load32(
savedata_utility.parameter_address + kSavedataModeOffset);
if (!savedata_utility.slot_selection_complete) {
const SavedataSlotPreparation selection = prepare_savedata_list_selection(
rt, savedata_utility.parameter_address, mode);
savedata_utility.slot_selection_complete = true;
if (selection == SavedataSlotPreparation::Cancelled) {
rt.memory().store32(savedata_utility.parameter_address +
kUtilityCommonResultOffset, 0u);
savedata_utility.operation_complete = true;
savedata_utility.status = UtilityStatus::Quit;
set_success(ctx);
return;
}
}
const std::uint32_t result = execute_savedata_operation(rt, savedata_utility.parameter_address);
rt.memory().store32(savedata_utility.parameter_address + kUtilityCommonResultOffset, result);
savedata_utility.operation_complete = true;
@@ -9638,6 +9765,26 @@ bool run_profile_self_tests(std::string &error) {
virtual_time_us = previous_time;
}
// sceAudioGetChannelRestLength reports samples in the channel's source
// rate. Returning a 44.1-kHz count for a 22.05/24/32-kHz SRC channel
// makes the guest believe much more audio remains than the DAC will
// actually consume, which eventually stretches low-rate radio/news.
{
const std::uint64_t previous_time = virtual_time_us;
virtual_time_us = 2'000'000u;
AudioChannelState channel{};
channel.reserved = true;
channel.sample_count = 4096u;
channel.frequency = 22050u;
channel.busy_until_us = virtual_time_us + 10'000u;
require(audio_remaining_samples(channel) == 221u,
"audio remaining length ignored the SRC channel frequency");
channel.frequency = 24000u;
require(audio_remaining_samples(channel) == 240u,
"audio remaining length drifted for a 24-kHz channel");
virtual_time_us = previous_time;
}
// A voice configured through __sceSasSetADSR alone -- rates only, no
// call to __sceSasSetADSRmode -- must still retire when the game keys
// it off. VCS does exactly this for the vehicle engine, and the old
@@ -9668,6 +9815,26 @@ bool run_profile_self_tests(std::string &error) {
"a released voice was retired with a non-zero envelope");
}
// A zero decoded release rate is not allowed to make a looped VAG
// immortal after KeyOff. Vehicle engine and horn voices are exactly the
// kind of long/looping effects where this turns into an obvious stuck
// sound, so use the short de-click fallback release in that case.
{
SasVoiceState voice{};
voice.type = SasVoiceType::Vag;
voice.adsr_configured = true;
voice.loop = true;
voice.playing = true;
voice.on = false;
voice.envelope_height = kSasEnvelopeMaximum;
voice.envelope_phase = SasEnvelopePhase::Release;
voice.adsr_rates[3] = 0;
for (std::uint32_t step = 0u; step < 64u && voice.playing; ++step)
sas_step_envelope(voice);
require(!voice.playing && voice.envelope_height == 0u,
"zero-rate KeyOff left a looping SAS voice alive forever");
}
// A GE context supplied to sceGeListEnQueue is a real serialized PSP
// context, not merely a command-memory snapshot. It must include matrix
// DATA words and the global renderer state must be restored after END.
+1 -1
View File
@@ -1,5 +1,6 @@
#include "vcs_project2dfx.hpp"
#include "vcs_project2dfx_lights.hpp"
#include "vcs_draw_distance_patch.hpp"
#include "ge_gpu_backend.hpp"
#include "psprecomp/runtime.hpp"
@@ -25,7 +26,6 @@
#include <vector>
namespace vcs {
void install_draw_distance_patch(psprecomp::Runtime &, const std::filesystem::path &);
namespace {
constexpr float kPi = 3.14159265358979323846f;
@@ -0,0 +1,243 @@
@echo off
setlocal EnableExtensions EnableDelayedExpansion
for %%I in ("%~dp0..\..\..") do set "REPO=%%~fI"
set "PROFILE=%REPO%\profiles\vcs"
set "BUILD=%REPO%\out\vcs-release-ninja"
if not exist "%REPO%\CMakeLists.txt" (
echo ERROR: PSPRecomp root was not resolved correctly:
echo %REPO%
pause
exit /b 20
)
if not exist "%PROFILE%\CMakeLists.txt" (
echo ERROR: VCS profile was not found:
echo %PROFILE%
pause
exit /b 21
)
rem ---------------------------------------------------------------------------
rem CRITICAL: ZIPs created in a UTC environment can extract on a UTC-3 Windows
rem machine with source mtimes several hours in the future. CMake/Ninja then
rem loops forever regenerating build.ninja because an input always remains newer
rem than the freshly written manifest.
rem
rem Clamp ONLY source files whose timestamp is actually > current local time.
rem Existing normal files and all out/build artifacts remain untouched.
rem ---------------------------------------------------------------------------
echo [0/7] Checking for future-dated source files...
call "%PROFILE%\FIX_FUTURE_TIMESTAMPS.bat"
if errorlevel 1 goto :FAIL
call "%PROFILE%\scripts\pick_jobs.bat"
if errorlevel 1 goto :FAIL
if defined PSPRECOMP_NINJA_JOBS set "JOBS=%PSPRECOMP_NINJA_JOBS%"
if not defined JOBS set "JOBS=2"
set "VSROOT="
if exist "%ProgramFiles%\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat" (
set "VSROOT=%ProgramFiles%\Microsoft Visual Studio\2022\Community"
)
if not defined VSROOT if exist "%ProgramFiles%\Microsoft Visual Studio\2022\Professional\Common7\Tools\VsDevCmd.bat" (
set "VSROOT=%ProgramFiles%\Microsoft Visual Studio\2022\Professional"
)
if not defined VSROOT if exist "%ProgramFiles%\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" (
set "VSROOT=%ProgramFiles%\Microsoft Visual Studio\2022\Enterprise"
)
if not defined VSROOT if exist "%ProgramFiles%\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\VsDevCmd.bat" (
set "VSROOT=%ProgramFiles%\Microsoft Visual Studio\2022\BuildTools"
)
if not defined VSROOT (
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if not exist "!VSWHERE!" set "VSWHERE=%ProgramFiles%\Microsoft Visual Studio\Installer\vswhere.exe"
if exist "!VSWHERE!" (
set "VSWHERE_OUT=%TEMP%\psprecomp_vswhere_%RANDOM%_%RANDOM%.txt"
"!VSWHERE!" -latest -version "[17.0,18.0)" -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath > "!VSWHERE_OUT!" 2>nul
if exist "!VSWHERE_OUT!" (
set /p VSROOT=<"!VSWHERE_OUT!"
del /q "!VSWHERE_OUT!" >nul 2>nul
)
)
)
if not defined VSROOT goto :NO_VS
if not exist "%VSROOT%\Common7\Tools\VsDevCmd.bat" goto :NO_VS
echo Initializing VS2022 x64 environment...
echo VS root: %VSROOT%
call "%VSROOT%\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64 >nul
if errorlevel 1 goto :VS_ENV_FAIL
where cl.exe >nul 2>nul
if errorlevel 1 goto :NO_CL
set "CMAKE_EXE=%VSROOT%\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
if not exist "%CMAKE_EXE%" set "CMAKE_EXE="
if not defined CMAKE_EXE (
for /f "delims=" %%I in ('where cmake.exe 2^>nul') do if not defined CMAKE_EXE set "CMAKE_EXE=%%~fI"
)
if not defined CMAKE_EXE if exist "%ProgramFiles%\CMake\bin\cmake.exe" set "CMAKE_EXE=%ProgramFiles%\CMake\bin\cmake.exe"
if not defined CMAKE_EXE goto :NO_CMAKE
set "NINJA_EXE=%VSROOT%\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja\ninja.exe"
if not exist "%NINJA_EXE%" set "NINJA_EXE="
if not defined NINJA_EXE (
for /f "delims=" %%I in ('where ninja.exe 2^>nul') do if not defined NINJA_EXE set "NINJA_EXE=%%~fI"
)
if not defined NINJA_EXE goto :NO_NINJA
for %%I in ("%CMAKE_EXE%") do set "CTEST_EXE=%%~dpIctest.exe"
if not exist "%CTEST_EXE%" set "CTEST_EXE=ctest.exe"
set "NINJA_STATUS=[%%f/%%t %%p ^| %%e elapsed ^| %%r running] "
echo ================================================================
echo VCS - NINJA PERFORMANCE INCREMENTAL BUILD
echo.
echo Repository: %REPO%
echo Build tree: %BUILD%
echo Visual Studio: %VSROOT%
echo CMake: %CMAKE_EXE%
echo Ninja: %NINJA_EXE%
echo Ninja workers: %JOBS%
echo cl.exe /MP: OFF ^(Ninja owns compile parallelism^)
echo Generated AOT: O3, cold /Ob0, measured hot /Ob3, /GL-
echo Host/core LTCG: ON
echo AVX2/fast paths: ON
echo ================================================================
echo.
echo [1/7] Configuring persistent Ninja Release tree...
"%CMAKE_EXE%" -S "%REPO%" -B "%BUILD%" -G Ninja ^
"-DCMAKE_MAKE_PROGRAM=%NINJA_EXE%" ^
-DCMAKE_BUILD_TYPE=Release ^
-DPSPRECOMP_PROFILE=vcs ^
-DPSPRECOMP_GENERATED_OPT_LEVEL=3 ^
-DPSPRECOMP_LTO=ON ^
-DPSPRECOMP_NATIVE_AVX2=ON ^
-DPSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH=ON ^
-DPSPRECOMP_AOT_PRODUCTION_FASTPATHS=ON ^
-DPSPRECOMP_MSVC_CGTHREADS=0 ^
-DPSPRECOMP_MSVC_MP_JOBS=1 ^
-DPSPRECOMP_PROFILE_GUIDED_AOT=ON ^
-DPSPRECOMP_HOT_GENERATED_OPT_LEVEL=3 ^
-DPSPRECOMP_GENERATED_INLINE_LEVEL=0 ^
-DPSPRECOMP_HOT_GENERATED_INLINE_LEVEL=3 ^
-DPSPRECOMP_VCS_AOT_LTO=OFF ^
-DPSPRECOMP_BUILD_TESTS=ON ^
-DPSPRECOMP_BUILD_PROFILE_TESTS=ON
if errorlevel 1 goto :FAIL
echo.
echo [2/7] Building VCSNative with Ninja...
"%CMAKE_EXE%" --build "%BUILD%" --parallel %JOBS% --target VCSNative
if errorlevel 1 goto :FAIL
echo.
echo [2b/7] Building tests and DX12 probes...
"%CMAKE_EXE%" --build "%BUILD%" --parallel %JOBS% --target ^
psprecomp_tests vcs_profile_tests vcs_config_tests audio_resampler_tests ^
vcs_bootstrap_paths_tests vcs_dx12_probe vcs_dx12_ge_probe
if errorlevel 1 goto :FAIL
echo.
echo [3/7] Running regression tests...
"%CTEST_EXE%" --test-dir "%BUILD%" --output-on-failure
if errorlevel 1 goto :TEST_FAIL
set "BIN=%BUILD%\bin\Release"
if not exist "%BIN%\VCSNative.exe" (
echo ERROR: VCSNative.exe was not produced:
echo %BIN%\VCSNative.exe
goto :FAIL
)
echo.
echo [4/7] DX12 device/swapchain probe...
"%BIN%\vcs_dx12_probe.exe"
if errorlevel 1 goto :DX12_FAIL
echo.
echo [5/7] GE compatibility probe...
set "PSPRECOMP_DX12_GE_STRICT=1"
set "PSPRECOMP_GE_PARALLEL_VERTEX_DECODE=0"
set "PSPRECOMP_GE_DIRECT_NONINDEXED_DRAW=0"
set "PSPRECOMP_DX12_PACKED_0115=0"
set "PSPRECOMP_DX12_NATIVE_INDEXED_DRAW=0"
set "PSPRECOMP_DX12_BATCH_MERGE=0"
"%BIN%\vcs_dx12_ge_probe.exe"
if errorlevel 1 goto :GE_FAIL
set "PSPRECOMP_DX12_GE_STRICT="
echo.
echo [6/7] GE production probe...
set "PSPRECOMP_DX12_GE_STRICT=1"
set "PSPRECOMP_GE_GPU_HW_CULL=1"
set "PSPRECOMP_GE_PARALLEL_VERTEX_DECODE=0"
set "PSPRECOMP_GE_DIRECT_NONINDEXED_DRAW=1"
set "PSPRECOMP_DX12_PACKED_0115=1"
set "PSPRECOMP_DX12_NATIVE_INDEXED_DRAW=1"
set "PSPRECOMP_DX12_BATCH_MERGE=1"
"%BIN%\vcs_dx12_ge_probe.exe"
if errorlevel 1 goto :GE_PROD_FAIL
set "PSPRECOMP_DX12_GE_STRICT="
echo.
echo [7/7] Installing current VCS config...
copy /Y "%PROFILE%\config\VCSNative.ini" "%BIN%\VCSNative.ini" >nul
if errorlevel 1 goto :FAIL
echo.
echo ================================================================
echo NINJA BUILD OK
echo EXE:
echo %BIN%\VCSNative.exe
echo.
echo Later runs reuse:
echo %BUILD%
echo ================================================================
exit /b 0
:NO_VS
echo ERROR: Visual Studio 2022 with Desktop C++ tools was not found.
pause
exit /b 2
:VS_ENV_FAIL
echo ERROR: VsDevCmd.bat failed to initialize x64.
pause
exit /b 3
:NO_CL
echo ERROR: cl.exe was not found after initializing VS2022.
pause
exit /b 4
:NO_CMAKE
echo ERROR: CMake was not found.
pause
exit /b 5
:NO_NINJA
echo ERROR: ninja.exe was not found.
pause
exit /b 6
:TEST_FAIL
echo ERROR: regression tests failed.
pause
exit /b 8
:DX12_FAIL
echo ERROR: DX12 probe failed.
pause
exit /b 9
:GE_FAIL
echo ERROR: compatibility GE probe failed.
pause
exit /b 10
:GE_PROD_FAIL
echo ERROR: production GE probe failed.
pause
exit /b 11
:FAIL
echo.
echo ERROR: Ninja VCS build failed.
echo Send the FIRST real error shown above.
pause
exit /b 12
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
from pathlib import Path
import sys
profile = Path(__file__).resolve().parents[1]
errors = []
def require(path, needle, label):
text = path.read_text(encoding="utf-8", errors="replace")
if needle not in text:
errors.append(f"{label}: missing {needle!r}")
host = profile / "host"
generated = profile / "generated"
tools = profile / "tools"
require(host / "vcs_profile.cpp", "if (voice.adsr_rates[3] <= 0)", "audio zero-release guard")
require(host / "vcs_profile.cpp", "height -= kSasFallbackReleaseStep;", "audio zero-release fallback")
require(host / "vcs_profile.cpp", "const std::uint64_t rate = channel.frequency == 0u ? 44100u : channel.frequency;",
"audio remaining samples uses channel rate")
require(host / "vcs_profile.cpp",
"// limit_frame_rate() may advance virtual_time_us when the host misses the",
"post-frame-limit audio timeline seal")
require(profile / "CMakeLists.txt", "host/savedata_dialog.cpp", "savedata dialog linked")
require(host / "vcs_profile.cpp", "kSavedataSaveNameListOffset = 0x60u", "savedata saveNameList")
require(host / "vcs_profile.cpp", "mode != 4u && mode != 5u && mode != 6u", "savedata LIST modes")
require(host / "vcs_profile.cpp", "choose_savedata_slot", "savedata chooser invoked")
require(host / "savedata_dialog.cpp", "VCSNativeSavedataSlotDialog", "Win32 savedata chooser")
require(generated / "generated_units.hpp", '#include "vcs_draw_distance_patch.hpp"',
"generated draw-distance state header")
checks = [
("generated_unit_0136.cpp", "g_draw_distance_runtime_scales.entity", "entity local label"),
("generated_unit_0113.cpp", "g_draw_distance_runtime_scales.npcs", "NPC local label"),
("generated_unit_0208.cpp", "g_draw_distance_runtime_scales.vehicles", "vehicle local label"),
]
for filename, needle, label in checks:
require(generated / filename, needle, label)
codegen = tools / "vcs_codegen_main.cpp"
for pc in ("0x08A24128u", "0x089CB38Cu", "0x08B45AC0u"):
require(codegen, pc, f"future codegen preserves {pc}")
require(host / "vcs_profile.cpp", "draw_distance_vblank_tick(rt, ctx.gpr[28], display_vblank_index);",
"world draw-distance vblank maintenance")
dx12 = host / "ge_gpu_backend_dx12.cpp"
require(dx12, "Dx12BlendPlan dx12_blend_plan", "DX12 generalized PSP blend mapper")
require(dx12, "D3D12_BLEND_BLEND_FACTOR", "DX12 FIX blend constant")
require(dx12, "D3D12_BLEND_INV_BLEND_FACTOR", "DX12 complementary FIX blend")
require(dx12, "plan.src = D3D12_BLEND_SRC_ALPHA;", "DX12 safe translucent fallback")
require(dx12, "PSPRECOMP_DX12_BLEND_DIAG", "DX12 unsupported-blend diagnostic")
ini = (profile / "config/VCSNative.ini").read_text(encoding="utf-8", errors="replace")
dd = ini.split("[DrawDistance]", 1)[1].split("[", 1)[0]
if "Enabled=false" not in dd or "World=1.50" not in dd:
errors.append("DrawDistance config must remain opt-in with 1.50x values prefilled")
if errors:
print("CORRECTNESS 0.1 SOURCE AUDIT: FAIL")
for e in errors:
print(" -", e)
sys.exit(1)
print("CORRECTNESS 0.1 SOURCE AUDIT: PASS")
print(" - audio release/timing guards present")
print(" - savedata list selector linked")
print(" - draw-distance local AOT labels + regeneration hooks present")
print(" - DX12 unsafe opaque blend fallback removed")
print(" - draw distance remains opt-in for baseline FPS comparison")
+39 -1
View File
@@ -990,6 +990,44 @@ std::string emit_function_source(const GeneratedFunctionInput &function,
break;
}
// VCS draw-distance local-label patches. These addresses are valid
// AOT entries, but normal gameplay reaches them through local gotos,
// which bypasses Runtime::register_function replacements. Keep the
// exact original path at scale 1.0 and only short-circuit when the
// user explicitly enables extended draw distance.
if (pc == 0x08A24128u) {
body << " if (vcs::g_draw_distance_runtime_scales.entity > 1.0f) {\n"
<< " const float dd_base = std::bit_cast<float>(aot_mem.aot_load32(ctx.gpr[16] + static_cast<std::uint32_t>(1960)));\n"
<< " const float dd_scale = vcs::g_draw_distance_runtime_scales.entity;\n"
<< " aot_mem.aot_store32(ctx.gpr[16] + static_cast<std::uint32_t>(1952), std::bit_cast<std::uint32_t>(dd_base * dd_scale));\n"
<< " ctx.fpr[0] = dd_scale;\n"
<< " goto L_08A24138;\n"
<< " }\n";
}
if (pc == 0x089CB38Cu) {
body << " if (vcs::g_draw_distance_runtime_scales.npcs > 1.0f) {\n"
<< " const float dd_scale = vcs::g_draw_distance_runtime_scales.npcs;\n"
<< " ctx.gpr[19] = ctx.gpr[29] + static_cast<std::uint32_t>(64);\n"
<< " ctx.gpr[30] = ctx.gpr[29] + static_cast<std::uint32_t>(16);\n"
<< " ctx.gpr[23] = ctx.gpr[29] + static_cast<std::uint32_t>(32);\n"
<< " ctx.fpr[22] = 120.0f;\n"
<< " ctx.fpr[28] = 51.0f * dd_scale;\n"
<< " ctx.fpr[26] = 25.0f * dd_scale;\n"
<< " ctx.fpr[24] = 80.0f * dd_scale;\n"
<< " ctx.gpr[4] = ctx.gpr[18] << 5u;\n"
<< " ctx.gpr[20] = ctx.gpr[4];\n"
<< " ctx.gpr[4] <<= 4u;\n"
<< " ctx.gpr[20] += ctx.gpr[4];\n"
<< " goto L_089CB3C8;\n"
<< " }\n";
}
if (pc == 0x08B45AC0u) {
body << " if (vcs::g_draw_distance_runtime_scales.vehicles > 1.0f) {\n"
<< " ctx.fpr[12] = 60.0f * vcs::g_draw_distance_runtime_scales.vehicles;\n"
<< " goto L_08B45AC8;\n"
<< " }\n";
}
// Verified VCS raw-DEFLATE hot loop. The original Allegrex code at
// 0x08B64AD8 performs a forward LZ back-reference copy one byte at
// a time. Preserve its register/error semantics while lowering the
@@ -1588,7 +1626,7 @@ int generate_auto(const std::filesystem::path &elf_path,
// instead of forcing every known edge through a function-pointer branch.
const auto units_header_path = output_dir / "generated_units.hpp";
std::ostringstream units_header;
units_header << "#pragma once\n\n#include <cstdint>\n#include \"psprecomp/guest_memory.hpp\"\n\nnamespace psprecomp {\nclass Runtime;\nstruct AllegrexContext;\n";
units_header << "#pragma once\n\n#include <cstdint>\n#include \"psprecomp/guest_memory.hpp\"\n#include \"vcs_draw_distance_patch.hpp\"\n\nnamespace psprecomp {\nclass Runtime;\nstruct AllegrexContext;\n";
for (const auto &unit : units) {
units_header << "void " << generated_unit_cpp_name(unit.bucket)
<< "(Runtime &, AllegrexContext &);\n";