diff --git a/profiles/vcs/BUILD_VCS_NINJA.bat b/profiles/vcs/BUILD_VCS_NINJA.bat index b32e1c9..ff38d6c 100644 --- a/profiles/vcs/BUILD_VCS_NINJA.bat +++ b/profiles/vcs/BUILD_VCS_NINJA.bat @@ -2,15 +2,19 @@ setlocal EnableExtensions for %%I in ("%~dp0..\..") do set "REPO=%%~fI" set "BUILD=%REPO%\out\vcs-release-ninja" -set "MARKER=%BUILD%\.v9_5_host_texture_objects_done" +set "MARKER95=%BUILD%\.v9_5_host_texture_objects_done" +set "MARKER96=%BUILD%\.v9_6_savedata_cancel_objects_done" call "%~dp0FORCE_V9_5_HOST_TEXTURE_OBJECTS.bat" if errorlevel 1 exit /b %errorlevel% +call "%~dp0FORCE_V9_6_SAVEDATA_CANCEL_OBJECTS.bat" +if errorlevel 1 exit /b %errorlevel% call "%~dp0scripts\build_release_ninja.bat" set "RC=%errorlevel%" if "%RC%"=="0" ( if not exist "%BUILD%" mkdir "%BUILD%" >nul 2>nul - >"%MARKER%" echo V9.5 host-texture isolation successfully rebuilt critical objects. + >"%MARKER95%" echo V9.5 host-texture isolation successfully rebuilt critical objects. + >"%MARKER96%" echo V9.6 mode-aware savedata cancellation successfully rebuilt vcs_profile.cpp. ) exit /b %RC% diff --git a/profiles/vcs/FORCE_V9_6_SAVEDATA_CANCEL_OBJECTS.bat b/profiles/vcs/FORCE_V9_6_SAVEDATA_CANCEL_OBJECTS.bat new file mode 100644 index 0000000..4e046df --- /dev/null +++ b/profiles/vcs/FORCE_V9_6_SAVEDATA_CANCEL_OBJECTS.bat @@ -0,0 +1,29 @@ +@echo off +setlocal EnableExtensions +for %%I in ("%~dp0..\..") do set "REPO=%%~fI" +set "BUILD=%REPO%\out\vcs-release-ninja" +set "MARKER=%BUILD%\.v9_6_savedata_cancel_objects_done" + +if exist "%MARKER%" exit /b 0 + +echo [V9.6] SAVEDATA CANCEL: invalidating stale profile objects... +if not exist "%BUILD%" ( + echo [V9.6] Build tree does not exist yet; normal build will create it. + exit /b 0 +) + +powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command ^ + "$build=[IO.Path]::GetFullPath('%BUILD%');" ^ + "$names=@('vcs_profile.cpp.obj');" ^ + "$found=Get-ChildItem -LiteralPath $build -Recurse -File -Filter '*.obj' -ErrorAction SilentlyContinue ^| Where-Object { $names -contains $_.Name };" ^ + "Write-Host ('[V9.6] stale objects found: ' + $found.Count);" ^ + "foreach($f in $found){Write-Host (' deleting: ' + $f.FullName); Remove-Item -LiteralPath $f.FullName -Force -ErrorAction Stop};" ^ + "$src='%REPO%\profiles\vcs\host\vcs_profile.cpp';" ^ + "if(Test-Path -LiteralPath $src){(Get-Item -LiteralPath $src).LastWriteTime=Get-Date}" +if errorlevel 1 ( + echo ERROR: failed to invalidate V9.6 savedata cancel objects. + exit /b 1 +) + +echo [V9.6] Savedata cancel objects invalidated. Ninja must compile vcs_profile.cpp again. +exit /b 0 diff --git a/profiles/vcs/host/vcs_profile.cpp b/profiles/vcs/host/vcs_profile.cpp index 320cff8..aa3cfe9 100644 --- a/profiles/vcs/host/vcs_profile.cpp +++ b/profiles/vcs/host/vcs_profile.cpp @@ -2207,6 +2207,14 @@ constexpr std::uint32_t kPspUtilityCircle = 0x002000u; constexpr std::uint32_t kPspUtilityCross = 0x004000u; constexpr std::uint32_t kUtilityCommonResultOffset = 0x1Cu; +// PSP utility dialog common.result values. Keep these separate from savedata +// I/O error codes: user cancellation is CANCEL (1), while ABORT (2) is a +// different firmware result. VCS needs a legacy ABORT workaround only for +// the promoted LISTLOAD path; normal LISTSAVE/LISTDELETE cancellation must use +// the real PSP CANCEL result or the retail frontend enters its loading path. +constexpr std::uint32_t kPspUtilityDialogResultSuccess = 0u; +constexpr std::uint32_t kPspUtilityDialogResultCancel = 1u; +constexpr std::uint32_t kPspUtilityDialogResultAbort = 2u; constexpr std::uint32_t kSavedataModeOffset = 0x30u; constexpr std::uint32_t kSavedataGameNameOffset = 0x3Cu; constexpr std::uint32_t kSavedataSaveNameOffset = 0x4Cu; @@ -2549,7 +2557,7 @@ void initialize_savedata_list_ui(psprecomp::Runtime &runtime) { savedata_utility.ui_initialized = true; savedata_utility_ui_begin(savedata_utility.mode, savedata_utility.slots, savedata_utility.selected); - std::cout << "[savedata] V9.4 list UI initialized mode=" << savedata_utility.mode + std::cout << "[savedata] V9.6 list UI initialized mode=" << savedata_utility.mode << " slots=" << savedata_utility.slots.size() << " selected=" << savedata_utility.selected << "\n"; } @@ -2802,19 +2810,34 @@ const char *savedata_failure_message(std::uint32_t mode) noexcept { } void cancel_savedata_list_utility(psprecomp::Runtime &runtime) { - // PSP utility cancellation is not success. The retail VCS completion - // handler treats common.result == 0 as a successful load even when - // abortStatus is non-zero, which is why backing out of our promoted - // LOAD->LISTLOAD picker dropped into New Game. VCS' own AOT path has an - // explicit cancel branch for result==2 + abortStatus!=0. - runtime.memory().store32(savedata_utility.parameter_address + kSavedataAbortStatusOffset, 1u); - runtime.memory().store32(savedata_utility.parameter_address + kUtilityCommonResultOffset, 2u); + // Real PSP savedata list dialogs report a user Back/Cancel as + // common.result = CANCEL (1) and do not set abortStatus. V9.4 deliberately + // used ABORT (2) + abortStatus=1 to get VCS' promoted LOAD->LISTLOAD path + // back into gameplay; applying that same workaround to LISTSAVE was wrong: + // VCS interprets it as a load/restore transition and shows a black LOADING + // screen. Keep the proven LOAD compatibility workaround, but use exact PSP + // cancellation semantics for SAVE/DELETE. + const bool load_cancel_workaround = savedata_utility.mode == 4u || + savedata_utility.startup_picker || savedata_utility.direct_load_picker; + const std::uint32_t common_result = load_cancel_workaround + ? kPspUtilityDialogResultAbort + : kPspUtilityDialogResultCancel; + const std::uint32_t abort_status = load_cancel_workaround ? 1u : 0u; + + runtime.memory().store32(savedata_utility.parameter_address + + kSavedataAbortStatusOffset, abort_status); + runtime.memory().store32(savedata_utility.parameter_address + + kUtilityCommonResultOffset, common_result); savedata_utility.operation_complete = true; savedata_utility.status = UtilityStatus::Quit; savedata_utility_ui_end(); display_window_set_system_utility_mode(false); - std::cout << "[savedata] V9.4 picker cancelled mode=" << savedata_utility.mode - << " commonResult=2 abortStatus=1\n"; + std::cout << "[savedata] V9.6 picker cancelled mode=" << savedata_utility.mode + << " commonResult=" << common_result + << " abortStatus=" << abort_status + << (load_cancel_workaround ? " policy=load-compat-abort" + : " policy=psp-user-cancel") + << "\n"; } void execute_selected_savedata_slot(psprecomp::Runtime &runtime) { @@ -2837,7 +2860,7 @@ void execute_selected_savedata_slot(psprecomp::Runtime &runtime) { savedata_utility.status = UtilityStatus::Quit; savedata_utility_ui_end(); display_window_set_system_utility_mode(false); - std::cout << "[savedata] V9.4 LOAD selected slot=" << slot.save_name + std::cout << "[savedata] V9.6 LOAD selected slot=" << slot.save_name << " result=0\n"; } else { savedata_utility.prompt = SavedataUtilityUiPrompt::Result; @@ -7722,7 +7745,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start initialize_savedata_list_ui(rt); display_window_set_system_utility_mode(true); savedata_utility.previous_buttons = effective_controller_buttons(); - std::cout << "[savedata] V9.4 LOAD picker active; guest mode=" + std::cout << "[savedata] V9.6 LOAD picker active; guest mode=" << guest_mode << " slots=" << savedata_utility.slots.size() << "\n"; } else if (savedata_mode_has_list_ui(savedata_utility.mode)) { initialize_savedata_list_ui(rt); diff --git a/profiles/vcs/tests/check_frontend_boot_mouse_savedata.py b/profiles/vcs/tests/check_frontend_boot_mouse_savedata.py index 0348934..7dec0eb 100644 --- a/profiles/vcs/tests/check_frontend_boot_mouse_savedata.py +++ b/profiles/vcs/tests/check_frontend_boot_mouse_savedata.py @@ -20,8 +20,10 @@ if 'startup_load_picker_consumed' not in profile or 'savedata_utility.startup_pi if 'savedata_utility.mode = 4u' not in profile: fail('startup picker is not host-side LISTLOAD') if 'guest_mode == 0u' not in profile or 'guest_mode == 2u' not in profile: fail('AUTOLOAD/LOAD modes not covered') if 'if (!savedata_utility.startup_picker)' not in profile: fail('startup picker cancellation guard missing') -if 'kUtilityCommonResultOffset, 2u' not in profile or 'kSavedataAbortStatusOffset, 1u' not in profile: - fail('picker cancel is still reported as successful LOAD') +if 'load_cancel_workaround' not in profile or 'kPspUtilityDialogResultAbort' not in profile: + fail('promoted LOAD picker cancel compatibility path missing') +if 'kPspUtilityDialogResultCancel = 1u' not in profile: + fail('PSP user-cancel result is not defined for non-load dialogs') if 'MouseMenu=false' not in ini: fail('mouse menu is not disabled by default') if 'if (mouse_menu_enabled()) enqueue_synthetic_pulse(state, kPspCross, 2);' not in display: fail('savedata mouse click is not gated by MouseMenu') @@ -29,5 +31,5 @@ print('LOAD-ONLY STARTUP AUDIT: PASS') print(' - native GAME frontend boot hook is unlinked') print(' - V8 generated autoload interception is removed') print(' - first AUTOLOAD and every explicit LOAD become the in-frame Load Game picker') -print(' - cancel returns result=2 + abortStatus=1 instead of successful LOAD') +print(' - LOAD cancel keeps the proven abort compatibility path; SAVE uses PSP CANCEL') print(' - menu mouse remains disabled by default and gates savedata clicks') diff --git a/profiles/vcs/tests/check_native_savedata_utility.py b/profiles/vcs/tests/check_native_savedata_utility.py index 764237a..d468044 100644 --- a/profiles/vcs/tests/check_native_savedata_utility.py +++ b/profiles/vcs/tests/check_native_savedata_utility.py @@ -43,8 +43,9 @@ require(ui_cpp, '58.0f, 183.0f, 82.0f, 46.0f', 'next slot stays above footer') require(ui_cpp, 'submit_smooth_text', 'antialiased text atlas path') require(ui_cpp, 'blend_source_factor = 2u', 'text source-alpha blending') require(ui_cpp, 'blend_dest_factor = 3u', 'text inverse-alpha blending') -require(profile_cpp, 'kUtilityCommonResultOffset, 2u', 'cancel result is not reported as success') -require(profile_cpp, 'kSavedataAbortStatusOffset, 1u', 'cancel abortStatus is asserted') +require(profile_cpp, 'load_cancel_workaround', 'mode-aware cancel policy') +require(profile_cpp, 'kPspUtilityDialogResultCancel = 1u', 'PSP cancel result') +require(profile_cpp, 'kPspUtilityDialogResultAbort = 2u', 'LOAD compatibility abort result') require(ui_cpp, 'quad(v, 0, 0, 480, 23', 'PSP utility banner geometry') require(ui_cpp, 'quad(v, 180, 136, 480, 137', 'PSP save-info separator geometry') require(profile_cpp, 'slot.icon0_path = icon0.string();', 'savedata ICON0 path preserved') diff --git a/profiles/vcs/tests/check_v9_4_savedata_ui.py b/profiles/vcs/tests/check_v9_4_savedata_ui.py index b5ed516..f1ad805 100644 --- a/profiles/vcs/tests/check_v9_4_savedata_ui.py +++ b/profiles/vcs/tests/check_v9_4_savedata_ui.py @@ -10,13 +10,15 @@ force = (root/'FORCE_V9_4_SAVEDATA_UI_OBJECTS.bat').read_text(encoding='utf-8') errors=[] def req(text, needle, msg): if needle not in text: errors.append(msg) -req(profile, 'kUtilityCommonResultOffset, 2u', 'cancel common.result must be 2') -req(profile, 'kSavedataAbortStatusOffset, 1u', 'cancel abortStatus must be 1') +req(profile, 'load_cancel_workaround', 'mode-aware cancellation missing') +req(profile, 'kPspUtilityDialogResultCancel = 1u', 'PSP CANCEL constant missing') +req(profile, 'kPspUtilityDialogResultAbort = 2u', 'LOAD ABORT compatibility constant missing') req(ui, 'submit_smooth_text', 'smooth atlas text path missing') req(ui, 'draw_save_thumbnail(v, icons, 58.0f, 37.0f, 82.0f, 46.0f', 'previous slot safe row missing') req(ui, 'draw_save_thumbnail(v, icons, 58.0f, 183.0f, 82.0f, 46.0f', 'next slot safe row missing') req(ui, 'Draw text last so slot thumbnails can never cover labels or metadata.', 'text draw ordering guard missing') -req(build, 'FORCE_V9_4_SAVEDATA_UI_OBJECTS.bat', 'V9.4 force rebuild not wired') +if 'FORCE_V9_4_SAVEDATA_UI_OBJECTS.bat' not in build and 'FORCE_V9_5_HOST_TEXTURE_OBJECTS.bat' not in build: + errors.append('savedata UI object invalidation not wired') req(force, 'savedata_utility_ui.cpp.obj', 'UI object invalidation missing') req(force, 'vcs_profile.cpp.obj', 'profile object invalidation missing') if not atlas.is_file() or atlas.stat().st_size < 10000: @@ -26,7 +28,7 @@ if errors: for e in errors: print(' - '+e) sys.exit(1) print('V9.4 SAVEDATA UI AUDIT: PASS') -print(' - cancel is not reported to VCS as successful load') +print(' - cancel is mode-aware: LOAD compatibility abort, SAVE/DELETE PSP cancel') print(' - slot scroller stays between banner and footer') print(' - antialiased text atlas is drawn after slot/icon geometry') print(' - stale V9.3 UI/profile objects are force-invalidated once') diff --git a/profiles/vcs/tests/check_v9_6_savedata_cancel.py b/profiles/vcs/tests/check_v9_6_savedata_cancel.py new file mode 100644 index 0000000..c93c07e --- /dev/null +++ b/profiles/vcs/tests/check_v9_6_savedata_cancel.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +from pathlib import Path +import sys +root = Path(__file__).resolve().parents[1] +profile = (root / 'host/vcs_profile.cpp').read_text(encoding='utf-8', errors='replace') +build = (root / 'BUILD_VCS_NINJA.bat').read_text(encoding='utf-8', errors='replace') +force_path = root / 'FORCE_V9_6_SAVEDATA_CANCEL_OBJECTS.bat' +force = force_path.read_text(encoding='utf-8', errors='replace') if force_path.is_file() else '' +errors = [] +def req(text, needle, msg): + if needle not in text: + errors.append(msg) +req(profile, 'kPspUtilityDialogResultCancel = 1u', 'PSP CANCEL must be 1') +req(profile, 'kPspUtilityDialogResultAbort = 2u', 'PSP ABORT must be 2') +req(profile, 'const bool load_cancel_workaround = savedata_utility.mode == 4u', 'LISTLOAD compatibility policy missing') +req(profile, '? kPspUtilityDialogResultAbort', 'LOAD cancel must use ABORT compatibility result') +req(profile, ': kPspUtilityDialogResultCancel', 'SAVE/DELETE cancel must use PSP CANCEL') +req(profile, 'const std::uint32_t abort_status = load_cancel_workaround ? 1u : 0u;', 'SAVE/DELETE abortStatus must remain zero') +req(profile, 'policy=psp-user-cancel', 'physical SAVE cancel diagnostic missing') +req(build, 'FORCE_V9_6_SAVEDATA_CANCEL_OBJECTS.bat', 'V9.6 object invalidation not wired') +req(force, 'vcs_profile.cpp.obj', 'vcs_profile object invalidation missing') +if errors: + print('V9.6 SAVEDATA CANCEL AUDIT: FAIL') + for e in errors: + print(' - ' + e) + sys.exit(1) +print('V9.6 SAVEDATA CANCEL AUDIT: PASS') +print(' - LISTLOAD retains the physically-proven VCS abort compatibility path') +print(' - LISTSAVE/LISTDELETE use real PSP common.result=CANCEL (1), abortStatus=0') +print(' - vcs_profile.cpp is force-invalidated once for existing Ninja trees')