mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-23 20:36:58 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60e53a39cc | |||
| 1b21161aac | |||
| 1195af2510 | |||
| 9a5658c038 | |||
| 199d0a9d92 | |||
| e679486ed3 | |||
| 6ef406542c | |||
| 35c6e8fff6 | |||
| 4852721005 |
+7
-2
@@ -419,7 +419,10 @@ add_executable(trophy_system_tests EXCLUDE_FROM_ALL
|
||||
target_link_libraries(trophy_system_tests common)
|
||||
target_include_directories(trophy_system_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(avplayer_file_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/AvPlayerFileTests.cpp")
|
||||
add_executable(avplayer_file_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/AvPlayerFileTests.cpp"
|
||||
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
|
||||
)
|
||||
target_link_libraries(avplayer_file_tests common FFmpeg::ffmpeg ZArchive::zarchive)
|
||||
target_include_directories(avplayer_file_tests PRIVATE ${inc_headers})
|
||||
|
||||
@@ -689,6 +692,7 @@ if(BUILD_TESTING)
|
||||
memory_tracker_tests
|
||||
page_manager_tests
|
||||
archive_file_tests
|
||||
trophy_system_tests
|
||||
avplayer_file_tests
|
||||
bit_array_tests
|
||||
lru_cache_tests
|
||||
@@ -719,8 +723,9 @@ if(APPLE AND (CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" OR
|
||||
add_executable(macos_sse4a_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/macosSse4aTests.cpp"
|
||||
"${KYTY_SOURCE_DIR}/loader/x64InstructionEmulator.cpp"
|
||||
"${KYTY_SOURCE_DIR}/loader/guestInstructionPatcher.cpp"
|
||||
)
|
||||
target_link_libraries(macos_sse4a_tests common fmt::fmt Zydis::Zydis)
|
||||
target_link_libraries(macos_sse4a_tests common fmt::fmt Zydis::Zydis xbyak::xbyak)
|
||||
target_include_directories(macos_sse4a_tests PRIVATE ${inc_headers})
|
||||
if(BUILD_TESTING)
|
||||
add_test(NAME macos_sse4a COMMAND $<TARGET_FILE:macos_sse4a_tests>)
|
||||
|
||||
@@ -382,6 +382,7 @@ enum class Opcode {
|
||||
V_CMPX_LG_F32,
|
||||
V_CMPX_GE_F32,
|
||||
V_CMPX_O_F32,
|
||||
V_CMPX_U_F32,
|
||||
V_CMPX_NGE_F32,
|
||||
V_CMPX_NLG_F32,
|
||||
V_CMPX_NGT_F32,
|
||||
|
||||
@@ -232,7 +232,7 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
|
||||
{0x11u, Opcode::V_CMPX_LT_F32}, {0x12u, Opcode::V_CMPX_EQ_F32},
|
||||
{0x13u, Opcode::V_CMPX_LE_F32}, {0x14u, Opcode::V_CMPX_GT_F32},
|
||||
{0x15u, Opcode::V_CMPX_LG_F32}, {0x16u, Opcode::V_CMPX_GE_F32},
|
||||
{0x17u, Opcode::V_CMPX_O_F32},
|
||||
{0x17u, Opcode::V_CMPX_O_F32}, {0x18u, Opcode::V_CMPX_U_F32},
|
||||
{0x19u, Opcode::V_CMPX_NGE_F32}, {0x1au, Opcode::V_CMPX_NLG_F32},
|
||||
{0x1bu, Opcode::V_CMPX_NGT_F32}, {0x1cu, Opcode::V_CMPX_NLE_F32},
|
||||
{0x1du, Opcode::V_CMPX_NEQ_F32}, {0x1eu, Opcode::V_CMPX_NLT_F32},
|
||||
@@ -892,6 +892,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
|
||||
case Opcode::V_CMPX_LG_F32:
|
||||
case Opcode::V_CMPX_GE_F32:
|
||||
case Opcode::V_CMPX_O_F32:
|
||||
case Opcode::V_CMPX_U_F32:
|
||||
case Opcode::V_CMPX_NGE_F32:
|
||||
case Opcode::V_CMPX_NLG_F32:
|
||||
case Opcode::V_CMPX_NGT_F32:
|
||||
@@ -1449,6 +1450,9 @@ bool SupportsNativeVop3ResultModifiers(Opcode opcode) {
|
||||
case Opcode::V_MAD_F32:
|
||||
case Opcode::V_FMA_F32:
|
||||
case Opcode::V_FMA_F16:
|
||||
case Opcode::V_MIN3_F16:
|
||||
case Opcode::V_MAX3_F16:
|
||||
case Opcode::V_MED3_F16:
|
||||
case Opcode::V_CUBEID_F32:
|
||||
case Opcode::V_CVT_PKRTZ_F16_F32:
|
||||
case Opcode::V_LDEXP_F32:
|
||||
@@ -1479,7 +1483,8 @@ bool HasUnsupportedNativeVop3Modifiers(Opcode opcode, bool permlane, bool mad_mi
|
||||
return clamp != 0u || omod != 0u;
|
||||
}
|
||||
if (IsNativeVop3F16TernaryOpcode(opcode)) {
|
||||
return opcode != Opcode::V_FMA_F16 && (clamp != 0u || omod != 0u);
|
||||
// Clamp is applied by WriteF16 for every f16 ternary; omod only for V_FMA_F16.
|
||||
return opcode != Opcode::V_FMA_F16 && omod != 0u;
|
||||
}
|
||||
if (IsNativeVop3I16TernaryOpcode(opcode)) {
|
||||
return abs != 0u || (clamp != 0u && !clamp_modifier) || omod != 0u || neg != 0u;
|
||||
@@ -1529,6 +1534,7 @@ bool IsVopcCompareExec(Opcode opcode) {
|
||||
case Opcode::V_CMPX_LG_F32:
|
||||
case Opcode::V_CMPX_GE_F32:
|
||||
case Opcode::V_CMPX_O_F32:
|
||||
case Opcode::V_CMPX_U_F32:
|
||||
case Opcode::V_CMPX_NGE_F32:
|
||||
case Opcode::V_CMPX_NLG_F32:
|
||||
case Opcode::V_CMPX_NGT_F32:
|
||||
|
||||
@@ -344,6 +344,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_CMP_O_F32: EmitFloatOrderedCompare(inst, true, false); return;
|
||||
case O::V_CMP_U_F32: EmitFloatOrderedCompare(inst, false, false); return;
|
||||
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true, true); return;
|
||||
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false, true); return;
|
||||
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
|
||||
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
|
||||
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
|
||||
|
||||
@@ -3200,6 +3200,15 @@ static void* RunThread(void* arg) {
|
||||
os_thread_id = GetHostThreadId();
|
||||
#endif
|
||||
thread->host_thread_id = os_thread_id;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX
|
||||
if (!thread->name.empty()) {
|
||||
#if defined(__APPLE__)
|
||||
pthread_setname_np(thread->name.substr(0, 63).c_str());
|
||||
#else
|
||||
pthread_setname_np(pthread_self(), thread->name.substr(0, 15).c_str());
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
LOGF("\tPthread run begin: %s, id = %d, os_thread_id = %" PRIu64 ", entry = 0x%016" PRIx64
|
||||
", arg = 0x%016" PRIx64 ", stack_addr = 0x%016" PRIx64 ", stack_size = %" PRIu64 "\n",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>840</width>
|
||||
<width>900</width>
|
||||
<height>620</height>
|
||||
</rect>
|
||||
</property>
|
||||
@@ -400,6 +400,13 @@ To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav wi
|
||||
<property name="toolTip"><string>Restore default settings</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="settings_button">
|
||||
<property name="minimumSize"><size><width>80</width><height>25</height></size></property>
|
||||
<property name="text"><string>Game Configs</string></property>
|
||||
<property name="toolTip"><string>Import or export game configs</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="buttonSpacer">
|
||||
<property name="orientation"><enum>Qt::Horizontal</enum></property>
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Edit configuration</p></body></html></string>
|
||||
<string>Edit game config</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
@@ -150,7 +150,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Delete configuration</p></body></html></string>
|
||||
<string>Clear game config</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true"/>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QColor>
|
||||
#include <QJsonObject>
|
||||
#include <QMetaEnum>
|
||||
#include <QMetaType>
|
||||
#include <QObject>
|
||||
@@ -14,9 +15,6 @@
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
|
||||
#define KYTY_CFG_SET(n) s->setValue(#n, QVariant::fromValue(n).toString());
|
||||
#define KYTY_CFG_GET(n) n = s->value(#n).value<decltype(n)>();
|
||||
|
||||
template <class T>
|
||||
inline QStringList EnumToList() {
|
||||
QStringList ret;
|
||||
@@ -126,7 +124,7 @@ public:
|
||||
int vblank_frequency = 60;
|
||||
int console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
bool vulkan_validation_enabled = false;
|
||||
bool shader_validation_enabled = true;
|
||||
bool shader_validation_enabled = false;
|
||||
ShaderOptimizationType shader_optimization_type = ShaderOptimizationType::Performance;
|
||||
LogDirection shader_log_direction = LogDirection::Silent;
|
||||
QString shader_log_folder = "_Shaders";
|
||||
@@ -188,90 +186,16 @@ public:
|
||||
game_comment = other.game_comment;
|
||||
}
|
||||
|
||||
void WriteSettings(QSettings* s) const {
|
||||
KYTY_CFG_SET(name);
|
||||
KYTY_CFG_SET(basedir);
|
||||
KYTY_CFG_SET(game_path);
|
||||
KYTY_CFG_SET(custom_settings);
|
||||
KYTY_CFG_SET(screen_resolution);
|
||||
KYTY_CFG_SET(user_name);
|
||||
KYTY_CFG_SET(user_id);
|
||||
KYTY_CFG_SET(audio_input_device);
|
||||
KYTY_CFG_SET(present_mode);
|
||||
KYTY_CFG_SET(gpu_index);
|
||||
KYTY_CFG_SET(fullscreen_enabled);
|
||||
KYTY_CFG_SET(hide_cursor_enabled);
|
||||
KYTY_CFG_SET(readback_linear_images);
|
||||
KYTY_CFG_SET(tessellation_enabled);
|
||||
KYTY_CFG_SET(trophy_enabled);
|
||||
KYTY_CFG_SET(vblank_frequency);
|
||||
KYTY_CFG_SET(console_language);
|
||||
KYTY_CFG_SET(vulkan_validation_enabled);
|
||||
KYTY_CFG_SET(shader_validation_enabled);
|
||||
KYTY_CFG_SET(shader_optimization_type);
|
||||
KYTY_CFG_SET(shader_log_direction);
|
||||
KYTY_CFG_SET(shader_log_folder);
|
||||
KYTY_CFG_SET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_SET(command_buffer_dump_folder);
|
||||
KYTY_CFG_SET(printf_direction);
|
||||
KYTY_CFG_SET(printf_output_file);
|
||||
KYTY_CFG_SET(profiler_enabled);
|
||||
KYTY_CFG_SET(renderdoc_enabled);
|
||||
KYTY_CFG_SET(amd_cpu_enabled);
|
||||
#if defined(_WIN32)
|
||||
KYTY_CFG_SET(red_zone_protection_enabled);
|
||||
#endif
|
||||
s->setValue("host_input_mapping", host_input_mapping);
|
||||
KYTY_CFG_SET(elf);
|
||||
}
|
||||
void WriteSettings(QSettings* s) const;
|
||||
void ReadSettings(QSettings* s);
|
||||
|
||||
void ReadSettings(QSettings* s) {
|
||||
KYTY_CFG_GET(name);
|
||||
KYTY_CFG_GET(basedir);
|
||||
KYTY_CFG_GET(game_path);
|
||||
KYTY_CFG_GET(custom_settings);
|
||||
KYTY_CFG_GET(screen_resolution);
|
||||
user_name = s->value("user_name", user_name).toString();
|
||||
bool user_id_ok = false;
|
||||
auto saved_user_id = s->value("user_id", user_id).toInt(&user_id_ok);
|
||||
user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id)
|
||||
? saved_user_id
|
||||
: Config::DEFAULT_USER_ID;
|
||||
audio_input_device = s->value("audio_input_device", audio_input_device).toString();
|
||||
KYTY_CFG_GET(present_mode);
|
||||
gpu_index = s->value("gpu_index", -1).toInt();
|
||||
if (EnumToText(present_mode).isEmpty()) {
|
||||
present_mode = PresentMode::Mailbox;
|
||||
}
|
||||
KYTY_CFG_GET(fullscreen_enabled);
|
||||
KYTY_CFG_GET(hide_cursor_enabled);
|
||||
KYTY_CFG_GET(readback_linear_images);
|
||||
KYTY_CFG_GET(tessellation_enabled);
|
||||
trophy_enabled = s->value("trophy_enabled", trophy_enabled).toBool();
|
||||
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
|
||||
console_language = s->value("console_language", console_language).toInt();
|
||||
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
|
||||
console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
}
|
||||
KYTY_CFG_GET(vulkan_validation_enabled);
|
||||
KYTY_CFG_GET(shader_validation_enabled);
|
||||
KYTY_CFG_GET(shader_optimization_type);
|
||||
KYTY_CFG_GET(shader_log_direction);
|
||||
KYTY_CFG_GET(shader_log_folder);
|
||||
KYTY_CFG_GET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_GET(command_buffer_dump_folder);
|
||||
KYTY_CFG_GET(printf_direction);
|
||||
KYTY_CFG_GET(printf_output_file);
|
||||
KYTY_CFG_GET(profiler_enabled);
|
||||
KYTY_CFG_GET(renderdoc_enabled);
|
||||
amd_cpu_enabled = s->value("amd_cpu_enabled", false).toBool();
|
||||
#if defined(_WIN32)
|
||||
red_zone_protection_enabled =
|
||||
s->value("red_zone_protection_enabled", red_zone_protection_enabled).toBool();
|
||||
#endif
|
||||
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
|
||||
elf = s->value("elf", elf).toString();
|
||||
}
|
||||
[[nodiscard]] QVariantMap GameSettings() const;
|
||||
// Call on a temporary configuration: validation can fail after loading values.
|
||||
bool SetGameSettings(const QJsonObject& settings, QString& error);
|
||||
|
||||
private:
|
||||
template <class Settings>
|
||||
void ReadGameSettingsValues(const Settings& s);
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Configuration*)
|
||||
|
||||
@@ -33,6 +33,8 @@ public:
|
||||
|
||||
signals:
|
||||
void PreviewControllerColor(const QString& color);
|
||||
void ImportGameSettings();
|
||||
void ExportGameSettings();
|
||||
|
||||
private:
|
||||
Ui::ConfigurationEditDialog* m_ui = nullptr;
|
||||
|
||||
@@ -74,6 +74,8 @@ private:
|
||||
void SelectItem(QTreeWidgetItem* witem);
|
||||
void ApplyCompatibility();
|
||||
void UpdateToolbarIcons();
|
||||
void ImportGameSettings(QWidget* parent);
|
||||
void ExportGameSettings(QWidget* parent) const;
|
||||
[[nodiscard]] bool HasValidGameDirectory() const;
|
||||
|
||||
ConfigurationItem* m_selected_item = nullptr;
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "configuration.h"
|
||||
|
||||
QVariantMap Configuration::GameSettings() const {
|
||||
QVariantMap values;
|
||||
#define KYTY_CFG_SET(n) values.insert(QStringLiteral(#n), QVariant::fromValue(n).toString())
|
||||
KYTY_CFG_SET(screen_resolution);
|
||||
KYTY_CFG_SET(user_name);
|
||||
KYTY_CFG_SET(user_id);
|
||||
KYTY_CFG_SET(audio_input_device);
|
||||
KYTY_CFG_SET(present_mode);
|
||||
KYTY_CFG_SET(gpu_index);
|
||||
KYTY_CFG_SET(fullscreen_enabled);
|
||||
KYTY_CFG_SET(hide_cursor_enabled);
|
||||
KYTY_CFG_SET(readback_linear_images);
|
||||
KYTY_CFG_SET(tessellation_enabled);
|
||||
KYTY_CFG_SET(trophy_enabled);
|
||||
KYTY_CFG_SET(vblank_frequency);
|
||||
KYTY_CFG_SET(console_language);
|
||||
KYTY_CFG_SET(vulkan_validation_enabled);
|
||||
KYTY_CFG_SET(shader_validation_enabled);
|
||||
KYTY_CFG_SET(shader_optimization_type);
|
||||
KYTY_CFG_SET(shader_log_direction);
|
||||
KYTY_CFG_SET(shader_log_folder);
|
||||
KYTY_CFG_SET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_SET(command_buffer_dump_folder);
|
||||
KYTY_CFG_SET(printf_direction);
|
||||
KYTY_CFG_SET(printf_output_file);
|
||||
KYTY_CFG_SET(profiler_enabled);
|
||||
KYTY_CFG_SET(renderdoc_enabled);
|
||||
KYTY_CFG_SET(amd_cpu_enabled);
|
||||
#if defined(_WIN32)
|
||||
KYTY_CFG_SET(red_zone_protection_enabled);
|
||||
#endif
|
||||
KYTY_CFG_SET(elf);
|
||||
#undef KYTY_CFG_SET
|
||||
return values;
|
||||
}
|
||||
|
||||
template <class Settings>
|
||||
void Configuration::ReadGameSettingsValues(const Settings& s) {
|
||||
#define KYTY_CFG_GET(n) n = s.value(QStringLiteral(#n)).template value<decltype(n)>()
|
||||
KYTY_CFG_GET(screen_resolution);
|
||||
user_name = s.value("user_name", user_name).toString();
|
||||
bool user_id_ok = false;
|
||||
auto saved_user_id = s.value("user_id", user_id).toInt(&user_id_ok);
|
||||
user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id)
|
||||
? saved_user_id
|
||||
: Config::DEFAULT_USER_ID;
|
||||
audio_input_device = s.value("audio_input_device", audio_input_device).toString();
|
||||
KYTY_CFG_GET(present_mode);
|
||||
gpu_index = s.value("gpu_index", -1).toInt();
|
||||
if (EnumToText(present_mode).isEmpty()) {
|
||||
present_mode = PresentMode::Mailbox;
|
||||
}
|
||||
KYTY_CFG_GET(fullscreen_enabled);
|
||||
KYTY_CFG_GET(hide_cursor_enabled);
|
||||
KYTY_CFG_GET(readback_linear_images);
|
||||
KYTY_CFG_GET(tessellation_enabled);
|
||||
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
|
||||
vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt();
|
||||
console_language = s.value("console_language", console_language).toInt();
|
||||
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
|
||||
console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
}
|
||||
KYTY_CFG_GET(vulkan_validation_enabled);
|
||||
KYTY_CFG_GET(shader_validation_enabled);
|
||||
KYTY_CFG_GET(shader_optimization_type);
|
||||
KYTY_CFG_GET(shader_log_direction);
|
||||
KYTY_CFG_GET(shader_log_folder);
|
||||
KYTY_CFG_GET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_GET(command_buffer_dump_folder);
|
||||
KYTY_CFG_GET(printf_direction);
|
||||
KYTY_CFG_GET(printf_output_file);
|
||||
KYTY_CFG_GET(profiler_enabled);
|
||||
KYTY_CFG_GET(renderdoc_enabled);
|
||||
amd_cpu_enabled = s.value("amd_cpu_enabled", false).toBool();
|
||||
#if defined(_WIN32)
|
||||
red_zone_protection_enabled =
|
||||
s.value("red_zone_protection_enabled", red_zone_protection_enabled).toBool();
|
||||
#endif
|
||||
elf = s.value("elf", elf).toString();
|
||||
#undef KYTY_CFG_GET
|
||||
}
|
||||
|
||||
void Configuration::WriteSettings(QSettings* s) const {
|
||||
const auto values = GameSettings();
|
||||
for (auto it = values.constBegin(); it != values.constEnd(); ++it) {
|
||||
s->setValue(it.key(), it.value());
|
||||
}
|
||||
s->setValue("name", name);
|
||||
s->setValue("basedir", basedir);
|
||||
s->setValue("game_path", game_path);
|
||||
s->setValue("custom_settings", QVariant::fromValue(custom_settings).toString());
|
||||
s->setValue("host_input_mapping", host_input_mapping);
|
||||
}
|
||||
|
||||
void Configuration::ReadSettings(QSettings* s) {
|
||||
ReadGameSettingsValues(*s);
|
||||
name = s->value("name").toString();
|
||||
basedir = s->value("basedir").toString();
|
||||
game_path = s->value("game_path").toString();
|
||||
custom_settings = s->value("custom_settings").toBool();
|
||||
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
|
||||
}
|
||||
|
||||
bool Configuration::SetGameSettings(const QJsonObject& settings, QString& error) {
|
||||
const auto fail = [&error](const QString& message) {
|
||||
error = message;
|
||||
return false;
|
||||
};
|
||||
auto values = GameSettings();
|
||||
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
|
||||
if (!values.contains(it.key())) {
|
||||
return fail(tr("Unknown game setting: %1").arg(it.key()));
|
||||
}
|
||||
if (!it.value().isString()) {
|
||||
return fail(tr("Game setting %1 must be a string.").arg(it.key()));
|
||||
}
|
||||
values.insert(it.key(), it.value().toString());
|
||||
}
|
||||
|
||||
ReadGameSettingsValues(values);
|
||||
const auto canonical = GameSettings();
|
||||
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
|
||||
if (canonical.value(it.key()) != values.value(it.key())) {
|
||||
return fail(tr("Invalid value for game setting: %1").arg(it.key()));
|
||||
}
|
||||
}
|
||||
if (user_name.trimmed().isEmpty() || user_name.toUtf8().size() > Config::MAX_USER_NAME_LENGTH) {
|
||||
return fail(tr("User name must contain 1-16 UTF-8 bytes."));
|
||||
}
|
||||
if (gpu_index < -1) {
|
||||
return fail(tr("GPU index must be -1 or greater."));
|
||||
}
|
||||
if (vblank_frequency < 30 || vblank_frequency > 360) {
|
||||
return fail(tr("Vblank frequency must be between 30 and 360."));
|
||||
}
|
||||
if ((shader_log_direction == LogDirection::File && shader_log_folder.isEmpty()) ||
|
||||
(printf_direction == LogDirection::File && printf_output_file.isEmpty()) ||
|
||||
(command_buffer_dump_enabled && command_buffer_dump_folder.isEmpty())) {
|
||||
return fail(tr("Enabled logging and command buffer dumps require an output path."));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QResizeEvent>
|
||||
@@ -126,6 +127,7 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
|
||||
setMinimumWidth(width());
|
||||
InitGameDirectories();
|
||||
m_ui->controller_group->setVisible(false);
|
||||
m_ui->settings_button->setVisible(false);
|
||||
|
||||
connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save);
|
||||
connect(m_ui->cancel_button, &QPushButton::clicked, this, &QDialog::reject);
|
||||
@@ -354,6 +356,11 @@ void ConfigurationEditDialog::InitGameDirectories() {
|
||||
|
||||
void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs) {
|
||||
m_global_settings = true;
|
||||
auto* menu = new QMenu(m_ui->settings_button);
|
||||
menu->addAction(tr("Import..."), this, &ConfigurationEditDialog::ImportGameSettings);
|
||||
menu->addAction(tr("Export..."), this, &ConfigurationEditDialog::ExportGameSettings);
|
||||
m_ui->settings_button->setMenu(menu);
|
||||
m_ui->settings_button->setVisible(true);
|
||||
m_game_dirs_list->clear();
|
||||
m_game_dirs_group->setMinimumWidth(GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH);
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
#include <QDialog>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QHeaderView>
|
||||
#include <QIcon>
|
||||
#include <QInputDialog>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
@@ -40,6 +42,7 @@
|
||||
#include <QProcess>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QSaveFile>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QSignalBlocker>
|
||||
@@ -765,7 +768,7 @@ void ConfigurationListWidget::edit_configuration() {
|
||||
|
||||
auto info = CreateConfiguration(*item);
|
||||
ConfigurationEditDialog dlg(*info, this);
|
||||
dlg.setWindowTitle(tr("Edit game settings"));
|
||||
dlg.setWindowTitle(tr("Edit game config"));
|
||||
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
|
||||
&ConfigurationListWidget::PreviewControllerColor);
|
||||
|
||||
@@ -789,8 +792,8 @@ void ConfigurationListWidget::delete_configuartion() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear custom settings"),
|
||||
tr("Do you want to clear custom settings?"))) {
|
||||
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear game config"),
|
||||
tr("Clear this game's config and use global settings?"))) {
|
||||
delete m_custom_infos.take(item->GetInfo().game_path);
|
||||
item->GetInfo().custom_settings = false;
|
||||
WriteSettings();
|
||||
@@ -810,6 +813,10 @@ void ConfigurationListWidget::edit_global_settings() {
|
||||
dlg.SetGlobalSettings(m_game_dirs);
|
||||
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
|
||||
&ConfigurationListWidget::PreviewControllerColor);
|
||||
connect(&dlg, &ConfigurationEditDialog::ImportGameSettings, this,
|
||||
[this, &dlg]() { ImportGameSettings(&dlg); });
|
||||
connect(&dlg, &ConfigurationEditDialog::ExportGameSettings, this,
|
||||
[this, &dlg]() { ExportGameSettings(&dlg); });
|
||||
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
m_global_info.CopyEmulatorSettingsFrom(info);
|
||||
@@ -822,7 +829,133 @@ void ConfigurationListWidget::edit_global_settings() {
|
||||
ScanGameDirectory();
|
||||
}
|
||||
}
|
||||
emit Select();
|
||||
SelectItem(m_ui->cfgs_list->currentItem());
|
||||
}
|
||||
|
||||
static bool IsGameSettingsTitleId(const QString& title_id) {
|
||||
static const QRegularExpression pattern(QStringLiteral("\\APPSA[0-9]{5}\\z"));
|
||||
return pattern.match(title_id).hasMatch();
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::ImportGameSettings(QWidget* parent) {
|
||||
const auto path = QFileDialog::getOpenFileName(parent, tr("Import game configs"), {},
|
||||
tr("JSON files (*.json)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
QMessageBox::warning(parent, tr("Import failed"), file.errorString());
|
||||
return;
|
||||
}
|
||||
const auto document = QJsonDocument::fromJson(file.readAll());
|
||||
if (!document.isObject() || document.object().isEmpty()) {
|
||||
QMessageBox::warning(
|
||||
parent, tr("Import failed"),
|
||||
tr("Expected a JSON object containing game configs keyed by PPSA code."));
|
||||
return;
|
||||
}
|
||||
const auto settings = document.object();
|
||||
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
|
||||
Configuration info;
|
||||
QString error;
|
||||
if (!IsGameSettingsTitleId(it.key()) || !it.value().isObject() ||
|
||||
it.value().toObject().isEmpty() ||
|
||||
!info.SetGameSettings(it.value().toObject(), error)) {
|
||||
QMessageBox::warning(parent, tr("Import failed"),
|
||||
tr("Invalid game config for %1. %2").arg(it.key(), error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
struct ImportedGame {
|
||||
ConfigurationItem* item;
|
||||
std::unique_ptr<Configuration> info;
|
||||
};
|
||||
std::vector<ImportedGame> imported;
|
||||
auto unmatched = settings.keys();
|
||||
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); ++index) {
|
||||
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
|
||||
const auto title_id = item->GetInfo().title_id.trimmed().toUpper();
|
||||
if (!settings.contains(title_id)) {
|
||||
continue;
|
||||
}
|
||||
auto info = CreateConfiguration(*item);
|
||||
QString error;
|
||||
if (!info->SetGameSettings(settings.value(title_id).toObject(), error)) {
|
||||
QMessageBox::warning(parent, tr("Import failed"),
|
||||
tr("Invalid game config for %1. %2").arg(title_id, error));
|
||||
return;
|
||||
}
|
||||
info->custom_settings = true;
|
||||
imported.push_back({item, std::move(info)});
|
||||
unmatched.removeAll(title_id);
|
||||
}
|
||||
auto message = imported.empty() ? tr("No matching games found in the library.")
|
||||
: tr("Configs will be updated for the following games:\n");
|
||||
for (const auto& game: imported) {
|
||||
message += tr("\n%1 — %2").arg(game.info->title_id.trimmed().toUpper(), game.info->name);
|
||||
}
|
||||
if (!unmatched.isEmpty()) {
|
||||
message += tr("\nGames not found: %1").arg(unmatched.join(QStringLiteral(", ")));
|
||||
}
|
||||
if (imported.empty()) {
|
||||
QMessageBox::information(parent, tr("Import game configs"), message);
|
||||
return;
|
||||
}
|
||||
QMessageBox confirmation(QMessageBox::Question, tr("Import game configs"), message,
|
||||
QMessageBox::Apply | QMessageBox::Cancel, parent);
|
||||
confirmation.setTextFormat(Qt::PlainText);
|
||||
confirmation.setDefaultButton(QMessageBox::Cancel);
|
||||
if (confirmation.exec() != QMessageBox::Apply) {
|
||||
return;
|
||||
}
|
||||
for (auto& [item, info]: imported) {
|
||||
const auto game_path = info->game_path;
|
||||
delete m_custom_infos.take(game_path);
|
||||
m_custom_infos.insert(game_path, info.release());
|
||||
item->GetInfo().custom_settings = true;
|
||||
item->Update();
|
||||
}
|
||||
WriteSettings();
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::ExportGameSettings(QWidget* parent) const {
|
||||
QMap<QString, const ConfigurationItem*> games;
|
||||
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); ++index) {
|
||||
const auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
|
||||
const auto& game = item->GetInfo();
|
||||
const auto title_id = game.title_id.trimmed().toUpper();
|
||||
if (m_custom_infos.contains(game.game_path) && IsGameSettingsTitleId(title_id)) {
|
||||
games.insert(tr("%1 — %2 (%3)").arg(title_id, game.name, game.game_path), item);
|
||||
}
|
||||
}
|
||||
if (games.isEmpty()) {
|
||||
QMessageBox::information(parent, tr("Export game config"),
|
||||
tr("No game configs with a PPSA code were found."));
|
||||
return;
|
||||
}
|
||||
bool accepted = false;
|
||||
const auto selected = QInputDialog::getItem(parent, tr("Export game config"), tr("Game:"),
|
||||
games.keys(), 0, false, &accepted);
|
||||
if (!accepted) {
|
||||
return;
|
||||
}
|
||||
const auto& game = games.value(selected)->GetInfo();
|
||||
const auto title_id = game.title_id.trimmed().toUpper();
|
||||
const auto path =
|
||||
QFileDialog::getSaveFileName(parent, tr("Export game config"),
|
||||
title_id + QStringLiteral(".json"), tr("JSON files (*.json)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto settings =
|
||||
QJsonObject::fromVariantMap(m_custom_infos.value(game.game_path)->GameSettings());
|
||||
const auto data =
|
||||
QJsonDocument(QJsonObject {{title_id, settings}}).toJson(QJsonDocument::Indented);
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) {
|
||||
QMessageBox::warning(parent, tr("Export failed"), file.errorString());
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::edit_input_mapping() {
|
||||
@@ -1040,11 +1173,11 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
|
||||
tr("Remove save data..."), this, SLOT(remove_save_data()));
|
||||
menu.addSeparator();
|
||||
QAction* action_edit =
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_FileIcon), tr("Edit game settings..."),
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_FileIcon), tr("Edit game config..."),
|
||||
this, SLOT(edit_configuration()));
|
||||
QAction* action_delete =
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
|
||||
tr("Clear custom settings"), this, SLOT(delete_configuartion()));
|
||||
tr("Clear game config"), this, SLOT(delete_configuartion()));
|
||||
|
||||
if (item == nullptr) {
|
||||
menu.addSeparator();
|
||||
|
||||
+26
-2
@@ -89,6 +89,10 @@ int KYTY_SYSV_ABI NetBind(int s, const void* addr, uint32_t addrlen) {
|
||||
return FinishSocketCall(Net::Bind(s, addr, addrlen));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetConnect(int s, const void* addr, uint32_t addrlen) {
|
||||
return FinishSocketCall(Net::Connect(s, addr, addrlen));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetListen(int s, int backlog) {
|
||||
return FinishSocketCall(Net::Listen(s, backlog));
|
||||
}
|
||||
@@ -170,14 +174,24 @@ int KYTY_SYSV_ABI NetSocketClose(int s) {
|
||||
return FinishNetCall(Net::SocketClose(s));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetGetsockopt(int s, int level, int optname, void* optval, uint32_t* optlen) {
|
||||
return FinishSocketCall(Net::Getsockopt(s, level, optname, optval, optlen));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetSetsockopt(int s, int level, int optname, const void* optval,
|
||||
uint32_t optlen) {
|
||||
return FinishSocketCall(Net::Setsockopt(s, level, optname, optval, optlen));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetSend(int s, const void* buf, size_t len, int flags) {
|
||||
int KYTY_SYSV_ABI NetSendto(int s, const void* buf, size_t len, int flags, const void* addr,
|
||||
uint32_t addrlen) {
|
||||
const auto size = std::min<size_t>(len, std::numeric_limits<int>::max());
|
||||
return FinishSocketCall(static_cast<int>(Net::Send(s, buf, size, flags | 0x20000)));
|
||||
return FinishSocketCall(
|
||||
static_cast<int>(Net::Sendto(s, buf, size, flags | 0x20000, addr, addrlen)));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetSend(int s, const void* buf, size_t len, int flags) {
|
||||
return NetSendto(s, buf, len, flags, nullptr, 0);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetRecv(int s, void* buf, size_t len, int flags) {
|
||||
@@ -185,6 +199,12 @@ int KYTY_SYSV_ABI NetRecv(int s, void* buf, size_t len, int flags) {
|
||||
return FinishSocketCall(static_cast<int>(Net::Recv(s, buf, size, flags)));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetRecvfrom(int s, void* buf, size_t len, int flags, void* addr,
|
||||
uint32_t* addrlen) {
|
||||
const auto size = std::min<size_t>(len, std::numeric_limits<int>::max());
|
||||
return FinishSocketCall(static_cast<int>(Net::Recvfrom(s, buf, size, flags, addr, addrlen)));
|
||||
}
|
||||
|
||||
uint32_t KYTY_SYSV_ABI NetHtonl(uint32_t host32) {
|
||||
return ((host32 & 0x000000ffu) << 24u) | ((host32 & 0x0000ff00u) << 8u) |
|
||||
((host32 & 0x00ff0000u) >> 8u) | ((host32 & 0xff000000u) >> 24u);
|
||||
@@ -209,6 +229,7 @@ LIB_DEFINE(InitNet_1_Net) {
|
||||
LIB_FUNC("HQOwnfMGipQ", LibNet::GetNetErrorAddr);
|
||||
LIB_FUNC("PIWqhn9oSxc", LibNet::NetAccept);
|
||||
LIB_FUNC("bErx49PgxyY", LibNet::NetBind);
|
||||
LIB_FUNC("OXXX4mUk3uk", LibNet::NetConnect);
|
||||
LIB_FUNC("dgJBaeJnGpo", LibNet::NetPoolCreate);
|
||||
LIB_FUNC("K7RlrTkI-mw", LibNet::NetPoolDestroy);
|
||||
LIB_FUNC("C4UgDHHPvdw", LibNet::NetResolverCreate);
|
||||
@@ -229,9 +250,12 @@ LIB_DEFINE(InitNet_1_Net) {
|
||||
LIB_FUNC("TSM6whtekok", LibNet::NetShutdown);
|
||||
LIB_FUNC("Q4qBuN-c0ZM", LibNet::NetSocket);
|
||||
LIB_FUNC("45ggEzakPJQ", LibNet::NetSocketClose);
|
||||
LIB_FUNC("xphrZusl78E", LibNet::NetGetsockopt);
|
||||
LIB_FUNC("2mKX2Spso7I", LibNet::NetSetsockopt);
|
||||
LIB_FUNC("beRjXBn-z+o", LibNet::NetSend);
|
||||
LIB_FUNC("gvD1greCu0A", LibNet::NetSendto);
|
||||
LIB_FUNC("9wO9XrMsNhc", LibNet::NetRecv);
|
||||
LIB_FUNC("304ooNZxWDY", LibNet::NetRecvfrom);
|
||||
LIB_FUNC("9T2pDF2Ryqg", LibNet::NetHtonl);
|
||||
LIB_FUNC("iWQWrwiSt8A", LibNet::NetHtons);
|
||||
LIB_FUNC("pQGpHYopAIY", LibNet::NetNtohl);
|
||||
|
||||
+84
-12
@@ -992,6 +992,7 @@ static int ConvertHostSocketError(int error) {
|
||||
case WSAENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
|
||||
case WSAENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
|
||||
case WSAENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
|
||||
case WSAENOPROTOOPT: posix_error = Posix::POSIX_ENOPROTOOPT; break;
|
||||
case WSAEOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
|
||||
case WSAEPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
|
||||
case WSAESHUTDOWN: posix_error = Posix::POSIX_ESHUTDOWN; break;
|
||||
@@ -1028,6 +1029,7 @@ static int ConvertHostSocketError(int error) {
|
||||
case ENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
|
||||
case ENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
|
||||
case ENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
|
||||
case ENOPROTOOPT: posix_error = Posix::POSIX_ENOPROTOOPT; break;
|
||||
case EOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
|
||||
case EPIPE: posix_error = Posix::POSIX_EPIPE; break;
|
||||
case EPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
|
||||
@@ -1066,6 +1068,26 @@ static int ConvertSocketOptionLevel(int level) {
|
||||
return (level == 0xffff ? SOL_SOCKET : level);
|
||||
}
|
||||
|
||||
static int ConvertSocketOptionName(int level, int option) {
|
||||
if (level == 0xffff) {
|
||||
switch (option) {
|
||||
case 0x0020: return SO_BROADCAST;
|
||||
case 0x1001: return SO_SNDBUF;
|
||||
case 0x1002: return SO_RCVBUF;
|
||||
case 0x1007: return SO_ERROR;
|
||||
case 0x1105: return SO_SNDTIMEO;
|
||||
default: break;
|
||||
}
|
||||
} else if (level == 6 && option == 1) {
|
||||
return TCP_NODELAY;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
return option;
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int* P2pSocketOption(P2pEndpoint& endpoint, int option) {
|
||||
switch (option) {
|
||||
case 0x0020: return &endpoint.broadcast;
|
||||
@@ -2149,21 +2171,41 @@ int KYTY_SYSV_ABI Getsockopt(int s, int level, int optname, void* optval, uint32
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Guest socket options: SOL_SOCKET=0xffff, SO_ERROR=0x1007.
|
||||
const bool socket_error = (level == 0xffff && optname == 0x1007);
|
||||
#if !defined(_WIN32)
|
||||
if (!socket_error) {
|
||||
const bool send_timeout = (level == 0xffff && optname == 0x1105);
|
||||
optname = ConvertSocketOptionName(level, optname);
|
||||
if (optname < 0) {
|
||||
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
DWORD timeout {};
|
||||
#else
|
||||
timeval timeout {};
|
||||
#endif
|
||||
if (socket_error) {
|
||||
optname = SO_ERROR;
|
||||
}
|
||||
SocketLength len = static_cast<SocketLength>(*optlen);
|
||||
if (::getsockopt(socket, ConvertSocketOptionLevel(level), optname, static_cast<char*>(optval),
|
||||
void* value = optval;
|
||||
if (send_timeout) {
|
||||
if (*optlen < sizeof(int)) {
|
||||
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||
}
|
||||
value = &timeout;
|
||||
len = sizeof(timeout);
|
||||
}
|
||||
if (::getsockopt(socket, ConvertSocketOptionLevel(level), optname, static_cast<char*>(value),
|
||||
&len) != 0) {
|
||||
return SetHostSocketError();
|
||||
}
|
||||
if (send_timeout) {
|
||||
#if defined(_WIN32)
|
||||
const int64_t usec = static_cast<int64_t>(timeout) * 1000;
|
||||
#else
|
||||
const int64_t usec = static_cast<int64_t>(timeout.tv_sec) * 1'000'000 + timeout.tv_usec;
|
||||
#endif
|
||||
const int guest_timeout =
|
||||
static_cast<int>(std::min<int64_t>(usec, std::numeric_limits<int>::max()));
|
||||
std::memcpy(optval, &guest_timeout, sizeof(guest_timeout));
|
||||
len = sizeof(guest_timeout);
|
||||
}
|
||||
if (socket_error && len >= static_cast<SocketLength>(sizeof(int))) {
|
||||
auto* error = static_cast<int*>(optval);
|
||||
*error = ConvertHostSocketError(*error);
|
||||
@@ -2215,6 +2257,18 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
if (level == 0xffff && optname == 0x10000) {
|
||||
if (optlen != sizeof(int)) {
|
||||
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||
}
|
||||
int enabled = 0;
|
||||
std::memcpy(&enabled, optval, sizeof(enabled));
|
||||
// Native sends preserve 255.255.255.255 without changing broadcast permission.
|
||||
return enabled != 0 ? 0 : SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||
}
|
||||
#endif
|
||||
|
||||
constexpr int ORBIS_SO_NBIO = 0x1200;
|
||||
if (ConvertSocketOptionLevel(level) == SOL_SOCKET && optname == ORBIS_SO_NBIO &&
|
||||
optlen >= sizeof(int)) {
|
||||
@@ -2230,14 +2284,32 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
|
||||
#endif
|
||||
return failed ? SetHostSocketError() : 0;
|
||||
}
|
||||
#if !defined(_WIN32)
|
||||
// Guest TCP options: IPPROTO_TCP=6, TCP_NODELAY=1.
|
||||
if (level != 6 || optname != 1) {
|
||||
const bool send_timeout = (level == 0xffff && optname == 0x1105);
|
||||
optname = ConvertSocketOptionName(level, optname);
|
||||
if (optname < 0) {
|
||||
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||
}
|
||||
level = IPPROTO_TCP;
|
||||
optname = TCP_NODELAY;
|
||||
#if defined(_WIN32)
|
||||
DWORD timeout {};
|
||||
#else
|
||||
timeval timeout {};
|
||||
#endif
|
||||
if (send_timeout) {
|
||||
if (optlen != sizeof(int)) {
|
||||
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||
}
|
||||
int usec = 0;
|
||||
std::memcpy(&usec, optval, sizeof(usec));
|
||||
usec = std::max(usec, 0);
|
||||
#if defined(_WIN32)
|
||||
timeout = static_cast<DWORD>((static_cast<int64_t>(usec) + 999) / 1000);
|
||||
#else
|
||||
timeout.tv_sec = usec / 1'000'000;
|
||||
timeout.tv_usec = usec % 1'000'000;
|
||||
#endif
|
||||
optval = &timeout;
|
||||
optlen = sizeof(timeout);
|
||||
}
|
||||
|
||||
if (::setsockopt(socket, ConvertSocketOptionLevel(level), optname,
|
||||
static_cast<const char*>(optval), static_cast<SocketLength>(optlen)) != 0) {
|
||||
|
||||
@@ -21,10 +21,8 @@
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#if !defined(__APPLE__)
|
||||
#include <xbyak/xbyak.h>
|
||||
#include <xbyak/xbyak_util.h>
|
||||
#endif
|
||||
|
||||
#ifdef min
|
||||
#undef min
|
||||
@@ -33,9 +31,7 @@
|
||||
#undef max
|
||||
#endif
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
using namespace Xbyak::util;
|
||||
#endif
|
||||
|
||||
namespace Loader {
|
||||
|
||||
@@ -52,8 +48,6 @@ using s64 = int64_t;
|
||||
|
||||
constexpr size_t NearJumpSize = 5;
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
|
||||
struct PatchModule {
|
||||
std::mutex mutex {};
|
||||
u8* start = nullptr;
|
||||
@@ -1573,19 +1567,6 @@ GuestInstructionPatchResult PatchGuestInstructions(u64 segment_addr, u64 segment
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
GuestInstructionHostFeatures GetGuestInstructionHostFeatures() {
|
||||
return {};
|
||||
}
|
||||
|
||||
GuestInstructionPatchResult PatchGuestInstructions(u64, u64, std::span<const uintptr_t>, bool, bool,
|
||||
GuestInstructionHostFeatures) {
|
||||
return {};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t DW_EH_PE_FORMAT_MASK = 0x0f;
|
||||
@@ -1763,7 +1744,6 @@ bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_fram
|
||||
|
||||
void RegisterGuestInstructionPatchModule(void* module_ptr, uint64_t module_size,
|
||||
void* trampoline_area_ptr, uint64_t trampoline_area_size) {
|
||||
#if !defined(__APPLE__)
|
||||
EXIT_IF(module_ptr == nullptr || module_size == 0 || trampoline_area_ptr == nullptr ||
|
||||
trampoline_area_size == 0);
|
||||
const auto module_addr = reinterpret_cast<u64>(module_ptr);
|
||||
@@ -1772,20 +1752,10 @@ void RegisterGuestInstructionPatchModule(void* module_ptr, uint64_t module_size,
|
||||
std::forward_as_tuple(static_cast<u8*>(module_ptr), module_size,
|
||||
static_cast<u8*>(trampoline_area_ptr),
|
||||
trampoline_area_size));
|
||||
#else
|
||||
(void)module_ptr;
|
||||
(void)module_size;
|
||||
(void)trampoline_area_ptr;
|
||||
(void)trampoline_area_size;
|
||||
#endif
|
||||
}
|
||||
|
||||
void UnregisterGuestInstructionPatchModule(void* module_ptr) {
|
||||
#if !defined(__APPLE__)
|
||||
g_patch_modules.erase(reinterpret_cast<u64>(module_ptr));
|
||||
#else
|
||||
(void)module_ptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
#undef ASSERT
|
||||
|
||||
@@ -1670,11 +1670,7 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
||||
uint64_t tls_handler_size = is_shared ? 0 : Jit::SafeCall::GetSize();
|
||||
EXIT_IF(tls_handler_size > UINT64_MAX - program->base_size_aligned);
|
||||
program->mapped_size = program->base_size_aligned + tls_handler_size;
|
||||
#if !defined(__APPLE__)
|
||||
const bool emulate_amd = Config::AmdCpuEnabled();
|
||||
#else
|
||||
const bool emulate_amd = false;
|
||||
#endif
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
const bool protect_memory_faults = Config::RedZoneProtectionEnabled();
|
||||
#else
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
#elif defined(__APPLE__)
|
||||
#include <sched.h>
|
||||
#include <sys/ucontext.h>
|
||||
#else
|
||||
#include <sched.h>
|
||||
@@ -468,6 +469,16 @@ struct Context {
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
// The kernel hands AVX threads the longer context that carries YMM[255:128]; a plain SSE
|
||||
// context has no such state to clear.
|
||||
void ClearUpperYmm(uint8_t index) const {
|
||||
if (index >= 16 || native->uc_mcsize < sizeof(_STRUCT_MCONTEXT_AVX64)) {
|
||||
return;
|
||||
}
|
||||
auto* avx = reinterpret_cast<_STRUCT_MCONTEXT_AVX64*>(native->uc_mcontext);
|
||||
auto* ymmh = &avx->__fs.__fpu_ymmh0;
|
||||
ymmh[index] = {};
|
||||
}
|
||||
#else
|
||||
ucontext_t* native;
|
||||
|
||||
@@ -703,8 +714,6 @@ static bool TryEmulateCpuExtensions(Context& context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
|
||||
static bool TryEmulateMonitorxMwaitx(Context& context) {
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(context.Rip());
|
||||
if (rip[0] != 0x0f || rip[1] != 0x01 || (rip[2] != 0xfa && rip[2] != 0xfb)) {
|
||||
@@ -790,8 +799,6 @@ static bool TryEmulateReciprocalSquareRoot(Context& context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool TryEmulate(void* native_context) {
|
||||
if (native_context == nullptr) {
|
||||
return false;
|
||||
@@ -807,12 +814,12 @@ bool TryEmulate(void* native_context) {
|
||||
#else
|
||||
Context context {static_cast<ucontext_t*>(native_context)};
|
||||
#endif
|
||||
#if !defined(__APPLE__)
|
||||
return TryEmulateReciprocalSquareRoot(context) || TryEmulateMonitorxMwaitx(context) ||
|
||||
TryEmulateSse4a(context) || TryEmulateShaNi(context) || TryEmulateCpuExtensions(context);
|
||||
#else
|
||||
return TryEmulateSse4a(context) || TryEmulateCpuExtensions(context);
|
||||
TryEmulateSse4a(context) ||
|
||||
#if !defined(__APPLE__)
|
||||
TryEmulateShaNi(context) ||
|
||||
#endif
|
||||
TryEmulateCpuExtensions(context);
|
||||
}
|
||||
|
||||
} // namespace Loader::X64InstructionEmulator
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Libs::LibKernelApr {
|
||||
void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols);
|
||||
}
|
||||
@@ -1061,19 +1066,40 @@ void CheckSocketWakeup() {
|
||||
namespace Net = Libs::Network::Net;
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::LibNet::InitNet_1_Net(&symbols);
|
||||
const auto *connect_symbol = symbols.Find(
|
||||
{"OXXX4mUk3uk", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *getsockopt_symbol = symbols.Find(
|
||||
{"xphrZusl78E", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *setsockopt_symbol = symbols.Find(
|
||||
{"2mKX2Spso7I", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *send_symbol = symbols.Find(
|
||||
{"beRjXBn-z+o", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *sendto_symbol = symbols.Find(
|
||||
{"gvD1greCu0A", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *recv_symbol = symbols.Find(
|
||||
{"9wO9XrMsNhc", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *recvfrom_symbol = symbols.Find(
|
||||
{"304ooNZxWDY", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *errno_symbol = symbols.Find(
|
||||
{"HQOwnfMGipQ", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
Check(send_symbol && recv_symbol && errno_symbol,
|
||||
"Net send, receive and errno exports resolve with the guest ABI versions");
|
||||
Check(connect_symbol && getsockopt_symbol && setsockopt_symbol && send_symbol && sendto_symbol &&
|
||||
recv_symbol && recvfrom_symbol && errno_symbol,
|
||||
"Net socket and errno exports resolve with the guest ABI versions");
|
||||
using Connect = int (KYTY_SYSV_ABI *)(int, const void *, uint32_t);
|
||||
using Getsockopt = int (KYTY_SYSV_ABI *)(int, int, int, void *, uint32_t *);
|
||||
using Setsockopt = int (KYTY_SYSV_ABI *)(int, int, int, const void *, uint32_t);
|
||||
using Send = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int);
|
||||
using Sendto = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int, const void *, uint32_t);
|
||||
using Recv = int (KYTY_SYSV_ABI *)(int, void *, size_t, int);
|
||||
using Recvfrom = int (KYTY_SYSV_ABI *)(int, void *, size_t, int, void *, uint32_t *);
|
||||
using Errno = int *(KYTY_SYSV_ABI *)();
|
||||
const auto net_connect = reinterpret_cast<Connect>(connect_symbol->vaddr);
|
||||
const auto net_getsockopt = reinterpret_cast<Getsockopt>(getsockopt_symbol->vaddr);
|
||||
const auto net_setsockopt = reinterpret_cast<Setsockopt>(setsockopt_symbol->vaddr);
|
||||
const auto net_send = reinterpret_cast<Send>(send_symbol->vaddr);
|
||||
const auto net_sendto = reinterpret_cast<Sendto>(sendto_symbol->vaddr);
|
||||
const auto net_recv = reinterpret_cast<Recv>(recv_symbol->vaddr);
|
||||
const auto net_recvfrom = reinterpret_cast<Recvfrom>(recvfrom_symbol->vaddr);
|
||||
auto *net_errno = reinterpret_cast<Errno>(errno_symbol->vaddr)();
|
||||
const auto [reader, writer] = CreateTcpPair();
|
||||
const int enabled = 1;
|
||||
@@ -1082,10 +1108,15 @@ void CheckSocketWakeup() {
|
||||
int socket_error = -1;
|
||||
uint32_t error_size = sizeof(socket_error);
|
||||
*Libs::Posix::GetErrorAddr() = Libs::Posix::POSIX_EINVAL;
|
||||
Check(Net::Getsockopt(writer, 0xffff, 0x1007, &socket_error, &error_size) == 0 &&
|
||||
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||
Check(net_getsockopt(writer, 0xffff, 0x1007, &socket_error, &error_size) == 0 &&
|
||||
socket_error == 0 && error_size == sizeof(socket_error) &&
|
||||
*net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
|
||||
"SO_ERROR reports socket status without changing guest errno");
|
||||
"Net SO_ERROR reports socket status without changing either guest errno");
|
||||
Check(net_getsockopt(writer, 0xffff, 0x1007, nullptr, &error_size) ==
|
||||
Libs::Network::NET_ERROR_EFAULT && *net_errno == Libs::Posix::POSIX_EFAULT,
|
||||
"Net getsockopt translates an invalid output buffer");
|
||||
|
||||
std::array<uint64_t, 16> readable {};
|
||||
const auto bit = uint64_t {1} << (reader % 64);
|
||||
@@ -1122,6 +1153,131 @@ void CheckSocketWakeup() {
|
||||
*net_errno == Libs::Posix::POSIX_EWOULDBLOCK,
|
||||
"Net nonblocking receive translates POSIX failure and Net errno");
|
||||
#endif
|
||||
const int datagram = Net::Socket(2, 2, 0);
|
||||
const int datagram_writer = Net::Socket(2, 2, 0);
|
||||
std::array<uint8_t, 16> address {16, 2, 0, 0, 127, 0, 0, 1};
|
||||
std::array<uint8_t, 16> peer {}, expected_peer {};
|
||||
uint32_t address_size = address.size(), peer_size = peer.size();
|
||||
uint32_t expected_peer_size = expected_peer.size();
|
||||
Check(datagram >= 0 && datagram_writer >= 0 &&
|
||||
Net::Bind(datagram, address.data(), address.size()) == 0 &&
|
||||
Net::Bind(datagram_writer, address.data(), address.size()) == 0 &&
|
||||
Net::Getsockname(datagram, address.data(), &address_size) == 0 &&
|
||||
Net::Getsockname(datagram_writer, expected_peer.data(), &expected_peer_size) == 0 &&
|
||||
Net::Setsockopt(datagram, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0,
|
||||
"create nonblocking loopback datagrams for Net ABI verification");
|
||||
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||
*Libs::Posix::GetErrorAddr() = Libs::Posix::POSIX_EINVAL;
|
||||
for (const int option : {0x1001, 0x1002}) {
|
||||
constexpr int requested = 16384;
|
||||
int actual = 0;
|
||||
uint32_t size = sizeof(actual);
|
||||
Check(net_setsockopt(datagram, 0xffff, option, &requested, sizeof(requested)) == 0 &&
|
||||
net_getsockopt(datagram, 0xffff, option, &actual, &size) == 0 &&
|
||||
actual >= requested && size == sizeof(actual),
|
||||
"Net UDP send and receive buffers accept guest option numbers");
|
||||
}
|
||||
for (const int value : {1, 0}) {
|
||||
int actual = -1;
|
||||
uint32_t size = sizeof(actual);
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x20, &value, sizeof(value)) == 0 &&
|
||||
net_getsockopt(datagram, 0xffff, 0x20, &actual, &size) == 0 &&
|
||||
actual == value && size == sizeof(actual),
|
||||
"Net UDP broadcast option can be enabled and disabled");
|
||||
}
|
||||
int timeout = 0;
|
||||
int *timeout_value = &timeout;
|
||||
#if defined(__linux__)
|
||||
int broadcast = -1;
|
||||
uint32_t broadcast_size = sizeof(broadcast);
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x10000, &enabled, sizeof(enabled)) == 0 &&
|
||||
net_getsockopt(datagram, 0xffff, 0x20, &broadcast, &broadcast_size) == 0 &&
|
||||
broadcast == 0,
|
||||
"preserving the all-ones destination does not enable broadcast permission");
|
||||
const long page_size = sysconf(_SC_PAGESIZE);
|
||||
Check(page_size > 0, "get host page size for socket timeout boundary");
|
||||
void *timeout_pages = mmap(nullptr, page_size * 2, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Check(timeout_pages != MAP_FAILED &&
|
||||
mprotect(static_cast<char *>(timeout_pages) + page_size, page_size,
|
||||
PROT_NONE) == 0,
|
||||
"guard memory after the four-byte socket timeout");
|
||||
timeout_value = reinterpret_cast<int *>(static_cast<char *>(timeout_pages) +
|
||||
page_size - sizeof(int));
|
||||
#endif
|
||||
for (const int value : {1500000, 0, -1}) {
|
||||
*timeout_value = value;
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x1105, timeout_value, sizeof(int)) == 0,
|
||||
"Net send timeout reads a four-byte microsecond value");
|
||||
*timeout_value = -2;
|
||||
uint32_t size = sizeof(int);
|
||||
Check(net_getsockopt(datagram, 0xffff, 0x1105, timeout_value, &size) == 0 &&
|
||||
*timeout_value == std::max(value, 0) && size == sizeof(int),
|
||||
"Net send timeout returns four-byte microseconds and disables nonpositive values");
|
||||
}
|
||||
#if defined(__linux__)
|
||||
Check(munmap(timeout_pages, page_size * 2) == 0, "free socket timeout guard pages");
|
||||
#endif
|
||||
Check(*net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
|
||||
"successful Net socket option calls preserve both guest errno values");
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x1105, nullptr, sizeof(timeout)) ==
|
||||
Libs::Network::NET_ERROR_EFAULT && *net_errno == Libs::Posix::POSIX_EFAULT &&
|
||||
net_getsockopt(datagram, 0xffff, 0x1105, &timeout, nullptr) ==
|
||||
Libs::Network::NET_ERROR_EFAULT && *net_errno == Libs::Posix::POSIX_EFAULT,
|
||||
"Net socket options reject null value and length pointers");
|
||||
uint32_t short_size = sizeof(timeout) - 1;
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x1105, &timeout, short_size) ==
|
||||
Libs::Network::NET_ERROR_EINVAL && *net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||
net_getsockopt(datagram, 0xffff, 0x1105, &timeout, &short_size) ==
|
||||
Libs::Network::NET_ERROR_EINVAL && *net_errno == Libs::Posix::POSIX_EINVAL,
|
||||
"Net send timeout rejects undersized values");
|
||||
uint32_t option_size = sizeof(timeout);
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x7fffffff, &timeout, option_size) ==
|
||||
Libs::Network::NET_ERROR_ENOPROTOOPT &&
|
||||
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT &&
|
||||
net_getsockopt(datagram, 0xffff, 0x7fffffff, &timeout, &option_size) ==
|
||||
Libs::Network::NET_ERROR_ENOPROTOOPT &&
|
||||
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT,
|
||||
"Net unknown socket options return protocol-option errors");
|
||||
#if defined(__linux__)
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x1007, &timeout, option_size) ==
|
||||
Libs::Network::NET_ERROR_ENOPROTOOPT &&
|
||||
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT,
|
||||
"Net native protocol-option errors retain their guest error code");
|
||||
#endif
|
||||
received.fill(0);
|
||||
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||
Check(net_sendto(datagram_writer, payload, sizeof(payload), 0,
|
||||
address.data(), address_size) == sizeof(payload) &&
|
||||
*net_errno == Libs::Posix::POSIX_EINVAL,
|
||||
"Net sendto delivers to a guest sockaddr and preserves errno on success");
|
||||
Check(net_recvfrom(datagram, received.data(), received.size(), 0,
|
||||
peer.data(), &peer_size) == sizeof(payload) &&
|
||||
std::memcmp(received.data(), payload, sizeof(payload)) == 0 &&
|
||||
peer_size == expected_peer_size && peer == expected_peer &&
|
||||
*net_errno == Libs::Posix::POSIX_EINVAL,
|
||||
"Net recvfrom returns datagram bytes and sender address while preserving errno");
|
||||
Check(net_recvfrom(datagram, received.data(), received.size(), 0, nullptr, nullptr) ==
|
||||
Libs::Network::NET_ERROR_EWOULDBLOCK &&
|
||||
*net_errno == Libs::Posix::POSIX_EWOULDBLOCK,
|
||||
"Net recvfrom translates nonblocking failure with an omitted sender address");
|
||||
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||
Check(net_connect(datagram_writer, address.data(), address_size) == 0 &&
|
||||
*net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||
net_sendto(datagram_writer, payload, sizeof(payload), 0, nullptr, 0) ==
|
||||
sizeof(payload) && *net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||
net_recvfrom(datagram, received.data(), received.size(), 0, nullptr, nullptr) ==
|
||||
sizeof(payload) && std::memcmp(received.data(), payload, sizeof(payload)) == 0,
|
||||
"Net connect selects the peer used by sendto with an omitted destination");
|
||||
Check(net_connect(-1, address.data(), address_size) == Libs::Network::NET_ERROR_EBADF &&
|
||||
*net_errno == Libs::Posix::POSIX_EBADF,
|
||||
"Net connect translates an invalid socket");
|
||||
Check(net_sendto(-1, payload, sizeof(payload), 0, address.data(), address_size) ==
|
||||
Libs::Network::NET_ERROR_EBADF && *net_errno == Libs::Posix::POSIX_EBADF,
|
||||
"Net sendto translates an invalid socket");
|
||||
Check(Net::SocketClose(datagram) == 0 && Net::SocketClose(datagram_writer) == 0,
|
||||
"close Net loopback datagrams");
|
||||
Check(net_send(-1, payload, sizeof(payload), 0) == Libs::Network::NET_ERROR_EBADF &&
|
||||
*net_errno == Libs::Posix::POSIX_EBADF,
|
||||
"Net send translates an invalid socket instead of returning POSIX minus one");
|
||||
@@ -1138,10 +1294,12 @@ void CheckSocketWakeup() {
|
||||
const auto previous_sigpipe = std::signal(SIGPIPE, SIG_DFL);
|
||||
Check(previous_sigpipe != SIG_ERR, "set default SIGPIPE disposition for Net send");
|
||||
const auto broken_send = net_send(disconnected, payload, sizeof(payload), 0);
|
||||
const auto broken_sendto = net_sendto(disconnected, payload, sizeof(payload), 0, nullptr, 0);
|
||||
std::signal(SIGPIPE, previous_sigpipe);
|
||||
Check(broken_send == Libs::Network::NET_ERROR_EPIPE &&
|
||||
broken_sendto == Libs::Network::NET_ERROR_EPIPE &&
|
||||
*net_errno == Libs::Posix::POSIX_EPIPE,
|
||||
"Net send reports a broken pipe without raising host SIGPIPE");
|
||||
"Net send and sendto report a broken pipe without raising host SIGPIPE");
|
||||
Check(Net::SocketClose(disconnected) == 0, "close unconnected socket");
|
||||
#endif
|
||||
#if defined(_WIN32)
|
||||
|
||||
@@ -33541,7 +33541,8 @@ void CheckIndirectBufferStore(VulkanHarness &vulkan) {
|
||||
candidate.indirect_search_iterations = 0;
|
||||
candidate.indirect_resources.clear();
|
||||
program.descriptor_sources.resize(1);
|
||||
program.descriptor_sources[0].indirect_descriptor.emplace().table_stride = 16;
|
||||
program.descriptor_sources[0].indirect_descriptor =
|
||||
DescriptorSource::IndirectDescriptor{.table_stride = 16};
|
||||
ShaderComputeInputInfo compute;
|
||||
compute.wave_size = 32;
|
||||
compute.host_subgroup_size = vulkan.SubgroupSize();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "common/hostException.h"
|
||||
#include "loader/guestInstructionPatcher.h"
|
||||
#include "loader/x64InstructionEmulator.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
@@ -82,6 +84,53 @@ constexpr uint8_t kUd2[] = {
|
||||
0xc3, // ret
|
||||
};
|
||||
|
||||
// VRSQRTPS xmm1, xmm0 as the AMD CPU patcher leaves it when it cannot relocate the instruction: a
|
||||
// reserved VEX.vvvv bit is cleared (c5 f8 -> c5 f0), which raises #UD on every host. Rosetta aborted
|
||||
// the guest on exactly this encoding until the emulator handled it there too.
|
||||
constexpr uint8_t kTrappedVrsqrtpsXmm1Xmm0[] = {
|
||||
0x0f, 0x10, 0x06, // movups (%rsi), %xmm0
|
||||
0xc5, 0xfc, 0x10, 0x0e, // vmovups ymm1, [rsi]: dirty the upper half of ymm1
|
||||
0xc5, 0xf0, 0x52, 0xc8, // vrsqrtps xmm1, xmm0 with the vvvv trap mark
|
||||
0x0f, 0x11, 0x0f, // movups %xmm1, (%rdi)
|
||||
0xc4, 0xe3, 0x7d, 0x19, 0x4f, 0x10, 0x01, // vextractf128 [rdi + 16], ymm1, 1
|
||||
0xc5, 0xf8, 0x77, // vzeroupper
|
||||
0xc3, // ret
|
||||
};
|
||||
|
||||
// The same sequence with the ordinary VEX.128 VRSQRTPS encoding (vvvv = 1111), as it appears in guest
|
||||
// code before the patcher marks it as a trap.
|
||||
constexpr uint8_t kPlainVrsqrtpsXmm1Xmm0[] = {
|
||||
0x0f, 0x10, 0x06, // movups (%rsi), %xmm0
|
||||
0xc5, 0xfc, 0x10, 0x0e, // vmovups ymm1, [rsi]: dirty the upper half of ymm1
|
||||
0xc5, 0xf8, 0x52, 0xc8, // vrsqrtps xmm1, xmm0
|
||||
0x0f, 0x11, 0x0f, // movups %xmm1, (%rdi)
|
||||
0xc4, 0xe3, 0x7d, 0x19, 0x4f, 0x10, 0x01, // vextractf128 [rdi + 16], ymm1, 1
|
||||
0xc5, 0xf8, 0x77, // vzeroupper
|
||||
0xc3, // ret
|
||||
};
|
||||
|
||||
// The same again behind a never taken indirect branch. A function with an indirect branch does not
|
||||
// let the patcher relocate neighbours, and the 4-byte VRSQRTPS cannot hold a jump on its own, so it
|
||||
// is left in place with the trap mark.
|
||||
constexpr uint8_t kPlainVrsqrtpsIndirectBranch[] = {
|
||||
0x31, 0xc0, // xor %eax, %eax
|
||||
0x85, 0xc0, // test %eax, %eax
|
||||
0x74, 0x02, // jz over
|
||||
0xff, 0xe0, // jmp *%rax (never taken)
|
||||
0x0f, 0x10, 0x06, // movups (%rsi), %xmm0
|
||||
0xc5, 0xfc, 0x10, 0x0e, // vmovups ymm1, [rsi]: dirty the upper half of ymm1
|
||||
0xc5, 0xf8, 0x52, 0xc8, // vrsqrtps xmm1, xmm0
|
||||
0x0f, 0x11, 0x0f, // movups %xmm1, (%rdi)
|
||||
0xc4, 0xe3, 0x7d, 0x19, 0x4f, 0x10, 0x01, // vextractf128 [rdi + 16], ymm1, 1
|
||||
0xc5, 0xf8, 0x77, // vzeroupper
|
||||
0xc3, // ret
|
||||
};
|
||||
|
||||
constexpr uint8_t kMwaitx[] = {
|
||||
0x0f, 0x01, 0xfb, // mwaitx
|
||||
0xc3, // ret
|
||||
};
|
||||
|
||||
constexpr size_t kPageSize = 4096;
|
||||
|
||||
std::atomic<int> g_illegal_hits {0};
|
||||
@@ -233,6 +282,13 @@ void TestInsertqRegister() {
|
||||
}
|
||||
}
|
||||
|
||||
void TestMwaitxEmulated() {
|
||||
auto fn = MapCode<VoidBlobFn>(kMwaitx);
|
||||
const int before = IllegalHits();
|
||||
fn();
|
||||
Check(IllegalHits() > before, "host raises SIGILL for MWAITX");
|
||||
}
|
||||
|
||||
void TestUnknownInstructionRefused() {
|
||||
g_refused.store(false, std::memory_order_relaxed);
|
||||
g_refuse_skip.store(2, std::memory_order_relaxed);
|
||||
@@ -244,6 +300,170 @@ void TestUnknownInstructionRefused() {
|
||||
g_refuse_skip.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// The loader rewrites SSE4a instructions into native trampolines when the patcher is enabled. The
|
||||
// trampoline area lives right behind the code page, like it does behind a mapped guest module.
|
||||
struct PatchedCode {
|
||||
void *code = nullptr;
|
||||
Loader::GuestInstructionPatchResult result {};
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
PatchedCode MapPatchedCode(const uint8_t (&blob)[N], bool emulate_amd) {
|
||||
void *area = ::mmap(nullptr, 2 * kPageSize, PROT_READ | PROT_WRITE | PROT_EXEC,
|
||||
MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
Check(area != MAP_FAILED, "map RWX code and trampoline area");
|
||||
std::memcpy(area, blob, N);
|
||||
auto *trampoline = static_cast<uint8_t *>(area) + kPageSize;
|
||||
Loader::RegisterGuestInstructionPatchModule(area, kPageSize, trampoline, kPageSize);
|
||||
const std::array<uintptr_t, 1> function_starts {reinterpret_cast<uintptr_t>(area)};
|
||||
// A host without SSE4a, RDPID and CLWB, which is what Rosetta presents.
|
||||
const Loader::GuestInstructionHostFeatures host {false, false, false};
|
||||
PatchedCode patched;
|
||||
patched.code = area;
|
||||
patched.result = Loader::PatchGuestInstructions(reinterpret_cast<uintptr_t>(area), N, function_starts,
|
||||
false, emulate_amd, host);
|
||||
Loader::UnregisterGuestInstructionPatchModule(area);
|
||||
return patched;
|
||||
}
|
||||
|
||||
using PatchCounts = Loader::InstructionPatchCounts Loader::GuestInstructionPatchResult::*;
|
||||
|
||||
// Runs the blob through the SIGILL emulator (reference) and through the patched copy, which must
|
||||
// produce the same bytes. EXTRQ becomes a native trampoline without a single SIGILL. INSERTQ is
|
||||
// marked as a trap by the patcher and keeps going through the signal emulator.
|
||||
template <typename Fn, size_t N, typename Invoke>
|
||||
void CheckPatchedMatchesEmulator(const uint8_t (&blob)[N], PatchCounts counts, bool native,
|
||||
Invoke invoke, const char *text) {
|
||||
uint64_t reference[2] {};
|
||||
const int before_reference = IllegalHits();
|
||||
invoke(MapCode<Fn>(blob), reference);
|
||||
Check(IllegalHits() > before_reference, text);
|
||||
|
||||
const auto unpatched = MapPatchedCode(blob, false);
|
||||
Check((unpatched.result.*counts).found == 0, "patcher stays off without emulate_amd");
|
||||
uint64_t unpatched_out[2] {};
|
||||
const int before_off = IllegalHits();
|
||||
invoke(reinterpret_cast<Fn>(unpatched.code), unpatched_out);
|
||||
Check(IllegalHits() > before_off, "unpatched copy still raises SIGILL");
|
||||
|
||||
const auto patched = MapPatchedCode(blob, true);
|
||||
Check((patched.result.*counts).found == 1, text);
|
||||
Check((patched.result.*counts).native == (native ? 1u : 0u), "native trampoline count");
|
||||
Check((patched.result.*counts).trapped == (native ? 0u : 1u), "trapped (signal path) count");
|
||||
uint64_t out[2] {};
|
||||
const int before = IllegalHits();
|
||||
invoke(reinterpret_cast<Fn>(patched.code), out);
|
||||
if (native) {
|
||||
Check(IllegalHits() == before, "patched code raises no SIGILL");
|
||||
} else {
|
||||
Check(IllegalHits() > before, "trapped instruction still goes through the signal emulator");
|
||||
}
|
||||
Check(out[0] == reference[0] && out[1] == reference[1], "patched result equals emulated result");
|
||||
}
|
||||
|
||||
void TestPatchedExtrqImmediate() {
|
||||
CheckPatchedMatchesEmulator<BlobFn>(
|
||||
kExtrqXmm2Len40Index0, &Loader::GuestInstructionPatchResult::extrq, true,
|
||||
[](BlobFn fn, uint64_t *out) { fn(out, 0.0, 0.0, std::bit_cast<double>(0xaabbccddeeff1122ull)); },
|
||||
"EXTRQ immediate found by the patcher");
|
||||
}
|
||||
|
||||
void TestPatchedExtrqRexHigh() {
|
||||
CheckPatchedMatchesEmulator<LoadBlobFn>(
|
||||
kExtrqXmm10Len40Index0, &Loader::GuestInstructionPatchResult::extrq, true,
|
||||
[](LoadBlobFn fn, uint64_t *out) {
|
||||
const uint64_t in[2] {0xaabbccddeeff1122ull, 0x5555555555555555ull};
|
||||
fn(out, in);
|
||||
},
|
||||
"REX EXTRQ found by the patcher");
|
||||
}
|
||||
|
||||
void TestPatchedInsertqImmediate() {
|
||||
CheckPatchedMatchesEmulator<BlobFn>(
|
||||
kInsertqXmm2Xmm0Len32Index0, &Loader::GuestInstructionPatchResult::insertq, false,
|
||||
[](BlobFn fn, uint64_t *out) {
|
||||
fn(out, std::bit_cast<double>(0x1122334455667788ull), 0.0,
|
||||
std::bit_cast<double>(0x0123456789abcdefull));
|
||||
},
|
||||
"INSERTQ immediate found by the patcher");
|
||||
}
|
||||
|
||||
void TestPatchedInsertqIndexedRexHigh() {
|
||||
CheckPatchedMatchesEmulator<PairBlobFn>(
|
||||
kInsertqXmm10Xmm9Len16Index8, &Loader::GuestInstructionPatchResult::insertq, false,
|
||||
[](PairBlobFn fn, uint64_t *out) {
|
||||
const uint64_t src[2] {0x8877665544332211ull, 0xdeadbeefcafebabeull};
|
||||
const uint64_t dest[2] {0xaaaaaaaaaaaaaaaaull, 0xbbbbbbbbbbbbbbbbull};
|
||||
fn(out, src, dest);
|
||||
},
|
||||
"indexed REX INSERTQ found by the patcher");
|
||||
}
|
||||
|
||||
void TestPatchedInsertqRegister() {
|
||||
CheckPatchedMatchesEmulator<PairBlobFn>(
|
||||
kInsertqXmm10Xmm9, &Loader::GuestInstructionPatchResult::insertq, false,
|
||||
[](PairBlobFn fn, uint64_t *out) {
|
||||
const uint64_t src[2] {0x0123456789abcdefull, 0xdeadbeef0000c4c8ull};
|
||||
const uint64_t dest[2] {0x8877665544332211ull, 0xfedcba9876543210ull};
|
||||
fn(out, src, dest);
|
||||
},
|
||||
"register INSERTQ found by the patcher");
|
||||
}
|
||||
|
||||
// The ordinary VEX.128 VRSQRTPS must be found by the patcher and give the reference result, either
|
||||
// through a native trampoline (no SIGILL) or, when it cannot be relocated, marked as a trap (the
|
||||
// vvvv bit is cleared in place) and emulated from the signal handler.
|
||||
template <size_t N>
|
||||
void CheckPatchedReciprocalSquareRoot(const uint8_t (&blob)[N], bool native) {
|
||||
const uint32_t in_bits[8] {std::bit_cast<uint32_t>(4.0f), 0x00000000u, 0xbf800000u, 0x7f800000u,
|
||||
0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u};
|
||||
const auto Run = [&](LoadBlobFn fn, uint32_t (&out_bits)[8]) {
|
||||
fn(reinterpret_cast<uint64_t *>(out_bits), reinterpret_cast<const uint64_t *>(in_bits));
|
||||
};
|
||||
uint32_t reference[8] {};
|
||||
const int before_reference = IllegalHits();
|
||||
Run(MapCode<LoadBlobFn>(kTrappedVrsqrtpsXmm1Xmm0), reference);
|
||||
Check(IllegalHits() > before_reference, "host raises SIGILL for the pre-marked VRSQRTPS");
|
||||
|
||||
const auto unpatched = MapPatchedCode(blob, false);
|
||||
Check(unpatched.result.reciprocal_sqrt.found == 0, "patcher leaves VRSQRTPS alone without emulate_amd");
|
||||
|
||||
const auto patched = MapPatchedCode(blob, true);
|
||||
const auto &counts = patched.result.reciprocal_sqrt;
|
||||
Check(counts.found == 1, "patcher finds the ordinary VEX.128 VRSQRTPS");
|
||||
Check(counts.native == (native ? 1u : 0u) && counts.trapped == (native ? 0u : 1u),
|
||||
"VRSQRTPS native trampoline / trap count");
|
||||
const auto *code = static_cast<const uint8_t *>(patched.code);
|
||||
size_t site = 0;
|
||||
while (site + 2 < N && !(blob[site] == 0xc5 && blob[site + 1] == 0xf8 && blob[site + 2] == 0x52)) {
|
||||
++site;
|
||||
}
|
||||
Check(site + 2 < N, "locate VRSQRTPS in the test blob");
|
||||
if (!native) {
|
||||
Check(code[site] == 0xc5 && code[site + 1] == 0xf0 && code[site + 2] == 0x52,
|
||||
"the trap mark clears a reserved VEX.vvvv bit (c5 f8 -> c5 f0)");
|
||||
} else {
|
||||
Check(code[site] != 0xc5 || code[site + 1] != 0xf8, "native trampoline replaced the VRSQRTPS");
|
||||
}
|
||||
uint32_t out[8] {};
|
||||
const int before = IllegalHits();
|
||||
Run(reinterpret_cast<LoadBlobFn>(patched.code), out);
|
||||
if (native) {
|
||||
Check(IllegalHits() == before, "natively patched VRSQRTPS raises no SIGILL");
|
||||
} else {
|
||||
Check(IllegalHits() > before, "marked VRSQRTPS goes through the signal emulator");
|
||||
}
|
||||
Check(std::memcmp(out, reference, sizeof(out)) == 0, "patched VRSQRTPS result equals the emulated reference");
|
||||
Check(std::bit_cast<float>(out[0]) == 0.5f && out[1] == 0x7f800000u && out[2] == 0xffc00000u && out[3] == 0,
|
||||
"patched VRSQRTPS produces the exact reciprocal square roots");
|
||||
Check(out[4] == 0 && out[5] == 0 && out[6] == 0 && out[7] == 0, "patched VEX.128 form zeroes the upper half");
|
||||
}
|
||||
|
||||
void TestPatchedReciprocalSquareRoot() {
|
||||
CheckPatchedReciprocalSquareRoot(kPlainVrsqrtpsXmm1Xmm0, true);
|
||||
CheckPatchedReciprocalSquareRoot(kPlainVrsqrtpsIndirectBranch, false);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
@@ -255,7 +475,14 @@ int main() {
|
||||
TestRexHighRegisterExtrq();
|
||||
TestRexHighRegisterInsertqWithIndex();
|
||||
TestInsertqRegister();
|
||||
TestMwaitxEmulated();
|
||||
TestUnknownInstructionRefused();
|
||||
TestPatchedExtrqImmediate();
|
||||
TestPatchedExtrqRexHigh();
|
||||
TestPatchedInsertqImmediate();
|
||||
TestPatchedInsertqIndexedRexHigh();
|
||||
TestPatchedInsertqRegister();
|
||||
TestPatchedReciprocalSquareRoot();
|
||||
std::printf("macosSse4aTests: all passed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user