From 4297d2fea2228ff4afe2a7c244fb8b3f1a97491a Mon Sep 17 00:00:00 2001 From: MonsterDruide1 <5958456@gmail.com> Date: Fri, 18 Jun 2021 16:32:46 +0200 Subject: [PATCH] core: Hacky TAS syncing & load pausing To keep the TAS inputs synced to the game speed even through lag spikes and loading zones, deeper access is required. First, the `TAS::UpdateThread` has to be executed exactly once per frame. This is done by connecting it to the service method the game calls to pass parameters to the GPU: `Service::VI::QueueBuffer`. Second, the loading time of new subareas and/or kingdoms (SMO) can vary. To counteract that, the `CPU_BOOST_MODE` can be detected: In the `APM`-interface, the call to enabling/disabling the boost mode can be caught and forwarded to the TASing system, which can pause the script execution if neccessary and enabled in the settings. --- src/common/fs/fs_paths.h | 1 + src/common/fs/path_util.cpp | 3 +- src/common/settings.h | 9 +- src/core/hle/service/apm/apm_interface.cpp | 2 + src/core/hle/service/vi/vi.cpp | 3 + src/input_common/tas/tas_input.cpp | 142 +++++++++++------- src/input_common/tas/tas_input.h | 58 +++---- .../configuration/configure_filesystem.cpp | 2 +- src/yuzu/main.cpp | 27 +++- 9 files changed, 140 insertions(+), 107 deletions(-) diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index b32614797..84968b8e0 100644 --- a/src/common/fs/fs_paths.h +++ b/src/common/fs/fs_paths.h @@ -21,6 +21,7 @@ #define SCREENSHOTS_DIR "screenshots" #define SDMC_DIR "sdmc" #define SHADER_DIR "shader" +#define TAS_DIR "scripts" // yuzu-specific files diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index 5f76adedb..97d026eb8 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -116,8 +116,7 @@ private: GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR); GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); - - GenerateYuzuPath(YuzuPath::TASFile, fs::path{""}); + GenerateYuzuPath(YuzuPath::TASFile, yuzu_path / TAS_DIR); } ~PathManagerImpl() = default; diff --git a/src/common/settings.h b/src/common/settings.h index 884ea55f8..7333a64dc 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -500,7 +500,6 @@ struct Values { // Controls InputSetting> players; - std::shared_ptr inputSubsystem = NULL; Setting use_docked_mode{true, "use_docked_mode"}; @@ -514,9 +513,12 @@ struct Values { "motion_device"}; BasicSetting udp_input_servers{"127.0.0.1:26760", "udp_input_servers"}; - BasicSetting tas_enable{false, "tas_enable"}; + BasicSetting pause_tas_on_load { false, "pause_tas_on_load" }; + BasicSetting tas_enable{ false, "tas_enable" }; BasicSetting tas_reset{ false, "tas_reset" }; BasicSetting tas_record{ false, "tas_record" }; + BasicSetting is_cpu_boxted{ false, " BasicSetting is_cpu_boxted{ false, "cpuBoosted" }; +" }; BasicSetting mouse_panning{false, "mouse_panning"}; BasicRangedSetting mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"}; @@ -550,9 +552,6 @@ struct Values { BasicSetting gamecard_current_game{false, "gamecard_current_game"}; BasicSetting gamecard_path{std::string(), "gamecard_path"}; - // TAS - bool pauseTasOnLoad; - // Debugging bool record_frame_times; BasicSetting use_gdbstub{false, "use_gdbstub"}; diff --git a/src/core/hle/service/apm/apm_interface.cpp b/src/core/hle/service/apm/apm_interface.cpp index e58bad083..724483107 100644 --- a/src/core/hle/service/apm/apm_interface.cpp +++ b/src/core/hle/service/apm/apm_interface.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include "common/logging/log.h" +#include "common/settings.h" #include "core/hle/ipc_helpers.h" #include "core/hle/service/apm/apm.h" #include "core/hle/service/apm/apm_controller.h" @@ -120,6 +121,7 @@ void APM_Sys::SetCpuBoostMode(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_APM, "called, mode={:08X}", mode); + Settings::values.is_cpu_boosted = (static_cast(mode) == 1); controller.SetFromCpuBoostMode(mode); IPC::ResponseBuilder rb{ctx, 2}; diff --git a/src/core/hle/service/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp index 8e8fc40ca..f4eac0bca 100644 --- a/src/core/hle/service/vi/vi.cpp +++ b/src/core/hle/service/vi/vi.cpp @@ -32,6 +32,8 @@ #include "core/hle/service/vi/vi_s.h" #include "core/hle/service/vi/vi_u.h" +#include "input_common/tas/tas_input.h" + namespace Service::VI { constexpr ResultCode ERR_OPERATION_FAILED{ErrorModule::VI, 1}; @@ -595,6 +597,7 @@ private: IGBPQueueBufferResponseParcel response{1280, 720}; ctx.WriteBuffer(response.Serialize()); + Settings::values.input_subsystem->GetTas()->UpdateThread(); break; } case TransactionId::Query: { diff --git a/src/input_common/tas/tas_input.cpp b/src/input_common/tas/tas_input.cpp index 343641945..7320a7004 100644 --- a/src/input_common/tas/tas_input.cpp +++ b/src/input_common/tas/tas_input.cpp @@ -19,6 +19,29 @@ namespace TasInput { +constexpr std::array, 20> text_to_tas_button = { + std::pair{"KEY_A", TasButton::BUTTON_A}, + {"KEY_B", TasButton::BUTTON_B}, + {"KEY_X", TasButton::BUTTON_X}, + {"KEY_Y", TasButton::BUTTON_Y}, + {"KEY_LSTICK", TasButton::STICK_L}, + {"KEY_RSTICK", TasButton::STICK_R}, + {"KEY_L", TasButton::TRIGGER_L}, + {"KEY_R", TasButton::TRIGGER_R}, + {"KEY_PLUS", TasButton::BUTTON_PLUS}, + {"KEY_MINUS", TasButton::BUTTON_MINUS}, + {"KEY_DLEFT", TasButton::BUTTON_LEFT}, + {"KEY_DUP", TasButton::BUTTON_UP}, + {"KEY_DRIGHT", TasButton::BUTTON_RIGHT}, + {"KEY_DDOWN", TasButton::BUTTON_DOWN}, + {"KEY_SL", TasButton::BUTTON_SL}, + {"KEY_SR", TasButton::BUTTON_SR}, + {"KEY_CAPTURE", TasButton::BUTTON_CAPTURE}, + {"KEY_HOME", TasButton::BUTTON_HOME}, + {"KEY_ZL", TasButton::TRIGGER_ZL}, + {"KEY_ZR", TasButton::TRIGGER_ZR}, +}; + Tas::Tas() { LoadTasFiles(); } @@ -31,29 +54,31 @@ void Tas::RefreshTasFile() { refresh_tas_fle = true; } void Tas::LoadTasFiles() { - scriptLength = 0; - for (int i = 0; i < PLAYER_NUMBER; i++) { + script_length = 0; + for (size_t i = 0; i < PLAYER_NUMBER; i++) { LoadTasFile(i); - if (newCommands[i].size() > scriptLength) - scriptLength = newCommands[i].size(); + if (commands[i].size() > script_length) { + script_length = commands[i].size(); + } } } -void Tas::LoadTasFile(int playerIndex) { +void Tas::LoadTasFile(size_t player_index) { LOG_DEBUG(Input, "LoadTasFile()"); - if (!newCommands[playerIndex].empty()) { - newCommands[playerIndex].clear(); + if (!commands[player_index].empty()) { + commands[player_index].clear(); } std::string file = Common::FS::ReadStringFromFile( Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASFile) + "script0-" + - std::to_string(playerIndex + 1) + ".txt", + std::to_string(player_index + 1) + ".txt", Common::FS::FileType::BinaryFile); std::stringstream command_line(file); std::string line; int frameNo = 0; TASCommand empty = {.buttons = 0, .l_axis = {0.f, 0.f}, .r_axis = {0.f, 0.f}}; while (std::getline(command_line, line, '\n')) { - if (line.empty()) + if (line.empty()) { continue; + } LOG_DEBUG(Input, "Loading line: {}", line); std::smatch m; @@ -65,11 +90,12 @@ void Tas::LoadTasFile(int playerIndex) { seglist.push_back(segment); } - if (seglist.size() < 4) + if (seglist.size() < 4) { continue; + } while (frameNo < std::stoi(seglist.at(0))) { - newCommands[playerIndex].push_back(empty); + commands[player_index].push_back(empty); frameNo++; } @@ -78,7 +104,7 @@ void Tas::LoadTasFile(int playerIndex) { .l_axis = ReadCommandAxis(seglist.at(2)), .r_axis = ReadCommandAxis(seglist.at(3)), }; - newCommands[playerIndex].push_back(command); + commands[player_index].push_back(command); frameNo++; } LOG_INFO(Input, "TAS file loaded! {} frames", frameNo); @@ -87,84 +113,89 @@ void Tas::LoadTasFile(int playerIndex) { void Tas::WriteTasFile() { LOG_DEBUG(Input, "WriteTasFile()"); std::string output_text = ""; - for (int frame = 0; frame < (signed)recordCommands.size(); frame++) { - if (!output_text.empty()) + for (int frame = 0; frame < (signed)record_commands.size(); frame++) { + if (!output_text.empty()) { output_text += "\n"; - TASCommand line = recordCommands.at(frame); + } + TASCommand line = record_commands.at(frame); output_text += std::to_string(frame) + " " + WriteCommandButtons(line.buttons) + " " + WriteCommandAxis(line.l_axis) + " " + WriteCommandAxis(line.r_axis); } size_t bytesWritten = Common::FS::WriteStringToFile( Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASFile) + "record.txt", Common::FS::FileType::TextFile, output_text); - if (bytesWritten == output_text.size()) + if (bytesWritten == output_text.size()) { LOG_INFO(Input, "TAS file written to file!"); - else + } + else { LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytesWritten, output_text.size()); + } } -void Tas::RecordInput(u32 buttons, std::array, 2> axes) { - lastInput = {buttons, flipY(axes[0]), flipY(axes[1])}; -} - -std::pair Tas::flipY(std::pair old) const { +static std::pair FlipY(std::pair old) { auto [x, y] = old; return {x, -y}; } -std::string Tas::GetStatusDescription() { +void Tas::RecordInput(u32 buttons, const std::array, 2>& axes) { + last_input = {buttons, FlipY(axes[0]), FlipY(axes[1])}; +} + +std::tuple Tas::GetStatus() { + TasState state; if (Settings::values.tas_record) { - return "Recording TAS: " + std::to_string(recordCommands.size()); + return {TasState::RECORDING, record_commands.size(), record_commands.size()}; + } else if (Settings::values.tas_enable) { + state = TasState::RUNNING; + } else { + state = TasState::STOPPED; } - if (Settings::values.tas_enable) { - return "Playing TAS: " + std::to_string(current_command) + "/" + - std::to_string(scriptLength); - } - return "TAS not running: " + std::to_string(current_command) + "/" + - std::to_string(scriptLength); + + return {state, current_command, script_length}; } -std::string debugButtons(u32 buttons) { - return "{ " + TasInput::Tas::buttonsToString(buttons) + " }"; +static std::string DebugButtons(u32 buttons) { + return "{ " + TasInput::Tas::ButtonsToString(buttons) + " }"; } -std::string debugJoystick(float x, float y) { +static std::string DebugJoystick(float x, float y) { return "[ " + std::to_string(x) + "," + std::to_string(y) + " ]"; } -std::string debugInput(TasData data) { - return "{ " + debugButtons(data.buttons) + " , " + debugJoystick(data.axis[0], data.axis[1]) + - " , " + debugJoystick(data.axis[2], data.axis[3]) + " }"; +static std::string DebugInput(const TasData& data) { + return "{ " + DebugButtons(data.buttons) + " , " + DebugJoystick(data.axis[0], data.axis[1]) + + " , " + DebugJoystick(data.axis[2], data.axis[3]) + " }"; } -std::string debugInputs(std::array arr) { +static std::string DebugInputs(const std::array& arr) { std::string returns = "[ "; for (size_t i = 0; i < arr.size(); i++) { - returns += debugInput(arr[i]); - if (i != arr.size() - 1) + returns += DebugInput(arr[i]); + if (i != arr.size() - 1) { returns += " , "; + } } return returns + "]"; } void Tas::UpdateThread() { if (update_thread_running) { - if (Settings::values.pauseTasOnLoad && Settings::values.cpuBoosted) { - for (int i = 0; i < PLAYER_NUMBER; i++) { + if (Settings::values.pause_tas_on_load && Settings::values.is_cpu_boosted) { + for (size_t i = 0; i < PLAYER_NUMBER; i++) { tas_data[i].buttons = 0; tas_data[i].axis = {}; } } if (Settings::values.tas_record) { - recordCommands.push_back(lastInput); + record_commands.push_back(last_input); } - if (!Settings::values.tas_record && !recordCommands.empty()) { + if (!Settings::values.tas_record && !record_commands.empty()) { WriteTasFile(); Settings::values.tas_reset = true; refresh_tas_fle = true; - recordCommands.clear(); + record_commands.clear(); } if (Settings::values.tas_reset) { current_command = 0; @@ -177,12 +208,12 @@ void Tas::UpdateThread() { LOG_DEBUG(Input, "tas_reset done"); } if (Settings::values.tas_enable) { - if ((signed)current_command < scriptLength) { - LOG_INFO(Input, "Playing TAS {}/{}", current_command, scriptLength); + if ((signed)current_command < script_length) { + LOG_INFO(Input, "Playing TAS {}/{}", current_command, script_length); size_t frame = current_command++; - for (int i = 0; i < PLAYER_NUMBER; i++) { - if (frame < newCommands[i].size()) { - TASCommand command = newCommands[i][frame]; + for (size_t i = 0; i < PLAYER_NUMBER; i++) { + if (frame < commands[i].size()) { + TASCommand command = commands[i][frame]; tas_data[i].buttons = command.buttons; auto [l_axis_x, l_axis_y] = command.l_axis; tas_data[i].axis[0] = l_axis_x; @@ -198,22 +229,22 @@ void Tas::UpdateThread() { } else { Settings::values.tas_enable = false; current_command = 0; - for (int i = 0; i < PLAYER_NUMBER; i++) { + for (size_t i = 0; i < PLAYER_NUMBER; i++) { tas_data[i].buttons = 0; tas_data[i].axis = {}; } } } else { - for (int i = 0; i < PLAYER_NUMBER; i++) { + for (size_t i = 0; i < PLAYER_NUMBER; i++) { tas_data[i].buttons = 0; tas_data[i].axis = {}; } } } - LOG_DEBUG(Input, "TAS inputs: {}", debugInputs(tas_data)); + LOG_DEBUG(Input, "TAS inputs: {}", DebugInputs(tas_data)); } -TasAnalog Tas::ReadCommandAxis(const std::string line) const { +TasAnalog Tas::ReadCommandAxis(const std::string& line) const { std::stringstream linestream(line); std::string segment; std::vector seglist; @@ -228,7 +259,7 @@ TasAnalog Tas::ReadCommandAxis(const std::string line) const { return {x, y}; } -u32 Tas::ReadCommandButtons(const std::string data) const { +u32 Tas::ReadCommandButtons(const std::string& data) const { std::stringstream button_text(data); std::string line; u32 buttons = 0; @@ -262,8 +293,9 @@ std::string Tas::WriteCommandButtons(u32 data) const { if ((data & 1) == 1) { for (auto [text, tas_button] : text_to_tas_button) { if (tas_button == static_cast(1 << index)) { - if (line.size() > 0) + if (line.size() > 0) { line += ";"; + } line += text; break; } diff --git a/src/input_common/tas/tas_input.h b/src/input_common/tas/tas_input.h index 0a152a04f..8ee70bcaf 100644 --- a/src/input_common/tas/tas_input.h +++ b/src/input_common/tas/tas_input.h @@ -12,11 +12,17 @@ #include "core/frontend/input.h" #include "input_common/main.h" -#define PLAYER_NUMBER 8 - namespace TasInput { -using TasAnalog = std::tuple; +constexpr int PLAYER_NUMBER = 8; + +using TasAnalog = std::pair; + +enum class TasState { + RUNNING, + RECORDING, + STOPPED, +}; enum class TasButton : u32 { BUTTON_A = 0x000001, @@ -41,29 +47,6 @@ enum class TasButton : u32 { BUTTON_CAPTURE = 0x080000, }; -static const std::array, 20> text_to_tas_button = { - std::pair{"KEY_A", TasButton::BUTTON_A}, - {"KEY_B", TasButton::BUTTON_B}, - {"KEY_X", TasButton::BUTTON_X}, - {"KEY_Y", TasButton::BUTTON_Y}, - {"KEY_LSTICK", TasButton::STICK_L}, - {"KEY_RSTICK", TasButton::STICK_R}, - {"KEY_L", TasButton::TRIGGER_L}, - {"KEY_R", TasButton::TRIGGER_R}, - {"KEY_PLUS", TasButton::BUTTON_PLUS}, - {"KEY_MINUS", TasButton::BUTTON_MINUS}, - {"KEY_DLEFT", TasButton::BUTTON_LEFT}, - {"KEY_DUP", TasButton::BUTTON_UP}, - {"KEY_DRIGHT", TasButton::BUTTON_RIGHT}, - {"KEY_DDOWN", TasButton::BUTTON_DOWN}, - {"KEY_SL", TasButton::BUTTON_SL}, - {"KEY_SR", TasButton::BUTTON_SR}, - {"KEY_CAPTURE", TasButton::BUTTON_CAPTURE}, - {"KEY_HOME", TasButton::BUTTON_HOME}, - {"KEY_ZL", TasButton::TRIGGER_ZL}, - {"KEY_ZR", TasButton::TRIGGER_ZR}, -}; - enum class TasAxes : u8 { StickX, StickY, @@ -82,7 +65,7 @@ public: Tas(); ~Tas(); - static std::string buttonsToString(u32 button) { + static std::string ButtonsToString(u32 button) { std::string returns; if ((button & static_cast(TasInput::TasButton::BUTTON_A)) != 0) returns += ", A"; @@ -124,14 +107,14 @@ public: returns += ", HOME"; if ((button & static_cast(TasInput::TasButton::BUTTON_CAPTURE)) != 0) returns += ", CAPTURE"; - return returns.length() != 0 ? returns.substr(2) : ""; + return returns.empty() ? "" : returns.substr(2); } void RefreshTasFile(); void LoadTasFiles(); - void RecordInput(u32 buttons, std::array, 2> axes); + void RecordInput(u32 buttons, const std::array, 2>& axes); void UpdateThread(); - std::string GetStatusDescription(); + std::tuple GetStatus(); InputCommon::ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) const; InputCommon::AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) const; @@ -143,21 +126,20 @@ private: TasAnalog l_axis{}; TasAnalog r_axis{}; }; - void LoadTasFile(int playerIndex); + void LoadTasFile(size_t player_index); void WriteTasFile(); - TasAnalog ReadCommandAxis(const std::string line) const; - u32 ReadCommandButtons(const std::string line) const; + TasAnalog ReadCommandAxis(const std::string& line) const; + u32 ReadCommandButtons(const std::string& line) const; std::string WriteCommandButtons(u32 data) const; std::string WriteCommandAxis(TasAnalog data) const; - std::pair flipY(std::pair old) const; - size_t scriptLength{0}; + size_t script_length{0}; std::array tas_data; bool update_thread_running{true}; bool refresh_tas_fle{false}; - std::array, PLAYER_NUMBER> newCommands{}; - std::vector recordCommands{}; + std::array, PLAYER_NUMBER> commands{}; + std::vector record_commands{}; std::size_t current_command{0}; - TASCommand lastInput{}; // only used for recording + TASCommand last_input{}; // only used for recording }; } // namespace TasInput diff --git a/src/yuzu/configuration/configure_filesystem.cpp b/src/yuzu/configuration/configure_filesystem.cpp index cd9ba0a90..4636d476e 100644 --- a/src/yuzu/configuration/configure_filesystem.cpp +++ b/src/yuzu/configuration/configure_filesystem.cpp @@ -78,7 +78,7 @@ void ConfigureFilesystem::applyConfiguration() { Settings::values.gamecard_inserted = ui->gamecard_inserted->isChecked(); Settings::values.gamecard_current_game = ui->gamecard_current_game->isChecked(); - Settings::values.pauseTasOnLoad = ui->tas_pause_on_load->isChecked(); + Settings::values.pause_tas_on_load = ui->tas_pause_on_load->isChecked(); Settings::values.dump_exefs = ui->dump_exefs->isChecked(); Settings::values.dump_nso = ui->dump_nso->isChecked(); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 10057b9ca..0ee0fd8cd 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -826,11 +826,11 @@ void GMainWindow::InitializeWidgets() { }); statusBar()->insertPermanentWidget(0, renderer_status_button); - TASlabel = new QLabel(); - TASlabel->setObjectName(QStringLiteral("TASlabel")); - TASlabel->setText(tr("TAS not running")); - TASlabel->setFocusPolicy(Qt::NoFocus); - statusBar()->insertPermanentWidget(0, TASlabel); + tas_label = new QLabel(); + tas_label->setObjectName(QStringLiteral("TASlabel")); + tas_label->setText(tr("TAS not running")); + tas_label->setFocusPolicy(Qt::NoFocus); + statusBar()->insertPermanentWidget(0, tas_label); statusBar()->setVisible(true); setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); @@ -2896,13 +2896,28 @@ void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_vie } } +static std::string GetTasStateDescription(TasInput::TasState state) { + switch (state) { + case TasInput::TasState::RUNNING: + return "Running"; + case TasInput::TasState::RECORDING: + return "Recording"; + case TasInput::TasState::STOPPED: + return "Stopped"; + default: + return "INVALID STATE"; + } +} + void GMainWindow::UpdateStatusBar() { if (emu_thread == nullptr) { status_bar_update_timer.stop(); return; } - TASlabel->setText(tr(input_subsystem->GetTas()->GetStatusDescription().c_str())); + auto [tas_status, current_tas_frame, total_tas_frames] = input_subsystem->GetTas()->GetStatus(); + tas_label->setText(tr("%1 TAS %2/%3").arg(tr(GetTasStateDescription(tas_status).c_str())).arg(current_tas_frame).arg(total_tas_frames)); + auto& system = Core::System::GetInstance(); auto results = system.GetAndResetPerfStats(); auto& shader_notify = system.GPU().ShaderNotify();