Compare commits

...

1 Commits

Author SHA1 Message Date
patchzyy 37a0b68d85 keep steam desktop layout off while focused 2026-10-29 18:21:00 +01:00
6 changed files with 675 additions and 0 deletions
+3
View File
@@ -2,6 +2,7 @@
#include "friend_code_keyboard.h"
#include "settings_overlay.h"
#include "steam_input_layout.h"
#include "runtime_config.h"
#include "gx_worker.h"
#include "perf_stats.h"
@@ -63,6 +64,7 @@ inline void Flush(bool force = false) {
// process directly and do not run the crash/atexit paths.
[[noreturn]] inline void ExitForAuroraWindowClose() noexcept {
settings_overlay::ReleaseControllers();
SteamInputLayout::Release();
WindowPlacementPersistence::Flush(true);
#if defined(_WIN32)
::ExitProcess(0);
@@ -120,6 +122,7 @@ inline void ProcessAuroraEvents(const AuroraEvent* events) {
}
settings_overlay::HandleEvents(events);
FriendCodeKeyboard::HandleEvents(events);
SteamInputLayout::HandleEvents(events);
}
inline void ApplyPendingMkwDynamicAspectSurface() {
+23
View File
@@ -70,6 +70,10 @@ struct RuntimeUserConfig {
std::optional<int32_t> wiiIrSensitivity;
std::optional<int32_t> wiiSpeakerVolume;
std::optional<bool> wiiRumble;
// Outside a Steam launch, ask a running Steam client for a game controller layout while
// the window has focus instead of its desktop one (buttons bound to mouse and keyboard).
std::optional<bool> steamInputOverride;
std::optional<uint32_t> steamInputAppId;
std::optional<bool> networkEnabled;
std::optional<bool> discordPresenceEnabled;
// The application ID of the WiiCompiled Discord application. This is only
@@ -308,6 +312,12 @@ inline void EnsureConfigFile() {
"mix_worker = true\n\n"
"[network]\n"
"enabled = true\n\n"
"[controller]\n"
"# Outside a Steam launch, Steam's desktop layout binds controller buttons\n"
"# to mouse and keyboard. While the game has focus, ask a running Steam\n"
"# client to use the game layout of steam_input_app_id instead.\n"
"steam_input_override = true\n"
"# steam_input_app_id = 480\n\n"
"[discord]\n"
"# Rich Presence talks only to a locally-running Discord client.\n"
"# Retro Rewind supplies its official app ID automatically. Set this\n"
@@ -489,6 +499,10 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.wiiSpeakerVolume = *value;
}
config.wiiRumble = FindConfigValue<bool>(document, "controller", "wii_rumble");
config.steamInputOverride = FindConfigValue<bool>(document, "controller", "steam_input_override");
if (const auto value = FindConfigUint(document, "controller", "steam_input_app_id"); value && *value != 0) {
config.steamInputAppId = *value;
}
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
config.discordClientId = FindConfigValue<std::string>(document, "discord", "client_id");
@@ -895,6 +909,15 @@ inline bool SetWiiRemotesEnabled(bool value) {
return true;
}
inline bool SteamInputOverrideEnabled(bool fallback = true) {
return Get().steamInputOverride.value_or(fallback);
}
// Spacewar, Valve's Steamworks sample app, available to every Steam account.
inline uint32_t SteamInputAppId(uint32_t fallback = 480) {
return Get().steamInputAppId.value_or(fallback);
}
inline bool WiiAutoPairEnabled(bool fallback = true) {
return Get().wiiAutoPair.value_or(fallback);
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <aurora/event.h>
#include <cstdint>
// Outside a Steam launch, Steam applies its desktop controller layout, which binds
// controller buttons to mouse and keyboard actions. While the game window has focus,
// a running Steam client is asked to apply the given app's game layout instead.
namespace SteamInputLayout {
void Initialize(bool enabled, uint32_t appId);
void HandleEvents(const AuroraEvent* events);
// Restores the desktop layout. Idempotent; safe to call from any exit path.
void Release() noexcept;
} // namespace SteamInputLayout
+17
View File
@@ -41,6 +41,7 @@
#include <mmsystem.h>
#include <dbghelp.h>
#else
#include <fcntl.h>
#include <signal.h>
#if defined(__x86_64__)
// Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault
@@ -68,6 +69,7 @@
#include "aurora_events.h"
#include "wiimote_host.h"
#include "discord_presence.h"
#include "steam_input_layout.h"
#include "fiber_manager.h"
#include "gx_worker.h"
#include "hle_stubs.h"
@@ -337,6 +339,9 @@ bool InstallTranscriptPipe(int& outReadFd, int& outWriteFd, int targetFd) {
if (::pipe(pipeFds) != 0) {
return false;
}
// Spawned helpers must not keep the transcript open; its reader waits for EOF on shutdown.
::fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC);
::fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC);
#endif
outReadFd = pipeFds[0];
@@ -1098,6 +1103,7 @@ LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info) {
<< " at host address 0x" << record->ExceptionInformation[1];
}
popupDetails << ".\n\nThe process transcript and crash log contain the full CPU and stack diagnostics.";
SteamInputLayout::Release();
ShowRuntimeFatalPopup("a native crash occurred", popupDetails.str());
DumpHostStackTrace();
@@ -1198,6 +1204,7 @@ void PosixMemoryFaultHandler(int sig, siginfo_t* info, void* ucontextVoid) {
}
popupDetails << ".\n\nThe process transcript and crash log contain the full CPU and stack "
"diagnostics.";
SteamInputLayout::Release();
ShowRuntimeFatalPopup("a native crash occurred", popupDetails.str());
DumpHostStackTrace();
WriteFatalLogImpl(sig == SIGBUS ? "sigbus" : "sigsegv");
@@ -1231,6 +1238,7 @@ void AbortSignalHandler(int signum) {
// abort() bypasses atexit, so the guest-memory fault summary has to be
// emitted here too. It is idempotent, so a later AtExitHandler is a no-op.
GuestFlat::LogFaultSummary();
SteamInputLayout::Release();
ShowRuntimeFatalPopup("a fatal internal error occurred",
"The process called abort while running the game or Aurora renderer.\n\n"
@@ -1266,6 +1274,7 @@ void SetRuntimeExitCode(int code) {
// Global handler called via atexit() to flush buffers before any exit
static void AtExitHandler() {
SteamInputLayout::Release();
// End-of-run guest memory report. This runs before the fatal-report check
// below because the counters describe the whole session and are just as
// interesting after a crash as after a clean exit.
@@ -1290,6 +1299,7 @@ static void AtExitHandler() {
// Global terminate handler for uncaught exceptions
static void TerminateHandler() {
SteamInputLayout::Release();
// Skip detailed dump if already reported
if (g_fatalErrorReported.load(std::memory_order_acquire)) {
std::fflush(stderr);
@@ -1480,6 +1490,10 @@ int RuntimeMain(int argc, char** argv) {
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
auroraInfo.windowSize.native_fb_height);
settings_overlay::InitializeRuntimeSettings();
if (!g_headless) {
SteamInputLayout::Initialize(RuntimeConfigFile::SteamInputOverrideEnabled(),
RuntimeConfigFile::SteamInputAppId());
}
if (BootRace::Requested()) {
aurora_set_skip_unready_pipelines(false);
}
@@ -1525,6 +1539,7 @@ int RuntimeMain(int argc, char** argv) {
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
Wiimote::Shutdown();
SteamInputLayout::Release();
GxWorker::Stop();
aurora_shutdown();
DiscordPresence::Shutdown();
@@ -1539,6 +1554,7 @@ int RuntimeMain(int argc, char** argv) {
<< " len=0x" << ex.length()
<< std::dec << std::nouppercase
<< " reason=" << ex.reason();
SteamInputLayout::Release();
ShowRuntimeFatalPopup("a guest memory access was out of bounds", details.str());
WriteFatalLogImpl("access_violation", details.str());
SetRuntimeExitCodeImpl(1);
@@ -1553,6 +1569,7 @@ int RuntimeMain(int argc, char** argv) {
} catch (const std::exception& ex) {
std::cerr << "Runtime error: " << ex.what() << std::endl;
SystemBridge::DumpCpuState(TryGetCpuContext());
SteamInputLayout::Release();
ShowRuntimeFatalPopup("a runtime exception occurred", ex.what());
WriteFatalLogImpl("exception");
SetRuntimeExitCodeImpl(1);
+603
View File
@@ -0,0 +1,603 @@
#include "steam_input_layout.h"
#include "runtime_log.h"
#include <aurora/aurora.h>
#include <SDL3/SDL_video.h>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdlib>
#include <memory>
#include <mutex>
#include <string>
#include <system_error>
#include <thread>
#include <utility>
#include <vector>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <cwchar>
#include <filesystem>
#include <optional>
#else
#include <cerrno>
#include <csignal>
#include <fcntl.h>
#include <spawn.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#if defined(__APPLE__)
#include <libproc.h>
#include <sys/proc.h>
#include <sys/proc_info.h>
#else
#include <dirent.h>
#include <fstream>
#include <sstream>
#endif
extern char** environ;
#endif
namespace SteamInputLayout {
namespace {
using Clock = std::chrono::steady_clock;
enum class Handoff { None, Unconfirmed, Confirmed };
constexpr std::chrono::milliseconds kHandoffTimeout{2000};
#if defined(__linux__)
// xdg-open can return before the Steam forwarder it started has delivered the URL.
constexpr std::chrono::milliseconds kDeliverySpacing{1500};
#else
constexpr std::chrono::milliseconds kDeliverySpacing{0};
#endif
struct State {
std::mutex mutex;
std::condition_variable wake;
// Serializes requests to Steam so they arrive in order.
std::timed_mutex delivery;
bool enabled = false;
bool stopping = false;
bool desired = false;
bool sent = false;
bool maybeForced = false;
uint32_t appId = 0;
SDL_WindowID windowId = 0;
std::atomic<Clock::rep> lastDelivery{0};
std::atomic<bool> released{false};
};
// Never destroyed: the detached worker and late exit paths may still touch it.
std::atomic<State*> g_state{nullptr};
thread_local bool t_isWorker = false;
thread_local bool t_inRelease = false;
// The worker launches and publishes its request helper under this lock, so an exit that
// can't wait for the worker can cancel it: either the helper is published, or it never starts.
std::mutex g_helperMutex;
std::atomic<bool> g_helperCancelled{false};
#if defined(_WIN32)
HANDLE g_helper = nullptr;
#else
pid_t g_helper = 0;
#endif
// These locks are only held briefly; failing to get one means a crash inside it.
bool TryLockBriefly(std::unique_lock<std::mutex>& lock,
std::chrono::milliseconds timeout = std::chrono::milliseconds(250)) {
const auto deadline = Clock::now() + timeout;
while (!lock.try_lock()) {
if (Clock::now() >= deadline) {
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return true;
}
// Steam applies the launching app's own layout to everything it starts, children included.
bool LaunchedBySteam() {
for (const char* name : {"SteamGameId", "SteamAppId", "SteamClientLaunch"}) {
const char* value = std::getenv(name);
if (value != nullptr && *value != '\0') {
return true;
}
}
#if defined(_WIN32)
if (GetModuleHandleW(L"GameOverlayRenderer64.dll") != nullptr) {
return true;
}
#endif
return false;
}
#if defined(_WIN32)
std::vector<BYTE> TokenUserOf(HANDLE process) {
std::vector<BYTE> user;
HANDLE token = nullptr;
if (!OpenProcessToken(process, TOKEN_QUERY, &token)) {
return user;
}
DWORD size = 0;
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
user.resize(size);
if (size == 0 || !GetTokenInformation(token, TokenUser, user.data(), size, &size)) {
user.clear();
}
CloseHandle(token);
return user;
}
// A stale pid can be reused by another signed-in user's Steam.
bool OwnedByCurrentUser(HANDLE process) {
const auto theirs = TokenUserOf(process);
const auto ours = TokenUserOf(GetCurrentProcess());
return !theirs.empty() && !ours.empty() &&
EqualSid(reinterpret_cast<const TOKEN_USER*>(theirs.data())->User.Sid,
reinterpret_cast<const TOKEN_USER*>(ours.data())->User.Sid);
}
// ActiveProcess\pid outlives a Steam crash, so the process behind it is verified too.
std::optional<std::wstring> RunningSteamExecutable() {
DWORD pid = 0;
DWORD size = sizeof(pid);
if (RegGetValueW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam\\ActiveProcess", L"pid",
RRF_RT_REG_DWORD, nullptr, &pid, &size) != ERROR_SUCCESS ||
pid == 0) {
return std::nullopt;
}
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (process == nullptr) {
return std::nullopt;
}
std::wstring path(32768, L'\0');
DWORD length = static_cast<DWORD>(path.size());
DWORD exitCode = 0;
const bool alive = QueryFullProcessImageNameW(process, 0, path.data(), &length) &&
GetExitCodeProcess(process, &exitCode) && exitCode == STILL_ACTIVE &&
OwnedByCurrentUser(process);
CloseHandle(process);
if (!alive) {
return std::nullopt;
}
path.resize(length);
if (_wcsicmp(std::filesystem::path(path).filename().c_str(), L"steam.exe") != 0) {
return std::nullopt;
}
return path;
}
// Same command line as Steam's registered steam:// handler, sent to the verified client.
Handoff OpenSteamUrl(const std::string& url, bool bounded) {
const auto steam = RunningSteamExecutable();
if (!steam) {
return Handoff::None;
}
std::wstring commandLine = L"\"" + *steam + L"\" -- \"" + std::wstring(url.begin(), url.end()) + L"\"";
const std::wstring directory = std::filesystem::path(*steam).parent_path().wstring();
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
PROCESS_INFORMATION info{};
std::unique_lock launch(g_helperMutex, std::defer_lock);
if (!bounded) {
launch.lock();
if (g_helperCancelled) {
return Handoff::None;
}
}
// The worker's helper starts suspended so a cancellation that couldn't take the lock while
// CreateProcessW ran still stops it before it can reach Steam.
if (!CreateProcessW(steam->c_str(), commandLine.data(), nullptr, nullptr, FALSE,
bounded ? 0 : CREATE_SUSPENDED, nullptr, directory.c_str(), &startup, &info)) {
return Handoff::None;
}
if (!bounded) {
g_helper = info.hProcess;
launch.unlock();
if (g_helperCancelled) {
TerminateProcess(info.hProcess, 1);
} else {
ResumeThread(info.hThread);
}
}
CloseHandle(info.hThread);
const DWORD waited = WaitForSingleObject(info.hProcess, bounded ? static_cast<DWORD>(kHandoffTimeout.count()) : INFINITE);
if (!bounded) {
std::lock_guard lock(g_helperMutex);
g_helper = nullptr;
}
DWORD exitCode = 1;
const bool succeeded = waited == WAIT_OBJECT_0 && GetExitCodeProcess(info.hProcess, &exitCode) && exitCode == 0;
CloseHandle(info.hProcess);
return succeeded ? Handoff::Confirmed : Handoff::Unconfirmed;
}
void CancelWorkerRequests() {
g_helperCancelled = true;
std::unique_lock lock(g_helperMutex, std::defer_lock);
if (!TryLockBriefly(lock, kHandoffTimeout)) {
return;
}
if (g_helper != nullptr && TerminateProcess(g_helper, 1)) {
WaitForSingleObject(g_helper, static_cast<DWORD>(kHandoffTimeout.count()));
}
}
#else
// Only a live client of this user; a URL handler with no client running would start Steam.
bool SteamRunning() {
const uid_t uid = getuid();
#if defined(__APPLE__)
const int count = proc_listallpids(nullptr, 0);
if (count <= 0) {
return false;
}
std::vector<pid_t> pids(static_cast<size_t>(count) + 32);
const int filled = proc_listallpids(pids.data(), static_cast<int>(pids.size() * sizeof(pid_t)));
for (int i = 0; i < filled; ++i) {
char name[2 * MAXCOMLEN + 1] = {};
if (proc_name(pids[i], name, sizeof(name)) <= 0 || std::string(name) != "steam_osx") {
continue;
}
proc_bsdinfo info{};
if (proc_pidinfo(pids[i], PROC_PIDTBSDINFO, 0, &info, sizeof(info)) == sizeof(info) &&
info.pbi_uid == uid && info.pbi_status != SZOMB) {
return true;
}
}
return false;
#else
DIR* proc = opendir("/proc");
if (proc == nullptr) {
return false;
}
bool found = false;
while (const dirent* entry = readdir(proc)) {
if (entry->d_name[0] < '1' || entry->d_name[0] > '9') {
continue;
}
std::ifstream status(std::string("/proc/") + entry->d_name + "/status");
std::string line;
bool named = false;
bool live = false;
bool owned = false;
while (std::getline(status, line)) {
if (line.rfind("Name:", 0) == 0) {
named = line == "Name:\tsteam";
} else if (line.rfind("State:", 0) == 0) {
live = line.find_first_of("ZX") == std::string::npos;
} else if (line.rfind("Uid:", 0) == 0) {
std::istringstream fields(line.substr(4));
uid_t real = 0;
owned = static_cast<bool>(fields >> real) && real == uid;
break;
}
}
if (named && live && owned) {
found = true;
break;
}
}
closedir(proc);
return found;
#endif
}
Handoff OpenSteamUrl(const std::string& url, bool bounded) {
if (!SteamRunning()) {
return Handoff::None;
}
// The worker's helper waits for a go-ahead on fd 3 before it runs the URL handler, so a
// cancellation that couldn't take the lock while posix_spawn ran still stops it in time.
static char gateScript[] = "read -r go <&3 || exit 1; exec 3<&-; exec \"$0\" \"$@\"";
#if defined(__APPLE__)
const char* program = "/usr/bin/open";
char* argv[] = {const_cast<char*>("open"), const_cast<char*>("-g"), const_cast<char*>(url.c_str()), nullptr};
char* gated[] = {const_cast<char*>("sh"), const_cast<char*>("-c"), gateScript, const_cast<char*>(program),
const_cast<char*>("-g"), const_cast<char*>(url.c_str()), nullptr};
constexpr int kNoSignal = 0;
#else
const char* program = "xdg-open";
char* argv[] = {const_cast<char*>("xdg-open"), const_cast<char*>(url.c_str()), nullptr};
char* gated[] = {const_cast<char*>("sh"), const_cast<char*>("-c"), gateScript, const_cast<char*>(program),
const_cast<char*>(url.c_str()), nullptr};
constexpr int kNoSignal = MSG_NOSIGNAL;
#endif
int gate[2] = {-1, -1};
if (!bounded) {
if (socketpair(AF_UNIX, SOCK_STREAM, 0, gate) != 0) {
return Handoff::None;
}
// dup2(3, 3) keeps FD_CLOEXEC on macOS, which would close the gate in the child.
if (gate[0] == 3) {
std::swap(gate[0], gate[1]);
}
fcntl(gate[0], F_SETFD, FD_CLOEXEC);
fcntl(gate[1], F_SETFD, FD_CLOEXEC);
#if defined(__APPLE__)
int noSigPipe = 1;
setsockopt(gate[1], SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe));
#endif
}
const auto closeGate = [&gate] {
for (int& end : gate) {
if (end != -1) {
close(end);
end = -1;
}
}
};
posix_spawn_file_actions_t actions;
if (posix_spawn_file_actions_init(&actions) != 0) {
closeGate();
return Handoff::None;
}
if (!bounded) {
posix_spawn_file_actions_adddup2(&actions, gate[0], 3);
}
posix_spawn_file_actions_addopen(&actions, STDIN_FILENO, "/dev/null", O_RDONLY, 0);
posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, "/dev/null", O_WRONLY, 0);
posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, "/dev/null", O_WRONLY, 0);
// A crash handler can call this with signals blocked; the child should not inherit that.
posix_spawnattr_t attributes;
posix_spawnattr_init(&attributes);
sigset_t noSignals;
sigemptyset(&noSignals);
posix_spawnattr_setsigmask(&attributes, &noSignals);
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSIGMASK);
pid_t child = 0;
std::unique_lock launch(g_helperMutex, std::defer_lock);
if (!bounded) {
launch.lock();
}
int error = ECANCELED;
if (bounded) {
error = posix_spawnp(&child, program, &actions, &attributes, argv, environ);
} else if (!g_helperCancelled) {
error = posix_spawn(&child, "/bin/sh", &actions, &attributes, gated, environ);
}
posix_spawnattr_destroy(&attributes);
posix_spawn_file_actions_destroy(&actions);
if (error != 0) {
closeGate();
return Handoff::None;
}
int status = 0;
if (!bounded) {
g_helper = child;
if (!g_helperCancelled) {
send(gate[1], "1\n", 2, kNoSignal);
}
closeGate();
launch.unlock();
// Wait without reaping so the pid can't be reused while it is still published.
siginfo_t info{};
while (waitid(P_PID, static_cast<id_t>(child), &info, WEXITED | WNOWAIT) == -1 && errno == EINTR) {
}
{
std::lock_guard lock(g_helperMutex);
g_helper = 0;
}
}
const auto deadline = Clock::now() + kHandoffTimeout;
for (;;) {
const pid_t waited = waitpid(child, &status, bounded ? WNOHANG : 0);
if (waited == child) {
return WIFEXITED(status) && WEXITSTATUS(status) == 0 ? Handoff::Confirmed : Handoff::Unconfirmed;
}
if (waited == -1 && errno != EINTR) {
return Handoff::Unconfirmed;
}
if (waited == 0) {
if (Clock::now() >= deadline) {
try {
std::thread([child] { waitpid(child, nullptr, 0); }).detach();
} catch (...) {
}
return Handoff::Unconfirmed;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
}
void CancelWorkerRequests() {
g_helperCancelled = true;
std::unique_lock lock(g_helperMutex, std::defer_lock);
if (!TryLockBriefly(lock, kHandoffTimeout)) {
return;
}
if (g_helper == 0 || kill(g_helper, SIGKILL) != 0) {
return;
}
// The worker only reaps after clearing g_helper under this lock, so the pid stays ours.
const auto deadline = Clock::now() + kHandoffTimeout;
for (;;) {
siginfo_t info{};
if (waitid(P_PID, static_cast<id_t>(g_helper), &info, WEXITED | WNOWAIT | WNOHANG) != 0 ||
info.si_pid != 0 || Clock::now() >= deadline) {
return;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
#endif
// Caller holds state.delivery. Only exit paths bound the wait; the worker waits for each
// request to finish so a slow one can never land after the next.
Handoff Deliver(State& state, bool forced, uint32_t appId, bool bounded) {
const auto earliest = Clock::time_point(Clock::duration(state.lastDelivery.load())) + kDeliverySpacing;
if (const auto now = Clock::now(); now < earliest) {
std::this_thread::sleep_for(earliest - now);
}
const Handoff delivered = OpenSteamUrl("steam://forceinputappid/" + std::to_string(forced ? appId : 0), bounded);
if (delivered != Handoff::None) {
state.lastDelivery = Clock::now().time_since_epoch().count();
if (forced) {
RT_LOG(RT_TAG_RUNTIME) << "Steam Input: applying the controller layout of app " << appId << std::endl;
} else {
RT_LOG(RT_TAG_RUNTIME) << "Steam Input: restored the desktop controller layout" << std::endl;
}
}
return delivered;
}
void Worker(State& state) {
t_isWorker = true;
std::unique_lock lock(state.mutex);
for (;;) {
state.wake.wait(lock, [&] { return state.stopping || state.desired != state.sent; });
if (state.stopping) {
return;
}
const bool target = state.desired;
const uint32_t appId = state.appId;
// A failed request is retried on the next focus change rather than in a loop.
state.sent = target;
lock.unlock();
{
std::lock_guard delivery(state.delivery);
{
// Release may have run while this thread waited for the delivery lock.
std::lock_guard guard(state.mutex);
if (state.stopping) {
return;
}
}
const Handoff delivered = Deliver(state, target, appId, false);
std::lock_guard guard(state.mutex);
state.maybeForced = target ? delivered != Handoff::None || state.maybeForced
: state.maybeForced && delivered != Handoff::Confirmed;
}
lock.lock();
}
}
} // namespace
void Initialize(bool enabled, uint32_t appId) {
if (!enabled || appId == 0 || g_state.load() != nullptr) {
return;
}
if (LaunchedBySteam()) {
RT_LOG(RT_TAG_RUNTIME) << "Steam Input: launched by Steam, leaving its controller layout alone" << std::endl;
return;
}
SDL_Window* window = aurora_get_window();
if (window == nullptr) {
return;
}
auto* state = new State();
state->enabled = true;
state->appId = appId;
state->windowId = SDL_GetWindowID(window);
state->desired = (SDL_GetWindowFlags(window) & SDL_WINDOW_INPUT_FOCUS) != 0;
try {
std::thread(Worker, std::ref(*state)).detach();
} catch (const std::exception& error) {
RT_LOG(RT_TAG_RUNTIME) << "Steam Input: " << error.what() << std::endl;
delete state;
return;
}
g_state.store(state);
}
void HandleEvents(const AuroraEvent* events) {
State* state = g_state.load();
if (state == nullptr || events == nullptr) {
return;
}
int focus = -1;
for (const AuroraEvent* event = events; event->type != AURORA_NONE; ++event) {
if (event->type != AURORA_SDL_EVENT) {
continue;
}
const SDL_Event& sdl = event->sdl;
const bool gained = sdl.type == SDL_EVENT_WINDOW_FOCUS_GAINED;
if ((gained || sdl.type == SDL_EVENT_WINDOW_FOCUS_LOST) && sdl.window.windowID == state->windowId) {
focus = gained ? 1 : 0;
}
}
if (focus < 0) {
return;
}
std::lock_guard lock(state->mutex);
if (state->enabled) {
state->desired = focus == 1;
state->wake.notify_one();
}
}
void Release() noexcept {
State* state = g_state.load();
if (state == nullptr) {
return;
}
const auto releaseWait = kHandoffTimeout + kDeliverySpacing + std::chrono::milliseconds(500);
try {
{
std::unique_lock lock(state->mutex, std::defer_lock);
if (!TryLockBriefly(lock)) {
return;
}
if (!state->enabled) {
lock.unlock();
// Another thread is restoring; exiting under it would kill its request.
const auto deadline = Clock::now() + 2 * releaseWait;
while (!t_inRelease && !state->released.load() && Clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return;
}
t_inRelease = true;
state->enabled = false;
state->stopping = true;
state->wake.notify_one();
}
// Let an in-flight request land first so the restore is the last one Steam sees.
std::unique_lock delivery(state->delivery, std::defer_lock);
const bool ordered = !t_isWorker && delivery.try_lock_for(releaseWait);
bool maybeForced = true;
if (ordered) {
std::unique_lock lock(state->mutex, std::defer_lock);
maybeForced = !TryLockBriefly(lock) || state->maybeForced;
}
// A request still in flight, or about to start, could otherwise land after the restore.
CancelWorkerRequests();
if (maybeForced) {
// Discovery and spawning can block as well; they must not hold up the exit.
auto done = std::make_shared<std::atomic<bool>>(false);
try {
std::thread([state, done] {
Deliver(*state, false, 0, true);
done->store(true);
}).detach();
} catch (const std::system_error&) {
done->store(true);
}
const auto deadline = Clock::now() + releaseWait;
while (!done->load() && Clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
} catch (...) {
}
state->released.store(true);
}
} // namespace SteamInputLayout
+12
View File
@@ -78,6 +78,18 @@ int main() {
if (!Require(!config.motionTrick && !config.motionTrickSensitivity,
"out of range shake to trick sensitivity was accepted")) return 1;
}
{
std::istringstream input("[controller]\nsteam_input_override = false\nsteam_input_app_id = 1234\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "steam.toml");
if (!Require(config.steamInputOverride == std::optional<bool>{false} &&
config.steamInputAppId == std::optional<uint32_t>{1234},
"Steam Input settings were not loaded")) return 1;
}
{
std::istringstream input("[controller]\nsteam_input_app_id = 0\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "steam-zero.toml");
if (!Require(!config.steamInputAppId, "zero Steam Input app id was accepted")) return 1;
}
if (!SetsFps("[video]\nshow_fps = false\n", "bare key was not replaced")) return 1;
if (!SetsFps("[video]\n\"show_fps\" = false\n", "basic-quoted key was not replaced")) return 1;
if (!SetsFps("[video]\n'show_fps'=false\n", "literal-quoted key was not replaced")) return 1;