early-access version 1916
This commit is contained in:
		| @@ -1,7 +1,7 @@ | ||||
| yuzu emulator early access | ||||
| ============= | ||||
|  | ||||
| This is the source code for early-access 1915. | ||||
| This is the source code for early-access 1916. | ||||
|  | ||||
| ## Legal Notice | ||||
|  | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								dist/yuzu.ico
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								dist/yuzu.ico
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB | 
							
								
								
									
										2
									
								
								dist/yuzu.svg
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/yuzu.svg
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1 @@ | ||||
| <svg id="svg815" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 614.4 682.67"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#ff3c28;}.cls-4{fill:#0ab9e6;}</style><clipPath id="clip-path"><rect class="cls-1" x="-43" y="-46.67" width="699.6" height="777.33"/></clipPath></defs><title>Artboard 1</title><g id="g823"><g id="right"><g class="cls-2"><g id="g827"><g id="g833"><path id="path835" class="cls-3" d="M340.81,138V682.08c150.26,0,272.06-121.81,272.06-272.06S491.07,138,340.81,138M394,197.55a219.06,219.06,0,0,1,0,424.94V197.55"/></g></g></g></g><g id="left"><g class="cls-2"><g id="g839"><g id="g845"><path id="path847" class="cls-4" d="M272.79,1.92C122.53,1.92.73,123.73.73,274s121.8,272.07,272.06,272.07ZM219.65,61.51v425A219,219,0,0,1,118,119.18,217.51,217.51,0,0,1,219.65,61.51"/></g></g></g></g></g></svg> | ||||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612.15 680.17"><defs><style>.cls-1{fill:#c6c6c6;}.cls-2{fill:#ffdc00;}</style></defs><title>newAsset 7</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="g823"><g id="right"><g id="g827"><g id="g833"><path id="path835" class="cls-1" d="M340.08,136V680.17c150.26,0,272.07-121.81,272.07-272.07S490.34,136,340.08,136m53.14,59.6a219.06,219.06,0,0,1,0,424.94V195.63"/></g></g></g><g id="left"><g id="g839"><g id="g845"><path id="path847" class="cls-2" d="M272.07,0C121.81,0,0,121.81,0,272.07S121.81,544.13,272.07,544.13ZM218.93,59.6V484.54A219,219,0,0,1,117.26,117.26,217.44,217.44,0,0,1,218.93,59.6"/></g></g></g></g></g></g></svg> | ||||
| Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 717 B | 
| @@ -21,6 +21,7 @@ | ||||
| #define SCREENSHOTS_DIR "screenshots" | ||||
| #define SDMC_DIR "sdmc" | ||||
| #define SHADER_DIR "shader" | ||||
| #define TAS_DIR "tas" | ||||
|  | ||||
| // yuzu-specific files | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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. | ||||
| }; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -339,6 +339,7 @@ struct Values { | ||||
|     Setting<bool> use_nvdec_emulation{true, "use_nvdec_emulation"}; | ||||
|     Setting<bool> accelerate_astc{true, "accelerate_astc"}; | ||||
|     Setting<bool> use_vsync{true, "use_vsync"}; | ||||
|     BasicSetting<u16> fps_cap{1000, "fps_cap"}; | ||||
|     BasicSetting<bool> disable_fps_limit{false, "disable_fps_limit"}; | ||||
|     Setting<ShaderBackend> shader_backend{ShaderBackend::GLASM, "shader_backend"}; | ||||
|     Setting<bool> use_asynchronous_shaders{false, "use_asynchronous_shaders"}; | ||||
| @@ -376,6 +377,11 @@ struct Values { | ||||
|     BasicSetting<std::string> udp_input_servers{InputCommon::CemuhookUDP::DEFAULT_SRV, | ||||
|                                                 "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"}; | ||||
|     BasicSetting<u8> mouse_panning_sensitivity{10, "mouse_panning_sensitivity"}; | ||||
|     BasicSetting<bool> mouse_enabled{false, "mouse_enabled"}; | ||||
|   | ||||
| @@ -68,6 +68,7 @@ void nvhost_nvdec::OnOpen(DeviceFD fd) {} | ||||
| void nvhost_nvdec::OnClose(DeviceFD fd) { | ||||
|     LOG_INFO(Service_NVDRV, "NVDEC video stream ended"); | ||||
|     system.GPU().ClearCdmaInstance(); | ||||
|     system.GPU().MemoryManager().InvalidateQueuedCaches(); | ||||
| } | ||||
|  | ||||
| } // namespace Service::Nvidia::Devices | ||||
|   | ||||
| @@ -193,7 +193,13 @@ NvResult nvhost_nvdec_common::UnmapBuffer(const std::vector<u8>& input, std::vec | ||||
|             return NvResult::InvalidState; | ||||
|         } | ||||
|         if (const auto size{RemoveBufferMap(object->dma_map_addr)}; size) { | ||||
|             gpu.MemoryManager().Unmap(object->dma_map_addr, *size); | ||||
|             if (vic_device) { | ||||
|                 // UnmapVicFrame defers texture_cache invalidation of the frame address until | ||||
|                 // the stream is over | ||||
|                 gpu.MemoryManager().UnmapVicFrame(object->dma_map_addr, *size); | ||||
|             } else { | ||||
|                 gpu.MemoryManager().Unmap(object->dma_map_addr, *size); | ||||
|             } | ||||
|         } else { | ||||
|             // This occurs quite frequently, however does not seem to impact functionality | ||||
|             LOG_DEBUG(Service_NVDRV, "invalid offset=0x{:X} dma=0x{:X}", object->addr, | ||||
|   | ||||
| @@ -160,6 +160,7 @@ protected: | ||||
|  | ||||
|     s32_le nvmap_fd{}; | ||||
|     u32_le submit_timeout{}; | ||||
|     bool vic_device{}; | ||||
|     std::shared_ptr<nvmap> nvmap_dev; | ||||
|     SyncpointManager& syncpoint_manager; | ||||
|     std::array<u32, MaxSyncPoints> device_syncpoints{}; | ||||
|   | ||||
| @@ -12,8 +12,9 @@ | ||||
| namespace Service::Nvidia::Devices { | ||||
| nvhost_vic::nvhost_vic(Core::System& system_, std::shared_ptr<nvmap> nvmap_dev_, | ||||
|                        SyncpointManager& syncpoint_manager_) | ||||
|     : nvhost_nvdec_common{system_, std::move(nvmap_dev_), syncpoint_manager_} {} | ||||
|  | ||||
|     : nvhost_nvdec_common(system_, std::move(nvmap_dev_), syncpoint_manager_) { | ||||
|     vic_device = true; | ||||
| } | ||||
| nvhost_vic::~nvhost_vic() = default; | ||||
|  | ||||
| NvResult nvhost_vic::Ioctl1(DeviceFD fd, Ioctl command, const std::vector<u8>& input, | ||||
|   | ||||
| @@ -307,11 +307,12 @@ void NVFlinger::Compose() { | ||||
| } | ||||
|  | ||||
| s64 NVFlinger::GetNextTicks() const { | ||||
|     if (Settings::values.disable_fps_limit.GetValue()) { | ||||
|         return 0; | ||||
|     } | ||||
|     constexpr s64 max_hertz = 120LL; | ||||
|     return (1000000000 * (1LL << swap_interval)) / max_hertz; | ||||
|     static constexpr s64 max_hertz = 120LL; | ||||
|  | ||||
|     const auto& settings = Settings::values; | ||||
|     const bool unlocked_fps = settings.disable_fps_limit.GetValue(); | ||||
|     const s64 fps_cap = unlocked_fps ? static_cast<s64>(settings.fps_cap.GetValue()) : 1; | ||||
|     return (1000000000 * (1LL << swap_interval)) / (max_hertz * fps_cap); | ||||
| } | ||||
|  | ||||
| } // namespace Service::NVFlinger | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -14,15 +14,73 @@ | ||||
|  | ||||
| namespace GCAdapter { | ||||
|  | ||||
| class LibUSBContext { | ||||
| public: | ||||
|     explicit LibUSBContext() { | ||||
|         init_result = libusb_init(&ctx); | ||||
|     } | ||||
|  | ||||
|     ~LibUSBContext() { | ||||
|         libusb_exit(ctx); | ||||
|     } | ||||
|  | ||||
|     LibUSBContext& operator=(const LibUSBContext&) = delete; | ||||
|     LibUSBContext(const LibUSBContext&) = delete; | ||||
|  | ||||
|     LibUSBContext& operator=(LibUSBContext&&) noexcept = delete; | ||||
|     LibUSBContext(LibUSBContext&&) noexcept = delete; | ||||
|  | ||||
|     [[nodiscard]] int InitResult() const noexcept { | ||||
|         return init_result; | ||||
|     } | ||||
|  | ||||
|     [[nodiscard]] libusb_context* get() noexcept { | ||||
|         return ctx; | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     libusb_context* ctx; | ||||
|     int init_result{}; | ||||
| }; | ||||
|  | ||||
| class LibUSBDeviceHandle { | ||||
| public: | ||||
|     explicit LibUSBDeviceHandle(libusb_context* ctx, uint16_t vid, uint16_t pid) noexcept { | ||||
|         handle = libusb_open_device_with_vid_pid(ctx, vid, pid); | ||||
|     } | ||||
|  | ||||
|     ~LibUSBDeviceHandle() noexcept { | ||||
|         if (handle) { | ||||
|             libusb_release_interface(handle, 1); | ||||
|             libusb_close(handle); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     LibUSBDeviceHandle& operator=(const LibUSBDeviceHandle&) = delete; | ||||
|     LibUSBDeviceHandle(const LibUSBDeviceHandle&) = delete; | ||||
|  | ||||
|     LibUSBDeviceHandle& operator=(LibUSBDeviceHandle&&) noexcept = delete; | ||||
|     LibUSBDeviceHandle(LibUSBDeviceHandle&&) noexcept = delete; | ||||
|  | ||||
|     [[nodiscard]] libusb_device_handle* get() noexcept { | ||||
|         return handle; | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     libusb_device_handle* handle{}; | ||||
| }; | ||||
|  | ||||
| Adapter::Adapter() { | ||||
|     if (usb_adapter_handle != nullptr) { | ||||
|     if (usb_adapter_handle) { | ||||
|         return; | ||||
|     } | ||||
|     LOG_INFO(Input, "GC Adapter Initialization started"); | ||||
|  | ||||
|     const int init_res = libusb_init(&libusb_ctx); | ||||
|     libusb_ctx = std::make_unique<LibUSBContext>(); | ||||
|     const int init_res = libusb_ctx->InitResult(); | ||||
|     if (init_res == LIBUSB_SUCCESS) { | ||||
|         adapter_scan_thread = std::thread(&Adapter::AdapterScanThread, this); | ||||
|         adapter_scan_thread = | ||||
|             std::jthread([this](std::stop_token stop_token) { AdapterScanThread(stop_token); }); | ||||
|     } else { | ||||
|         LOG_ERROR(Input, "libusb could not be initialized. failed with error = {}", init_res); | ||||
|     } | ||||
| @@ -32,17 +90,15 @@ Adapter::~Adapter() { | ||||
|     Reset(); | ||||
| } | ||||
|  | ||||
| void Adapter::AdapterInputThread() { | ||||
| void Adapter::AdapterInputThread(std::stop_token stop_token) { | ||||
|     LOG_DEBUG(Input, "GC Adapter input thread started"); | ||||
|     s32 payload_size{}; | ||||
|     AdapterPayload adapter_payload{}; | ||||
|  | ||||
|     if (adapter_scan_thread.joinable()) { | ||||
|         adapter_scan_thread.join(); | ||||
|     } | ||||
|     adapter_scan_thread = {}; | ||||
|  | ||||
|     while (adapter_input_thread_running) { | ||||
|         libusb_interrupt_transfer(usb_adapter_handle, input_endpoint, adapter_payload.data(), | ||||
|     while (!stop_token.stop_requested()) { | ||||
|         libusb_interrupt_transfer(usb_adapter_handle->get(), input_endpoint, adapter_payload.data(), | ||||
|                                   static_cast<s32>(adapter_payload.size()), &payload_size, 16); | ||||
|         if (IsPayloadCorrect(adapter_payload, payload_size)) { | ||||
|             UpdateControllers(adapter_payload); | ||||
| @@ -52,7 +108,8 @@ void Adapter::AdapterInputThread() { | ||||
|     } | ||||
|  | ||||
|     if (restart_scan_thread) { | ||||
|         adapter_scan_thread = std::thread(&Adapter::AdapterScanThread, this); | ||||
|         adapter_scan_thread = | ||||
|             std::jthread([this](std::stop_token token) { AdapterScanThread(token); }); | ||||
|         restart_scan_thread = false; | ||||
|     } | ||||
| } | ||||
| @@ -64,7 +121,7 @@ bool Adapter::IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payloa | ||||
|                   adapter_payload[0]); | ||||
|         if (input_error_counter++ > 20) { | ||||
|             LOG_ERROR(Input, "GC adapter timeout, Is the adapter connected?"); | ||||
|             adapter_input_thread_running = false; | ||||
|             adapter_input_thread.request_stop(); | ||||
|             restart_scan_thread = true; | ||||
|         } | ||||
|         return false; | ||||
| @@ -96,7 +153,7 @@ void Adapter::UpdatePadType(std::size_t port, ControllerTypes pad_type) { | ||||
|         return; | ||||
|     } | ||||
|     // Device changed reset device and set new type | ||||
|     ResetDevice(port); | ||||
|     pads[port] = {}; | ||||
|     pads[port].type = pad_type; | ||||
| } | ||||
|  | ||||
| @@ -213,8 +270,9 @@ void Adapter::SendVibrations() { | ||||
|     const u8 p3 = pads[2].enable_vibration; | ||||
|     const u8 p4 = pads[3].enable_vibration; | ||||
|     std::array<u8, 5> payload = {rumble_command, p1, p2, p3, p4}; | ||||
|     const int err = libusb_interrupt_transfer(usb_adapter_handle, output_endpoint, payload.data(), | ||||
|                                               static_cast<s32>(payload.size()), &size, 16); | ||||
|     const int err = | ||||
|         libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, payload.data(), | ||||
|                                   static_cast<s32>(payload.size()), &size, 16); | ||||
|     if (err) { | ||||
|         LOG_DEBUG(Input, "Adapter libusb write failed: {}", libusb_error_name(err)); | ||||
|         if (output_error_counter++ > 5) { | ||||
| @@ -233,56 +291,53 @@ bool Adapter::RumblePlay(std::size_t port, u8 amplitude) { | ||||
|     return rumble_enabled; | ||||
| } | ||||
|  | ||||
| void Adapter::AdapterScanThread() { | ||||
|     adapter_scan_thread_running = true; | ||||
|     adapter_input_thread_running = false; | ||||
|     if (adapter_input_thread.joinable()) { | ||||
|         adapter_input_thread.join(); | ||||
|     } | ||||
|     ClearLibusbHandle(); | ||||
|     ResetDevices(); | ||||
|     while (adapter_scan_thread_running && !adapter_input_thread_running) { | ||||
|         Setup(); | ||||
|         std::this_thread::sleep_for(std::chrono::seconds(1)); | ||||
| void Adapter::AdapterScanThread(std::stop_token stop_token) { | ||||
|     usb_adapter_handle = nullptr; | ||||
|     pads = {}; | ||||
|     while (!stop_token.stop_requested() && !Setup()) { | ||||
|         std::this_thread::sleep_for(std::chrono::seconds(2)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void Adapter::Setup() { | ||||
|     usb_adapter_handle = libusb_open_device_with_vid_pid(libusb_ctx, 0x057e, 0x0337); | ||||
|  | ||||
|     if (usb_adapter_handle == NULL) { | ||||
|         return; | ||||
| bool Adapter::Setup() { | ||||
|     constexpr u16 nintendo_vid = 0x057e; | ||||
|     constexpr u16 gc_adapter_pid = 0x0337; | ||||
|     usb_adapter_handle = | ||||
|         std::make_unique<LibUSBDeviceHandle>(libusb_ctx->get(), nintendo_vid, gc_adapter_pid); | ||||
|     if (!usb_adapter_handle->get()) { | ||||
|         return false; | ||||
|     } | ||||
|     if (!CheckDeviceAccess()) { | ||||
|         ClearLibusbHandle(); | ||||
|         return; | ||||
|         usb_adapter_handle = nullptr; | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     libusb_device* device = libusb_get_device(usb_adapter_handle); | ||||
|     libusb_device* const device = libusb_get_device(usb_adapter_handle->get()); | ||||
|  | ||||
|     LOG_INFO(Input, "GC adapter is now connected"); | ||||
|     // GC Adapter found and accessible, registering it | ||||
|     if (GetGCEndpoint(device)) { | ||||
|         adapter_scan_thread_running = false; | ||||
|         adapter_input_thread_running = true; | ||||
|         rumble_enabled = true; | ||||
|         input_error_counter = 0; | ||||
|         output_error_counter = 0; | ||||
|         adapter_input_thread = std::thread(&Adapter::AdapterInputThread, this); | ||||
|         adapter_input_thread = | ||||
|             std::jthread([this](std::stop_token stop_token) { AdapterInputThread(stop_token); }); | ||||
|         return true; | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| bool Adapter::CheckDeviceAccess() { | ||||
|     // This fixes payload problems from offbrand GCAdapters | ||||
|     const s32 control_transfer_error = | ||||
|         libusb_control_transfer(usb_adapter_handle, 0x21, 11, 0x0001, 0, nullptr, 0, 1000); | ||||
|         libusb_control_transfer(usb_adapter_handle->get(), 0x21, 11, 0x0001, 0, nullptr, 0, 1000); | ||||
|     if (control_transfer_error < 0) { | ||||
|         LOG_ERROR(Input, "libusb_control_transfer failed with error= {}", control_transfer_error); | ||||
|     } | ||||
|  | ||||
|     s32 kernel_driver_error = libusb_kernel_driver_active(usb_adapter_handle, 0); | ||||
|     s32 kernel_driver_error = libusb_kernel_driver_active(usb_adapter_handle->get(), 0); | ||||
|     if (kernel_driver_error == 1) { | ||||
|         kernel_driver_error = libusb_detach_kernel_driver(usb_adapter_handle, 0); | ||||
|         kernel_driver_error = libusb_detach_kernel_driver(usb_adapter_handle->get(), 0); | ||||
|         if (kernel_driver_error != 0 && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) { | ||||
|             LOG_ERROR(Input, "libusb_detach_kernel_driver failed with error = {}", | ||||
|                       kernel_driver_error); | ||||
| @@ -290,15 +345,13 @@ bool Adapter::CheckDeviceAccess() { | ||||
|     } | ||||
|  | ||||
|     if (kernel_driver_error && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) { | ||||
|         libusb_close(usb_adapter_handle); | ||||
|         usb_adapter_handle = nullptr; | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     const int interface_claim_error = libusb_claim_interface(usb_adapter_handle, 0); | ||||
|     const int interface_claim_error = libusb_claim_interface(usb_adapter_handle->get(), 0); | ||||
|     if (interface_claim_error) { | ||||
|         LOG_ERROR(Input, "libusb_claim_interface failed with error = {}", interface_claim_error); | ||||
|         libusb_close(usb_adapter_handle); | ||||
|         usb_adapter_handle = nullptr; | ||||
|         return false; | ||||
|     } | ||||
| @@ -332,57 +385,17 @@ bool Adapter::GetGCEndpoint(libusb_device* device) { | ||||
|     // This transfer seems to be responsible for clearing the state of the adapter | ||||
|     // Used to clear the "busy" state of when the device is unexpectedly unplugged | ||||
|     unsigned char clear_payload = 0x13; | ||||
|     libusb_interrupt_transfer(usb_adapter_handle, output_endpoint, &clear_payload, | ||||
|     libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, &clear_payload, | ||||
|                               sizeof(clear_payload), nullptr, 16); | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| void Adapter::JoinThreads() { | ||||
|     restart_scan_thread = false; | ||||
|     adapter_input_thread_running = false; | ||||
|     adapter_scan_thread_running = false; | ||||
|  | ||||
|     if (adapter_scan_thread.joinable()) { | ||||
|         adapter_scan_thread.join(); | ||||
|     } | ||||
|  | ||||
|     if (adapter_input_thread.joinable()) { | ||||
|         adapter_input_thread.join(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void Adapter::ClearLibusbHandle() { | ||||
|     if (usb_adapter_handle) { | ||||
|         libusb_release_interface(usb_adapter_handle, 1); | ||||
|         libusb_close(usb_adapter_handle); | ||||
|         usb_adapter_handle = nullptr; | ||||
|     } | ||||
| } | ||||
|  | ||||
| void Adapter::ResetDevices() { | ||||
|     for (std::size_t i = 0; i < pads.size(); ++i) { | ||||
|         ResetDevice(i); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void Adapter::ResetDevice(std::size_t port) { | ||||
|     pads[port].type = ControllerTypes::None; | ||||
|     pads[port].enable_vibration = false; | ||||
|     pads[port].rumble_amplitude = 0; | ||||
|     pads[port].buttons = 0; | ||||
|     pads[port].last_button = PadButton::Undefined; | ||||
|     pads[port].axis_values.fill(0); | ||||
|     pads[port].reset_origin_counter = 0; | ||||
| } | ||||
|  | ||||
| void Adapter::Reset() { | ||||
|     JoinThreads(); | ||||
|     ClearLibusbHandle(); | ||||
|     ResetDevices(); | ||||
|  | ||||
|     if (libusb_ctx) { | ||||
|         libusb_exit(libusb_ctx); | ||||
|     } | ||||
|     adapter_scan_thread = {}; | ||||
|     adapter_input_thread = {}; | ||||
|     usb_adapter_handle = nullptr; | ||||
|     pads = {}; | ||||
|     libusb_ctx = nullptr; | ||||
| } | ||||
|  | ||||
| std::vector<Common::ParamPackage> Adapter::GetInputDevices() const { | ||||
|   | ||||
| @@ -3,11 +3,14 @@ | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <functional> | ||||
| #include <mutex> | ||||
| #include <stop_token> | ||||
| #include <thread> | ||||
| #include <unordered_map> | ||||
|  | ||||
| #include "common/common_types.h" | ||||
| #include "common/threadsafe_queue.h" | ||||
| #include "input_common/main.h" | ||||
| @@ -18,6 +21,9 @@ struct libusb_device_handle; | ||||
|  | ||||
| namespace GCAdapter { | ||||
|  | ||||
| class LibUSBContext; | ||||
| class LibUSBDeviceHandle; | ||||
|  | ||||
| enum class PadButton { | ||||
|     Undefined = 0x0000, | ||||
|     ButtonLeft = 0x0001, | ||||
| @@ -63,11 +69,11 @@ struct GCPadStatus { | ||||
| }; | ||||
|  | ||||
| struct GCController { | ||||
|     ControllerTypes type{}; | ||||
|     bool enable_vibration{}; | ||||
|     u8 rumble_amplitude{}; | ||||
|     u16 buttons{}; | ||||
|     PadButton last_button{}; | ||||
|     ControllerTypes type = ControllerTypes::None; | ||||
|     bool enable_vibration = false; | ||||
|     u8 rumble_amplitude = 0; | ||||
|     u16 buttons = 0; | ||||
|     PadButton last_button = PadButton::Undefined; | ||||
|     std::array<s16, 6> axis_values{}; | ||||
|     std::array<u8, 6> axis_origin{}; | ||||
|     u8 reset_origin_counter{}; | ||||
| @@ -109,9 +115,9 @@ private: | ||||
|     void UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload); | ||||
|     void UpdateVibrations(); | ||||
|  | ||||
|     void AdapterInputThread(); | ||||
|     void AdapterInputThread(std::stop_token stop_token); | ||||
|  | ||||
|     void AdapterScanThread(); | ||||
|     void AdapterScanThread(std::stop_token stop_token); | ||||
|  | ||||
|     bool IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size); | ||||
|  | ||||
| @@ -119,13 +125,7 @@ private: | ||||
|     void SendVibrations(); | ||||
|  | ||||
|     /// For use in initialization, querying devices to find the adapter | ||||
|     void Setup(); | ||||
|  | ||||
|     /// Resets status of all GC controller devices to a disconnected state | ||||
|     void ResetDevices(); | ||||
|  | ||||
|     /// Resets status of device connected to a disconnected state | ||||
|     void ResetDevice(std::size_t port); | ||||
|     bool Setup(); | ||||
|  | ||||
|     /// Returns true if we successfully gain access to GC Adapter | ||||
|     bool CheckDeviceAccess(); | ||||
| @@ -137,23 +137,15 @@ private: | ||||
|     /// For shutting down, clear all data, join all threads, release usb | ||||
|     void Reset(); | ||||
|  | ||||
|     // Join all threads | ||||
|     void JoinThreads(); | ||||
|  | ||||
|     // Release usb handles | ||||
|     void ClearLibusbHandle(); | ||||
|  | ||||
|     libusb_device_handle* usb_adapter_handle = nullptr; | ||||
|     std::unique_ptr<LibUSBDeviceHandle> usb_adapter_handle; | ||||
|     std::array<GCController, 4> pads; | ||||
|     Common::SPSCQueue<GCPadStatus> pad_queue; | ||||
|  | ||||
|     std::thread adapter_input_thread; | ||||
|     std::thread adapter_scan_thread; | ||||
|     bool adapter_input_thread_running; | ||||
|     bool adapter_scan_thread_running; | ||||
|     bool restart_scan_thread; | ||||
|     std::jthread adapter_input_thread; | ||||
|     std::jthread adapter_scan_thread; | ||||
|     bool restart_scan_thread{}; | ||||
|  | ||||
|     libusb_context* libusb_ctx; | ||||
|     std::unique_ptr<LibUSBContext> libusb_ctx; | ||||
|  | ||||
|     u8 input_endpoint{0}; | ||||
|     u8 output_endpoint{0}; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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(); | ||||
|  | ||||
|   | ||||
							
								
								
									
										432
									
								
								src/input_common/tas/tas_input.cpp
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										432
									
								
								src/input_common/tas/tas_input.cpp
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,432 @@ | ||||
| // 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) { | ||||
|         return; | ||||
|     } | ||||
|     LoadTasFiles(); | ||||
| } | ||||
|  | ||||
| Tas::~Tas() = default; | ||||
|  | ||||
| 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) { | ||||
|         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() { | ||||
|     is_running = !is_running; | ||||
|     if (is_running) { | ||||
|         SwapToTasController(); | ||||
|     } else { | ||||
|         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; | ||||
|         } | ||||
|  | ||||
|         auto tas_param = Common::ParamPackage{{"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(); | ||||
|         } | ||||
|     } | ||||
|     Settings::values.is_device_reload_pending.store(true); | ||||
| } | ||||
|  | ||||
| void Tas::SwapToStoredController() { | ||||
|     if (!Settings::values.tas_swap_controllers) { | ||||
|         return; | ||||
|     } | ||||
|     auto& players = Settings::values.players.GetValue(); | ||||
|     for (std::size_t index = 0; index < players.size(); index++) { | ||||
|         players[index] = player_mappings[index]; | ||||
|     } | ||||
|     Settings::values.is_device_reload_pending.store(true); | ||||
| } | ||||
|  | ||||
| void Tas::Reset() { | ||||
|     needs_reset = true; | ||||
| } | ||||
|  | ||||
| bool Tas::Record() { | ||||
|     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 | ||||
							
								
								
									
										233
									
								
								src/input_common/tas/tas_input.h
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										233
									
								
								src/input_common/tas/tas_input.h
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,233 @@ | ||||
| // 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 | ||||
| Emulation -> 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(); | ||||
|  | ||||
|     // 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_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 | ||||
							
								
								
									
										101
									
								
								src/input_common/tas/tas_poller.cpp
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										101
									
								
								src/input_common/tas/tas_poller.cpp
									
									
									
									
									
										Executable file
									
								
							| @@ -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 | ||||
							
								
								
									
										43
									
								
								src/input_common/tas/tas_poller.h
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										43
									
								
								src/input_common/tas/tas_poller.h
									
									
									
									
									
										Executable file
									
								
							| @@ -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 | ||||
| @@ -4,6 +4,8 @@ | ||||
|  | ||||
| #include <array> | ||||
| #include <vector> | ||||
| #include "common/scope_exit.h" | ||||
| #include "video_core/dirty_flags.h" | ||||
| #include "video_core/engines/maxwell_3d.h" | ||||
| #include "video_core/macro/macro_hle.h" | ||||
| #include "video_core/rasterizer_interface.h" | ||||
| @@ -56,6 +58,7 @@ void HLE_0217920100488FF7(Engines::Maxwell3D& maxwell3d, const std::vector<u32>& | ||||
|     maxwell3d.regs.index_array.first = parameters[3]; | ||||
|     maxwell3d.regs.reg_array[0x446] = element_base; // vertex id base? | ||||
|     maxwell3d.regs.index_array.count = parameters[1]; | ||||
|     maxwell3d.dirty.flags[VideoCommon::Dirty::IndexBuffer] = true; | ||||
|     maxwell3d.regs.vb_element_base = element_base; | ||||
|     maxwell3d.regs.vb_base_instance = base_instance; | ||||
|     maxwell3d.mme_draw.instance_count = instance_count; | ||||
| @@ -77,12 +80,70 @@ void HLE_0217920100488FF7(Engines::Maxwell3D& maxwell3d, const std::vector<u32>& | ||||
|     maxwell3d.CallMethodFromMME(0x8e5, 0x0); | ||||
|     maxwell3d.mme_draw.current_mode = Engines::Maxwell3D::MMEDrawMode::Undefined; | ||||
| } | ||||
|  | ||||
| // Multidraw Indirect | ||||
| void HLE_3f5e74b9c9a50164(Engines::Maxwell3D& maxwell3d, const std::vector<u32>& parameters) { | ||||
|     SCOPE_EXIT({ | ||||
|         // Clean everything. | ||||
|         maxwell3d.regs.reg_array[0x446] = 0x0; // vertex id base? | ||||
|         maxwell3d.regs.index_array.count = 0; | ||||
|         maxwell3d.regs.vb_element_base = 0x0; | ||||
|         maxwell3d.regs.vb_base_instance = 0x0; | ||||
|         maxwell3d.mme_draw.instance_count = 0; | ||||
|         maxwell3d.CallMethodFromMME(0x8e3, 0x640); | ||||
|         maxwell3d.CallMethodFromMME(0x8e4, 0x0); | ||||
|         maxwell3d.CallMethodFromMME(0x8e5, 0x0); | ||||
|         maxwell3d.mme_draw.current_mode = Engines::Maxwell3D::MMEDrawMode::Undefined; | ||||
|         maxwell3d.dirty.flags[VideoCommon::Dirty::IndexBuffer] = true; | ||||
|     }); | ||||
|     const u32 start_indirect = parameters[0]; | ||||
|     const u32 end_indirect = parameters[1]; | ||||
|     if (start_indirect >= end_indirect) { | ||||
|         // Nothing to do. | ||||
|         return; | ||||
|     } | ||||
|     const auto topology = | ||||
|         static_cast<Tegra::Engines::Maxwell3D::Regs::PrimitiveTopology>(parameters[2]); | ||||
|     maxwell3d.regs.draw.topology.Assign(topology); | ||||
|     const u32 padding = parameters[3]; | ||||
|     const std::size_t max_draws = parameters[4]; | ||||
|  | ||||
|     const u32 indirect_words = 5 + padding; | ||||
|     const std::size_t first_draw = start_indirect; | ||||
|     const std::size_t effective_draws = end_indirect - start_indirect; | ||||
|     const std::size_t last_draw = start_indirect + std::min(effective_draws, max_draws); | ||||
|  | ||||
|     for (std::size_t index = first_draw; index < last_draw; index++) { | ||||
|         const std::size_t base = index * indirect_words + 5; | ||||
|         const u32 num_vertices = parameters[base]; | ||||
|         const u32 instance_count = parameters[base + 1]; | ||||
|         const u32 first_index = parameters[base + 2]; | ||||
|         const u32 base_vertex = parameters[base + 3]; | ||||
|         const u32 base_instance = parameters[base + 4]; | ||||
|         maxwell3d.regs.index_array.first = first_index; | ||||
|         maxwell3d.regs.reg_array[0x446] = base_vertex; | ||||
|         maxwell3d.regs.index_array.count = num_vertices; | ||||
|         maxwell3d.regs.vb_element_base = base_vertex; | ||||
|         maxwell3d.regs.vb_base_instance = base_instance; | ||||
|         maxwell3d.mme_draw.instance_count = instance_count; | ||||
|         maxwell3d.CallMethodFromMME(0x8e3, 0x640); | ||||
|         maxwell3d.CallMethodFromMME(0x8e4, base_vertex); | ||||
|         maxwell3d.CallMethodFromMME(0x8e5, base_instance); | ||||
|         maxwell3d.dirty.flags[VideoCommon::Dirty::IndexBuffer] = true; | ||||
|         if (maxwell3d.ShouldExecute()) { | ||||
|             maxwell3d.Rasterizer().Draw(true, true); | ||||
|         } | ||||
|         maxwell3d.mme_draw.current_mode = Engines::Maxwell3D::MMEDrawMode::Undefined; | ||||
|     } | ||||
| } | ||||
|  | ||||
| } // Anonymous namespace | ||||
|  | ||||
| constexpr std::array<std::pair<u64, HLEFunction>, 3> hle_funcs{{ | ||||
| constexpr std::array<std::pair<u64, HLEFunction>, 4> hle_funcs{{ | ||||
|     {0x771BB18C62444DA0, &HLE_771BB18C62444DA0}, | ||||
|     {0x0D61FC9FAAC9FCAD, &HLE_0D61FC9FAAC9FCAD}, | ||||
|     {0x0217920100488FF7, &HLE_0217920100488FF7}, | ||||
|     {0x3f5e74b9c9a50164, &HLE_3f5e74b9c9a50164}, | ||||
| }}; | ||||
|  | ||||
| HLEMacro::HLEMacro(Engines::Maxwell3D& maxwell3d_) : maxwell3d{maxwell3d_} {} | ||||
|   | ||||
| @@ -118,6 +118,25 @@ void MemoryManager::TryUnlockPage(PageEntry page_entry, std::size_t size) { | ||||
|                .IsSuccess()); | ||||
| } | ||||
|  | ||||
| void MemoryManager::UnmapVicFrame(GPUVAddr gpu_addr, std::size_t size) { | ||||
|     if (!size) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const std::optional<VAddr> cpu_addr = GpuToCpuAddress(gpu_addr); | ||||
|     ASSERT(cpu_addr); | ||||
|     rasterizer->InvalidateExceptTextureCache(*cpu_addr, size); | ||||
|     cache_invalidate_queue.push_back({*cpu_addr, size}); | ||||
|  | ||||
|     UpdateRange(gpu_addr, PageEntry::State::Unmapped, size); | ||||
| } | ||||
|  | ||||
| void MemoryManager::InvalidateQueuedCaches() { | ||||
|     for (const auto& entry : cache_invalidate_queue) { | ||||
|         rasterizer->InvalidateTextureCache(entry.first, entry.second); | ||||
|     } | ||||
|     cache_invalidate_queue.clear(); | ||||
| } | ||||
| PageEntry MemoryManager::GetPageEntry(GPUVAddr gpu_addr) const { | ||||
|     return page_table[PageEntryIndex(gpu_addr)]; | ||||
| } | ||||
|   | ||||
| @@ -143,6 +143,14 @@ public: | ||||
|     [[nodiscard]] GPUVAddr Allocate(std::size_t size, std::size_t align); | ||||
|     void Unmap(GPUVAddr gpu_addr, std::size_t size); | ||||
|  | ||||
|     /** | ||||
|      * Some Decoded NVDEC frames require that texture cache does not get invalidated. | ||||
|      * UnmapVicFrame defers the texture cache invalidation until the stream ends | ||||
|      * by invoking InvalidateQueuedCaches to invalidate all frame texture caches. | ||||
|      */ | ||||
|     void UnmapVicFrame(GPUVAddr gpu_addr, std::size_t size); | ||||
|     void InvalidateQueuedCaches(); | ||||
|  | ||||
| private: | ||||
|     [[nodiscard]] PageEntry GetPageEntry(GPUVAddr gpu_addr) const; | ||||
|     void SetPageEntry(GPUVAddr gpu_addr, PageEntry page_entry, std::size_t size = page_size); | ||||
|   | ||||
| @@ -77,6 +77,12 @@ public: | ||||
|     /// Notify rasterizer that any caches of the specified region should be flushed to Switch memory | ||||
|     virtual void FlushRegion(VAddr addr, u64 size) = 0; | ||||
|  | ||||
|     /// Notify rasterizer to flush the texture cache to Switch memory | ||||
|     virtual void InvalidateExceptTextureCache(VAddr addr, u64 size) = 0; | ||||
|  | ||||
|     /// Notify rasterizer to invalidate the texture cache | ||||
|     virtual void InvalidateTextureCache(VAddr addr, u64 size) = 0; | ||||
|  | ||||
|     /// Check if the the specified memory area requires flushing to CPU Memory. | ||||
|     virtual bool MustFlushRegion(VAddr addr, u64 size) = 0; | ||||
|  | ||||
|   | ||||
| @@ -322,6 +322,26 @@ void RasterizerOpenGL::FlushRegion(VAddr addr, u64 size) { | ||||
|     query_cache.FlushRegion(addr, size); | ||||
| } | ||||
|  | ||||
| void RasterizerOpenGL::InvalidateExceptTextureCache(VAddr addr, u64 size) { | ||||
|     if (addr == 0 || size == 0) { | ||||
|         return; | ||||
|     } | ||||
|     shader_cache.InvalidateRegion(addr, size); | ||||
|     { | ||||
|         std::scoped_lock lock{buffer_cache.mutex}; | ||||
|         buffer_cache.WriteMemory(addr, size); | ||||
|     } | ||||
|     query_cache.InvalidateRegion(addr, size); | ||||
| } | ||||
|  | ||||
| void RasterizerOpenGL::InvalidateTextureCache(VAddr addr, u64 size) { | ||||
|     if (addr == 0 || size == 0) { | ||||
|         return; | ||||
|     } | ||||
|     std::scoped_lock lock{texture_cache.mutex}; | ||||
|     texture_cache.UnmapMemory(addr, size); | ||||
| } | ||||
|  | ||||
| bool RasterizerOpenGL::MustFlushRegion(VAddr addr, u64 size) { | ||||
|     std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; | ||||
|     if (!Settings::IsGPULevelHigh()) { | ||||
|   | ||||
| @@ -86,6 +86,8 @@ public: | ||||
|     void DisableGraphicsUniformBuffer(size_t stage, u32 index) override; | ||||
|     void FlushAll() override; | ||||
|     void FlushRegion(VAddr addr, u64 size) override; | ||||
|     void InvalidateExceptTextureCache(VAddr addr, u64 size) override; | ||||
|     void InvalidateTextureCache(VAddr addr, u64 size) override; | ||||
|     bool MustFlushRegion(VAddr addr, u64 size) override; | ||||
|     void InvalidateRegion(VAddr addr, u64 size) override; | ||||
|     void OnCPUWrite(VAddr addr, u64 size) override; | ||||
|   | ||||
| @@ -311,6 +311,26 @@ void RasterizerVulkan::FlushRegion(VAddr addr, u64 size) { | ||||
|     query_cache.FlushRegion(addr, size); | ||||
| } | ||||
|  | ||||
| void Vulkan::RasterizerVulkan::InvalidateExceptTextureCache(VAddr addr, u64 size) { | ||||
|     if (addr == 0 || size == 0) { | ||||
|         return; | ||||
|     } | ||||
|     pipeline_cache.InvalidateRegion(addr, size); | ||||
|     { | ||||
|         std::scoped_lock lock{buffer_cache.mutex}; | ||||
|         buffer_cache.WriteMemory(addr, size); | ||||
|     } | ||||
|     query_cache.InvalidateRegion(addr, size); | ||||
| } | ||||
|  | ||||
| void Vulkan::RasterizerVulkan::InvalidateTextureCache(VAddr addr, u64 size) { | ||||
|     if (addr == 0 || size == 0) { | ||||
|         return; | ||||
|     } | ||||
|     std::scoped_lock lock{texture_cache.mutex}; | ||||
|     texture_cache.UnmapMemory(addr, size); | ||||
| } | ||||
|  | ||||
| bool RasterizerVulkan::MustFlushRegion(VAddr addr, u64 size) { | ||||
|     std::scoped_lock lock{texture_cache.mutex, buffer_cache.mutex}; | ||||
|     if (!Settings::IsGPULevelHigh()) { | ||||
|   | ||||
| @@ -79,6 +79,8 @@ public: | ||||
|     void DisableGraphicsUniformBuffer(size_t stage, u32 index) override; | ||||
|     void FlushAll() override; | ||||
|     void FlushRegion(VAddr addr, u64 size) override; | ||||
|     void InvalidateExceptTextureCache(VAddr addr, u64 size) override; | ||||
|     void InvalidateTextureCache(VAddr addr, u64 size) override; | ||||
|     bool MustFlushRegion(VAddr addr, u64 size) override; | ||||
|     void InvalidateRegion(VAddr addr, u64 size) override; | ||||
|     void OnCPUWrite(VAddr addr, u64 size) override; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -112,6 +112,7 @@ void QtNXWebEngineView::LoadLocalWebPage(const std::string& main_url, | ||||
|     SetExitReason(Service::AM::Applets::WebExitReason::EndButtonPressed); | ||||
|     SetLastURL("http://localhost/"); | ||||
|     StartInputThread(); | ||||
|     FocusFirstLinkElement(); | ||||
|  | ||||
|     load(QUrl(QUrl::fromLocalFile(QString::fromStdString(main_url)).toString() + | ||||
|               QString::fromStdString(additional_args))); | ||||
| @@ -128,6 +129,8 @@ void QtNXWebEngineView::LoadExternalWebPage(const std::string& main_url, | ||||
|     StartInputThread(); | ||||
|  | ||||
|     load(QUrl(QString::fromStdString(main_url) + QString::fromStdString(additional_args))); | ||||
|  | ||||
|     FocusFirstLinkElement(); | ||||
| } | ||||
|  | ||||
| void QtNXWebEngineView::SetUserAgent(UserAgent user_agent) { | ||||
| @@ -208,7 +211,7 @@ void QtNXWebEngineView::HandleWindowFooterButtonPressedOnce() { | ||||
|         if (input_interpreter->IsButtonPressedOnce(button)) { | ||||
|             page()->runJavaScript( | ||||
|                 QStringLiteral("yuzu_key_callbacks[%1] == null;").arg(static_cast<u8>(button)), | ||||
|                 [&](const QVariant& variant) { | ||||
|                 [this, button](const QVariant& variant) { | ||||
|                     if (variant.toBool()) { | ||||
|                         switch (button) { | ||||
|                         case HIDButton::A: | ||||
| @@ -364,6 +367,17 @@ void QtNXWebEngineView::LoadExtractedFonts() { | ||||
|         Qt::QueuedConnection); | ||||
| } | ||||
|  | ||||
| void QtNXWebEngineView::FocusFirstLinkElement() { | ||||
|     QWebEngineScript focus_link_element; | ||||
|  | ||||
|     focus_link_element.setName(QStringLiteral("focus_link_element.js")); | ||||
|     focus_link_element.setSourceCode(QString::fromStdString(FOCUS_LINK_ELEMENT_SCRIPT)); | ||||
|     focus_link_element.setWorldId(QWebEngineScript::MainWorld); | ||||
|     focus_link_element.setInjectionPoint(QWebEngineScript::Deferred); | ||||
|     focus_link_element.setRunsOnSubFrames(true); | ||||
|     default_profile->scripts()->insert(focus_link_element); | ||||
| } | ||||
|  | ||||
| #endif | ||||
|  | ||||
| QtWebBrowser::QtWebBrowser(GMainWindow& main_window) { | ||||
|   | ||||
| @@ -161,6 +161,9 @@ private: | ||||
|     /// Loads the extracted fonts using JavaScript. | ||||
|     void LoadExtractedFonts(); | ||||
|  | ||||
|     /// Brings focus to the first available link element. | ||||
|     void FocusFirstLinkElement(); | ||||
|  | ||||
|     InputCommon::InputSubsystem* input_subsystem; | ||||
|  | ||||
|     std::unique_ptr<UrlRequestInterceptor> url_interceptor; | ||||
|   | ||||
| @@ -73,6 +73,12 @@ constexpr char LOAD_NX_FONT[] = R"( | ||||
| })(); | ||||
| )"; | ||||
|  | ||||
| constexpr char FOCUS_LINK_ELEMENT_SCRIPT[] = R"( | ||||
| if (document.getElementsByTagName("a").length > 0) { | ||||
|     document.getElementsByTagName("a")[0].focus(); | ||||
| } | ||||
| )"; | ||||
|  | ||||
| constexpr char GAMEPAD_SCRIPT[] = R"( | ||||
| window.addEventListener("gamepadconnected", function(e) { | ||||
|     console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.", | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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}}, | ||||
| @@ -564,6 +567,11 @@ void Config::ReadControlValues() { | ||||
|     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); | ||||
| @@ -823,6 +838,7 @@ void Config::ReadRendererValues() { | ||||
|     ReadGlobalSetting(Settings::values.bg_blue); | ||||
|  | ||||
|     if (global) { | ||||
|         ReadBasicSetting(Settings::values.fps_cap); | ||||
|         ReadBasicSetting(Settings::values.renderer_debug); | ||||
|         ReadBasicSetting(Settings::values.enable_nsight_aftermath); | ||||
|         ReadBasicSetting(Settings::values.disable_shader_loop_safety_checks); | ||||
| @@ -1185,6 +1201,11 @@ void Config::SaveControlValues() { | ||||
|     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(); | ||||
| } | ||||
|  | ||||
| @@ -1212,6 +1233,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); | ||||
| @@ -1357,6 +1382,7 @@ void Config::SaveRendererValues() { | ||||
|     WriteGlobalSetting(Settings::values.bg_blue); | ||||
|  | ||||
|     if (global) { | ||||
|         WriteBasicSetting(Settings::values.fps_cap); | ||||
|         WriteBasicSetting(Settings::values.renderer_debug); | ||||
|         WriteBasicSetting(Settings::values.enable_nsight_aftermath); | ||||
|         WriteBasicSetting(Settings::values.disable_shader_loop_safety_checks); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -48,6 +48,8 @@ void ConfigureGeneral::SetConfiguration() { | ||||
|     ui->toggle_frame_limit->setChecked(Settings::values.use_frame_limit.GetValue()); | ||||
|     ui->frame_limit->setValue(Settings::values.frame_limit.GetValue()); | ||||
|  | ||||
|     ui->fps_cap->setValue(Settings::values.fps_cap.GetValue()); | ||||
|  | ||||
|     ui->button_reset_defaults->setEnabled(runtime_lock); | ||||
|  | ||||
|     if (Settings::IsConfiguringGlobal()) { | ||||
| @@ -87,6 +89,8 @@ void ConfigureGeneral::ApplyConfiguration() { | ||||
|         UISettings::values.pause_when_in_background = ui->toggle_background_pause->isChecked(); | ||||
|         UISettings::values.hide_mouse = ui->toggle_hide_mouse->isChecked(); | ||||
|  | ||||
|         Settings::values.fps_cap.SetValue(ui->fps_cap->value()); | ||||
|  | ||||
|         // Guard if during game and set to game-specific value | ||||
|         if (Settings::values.use_frame_limit.UsingGlobal()) { | ||||
|             Settings::values.use_frame_limit.SetValue(ui->toggle_frame_limit->checkState() == | ||||
|   | ||||
| @@ -51,6 +51,36 @@ | ||||
|             </item> | ||||
|            </layout> | ||||
|           </item> | ||||
|           <item> | ||||
|            <layout class="QHBoxLayout" name="horizontalLayout_2"> | ||||
|             <item> | ||||
|              <widget class="QLabel" name="fps_cap_label"> | ||||
|               <property name="text"> | ||||
|                <string>Framerate Cap</string> | ||||
|               </property> | ||||
|               <property name="toolTip"> | ||||
|                 <string>Requires the use of the FPS Limiter Toggle hotkey to take effect.</string> | ||||
|               </property> | ||||
|              </widget> | ||||
|             </item> | ||||
|             <item> | ||||
|              <widget class="QSpinBox" name="fps_cap"> | ||||
|               <property name="suffix"> | ||||
|                <string>x</string> | ||||
|               </property> | ||||
|               <property name="minimum"> | ||||
|                <number>1</number> | ||||
|               </property> | ||||
|               <property name="maximum"> | ||||
|                <number>1000</number> | ||||
|               </property> | ||||
|               <property name="value"> | ||||
|                <number>500</number> | ||||
|               </property> | ||||
|              </widget> | ||||
|             </item> | ||||
|            </layout> | ||||
|           </item> | ||||
|           <item> | ||||
|            <widget class="QCheckBox" name="use_multi_core"> | ||||
|             <property name="text"> | ||||
|   | ||||
| @@ -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]"); | ||||
|         } | ||||
| @@ -923,9 +937,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")) { | ||||
| @@ -1042,8 +1056,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)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -173,7 +173,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; | ||||
| @@ -220,6 +220,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) { | ||||
| @@ -227,6 +240,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); | ||||
|   | ||||
| @@ -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; | ||||
| @@ -177,6 +179,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{}; | ||||
|   | ||||
							
								
								
									
										84
									
								
								src/yuzu/configuration/configure_tas.cpp
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										84
									
								
								src/yuzu/configuration/configure_tas.cpp
									
									
									
									
									
										Executable file
									
								
							| @@ -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.isNull() || 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(); | ||||
| } | ||||
							
								
								
									
										38
									
								
								src/yuzu/configuration/configure_tas.h
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										38
									
								
								src/yuzu/configuration/configure_tas.h
									
									
									
									
									
										Executable file
									
								
							| @@ -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; | ||||
| }; | ||||
							
								
								
									
										140
									
								
								src/yuzu/configuration/configure_tas.ui
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										140
									
								
								src/yuzu/configuration/configure_tas.ui
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| <?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 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>TAS Directories</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> | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| }; | ||||
|   | ||||
| @@ -38,7 +38,7 @@ void DiscordImpl::Update() { | ||||
|     if (Core::System::GetInstance().IsPoweredOn()) | ||||
|         Core::System::GetInstance().GetAppLoader().ReadTitle(title); | ||||
|     DiscordRichPresence presence{}; | ||||
|     presence.largeImageKey = "yuzu_logo"; | ||||
|     presence.largeImageKey = "yuzu_logo_ea"; | ||||
|     presence.largeImageText = "yuzu is an emulator for the Nintendo Switch"; | ||||
|     if (Core::System::GetInstance().IsPoweredOn()) { | ||||
|         presence.state = title.c_str(); | ||||
|   | ||||
| @@ -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" | ||||
| @@ -762,6 +764,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")); | ||||
| @@ -856,7 +863,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()); | ||||
|  | ||||
| @@ -1029,6 +1036,20 @@ void GMainWindow::InitializeHotkeys() { | ||||
|                     render_window->setAttribute(Qt::WA_Hover, true); | ||||
|                 } | ||||
|             }); | ||||
|     connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Start/Stop"), this), | ||||
|             &QShortcut::activated, this, [&] { 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, [&] { | ||||
|                 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() { | ||||
| @@ -1147,6 +1168,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); | ||||
|  | ||||
| @@ -2713,6 +2735,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()); | ||||
| @@ -2892,12 +2927,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(); | ||||
| @@ -2919,7 +2974,7 @@ void GMainWindow::UpdateStatusBar() { | ||||
|     } | ||||
|     if (Settings::values.disable_fps_limit) { | ||||
|         game_fps_label->setText( | ||||
|             tr("Game: %1 FPS (Limit off)").arg(results.average_game_fps, 0, 'f', 0)); | ||||
|             tr("Game: %1 FPS (Unlocked)").arg(results.average_game_fps, 0, 'f', 0)); | ||||
|     } else { | ||||
|         game_fps_label->setText(tr("Game: %1 FPS").arg(results.average_game_fps, 0, 'f', 0)); | ||||
|     } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -72,6 +72,7 @@ | ||||
|     <addaction name="action_Restart"/> | ||||
|     <addaction name="separator"/> | ||||
|     <addaction name="action_Configure"/> | ||||
|     <addaction name="action_Configure_Tas"/> | ||||
|     <addaction name="action_Configure_Current_Game"/> | ||||
|    </widget> | ||||
|    <widget class="QMenu" name="menu_View"> | ||||
| @@ -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> | ||||
|   | ||||
| @@ -457,6 +457,7 @@ void Config::ReadValues() { | ||||
|     ReadSetting("Renderer", Settings::values.gpu_accuracy); | ||||
|     ReadSetting("Renderer", Settings::values.use_asynchronous_gpu_emulation); | ||||
|     ReadSetting("Renderer", Settings::values.use_vsync); | ||||
|     ReadSetting("Renderer", Settings::values.fps_cap); | ||||
|     ReadSetting("Renderer", Settings::values.disable_fps_limit); | ||||
|     ReadSetting("Renderer", Settings::values.shader_backend); | ||||
|     ReadSetting("Renderer", Settings::values.use_asynchronous_shaders); | ||||
|   | ||||
| @@ -299,6 +299,10 @@ bg_red = | ||||
| bg_blue = | ||||
| bg_green = | ||||
|  | ||||
| # Caps the unlocked framerate to a multiple of the title's target FPS. | ||||
| # 1 - 1000: Target FPS multiple cap. 1000 (default) | ||||
| fps_cap = | ||||
|  | ||||
| [Audio] | ||||
| # Which audio output engine to use. | ||||
| # auto (default): Auto-select | ||||
|   | ||||
		Reference in New Issue
	
	Block a user