You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
config.hpp # INI configuration with callback setters
139
+
config.hpp # INI configuration with callback setters; also the input-binding fusions (register_press_combo / register_hold_combo) that pair an INI combo item with an InputManager press/hold binding, rebind it on reload via update_binding_combos, carry an optional per-binding "<ini_key>.Consume" suppression facet, and return an InputBindingGuard -- the hold guard synthesizes one balancing on_state_change(false) on cancel so a torn-down hold cannot strand the consumer as held (the teardown gate is the internal src/config_input_fusion.hpp HoldGate)
140
140
config_watcher.hpp # Filesystem watcher (ReadDirectoryChangesW) for INI hot-reload
141
141
input.hpp # Input polling (keyboard/mouse/XInput) + opt-in mouse-wheel capture and gamepad passthrough suppression; generation-checked BindingToken handles let a high-frequency consumer query a binding without the per-call name hash, failing closed on any reshape
142
142
input_codes.hpp # Unified InputCode type and named key tables (incl. WheelUp/Down/Left/Right)
| InputManager |`mutex` for lifecycle, `atomic<shared_ptr<InputPoller>>` for reads |`atomic<shared_ptr<InputPoller>>` acquire-load, then the poller's `shared_lock` + relaxed load (not lock-free) |
377
377
| InputIntercept (internal `src/input_intercept.*`) | File-scope atomics shared between the poll thread and the game's threads (XInput callers, window message thread); owns its safetyhook InlineHooks directly (not via HookManager) because the poll thread reads the trampoline and the hook lifetime is coupled to the poll thread; consume-until-release latch and wheel-pulse state are poll-thread-private; teardown skipped under loader lock (detours left installed against the pinned module) | Lock-free atomic loads in each detour; allocation-free, non-throwing detour bodies |
378
378
| Memory cache | Sharded `SRWLOCK` + epoch-based shutdown. Each shard is `alignas(64)` with its `SrwSharedMutex` and stampede `in_flight` flag stored inline (no per-shard heap allocation, so the shard array is a fixed-size `unique_ptr<CacheShard[]>` that never relocates), keeping one shard's lock word and flag off another shard's cache line. Reader liveness is tracked in cache-line-padded per-thread stripes summed at shutdown rather than one global counter, so concurrent readers do not re-serialize on a single line; the seq_cst stripe increment still Dekker-pairs with the `s_cache_initialized` load so the shutdown drain is exact | Shared reader locks per shard; one striped reader-count increment per call |
379
-
| Config | `mutex` for registration; deferred setter invocation outside lock (no reentrancy guard needed -- setters may call back into Config); `reload()` re-runs the registered items against the stashed INI path using the same deferred pattern and short-circuits on FNV-1a 64 hash match of the on-disk bytes to skip no-op reloads; bytes are read once per load/reload and fed to `CSimpleIniA::LoadData`, so the cached hash and the parsed INI state are guaranteed to reflect the same file snapshot (no TOCTOU between hash and parse); `enable_auto_reload()` owns a `ConfigWatcher` behind a separate `std::mutex` so start/stop transitions do not contend with registration traffic; setters invoked by the watcher run on the watcher thread, setters invoked by the reload hotkey run on a dedicated `ReloadServicer` thread (lazily started on first `register_reload_hotkey`, torn down in `clear_registered_items()`) so the `InputManager` poll thread never blocks on INI parsing; the servicer's press-request path takes its internal `m_mutex` around the predicate store before `cv.notify_one` to close the lost-wakeup window; all setters must be reentrant and thread-safe | N/A (startup only) |
379
+
| Config | `mutex` for registration; deferred setter invocation outside lock (no reentrancy guard needed -- setters may call back into Config); `reload()` re-runs the registered items against the stashed INI path using the same deferred pattern and short-circuits on FNV-1a 64 hash match of the on-disk bytes to skip no-op reloads; bytes are read once per load/reload and fed to `CSimpleIniA::LoadData`, so the cached hash and the parsed INI state are guaranteed to reflect the same file snapshot (no TOCTOU between hash and parse); `enable_auto_reload()` owns a `ConfigWatcher` behind a separate `std::mutex` so start/stop transitions do not contend with registration traffic; setters invoked by the watcher run on the watcher thread, setters invoked by the reload hotkey run on a dedicated `ReloadServicer` thread (lazily started on first `register_reload_hotkey`, torn down in `clear_registered_items()`) so the `InputManager` poll thread never blocks on INI parsing; the servicer's press-request path takes its internal `m_mutex` around the predicate store before `cv.notify_one` to close the lost-wakeup window; all setters must be reentrant and thread-safe; the hold-combo `InputBindingGuard` owns a per-binding `HoldGate` (`src/config_input_fusion.hpp`) whose `recursive_mutex` serializes the poll-thread callback wrapper against the control-plane `release()`, so a cancelled hold delivers exactly one balancing `on_state_change(false)` and never a stale `true` after it (a re-entrant self-release from inside the callback recurses without deadlock and defers its balancing edge to the wrapper's unwind) | N/A (startup only) |
380
380
| ConfigWatcher | One `StoppableWorker` per instance; worker opens the parent directory with `FILE_FLAG_BACKUP_SEMANTICS` and `FILE_FLAG_OVERLAPPED`, then pumps `ReadDirectoryChangesW` via `GetOverlappedResultEx` with a 100 ms timeout so `stop_token` is observed promptly; on stop the in-flight read is cancelled and drained with a bounded, escalating wait (timed `GetOverlappedResultEx`, then directory-handle close to force the orphaned IRP to complete, then leak the heap-bundled I/O buffer if completion still cannot be confirmed) so a deleted watched directory cannot hang teardown; debounce uses `steady_clock`; filename match is case-insensitive; `start()` and `stop()` are idempotent and serialized by an internal `std::mutex`; under loader lock the destructor pins the module, requests stop on the worker, and moves `Impl` into a per-call heap cell allocated via `new (std::nothrow)` (with a `release()` fallback on OOM that leaks the raw pointer instead of running `~Impl`) so the noexcept destructor stays honest, mirroring the `Logger::shutdown_internal` discipline | 100 ms `GetOverlappedResultEx` pump; idle CPU ~0 |
381
381
| EventDispatcher |`emit()` / `emit_safe()` with no user-visible mutex on the read path via `std::atomic<std::shared_ptr<const std::vector<Entry>>>` snapshot (copy-on-write publish, acquire-load on read); zero-subscriber fast path skips the snapshot load via an atomic handler counter; writers serialize on a small `std::mutex` that never touches the emit hot path; thread-local reentrancy guard rejects subscribe/unsubscribe from within handlers so the no-mutation-during-emit invariant holds; `emit()` propagates handler exceptions, `emit_safe()` catches and skips them | Atomic acquire-load of a `shared_ptr` snapshot plus linear iteration over a contiguous vector; no reader lock |
382
382
| Profiler | Lock-free ring buffer via atomic `fetch_add` on write position; odd/even sequence counter per sample slot prevents torn reads during concurrent export: `record()` opens and closes the slot with unconditional `fetch_add` (never a load-then-store) so concurrent producers racing on the same slot cannot roll the counter backwards, and the cold export path is a seqlock reader (load the sequence, copy the fields into locals, re-load the sequence behind an acquire fence, and drop the sample if it changed or is odd); `DMK_PROFILE_SCOPE(name)` requires `name` to be a string literal, enforced at compile time by a `ScopedProfile` constructor that only binds to `const char (&)[N]`| Single atomic increment + sequence-guarded field writes per sample |
- Named keys (`Ctrl`, `F3`, `Mouse1`, `Gamepad_A`), hex VK codes (`0x72`), and mixed formats
83
83
- Opt-out sentinels: an empty value or the literal `NONE` (case-insensitive, whole-string only) leaves the binding unbound silently. A non-empty value whose every token fails to parse is logged at WARNING level naming the binding and the offending raw string.
84
-
- Convenience helpers: `register_log_level` (parses an INI string into a `Logger::set_log_level` call) and `register_atomic<T>` for `int`/`bool`/`float` (writes the parsed value into a caller-supplied `std::atomic<T>` with `memory_order_relaxed`)
84
+
- Convenience helpers: `register_log_level` (parses an INI string into a `Logger::set_log_level` call) and `register_atomic<T>` for `int`/`bool`/`float` (writes the parsed value into a caller-supplied `std::atomic<T>` with `memory_order_relaxed`; the 4-argument overload uses the atomic's current value as the registration default)
85
85
-**Hot-reload** (see [Config Hot-Reload Guide](docs/config-hot-reload/README.md)):
86
86
-`Config::reload()` re-runs every registered setter against the last-loaded INI without touching registrations; skips setters when the on-disk bytes are byte-identical to the last load (FNV-1a content hash)
87
87
-`Config::enable_auto_reload()` starts a background `ConfigWatcher` (`config_watcher.hpp`) that debounces editor save-flurries and triggers `reload()` automatically; returns an `AutoReloadStatus` enum indicating outcome
@@ -301,6 +301,8 @@ See the [Config Hot-Reload Guide](docs/config-hot-reload/README.md) for the thre
301
301
- Named key resolution uses binary search for efficient lookup
302
302
- `register_press` and `register_hold` accept `KeyComboList` directly for zero-boilerplate binding of config-parsed key combos
303
303
- Live registration: `register_press` / `register_hold` append bindings to a running poller, and `clear_bindings()` / `remove_binding_by_name()` drop bindings without stopping the poll thread, so consumers can re-arm input on hot-reload without a full restart
304
+
- `Config::register_press_combo` / `Config::register_hold_combo` fuse an INI combo item, the matching `InputManager::register_press` / `register_hold` binding, automatic rebind on `reload()`, and an `InputBindingGuard` cancellation token into one call. The hold variant's guard synthesizes a single balancing `on_state_change(false)` when cancelled mid-hold, so tearing a hold down cannot strand the consumer in the held state
305
+
- Both combo registrars take an optional per-binding `consume` facet: pass `consume` to register a `"<ini_key>.Consume"` bool inline (wired to `set_consume`) instead of a separate `register_consume_flag` call. `std::nullopt` (the default) registers no extra key and preserves the prior behavior exactly; suppression honors the same gamepad-digital + mouse-wheel scope noted above, so a `.Consume` key on a keyboard-only binding is inert
- Feature flags that branch inside a hook callback.
115
115
- Strings displayed in UI.
116
-
- Key combos registered via `Config::register_press_combo`: the combo machinery calls `InputManager::update_binding_combos` on reload, which swaps keys/modifiers in place without re-registering the binding.
116
+
- Key combos registered via `Config::register_press_combo` / `Config::register_hold_combo`: the combo machinery calls `InputManager::update_binding_combos` on reload, which swaps keys/modifiers in place without re-registering the binding. A `consume` facet passed to either registrar adds a `"<ini_key>.Consume"` bool that hot-reloads alongside the combo.
117
117
118
118
**Restart required** (reloading silently has no effect, or is actively unsafe):
0 commit comments