From c84cce0a914c078b5fce07b2995e82dc7ba3f9ef Mon Sep 17 00:00:00 2001 From: Myle Date: Thu, 8 Oct 2026 22:01:28 +0300 Subject: [PATCH] feat(libSceMsgDialog): implement interactive message dialogs --- .../Execution/include/Driver/Driver.hpp | 2 +- .../Execution/include/Presentation.hpp | 1 + .../Execution/include/VulkanDevice.hpp | 1 + .../Execution/src/Driver/EntryPoints.cpp | 4 + .../Execution/src/Driver/Presentation.cpp | 8 +- .../Execution/src/VulkanDevice.cpp | 4 + .../prx/libSceCommonDialog/CMakeLists.txt | 3 + core/libs/prx/libSceCommonDialog/Export.cpp | 241 ++++++++++++++++-- .../prx/libSceMsgDialog.native/CMakeLists.txt | 6 +- .../prx/libSceMsgDialog.native/Export.cpp | 91 +------ core/libs/prx/libSceMsgDialog/CMakeLists.txt | 14 + core/libs/prx/libSceMsgDialog/Export.cpp | 184 +------------ core/libs/prx/libSceMsgDialog/MsgDialog.hpp | 36 +++ core/libs/prx/libSceVideoOut/CMakeLists.txt | 39 ++- .../include/MessageDialogUI.hpp | 25 ++ .../prx/libSceVideoOut/include/PadInput.hpp | 6 + .../libSceVideoOut/include/VideoOutDriver.hpp | 4 +- .../libSceVideoOut/src/MessageDialogUI.cpp | 159 ++++++++++++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 9 +- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 35 ++- .../libSceVideoOut/tests/MessageDialog.cpp | 58 +++++ .../libSceVideoOut/tests/MessageDialogPad.cpp | 45 ++++ .../tests/MessageDialogPresentation.cpp | 52 ++++ core/libs/tests/GuestMsgDialogNative.cpp | 190 ++++++++++---- docs/dev/TechnicalDebt.md | 8 +- 25 files changed, 872 insertions(+), 353 deletions(-) create mode 100644 core/libs/prx/libSceCommonDialog/CMakeLists.txt create mode 100644 core/libs/prx/libSceMsgDialog/CMakeLists.txt create mode 100644 core/libs/prx/libSceMsgDialog/MsgDialog.hpp create mode 100644 core/libs/prx/libSceVideoOut/include/MessageDialogUI.hpp create mode 100644 core/libs/prx/libSceVideoOut/src/MessageDialogUI.cpp create mode 100644 core/libs/prx/libSceVideoOut/tests/MessageDialog.cpp create mode 100644 core/libs/prx/libSceVideoOut/tests/MessageDialogPad.cpp create mode 100644 core/libs/prx/libSceVideoOut/tests/MessageDialogPresentation.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index b1835b72866f..de8f8dcf9c8c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -53,7 +53,7 @@ class Driver { void SuspendPoint(); void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output); void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output); - void Present(const PresentationWindow& window, const DisplayBuffer* buffer, bool opaque, void (*gpuReady)(void*), void* context); + void Present(const PresentationWindow& window, const DisplayBuffer* buffer, bool opaque, void (*gpuReady)(void*), void* context, std::span dialogPixels = {}); void ReleaseWindow(void* window); void RegisterShader(const Shader* shader); void ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp index 88dd83eade26..e0ff918cdce9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp @@ -25,6 +25,7 @@ struct PresentationWindow { } +extern "C" void AgcDriverPresentDialog_nid_postfix(const AgcDriver::PresentationWindow& window, std::span pixels, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverPresentBuffer_nid_postfix(const AgcDriver::PresentationWindow& window, const AgcDriver::DisplayBuffer& buffer, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 696cfbdc159b..a7c3a8f113aa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -190,6 +190,7 @@ class VulkanDevice { // mutex (the same wait under it is only the fallback when the answer changes in between). bool PresentWaitsForSlots(const DisplayBuffer* buffer) const; bool AcquireImage(); + bool PresentDialog(std::span pixels); bool PresentClear(std::uint32_t width, std::uint32_t height, bool opaque); void PresentPixels(std::uint32_t width, std::uint32_t height, std::span pixels); bool PresentDisplayBuffer(const DisplayBuffer& buffer); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 683dfc3155fc..2cb3fa56ae0a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -120,3 +120,7 @@ extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, co extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) { AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsStagesAbi(stages, context, primitive); } + +extern "C" void AgcDriverPresentDialog_nid_postfix(const AgcDriver::PresentationWindow& window, std::span pixels, void (*gpuReady)(void*), void* context) { + AgcDriver::DriverDetail::Driver::Get().Present(window, nullptr, true, gpuReady, context, pixels); +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp index e42553239ef7..225e749413fd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp @@ -23,7 +23,7 @@ void Driver::UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr dialogPixels) { PerformanceContext timingContext(window.timing.get()); PerformanceTimer timing("Driver.Present"); @@ -75,7 +75,7 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff if (presentable && !syncFlip) { if (inFlight != 0) { - waitedMs = presenting->RetirePresents(presenting->PresentWaitsForSlots(buffer) ? 0 : inFlight); + waitedMs = presenting->RetirePresents((!dialogPixels.empty() || presenting->PresentWaitsForSlots(buffer)) ? 0 : inFlight); timing.Mark("inflight_wait"); } presentable = presenting->AcquireImage(); @@ -85,7 +85,9 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Present); std::lock_guard lock(GuestMemory::GpuMutex()); timing.Mark("gpu_mutex_wait"); - if (buffer != nullptr) { + if (!dialogPixels.empty()) { + submitted = presenting->PresentDialog(dialogPixels); + } else if (buffer != nullptr) { if (syncFlip) { presenting->WaitIdle(); timing.Mark("device_idle_wait"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 223e6903041f..f5c7fcd45e75 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1869,6 +1869,10 @@ bool VulkanDevice::Presentable() const { return state->extent.width != 0 && state->extent.height != 0; } +bool VulkanDevice::PresentDialog(std::span pixels) { + return present(1280, 720, true, pixels); +} + bool VulkanDevice::PresentClear(std::uint32_t width, std::uint32_t height, bool opaque) { APS5_LOG_OUT_DEBUG("PresentClear width=%u height=%u opaque=%u", width, height, static_cast(opaque)); return present(width, height, opaque, {}); diff --git a/core/libs/prx/libSceCommonDialog/CMakeLists.txt b/core/libs/prx/libSceCommonDialog/CMakeLists.txt new file mode 100644 index 000000000000..e1acb3d37be2 --- /dev/null +++ b/core/libs/prx/libSceCommonDialog/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(libSceCommonDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceCommonDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceCommonDialog) diff --git a/core/libs/prx/libSceCommonDialog/Export.cpp b/core/libs/prx/libSceCommonDialog/Export.cpp index 8fb415d4eba8..a2948534e20e 100644 --- a/core/libs/prx/libSceCommonDialog/Export.cpp +++ b/core/libs/prx/libSceCommonDialog/Export.cpp @@ -1,23 +1,228 @@ -#include -#include -#include "SceTypes.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" -#include "prx/libc/include/General.hpp" +#include "prx/libSceMsgDialog/MsgDialog.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#elif defined(__APPLE__) +#include +#include +#endif -static bool g_initialized = false; - -extern "C" { - -int APS5_VABI sceCommonDialogInitialize(void) { - if (g_initialized) { - return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; - } - g_initialized = true; - return COMMON_DIALOG_OK; +namespace { +constexpr int Error(unsigned n) { return static_cast(0x80B80000u+n); } +std::mutex mutex; +bool initialized=false, renderer=false; +int status=0; +MsgDialog::View view; +MsgDialogResult result{}; +std::uint64_t generation=0; +std::size_t Accessible(const void* p, bool writable=false) { + if (!p) return 0; + const auto address=reinterpret_cast(p); +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION info{}; + if (!VirtualQuery(p,&info,sizeof(info)) || info.State!=MEM_COMMIT || (info.Protect&(PAGE_NOACCESS|PAGE_GUARD))) return 0; + if (writable && !(info.Protect&(PAGE_READWRITE|PAGE_WRITECOPY|PAGE_EXECUTE_READWRITE|PAGE_EXECUTE_WRITECOPY))) return 0; + return reinterpret_cast(info.BaseAddress)+info.RegionSize-address; +#elif defined(__APPLE__) + mach_vm_address_t start=address; mach_vm_size_t size=0; + vm_region_basic_info_data_64_t info{}; mach_msg_type_number_t count=VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object{}; + const auto code=mach_vm_region(mach_task_self(),&start,&size,VM_REGION_BASIC_INFO_64,reinterpret_cast(&info),&count,&object); + if(object) mach_port_deallocate(mach_task_self(),object); + if(code!=KERN_SUCCESS || start>address || !(info.protection&VM_PROT_READ) || (writable && !(info.protection&VM_PROT_WRITE))) return 0; + return start+size-address; +#else + std::FILE* maps=std::fopen("/proc/self/maps","r"); + if (!maps) return 0; + char line[1024]; std::size_t available=0; + while(std::fgets(line,sizeof(line),maps)) { + unsigned long long begin=0,end=0; char flags[5]{}; + if(std::sscanf(line,"%llx-%llx %4s",&begin,&end,flags)==3 && address>=begin && address(p); + if(bytes>std::numeric_limits::max()-address) return false; + while(bytes) { + const auto n=std::min(bytes,Accessible(reinterpret_cast(address),writable)); + if(!n) return false; + address+=n; bytes-=n; + } + return true; } - +template int Read(const T* p,T& out) { + if(!p) return Error(13); + if(!Range(p,sizeof(T))) return Error(10); + std::memcpy(&out,p,sizeof(T)); return 0; +} +int Copy(const char* p,std::string& out) { + if (!p) return Error(13); + constexpr std::size_t limit=65536; + std::string next; + for(std::size_t offset=0;offset(std::memchr(p+offset,0,n)); + next.append(p+offset,end?static_cast(end-p-offset):n); + if(end) { out=std::move(next); return 0; } + offset+=n; + } + return Error(10); +} +void Finish(int button=0, int code=0) { + result={}; result.mode=view.mode; result.result=code; result.button_id=button; + status=3; view.phase=MsgDialog::Phase::Finished; +} +int Prepare(const MsgDialog::Param& p, MsgDialog::View& next) { + if (p.size!=sizeof(p) || p.base.size!=sizeof(p.base)) return Error(10); + next.mode=p.mode; + if (p.mode==1) { + MsgDialog::UserParam u{}; + if(const int err=Read(p.user,u)) return err; next.type=u.buttonType; + if (u.buttonType<0 || u.buttonType>9 || u.buttonType==4) return Error(10); + if (const int err=Copy(u.msg,next.message)) return err; + switch (u.buttonType) { + case 0: next.buttons={"OK",""}; next.count=1; break; + case 1: case 7: next.buttons={"Yes","No"}; next.count=2; break; + case 2: break; + case 3: case 8: next.buttons={"OK","Cancel"}; next.count=2; next.cancellable=true; break; + case 5: next.buttons={"Wait",""}; next.count=1; break; + case 6: next.buttons={"Wait","Cancel"}; next.count=2; next.cancellable=true; break; + case 9: { + MsgDialog::ButtonsParam buttons; + if(const int err=Read(u.buttons,buttons)) return err; + if (const int err=Copy(buttons.msg1,next.buttons[0])) return err; + if (const int err=Copy(buttons.msg2,next.buttons[1])) return err; + next.count=2; break; + } + } + next.selected=(u.buttonType==7 || u.buttonType==8)?1:0; + } else if (p.mode==2) { + MsgDialog::ProgressParam progress; + if(const int err=Read(p.progress,progress)) return err; + if (progress.barType<0 || progress.barType>1) return Error(10); + next.type=progress.barType; + if (const int err=Copy(progress.msg,next.message)) return err; + next.cancellable=next.type==1; + if (next.cancellable) { next.count=1; next.buttons[0]="Cancel"; } + } else if (p.mode==3) { + MsgDialog::SystemParam system; + if(const int err=Read(p.system,system)) return err; + next.type=system.type; + switch(next.type) { + case 0: next.message="There are no products available in the store."; break; + case 1: next.message="Communication features are restricted for this account."; break; + case 2: next.message="User-generated content is restricted for this account."; break; + case 4: next.message="The camera is not connected."; break; + case 5: return Error(15); + default: return Error(10); + } + next.count=1; next.buttons[0]="OK"; + } else return Error(10); + return 0; +} +} +extern "C" int APS5_VABI sceCommonDialogInitialize() { + std::lock_guard lock(mutex); + if (initialized) return Error(2); + initialized=true; return 0; +} +extern "C" bool APS5_VABI sceCommonDialogIsUsed() { + std::lock_guard lock(mutex); return status!=0; +} +extern "C" int MsgDialogCall_nid_no_patch(MsgDialog::Operation op,const void* p,int target,std::uint32_t value) { + using MsgDialog::Operation; + std::lock_guard lock(mutex); + try { + switch(op) { + case Operation::Initialize: + if (!initialized) return Error(1); + if (status!=0) return Error(4); + status=1; return 0; + case Operation::Terminate: + if (status==0) return Error(3); + status=0; view={}; ++generation; result={}; return 0; + case Operation::GetStatus: return status; + case Operation::GetResult: + if (status!=3) return Error(5); + if (!p) return Error(13); + if(!Range(p,sizeof(result),true)) return Error(10); + *static_cast(const_cast(p))=result; return 0; + case Operation::Open: { + if (status!=1 && status!=3) return Error(6); + MsgDialog::Param param; + if(const int err=Read(static_cast(p),param)) return err; + MsgDialog::View next; + if (const int err=Prepare(param,next)) return err; + if (!renderer) return Error(15); + next.generation=++generation; next.phase=MsgDialog::Phase::Open; + view=std::move(next); result={}; status=2; return 0; + } + case Operation::Close: + if (status!=2) return Error(11); + Finish(); return 0; + default: + if (status!=2) return Error(11); + if (view.mode!=2) return Error(15); + if (target!=0) return Error(10); + if (op==Operation::SetMessage) return Copy(static_cast(p),view.message); + if (op==Operation::SetValue) { + if (value>100) return Error(10); + view.progress=value; + } else if (op==Operation::Inc) { + view.progress+=std::min(value,100-view.progress); + } else return Error(15); + return 0; + } + } catch(const std::bad_alloc&) { return Error(9); } +} +extern "C" bool MsgDialogView_nid_no_patch(MsgDialog::View* out) { + std::lock_guard lock(mutex); + if (status!=2) { if (view.phase==MsgDialog::Phase::Finished) view.phase=MsgDialog::Phase::Closed; return false; } + *out=view; return true; +} +extern "C" void MsgDialogPresented_nid_no_patch(std::uint64_t token) { + std::lock_guard lock(mutex); + if (status==2 && token==view.generation) view.phase=MsgDialog::Phase::Running; +} +extern "C" void MsgDialogLayout_nid_no_patch(std::uint64_t token, int maxScroll) { + std::lock_guard lock(mutex); + if(status==2 && token==view.generation) { + view.maxScroll=std::max(0,maxScroll); + view.scroll=std::min(view.scroll,view.maxScroll); + } +} +extern "C" void MsgDialogInput_nid_no_patch(std::uint64_t token,MsgDialog::Action action) { + using MsgDialog::Action; + std::lock_guard lock(mutex); + if (status!=2 || token!=view.generation || view.phase!=MsgDialog::Phase::Running) return; + switch(action) { + case Action::Previous: case Action::Next: + if (view.count==2) view.selected=1-view.selected; break; + case Action::Cancel: + if (view.cancellable) Finish(0,1); break; + case Action::Confirm: + if (view.count==0) break; + if (view.mode==2 || (view.cancellable && view.selected==1)) Finish(0,1); + else Finish(view.selected+1); + break; + case Action::ScrollUp: view.scroll=std::max(0,view.scroll-1); break; + case Action::ScrollDown: view.scroll=std::min(view.maxScroll,view.scroll+1); break; + } +} +extern "C" void MsgDialogRenderer_nid_no_patch(bool available) { + std::lock_guard lock(mutex); renderer=available; + if (!available && status==2) Finish(0,1); } diff --git a/core/libs/prx/libSceMsgDialog.native/CMakeLists.txt b/core/libs/prx/libSceMsgDialog.native/CMakeLists.txt index deefade5ae00..37b9227d2ede 100644 --- a/core/libs/prx/libSceMsgDialog.native/CMakeLists.txt +++ b/core/libs/prx/libSceMsgDialog.native/CMakeLists.txt @@ -1,11 +1,13 @@ -add_library(libSceMsgDialog.native SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceMsgDialog.native SHARED EXCLUDE_FROM_ALL + Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libSceMsgDialog/Export.cpp) target_include_directories(libSceMsgDialog.native PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceMsgDialog.native PRIVATE libSceCommonDialog) configure_windows_unwind(libSceMsgDialog.native) if(BUILD_TESTING) add_test_executable(guest_msg_dialog_native_tests ${LIBS_INCLUDE_DIR}/tests/GuestMsgDialogNative.cpp) target_include_directories(guest_msg_dialog_native_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_msg_dialog_native_tests PRIVATE libSceMsgDialog.native) + target_link_libraries(guest_msg_dialog_native_tests PRIVATE libSceMsgDialog.native libSceCommonDialog) add_test(NAME guest_msg_dialog_native COMMAND guest_msg_dialog_native_tests) if(WIN32) set_tests_properties(guest_msg_dialog_native PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index edb76df0f719..9a430bf135d6 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -1,90 +1 @@ -#include -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -namespace { -std::atomic g_status{0}; - -constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; -constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; -constexpr int COMMON_DIALOG_RESULT_OK = 0; -constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); -constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); -constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); -constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); -constexpr int BUTTON_ID_OK = 1; -} - -extern "C" { - -int SceMsgDialogNativeModuleLoaded_nid_no_patch = 1; - -int APS5_VABI sceMsgDialogClose(void) { - int expected = COMMON_DIALOG_STATUS_RUNNING; - if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_FINISHED)) return COMMON_DIALOG_ERROR_NOT_RUNNING; - return 0; -} - -int APS5_VABI sceMsgDialogGetResult(MsgDialogResult* result) { - const int status = g_status.load(); - if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - *result = MsgDialogResult{}; - result->result = COMMON_DIALOG_RESULT_OK; - result->button_id = BUTTON_ID_OK; - return 0; -} - -int APS5_VABI sceMsgDialogInitialize(void) { - int expected = 0; - if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("sceMsgDialogInitialize: already initialized"); - return 0; -} - -int APS5_VABI sceMsgDialogOpen(const void* param) { - const int status = g_status.load(); - if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - g_status = COMMON_DIALOG_STATUS_FINISHED; - return 0; -} - -int APS5_VABI sceMsgDialogTerminate(void) { - int expected = 1; - if (g_status.compare_exchange_strong(expected, 0)) return 0; - expected = COMMON_DIALOG_STATUS_FINISHED; - if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("sceMsgDialogTerminate: not initialized or still running"); - return 0; -} - -int APS5_VABI sceMsgDialogUpdateStatus(void) { - return g_status.load(); -} - -int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta) { - (void)target; - (void)delta; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; -} - -int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg) { - (void)target; - (void)msg; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; -} - -int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { - (void)target; - (void)rate; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; -} - -int APS5_VABI sceMsgDialogGetStatus(void) { - return g_status.load(); -} - -} +extern "C" { int SceMsgDialogNativeModuleLoaded_nid_no_patch = 1; } diff --git a/core/libs/prx/libSceMsgDialog/CMakeLists.txt b/core/libs/prx/libSceMsgDialog/CMakeLists.txt new file mode 100644 index 000000000000..0d5b96f6e0f4 --- /dev/null +++ b/core/libs/prx/libSceMsgDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceMsgDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceMsgDialog PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceMsgDialog PRIVATE libSceCommonDialog) +configure_windows_unwind(libSceMsgDialog) +if(BUILD_TESTING) + add_test_executable(guest_msg_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestMsgDialogNative.cpp) + target_include_directories(guest_msg_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_msg_dialog_tests PRIVATE libSceMsgDialog libSceCommonDialog) + add_test(NAME guest_msg_dialog COMMAND guest_msg_dialog_tests) + if(WIN32) + set_tests_properties(guest_msg_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceMsgDialog/Export.cpp b/core/libs/prx/libSceMsgDialog/Export.cpp index 1e02b26eab30..1a3f47204c47 100644 --- a/core/libs/prx/libSceMsgDialog/Export.cpp +++ b/core/libs/prx/libSceMsgDialog/Export.cpp @@ -1,173 +1,15 @@ -// libSceMsgDialog: message dialog state machine (NONE -> INITIALIZED -> RUNNING -> FINISHED) without a visible dialog. -// The dialog "closes" with the affirmative button after a couple of status polls, so a title waiting for the user -// never blocks. The message text is logged for the first calls. -#include -#include -#include -#include -#include -#include -#include - -#include "SceTypes.hpp" -#include "HitLog.hpp" -#include "prx/libc/include/General.hpp" - -#ifdef _WIN32 -#include -#endif - -namespace { -constexpr int kStatusNone = 0; -constexpr int kStatusInitialized = 1; -constexpr int kStatusRunning = 2; -constexpr int kStatusFinished = 3; - -constexpr int kErrOk = 0; -constexpr int kErrNotInitialized = static_cast(0x80B80003u); -constexpr int kErrNotFinished = static_cast(0x80B80005u); -constexpr int kErrBusy = static_cast(0x80B80007u); -constexpr int kErrNotRunning = static_cast(0x80B8000Bu); -constexpr int kErrArgNull = static_cast(0x80B8000Du); - -constexpr int kModeUserMsg = 1; -constexpr int kModeProgressBar = 2; -constexpr int kModeSystemMsg = 3; - -constexpr int kButtonTypeOk = 0; -constexpr int kButtonTypeYesNo = 1; -constexpr int kButtonTypeNone = 2; -constexpr int kButtonTypeOkCancel = 3; -constexpr int kButtonTypeYesNoFocusNo = 4; - -constexpr int kButtonIdOk = 1; // OK / YES / BUTTON1 -constexpr int kButtonIdNo = 2; // NO / BUTTON2 -constexpr int kResultOk = 0; - -std::mutex g_lock; -int g_status = kStatusNone; -int g_mode = kModeUserMsg; -int g_buttonId = kButtonIdOk; -int g_pollsLeft = 0; -std::chrono::steady_clock::time_point g_openedAt; - -bool Readable(const void* p, std::size_t n) { -#ifdef _WIN32 - if (p == nullptr) return false; - MEMORY_BASIC_INFORMATION mbi; - if (VirtualQuery(p, &mbi, sizeof(mbi)) == 0 || mbi.State != MEM_COMMIT) return false; - if ((mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) return false; - const auto end = reinterpret_cast(mbi.BaseAddress) + mbi.RegionSize; - return reinterpret_cast(p) + n <= end; -#else - return p != nullptr; -#endif -} - -// Best-effort inspection of SceMsgDialogParam: { SceCommonDialogBaseParam base (0x30); size_t size; int mode (0x38); -// UserMessageParam* userMsg (0x40); ProgressBarParam* progress (0x48); SystemMessageParam* system (0x50); ... }. -void Inspect(const void* param) { - const std::uint8_t* bytes = static_cast(param); - g_mode = kModeUserMsg; - g_buttonId = kButtonIdOk; - if (!Readable(bytes, 0x60)) return; - std::int32_t mode = 0; - std::memcpy(&mode, bytes + 0x38, 4); - if (mode >= kModeUserMsg && mode <= kModeSystemMsg) g_mode = mode; - if (g_mode != kModeUserMsg) return; - const std::uint8_t* userMsg = nullptr; - std::memcpy(&userMsg, bytes + 0x40, 8); - if (!Readable(userMsg, 16)) return; - std::int32_t buttonType = 0; - std::memcpy(&buttonType, userMsg, 4); - const char* msg = nullptr; - std::memcpy(&msg, userMsg + 8, 8); - if (buttonType == kButtonTypeYesNoFocusNo) g_buttonId = kButtonIdNo; - if (Readable(msg, 4)) { - char text[161] = {}; - for (int i = 0; i < 160 && Readable(msg + i, 1) && msg[i] != '\0'; ++i) text[i] = (msg[i] >= 0x20 || msg[i] == '\n') ? msg[i] : '?'; - APS5_HIT("MSGDIALOG", "open: buttons=%d text=\"%s\" -> auto answer", buttonType, text); - } -} - -int Poll() { - std::lock_guard g(g_lock); - if (g_status == kStatusRunning) { - bool done = false; - if (g_mode == kModeProgressBar) { - // Progress dialogs are ended by the title (sceMsgDialogClose); the cap only prevents a wait without end. - done = std::chrono::steady_clock::now() - g_openedAt > std::chrono::seconds(30); - } else { - done = --g_pollsLeft <= 0; - } - if (done) g_status = kStatusFinished; - } - return g_status; -} -} // namespace - +#include "prx/libSceMsgDialog/MsgDialog.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +using MsgDialog::Operation; extern "C" { - -int APS5_VABI sceMsgDialogInitialize(void) { - std::lock_guard g(g_lock); - if (g_status == kStatusNone) g_status = kStatusInitialized; - APS5_HIT("MSGDIALOG", "sceMsgDialogInitialize"); - return kErrOk; -} -int APS5_VABI sceMsgDialogTerminate(void) { - std::lock_guard g(g_lock); - const bool was = g_status != kStatusNone; - g_status = kStatusNone; - return was ? kErrOk : kErrNotInitialized; -} -int APS5_VABI sceMsgDialogOpen(const void* param) { - if (param == nullptr) return kErrArgNull; - std::lock_guard g(g_lock); - if (g_status == kStatusRunning) return kErrBusy; - // A title that skipped sceMsgDialogInitialize still gets its dialog (and its FINISHED status). - Inspect(param); - g_pollsLeft = 2; - g_openedAt = std::chrono::steady_clock::now(); - g_status = kStatusRunning; - return kErrOk; -} -int APS5_VABI sceMsgDialogGetStatus(void) { - return Poll(); -} -int APS5_VABI sceMsgDialogUpdateStatus(void) { - return Poll(); -} -// SceMsgDialogResult: { int32 mode; int32 result; int32 buttonId; uint8 reserved[32] }. -int APS5_VABI sceMsgDialogGetResult(void* result) { - if (result == nullptr) return kErrArgNull; - std::lock_guard g(g_lock); - if (g_status != kStatusFinished) return kErrNotFinished; - std::int32_t out[3] = {g_mode, kResultOk, g_buttonId}; - std::memcpy(result, out, sizeof(out)); - return kErrOk; -} -int APS5_VABI sceMsgDialogClose(void) { - std::lock_guard g(g_lock); - if (g_status != kStatusRunning) return kErrNotRunning; - g_status = kStatusFinished; - return kErrOk; -} -int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta) { - (void)target; - (void)delta; - std::lock_guard g(g_lock); - return g_status == kStatusRunning ? kErrOk : kErrNotRunning; -} -int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg) { - (void)target; - (void)msg; - std::lock_guard g(g_lock); - return g_status == kStatusRunning ? kErrOk : kErrNotRunning; -} -int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { - (void)target; - (void)rate; - std::lock_guard g(g_lock); - return g_status == kStatusRunning ? kErrOk : kErrNotRunning; -} +int APS5_VABI sceMsgDialogInitialize() { return MsgDialogCall_nid_no_patch(Operation::Initialize,nullptr,0,0); } +int APS5_VABI sceMsgDialogTerminate() { return MsgDialogCall_nid_no_patch(Operation::Terminate,nullptr,0,0); } +int APS5_VABI sceMsgDialogOpen(const void* p) { return MsgDialogCall_nid_no_patch(Operation::Open,p,0,0); } +int APS5_VABI sceMsgDialogClose() { return MsgDialogCall_nid_no_patch(Operation::Close,nullptr,0,0); } +int APS5_VABI sceMsgDialogGetStatus() { return MsgDialogCall_nid_no_patch(Operation::GetStatus,nullptr,0,0); } +int APS5_VABI sceMsgDialogUpdateStatus() { return sceMsgDialogGetStatus(); } +int APS5_VABI sceMsgDialogGetResult(MsgDialogResult* p) { return MsgDialogCall_nid_no_patch(Operation::GetResult,p,0,0); } +int APS5_VABI sceMsgDialogProgressBarInc(int t, std::uint32_t d) { return MsgDialogCall_nid_no_patch(Operation::Inc,nullptr,t,d); } +int APS5_VABI sceMsgDialogProgressBarSetValue(int t, std::uint32_t v) { return MsgDialogCall_nid_no_patch(Operation::SetValue,nullptr,t,v); } +int APS5_VABI sceMsgDialogProgressBarSetMsg(int t, const char* p) { return MsgDialogCall_nid_no_patch(Operation::SetMessage,p,t,0); } } diff --git a/core/libs/prx/libSceMsgDialog/MsgDialog.hpp b/core/libs/prx/libSceMsgDialog/MsgDialog.hpp new file mode 100644 index 000000000000..e08b7443e82f --- /dev/null +++ b/core/libs/prx/libSceMsgDialog/MsgDialog.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include +#include +#include "SceTypes.hpp" + +namespace MsgDialog { +struct BaseParam { std::size_t size; std::array reserved; std::uint32_t magic; }; +struct ButtonsParam { const char* msg1; const char* msg2; std::array reserved; }; +struct UserParam { std::int32_t buttonType; std::int32_t padding; const char* msg; const ButtonsParam* buttons; std::array reserved; }; +struct ProgressParam { std::int32_t barType; std::int32_t padding; const char* msg; std::array reserved; }; +struct SystemParam { std::int32_t type; std::array reserved; }; +struct Param { + BaseParam base; std::size_t size; std::int32_t mode; std::int32_t padding; + const UserParam* user; const ProgressParam* progress; const SystemParam* system; + std::int32_t userId; std::array reserved; std::int32_t padding2; +}; +static_assert(sizeof(BaseParam)==48 && sizeof(Param)==136); +static_assert(offsetof(Param,mode)==0x38 && offsetof(Param,user)==0x40); +static_assert(sizeof(UserParam)==48 && sizeof(ProgressParam)==80 && sizeof(MsgDialogResult)==44); +enum class Operation { Initialize, Terminate, Open, Close, GetStatus, GetResult, Inc, SetValue, SetMessage }; +enum class Action { Previous, Next, Confirm, Cancel, ScrollUp, ScrollDown }; +enum class Phase { Closed, Open, Running, Finished }; +struct View { + std::uint64_t generation{}; Phase phase{Phase::Closed}; int mode{}, type{}, selected{}, count{}, scroll{}, maxScroll{}; + std::uint32_t progress{}; bool cancellable{}; + std::string message; std::array buttons; +}; +} +extern "C" int MsgDialogCall_nid_no_patch(MsgDialog::Operation, const void*, int, std::uint32_t); +extern "C" bool MsgDialogView_nid_no_patch(MsgDialog::View*); +extern "C" void MsgDialogPresented_nid_no_patch(std::uint64_t); +extern "C" void MsgDialogLayout_nid_no_patch(std::uint64_t, int); +extern "C" void MsgDialogInput_nid_no_patch(std::uint64_t, MsgDialog::Action); +extern "C" void MsgDialogRenderer_nid_no_patch(bool); diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 075b2aab4b18..49907e99aeac 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/Output.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/VideoOutDriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/DisplayWindow.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/MessageDialogUI.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/PadInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MouseInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardInput.cpp @@ -16,7 +17,7 @@ target_include_directories(libSceVideoOut PRIVATE ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include ) -target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceMouse libSceKeyboard libc libkernel SDL2-static) +target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceMouse libSceKeyboard libc libkernel SDL2-static libSceCommonDialog freetype) if(WIN32) target_link_libraries(libSceVideoOut PRIVATE comctl32) endif() @@ -62,3 +63,39 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(message_dialog_ui_tests tests/MessageDialog.cpp src/MessageDialogUI.cpp) + target_include_directories(message_dialog_ui_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) + target_link_libraries(message_dialog_ui_tests PRIVATE libSceCommonDialog SDL2-static freetype) + configure_windows_unwind(message_dialog_ui_tests) + add_test(NAME message_dialog_ui COMMAND message_dialog_ui_tests) + if(WIN32) + set_tests_properties(message_dialog_ui PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() +if(BUILD_TESTING) + add_test_executable(message_dialog_presentation_tests tests/MessageDialogPresentation.cpp src/MessageDialogUI.cpp) + target_include_directories(message_dialog_presentation_tests PRIVATE ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) + target_link_libraries(message_dialog_presentation_tests PRIVATE libSceCommonDialog libSceAgcDriver libc SDL2-static freetype) + configure_windows_unwind(message_dialog_presentation_tests) + add_test(NAME message_dialog_presentation COMMAND message_dialog_presentation_tests) + set_tests_properties(message_dialog_presentation PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(message_dialog_presentation PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() +if(BUILD_TESTING) + add_test_executable(message_dialog_pad_tests tests/MessageDialogPad.cpp src/PadInput.cpp) + target_include_directories(message_dialog_pad_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) + target_link_libraries(message_dialog_pad_tests PRIVATE libc SDL2-static) + configure_windows_unwind(message_dialog_pad_tests) + add_test(NAME message_dialog_pad COMMAND message_dialog_pad_tests) + if(WIN32) + set_tests_properties(message_dialog_pad PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVideoOut/include/MessageDialogUI.hpp b/core/libs/prx/libSceVideoOut/include/MessageDialogUI.hpp new file mode 100644 index 000000000000..3410ac2f8927 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/MessageDialogUI.hpp @@ -0,0 +1,25 @@ +#pragma once +#include "prx/libSceMsgDialog/MsgDialog.hpp" +#include "SDL_events.h" +#include +#include +#include +#include +class MessageDialogUI { +public: + MessageDialogUI(); + ~MessageDialogUI(); + bool HandleEvent(const SDL_Event&, std::uint32_t windowId); + std::span Draw(); + void Presented(); + bool Active() const { return active; } +private: + struct Font; + std::unique_ptr font; + MsgDialog::View view; + std::vector pixels; + bool active=false; + int horizontal=0, vertical=0; + void rect(int,int,int,int,unsigned); + void text(const std::string&,int,int,int,int,int clipTop=0); +}; diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index f0cbea89cb23..1c8c6a01b16e 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -17,8 +17,14 @@ class PadInput { ~PadInput(); void HandleEvent(const SDL_Event& event, DisplayWindow& window); void Update(); + void SetDialogActive(bool value) { + if (dialogActive && !value) dialogReleasePending = true; + dialogActive = value; publish(); + } private: + bool dialogActive = false; + bool dialogReleasePending = false; void publish(); void setMouseMode(bool enabled); void openFirstAvailableController(); diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 97c1ef47f99e..bc2e5a8cb7d8 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -17,6 +17,7 @@ #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libSceVideoOut/include/MessageDialogUI.hpp" #include "prx/libSceVideoOut/include/BufferReuseTracker.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -204,7 +205,7 @@ class VideoOutDriver { void presentLoop(std::stop_token token); void vblankLoop(std::stop_token token); void vblankEnd(); - void processFlip(FlipRequest& req); + void processFlip(FlipRequest& req, std::span dialogPixels); void triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData); std::mutex mutex; @@ -215,6 +216,7 @@ class VideoOutDriver { std::shared_ptr flipQueue = std::make_shared(); DisplayWindow window; + std::unique_ptr messageDialog; std::jthread presentThread; std::jthread vblankThread; diff --git a/core/libs/prx/libSceVideoOut/src/MessageDialogUI.cpp b/core/libs/prx/libSceVideoOut/src/MessageDialogUI.cpp new file mode 100644 index 000000000000..cdaacaa466ed --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/MessageDialogUI.cpp @@ -0,0 +1,159 @@ +#include "prx/libSceVideoOut/include/MessageDialogUI.hpp" +#include "SDL.h" +#include +#include FT_FREETYPE_H +#include +#include +#include +#include + +struct MessageDialogUI::Font { + FT_Library library{}; FT_Face face{}; + Font() { + if (FT_Init_FreeType(&library)) return; + std::vector paths; + if (const char* dir=std::getenv("ANYPS5_SYSTEM_FONTS")) { + paths.emplace_back(std::filesystem::path(dir)/"NotoSans-Regular.ttf"); + paths.emplace_back(std::filesystem::path(dir)/"NotoSans-Light.ttf"); + } + if (char* base=SDL_GetBasePath()) { + paths.emplace_back(std::filesystem::path(base)/"anyps5-fonts"/"NotoSans-Regular.ttf"); + SDL_free(base); + } +#ifdef _WIN32 + if (const char* windows=std::getenv("WINDIR")) paths.emplace_back(std::filesystem::path(windows)/"Fonts"/"segoeui.ttf"); +#elif defined(__APPLE__) + paths.emplace_back("/System/Library/Fonts/Supplemental/Arial.ttf"); +#else + paths.emplace_back("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"); + paths.emplace_back("/usr/share/fonts/truetype/open-sans/OpenSans-Regular.ttf"); +#endif + for (const auto& path:paths) if (!FT_New_Face(library,path.string().c_str(),0,&face)) break; + if (face) FT_Set_Pixel_Sizes(face,0,26); + } + ~Font() { if(face) FT_Done_Face(face); if(library) FT_Done_FreeType(library); } +}; +MessageDialogUI::MessageDialogUI():font(std::make_unique()),pixels(1280*720*4) { + if (!font->face) std::fprintf(stderr,"[msgdialog] No UI font; install NotoSans-Regular.ttf in anyps5-fonts (or ANYPS5_SYSTEM_FONTS). Dialog open will return NOT_SUPPORTED.\n"); + MsgDialogRenderer_nid_no_patch(font->face!=nullptr); +} +MessageDialogUI::~MessageDialogUI() { MsgDialogRenderer_nid_no_patch(false); } +void MessageDialogUI::rect(int x,int y,int w,int h,unsigned color) { + for(int row=std::max(0,y);row>8)&255); p[2]=std::byte((color>>16)&255); p[3]=std::byte{255}; + } +} +namespace { +unsigned Codepoint(const std::string& s, std::size_t& i) { + unsigned c=static_cast(s[i++]); + if(c<128) return c; + int n=(c&0xe0)==0xc0?1:(c&0xf0)==0xe0?2:(c&0xf8)==0xf0?3:0; + if(!n) return 0xfffd; + unsigned cp=c&((1u<<(6-n))-1); + for(int k=0;k=s.size() || (static_cast(s[i])&0xc0)!=0x80) return 0xfffd; + cp=(cp<<6)|(static_cast(s[i++])&63); + } + if(cp>0x10ffff || (cp>=0xd800 && cp<=0xdfff) || cp<(n==1?0x80u:n==2?0x800u:0x10000u)) return 0xfffd; + return cp; +} +int Lines(FT_Face face, const std::string& s,int width) { + int lines=1,pen=0; + for(std::size_t i=0;i(face->glyph->advance.x>>6); + if(pen+advance>width) { ++lines; pen=0; } + pen+=advance; + } + return lines; +} +} +void MessageDialogUI::text(const std::string& s,int x,int y,int width,int bottom,int clipTop) { + int pen=x, baseline=y+26; + for(std::size_t i=0;iface,c,FT_LOAD_RENDER)) continue; + const auto& g=*font->face->glyph; + const int advance=static_cast(g.advance.x>>6); + if(pen+advance>x+width) { pen=x; baseline+=36; } + if(baseline-26>=bottom) break; + for(unsigned row=0;row=1280 || py=bottom || py>=720) continue; + const auto alpha=g.bitmap.buffer[row*g.bitmap.pitch+col]; + auto* p=pixels.data()+(py*1280+px)*4; + for(int channel=0;channel<3;++channel) p[channel]=std::byte((std::to_integer(p[channel])*(255-alpha)+255*alpha)/255); + } + pen+=advance; + } +} +std::span MessageDialogUI::Draw() { + const auto previousGeneration=view.generation; + active=MsgDialogView_nid_no_patch(&view); + if(view.generation!=previousGeneration) { horizontal=0; vertical=0; } + if(!active || !font->face) return {}; + rect(0,0,1280,720,0x071428); + rect(96,132,1088,2,0x586981); + rect(96,560,1088,2,0x586981); + const int maxScroll=std::max(0,Lines(font->face,view.message,1024)-9); + MsgDialogLayout_nid_no_patch(view.generation,maxScroll); + view.scroll=std::min(view.scroll,maxScroll); + text(view.message,128,180-view.scroll*36,1024,510,180); + if(view.mode==2) { + rect(128,520,928,12,0x33445c); + rect(128,520,928*view.progress/100,12,0xddeaff); + text(std::to_string(view.progress)+"%",1080,508,100,548); + } + for(int i=0;i18000?1:e.caxis.value< -18000?-1:0; + if(next && next!=direction) { + send(e.caxis.axis==SDL_CONTROLLER_AXIS_LEFTX?(next>0?Action::Next:Action::Previous):(next>0?Action::ScrollDown:Action::ScrollUp)); + } + direction=next; return true; + } + return false; +} diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index ab3aa11a8e01..41f7b09ffb0a 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -263,10 +263,10 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { void PadInput::Update() { if (controller != nullptr) SDL_GameControllerUpdate(); applyOutput(); - if (controller != nullptr) { - controllerState = sampleController(); - publish(); - } + if (controller != nullptr) controllerState = sampleController(); + if (dialogReleasePending && controllerState.buttons == 0 && + std::none_of(pressed.begin(), pressed.end(), [](bool value) { return value; })) dialogReleasePending = false; + publish(); const auto now = std::chrono::steady_clock::now(); bool released = false; for (std::size_t index = 0; index < bindings.size(); ++index) { @@ -300,6 +300,7 @@ void PadInput::Update() { } void PadInput::publish() { + if (dialogActive || dialogReleasePending) { PadPublishInput_nid_postfix(PadInputState{}); return; } PadInputState state; state.buttons = controllerState.buttons; state.sticks = controllerState.sticks; diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 4c490cb77ae4..f4580287cecc 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -209,6 +209,7 @@ VideoOutDriver::VideoOutDriver() { } try { AgcDriverWaitIdle_nid_postfix(); + messageDialog = std::make_unique(); presentThread = std::jthread([this](std::stop_token token) { presentLoop(token); }); vblankThread = std::jthread([this](std::stop_token token) { vblankLoop(token); }); LibcRegisterShutdown_nid_postfix([] { VideoOutDriver::Get().Shutdown(); }); @@ -383,7 +384,7 @@ void VideoOutDriver::vblankEnd() { } } -void VideoOutDriver::processFlip(FlipRequest& req) { +void VideoOutDriver::processFlip(FlipRequest& req, std::span dialogPixels) { AgcDriver::PerformanceContext timingContext(req.timing.get()); AgcDriver::PerformanceTimer timing("VideoOut.Flip"); { @@ -432,12 +433,15 @@ void VideoOutDriver::processFlip(FlipRequest& req) { request.gpuComplete = true; request.cfg->vblankCond.notify_all(); }; - if (req.index >= 0) { + if (!dialogPixels.empty()) { + AgcDriverPresentDialog_nid_postfix(target, dialogPixels, gpuReady, &req); + } else if (req.index >= 0) { const auto display = DescribeVideoOutBuffer(req.buffer, req.group); AgcDriverPresentBuffer_nid_postfix(target, display, gpuReady, &req); } else { AgcDriverPresentClear_nid_postfix(target, req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, gpuReady, &req); } + messageDialog->Presented(); timing.Mark("present"); window.UpdateTitle(); timing.Mark("window_title"); @@ -496,18 +500,40 @@ void VideoOutDriver::presentLoop(std::stop_token token) { LibcRequestExit_nid_postfix(0); throw ProcessShutdown{}; } + const bool dialogEvent = messageDialog->HandleEvent(event, window.Handle() ? SDL_GetWindowID(window.Handle()) : 0); padInput.HandleEvent(event, window); - if (window.Handle() != nullptr) { + if (window.Handle() != nullptr && (!dialogEvent || event.type == SDL_KEYUP)) { mouseInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); keyboardInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); } } + const auto dialogPixels = messageDialog->Draw(); + padInput.SetDialogActive(!dialogPixels.empty()); padInput.Update(); + if (!current && !dialogPixels.empty()) { + window.Ensure(1280, 720); + unsigned count = 0; + if (!SDL_Vulkan_GetInstanceExtensions(window.Handle(), &count, nullptr)) throw std::runtime_error(SDL_GetError()); + std::vector extensions(count); + if (!SDL_Vulkan_GetInstanceExtensions(window.Handle(), &count, extensions.data())) throw std::runtime_error(SDL_GetError()); + const AgcDriver::PresentationWindow target{window.Handle(), extensions, + [](void* context, VkInstance instance) { + VkSurfaceKHR surface{}; + if (!SDL_Vulkan_CreateSurface(static_cast(context), instance, &surface)) throw std::runtime_error(SDL_GetError()); + return surface; + }, [](void* context, std::uint32_t* width, std::uint32_t* height) { + int w=0, h=0; + if (!(SDL_GetWindowFlags(static_cast(context)) & SDL_WINDOW_MINIMIZED)) SDL_Vulkan_GetDrawableSize(static_cast(context), &w, &h); + *width=std::max(w,0); *height=std::max(h,0); + }, 1280, 720, {}}; + AgcDriverPresentDialog_nid_postfix(target, dialogPixels, [](void*) {}, this); + messageDialog->Presented(); + } if (current) { require(current->timing != nullptr, "missing presentation timing"); const auto dequeued = AgcDriver::FrameTiming::Clock::now(); current->timing->Add(current->timing->Get("VideoOut", "queue"), dequeued - current->queuedAt); - processFlip(*current); + processFlip(*current, dialogPixels); const auto finished = AgcDriver::FrameTiming::Clock::now(); AgcDriver::FrameTiming::Clock::duration interval{}; { @@ -552,6 +578,7 @@ void VideoOutDriver::presentLoop(std::stop_token token) { cancelled.swap(flipQueue->requests); } } + MsgDialogRenderer_nid_no_patch(false); AgcDriverShutdown_nid_postfix(); window.Destroy(); } diff --git a/core/libs/prx/libSceVideoOut/tests/MessageDialog.cpp b/core/libs/prx/libSceVideoOut/tests/MessageDialog.cpp new file mode 100644 index 000000000000..9f4258aa4eca --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/MessageDialog.cpp @@ -0,0 +1,58 @@ +#include "prx/libSceVideoOut/include/MessageDialogUI.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SDL.h" +#include +#include +#define CHECK(expr) do { if(!(expr)) { std::fprintf(stderr,"line %d: %s\n",__LINE__,#expr); std::abort(); } } while(0) +extern "C" int APS5_VABI sceCommonDialogInitialize(); +using namespace MsgDialog; +int Call(Operation op,const void* p=nullptr) { return MsgDialogCall_nid_no_patch(op,p,0,0); } +int main() { + CHECK(sceCommonDialogInitialize()==0); + MessageDialogUI ui; + CHECK(Call(Operation::Initialize)==0); + MsgDialog::Param p{}; p.base.size=sizeof(p.base); p.size=sizeof(p); p.mode=1; + UserParam u{}; u.buttonType=1; u.msg="Choose an option: café, 日本語\nSecond line"; p.user=&u; + CHECK(Call(Operation::Open,&p)==0); + auto pixels=ui.Draw(); CHECK(pixels.size()==1280*720*4); + ui.Presented(); + const auto checksum=[](std::span frame) { + std::uint64_t value=1469598103934665603ull; + for(const auto byte:frame) value=(value^std::to_integer(byte))*1099511628211ull; + return value; + }; + const auto firstFrame=checksum(pixels); + if(const char* path=std::getenv("ANYPS5_MSG_DIALOG_SCREENSHOT")) { + auto* surface=SDL_CreateRGBSurfaceWithFormatFrom(const_cast(pixels.data()),1280,720,32,1280*4,SDL_PIXELFORMAT_BGRA32); + CHECK(surface && SDL_SaveBMP(surface,path)==0); SDL_FreeSurface(surface); + } + SDL_Event event{}; event.type=SDL_KEYDOWN; event.key.windowID=42; event.key.keysym.sym=SDLK_LEFT; + CHECK(!ui.HandleEvent(event,43)); + CHECK(ui.HandleEvent(event,42)); + View v; CHECK(MsgDialogView_nid_no_patch(&v) && v.selected==1); + CHECK(checksum(ui.Draw())!=firstFrame); + event.key.keysym.sym=SDLK_RETURN; event.key.repeat=1; + CHECK(ui.HandleEvent(event,42)); CHECK(Call(Operation::GetStatus)==2); + event.key.repeat=0; CHECK(ui.HandleEvent(event,42)); + MsgDialogResult r; CHECK(Call(Operation::GetResult,&r)==0 && r.button_id==2); + CHECK(ui.Draw().empty()); + u.buttonType=3; CHECK(Call(Operation::Open,&p)==0); CHECK(!ui.Draw().empty()); ui.Presented(); + event.key.keysym.sym=SDLK_ESCAPE; CHECK(ui.HandleEvent(event,42)); + CHECK(Call(Operation::GetResult,&r)==0 && r.result==1 && r.button_id==0); + CHECK(Call(Operation::Open,&p)==0); ui.Draw(); ui.Presented(); + event={}; event.type=SDL_CONTROLLERAXISMOTION; event.caxis.axis=SDL_CONTROLLER_AXIS_LEFTX; event.caxis.value=24000; + CHECK(ui.HandleEvent(event,42)); CHECK(ui.HandleEvent(event,42)); + CHECK(MsgDialogView_nid_no_patch(&v) && v.selected==1); + event={}; event.type=SDL_CONTROLLERBUTTONDOWN; event.cbutton.button=SDL_CONTROLLER_BUTTON_A; + CHECK(ui.HandleEvent(event,42)); CHECK(Call(Operation::GetResult,&r)==0 && r.result==1); + u.buttonType=0; CHECK(Call(Operation::Open,&p)==0); ui.Draw(); ui.Presented(); + event.cbutton.button=SDL_CONTROLLER_BUTTON_B; CHECK(ui.HandleEvent(event,42)); CHECK(Call(Operation::GetStatus)==2); + event.cbutton.button=SDL_CONTROLLER_BUTTON_A; CHECK(ui.HandleEvent(event,42)); CHECK(Call(Operation::GetStatus)==3); + std::string longText(4000,'x'); u.msg=longText.c_str(); CHECK(Call(Operation::Open,&p)==0); ui.Draw(); ui.Presented(); + event={}; event.type=SDL_KEYDOWN; event.key.windowID=42; event.key.keysym.sym=SDLK_DOWN; + CHECK(ui.HandleEvent(event,42)); CHECK(MsgDialogView_nid_no_patch(&v) && v.scroll==1); + for(int i=0;i<1000;++i) CHECK(ui.HandleEvent(event,42)); + CHECK(MsgDialogView_nid_no_patch(&v) && v.scroll==v.maxScroll); + CHECK(!ui.Draw().empty()); CHECK(Call(Operation::Close)==0); + CHECK(ui.Draw().empty()); CHECK(Call(Operation::Terminate)==0); +} diff --git a/core/libs/prx/libSceVideoOut/tests/MessageDialogPad.cpp b/core/libs/prx/libSceVideoOut/tests/MessageDialogPad.cpp new file mode 100644 index 000000000000..039f32e04df2 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/MessageDialogPad.cpp @@ -0,0 +1,45 @@ +#include "prx/libSceVideoOut/include/PadInput.hpp" +#include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "SDL.h" +#include +#include +#define CHECK(expr) do { if(!(expr)) { std::fprintf(stderr,"line %d: %s\n",__LINE__,#expr); std::abort(); } } while(0) +static PadInputState published; +extern "C" void PadPublishInput_nid_postfix(const PadInputState& state) { published=state; } +extern "C" bool PadFetchOutput_nid_postfix(std::uint32_t*,PadOutputState*) { return false; } +std::vector Pad::LoadInputMapping() { return {InputMapping.begin(),InputMapping.end()}; } +DisplayWindow::~DisplayWindow()=default; +SDL_Window* DisplayWindow::Handle() const { return nullptr; } +void DisplayWindow::ToggleFullscreen() {} +int main() { + CHECK(SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER)==0); + const int index=SDL_JoystickAttachVirtual(SDL_JOYSTICK_TYPE_GAMECONTROLLER,SDL_CONTROLLER_AXIS_MAX,SDL_CONTROLLER_BUTTON_MAX,0); + CHECK(index>=0 && SDL_IsGameController(index)); + auto* joystick=SDL_JoystickOpen(index); CHECK(joystick); + { + PadInput input; DisplayWindow window; + constexpr auto cross=static_cast(Pad::PadButton::Cross); + CHECK(SDL_JoystickSetVirtualButton(joystick,SDL_CONTROLLER_BUTTON_A,1)==0); + input.Update(); CHECK(published.buttons&cross); + input.SetDialogActive(true); CHECK(published.buttons==0); + input.Update(); CHECK(published.buttons==0); + input.SetDialogActive(false); CHECK(published.buttons==0); + SDL_Event e{}; e.type=SDL_CONTROLLERBUTTONDOWN; e.cbutton.button=SDL_CONTROLLER_BUTTON_A; + input.HandleEvent(e,window); CHECK(published.buttons==0); + input.Update(); CHECK(published.buttons==0); + CHECK(SDL_JoystickSetVirtualButton(joystick,SDL_CONTROLLER_BUTTON_A,0)==0); + input.Update(); CHECK(published.buttons==0); + CHECK(SDL_JoystickSetVirtualButton(joystick,SDL_CONTROLLER_BUTTON_A,1)==0); + input.Update(); CHECK(published.buttons&cross); + CHECK(SDL_JoystickSetVirtualButton(joystick,SDL_CONTROLLER_BUTTON_A,0)==0); + input.Update(); + input.SetDialogActive(true); + e={}; e.type=SDL_KEYDOWN; e.key.keysym.scancode=SDL_SCANCODE_RETURN; + input.HandleEvent(e,window); CHECK(published.buttons==0); + input.SetDialogActive(false); input.Update(); CHECK(published.buttons==0); + e.type=SDL_KEYUP; input.HandleEvent(e,window); input.Update(); CHECK(published.buttons==0); + e.type=SDL_KEYDOWN; input.HandleEvent(e,window); CHECK(published.buttons&cross); + } + SDL_JoystickClose(joystick); CHECK(SDL_JoystickDetachVirtual(index)==0); + SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); +} diff --git a/core/libs/prx/libSceVideoOut/tests/MessageDialogPresentation.cpp b/core/libs/prx/libSceVideoOut/tests/MessageDialogPresentation.cpp new file mode 100644 index 000000000000..1427813e2dec --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/MessageDialogPresentation.cpp @@ -0,0 +1,52 @@ +#include "prx/libSceVideoOut/include/MessageDialogUI.hpp" +#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "SDL.h" +#include "SDL_vulkan.h" +#include +#include +#include +extern "C" int APS5_VABI sceCommonDialogInitialize(); +int main() { + if(SDL_InitSubSystem(SDL_INIT_VIDEO)!=0) { std::fprintf(stderr,"SKIP: SDL video: %s\n",SDL_GetError()); return 77; } + auto* window=SDL_CreateWindow("AnyPS5 message dialog integration",0,0,1280,720,SDL_WINDOW_VULKAN|SDL_WINDOW_HIDDEN); + if(!window) { std::fprintf(stderr,"SKIP: SDL Vulkan window: %s\n",SDL_GetError()); SDL_QuitSubSystem(SDL_INIT_VIDEO); return 77; } + try { + sceCommonDialogInitialize(); + MessageDialogUI ui; + using MsgDialog::Operation; + const auto call=[](Operation op,const void* p=nullptr) { return MsgDialogCall_nid_no_patch(op,p,0,0); }; + if(call(Operation::Initialize)) throw std::runtime_error("initialize failed"); + MsgDialog::Param p{}; p.size=sizeof(p); p.base.size=sizeof(p.base); p.mode=1; + MsgDialog::UserParam u{}; u.msg="Message dialog presentation test"; p.user=&u; + if(call(Operation::Open,&p)) throw std::runtime_error("open failed"); + unsigned count=0; + if(!SDL_Vulkan_GetInstanceExtensions(window,&count,nullptr)) throw std::runtime_error(SDL_GetError()); + std::vector extensions(count); + if(!SDL_Vulkan_GetInstanceExtensions(window,&count,extensions.data())) throw std::runtime_error(SDL_GetError()); + const AgcDriver::PresentationWindow target{window,extensions, + [](void* c,VkInstance instance) { + VkSurfaceKHR surface{}; + if(!SDL_Vulkan_CreateSurface(static_cast(c),instance,&surface)) throw std::runtime_error(SDL_GetError()); + return surface; + }, [](void* c,std::uint32_t* w,std::uint32_t* h) { + int width=0,height=0; SDL_Vulkan_GetDrawableSize(static_cast(c),&width,&height); + *w=width; *h=height; + },1280,720,{}}; + bool ready=false; + AgcDriverPresentDialog_nid_postfix(target,ui.Draw(),[](void* c) { *static_cast(c)=true; },&ready); + if(!ready) throw std::runtime_error("GPU callback missing"); + ui.Presented(); + SDL_Event event{}; event.type=SDL_KEYDOWN; event.key.windowID=SDL_GetWindowID(window); event.key.keysym.sym=SDLK_RETURN; + ui.HandleEvent(event,event.key.windowID); + MsgDialogResult result{}; + if(call(Operation::GetResult,&result) || result.button_id!=1) throw std::runtime_error("selection not returned"); + call(Operation::Terminate); + AgcDriverReleaseWindow_nid_postfix(window); + AgcDriverShutdown_nid_postfix(); + SDL_DestroyWindow(window); SDL_QuitSubSystem(SDL_INIT_VIDEO); + LibcRunShutdown_nid_postfix(); return 0; + } catch(const std::exception& error) { std::fprintf(stderr,"%s\n",error.what()); return 1; } +} diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index dd43d10b4cb3..9d4819d26e1e 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -1,62 +1,140 @@ -#include "SceTypes.hpp" +#include "prx/libSceMsgDialog/MsgDialog.hpp" #include "prx/libc/include/general/VabiMacros.hpp" -#include #include - +#include +#include +#include extern "C" { -int APS5_VABI sceMsgDialogInitialize(void); -int APS5_VABI sceMsgDialogOpen(const void* param); -int APS5_VABI sceMsgDialogGetStatus(void); -int APS5_VABI sceMsgDialogUpdateStatus(void); -int APS5_VABI sceMsgDialogGetResult(MsgDialogResult* result); -int APS5_VABI sceMsgDialogClose(void); -int APS5_VABI sceMsgDialogTerminate(void); -int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta); -int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg); -int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate); +int APS5_VABI sceCommonDialogInitialize(); +bool APS5_VABI sceCommonDialogIsUsed(); +int APS5_VABI sceMsgDialogInitialize(); +int APS5_VABI sceMsgDialogOpen(const void*); +int APS5_VABI sceMsgDialogGetStatus(); +int APS5_VABI sceMsgDialogUpdateStatus(); +int APS5_VABI sceMsgDialogGetResult(MsgDialogResult*); +int APS5_VABI sceMsgDialogClose(); +int APS5_VABI sceMsgDialogTerminate(); +int APS5_VABI sceMsgDialogProgressBarInc(int,std::uint32_t); +int APS5_VABI sceMsgDialogProgressBarSetValue(int,std::uint32_t); +int APS5_VABI sceMsgDialogProgressBarSetMsg(int,const char*); } - -namespace { - -void Require(bool value) { if (!value) std::abort(); } - -constexpr int kStatusInitialized = 1; -constexpr int kStatusFinished = 3; -constexpr int kErrNotRunning = static_cast(0x80B8000Bu); -constexpr int kButtonIdOk = 1; - -} - +#define CHECK(expr) do { if(!(expr)) { std::fprintf(stderr,"line %d: %s\n",__LINE__,#expr); std::abort(); } } while(0) +constexpr int E(unsigned n) { return static_cast(0x80B80000u+n); } +using namespace MsgDialog; int main() { - std::uint8_t param[0x60]{}; - - Require(sceMsgDialogClose() == kErrNotRunning); - Require(sceMsgDialogUpdateStatus() == 0); - Require(sceMsgDialogGetStatus() == 0); - - Require(sceMsgDialogInitialize() == 0); - Require(sceMsgDialogClose() == kErrNotRunning); - Require(sceMsgDialogUpdateStatus() == kStatusInitialized); - Require(sceMsgDialogGetStatus() == kStatusInitialized); - - Require(sceMsgDialogProgressBarSetValue(0, 50) == kErrNotRunning); - Require(sceMsgDialogOpen(param) == 0); - Require(sceMsgDialogUpdateStatus() == kStatusFinished); - Require(sceMsgDialogGetStatus() == kStatusFinished); - Require(sceMsgDialogProgressBarInc(0, 10) == kErrNotRunning); - Require(sceMsgDialogProgressBarSetMsg(0, "progress") == kErrNotRunning); - Require(sceMsgDialogProgressBarSetValue(0, 100) == kErrNotRunning); - Require(sceMsgDialogClose() == kErrNotRunning); - Require(sceMsgDialogUpdateStatus() == kStatusFinished); - Require(sceMsgDialogGetStatus() == kStatusFinished); - - MsgDialogResult result{}; - result.button_id = -1; - Require(sceMsgDialogGetResult(&result) == 0); - Require(result.result == 0); - Require(result.button_id == kButtonIdOk); - - Require(sceMsgDialogTerminate() == 0); - Require(sceMsgDialogClose() == kErrNotRunning); - Require(sceMsgDialogGetStatus() == 0); + MsgDialog::Param p{}; p.size=sizeof(p); p.base.size=sizeof(p.base); p.mode=1; + UserParam u{}; u.msg="Message"; p.user=&u; + CHECK(sceMsgDialogGetStatus()==0); + CHECK(sceMsgDialogInitialize()==E(1)); + CHECK(sceMsgDialogOpen(&p)==E(6)); + CHECK(sceMsgDialogClose()==E(11)); + CHECK(sceMsgDialogTerminate()==E(3)); + CHECK(sceMsgDialogGetResult(nullptr)==E(5)); + CHECK(sceCommonDialogInitialize()==0); + CHECK(sceCommonDialogInitialize()==E(2)); + CHECK(sceMsgDialogInitialize()==0); + CHECK(sceCommonDialogIsUsed()); + CHECK(sceMsgDialogInitialize()==E(4)); + CHECK(sceMsgDialogOpen(nullptr)==E(13)); + CHECK(sceMsgDialogOpen(reinterpret_cast(1))==E(10)); + CHECK(sceMsgDialogOpen(&p)==E(15)); + MsgDialogRenderer_nid_no_patch(true); + p.size=0; CHECK(sceMsgDialogOpen(&p)==E(10)); p.size=sizeof(p); + p.base.size=0; CHECK(sceMsgDialogOpen(&p)==E(10)); p.base.size=sizeof(p.base); + p.mode=0; CHECK(sceMsgDialogOpen(&p)==E(10)); p.mode=1; + p.user=nullptr; CHECK(sceMsgDialogOpen(&p)==E(13)); p.user=&u; + u.msg=reinterpret_cast(1); CHECK(sceMsgDialogOpen(&p)==E(10)); + u.msg=nullptr; CHECK(sceMsgDialogOpen(&p)==E(13)); u.msg="Message"; + u.buttonType=4; CHECK(sceMsgDialogOpen(&p)==E(10)); + u.buttonType=10; CHECK(sceMsgDialogOpen(&p)==E(10)); + ButtonsParam b{}; b.msg1="Continue"; b.msg2="Back"; u.buttons=&b; + MsgDialogResult r{}; + std::uint64_t stale=0; + for(int type: {0,1,2,3,5,6,7,8,9}) { + u.buttonType=type; + CHECK(sceMsgDialogOpen(&p)==0); + CHECK(sceMsgDialogOpen(&p)==E(6)); + CHECK(sceMsgDialogGetResult(&r)==E(5)); + CHECK(sceMsgDialogProgressBarSetValue(0,20)==E(15)); + View v; CHECK(MsgDialogView_nid_no_patch(&v)); + CHECK(v.phase==Phase::Open); + CHECK(v.selected==((type==7 || type==8)?1:0)); + MsgDialogInput_nid_no_patch(v.generation,Action::Confirm); + CHECK(sceMsgDialogGetStatus()==2); + MsgDialogPresented_nid_no_patch(v.generation); + MsgDialogInput_nid_no_patch(stale,Action::Confirm); + CHECK(sceMsgDialogGetStatus()==2); + for(int i=0;i<200;++i) CHECK(sceMsgDialogUpdateStatus()==2); + MsgDialogInput_nid_no_patch(v.generation,Action::Cancel); + if(v.cancellable) { + CHECK(sceMsgDialogGetStatus()==3); + CHECK(sceMsgDialogGetResult(&r)==0 && r.result==1 && r.button_id==0); + } else { + CHECK(sceMsgDialogGetStatus()==2); + MsgDialogInput_nid_no_patch(v.generation,Action::Confirm); + if(type==2) { CHECK(sceMsgDialogGetStatus()==2); CHECK(sceMsgDialogClose()==0); } + CHECK(sceMsgDialogGetResult(&r)==0); + CHECK(r.mode==1 && r.result==0); + CHECK(r.button_id==(type==2?0:type==7?2:1)); + } + CHECK(sceMsgDialogGetResult(nullptr)==E(13)); + CHECK(sceMsgDialogGetResult(reinterpret_cast(1))==E(10)); + CHECK(sceMsgDialogClose()==E(11)); + for(char c:r.reserved) CHECK(c==0); + CHECK(!MsgDialogView_nid_no_patch(&v)); + stale=v.generation; + } + for(int type: {1,3,6,7,8,9}) { + u.buttonType=type; CHECK(sceMsgDialogOpen(&p)==0); + View v; CHECK(MsgDialogView_nid_no_patch(&v)); MsgDialogPresented_nid_no_patch(v.generation); + if(v.selected==0) MsgDialogInput_nid_no_patch(v.generation,Action::Next); + MsgDialogInput_nid_no_patch(v.generation,Action::Confirm); + CHECK(sceMsgDialogGetResult(&r)==0); + const bool cancel=type==3 || type==6 || type==8; + CHECK(r.result==(cancel?1:0) && r.button_id==(cancel?0:2)); + } + u.buttonType=9; u.buttons=nullptr; CHECK(sceMsgDialogOpen(&p)==E(13)); u.buttons=&b; + b.msg2=nullptr; CHECK(sceMsgDialogOpen(&p)==E(13)); b.msg2="Back"; + std::string oversized(65536,'a'); u.msg=oversized.c_str(); CHECK(sceMsgDialogOpen(&p)==E(10)); + std::string owned="Owned message"; u.msg=owned.c_str(); u.buttonType=0; + CHECK(sceMsgDialogOpen(&p)==0); owned.assign("Changed"); + View v; CHECK(MsgDialogView_nid_no_patch(&v)); CHECK(v.message=="Owned message"); + CHECK(sceMsgDialogClose()==0); u.msg="Message"; + ProgressParam progress{}; progress.msg="Loading"; p.mode=2; p.progress=&progress; + for(int type:{0,1}) { + progress.barType=type; CHECK(sceMsgDialogOpen(&p)==0); + CHECK(sceMsgDialogProgressBarSetValue(1,50)==E(10)); + CHECK(sceMsgDialogProgressBarSetValue(0,101)==E(10)); + CHECK(sceMsgDialogProgressBarSetMsg(0,nullptr)==E(13)); + CHECK(sceMsgDialogProgressBarSetValue(0,40)==0); + CHECK(sceMsgDialogProgressBarInc(0,std::numeric_limits::max())==0); + CHECK(sceMsgDialogProgressBarSetMsg(0,"Complete")==0); + CHECK(MsgDialogView_nid_no_patch(&v)); CHECK(v.progress==100 && v.message=="Complete"); + MsgDialogPresented_nid_no_patch(v.generation); + CHECK(sceMsgDialogUpdateStatus()==2); + MsgDialogInput_nid_no_patch(v.generation,Action::Cancel); + if(type==0) { CHECK(sceMsgDialogGetStatus()==2); CHECK(sceMsgDialogClose()==0); } + CHECK(sceMsgDialogGetResult(&r)==0 && r.result==type && r.button_id==0); + } + progress.barType=2; CHECK(sceMsgDialogOpen(&p)==E(10)); + SystemParam system{}; p.mode=3; p.system=&system; + for(int type:{0,1,2,4}) { + system.type=type; CHECK(sceMsgDialogOpen(&p)==0); + CHECK(MsgDialogView_nid_no_patch(&v)); CHECK(!v.message.empty()); + MsgDialogPresented_nid_no_patch(v.generation); MsgDialogInput_nid_no_patch(v.generation,Action::Confirm); + CHECK(sceMsgDialogGetResult(&r)==0 && r.button_id==1); + } + system.type=3; CHECK(sceMsgDialogOpen(&p)==E(10)); + system.type=5; CHECK(sceMsgDialogOpen(&p)==E(15)); + p.mode=1; u.buttonType=0; CHECK(sceMsgDialogOpen(&p)==0); + CHECK(MsgDialogView_nid_no_patch(&v)); + std::thread poller([]{ for(int i=0;i<10000;++i) { int s=sceMsgDialogGetStatus(); CHECK(s==0 || s==2 || s==3); } }); + std::thread input([&]{ for(int i=0;i<1000;++i) { MsgDialogPresented_nid_no_patch(v.generation); MsgDialogInput_nid_no_patch(v.generation,Action::Confirm); } }); + CHECK(sceMsgDialogTerminate()==0); poller.join(); input.join(); + CHECK(!sceCommonDialogIsUsed()); CHECK(sceMsgDialogGetStatus()==0); + CHECK(sceMsgDialogProgressBarInc(0,1)==E(11)); + CHECK(sceMsgDialogInitialize()==0); CHECK(sceMsgDialogOpen(&p)==0); + MsgDialogRenderer_nid_no_patch(false); + CHECK(sceMsgDialogGetResult(&r)==0 && r.result==1); + CHECK(sceMsgDialogTerminate()==0); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 371c4318bcfa..a167b0669a62 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -12,8 +12,6 @@ Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: - [libSceSaveDataDialog.native](../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Progress-bar mode (5) stays running until the caller closes it, matching Skyrim PPSA03747's save sequence; other modes still finish immediately and return button 1 and the first nonempty directory name. Console dialog timing and selection behavior remain unverified. -- [libSceMsgDialog.native](../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog -- [libSceCommonDialog](../../core/libs/prx/libSceCommonDialog/Export.cpp) - [libSceErrorDialog](../../core/libs/prx/libSceErrorDialog/Export.cpp) - the error dialog runs the state machine but shows nothing; the error code passed to `sceErrorDialogOpen` is only logged - [libSceLoginDialog](../../core/libs/prx/libSceLoginDialog/Export.cpp) - the login dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled - [libSceHmd](../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. @@ -342,3 +340,9 @@ Throughout the project, every function at every stage either **does exactly what - [Guest path resolution](../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. - [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require one call per ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and one later `s_setpc_b64` return reading that exact pair. Regions must nest or be disjoint; recursion, shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. - [s_call_b64](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Native RDNA2/PS5 execution and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. + +### Message dialogs + +- [Message dialogs](../../core/libs/prx/libSceMsgDialog/MsgDialog.hpp) share state between the standard and native libraries. PS5 hardware behavior, base magic and user-id validation, and arbitration with other common-dialog libraries remain unverified. +- System message type 5 (profile picture/name sharing warning) returns `NOT_SUPPORTED`: console privacy/profile settings and their follow-up actions are unavailable. Reserved enum holes are rejected, rather than made into new dialog types. +- The [SDL/Vulkan presenter](../../core/libs/prx/libSceVideoOut/src/MessageDialogUI.cpp) uses host fonts; console font coverage and complex-script shaping are not implemented. Restoring the guest image after dismissal without a new guest flip remains unsupported. A missing presenter or UI font returns `NOT_SUPPORTED`.