Compare commits

...

6 Commits

Author SHA1 Message Date
nmzik e7f6327068 net: emulate Windows PEEK and WAITALL receives
Co-authored-by: Firas Ben Mbarek <firasbenmbarek@outlook.com>
Co-authored-by: rudy-07 <kanhaiffco2007@gmail.com>
Co-authored-by: Matthew Hurley <171001439+theantipopau@users.noreply.github.com>
Co-authored-by: usopp-sama <go4av05.pvtx@gmail.com>
Co-authored-by: wruwami <wruwami@users.noreply.github.com>
2026-10-03 17:14:41 +02:00
nmzik 19a9464b7d loader: add INSERTQ RDPID and CLWB fallbacks
Co-authored-by: nomolao2-cell <nomolao2@gmail.com>
2026-10-03 20:14:50 +04:00
Ewen trividic a4d26cf81a loader: replace SSE4a EXTRQ with native trampolines on hosts without SSE4a (#839)
loader: add native EXTRQ patches with trap fallback

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-03 19:40:51 +04:00
Ewen trividic b11aea803c loader: native trampoline for VRSQRTPS under --amd-cpu (#893)
loader: add AMD CPU compatibility patches

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-03 19:13:12 +04:00
Aryan Sharma feeced81d2 window: auto-hide the cursor over the game window (#958)
* window: auto-hide the cursor over the game window

Hide the cursor when the game window is created and whenever the mouse
has been idle for two seconds; any mouse motion, button or wheel event
shows it again. Games that draw their own cursor (e.g. Blue Prince) no
longer show the host pointer on top of it.

Only real mouse input counts as activity: synthetic touch/pen mouse
events and zero-delta motion (sent by some platforms on focus or cursor
visibility changes) are ignored. The event loop only waits with a
timeout while the cursor is visible, so it still blocks indefinitely
once the cursor is hidden. The cursor is restored on shutdown.

Uses SDL_HideCursor/SDL_ShowCursor, which SDL implements for X11,
Wayland, Cocoa and Win32 and which only affect the cursor over the
emulator window.

Closes #282

* window: make cursor auto-hide optional in CLI and launcher

Co-authored-by: Anatolii Kucheruk <anatolii.kucheruk01@gmail.com>

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
Co-authored-by: Anatolii Kucheruk <anatolii.kucheruk01@gmail.com>
2026-10-03 18:15:15 +04:00
Pyxellator 820856b301 launcher: dualsense vibration/volume setting (#973)
* pad: send DualSense speaker and haptic audio over Bluetooth

* ci: fix Opus clang-cl build and macOS memory tracker test

* docs: remove Bluetooth notes from project README

* pad: keep Bluetooth audio work off emulation timing path

* pad: smooth DualSense speaker audio on USB and Bluetooth

* tests: find available macOS guest range for page manager

* pad: keep haptic waveform when Bluetooth queue overflows

* audio: reject zero output sample frequency

* Simplify DualSense audio routing and fix Bluetooth batching

* controller: add global DualSense volume and vibration controls

* Simplify global controller settings

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-03 18:02:59 +04:00
25 changed files with 2320 additions and 525 deletions
+2 -1
View File
@@ -25,7 +25,7 @@ FetchContent_MakeAvailable(opus)
set(BUILD_TESTING "${kyty_build_testing_saved}")
install(FILES "${opus_SOURCE_DIR}/COPYING" DESTINATION "licenses/opus")
# The Windows guest red-zone patcher needs a decoder/encoder and a small
# The guest instruction patcher needs a decoder/encoder and a small
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
@@ -48,6 +48,7 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(xbyak zydis)
target_compile_definitions(xbyak INTERFACE XBYAK_NO_EXCEPTION)
# ZArchive provides random-access reads for compressed game dumps. Build only
# the reader/writer library and its zstd dependency; the command-line tool is
+13
View File
@@ -23,6 +23,7 @@ void Load(const ConfigOptions& cfg) {
EXIT_IF(g_config == nullptr);
EXIT_IF(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH);
EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id));
EXIT_IF(cfg.controller_speaker_volume > 100 || cfg.controller_vibration_intensity > 100);
*g_config = cfg;
}
@@ -51,6 +52,14 @@ const std::optional<ControllerColor>& GetControllerColor() {
return g_config->controller_color;
}
uint32_t GetControllerSpeakerVolume() {
return g_config->controller_speaker_volume;
}
uint32_t GetControllerVibrationIntensity() {
return g_config->controller_vibration_intensity;
}
PresentMode GetPresentMode() {
return g_config->present_mode;
}
@@ -63,6 +72,10 @@ bool FullscreenEnabled() {
return g_config->fullscreen_enabled;
}
bool HideCursorEnabled() {
return g_config->hide_cursor_enabled;
}
bool VrEnabled() {
return g_config->vr_enabled;
}
+6
View File
@@ -48,9 +48,12 @@ struct ConfigOptions {
int32_t user_id = DEFAULT_USER_ID;
std::string audio_input_device;
std::optional<ControllerColor> controller_color;
uint32_t controller_speaker_volume = 100;
uint32_t controller_vibration_intensity = 100;
PresentMode present_mode = PresentMode::Mailbox;
int32_t gpu_index = -1;
bool fullscreen_enabled = false;
bool hide_cursor_enabled = false;
bool vr_enabled = false;
bool amd_cpu_enabled = false;
uint32_t vblank_frequency = 60;
@@ -86,9 +89,12 @@ const std::string& GetUserName();
int32_t GetUserId();
const std::string& GetAudioInputDevice();
const std::optional<ControllerColor>& GetControllerColor();
uint32_t GetControllerSpeakerVolume();
uint32_t GetControllerVibrationIntensity();
PresentMode GetPresentMode();
int32_t GetGpuIndex();
bool FullscreenEnabled();
bool HideCursorEnabled();
bool VrEnabled();
bool AmdCpuEnabled();
uint32_t GetVblankFrequency();
+50 -8
View File
@@ -72,6 +72,7 @@ struct Binding {
};
constexpr int MOUSE_POLL_INTERVAL_MS = 33;
constexpr uint64_t CURSOR_IDLE_HIDE_MS = 2000;
constexpr std::string_view MOUSE_SENSITIVITY = "MouseSensitivity=";
struct MouseJoystickState {
@@ -81,7 +82,8 @@ struct MouseJoystickState {
};
MouseJoystickState g_mouse;
SDL_Window* g_mouse_window = nullptr;
SDL_Window* g_mouse_window = nullptr;
uint64_t g_cursor_hide_at = 0;
std::size_t ControlFromName(std::string_view name) {
const auto info = std::find_if(CONTROL_INFO.begin(), CONTROL_INFO.end(),
@@ -380,11 +382,31 @@ int PollMouse(uint64_t now_ms) {
return MOUSE_POLL_INTERVAL_MS;
}
bool IsCursorActivity(const SDL_Event& event) {
SDL_MouseID which;
switch (event.type) {
case SDL_EVENT_MOUSE_MOTION:
if (event.motion.xrel == 0.0f && event.motion.yrel == 0.0f) {
return false;
}
which = event.motion.which;
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP: which = event.button.which; break;
case SDL_EVENT_MOUSE_WHEEL: which = event.wheel.which; break;
default: return false;
}
return which != SDL_TOUCH_MOUSEID && which != SDL_PEN_MOUSEID;
}
} // namespace
void HostInputInit(SDL_Window* window) {
GetInputMap();
g_mouse_window = window;
if (Config::HideCursorEnabled()) {
g_cursor_hide_at = SDL_GetTicks() + CURSOR_IDLE_HIDE_MS;
}
}
void HostInputShutdown() {
@@ -393,7 +415,11 @@ void HostInputShutdown() {
CenterMouseStick();
g_mouse = {};
}
g_mouse_window = nullptr;
g_mouse_window = nullptr;
g_cursor_hide_at = 0;
if (Config::HideCursorEnabled()) {
SDL_ShowCursor();
}
}
void HostInputKey(int key_code, bool down) {
@@ -429,20 +455,36 @@ void HostInputToggleMouseToJoystick() {
}
bool HostInputWaitEvent(SDL_Event* event) {
bool has_event;
int timeout = -1;
if (!g_mouse.enabled || SDL_GetKeyboardFocus() != g_mouse_window) {
g_mouse.next_poll = 0;
CenterMouseStick();
has_event = SDL_WaitEvent(event);
if (!has_event) {
EXIT("%s\n", SDL_GetError());
}
} else {
if (g_mouse.next_poll == 0) {
SDL_GetRelativeMouseState(nullptr, nullptr);
g_mouse.next_poll = SDL_GetTicks() + MOUSE_POLL_INTERVAL_MS;
}
has_event = SDL_WaitEventTimeout(event, PollMouse(SDL_GetTicks()));
timeout = PollMouse(SDL_GetTicks());
}
if (g_cursor_hide_at != 0) {
const auto now_ms = SDL_GetTicks();
const int cursor_timeout =
now_ms < g_cursor_hide_at ? static_cast<int>(g_cursor_hide_at - now_ms) : 0;
timeout = timeout < 0 ? cursor_timeout : std::min(timeout, cursor_timeout);
}
const bool has_event = SDL_WaitEventTimeout(event, timeout);
if (Config::HideCursorEnabled()) {
const auto now_ms = SDL_GetTicks();
if (has_event && !g_mouse.enabled && IsCursorActivity(*event) &&
SDL_GetWindowFromEvent(event) == g_mouse_window) {
SDL_ShowCursor();
g_cursor_hide_at = now_ms + CURSOR_IDLE_HIDE_MS;
} else if (g_cursor_hide_at != 0 && now_ms >= g_cursor_hide_at) {
SDL_HideCursor();
g_cursor_hide_at = 0;
}
}
if (has_event && event->type == SDL_EVENT_WINDOW_FOCUS_LOST &&
+83 -23
View File
@@ -23,7 +23,7 @@
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="0">
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="sectionsLayout" columnstretch="1,1">
<property name="horizontalSpacing">
<number>8</number>
@@ -72,27 +72,6 @@
<property name="toolTip"><string>Language</string></property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_controller_color">
<property name="text"><string>DualSense lightbar:</string></property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="controllerColorLayout">
<item>
<widget class="QPushButton" name="button_controller_color">
<property name="toolTip"><string>Choose and preview a fixed LED color. Bluetooth DualSense may take several seconds to update.</string></property>
<property name="text"><string>Custom</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_controller_color_reset">
<property name="toolTip"><string>Let the game control the lightbar</string></property>
<property name="text"><string>Default</string></property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
@@ -194,6 +173,12 @@
<property name="text"><string>Enable tessellation support (experimental)</string></property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="checkBox_hide_cursor">
<property name="toolTip"><string>Hide the host cursor after 2 seconds without mouse activity</string></property>
<property name="text"><string>Auto-hide cursor</string></property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -316,7 +301,82 @@
</item>
</layout>
</item>
<item row="2" column="0">
<item row="1" column="1">
<widget class="QGroupBox" name="controller_group">
<property name="title"><string>Controller</string></property>
<layout class="QFormLayout" name="controllerLayout">
<property name="fieldGrowthPolicy"><enum>QFormLayout::AllNonFixedFieldsGrow</enum></property>
<item row="0" column="0">
<widget class="QLabel" name="label_controller_color">
<property name="text"><string>DualSense lightbar:</string></property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="controllerColorLayout">
<item>
<widget class="QPushButton" name="button_controller_color">
<property name="toolTip"><string>Choose and preview a fixed LED color. Bluetooth DualSense may take several seconds to update.</string></property>
<property name="text"><string>Custom</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_controller_color_reset">
<property name="toolTip"><string>Let the game control the lightbar</string></property>
<property name="text"><string>Default</string></property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_controller_vibration">
<property name="text"><string>Vibration intensity:</string></property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="controllerVibrationLayout">
<item>
<widget class="QSlider" name="slider_controller_vibration">
<property name="toolTip"><string>Scale DualSense vibration over USB and Bluetooth</string></property>
<property name="maximum"><number>100</number></property>
<property name="value"><number>100</number></property>
<property name="orientation"><enum>Qt::Horizontal</enum></property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_controller_vibration_value">
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
<property name="text"><string>100%</string></property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_controller_volume">
<property name="text"><string>Speaker volume:</string></property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="controllerVolumeLayout">
<item>
<widget class="QSlider" name="slider_controller_volume">
<property name="toolTip"><string>Scale the DualSense controller speaker over USB and Bluetooth</string></property>
<property name="maximum"><number>100</number></property>
<property name="value"><number>100</number></property>
<property name="orientation"><enum>Qt::Horizontal</enum></property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_controller_volume_value">
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
<property name="text"><string>100%</string></property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="buttonLayout">
<item>
<widget class="QPushButton" name="clear_button">
+31 -5
View File
@@ -46,6 +46,30 @@ QString EnumToText(T value) {
: key);
}
struct ControllerSettings {
QString color;
int speaker_volume = 100;
int vibration_intensity = 100;
void WriteSettings(QSettings* s) const {
s->setValue("controller_color", color);
s->setValue("controller_speaker_volume", speaker_volume);
s->setValue("controller_vibration_intensity", vibration_intensity);
}
void ReadSettings(QSettings* s) {
const QColor saved_color(s->value("controller_color").toString());
color = saved_color.isValid() ? saved_color.name(QColor::HexRgb) : QString {};
const auto read_percent = [s](const char* key) {
bool ok = false;
const int value = s->value(key, 100).toInt(&ok);
return ok ? qBound(0, value, 100) : 100;
};
speaker_volume = read_percent("controller_speaker_volume");
vibration_intensity = read_percent("controller_vibration_intensity");
}
};
class Configuration: public QObject {
Q_OBJECT
@@ -85,14 +109,17 @@ public:
GameStatus game_status = GameStatus::Unknown;
QString game_comment;
// Controller preferences always come from the global configuration.
ControllerSettings controller;
Resolution screen_resolution = Resolution::R1280X720;
QString user_name = "Kyty";
int user_id = Config::DEFAULT_USER_ID;
QString audio_input_device;
QString controller_color;
PresentMode present_mode = PresentMode::Mailbox;
int gpu_index = -1;
bool fullscreen_enabled = false;
bool hide_cursor_enabled = false;
bool readback_linear_images = false;
bool tessellation_enabled = false;
int vblank_frequency = 60;
@@ -121,10 +148,10 @@ public:
user_name = other.user_name;
user_id = other.user_id;
audio_input_device = other.audio_input_device;
controller_color = other.controller_color;
present_mode = other.present_mode;
gpu_index = other.gpu_index;
fullscreen_enabled = other.fullscreen_enabled;
hide_cursor_enabled = other.hide_cursor_enabled;
readback_linear_images = other.readback_linear_images;
tessellation_enabled = other.tessellation_enabled;
vblank_frequency = other.vblank_frequency;
@@ -168,10 +195,10 @@ public:
KYTY_CFG_SET(user_name);
KYTY_CFG_SET(user_id);
KYTY_CFG_SET(audio_input_device);
KYTY_CFG_SET(controller_color);
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(vblank_frequency);
@@ -208,14 +235,13 @@ public:
? saved_user_id
: Config::DEFAULT_USER_ID;
audio_input_device = s->value("audio_input_device", audio_input_device).toString();
const QColor color(s->value("controller_color", controller_color).toString());
controller_color = color.isValid() ? color.name(QColor::HexRgb) : QString {};
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);
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
@@ -28,7 +28,7 @@ public:
static void WriteSettings(QSettings& s);
static void ReadSettings(QSettings& s);
void SetGameDirectories(const QStringList& dirs);
void SetGlobalSettings(const QStringList& dirs);
[[nodiscard]] QStringList GetGameDirectories() const;
signals:
@@ -40,7 +40,7 @@ private:
QGroupBox* m_game_dirs_group = nullptr;
QListWidget* m_game_dirs_list = nullptr;
QToolButton* m_remove_game_dir_button = nullptr;
bool m_show_game_dirs = false;
bool m_global_settings = false;
protected:
void Init(const Configuration& info);
@@ -34,7 +34,7 @@ public:
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
[[nodiscard]] const QString& GetGlobalControllerColor() const {
return m_global_info.controller_color;
return m_global_info.controller.color;
}
[[nodiscard]] std::unique_ptr<Configuration>
CreateConfiguration(const ConfigurationItem& item) const;
+27 -10
View File
@@ -23,6 +23,7 @@
#include <QPushButton>
#include <QResizeEvent>
#include <QSettings>
#include <QSlider>
#include <QSpinBox>
#include <QStyle>
#include <QToolButton>
@@ -39,7 +40,7 @@
constexpr char SETTINGS_CFG_DIALOG[] = "ConfigurationEditDialog";
constexpr char SETTINGS_CFG_LAST_GEOMETRY[] = "geometry";
constexpr int GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH = 560;
constexpr int GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH = 400;
static void UpdateControllerColorButton(QPushButton* button, const QString& hex) {
const QColor color(hex);
@@ -123,6 +124,7 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
m_info(info) {
m_ui->setupUi(this);
InitGameDirectories();
m_ui->controller_group->setVisible(false);
connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save);
connect(m_ui->cancel_button, &QPushButton::clicked, this, &QDialog::reject);
@@ -142,6 +144,12 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
UpdateControllerColorButton(m_ui->button_controller_color, {});
emit PreviewControllerColor({});
});
connect(m_ui->slider_controller_vibration, &QSlider::valueChanged, this, [this](int value) {
m_ui->label_controller_vibration_value->setText(tr("%1%").arg(value));
});
connect(m_ui->slider_controller_volume, &QSlider::valueChanged, this, [this](int value) {
m_ui->label_controller_volume_value->setText(tr("%1%").arg(value));
});
connect(m_ui->comboBox_shader_log_direction, &QComboBox::currentTextChanged, this,
[this](const QString& text) {
auto log = TextToEnum<Configuration::LogDirection>(text);
@@ -201,7 +209,9 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->lineEdit_user_name->setMaxLength(static_cast<int>(Config::MAX_USER_NAME_LENGTH));
m_ui->lineEdit_user_name->setText(info.user_name);
m_ui->spinBox_user_id->setValue(info.user_id);
UpdateControllerColorButton(m_ui->button_controller_color, info.controller_color);
UpdateControllerColorButton(m_ui->button_controller_color, info.controller.color);
m_ui->slider_controller_vibration->setValue(info.controller.vibration_intensity);
m_ui->slider_controller_volume->setValue(info.controller.speaker_volume);
auto* microphone = m_ui->comboBox_audio_input_device;
microphone->clear();
microphone->addItem(tr("None"), QString {});
@@ -263,6 +273,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
info.gpu_index >= 0 && info.gpu_index < m_ui->comboBox_gpu->count() - 1 ? info.gpu_index + 1
: 0);
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
@@ -342,8 +353,8 @@ void ConfigurationEditDialog::InitGameDirectories() {
update_game_directory_buttons();
}
void ConfigurationEditDialog::SetGameDirectories(const QStringList& dirs) {
m_show_game_dirs = true;
void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs) {
m_global_settings = true;
m_game_dirs_list->clear();
m_game_dirs_group->setMinimumWidth(GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH);
@@ -352,6 +363,7 @@ void ConfigurationEditDialog::SetGameDirectories(const QStringList& dirs) {
}
m_game_dirs_group->setVisible(true);
m_ui->controller_group->setVisible(true);
update_game_directory_buttons();
layout()->activate();
resize(size().expandedTo(minimumSizeHint()));
@@ -359,7 +371,7 @@ void ConfigurationEditDialog::SetGameDirectories(const QStringList& dirs) {
QStringList ConfigurationEditDialog::GetGameDirectories() const {
QStringList dirs;
if (!m_show_game_dirs) {
if (!m_global_settings) {
return dirs;
}
@@ -399,17 +411,22 @@ void ConfigurationEditDialog::resizeEvent(QResizeEvent* event) {
g_last_geometry = saveGeometry();
}
static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, bool global_settings) {
info.user_name = ui.lineEdit_user_name->text().trimmed();
info.user_id = ui.spinBox_user_id->value();
info.audio_input_device = ui.comboBox_audio_input_device->currentData().toString();
info.controller_color = ui.button_controller_color->property("controllerColor").toString();
if (global_settings) {
info.controller.color = ui.button_controller_color->property("controllerColor").toString();
info.controller.vibration_intensity = ui.slider_controller_vibration->value();
info.controller.speaker_volume = ui.slider_controller_volume->value();
}
info.screen_resolution =
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
info.present_mode =
TextToEnum<Configuration::PresentMode>(ui.comboBox_present_mode->currentText());
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
@@ -453,7 +470,7 @@ void ConfigurationEditDialog::save() {
return;
}
UpdateInfo(m_info, *m_ui);
UpdateInfo(m_info, *m_ui, m_global_settings);
emit accept();
}
@@ -461,9 +478,9 @@ void ConfigurationEditDialog::save() {
void ConfigurationEditDialog::clear() {
Configuration default_info;
Init(default_info);
emit PreviewControllerColor({});
if (m_show_game_dirs) {
if (m_global_settings) {
emit PreviewControllerColor({});
m_game_dirs_list->clear();
update_game_directory_buttons();
}
+6 -1
View File
@@ -314,6 +314,7 @@ void ConfigurationListWidget::WriteSettings() {
s->remove(CONF_GLOBAL);
s->beginGroup(CONF_GLOBAL);
m_global_info.WriteSettings(s.get());
m_global_info.controller.WriteSettings(s.get());
s->endGroup();
s->remove(CONF_SECTION_NAME);
@@ -357,6 +358,7 @@ void ConfigurationListWidget::ReadSettings() {
if (!s->childKeys().isEmpty()) {
m_global_info.ReadSettings(s.get());
}
m_global_info.controller.ReadSettings(s.get());
s->endGroup();
qDeleteAll(m_custom_infos);
@@ -409,6 +411,7 @@ ConfigurationListWidget::CreateConfiguration(const ConfigurationItem& item) cons
const auto* custom = m_custom_infos.value(item.GetInfo().game_path);
info->CopyGameInfoFrom(item.GetInfo());
info->CopyEmulatorSettingsFrom(custom != nullptr ? *custom : m_global_info);
info->controller = m_global_info.controller;
if (custom != nullptr && !custom->elf.isEmpty()) {
info->elf = custom->elf;
}
@@ -787,16 +790,18 @@ void ConfigurationListWidget::delete_configuartion() {
void ConfigurationListWidget::edit_global_settings() {
Configuration info;
info.CopyEmulatorSettingsFrom(m_global_info);
info.controller = m_global_info.controller;
info.name = tr("Global settings");
ConfigurationEditDialog dlg(info, this);
dlg.setWindowTitle(tr("Global settings"));
dlg.SetGameDirectories(m_game_dirs);
dlg.SetGlobalSettings(m_game_dirs);
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
&ConfigurationListWidget::PreviewControllerColor);
if (dlg.exec() == QDialog::Accepted) {
m_global_info.CopyEmulatorSettingsFrom(info);
m_global_info.controller = info.controller;
const auto game_dirs = NormalizeGameDirectories(dlg.GetGameDirectories());
const bool game_dirs_changed = game_dirs != m_game_dirs;
m_game_dirs = game_dirs;
+8 -4
View File
@@ -228,9 +228,11 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
if (!info.audio_input_device.isEmpty()) {
args << "--mic" << info.audio_input_device;
}
if (!info.controller_color.isEmpty()) {
args << "--controller-color" << info.controller_color;
if (!info.controller.color.isEmpty()) {
args << "--controller-color" << info.controller.color;
}
args << "--controller-volume" << QString::number(info.controller.speaker_volume);
args << "--controller-vibration" << QString::number(info.controller.vibration_intensity);
args << "--present-mode" << EnumToText(info.present_mode);
if (info.gpu_index >= 0) {
args << "--gpu" << QString::number(info.gpu_index);
@@ -238,6 +240,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
if (info.fullscreen_enabled) {
args << "--fullscreen";
}
if (info.hide_cursor_enabled) {
args << "--hide-cursor";
}
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
if (info.tessellation_enabled) {
args << "--tessellation";
@@ -529,8 +534,7 @@ void MainDialogPrivate::Update() {
m_lightbar.Stop();
return;
}
m_lightbar.SetColor(item != nullptr ? m_ui->widget->CreateConfiguration(*item)->controller_color
: m_ui->widget->GetGlobalControllerColor());
m_lightbar.SetColor(m_ui->widget->GetGlobalControllerColor());
}
#include "mainDialog.moc"
+7 -1
View File
@@ -223,7 +223,13 @@ void GameController::CycleSetting(Setting setting) {
float GameController::GetSettingScale(Setting setting) const {
const auto step = m_setting_steps[static_cast<size_t>(setting)].load(std::memory_order_relaxed);
return setting == Setting::SpeakerVolume ? SPEAKER_VOLUME[step] : INTENSITY[step];
if (setting == Setting::SpeakerVolume) {
return SPEAKER_VOLUME[step] * (Config::GetControllerSpeakerVolume() / 100.0f);
}
if (setting == Setting::VibrationIntensity) {
return INTENSITY[step] * (Config::GetControllerVibrationIntensity() / 100.0f);
}
return INTENSITY[step];
}
static uint8_t Scale(uint8_t value, float scale) {
+218 -24
View File
@@ -849,20 +849,38 @@ using SocketIoLength = size_t;
#endif
struct SocketTransport {
explicit SocketTransport(NativeSocket value): socket(value) {}
SocketTransport(NativeSocket value, int socket_type): socket(value), type(socket_type) {}
~SocketTransport() { Close(); }
bool HasPendingReceive() const {
#if defined(_WIN32)
return buffered_bytes.load(std::memory_order_relaxed) != 0 ||
receive_shutdown.load(std::memory_order_relaxed);
#else
return false;
#endif
}
int Close() {
if (socket == INVALID_NATIVE_SOCKET) {
#if defined(_WIN32)
receive_shutdown.store(true, std::memory_order_relaxed);
#endif
const auto value = socket.exchange(INVALID_NATIVE_SOCKET);
if (value == INVALID_NATIVE_SOCKET) {
return 0;
}
const auto value = std::exchange(socket, INVALID_NATIVE_SOCKET);
#if defined(_WIN32)
return closesocket(value);
#else
return ::close(value);
#endif
}
NativeSocket socket;
std::atomic<NativeSocket> socket;
const int type;
#if defined(_WIN32)
std::mutex receive_mutex;
std::vector<char> receive_buffer;
std::atomic_size_t buffered_bytes = 0;
std::atomic_bool receive_shutdown = false;
#endif
};
struct P2pEndpoint {
@@ -1164,7 +1182,8 @@ static int ConvertHostSockaddr(const sockaddr_storage* addr, SocketLength addrle
return 0;
}
static bool GetSocketBackend(int guest_fd, NativeSocket* out, SocketSlot* state = nullptr) {
static bool GetSocketBackend(int guest_fd, NativeSocket* out, SocketSlot* state = nullptr,
std::shared_ptr<SocketTransport>* transport = nullptr) {
EXIT_IF(out == nullptr);
if (guest_fd < 0 || guest_fd >= SOCKET_FD_MAX) {
@@ -1188,6 +1207,9 @@ static bool GetSocketBackend(int guest_fd, NativeSocket* out, SocketSlot* state
if (state != nullptr) {
*state = slot;
}
if (transport != nullptr) {
*transport = slot.transport;
}
return true;
}
@@ -1234,6 +1256,63 @@ static int AllocSocketFd(const std::shared_ptr<SocketTransport>& transport, bool
return -1;
}
static int SelectSockets(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds,
timeval* timeout,
const std::vector<std::shared_ptr<SocketTransport>>& stream_reads) {
#if defined(_WIN32)
if (!stream_reads.empty()) {
const auto requested_read = *readfds;
const auto requested_write = writefds != nullptr ? *writefds : fd_set {};
const auto requested_except = exceptfds != nullptr ? *exceptfds : fd_set {};
std::optional<std::chrono::steady_clock::time_point> deadline;
if (timeout != nullptr) {
deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout->tv_sec) +
std::chrono::microseconds(timeout->tv_usec);
}
for (;;) {
*readfds = requested_read;
if (writefds != nullptr) {
*writefds = requested_write;
}
if (exceptfds != nullptr) {
*exceptfds = requested_except;
}
const bool ready = std::any_of(stream_reads.begin(), stream_reads.end(),
[](const auto& transport) { return transport->HasPendingReceive(); });
// A concurrent peek can move native bytes into the guest buffer after this check.
// Bound native waits so both readiness sources are rechecked without holding receive locks.
auto wait = std::chrono::microseconds(ready ? 0 : 10'000);
if (deadline) {
const auto remaining = std::chrono::duration_cast<std::chrono::microseconds>(
*deadline - std::chrono::steady_clock::now());
wait = std::min(wait, std::max(remaining, std::chrono::microseconds::zero()));
}
timeval slice {};
slice.tv_usec = static_cast<long>(wait.count());
int result = ::select(nfds, readfds, writefds, exceptfds, &slice);
if (result < 0) {
return result;
}
for (const auto& transport: stream_reads) {
const auto socket = transport->socket.load();
if (socket == INVALID_NATIVE_SOCKET) {
WSASetLastError(WSAENOTSOCK);
return SOCKET_ERROR;
}
if (transport->HasPendingReceive() && !FD_ISSET(socket, readfds)) {
FD_SET(socket, readfds);
result++;
}
}
if (result != 0 || (deadline && std::chrono::steady_clock::now() >= *deadline)) {
return result;
}
}
}
#endif
return ::select(nfds, readfds, writefds, exceptfds, timeout);
}
static bool GuestFdIsSet(const void* fds, int fd) {
if (fds == nullptr || fd < 0 || fd >= SOCKET_FD_MAX) {
return false;
@@ -1621,6 +1700,7 @@ int KYTY_SYSV_ABI EpollWait(int eid, NetEpollEvent* events, int maxevents, int t
FD_ZERO(&host_except);
std::vector<HostRegistration> host_registrations;
std::vector<std::shared_ptr<SocketTransport>> stream_reads;
host_registrations.reserve(registrations.size());
int host_nfds = 0;
for (const auto& registration: registrations) {
@@ -1641,6 +1721,11 @@ int KYTY_SYSV_ABI EpollWait(int eid, NetEpollEvent* events, int maxevents, int t
#endif
if ((registration.event.events & EPOLL_IN) != 0) {
FD_SET(socket, &host_read);
#if defined(_WIN32)
if (state.transport->type == SOCK_STREAM) {
stream_reads.push_back(state.transport);
}
#endif
}
if ((registration.event.events & EPOLL_OUT) != 0) {
FD_SET(socket, &host_write);
@@ -1660,8 +1745,8 @@ int KYTY_SYSV_ABI EpollWait(int eid, NetEpollEvent* events, int maxevents, int t
host_timeout.tv_usec = timeout % 1000000;
host_timeout_ptr = &host_timeout;
}
const int result = ::select(host_nfds, &host_read, &host_write, &host_except, host_timeout_ptr);
const int result = SelectSockets(host_nfds, &host_read, &host_write, &host_except,
host_timeout_ptr, stream_reads);
if (result < 0) {
return SetHostSocketError();
}
@@ -1727,7 +1812,12 @@ int KYTY_SYSV_ABI SocketClose(int s) {
}
RemoveSocketFromEpolls(s);
if (slot.transport.use_count() == 1 && slot.transport->Close() != 0) {
bool close_transport = slot.transport.use_count() == 1;
#if defined(_WIN32)
// Retained receive/readiness state must not defer native TCP close cancellation.
close_transport |= !slot.p2p && slot.transport->type == SOCK_STREAM;
#endif
if (close_transport && slot.transport->Close() != 0) {
return NET_ERROR_EBADF;
}
@@ -1762,7 +1852,7 @@ int KYTY_SYSV_ABI Socket(int family, int type, int protocol) {
return SetHostSocketError();
}
auto transport = std::make_shared<SocketTransport>(socket);
auto transport = std::make_shared<SocketTransport>(socket, p2p ? SOCK_DGRAM : type);
if (p2p) {
// Logical sockets keep their own blocking mode; the shared transport never blocks.
#if defined(_WIN32)
@@ -1945,7 +2035,7 @@ int KYTY_SYSV_ABI Accept(int s, void* addr, uint32_t* addrlen) {
return SetHostSocketError();
}
auto transport = std::make_shared<SocketTransport>(accepted);
auto transport = std::make_shared<SocketTransport>(accepted, SOCK_STREAM);
const int fd = AllocSocketFd(transport);
if (fd < 0) {
*Posix::GetErrorAddr() = Posix::POSIX_EMFILE;
@@ -1974,8 +2064,9 @@ int KYTY_SYSV_ABI Shutdown(int s, int how) {
"\t how = %d\n",
s, how);
NativeSocket socket = INVALID_NATIVE_SOCKET;
if (!GetSocketBackend(s, &socket)) {
NativeSocket socket = INVALID_NATIVE_SOCKET;
std::shared_ptr<SocketTransport> transport;
if (!GetSocketBackend(s, &socket, nullptr, &transport)) {
return -1;
}
@@ -1988,6 +2079,9 @@ int KYTY_SYSV_ABI Shutdown(int s, int how) {
if (::shutdown(socket, how) == SOCKET_ERROR) {
return SetHostSocketError();
}
if (how == SD_RECEIVE || how == SD_BOTH) {
transport->receive_shutdown.store(true, std::memory_order_relaxed);
}
return 0;
#else
@@ -2205,6 +2299,77 @@ int64_t KYTY_SYSV_ABI Sendto(int s, const void* buf, uint64_t len, int flags, co
return result;
}
#if defined(_WIN32)
static int ReceiveStream(SocketTransport& transport, char* output, int length, int flags,
bool dontwait) {
std::unique_lock lock(transport.receive_mutex, std::defer_lock);
if (dontwait) {
if (!lock.try_lock()) {
return SetGuestSocketError(Posix::POSIX_EWOULDBLOCK);
}
} else {
lock.lock();
}
if (transport.receive_shutdown.load(std::memory_order_relaxed)) {
return SetGuestSocketError(Posix::POSIX_ESHUTDOWN);
}
auto& buffered = transport.receive_buffer;
const bool peek = (flags & MSG_PEEK) != 0;
const bool waitall = (flags & MSG_WAITALL) != 0;
int copied = static_cast<int>(std::min<size_t>(buffered.size(), length));
if (copied != 0) {
std::memcpy(output, buffered.data(), copied);
if (!peek) {
buffered.erase(buffered.begin(), buffered.begin() + copied);
transport.buffered_bytes.store(buffered.size(), std::memory_order_relaxed);
}
}
if (copied == length || (copied != 0 && !waitall)) {
return copied;
}
// Winsock rejects PEEK | WAITALL. Consume into transport-owned storage when peeking,
// and let ordinary recv report EOF, timeouts, or nonblocking partial progress.
flags &= ~(MSG_PEEK | MSG_WAITALL);
do {
if (transport.receive_shutdown.load(std::memory_order_relaxed)) {
return copied != 0 ? copied : SetGuestSocketError(Posix::POSIX_ESHUTDOWN);
}
const auto socket = transport.socket.load();
if (dontwait) {
fd_set readable {};
FD_SET(socket, &readable);
timeval immediate {};
const int ready = ::select(0, &readable, nullptr, nullptr, &immediate);
if (ready <= 0) {
return copied != 0 ? copied : (ready < 0 ? SetHostSocketError() :
SetGuestSocketError(Posix::POSIX_EWOULDBLOCK));
}
}
const auto previous_size = buffered.size();
const int requested = peek ? std::min(length - copied, 64 * 1024) : length - copied;
char* destination = output + copied;
if (peek) {
buffered.resize(previous_size + requested);
destination = buffered.data() + previous_size;
}
const int received = ::recv(socket, destination, requested, flags);
if (peek) {
if (received > 0) {
std::memcpy(output + copied, destination, received);
}
buffered.resize(previous_size + std::max(received, 0));
transport.buffered_bytes.store(buffered.size(), std::memory_order_relaxed);
}
if (received <= 0) {
return copied != 0 || received == 0 ? copied : SetHostSocketError();
}
copied += received;
} while (waitall && copied < length);
return copied;
}
#endif
int64_t KYTY_SYSV_ABI Recv(int s, void* buf, uint64_t len, int flags) {
return Recvfrom(s, buf, len, flags, nullptr, nullptr);
}
@@ -2227,8 +2392,9 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
return -1;
}
NativeSocket socket = INVALID_NATIVE_SOCKET;
if (!GetSocketBackend(s, &socket)) {
NativeSocket socket = INVALID_NATIVE_SOCKET;
std::shared_ptr<SocketTransport> transport;
if (!GetSocketBackend(s, &socket, nullptr, &transport)) {
return -1;
}
@@ -2242,11 +2408,27 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
sockaddr_storage host_addr {};
SocketLength host_addrlen = sizeof(host_addr);
int64_t result = 0;
if (addr == nullptr) {
result = ::recv(socket, static_cast<char*>(buf), host_len, host_flags);
} else {
result = ::recvfrom(socket, static_cast<char*>(buf), host_len, host_flags,
reinterpret_cast<sockaddr*>(&host_addr), &host_addrlen);
#if defined(_WIN32)
if (transport->type == SOCK_STREAM) {
if (addr != nullptr &&
::getpeername(socket, reinterpret_cast<sockaddr*>(&host_addr), &host_addrlen) != 0) {
return SetHostSocketError();
}
result = ReceiveStream(*transport, static_cast<char*>(buf), host_len, host_flags,
(flags & 0x80) != 0);
if (result < 0) {
return result;
}
} else
#endif
{
transport.reset();
if (addr == nullptr) {
result = ::recv(socket, static_cast<char*>(buf), host_len, host_flags);
} else {
result = ::recvfrom(socket, static_cast<char*>(buf), host_len, host_flags,
reinterpret_cast<sockaddr*>(&host_addr), &host_addrlen);
}
}
#if defined(_WIN32)
if (result < 0 && WSAGetLastError() == WSAEMSGSIZE) {
@@ -2303,6 +2485,7 @@ int KYTY_SYSV_ABI Select(int nfds, void* readfds, void* writefds, void* exceptfd
FD_ZERO(&host_except);
std::vector<std::pair<int, NativeSocket>> descriptors;
std::vector<std::shared_ptr<SocketTransport>> stream_reads;
int host_nfds = 0;
for (int fd = 0; fd < nfds; fd++) {
const bool read = GuestFdIsSet(readfds, fd);
@@ -2311,13 +2494,14 @@ int KYTY_SYSV_ABI Select(int nfds, void* readfds, void* writefds, void* exceptfd
if (!read && !write && !except) {
continue;
}
NativeSocket socket = INVALID_NATIVE_SOCKET;
NativeSocket socket = INVALID_NATIVE_SOCKET;
std::shared_ptr<SocketTransport> transport;
#if !defined(_WIN32)
if (fd < 3) {
socket = fd;
} else
#endif
if (!GetSocketBackend(fd, &socket)) {
if (!GetSocketBackend(fd, &socket, nullptr, &transport)) {
return -1;
}
#if defined(_WIN32)
@@ -2335,6 +2519,11 @@ int KYTY_SYSV_ABI Select(int nfds, void* readfds, void* writefds, void* exceptfd
descriptors.emplace_back(fd, socket);
if (read) {
FD_SET(socket, &host_read);
#if defined(_WIN32)
if (transport->type == SOCK_STREAM) {
stream_reads.push_back(std::move(transport));
}
#endif
}
if (write) {
FD_SET(socket, &host_write);
@@ -2365,9 +2554,10 @@ int KYTY_SYSV_ABI Select(int nfds, void* readfds, void* writefds, void* exceptfd
} else
#endif
{
result = ::select(host_nfds, readfds != nullptr ? &host_read : nullptr,
writefds != nullptr ? &host_write : nullptr,
exceptfds != nullptr ? &host_except : nullptr, host_timeout_ptr);
result = SelectSockets(host_nfds, readfds != nullptr ? &host_read : nullptr,
writefds != nullptr ? &host_write : nullptr,
exceptfds != nullptr ? &host_except : nullptr, host_timeout_ptr,
stream_reads);
if (result < 0) {
return SetHostSocketError();
}
@@ -2376,15 +2566,19 @@ int KYTY_SYSV_ABI Select(int nfds, void* readfds, void* writefds, void* exceptfd
GuestFdZero(readfds, nfds);
GuestFdZero(writefds, nfds);
GuestFdZero(exceptfds, nfds);
result = 0;
for (const auto& [fd, socket]: descriptors) {
if (FD_ISSET(socket, &host_read)) {
GuestFdSet(readfds, fd);
result++;
}
if (FD_ISSET(socket, &host_write)) {
GuestFdSet(writefds, fd);
result++;
}
if (FD_ISSET(socket, &host_except)) {
GuestFdSet(exceptfds, fd);
result++;
}
}
return result;
@@ -1,12 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "loader/redZonePatcher.h"
#include "loader/guestInstructionPatcher.h"
#include "common/assert.h"
#include "common/logging/log.h"
#include "common/virtualMemory.h"
#include "loader/x64InstructionEmulator.h"
#include <Zydis/Zydis.h>
#include <algorithm>
@@ -22,7 +21,7 @@
#include <set>
#include <unordered_set>
#include <vector>
#if defined(_WIN32)
#if !defined(__APPLE__)
#include <xbyak/xbyak.h>
#include <xbyak/xbyak_util.h>
#endif
@@ -34,7 +33,7 @@
#undef max
#endif
#if defined(_WIN32)
#if !defined(__APPLE__)
using namespace Xbyak::util;
#endif
@@ -53,7 +52,7 @@ using s64 = int64_t;
constexpr size_t NearJumpSize = 5;
#if defined(_WIN32)
#if !defined(__APPLE__)
struct PatchModule {
std::mutex mutex {};
@@ -62,7 +61,7 @@ struct PatchModule {
std::set<u8*> patched;
Xbyak::CodeGenerator patch_gen;
Xbyak::CodeGenerator trampoline_gen;
bool trampoline_exhausted = false;
bool trampoline_exhaustion_reported = false;
PatchModule(u8* module_ptr, u64 module_size, u8* trampoline_ptr, u64 trampoline_size)
: start(module_ptr), end(module_ptr + module_size), patch_gen(module_size, module_ptr),
@@ -81,14 +80,14 @@ static PatchModule* GetContainingModule(const void* ptr) {
return address >= module->start && address < module->end ? module : nullptr;
}
static bool HandleTrampolineError(PatchModule* module, const Xbyak::Error& error) {
if (static_cast<int>(error) != Xbyak::ERR_CODE_IS_TOO_BIG) {
static bool HandleTrampolineError(PatchModule* module, int error) {
if (error != Xbyak::ERR_CODE_IS_TOO_BIG) {
return false;
}
if (!module->trampoline_exhausted) {
LOGF("Windows guest red-zone trampoline space exhausted for module %p\n",
if (!module->trampoline_exhaustion_reported) {
LOGF("Guest instruction trampoline space exhausted for module %p\n",
static_cast<void*>(module->start));
module->trampoline_exhausted = true;
module->trampoline_exhaustion_reported = true;
}
return true;
}
@@ -103,9 +102,6 @@ static ZydisDecoder& GetDecoder() {
return decoder;
}
#endif
#if defined(_WIN32)
namespace {
constexpr size_t GuestRedZoneSize = 128;
@@ -136,11 +132,39 @@ struct DecodedFunction {
bool requires_conservative_red_zone_tracking {};
};
struct InstructionRewrite {
bool protect_red_zone {};
bool protected_indirect_call {};
enum class InstructionReplacement {
None,
ReciprocalSquareRoot,
ExtractQ,
InsertQ,
ReadProcessorId,
CacheLineWriteBack
};
struct InstructionRewrite {
bool protect_red_zone {};
bool protected_indirect_call {};
InstructionReplacement replacement {};
};
InstructionPatchCounts& ReplacementCounts(GuestInstructionPatchResult& result,
InstructionReplacement replacement) {
ASSERT(replacement != InstructionReplacement::None);
switch (replacement) {
case InstructionReplacement::ReciprocalSquareRoot: return result.reciprocal_sqrt;
case InstructionReplacement::ExtractQ: return result.extrq;
case InstructionReplacement::InsertQ: return result.insertq;
case InstructionReplacement::ReadProcessorId: return result.rdpid;
default: return result.clwb;
}
}
bool UsesInstructionTrap(InstructionReplacement replacement, bool trap_replacements) {
return replacement != InstructionReplacement::None &&
(trap_replacements || replacement == InstructionReplacement::InsertQ ||
replacement == InstructionReplacement::ReadProcessorId);
}
bool IsStackPointerRegister(ZydisRegister reg) {
return reg != ZYDIS_REGISTER_NONE &&
ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, reg) == ZYDIS_REGISTER_RSP;
@@ -210,10 +234,11 @@ DecodedCodeInstruction DecodeCodeInstruction(uintptr_t address, uintptr_t end) {
if (decoded.changes_stack_pointer) {
const auto& operands = decoded.operands;
if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_MOV &&
operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
if (operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
IsStackPointerRegister(operands[0].reg.value) &&
operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY) {
(decoded.instruction.mnemonic == ZYDIS_MNEMONIC_RDPID ||
(decoded.instruction.mnemonic == ZYDIS_MNEMONIC_MOV &&
operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY))) {
decoded.replaces_stack_pointer = true;
} else if ((decoded.instruction.mnemonic == ZYDIS_MNEMONIC_ADD ||
decoded.instruction.mnemonic == ZYDIS_MNEMONIC_SUB) &&
@@ -266,7 +291,7 @@ DecodedCodeInstruction DecodeCodeInstruction(uintptr_t address, uintptr_t end) {
const s64 access_start = operand.mem.disp.value;
const s64 access_size = std::max<s64>(operand.size / 8, 1);
const s64 range_start = std::max(access_start, -static_cast<s64>(GuestRedZoneSize));
const s64 range_end = std::min(access_start + access_size, 0LL);
const s64 range_end = std::min<s64>(access_start + access_size, 0);
for (s64 offset = range_start; offset < range_end; ++offset) {
const size_t bit = static_cast<size_t>(offset + static_cast<s64>(GuestRedZoneSize));
if ((operand.actions & ZYDIS_OPERAND_ACTION_MASK_READ) != 0) {
@@ -647,13 +672,17 @@ void AnalyzeRedZoneLiveness(DecodedFunction& function) {
}
bool EncodeRelocatedInstruction(const DecodedCodeInstruction& decoded,
Xbyak::CodeGenerator& generator) {
Xbyak::CodeGenerator& generator, s64 stack_adjustment = 0,
ZydisMnemonic replacement = ZYDIS_MNEMONIC_INVALID) {
ZydisEncoderRequest request;
if (!ZYAN_SUCCESS(ZydisEncoderDecodedInstructionToEncoderRequest(
&decoded.instruction, decoded.operands.data(),
decoded.instruction.operand_count_visible, &request))) {
return false;
}
if (replacement != ZYDIS_MNEMONIC_INVALID) {
request.mnemonic = replacement;
}
for (u8 index = 0; index < decoded.instruction.operand_count_visible; ++index) {
const auto& operand = decoded.operands[index];
@@ -673,6 +702,9 @@ bool EncodeRelocatedInstruction(const DecodedCodeInstruction& decoded,
return false;
}
request.operands[index].mem.displacement = static_cast<ZyanI64>(absolute_address);
} else if (operand.type == ZYDIS_OPERAND_TYPE_MEMORY &&
IsStackPointerRegister(operand.mem.base)) {
request.operands[index].mem.displacement += stack_adjustment;
}
}
@@ -738,9 +770,9 @@ bool GenerateProtectedIndirectCall(const DecodedCodeInstruction& decoded,
return true;
}
void CollectRedZoneMemoryInstructions(const DecodedFunction& function,
void CollectRedZoneMemoryInstructions(const DecodedFunction& function,
std::map<uintptr_t, InstructionRewrite>& rewrite_sites,
RedZonePatchResult& result) {
GuestInstructionPatchResult& result) {
if (!function.uses_red_zone) {
return;
}
@@ -777,98 +809,284 @@ void CollectRedZoneMemoryInstructions(const DecodedFunction& function,
}
}
struct ReciprocalSquareRootSite {
uintptr_t address;
u8 length;
bool requires_red_zone_protection;
};
// Compute 1/sqrt in double precision, rounded to float, with denormals treated as
// signed zero. Preserve guest MXCSR, RFLAGS and the scratch register's full YMM.
void GenerateReciprocalSquareRoot(const DecodedCodeInstruction& decoded,
Xbyak::CodeGenerator& generator) {
const int destination = decoded.operands[0].reg.value - ZYDIS_REGISTER_XMM0;
const int source = decoded.operands[1].reg.value - ZYDIS_REGISTER_XMM0;
int scratch_index = 0;
while (scratch_index == destination || scratch_index == source) {
++scratch_index;
}
const Xbyak::Ymm scratch(scratch_index);
Xbyak::Label one;
Xbyak::Label done;
constexpr size_t SpillSize = 48;
void CollectReciprocalSquareRoots(const DecodedFunction& function,
std::map<uintptr_t, InstructionRewrite>& rewrite_sites,
std::vector<ReciprocalSquareRootSite>& sites) {
// Guest code uses the SysV red zone on both hosts; put our spills below it.
// LEA/MOV/SIMD leave RFLAGS intact, including when the guest red zone is live.
generator.lea(rsp, ptr[rsp - GuestRedZoneSize - SpillSize]);
generator.vmovdqu(ptr[rsp], scratch);
generator.stmxcsr(ptr[rsp + 32]);
generator.mov(dword[rsp + 36], 0x1fc0); // round to nearest, exceptions masked, DAZ
generator.ldmxcsr(ptr[rsp + 36]);
generator.vcvtps2pd(scratch, Xbyak::Xmm(source));
generator.vsqrtpd(scratch, scratch);
generator.vbroadcastsd(Xbyak::Ymm(destination), ptr[rip + one]);
generator.vdivpd(scratch, Xbyak::Ymm(destination), scratch);
generator.vcvtpd2ps(Xbyak::Xmm(destination), scratch); // Clears upper YMM lanes.
generator.ldmxcsr(ptr[rsp + 32]);
generator.vmovdqu(scratch, ptr[rsp]);
generator.lea(rsp, ptr[rsp + GuestRedZoneSize + SpillSize]);
generator.jmp(done);
// A failed label reference can have an incomplete displacement. Do not bind it.
if (Xbyak::GetError() != 0) {
return;
}
generator.L(one);
generator.dq(0x3ff0000000000000ULL);
generator.L(done);
}
bool IsSupportedBitFieldInstruction(const DecodedCodeInstruction& decoded) {
const bool insert = decoded.instruction.mnemonic == ZYDIS_MNEMONIC_INSERTQ;
if (!insert && decoded.instruction.mnemonic != ZYDIS_MNEMONIC_EXTRQ) {
return false;
}
const auto is_xmm = [](const ZydisDecodedOperand& operand) {
return operand.type == ZYDIS_OPERAND_TYPE_REGISTER &&
operand.reg.value >= ZYDIS_REGISTER_XMM0 &&
operand.reg.value <= ZYDIS_REGISTER_XMM15;
};
const size_t immediate_index = insert ? 2 : 1;
const bool immediate =
decoded.instruction.operand_count_visible == immediate_index + 2 &&
decoded.operands[immediate_index].type == ZYDIS_OPERAND_TYPE_IMMEDIATE &&
decoded.operands[immediate_index + 1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE;
if (!is_xmm(decoded.operands[0]) || (insert && !is_xmm(decoded.operands[1])) ||
(!immediate &&
!(decoded.instruction.operand_count_visible == 2 && is_xmm(decoded.operands[1])))) {
return false;
}
// Match the trap handler's encoding support: 66/F2 [REX] 0F 78/79 ModRM [imm8 imm8].
const auto* code = reinterpret_cast<const u8*>(decoded.address);
const size_t opcode = (code[1] & 0xf0u) == 0x40u ? 2 : 1;
return code[0] == (insert ? 0xf2 : 0x66) && code[opcode] == 0x0f &&
code[opcode + 1] == (immediate ? 0x78 : 0x79) &&
decoded.instruction.length == opcode + (immediate ? 5 : 3);
}
void GenerateExtractQ(const DecodedCodeInstruction& decoded, Xbyak::CodeGenerator& generator) {
const Xbyak::Xmm destination(decoded.operands[0].reg.value - ZYDIS_REGISTER_XMM0);
// Keep spills below the guest red zone on both hosts, and restore all modified flags/GPRs.
generator.lea(rsp, ptr[rsp - GuestRedZoneSize]);
generator.pushfq();
generator.push(rax);
generator.push(rcx);
generator.push(rdx);
if (decoded.operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE) {
const u32 length = decoded.operands[1].imm.value.u & 63u;
const u32 index = decoded.operands[2].imm.value.u & 63u;
const u64 mask = length == 0 ? UINT64_MAX : (u64 {1} << length) - 1;
generator.movq(rax, destination);
if (index != 0) {
generator.shr(rax, index);
}
generator.mov(rdx, mask);
} else {
const Xbyak::Xmm source(decoded.operands[1].reg.value - ZYDIS_REGISTER_XMM0);
generator.movq(rax, source);
generator.mov(ecx, eax);
generator.neg(ecx);
generator.mov(rdx, UINT64_MAX);
// CL shifts use six bits: -length gives 64-length, with zero retaining all 64 bits.
generator.shr(rdx, cl);
generator.shr(rax, 8);
generator.mov(ecx, eax);
generator.movq(rax, destination);
generator.shr(rax, cl);
}
generator.and_(rax, rdx);
// Match the emulator: clear the upper XMM half, preserving the upper YMM half.
generator.movq(destination, rax);
generator.pop(rdx);
generator.pop(rcx);
generator.pop(rax);
generator.popfq();
generator.lea(rsp, ptr[rsp + GuestRedZoneSize]);
}
void CollectAmdInstructions(const DecodedFunction& function,
std::map<uintptr_t, InstructionRewrite>& rewrite_sites,
GuestInstructionPatchResult& result,
GuestInstructionHostFeatures host_features) {
for (const auto& [address, decoded]: function.instructions) {
if (!X64InstructionEmulator::IsReciprocalSquareRoot(decoded.instruction,
decoded.operands.data())) {
const auto& instruction = decoded.instruction;
InstructionReplacement replacement {};
if (instruction.mnemonic == ZYDIS_MNEMONIC_VRSQRTPS &&
instruction.encoding == ZYDIS_INSTRUCTION_ENCODING_VEX &&
instruction.raw.vex.offset == 0 && decoded.operands[0].size == 128 &&
decoded.operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER) {
replacement = InstructionReplacement::ReciprocalSquareRoot;
} else if (!host_features.sse4a && IsSupportedBitFieldInstruction(decoded)) {
replacement = instruction.mnemonic == ZYDIS_MNEMONIC_EXTRQ
? InstructionReplacement::ExtractQ
: InstructionReplacement::InsertQ;
} else if (!host_features.rdpid && instruction.mnemonic == ZYDIS_MNEMONIC_RDPID) {
replacement = InstructionReplacement::ReadProcessorId;
} else if (!host_features.clwb && instruction.mnemonic == ZYDIS_MNEMONIC_CLWB) {
replacement = InstructionReplacement::CacheLineWriteBack;
} else {
continue;
}
const bool protect_red_zone = decoded.red_zone_live.any();
sites.push_back({address, decoded.instruction.length, protect_red_zone});
if (protect_red_zone) {
rewrite_sites[address].protect_red_zone = true;
}
++ReplacementCounts(result, replacement).found;
rewrite_sites[address].replacement = replacement;
}
}
uint64_t ApplyReciprocalSquareRootPatches(const PatchModule& module,
std::span<const ReciprocalSquareRootSite> sites,
uint64_t trampoline_addr, uint64_t trampoline_size) {
// Validate every required relocation before introducing any traps.
for (const auto& site: sites) {
if (site.requires_red_zone_protection &&
!module.patched.contains(reinterpret_cast<u8*>(site.address))) {
EXIT("Cannot preserve the guest red zone at emulated VRSQRTPS 0x%016" PRIx64 "\n",
static_cast<u64>(site.address));
}
void MarkInstructionTrap(u8* code, const ZydisDecodedInstruction& instruction,
InstructionReplacement replacement) {
if (replacement == InstructionReplacement::ReciprocalSquareRoot) {
// Clear a reserved VEX.vvvv bit, retaining both register operands for the handler.
code[instruction.raw.vex.size - 1] &= ~0x08u;
}
uint64_t patched = 0;
for (const auto& site: sites) {
if (!module.patched.contains(reinterpret_cast<u8*>(site.address))) {
patched += X64InstructionEmulator::PatchReciprocalSquareRoots(site.address, site.length);
}
}
return patched +
X64InstructionEmulator::PatchReciprocalSquareRoots(trampoline_addr, trampoline_size);
// Other replacements already raise #UD on hosts without their CPU feature.
}
void RelocateRedZoneInstructions(PatchModule* module, const DecodedFunction& function,
const std::map<uintptr_t, InstructionRewrite>& rewrite_sites,
RedZonePatchResult& result) {
bool GenerateInstructionTrap(const DecodedCodeInstruction& decoded,
InstructionReplacement replacement, Xbyak::CodeGenerator& generator,
s64 stack_adjustment) {
if (replacement == InstructionReplacement::CacheLineWriteBack) {
return EncodeRelocatedInstruction(decoded, generator, stack_adjustment);
}
std::array<u8, ZYDIS_MAX_INSTRUCTION_LENGTH> code {};
std::memcpy(code.data(), reinterpret_cast<const void*>(decoded.address),
decoded.instruction.length);
MarkInstructionTrap(code.data(), decoded.instruction, replacement);
generator.db(code.data(), decoded.instruction.length);
return true;
}
bool NeedsTrapRedZoneProtection(const DecodedCodeInstruction& decoded) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
return decoded.red_zone_live.any();
#else
return false;
#endif
}
void TrapUnrelocatedInstructions(const PatchModule& module, const DecodedFunction& function,
const std::map<uintptr_t, InstructionRewrite>& rewrite_sites,
GuestInstructionPatchResult& result) {
for (const auto& [address, rewrite]: rewrite_sites) {
if (rewrite.replacement == InstructionReplacement::None ||
module.patched.contains(reinterpret_cast<u8*>(address))) {
continue;
}
const auto& decoded = function.instructions.at(address);
if (NeedsTrapRedZoneProtection(decoded)) {
if (rewrite.replacement != InstructionReplacement::ReciprocalSquareRoot) {
// Leaving an unsupported instruction unchanged still traps with a live red zone.
EXIT("AMD CPU compatibility: cannot safely trap %s at %p (guest red zone is "
"live)\n",
ZydisMnemonicGetString(decoded.instruction.mnemonic),
reinterpret_cast<void*>(address));
}
continue;
}
MarkInstructionTrap(reinterpret_cast<u8*>(address), decoded.instruction,
rewrite.replacement);
++ReplacementCounts(result, rewrite.replacement).trapped;
}
}
void RelocateGuestInstructions(PatchModule* module, const DecodedFunction& function,
const std::map<uintptr_t, InstructionRewrite>& rewrite_sites,
GuestInstructionPatchResult& result) {
struct RelocationSpan {
std::vector<const DecodedCodeInstruction*> instructions;
uintptr_t patch_start {};
uintptr_t continuation {};
size_t patch_size {};
bool trap_replacements {};
};
const auto emit_span = [&](const RelocationSpan& span) -> std::optional<size_t> {
const size_t trampoline_offset = module->trampoline_gen.getSize();
if (module->trampoline_exhausted) {
return std::nullopt;
}
try {
const auto emit_span = [&](RelocationSpan& span) -> std::optional<size_t> {
auto& generator = module->trampoline_gen;
const size_t trampoline_offset = generator.getSize();
const bool has_replacements =
std::ranges::any_of(span.instructions, [&](const auto* decoded) {
const auto rewrite = rewrite_sites.find(decoded->address);
return rewrite != rewrite_sites.end() &&
rewrite->second.replacement != InstructionReplacement::None;
});
for (bool trap_replacements: {false, true}) {
span.trap_replacements = trap_replacements;
Xbyak::ClearError();
bool encoded = true;
for (const auto* decoded: span.instructions) {
const auto rewrite = rewrite_sites.find(decoded->address);
const auto rewrite = rewrite_sites.find(decoded->address);
const auto replacement = rewrite != rewrite_sites.end()
? rewrite->second.replacement
: InstructionReplacement::None;
const bool uses_trap = UsesInstructionTrap(replacement, trap_replacements);
const bool protected_indirect_call =
rewrite != rewrite_sites.end() && rewrite->second.protected_indirect_call;
const bool protect_red_zone = rewrite != rewrite_sites.end() &&
rewrite->second.protect_red_zone &&
!protected_indirect_call;
const bool protect_red_zone =
(rewrite != rewrite_sites.end() && rewrite->second.protect_red_zone &&
!protected_indirect_call) ||
((uses_trap || replacement == InstructionReplacement::CacheLineWriteBack) &&
NeedsTrapRedZoneProtection(*decoded));
if (protect_red_zone) {
module->trampoline_gen.lea(rsp, ptr[rsp - GuestRedZoneSize]);
generator.lea(rsp, ptr[rsp - GuestRedZoneSize]);
}
if (protected_indirect_call) {
if (!GenerateProtectedIndirectCall(*decoded, module->trampoline_gen)) {
module->trampoline_gen.setSize(trampoline_offset);
return std::nullopt;
if (replacement != InstructionReplacement::None) {
if (uses_trap) {
encoded = GenerateInstructionTrap(*decoded, replacement, generator,
protect_red_zone ? GuestRedZoneSize : 0);
} else if (replacement == InstructionReplacement::ReciprocalSquareRoot) {
GenerateReciprocalSquareRoot(*decoded, generator);
} else if (replacement == InstructionReplacement::ExtractQ) {
GenerateExtractQ(*decoded, generator);
} else {
// CLFLUSH writes back dirty data too; invalidation is a permitted stronger
// action.
encoded = EncodeRelocatedInstruction(
*decoded, generator, protect_red_zone ? GuestRedZoneSize : 0,
ZYDIS_MNEMONIC_CLFLUSH);
}
} else if (!EncodeRelocatedInstruction(*decoded, module->trampoline_gen)) {
module->trampoline_gen.setSize(trampoline_offset);
return std::nullopt;
} else if (protected_indirect_call) {
encoded = GenerateProtectedIndirectCall(*decoded, generator);
} else {
encoded = EncodeRelocatedInstruction(*decoded, generator);
}
if (protect_red_zone && !decoded->replaces_stack_pointer) {
module->trampoline_gen.lea(rsp, ptr[rsp + GuestRedZoneSize]);
generator.lea(rsp, ptr[rsp + GuestRedZoneSize]);
}
if (!encoded || Xbyak::GetError() != 0) {
break;
}
}
module->trampoline_gen.jmp(reinterpret_cast<void*>(span.continuation));
} catch (const Xbyak::Error& error) {
module->trampoline_gen.setSize(trampoline_offset);
if (HandleTrampolineError(module, error)) {
return std::nullopt;
if (encoded && Xbyak::GetError() == 0) {
generator.jmp(reinterpret_cast<void*>(span.continuation));
if (Xbyak::GetError() == 0) {
return trampoline_offset;
}
}
const int error = Xbyak::GetError();
// Roll back before retrying this uncommitted span with smaller trap replacements.
generator.reset();
generator.setSize(trampoline_offset);
if (error != 0 && !HandleTrampolineError(module, error)) {
EXIT("Guest instruction trampoline generation failed: %s\n",
Xbyak::ConvertErrorToString(error));
}
if (error != Xbyak::ERR_CODE_IS_TOO_BIG || !has_replacements) {
break;
}
throw;
}
return trampoline_offset;
return std::nullopt;
};
const auto record_rewrites = [&](const RelocationSpan& span) {
@@ -881,6 +1099,14 @@ void RelocateRedZoneInstructions(PatchModule* module, const DecodedFunction& fun
if (rewrite->second.protect_red_zone && decoded->accesses_memory) {
++result.patched_memory_instruction_count;
}
if (rewrite->second.replacement != InstructionReplacement::None) {
auto& counts = ReplacementCounts(result, rewrite->second.replacement);
if (UsesInstructionTrap(rewrite->second.replacement, span.trap_replacements)) {
++counts.trapped;
} else {
++counts.native;
}
}
}
};
@@ -983,6 +1209,7 @@ void RelocateRedZoneInstructions(PatchModule* module, const DecodedFunction& fun
patch_gen.setSize(span.patch_start - reinterpret_cast<uintptr_t>(patch_gen.getCode()));
patch_gen.jmp(trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR);
patch_gen.nop(span.patch_size - NearJumpSize);
EXIT_IF(Xbyak::GetError() != 0);
record_rewrites(span);
patched_spans.emplace_back(span.patch_start, span.continuation);
@@ -1021,7 +1248,7 @@ void RelocateRedZoneInstructions(PatchModule* module, const DecodedFunction& fun
continue;
}
const RelocationSpan site_span {
RelocationSpan site_span {
.instructions = {&site_instruction->second},
.patch_start = site,
.continuation = site + site_instruction->second.instruction.length,
@@ -1049,12 +1276,14 @@ void RelocateRedZoneInstructions(PatchModule* module, const DecodedFunction& fun
patch_gen.reset();
patch_gen.setSize(*relay_slot - reinterpret_cast<uintptr_t>(patch_gen.getCode()));
patch_gen.jmp(site_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR);
EXIT_IF(Xbyak::GetError() != 0);
patch_gen.reset();
patch_gen.setSize(site - reinterpret_cast<uintptr_t>(patch_gen.getCode()));
patch_gen.jmp(reinterpret_cast<void*>(*relay_slot),
Xbyak::CodeGenerator::LabelType::T_SHORT);
patch_gen.nop(site_span.patch_size - ShortJumpSize);
EXIT_IF(Xbyak::GetError() != 0);
record_rewrites(site_span);
patched_spans.emplace_back(site_span.patch_start, site_span.continuation);
@@ -1187,6 +1416,7 @@ void RelocateRedZoneInstructions(PatchModule* module, const DecodedFunction& fun
patch_gen.reset();
patch_gen.setSize(relay_address - reinterpret_cast<uintptr_t>(patch_gen.getCode()));
patch_gen.jmp(site_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR);
EXIT_IF(Xbyak::GetError() != 0);
std::erase(relay_slots, relay_address);
std::erase(short_relay_slots, relay_address);
} else {
@@ -1201,6 +1431,7 @@ void RelocateRedZoneInstructions(PatchModule* module, const DecodedFunction& fun
patch_gen.jmp(host_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR);
patch_gen.jmp(site_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR);
patch_gen.nop(host_span->patch_size - NearJumpSize * 2);
EXIT_IF(Xbyak::GetError() != 0);
}
uintptr_t jump_target = relay_address;
@@ -1210,6 +1441,7 @@ void RelocateRedZoneInstructions(PatchModule* module, const DecodedFunction& fun
reinterpret_cast<uintptr_t>(patch_gen.getCode()));
patch_gen.jmp(reinterpret_cast<void*>(jump_target),
Xbyak::CodeGenerator::LabelType::T_SHORT);
EXIT_IF(Xbyak::GetError() != 0);
jump_target = final_short_jump;
const auto parent = relay_parent.find(final_short_jump);
ASSERT(parent != relay_parent.end());
@@ -1223,6 +1455,7 @@ void RelocateRedZoneInstructions(PatchModule* module, const DecodedFunction& fun
patch_gen.jmp(reinterpret_cast<void*>(jump_target),
Xbyak::CodeGenerator::LabelType::T_SHORT);
patch_gen.nop(site_span.patch_size - ShortJumpSize);
EXIT_IF(Xbyak::GetError() != 0);
if (host_span) {
record_rewrites(*host_span);
@@ -1240,10 +1473,30 @@ void RelocateRedZoneInstructions(PatchModule* module, const DecodedFunction& fun
}
} // namespace
RedZonePatchResult PatchGuestInstructions(u64 segment_addr, u64 segment_size,
std::span<const uintptr_t> function_starts,
bool protect_memory, bool emulate_rsqrt) {
RedZonePatchResult result {};
GuestInstructionHostFeatures GetGuestInstructionHostFeatures() {
static const auto features = [] {
const Xbyak::util::Cpu cpu;
uint32_t leaf[4] {};
Xbyak::util::Cpu::getCpuid(0, leaf);
bool rdpid = false;
if (leaf[0] >= 7) {
Xbyak::util::Cpu::getCpuidEx(7, 0, leaf);
rdpid = (leaf[2] & (1u << 22)) != 0;
}
return GuestInstructionHostFeatures {cpu.has(Xbyak::util::Cpu::tSSE4a), rdpid,
cpu.has(Xbyak::util::Cpu::tCLWB)};
}();
return features;
}
GuestInstructionPatchResult PatchGuestInstructions(u64 segment_addr, u64 segment_size,
std::span<const uintptr_t> function_starts,
bool protect_memory, bool emulate_amd,
GuestInstructionHostFeatures host_features) {
GuestInstructionPatchResult result {};
if (!protect_memory && !emulate_amd) {
return result;
}
auto* module = GetContainingModule(reinterpret_cast<void*>(segment_addr));
if (module == nullptr || function_starts.empty()) {
return result;
@@ -1262,8 +1515,11 @@ RedZonePatchResult PatchGuestInstructions(u64 segment_addr, u64 segment_size,
starts.erase(unique_end, starts.end());
std::unique_lock lock {module->mutex};
const size_t trampoline_begin = module->trampoline_gen.getSize();
std::vector<ReciprocalSquareRootSite> reciprocal_sqrt_sites;
const size_t trampoline_begin = module->trampoline_gen.getSize();
bool analyze_red_zone = protect_memory;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
analyze_red_zone |= emulate_amd;
#endif
for (size_t function_index = 0; function_index < starts.size(); ++function_index) {
const uintptr_t function_start = starts[function_index];
const uintptr_t function_end =
@@ -1274,7 +1530,9 @@ RedZonePatchResult PatchGuestInstructions(u64 segment_addr, u64 segment_size,
++result.function_count;
auto function = DecodeFunction(function_start, function_end, segment_addr, segment_end);
AnalyzeRedZoneLiveness(function);
if (analyze_red_zone) {
AnalyzeRedZoneLiveness(function);
}
result.instruction_count += function.instructions.size();
std::map<uintptr_t, InstructionRewrite> rewrite_sites;
@@ -1286,22 +1544,17 @@ RedZonePatchResult PatchGuestInstructions(u64 segment_addr, u64 segment_size,
if (protect_memory) {
CollectRedZoneMemoryInstructions(function, rewrite_sites, result);
}
if (emulate_rsqrt) {
CollectReciprocalSquareRoots(function, rewrite_sites, reciprocal_sqrt_sites);
if (emulate_amd) {
CollectAmdInstructions(function, rewrite_sites, result, host_features);
}
if (!rewrite_sites.empty()) {
RelocateRedZoneInstructions(module, function, rewrite_sites, result);
RelocateGuestInstructions(module, function, rewrite_sites, result);
TrapUnrelocatedInstructions(*module, function, rewrite_sites, result);
}
}
// Preserve valid instruction encodings throughout CFG analysis and relocation.
// Only now turn reciprocal roots into traps, including the relocated copies.
const auto trampoline_addr =
reinterpret_cast<u64>(module->trampoline_gen.getCode()) + trampoline_begin;
const auto trampoline_size = module->trampoline_gen.getSize() - trampoline_begin;
if (emulate_rsqrt) {
result.reciprocal_sqrt_instruction_count = ApplyReciprocalSquareRootPatches(
*module, reciprocal_sqrt_sites, trampoline_addr, trampoline_size);
}
Common::VirtualMemory::FlushInstructionCache(segment_addr, segment_size);
if (trampoline_size != 0) {
Common::VirtualMemory::FlushInstructionCache(trampoline_addr, trampoline_size);
@@ -1311,7 +1564,12 @@ RedZonePatchResult PatchGuestInstructions(u64 segment_addr, u64 segment_size,
#else
RedZonePatchResult PatchGuestInstructions(u64, u64, std::span<const uintptr_t>, bool, bool) {
GuestInstructionHostFeatures GetGuestInstructionHostFeatures() {
return {};
}
GuestInstructionPatchResult PatchGuestInstructions(u64, u64, std::span<const uintptr_t>, bool, bool,
GuestInstructionHostFeatures) {
return {};
}
@@ -1492,9 +1750,9 @@ bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_fram
return true;
}
void RegisterRedZonePatchModule(void* module_ptr, uint64_t module_size, void* trampoline_area_ptr,
uint64_t trampoline_area_size) {
#if defined(_WIN32)
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);
@@ -1511,8 +1769,8 @@ void RegisterRedZonePatchModule(void* module_ptr, uint64_t module_size, void* tr
#endif
}
void UnregisterRedZonePatchModule(void* module_ptr) {
#if defined(_WIN32)
void UnregisterGuestInstructionPatchModule(void* module_ptr) {
#if !defined(__APPLE__)
g_patch_modules.erase(reinterpret_cast<u64>(module_ptr));
#else
(void)module_ptr;
+70
View File
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#ifndef KYTY_LOADER_GUEST_INSTRUCTION_PATCHER_H_
#define KYTY_LOADER_GUEST_INSTRUCTION_PATCHER_H_
#include "common/common.h"
#include <span>
#include <vector>
namespace Loader {
struct GuestInstructionHostFeatures {
bool sse4a = false;
bool rdpid = false;
bool clwb = false;
};
GuestInstructionHostFeatures GetGuestInstructionHostFeatures();
struct InstructionPatchCounts {
uint64_t found = 0;
uint64_t native = 0;
uint64_t trapped = 0;
uint64_t Skipped() const { return found - native - trapped; }
InstructionPatchCounts& operator+=(const InstructionPatchCounts& other) {
found += other.found;
native += other.native;
trapped += other.trapped;
return *this;
}
};
struct GuestInstructionPatchResult {
uint64_t function_count = 0;
uint64_t instruction_count = 0;
uint64_t red_zone_function_count = 0;
uint64_t memory_instruction_count = 0;
uint64_t short_memory_instruction_count = 0;
uint64_t patched_memory_instruction_count = 0;
uint64_t stack_dependent_memory_instruction_count = 0;
uint64_t control_flow_memory_instruction_count = 0;
uint64_t unrelocatable_memory_instruction_count = 0;
uint64_t indirect_red_zone_function_count = 0;
InstructionPatchCounts reciprocal_sqrt;
InstructionPatchCounts extrq;
InstructionPatchCounts insertq;
InstructionPatchCounts rdpid;
InstructionPatchCounts clwb;
};
void RegisterGuestInstructionPatchModule(void* module_ptr, uint64_t module_size,
void* trampoline_area_ptr, uint64_t trampoline_area_size);
void UnregisterGuestInstructionPatchModule(void* module_ptr);
// Apply enabled instruction fixes using native trampolines or safe trap fallbacks.
GuestInstructionPatchResult PatchGuestInstructions(
uint64_t segment_addr, uint64_t segment_size, std::span<const uintptr_t> function_starts,
bool protect_memory, bool emulate_amd,
GuestInstructionHostFeatures host_features = GetGuestInstructionHostFeatures());
bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_frame_header_size,
std::vector<uintptr_t>* function_starts);
} // namespace Loader
#endif /* KYTY_LOADER_GUEST_INSTRUCTION_PATCHER_H_ */
-42
View File
@@ -1,42 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#ifndef KYTY_LOADER_RED_ZONE_PATCHER_H_
#define KYTY_LOADER_RED_ZONE_PATCHER_H_
#include "common/common.h"
#include <span>
#include <vector>
namespace Loader {
struct RedZonePatchResult {
uint64_t function_count = 0;
uint64_t instruction_count = 0;
uint64_t red_zone_function_count = 0;
uint64_t memory_instruction_count = 0;
uint64_t short_memory_instruction_count = 0;
uint64_t patched_memory_instruction_count = 0;
uint64_t stack_dependent_memory_instruction_count = 0;
uint64_t control_flow_memory_instruction_count = 0;
uint64_t unrelocatable_memory_instruction_count = 0;
uint64_t indirect_red_zone_function_count = 0;
uint64_t reciprocal_sqrt_instruction_count = 0;
};
void RegisterRedZonePatchModule(void* module_ptr, uint64_t module_size, void* trampoline_area_ptr,
uint64_t trampoline_area_size);
void UnregisterRedZonePatchModule(void* module_ptr);
// Analyze and protect fault sites before replacing reciprocal roots with traps.
RedZonePatchResult PatchGuestInstructions(uint64_t segment_addr, uint64_t segment_size,
std::span<const uintptr_t> function_starts,
bool protect_memory, bool emulate_rsqrt);
bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_frame_header_size,
std::vector<uintptr_t>* function_starts);
} // namespace Loader
#endif /* KYTY_LOADER_RED_ZONE_PATCHER_H_ */
+82 -69
View File
@@ -17,8 +17,8 @@
#include "kernel/pthread.h"
#include "loader/elf.h"
#include "loader/gamePatch.h"
#include "loader/guestInstructionPatcher.h"
#include "loader/jit.h"
#include "loader/redZonePatcher.h"
#include "loader/symbolDatabase.h"
#include "loader/x64InstructionEmulator.h"
@@ -1812,38 +1812,39 @@ 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;
const bool emulate_rsqrt = Config::AmdCpuEnabled();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
const bool protect_memory_faults = Config::RedZoneProtectionEnabled();
const bool use_red_zone_protection = protect_memory_faults || emulate_rsqrt;
constexpr uint64_t RED_ZONE_TRAMPOLINE_SIZE = 8u * 1024u * 1024u;
if (use_red_zone_protection) {
EXIT_IF(RED_ZONE_TRAMPOLINE_SIZE > UINT64_MAX - program->mapped_size);
program->mapped_size += RED_ZONE_TRAMPOLINE_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
const bool protect_memory_faults = false;
#endif
const bool patch_guest_instructions = protect_memory_faults || emulate_amd;
constexpr uint64_t INSTRUCTION_TRAMPOLINE_SIZE = 8u * 1024u * 1024u;
if (patch_guest_instructions) {
EXIT_IF(INSTRUCTION_TRAMPOLINE_SIZE > UINT64_MAX - program->mapped_size);
program->mapped_size += INSTRUCTION_TRAMPOLINE_SIZE;
}
program->base_vaddr = Libs::LibKernel::Memory::AllocateProgramMemory(
g_desired_base_addr, program->mapped_size, Common::VirtualMemory::Mode::ExecuteReadWrite,
Common::PathToString(program->file_name.filename()).c_str());
EXIT_IF(program->base_vaddr == 0);
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (use_red_zone_protection) {
program->red_zone_trampoline_vaddr = program->base_vaddr + program->base_size_aligned;
program->red_zone_trampoline_size = RED_ZONE_TRAMPOLINE_SIZE;
RegisterRedZonePatchModule(reinterpret_cast<void*>(program->base_vaddr),
program->base_size_aligned,
reinterpret_cast<void*>(program->red_zone_trampoline_vaddr),
program->red_zone_trampoline_size);
if (patch_guest_instructions) {
const auto trampoline_addr = program->base_vaddr + program->base_size_aligned;
program->instruction_trampoline_size = INSTRUCTION_TRAMPOLINE_SIZE;
RegisterGuestInstructionPatchModule(
reinterpret_cast<void*>(program->base_vaddr), program->base_size_aligned,
reinterpret_cast<void*>(trampoline_addr), program->instruction_trampoline_size);
}
#endif
if (!is_shared) {
program->tls.handler_vaddr = program->base_vaddr + program->base_size_aligned;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
program->tls.handler_vaddr += program->red_zone_trampoline_size;
#endif
program->tls.handler_vaddr =
program->base_vaddr + program->base_size_aligned + program->instruction_trampoline_size;
}
g_desired_base_addr += CODE_BASE_INCR * (1 + program->mapped_size / CODE_BASE_INCR);
@@ -1863,10 +1864,8 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
}
std::vector<std::pair<uint64_t, uint64_t>> executable_segments;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
uint64_t eh_frame_header_addr = 0;
uint64_t eh_frame_header_size = 0;
#endif
for (Elf64_Half i = 0; i < ehdr->e_phnum; i++) {
if (phdr[i].p_memsz != 0 && (phdr[i].p_type == PT_LOAD || phdr[i].p_type == PT_OS_RELRO)) {
@@ -1925,52 +1924,68 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
program->proc_param_vaddr = phdr[i].p_vaddr + program->base_vaddr;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (use_red_zone_protection && phdr[i].p_type == PT_GNU_EH_FRAME) {
if (patch_guest_instructions && phdr[i].p_type == PT_GNU_EH_FRAME) {
eh_frame_header_addr = phdr[i].p_vaddr + program->base_vaddr;
eh_frame_header_size = phdr[i].p_memsz;
}
#endif
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
std::vector<uintptr_t> function_starts;
if (use_red_zone_protection) {
if (!DecodeEhFrameFunctionStarts(eh_frame_header_addr, eh_frame_header_size,
&function_starts)) {
LOGF("Windows guest red-zone patching could not decode function boundaries for %s\n",
Common::PathToString(program->file_name).c_str());
if (patch_guest_instructions) {
std::vector<uintptr_t> function_starts;
const bool have_function_starts =
DecodeEhFrameFunctionStarts(eh_frame_header_addr, eh_frame_header_size,
&function_starts) &&
!function_starts.empty();
const auto module_name = Common::PathToString(program->file_name.filename());
if (!have_function_starts) {
Log::WriteToConsoleAndLog(
fmt::format("{}: {} not patched (function boundaries unavailable)\n",
emulate_amd ? "AMD CPU compatibility" : "Guest red-zone protection",
module_name));
}
}
#endif
for (const auto& [segment_addr, segment_size]: executable_segments) {
uint64_t reciprocal_sqrt_count = 0;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (use_red_zone_protection) {
const auto result =
PatchGuestInstructions(segment_addr, segment_size, function_starts,
protect_memory_faults, emulate_rsqrt);
LOGF("Windows guest red-zone patching: %s, functions=%" PRIu64 ", red_zone=%" PRIu64
", memory=%" PRIu64 ", patched=%" PRIu64 ", short=%" PRIu64 ", stack=%" PRIu64
", control=%" PRIu64 ", unrelocatable=%" PRIu64 "\n",
Common::PathToString(program->file_name.filename()).c_str(), result.function_count,
result.red_zone_function_count, result.memory_instruction_count,
result.patched_memory_instruction_count, result.short_memory_instruction_count,
result.stack_dependent_memory_instruction_count,
result.control_flow_memory_instruction_count,
result.unrelocatable_memory_instruction_count);
reciprocal_sqrt_count = result.reciprocal_sqrt_instruction_count;
GuestInstructionPatchResult totals {};
for (const auto& [segment_addr, segment_size]: executable_segments) {
const auto result = PatchGuestInstructions(segment_addr, segment_size, function_starts,
protect_memory_faults, emulate_amd);
totals.reciprocal_sqrt += result.reciprocal_sqrt;
totals.extrq += result.extrq;
totals.insertq += result.insertq;
totals.rdpid += result.rdpid;
totals.clwb += result.clwb;
if (protect_memory_faults) {
LOGF("Windows guest red-zone patching: %s, functions=%" PRIu64 ", red_zone=%" PRIu64
", memory=%" PRIu64 ", patched=%" PRIu64 ", short=%" PRIu64 ", stack=%" PRIu64
", control=%" PRIu64 ", unrelocatable=%" PRIu64 "\n",
module_name.c_str(),
result.function_count, result.red_zone_function_count,
result.memory_instruction_count, result.patched_memory_instruction_count,
result.short_memory_instruction_count,
result.stack_dependent_memory_instruction_count,
result.control_flow_memory_instruction_count,
result.unrelocatable_memory_instruction_count);
}
}
#else
if (emulate_rsqrt) {
reciprocal_sqrt_count =
X64InstructionEmulator::PatchReciprocalSquareRoots(segment_addr, segment_size);
Common::VirtualMemory::FlushInstructionCache(segment_addr, segment_size);
}
#endif
if (reciprocal_sqrt_count != 0) {
LOGF("Guest VRSQRTPS emulation: %s, instructions=%" PRIu64 "\n",
Common::PathToString(program->file_name.filename()).c_str(), reciprocal_sqrt_count);
if (emulate_amd && have_function_starts) {
InstructionPatchCounts combined {};
std::string details;
for (const auto& [name, counts]: {std::pair {"VRSQRTPS", totals.reciprocal_sqrt},
{"EXTRQ", totals.extrq},
{"INSERTQ", totals.insertq},
{"RDPID", totals.rdpid},
{"CLWB", totals.clwb}}) {
combined += counts;
if (!details.empty()) details += "; ";
details += fmt::format("{}: native={}, trapped={}, skipped={}", name, counts.native,
counts.trapped, counts.Skipped());
}
const auto found = combined.found;
const auto skipped = combined.Skipped();
const char* status = found == 0 ? "no matching instructions"
: skipped == found ? "not patched"
: skipped != 0 ? "partially patched"
: "patched";
Log::WriteToConsoleAndLog(
fmt::format("AMD CPU compatibility: {} {} ({})\n", module_name, status, details));
}
}
@@ -1999,11 +2014,9 @@ void RuntimeLinker::DeleteProgram(Program* p) {
if (program->base_vaddr != 0 || program->mapped_size != 0) {
EXIT_IF(program->base_vaddr == 0 || program->mapped_size == 0);
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (program->red_zone_trampoline_size != 0) {
UnregisterRedZonePatchModule(reinterpret_cast<void*>(program->base_vaddr));
if (program->instruction_trampoline_size != 0) {
UnregisterGuestInstructionPatchModule(reinterpret_cast<void*>(program->base_vaddr));
}
#endif
EXIT_IF(
!Libs::LibKernel::Memory::FreeGuestMemory(program->base_vaddr, program->mapped_size));
}
+1 -4
View File
@@ -123,10 +123,7 @@ struct Program {
uint64_t base_size = 0;
uint64_t base_size_aligned = 0;
uint64_t mapped_size = 0;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
uint64_t red_zone_trampoline_vaddr = 0;
uint64_t red_zone_trampoline_size = 0;
#endif
uint64_t instruction_trampoline_size = 0;
std::unique_ptr<SymbolDatabase> export_symbols;
std::unique_ptr<SymbolDatabase> import_symbols;
ThreadLocalStorage tls;
+103 -67
View File
@@ -5,10 +5,9 @@
#include <Zydis/Zydis.h>
#include <bit>
#include <cstring>
#if !defined(__APPLE__)
#include <emmintrin.h>
#include <xmmintrin.h>
#endif
#include <cpuid.h>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#include <windows.h> // IWYU pragma: keep
@@ -407,12 +406,12 @@ struct Context {
void Advance(size_t length) { native->Rip += length; }
[[nodiscard]] void* Xmm(uint8_t index) const { return &native->Xmm0 + index; }
void LoadGprs(uint64_t (&gpr)[16]) const {
const uint64_t registers[] = {native->Rax, native->Rcx, native->Rdx, native->Rbx,
native->Rsp, native->Rbp, native->Rsi, native->Rdi,
native->R8, native->R9, native->R10, native->R11,
native->R12, native->R13, native->R14, native->R15};
std::memcpy(gpr, registers, sizeof(gpr));
[[nodiscard]] auto& Gpr(uint8_t index) const {
DWORD64* registers[] = {&native->Rax, &native->Rcx, &native->Rdx, &native->Rbx,
&native->Rsp, &native->Rbp, &native->Rsi, &native->Rdi,
&native->R8, &native->R9, &native->R10, &native->R11,
&native->R12, &native->R13, &native->R14, &native->R15};
return *registers[index];
}
void ClearUpperYmm(uint8_t index) const {
@@ -438,6 +437,14 @@ struct Context {
void Advance(size_t length) {
native->uc_mcontext->__ss.__rip += static_cast<uint64_t>(length);
}
[[nodiscard]] auto& Gpr(uint8_t index) const {
auto* state = &native->uc_mcontext->__ss;
uint64_t* registers[] = {&state->__rax, &state->__rcx, &state->__rdx, &state->__rbx,
&state->__rsp, &state->__rbp, &state->__rsi, &state->__rdi,
&state->__r8, &state->__r9, &state->__r10, &state->__r11,
&state->__r12, &state->__r13, &state->__r14, &state->__r15};
return *registers[index];
}
// Darwin names the XMM file __fpu_xmm0..__fpu_xmm15 instead of exposing an array.
[[nodiscard]] void* Xmm(uint8_t index) const {
auto* fs = &native->uc_mcontext->__fs;
@@ -477,13 +484,11 @@ struct Context {
return native->uc_mcontext.fpregs->_xmm[index].element;
}
void LoadGprs(uint64_t (&gpr)[16]) const {
[[nodiscard]] auto& Gpr(uint8_t index) const {
constexpr int registers[] = {REG_RAX, REG_RCX, REG_RDX, REG_RBX, REG_RSP, REG_RBP,
REG_RSI, REG_RDI, REG_R8, REG_R9, REG_R10, REG_R11,
REG_R12, REG_R13, REG_R14, REG_R15};
for (size_t i = 0; i < 16; ++i) {
gpr[i] = static_cast<uint64_t>(native->uc_mcontext.gregs[registers[i]]);
}
return native->uc_mcontext.gregs[registers[index]];
}
void ClearUpperYmm(uint8_t index) const {
@@ -507,6 +512,12 @@ struct Context {
}
}
#endif
void LoadGprs(uint64_t (&gpr)[16]) const {
for (uint8_t i = 0; i < 16; ++i) {
gpr[i] = static_cast<uint64_t>(Gpr(i));
}
}
};
#if !defined(__APPLE__)
@@ -575,8 +586,8 @@ static bool TryEmulateSse4a(Context& context) {
if (rip[offset] != 0x0f) {
return false;
}
const bool register_extract = prefix == 0x66 && rip[offset + 1] == 0x79;
if (rip[offset + 1] != 0x78 && !register_extract) {
const bool register_form = rip[offset + 1] == 0x79;
if (rip[offset + 1] != 0x78 && !register_form) {
return false;
}
@@ -590,7 +601,7 @@ static bool TryEmulateSse4a(Context& context) {
// Immediate EXTRQ encodes its destination in r/m; the two-register form uses reg.
uint8_t dest_index = reg;
if (prefix == 0x66 && !register_extract) {
if (prefix == 0x66 && !register_form) {
dest_index = rm;
}
auto* dest_xmm = context.Xmm(dest_index);
@@ -599,15 +610,17 @@ static bool TryEmulateSse4a(Context& context) {
return false;
}
uint64_t dest[2] {};
uint64_t source = 0;
uint64_t source[2] {};
std::memcpy(dest, dest_xmm, sizeof(dest));
std::memcpy(&source, src_xmm, sizeof(source));
std::memcpy(source, src_xmm, sizeof(source));
uint8_t length = 0;
uint8_t index = 0;
size_t instruction_length = offset + 3;
if (register_extract) {
length = static_cast<uint8_t>(source);
index = static_cast<uint8_t>(source >> 8u);
if (register_form) {
// EXTRQ controls occupy the low quadword; INSERTQ uses the upper quadword.
const uint64_t controls = source[prefix == 0x66 ? 0 : 1];
length = static_cast<uint8_t>(controls);
index = static_cast<uint8_t>(controls >> 8u);
} else {
length = rip[offset + 3];
index = rip[offset + 4];
@@ -617,13 +630,79 @@ static bool TryEmulateSse4a(Context& context) {
dest[0] = ExtractBitField(dest[0], length, index);
dest[1] = 0;
} else {
dest[0] = InsertBitField(dest[0], source, length, index);
dest[0] = InsertBitField(dest[0], source[0], length, index);
}
std::memcpy(dest_xmm, dest, sizeof(dest));
context.Advance(instruction_length);
return true;
}
static bool ReadTscAux(uint32_t& auxiliary) {
uint32_t eax = 0;
uint32_t ebx = 0;
uint32_t ecx = 0;
uint32_t edx = 0;
if (!__get_cpuid(0x80000001u, &eax, &ebx, &ecx, &edx) || (edx & (1u << 27u)) == 0) {
return false;
}
asm volatile("rdtscp" : "=a"(eax), "=d"(edx), "=c"(auxiliary));
return true;
}
static void FlushCacheLine(uint64_t address, ZydisRegister segment) {
// Preserve FS/GS addressing rather than treating a segment-relative offset as a pointer.
if (segment == ZYDIS_REGISTER_FS) {
asm volatile("clflush %%fs:(%0)" : : "r"(address) : "memory");
} else if (segment == ZYDIS_REGISTER_GS) {
asm volatile("clflush %%gs:(%0)" : : "r"(address) : "memory");
} else {
_mm_clflush(reinterpret_cast<const void*>(address));
}
}
static bool TryEmulateCpuExtensions(Context& context) {
ZydisDecoder decoder {};
ZydisDecodedInstruction instruction {};
ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT] {};
if (!ZYAN_SUCCESS(
ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64)) ||
!ZYAN_SUCCESS(ZydisDecoderDecodeFull(&decoder, reinterpret_cast<const void*>(context.Rip()),
ZYDIS_MAX_INSTRUCTION_LENGTH, &instruction,
operands))) {
return false;
}
if (instruction.mnemonic == ZYDIS_MNEMONIC_RDPID &&
operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER) {
const auto reg_class = ZydisRegisterGetClass(operands[0].reg.value);
const auto index = ZydisRegisterGetId(operands[0].reg.value);
uint32_t auxiliary = 0;
if ((reg_class != ZYDIS_REGCLASS_GPR32 && reg_class != ZYDIS_REGCLASS_GPR64) || index < 0 ||
index >= 16 || !ReadTscAux(auxiliary)) {
return false;
}
// RDPID and guest RDTSCP observe the same host TSC_AUX; only RDPID's destination changes.
context.Gpr(static_cast<uint8_t>(index)) = auxiliary;
} else if (instruction.mnemonic == ZYDIS_MNEMONIC_CLWB &&
operands[0].type == ZYDIS_OPERAND_TYPE_MEMORY) {
ZydisRegisterContext registers {};
for (uint8_t i = 0; i < 16; ++i) {
const auto value = static_cast<uint64_t>(context.Gpr(i));
registers.values[ZYDIS_REGISTER_RAX + i] = value;
registers.values[ZYDIS_REGISTER_EAX + i] = static_cast<uint32_t>(value);
}
uint64_t address = 0;
if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddressEx(&instruction, &operands[0], context.Rip(),
&registers, &address))) {
return false;
}
FlushCacheLine(address, operands[0].mem.segment);
} else {
return false;
}
context.Advance(instruction.length);
return true;
}
#if !defined(__APPLE__)
static bool TryEmulateMonitorxMwaitx(Context& context) {
@@ -713,46 +792,6 @@ static bool TryEmulateReciprocalSquareRoot(Context& context) {
#endif
bool IsReciprocalSquareRoot(const ZydisDecodedInstruction& instruction,
const ZydisDecodedOperand* operands) {
return instruction.mnemonic == ZYDIS_MNEMONIC_VRSQRTPS &&
instruction.encoding == ZYDIS_INSTRUCTION_ENCODING_VEX &&
instruction.raw.vex.offset == 0 && operands[0].size == 128 &&
operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER;
}
uint64_t PatchReciprocalSquareRoots(uint64_t address, uint64_t size) {
uint64_t patched = 0;
#if !defined(__APPLE__)
ZydisDecoder decoder {};
if (!ZYAN_SUCCESS(
ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64))) {
return 0;
}
for (uint64_t offset = 0; offset < size;) {
auto* code = reinterpret_cast<uint8_t*>(address + offset);
ZydisDecodedInstruction instruction {};
ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT] {};
if (!ZYAN_SUCCESS(
ZydisDecoderDecodeFull(&decoder, code, size - offset, &instruction, operands))) {
++offset;
continue;
}
if (IsReciprocalSquareRoot(instruction, operands)) {
// vvvv is reserved (must be 1111b). Clear one bit to route this
// otherwise intact instruction through the illegal-instruction emulator.
code[instruction.raw.vex.size - 1] &= ~0x08u;
++patched;
}
offset += instruction.length;
}
#else
(void)address;
(void)size;
#endif
return patched;
}
bool TryEmulate(void* native_context) {
if (native_context == nullptr) {
return false;
@@ -769,13 +808,10 @@ bool TryEmulate(void* native_context) {
Context context {static_cast<ucontext_t*>(native_context)};
#endif
#if !defined(__APPLE__)
if (TryEmulateReciprocalSquareRoot(context)) {
return true;
}
return TryEmulateMonitorxMwaitx(context) || TryEmulateSse4a(context) ||
TryEmulateShaNi(context);
return TryEmulateReciprocalSquareRoot(context) || TryEmulateMonitorxMwaitx(context) ||
TryEmulateSse4a(context) || TryEmulateShaNi(context) || TryEmulateCpuExtensions(context);
#else
return TryEmulateSse4a(context);
return TryEmulateSse4a(context) || TryEmulateCpuExtensions(context);
#endif
}
-6
View File
@@ -1,14 +1,8 @@
#ifndef KYTY_LOADER_X64_INSTRUCTION_EMULATOR_H_
#define KYTY_LOADER_X64_INSTRUCTION_EMULATOR_H_
#include <Zydis/DecoderTypes.h>
#include <cstdint>
namespace Loader::X64InstructionEmulator {
[[nodiscard]] bool IsReciprocalSquareRoot(const ZydisDecodedInstruction& instruction,
const ZydisDecodedOperand* operands);
uint64_t PatchReciprocalSquareRoots(uint64_t address, uint64_t size);
[[nodiscard]] bool TryEmulate(void* native_context);
} // namespace Loader::X64InstructionEmulator
+21
View File
@@ -54,11 +54,15 @@ static void PrintUsage() {
Config::DEFAULT_USER_ID);
::printf(" --mic <name> Capture from this microphone; omit for silence.\n");
::printf(" --controller-color <#RRGGBB> Override the controller lightbar color.\n");
::printf(" --controller-volume <0-100> DualSense speaker volume. Default: 100.\n");
::printf(" --controller-vibration <0-100> DualSense vibration intensity. Default: 100.\n");
::printf(
" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Mailbox.\n");
::printf(
" --gpu <index> Vulkan physical device index. Default: auto.\n");
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
::printf(
" --hide-cursor Hide the cursor after 2 s idle. Default: off.\n");
::printf(" --vr Enable the virtual VR headset.\n");
::printf(" --amd-cpu Apply AMD CPU instruction patches.\n");
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
@@ -203,6 +207,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
continue;
}
if (arg == "--hide-cursor") {
options.config.hide_cursor_enabled = true;
continue;
}
if (arg == "--vr") {
options.config.vr_enabled = true;
continue;
@@ -324,6 +333,18 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
return false;
}
options.config.controller_color = color;
} else if (arg == "--controller-volume") {
if (!ParseUint32(value, options.config.controller_speaker_volume) ||
options.config.controller_speaker_volume > 100) {
::printf("invalid controller volume: %s\n", value.c_str());
return false;
}
} else if (arg == "--controller-vibration") {
if (!ParseUint32(value, options.config.controller_vibration_intensity) ||
options.config.controller_vibration_intensity > 100) {
::printf("invalid controller vibration intensity: %s\n", value.c_str());
return false;
}
} else if (arg == "--present-mode") {
if (!ParseEnum(value, options.config.present_mode)) {
::printf("invalid present mode: %s\n", value.c_str());
+47 -2
View File
@@ -111,8 +111,10 @@ struct Controller {
haptics_handles_rumble = false;
Initialize();
Connect(1);
Check(GetSettingScale(Setting::SpeakerVolume) == 1.0f &&
GetSettingScale(Setting::VibrationIntensity) == 1.0f &&
Check(GetSettingScale(Setting::SpeakerVolume) ==
Config::GetControllerSpeakerVolume() / 100.0f &&
GetSettingScale(Setting::VibrationIntensity) ==
Config::GetControllerVibrationIntensity() / 100.0f &&
GetSettingScale(Setting::TriggerEffectIntensity) == 1.0f,
"controller initialization retained old settings");
rumble.clear();
@@ -161,6 +163,48 @@ void TestSettingCycles() {
CycleSetting(Setting::TriggerEffectIntensity);
}
void TestGlobalControllerLevels() {
Config::ConfigOptions options;
options.controller_speaker_volume = 50;
options.controller_vibration_intensity = 25;
Config::Load(options);
{
Controller controller;
Check(GetSettingScale(Setting::SpeakerVolume) == 0.5f &&
GetSettingScale(Setting::VibrationIntensity) == 0.25f &&
GetSettingScale(Setting::TriggerEffectIntensity) == 1.0f,
"global controller levels did not scale their outputs independently");
SetRumble(200, 100);
Check(rumble.back().large == 50 * 257 && rumble.back().small == 25 * 257,
"global vibration level did not scale rumble");
CycleSetting(Setting::SpeakerVolume);
CycleSetting(Setting::VibrationIntensity);
Check(GetSettingScale(Setting::SpeakerVolume) == 0.0f &&
GetSettingScale(Setting::VibrationIntensity) == 0.0f,
"cycle hotkeys did not mute globally scaled outputs");
CycleSetting(Setting::SpeakerVolume);
CycleSetting(Setting::VibrationIntensity);
Check(GetSettingScale(Setting::SpeakerVolume) == 0.01f &&
GetSettingScale(Setting::VibrationIntensity) == 0.0825f,
"cycle hotkeys did not multiply the global controller levels");
Check(rumble.back().large == 17 * 257 && rumble.back().small == 8 * 257,
"cycling vibration did not apply the combined intensity to cached rumble");
}
options.controller_speaker_volume = 0;
options.controller_vibration_intensity = 0;
Config::Load(options);
{
Controller controller;
Check(GetSettingScale(Setting::SpeakerVolume) == 0.0f &&
GetSettingScale(Setting::VibrationIntensity) == 0.0f,
"zero global controller levels did not mute outputs");
SetRumble(200, 100);
Check(rumble.back().large == 0 && rumble.back().small == 0,
"zero global vibration level did not mute motors");
}
Config::Load(Config::ConfigOptions {});
}
void TestVibrationLifetime() {
Controller controller;
SetRumble(255, 1);
@@ -287,6 +331,7 @@ void TestIndependentOutputsAndPadSwitch() {
int main() {
Config::Initialize();
TestSettingCycles();
TestGlobalControllerLevels();
// Each following test initializes another controller and requires strong defaults.
TestVibrationLifetime();
TestMaskedTriggersAndValidation();
+211 -15
View File
@@ -25,10 +25,12 @@
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <future>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
namespace Libs::LibKernelApr {
@@ -811,6 +813,210 @@ void CheckAprPaths(const std::filesystem::path &root) {
FileSystem::Umount("/app0");
}
std::array<int, 2> CreateTcpPair() {
namespace Net = Libs::Network::Net;
// Guest sockaddr_in: length, family, network-order port/address, padding.
std::array<uint8_t, 16> address {16, 2, 0, 0, 127, 0, 0, 1};
const int listener = Net::Socket(2, 1, 0);
Check(listener >= 0, "create loopback listener");
Check(Net::Bind(listener, address.data(), address.size()) == 0, "bind loopback");
Check(Net::Listen(listener, 1) == 0, "listen on loopback");
uint32_t address_size = address.size();
Check(Net::Getsockname(listener, address.data(), &address_size) == 0,
"get assigned loopback port");
const int writer = Net::Socket(2, 1, 0);
Check(writer >= 0 && Net::Connect(writer, address.data(), address_size) == 0,
"connect wake socket");
const int reader = Net::Accept(listener, nullptr, nullptr);
Check(reader >= 0, "accept wake socket");
Check(Net::SocketClose(listener) == 0, "close listener");
return {reader, writer};
}
#if defined(_WIN32)
void CheckSocketReceiveConcurrency() {
namespace Net = Libs::Network::Net;
const auto [reader, writer] = CreateTcpPair();
const int timeout_ms = 3000;
Check(Net::Setsockopt(reader, 0xffff, 0x1006, &timeout_ms, sizeof(timeout_ms)) == 0,
"bound the blocked receive concurrency fixture");
std::promise<void> entered;
std::promise<int64_t> completed;
auto entered_future = entered.get_future();
auto completed_future = completed.get_future();
std::jthread receiver([&] {
char byte = 0;
entered.set_value();
completed.set_value(Net::Recv(reader, &byte, 1, 0x42));
});
Check(entered_future.wait_for(std::chrono::seconds(2)) == std::future_status::ready,
"start blocked reader");
std::this_thread::sleep_for(std::chrono::milliseconds(50));
char byte = 0;
const auto before_nonblocking = std::chrono::steady_clock::now();
Check(Net::Recv(reader, &byte, 1, 0x80) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK &&
std::chrono::steady_clock::now() - before_nonblocking < std::chrono::milliseconds(500) &&
completed_future.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout,
"DONTWAIT does not wait behind another blocked reader");
const auto before_close = std::chrono::steady_clock::now();
Check(Net::SocketClose(reader) == 0, "close succeeds while another thread receives");
Check(std::chrono::steady_clock::now() - before_close < std::chrono::seconds(1),
"close does not wait for the blocked receive timeout");
Check(completed_future.wait_for(std::chrono::seconds(1)) == std::future_status::ready,
"close wakes the blocked receive within one second");
Check(completed_future.get() <= 0, "closed receive reports EOF or error");
Check(std::chrono::steady_clock::now() - before_close < std::chrono::seconds(1),
"closing a socket wakes its blocked receive before the receive timeout");
receiver.join();
Check(Net::SocketClose(writer) == 0, "close blocked receive writer");
const auto [shutdown_reader, shutdown_writer] = CreateTcpPair();
Check(Net::Setsockopt(shutdown_reader, 0xffff, 0x1006, &timeout_ms, sizeof(timeout_ms)) == 0 &&
Net::Send(shutdown_writer, "x", 1, 0) == 1 &&
Net::Recv(shutdown_reader, &byte, 1, 0x42) == 1 && byte == 'x' &&
Net::Shutdown(shutdown_reader, 0) == 0 &&
Net::Recv(shutdown_reader, &byte, 1, 0) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ESHUTDOWN,
"receive shutdown rejects bytes retained by an earlier peek");
Check(Net::SocketClose(shutdown_reader) == 0 && Net::SocketClose(shutdown_writer) == 0,
"close receive shutdown fixture");
}
void CheckSocketReceiveBuffer(int reader, int writer) {
namespace Net = Libs::Network::Net;
constexpr int PeekWaitAll = 0x42;
constexpr char Payload[] = "abcdef";
const int timeout_ms = 2000;
Check(Net::Setsockopt(reader, 0xffff, 0x1006, &timeout_ms, sizeof(timeout_ms)) == 0,
"bound blocking receive regressions with SO_RCVTIMEO");
Check(Net::Send(writer, Payload, 2, 0) == 2, "send first TCP fragment");
std::jthread sender([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
Check(Net::Send(writer, Payload + 2, 4, 0) == 4, "send second TCP fragment");
});
std::array<char, 8> received {};
Check(Net::Recv(reader, received.data(), 6, PeekWaitAll) == 6 &&
std::memcmp(received.data(), Payload, 6) == 0,
"combined peek waits for fragmented TCP bytes");
sender.join();
received.fill(0);
Check(Net::Recv(reader, received.data(), 6, PeekWaitAll) == 6 &&
std::memcmp(received.data(), Payload, 6) == 0,
"repeated combined peek preserves all bytes");
const int epoll = Net::EpollCreate("buffered receive", 0);
Net::NetEpollEvent registration {};
registration.events = 1;
registration.data.u64 = 0x12345678;
Check(epoll >= 0 && Net::EpollControl(epoll, 1, reader, &registration) == 0,
"register buffered socket readability");
const auto check_readable = [&](bool expected) {
std::array<uint64_t, 16> readable {};
const auto bit = uint64_t {1} << (reader % 64);
readable[reader / 64] = bit;
const std::array<int64_t, 2> timeout {expected ? 1 : 0, 0};
Check(Net::Select(reader + 1, readable.data(), nullptr, nullptr, timeout.data()) ==
(expected ? 1 : 0) && readable[reader / 64] == (expected ? bit : 0),
"select reflects buffered bytes and their removal");
if (expected) {
auto writable = readable;
Check(Net::Select(reader + 1, readable.data(), writable.data(), nullptr,
timeout.data()) == 2 && readable[reader / 64] == bit &&
writable[reader / 64] == bit,
"select counts readable and writable bits once each");
}
Net::NetEpollEvent event {};
Check(Net::EpollWait(epoll, &event, 1, expected ? 1000000 : 0) ==
(expected ? 1 : 0) &&
(!expected || (event.events == 1 && event.data.u64 == registration.data.u64)),
"epoll reflects buffered bytes and preserves registration data");
};
check_readable(true);
std::array<uint8_t, 16> peer {}, expected_peer {};
uint32_t peer_size = peer.size(), expected_peer_size = expected_peer.size();
Check(Net::Getsockname(writer, expected_peer.data(), &expected_peer_size) == 0 &&
Net::Recv(reader, received.data(), 0, PeekWaitAll) == 0 &&
Net::Recvfrom(reader, received.data(), 2, 0, peer.data(), &peer_size) == 2 &&
peer_size == expected_peer_size && peer == expected_peer &&
std::memcmp(received.data(), Payload, 2) == 0 &&
FileSystem::KernelRead(reader, received.data(), 2) == 2 &&
std::memcmp(received.data(), Payload + 2, 2) == 0,
"zero-length peek preserves bytes; recvfrom returns peer and shares kernel read buffer");
check_readable(true);
Check(Net::Send(writer, "gh", 2, 0) == 2 &&
Net::Recv(reader, received.data(), 4, 0x40) == 4 &&
std::memcmp(received.data(), "efgh", 4) == 0,
"waitall joins buffered bytes with newly received bytes");
check_readable(false);
Check(Net::Send(writer, "mn", 2, 0) == 2, "send plain peek fixture");
const auto before_peek = std::chrono::steady_clock::now();
Check(Net::Recv(reader, received.data(), 4, 2) == 2 &&
std::memcmp(received.data(), "mn", 2) == 0 &&
std::chrono::steady_clock::now() - before_peek < std::chrono::milliseconds(500) &&
Net::Recv(reader, received.data(), 4, 0) == 2 &&
std::memcmp(received.data(), "mn", 2) == 0,
"plain peek returns available bytes promptly and preserves them for ordinary receive");
const int enabled = 1, disabled = 0;
Check(Net::Setsockopt(reader, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0 &&
Net::Recv(reader, received.data(), 4, PeekWaitAll) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK,
"empty nonblocking combined peek returns would-block");
Check(Net::Send(writer, "ij", 2, 0) == 2, "send nonblocking receive fixture");
check_readable(true);
*Libs::Posix::GetErrorAddr() = Libs::Posix::POSIX_EINVAL;
for (int i = 0; i < 2; ++i) {
Check(Net::Recv(reader, received.data(), 4, PeekWaitAll) == 2 &&
std::memcmp(received.data(), "ij", 2) == 0 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
"nonblocking combined peek preserves a short result and successful errno");
}
Check(Net::Recv(reader, received.data(), 4, 0) == 2 &&
std::memcmp(received.data(), "ij", 2) == 0,
"ordinary receive consumes a short buffered result");
check_readable(false);
Check(Net::Setsockopt(reader, 0xffff, 0x1200, &disabled, sizeof(disabled)) == 0 &&
Net::EpollDestroy(epoll) == 0,
"restore blocking receives and destroy epoll");
Check(Net::Recv(reader, received.data(), 4, PeekWaitAll | 0x80) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK,
"per-call nonblocking combined peek returns would-block on a blocking socket");
Check(Net::Send(writer, "kl", 2, 0) == 2 && Net::Shutdown(writer, 1) == 0,
"send a final partial payload and EOF");
for (int i = 0; i < 2; ++i) {
Check(Net::Recv(reader, received.data(), 4, PeekWaitAll) == 2 &&
std::memcmp(received.data(), "kl", 2) == 0,
"EOF ends waitall while repeated peeks preserve the short payload");
}
Check(Net::Recv(reader, received.data(), 4, 0x40) == 2 &&
std::memcmp(received.data(), "kl", 2) == 0 &&
Net::Recv(reader, received.data(), 4, PeekWaitAll) == 0,
"draining the final buffered payload exposes EOF");
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};
uint32_t address_size = address.size();
Check(datagram >= 0 && datagram_writer >= 0 &&
Net::Bind(datagram, address.data(), address.size()) == 0 &&
Net::Getsockname(datagram, address.data(), &address_size) == 0 &&
Net::Setsockopt(datagram, 0xffff, 0x1006, &timeout_ms, sizeof(timeout_ms)) == 0 &&
Net::Sendto(datagram_writer, Payload, 6, 0, address.data(), address_size) == 6,
"create bounded UDP truncation fixture");
*Libs::Posix::GetErrorAddr() = Libs::Posix::POSIX_EINVAL;
Check(Net::Recvfrom(datagram, received.data(), 2, 0, nullptr, nullptr) == 2 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL &&
std::memcmp(received.data(), Payload, 2) == 0 &&
Net::Setsockopt(datagram, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0 &&
Net::Recv(datagram, received.data(), received.size(), 0) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK,
"UDP truncation returns its prefix and discards the rest of the datagram");
Check(Net::SocketClose(datagram) == 0 && Net::SocketClose(datagram_writer) == 0,
"close UDP truncation fixture");
}
#endif
void CheckSocketWakeup() {
namespace Net = Libs::Network::Net;
Loader::SymbolDatabase symbols;
@@ -829,21 +1035,7 @@ void CheckSocketWakeup() {
const auto net_send = reinterpret_cast<Send>(send_symbol->vaddr);
const auto net_recv = reinterpret_cast<Recv>(recv_symbol->vaddr);
auto *net_errno = reinterpret_cast<Errno>(errno_symbol->vaddr)();
// Guest sockaddr_in: length, family, network-order port/address, padding.
std::array<uint8_t, 16> address {16, 2, 0, 0, 127, 0, 0, 1};
const int listener = Net::Socket(2, 1, 0);
Check(listener >= 0, "create loopback listener");
Check(Net::Bind(listener, address.data(), address.size()) == 0, "bind loopback");
Check(Net::Listen(listener, 1) == 0, "listen on loopback");
uint32_t address_size = address.size();
Check(Net::Getsockname(listener, address.data(), &address_size) == 0,
"get assigned loopback port");
const int writer = Net::Socket(2, 1, 0);
Check(writer >= 0 && Net::Connect(writer, address.data(), address_size) == 0,
"connect wake socket");
const int reader = Net::Accept(listener, nullptr, nullptr);
Check(reader >= 0, "accept wake socket");
Check(Net::SocketClose(listener) == 0, "close listener");
const auto [reader, writer] = CreateTcpPair();
const int enabled = 1;
Check(Net::Setsockopt(writer, 6, 1, &enabled, sizeof(enabled)) == 0,
"enable TCP_NODELAY");
@@ -911,6 +1103,10 @@ void CheckSocketWakeup() {
*net_errno == Libs::Posix::POSIX_EPIPE,
"Net send reports a broken pipe without raising host SIGPIPE");
Check(Net::SocketClose(disconnected) == 0, "close unconnected socket");
#endif
#if defined(_WIN32)
CheckSocketReceiveBuffer(reader, writer);
CheckSocketReceiveConcurrency();
#endif
Check(Net::SocketClose(writer) == 0, "close wake writer");
*net_errno = Libs::Posix::POSIX_EINVAL;
File diff suppressed because it is too large Load Diff
+42
View File
@@ -8,6 +8,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <initializer_list>
#include <sys/mman.h>
#include <sys/ucontext.h>
#include <unistd.h>
@@ -61,6 +62,21 @@ constexpr uint8_t kInsertqXmm10Xmm9Len16Index8[] = {
0xc3, // ret
};
constexpr uint8_t kInsertqXmm10Xmm9[] = {
0xf3, 0x44, 0x0f, 0x6f, 0x0e, // movdqu (%rsi), %xmm9
0xf3, 0x44, 0x0f, 0x6f, 0x12, // movdqu (%rdx), %xmm10
0xf2, 0x45, 0x0f, 0x79, 0xd1, // insertq xmm10, xmm9
0xf3, 0x44, 0x0f, 0x7f, 0x17, // movdqu %xmm10, (%rdi)
0xc3,
};
constexpr uint8_t kInsertqXmm9Xmm9[] = {
0xf3, 0x44, 0x0f, 0x6f, 0x0e, // movdqu (%rsi), %xmm9
0xf2, 0x45, 0x0f, 0x79, 0xc9, // insertq xmm9, xmm9
0xf3, 0x44, 0x0f, 0x7f, 0x0f, // movdqu %xmm9, (%rdi)
0xc3,
};
constexpr uint8_t kUd2[] = {
0x0f, 0x0b, // ud2
0xc3, // ret
@@ -192,6 +208,31 @@ void TestRexHighRegisterInsertqWithIndex() {
Check(out[1] == 0xbbbbbbbbbbbbbbbbull, "insertq xmm10, xmm9, 16, 8 leaves the upper 64 bits untouched");
}
void TestInsertqRegister() {
const auto separate = MapCode<PairBlobFn>(kInsertqXmm10Xmm9);
const auto aliased = MapCode<PairBlobFn>(kInsertqXmm9Xmm9);
for (const auto fn: {separate, aliased}) {
for (const uint64_t controls: {0xc4c8ull, 0xc0c0ull, 0xffc1ull}) {
const uint64_t source[2] {0x0123456789abcdefull, 0xdeadbeef00000000ull | controls};
const uint64_t destination[2] {0x8877665544332211ull, 0xfedcba9876543210ull};
const auto* initial = fn == aliased ? source : destination;
uint64_t expected = initial[0];
const auto length = (controls & 63) == 0 ? 64 : controls & 63;
const auto index = (controls >> 8) & 63;
for (uint64_t bit = 0; bit < length && index + bit < 64; ++bit) {
const uint64_t mask = uint64_t {1} << (index + bit);
expected = (expected & ~mask) | (((source[0] >> bit) & 1) << (index + bit));
}
uint64_t out[2] {};
const int before = IllegalHits();
fn(out, source, destination);
Check(IllegalHits() == before + 1, "register INSERTQ raises one SIGILL");
Check(out[0] == expected && out[1] == initial[1],
"register INSERTQ uses upper source controls and preserves upper destination");
}
}
}
void TestUnknownInstructionRefused() {
g_refused.store(false, std::memory_order_relaxed);
g_refuse_skip.store(2, std::memory_order_relaxed);
@@ -213,6 +254,7 @@ int main() {
TestInsertqImmediate();
TestRexHighRegisterExtrq();
TestRexHighRegisterInsertqWithIndex();
TestInsertqRegister();
TestUnknownInstructionRefused();
std::printf("macosSse4aTests: all passed\n");
return 0;