diff options
Diffstat (limited to '')
58 files changed, 1627 insertions, 171 deletions
diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index b32614797..5d447f108 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 "tas" // yuzu-specific files diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index 6cdd14f13..43b79bd6d 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -116,6 +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::TASDir, yuzu_path / TAS_DIR); } ~PathManagerImpl() = default; diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index f956ac9a2..0a9e3a145 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -23,6 +23,7 @@ enum class YuzuPath { ScreenshotsDir, // Where yuzu screenshots are stored. SDMCDir, // Where the emulated SDMC is stored. ShaderDir, // Where shaders are stored. + TASDir, // Where TAS scripts are stored. }; /** diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 0d2df80a8..69f0bd8c0 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -61,7 +61,6 @@ void LogSettings() { log_setting("Renderer_UseAsynchronousShaders", values.use_asynchronous_shaders.GetValue()); log_setting("Renderer_AnisotropicFilteringLevel", values.max_anisotropy.GetValue()); log_setting("Audio_OutputEngine", values.sink_id.GetValue()); - log_setting("Audio_EnableAudioStretching", values.enable_audio_stretching.GetValue()); log_setting("Audio_OutputDevice", values.audio_device_id.GetValue()); log_setting("DataStorage_UseVirtualSd", values.use_virtual_sd.GetValue()); log_path("DataStorage_CacheDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir)); @@ -72,6 +71,9 @@ void LogSettings() { log_setting("Debugging_ProgramArgs", values.program_args.GetValue()); log_setting("Services_BCATBackend", values.bcat_backend.GetValue()); log_setting("Services_BCATBoxcatLocal", values.bcat_boxcat_local.GetValue()); + log_setting("Input_EnableMotion", values.motion_enabled.GetValue()); + log_setting("Input_EnableVibration", values.vibration_enabled.GetValue()); + log_setting("Input_EnableRawInput", values.enable_raw_input.GetValue()); } bool IsConfiguringGlobal() { @@ -112,7 +114,6 @@ void RestoreGlobalState(bool is_powered_on) { } // Audio - values.enable_audio_stretching.SetGlobal(true); values.volume.SetGlobal(true); // Core diff --git a/src/common/settings.h b/src/common/settings.h index b7195670b..c53d5acc3 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -16,7 +16,6 @@ #include "common/common_types.h" #include "common/settings_input.h" -#include "input_common/udp/client.h" namespace Settings { @@ -415,7 +414,6 @@ struct Values { BasicSetting<std::string> audio_device_id{"auto", "output_device"}; BasicSetting<std::string> sink_id{"auto", "output_engine"}; BasicSetting<bool> audio_muted{false, "audio_muted"}; - Setting<bool> enable_audio_stretching{true, "enable_audio_stretching"}; RangedSetting<u8> volume{100, 0, 100, "volume"}; // Core @@ -504,14 +502,20 @@ struct Values { Setting<bool> use_docked_mode{true, "use_docked_mode"}; + BasicSetting<bool> enable_raw_input{false, "enable_raw_input"}; + Setting<bool> vibration_enabled{true, "vibration_enabled"}; Setting<bool> enable_accurate_vibrations{false, "enable_accurate_vibrations"}; Setting<bool> motion_enabled{true, "motion_enabled"}; BasicSetting<std::string> motion_device{"engine:motion_emu,update_period:100,sensitivity:0.01", "motion_device"}; - BasicSetting<std::string> udp_input_servers{InputCommon::CemuhookUDP::DEFAULT_SRV, - "udp_input_servers"}; + BasicSetting<std::string> udp_input_servers{"127.0.0.1:26760", "udp_input_servers"}; + + BasicSetting<bool> pause_tas_on_load{true, "pause_tas_on_load"}; + BasicSetting<bool> tas_enable{false, "tas_enable"}; + BasicSetting<bool> tas_loop{false, "tas_loop"}; + BasicSetting<bool> tas_swap_controllers{true, "tas_swap_controllers"}; BasicSetting<bool> mouse_panning{false, "mouse_panning"}; BasicRangedSetting<u8> mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"}; diff --git a/src/common/threadsafe_queue.h b/src/common/threadsafe_queue.h index 8430b9778..2c8c2b90e 100644 --- a/src/common/threadsafe_queue.h +++ b/src/common/threadsafe_queue.h @@ -14,7 +14,7 @@ #include <utility> namespace Common { -template <typename T> +template <typename T, bool with_stop_token = false> class SPSCQueue { public: SPSCQueue() { @@ -84,7 +84,7 @@ public: void Wait() { if (Empty()) { std::unique_lock lock{cv_mutex}; - cv.wait(lock, [this]() { return !Empty(); }); + cv.wait(lock, [this] { return !Empty(); }); } } @@ -95,6 +95,19 @@ public: return t; } + T PopWait(std::stop_token stop_token) { + if (Empty()) { + std::unique_lock lock{cv_mutex}; + cv.wait(lock, stop_token, [this] { return !Empty(); }); + } + if (stop_token.stop_requested()) { + return T{}; + } + T t; + Pop(t); + return t; + } + // not thread-safe void Clear() { size.store(0); @@ -123,13 +136,13 @@ private: ElementPtr* read_ptr; std::atomic_size_t size{0}; std::mutex cv_mutex; - std::condition_variable cv; + std::conditional_t<with_stop_token, std::condition_variable_any, std::condition_variable> cv; }; // a simple thread-safe, // single reader, multiple writer queue -template <typename T> +template <typename T, bool with_stop_token = false> class MPSCQueue { public: [[nodiscard]] std::size_t Size() const { @@ -166,13 +179,17 @@ public: return spsc_queue.PopWait(); } + T PopWait(std::stop_token stop_token) { + return spsc_queue.PopWait(stop_token); + } + // not thread-safe void Clear() { spsc_queue.Clear(); } private: - SPSCQueue<T> spsc_queue; + SPSCQueue<T, with_stop_token> spsc_queue; std::mutex write_lock; }; } // namespace Common diff --git a/src/core/core.cpp b/src/core/core.cpp index ba4629993..54ebed2c1 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -83,6 +83,12 @@ FileSys::StorageId GetStorageIdForFrontendSlot( } } +void KProcessDeleter(Kernel::KProcess* process) { + process->Destroy(); +} + +using KProcessPtr = std::unique_ptr<Kernel::KProcess, decltype(&KProcessDeleter)>; + } // Anonymous namespace FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs, @@ -233,8 +239,8 @@ struct System::Impl { } telemetry_session->AddInitialInfo(*app_loader, fs_controller, *content_provider); - auto main_process = Kernel::KProcess::Create(system.Kernel()); - ASSERT(Kernel::KProcess::Initialize(main_process, system, "main", + main_process = KProcessPtr{Kernel::KProcess::Create(system.Kernel()), KProcessDeleter}; + ASSERT(Kernel::KProcess::Initialize(main_process.get(), system, "main", Kernel::KProcess::ProcessType::Userland) .IsSuccess()); main_process->Open(); @@ -247,7 +253,7 @@ struct System::Impl { static_cast<u32>(load_result)); } AddGlueRegistrationForProcess(*app_loader, *main_process); - kernel.MakeCurrentProcess(main_process); + kernel.MakeCurrentProcess(main_process.get()); kernel.InitializeCores(); // Initialize cheat engine @@ -299,10 +305,7 @@ struct System::Impl { is_powered_on = false; exit_lock = false; - if (gpu_core) { - gpu_core->ShutDown(); - } - + gpu_core.reset(); services.reset(); service_manager.reset(); cheat_engine.reset(); @@ -311,11 +314,12 @@ struct System::Impl { time_manager.Shutdown(); core_timing.Shutdown(); app_loader.reset(); - gpu_core.reset(); perf_stats.reset(); kernel.Shutdown(); memory.Reset(); applet_manager.ClearAll(); + // TODO: The main process should be freed based on KAutoObject ref counting. + main_process.reset(); LOG_DEBUG(Core, "Shutdown OK"); } @@ -374,6 +378,7 @@ struct System::Impl { std::unique_ptr<Tegra::GPU> gpu_core; std::unique_ptr<Hardware::InterruptManager> interrupt_manager; std::unique_ptr<Core::DeviceMemory> device_memory; + KProcessPtr main_process{nullptr, KProcessDeleter}; Core::Memory::Memory memory; CpuManager cpu_manager; std::atomic_bool is_powered_on{}; diff --git a/src/core/file_sys/vfs.cpp b/src/core/file_sys/vfs.cpp index 368419eca..f5ad10b15 100644 --- a/src/core/file_sys/vfs.cpp +++ b/src/core/file_sys/vfs.cpp @@ -273,6 +273,10 @@ VirtualFile VfsDirectory::GetFile(std::string_view name) const { return iter == files.end() ? nullptr : *iter; } +FileTimeStampRaw VfsDirectory::GetFileTimeStamp([[maybe_unused]] std::string_view path) const { + return {}; +} + VirtualDir VfsDirectory::GetSubdirectory(std::string_view name) const { const auto& subs = GetSubdirectories(); const auto iter = std::find_if(subs.begin(), subs.end(), diff --git a/src/core/file_sys/vfs.h b/src/core/file_sys/vfs.h index afd64e95c..ff6935da6 100644 --- a/src/core/file_sys/vfs.h +++ b/src/core/file_sys/vfs.h @@ -199,6 +199,9 @@ public: // file with name. virtual VirtualFile GetFile(std::string_view name) const; + // Returns a struct containing the file's timestamp. + virtual FileTimeStampRaw GetFileTimeStamp(std::string_view path) const; + // Returns a vector containing all of the subdirectories in this directory. virtual std::vector<VirtualDir> GetSubdirectories() const = 0; // Returns the directory with name matching name. Returns nullptr if directory dosen't have a diff --git a/src/core/file_sys/vfs_real.cpp b/src/core/file_sys/vfs_real.cpp index 3dad54f49..f4073b76a 100644 --- a/src/core/file_sys/vfs_real.cpp +++ b/src/core/file_sys/vfs_real.cpp @@ -13,6 +13,13 @@ #include "common/logging/log.h" #include "core/file_sys/vfs_real.h" +// For FileTimeStampRaw +#include <sys/stat.h> + +#ifdef _MSC_VER +#define stat _stat64 +#endif + namespace FileSys { namespace FS = Common::FS; @@ -392,6 +399,28 @@ std::vector<VirtualFile> RealVfsDirectory::GetFiles() const { return IterateEntries<RealVfsFile, VfsFile>(); } +FileTimeStampRaw RealVfsDirectory::GetFileTimeStamp(std::string_view path_) const { + const auto full_path = FS::SanitizePath(path + '/' + std::string(path_)); + const auto fs_path = std::filesystem::path{FS::ToU8String(full_path)}; + struct stat file_status; + +#ifdef _WIN32 + const auto stat_result = _wstat64(fs_path.c_str(), &file_status); +#else + const auto stat_result = stat(fs_path.c_str(), &file_status); +#endif + + if (stat_result != 0) { + return {}; + } + + return { + .created{static_cast<u64>(file_status.st_ctime)}, + .accessed{static_cast<u64>(file_status.st_atime)}, + .modified{static_cast<u64>(file_status.st_mtime)}, + }; +} + std::vector<VirtualDir> RealVfsDirectory::GetSubdirectories() const { return IterateEntries<RealVfsDirectory, VfsDirectory>(); } diff --git a/src/core/file_sys/vfs_real.h b/src/core/file_sys/vfs_real.h index e4d1bba79..746e624cb 100644 --- a/src/core/file_sys/vfs_real.h +++ b/src/core/file_sys/vfs_real.h @@ -86,6 +86,7 @@ public: VirtualDir CreateDirectoryRelative(std::string_view relative_path) override; bool DeleteSubdirectoryRecursive(std::string_view name) override; std::vector<VirtualFile> GetFiles() const override; + FileTimeStampRaw GetFileTimeStamp(std::string_view path) const override; std::vector<VirtualDir> GetSubdirectories() const override; bool IsWritable() const override; bool IsReadable() const override; diff --git a/src/core/file_sys/vfs_types.h b/src/core/file_sys/vfs_types.h index 6215ed7af..ed0724717 100644 --- a/src/core/file_sys/vfs_types.h +++ b/src/core/file_sys/vfs_types.h @@ -6,6 +6,8 @@ #include <memory> +#include "common/common_types.h" + namespace FileSys { class VfsDirectory; @@ -18,4 +20,11 @@ using VirtualDir = std::shared_ptr<VfsDirectory>; using VirtualFile = std::shared_ptr<VfsFile>; using VirtualFilesystem = std::shared_ptr<VfsFilesystem>; +struct FileTimeStampRaw { + u64 created{}; + u64 accessed{}; + u64 modified{}; + u64 padding{}; +}; + } // namespace FileSys diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp index c8d65f328..f8f9e32f7 100644 --- a/src/core/hle/service/filesystem/filesystem.cpp +++ b/src/core/hle/service/filesystem/filesystem.cpp @@ -261,6 +261,18 @@ ResultVal<FileSys::EntryType> VfsDirectoryServiceWrapper::GetEntryType( return FileSys::ERROR_PATH_NOT_FOUND; } +ResultVal<FileSys::FileTimeStampRaw> VfsDirectoryServiceWrapper::GetFileTimeStampRaw( + const std::string& path) const { + auto dir = GetDirectoryRelativeWrapped(backing, Common::FS::GetParentPath(path)); + if (dir == nullptr) { + return FileSys::ERROR_PATH_NOT_FOUND; + } + if (GetEntryType(path).Failed()) { + return FileSys::ERROR_PATH_NOT_FOUND; + } + return MakeResult(dir->GetFileTimeStamp(Common::FS::GetFilename(path))); +} + FileSystemController::FileSystemController(Core::System& system_) : system{system_} {} FileSystemController::~FileSystemController() = default; diff --git a/src/core/hle/service/filesystem/filesystem.h b/src/core/hle/service/filesystem/filesystem.h index d387af3cb..b155e0811 100644 --- a/src/core/hle/service/filesystem/filesystem.h +++ b/src/core/hle/service/filesystem/filesystem.h @@ -240,6 +240,12 @@ public: */ ResultVal<FileSys::EntryType> GetEntryType(const std::string& path) const; + /** + * Get the timestamp of the specified path + * @return The timestamp of the specified path or error code + */ + ResultVal<FileSys::FileTimeStampRaw> GetFileTimeStampRaw(const std::string& path) const; + private: FileSys::VirtualDir backing; }; diff --git a/src/core/hle/service/filesystem/fsp_srv.cpp b/src/core/hle/service/filesystem/fsp_srv.cpp index db4d44c12..50c788dd6 100644 --- a/src/core/hle/service/filesystem/fsp_srv.cpp +++ b/src/core/hle/service/filesystem/fsp_srv.cpp @@ -326,7 +326,7 @@ public: {11, &IFileSystem::GetFreeSpaceSize, "GetFreeSpaceSize"}, {12, &IFileSystem::GetTotalSpaceSize, "GetTotalSpaceSize"}, {13, &IFileSystem::CleanDirectoryRecursively, "CleanDirectoryRecursively"}, - {14, nullptr, "GetFileTimeStampRaw"}, + {14, &IFileSystem::GetFileTimeStampRaw, "GetFileTimeStampRaw"}, {15, nullptr, "QueryEntry"}, }; RegisterHandlers(functions); @@ -501,6 +501,24 @@ public: rb.Push(size.get_total_size()); } + void GetFileTimeStampRaw(Kernel::HLERequestContext& ctx) { + const auto file_buffer = ctx.ReadBuffer(); + const std::string name = Common::StringFromBuffer(file_buffer); + + LOG_WARNING(Service_FS, "(Partial Implementation) called. file={}", name); + + auto result = backend.GetFileTimeStampRaw(name); + if (result.Failed()) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result.Code()); + return; + } + + IPC::ResponseBuilder rb{ctx, 10}; + rb.Push(ResultSuccess); + rb.PushRaw(*result); + } + private: VfsDirectoryServiceWrapper backend; SizeGetter size; diff --git a/src/core/hle/service/ngct/ngct.cpp b/src/core/hle/service/ngct/ngct.cpp index deb3abb28..8ec7d5266 100644 --- a/src/core/hle/service/ngct/ngct.cpp +++ b/src/core/hle/service/ngct/ngct.cpp @@ -15,7 +15,7 @@ public: explicit IService(Core::System& system_) : ServiceFramework{system_, "ngct:u"} { // clang-format off static const FunctionInfo functions[] = { - {0, nullptr, "Match"}, + {0, &IService::Match, "Match"}, {1, &IService::Filter, "Filter"}, }; // clang-format on @@ -24,6 +24,19 @@ public: } private: + void Match(Kernel::HLERequestContext& ctx) { + const auto buffer = ctx.ReadBuffer(); + const auto text = Common::StringFromFixedZeroTerminatedBuffer( + reinterpret_cast<const char*>(buffer.data()), buffer.size()); + + LOG_WARNING(Service_NGCT, "(STUBBED) called, text={}", text); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(ResultSuccess); + // Return false since we don't censor anything + rb.Push(false); + } + void Filter(Kernel::HLERequestContext& ctx) { const auto buffer = ctx.ReadBuffer(); const auto text = Common::StringFromFixedZeroTerminatedBuffer( diff --git a/src/core/telemetry_session.cpp b/src/core/telemetry_session.cpp index 1f1607998..191475f71 100644 --- a/src/core/telemetry_session.cpp +++ b/src/core/telemetry_session.cpp @@ -226,8 +226,6 @@ void TelemetrySession::AddInitialInfo(Loader::AppLoader& app_loader, // Log user configuration information constexpr auto field_type = Telemetry::FieldType::UserConfig; AddField(field_type, "Audio_SinkId", Settings::values.sink_id.GetValue()); - AddField(field_type, "Audio_EnableAudioStretching", - Settings::values.enable_audio_stretching.GetValue()); AddField(field_type, "Core_UseMultiCore", Settings::values.use_multi_core.GetValue()); AddField(field_type, "Renderer_Backend", TranslateRenderer(Settings::values.renderer_backend.GetValue())); diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index c4283a952..dd13d948f 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -21,6 +21,10 @@ add_library(input_common STATIC mouse/mouse_poller.h sdl/sdl.cpp sdl/sdl.h + tas/tas_input.cpp + tas/tas_input.h + tas/tas_poller.cpp + tas/tas_poller.h udp/client.cpp udp/client.h udp/protocol.cpp diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index ff23230f0..18d7d8817 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -5,6 +5,7 @@ #include <memory> #include <thread> #include "common/param_package.h" +#include "common/settings.h" #include "input_common/analog_from_button.h" #include "input_common/gcadapter/gc_adapter.h" #include "input_common/gcadapter/gc_poller.h" @@ -13,6 +14,8 @@ #include "input_common/motion_from_button.h" #include "input_common/mouse/mouse_input.h" #include "input_common/mouse/mouse_poller.h" +#include "input_common/tas/tas_input.h" +#include "input_common/tas/tas_poller.h" #include "input_common/touch_from_button.h" #include "input_common/udp/client.h" #include "input_common/udp/udp.h" @@ -60,6 +63,12 @@ struct InputSubsystem::Impl { Input::RegisterFactory<Input::MotionDevice>("mouse", mousemotion); mousetouch = std::make_shared<MouseTouchFactory>(mouse); Input::RegisterFactory<Input::TouchDevice>("mouse", mousetouch); + + tas = std::make_shared<TasInput::Tas>(); + tasbuttons = std::make_shared<TasButtonFactory>(tas); + Input::RegisterFactory<Input::ButtonDevice>("tas", tasbuttons); + tasanalog = std::make_shared<TasAnalogFactory>(tas); + Input::RegisterFactory<Input::AnalogDevice>("tas", tasanalog); } void Shutdown() { @@ -94,6 +103,12 @@ struct InputSubsystem::Impl { mouseanalog.reset(); mousemotion.reset(); mousetouch.reset(); + + Input::UnregisterFactory<Input::ButtonDevice>("tas"); + Input::UnregisterFactory<Input::AnalogDevice>("tas"); + + tasbuttons.reset(); + tasanalog.reset(); } [[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const { @@ -101,6 +116,10 @@ struct InputSubsystem::Impl { Common::ParamPackage{{"display", "Any"}, {"class", "any"}}, Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "keyboard"}}, }; + if (Settings::values.tas_enable) { + devices.emplace_back( + Common::ParamPackage{{"display", "TAS Controller"}, {"class", "tas"}}); + } #ifdef HAVE_SDL2 auto sdl_devices = sdl->GetInputDevices(); devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end()); @@ -120,6 +139,9 @@ struct InputSubsystem::Impl { if (params.Get("class", "") == "gcpad") { return gcadapter->GetAnalogMappingForDevice(params); } + if (params.Get("class", "") == "tas") { + return tas->GetAnalogMappingForDevice(params); + } #ifdef HAVE_SDL2 if (params.Get("class", "") == "sdl") { return sdl->GetAnalogMappingForDevice(params); @@ -136,6 +158,9 @@ struct InputSubsystem::Impl { if (params.Get("class", "") == "gcpad") { return gcadapter->GetButtonMappingForDevice(params); } + if (params.Get("class", "") == "tas") { + return tas->GetButtonMappingForDevice(params); + } #ifdef HAVE_SDL2 if (params.Get("class", "") == "sdl") { return sdl->GetButtonMappingForDevice(params); @@ -174,9 +199,12 @@ struct InputSubsystem::Impl { std::shared_ptr<MouseAnalogFactory> mouseanalog; std::shared_ptr<MouseMotionFactory> mousemotion; std::shared_ptr<MouseTouchFactory> mousetouch; + std::shared_ptr<TasButtonFactory> tasbuttons; + std::shared_ptr<TasAnalogFactory> tasanalog; std::shared_ptr<CemuhookUDP::Client> udp; std::shared_ptr<GCAdapter::Adapter> gcadapter; std::shared_ptr<MouseInput::Mouse> mouse; + std::shared_ptr<TasInput::Tas> tas; }; InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {} @@ -207,6 +235,14 @@ const MouseInput::Mouse* InputSubsystem::GetMouse() const { return impl->mouse.get(); } +TasInput::Tas* InputSubsystem::GetTas() { + return impl->tas.get(); +} + +const TasInput::Tas* InputSubsystem::GetTas() const { + return impl->tas.get(); +} + std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const { return impl->GetInputDevices(); } @@ -287,6 +323,22 @@ const MouseTouchFactory* InputSubsystem::GetMouseTouch() const { return impl->mousetouch.get(); } +TasButtonFactory* InputSubsystem::GetTasButtons() { + return impl->tasbuttons.get(); +} + +const TasButtonFactory* InputSubsystem::GetTasButtons() const { + return impl->tasbuttons.get(); +} + +TasAnalogFactory* InputSubsystem::GetTasAnalogs() { + return impl->tasanalog.get(); +} + +const TasAnalogFactory* InputSubsystem::GetTasAnalogs() const { + return impl->tasanalog.get(); +} + void InputSubsystem::ReloadInputDevices() { if (!impl->udp) { return; diff --git a/src/input_common/main.h b/src/input_common/main.h index 5d6f26385..6390d3f09 100644 --- a/src/input_common/main.h +++ b/src/input_common/main.h @@ -29,6 +29,10 @@ namespace MouseInput { class Mouse; } +namespace TasInput { +class Tas; +} + namespace InputCommon { namespace Polling { @@ -64,6 +68,8 @@ class MouseButtonFactory; class MouseAnalogFactory; class MouseMotionFactory; class MouseTouchFactory; +class TasButtonFactory; +class TasAnalogFactory; class Keyboard; /** @@ -103,6 +109,11 @@ public: /// Retrieves the underlying mouse device. [[nodiscard]] const MouseInput::Mouse* GetMouse() const; + /// Retrieves the underlying tas device. + [[nodiscard]] TasInput::Tas* GetTas(); + + /// Retrieves the underlying tas device. + [[nodiscard]] const TasInput::Tas* GetTas() const; /** * Returns all available input devices that this Factory can create a new device with. * Each returned ParamPackage should have a `display` field used for display, a class field for @@ -144,30 +155,42 @@ public: /// Retrieves the underlying udp touch handler. [[nodiscard]] const UDPTouchFactory* GetUDPTouch() const; - /// Retrieves the underlying GameCube button handler. + /// Retrieves the underlying mouse button handler. [[nodiscard]] MouseButtonFactory* GetMouseButtons(); - /// Retrieves the underlying GameCube button handler. + /// Retrieves the underlying mouse button handler. [[nodiscard]] const MouseButtonFactory* GetMouseButtons() const; - /// Retrieves the underlying udp touch handler. + /// Retrieves the underlying mouse analog handler. [[nodiscard]] MouseAnalogFactory* GetMouseAnalogs(); - /// Retrieves the underlying udp touch handler. + /// Retrieves the underlying mouse analog handler. [[nodiscard]] const MouseAnalogFactory* GetMouseAnalogs() const; - /// Retrieves the underlying udp motion handler. + /// Retrieves the underlying mouse motion handler. [[nodiscard]] MouseMotionFactory* GetMouseMotions(); - /// Retrieves the underlying udp motion handler. + /// Retrieves the underlying mouse motion handler. [[nodiscard]] const MouseMotionFactory* GetMouseMotions() const; - /// Retrieves the underlying udp touch handler. + /// Retrieves the underlying mouse touch handler. [[nodiscard]] MouseTouchFactory* GetMouseTouch(); - /// Retrieves the underlying udp touch handler. + /// Retrieves the underlying mouse touch handler. [[nodiscard]] const MouseTouchFactory* GetMouseTouch() const; + /// Retrieves the underlying tas button handler. + [[nodiscard]] TasButtonFactory* GetTasButtons(); + + /// Retrieves the underlying tas button handler. + [[nodiscard]] const TasButtonFactory* GetTasButtons() const; + + /// Retrieves the underlying tas analogs handler. + [[nodiscard]] TasAnalogFactory* GetTasAnalogs(); + + /// Retrieves the underlying tas analogs handler. + [[nodiscard]] const TasAnalogFactory* GetTasAnalogs() const; + /// Reloads the input devices void ReloadInputDevices(); diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp index f102410d1..03888b7cb 100644 --- a/src/input_common/sdl/sdl_impl.cpp +++ b/src/input_common/sdl/sdl_impl.cpp @@ -21,7 +21,7 @@ #include "common/logging/log.h" #include "common/math_util.h" #include "common/param_package.h" -#include "common/settings_input.h" +#include "common/settings.h" #include "common/threadsafe_queue.h" #include "core/frontend/input.h" #include "input_common/motion_input.h" @@ -889,8 +889,10 @@ SDLState::SDLState() { RegisterFactory<VibrationDevice>("sdl", vibration_factory); RegisterFactory<MotionDevice>("sdl", motion_factory); - // Disable raw input. When enabled this setting causes SDL to die when a web applet opens - SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, "0"); + if (!Settings::values.enable_raw_input) { + // Disable raw input. When enabled this setting causes SDL to die when a web applet opens + SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, "0"); + } // Enable HIDAPI rumble. This prevents SDL from disabling motion on PS4 and PS5 controllers SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1"); @@ -898,10 +900,10 @@ SDLState::SDLState() { // Tell SDL2 to use the hidapi driver. This will allow joycons to be detected as a // GameController and not a generic one - SDL_SetHint("SDL_JOYSTICK_HIDAPI_JOY_CONS", "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1"); // Turn off Pro controller home led - SDL_SetHint("SDL_JOYSTICK_HIDAPI_SWITCH_HOME_LED", "0"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_HOME_LED, "0"); // If the frontend is going to manage the event loop, then we don't start one here start_thread = SDL_WasInit(SDL_INIT_JOYSTICK) == 0; diff --git a/src/input_common/tas/tas_input.cpp b/src/input_common/tas/tas_input.cpp new file mode 100644 index 000000000..1598092b6 --- /dev/null +++ b/src/input_common/tas/tas_input.cpp @@ -0,0 +1,455 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include <cstring> +#include <regex> + +#include "common/fs/file.h" +#include "common/fs/fs_types.h" +#include "common/fs/path_util.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "input_common/tas/tas_input.h" + +namespace TasInput { + +// Supported keywords and buttons from a TAS file +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() { + if (!Settings::values.tas_enable) { + needs_reset = true; + return; + } + LoadTasFiles(); +} + +Tas::~Tas() { + Stop(); +}; + +void Tas::LoadTasFiles() { + script_length = 0; + for (size_t i = 0; i < commands.size(); i++) { + LoadTasFile(i); + if (commands[i].size() > script_length) { + script_length = commands[i].size(); + } + } +} + +void Tas::LoadTasFile(size_t player_index) { + if (!commands[player_index].empty()) { + commands[player_index].clear(); + } + std::string file = + Common::FS::ReadStringFromFile(Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) / + fmt::format("script0-{}.txt", player_index + 1), + Common::FS::FileType::BinaryFile); + std::stringstream command_line(file); + std::string line; + int frame_no = 0; + while (std::getline(command_line, line, '\n')) { + if (line.empty()) { + continue; + } + LOG_DEBUG(Input, "Loading line: {}", line); + std::smatch m; + + std::stringstream linestream(line); + std::string segment; + std::vector<std::string> seglist; + + while (std::getline(linestream, segment, ' ')) { + seglist.push_back(segment); + } + + if (seglist.size() < 4) { + continue; + } + + while (frame_no < std::stoi(seglist.at(0))) { + commands[player_index].push_back({}); + frame_no++; + } + + TASCommand command = { + .buttons = ReadCommandButtons(seglist.at(1)), + .l_axis = ReadCommandAxis(seglist.at(2)), + .r_axis = ReadCommandAxis(seglist.at(3)), + }; + commands[player_index].push_back(command); + frame_no++; + } + LOG_INFO(Input, "TAS file loaded! {} frames", frame_no); +} + +void Tas::WriteTasFile(std::u8string file_name) { + std::string output_text; + for (size_t frame = 0; frame < record_commands.size(); frame++) { + if (!output_text.empty()) { + output_text += "\n"; + } + const TASCommand& line = record_commands[frame]; + output_text += std::to_string(frame) + " " + WriteCommandButtons(line.buttons) + " " + + WriteCommandAxis(line.l_axis) + " " + WriteCommandAxis(line.r_axis); + } + const auto bytes_written = Common::FS::WriteStringToFile( + Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) / file_name, + Common::FS::FileType::TextFile, output_text); + if (bytes_written == output_text.size()) { + LOG_INFO(Input, "TAS file written to file!"); + } else { + LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytes_written, + output_text.size()); + } +} + +std::pair<float, float> Tas::FlipAxisY(std::pair<float, float> old) { + auto [x, y] = old; + return {x, -y}; +} + +void Tas::RecordInput(u32 buttons, const std::array<std::pair<float, float>, 2>& axes) { + last_input = {buttons, FlipAxisY(axes[0]), FlipAxisY(axes[1])}; +} + +std::tuple<TasState, size_t, size_t> Tas::GetStatus() const { + TasState state; + if (is_recording) { + return {TasState::Recording, 0, record_commands.size()}; + } + + if (is_running) { + state = TasState::Running; + } else { + state = TasState::Stopped; + } + + return {state, current_command, script_length}; +} + +std::string Tas::DebugButtons(u32 buttons) const { + return fmt::format("{{ {} }}", TasInput::Tas::ButtonsToString(buttons)); +} + +std::string Tas::DebugJoystick(float x, float y) const { + return fmt::format("[ {} , {} ]", std::to_string(x), std::to_string(y)); +} + +std::string Tas::DebugInput(const TasData& data) const { + return fmt::format("{{ {} , {} , {} }}", DebugButtons(data.buttons), + DebugJoystick(data.axis[0], data.axis[1]), + DebugJoystick(data.axis[2], data.axis[3])); +} + +std::string Tas::DebugInputs(const std::array<TasData, PLAYER_NUMBER>& arr) const { + std::string returns = "[ "; + for (size_t i = 0; i < arr.size(); i++) { + returns += DebugInput(arr[i]); + if (i != arr.size() - 1) { + returns += " , "; + } + } + return returns + "]"; +} + +std::string Tas::ButtonsToString(u32 button) const { + std::string returns; + for (auto [text_button, tas_button] : text_to_tas_button) { + if ((button & static_cast<u32>(tas_button)) != 0) + returns += fmt::format(", {}", text_button.substr(4)); + } + return returns.empty() ? "" : returns.substr(2); +} + +void Tas::UpdateThread() { + if (!Settings::values.tas_enable) { + if (is_running) { + Stop(); + } + return; + } + + if (is_recording) { + record_commands.push_back(last_input); + } + if (needs_reset) { + current_command = 0; + needs_reset = false; + LoadTasFiles(); + LOG_DEBUG(Input, "tas_reset done"); + } + + if (!is_running) { + tas_data.fill({}); + return; + } + if (current_command < script_length) { + LOG_DEBUG(Input, "Playing TAS {}/{}", current_command, script_length); + size_t frame = current_command++; + for (size_t i = 0; i < commands.size(); 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; + tas_data[i].axis[1] = l_axis_y; + auto [r_axis_x, r_axis_y] = command.r_axis; + tas_data[i].axis[2] = r_axis_x; + tas_data[i].axis[3] = r_axis_y; + } else { + tas_data[i] = {}; + } + } + } else { + is_running = Settings::values.tas_loop.GetValue(); + current_command = 0; + tas_data.fill({}); + if (!is_running) { + SwapToStoredController(); + } + } + LOG_DEBUG(Input, "TAS inputs: {}", DebugInputs(tas_data)); +} + +TasAnalog Tas::ReadCommandAxis(const std::string& line) const { + std::stringstream linestream(line); + std::string segment; + std::vector<std::string> seglist; + + while (std::getline(linestream, segment, ';')) { + seglist.push_back(segment); + } + + const float x = std::stof(seglist.at(0)) / 32767.0f; + const float y = std::stof(seglist.at(1)) / 32767.0f; + + return {x, y}; +} + +u32 Tas::ReadCommandButtons(const std::string& data) const { + std::stringstream button_text(data); + std::string line; + u32 buttons = 0; + while (std::getline(button_text, line, ';')) { + for (auto [text, tas_button] : text_to_tas_button) { + if (text == line) { + buttons |= static_cast<u32>(tas_button); + break; + } + } + } + return buttons; +} + +std::string Tas::WriteCommandAxis(TasAnalog data) const { + auto [x, y] = data; + std::string line; + line += std::to_string(static_cast<int>(x * 32767)); + line += ";"; + line += std::to_string(static_cast<int>(y * 32767)); + return line; +} + +std::string Tas::WriteCommandButtons(u32 data) const { + if (data == 0) { + return "NONE"; + } + + std::string line; + u32 index = 0; + while (data > 0) { + 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) { + line += ";"; + } + line += text; + break; + } + } + } + index++; + data >>= 1; + } + return line; +} + +void Tas::StartStop() { + if (!Settings::values.tas_enable) { + return; + } + if (is_running) { + Stop(); + } else { + is_running = true; + SwapToTasController(); + } +} + +void Tas::Stop() { + is_running = false; + SwapToStoredController(); +} + +void Tas::SwapToTasController() { + if (!Settings::values.tas_swap_controllers) { + return; + } + auto& players = Settings::values.players.GetValue(); + for (std::size_t index = 0; index < players.size(); index++) { + auto& player = players[index]; + player_mappings[index] = player; + + // Only swap active controllers + if (!player.connected) { + continue; + } + + Common::ParamPackage tas_param; + tas_param.Set("pad", static_cast<u8>(index)); + auto button_mapping = GetButtonMappingForDevice(tas_param); + auto analog_mapping = GetAnalogMappingForDevice(tas_param); + auto& buttons = player.buttons; + auto& analogs = player.analogs; + + for (std::size_t i = 0; i < buttons.size(); ++i) { + buttons[i] = button_mapping[static_cast<Settings::NativeButton::Values>(i)].Serialize(); + } + for (std::size_t i = 0; i < analogs.size(); ++i) { + analogs[i] = analog_mapping[static_cast<Settings::NativeAnalog::Values>(i)].Serialize(); + } + } + is_old_input_saved = true; + Settings::values.is_device_reload_pending.store(true); +} + +void Tas::SwapToStoredController() { + if (!is_old_input_saved) { + return; + } + auto& players = Settings::values.players.GetValue(); + for (std::size_t index = 0; index < players.size(); index++) { + players[index] = player_mappings[index]; + } + is_old_input_saved = false; + Settings::values.is_device_reload_pending.store(true); +} + +void Tas::Reset() { + if (!Settings::values.tas_enable) { + return; + } + needs_reset = true; +} + +bool Tas::Record() { + if (!Settings::values.tas_enable) { + return true; + } + is_recording = !is_recording; + return is_recording; +} + +void Tas::SaveRecording(bool overwrite_file) { + if (is_recording) { + return; + } + if (record_commands.empty()) { + return; + } + WriteTasFile(u8"record.txt"); + if (overwrite_file) { + WriteTasFile(u8"script0-1.txt"); + } + needs_reset = true; + record_commands.clear(); +} + +InputCommon::ButtonMapping Tas::GetButtonMappingForDevice( + const Common::ParamPackage& params) const { + // This list is missing ZL/ZR since those are not considered buttons. + // We will add those afterwards + // This list also excludes any button that can't be really mapped + static constexpr std::array<std::pair<Settings::NativeButton::Values, TasButton>, 20> + switch_to_tas_button = { + std::pair{Settings::NativeButton::A, TasButton::BUTTON_A}, + {Settings::NativeButton::B, TasButton::BUTTON_B}, + {Settings::NativeButton::X, TasButton::BUTTON_X}, + {Settings::NativeButton::Y, TasButton::BUTTON_Y}, + {Settings::NativeButton::LStick, TasButton::STICK_L}, + {Settings::NativeButton::RStick, TasButton::STICK_R}, + {Settings::NativeButton::L, TasButton::TRIGGER_L}, + {Settings::NativeButton::R, TasButton::TRIGGER_R}, + {Settings::NativeButton::Plus, TasButton::BUTTON_PLUS}, + {Settings::NativeButton::Minus, TasButton::BUTTON_MINUS}, + {Settings::NativeButton::DLeft, TasButton::BUTTON_LEFT}, + {Settings::NativeButton::DUp, TasButton::BUTTON_UP}, + {Settings::NativeButton::DRight, TasButton::BUTTON_RIGHT}, + {Settings::NativeButton::DDown, TasButton::BUTTON_DOWN}, + {Settings::NativeButton::SL, TasButton::BUTTON_SL}, + {Settings::NativeButton::SR, TasButton::BUTTON_SR}, + {Settings::NativeButton::Screenshot, TasButton::BUTTON_CAPTURE}, + {Settings::NativeButton::Home, TasButton::BUTTON_HOME}, + {Settings::NativeButton::ZL, TasButton::TRIGGER_ZL}, + {Settings::NativeButton::ZR, TasButton::TRIGGER_ZR}, + }; + + InputCommon::ButtonMapping mapping{}; + for (const auto& [switch_button, tas_button] : switch_to_tas_button) { + Common::ParamPackage button_params({{"engine", "tas"}}); + button_params.Set("pad", params.Get("pad", 0)); + button_params.Set("button", static_cast<int>(tas_button)); + mapping.insert_or_assign(switch_button, std::move(button_params)); + } + + return mapping; +} + +InputCommon::AnalogMapping Tas::GetAnalogMappingForDevice( + const Common::ParamPackage& params) const { + + InputCommon::AnalogMapping mapping = {}; + Common::ParamPackage left_analog_params; + left_analog_params.Set("engine", "tas"); + left_analog_params.Set("pad", params.Get("pad", 0)); + left_analog_params.Set("axis_x", static_cast<int>(TasAxes::StickX)); + left_analog_params.Set("axis_y", static_cast<int>(TasAxes::StickY)); + mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params)); + Common::ParamPackage right_analog_params; + right_analog_params.Set("engine", "tas"); + right_analog_params.Set("pad", params.Get("pad", 0)); + right_analog_params.Set("axis_x", static_cast<int>(TasAxes::SubstickX)); + right_analog_params.Set("axis_y", static_cast<int>(TasAxes::SubstickY)); + mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params)); + return mapping; +} + +const TasData& Tas::GetTasState(std::size_t pad) const { + return tas_data[pad]; +} +} // namespace TasInput diff --git a/src/input_common/tas/tas_input.h b/src/input_common/tas/tas_input.h new file mode 100644 index 000000000..3e2db8f00 --- /dev/null +++ b/src/input_common/tas/tas_input.h @@ -0,0 +1,237 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> + +#include "common/common_types.h" +#include "common/settings_input.h" +#include "core/frontend/input.h" +#include "input_common/main.h" + +/* +To play back TAS scripts on Yuzu, select the folder with scripts in the configuration menu below +Tools -> Configure TAS. The file itself has normal text format and has to be called script0-1.txt +for controller 1, script0-2.txt for controller 2 and so forth (with max. 8 players). + +A script file has the same format as TAS-nx uses, so final files will look like this: + +1 KEY_B 0;0 0;0 +6 KEY_ZL 0;0 0;0 +41 KEY_ZL;KEY_Y 0;0 0;0 +43 KEY_X;KEY_A 32767;0 0;0 +44 KEY_A 32767;0 0;0 +45 KEY_A 32767;0 0;0 +46 KEY_A 32767;0 0;0 +47 KEY_A 32767;0 0;0 + +After placing the file at the correct location, it can be read into Yuzu with the (default) hotkey +CTRL+F6 (refresh). In the bottom left corner, it will display the amount of frames the script file +has. Playback can be started or stopped using CTRL+F5. + +However, for playback to actually work, the correct input device has to be selected: In the Controls +menu, select TAS from the device list for the controller that the script should be played on. + +Recording a new script file is really simple: Just make sure that the proper device (not TAS) is +connected on P1, and press CTRL+F7 to start recording. When done, just press the same keystroke +again (CTRL+F7). The new script will be saved at the location previously selected, as the filename +record.txt. + +For debugging purposes, the common controller debugger can be used (View -> Debugging -> Controller +P1). +*/ + +namespace TasInput { + +constexpr size_t PLAYER_NUMBER = 8; + +using TasAnalog = std::pair<float, float>; + +enum class TasState { + Running, + Recording, + Stopped, +}; + +enum class TasButton : u32 { + BUTTON_A = 1U << 0, + BUTTON_B = 1U << 1, + BUTTON_X = 1U << 2, + BUTTON_Y = 1U << 3, + STICK_L = 1U << 4, + STICK_R = 1U << 5, + TRIGGER_L = 1U << 6, + TRIGGER_R = 1U << 7, + TRIGGER_ZL = 1U << 8, + TRIGGER_ZR = 1U << 9, + BUTTON_PLUS = 1U << 10, + BUTTON_MINUS = 1U << 11, + BUTTON_LEFT = 1U << 12, + BUTTON_UP = 1U << 13, + BUTTON_RIGHT = 1U << 14, + BUTTON_DOWN = 1U << 15, + BUTTON_SL = 1U << 16, + BUTTON_SR = 1U << 17, + BUTTON_HOME = 1U << 18, + BUTTON_CAPTURE = 1U << 19, +}; + +enum class TasAxes : u8 { + StickX, + StickY, + SubstickX, + SubstickY, + Undefined, +}; + +struct TasData { + u32 buttons{}; + std::array<float, 4> axis{}; +}; + +class Tas { +public: + Tas(); + ~Tas(); + + // Changes the input status that will be stored in each frame + void RecordInput(u32 buttons, const std::array<std::pair<float, float>, 2>& axes); + + // Main loop that records or executes input + void UpdateThread(); + + // Sets the flag to start or stop the TAS command excecution and swaps controllers profiles + void StartStop(); + + // Stop the TAS and reverts any controller profile + void Stop(); + + // Sets the flag to reload the file and start from the begining in the next update + void Reset(); + + /** + * Sets the flag to enable or disable recording of inputs + * @return Returns true if the current recording status is enabled + */ + bool Record(); + + // Saves contents of record_commands on a file if overwrite is enabled player 1 will be + // overwritten with the recorded commands + void SaveRecording(bool overwrite_file); + + /** + * Returns the current status values of TAS playback/recording + * @return Tuple of + * TasState indicating the current state out of Running, Recording or Stopped ; + * Current playback progress or amount of frames (so far) for Recording ; + * Total length of script file currently loaded or amount of frames (so far) for Recording + */ + std::tuple<TasState, size_t, size_t> GetStatus() const; + + // Retuns an array of the default button mappings + InputCommon::ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) const; + + // Retuns an array of the default analog mappings + InputCommon::AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) const; + [[nodiscard]] const TasData& GetTasState(std::size_t pad) const; + +private: + struct TASCommand { + u32 buttons{}; + TasAnalog l_axis{}; + TasAnalog r_axis{}; + }; + + // Loads TAS files from all players + void LoadTasFiles(); + + // Loads TAS file from the specified player + void LoadTasFile(size_t player_index); + + // Writes a TAS file from the recorded commands + void WriteTasFile(std::u8string file_name); + + /** + * Parses a string containing the axis values with the following format "x;y" + * X and Y have a range from -32767 to 32767 + * @return Returns a TAS analog object with axis values with range from -1.0 to 1.0 + */ + TasAnalog ReadCommandAxis(const std::string& line) const; + + /** + * Parses a string containing the button values with the following format "a;b;c;d..." + * Each button is represented by it's text format specified in text_to_tas_button array + * @return Returns a u32 with each bit representing the status of a button + */ + u32 ReadCommandButtons(const std::string& line) const; + + /** + * Converts an u32 containing the button status into the text equivalent + * @return Returns a string with the name of the buttons to be written to the file + */ + std::string WriteCommandButtons(u32 data) const; + + /** + * Converts an TAS analog object containing the axis status into the text equivalent + * @return Returns a string with the value of the axis to be written to the file + */ + std::string WriteCommandAxis(TasAnalog data) const; + + // Inverts the Y axis polarity + std::pair<float, float> FlipAxisY(std::pair<float, float> old); + + /** + * Converts an u32 containing the button status into the text equivalent + * @return Returns a string with the name of the buttons to be printed on console + */ + std::string DebugButtons(u32 buttons) const; + + /** + * Converts an TAS analog object containing the axis status into the text equivalent + * @return Returns a string with the value of the axis to be printed on console + */ + std::string DebugJoystick(float x, float y) const; + + /** + * Converts the given TAS status into the text equivalent + * @return Returns a string with the value of the TAS status to be printed on console + */ + std::string DebugInput(const TasData& data) const; + + /** + * Converts the given TAS status of multiple players into the text equivalent + * @return Returns a string with the value of the status of all TAS players to be printed on + * console + */ + std::string DebugInputs(const std::array<TasData, PLAYER_NUMBER>& arr) const; + + /** + * Converts an u32 containing the button status into the text equivalent + * @return Returns a string with the name of the buttons + */ + std::string ButtonsToString(u32 button) const; + + // Stores current controller configuration and sets a TAS controller for every active controller + // to the current config + void SwapToTasController(); + + // Sets the stored controller configuration to the current config + void SwapToStoredController(); + + size_t script_length{0}; + std::array<TasData, PLAYER_NUMBER> tas_data; + bool is_old_input_saved{false}; + bool is_recording{false}; + bool is_running{false}; + bool needs_reset{false}; + std::array<std::vector<TASCommand>, PLAYER_NUMBER> commands{}; + std::vector<TASCommand> record_commands{}; + size_t current_command{0}; + TASCommand last_input{}; // only used for recording + + // Old settings for swapping controllers + std::array<Settings::PlayerInput, 10> player_mappings; +}; +} // namespace TasInput diff --git a/src/input_common/tas/tas_poller.cpp b/src/input_common/tas/tas_poller.cpp new file mode 100644 index 000000000..15810d6b0 --- /dev/null +++ b/src/input_common/tas/tas_poller.cpp @@ -0,0 +1,101 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <mutex> +#include <utility> + +#include "common/settings.h" +#include "common/threadsafe_queue.h" +#include "input_common/tas/tas_input.h" +#include "input_common/tas/tas_poller.h" + +namespace InputCommon { + +class TasButton final : public Input::ButtonDevice { +public: + explicit TasButton(u32 button_, u32 pad_, const TasInput::Tas* tas_input_) + : button(button_), pad(pad_), tas_input(tas_input_) {} + + bool GetStatus() const override { + return (tas_input->GetTasState(pad).buttons & button) != 0; + } + +private: + const u32 button; + const u32 pad; + const TasInput::Tas* tas_input; +}; + +TasButtonFactory::TasButtonFactory(std::shared_ptr<TasInput::Tas> tas_input_) + : tas_input(std::move(tas_input_)) {} + +std::unique_ptr<Input::ButtonDevice> TasButtonFactory::Create(const Common::ParamPackage& params) { + const auto button_id = params.Get("button", 0); + const auto pad = params.Get("pad", 0); + + return std::make_unique<TasButton>(button_id, pad, tas_input.get()); +} + +class TasAnalog final : public Input::AnalogDevice { +public: + explicit TasAnalog(u32 pad_, u32 axis_x_, u32 axis_y_, const TasInput::Tas* tas_input_) + : pad(pad_), axis_x(axis_x_), axis_y(axis_y_), tas_input(tas_input_) {} + + float GetAxis(u32 axis) const { + std::lock_guard lock{mutex}; + return tas_input->GetTasState(pad).axis.at(axis); + } + + std::pair<float, float> GetAnalog(u32 analog_axis_x, u32 analog_axis_y) const { + float x = GetAxis(analog_axis_x); + float y = GetAxis(analog_axis_y); + + // Make sure the coordinates are in the unit circle, + // otherwise normalize it. + float r = x * x + y * y; + if (r > 1.0f) { + r = std::sqrt(r); + x /= r; + y /= r; + } + + return {x, y}; + } + + std::tuple<float, float> GetStatus() const override { + return GetAnalog(axis_x, axis_y); + } + + Input::AnalogProperties GetAnalogProperties() const override { + return {0.0f, 1.0f, 0.5f}; + } + +private: + const u32 pad; + const u32 axis_x; + const u32 axis_y; + const TasInput::Tas* tas_input; + mutable std::mutex mutex; +}; + +/// An analog device factory that creates analog devices from GC Adapter +TasAnalogFactory::TasAnalogFactory(std::shared_ptr<TasInput::Tas> tas_input_) + : tas_input(std::move(tas_input_)) {} + +/** + * Creates analog device from joystick axes + * @param params contains parameters for creating the device: + * - "port": the nth gcpad on the adapter + * - "axis_x": the index of the axis to be bind as x-axis + * - "axis_y": the index of the axis to be bind as y-axis + */ +std::unique_ptr<Input::AnalogDevice> TasAnalogFactory::Create(const Common::ParamPackage& params) { + const auto pad = static_cast<u32>(params.Get("pad", 0)); + const auto axis_x = static_cast<u32>(params.Get("axis_x", 0)); + const auto axis_y = static_cast<u32>(params.Get("axis_y", 1)); + + return std::make_unique<TasAnalog>(pad, axis_x, axis_y, tas_input.get()); +} + +} // namespace InputCommon diff --git a/src/input_common/tas/tas_poller.h b/src/input_common/tas/tas_poller.h new file mode 100644 index 000000000..09e426cef --- /dev/null +++ b/src/input_common/tas/tas_poller.h @@ -0,0 +1,43 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include "core/frontend/input.h" +#include "input_common/tas/tas_input.h" + +namespace InputCommon { + +/** + * A button device factory representing a tas bot. It receives tas events and forward them + * to all button devices it created. + */ +class TasButtonFactory final : public Input::Factory<Input::ButtonDevice> { +public: + explicit TasButtonFactory(std::shared_ptr<TasInput::Tas> tas_input_); + + /** + * Creates a button device from a button press + * @param params contains parameters for creating the device: + * - "code": the code of the key to bind with the button + */ + std::unique_ptr<Input::ButtonDevice> Create(const Common::ParamPackage& params) override; + +private: + std::shared_ptr<TasInput::Tas> tas_input; +}; + +/// An analog device factory that creates analog devices from tas +class TasAnalogFactory final : public Input::Factory<Input::AnalogDevice> { +public: + explicit TasAnalogFactory(std::shared_ptr<TasInput::Tas> tas_input_); + + std::unique_ptr<Input::AnalogDevice> Create(const Common::ParamPackage& params) override; + +private: + std::shared_ptr<TasInput::Tas> tas_input; +}; + +} // namespace InputCommon diff --git a/src/input_common/udp/client.h b/src/input_common/udp/client.h index a11ea3068..380f9bb76 100644 --- a/src/input_common/udp/client.h +++ b/src/input_common/udp/client.h @@ -21,8 +21,6 @@ namespace InputCommon::CemuhookUDP { -constexpr char DEFAULT_SRV[] = "127.0.0.1:26760"; - class Socket; namespace Response { diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index ff024f530..2ae3639b5 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -531,14 +531,6 @@ void GPU::TriggerCpuInterrupt(const u32 syncpoint_id, const u32 value) const { interrupt_manager.GPUInterruptSyncpt(syncpoint_id, value); } -void GPU::ShutDown() { - // Signal that threads should no longer block on syncpoint fences - shutting_down.store(true, std::memory_order_relaxed); - sync_cv.notify_all(); - - gpu_thread.ShutDown(); -} - void GPU::OnCommandListEnd() { if (is_async) { // This command only applies to asynchronous GPU mode diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h index a8e98e51b..e6a02a71b 100644 --- a/src/video_core/gpu.h +++ b/src/video_core/gpu.h @@ -219,9 +219,6 @@ public: return *shader_notify; } - // Stops the GPU execution and waits for the GPU to finish working - void ShutDown(); - /// Allows the CPU/NvFlinger to wait on the GPU before presenting a frame. void WaitFence(u32 syncpoint_id, u32 value); diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp index 46f642b19..9547f277a 100644 --- a/src/video_core/gpu_thread.cpp +++ b/src/video_core/gpu_thread.cpp @@ -17,9 +17,9 @@ namespace VideoCommon::GPUThread { /// Runs the GPU thread -static void RunThread(Core::System& system, VideoCore::RendererBase& renderer, - Core::Frontend::GraphicsContext& context, Tegra::DmaPusher& dma_pusher, - SynchState& state) { +static void RunThread(std::stop_token stop_token, Core::System& system, + VideoCore::RendererBase& renderer, Core::Frontend::GraphicsContext& context, + Tegra::DmaPusher& dma_pusher, SynchState& state) { std::string name = "yuzu:GPU"; MicroProfileOnThreadCreate(name.c_str()); SCOPE_EXIT({ MicroProfileOnThreadExit(); }); @@ -28,20 +28,14 @@ static void RunThread(Core::System& system, VideoCore::RendererBase& renderer, Common::SetCurrentThreadPriority(Common::ThreadPriority::High); system.RegisterHostThread(); - // Wait for first GPU command before acquiring the window context - state.queue.Wait(); - - // If emulation was stopped during disk shader loading, abort before trying to acquire context - if (!state.is_running) { - return; - } - auto current_context = context.Acquire(); VideoCore::RasterizerInterface* const rasterizer = renderer.ReadRasterizer(); - CommandDataContainer next; - while (state.is_running) { - next = state.queue.PopWait(); + while (!stop_token.stop_requested()) { + CommandDataContainer next = state.queue.PopWait(stop_token); + if (stop_token.stop_requested()) { + break; + } if (auto* submit_list = std::get_if<SubmitListCommand>(&next.data)) { dma_pusher.Push(std::move(submit_list->entries)); dma_pusher.DispatchCalls(); @@ -55,8 +49,6 @@ static void RunThread(Core::System& system, VideoCore::RendererBase& renderer, rasterizer->FlushRegion(flush->addr, flush->size); } else if (const auto* invalidate = std::get_if<InvalidateRegionCommand>(&next.data)) { rasterizer->OnCPUWrite(invalidate->addr, invalidate->size); - } else if (std::holds_alternative<EndProcessingCommand>(next.data)) { - ASSERT(state.is_running == false); } else { UNREACHABLE(); } @@ -73,16 +65,14 @@ static void RunThread(Core::System& system, VideoCore::RendererBase& renderer, ThreadManager::ThreadManager(Core::System& system_, bool is_async_) : system{system_}, is_async{is_async_} {} -ThreadManager::~ThreadManager() { - ShutDown(); -} +ThreadManager::~ThreadManager() = default; void ThreadManager::StartThread(VideoCore::RendererBase& renderer, Core::Frontend::GraphicsContext& context, Tegra::DmaPusher& dma_pusher) { rasterizer = renderer.ReadRasterizer(); - thread = std::thread(RunThread, std::ref(system), std::ref(renderer), std::ref(context), - std::ref(dma_pusher), std::ref(state)); + thread = std::jthread(RunThread, std::ref(system), std::ref(renderer), std::ref(context), + std::ref(dma_pusher), std::ref(state)); } void ThreadManager::SubmitList(Tegra::CommandList&& entries) { @@ -117,26 +107,6 @@ void ThreadManager::FlushAndInvalidateRegion(VAddr addr, u64 size) { rasterizer->OnCPUWrite(addr, size); } -void ThreadManager::ShutDown() { - if (!state.is_running) { - return; - } - - { - std::lock_guard lk(state.write_lock); - state.is_running = false; - state.cv.notify_all(); - } - - if (!thread.joinable()) { - return; - } - - // Notify GPU thread that a shutdown is pending - PushCommand(EndProcessingCommand()); - thread.join(); -} - void ThreadManager::OnCommandListEnd() { PushCommand(OnCommandListEndCommand()); } @@ -152,9 +122,8 @@ u64 ThreadManager::PushCommand(CommandData&& command_data, bool block) { state.queue.Push(CommandDataContainer(std::move(command_data), fence, block)); if (block) { - state.cv.wait(lk, [this, fence] { - return fence <= state.signaled_fence.load(std::memory_order_relaxed) || - !state.is_running; + state.cv.wait(lk, thread.get_stop_token(), [this, fence] { + return fence <= state.signaled_fence.load(std::memory_order_relaxed); }); } diff --git a/src/video_core/gpu_thread.h b/src/video_core/gpu_thread.h index 11a648f38..91bada925 100644 --- a/src/video_core/gpu_thread.h +++ b/src/video_core/gpu_thread.h @@ -33,9 +33,6 @@ class RendererBase; namespace VideoCommon::GPUThread { -/// Command to signal to the GPU thread that processing has ended -struct EndProcessingCommand final {}; - /// Command to signal to the GPU thread that a command list is ready for processing struct SubmitListCommand final { explicit SubmitListCommand(Tegra::CommandList&& entries_) : entries{std::move(entries_)} {} @@ -83,7 +80,7 @@ struct OnCommandListEndCommand final {}; struct GPUTickCommand final {}; using CommandData = - std::variant<EndProcessingCommand, SubmitListCommand, SwapBuffersCommand, FlushRegionCommand, + std::variant<std::monostate, SubmitListCommand, SwapBuffersCommand, FlushRegionCommand, InvalidateRegionCommand, FlushAndInvalidateRegionCommand, OnCommandListEndCommand, GPUTickCommand>; @@ -100,14 +97,12 @@ struct CommandDataContainer { /// Struct used to synchronize the GPU thread struct SynchState final { - std::atomic_bool is_running{true}; - - using CommandQueue = Common::SPSCQueue<CommandDataContainer>; + using CommandQueue = Common::SPSCQueue<CommandDataContainer, true>; std::mutex write_lock; CommandQueue queue; u64 last_fence{}; std::atomic<u64> signaled_fence{}; - std::condition_variable cv; + std::condition_variable_any cv; }; /// Class used to manage the GPU thread @@ -149,7 +144,7 @@ private: VideoCore::RasterizerInterface* rasterizer = nullptr; SynchState state; - std::thread thread; + std::jthread thread; }; } // namespace VideoCommon::GPUThread diff --git a/src/video_core/renderer_opengl/gl_shader_cache.cpp b/src/video_core/renderer_opengl/gl_shader_cache.cpp index b0e14182e..02682bd76 100644 --- a/src/video_core/renderer_opengl/gl_shader_cache.cpp +++ b/src/video_core/renderer_opengl/gl_shader_cache.cpp @@ -293,6 +293,8 @@ void ShaderCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading, }}; LoadPipelines(stop_loading, shader_cache_filename, CACHE_VERSION, load_compute, load_graphics); + LOG_INFO(Render_OpenGL, "Total Pipeline Count: {}", state.total); + std::unique_lock lock{state.mutex}; callback(VideoCore::LoadCallbackStage::Build, 0, state.total); state.has_loaded = true; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 31bfbcb06..eb8b4e08b 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -447,6 +447,8 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading VideoCommon::LoadPipelines(stop_loading, pipeline_cache_filename, CACHE_VERSION, load_compute, load_graphics); + LOG_INFO(Render_Vulkan, "Total Pipeline Count: {}", state.total); + std::unique_lock lock{state.mutex}; callback(VideoCore::LoadCallbackStage::Build, 0, state.total); state.has_loaded = true; diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp index 1d438787a..0c11c814f 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.cpp +++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp @@ -43,17 +43,10 @@ VKScheduler::VKScheduler(const Device& device_, StateTracker& state_tracker_) command_pool{std::make_unique<CommandPool>(*master_semaphore, device)} { AcquireNewChunk(); AllocateWorkerCommandBuffer(); - worker_thread = std::thread(&VKScheduler::WorkerThread, this); + worker_thread = std::jthread([this](std::stop_token token) { WorkerThread(token); }); } -VKScheduler::~VKScheduler() { - { - std::lock_guard lock{work_mutex}; - quit = true; - } - work_cv.notify_all(); - worker_thread.join(); -} +VKScheduler::~VKScheduler() = default; void VKScheduler::Flush(VkSemaphore signal_semaphore, VkSemaphore wait_semaphore) { SubmitExecution(signal_semaphore, wait_semaphore); @@ -135,7 +128,7 @@ bool VKScheduler::UpdateGraphicsPipeline(GraphicsPipeline* pipeline) { return true; } -void VKScheduler::WorkerThread() { +void VKScheduler::WorkerThread(std::stop_token stop_token) { Common::SetCurrentThreadName("yuzu:VulkanWorker"); do { if (work_queue.empty()) { @@ -144,8 +137,8 @@ void VKScheduler::WorkerThread() { std::unique_ptr<CommandChunk> work; { std::unique_lock lock{work_mutex}; - work_cv.wait(lock, [this] { return !work_queue.empty() || quit; }); - if (quit) { + work_cv.wait(lock, stop_token, [this] { return !work_queue.empty(); }); + if (stop_token.stop_requested()) { continue; } work = std::move(work_queue.front()); @@ -158,7 +151,7 @@ void VKScheduler::WorkerThread() { } std::lock_guard reserve_lock{reserve_mutex}; chunk_reserve.push_back(std::move(work)); - } while (!quit); + } while (!stop_token.stop_requested()); } void VKScheduler::AllocateWorkerCommandBuffer() { diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h index 759ed5a48..bd22e4e83 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.h +++ b/src/video_core/renderer_vulkan/vk_scheduler.h @@ -187,7 +187,7 @@ private: GraphicsPipeline* graphics_pipeline = nullptr; }; - void WorkerThread(); + void WorkerThread(std::stop_token stop_token); void AllocateWorkerCommandBuffer(); @@ -212,7 +212,7 @@ private: vk::CommandBuffer current_cmdbuf; std::unique_ptr<CommandChunk> chunk; - std::thread worker_thread; + std::jthread worker_thread; State state; @@ -224,9 +224,8 @@ private: std::vector<std::unique_ptr<CommandChunk>> chunk_reserve; std::mutex reserve_mutex; std::mutex work_mutex; - std::condition_variable work_cv; + std::condition_variable_any work_cv; std::condition_variable wait_cv; - std::atomic_bool quit{}; }; } // namespace Vulkan diff --git a/src/video_core/vulkan_common/vulkan_debug_callback.cpp b/src/video_core/vulkan_common/vulkan_debug_callback.cpp index 0f60765bb..cf94e1d39 100644 --- a/src/video_core/vulkan_common/vulkan_debug_callback.cpp +++ b/src/video_core/vulkan_common/vulkan_debug_callback.cpp @@ -16,6 +16,7 @@ VkBool32 Callback(VkDebugUtilsMessageSeverityFlagBitsEXT severity, switch (static_cast<u32>(data->messageIdNumber)) { case 0x682a878au: // VUID-vkCmdBindVertexBuffers2EXT-pBuffers-parameter case 0x99fb7dfdu: // UNASSIGNED-RequiredParameter (vkCmdBindVertexBuffers2EXT pBuffers[0]) + case 0xe8616bf2u: // Bound VkDescriptorSet 0x0[] was destroyed. Likely push_descriptor related return VK_FALSE; default: break; diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 24fb50db9..c2ec9f76a 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -243,7 +243,6 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR SetupFamilies(surface); SetupFeatures(); SetupProperties(); - CollectTelemetryParameters(); const auto queue_cis = GetDeviceQueueCreateInfos(); const std::vector extensions = LoadExtensions(surface != nullptr); @@ -369,20 +368,9 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR }; SetNext(next, demote); - if (driver_id == VK_DRIVER_ID_AMD_PROPRIETARY || driver_id == VK_DRIVER_ID_AMD_OPEN_SOURCE) { - const u32 version = properties.driverVersion; - // Broken in this driver - if (version > VK_MAKE_API_VERSION(0, 2, 0, 193)) { - LOG_WARNING(Render_Vulkan, "AMD proprietary driver versions newer than 21.9.1 " - "(windows) / 0.2.0.194 (amdvlk) have " - "broken VkPhysicalDeviceFloat16Int8FeaturesKHR"); - is_int8_supported = false; - is_float16_supported = false; - } - } - + VkPhysicalDeviceFloat16Int8FeaturesKHR float16_int8; if (is_int8_supported || is_float16_supported) { - VkPhysicalDeviceFloat16Int8FeaturesKHR float16_int8{ + float16_int8 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FLOAT16_INT8_FEATURES_KHR, .pNext = nullptr, .shaderFloat16 = is_float16_supported, @@ -573,6 +561,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR logical = vk::Device::Create(physical, queue_cis, extensions, first_next, dld); CollectPhysicalMemoryInfo(); + CollectTelemetryParameters(); CollectToolingInfo(); if (driver_id == VK_DRIVER_ID_NVIDIA_PROPRIETARY_KHR) { diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 19ba0dbba..b6dda283d 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -108,6 +108,9 @@ add_executable(yuzu configuration/configure_system.cpp configuration/configure_system.h configuration/configure_system.ui + configuration/configure_tas.cpp + configuration/configure_tas.h + configuration/configure_tas.ui configuration/configure_touch_from_button.cpp configuration/configure_touch_from_button.h configuration/configure_touch_from_button.ui diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index 2e0ade815..1519a46ed 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -36,6 +36,7 @@ #include "input_common/keyboard.h" #include "input_common/main.h" #include "input_common/mouse/mouse_input.h" +#include "input_common/tas/tas_input.h" #include "video_core/renderer_base.h" #include "video_core/video_core.h" #include "yuzu/bootmanager.h" @@ -312,6 +313,7 @@ GRenderWindow::~GRenderWindow() { } void GRenderWindow::OnFrameDisplayed() { + input_subsystem->GetTas()->UpdateThread(); if (!first_frame) { first_frame = true; emit FirstFrameDisplayed(); diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 8744d8e5d..27b67fd9e 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -221,7 +221,7 @@ const std::array<int, Settings::NativeKeyboard::NumKeyboardMods> Config::default // This must be in alphabetical order according to action name as it must have the same order as // UISetting::values.shortcuts, which is alphabetically ordered. // clang-format off -const std::array<UISettings::Shortcut, 18> Config::default_hotkeys{{ +const std::array<UISettings::Shortcut, 21> Config::default_hotkeys{{ {QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::WidgetWithChildrenShortcut}}, {QStringLiteral("Change Docked Mode"), QStringLiteral("Main Window"), {QStringLiteral("F10"), Qt::ApplicationShortcut}}, {QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}}, @@ -235,6 +235,9 @@ const std::array<UISettings::Shortcut, 18> Config::default_hotkeys{{ {QStringLiteral("Mute Audio"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+M"), Qt::WindowShortcut}}, {QStringLiteral("Restart Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F6"), Qt::WindowShortcut}}, {QStringLiteral("Stop Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F5"), Qt::WindowShortcut}}, + {QStringLiteral("TAS Start/Stop"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F5"), Qt::ApplicationShortcut}}, + {QStringLiteral("TAS Reset"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F6"), Qt::ApplicationShortcut}}, + {QStringLiteral("TAS Record"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F7"), Qt::ApplicationShortcut}}, {QStringLiteral("Toggle Filter Bar"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F"), Qt::WindowShortcut}}, {QStringLiteral("Toggle Framerate Limit"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+U"), Qt::ApplicationShortcut}}, {QStringLiteral("Toggle Mouse Panning"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F9"), Qt::ApplicationShortcut}}, @@ -542,7 +545,6 @@ void Config::ReadAudioValues() { ReadBasicSetting(Settings::values.audio_device_id); ReadBasicSetting(Settings::values.sink_id); } - ReadGlobalSetting(Settings::values.enable_audio_stretching); ReadGlobalSetting(Settings::values.volume); qt_config->endGroup(); @@ -560,10 +562,16 @@ void Config::ReadControlValues() { ReadTouchscreenValues(); ReadMotionTouchValues(); + ReadBasicSetting(Settings::values.enable_raw_input); ReadBasicSetting(Settings::values.emulate_analog_keyboard); Settings::values.mouse_panning = false; ReadBasicSetting(Settings::values.mouse_panning_sensitivity); + ReadBasicSetting(Settings::values.tas_enable); + ReadBasicSetting(Settings::values.tas_loop); + ReadBasicSetting(Settings::values.tas_swap_controllers); + ReadBasicSetting(Settings::values.pause_tas_on_load); + ReadGlobalSetting(Settings::values.use_docked_mode); // Disable docked mode if handheld is selected @@ -661,6 +669,13 @@ void Config::ReadDataStorageValues() { QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir))) .toString() .toStdString()); + FS::SetYuzuPath(FS::YuzuPath::TASDir, + qt_config + ->value(QStringLiteral("tas_directory"), + QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir))) + .toString() + .toStdString()); + ReadBasicSetting(Settings::values.gamecard_inserted); ReadBasicSetting(Settings::values.gamecard_current_game); ReadBasicSetting(Settings::values.gamecard_path); @@ -1163,7 +1178,6 @@ void Config::SaveAudioValues() { WriteBasicSetting(Settings::values.sink_id); WriteBasicSetting(Settings::values.audio_device_id); } - WriteGlobalSetting(Settings::values.enable_audio_stretching); WriteGlobalSetting(Settings::values.volume); qt_config->endGroup(); @@ -1184,10 +1198,16 @@ void Config::SaveControlValues() { WriteGlobalSetting(Settings::values.vibration_enabled); WriteGlobalSetting(Settings::values.enable_accurate_vibrations); WriteGlobalSetting(Settings::values.motion_enabled); + WriteBasicSetting(Settings::values.enable_raw_input); WriteBasicSetting(Settings::values.keyboard_enabled); WriteBasicSetting(Settings::values.emulate_analog_keyboard); WriteBasicSetting(Settings::values.mouse_panning_sensitivity); + WriteBasicSetting(Settings::values.tas_enable); + WriteBasicSetting(Settings::values.tas_loop); + WriteBasicSetting(Settings::values.tas_swap_controllers); + WriteBasicSetting(Settings::values.pause_tas_on_load); + qt_config->endGroup(); } @@ -1215,6 +1235,10 @@ void Config::SaveDataStorageValues() { WriteSetting(QStringLiteral("dump_directory"), QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)), QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir))); + WriteSetting(QStringLiteral("tas_directory"), + QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir)), + QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir))); + WriteBasicSetting(Settings::values.gamecard_inserted); WriteBasicSetting(Settings::values.gamecard_current_game); WriteBasicSetting(Settings::values.gamecard_path); diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h index 4733227b6..3ee694e7c 100644 --- a/src/yuzu/configuration/config.h +++ b/src/yuzu/configuration/config.h @@ -42,7 +42,7 @@ public: default_mouse_buttons; static const std::array<int, Settings::NativeKeyboard::NumKeyboardKeys> default_keyboard_keys; static const std::array<int, Settings::NativeKeyboard::NumKeyboardMods> default_keyboard_mods; - static const std::array<UISettings::Shortcut, 18> default_hotkeys; + static const std::array<UISettings::Shortcut, 21> default_hotkeys; private: void Initialize(const std::string& config_name); diff --git a/src/yuzu/configuration/configure_audio.cpp b/src/yuzu/configuration/configure_audio.cpp index 1d84bf4ed..f437cb53d 100644 --- a/src/yuzu/configuration/configure_audio.cpp +++ b/src/yuzu/configuration/configure_audio.cpp @@ -50,8 +50,6 @@ void ConfigureAudio::SetConfiguration() { const auto volume_value = static_cast<int>(Settings::values.volume.GetValue()); ui->volume_slider->setValue(volume_value); - ui->toggle_audio_stretching->setChecked(Settings::values.enable_audio_stretching.GetValue()); - if (!Settings::IsConfiguringGlobal()) { if (Settings::values.volume.UsingGlobal()) { ui->volume_combo_box->setCurrentIndex(0); @@ -100,8 +98,6 @@ void ConfigureAudio::SetVolumeIndicatorText(int percentage) { } void ConfigureAudio::ApplyConfiguration() { - ConfigurationShared::ApplyPerGameSetting(&Settings::values.enable_audio_stretching, - ui->toggle_audio_stretching, enable_audio_stretching); if (Settings::IsConfiguringGlobal()) { Settings::values.sink_id = @@ -162,15 +158,10 @@ void ConfigureAudio::RetranslateUI() { void ConfigureAudio::SetupPerGameUI() { if (Settings::IsConfiguringGlobal()) { ui->volume_slider->setEnabled(Settings::values.volume.UsingGlobal()); - ui->toggle_audio_stretching->setEnabled( - Settings::values.enable_audio_stretching.UsingGlobal()); return; } - ConfigurationShared::SetColoredTristate(ui->toggle_audio_stretching, - Settings::values.enable_audio_stretching, - enable_audio_stretching); connect(ui->volume_combo_box, qOverload<int>(&QComboBox::activated), this, [this](int index) { ui->volume_slider->setEnabled(index == 1); ConfigurationShared::SetHighlight(ui->volume_layout, index == 1); diff --git a/src/yuzu/configuration/configure_audio.h b/src/yuzu/configuration/configure_audio.h index 9dbd3d93e..5a01c8de7 100644 --- a/src/yuzu/configuration/configure_audio.h +++ b/src/yuzu/configuration/configure_audio.h @@ -41,6 +41,4 @@ private: void SetupPerGameUI(); std::unique_ptr<Ui::ConfigureAudio> ui; - - ConfigurationShared::CheckState enable_audio_stretching; }; diff --git a/src/yuzu/configuration/configure_audio.ui b/src/yuzu/configuration/configure_audio.ui index 9bd0cca96..bf736fc2c 100644 --- a/src/yuzu/configuration/configure_audio.ui +++ b/src/yuzu/configuration/configure_audio.ui @@ -32,16 +32,6 @@ </layout> </item> <item> - <widget class="QCheckBox" name="toggle_audio_stretching"> - <property name="toolTip"> - <string>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</string> - </property> - <property name="text"> - <string>Enable audio stretching</string> - </property> - </widget> - </item> - <item> <layout class="QHBoxLayout" name="_2"> <item> <widget class="QLabel" name="audio_device_label"> diff --git a/src/yuzu/configuration/configure_input_advanced.cpp b/src/yuzu/configuration/configure_input_advanced.cpp index 2f1419b5b..d20fd86b6 100644 --- a/src/yuzu/configuration/configure_input_advanced.cpp +++ b/src/yuzu/configuration/configure_input_advanced.cpp @@ -126,6 +126,7 @@ void ConfigureInputAdvanced::ApplyConfiguration() { Settings::values.mouse_panning_sensitivity = static_cast<float>(ui->mouse_panning_sensitivity->value()); Settings::values.touchscreen.enabled = ui->touchscreen_enabled->isChecked(); + Settings::values.enable_raw_input = ui->enable_raw_input->isChecked(); } void ConfigureInputAdvanced::LoadConfiguration() { @@ -155,6 +156,7 @@ void ConfigureInputAdvanced::LoadConfiguration() { ui->mouse_panning->setChecked(Settings::values.mouse_panning.GetValue()); ui->mouse_panning_sensitivity->setValue(Settings::values.mouse_panning_sensitivity.GetValue()); ui->touchscreen_enabled->setChecked(Settings::values.touchscreen.enabled); + ui->enable_raw_input->setChecked(Settings::values.enable_raw_input.GetValue()); UpdateUIEnabled(); } diff --git a/src/yuzu/configuration/configure_input_advanced.ui b/src/yuzu/configuration/configure_input_advanced.ui index d3ef5bd06..9095206a0 100644 --- a/src/yuzu/configuration/configure_input_advanced.ui +++ b/src/yuzu/configuration/configure_input_advanced.ui @@ -2672,6 +2672,22 @@ </property> </widget> </item> + <item row="9" column="0"> + <widget class="QCheckBox" name="enable_raw_input"> + <property name="toolTip"> + <string>Requires restarting yuzu</string> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>23</height> + </size> + </property> + <property name="text"> + <string>Enable XInput 8 player support (disables web applet)</string> + </property> + </widget> + </item> </layout> </widget> </item> diff --git a/src/yuzu/configuration/configure_input_player.cpp b/src/yuzu/configuration/configure_input_player.cpp index 7527c068b..88f4bf388 100644 --- a/src/yuzu/configuration/configure_input_player.cpp +++ b/src/yuzu/configuration/configure_input_player.cpp @@ -124,6 +124,19 @@ QString ButtonToText(const Common::ParamPackage& param) { return GetKeyName(param.Get("code", 0)); } + if (param.Get("engine", "") == "tas") { + if (param.Has("axis")) { + const QString axis_str = QString::fromStdString(param.Get("axis", "")); + + return QObject::tr("TAS Axis %1").arg(axis_str); + } + if (param.Has("button")) { + const QString button_str = QString::number(int(std::log2(param.Get("button", 0)))); + return QObject::tr("TAS Btn %1").arg(button_str); + } + return GetKeyName(param.Get("code", 0)); + } + if (param.Get("engine", "") == "cemuhookudp") { if (param.Has("pad_index")) { const QString motion_str = QString::fromStdString(param.Get("pad_index", "")); @@ -187,7 +200,8 @@ QString AnalogToText(const Common::ParamPackage& param, const std::string& dir) const QString axis_y_str = QString::fromStdString(param.Get("axis_y", "")); const bool invert_x = param.Get("invert_x", "+") == "-"; const bool invert_y = param.Get("invert_y", "+") == "-"; - if (engine_str == "sdl" || engine_str == "gcpad" || engine_str == "mouse") { + if (engine_str == "sdl" || engine_str == "gcpad" || engine_str == "mouse" || + engine_str == "tas") { if (dir == "modifier") { return QObject::tr("[unused]"); } @@ -926,9 +940,9 @@ void ConfigureInputPlayer::UpdateUI() { int slider_value; auto& param = analogs_param[analog_id]; - const bool is_controller = param.Get("engine", "") == "sdl" || - param.Get("engine", "") == "gcpad" || - param.Get("engine", "") == "mouse"; + const bool is_controller = + param.Get("engine", "") == "sdl" || param.Get("engine", "") == "gcpad" || + param.Get("engine", "") == "mouse" || param.Get("engine", "") == "tas"; if (is_controller) { if (!param.Has("deadzone")) { @@ -1045,8 +1059,12 @@ int ConfigureInputPlayer::GetIndexFromControllerType(Settings::ControllerType ty void ConfigureInputPlayer::UpdateInputDevices() { input_devices = input_subsystem->GetInputDevices(); ui->comboDevices->clear(); - for (auto device : input_devices) { - ui->comboDevices->addItem(QString::fromStdString(device.Get("display", "Unknown")), {}); + for (auto& device : input_devices) { + const std::string display = device.Get("display", "Unknown"); + ui->comboDevices->addItem(QString::fromStdString(display), {}); + if (display == "TAS") { + device.Set("pad", static_cast<u8>(player_index)); + } } } diff --git a/src/yuzu/configuration/configure_input_player_widget.cpp b/src/yuzu/configuration/configure_input_player_widget.cpp index 9c890ed5d..da328d904 100644 --- a/src/yuzu/configuration/configure_input_player_widget.cpp +++ b/src/yuzu/configuration/configure_input_player_widget.cpp @@ -175,7 +175,7 @@ void PlayerControlPreview::ResetInputs() { } void PlayerControlPreview::UpdateInput() { - if (!is_enabled && !mapping_active) { + if (!is_enabled && !mapping_active && !Settings::values.tas_enable) { return; } bool input_changed = false; @@ -222,6 +222,19 @@ void PlayerControlPreview::UpdateInput() { if (input_changed) { update(); + if (controller_callback.input != nullptr) { + ControllerInput input{ + .axis_values = {std::pair<float, float>{ + axis_values[Settings::NativeAnalog::LStick].value.x(), + axis_values[Settings::NativeAnalog::LStick].value.y()}, + std::pair<float, float>{ + axis_values[Settings::NativeAnalog::RStick].value.x(), + axis_values[Settings::NativeAnalog::RStick].value.y()}}, + .button_values = button_values, + .changed = true, + }; + controller_callback.input(std::move(input)); + } } if (mapping_active) { @@ -229,6 +242,10 @@ void PlayerControlPreview::UpdateInput() { } } +void PlayerControlPreview::SetCallBack(ControllerCallback callback_) { + controller_callback = std::move(callback_); +} + void PlayerControlPreview::paintEvent(QPaintEvent* event) { QFrame::paintEvent(event); QPainter p(this); diff --git a/src/yuzu/configuration/configure_input_player_widget.h b/src/yuzu/configuration/configure_input_player_widget.h index f4a6a5e1b..f4bbfa528 100644 --- a/src/yuzu/configuration/configure_input_player_widget.h +++ b/src/yuzu/configuration/configure_input_player_widget.h @@ -9,6 +9,7 @@ #include <QPointer> #include "common/settings.h" #include "core/frontend/input.h" +#include "yuzu/debugger/controller.h" class QLabel; @@ -33,6 +34,7 @@ public: void BeginMappingAnalog(std::size_t button_id); void EndMapping(); void UpdateInput(); + void SetCallBack(ControllerCallback callback_); protected: void paintEvent(QPaintEvent* event) override; @@ -181,6 +183,7 @@ private: using StickArray = std::array<std::unique_ptr<Input::AnalogDevice>, Settings::NativeAnalog::NUM_STICKS_HID>; + ControllerCallback controller_callback; bool is_enabled{}; bool mapping_active{}; int blink_counter{}; diff --git a/src/yuzu/configuration/configure_tas.cpp b/src/yuzu/configuration/configure_tas.cpp new file mode 100644 index 000000000..b666b175a --- /dev/null +++ b/src/yuzu/configuration/configure_tas.cpp @@ -0,0 +1,84 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <QFileDialog> +#include <QMessageBox> +#include "common/fs/fs.h" +#include "common/fs/path_util.h" +#include "common/settings.h" +#include "ui_configure_tas.h" +#include "yuzu/configuration/configure_tas.h" +#include "yuzu/uisettings.h" + +ConfigureTasDialog::ConfigureTasDialog(QWidget* parent) + : QDialog(parent), ui(std::make_unique<Ui::ConfigureTas>()) { + + ui->setupUi(this); + + setFocusPolicy(Qt::ClickFocus); + setWindowTitle(tr("TAS Configuration")); + + connect(ui->tas_path_button, &QToolButton::pressed, this, + [this] { SetDirectory(DirectoryTarget::TAS, ui->tas_path_edit); }); + + LoadConfiguration(); +} + +ConfigureTasDialog::~ConfigureTasDialog() = default; + +void ConfigureTasDialog::LoadConfiguration() { + ui->tas_path_edit->setText( + QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASDir))); + ui->tas_enable->setChecked(Settings::values.tas_enable.GetValue()); + ui->tas_control_swap->setChecked(Settings::values.tas_swap_controllers.GetValue()); + ui->tas_loop_script->setChecked(Settings::values.tas_loop.GetValue()); + ui->tas_pause_on_load->setChecked(Settings::values.pause_tas_on_load.GetValue()); +} + +void ConfigureTasDialog::ApplyConfiguration() { + Common::FS::SetYuzuPath(Common::FS::YuzuPath::TASDir, ui->tas_path_edit->text().toStdString()); + Settings::values.tas_enable.SetValue(ui->tas_enable->isChecked()); + Settings::values.tas_swap_controllers.SetValue(ui->tas_control_swap->isChecked()); + Settings::values.tas_loop.SetValue(ui->tas_loop_script->isChecked()); + Settings::values.pause_tas_on_load.SetValue(ui->tas_pause_on_load->isChecked()); +} + +void ConfigureTasDialog::SetDirectory(DirectoryTarget target, QLineEdit* edit) { + QString caption; + + switch (target) { + case DirectoryTarget::TAS: + caption = tr("Select TAS Load Directory..."); + break; + } + + QString str = QFileDialog::getExistingDirectory(this, caption, edit->text()); + + if (str.isEmpty()) { + return; + } + + if (str.back() != QChar::fromLatin1('/')) { + str.append(QChar::fromLatin1('/')); + } + + edit->setText(str); +} + +void ConfigureTasDialog::changeEvent(QEvent* event) { + if (event->type() == QEvent::LanguageChange) { + RetranslateUI(); + } + + QDialog::changeEvent(event); +} + +void ConfigureTasDialog::RetranslateUI() { + ui->retranslateUi(this); +} + +void ConfigureTasDialog::HandleApplyButtonClicked() { + UISettings::values.configuration_applied = true; + ApplyConfiguration(); +} diff --git a/src/yuzu/configuration/configure_tas.h b/src/yuzu/configuration/configure_tas.h new file mode 100644 index 000000000..1546bf16f --- /dev/null +++ b/src/yuzu/configuration/configure_tas.h @@ -0,0 +1,38 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <QDialog> + +namespace Ui { +class ConfigureTas; +} + +class ConfigureTasDialog : public QDialog { + Q_OBJECT + +public: + explicit ConfigureTasDialog(QWidget* parent); + ~ConfigureTasDialog() override; + + /// Save all button configurations to settings file + void ApplyConfiguration(); + +private: + enum class DirectoryTarget { + TAS, + }; + + void LoadConfiguration(); + + void SetDirectory(DirectoryTarget target, QLineEdit* edit); + + void changeEvent(QEvent* event) override; + void RetranslateUI(); + + void HandleApplyButtonClicked(); + + std::unique_ptr<Ui::ConfigureTas> ui; +}; diff --git a/src/yuzu/configuration/configure_tas.ui b/src/yuzu/configuration/configure_tas.ui new file mode 100644 index 000000000..8a3ecb834 --- /dev/null +++ b/src/yuzu/configuration/configure_tas.ui @@ -0,0 +1,183 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ConfigureTas</class> + <widget class="QDialog" name="ConfigureTas"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_1"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>TAS</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="1"> + <widget class="QLabel" name="label_1"> + <property name="text"> + <string>Reads controller input from scripts in the same format as TAS-nx scripts. For a more detailed explanation please consult the FAQ on the yuzu website.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0" colspan="1"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>To check which hotkeys control the playback/recording, please refer to the Hotkey settings (General -> Hotkeys).</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0" colspan="1"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>WARNING: This is an experimental feature. It will not play back scripts frame perfectly with the current, imperfect syncing method.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Settings</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="4"> + <widget class="QCheckBox" name="tas_enable"> + <property name="text"> + <string>Enable TAS features</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="4"> + <widget class="QCheckBox" name="tas_control_swap"> + <property name="text"> + <string>Automatic controller profile swapping</string> + </property> + </widget> + </item> + <item row="2" column="0" colspan="4"> + <widget class="QCheckBox" name="tas_loop_script"> + <property name="text"> + <string>Loop script</string> + </property> + </widget> + </item> + <item row="3" column="0" colspan="4"> + <widget class="QCheckBox" name="tas_pause_on_load"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Pause execution during loads</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Script Directory</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Path</string> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QToolButton" name="tas_path_button"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLineEdit" name="tas_path_edit"/> + </item> + <item row="0" column="1"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>60</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ConfigureTas</receiver> + <slot>accept()</slot> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ConfigureTas</receiver> + <slot>reject()</slot> + </connection> + </connections> +</ui> diff --git a/src/yuzu/configuration/configure_vibration.cpp b/src/yuzu/configuration/configure_vibration.cpp index 9d92c4949..46a0f3025 100644 --- a/src/yuzu/configuration/configure_vibration.cpp +++ b/src/yuzu/configuration/configure_vibration.cpp @@ -99,7 +99,7 @@ void ConfigureVibration::SetVibrationDevices(std::size_t player_index) { const auto guid = param.Get("guid", ""); const auto port = param.Get("port", ""); - if (engine.empty() || engine == "keyboard" || engine == "mouse") { + if (engine.empty() || engine == "keyboard" || engine == "mouse" || engine == "tas") { continue; } diff --git a/src/yuzu/debugger/controller.cpp b/src/yuzu/debugger/controller.cpp index c1fc69578..5a844409b 100644 --- a/src/yuzu/debugger/controller.cpp +++ b/src/yuzu/debugger/controller.cpp @@ -6,10 +6,13 @@ #include <QLayout> #include <QString> #include "common/settings.h" +#include "input_common/main.h" +#include "input_common/tas/tas_input.h" #include "yuzu/configuration/configure_input_player_widget.h" #include "yuzu/debugger/controller.h" -ControllerDialog::ControllerDialog(QWidget* parent) : QWidget(parent, Qt::Dialog) { +ControllerDialog::ControllerDialog(QWidget* parent, InputCommon::InputSubsystem* input_subsystem_) + : QWidget(parent, Qt::Dialog), input_subsystem{input_subsystem_} { setObjectName(QStringLiteral("Controller")); setWindowTitle(tr("Controller P1")); resize(500, 350); @@ -38,6 +41,9 @@ void ControllerDialog::refreshConfiguration() { constexpr std::size_t player = 0; widget->SetPlayerInputRaw(player, players[player].buttons, players[player].analogs); widget->SetControllerType(players[player].controller_type); + ControllerCallback callback{[this](ControllerInput input) { InputController(input); }}; + widget->SetCallBack(callback); + widget->repaint(); widget->SetConnectedStatus(players[player].connected); } @@ -67,3 +73,13 @@ void ControllerDialog::hideEvent(QHideEvent* ev) { widget->SetConnectedStatus(false); QWidget::hideEvent(ev); } + +void ControllerDialog::InputController(ControllerInput input) { + u32 buttons = 0; + int index = 0; + for (bool btn : input.button_values) { + buttons |= (btn ? 1U : 0U) << index; + index++; + } + input_subsystem->GetTas()->RecordInput(buttons, input.axis_values); +} diff --git a/src/yuzu/debugger/controller.h b/src/yuzu/debugger/controller.h index c54750070..7742db58b 100644 --- a/src/yuzu/debugger/controller.h +++ b/src/yuzu/debugger/controller.h @@ -4,18 +4,35 @@ #pragma once +#include <QFileSystemWatcher> #include <QWidget> +#include "common/settings.h" class QAction; class QHideEvent; class QShowEvent; class PlayerControlPreview; +namespace InputCommon { +class InputSubsystem; +} + +struct ControllerInput { + std::array<std::pair<float, float>, Settings::NativeAnalog::NUM_STICKS_HID> axis_values{}; + std::array<bool, Settings::NativeButton::NumButtons> button_values{}; + bool changed{}; +}; + +struct ControllerCallback { + std::function<void(ControllerInput)> input; +}; + class ControllerDialog : public QWidget { Q_OBJECT public: - explicit ControllerDialog(QWidget* parent = nullptr); + explicit ControllerDialog(QWidget* parent = nullptr, + InputCommon::InputSubsystem* input_subsystem_ = nullptr); /// Returns a QAction that can be used to toggle visibility of this dialog. QAction* toggleViewAction(); @@ -26,6 +43,9 @@ protected: void hideEvent(QHideEvent* ev) override; private: + void InputController(ControllerInput input); QAction* toggle_view_action = nullptr; + QFileSystemWatcher* watcher = nullptr; PlayerControlPreview* widget; + InputCommon::InputSubsystem* input_subsystem; }; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 77d53e7bc..3c2824362 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -19,6 +19,7 @@ #include "common/nvidia_flags.h" #include "configuration/configure_input.h" #include "configuration/configure_per_game.h" +#include "configuration/configure_tas.h" #include "configuration/configure_vibration.h" #include "core/file_sys/vfs.h" #include "core/file_sys/vfs_real.h" @@ -102,6 +103,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "core/perf_stats.h" #include "core/telemetry_session.h" #include "input_common/main.h" +#include "input_common/tas/tas_input.h" #include "util/overlay_dialog.h" #include "video_core/gpu.h" #include "video_core/renderer_base.h" @@ -557,7 +559,8 @@ void GMainWindow::WebBrowserOpenWebPage(const std::string& main_url, const std::string& additional_args, bool is_local) { #ifdef YUZU_USE_QT_WEB_ENGINE - if (disable_web_applet) { + // Raw input breaks with the web applet, Disable web applets if enabled + if (disable_web_applet || Settings::values.enable_raw_input) { emit WebBrowserClosed(Service::AM::Applets::WebExitReason::WindowClosed, "http://localhost/"); return; @@ -746,6 +749,11 @@ void GMainWindow::InitializeWidgets() { statusBar()->addPermanentWidget(label); } + tas_label = new QLabel(); + tas_label->setObjectName(QStringLiteral("TASlabel")); + tas_label->setFocusPolicy(Qt::NoFocus); + statusBar()->insertPermanentWidget(0, tas_label); + // Setup Dock button dock_status_button = new QPushButton(); dock_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); @@ -840,7 +848,7 @@ void GMainWindow::InitializeDebugWidgets() { waitTreeWidget->hide(); debug_menu->addAction(waitTreeWidget->toggleViewAction()); - controller_dialog = new ControllerDialog(this); + controller_dialog = new ControllerDialog(this, input_subsystem.get()); controller_dialog->hide(); debug_menu->addAction(controller_dialog->toggleViewAction()); @@ -1013,6 +1021,28 @@ void GMainWindow::InitializeHotkeys() { render_window->setAttribute(Qt::WA_Hover, true); } }); + connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Start/Stop"), this), + &QShortcut::activated, this, [&] { + if (!emulation_running) { + return; + } + input_subsystem->GetTas()->StartStop(); + }); + connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Reset"), this), + &QShortcut::activated, this, [&] { input_subsystem->GetTas()->Reset(); }); + connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Record"), this), + &QShortcut::activated, this, [&] { + if (!emulation_running) { + return; + } + bool is_recording = input_subsystem->GetTas()->Record(); + if (!is_recording) { + const auto res = QMessageBox::question(this, tr("TAS Recording"), + tr("Overwrite file of player 1?"), + QMessageBox::Yes | QMessageBox::No); + input_subsystem->GetTas()->SaveRecording(res == QMessageBox::Yes); + } + }); } void GMainWindow::SetDefaultUIGeometry() { @@ -1131,6 +1161,7 @@ void GMainWindow::ConnectMenuEvents() { connect(ui.action_Open_FAQ, &QAction::triggered, this, &GMainWindow::OnOpenFAQ); connect(ui.action_Restart, &QAction::triggered, this, [this] { BootGame(QString(game_path)); }); connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure); + connect(ui.action_Configure_Tas, &QAction::triggered, this, &GMainWindow::OnConfigureTas); connect(ui.action_Configure_Current_Game, &QAction::triggered, this, &GMainWindow::OnConfigurePerGame); @@ -1463,6 +1494,8 @@ void GMainWindow::ShutdownGame() { game_list->show(); } game_list->SetFilterFocus(); + tas_label->clear(); + input_subsystem->GetTas()->Stop(); render_window->removeEventFilter(render_window); render_window->setAttribute(Qt::WA_Hover, false); @@ -2697,6 +2730,19 @@ void GMainWindow::OnConfigure() { UpdateStatusButtons(); } +void GMainWindow::OnConfigureTas() { + const auto& system = Core::System::GetInstance(); + ConfigureTasDialog dialog(this); + const auto result = dialog.exec(); + + if (result != QDialog::Accepted && !UISettings::values.configuration_applied) { + Settings::RestoreGlobalState(system.IsPoweredOn()); + return; + } else if (result == QDialog::Accepted) { + dialog.ApplyConfiguration(); + } +} + void GMainWindow::OnConfigurePerGame() { const u64 title_id = Core::System::GetInstance().CurrentProcess()->GetTitleID(); OpenPerGameConfiguration(title_id, game_path.toStdString()); @@ -2873,12 +2919,32 @@ void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_vie } } +QString GMainWindow::GetTasStateDescription() const { + auto [tas_status, current_tas_frame, total_tas_frames] = input_subsystem->GetTas()->GetStatus(); + switch (tas_status) { + case TasInput::TasState::Running: + return tr("TAS state: Running %1/%2").arg(current_tas_frame).arg(total_tas_frames); + case TasInput::TasState::Recording: + return tr("TAS state: Recording %1").arg(total_tas_frames); + case TasInput::TasState::Stopped: + return tr("TAS state: Idle %1/%2").arg(current_tas_frame).arg(total_tas_frames); + default: + return tr("TAS State: Invalid"); + } +} + void GMainWindow::UpdateStatusBar() { if (emu_thread == nullptr) { status_bar_update_timer.stop(); return; } + if (Settings::values.tas_enable) { + tas_label->setText(GetTasStateDescription()); + } else { + tas_label->clear(); + } + auto& system = Core::System::GetInstance(); auto results = system.GetAndResetPerfStats(); auto& shader_notify = system.GPU().ShaderNotify(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 38e66ccd0..36eed6103 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -259,6 +259,7 @@ private slots: void OnMenuInstallToNAND(); void OnMenuRecentFile(); void OnConfigure(); + void OnConfigureTas(); void OnConfigurePerGame(); void OnLoadAmiibo(); void OnOpenYuzuFolder(); @@ -300,6 +301,7 @@ private: void OpenURL(const QUrl& url); void LoadTranslation(); void OpenPerGameConfiguration(u64 title_id, const std::string& file_name); + QString GetTasStateDescription() const; Ui::MainWindow ui; @@ -318,6 +320,7 @@ private: QLabel* emu_speed_label = nullptr; QLabel* game_fps_label = nullptr; QLabel* emu_frametime_label = nullptr; + QLabel* tas_label = nullptr; QPushButton* gpu_accuracy_button = nullptr; QPushButton* renderer_status_button = nullptr; QPushButton* dock_status_button = nullptr; diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 048870687..653c010d8 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -100,6 +100,7 @@ <addaction name="action_Rederive"/> <addaction name="separator"/> <addaction name="action_Capture_Screenshot"/> + <addaction name="action_Configure_Tas"/> </widget> <widget class="QMenu" name="menu_Help"> <property name="title"> @@ -294,6 +295,11 @@ <string>&Capture Screenshot</string> </property> </action> + <action name="action_Configure_Tas"> + <property name="text"> + <string>Configure &TAS...</string> + </property> + </action> <action name="action_Configure_Current_Game"> <property name="enabled"> <bool>false</bool> diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp index 891f7be6f..d74eb7e2b 100644 --- a/src/yuzu_cmd/config.cpp +++ b/src/yuzu_cmd/config.cpp @@ -475,7 +475,6 @@ void Config::ReadValues() { // Audio ReadSetting("Audio", Settings::values.sink_id); - ReadSetting("Audio", Settings::values.enable_audio_stretching); ReadSetting("Audio", Settings::values.audio_device_id); ReadSetting("Audio", Settings::values.volume); |