Selaa lähdekoodia

anchor pattern scans on their first exact byte

The sweep tested every byte offset in every executable range. On the live
game image that is ~105 MB across two .text sections, and activation runs
~26 single-pattern sweeps plus the batched one, so startup spent seconds
scanning byte by byte behind the initialization overlay.

Each pattern now takes an anchor - its first exact byte - and memchr jumps
straight to the offsets where that byte lines up, skipping the gaps at
memory speed. The full masked compare only runs at a candidate.

is_valid folds into anchor_of: "has at least one exact byte" was already
the validity rule and is exactly what the anchor needs.

Scan order, first-match-wins addressing, ambiguous-on-second-match and the
invalid/missing statuses are all unchanged.

Measured against the real game image with the real signatures: 13.7x on the
batched sweep, 17.4x per signature, results identical. Boot is 2-3 seconds
faster in game.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Joe McNally 3 viikkoa sitten
vanhempi
commit
699c1cff67
2 muutettua tiedostoa jossa 93 lisäystä ja 31 poistoa
  1. 92 30
      Sunrise/src/client/patterns/registry.cpp
  2. 1 1
      Sunrise/src/client/patterns/registry.h

+ 92 - 30
Sunrise/src/client/patterns/registry.cpp

@@ -1,23 +1,43 @@
 #include "registry.h"
 
