mirror of
https://github.com/jessicanataliagta/PSPRecomp
synced 2026-09-26 16:49:34 -04:00
temp stuff, might not good
temp stuff, might not good
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
call "%~dp0scripts\build_release_ninja.bat"
|
||||
exit /b %errorlevel%
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,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")
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user