From 6c5686d7ea9fef28cdace9dd46ead8261ae4a7b6 Mon Sep 17 00:00:00 2001 From: Rick <99503318+0xRickx@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:46:40 +0200 Subject: [PATCH] feat(libSceImeDialog): take the dialog's text from the host keyboard sceImeDialogInit wrote "Slayer" into an empty text buffer and the dialog finished on the second status poll, so a title never got text from the user: ProsperoRadio's search typed "Slayer" by itself and found no station. The dialog now runs until the user answers on the host keyboard, the way #4756 made sceImeOpen's panel-less IME take it. While no IME panel is open, libSceIme hands the game window's text and editing keys to a sink that the dialog registers. Typing edits the text up to maxTextLength without splitting a surrogate pair, Enter writes it to the title's buffer and finishes with end status 0, and Escape finishes with 1. Abort still finishes with 2. A second Init reports busy, and a null parameter or buffer or a maximum length outside 1 to 2048 is refused, as in shadPS4. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceIme/Export.cpp | 26 ++- core/libs/prx/libSceIme/include/ImeInput.hpp | 5 + core/libs/prx/libSceImeDialog/CMakeLists.txt | 17 ++ core/libs/prx/libSceImeDialog/Export.cpp | 153 +++++++++++++----- core/libs/tests/GuestImeDialog.cpp | 158 +++++++++++++++++++ docs/dev/debt/libSceImeDialog.md | 9 ++ 6 files changed, 329 insertions(+), 39 deletions(-) create mode 100644 core/libs/prx/libSceImeDialog/CMakeLists.txt create mode 100644 core/libs/tests/GuestImeDialog.cpp create mode 100644 docs/dev/debt/libSceImeDialog.md diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index d686445211..0a3fa7d94f 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -67,6 +68,7 @@ std::mutex g_keyboardMutex; std::set g_openKeyboards; std::mutex g_imeMutex; std::optional g_session; +std::atomic g_textSink{nullptr}; void PanelSize(const Param& param, uint32_t* width, uint32_t* height) { const uint32_t scale = (param.option & OptionUseOver2K) != 0 ? 2 : 1; @@ -284,13 +286,31 @@ int ValidateOpen(const Param* param, const ExtendedParam* extended) { extern "C" { void ImePublishInput_nid_postfix(const ImeHostInput& input) { - std::lock_guard lock(g_imeMutex); - if (g_session) Apply(*g_session, input); + { + std::lock_guard lock(g_imeMutex); + if (g_session) { + Apply(*g_session, input); + return; + } + } + const ImeHostTextSink sink = g_textSink.load(); + if (sink == nullptr) return; + std::u16string units; + if (input.kind == ImeHostInputKind::Text) { + std::size_t size = 0; + while (size < sizeof(input.text) && input.text[size] != '\0') ++size; + units = DecodeUtf8(input.text, size); + } + sink(input.kind, units.data(), units.size()); } bool ImeCapturesKeyboard_nid_postfix() { std::lock_guard lock(g_imeMutex); - return g_session.has_value(); + return g_session.has_value() || g_textSink.load() != nullptr; +} + +void ImeSetHostTextSink_nid_no_patch(ImeHostTextSink sink) { + g_textSink = sink; } int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended) { diff --git a/core/libs/prx/libSceIme/include/ImeInput.hpp b/core/libs/prx/libSceIme/include/ImeInput.hpp index 4212dd61ea..3d27316f20 100644 --- a/core/libs/prx/libSceIme/include/ImeInput.hpp +++ b/core/libs/prx/libSceIme/include/ImeInput.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP #define CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP +#include #include enum class ImeHostInputKind : std::uint32_t { @@ -23,4 +24,8 @@ struct ImeHostInput { extern "C" void ImePublishInput_nid_postfix(const ImeHostInput& input); extern "C" bool ImeCapturesKeyboard_nid_postfix(); +using ImeHostTextSink = void (*)(ImeHostInputKind kind, const char16_t* text, std::size_t length); + +extern "C" void ImeSetHostTextSink_nid_no_patch(ImeHostTextSink sink); + #endif diff --git a/core/libs/prx/libSceImeDialog/CMakeLists.txt b/core/libs/prx/libSceImeDialog/CMakeLists.txt new file mode 100644 index 0000000000..69efa4be3f --- /dev/null +++ b/core/libs/prx/libSceImeDialog/CMakeLists.txt @@ -0,0 +1,17 @@ +add_library(libSceImeDialog SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +target_include_directories(libSceImeDialog PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceImeDialog PRIVATE libSceIme) +configure_windows_unwind(libSceImeDialog) + +if(BUILD_TESTING) + add_test_executable(guest_ime_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeDialog.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/ImeTextInput.cpp) + target_include_directories(guest_ime_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(guest_ime_dialog_tests PRIVATE libSceImeDialog libSceIme libc) + configure_windows_unwind(guest_ime_dialog_tests) + add_test(NAME guest_ime_dialog COMMAND guest_ime_dialog_tests) + if(WIN32) + set_tests_properties(guest_ime_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceImeDialog/Export.cpp b/core/libs/prx/libSceImeDialog/Export.cpp index 84fe1c7e23..71df834abb 100644 --- a/core/libs/prx/libSceImeDialog/Export.cpp +++ b/core/libs/prx/libSceImeDialog/Export.cpp @@ -1,9 +1,11 @@ -#include +#include #include #include -#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" namespace { @@ -11,21 +13,105 @@ constexpr int StatusNone = 0; constexpr int StatusRunning = 1; constexpr int StatusFinished = 2; constexpr std::uint32_t EndStatusOk = 0; +constexpr std::uint32_t EndStatusUserCanceled = 1; constexpr std::uint32_t EndStatusAborted = 2; +constexpr std::uint32_t MaxTextLength = 2048; +constexpr int ErrorBusy = static_cast(0x80bc0001u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); +constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); +constexpr int ErrorInvalidInputTextBuffer = static_cast(0x80bc0017u); +constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); -std::atomic status{StatusNone}; -std::atomic polls{0}; -std::atomic endStatus{EndStatusOk}; +struct Dialog { + int status = StatusNone; + std::uint32_t endStatus = EndStatusOk; + char16_t* buffer = nullptr; + std::uint32_t maxLength = 0; + std::u16string text; + std::size_t caret = 0; +}; + +std::mutex g_mutex; +Dialog g_dialog; + +bool IsHighSurrogate(char16_t unit) { return unit >= 0xd800 && unit <= 0xdbff; } +bool IsLowSurrogate(char16_t unit) { return unit >= 0xdc00 && unit <= 0xdfff; } + +std::size_t UnitsBefore(const Dialog& dialog) { + if (dialog.caret == 0) return 0; + return dialog.caret >= 2 && IsLowSurrogate(dialog.text[dialog.caret - 1]) && IsHighSurrogate(dialog.text[dialog.caret - 2]) ? 2 : 1; +} + +std::size_t UnitsAfter(const Dialog& dialog) { + if (dialog.caret >= dialog.text.size()) return 0; + return dialog.caret + 1 < dialog.text.size() && IsHighSurrogate(dialog.text[dialog.caret]) && IsLowSurrogate(dialog.text[dialog.caret + 1]) ? 2 : 1; +} + +void Finish(std::uint32_t endStatus) { + g_dialog.endStatus = endStatus; + g_dialog.status = StatusFinished; + ImeSetHostTextSink_nid_no_patch(nullptr); +} + +void Insert(Dialog& dialog, const char16_t* units, std::size_t length) { + std::u16string accepted; + for (std::size_t i = 0; i < length; ++i) { + if (units[i] != u'\n' && units[i] != u'\r') accepted.push_back(units[i]); + } + std::size_t count = std::min(accepted.size(), dialog.maxLength - dialog.text.size()); + if (count > 0 && count < accepted.size() && IsHighSurrogate(accepted[count - 1])) --count; + dialog.text.insert(dialog.caret, accepted, 0, count); + dialog.caret += count; +} + +void Receive(ImeHostInputKind kind, const char16_t* units, std::size_t length) { + std::lock_guard lock(g_mutex); + Dialog& dialog = g_dialog; + if (dialog.status != StatusRunning) return; + switch (kind) { + case ImeHostInputKind::Text: + Insert(dialog, units, length); + return; + case ImeHostInputKind::Backspace: { + const std::size_t count = UnitsBefore(dialog); + dialog.caret -= count; + dialog.text.erase(dialog.caret, count); + return; + } + case ImeHostInputKind::Delete: + dialog.text.erase(dialog.caret, UnitsAfter(dialog)); + return; + case ImeHostInputKind::Left: + dialog.caret -= UnitsBefore(dialog); + return; + case ImeHostInputKind::Right: + dialog.caret += UnitsAfter(dialog); + return; + case ImeHostInputKind::Home: + dialog.caret = 0; + return; + case ImeHostInputKind::End: + dialog.caret = dialog.text.size(); + return; + case ImeHostInputKind::Enter: + std::copy(dialog.text.begin(), dialog.text.end(), dialog.buffer); + dialog.buffer[dialog.text.size()] = u'\0'; + Finish(EndStatusOk); + return; + case ImeHostInputKind::Escape: + Finish(EndStatusUserCanceled); + return; + } +} } extern "C" { int APS5_VABI sceImeDialogAbort(void) { - if (status.load() != StatusRunning) return ErrorNotOpened; - endStatus = EndStatusAborted; - status = StatusFinished; + std::lock_guard lock(g_mutex); + if (g_dialog.status != StatusRunning) return ErrorNotOpened; + Finish(EndStatusAborted); return 0; } @@ -50,47 +136,42 @@ int APS5_VABI sceImeDialogGetPanelSizeExtended(const Param* param, const Extende } int APS5_VABI sceImeDialogGetResult(Result* result) { - std::fprintf(stderr, "[ime] GetResult (status %d)\n", status.load()); - if (result == nullptr || status.load() != StatusFinished) return ErrorNotOpened; + std::lock_guard lock(g_mutex); + if (result == nullptr || g_dialog.status != StatusFinished) return ErrorNotOpened; *result = Result{}; - result->endstatus = endStatus.load(); + result->endstatus = g_dialog.endStatus; return 0; } int APS5_VABI sceImeDialogGetStatus(void) { - if (status.load() == StatusRunning && ++polls > 1) status = StatusFinished; - static std::atomic traced{0}; - const int count = traced.fetch_add(1); - if (count < 8 || count % 2000 == 0) std::fprintf(stderr, "[ime] GetStatus #%d -> %d\n", count, status.load()); - return status.load(); + std::lock_guard lock(g_mutex); + return g_dialog.status; } int APS5_VABI sceImeDialogInit(const Param* param, const ExtendedParam* extended) { (void)extended; - static std::atomic reported{0}; - bool filled = false; - if (param != nullptr && param->input_text_buffer != nullptr && param->max_text_length != 0 && param->input_text_buffer[0] == u'\0') { - static constexpr char16_t defaultText[] = u"Slayer"; - std::uint32_t length = 0; - while (defaultText[length] != u'\0' && length < param->max_text_length) { - param->input_text_buffer[length] = defaultText[length]; - ++length; - } - param->input_text_buffer[length] = u'\0'; - filled = true; - } - std::fprintf(stderr, "[ime] Init #%d\n", reported.load()); - if (reported.fetch_add(1) == 0) std::fprintf(stderr, "[ime] dialog requested (max text length %u): finishing at once with %s\n", param != nullptr ? param->max_text_length : 0u, filled ? "a default text" : "the text unchanged"); - polls = 0; - endStatus = EndStatusOk; - status = StatusRunning; + std::lock_guard lock(g_mutex); + if (g_dialog.status != StatusNone) return ErrorBusy; + if (param == nullptr) return ErrorInvalidAddress; + if (param->max_text_length == 0 || param->max_text_length > MaxTextLength) return ErrorInvalidMaxTextLength; + if (param->input_text_buffer == nullptr) return ErrorInvalidInputTextBuffer; + g_dialog = Dialog{}; + g_dialog.buffer = param->input_text_buffer; + g_dialog.maxLength = param->max_text_length; + std::size_t length = 0; + while (length < param->max_text_length && param->input_text_buffer[length] != u'\0') ++length; + g_dialog.text.assign(param->input_text_buffer, length); + g_dialog.caret = length; + g_dialog.status = StatusRunning; + ImeSetHostTextSink_nid_no_patch(&Receive); return 0; } int APS5_VABI sceImeDialogTerm(void) { - std::fprintf(stderr, "[ime] Term (status %d)\n", status.load()); - if (status.load() == StatusNone) return ErrorNotOpened; - status = StatusNone; + std::lock_guard lock(g_mutex); + if (g_dialog.status == StatusNone) return ErrorNotOpened; + ImeSetHostTextSink_nid_no_patch(nullptr); + g_dialog = Dialog{}; return 0; } diff --git a/core/libs/tests/GuestImeDialog.cpp b/core/libs/tests/GuestImeDialog.cpp new file mode 100644 index 0000000000..3bfec70576 --- /dev/null +++ b/core/libs/tests/GuestImeDialog.cpp @@ -0,0 +1,158 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceImeDialogInit(const Param*, const ExtendedParam*); +int APS5_VABI sceImeDialogGetStatus(void); +int APS5_VABI sceImeDialogGetResult(Result*); +int APS5_VABI sceImeDialogAbort(void); +int APS5_VABI sceImeDialogTerm(void); +} + +namespace { + +constexpr int StatusNone = 0; +constexpr int StatusRunning = 1; +constexpr int StatusFinished = 2; +constexpr std::uint32_t EndStatusOk = 0; +constexpr std::uint32_t EndStatusUserCanceled = 1; +constexpr std::uint32_t EndStatusAborted = 2; +constexpr int ErrorBusy = static_cast(0x80bc0001u); +constexpr int ErrorNotOpened = static_cast(0x80bc0002u); +constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); +constexpr int ErrorInvalidInputTextBuffer = static_cast(0x80bc0017u); +constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); + +void Require(bool value) { if (!value) std::abort(); } + +constexpr unsigned WindowId = 7; +ImeTextInput g_window; + +void Type(const char* utf8) { + SDL_Event event{}; + event.type = SDL_TEXTINPUT; + event.text.windowID = WindowId; + std::strncpy(event.text.text, utf8, sizeof(event.text.text) - 1); + g_window.HandleEvent(event, WindowId); +} + +void Press(SDL_Scancode scancode) { + SDL_Event event{}; + event.type = SDL_KEYDOWN; + event.key.windowID = WindowId; + event.key.keysym.scancode = scancode; + g_window.HandleEvent(event, WindowId); +} + +Param DialogParam(char16_t* buffer, std::uint32_t maxLength) { + Param param{}; + param.max_text_length = maxLength; + param.input_text_buffer = buffer; + return param; +} + +std::uint32_t EndStatus() { + Result result{}; + Require(sceImeDialogGetResult(&result) == 0); + return result.endstatus; +} + +void Open(const Param& param) { + Require(sceImeDialogInit(¶m, nullptr) == 0); + Require(sceImeDialogGetStatus() == StatusRunning); + Require(ImeCapturesKeyboard_nid_postfix()); +} + +void Close() { + Require(sceImeDialogTerm() == 0); + Require(sceImeDialogGetStatus() == StatusNone); +} + +} + +int main() { + char16_t buffer[16] = u"Hi"; + const Param param = DialogParam(buffer, 8); + Require(!ImeCapturesKeyboard_nid_postfix()); + Require(sceImeDialogGetStatus() == StatusNone); + Require(sceImeDialogTerm() == ErrorNotOpened); + + Open(param); + for (int poll = 0; poll < 8; ++poll) Require(sceImeDialogGetStatus() == StatusRunning); + Require(sceImeDialogInit(¶m, nullptr) == ErrorBusy); + Result early{}; + Require(sceImeDialogGetResult(&early) == ErrorNotOpened); + Type("\xc3\xa9"); + Type("\xf0\x9f\x98\x80"); + Require(std::u16string(buffer) == u"Hi"); + Press(SDL_SCANCODE_RETURN); + Require(sceImeDialogGetStatus() == StatusFinished); + Require(!ImeCapturesKeyboard_nid_postfix()); + Require(EndStatus() == EndStatusOk); + Require(std::u16string(buffer) == u"Hi\u00e9\U0001F600"); + Require(sceImeDialogAbort() == ErrorNotOpened); + Close(); + Require(sceImeDialogTerm() == ErrorNotOpened); + + buffer[0] = u'\0'; + Open(param); + Type("abc"); + Press(SDL_SCANCODE_HOME); + Press(SDL_SCANCODE_DELETE); + Press(SDL_SCANCODE_END); + Type("\xf0\x9f\x98\x80"); + Press(SDL_SCANCODE_BACKSPACE); + Press(SDL_SCANCODE_LEFT); + Type("X"); + Type("1234"); + Press(SDL_SCANCODE_END); + Type("\xf0\x9f\x98\x80"); + Type("98"); + Type("Z"); + Press(SDL_SCANCODE_RETURN); + Require(EndStatus() == EndStatusOk); + Require(std::u16string(buffer) == u"bX1234c9"); + Close(); + + buffer[0] = u'\0'; + Open(param); + Press(SDL_SCANCODE_RETURN); + Require(EndStatus() == EndStatusOk); + Require(buffer[0] == u'\0'); + Close(); + + std::memcpy(buffer, u"keep", sizeof(u"keep")); + Open(param); + Type("zz"); + Press(SDL_SCANCODE_ESCAPE); + Require(sceImeDialogGetStatus() == StatusFinished); + Require(EndStatus() == EndStatusUserCanceled); + Require(std::u16string(buffer) == u"keep"); + Close(); + + Open(param); + Require(sceImeDialogAbort() == 0); + Require(sceImeDialogGetStatus() == StatusFinished); + Require(!ImeCapturesKeyboard_nid_postfix()); + Require(EndStatus() == EndStatusAborted); + Type("ignored"); + Press(SDL_SCANCODE_RETURN); + Require(std::u16string(buffer) == u"keep"); + Close(); + + Require(sceImeDialogInit(nullptr, nullptr) == ErrorInvalidAddress); + const Param empty = DialogParam(buffer, 0); + Require(sceImeDialogInit(&empty, nullptr) == ErrorInvalidMaxTextLength); + const Param tooLong = DialogParam(buffer, 2049); + Require(sceImeDialogInit(&tooLong, nullptr) == ErrorInvalidMaxTextLength); + const Param noBuffer = DialogParam(nullptr, 8); + Require(sceImeDialogInit(&noBuffer, nullptr) == ErrorInvalidInputTextBuffer); + Require(sceImeDialogGetStatus() == StatusNone); + Require(!ImeCapturesKeyboard_nid_postfix()); +} diff --git a/docs/dev/debt/libSceImeDialog.md b/docs/dev/debt/libSceImeDialog.md new file mode 100644 index 0000000000..01003701d8 --- /dev/null +++ b/docs/dev/debt/libSceImeDialog.md @@ -0,0 +1,9 @@ +# libSceImeDialog technical debt + +### Silent stubs + +- [sceImeDialogInit](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) shows no dialog: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window through libSceIme while no IME panel is open. Text, Backspace, Delete, Left, Right, Home and End edit the text, Enter accepts it (end status 0) and Escape cancels (end status 1). The controller cannot type, the title, placeholder, type and options are not shown or applied, and line breaks cannot be typed. While the dialog runs, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp) + +### Unknown function info + +- [sceImeDialogInit, sceImeDialogGetStatus, sceImeDialogGetResult, sceImeDialogAbort, sceImeDialogTerm](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - the statuses (none 0, running 1, finished 2), the end statuses (ok 0, user canceled 1, aborted 2) and the errors of `sceImeDialogInit` for an open dialog (`0x80bc0001`), a null parameter (`0x80bc0031`), a maximum text length outside 1 to 2048 (`0x80bc0016`) and a null text buffer (`0x80bc0017`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/ime/ime_dialog.cpp); none is measured on a PS5. A result, abort or term on a dialog that is not finished, running or open returns `0x80bc0002`, where shadPS4 returns `0x80bc0105` to `0x80bc0107`, and `sceImeDialogTerm` also closes a running dialog, which shadPS4 refuses. The text reaches the title's buffer only when Enter accepts it