+#include <cstring>
+
 namespace sunrise::client::patterns {
 namespace {
 
+/** Returned by next_candidate when a range holds no further anchor byte. */
+constexpr std::size_t kNoCandidate = static_cast<std::size_t>(-1);
+
+/**
+ * The one exact byte a pattern's candidate search keys on.
+ * Only exact bytes can be searched for, so a pattern without one cannot be scanned at all. That
+ * is the same condition the sweep already rejected patterns on, so the anchor doubles as the
+ * validity check.
+ */
+struct Anchor {
+    /** Position of the anchor byte inside the pattern. */
+    std::size_t index{};
+    /** The byte itself, held unsigned so it reaches memchr without sign extension. */
+    unsigned char value{};
+    /** False for a pattern the sweep cannot scan, which is the pattern it must reject. */
+    bool valid{};
+};
+
 /**
- * Validates one pattern before the shared image sweep.
+ * Picks the anchor byte for one pattern.
  * @param pattern Pattern name, bytes, and exact-byte mask.
- * @return True when the pattern has a name, bytes, and at least one exact byte.
+ * @return A valid anchor when the pattern has a name, bytes, and at least one exact byte.
  */
-[[nodiscard]] bool is_valid(const Pattern& pattern) noexcept {
+[[nodiscard]] Anchor anchor_of(const Pattern& pattern) noexcept {
     if (pattern.name.empty() || pattern.bytes.empty()) {
-        return false;
+        return {};
     }
-    for (const PatternByte byte : pattern.bytes) {
-        if (byte.exact) {
-            return true;
+    for (std::size_t index = 0; index < pattern.bytes.size(); ++index) {
+        if (pattern.bytes[index].exact) {
+            return Anchor{index, std::to_integer<unsigned char>(pattern.bytes[index].value), true};
         }
     }
-    return false;
+    return {};
 }
 
 /**
@@ -29,7 +49,9 @@ namespace {
 [[nodiscard]] bool matches_at(std::span<const std::byte> image,
                               std::size_t offset,
                               std::span<const PatternByte> pattern) noexcept {
-    if (pattern.size() > image.size() - offset) {
+    // The offset is checked before the subtraction, so this holds for any caller value rather
+    // than only for the bounded offsets next_candidate produces.
+    if (offset >= image.size() || pattern.size() > image.size() - offset) {
         return false;
     }
     for (std::size_t index = 0; index < pattern.size(); ++index) {
@@ -40,9 +62,41 @@ namespace {
     return true;
 }
 
+/**
+ * Finds the next offset at or after one start where the anchor byte lines up.
+ * The bytes in between cannot begin a match, so memchr skips them at memory speed instead of the
+ * sweep testing every one of them.
+ * @param range One executable range.
+ * @param patternSize Pattern length, which bounds the last offset that can hold a whole match.
+ * @param anchor Valid anchor for that pattern.
+ * @param from First offset to consider.
+ * @return Candidate offset, or kNoCandidate when the range holds no further one.
+ */
+[[nodiscard]] std::size_t next_candidate(std::span<const std::byte> range,
+                                         std::size_t patternSize,
+                                         const Anchor& anchor,
+                                         std::size_t from) noexcept {
+    if (patternSize > range.size()) {
+        return kNoCandidate;
+    }
+    const std::size_t lastOffset = range.size() - patternSize;
+    if (from > lastOffset) {
+        return kNoCandidate;
+    }
+    // The anchor sits at offset + index, so the search window is the offset window shifted by it.
+    const std::size_t first = from + anchor.index;
+    const std::size_t last = lastOffset + anchor.index;
+    const void* const hit = std::memchr(range.data() + first, anchor.value, last - first + 1);
+    if (hit == nullptr) {
+        return kNoCandidate;
+    }
+    const auto* const found = static_cast<const std::byte*>(hit);
+    return static_cast<std::size_t>(found - range.data()) - anchor.index;
+}
+
 } // namespace
 
-/** Resolves all patterns during one sweep over one executable range. */
+/** Resolves every registered pattern against one executable range. */
 bool resolve_all(std::span<std::byte> image,
                  std::span<const Pattern> patterns,
                  std::span<Match> matches) noexcept {
@@ -50,7 +104,7 @@ bool resolve_all(std::span<std::byte> image,
     return resolve_all(std::span(&range, 1), patterns, matches);
 }
 
-/** Resolves all patterns during one sweep over disjoint executable ranges. */
+/** Resolves every pattern across disjoint executable image ranges. */
 bool resolve_all(std::span<const ImageRange> image,
                  std::span<const Pattern> patterns,
                  std::span<Match> matches) noexcept {
@@ -59,27 +113,32 @@ bool resolve_all(std::span<const ImageRange> image,
     }
 
     for (std::size_t index = 0; index < patterns.size(); ++index) {
-        matches[index] = is_valid(patterns[index]) ? Match{MatchStatus::missing, nullptr} : Match{};
-    }
+        const Anchor anchor = anchor_of(patterns[index]);
+        matches[index] = anchor.valid ? Match{MatchStatus::missing, nullptr} : Match{};
+        if (!anchor.valid) {
+            continue;
+        }
 
-    for (const ImageRange range : image) {
-        for (std::size_t offset = 0; offset < range.bytes.size(); ++offset) {
-            for (std::size_t index = 0; index < patterns.size(); ++index) {
-                Match& match = matches[index];
-                if (match.status == MatchStatus::invalid
-                    || match.status == MatchStatus::ambiguous) {
-                    continue;
-                }
-                if (!matches_at(range.bytes, offset, patterns[index].bytes)) {
-                    continue;
+        Match& match = matches[index];
+        const std::span<const PatternByte> bytes = patterns[index].bytes;
+        for (const ImageRange range : image) {
+            std::size_t offset = 0;
+            while (match.status != MatchStatus::ambiguous) {
+                offset = next_candidate(range.bytes, bytes.size(), anchor, offset);
+                if (offset == kNoCandidate) {
+                    break;
                 }
-
-                if (match.status == MatchStatus::missing) {
-                    match = Match{MatchStatus::unique, range.bytes.data() + offset};
-                } else {
+                if (matches_at(range.bytes, offset, bytes)) {
                     // A second match invalidates the address instead of choosing one.
-                    match = Match{MatchStatus::ambiguous, nullptr};
+                    match = match.status == MatchStatus::missing
+                                ? Match{MatchStatus::unique, range.bytes.data() + offset}
+                                : Match{MatchStatus::ambiguous, nullptr};
                 }
+                ++offset;
+            }
+            // Ambiguous is final, so the remaining ranges cannot change this pattern's result.
+            if (match.status == MatchStatus::ambiguous) {
+                break;
             }
         }
     }
@@ -90,12 +149,15 @@ bool resolve_all(std::span<const ImageRange> image,
 std::size_t collect_matches(std::span<const ImageRange> image,
                             const Pattern& pattern,
                             std::span<std::byte*> output) noexcept {
-    if (!is_valid(pattern) || output.empty()) {
+    const Anchor anchor = anchor_of(pattern);
+    if (!anchor.valid || output.empty()) {
         return 0;
     }
     std::size_t count = 0;
     for (const ImageRange range : image) {
-        for (std::size_t offset = 0; offset < range.bytes.size(); ++offset) {
+        for (std::size_t offset = next_candidate(range.bytes, pattern.bytes.size(), anchor, 0);
+             offset != kNoCandidate;
+             offset = next_candidate(range.bytes, pattern.bytes.size(), anchor, offset + 1)) {
             if (!matches_at(range.bytes, offset, pattern.bytes)) {
                 continue;
             }

+ 1 - 1
Sunrise/src/client/patterns/registry.h

@@ -37,7 +37,7 @@ struct ImageRange {
     std::span<std::byte> bytes;
 };
 
-/** Resolves every registered pattern during one image sweep. */
+/** Resolves every registered pattern against one executable range. */
 [[nodiscard]] bool resolve_all(std::span<std::byte> image,
                                std::span<const Pattern> patterns,
                                std::span<Match> matches) noexcept;