mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 03:00:14 +02:00
1251 lines
57 KiB
C++
1251 lines
57 KiB
C++
#include "settings_overlay.h"
|
|
#include "audio_backend.h"
|
|
#include "controller_mapping_wizard.h"
|
|
#include "game_graphics_options.h"
|
|
#include "music_attenuation.h"
|
|
#include "runtime_config.h"
|
|
#include "runtime_log.h"
|
|
#include "vr/mkw_vr_first_person.h"
|
|
#include "vr/mkw_vr_policy.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>
|
|
#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;
|
|
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);
|
|
bool g_vrEnabled = RuntimeConfigFile::VrEnabled(false);
|
|
bool g_vrStopAtDisplayCopy = RuntimeConfigFile::VrStopAtDisplayCopy(true);
|
|
bool g_vrSkipCopyClears = RuntimeConfigFile::VrSkipCopyClears(true);
|
|
bool g_vrHudVirtualScreen = RuntimeConfigFile::VrHudVirtualScreen(true);
|
|
bool g_vrFirstPerson = RuntimeConfigFile::VrFirstPerson(false);
|
|
float g_vrFirstPersonUnitsPerMeter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter();
|
|
float g_vrFirstPersonHeadUp = RuntimeConfigFile::VrFirstPersonHeadUpMeters();
|
|
float g_vrFirstPersonHeadForward = RuntimeConfigFile::VrFirstPersonHeadForwardMeters();
|
|
float g_vrFirstPersonHeadRight = RuntimeConfigFile::VrFirstPersonHeadRightMeters();
|
|
bool g_vrFirstPersonHideDriver = RuntimeConfigFile::VrFirstPersonHideDriver();
|
|
int g_vrFirstPersonHiddenModel = RuntimeConfigFile::VrFirstPersonHiddenModel();
|
|
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;
|
|
}();
|
|
|
|
struct ControllerButtonItem {
|
|
const char* configKey;
|
|
const char* label;
|
|
PADButton padButton;
|
|
};
|
|
|
|
constexpr std::array<ControllerButtonItem, PAD_BUTTON_COUNT> kControllerButtons = {{
|
|
{"a", "A", PAD_BUTTON_A},
|
|
{"b", "B", PAD_BUTTON_B},
|
|
{"x", "X", PAD_BUTTON_X},
|
|
{"y", "Y", PAD_BUTTON_Y},
|
|
{"start", "Start", PAD_BUTTON_START},
|
|
{"z", "Z", PAD_TRIGGER_Z},
|
|
{"l", "L", PAD_TRIGGER_L},
|
|
{"r", "R", PAD_TRIGGER_R},
|
|
{"up", "D-pad Up", PAD_BUTTON_UP},
|
|
{"down", "D-pad Down", PAD_BUTTON_DOWN},
|
|
{"left", "D-pad Left", PAD_BUTTON_LEFT},
|
|
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
|
|
}};
|
|
|
|
struct NativeButtonItem {
|
|
const char* configName;
|
|
const char* label;
|
|
uint32_t nativeButton;
|
|
};
|
|
|
|
constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{
|
|
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID},
|
|
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
|
|
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
|
|
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
|
|
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
|
|
{"back", "Back / Select", SDL_GAMEPAD_BUTTON_BACK},
|
|
{"guide", "Guide / Home", SDL_GAMEPAD_BUTTON_GUIDE},
|
|
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
|
|
{"left_stick", "Left stick click", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
|
{"right_stick", "Right stick click", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
|
{"left_shoulder", "Left shoulder", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
|
{"right_shoulder", "Right shoulder", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
|
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
|
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
|
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
|
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
|
{"misc1", "Misc 1 / Share", SDL_GAMEPAD_BUTTON_MISC1},
|
|
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
|
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
|
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
|
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
|
{"touchpad", "Touchpad", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
|
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
|
|
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
|
|
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
|
|
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
|
|
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
|
|
}};
|
|
|
|
// 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",
|
|
};
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
const NativeButtonItem* FindNativeButton(std::string value) {
|
|
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), [&](const NativeButtonItem& item) {
|
|
return value == item.configName;
|
|
});
|
|
return it == kNativeButtons.end() ? nullptr : &*it;
|
|
}
|
|
|
|
struct ControllerBindingPair {
|
|
std::string primary;
|
|
std::string secondary;
|
|
};
|
|
|
|
std::string TrimBindingToken(const std::string& token) {
|
|
const size_t begin = token.find_first_not_of(" \t");
|
|
if (begin == std::string::npos) {
|
|
return {};
|
|
}
|
|
const size_t end = token.find_last_not_of(" \t");
|
|
return token.substr(begin, end - begin + 1);
|
|
}
|
|
|
|
// 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 {TrimBindingToken(value), {}};
|
|
}
|
|
return {TrimBindingToken(value.substr(0, comma)), TrimBindingToken(value.substr(comma + 1))};
|
|
}
|
|
|
|
const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
|
|
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), [&](const NativeButtonItem& item) {
|
|
return nativeButton == item.nativeButton;
|
|
});
|
|
return it == kNativeButtons.end() ? kNativeButtons.front() : *it;
|
|
}
|
|
|
|
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(true);
|
|
|
|
// 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.
|
|
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);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Classic Controller Pro")) {
|
|
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
|
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
|
|
if (const NativeButtonItem* native = FindNativeButton(kClassicProPreset[i])) {
|
|
PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton});
|
|
PADSetAltButtonMapping(port,
|
|
PADButtonMapping{PAD_NATIVE_BUTTON_INVALID, kControllerButtons[i].padButton});
|
|
RuntimeConfigFile::SetControllerButton(i, kClassicProPreset[i]);
|
|
}
|
|
}
|
|
altRowExpanded.fill(false);
|
|
PADSerializeMappings();
|
|
mappings = PADGetButtonMappings(port, &mappingCount);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
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.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// The virtual screen's placement comes from the launch-time [vr] geometry, the
|
|
// same metres the menu quad is built from, converted into the world units the
|
|
// eye replay works in. Those units follow the camera: the first-person view
|
|
// renders at its own scale, and the screen has to be sized at the same one or
|
|
// it would not stay 2 m across in front of the player.
|
|
void ApplyVrHudVirtualScreen() {
|
|
const float unitsPerMeter = mkw::vr::MkwVRPolicyGetSnapshot().EffectiveUnitsPerMeter();
|
|
aurora_set_stereo_hud_screen(g_vrHudVirtualScreen,
|
|
RuntimeConfigFile::VrHudWidthMeters(2.4f) * unitsPerMeter,
|
|
RuntimeConfigFile::VrHudDistanceMeters(2.0f) * unitsPerMeter);
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
// The VR settings live in their own top-bar menu: they are a self-contained
|
|
// group, and keeping them out of Graphics stops that menu from running off the
|
|
// bottom of the screen.
|
|
void DrawVrSettings() {
|
|
if (ImGui::Checkbox("Enable OpenXR VR", &g_vrEnabled)) {
|
|
RuntimeConfigFile::SetVrEnabled(g_vrEnabled);
|
|
}
|
|
ImGui::TextDisabled("OpenXR mode changes take effect after restarting the game.");
|
|
|
|
// Unlike the toggle above, these two apply to the very next frame, so they
|
|
// can be compared against each other from inside a running race.
|
|
ImGui::Separator();
|
|
ImGui::Text("VR eye replay (EFB)");
|
|
if (ImGui::Checkbox("Stop eye at display copy", &g_vrStopAtDisplayCopy)) {
|
|
aurora_set_stereo_stop_at_display_copy(g_vrStopAtDisplayCopy);
|
|
RuntimeConfigFile::SetVrStopAtDisplayCopy(g_vrStopAtDisplayCopy);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip(
|
|
"Ends each eye at the frame's final GXCopyDisp, so an eye holds exactly the "
|
|
"image the game presented. Turn off to replay the whole pass list.");
|
|
}
|
|
if (ImGui::Checkbox("Skip EFB copy clears", &g_vrSkipCopyClears)) {
|
|
aurora_set_stereo_skip_copy_clears(g_vrSkipCopyClears);
|
|
RuntimeConfigFile::SetVrSkipCopyClears(g_vrSkipCopyClears);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip(
|
|
"Drops the EFB reset a GX copy performs after copying. That reset prepares the "
|
|
"Wii's reused EFB for the next frame; an eye attachment is built fresh, so "
|
|
"replaying it only erases the eye.");
|
|
}
|
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
|
ImGui::TextDisabled(
|
|
"Both apply on the next frame. Turning either off restores the raw replay and is "
|
|
"expected to black out the eyes.");
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::Separator();
|
|
ImGui::Text("VR 2D layer");
|
|
if (ImGui::Checkbox("2D layer on a virtual screen", &g_vrHudVirtualScreen)) {
|
|
ApplyVrHudVirtualScreen();
|
|
RuntimeConfigFile::SetVrHudVirtualScreen(g_vrHudVirtualScreen);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip(
|
|
"Puts the minimap, race position, item roulette and the rest of the race HUD on a "
|
|
"screen fixed ahead of the kart camera. Turn off to leave them stretched across "
|
|
"the whole view. Its size and distance are the [vr] hud_width_meters and "
|
|
"hud_distance_meters read at launch.");
|
|
}
|
|
ImGui::Separator();
|
|
ImGui::Text("VR camera");
|
|
if (ImGui::Checkbox("First-person camera", &g_vrFirstPerson)) {
|
|
RuntimeConfigFile::SetVrFirstPerson(g_vrFirstPerson);
|
|
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip(
|
|
"Moves the camera to the Player 1 driver's head and keeps the horizon level, "
|
|
"instead of riding behind the kart. Applies during a single-screen race; menus "
|
|
"and split-screen are unaffected. The world scale below replaces "
|
|
"world_units_per_meter while it is engaged.");
|
|
}
|
|
// These are the tuning loop for the anchor: the right head height is a
|
|
// per-taste value that can only really be judged from inside the headset.
|
|
if (ImGui::SliderFloat("World units per metre (first person)", &g_vrFirstPersonUnitsPerMeter,
|
|
1.0f, 200.0f, "%.1f")) {
|
|
RuntimeConfigFile::SetVrFirstPersonUnitsPerMeter(g_vrFirstPersonUnitsPerMeter);
|
|
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
|
// The virtual screen's metres are converted at this same scale.
|
|
ApplyVrHudVirtualScreen();
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip(
|
|
"Mario Kart Wii is authored at about 10 units per metre, which is what makes the "
|
|
"race read life-size. Raising this shrinks the world around you.");
|
|
}
|
|
bool headOffsetsChanged = false;
|
|
headOffsetsChanged |=
|
|
ImGui::SliderFloat("Head height (m)", &g_vrFirstPersonHeadUp, -RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, "%.2f");
|
|
headOffsetsChanged |=
|
|
ImGui::SliderFloat("Head forward (m)", &g_vrFirstPersonHeadForward, -20.0f, 20.0f, "%.2f");
|
|
headOffsetsChanged |=
|
|
ImGui::SliderFloat("Head sideways (m)", &g_vrFirstPersonHeadRight, -RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, "%.2f");
|
|
if (headOffsetsChanged) {
|
|
RuntimeConfigFile::SetVrFirstPersonHeadUpMeters(g_vrFirstPersonHeadUp);
|
|
RuntimeConfigFile::SetVrFirstPersonHeadForwardMeters(g_vrFirstPersonHeadForward);
|
|
RuntimeConfigFile::SetVrFirstPersonHeadRightMeters(g_vrFirstPersonHeadRight);
|
|
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
|
}
|
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
|
ImGui::TextDisabled("Where the head sits in the kart's own frame.");
|
|
ImGui::PopTextWrapPos();
|
|
// Two presentations of one setting: which models go, or none at all.
|
|
// Ticking either replaces the other, and unticking both shows everything.
|
|
const auto applyHiding = [](bool enabled, int model) {
|
|
g_vrFirstPersonHideDriver = enabled;
|
|
if (enabled) {
|
|
g_vrFirstPersonHiddenModel = model;
|
|
RuntimeConfigFile::SetVrFirstPersonHiddenModel(model);
|
|
}
|
|
RuntimeConfigFile::SetVrFirstPersonHideDriver(enabled);
|
|
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
|
};
|
|
bool hideDriver = g_vrFirstPersonHideDriver && g_vrFirstPersonHiddenModel >= 0;
|
|
bool hideDriverAndKart = g_vrFirstPersonHideDriver && g_vrFirstPersonHiddenModel < 0;
|
|
if (ImGui::Checkbox("Hide driver", &hideDriver)) {
|
|
applyHiding(hideDriver, RuntimeConfigFile::kVrFirstPersonHiddenModelDefault);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip(
|
|
"Removes your character and leaves the kart around you. Their head would "
|
|
"otherwise be where your eyes are. Other racers are unaffected.");
|
|
}
|
|
if (ImGui::Checkbox("Hide driver and kart", &hideDriverAndKart)) {
|
|
applyHiding(hideDriverAndKart, -1);
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Removes the vehicle as well, leaving nothing of your own kart.");
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::Button("Reset first-person defaults")) {
|
|
g_vrFirstPersonUnitsPerMeter = RuntimeConfigFile::kVrFirstPersonUnitsPerMeterDefault;
|
|
g_vrFirstPersonHeadUp = RuntimeConfigFile::kVrFirstPersonHeadUpDefault;
|
|
g_vrFirstPersonHeadForward = RuntimeConfigFile::kVrFirstPersonHeadForwardDefault;
|
|
g_vrFirstPersonHeadRight = RuntimeConfigFile::kVrFirstPersonHeadRightDefault;
|
|
g_vrFirstPersonHideDriver = RuntimeConfigFile::kVrFirstPersonHideDriverDefault;
|
|
g_vrFirstPersonHiddenModel = RuntimeConfigFile::kVrFirstPersonHiddenModelDefault;
|
|
RuntimeConfigFile::SetVrFirstPersonUnitsPerMeter(g_vrFirstPersonUnitsPerMeter);
|
|
RuntimeConfigFile::SetVrFirstPersonHeadUpMeters(g_vrFirstPersonHeadUp);
|
|
RuntimeConfigFile::SetVrFirstPersonHeadForwardMeters(g_vrFirstPersonHeadForward);
|
|
RuntimeConfigFile::SetVrFirstPersonHeadRightMeters(g_vrFirstPersonHeadRight);
|
|
RuntimeConfigFile::SetVrFirstPersonHideDriver(g_vrFirstPersonHideDriver);
|
|
RuntimeConfigFile::SetVrFirstPersonHiddenModel(g_vrFirstPersonHiddenModel);
|
|
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
|
ApplyVrHudVirtualScreen();
|
|
}
|
|
}
|
|
|
|
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("VR")) {
|
|
DrawVrSettings();
|
|
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 {
|
|
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_stereo_stop_at_display_copy(g_vrStopAtDisplayCopy);
|
|
aurora_set_stereo_skip_copy_clears(g_vrSkipCopyClears);
|
|
ApplyVrHudVirtualScreen();
|
|
aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines);
|
|
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
|
g_strapInputAccepted.store(false, std::memory_order_relaxed);
|
|
g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed);
|
|
PADBlockInput(false);
|
|
}
|
|
|
|
void RefreshVrHudVirtualScreen() noexcept { ApplyVrHudVirtualScreen(); }
|
|
|
|
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.
|
|
PADBlockInput(controller_mapping_wizard::IsActive());
|
|
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
|