#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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #include #include #endif #include extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled); #include #include #include 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(std::lround(RuntimeConfigFile::AudioVolume(1.0f) * 100.0f)); int g_musicVolumePercent = static_cast(std::lround(RuntimeConfigFile::MusicVolume(1.0f) * 100.0f)); int g_soundEffectsVolumePercent = static_cast(std::lround(RuntimeConfigFile::SoundEffectsVolume(1.0f) * 100.0f)); int g_uiVolumePercent = static_cast(std::lround(RuntimeConfigFile::UiVolume(1.0f) * 100.0f)); int g_voicesVolumePercent = static_cast(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(AURORA_DISPLAY_MODE_BORDERLESS); } if (mode == "exclusive") { return static_cast(AURORA_DISPLAY_MODE_EXCLUSIVE); } return static_cast(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 g_configuredControllerIndices = [] { std::array indices{}; indices.fill(std::numeric_limits::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 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 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 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 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 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(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; uint32_t ConfiguredNativeButton(const NativeButtonItem& item, const std::string& token) { if (!PADIsAxisButton(item.nativeButton)) return item.nativeButton; const size_t separator = token.find('@'); if (separator == std::string::npos) return item.nativeButton; uint32_t threshold = 0; const char* end = token.data() + token.size(); const auto parsed = std::from_chars(token.data() + separator + 1, end, threshold); if (parsed.ec != std::errc{} || parsed.ptr != end || threshold < 1 || threshold > 100) return item.nativeButton; return PADAxisButtonIdentity(item.nativeButton) | (threshold << 8); } 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; std::string NativeBindingConfig(uint32_t binding) { std::string value = NativeButtonForValue(binding).configName; if (PADIsAxisButton(binding)) value += '@' + std::to_string(PADAxisButtonThreshold(binding)); return value; } 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{ConfiguredNativeButton(*native, binding.primary), 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 = ConfiguredNativeButton(*native, binding.secondary); } 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 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(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(sample.clLStickRaw[0]), static_cast(sample.clLStickRaw[1]), sample.clRStick[0], sample.clRStick[1], static_cast(sample.clRStickRaw[0]), static_cast(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(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(); } const char* KeyBindingName(int scancode) { switch (scancode) { case PAD_KEY_MOUSE_LEFT: return "Mouse left"; case PAD_KEY_MOUSE_RIGHT: return "Mouse right"; case PAD_KEY_MOUSE_MIDDLE: return "Mouse middle"; case PAD_KEY_MOUSE_X1: return "Mouse side 1"; case PAD_KEY_MOUSE_X2: return "Mouse side 2"; case PAD_KEY_INVALID: return "Unmapped"; default: return scancode >= 0 && scancode < SDL_SCANCODE_COUNT ? SDL_GetScancodeName(static_cast(scancode)) : "Unknown"; } } enum class RebindKind { KeyboardButton, KeyboardAxis, Controller }; struct RebindState { bool active = false; bool openPopup = false; RebindKind kind{}; uint32_t port = 0; uint16_t target = 0; bool secondary = false; SDL_JoystickID instance = 0; Clock::time_point deadline{}; std::string label; std::array keys{}; uint32_t mouse = 0; std::array buttons{}; std::array axesReady{}; } g_rebind; void BeginRebind(RebindKind kind, uint16_t target, const char* label, bool secondary = false) { g_rebind = {}; g_rebind.active = true; g_rebind.openPopup = true; g_rebind.kind = kind; g_rebind.port = static_cast(g_controllerPort); g_rebind.target = target; g_rebind.secondary = secondary; g_rebind.label = label; g_rebind.deadline = Clock::now() + std::chrono::seconds(10); int count = 0; const bool* keys = SDL_GetKeyboardState(&count); std::copy_n(keys, std::min(count, static_cast(g_rebind.keys.size())), g_rebind.keys.begin()); g_rebind.mouse = SDL_GetMouseState(nullptr, nullptr); const int index = PADGetIndexForPort(g_rebind.port); if (kind == RebindKind::Controller && index >= 0) { if (auto* pad = PADGetSDLGamepadForIndex(index)) { g_rebind.instance = SDL_GetGamepadID(pad); for (int i = 0; i < SDL_GAMEPAD_BUTTON_COUNT; ++i) g_rebind.buttons[i] = SDL_GetGamepadButton(pad, static_cast(i)); for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT; ++i) g_rebind.axesReady[i] = std::abs(static_cast(SDL_GetGamepadAxis(pad, static_cast(i)))) < 8000; } } } void CompleteRebind(uint32_t value) { const auto& capture = g_rebind; if (capture.kind == RebindKind::Controller) { const int index = PADGetIndexForPort(capture.port); auto* pad = index >= 0 ? PADGetSDLGamepadForIndex(index) : nullptr; if (pad == nullptr || SDL_GetGamepadID(pad) != capture.instance) { g_rebind.active = false; return; } if (capture.secondary) PADSetAltButtonMapping(capture.port, {value, capture.target}); else PADSetButtonMapping(capture.port, {value, capture.target}); uint32_t count = 0, altCount = 0; auto* primary = PADGetButtonMappings(capture.port, &count); auto* alternate = PADGetAltButtonMappings(capture.port, &altCount); uint32_t primaryValue = PAD_NATIVE_BUTTON_INVALID, alternateValue = PAD_NATIVE_BUTTON_INVALID; for (uint32_t i = 0; i < count; ++i) if (primary[i].padButton == capture.target) primaryValue = primary[i].nativeButton; for (uint32_t i = 0; i < altCount; ++i) if (alternate[i].padButton == capture.target) alternateValue = alternate[i].nativeButton; std::string config = NativeBindingConfig(primaryValue); if (alternateValue != PAD_NATIVE_BUTTON_INVALID) config += ',' + NativeBindingConfig(alternateValue); for (size_t i = 0; i < kControllerButtons.size(); ++i) if (kControllerButtons[i].padButton == capture.target) RuntimeConfigFile::SetControllerButton(i, config); } else if (capture.kind == RebindKind::KeyboardButton) { PADSetKeyButtonBinding(capture.port, {static_cast(value), capture.target}); } else { PADSetKeyAxisBinding(capture.port, {static_cast(value), capture.target, 1}); } PADSerializeMappings(); g_rebind.active = false; } void DrawRebindPrompt() { if (g_rebind.openPopup) { ImGui::OpenPopup("Rebind input"); g_rebind.openPopup = false; } if (!ImGui::BeginPopupModal("Rebind input", &g_rebind.active, ImGuiWindowFlags_AlwaysAutoResize)) { g_rebind.active = false; return; } if (g_rebind.active) { ImGui::Text("Rebind: %s", g_rebind.label.c_str()); ImGui::TextUnformatted(g_rebind.kind == RebindKind::Controller ? "Press a controller button, pull a trigger, or move a stick." : "Press a keyboard key or click a mouse button."); ImGui::TextUnformatted("Release any held input first. Escape cancels. F10 is reserved for settings."); const float remaining = std::chrono::duration(g_rebind.deadline - Clock::now()).count(); ImGui::Text("Unmapped in %d seconds", std::max(0, static_cast(std::ceil(remaining)))); if (remaining <= 0.0f) { CompleteRebind(g_rebind.kind == RebindKind::Controller ? PAD_NATIVE_BUTTON_DISABLED : static_cast(PAD_KEY_INVALID)); } else if (SDL_GetKeyboardFocus() != nullptr && g_rebind.kind != RebindKind::Controller) { int count = 0; const bool* keys = SDL_GetKeyboardState(&count); for (int i = 1; i < std::min(count, static_cast(SDL_SCANCODE_COUNT)) && g_rebind.active; ++i) { if (keys[i] && !g_rebind.keys[i] && i != SDL_SCANCODE_F10) CompleteRebind(i); g_rebind.keys[i] = keys[i]; } const uint32_t mouse = SDL_GetMouseState(nullptr, nullptr); for (int i = 1; i <= 5 && g_rebind.active; ++i) if ((mouse & ~g_rebind.mouse & (1u << (i - 1))) != 0) CompleteRebind(static_cast(-i - 1)); g_rebind.mouse = mouse; } else if (SDL_GetKeyboardFocus() != nullptr && g_rebind.kind == RebindKind::Controller) { auto* pad = SDL_GetGamepadFromID(g_rebind.instance); if (pad != nullptr) { for (int i = 0; i < SDL_GAMEPAD_BUTTON_COUNT && g_rebind.active; ++i) { const bool pressed = SDL_GetGamepadButton(pad, static_cast(i)); if (pressed && !g_rebind.buttons[i]) CompleteRebind(i); g_rebind.buttons[i] = pressed; } for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT && g_rebind.active; ++i) { const int value = SDL_GetGamepadAxis(pad, static_cast(i)); if (std::abs(value) < 8000) g_rebind.axesReady[i] = true; if (g_rebind.axesReady[i] && std::abs(value) >= 16384) CompleteRebind(PADEncodeAxisButton(i, value < 0)); } } } } if (!g_rebind.active) ImGui::CloseCurrentPopup(); ImGui::EndPopup(); } void DrawKeyBinding(const char* label, int scancode, RebindKind kind, uint16_t target) { const std::string caption = std::string(KeyBindingName(scancode)) + "##binding"; if (ImGui::Button(caption.c_str(), ImVec2(220.0f, 0.0f))) BeginRebind(kind, target, label); ImGui::SameLine(); ImGui::TextUnformatted(label); } bool DrawKeyboardSettings(uint32_t port) { uint32_t count = 0; auto* buttons = PADGetKeyButtonBindings(port, &count); bool enabled = buttons != nullptr; bool usePreset = false; if (ImGui::Checkbox("Keyboard and mouse", &enabled)) { PADSetKeyboardActive(port, enabled); PADSerializeMappings(); buttons = PADGetKeyButtonBindings(port, &count); usePreset = enabled && std::all_of(buttons, buttons + count, [](const auto& binding) { return binding.scancode == PAD_KEY_INVALID; }); } if (!enabled) return false; ImGui::TextDisabled("Replaces the gamepad on this port. F10 opens settings."); if (ImGui::Button("Use WASD + mouse preset") || usePreset) { const std::array keys = { PAD_KEY_MOUSE_LEFT, SDL_SCANCODE_SPACE, SDL_SCANCODE_E, SDL_SCANCODE_Q, SDL_SCANCODE_RETURN, PAD_KEY_MOUSE_MIDDLE, SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT, SDL_SCANCODE_UP, SDL_SCANCODE_DOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT, }; for (size_t i = 0; i < keys.size(); ++i) PADSetKeyButtonBinding(port, {keys[i], kControllerButtons[i].padButton}); const std::array axes = { SDL_SCANCODE_D, SDL_SCANCODE_A, SDL_SCANCODE_W, SDL_SCANCODE_S, SDL_SCANCODE_L, SDL_SCANCODE_J, SDL_SCANCODE_I, SDL_SCANCODE_K, SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT, }; uint32_t axisCount = 0; auto* mappings = PADGetKeyAxisBindings(port, &axisCount); for (uint32_t i = 0; i < axisCount; ++i) PADSetKeyAxisBinding(port, {axes[i], mappings[i].padAxis, 1}); PADSerializeMappings(); } ImGui::SeparatorText("Button mapping"); for (uint32_t i = 0; i < count; ++i) { int key = buttons[i].scancode; ImGui::PushID(static_cast(i)); ImGui::SetNextItemWidth(220.0f); DrawKeyBinding(PADGetButtonName(buttons[i].padButton), key, RebindKind::KeyboardButton, buttons[i].padButton); ImGui::PopID(); } ImGui::SeparatorText("Stick and trigger mapping"); uint32_t axisCount = 0; auto* axes = PADGetKeyAxisBindings(port, &axisCount); for (uint32_t i = 0; i < axisCount; ++i) { int key = axes[i].scancode; ImGui::PushID(static_cast(count + i)); const char* direction = PADGetAxisDirectionLabel(axes[i].padAxis); const std::string label = std::string(PADGetAxisName(axes[i].padAxis)) + " " + (direction != nullptr ? direction : ""); ImGui::SetNextItemWidth(220.0f); DrawKeyBinding(label.c_str(), key, RebindKind::KeyboardAxis, axes[i].padAxis); ImGui::PopID(); } return true; } // Controller settings menu: port selection, controller assignment and button mapping. int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) { if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { auto* text = static_cast(data->UserData); text->resize(static_cast(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 errors; static std::array buffers; static std::string importStatus; static int loadedPort = -1; static bool reloadBuffers = true; const auto port = static_cast(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(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 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(g_controllerPort); if (DrawKeyboardSettings(selectedGamePort)) { return; } ImGui::Separator(); 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::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(index)); if (ImGui::MenuItem(name != nullptr ? name : "Unknown controller")) { PADSetPortForIndex(index, selectedGamePort); g_configuredControllerIndices.fill(std::numeric_limits::min()); ApplyConfiguredMappings(); } ImGui::PopID(); } ImGui::EndMenu(); } uint32_t mappingCount = 0; PADButtonMapping* mappings = PADGetButtonMappings(static_cast(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(g_controllerPort), &altMappingCount); const auto writeBinding = [](size_t index, uint32_t primaryNative, uint32_t altNative) { std::string value = NativeBindingConfig(primaryNative); if (altNative != PAD_NATIVE_BUTTON_INVALID) { value += ','; value += NativeBindingConfig(altNative); } 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 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(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& preset) { const uint32_t port = static_cast(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"); ImGui::TextDisabled("LT / L2 = left trigger. RT / R2 = right trigger."); ImGui::TextDisabled("LB / L1 = left shoulder. RB / R1 = right shoulder."); ImGui::TextDisabled("Click a binding, then press an input. No input for 10 seconds clears it."); const float bindingWidth = ImGui::CalcTextSize("Right shoulder (RB / R1)").x + ImGui::GetFrameHeight() + ImGui::GetStyle().FramePadding.x * 2.0f; 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(i)); const auto drawThreshold = [&](PADButtonMapping* mapping, bool secondary) { if (!PADIsAxisButton(mapping->nativeButton)) return; int threshold = static_cast(PADAxisButtonThreshold(mapping->nativeButton)); ImGui::SetNextItemWidth(bindingWidth); if (ImGui::SliderInt(secondary ? "##altThreshold" : "##primaryThreshold", &threshold, 1, 100, "Threshold: %d%%", ImGuiSliderFlags_AlwaysClamp)) { const PADButtonMapping updated = { PADAxisButtonIdentity(mapping->nativeButton) | (static_cast(threshold) << 8), mapping->padButton, }; if (secondary) PADSetAltButtonMapping(selectedGamePort, updated); else PADSetButtonMapping(selectedGamePort, updated); } if (ImGui::IsItemDeactivatedAfterEdit()) { writeBinding(i, mappingIt->nativeButton, altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID); PADSerializeMappings(); } }; ImGui::BeginGroup(); ImGui::SetNextItemWidth(bindingWidth); const std::string primaryCaption = std::string(current.label) + "##primary"; if (ImGui::Button(primaryCaption.c_str(), ImVec2(bindingWidth, 0.0f))) { BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label); } drawThreshold(mappingIt, false); ImGui::EndGroup(); if (altIt != nullptr) { const bool altBound = altIt->nativeButton != PAD_NATIVE_BUTTON_INVALID; if (!altBound && !altRowExpanded[i]) { ImGui::SameLine(); if (ImGui::SmallButton("+")) { altRowExpanded[i] = true; BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Add a second binding; pressing either one works"); } } else { ImGui::SameLine(); ImGui::TextUnformatted("or"); ImGui::SameLine(); ImGui::BeginGroup(); const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None"; ImGui::SetNextItemWidth(bindingWidth); const std::string altCaption = std::string(altLabel) + "##alt"; if (ImGui::Button(altCaption.c_str(), ImVec2(bindingWidth, 0.0f))) { BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true); } drawThreshold(altIt, true); ImGui::EndGroup(); } } 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(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(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(g_soundEffectsVolumePercent) / 100.0f; MusicAttenuation::SetSoundEffectsVolume(volume); RuntimeConfigFile::SetSoundEffectsVolume(volume); } if (ImGui::SliderInt("UI", &g_uiVolumePercent, 0, 100, "%d%%")) { const float volume = static_cast(g_uiVolumePercent) / 100.0f; MusicAttenuation::SetUiVolume(volume); RuntimeConfigFile::SetUiVolume(volume); } if (ImGui::SliderInt("Voices", &g_voicesVolumePercent, 0, 100, "%d%%")) { const float volume = static_cast(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(aurora_get_display_mode()); struct EffectFlag { const char* label; uint32_t flag; }; static constexpr std::array 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(std::size(kDisplayModes)))) { const auto mode = static_cast(g_displayMode); aurora_set_display_mode(mode); const AuroraDisplayMode activeMode = aurora_get_display_mode(); if (activeMode == mode) { RuntimeConfigFile::SetDisplayMode(std::string(kDisplayModeConfigNames[static_cast(g_displayMode)])); } else { g_displayMode = static_cast(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 kFrameInterpolationModes{ "Off", "120 FPS", "180 FPS", }; const char* currentFrameInterpolationMode = kFrameInterpolationModes[static_cast(g_frameInterpolationMode)]; bool frameInterpolationModeChanged = false; if (ImGui::BeginCombo("Race frame interpolation (experimental)", currentFrameInterpolationMode)) { for (int mode = 0; mode < static_cast(kFrameInterpolationModes.size()); ++mode) { const bool selected = g_frameInterpolationMode == mode; if (ImGui::Selectable(kFrameInterpolationModes[static_cast(mode)], selected)) { g_frameInterpolationMode = mode; frameInterpolationModeChanged = true; } if (selected) { ImGui::SetItemDefaultFocus(); } } ImGui::EndCombo(); } if (frameInterpolationModeChanged) { const uint32_t targetFps = kFrameInterpolationTargetFps[static_cast(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(); // Nest capture under this menu so opening/closing the modal preserves // the settings popup and its current port and scroll position. DrawRebindPrompt(); 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(aurora_get_display_mode()); if (active == g_displayMode) { return; } g_displayMode = active; RuntimeConfigFile::SetDisplayMode(std::string(kDisplayModeConfigNames[static_cast(active)])); } } // namespace void InitializeRuntimeSettings() noexcept { PAD_HLE_SetRumbleEnabled(g_rumbleEnabled); InputBindings::Reload(); controller_mapping_wizard::LoadPersistedMappings(); ApplyConfiguredMappings(); AudioBackend::Instance().SetMasterVolume(static_cast(g_audioVolumePercent) / 100.0f); AudioBackend::Instance().SetMuted(g_audioMuted); MusicAttenuation::SetMusicVolume(static_cast(g_musicVolumePercent) / 100.0f); MusicAttenuation::SetSoundEffectsVolume(static_cast(g_soundEffectsVolumePercent) / 100.0f); MusicAttenuation::SetUiVolume(static_cast(g_uiVolumePercent) / 100.0f); MusicAttenuation::SetVoicesVolume(static_cast(g_voicesVolumePercent) / 100.0f); MusicAttenuation::SetEnabled(g_attenuateMusicWhenMediaPlays); RuntimeGameGraphicsOptions::SetDisabledPostProcessingPaths(g_disabledPostProcessingPaths); const uint32_t targetFps = kFrameInterpolationTargetFps[static_cast(g_frameInterpolationMode)]; LimitResolutionForFrameRate(); aurora_set_frame_interpolation_fps(targetFps); aurora_set_display_mode(static_cast(g_displayMode)); g_displayMode = static_cast(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::min()); } if (ev->type != AURORA_SDL_EVENT) { continue; } controller_mapping_wizard::HandleSdlEvent(ev->sdl); if (g_rebind.active && IsToggleKey(ev->sdl, SDL_SCANCODE_ESCAPE)) { g_rebind.active = false; } if (!g_rebind.active && 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() || g_topBarVisible || g_rebind.active; 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