Files
2026-10-13 20:30:22 +00:00

301 lines
14 KiB
C++

// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Player settings: settings.toml in GetUserPath(), edited by the in-game
// menu and read by the systems that follow them.
//
// - One table (Options()) describes every option: its TOML key, type,
// default and range, menu category, label and description, the
// environment variable that overrides it, and whether a change applies
// live or after a restart. The menu iterates it.
// - Reads are lock-free from any thread (an acquire and a relaxed load,
// inlined), so per-frame and per-draw code reads settings where it uses
// them instead of caching them in statics. Writes (the menu, Load) are
// serialised. Nothing may read a setting from a static initializer:
// that runs before main() calls Load(), so it would miss the file (it
// gets the environment and defaults, with a warning in the log).
// - An environment variable (NFSMW_AA=0, ...) wins over the file for that
// run and is never written back; EnvOverride() tells the menu. Key
// options have none.
// - A Restart option keeps its launch value for the whole run. Its
// consumers read it once (the window's size), so a change mid-run would
// reach only part of them. The menu edits the stored value and shows
// RestartPending(). Every option the menu shows is Live: Internal
// Resolution and Mipmaps rebuild what depends on them between two frames
// (gpu/renderer's ApplyLiveSettings).
// - Save() writes a temporary file, syncs it and renames it over
// settings.toml: a crash or a full disk leaves the previous file whole.
#pragma once
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <span>
#include <string>
#include <string_view>
namespace settings
{
// Every option, grouped by menu category in menu order.
enum class Id : uint8_t
{
// Display, Graphics (the [video] table)
Fullscreen,
VSync,
FrameRate,
PresentScale, // shown on iOS only
AntiAliasing,
Scaling,
AspectRatio,
Brightness, // the game's picture (video/display_tone.h)
DisplayGamma, // the 360's display gamma ramp (video/display_gamma.h)
Sharpening,
Anisotropy,
Mipmaps,
InternalResolution,
WindowWidth, // hidden: the window's size, remembered between runs
WindowHeight, // hidden
// Audio
MasterVolume,
Mute,
MuteInBackground,
// Input
StickDeadzone,
TriggerDeadzone,
Vibration,
// Controls (the [keys] table): the two keyboard keys that drive
// each Xbox 360 control, as key codes (user/key_names.h); the
// second of a pair is always the first's id + 1 (hid/key_map.h
// pairs them). Page order.
KeyRightTrigger, KeyRightTrigger2,
KeyLeftTrigger, KeyLeftTrigger2,
KeyLeftStickLeft, KeyLeftStickLeft2,
KeyLeftStickRight, KeyLeftStickRight2,
KeyLeftStickUp, KeyLeftStickUp2,
KeyLeftStickDown, KeyLeftStickDown2,
KeyA, KeyA2,
KeyB, KeyB2,
KeyX, KeyX2,
KeyY, KeyY2,
KeyLB, KeyLB2,
KeyRB, KeyRB2,
KeyDpadUp, KeyDpadUp2,
KeyDpadDown, KeyDpadDown2,
KeyDpadLeft, KeyDpadLeft2,
KeyDpadRight, KeyDpadRight2,
KeyStart, KeyStart2,
KeyBack, KeyBack2,
KeyRightStickUp, KeyRightStickUp2,
KeyRightStickDown, KeyRightStickDown2,
KeyRightStickLeft, KeyRightStickLeft2,
KeyRightStickRight, KeyRightStickRight2,
KeyLeftStickClick, KeyLeftStickClick2,
KeyRightStickClick, KeyRightStickClick2,
// Advanced
AsyncShaders,
PerformanceOverlay,
Count
};
constexpr size_t kCount = size_t(Id::Count);
// Key: a key code of user/key_names.h (0: no key), saved by its name
// there ("w", "left_shift", "none"); the menu takes it from a key press.
enum class Type : uint8_t { Bool, Int, Float, Enum, Key };
enum class Category : uint8_t { Display, Graphics, Audio, Input, Controls, Advanced, Count };
enum class Apply : uint8_t { Live, Restart };
// Values of the enum options (GetInt returns them).
// Upscaling: FSR 1 is used only where the picture is enlarged; frames
// shown at their size or smaller take the bicubic pass
// (video/upscale.h, whose Mode has the same values).
enum class ScalingMode : int32_t { Bicubic = 0, Linear = 1, Fsr1 = 2 };
// Frame Rate: 60, 30, or Auto (video/frame_rate.h: 60, or 30 while the
// device is hot or can't hold 60).
constexpr int32_t kFrameRateAuto = 0;
// Internal resolution: the multiple of 720p it renders at, in half steps
// (2: 1x, 3: 1.5x = 1080p, 4: 2x, 6: 3x; only the tokens "1x", "1.5x"...
// are saved, so the numbers are free to change); Auto picks it from the
// screen area the picture covers (see renderer).
constexpr int32_t kResolutionAuto = 0;
// Auto: a screen of another shape is filled (video/picture_fit), the HUD
// kept in a centred 16:9 block: a wider one with a wider view (Hor+), a
// narrower one (16:10: the Steam Deck, 1280x800 Macs; 3:2, 4:3) with a
// taller view (Vert+, down to 4:3). Only a screen within 1% of 16:9 keeps
// the 16:9 frame (video::kTallMinStretch). Widescreen: 16:9 with bars.
enum class AspectMode : int32_t { Auto = 0, Widescreen = 1 };
// Anisotropy: 1 (off), 2, 4, 8, 16, or one of these.
constexpr int32_t kAnisotropyAuto = 0; // 16x on a discrete GPU or an M-series iPad, 4x on other integrated ones
constexpr int32_t kAnisotropyGame = -1; // each texture's own setting, as on the 360
struct EnumEntry
{
int32_t value;
const char* token; // in settings.toml and the environment variable
const char* label; // in the menu
};
struct Option
{
Id id;
Category category;
Type type;
Apply apply;
bool hidden = false; // saved, but not shown in the menu
const char* table; // settings.toml table, e.g. "video"
const char* key; // key in that table, e.g. "anti_aliasing"
const char* label; // menu label
const char* description; // one-line menu help
const char* env; // the environment variable that overrides it; nullptr: none (Key options)
int envField = 0; // 0: the whole variable; n: its n-th 'x'-separated field (NFSMW_WINDOW=WxH)
double defaultValue; // Bool: 0 or 1; Enum: an entry's value
// Int and Float: the range and the menu's step. Enum: the range a
// number in the file or the environment is clamped to before it
// takes the entry at or below it (NFSMW_ANISOTROPY=12 is 8x); 0, 0:
// tokens only.
double min = 0, max = 0, step = 0;
const char* unit = nullptr; // FormatValue suffix; "%" on a Float shows value x 100
std::span<const EnumEntry> entries = {}; // Enum only
};
enum class SetResult : uint8_t
{
Applied, // in effect now
Deferred, // stored; takes effect at the next launch (a Restart option)
Overridden, // stored; this run keeps the environment variable's value
Unchanged, // it already had that value
Invalid, // not a value of this option (NaN, not an enum value)
};
// The option table, in Id order.
std::span<const Option> Options();
const Option& Info(Id id);
const Option* Find(std::string_view table, std::string_view key);
const char* CategoryName(Category category);
namespace detail
{
// 0: untouched, 1: environment and defaults applied (read before
// Load), 2: loaded.
extern std::atomic<uint8_t> g_state;
extern std::atomic<double> g_effective[kCount];
extern std::atomic<uint64_t> g_generation;
void InitBeforeLoad(Id first);
}
// The value in effect for this run: the environment's if it overrides
// the option, else the stored one (a Restart option: as at launch).
// Any thread; lock-free.
inline double GetValue(Id id)
{
if (detail::g_state.load(std::memory_order_acquire) == 0) [[unlikely]]
detail::InitBeforeLoad(id);
return detail::g_effective[size_t(id)].load(std::memory_order_relaxed);
}
inline bool GetBool(Id id) { return GetValue(id) != 0.0; }
inline int32_t GetInt(Id id) { return int32_t(GetValue(id)); } // Int, Enum (the entry's value), Bool (0/1)
inline float GetFloat(Id id) { return float(GetValue(id)); }
// Bumped by every change of any option's stored or effective value
// (Set, Reset, Load). A consumer that caches settings compares it once
// a frame and re-reads when it moved.
inline uint64_t Generation() { return detail::g_generation.load(std::memory_order_acquire); }
// What the menu edits and Save() writes. Differs from GetValue() while
// the environment overrides the option or a Restart option waits for a
// restart.
double GetStoredValue(Id id);
bool IsDefault(Id id); // stored value == default
// Main thread, before Load(): `value` is the option's default on this
// device instead of the table's (Frame Rate's Auto on an iPhone): what a
// settings.toml without the option gives, and what Reset and IsDefault
// use. Logged, with `why`. A settings.toml that a save wrote holds every
// option, so it keeps the value it has.
void SetDeviceDefault(Id id, double value, std::string_view why);
// The option's default on this device: the table's, or SetDeviceDefault's.
double DefaultValue(Id id);
// The environment variable's name while it overrides this option for
// this run; nullptr otherwise.
const char* EnvOverride(Id id);
// A Restart option whose stored value differs from the running one.
bool RestartPending(Id id);
// RestartPending for any option the menu shows. Hidden ones don't count:
// the presenter stores the window's size on every resize, which isn't a
// reason to ask the player to restart.
bool AnyRestartPending();
// Some stored value differs from what the last Load read or Save wrote
// (changing an option and back again isn't dirty).
bool Dirty();
// Store a value (normalised: Bool to 0/1, Int rounded and clamped, Float
// clamped and rounded to 1e-4, Enum an entry's value) and apply it if it
// can be. Any thread. Listeners run on the calling thread afterwards.
SetResult Set(Id id, double value);
SetResult Reset(Id id); // to the default
void ResetAll(); // every option, hidden ones included
// The value a left/right press (delta -1/+1) moves to from `value`:
// Bool toggles, Enum takes the next or previous entry, Int and Float
// move by step on the step grid, clamped. From a value between grid
// points (a hand edit: 58%) a press goes to the next point that way (60%
// or 55%). Any input gives a value of the option (NaN: the default). It
// doesn't Set anything.
double StepValue(Id id, double value, int delta);
// For the menu: "On", "Bicubic", "85%", "8%", "0.30", "1280 px". Numbers
// are shown clamped to the option's range; NaN or a non-entry is "?".
std::string FormatValue(Id id, double value);
// Called with an option's id after its effective value changes, on the
// thread that changed it (the menu's, or Load's), outside the settings
// lock (a listener may read or Set). Keep it short; hand thread-bound
// work (SDL window calls) to the owning thread, or have that thread poll
// Generation() instead. Unsubscribe doesn't wait for a call already
// under way on another thread.
using Listener = std::function<void(Id)>;
uint32_t Subscribe(Listener listener);
void Unsubscribe(uint32_t handle);
// GetUserPath() / "settings.toml".
std::filesystem::path DefaultPath();
// Reads the file (missing: defaults), then the environment. Unknown keys
// and unusable values are skipped with a log line; out-of-range values
// are clamped. A file that doesn't parse is copied to settings.toml.bad
// and every option starts at its default. Something that isn't a regular
// file (a FIFO would block the launch) or is over 1 MiB (the real one is
// ~3 KB) is refused the same way, without the copy. Restart options take
// their running values from the first Load; a later Load (a reload)
// replaces unsaved changes. Returns false only when the file exists but
// couldn't be read or parsed (LastError() says why).
bool Load();
bool Load(const std::filesystem::path& path);
// Writes every option's stored value (never an environment override) to
// the path last loaded, or DefaultPath(). Creates the directory. Through
// a symlinked settings.toml it replaces the file the link points at.
// Atomic, also against another instance of the game saving at the same
// moment (each process writes its own temporary file). Takes a few
// milliseconds (a full flush to disk): on menu close or exit, not per
// frame.
bool Save();
bool Save(const std::filesystem::path& path);
// Why the last Load() or Save() failed, for the menu to show the player:
// "" after one that succeeded (a missing file is a success). Values
// skipped one by one aren't failures; they're only in the log.
std::string LastError();
// The game's own record in settings.toml, beside the options: [app]
// last_version, the SpeedBreaker version that ran last
// (user/version_check.h). "" when the file has none (a new player, or a
// build from before versions were recorded).
std::string LastVersion();
// The last Load() found a settings.toml (whether or not it loaded).
bool FileFound();
// Main thread, after Load(): records `version` as the one that ran last,
// in memory and in the file. Only the [app] table is rewritten (atomic,
// like Save): the options stay as the file has them, so a player's file
// that holds none keeps following the defaults. A file that didn't load
// is left alone (false, logged), as is one already recording `version`.
// Every Save() writes it too.
bool RecordVersion(std::string_view version);
}