Skip to content

Commit 621ddd0

Browse files
authored
feat(scanner): parallel cascade and anchor batch resolution (#126)
1 parent dcfa196 commit 621ddd0

18 files changed

Lines changed: 935 additions & 195 deletions

AGENTS.md

Lines changed: 10 additions & 9 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ DetourModKit is a full-featured C++23 toolkit designed to simplify common tasks
1010

1111
| Module | Description | Header |
1212
|--------|-------------|--------|
13-
| AOB Scanner | SIMD-accelerated pattern scanning with full-byte and per-nibble wildcards, cross-region-boundary overlap, RIP resolution, multi-candidate cascade resolver with E9/FF25 prologue-recovery fallback, host-EXE cascade overloads, in-code constant (immediate/displacement) extraction, and string-reference (xref) resolution (load site, enclosing function, or cached global pointer slot; fast lea/mov shape scan plus an opt-in Zydis sweep for cmp/push/no-REX shapes) | `scanner.hpp` |
13+
| AOB Scanner | SIMD-accelerated pattern scanning with full-byte and per-nibble wildcards, cross-region-boundary overlap, RIP resolution, raw pattern batch scanning, multi-candidate cascade resolver with E9/FF25 prologue-recovery fallback, parallel cascade batch resolution, host-EXE cascade overloads, in-code constant (immediate/displacement) extraction, and string-reference (xref) resolution (load site, enclosing function, or cached global pointer slot; fast lea/mov shape scan plus an opt-in Zydis sweep for cmp/push/no-REX shapes) | `scanner.hpp` |
1414
| Hook Manager | Inline, mid-function, and VMT hooks via SafetyHook with cross-module duplicate-hook detection | `hook_manager.hpp` |
1515
| Configuration | INI-based settings with key combo support and hot-reload (file watcher + hotkey) | `config.hpp`, `config_watcher.hpp` |
1616
| Logger | Synchronous singleton logger with format strings | `logger.hpp` |
1717
| Async Logger | Lock-free bounded queue logger with batched writes | `async_logger.hpp` |
1818
| Memory Utilities | Readability checks, region cache, safe pointer reads, typed SEH reads, PE module range queries | `memory.hpp` |
1919
| MSVC RTTI Walker | Recover mangled type names from runtime vtables; pointer-table scan with caller-owned cache; reverse name-to-vtable resolver and cached identity handle | `rtti.hpp` |
2020
| RTTI Self-Heal | Reverse-identify the object behind a pointer slot (typed-error and ordered candidate-fallback forms); self-heal a field offset after a patch shifts the struct layout; rigid multi-field drift solver; drift-telemetry report with a durable, diffable manifest (open-failure distinguished from corrupt) | `rtti_dissect.hpp`, `drift_manifest.hpp` |
21-
| Anchor Registry | One declarative table over the self-healing backends (vtable-by-name, AOB/RIP cascade, in-code constant, string xref, pinned literal) plus two-signal quorum corroboration with sub-anchor independence checks, optional post-resolve validators and opt-in validator policies, a manifest quality diagnostic, and a per-game scan profile (broad-mode default, candidate order, backend deny-list), resolved and reported in a single pass | `anchors.hpp`, `profile.hpp` |
21+
| Anchor Registry | One declarative table over the self-healing backends (vtable-by-name, AOB/RIP cascade, in-code constant, string xref, pinned literal) plus two-signal quorum corroboration with sub-anchor independence checks, optional post-resolve validators and opt-in validator policies, a manifest quality diagnostic, opt-in parallel table resolution, and a per-game scan profile (broad-mode default, candidate order, backend deny-list), resolved and reported in a single pass | `anchors.hpp`, `profile.hpp` |
2222
| Event Dispatcher | Typed pub/sub with RAII subscriptions | `event_dispatcher.hpp` |
2323
| Profiler | Scoped timing with Chrome Tracing export (zero-cost when disabled) | `profiler.hpp` |
2424
| Format Utilities | `std::format` helpers for addresses, bytes, and VK codes; string trim | `format.hpp` |
@@ -45,6 +45,7 @@ DetourModKit is a full-featured C++23 toolkit designed to simplify common tasks
4545
- `scan_readable_regions()` -- the data-section sibling of `scan_executable_regions()` -- sweeps every committed readable page (`.rdata` / `.data`, read-only heaps) to reach C++ vtables, RTTI type descriptors, and read-only metadata the executable-only sweep cannot see (guard / no-access / uncommitted pages are skipped); opt a cascade into it with `resolve_cascade(..., ScannerKind::Readable)`
4646
- The region-walking sweeps (`scan_executable_regions` / `scan_readable_regions` and the module-scoped scans) carry a `pattern_len - 1` overlap across adjacent accepted `VirtualQuery` regions, so a signature that straddles a protection split (two adjacent regions whose base protections differ, e.g. after a sibling `VirtualProtect` carves up `.text`) is still found, without re-counting a match that lies wholly inside one region
4747
- `scan_regions_batch()` / `scan_module_batch()` -- opt-in fork-join batch scanners that resolve many compiled patterns concurrently (whole process or one mapped image). Each `BatchScanItem` is one independent serial scan distributed across a transient worker pool, so a startup set of N signatures resolves in roughly the time of the slowest single scan rather than their sum; results come back in input order, each item fails closed on a null/empty pattern, zero occurrence, or no match, and patterns are shared read-only (a compiled `CompiledPattern` is immutable during scanning, so no cloning). Setup/control-plane only -- it spawns and joins threads, so never call it from a hook/input callback or under the loader lock
48+
- `resolve_cascade_batch()` -- opt-in fork-join resolver for startup target tables. Each `CascadeRequest` dispatches to the existing serial cascade resolver (whole-process, module-scoped, with or without prologue fallback), so candidate priority, uniqueness checks, typed errors, and `winning_name` aliasing stay identical to the one-at-a-time calls while results come back in input order
4849
- `resolve_cascade()` and the module-scoped `resolve_cascade_in_module()` -- ordered multi-candidate resolution (try signatures in priority order, return the first that resolves), with an optional hooked-prologue recovery pass that recovers both an `E9` near-jump and an `FF 25` RIP-relative indirect-jump overwritten prologue (so a target inline-hooked by another mod with either a near or a far jump is recovered); the `_in_module` variants confine the scan to one mapped image `[base, end)` and reject out-of-module resolutions, so a generic signature that also matches inside another injected module (a graphics overlay, a sibling mod) cannot shadow the correct in-module target. By default each candidate must match uniquely in the scanned scope: an ambiguous signature (more than one match) falls through to the next candidate instead of silently committing to an arbitrary match, so a too-loose pattern surfaces as a clean failure to fix rather than a wrong hook. Set `require_unique = false` per candidate only to opt out a deliberately non-unique, separately-verified candidate
4950
- `resolve_cascade_in_host_module()` / `resolve_cascade_in_host_module_with_prologue_fallback()` -- one-line convenience overloads that scope a cascade to the host EXE (`host_module_range()`), removing the boilerplate of building the range at every call site. They return `ResolveError::InvalidRange` if the host range cannot be determined. Use them only when the target lives in the host EXE; for a game whose logic is in a separate module (an engine DLL), resolve that module's range and call `resolve_cascade_in_module()` instead
5051
- `read_code_constant(cc, range?)` -- the code-side twin of the RTTI self-heal: declare an instruction site (an AOB cascade) plus which operand to read, and it decodes the live instruction and returns the current immediate or `[reg + disp]` displacement, so a hand-read array stride or struct displacement re-derives itself after a patch instead of being a baked literal. It always decodes (the `nominal` field is telemetry only, never a short-circuit, so a same-shape / different-value drift is reported as the new value), indexes the **visible** operands, resolves a RIP-relative operand to its absolute target, and fails closed (`DecodeFailed` / `UnexpectedShape` / `OperandOutOfRange`). Built on a Zydis decoder kept entirely inside the implementation, so no consumer needs Zydis headers
@@ -190,7 +191,7 @@ See the [Config Hot-Reload Guide](docs/config-hot-reload/README.md) for the thre
190191
- One declarative table (`Anchors::Anchor[]`) over the self-healing backends, so every magic constant a mod depends on is declared once with its kind and inputs, then resolved and reported in a single pass instead of a scattered wall of hand-maintained offsets and per-call-site resolvers
191192
- `AnchorKind` covers the backends that resolve from a module range alone: `VtableIdentity` (`Rtti::vtable_for_type`), `RipGlobal` (an AOB/RIP cascade returning an absolute address), `CodeOperand` (`Scanner::read_code_constant`), `StringXref` (`Scanner::find_string_xref`, the most update-resilient kind: the unique instruction or enclosing function that references an immutable string literal), and `Manual` (a pinned literal, surfaced as at-risk in a report). `Quorum` layers corroboration on top: it accepts a target only when two independent sub-anchors resolve and agree (exact or within a tolerance), so a coincidental match must fool both signals. `CallArgHome` is reserved for a future prologue-dataflow backend and reports `Unsupported`
192193
- Any backend-resolved anchor may carry an optional `validator` (`bool(*)(value, context) noexcept`) that screens the resolved value and fails the anchor closed when it returns false, letting a caller assert a domain invariant a generic backend cannot know
193-
- `Anchors::resolve(anchor, range?)` resolves one entry; `resolve_all(anchors, out, range?)` fills a parallel `ResolvedAnchor` report (`{label, kind, status, value}`). Resolution is idempotent and side-effect-free, so re-heal-on-miss is just re-running `resolve` on the failing anchor
194+
- `Anchors::resolve(anchor, range?)` resolves one entry; `resolve_all(anchors, out, range?)` fills a `ResolvedAnchor` report (`{label, kind, status, value}`), and `resolve_all_parallel` does the same work through an opt-in fork-join table resolver. Profile-aware callers have matching `resolve_all_with_profile` and `resolve_all_with_profile_parallel` entry points. Resolution is idempotent and side-effect-free, so re-heal-on-miss is just re-running `resolve` on the failing anchor
194195
- RTTI pointer-field offset healing (`heal_landmark`) is intentionally not a registry kind: it needs a runtime struct base resolved from another anchor, so it is driven directly once that base is known
195196
196197
</details>

0 commit comments

Comments
 (0)