early-access version 1916

main
pineappleEA 2021-07-26 03:00:19 +02:00
parent 6f332e2512
commit 78a5c08bea
52 changed files with 1692 additions and 149 deletions

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 25 KiB

2
dist/yuzu.svg vendored
View File

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

View File

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

View File

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

View File

@ -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.
};
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
View 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

View 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

View 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

View File

@ -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_} {}

View File

@ -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)];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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();
}

View 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;
};

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>&amp;Capture Screenshot</string>
</property>
</action>
<action name="action_Configure_Tas">
<property name="text">
<string>Configure &amp;TAS...</string>
</property>
</action>
<action name="action_Configure_Current_Game">
<property name="enabled">
<bool>false</bool>

View File

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

View File

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