From 4a6b83aede336011ab0134da4683a41f33a819cd Mon Sep 17 00:00:00 2001 From: Jessica_Natalia Date: Sat, 15 Aug 2026 11:12:49 -0300 Subject: [PATCH] temp stuff, might not good temp stuff, might not good --- ...ECTNESS_0_1_SAVEDATA_WINDOWS_COMPILE.patch | 35 ++ profiles/vcs/BUILD_VCS_NINJA.bat | 3 + profiles/vcs/CMakeLists.txt | 1 + profiles/vcs/FIX_FUTURE_TIMESTAMPS.bat | 40 ++ profiles/vcs/config/VCSNative.ini | 9 +- .../vcs/generated/generated_unit_0113.cpp | 15 + .../vcs/generated/generated_unit_0136.cpp | 8 + .../vcs/generated/generated_unit_0208.cpp | 4 + profiles/vcs/generated/generated_units.hpp | 1 + profiles/vcs/host/ge_gpu_backend_dx12.cpp | 302 ++++++++-- profiles/vcs/host/savedata_dialog.cpp | 549 ++++++++++++++++++ profiles/vcs/host/savedata_dialog.hpp | 27 + profiles/vcs/host/vcs_draw_distance_patch.cpp | 209 +++---- profiles/vcs/host/vcs_draw_distance_patch.hpp | 30 + profiles/vcs/host/vcs_profile.cpp | 173 +++++- profiles/vcs/host/vcs_project2dfx.cpp | 2 +- profiles/vcs/scripts/build_release_ninja.bat | 243 ++++++++ profiles/vcs/tests/check_correctness_0_1.py | 69 +++ profiles/vcs/tools/vcs_codegen_main.cpp | 40 +- 19 files changed, 1571 insertions(+), 189 deletions(-) create mode 100644 VCS_CORRECTNESS_0_1_SAVEDATA_WINDOWS_COMPILE.patch create mode 100644 profiles/vcs/BUILD_VCS_NINJA.bat create mode 100644 profiles/vcs/FIX_FUTURE_TIMESTAMPS.bat create mode 100644 profiles/vcs/host/savedata_dialog.cpp create mode 100644 profiles/vcs/host/savedata_dialog.hpp create mode 100644 profiles/vcs/host/vcs_draw_distance_patch.hpp create mode 100644 profiles/vcs/scripts/build_release_ninja.bat create mode 100644 profiles/vcs/tests/check_correctness_0_1.py diff --git a/VCS_CORRECTNESS_0_1_SAVEDATA_WINDOWS_COMPILE.patch b/VCS_CORRECTNESS_0_1_SAVEDATA_WINDOWS_COMPILE.patch new file mode 100644 index 0000000..4863067 --- /dev/null +++ b/VCS_CORRECTNESS_0_1_SAVEDATA_WINDOWS_COMPILE.patch @@ -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(kListId), nullptr, nullptr); ++ 16, 42, 470, 224, window, reinterpret_cast(static_cast(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(kAcceptId), nullptr, nullptr); ++ 310, 280, 82, 30, window, reinterpret_cast(static_cast(kAcceptId)), nullptr, nullptr); + CreateWindowExW(0, L"BUTTON", L"Cancel", + WS_CHILD | WS_VISIBLE | WS_TABSTOP, +- 404, 280, 82, 30, window, reinterpret_cast(kCancelId), nullptr, nullptr); ++ 404, 280, 82, 30, window, reinterpret_cast(static_cast(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(COLOR_WINDOW + 1); ++ cls.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512)); ++ cls.hbrBackground = reinterpret_cast(static_cast(COLOR_WINDOW + 1)); + cls.lpszClassName = kClassName; + registered = RegisterClassW(&cls) != 0 || GetLastError() == ERROR_CLASS_ALREADY_EXISTS; + } diff --git a/profiles/vcs/BUILD_VCS_NINJA.bat b/profiles/vcs/BUILD_VCS_NINJA.bat new file mode 100644 index 0000000..66d079c --- /dev/null +++ b/profiles/vcs/BUILD_VCS_NINJA.bat @@ -0,0 +1,3 @@ +@echo off +call "%~dp0scripts\build_release_ninja.bat" +exit /b %errorlevel% diff --git a/profiles/vcs/CMakeLists.txt b/profiles/vcs/CMakeLists.txt index 4800b7d..7aeb232 100644 --- a/profiles/vcs/CMakeLists.txt +++ b/profiles/vcs/CMakeLists.txt @@ -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 diff --git a/profiles/vcs/FIX_FUTURE_TIMESTAMPS.bat b/profiles/vcs/FIX_FUTURE_TIMESTAMPS.bat new file mode 100644 index 0000000..8c4dc8b --- /dev/null +++ b/profiles/vcs/FIX_FUTURE_TIMESTAMPS.bat @@ -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 diff --git a/profiles/vcs/config/VCSNative.ini b/profiles/vcs/config/VCSNative.ini index 6a6b3d9..bfdd705 100644 --- a/profiles/vcs/config/VCSNative.ini +++ b/profiles/vcs/config/VCSNative.ini @@ -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 diff --git a/profiles/vcs/generated/generated_unit_0113.cpp b/profiles/vcs/generated/generated_unit_0113.cpp index 8ed6996..effe3e0 100644 --- a/profiles/vcs/generated/generated_unit_0113.cpp +++ b/profiles/vcs/generated/generated_unit_0113.cpp @@ -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(64); + ctx.gpr[30] = ctx.gpr[29] + static_cast(16); + ctx.gpr[23] = ctx.gpr[29] + static_cast(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(64)); ctx.gpr[30] = (ctx.gpr[29] + static_cast(16)); ctx.gpr[23] = (ctx.gpr[29] + static_cast(32)); diff --git a/profiles/vcs/generated/generated_unit_0136.cpp b/profiles/vcs/generated/generated_unit_0136.cpp index 2994f4f..e9a505c 100644 --- a/profiles/vcs/generated/generated_unit_0136.cpp +++ b/profiles/vcs/generated/generated_unit_0136.cpp @@ -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(aot_mem.aot_load32(ctx.gpr[16] + static_cast(1960))); + const float dd_scale = vcs::g_draw_distance_runtime_scales.entity; + aot_mem.aot_store32(ctx.gpr[16] + static_cast(1952), + std::bit_cast(dd_base * dd_scale)); + ctx.fpr[0] = dd_scale; + goto L_08A24138; + } ctx.fpr[12] = std::bit_cast(aot_mem.aot_load32(ctx.gpr[16] + static_cast(1960))); aot_mem.aot_store32(ctx.gpr[16] + static_cast(1952), std::bit_cast(ctx.fpr[12])); ctx.gpr[31] = (0x08A24138u); diff --git a/profiles/vcs/generated/generated_unit_0208.cpp b/profiles/vcs/generated/generated_unit_0208.cpp index a748333..83d51c0 100644 --- a/profiles/vcs/generated/generated_unit_0208.cpp +++ b/profiles/vcs/generated/generated_unit_0208.cpp @@ -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(ctx.gpr[4]); goto L_08B45AC8; diff --git a/profiles/vcs/generated/generated_units.hpp b/profiles/vcs/generated/generated_units.hpp index cf71dc1..de17e0b 100644 --- a/profiles/vcs/generated/generated_units.hpp +++ b/profiles/vcs/generated/generated_units.hpp @@ -2,6 +2,7 @@ #include #include "psprecomp/guest_memory.hpp" +#include "vcs_draw_distance_patch.hpp" namespace psprecomp { class Runtime; diff --git a/profiles/vcs/host/ge_gpu_backend_dx12.cpp b/profiles/vcs/host/ge_gpu_backend_dx12.cpp index 26f80af..0ba66c8 100644 --- a/profiles/vcs/host/ge_gpu_backend_dx12.cpp +++ b/profiles/vcs/host/ge_gpu_backend_dx12.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -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(draw.depth_test_enabled ? 1u : 0u); key |= static_cast(draw.depth_write_enabled ? 1u : 0u) << 1u; key |= static_cast(draw.depth_function & 7u) << 2u; - key |= static_cast(blend_variant(draw) & 7u) << 5u; - key |= static_cast(color_write_mask(draw) & 0xFu) << 8u; + key |= static_cast(blend.enabled ? 1u : 0u) << 5u; + key |= static_cast(blend.exact ? 0u : 1u) << 6u; + key |= static_cast(static_cast(blend.src) & 0x1Fu) << 7u; + key |= static_cast(static_cast(blend.dst) & 0x1Fu) << 12u; + key |= static_cast(static_cast(blend.src_alpha) & 0x1Fu) << 17u; + key |= static_cast(static_cast(blend.dst_alpha) & 0x1Fu) << 22u; + key |= static_cast(static_cast(blend.op) & 0x7u) << 27u; + key |= static_cast(static_cast(blend.op_alpha) & 0x7u) << 30u; + key |= static_cast(color_write_mask(draw) & 0xFu) << 33u; + key |= static_cast(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 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(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; diff --git a/profiles/vcs/host/savedata_dialog.cpp b/profiles/vcs/host/savedata_dialog.cpp new file mode 100644 index 0000000..9b1c346 --- /dev/null +++ b/profiles/vcs/host/savedata_dialog.cpp @@ -0,0 +1,549 @@ +#include "savedata_dialog.hpp" +#include "display_window.hpp" + +#include +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif + +namespace vcs { +namespace { + +SavedataDialogChoice deterministic_choice(const std::vector &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(text.size()), nullptr, 0); + if (needed <= 0) return std::wstring(text.begin(), text.end()); + std::wstring result(static_cast(needed), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, text.data(), static_cast(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 *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(static_cast(x) + (static_cast(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(y) / static_cast(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(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(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(SelectObject(hdc, skyline_brush)); + HPEN skyline_pen = CreatePen(PS_SOLID, 1, kHorizon); + old_pen = static_cast(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(SelectObject(hdc, border_pen)); + HBRUSH hollow = static_cast(GetStockObject(HOLLOW_BRUSH)); + HBRUSH old_brush = static_cast(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(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(selected); + if (index < 0 || static_cast(index) >= state.slots->size()) return; + if (!state.saving && !(*state.slots)[static_cast(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(SelectObject(hdc, brush)); + HPEN old_pen = static_cast(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(-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(GetWindowLongPtrW(window, GWLP_USERDATA)); + if (message == WM_NCCREATE) { + const auto *create = reinterpret_cast(lparam); + state = static_cast(create->lpCreateParams); + SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(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(static_cast(kListId)), nullptr, nullptr); + if (!state->list) return -1; + SendMessageW(state->list, WM_SETFONT, reinterpret_cast(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(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(static_cast(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(static_cast(kCancelId)), nullptr, nullptr); + SendMessageW(state->accept_button, WM_SETFONT, reinterpret_cast(state->button_font), TRUE); + SendMessageW(state->cancel_button, WM_SETFONT, reinterpret_cast(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(lparam); + if (measure && measure->CtlID == kListId) { + measure->itemHeight = 28; + return TRUE; + } + break; + } + case WM_DRAWITEM: { + auto *draw = reinterpret_cast(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(lparam) == state->list) { + HDC hdc = reinterpret_cast(wparam); + SetBkColor(hdc, kListFill); + SetTextColor(hdc, kListSavedText); + return reinterpret_cast(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 &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(static_cast(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(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(i); + break; + } + } + if (initial < 0) { + for (std::size_t i = 0; i < slots.size(); ++i) { + if (saving || slots[i].exists) { initial = static_cast(i); break; } + } + } + if (initial >= 0) { + SendMessageW(state.list, LB_SETCURSEL, static_cast(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(state.selected) < slots.size()) { + return {true, slots[static_cast(state.selected)].save_name}; + } + return {}; +} +#endif + +} // namespace + +SavedataDialogChoice choose_savedata_slot(const std::vector &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 diff --git a/profiles/vcs/host/savedata_dialog.hpp b/profiles/vcs/host/savedata_dialog.hpp new file mode 100644 index 0000000..0d36682 --- /dev/null +++ b/profiles/vcs/host/savedata_dialog.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +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 &slots, + bool saving, + std::string_view current_save_name); + +} // namespace vcs diff --git a/profiles/vcs/host/vcs_draw_distance_patch.cpp b/profiles/vcs/host/vcs_draw_distance_patch.cpp index f5ef1d3..4a4d36b 100644 --- a/profiles/vcs/host/vcs_draw_distance_patch.cpp +++ b/profiles/vcs/host/vcs_draw_distance_patch.cpp @@ -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 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 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 diff --git a/profiles/vcs/host/vcs_draw_distance_patch.hpp b/profiles/vcs/host/vcs_draw_distance_patch.hpp new file mode 100644 index 0000000..ba7bc73 --- /dev/null +++ b/profiles/vcs/host/vcs_draw_distance_patch.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +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 diff --git a/profiles/vcs/host/vcs_profile.cpp b/profiles/vcs/host/vcs_profile.cpp index 6f81f8f..7b5f0a9 100644 --- a/profiles/vcs/host/vcs_profile.cpp +++ b/profiles/vcs/host/vcs_profile.cpp @@ -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(capacity - 1u, value.size()); + for (std::size_t i = 0; i < count; ++i) + memory.store8(address + static_cast(i), + static_cast(value[i])); +} + +std::vector savedata_slot_entries(psprecomp::Runtime &runtime, + std::uint32_t parameter_address, + bool saving) { + std::vector 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::min(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. diff --git a/profiles/vcs/host/vcs_project2dfx.cpp b/profiles/vcs/host/vcs_project2dfx.cpp index d05ba02..37a2966 100644 --- a/profiles/vcs/host/vcs_project2dfx.cpp +++ b/profiles/vcs/host/vcs_project2dfx.cpp @@ -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 namespace vcs { -void install_draw_distance_patch(psprecomp::Runtime &, const std::filesystem::path &); namespace { constexpr float kPi = 3.14159265358979323846f; diff --git a/profiles/vcs/scripts/build_release_ninja.bat b/profiles/vcs/scripts/build_release_ninja.bat new file mode 100644 index 0000000..aa1f120 --- /dev/null +++ b/profiles/vcs/scripts/build_release_ninja.bat @@ -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 diff --git a/profiles/vcs/tests/check_correctness_0_1.py b/profiles/vcs/tests/check_correctness_0_1.py new file mode 100644 index 0000000..42eb949 --- /dev/null +++ b/profiles/vcs/tests/check_correctness_0_1.py @@ -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") diff --git a/profiles/vcs/tools/vcs_codegen_main.cpp b/profiles/vcs/tools/vcs_codegen_main.cpp index 8c3b031..222d529 100644 --- a/profiles/vcs/tools/vcs_codegen_main.cpp +++ b/profiles/vcs/tools/vcs_codegen_main.cpp @@ -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(aot_mem.aot_load32(ctx.gpr[16] + static_cast(1960)));\n" + << " const float dd_scale = vcs::g_draw_distance_runtime_scales.entity;\n" + << " aot_mem.aot_store32(ctx.gpr[16] + static_cast(1952), std::bit_cast(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(64);\n" + << " ctx.gpr[30] = ctx.gpr[29] + static_cast(16);\n" + << " ctx.gpr[23] = ctx.gpr[29] + static_cast(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 \n#include \"psprecomp/guest_memory.hpp\"\n\nnamespace psprecomp {\nclass Runtime;\nstruct AllegrexContext;\n"; + units_header << "#pragma once\n\n#include \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";