mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-11 09:25:05 -04:00
1126 lines
49 KiB
C++
1126 lines
49 KiB
C++
#include "settings_overlay.h"
|
|
#include "audio_backend.h"
|
|
#include "controller_button_names.h"
|
|
#include "controller_mapping_wizard.h"
|
|
#include "input_bindings.h"
|
|
#include "game_graphics_options.h"
|
|
#include "music_attenuation.h"
|
|
#include "runtime_config.h"
|
|
#include "runtime_log.h"
|
|
#include "wii_remote_input.h"
|
|
|
|
#include <imgui.h>
|
|
#include <SDL3/SDL_events.h>
|
|
#include <SDL3/SDL_gamepad.h>
|
|
#include <SDL3/SDL_keyboard.h>
|
|
#include <SDL3/SDL_mouse.h>
|
|
#include <SDL3/SDL_scancode.h>
|
|
|
|
#include <array>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cctype>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
|
|
#if defined(_WIN32)
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <Windows.h>
|
|
#include <shellapi.h>
|
|
#endif
|
|
|
|
#include <dolphin/pad.h>
|
|
|
|
extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled);
|
|
#include <dolphin/vi.h>
|
|
#include <aurora/aurora.h>
|
|
#include <aurora/gfx.h>
|
|
|
|
extern "C" int g_gxFrameCount;
|
|
|
|
// Defined in runtime/src/hle/audio/ax_mix.cpp. That header is private to the HLE
|
|
// directory and is not on this target's include path.
|
|
namespace AxDspHle {
|
|
void SetMixWorkerEnabled(bool enabled);
|
|
}
|
|
|
|
namespace settings_overlay {
|
|
namespace {
|
|
|
|
const char* GraphicsApiDisplayName() {
|
|
switch (aurora_get_backend()) {
|
|
case BACKEND_D3D11: return "Direct3D 11";
|
|
case BACKEND_D3D12: return "Direct3D 12";
|
|
case BACKEND_METAL: return "Metal";
|
|
case BACKEND_VULKAN: return "Vulkan";
|
|
case BACKEND_OPENGL: return "OpenGL";
|
|
case BACKEND_OPENGLES: return "OpenGL ES";
|
|
case BACKEND_WEBGPU: return "WebGPU";
|
|
case BACKEND_NULL: return "Null";
|
|
case BACKEND_AUTO: return "Automatic";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
bool g_topBarVisible = false;
|
|
bool g_rumbleEnabled = RuntimeConfigFile::RumbleEnabled(true);
|
|
int g_controllerPort = 0;
|
|
float g_resolutionScale = RuntimeConfigFile::ResolutionMultiplier(1.0f);
|
|
int g_audioVolumePercent = static_cast<int>(std::lround(RuntimeConfigFile::AudioVolume(1.0f) * 100.0f));
|
|
int g_musicVolumePercent = static_cast<int>(std::lround(RuntimeConfigFile::MusicVolume(1.0f) * 100.0f));
|
|
int g_soundEffectsVolumePercent =
|
|
static_cast<int>(std::lround(RuntimeConfigFile::SoundEffectsVolume(1.0f) * 100.0f));
|
|
int g_uiVolumePercent = static_cast<int>(std::lround(RuntimeConfigFile::UiVolume(1.0f) * 100.0f));
|
|
int g_voicesVolumePercent = static_cast<int>(std::lround(RuntimeConfigFile::VoicesVolume(1.0f) * 100.0f));
|
|
bool g_audioMuted = RuntimeConfigFile::AudioMuted(false);
|
|
bool g_audioMixWorker = RuntimeConfigFile::AudioMixWorkerEnabled(true);
|
|
bool g_attenuateMusicWhenMediaPlays = RuntimeConfigFile::AttenuateMusicWhenMediaPlays(false);
|
|
int g_frameInterpolationMode = [] {
|
|
switch (RuntimeConfigFile::FrameInterpolationFps(0)) {
|
|
case 120:
|
|
return 1;
|
|
case 180:
|
|
return 2;
|
|
default:
|
|
return 0;
|
|
}
|
|
}();
|
|
int g_displayMode = [] {
|
|
const std::string mode = RuntimeConfigFile::DisplayMode("windowed");
|
|
if (mode == "borderless") {
|
|
return static_cast<int>(AURORA_DISPLAY_MODE_BORDERLESS);
|
|
}
|
|
if (mode == "exclusive") {
|
|
return static_cast<int>(AURORA_DISPLAY_MODE_EXCLUSIVE);
|
|
}
|
|
return static_cast<int>(AURORA_DISPLAY_MODE_WINDOWED);
|
|
}();
|
|
bool g_skipUnreadyPipelines = RuntimeConfigFile::SkipUnreadyPipelines(true);
|
|
bool g_disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true);
|
|
bool g_showFps = RuntimeConfigFile::ShowFps(true);
|
|
uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0);
|
|
std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
|
|
std::array<int32_t, PAD_MAX_CONTROLLERS> indices{};
|
|
indices.fill(std::numeric_limits<int32_t>::min());
|
|
return indices;
|
|
}();
|
|
|
|
using ControllerNames::kNativeButtons;
|
|
using ControllerNames::NativeButtonItem;
|
|
constexpr const auto& kControllerButtons = ControllerNames::kGameCubeButtons;
|
|
|
|
// Classic Controller Pro layout, indexed like kControllerButtons: the SNES-style
|
|
// diamond (A right, B bottom, X top, Y left) with digital bumpers driving the GC
|
|
// triggers and Z on Back/Select (the same home the NSO GC default gives it).
|
|
constexpr std::array<const char*, PAD_BUTTON_COUNT> kClassicProPreset = {
|
|
"east", // A
|
|
"south", // B
|
|
"north", // X
|
|
"west", // Y
|
|
"start", // Start
|
|
"back", // Z
|
|
"left_shoulder", // L
|
|
"right_shoulder", // R
|
|
"dpad_up", "dpad_down", "dpad_left", "dpad_right",
|
|
};
|
|
|
|
// PlayStation layout: bumpers drive the GC triggers, Z moves to Create/Share.
|
|
constexpr std::array<const char*, PAD_BUTTON_COUNT> kPlayStationPreset = {
|
|
"south", "east", "west", "north", "start", "back",
|
|
"left_shoulder", "right_shoulder",
|
|
"dpad_up", "dpad_down", "dpad_left", "dpad_right",
|
|
};
|
|
|
|
struct ResolutionItem {
|
|
const char* label;
|
|
float scale;
|
|
};
|
|
|
|
using Clock = std::chrono::steady_clock;
|
|
|
|
constexpr auto kCursorAutoHideDelay = std::chrono::seconds(5);
|
|
Clock::time_point g_lastMouseActivity{Clock::now()};
|
|
bool g_cursorHidden = false;
|
|
|
|
constexpr std::array<std::string_view, 3> kDisplayModeConfigNames = {
|
|
"windowed", "borderless", "exclusive",
|
|
};
|
|
|
|
uint64_t g_presentedFrame = 0;
|
|
std::atomic_bool g_strapInputAccepted = false;
|
|
std::atomic_uint64_t g_startupDismissFrame = UINT64_MAX;
|
|
constexpr uint64_t kStrapTransitionCoverFrames = 60;
|
|
|
|
constexpr std::array<ResolutionItem, 8> kResolutions = {{
|
|
{"Auto (window size)", 0.0f}, {"Native (1x)", 1.0f}, {"1.5x", 1.5f}, {"2x", 2.0f},
|
|
{"3x", 3.0f}, {"4x", 4.0f}, {"6x", 6.0f}, {"8x", 8.0f},
|
|
}};
|
|
|
|
constexpr std::array<uint32_t, 3> kFrameInterpolationTargetFps{0, 120, 180};
|
|
|
|
bool IsHighResolutionScale(float scale) {
|
|
return std::fabs(scale - 6.0f) < 0.001f || std::fabs(scale - 8.0f) < 0.001f;
|
|
}
|
|
|
|
bool IsHighFrameRateMode() {
|
|
return kFrameInterpolationTargetFps[static_cast<size_t>(g_frameInterpolationMode)] > 60;
|
|
}
|
|
|
|
void SetResolutionScale(float scale) {
|
|
g_resolutionScale = scale;
|
|
VISetFrameBufferScale(scale);
|
|
RuntimeConfigFile::SetResolutionMultiplier(scale);
|
|
}
|
|
|
|
void LimitResolutionForFrameRate() {
|
|
if (IsHighFrameRateMode() && IsHighResolutionScale(g_resolutionScale)) {
|
|
SetResolutionScale(4.0f);
|
|
}
|
|
}
|
|
|
|
using ControllerNames::FindNativeButton;
|
|
|
|
struct ControllerBindingPair {
|
|
std::string primary;
|
|
std::string secondary;
|
|
};
|
|
|
|
|
|
// Config values hold up to two comma-separated button names ("dpad_up" or
|
|
// "dpad_up,left_shoulder"); pressing either one counts as the GC button.
|
|
ControllerBindingPair SplitControllerBinding(const std::string& value) {
|
|
const size_t comma = value.find(',');
|
|
if (comma == std::string::npos) {
|
|
return {ControllerNames::TrimToken(value), {}};
|
|
}
|
|
return {ControllerNames::TrimToken(value.substr(0, comma)), ControllerNames::TrimToken(value.substr(comma + 1))};
|
|
}
|
|
|
|
using ControllerNames::NativeButtonForValue;
|
|
|
|
void SetTopBarVisible(bool visible) {
|
|
if (g_topBarVisible == visible) {
|
|
return;
|
|
}
|
|
g_topBarVisible = visible;
|
|
}
|
|
|
|
void ApplyConfiguredMappings() {
|
|
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
|
const int32_t controllerIndex = PADGetIndexForPort(port);
|
|
if (controllerIndex == g_configuredControllerIndices[port]) {
|
|
continue;
|
|
}
|
|
g_configuredControllerIndices[port] = controllerIndex;
|
|
if (controllerIndex < 0) {
|
|
continue;
|
|
}
|
|
// The [controller] bindings are positional and shared by every port, so
|
|
// they describe whatever pad the user set them up with (usually an Xbox
|
|
// layout: a = south). A Wii U Pro Controller has a fixed, known layout
|
|
// (A on the east position) that aurora already maps by name; applying
|
|
// the shared bindings on top swaps A/B and X/Y. (Wii Remotes with any
|
|
// extension never reach the PAD layer: the game reads them through KPAD.)
|
|
if (WiiRemoteInput::KindForPort(port) == WiiRemoteInput::Kind::WiiUPro) {
|
|
continue;
|
|
}
|
|
|
|
uint32_t count = 0;
|
|
if (PADGetButtonMappings(port, &count) == nullptr || count != PAD_BUTTON_COUNT) {
|
|
continue;
|
|
}
|
|
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
|
|
const auto& configured = RuntimeConfigFile::ControllerButton(i);
|
|
if (!configured) {
|
|
continue;
|
|
}
|
|
const ControllerBindingPair binding = SplitControllerBinding(*configured);
|
|
if (const NativeButtonItem* native = FindNativeButton(binding.primary)) {
|
|
PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton});
|
|
} else {
|
|
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
|
|
<< " button '" << binding.primary << "'" << std::endl;
|
|
}
|
|
uint32_t altNative = PAD_NATIVE_BUTTON_INVALID;
|
|
if (!binding.secondary.empty()) {
|
|
if (const NativeButtonItem* native = FindNativeButton(binding.secondary)) {
|
|
altNative = native->nativeButton;
|
|
} else {
|
|
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
|
|
<< " secondary button '" << binding.secondary << "'" << std::endl;
|
|
}
|
|
}
|
|
PADSetAltButtonMapping(port, PADButtonMapping{altNative, kControllerButtons[i].padButton});
|
|
}
|
|
}
|
|
}
|
|
|
|
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled(true);
|
|
bool g_wiiContinuousScan = RuntimeConfigFile::WiiContinuousScanEnabled(false);
|
|
|
|
// Accelerometer readout and zero-point calibration for a bare remote / remote + Nunchuk.
|
|
void DrawWiiRemoteAccelerometer(uint32_t port) {
|
|
ImGui::SeparatorText("Accelerometer");
|
|
float sdlG[3] = {};
|
|
float kpad[3] = {};
|
|
if (WiiRemoteInput::ReadAccelDebug(port, sdlG, kpad)) {
|
|
ImGui::Text("KPAD acc: x %+.2f y %+.2f z %+.2f g", kpad[0], kpad[1], kpad[2]);
|
|
ImGui::TextDisabled("Flat, buttons up: (0, -1, 0). Sideways as a wheel: (1, 0, 0); z follows the turn.");
|
|
} else {
|
|
ImGui::TextDisabled("No accelerometer data yet.");
|
|
}
|
|
// SDL's read of the remote's calibration block often times out over Bluetooth
|
|
// and it falls back to a nominal zero point, leaving a small per-axis bias;
|
|
// measured here with the remote at rest.
|
|
if (WiiRemoteInput::IsAccelCalibrating()) {
|
|
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
|
|
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
|
|
WiiRemoteInput::StartAccelCalibration(port);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Put the remote down on a flat surface with the buttons facing up and do not touch it\n"
|
|
"for about two seconds. Corrects the steering offset of a remote held sideways.");
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::BeginDisabled(!RuntimeConfigFile::HasWiiAccelOffset() || WiiRemoteInput::IsAccelCalibrating());
|
|
if (ImGui::Button("Clear calibration")) {
|
|
WiiRemoteInput::ClearAccelCalibration();
|
|
}
|
|
ImGui::EndDisabled();
|
|
if (const char* message = WiiRemoteInput::AccelCalibrationMessage()) {
|
|
ImGui::TextWrapped("%s", message);
|
|
} else if (RuntimeConfigFile::HasWiiAccelOffset()) {
|
|
const std::array<double, 3> offset = RuntimeConfigFile::WiiAccelOffset();
|
|
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
|
|
} else {
|
|
ImGui::TextDisabled("Not calibrated (using SDL's zero point; see console.log for \"fallback accelerometer calibration\").");
|
|
}
|
|
}
|
|
|
|
// Wii Remotes (Bluetooth) menu: driver switch, pairing help, continuous scanning and the port's controller kind.
|
|
void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
|
|
if (!ImGui::BeginMenu("Wii Remotes (Bluetooth)")) {
|
|
return;
|
|
}
|
|
if (ImGui::Checkbox("Use Wii Remotes / Wii U Pro Controllers", &g_wiiRemotesEnabled)) {
|
|
RuntimeConfigFile::SetWiiRemotesEnabled(g_wiiRemotesEnabled);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Takes effect on the next launch. Turn this off if you use a Mayflash DolphinBar.");
|
|
}
|
|
ImGui::TextDisabled("Pairing: Windows Settings > Bluetooth > Add device, then press 1+2");
|
|
ImGui::TextDisabled("(or the red SYNC button) on the remote. Leave the PIN empty.");
|
|
ImGui::TextDisabled("A remote that was paired before also needs to be turned on with 1+2/SYNC.");
|
|
if (ImGui::Checkbox("Keep scanning for Wii Remotes (like Dolphin's Continuous Scanning)",
|
|
&g_wiiContinuousScan)) {
|
|
RuntimeConfigFile::SetWiiContinuousScanEnabled(g_wiiContinuousScan);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("While no Wii controller is connected, re-check Bluetooth every 2 seconds so a\n"
|
|
"remote that dropped out (\"Communications with the controller have been\n"
|
|
"interrupted\") or was turned on after launch comes back by itself.");
|
|
}
|
|
// The driver hint is only read at launch, so a rescan after the user turned
|
|
// the setting off would still re-enumerate Wii devices in this session.
|
|
ImGui::BeginDisabled(!g_wiiRemotesEnabled);
|
|
if (ImGui::Button("Rescan now")) {
|
|
WiiRemoteInput::RescanNow();
|
|
}
|
|
ImGui::EndDisabled();
|
|
ImGui::SameLine();
|
|
if (WiiRemoteInput::IsScanning()) {
|
|
ImGui::TextDisabled("Scanning... (%u so far) - press 1+2 on the remote", WiiRemoteInput::ScanCount());
|
|
} else {
|
|
ImGui::TextDisabled("Not scanning");
|
|
}
|
|
ImGui::Separator();
|
|
|
|
const WiiRemoteInput::Kind kind = WiiRemoteInput::KindForPort(selectedGamePort);
|
|
ImGui::Text("Port %u: %s", static_cast<unsigned>(selectedGamePort + 1), WiiRemoteInput::KindLabel(kind));
|
|
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) {
|
|
WiiRemoteInput::KpadSample sample;
|
|
if (WiiRemoteInput::ReadKpadSample(selectedGamePort, sample)) {
|
|
// WPAD_CL_BUTTON_* bits, in the game's own layout (no mapping involved).
|
|
const auto held = [&](uint32_t bit, const char* on, const char* off) { return (sample.clHold & bit) ? on : off; };
|
|
ImGui::Text("Classic: %s %s %s %s %s %s %s %s %s %s %s %s %s %s", held(0x0010, "A", "a"),
|
|
held(0x0040, "B", "b"), held(0x0008, "X", "x"), held(0x0020, "Y", "y"), held(0x2000, "L", "l"),
|
|
held(0x0200, "R", "r"), held(0x0080, "ZL", "zl"), held(0x0004, "ZR", "zr"),
|
|
held(0x0400, "PLUS", "plus"), held(0x1000, "MINUS", "minus"), held(0x0001, "UP", "up"),
|
|
held(0x4000, "DOWN", "down"), held(0x0002, "LEFT", "left"), held(0x8000, "RIGHT", "right"));
|
|
ImGui::Text("Sticks: L %+.2f %+.2f (WPAD %+d %+d) R %+.2f %+.2f (WPAD %+d %+d)", sample.clLStick[0],
|
|
sample.clLStick[1], static_cast<int>(sample.clLStickRaw[0]),
|
|
static_cast<int>(sample.clLStickRaw[1]), sample.clRStick[0], sample.clRStick[1],
|
|
static_cast<int>(sample.clRStickRaw[0]), static_cast<int>(sample.clRStickRaw[1]));
|
|
ImGui::TextDisabled("Capitals = held. The game reads this Classic Controller through KPAD, as on the");
|
|
ImGui::TextDisabled("console: its buttons mean what the game says they mean, no mapping applies.");
|
|
}
|
|
}
|
|
if (kind == WiiRemoteInput::Kind::WiiUPro) {
|
|
if (SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(selectedGamePort))) {
|
|
// SDL's Wii driver posts the D-pad as joystick buttons 11-14 (the
|
|
// SDL_GAMEPAD_BUTTON_DPAD_* values) while its default HIDAPI mapping
|
|
// expects a hat, so SDL_GetGamepadButton never sees them; read the
|
|
// joystick directly, like the fallback in aurora's PADRead does.
|
|
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
|
|
const auto rawButton = [&](int index) {
|
|
return joystick != nullptr && SDL_GetJoystickButton(joystick, index);
|
|
};
|
|
ImGui::Text("Raw D-pad: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_DPAD_UP) ? "UP" : "up",
|
|
rawButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN) ? "DOWN" : "down",
|
|
rawButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT) ? "LEFT" : "left",
|
|
rawButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT) ? "RIGHT" : "right");
|
|
ImGui::Text("Raw face buttons: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_EAST) ? "A" : "a",
|
|
rawButton(SDL_GAMEPAD_BUTTON_SOUTH) ? "B" : "b", rawButton(SDL_GAMEPAD_BUTTON_NORTH) ? "X" : "x",
|
|
rawButton(SDL_GAMEPAD_BUTTON_WEST) ? "Y" : "y");
|
|
ImGui::Text("Raw ZL/ZR: %d / %d (pressed above 0)",
|
|
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER),
|
|
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER));
|
|
ImGui::TextDisabled("Capitals = held. If a button never turns to capitals while physically held,");
|
|
ImGui::TextDisabled("that press is not reaching SDL at all (a driver-level issue, not a mapping one).");
|
|
ImGui::TextDisabled("This pad uses Nintendo's own layout (a/b/x/y as labelled); the shared");
|
|
ImGui::TextDisabled("button mapping above does not apply to it.");
|
|
}
|
|
}
|
|
if (kind == WiiRemoteInput::Kind::Remote || kind == WiiRemoteInput::Kind::RemoteWithNunchuk ||
|
|
kind == WiiRemoteInput::Kind::RemoteWithClassic) {
|
|
DrawWiiRemoteAccelerometer(selectedGamePort);
|
|
}
|
|
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
// Controller settings menu: port selection, controller assignment and button mapping.
|
|
int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) {
|
|
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
|
|
auto* text = static_cast<std::string*>(data->UserData);
|
|
text->resize(static_cast<size_t>(data->BufTextLen));
|
|
data->Buf = text->data();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void DrawExpressionSettings() {
|
|
ImGui::SeparatorText("Expressions (Dolphin syntax)");
|
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 440.0f);
|
|
ImGui::TextDisabled(
|
|
"Optional. An expression overrides nothing: its result is combined with the "
|
|
"button mapping above. Operators ! & | ^ and functions if, min, max, clamp, "
|
|
"timer, toggle, hold, tap, pulse, smooth, deadzone behave as they do in Dolphin.");
|
|
ImGui::PopTextWrapPos();
|
|
|
|
static std::array<std::string, InputBindings::kControls.size()> errors;
|
|
static std::array<std::string, InputBindings::kControls.size()> buffers;
|
|
static std::string importStatus;
|
|
static int loadedPort = -1;
|
|
static bool reloadBuffers = true;
|
|
const auto port = static_cast<uint32_t>(g_controllerPort);
|
|
|
|
if (loadedPort != g_controllerPort || reloadBuffers) {
|
|
for (size_t i = 0; i < buffers.size(); ++i) {
|
|
buffers[i] = InputBindings::GetExpression(port, i);
|
|
}
|
|
errors.fill(std::string());
|
|
loadedPort = g_controllerPort;
|
|
reloadBuffers = false;
|
|
}
|
|
|
|
if (ImGui::Button("Import from Dolphin")) {
|
|
const std::string path = InputBindings::DefaultDolphinConfigPath();
|
|
std::string summary;
|
|
std::string error;
|
|
if (InputBindings::ImportDolphinConfig(path, g_controllerPort + 1, port, summary, error) < 0) {
|
|
importStatus = error;
|
|
} else {
|
|
importStatus = summary;
|
|
errors.fill(std::string());
|
|
reloadBuffers = true;
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Reads [GCPad%d] from %%APPDATA%%\\Dolphin Emulator\\Config\\GCPadNew.ini,\n"
|
|
"or GCPadNew.ini next to the executable.", g_controllerPort + 1);
|
|
}
|
|
if (!importStatus.empty()) {
|
|
ImGui::TextDisabled("%s", importStatus.c_str());
|
|
}
|
|
|
|
for (size_t i = 0; i < InputBindings::kControls.size(); ++i) {
|
|
ImGui::PushID(static_cast<int>(i) + 2000);
|
|
std::string& text = buffers[i];
|
|
ImGui::SetNextItemWidth(300.0f);
|
|
if (ImGui::InputText(InputBindings::kControls[i].label, text.data(), text.capacity() + 1,
|
|
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackResize,
|
|
ExpressionResizeCallback, &text)) {
|
|
std::string error;
|
|
errors[i] = InputBindings::SetExpression(port, i, text, error) ? std::string() : error;
|
|
}
|
|
if (InputBindings::IsActive(port, i)) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "active");
|
|
}
|
|
if (!errors[i].empty()) {
|
|
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.3f, 1.0f), "%s", errors[i].c_str());
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
|
|
void DrawRumbleSettings() {
|
|
ImGui::SeparatorText("Vibration");
|
|
if (ImGui::Checkbox("Controller vibration", &g_rumbleEnabled)) {
|
|
PAD_HLE_SetRumbleEnabled(g_rumbleEnabled);
|
|
RuntimeConfigFile::SetRumbleEnabled(g_rumbleEnabled);
|
|
if (!g_rumbleEnabled) {
|
|
// Stop whatever is already running: the game will not send another
|
|
// motor command until its own state machine decides to.
|
|
constexpr std::array<uint32_t, PAD_MAX_CONTROLLERS> stopAll{
|
|
PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD,
|
|
};
|
|
PADControlAllMotors(stopAll.data());
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Applies to every port.");
|
|
}
|
|
}
|
|
|
|
void DrawControllerSettings() {
|
|
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
|
const std::string label = "Port " + std::to_string(port + 1);
|
|
ImGui::RadioButton(label.c_str(), &g_controllerPort, port);
|
|
if (port + 1 < PAD_MAX_CONTROLLERS) {
|
|
ImGui::SameLine();
|
|
}
|
|
}
|
|
|
|
ImGui::Separator();
|
|
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
|
|
const char* currentName = PADGetName(selectedGamePort);
|
|
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
|
|
if (ImGui::MenuItem("Unassign controller")) {
|
|
PADClearPort(selectedGamePort);
|
|
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
|
}
|
|
ImGui::Separator();
|
|
controller_mapping_wizard::DrawSetupList();
|
|
DrawWiiRemoteSettings(selectedGamePort);
|
|
const uint32_t controllerCount = PADCount();
|
|
if (controllerCount == 0) {
|
|
ImGui::TextDisabled("No controller connected");
|
|
return;
|
|
}
|
|
|
|
if (ImGui::BeginMenu("Assign connected controller")) {
|
|
for (uint32_t index = 0; index < controllerCount; ++index) {
|
|
const char* name = PADGetNameForControllerIndex(index);
|
|
ImGui::PushID(static_cast<int>(index));
|
|
if (ImGui::MenuItem(name != nullptr ? name : "Unknown controller")) {
|
|
PADSetPortForIndex(index, selectedGamePort);
|
|
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
|
ApplyConfiguredMappings();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
uint32_t mappingCount = 0;
|
|
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
|
|
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
|
|
ImGui::TextDisabled("Assign a controller to edit its buttons");
|
|
return;
|
|
}
|
|
|
|
uint32_t altMappingCount = 0;
|
|
PADButtonMapping* altMappings =
|
|
PADGetAltButtonMappings(static_cast<uint32_t>(g_controllerPort), &altMappingCount);
|
|
|
|
const auto writeBinding = [](size_t index, uint32_t primaryNative, uint32_t altNative) {
|
|
std::string value = NativeButtonForValue(primaryNative).configName;
|
|
if (altNative != PAD_NATIVE_BUTTON_INVALID) {
|
|
value += ',';
|
|
value += NativeButtonForValue(altNative).configName;
|
|
}
|
|
RuntimeConfigFile::SetControllerButton(index, value);
|
|
};
|
|
|
|
// Which rows show the second-binding combo without one being bound yet;
|
|
// reset when the user switches ports so a stale "+" click doesn't linger.
|
|
static std::array<bool, PAD_BUTTON_COUNT> altRowExpanded{};
|
|
static int altRowExpandedPort = -1;
|
|
if (altRowExpandedPort != g_controllerPort) {
|
|
altRowExpandedPort = g_controllerPort;
|
|
altRowExpanded.fill(false);
|
|
}
|
|
|
|
ImGui::SeparatorText("Presets");
|
|
if (ImGui::Button("GameCube")) {
|
|
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
|
PADRestoreDefaultMapping(port);
|
|
uint32_t restoredCount = 0;
|
|
if (PADButtonMapping* restored = PADGetButtonMappings(port, &restoredCount)) {
|
|
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
|
|
const auto it = std::find_if(restored, restored + restoredCount, [&](const PADButtonMapping& mapping) {
|
|
return mapping.padButton == kControllerButtons[i].padButton;
|
|
});
|
|
if (it != restored + restoredCount) {
|
|
RuntimeConfigFile::SetControllerButton(i, NativeButtonForValue(it->nativeButton).configName);
|
|
}
|
|
}
|
|
}
|
|
altRowExpanded.fill(false);
|
|
PADSerializeMappings();
|
|
mappings = PADGetButtonMappings(port, &mappingCount);
|
|
}
|
|
const auto applyPreset = [&](const std::array<const char*, PAD_BUTTON_COUNT>& preset) {
|
|
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
|
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
|
|
if (const NativeButtonItem* native = FindNativeButton(preset[i])) {
|
|
PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton});
|
|
PADSetAltButtonMapping(port,
|
|
PADButtonMapping{PAD_NATIVE_BUTTON_INVALID, kControllerButtons[i].padButton});
|
|
RuntimeConfigFile::SetControllerButton(i, preset[i]);
|
|
}
|
|
}
|
|
altRowExpanded.fill(false);
|
|
PADSerializeMappings();
|
|
mappings = PADGetButtonMappings(port, &mappingCount);
|
|
};
|
|
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Classic Controller Pro")) {
|
|
applyPreset(kClassicProPreset);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("PlayStation")) {
|
|
applyPreset(kPlayStationPreset);
|
|
}
|
|
|
|
ImGui::SeparatorText("Button mapping");
|
|
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
|
|
auto mappingIt = std::find_if(mappings, mappings + mappingCount, [&](const PADButtonMapping& mapping) {
|
|
return mapping.padButton == kControllerButtons[i].padButton;
|
|
});
|
|
if (mappingIt == mappings + mappingCount) {
|
|
continue;
|
|
}
|
|
PADButtonMapping* altIt = nullptr;
|
|
if (altMappings != nullptr && altMappingCount == PAD_BUTTON_COUNT) {
|
|
const auto it = std::find_if(altMappings, altMappings + altMappingCount, [&](const PADButtonMapping& mapping) {
|
|
return mapping.padButton == kControllerButtons[i].padButton;
|
|
});
|
|
if (it != altMappings + altMappingCount) {
|
|
altIt = it;
|
|
}
|
|
}
|
|
|
|
const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton);
|
|
ImGui::PushID(static_cast<int>(i));
|
|
ImGui::SetNextItemWidth(190.0f);
|
|
if (ImGui::BeginCombo("##primary", current.label)) {
|
|
for (const auto& candidate : kNativeButtons) {
|
|
const bool selected = candidate.nativeButton == mappingIt->nativeButton;
|
|
if (ImGui::Selectable(candidate.label, selected)) {
|
|
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
|
PADSetButtonMapping(port, PADButtonMapping{candidate.nativeButton, kControllerButtons[i].padButton});
|
|
writeBinding(i, candidate.nativeButton,
|
|
altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID);
|
|
PADSerializeMappings();
|
|
mappings = PADGetButtonMappings(port, &mappingCount);
|
|
}
|
|
if (selected) {
|
|
ImGui::SetItemDefaultFocus();
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
if (altIt != nullptr) {
|
|
const bool altBound = altIt->nativeButton != PAD_NATIVE_BUTTON_INVALID;
|
|
if (!altBound && !altRowExpanded[i]) {
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("+")) {
|
|
altRowExpanded[i] = true;
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Add a second binding; pressing either one works");
|
|
}
|
|
} else {
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted("or");
|
|
ImGui::SameLine();
|
|
const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None";
|
|
ImGui::SetNextItemWidth(190.0f);
|
|
if (ImGui::BeginCombo("##alt", altLabel)) {
|
|
for (const auto& candidate : kNativeButtons) {
|
|
const bool isNone = candidate.nativeButton == PAD_NATIVE_BUTTON_INVALID;
|
|
const bool selected = candidate.nativeButton == altIt->nativeButton;
|
|
if (ImGui::Selectable(isNone ? "None" : candidate.label, selected)) {
|
|
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
|
PADSetAltButtonMapping(
|
|
port, PADButtonMapping{candidate.nativeButton, kControllerButtons[i].padButton});
|
|
writeBinding(i, mappingIt->nativeButton, candidate.nativeButton);
|
|
if (isNone) {
|
|
altRowExpanded[i] = false;
|
|
}
|
|
}
|
|
if (selected) {
|
|
ImGui::SetItemDefaultFocus();
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted(kControllerButtons[i].label);
|
|
ImGui::PopID();
|
|
}
|
|
DrawExpressionSettings();
|
|
DrawRumbleSettings();
|
|
}
|
|
|
|
void DrawAudioSettings() {
|
|
ImGui::SetNextItemWidth(220.0f);
|
|
if (ImGui::SliderInt("Master", &g_audioVolumePercent, 0, 100, "%d%%")) {
|
|
const float volume = static_cast<float>(g_audioVolumePercent) / 100.0f;
|
|
AudioBackend::Instance().SetMasterVolume(volume);
|
|
RuntimeConfigFile::SetAudioVolume(volume);
|
|
}
|
|
if (ImGui::SliderInt("Music", &g_musicVolumePercent, 0, 100, "%d%%")) {
|
|
const float volume = static_cast<float>(g_musicVolumePercent) / 100.0f;
|
|
MusicAttenuation::SetMusicVolume(volume);
|
|
RuntimeConfigFile::SetMusicVolume(volume);
|
|
}
|
|
if (ImGui::SliderInt("Sound Effects", &g_soundEffectsVolumePercent, 0, 100, "%d%%")) {
|
|
const float volume = static_cast<float>(g_soundEffectsVolumePercent) / 100.0f;
|
|
MusicAttenuation::SetSoundEffectsVolume(volume);
|
|
RuntimeConfigFile::SetSoundEffectsVolume(volume);
|
|
}
|
|
if (ImGui::SliderInt("UI", &g_uiVolumePercent, 0, 100, "%d%%")) {
|
|
const float volume = static_cast<float>(g_uiVolumePercent) / 100.0f;
|
|
MusicAttenuation::SetUiVolume(volume);
|
|
RuntimeConfigFile::SetUiVolume(volume);
|
|
}
|
|
if (ImGui::SliderInt("Voices", &g_voicesVolumePercent, 0, 100, "%d%%")) {
|
|
const float volume = static_cast<float>(g_voicesVolumePercent) / 100.0f;
|
|
MusicAttenuation::SetVoicesVolume(volume);
|
|
RuntimeConfigFile::SetVoicesVolume(volume);
|
|
}
|
|
if (ImGui::Checkbox("Mute", &g_audioMuted)) {
|
|
AudioBackend::Instance().SetMuted(g_audioMuted);
|
|
RuntimeConfigFile::SetAudioMuted(g_audioMuted);
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::Checkbox("Mix audio on a worker thread", &g_audioMixWorker)) {
|
|
// Applies immediately: SetMixWorkerEnabled joins any in-flight mix
|
|
// before switching, so the change never lands mid-frame.
|
|
AxDspHle::SetMixWorkerEnabled(g_audioMixWorker);
|
|
RuntimeConfigFile::SetAudioMixWorker(g_audioMixWorker);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip(
|
|
"Runs the AX/DSP voice mix off the game thread. Turn this off if you "
|
|
"suspect an audio problem; the mix then runs inline as it used to.");
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::Checkbox("Mute game music while external media is playing",
|
|
&g_attenuateMusicWhenMediaPlays)) {
|
|
MusicAttenuation::SetEnabled(g_attenuateMusicWhenMediaPlays);
|
|
RuntimeConfigFile::SetAttenuateMusicWhenMediaPlays(g_attenuateMusicWhenMediaPlays);
|
|
}
|
|
if (g_attenuateMusicWhenMediaPlays) {
|
|
if (MusicAttenuation::IsExternalMediaPlaying()) {
|
|
ImGui::TextDisabled("External media is playing; game music is muted.");
|
|
} else if (!MusicAttenuation::IsMediaControlInitializationComplete()) {
|
|
ImGui::TextDisabled("Waiting for media controls...");
|
|
} else if (!MusicAttenuation::IsMediaControlAvailable()) {
|
|
ImGui::TextDisabled("Media controls are unavailable.");
|
|
} else {
|
|
ImGui::TextDisabled("No external media is currently playing.");
|
|
}
|
|
}
|
|
}
|
|
|
|
void DrawGraphicsSettings() {
|
|
g_displayMode = static_cast<int>(aurora_get_display_mode());
|
|
struct EffectFlag {
|
|
const char* label;
|
|
uint32_t flag;
|
|
};
|
|
static constexpr std::array<EffectFlag, 1> kEffectFlags = {{
|
|
{"Disable bloom", 0x10u},
|
|
}};
|
|
|
|
for (const auto& effect : kEffectFlags) {
|
|
bool disabled = (g_disabledPostProcessingPaths & effect.flag) != 0;
|
|
if (ImGui::Checkbox(effect.label, &disabled)) {
|
|
if (disabled) {
|
|
g_disabledPostProcessingPaths |= effect.flag;
|
|
} else {
|
|
g_disabledPostProcessingPaths &= ~effect.flag;
|
|
}
|
|
RuntimeGameGraphicsOptions::SetDisabledPostProcessingPaths(g_disabledPostProcessingPaths);
|
|
RuntimeConfigFile::SetDisabledPostProcessingPaths(g_disabledPostProcessingPaths);
|
|
}
|
|
}
|
|
ImGui::TextDisabled("Applied when the next scene renderer is created.");
|
|
ImGui::Separator();
|
|
static constexpr const char* kDisplayModes[] = {
|
|
"Windowed",
|
|
"Borderless fullscreen",
|
|
"Exclusive fullscreen",
|
|
};
|
|
if (ImGui::Combo("Display mode", &g_displayMode, kDisplayModes, static_cast<int>(std::size(kDisplayModes)))) {
|
|
const auto mode = static_cast<AuroraDisplayMode>(g_displayMode);
|
|
aurora_set_display_mode(mode);
|
|
const AuroraDisplayMode activeMode = aurora_get_display_mode();
|
|
if (activeMode == mode) {
|
|
RuntimeConfigFile::SetDisplayMode(std::string(kDisplayModeConfigNames[static_cast<size_t>(g_displayMode)]));
|
|
} else {
|
|
g_displayMode = static_cast<int>(activeMode);
|
|
}
|
|
}
|
|
if (g_displayMode == AURORA_DISPLAY_MODE_EXCLUSIVE) {
|
|
ImGui::TextDisabled(
|
|
"Requests the closest native-resolution display mode to the output frame "
|
|
"rate (60 Hz, or the frame interpolation target).");
|
|
}
|
|
constexpr std::array<const char*, 3> kFrameInterpolationModes{
|
|
"Off", "120 FPS", "180 FPS",
|
|
};
|
|
const char* currentFrameInterpolationMode =
|
|
kFrameInterpolationModes[static_cast<size_t>(g_frameInterpolationMode)];
|
|
bool frameInterpolationModeChanged = false;
|
|
if (ImGui::BeginCombo("Race frame interpolation (experimental)", currentFrameInterpolationMode)) {
|
|
for (int mode = 0; mode < static_cast<int>(kFrameInterpolationModes.size()); ++mode) {
|
|
const bool selected = g_frameInterpolationMode == mode;
|
|
if (ImGui::Selectable(kFrameInterpolationModes[static_cast<size_t>(mode)], selected)) {
|
|
g_frameInterpolationMode = mode;
|
|
frameInterpolationModeChanged = true;
|
|
}
|
|
if (selected) {
|
|
ImGui::SetItemDefaultFocus();
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
if (frameInterpolationModeChanged) {
|
|
const uint32_t targetFps = kFrameInterpolationTargetFps[static_cast<size_t>(g_frameInterpolationMode)];
|
|
aurora_set_frame_interpolation_fps(targetFps);
|
|
RuntimeConfigFile::SetFrameInterpolationFps(targetFps);
|
|
LimitResolutionForFrameRate();
|
|
if (aurora_get_display_mode() == AURORA_DISPLAY_MODE_EXCLUSIVE) {
|
|
// Re-apply exclusive mode so the display refresh tracks the new target.
|
|
aurora_set_display_mode(AURORA_DISPLAY_MODE_EXCLUSIVE);
|
|
}
|
|
}
|
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
|
ImGui::TextDisabled("Frame interpolation is experimental, you might find visual artifacts");
|
|
ImGui::PopTextWrapPos();
|
|
if (ImGui::Checkbox("Disable copy filter", &g_disableCopyFilter)) {
|
|
aurora_set_disable_copy_filter(g_disableCopyFilter);
|
|
RuntimeConfigFile::SetDisableCopyFilter(g_disableCopyFilter);
|
|
}
|
|
if (ImGui::Checkbox("Skip draws while shaders compile", &g_skipUnreadyPipelines)) {
|
|
aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines);
|
|
RuntimeConfigFile::SetSkipUnreadyPipelines(g_skipUnreadyPipelines);
|
|
}
|
|
if (ImGui::Checkbox("Show FPS", &g_showFps)) {
|
|
RuntimeConfigFile::SetShowFps(g_showFps);
|
|
}
|
|
ImGui::Separator();
|
|
ImGui::Text("Graphics API: %s", GraphicsApiDisplayName());
|
|
}
|
|
|
|
void DrawFpsOverlay() {
|
|
AuroraPresentTiming presentTiming{};
|
|
aurora_get_present_timing(&presentTiming);
|
|
if (!g_showFps) {
|
|
return;
|
|
}
|
|
|
|
const ImGuiIO& io = ImGui::GetIO();
|
|
constexpr float kMargin = 10.0f;
|
|
const float top = g_topBarVisible ? ImGui::GetFrameHeight() + kMargin : kMargin;
|
|
ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x - kMargin, top), ImGuiCond_Always, ImVec2(1.0f, 0.0f));
|
|
ImGui::SetNextWindowBgAlpha(0.55f);
|
|
constexpr ImGuiWindowFlags kFlags = ImGuiWindowFlags_AlwaysAutoResize |
|
|
ImGuiWindowFlags_NoDecoration |
|
|
ImGuiWindowFlags_NoFocusOnAppearing |
|
|
ImGuiWindowFlags_NoInputs |
|
|
ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoNav |
|
|
ImGuiWindowFlags_NoSavedSettings;
|
|
if (ImGui::Begin("FPS Overlay", nullptr, kFlags)) {
|
|
if (presentTiming.sampleCount == 0) {
|
|
ImGui::TextUnformatted("FPS: --");
|
|
} else {
|
|
// Present timing includes the additional frames produced by
|
|
// interpolation, so this remains the actual displayed FPS.
|
|
ImGui::Text("FPS: %.1f", presentTiming.framesPerSecond);
|
|
// Replay-unsafe frames hold the presented cadence with duplicated
|
|
// slots, so the counter alone reads 180 while the motion on screen
|
|
// is 60 Hz. Surface the divergence instead of hiding it.
|
|
if (presentTiming.effectiveFramesPerSecond <
|
|
presentTiming.framesPerSecond * 0.95) {
|
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "Motion: %.1f",
|
|
presentTiming.effectiveFramesPerSecond);
|
|
}
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void DrawShaderCompilationStatus() {
|
|
const uint32_t queuedPipelines = aurora_get_queued_pipeline_count();
|
|
if (queuedPipelines == 0) {
|
|
return;
|
|
}
|
|
|
|
constexpr float kMargin = 10.0f;
|
|
const float top = g_topBarVisible ? ImGui::GetFrameHeight() + kMargin : kMargin;
|
|
ImGui::SetNextWindowPos(ImVec2(kMargin, top), ImGuiCond_Always);
|
|
ImGui::SetNextWindowBgAlpha(0.55f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(7.0f, 4.0f));
|
|
constexpr ImGuiWindowFlags kFlags = ImGuiWindowFlags_AlwaysAutoResize |
|
|
ImGuiWindowFlags_NoDecoration |
|
|
ImGuiWindowFlags_NoFocusOnAppearing |
|
|
ImGuiWindowFlags_NoInputs |
|
|
ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoNav |
|
|
ImGuiWindowFlags_NoSavedSettings;
|
|
if (ImGui::Begin("Shader Compilation Status", nullptr, kFlags)) {
|
|
ImGui::SetWindowFontScale(0.85f);
|
|
ImGui::Text("%u shader%s compiling", queuedPipelines, queuedPipelines == 1 ? "" : "s");
|
|
}
|
|
ImGui::End();
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
void DrawStartupScreen() {
|
|
if (!StartupScreenVisible()) {
|
|
return;
|
|
}
|
|
|
|
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
|
ImGui::SetNextWindowPos(viewport->Pos, ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(viewport->Size, ImGuiCond_Always);
|
|
ImGui::SetNextWindowBgAlpha(1.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, IM_COL32(0, 0, 0, 255));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
|
constexpr ImGuiWindowFlags kFlags = ImGuiWindowFlags_NoDecoration |
|
|
ImGuiWindowFlags_NoFocusOnAppearing |
|
|
ImGuiWindowFlags_NoInputs |
|
|
ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoNav |
|
|
ImGuiWindowFlags_NoSavedSettings |
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
if (ImGui::Begin("Wiicompiled Startup", nullptr, kFlags)) {
|
|
ImGui::SetWindowFontScale(1.25f);
|
|
constexpr const char* kTitle = "WiiCompiled";
|
|
const ImVec2 titleSize = ImGui::CalcTextSize(kTitle);
|
|
const float titleX = std::max(0.0f, (viewport->Size.x - titleSize.x) * 0.5f);
|
|
const float startY = std::max(0.0f, (viewport->Size.y - titleSize.y) * 0.5f);
|
|
ImGui::SetCursorPos(ImVec2(titleX, startY));
|
|
ImGui::TextUnformatted(kTitle);
|
|
}
|
|
ImGui::End();
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
void DrawTopBar() {
|
|
if (!g_topBarVisible || !ImGui::BeginMainMenuBar()) {
|
|
return;
|
|
}
|
|
|
|
ImGui::TextUnformatted("WiiCompiled");
|
|
ImGui::Separator();
|
|
const auto resolutionIt = std::find_if(kResolutions.begin(), kResolutions.end(), [](const ResolutionItem& item) {
|
|
return std::fabs(item.scale - g_resolutionScale) < 0.001f;
|
|
});
|
|
const char* resolutionLabel = resolutionIt != kResolutions.end() ? resolutionIt->label : "Custom";
|
|
const std::string resolutionMenuLabel = std::string("Resolution: ") + resolutionLabel;
|
|
if (ImGui::BeginMenu(resolutionMenuLabel.c_str())) {
|
|
for (const auto& resolution : kResolutions) {
|
|
const bool selected = std::fabs(resolution.scale - g_resolutionScale) < 0.001f;
|
|
const bool disabled = IsHighFrameRateMode() && IsHighResolutionScale(resolution.scale);
|
|
ImGui::BeginDisabled(disabled);
|
|
const bool clicked = ImGui::MenuItem(resolution.label, nullptr, selected);
|
|
ImGui::EndDisabled();
|
|
if (clicked) {
|
|
SetResolutionScale(resolution.scale);
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::BeginMenu("Graphics")) {
|
|
DrawGraphicsSettings();
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::BeginMenu("Controller settings")) {
|
|
DrawControllerSettings();
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
const std::string audioLabel = g_audioMuted
|
|
? "Audio: Muted"
|
|
: "Audio: " + std::to_string(g_audioVolumePercent) + "%";
|
|
// Keep the popup ID stable while the Master slider changes the visible
|
|
// label. Without the ### suffix, ImGui treats every new percentage as a
|
|
// different menu and closes the popup on the first drag update.
|
|
const std::string audioMenuLabel = audioLabel + "###AudioSettingsMenu";
|
|
if (ImGui::BeginMenu(audioMenuLabel.c_str())) {
|
|
DrawAudioSettings();
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
const float hideWidth = ImGui::CalcTextSize("Hide (F10)").x + ImGui::GetStyle().FramePadding.x * 2.0f;
|
|
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() - hideWidth - 8.0f));
|
|
if (ImGui::MenuItem("Hide (F10)")) {
|
|
SetTopBarVisible(false);
|
|
}
|
|
ImGui::EndMainMenuBar();
|
|
}
|
|
|
|
bool IsToggleKey(const SDL_Event& event, SDL_Scancode code) {
|
|
return event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && event.key.scancode == code;
|
|
}
|
|
|
|
bool IsMouseActivity(const SDL_Event& event) {
|
|
switch (event.type) {
|
|
case SDL_EVENT_MOUSE_MOTION:
|
|
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
|
case SDL_EVENT_MOUSE_BUTTON_UP:
|
|
case SDL_EVENT_MOUSE_WHEEL:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Runs on the thread that pumps SDL events (the same one that calls Draw), so
|
|
// the SDL cursor calls are safe here.
|
|
void UpdateCursorAutoHide() {
|
|
const bool shouldHide =
|
|
!g_topBarVisible && Clock::now() - g_lastMouseActivity >= kCursorAutoHideDelay;
|
|
if (shouldHide == g_cursorHidden) {
|
|
return;
|
|
}
|
|
g_cursorHidden = shouldHide;
|
|
if (shouldHide) {
|
|
SDL_HideCursor();
|
|
} else {
|
|
SDL_ShowCursor();
|
|
}
|
|
}
|
|
|
|
// Alt+Enter toggles the display mode inside aurora without going through the
|
|
// F10 combo, so the active mode is compared against the last persisted one
|
|
// every frame and written back on change.
|
|
void PersistDisplayModeIfChanged() {
|
|
const int active = static_cast<int>(aurora_get_display_mode());
|
|
if (active == g_displayMode) {
|
|
return;
|
|
}
|
|
g_displayMode = active;
|
|
RuntimeConfigFile::SetDisplayMode(std::string(kDisplayModeConfigNames[static_cast<size_t>(active)]));
|
|
}
|
|
} // namespace
|
|
|
|
void InitializeRuntimeSettings() noexcept {
|
|
PAD_HLE_SetRumbleEnabled(g_rumbleEnabled);
|
|
InputBindings::Reload();
|
|
controller_mapping_wizard::LoadPersistedMappings();
|
|
ApplyConfiguredMappings();
|
|
AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f);
|
|
AudioBackend::Instance().SetMuted(g_audioMuted);
|
|
MusicAttenuation::SetMusicVolume(static_cast<float>(g_musicVolumePercent) / 100.0f);
|
|
MusicAttenuation::SetSoundEffectsVolume(static_cast<float>(g_soundEffectsVolumePercent) / 100.0f);
|
|
MusicAttenuation::SetUiVolume(static_cast<float>(g_uiVolumePercent) / 100.0f);
|
|
MusicAttenuation::SetVoicesVolume(static_cast<float>(g_voicesVolumePercent) / 100.0f);
|
|
MusicAttenuation::SetEnabled(g_attenuateMusicWhenMediaPlays);
|
|
RuntimeGameGraphicsOptions::SetDisabledPostProcessingPaths(g_disabledPostProcessingPaths);
|
|
const uint32_t targetFps = kFrameInterpolationTargetFps[static_cast<size_t>(g_frameInterpolationMode)];
|
|
LimitResolutionForFrameRate();
|
|
aurora_set_frame_interpolation_fps(targetFps);
|
|
aurora_set_display_mode(static_cast<AuroraDisplayMode>(g_displayMode));
|
|
g_displayMode = static_cast<int>(aurora_get_display_mode());
|
|
aurora_set_disable_copy_filter(g_disableCopyFilter);
|
|
aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines);
|
|
g_strapInputAccepted.store(false, std::memory_order_relaxed);
|
|
g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed);
|
|
PADBlockInput(false);
|
|
InputBindings::SetInputBlocked(false);
|
|
}
|
|
|
|
void HandleEvents(const AuroraEvent* events) noexcept {
|
|
if (!events) {
|
|
return;
|
|
}
|
|
for (const AuroraEvent* ev = events; ev->type != AURORA_NONE; ++ev) {
|
|
if (ev->type == AURORA_CONTROLLER_ADDED || ev->type == AURORA_CONTROLLER_REMOVED) {
|
|
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
|
}
|
|
if (ev->type != AURORA_SDL_EVENT) {
|
|
continue;
|
|
}
|
|
controller_mapping_wizard::HandleSdlEvent(ev->sdl);
|
|
if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
|
|
SetTopBarVisible(!g_topBarVisible);
|
|
}
|
|
if (IsMouseActivity(ev->sdl)) {
|
|
g_lastMouseActivity = Clock::now();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Draw() noexcept {
|
|
// Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists
|
|
// and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands.
|
|
aurora_wait_for_frame_worker();
|
|
// Also drive the Wii Remote rescan from here: PADRead runs it too, but this
|
|
// runs once per presented frame whatever the game is doing (e.g. sitting in
|
|
// its "communications interrupted" prompt without polling pads). Same guest
|
|
// thread as PADRead, so no concurrent access to the scanner's state.
|
|
WiiRemoteInput::Poll();
|
|
ApplyConfiguredMappings();
|
|
PersistDisplayModeIfChanged();
|
|
UpdateCursorAutoHide();
|
|
if (!StartupScreenVisible()) {
|
|
DrawShaderCompilationStatus();
|
|
}
|
|
DrawFpsOverlay();
|
|
DrawTopBar();
|
|
controller_mapping_wizard::Draw();
|
|
// The wizard captures raw presses; keep them out of the game.
|
|
const bool inputBlocked = controller_mapping_wizard::IsActive();
|
|
PADBlockInput(inputBlocked);
|
|
InputBindings::SetInputBlocked(inputBlocked);
|
|
DrawStartupScreen();
|
|
}
|
|
|
|
bool StartupScreenVisible() noexcept {
|
|
return !g_strapInputAccepted.load(std::memory_order_acquire) ||
|
|
g_presentedFrame < g_startupDismissFrame.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
void NotifyStrapInputAccepted() noexcept {
|
|
bool expected = false;
|
|
if (g_strapInputAccepted.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
|
|
g_startupDismissFrame.store(g_presentedFrame + kStrapTransitionCoverFrames,
|
|
std::memory_order_release);
|
|
}
|
|
}
|
|
|
|
void AdvancePresentedFrame() noexcept { ++g_presentedFrame; }
|
|
} // namespace settings_overlay
|