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.
This commit is contained in:
MonsterDruide1 2021-06-18 16:32:46 +02:00
parent 3a7b37238b
commit 4297d2fea2
9 changed files with 140 additions and 107 deletions

View File

@ -21,6 +21,7 @@
#define SCREENSHOTS_DIR "screenshots"
#define SDMC_DIR "sdmc"
#define SHADER_DIR "shader"
#define TAS_DIR "scripts"
// yuzu-specific files

View File

@ -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;

View File

@ -500,7 +500,6 @@ struct Values {
// Controls
InputSetting<std::array<PlayerInput, 10>> players;
std::shared_ptr<InputCommon::InputSubsystem> inputSubsystem = NULL;
Setting<bool> use_docked_mode{true, "use_docked_mode"};
@ -514,9 +513,12 @@ struct Values {
"motion_device"};
BasicSetting<std::string> udp_input_servers{"127.0.0.1:26760", "udp_input_servers"};
BasicSetting<bool> tas_enable{false, "tas_enable"};
BasicSetting<bool> pause_tas_on_load { false, "pause_tas_on_load" };
BasicSetting<bool> tas_enable{ false, "tas_enable" };
BasicSetting<bool> tas_reset{ false, "tas_reset" };
BasicSetting<bool> tas_record{ false, "tas_record" };
BasicSetting<bool> is_cpu_boxted{ false, " BasicSetting<bool> is_cpu_boxted{ false, "cpuBoosted" };
" };
BasicSetting<bool> mouse_panning{false, "mouse_panning"};
BasicRangedSetting<u8> mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"};
@ -550,9 +552,6 @@ struct Values {
BasicSetting<bool> gamecard_current_game{false, "gamecard_current_game"};
BasicSetting<std::string> gamecard_path{std::string(), "gamecard_path"};
// TAS
bool pauseTasOnLoad;
// Debugging
bool record_frame_times;
BasicSetting<bool> use_gdbstub{false, "use_gdbstub"};

View File

@ -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<u32>(mode) == 1);
controller.SetFromCpuBoostMode(mode);
IPC::ResponseBuilder rb{ctx, 2};

View File

@ -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: {

View File

@ -19,6 +19,29 @@
namespace TasInput {
constexpr std::array<std::pair<std::string_view, TasButton>, 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<std::pair<float, float>, 2> axes) {
lastInput = {buttons, flipY(axes[0]), flipY(axes[1])};
}
std::pair<float, float> Tas::flipY(std::pair<float, float> old) const {
static std::pair<float, float> FlipY(std::pair<float, float> old) {
auto [x, y] = old;
return {x, -y};
}
std::string Tas::GetStatusDescription() {
void Tas::RecordInput(u32 buttons, const std::array<std::pair<float, float>, 2>& axes) {
last_input = {buttons, FlipY(axes[0]), FlipY(axes[1])};
}
std::tuple<TasState, size_t, size_t> 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<TasData, PLAYER_NUMBER> arr) {
static std::string DebugInputs(const std::array<TasData, PLAYER_NUMBER>& 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<std::string> 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<TasButton>(1 << index)) {
if (line.size() > 0)
if (line.size() > 0) {
line += ";";
}
line += text;
break;
}

View File

@ -12,11 +12,17 @@
#include "core/frontend/input.h"
#include "input_common/main.h"
#define PLAYER_NUMBER 8
namespace TasInput {
using TasAnalog = std::tuple<float, float>;
constexpr int PLAYER_NUMBER = 8;
using TasAnalog = std::pair<float, float>;
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<std::pair<std::string, TasButton>, 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<u32>(TasInput::TasButton::BUTTON_A)) != 0)
returns += ", A";
@ -124,14 +107,14 @@ public:
returns += ", HOME";
if ((button & static_cast<u32>(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<std::pair<float, float>, 2> axes);
void RecordInput(u32 buttons, const std::array<std::pair<float, float>, 2>& axes);
void UpdateThread();
std::string GetStatusDescription();
std::tuple<TasState, size_t, size_t> 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<float, float> flipY(std::pair<float, float> old) const;
size_t scriptLength{0};
size_t script_length{0};
std::array<TasData, PLAYER_NUMBER> tas_data;
bool update_thread_running{true};
bool refresh_tas_fle{false};
std::array<std::vector<TASCommand>, PLAYER_NUMBER> newCommands{};
std::vector<TASCommand> recordCommands{};
std::array<std::vector<TASCommand>, PLAYER_NUMBER> commands{};
std::vector<TASCommand> record_commands{};
std::size_t current_command{0};
TASCommand lastInput{}; // only used for recording
TASCommand last_input{}; // only used for recording
};
} // namespace TasInput

View File

@ -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();

View File

@ -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();