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) <noreply@anthropic.com>
This commit is contained in:
RickandClaude Opus 5.5 committed 2026-10-15 19:52:28 +02:00
1 parent 03c4cb5e78
commit 6c5686d7ea
6 files changed
+329 -39

No files matched your search

+23 -3
View File
@@ -1,3 +1,4 @@
#include <atomic>
#include <algorithm>
#include <cstdint>
#include <cstddef>
@@ -67,6 +68,7 @@ std::mutex g_keyboardMutex;
std::set<int32_t> g_openKeyboards;
std::mutex g_imeMutex;
std::optional<Session> g_session;
std::atomic<ImeHostTextSink> 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) {
@@ -1,6 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP
#define CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP
#include <cstddef>
#include <cstdint>
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
@@ -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:$<TARGET_FILE_DIR:libSceImeDialog>")
endif()
endif()
+117 -36
View File
@@ -1,9 +1,11 @@
#include <atomic>
#include <algorithm>
#include <cstdint>
#include <cstddef>
#include <cstdio>
#include <mutex>
#include <string>
#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<int>(0x80bc0001u);
constexpr int ErrorNotOpened = static_cast<int>(0x80bc0002u);
constexpr int ErrorInvalidMaxTextLength = static_cast<int>(0x80bc0016u);
constexpr int ErrorInvalidInputTextBuffer = static_cast<int>(0x80bc0017u);
constexpr int ErrorInvalidAddress = static_cast<int>(0x80bc0031u);
std::atomic<int> status{StatusNone};
std::atomic<int> polls{0};
std::atomic<std::uint32_t> 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<std::size_t>(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<int> 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<int> 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;
}
+158
View File
@@ -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 <cstdint>
#include <cstdlib>
#include <cstring>
#include <string>
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<int>(0x80bc0001u);
constexpr int ErrorNotOpened = static_cast<int>(0x80bc0002u);
constexpr int ErrorInvalidMaxTextLength = static_cast<int>(0x80bc0016u);
constexpr int ErrorInvalidInputTextBuffer = static_cast<int>(0x80bc0017u);
constexpr int ErrorInvalidAddress = static_cast<int>(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(&param, 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(&param, 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());
}
+9
View File
@@ -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