JaffarPlus
High-performance best-first search optimizer for tool-assisted speedruns
Loading...
Searching...
No Matches
engine.hpp
Go to the documentation of this file.
1#pragma once
2
10#include "game.hpp"
11#include "hashDb.hpp"
12#include "numa.hpp"
13#include "runner.hpp"
14#include "stateDb.hpp"
15#include <algorithm>
16#include <emulatorList.hpp>
17#include <fstream>
18#include <gameList.hpp>
19#include <jaffarCommon/deserializers/base.hpp>
20#include <jaffarCommon/file.hpp>
21#include <jaffarCommon/hash.hpp>
22#include <jaffarCommon/json.hpp>
23#include <jaffarCommon/logger.hpp>
24#include <jaffarCommon/parallel.hpp>
25#include <jaffarCommon/serializers/base.hpp>
26#include <jaffarCommon/timing.hpp>
27#include <mutex>
28#include <set>
29
30// Fine-grained per-operation timing for the engine hot loop. Each timed region costs two
31// clock_gettime calls; multiplied across millions of states processed per step this is a real
32// overhead for a light emulator (~25% with QuickerSDLPoP), while being negligible under a heavy one
33// (QuickerNES is ~92% emulation). It is therefore compiled out unless built with
34// -DdetailedProfiling=true. When disabled, JAFFAR_PROF_DECL declares no variable and JAFFAR_PROF_ACC
35// is a no-op, so there is neither timing overhead nor an unused-variable warning. The coarse
36// per-step timers (step wall time, throughput, serial DB-advance stages) are always kept.
37#ifdef JAFFARPLUS_DETAILED_PROFILING
39#define JAFFAR_PROF_DECL(var) const auto var = jaffarCommon::timing::now()
41#define JAFFAR_PROF_ACC(field, var) field += jaffarCommon::timing::timeDeltaMicroseconds(jaffarCommon::timing::now(), var)
42#else
43#define JAFFAR_PROF_DECL(var) ((void)0)
44#define JAFFAR_PROF_ACC(field, var) ((void)0)
45#endif
46
47namespace jaffarPlus
48{
49
60class Engine final
61{
62public:
71 Engine(const nlohmann::json& emulatorConfig, const nlohmann::json& gameConfig, const nlohmann::json& runnerConfig, const nlohmann::json& engineConfig)
72 {
73 // Initializing NUMA and threading subsystems
75
76 // Sanity check
77 if (_threadCount == 0) JAFFAR_THROW_LOGIC("The number of worker threads must be at least one. Provided: %lu\n", _threadCount);
78
79 // Printing initial information
80 jaffarCommon::logger::log("[J+] Using %lu worker threads.\n", _threadCount);
81
82 // Creating storage for the runnners (one per thread)
83 _runners.resize(_threadCount);
84
85 // Creating runners, one per thread
86 JAFFAR_PARALLEL
87 {
88 // Creating runner from the configuration
89 auto r = jaffarPlus::Runner::getRunner(emulatorConfig, gameConfig, runnerConfig);
90
91 // Storing runner. Index by the (dense) OpenMP thread id, NOT sched_getcpu(): the latter is only
92 // equal to the thread id when threads are pinned 1:1 (OMP_PROC_BIND), and otherwise leaves
93 // _runners[0] null -> the *_runners[0] use below (and workerFunction, which already uses the
94 // thread id) would dereference a null runner. This bites CI runners that don't pin threads.
95 _runners[jaffarCommon::parallel::getThreadId()] = std::move(r);
96 }
97
98 // Grabbing a runner to do continue build the state databases
99 auto& r = *_runners[0];
100
101 // Mutable copy so unrecognized Engine keys can be flagged after the known ones are consumed
102 auto engineConfigRemaining = engineConfig;
103
104 // Creating State database
105 auto stateDatabaseJs = jaffarCommon::json::popObject(engineConfigRemaining, "State Database");
106 _stateDb = std::make_unique<jaffarPlus::StateDb>(r, stateDatabaseJs);
107
108 // Creating hash database
109 auto hashDbConfig = jaffarCommon::json::popObject(engineConfigRemaining, "Hash Database");
110 _hashDbEnabled = jaffarCommon::json::getBoolean(hashDbConfig, "Enabled");
111 if (_hashDbEnabled == true) _hashDb = std::make_unique<jaffarPlus::HashDb>(hashDbConfig);
112
113 // Base-state pull batch size (per-worker queue-pull granularity). Optional: an explicit value
114 // always wins; otherwise 0 here means "auto-tune", resolved in initialize() from the measured
115 // per-state cost (light cores -> larger batch to amortize the queue lock; heavy/variable cores
116 // -> small batch for load balance).
117 _baseStateBatch = engineConfigRemaining.contains("Base State Batch Size") ? jaffarCommon::json::popNumber<size_t>(engineConfigRemaining, "Base State Batch Size") : 0;
119
120 // Log verbosity: "Full" (default, everything) or "Compact" (per-step engine block collapses to
121 // step timing, checkpoint, state-flow counters, and one-line DB summaries; the game module's own
122 // print is unaffected -- it is what live observers actually watch). Labels that external
123 // tooling greps ("Win States:", "Checkpoint (Level/Tolerance/Cutoff)") are kept verbatim.
124 _compactLog = false;
125 if (engineConfigRemaining.contains("Log Verbosity"))
126 {
127 const auto v = jaffarCommon::json::popString(engineConfigRemaining, "Log Verbosity");
128 if (v != "Full" && v != "Compact") JAFFAR_THROW_LOGIC("[ERROR] 'Log Verbosity' must be 'Full' or 'Compact' (got '%s')\n", v.c_str());
129 _compactLog = (v == "Compact");
130 }
131
132 // Optional reference pinning: keep a known (reference) solution's lineage anchored in the frontier so
133 // it is never evicted, and reward being AHEAD of it. We load the reference's per-depth state hashes;
134 // when a newly generated state's hash matches the reference hash at depth d (its own depth), or at
135 // d+1..d+Lookahead (i.e. it has reached the reference's near-future state early -- it is 1..L frames
136 // ahead), we add a large reward bonus, escalating with how far ahead it is. The base match (k=0)
137 // guarantees the reference path is never the worst state (never evicted); the k>0 matches steer the
138 // search to stay slightly ahead. Purely additive to the DB-ordering reward; the reference-floor
139 // comparison uses the unbiased floor reward and is unaffected.
140 // Hash Lookahead: hash the state as it looks after N null-input advances instead of the
141 // current state (default 0 = off). Pending-input residue (controller latch, buffered
142 // presses, dig edge triggers, facing) materializes into RAM within a frame, so would-be
143 // divergent twins get distinct hashes WITHOUT hashing transient bits (which explodes the
144 // state DB). Cost: N extra advances + a serialize/deserialize round-trip per hashed state.
145 _hashLookahead = engineConfigRemaining.contains("Hash Lookahead") ? jaffarCommon::json::popNumber<size_t>(engineConfigRemaining, "Hash Lookahead") : 0;
146
147 _refPinEnabled = false;
148 if (engineConfigRemaining.contains("Reference Pinning"))
149 {
150 auto pinJs = jaffarCommon::json::popObject(engineConfigRemaining, "Reference Pinning");
151 _refPinEnabled = jaffarCommon::json::popBoolean(pinJs, "Enabled");
152 _refPinBonus = jaffarCommon::json::popNumber<float>(pinJs, "Bonus");
153 _refPinLookahead = jaffarCommon::json::popNumber<size_t>(pinJs, "Lookahead");
154 _refPinLookaheadBonus = jaffarCommon::json::popNumber<float>(pinJs, "Lookahead Bonus");
155 const auto pinPath = jaffarCommon::json::popString(pinJs, "Path");
156 // Optional: byte-exact verification of hash-matched pin candidates against the captured
157 // reference states. Disable for cores whose serialize/advance round-trip accumulates
158 // gameplay-neutral drift in internal timing bytes (the drift never byte-matches the live
159 // capture, so exact verification starves the pin) -- with a discriminating game hash,
160 // hash-only pinning is sound and a tolerance-0 reference floor acts as the tripwire.
161 _refPinExactVerify = pinJs.contains("Exact Verification") ? jaffarCommon::json::popBoolean(pinJs, "Exact Verification") : true;
162 jaffarCommon::json::checkEmpty(pinJs, "Engine Configuration > Reference Pinning");
163 if (_refPinEnabled && std::getenv("JAFFAR_IS_DRY_RUN") == nullptr)
164 {
165 std::ifstream f(pinPath);
166 if (f.good() == false) JAFFAR_THROW_RUNTIME("[ERROR] Could not open 'Reference Pinning' > 'Path': '%s'\n", pinPath.c_str());
167 // Format matches jaffar-player --dumpHashes: "<step>\t<016X first><016X second>" per line.
168 size_t step;
169 std::string hex;
170 while (f >> step >> hex)
171 {
172 if (hex.size() != 32) continue;
173 jaffarCommon::hash::hash_t h;
174 h.first = std::stoull(hex.substr(0, 16), nullptr, 16);
175 h.second = std::stoull(hex.substr(16, 16), nullptr, 16);
176 if (step >= _refPinHashes.size()) _refPinHashes.resize(step + 1);
177 _refPinHashes[step] = h;
178 }
179 _refPinnedAtDepth = std::make_unique<std::atomic<uint8_t>[]>(_refPinHashes.size());
180 for (size_t i = 0; i < _refPinHashes.size(); i++) _refPinnedAtDepth[i].store(0, std::memory_order_relaxed);
181 jaffarCommon::logger::log("[J+] Reference pinning enabled: %lu hashes loaded, bonus %.1f, lookahead %lu (+%.1f/frame ahead)\n", _refPinHashes.size(), _refPinBonus,
183 }
184 }
185
186 // Reference-reward pruning (greedy polish mode): drop every produced non-win state whose floor
187 // reward falls below the reference trace at its depth beyond Tolerance. The trace itself comes
188 // from the driver's "Reference Reward Floor" source (Solution File or Path) -- the driver arms
189 // it via setReferencePruneTrace() once the trace exists; until then the prune is inert.
190 if (engineConfigRemaining.contains("Reference Reward Prune"))
191 {
192 auto pruneJs = jaffarCommon::json::popObject(engineConfigRemaining, "Reference Reward Prune");
193 _refPruneRequested = jaffarCommon::json::popBoolean(pruneJs, "Enabled");
194 _refPruneTolerance = jaffarCommon::json::popNumber<float>(pruneJs, "Tolerance");
195 // Step Grace N: compare each state against the reference N steps EARLIER, so lineages may
196 // trade an accumulated lead for a transient wait (e.g. an enemy-corridor timing gate)
197 // without execution; real lateness beyond N still prunes. Mirrors the floor's knob.
198 _refPruneStepGrace = pruneJs.contains("Step Grace") ? jaffarCommon::json::popNumber<size_t>(pruneJs, "Step Grace") : 0;
199 jaffarCommon::json::checkEmpty(pruneJs, "Engine Configuration > Reference Reward Prune");
200 }
201
202 // Any remaining Engine key is unrecognized
203 jaffarCommon::json::checkEmpty(engineConfigRemaining, "Engine Configuration");
204
205 // Reserving storage for timing information
206 _threadStepTime.resize(_threadCount);
207
208 // Reserving per-thread accumulators (cache-line aligned to avoid false sharing).
209 // These collect all hot-loop timing/counter increments without atomics, and are
210 // reduced into the shared totals once per step (see runStep).
211 _threadAccumulators.resize(_threadCount);
212 };
213
223 {
224 // Initializing state counters
227
228 // Initializing cumulative timing
242
243 // Resetting total running time
245
246 // Resetting state counts
251 _repeatedStates = 0;
252 _failedStates = 0;
253 _winStates = 0;
254 _normalStates = 0;
255
256 // Resetting checkpoint counters
260
261 // Resetting counter for the current step
262 _currentStep = 0;
263
264 // Resetting last active manually solution save rule id
266
267 // Create the one shared input-history backing (e.g. the trie, for the "Trie" strategy; null for
268 // raw/none) and inject it into every worker runner BEFORE they initialize, so all workers share it.
269 // It is sized with one free-list shard per worker thread plus one for the driver's intermediate-result
270 // thread (contention-free alloc/free). The StateDb reaches the strategy via the reference runner.
271 _inputHistoryBacking = inputHistory::createSharedBacking(_runners[0]->getInputHistoryConfig(), _threadCount + 1);
272
273 // Initializing runners, one per thread
274 JAFFAR_PARALLEL
275 {
276 // Creating thread's own runner (index by OpenMP thread id, consistent with construction/workerFunction)
277 auto& r = _runners[jaffarCommon::parallel::getThreadId()];
278
279 // Share the one backing (prefix sharing) and give each worker its own free-list shard (its thread id).
280 r->setInputHistoryBacking(_inputHistoryBacking, (uint32_t)jaffarCommon::parallel::getThreadId(), _threadCount + 1);
281 r->initialize();
282 }
283
284 // Combined RAM guard: the per-NUMA check inside StateDb::initialize() validates ONLY the state
285 // DB, and the hash DB's peak footprint is (Max Store Size x Max Store Count) -- NOT just Max Store
286 // Size. Summing both against total free RAM here catches a silent overcommit that the OS would
287 // otherwise resolve by OOM-killing the process mid-run (hours in). Done before any DB allocates.
288 {
289 const size_t stateBudget = _stateDb->getMaxBudgetBytes();
290 const size_t hashBudget = (_hashDbEnabled == true) ? _hashDb->getMaxBudgetBytes() : 0;
291 // Shared input-history trie: an uncounted structure that grows ~ live-states x depth up to its hard
292 // node cap (~384 GiB for the Trie strategy on this build). Reserve its ceiling. (0 for None/Raw.)
293 const size_t historyBound = inputHistory::getSharedBackingMaxMemoryBytes(_inputHistoryBacking);
294 // The state DB is a fixed pool (1x; full slots are scavenged from the current step, never doubled).
295 // The hash DB phmap grows by doubling, so during a rehash the old table and the new 2x table briefly
296 // coexist -- reserve ~2x the hash budget for that transient. (Empirically a 270 GB state+hash config
297 // peaked at ~520 GB RSS = state + ~2*hash + trie, then OOM-killed mid-run.)
298 const size_t hashPeak = hashBudget * 2;
299 const size_t totalBudget = stateBudget + hashPeak + historyBound;
300 size_t freeRam = 0;
301 for (int i = 0; i < _numaCount; i++)
302 {
303 long long nodeFree = 0;
304 numa_node_size64(i, &nodeFree);
305 freeRam += (size_t)nodeFree;
306 }
307 const size_t usable = (size_t)((double)freeRam * 0.90); // 10% headroom: emulators, OS, fragmentation
308 if (totalBudget > usable)
309 {
310 const double GB = 1024.0 * 1024.0 * 1024.0;
311 JAFFAR_THROW_RUNTIME("Configured database budget exceeds available memory:\n"
312 " State DB ('Max Size (Mb)', fixed pool) = %.1f GB\n"
313 " Hash DB peak (2 x 'Max Store Size' x 'Max Store Count') = %.1f GB\n"
314 " Input-history trie (hard node-storage ceiling) = %.1f GB\n"
315 " TOTAL = %.1f GB\n"
316 " Usable (90%% of %.1f GB free RAM) = %.1f GB\n"
317 "Reduce 'State Database/Max Size (Mb)' and/or 'Hash Database/Max Store Size (Mb)'.\n"
318 "NOTE: the hash DB phmap doubles on growth, so a rehash briefly holds the old + new (2x) table; the\n"
319 "Trie input-history is a separate structure that grows up to ~384 GiB. Both are otherwise uncounted.\n",
320 (double)stateBudget / GB, (double)hashPeak / GB, (double)historyBound / GB, (double)totalBudget / GB, (double)freeRam / GB, (double)usable / GB);
321 }
322 }
323
324 // Initializing State Db
325 _stateDb->initialize();
326
327 // Initializing hash database
328 if (_hashDbEnabled == true) _hashDb->initialize();
329
330 // Grabbing a runner to do continue initialization
331 auto& r = *_runners[0];
332
333 // Auto-tune the base-state pull batch (when "Base State Batch Size" was not set in the config).
334 // We time a burst of state advances on the (just-loaded) initial state -- which is representative
335 // of the core's per-state cost -- then choose a batch so each batch is ~TARGET_BATCH_NS of work:
336 // enough to amortize the per-NUMA queue lock on cheap cores (large batch) while keeping a small
337 // batch on heavy/variable cores so end-of-step load imbalance stays low. The measurement state is
338 // saved and restored so the search still starts from the exact initial state. Batch size never
339 // affects which states are explored, only the work distribution.
340 if (_baseStateBatch == 0)
341 {
342 const size_t stateSize = r.getStateSize();
343 std::vector<char> scratch(stateSize);
344 {
345 jaffarCommon::serializer::Contiguous s(scratch.data(), stateSize);
346 r.serializeState(s);
347 }
348
349 const auto allowedInputs = r.getAllowedInputs();
350 const InputSet::inputIndex_t calInput = allowedInputs.empty() ? (InputSet::inputIndex_t)0 : allowedInputs[0];
351
352 const size_t CAL_FRAMES = 200;
353 const auto tCal0 = jaffarCommon::timing::now();
354 for (size_t i = 0; i < CAL_FRAMES; i++) r.advanceState(calInput);
355 const size_t perStateNs = jaffarCommon::timing::timeDeltaNanoseconds(jaffarCommon::timing::now(), tCal0) / CAL_FRAMES;
356
357 // Restore the exact initial state
358 {
359 jaffarCommon::deserializer::Contiguous d(scratch.data(), stateSize);
360 r.deserializeState(d);
361 }
362
363 // ~200us of work per batch: SDLPoP (~10us/state) -> ~16, Genesis (~600us/state) -> 1.
364 constexpr size_t TARGET_BATCH_NS = 200000;
365 size_t b = (perStateNs > 0) ? ((TARGET_BATCH_NS + perStateNs / 2) / perStateNs) : BASE_STATE_BATCH_MAX;
366 if (b < 1) b = 1;
368 _baseStateBatch = b;
369 jaffarCommon::logger::log("[J+] Auto-tuned base-state batch size: %lu (measured %.1f us/state)\n", _baseStateBatch, (double)perStateNs / 1000.0);
370 }
371 if (_baseStateBatch < 1) _baseStateBatch = 1;
372
373 // Evaluate game rules on the initial state
374 r.getGame()->evaluateRules();
375
376 // Determining new game state type
377 r.getGame()->updateGameStateType();
378
379 // Running game-specific rule actions
380 r.getGame()->runGameSpecificRuleActions();
381
382 // Updating game reward
383 r.getGame()->updateReward();
384
385 // Getting reward for the initial state
386 const auto reward = r.getGame()->getReward();
387
388 // Getting a free state data pointer to store the state into (serial init path -> thread 0)
389 auto stateData = _stateDb->getFreeState(0);
390
391 // Pushing initial state to the next state database
392 _stateDb->pushState(reward, r, stateData);
393
394 // Advancing the step in the state database
395 _stateDb->advanceStep();
396
397 // Getting memory for the reference state
398 _stateSizeInDatabase = _stateDb->getStateSizeInDatabase();
399
400 // Standalone snapshots hold the FULL self-contained state ([hot][history]); the DB slot is hot-only.
401 _fullStateSize = _stateDb->getFullStateSize();
402
403 // Allocating memory for the best win state
405
406 // Allocating memory for manual state saving
408
409 // Getting hash from first state (computeStateHash self-restores under Hash Lookahead)
410 const auto hash = computeStateHash(r);
411
412 // Adding it to the hash DB
413 if (_hashDbEnabled == true) _hashDb->insertHash(hash);
414 }
415
424 void runStep()
425 {
426 // Computing step time
427 const auto tStep = jaffarCommon::timing::now();
428
429 // Clearing step timing for the serially-measured stages (the rest are reduced
430 // from the per-thread accumulators after the parallel region)
433
434 // Resetting per-thread accumulators for this step
435 for (ssize_t i = 0; i < _threadCount; i++) _threadAccumulators[i].reset();
436
437 // Clearing win state reward
438 _stepBestWinState.reward = -std::numeric_limits<float>::infinity();
439
440 // Clearing manually saved state
441 _manualSaveSolution.reward = -std::numeric_limits<float>::infinity();
444
445 // (Manual solution storing) Resetting last active rule id flag
447
448 // Performing one computation step in parallel
449 JAFFAR_PARALLEL
451
452 // Reducing per-thread accumulators into the shared totals (serial, ~threadCount adds).
453 // The per-step raw timers and step counters are zeroed here, then summed; the
454 // run-long state-type counters keep accumulating across steps.
469 for (ssize_t i = 0; i < _threadCount; i++)
470 {
471 const auto& a = _threadAccumulators[i];
472 _runnerStateAdvanceThreadRawTime += a.runnerStateAdvance;
473 _runnerStateLoadThreadRawTime += a.runnerStateLoad;
474 _runnerStateSaveThreadRawTime += a.runnerStateSave;
475 _calculateHashThreadRawTime += a.calculateHash;
476 _checkHashThreadRawTime += a.checkHash;
477 _ruleCheckingThreadRawTime += a.ruleChecking;
478 _getFreeStateThreadRawTime += a.getFreeState;
479 _returnFreeStateThreadRawTime += a.returnFreeState;
480 _calculateRewardThreadRawTime += a.calculateReward;
481 _getAllowedInputsThreadRawTime += a.getAllowedInputs;
482 _getCandidateInputsThreadRawTime += a.getCandidateInputs;
483 _popBaseStateDbThreadRawTime += a.popBaseStateDb;
484 _stepBaseStatesProcessed += a.baseStatesProcessed;
485 _stepNewStatesProcessed += a.newStatesProcessed;
486 _normalStates += a.normalStates;
487 _repeatedStates += a.repeatedStates;
488 _failedStates += a.failedStates;
489 _winStates += a.winStates;
490 _droppedStatesNoStorage += a.droppedStatesNoStorage;
491 _droppedStatesFailedSerialization += a.droppedStatesFailedSerialization;
492 _droppedStatesCheckpoint += a.droppedStatesCheckpoint;
493 _droppedStatesBelowReference += a.droppedStatesBelowReference;
494 }
495
496 // Advancing hash database state
497 const auto t0 = jaffarCommon::timing::now();
498 if (_hashDbEnabled == true) _hashDb->advanceStep();
499 _advanceHashDbThreadRawTime += jaffarCommon::timing::timeDeltaMicroseconds(jaffarCommon::timing::now(), t0);
500
501 // Swapping next and current state databases
502 const auto t1 = jaffarCommon::timing::now();
503 _stateDb->advanceStep();
504 _advanceStateDbThreadRawTime += jaffarCommon::timing::timeDeltaMicroseconds(jaffarCommon::timing::now(), t1);
505
506 // Updating last active last rule Id
508 {
511 }
512
513 // Computing step time
514 _currentStepTime = jaffarCommon::timing::timeDeltaMicroseconds(jaffarCommon::timing::now(), tStep);
515
516 // Computing total running time
518
519 // Getting maximum thread step time
522 for (ssize_t i = 0; i < _threadCount; i++)
524 {
527 }
528
529 // Processing thread-average step timing
544
545 // Sub-total thread-average step timing
561
562 // Processing cumulative timing
577
578 // Sub-total cumulative time calculation
594
595 // Processing state counters
598
599 // Checkpoint cohort extinction guard: if this step stored ZERO states at the current
600 // checkpoint level, the milestone's surviving lineage is gone -- executing the purge would
601 // wipe the whole database for the benefit of nobody. Demote the level so the search can
602 // re-achieve the milestone; the next genuine rise re-arms the cutoff.
603 if (_checkpointLevel.load(std::memory_order_relaxed) > 0 && _stepMaxLevelStored.load(std::memory_order_relaxed) == 0 && _stateDb->getStateCount() > 0)
604 {
605 std::lock_guard<std::mutex> lk(_checkpointMutex);
606 const auto lvl = _checkpointLevel.load(std::memory_order_relaxed);
607 if (lvl > 0)
608 {
609 _checkpointLevel.store(lvl - 1, std::memory_order_release);
610 jaffarCommon::logger::log("[J+] Checkpoint level %lu cohort extinct -- demoting to %lu\n", lvl, lvl - 1);
611 }
612 }
613 _stepMaxLevelStored.store(0, std::memory_order_relaxed);
614
615 // Advancing step
616 _currentStep++;
617 }
618
623 {
624 // Free the state buffers allocated in initialize() (raw malloc; see _stateSizeInDatabase use above)
627 }
628
629 // Relevant data for the driver
630
632 auto& getStateDb() const { return _stateDb; }
634 auto getStepBestWinState() const { return _stepBestWinState; }
635
638 {
639 std::lock_guard<std::mutex> l(_winCollectLock);
640 return _winKeysSeen.size();
641 }
644 {
645 std::lock_guard<std::mutex> l(_winCollectLock);
647 }
651 size_t getWinCollectionMax() const { return _winCollectMax; }
652
662 void setWinStateCollection(const std::vector<std::string>& dedupProps, const std::string& pathPrefix, const size_t maxFiles)
663 {
664 _winDedupPropNames = dedupProps;
665 _winCollectPrefix = pathPrefix;
666 _winCollectMax = maxFiles;
667 _winCollectEnabled = true;
668 }
669
673
683 void setReferencePruneTrace(const std::vector<float>& trace)
684 {
685 _refPruneTrace = trace;
686 _refPruneEnabled = true;
687 }
688
692 auto getWinStatesFound() const { return _winStates.load(); }
694 auto getStateCount() const { return _stateDb->getStateCount(); }
695
697 size_t getInputHistoryMaxMemoryBytes() const { return inputHistory::getSharedBackingMaxMemoryBytes(_inputHistoryBacking); }
699 size_t getInputHistoryApproxMemoryBytes() const { return inputHistory::getSharedBackingApproxMemoryBytes(_inputHistoryBacking); }
701 bool isInputHistoryExhausted() const { return inputHistory::isSharedBackingExhausted(_inputHistoryBacking); }
702
707 {
708 // Compact mode: the handful of lines observers actually use, then out. Grep-stable labels.
709 if (_compactLog)
710 {
711 jaffarCommon::logger::log("[J+] + Elapsed Time (Step/Total): %9.3fs / %9.3fs\n", 1.0e-6 * (double)(_currentStepTime),
712 1.0e-6 * (double)(_totalRunningTime));
713 jaffarCommon::logger::log("[J+] + Checkpoint (Level/Tolerance/Cutoff): %lu / %lu / %lu\n", _checkpointLevel.load(), _checkpointTolerance.load(),
714 _checkpointCutoff.load());
715 jaffarCommon::logger::log("[J+] + New States Processed: %.3f Mstates (Total: %.3f Mstates) @ %.3f Mstates/s\n",
716 1.0e-6 * (double)_stepNewStatesProcessed, 1.0e-6 * (double)_totalNewStatesProcessed,
717 1.0e-6 * (double)_stepNewStatesProcessed / (1.0e-6 * (double)_currentStepTime));
718 jaffarCommon::logger::log("[J+] + States (fail/rep/drop cumulative): %lu / %lu / %lu\n", _failedStates.load(), _repeatedStates.load(),
720 jaffarCommon::logger::log("[J+] + Win States: %lu (%5.3f%% of New States Processed) \n", _winStates.load(),
721 100.0 * (double)_winStates.load() / (double)_totalNewStatesProcessed);
723 jaffarCommon::logger::log("[J+] + Win States Collected: %lu/%lu\n", (unsigned long)getWinCollectedCount(), (unsigned long)_winCollectMax);
725 jaffarCommon::logger::log("[J+] + Dropped States (Below Reference): %lu (%5.3f%% of New States Processed, prune tol %.1f) \n",
727 if (_refPinEnabled)
728 jaffarCommon::logger::log("[J+] + Reference Pin Hits (cumulative): %lu (deepest %lu / %lu; ref-depth matches seen %lu)\n", _refPinHits.load(),
730 jaffarCommon::logger::log("[J+] + State Db States: %lu (%.2f / %.2f GB, %.1f%% full)\n", _stateDb->getStateCount(),
731 (double)(_stateDb->getStateCount() * _stateDb->getStateSizeInDatabase()) / (1024.0 * 1024.0 * 1024.0),
732 (double)_stateDb->getMaxBudgetBytes() / (1024.0 * 1024.0 * 1024.0),
733 100.0 * (double)(_stateDb->getStateCount() * _stateDb->getStateSizeInDatabase()) / (double)_stateDb->getMaxBudgetBytes());
734 return;
735 }
736 // Printing information
737 jaffarCommon::logger::log("[J+] Thread Count / NUMA Domains: %3d / %d\n", _threadCount, _numaCount);
738#ifdef JAFFARPLUS_DETAILED_PROFILING
739 jaffarCommon::logger::log("[J+] Elapsed Time (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_currentStepTime),
740 100.0 * ((double)(_subTotalAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_totalRunningTime),
741 100.0 * ((double)_subTotalAverageCumulativeTime) / (double)(_totalRunningTime));
742
743 jaffarCommon::logger::log("[J+] + Runner State Avance (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_runnerStateAdvanceAverageTime),
744 100.0 * ((double)(_runnerStateAdvanceAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_runnerStateAdvanceAverageCumulativeTime),
745 100.0 * ((double)_runnerStateAdvanceAverageCumulativeTime) / (double)(_totalRunningTime));
746
747 jaffarCommon::logger::log("[J+] + Runner State Load (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_runnerStateLoadAverageTime),
748 100.0 * ((double)(_runnerStateLoadAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_runnerStateLoadAverageCumulativeTime),
749 100.0 * ((double)_runnerStateLoadAverageCumulativeTime) / (double)(_totalRunningTime));
750
751 jaffarCommon::logger::log("[J+] + Runner State Save (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_runnerStateSaveAverageTime),
752 100.0 * ((double)(_runnerStateSaveAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_runnerStateSaveAverageCumulativeTime),
753 100.0 * ((double)_runnerStateSaveAverageCumulativeTime) / (double)(_totalRunningTime));
754
755 jaffarCommon::logger::log("[J+] + Hash Calculation (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_calculateHashAverageTime),
756 100.0 * ((double)(_calculateHashAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_calculateHashAverageCumulativeTime),
757 100.0 * ((double)_calculateHashAverageCumulativeTime) / (double)(_totalRunningTime));
758
759 jaffarCommon::logger::log("[J+] + Hash Checking (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_checkHashAverageTime),
760 100.0 * ((double)(_checkHashAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_checkHashAverageCumulativeTime),
761 100.0 * ((double)_checkHashAverageCumulativeTime) / (double)(_totalRunningTime));
762
763 jaffarCommon::logger::log("[J+] + Rule Checking (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_ruleCheckingAverageTime),
764 100.0 * ((double)(_ruleCheckingAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_ruleCheckingAverageCumulativeTime),
765 100.0 * ((double)_ruleCheckingAverageCumulativeTime) / (double)(_totalRunningTime));
766
767 jaffarCommon::logger::log("[J+] + Get Free State (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_getFreeStateAverageTime),
768 100.0 * ((double)(_getFreeStateAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_getFreeStateAverageCumulativeTime),
769 100.0 * ((double)_getFreeStateAverageCumulativeTime) / (double)(_totalRunningTime));
770
771 jaffarCommon::logger::log("[J+] + Return Free State (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_returnFreeStateAverageTime),
772 100.0 * ((double)(_returnFreeStateAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_returnFreeStateAverageCumulativeTime),
773 100.0 * ((double)_returnFreeStateAverageCumulativeTime) / (double)(_totalRunningTime));
774
775 jaffarCommon::logger::log("[J+] + Calculate Reward (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_calculateRewardAverageTime),
776 100.0 * ((double)(_calculateRewardAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_calculateRewardAverageCumulativeTime),
777 100.0 * ((double)_calculateRewardAverageCumulativeTime) / (double)(_totalRunningTime));
778
779 jaffarCommon::logger::log("[J+] + Popping Base State (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_popBaseStateDbAverageTime),
780 100.0 * ((double)(_popBaseStateDbAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_popBaseStateDbAverageCumulativeTime),
781 100.0 * ((double)_popBaseStateDbAverageCumulativeTime) / (double)(_totalRunningTime));
782
783 jaffarCommon::logger::log("[J+] + Get Allowed Inputs (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_getAllowedInputsAverageTime),
784 100.0 * ((double)(_getAllowedInputsAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_getAllowedInputsAverageCumulativeTime),
785 100.0 * ((double)_getAllowedInputsAverageCumulativeTime) / (double)(_totalRunningTime));
786
787 jaffarCommon::logger::log("[J+] + Get Candidate Inputs (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_getCandidateInputsAverageTime),
788 100.0 * ((double)(_getCandidateInputsAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_getCandidateInputsAverageCumulativeTime),
789 100.0 * ((double)_getCandidateInputsAverageCumulativeTime) / (double)(_totalRunningTime));
790#else
791 // Detailed per-operation profiling is compiled out (default). Only the coarse step wall time and
792 // the serially-measured DB-advance stages below are available; build -DdetailedProfiling=true for
793 // the full per-operation breakdown.
794 jaffarCommon::logger::log("[J+] Elapsed Time (Step/Total): %9.3fs / %9.3fs (per-operation breakdown disabled; build -DdetailedProfiling=true)\n",
795 1.0e-6 * (double)(_currentStepTime), 1.0e-6 * (double)(_totalRunningTime));
796#endif
797
798 jaffarCommon::logger::log("[J+] + Advance Hash Db (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_advanceHashDbAverageTime),
799 100.0 * ((double)(_advanceHashDbAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_advanceHashDbAverageCumulativeTime),
800 100.0 * ((double)_advanceHashDbAverageCumulativeTime) / (double)(_totalRunningTime));
801
802 jaffarCommon::logger::log("[J+] + Advance State Db (Step/Total): %9.3fs (%7.3f%%) / %9.3fs (%3.3f%%)\n", 1.0e-6 * (double)(_advanceStateDbAverageTime),
803 100.0 * ((double)(_advanceStateDbAverageTime) / (double)(_currentStepTime)), 1.0e-6 * (double)(_advanceStateDbAverageCumulativeTime),
804 100.0 * ((double)_advanceStateDbAverageCumulativeTime) / (double)(_totalRunningTime));
805
806 jaffarCommon::logger::log("[J+] Checkpoint (Level/Tolerance/Cutoff): %lu / %lu / %lu\n", _checkpointLevel.load(), _checkpointTolerance.load(),
807 _checkpointCutoff.load());
808 jaffarCommon::logger::log("[J+] Base States Processed: %.3f Mstates (Total: %.3f Mstates)\n", 1.0e-6 * (double)_stepBaseStatesProcessed,
809 1.0e-6 * (double)_totalBaseStatesProcessed);
810 jaffarCommon::logger::log("[J+] New States Processed: %.3f Mstates (Total: %.3f Mstates)\n", 1.0e-6 * (double)_stepNewStatesProcessed,
811 1.0e-6 * (double)_totalNewStatesProcessed);
812 if (_refPinEnabled)
813 jaffarCommon::logger::log("[J+] Reference Pin Hits (cumulative): %lu (deepest %lu / %lu; ref-depth matches seen %lu)\n", _refPinHits.load(),
815
816 jaffarCommon::logger::log("[J+] Base States Performance: %.3f Mstates/s (Average: %.3f Mstates/s)\n",
817 1.0e-6 * (double)_stepBaseStatesProcessed / (1.0e-6 * (double)_currentStepTime),
818 1.0e-6 * (double)_totalBaseStatesProcessed / (1.0e-6 * (double)_totalRunningTime));
819 jaffarCommon::logger::log("[J+] New States Performance: %.3f Mstates/s (Average: %.3f Mstates/s)\n",
820 1.0e-6 * (double)_stepNewStatesProcessed / (1.0e-6 * (double)_currentStepTime),
821 1.0e-6 * (double)_totalNewStatesProcessed / (1.0e-6 * (double)_totalRunningTime));
822
823 jaffarCommon::logger::log("[J+] Dropped States (No Storage Available): %lu (%5.3f%% of New States Processed) \n", _droppedStatesNoStorage.load(),
824 100.0 * (double)_droppedStatesNoStorage.load() / (double)_totalNewStatesProcessed);
825 jaffarCommon::logger::log("[J+] Dropped States (Failed Serialization): %lu (%5.3f%% of New States Processed) \n", _droppedStatesFailedSerialization.load(),
826 100.0 * (double)_droppedStatesFailedSerialization.load() / (double)_totalNewStatesProcessed);
827 jaffarCommon::logger::log("[J+] Dropped States (Checkpoint): %lu (%5.3f%% of New States Processed) \n", _droppedStatesCheckpoint.load(),
828 100.0 * (double)_droppedStatesCheckpoint.load() / (double)_totalNewStatesProcessed);
830 jaffarCommon::logger::log("[J+] Dropped States (Below Reference): %lu (%5.3f%% of New States Processed, prune tol %.1f) \n", _droppedStatesBelowReference.load(),
832 jaffarCommon::logger::log("[J+] Failed States: %lu (%5.3f%% of New States Processed) \n", _failedStates.load(),
833 100.0 * (double)_failedStates.load() / (double)_totalNewStatesProcessed);
834 jaffarCommon::logger::log("[J+] Repeated States: %lu (%5.3f%% of New States Processed) \n", _repeatedStates.load(),
835 100.0 * (double)_repeatedStates.load() / (double)_totalNewStatesProcessed);
836 jaffarCommon::logger::log("[J+] Normal States: %lu (%5.3f%% of New States Processed) \n", _normalStates.load(),
837 100.0 * (double)_normalStates.load() / (double)_totalNewStatesProcessed);
838 jaffarCommon::logger::log("[J+] Win States: %lu (%5.3f%% of New States Processed) \n", _winStates.load(),
839 100.0 * (double)_winStates.load() / (double)_totalNewStatesProcessed);
840
842 jaffarCommon::logger::log("[J+] Win States Collected: %lu/%lu\n", (unsigned long)getWinCollectedCount(), (unsigned long)_winCollectMax);
843 // Print state database information
844 jaffarCommon::logger::log("[J+] State Database Information:\n");
845 _stateDb->printInfo();
846
847 if (_hashDbEnabled == true)
848 {
849 jaffarCommon::logger::log("[J+] Hash Database Information:\n");
850 _hashDb->printInfo();
851 }
852
853 jaffarCommon::logger::log("[J+] Manually Saved Solution:\n");
854 jaffarCommon::logger::log("[J+] + Path: '%s'\n", _manualSaveSolution.path.c_str());
855 jaffarCommon::logger::log("[J+] + Reward: %f\n", _manualSaveSolution.reward);
856 jaffarCommon::logger::log("[J+] + Last Rule Idx: %ld (Active: %ld, Path: '%s')\n", _manualSaveSolution.lastRuleIdx,
858
859 // Printing candidate inpts
860 jaffarCommon::logger::log("[J+] Candidate Inputs:\n");
861 for (const auto& entry : _candidateInputsDetected)
862 {
863 jaffarCommon::logger::log("[J+] + Hash: %s\n", jaffarCommon::hash::hashToString(entry.first).c_str());
864 for (const auto input : entry.second) jaffarCommon::logger::log("[J+] + %3lu %s\n", input, _runners[0]->getInputStringFromIndex(input).c_str());
865 }
866 }
867
869 __INLINE__ size_t getStateSizeInDatabase() const { return _stateSizeInDatabase; }
870
872 __INLINE__ size_t getFullStateSize() const { return _fullStateSize; }
873
874private:
876 bool _winCollectEnabled = false;
877
879 std::vector<std::string> _winDedupPropNames;
880
882 std::string _winCollectPrefix;
883
885 size_t _winCollectMax = 1000;
886
888 std::set<std::string> _winKeysSeen;
889
891 std::mutex _winCollectLock;
892
894 bool _refPruneRequested = false;
895
897 bool _refPruneEnabled = false;
898
900 std::vector<float> _refPruneTrace;
901
903 float _refPruneTolerance = 0.0f;
905
907 // Number of base states a worker pulls from the shared per-NUMA state-DB queue per lock
908 // acquisition (into a thread-local buffer), instead of locking once per state. On a light
909 // emulator with dozens of worker threads per NUMA domain, that single-state lock/unlock was the
910 // dominant cost ("Popping Base State" ~37% of wall time); batching collapses it to <0.5%.
911 //
912 // The value is empirically tuned (EPYC 9755, 256 threads, SDLPoP lvl01): throughput is flat-
913 // optimal across 4-16 and falls off above it (32 ~ -4%, 64 ~ -10%, 512 ~ -9%). The reason is that
914 // once the lock contention is gone, the dominant effect is intra-step load balancing across 256
915 // threads -- a larger batch lets one straggler hold many states while others see the queue drain
916 // and go idle. 16 sits at the top of the flat region while keeping enough amortization headroom to
917 // stay robust for workloads with cheaper per-state work or higher thread counts. (A page-sized
918 // batch of 512 pointers was tried, on the theory the buffer would be cache/TLB friendly, but the
919 // buffer is tiny and L1-resident at any of these sizes, so the load-imbalance cost dominated.)
920 // The batch size is configurable ("Base State Batch Size"); when unset it comes from the
921 // emulator's getSuggestedStateBatchSize() (heavy cores 1 for load balance, light cores ~16 to
922 // amortize the queue lock). BASE_STATE_BATCH_MAX bounds the thread-local pull buffer; the active
923 // count is _baseStateBatch (clamped to [1, MAX]).
924 static constexpr size_t BASE_STATE_BATCH_MAX = 16;
925
938
941 {
942 float reward;
943 void* stateData = nullptr;
944 size_t stepCount = 0;
945 };
946
949 {
950 std::string path;
951 float reward;
952 void* stateData = nullptr;
953 ssize_t lastRuleIdx;
954 size_t stepCount = 0;
955 };
956
998
1008 {
1009 // Getting my thread id
1010 const auto threadId = jaffarCommon::parallel::getThreadId();
1011
1012 // Getting my thread-local accumulator (no atomics in the hot path)
1013 auto& acc = _threadAccumulators[threadId];
1014
1015 // Starting to measure thread-specific step time
1016 const auto threadTime0 = jaffarCommon::timing::now();
1017
1018 // Getting my runner
1019 auto& r = _runners[threadId];
1020
1021 // Base states are pulled from the database in batches into this thread-local buffer, so the
1022 // shared per-NUMA queue lock is acquired once per BASE_STATE_BATCH states instead of once per
1023 // state. With many worker threads per NUMA domain and cheap per-state work, the single-state
1024 // lock/unlock dominates wall time ("Popping Base State"); batching amortizes it away.
1025 void* baseStateBatch[BASE_STATE_BATCH_MAX];
1027 size_t batchCount = _stateDb->popStates(baseStateBatch, _baseStateBatch, threadId);
1028 JAFFAR_PROF_ACC(acc.popBaseStateDb, t);
1029 size_t batchIdx = 0;
1030
1031 // While there are still states in the database, keep on grabbing them
1032 while (batchIdx < batchCount)
1033 {
1034 // Taking the next base state from the local batch
1035 void* baseStateData = baseStateBatch[batchIdx++];
1036
1037 // Increasing base state counter
1038 acc.baseStatesProcessed++;
1039
1040 // Load state into runner via the state database (base states are slab slots: hot slot + cold path)
1041 JAFFAR_PROF_DECL(t0);
1042 _stateDb->loadStateFromSlot(*r, baseStateData);
1043 r->setSearchStep(_currentStep); // base state's depth = current search step (count is not stored per-state)
1044 JAFFAR_PROF_ACC(acc.runnerStateLoad, t0);
1045
1046 // Getting allowed inputs
1047 JAFFAR_PROF_DECL(t1);
1048 const auto allowedInputs = r->getAllowedInputs();
1049 JAFFAR_PROF_ACC(acc.getAllowedInputs, t1);
1050
1051 // Getting candidate inputs (those not already covered by the allowed set). Computed here,
1052 // before the allowed inputs are tried, while the runner still holds the unperturbed base state.
1053 JAFFAR_PROF_DECL(t2);
1054 auto candidateInputs = r->getCandidateInputs();
1055 JAFFAR_PROF_ACC(acc.getCandidateInputs, t2);
1056
1057 // Finding unique candidate inputs
1058 std::vector<InputSet::inputIndex_t> uniqueCandidateInputs;
1059 for (const auto& input : candidateInputs)
1060 if (std::find(allowedInputs.begin(), allowedInputs.end(), input) == allowedInputs.end()) uniqueCandidateInputs.push_back(input);
1061
1062 // Discriminating hash of the *base* state (the situation being expanded), used to dedup
1063 // candidate-input probing across like states. It must be taken from the base state, so it is
1064 // captured now -- before the allowed/candidate inputs below advance the runner away from it.
1065 jaffarCommon::hash::hash_t baseStateInputHash{};
1066 if (uniqueCandidateInputs.empty() == false) baseStateInputHash = r->getGame()->getStateInputHash();
1067
1068 // Trying out each possible input in the set
1069 for (auto inputItr = allowedInputs.begin(); inputItr != allowedInputs.end(); inputItr++) runNewInput(*r, baseStateData, *inputItr, acc, threadId);
1070
1071 // Run each candidate input, keyed by the base state's discriminating hash
1072 for (const auto input : uniqueCandidateInputs)
1073 {
1074 // Making sure we don't try the input if it was already detected for this type of state
1075 if (_candidateInputsDetected.contains(baseStateInputHash))
1076 if (_candidateInputsDetected[baseStateInputHash].contains(input)) continue;
1077
1078 // Running input
1079 const auto result = runNewInput(*r, baseStateData, input, acc, threadId);
1080
1081 // If this is not a repeated state, store it as new candidate input
1082 if (result != inputResult_t::repeated) _candidateInputsDetected[baseStateInputHash].insert(input);
1083 }
1084
1085 // Return base state to the free state queue
1086 JAFFAR_PROF_DECL(t8);
1087 _stateDb->returnFreeState(baseStateData, threadId);
1088 JAFFAR_PROF_ACC(acc.returnFreeState, t8);
1089
1090 // When the local batch is exhausted, pull the next batch from the database
1091 if (batchIdx >= batchCount)
1092 {
1093 JAFFAR_PROF_DECL(t9);
1094 batchCount = _stateDb->popStates(baseStateBatch, _baseStateBatch, threadId);
1095 JAFFAR_PROF_ACC(acc.popBaseStateDb, t9);
1096 batchIdx = 0;
1097 }
1098 }
1099
1100 // Taking final thread-specific time measurement
1101 _threadStepTime[threadId] = jaffarCommon::timing::timeDeltaMicroseconds(jaffarCommon::timing::now(), threadTime0);
1102 }
1103
1113 __INLINE__ inputResult_t runNewInput(Runner& r, const void* baseStateData, const InputSet::inputIndex_t input, threadAccumulator_t& acc, const size_t threadId)
1114 {
1115 // Increasing new state counter
1116 acc.newStatesProcessed++;
1117
1118 // Re-loading base state (slab slot: hot slot + cold path)
1119 JAFFAR_PROF_DECL(t0);
1120 _stateDb->loadStateFromSlot(r, baseStateData);
1121 r.setSearchStep(_currentStep); // base state's depth = current search step (count is not stored per-state)
1123
1124 // Running input
1125 const auto result = runInput(r, baseStateData, input, acc, threadId);
1126
1127 // Update counters depending on the outcomes
1128 if (result == inputResult_t::normal) acc.normalStates++;
1129 if (result == inputResult_t::repeated) acc.repeatedStates++;
1130 if (result == inputResult_t::failed) acc.failedStates++;
1131 if (result == inputResult_t::win) acc.winStates++;
1136
1137 // Checking whether this state's checkpoint is new. Only states that were actually stored
1138 // (normal/win) may raise the global level: a failed or repeated state that satisfies a
1139 // checkpoint rule leaves no surviving lineage at that level, and raising the level from it
1140 // purges the entire database (guaranteed starvation at tolerance 0).
1141 if (result == inputResult_t::normal || result == inputResult_t::win)
1142 {
1143 const auto stateCheckpointLevel = r.getGame()->getCheckpointLevel();
1144 const auto stateCheckpointTolerance = r.getGame()->getCheckpointTolerance();
1145 if (stateCheckpointLevel >= _checkpointLevel.load(std::memory_order_relaxed)) _stepMaxLevelStored.fetch_add(1, std::memory_order_relaxed);
1146 if (stateCheckpointLevel > _checkpointLevel.load(std::memory_order_relaxed))
1147 {
1148 // Publication order is load-bearing: the new cutoff must become visible BEFORE the new
1149 // level, or a concurrent thread can observe (new level, stale cutoff) and purge states
1150 // that are inside the new tolerance window -- wiping the database in a single step.
1151 std::lock_guard<std::mutex> lk(_checkpointMutex);
1152 if (stateCheckpointLevel > _checkpointLevel.load(std::memory_order_relaxed))
1153 {
1154 _checkpointTolerance.store(stateCheckpointTolerance, std::memory_order_relaxed);
1155 _checkpointCutoff.store(_currentStep + stateCheckpointTolerance, std::memory_order_relaxed);
1156 _checkpointLevel.store(stateCheckpointLevel, std::memory_order_release);
1157 }
1158 }
1159 }
1160
1161 // Returning result
1162 return result;
1163 }
1164
1180 __INLINE__ inputResult_t runInput(Runner& r, const void* baseStateData, const InputSet::inputIndex_t input, threadAccumulator_t& acc, const size_t threadId)
1181 {
1182 // Now advancing state with the provided input
1183 JAFFAR_PROF_DECL(t1);
1184 r.advanceState(input);
1186
1187 // Computing runner hash. With Hash Lookahead, hash the state as it looks after N null
1188 // advances -- WITHOUT saving first: repeats are simply discarded in the advanced posture
1189 // (the caller reloads the base state per candidate anyway), and survivors are rebuilt
1190 // pristine below by re-loading the base and re-advancing the input. Dups thus pay only
1191 // the lookahead advances; nobody pays serialization.
1192 JAFFAR_PROF_DECL(t2);
1193 if (_hashLookahead > 0)
1194 {
1195 const auto nullIdx = r.getGame()->getNullInputIndex();
1196 for (size_t i = 0; i < _hashLookahead; i++) r.advanceState(nullIdx);
1197 }
1198 const auto hash = r.computeHash();
1200
1201 // Checking if hash is repeated (i.e., has been seen before)
1202 JAFFAR_PROF_DECL(t3);
1203 bool hashExists = _hashDbEnabled ? _hashDb->checkHashExists(hash) : false;
1204 JAFFAR_PROF_ACC(acc.checkHash, t3);
1205
1206 // Reference pinning (decided BEFORE the dedup drop): if this child matches the reference lineage at
1207 // its own depth (k=0) or a near-future depth (k=1..Lookahead, i.e. it is 1..L frames ahead), claim
1208 // that reference depth EXACTLY ONCE (compare_exchange) and pin this copy. Claiming before dedup is
1209 // essential: the same reference RAM is reached by many convergent paths (and can be first reached --
1210 // and added UNPINNED -- at a shallower depth by a path more than Lookahead frames ahead), so if we
1211 // waited until after the dedup check the reference lineage would be deduped away before it could be
1212 // pinned. The single claimed copy bypasses dedup so it is always anchored; all later duplicates are
1213 // dropped normally. Only the DB-ordering reward is affected (the floor uses the unbiased reward).
1214 float pinBonus = 0.0f;
1215 bool isRefPin = false;
1216 if (_refPinEnabled)
1217 {
1218 const size_t newDepth = _currentStep + 1;
1219 for (size_t k = 0; k <= _refPinLookahead; k++)
1220 {
1221 const size_t idx = newDepth + k;
1222 if (idx < _refPinHashes.size() && hash == _refPinHashes[idx])
1223 {
1224 _refPinSeenPreDedup.fetch_add(1, std::memory_order_relaxed);
1225 // Hash equality is only a PRE-FILTER: under a quantized dedup hash a whole cell of states
1226 // shares the reference's hash. When exact reference states are available (floor Solution File
1227 // replay), verify by byte comparison so only the true reference lineage is pinned.
1228 bool verified = true;
1229 if (_refPinExactVerify && idx < _refStates.size() && _refStates[idx].size() > 0)
1230 {
1231 thread_local std::vector<uint8_t> scratch;
1232 scratch.resize(_refStates[idx].size());
1233 jaffarCommon::serializer::Contiguous s(scratch.data(), scratch.size());
1234 r.serializeState(s);
1235 verified = true;
1236 for (size_t i = 0; i < scratch.size(); i++)
1237 if (_refVolatileMask[i] == 0 && scratch[i] != _refStates[idx][i])
1238 {
1239 verified = false;
1240 break;
1241 }
1242 }
1243 if (verified)
1244 {
1245 // The dedup bypass (and bonus) applies ONCE per reference depth -- the CAS is the gate,
1246 // not just a hit counter. Without this, any mode that collapses the frontier onto the
1247 // reference path (e.g. reference-prune polishing) has EVERY lineage re-create the
1248 // reference states each step, and the pinned copies -- exempt from dedup -- flood the
1249 // state DB with duplicates of the same ~L reference states (measured: >80% of a 10 GB
1250 // DB were copies). Later re-creations are ordinary states: the hash exists, dedup
1251 // drops them, and the one anchored copy already guarantees the reference survives.
1252 uint8_t expected = 0;
1253 if (_refPinnedAtDepth[idx].compare_exchange_strong(expected, 1, std::memory_order_relaxed))
1254 {
1255 pinBonus = _refPinBonus + (float)k * _refPinLookaheadBonus;
1256 isRefPin = true;
1257 _refPinHits.fetch_add(1, std::memory_order_relaxed);
1258 _refPinMaxDepthHit.store(std::max(_refPinMaxDepthHit.load(std::memory_order_relaxed), idx), std::memory_order_relaxed);
1259 }
1260 }
1261 break;
1262 }
1263 }
1264 }
1265
1266 // If state is repeated then we are not interested in it -- UNLESS it is the reference copy we just
1267 // claimed, which we keep and anchor regardless (bypassing dedup for the pinned lineage).
1268 // (With Hash Lookahead, repeated states are discarded while still in the advanced scratch
1269 // posture -- no restore needed, per the discard-without-restore optimization.)
1270 if (hashExists == true && isRefPin == false) return inputResult_t::repeated;
1271
1272 // Survivor: rebuild the pristine child (base + input) before rules/serialization see it.
1273 if (_hashLookahead > 0)
1274 {
1275 _stateDb->loadStateFromSlot(r, baseStateData);
1277 r.advanceState(input);
1278 }
1279
1280 // Evaluating game rules based on the new state
1281 JAFFAR_PROF_DECL(t4);
1282 r.getGame()->evaluateRules();
1283
1284 // Checking whether this state meets checkpoint
1285 {
1286 // Read the level with acquire FIRST: seeing a new level guarantees the matching cutoff
1287 // (written before it under release ordering) is also visible.
1288 const auto globalCheckpointLevel = _checkpointLevel.load(std::memory_order_acquire);
1289 const auto stateCheckpointLevel = r.getGame()->getCheckpointLevel();
1290
1291 // If state does not meet checkpoint past the cutoff, then do not process it further
1292 if (stateCheckpointLevel < globalCheckpointLevel && _currentStep > _checkpointCutoff.load(std::memory_order_relaxed)) return inputResult_t::droppedCheckpoint;
1293 }
1294
1295 // Determining state type
1297
1298 // Getting state type
1299 const auto stateType = r.getGame()->getStateType();
1301
1302 // Now we have determined the state is not repeated, check if it's not a failed state
1303 if (stateType == Game::stateType_t::fail) return inputResult_t::failed;
1304
1305 // Now that the state is not failed nor repeated, this is effectively a new state to add
1306 JAFFAR_PROF_DECL(t5);
1307 void* newStateData = _stateDb->getFreeState(threadId);
1309
1310 // If couldn't get any memory, simply drop the state
1311 if (newStateData == nullptr) return inputResult_t::droppedNoStorage;
1312
1313 // Updating state reward
1314 JAFFAR_PROF_DECL(t6);
1315 r.getGame()->updateReward();
1316
1317 // Getting state reward, plus the reference-pin bonus decided before the dedup check above.
1318 auto reward = r.getGame()->getReward() + pinBonus;
1320
1321 // Reference-reward pruning (greedy polish mode): drop any non-win child whose floor reward is
1322 // below the reference trace at its depth beyond the tolerance. Children produced during this
1323 // step sit at depth _currentStep + 1 (the step counter advances at the end of runStep). Win
1324 // states are exempt (a win below the trace still ends the search) and so is the pinned
1325 // reference lineage, which must survive at tolerance 0 despite float equality being its floor.
1326 if (_refPruneEnabled && stateType != Game::stateType_t::win && isRefPin == false)
1327 {
1328 const size_t childDepth = _currentStep + 1;
1329 const size_t graceDepth = childDepth >= _refPruneStepGrace ? childDepth - _refPruneStepGrace : 0;
1330 if (graceDepth < _refPruneTrace.size() && r.getGame()->getFloorReward() < _refPruneTrace[graceDepth] - _refPruneTolerance)
1331 {
1332 _stateDb->returnFreeState(newStateData, threadId);
1334 }
1335 }
1336
1337 // If this is a win state, register it and return
1338 if (stateType == Game::stateType_t::win)
1339 {
1341
1342 // Check if the new win state is the best and store it in that case
1343 // Win-state collection: the runner holds the live win state right now -- read the dedup
1344 // key straight from its game properties and write its input history if the key is new.
1346 {
1347 std::string key;
1348 char hx[4];
1349 for (const auto& pn : _winDedupPropNames)
1350 {
1351 auto* prop = r.getGame()->findProperty(pn);
1352 if (prop == nullptr) JAFFAR_THROW_LOGIC("[ERROR] Win State Collection dedup property '%s' not registered\n", pn.c_str());
1353 const auto* bytes = (const uint8_t*)prop->getPointer();
1354 for (size_t bi = 0; bi < prop->getSize(); bi++)
1355 {
1356 snprintf(hx, sizeof(hx), "%02x", bytes[bi]);
1357 key += hx;
1358 }
1359 }
1360 _winCollectLock.lock();
1361 if (_winKeysSeen.contains(key) == false && _winKeysSeen.size() < _winCollectMax)
1362 {
1363 _winKeysSeen.insert(key);
1364 const auto solution = r.getInputHistoryString();
1365 jaffarCommon::file::saveStringToFile(solution, _winCollectPrefix + key + ".sol");
1366 // No per-capture log line (screen churn): progress is reported as a collected/max
1367 // counter in the regular per-step block; the manifest records key/step per capture.
1368 {
1369 std::ofstream mf(_winCollectPrefix + "manifest.txt", std::ios::app);
1370 mf << "key " << key << " step " << r.getStepCount() << "\n";
1371 }
1372 }
1373 _winCollectLock.unlock();
1374 }
1375 _stepBestWinStateLock.lock();
1376 if (reward > _stepBestWinState.reward)
1377 {
1378 _stateDb->saveStateFromRunner(r, _stepBestWinState.stateData);
1379 _stepBestWinState.reward = reward;
1380 _stepBestWinState.stepCount = r.getStepCount(); // record depth: the count is not serialized per-state
1381 }
1382 // Persist the winning input history DIRECTLY, now, when the runner still holds the exact path that
1383 // reached this win. The driver's post-search "best state" render loads a saved state and rebuilds
1384 // its history, which is unreliable for terminal win states (a win is never pushed to the DB, so the
1385 // best-state saver tracks the non-win frontier instead). This writes only on a strictly-better win
1386 // (rare, under the lock), so the file always holds the highest-reward winning solution found.
1387 if (reward > _bestWinSolutionReward)
1388 {
1389 _bestWinSolutionReward = reward;
1390 jaffarCommon::file::saveStringToFile(r.getInputHistoryString(), _winSolutionPath);
1391 }
1392 _stepBestWinStateLock.unlock();
1393
1394 // Freeing up the state data
1395 JAFFAR_PROF_DECL(t7);
1396 _stateDb->returnFreeState(newStateData, threadId);
1398
1399 // Returning a win result
1400 return inputResult_t::win;
1401 }
1402
1403 // If this is a normal state and has possible inputs store it in the next state database
1404 if (stateType == Game::stateType_t::normal)
1405 {
1406 // Debug probe (JAFFAR_DEBUG_SAMPLE_RATE=N): log every Nth stored state's identity (depth,
1407 // floor reward, key game properties) and hard-check the reference-prune invariant on it.
1408 // Zero overhead unless the env var is set (checked once).
1409 {
1410 static const long sampleRate = []
1411 {
1412 const char* e = std::getenv("JAFFAR_DEBUG_SAMPLE_RATE");
1413 return e ? std::atol(e) : 0;
1414 }();
1415 if (sampleRate > 0)
1416 {
1417 static std::atomic<uint64_t> pushCounter{0};
1418 const auto n = pushCounter.fetch_add(1, std::memory_order_relaxed);
1419 const float fr = r.getGame()->getFloorReward();
1420 const size_t cd = _currentStep + 1;
1421 if (_refPruneEnabled && cd < _refPruneTrace.size() && fr < _refPruneTrace[cd] - _refPruneTolerance)
1422 jaffarCommon::logger::log("[J+] SAMPLE-VIOLATION depth %lu floorReward %.6f < trace %.6f - tol %.1f\n", cd, fr, _refPruneTrace[cd], _refPruneTolerance);
1423 if (n % (uint64_t)sampleRate == 0)
1424 {
1425 char props[512];
1426 int pp = 0;
1427 const char* names[] = {"Player Pos X", "Player Pos Y", "RNG State 1", "RNG State 2", "RNG State 3", "RNG State 4"};
1428 for (const auto* pn : names)
1429 {
1430 auto* prop = r.getGame()->findProperty(pn);
1431 if (prop == nullptr) continue;
1432 const auto* b = (const uint8_t*)prop->getPointer();
1433 for (size_t bi = 0; bi < prop->getSize() && pp < 480; bi++) pp += snprintf(props + pp, sizeof(props) - pp, "%02x", b[bi]);
1434 pp += snprintf(props + pp, sizeof(props) - pp, " ");
1435 }
1436 jaffarCommon::logger::log("[J+] SAMPLE depth %lu reward %.6f floor %.6f hash %016lX%016lX props %s\n", cd, reward, fr, hash.first, hash.second, props);
1437 }
1438 }
1439 }
1440 // If this is a normal state, push into the state database
1441 JAFFAR_PROF_DECL(t8);
1442 auto success = _stateDb->pushState(reward, r, newStateData);
1444
1445 // If pushing the state failed (e.g. serialization error), drop it and continue, keeping a counter
1446 if (success == false)
1447 {
1448 // Freeing up state memory
1449 JAFFAR_PROF_DECL(t9);
1450 _stateDb->returnFreeState(newStateData, threadId);
1452
1453 // Returning dropped result by failed serialization
1455 }
1456 }
1457
1458 // Checking for manual saved solution is required
1459 const auto currentLastRuleIdx = r.getGame()->getSaveSolutionCurrentLastRuleIdx();
1460 if (r.getGame()->isSaveSolution() && currentLastRuleIdx > _manualSaveSolutionActiveLastRuleId)
1461 {
1462 // Grab lock
1464
1465 // Do this only if reward is better
1466 if (reward > _manualSaveSolution.reward)
1467 {
1468 // Store path, data, and reward
1469 _stateDb->saveStateFromRunner(r, _manualSaveSolution.stateData);
1471 _manualSaveSolution.reward = reward;
1472 _manualSaveSolution.lastRuleIdx = currentLastRuleIdx;
1473 _manualSaveSolution.stepCount = r.getStepCount(); // record depth (the count is not serialized per-state)
1475 }
1476
1477 // Release lock
1478 _manualSaveSolutionLock.unlock();
1479 }
1480
1481 // If store succeeded, return a normal execution
1482 return inputResult_t::normal;
1483 }
1484
1486 jaffarCommon::concurrent::HashMap_t<jaffarCommon::hash::hash_t, jaffarCommon::concurrent::HashSet_t<InputSet::inputIndex_t>> _candidateInputsDetected;
1487
1489 size_t _currentStep = 0;
1490
1492 std::vector<std::unique_ptr<Runner>> _runners;
1493
1495 std::unique_ptr<jaffarPlus::StateDb> _stateDb;
1496
1499 std::shared_ptr<void> _inputHistoryBacking;
1500
1504
1505 // Reference pinning + lookahead (see constructor). Anchors a reference lineage in the frontier and
1506 // rewards being 1..Lookahead frames ahead of it.
1507 size_t _hashLookahead = 0;
1508 bool _refPinEnabled = false;
1510 float _refPinBonus = 0.0f;
1511 size_t _refPinLookahead = 0;
1513 std::vector<jaffarCommon::hash::hash_t> _refPinHashes;
1514 std::atomic<size_t> _refPinHits{0};
1515 bool _compactLog = false;
1516 std::atomic<size_t> _refPinMaxDepthHit{0};
1517 std::vector<std::vector<uint8_t>> _refStates;
1518 std::atomic<size_t> _refPinSeenPreDedup{0};
1519 std::unique_ptr<std::atomic<uint8_t>[]> _refPinnedAtDepth;
1520
1521public:
1529 __INLINE__ jaffarCommon::hash::hash_t computeStateHash(Runner& r)
1530 {
1531 if (_hashLookahead == 0) return r.computeHash();
1532 thread_local std::vector<uint8_t> scratch;
1533 if (scratch.size() != r.getStateSize()) scratch.resize(r.getStateSize());
1534 {
1535 jaffarCommon::serializer::Contiguous ser(scratch.data(), scratch.size());
1536 r.serializeState(ser);
1537 }
1538 const auto nullIdx = r.getGame()->getNullInputIndex();
1539 for (size_t i = 0; i < _hashLookahead; i++) r.advanceState(nullIdx);
1540 const auto h = r.computeHash();
1541 {
1542 jaffarCommon::deserializer::Contiguous des(scratch.data(), scratch.size());
1543 r.deserializeState(des);
1544 }
1545 return h;
1546 }
1547
1550 const std::vector<std::vector<uint8_t>>& getRefStates() const { return _refStates; }
1551
1553 const std::vector<uint8_t>& getRefVolatileMask() const { return _refVolatileMask; }
1554
1559 void setReferenceStates(std::vector<std::vector<uint8_t>>&& states, const std::vector<std::string>& refInputs)
1560 {
1561 _refStates = std::move(states);
1562 if (_refStates.size() < 2 || refInputs.size() == 0) return;
1563 std::vector<uint8_t> scratch(_refStates[0].size());
1564 _refVolatileMask.assign(_refStates[0].size(), 0);
1565 // Probe depths spread across the whole span: the volatile region (audio ring residue) slides over
1566 // time, so distant depths expose different parts of it. Each hit is padded so the union covers the
1567 // sliding band.
1568 const size_t maxDepth = std::min(_refStates.size() - 1, refInputs.size());
1569 const size_t probeCount = std::min<size_t>(32, maxDepth);
1570 // Every worker runner instance carries its own residue pattern -- probe across several instances
1571 // so the mask covers the union.
1572 const size_t runnersToProbe = std::min<size_t>(16, _runners.size());
1573 for (size_t ri = 0; ri < runnersToProbe; ri++)
1574 for (size_t pi = 0; pi < probeCount; pi++)
1575 {
1576 auto& r = *_runners[ri];
1577 const size_t k = (pi * maxDepth) / probeCount;
1578 if (refInputs[k].empty()) continue;
1579 {
1580 jaffarCommon::deserializer::Contiguous d(_refStates[k].data(), _refStates[k].size());
1581 r.deserializeState(d);
1582 }
1583 r.advanceState(r.getGame()->getEmulator()->registerInput(refInputs[k]));
1584 {
1585 jaffarCommon::serializer::Contiguous s(scratch.data(), scratch.size());
1586 r.serializeState(s);
1587 }
1588 for (size_t i = 0; i < scratch.size(); i++)
1589 if (scratch[i] != _refStates[k + 1][i])
1590 {
1591 const size_t lo = i >= 128 ? i - 128 : 0;
1592 const size_t hi = std::min(i + 128, scratch.size() - 1);
1593 for (size_t j = lo; j <= hi; j++) _refVolatileMask[j] = 1;
1594 }
1595 }
1596 size_t total = 0;
1597 for (auto m : _refVolatileMask) total += m;
1598 jaffarCommon::logger::log("[J+] Reference pin exact-verification: volatile mask covers %lu / %lu bytes\n", total, _refVolatileMask.size());
1599 }
1600
1601private:
1602 std::vector<uint8_t> _refVolatileMask;
1603
1605 std::unique_ptr<jaffarPlus::HashDb> _hashDb;
1606
1609
1612
1617 float _bestWinSolutionReward = -std::numeric_limits<float>::infinity();
1618 std::string _winSolutionPath = "/tmp/jaffar.winsolution.sol";
1619
1620 // Storage for manually triggered save solutionm
1621
1627
1628 // Checkpoint information
1629 std::atomic<size_t> _checkpointLevel;
1630 std::atomic<size_t> _checkpointTolerance;
1631 std::atomic<size_t> _checkpointCutoff;
1632 std::mutex _checkpointMutex;
1633 std::atomic<size_t> _stepMaxLevelStored;
1634
1636
1638 std::atomic<size_t> _droppedStatesNoStorage;
1639
1642
1644 std::atomic<size_t> _droppedStatesCheckpoint;
1645
1647 std::atomic<size_t> _droppedStatesBelowReference;
1648
1650 std::atomic<size_t> _repeatedStates;
1651
1653 std::atomic<size_t> _failedStates;
1654
1656 std::atomic<size_t> _winStates;
1657
1659 std::atomic<size_t> _normalStates;
1660
1661 std::atomic<size_t> _stepBaseStatesProcessed;
1662 std::atomic<size_t> _totalBaseStatesProcessed;
1663
1664 std::atomic<size_t> _stepNewStatesProcessed;
1665 std::atomic<size_t> _totalNewStatesProcessed;
1666
1668
1671
1672 std::vector<size_t> _threadStepTime;
1675
1677 std::vector<threadAccumulator_t> _threadAccumulators;
1678
1681
1682 // Time spent advancing runner state per step
1686
1687 // Time spent loading states into the runner
1688 std::atomic<size_t> _runnerStateLoadThreadRawTime;
1689 std::atomic<size_t> _runnerStateLoadAverageTime;
1691
1692 // Time spent saving runner states into the state db
1693 std::atomic<size_t> _runnerStateSaveThreadRawTime;
1694 std::atomic<size_t> _runnerStateSaveAverageTime;
1696
1697 // Time spent calculating hash
1698 std::atomic<size_t> _calculateHashThreadRawTime;
1699 std::atomic<size_t> _calculateHashAverageTime;
1701
1702 // Time spent checking hash
1703 std::atomic<size_t> _checkHashThreadRawTime;
1704 std::atomic<size_t> _checkHashAverageTime;
1706
1707 // Rule checking time
1708 std::atomic<size_t> _ruleCheckingThreadRawTime;
1709 std::atomic<size_t> _ruleCheckingAverageTime;
1711
1712 // Get free state time
1713 std::atomic<size_t> _getFreeStateThreadRawTime;
1714 std::atomic<size_t> _getFreeStateAverageTime;
1716
1717 // Return free state time
1718 std::atomic<size_t> _returnFreeStateThreadRawTime;
1719 std::atomic<size_t> _returnFreeStateAverageTime;
1721
1722 // Reward calculation time
1723 std::atomic<size_t> _calculateRewardThreadRawTime;
1724 std::atomic<size_t> _calculateRewardAverageTime;
1726
1727 // Get allowed inputs time
1729 std::atomic<size_t> _getAllowedInputsAverageTime;
1731
1732 // Get candidate inputs time
1736
1737 // Advance Hash DB time
1738 std::atomic<size_t> _advanceHashDbThreadRawTime;
1739 std::atomic<size_t> _advanceHashDbAverageTime;
1741
1742 // Advance State DB time
1743 std::atomic<size_t> _advanceStateDbThreadRawTime;
1744 std::atomic<size_t> _advanceStateDbAverageTime;
1746
1747 // Popping states from the State DB time
1748 std::atomic<size_t> _popBaseStateDbThreadRawTime;
1749 std::atomic<size_t> _popBaseStateDbAverageTime;
1751
1754};
1755
1756} // namespace jaffarPlus
Parallel state-space search engine.
Definition engine.hpp:61
std::atomic< size_t > _getFreeStateAverageTime
Per-thread-average get-free-state time for the step.
Definition engine.hpp:1714
std::atomic< size_t > _advanceHashDbAverageTime
Hash-DB advance time reported for the step.
Definition engine.hpp:1739
std::atomic< size_t > _getCandidateInputsAverageTime
Per-thread-average get-candidate-inputs time for the step.
Definition engine.hpp:1734
std::atomic< size_t > _checkHashThreadRawTime
Summed per-thread hash-checking time for the step.
Definition engine.hpp:1703
bool _refPruneRequested
Whether "Reference Reward Prune" was enabled in the engine configuration.
Definition engine.hpp:894
std::atomic< size_t > _advanceStateDbThreadRawTime
Serially-measured state-DB advance time for the step.
Definition engine.hpp:1743
size_t getStateSizeInDatabase() const
Returns the size, in bytes, of a single state as stored in the database.
Definition engine.hpp:869
bool isWinCollectionFull()
Whether win collection is enabled and has reached its Max Files cap.
Definition engine.hpp:643
std::atomic< size_t > _droppedStatesFailedSerialization
Counter for states dropped due to failed serialization.
Definition engine.hpp:1641
std::atomic< size_t > _popBaseStateDbThreadRawTime
Summed per-thread base-state pop time for the step.
Definition engine.hpp:1748
std::atomic< size_t > _stepMaxLevelStored
States stored this step whose level >= the global checkpoint level.
Definition engine.hpp:1633
void setReferenceStates(std::vector< std::vector< uint8_t > > &&states, const std::vector< std::string > &refInputs)
Installs the per-depth canonical reference states (from the driver's floor replay) and derives the vo...
Definition engine.hpp:1559
size_t getInputHistoryMaxMemoryBytes() const
Hard memory ceiling (bytes) of the shared input-history backing; 0 for None/Raw (no ceiling).
Definition engine.hpp:697
std::atomic< size_t > _advanceHashDbThreadRawTime
Serially-measured hash-DB advance time for the step.
Definition engine.hpp:1738
std::atomic< size_t > _getCandidateInputsAverageCumulativeTime
Cumulative per-thread-average get-candidate-inputs time.
Definition engine.hpp:1735
void setWinStateCollection(const std::vector< std::string > &dedupProps, const std::string &pathPrefix, const size_t maxFiles)
Enables win-state collection: every win state's solution is saved to pathPrefix + hex(dedup property ...
Definition engine.hpp:662
inputResult_t runInput(Runner &r, const void *baseStateData, const InputSet::inputIndex_t input, threadAccumulator_t &acc, const size_t threadId)
Advances the runner by one input and classifies/stores the resulting state.
Definition engine.hpp:1180
std::atomic< size_t > _checkHashAverageCumulativeTime
Cumulative per-thread-average hash-checking time.
Definition engine.hpp:1705
std::vector< float > _refPruneTrace
Per-depth floor-reward trace of the reference solution for pruning.
Definition engine.hpp:900
std::atomic< size_t > _totalBaseStatesProcessed
Base states processed across all steps so far.
Definition engine.hpp:1662
std::atomic< size_t > _returnFreeStateAverageTime
Per-thread-average return-free-state time for the step.
Definition engine.hpp:1719
static constexpr size_t BASE_STATE_BATCH_MAX
Number of base states a worker pulls from the state-DB queue per lock acquisition (batch size).
Definition engine.hpp:924
std::atomic< size_t > _ruleCheckingThreadRawTime
Summed per-thread rule-checking time for the step.
Definition engine.hpp:1708
size_t _hashLookahead
N null-advances before hashing (see "Hash Lookahead"); 0 = hash the current state.
Definition engine.hpp:1507
size_t getWinCollectedCount()
Number of distinct win solutions collected so far (see setWinStateCollection).
Definition engine.hpp:637
std::atomic< size_t > _refPinHits
Cumulative reference-pin hits (diagnostic).
Definition engine.hpp:1514
std::string _winCollectPrefix
Path prefix for saved win-collection .sol files and the manifest.
Definition engine.hpp:882
std::unique_ptr< jaffarPlus::HashDb > _hashDb
Thread-safe hash database used to detect repeated states.
Definition engine.hpp:1605
std::atomic< size_t > _getFreeStateThreadRawTime
Summed per-thread get-free-state time for the step.
Definition engine.hpp:1713
void initialize()
Resets execution back to step zero and clears all databases and counters.
Definition engine.hpp:222
std::atomic< size_t > _checkpointLevel
Highest checkpoint level reached so far.
Definition engine.hpp:1629
size_t _fullStateSize
Full self-contained serialized state size ([hot]+[history]) for standalone snapshot buffers.
Definition engine.hpp:1611
std::atomic< size_t > _advanceStateDbAverageCumulativeTime
Cumulative state-DB advance time.
Definition engine.hpp:1745
std::string _manualSaveSolutionLastPath
Path of the most recently activated manual-save solution.
Definition engine.hpp:1626
std::atomic< size_t > _runnerStateLoadAverageTime
Per-thread-average state-load time for the step.
Definition engine.hpp:1689
std::atomic< size_t > _stepBaseStatesProcessed
Base states processed during the current step.
Definition engine.hpp:1661
std::atomic< size_t > _calculateRewardAverageTime
Per-thread-average reward-calculation time for the step.
Definition engine.hpp:1724
std::atomic< size_t > _runnerStateSaveAverageTime
Per-thread-average state-save time for the step.
Definition engine.hpp:1694
size_t _maxThreadStepTime
Maximum per-thread step time for the current step.
Definition engine.hpp:1673
std::vector< std::unique_ptr< Runner > > _runners
Collection of runners for the workers to use (one per thread).
Definition engine.hpp:1492
manualSaveSolution_t _manualSaveSolution
Best manually saved solution for the current step.
Definition engine.hpp:1623
size_t _totalRunningTime
Total running time so far, in microseconds.
Definition engine.hpp:1680
std::atomic< size_t > _popBaseStateDbAverageTime
Per-thread-average base-state pop time for the step.
Definition engine.hpp:1749
std::atomic< size_t > _calculateHashAverageCumulativeTime
Cumulative per-thread-average hash-calculation time.
Definition engine.hpp:1700
std::atomic< size_t > _checkHashAverageTime
Per-thread-average hash-checking time for the step.
Definition engine.hpp:1704
size_t _subTotalAverageTime
Sum of all per-operation average times for the current step.
Definition engine.hpp:1752
std::atomic< size_t > _winStates
Counter for win states.
Definition engine.hpp:1656
stateInfo_t _stepBestWinState
Best win state (by reward) found during the current step.
Definition engine.hpp:1614
std::atomic< size_t > _runnerStateSaveThreadRawTime
Summed per-thread state-save time for the step.
Definition engine.hpp:1693
std::atomic< size_t > _calculateRewardAverageCumulativeTime
Cumulative per-thread-average reward-calculation time.
Definition engine.hpp:1725
size_t _currentStepTime
Overall running time of the current step, in microseconds.
Definition engine.hpp:1670
std::atomic< size_t > _runnerStateAdvanceAverageCumulativeTime
Cumulative per-thread-average runner-advance time.
Definition engine.hpp:1685
std::atomic< size_t > _droppedStatesBelowReference
Number of states dropped for falling below the reference reward trace (reference pruning).
Definition engine.hpp:1647
bool _refPinEnabled
Whether reference pinning is active.
Definition engine.hpp:1508
bool _hashDbEnabled
Whether hashing is enabled. Games that cannot loop skip the hash DB to save memory and computation.
Definition engine.hpp:1502
std::atomic< size_t > _droppedStatesCheckpoint
Counter for states dropped due to not meeting the checkpoint.
Definition engine.hpp:1644
std::vector< std::string > _winDedupPropNames
Names of the game properties whose bytes form the win-collection dedup key.
Definition engine.hpp:879
auto getStepBestWinState() const
Returns a copy of the best win state recorded in the current step.
Definition engine.hpp:634
std::atomic< size_t > _popBaseStateDbAverageCumulativeTime
Cumulative per-thread-average base-state pop time.
Definition engine.hpp:1750
std::atomic< size_t > _getAllowedInputsAverageCumulativeTime
Cumulative per-thread-average get-allowed-inputs time.
Definition engine.hpp:1730
jaffarCommon::concurrent::HashMap_t< jaffarCommon::hash::hash_t, jaffarCommon::concurrent::HashSet_t< InputSet::inputIndex_t > > _candidateInputsDetected
Per-base-state-input-hash set of candidate inputs already detected, used to dedup candidate-input pro...
Definition engine.hpp:1486
std::atomic< size_t > _getAllowedInputsThreadRawTime
Summed per-thread get-allowed-inputs time for the step.
Definition engine.hpp:1728
void workerFunction()
Worker body executed in parallel by every thread during a step.
Definition engine.hpp:1007
std::vector< threadAccumulator_t > _threadAccumulators
Per-thread accumulators for hot-loop timing/counters, reduced once per step.
Definition engine.hpp:1677
const std::vector< std::vector< uint8_t > > & getRefStates() const
Audit accessors: the per-depth canonical reference states and the volatile-byte mask (instance-depend...
Definition engine.hpp:1550
std::mutex _winCollectLock
Guards _winKeysSeen and the win-collection file writes across worker threads.
Definition engine.hpp:891
size_t getFullStateSize() const
Full self-contained state size ([hot]+[history]); for standalone snapshots outside the slabs.
Definition engine.hpp:872
std::atomic< size_t > _refPinMaxDepthHit
Deepest reference depth a pin matched (diagnostic).
Definition engine.hpp:1516
std::unique_ptr< jaffarPlus::StateDb > _stateDb
Thread-safe state database holding the current and next step's states.
Definition engine.hpp:1495
size_t _refPinLookahead
How many future depths to also match (being ahead).
Definition engine.hpp:1511
jaffarCommon::hash::hash_t computeStateHash(Runner &r)
Supplies per-depth serialized reference states for exact pin verification, and computes the volatile-...
Definition engine.hpp:1529
std::atomic< size_t > _calculateHashAverageTime
Per-thread-average hash-calculation time for the step.
Definition engine.hpp:1699
bool isWinCollectionEnabled() const
Whether win collection is enabled.
Definition engine.hpp:649
std::atomic< size_t > _runnerStateSaveAverageCumulativeTime
Cumulative per-thread-average state-save time.
Definition engine.hpp:1695
std::atomic< size_t > _checkpointCutoff
Step index after which states below _checkpointLevel are dropped.
Definition engine.hpp:1631
bool _manualSaveSolutionUpdatedLastRuleId
Whether the manual-save last-rule id changed this step.
Definition engine.hpp:1624
void setReferencePruneTrace(const std::vector< float > &trace)
Arms reference-reward pruning with the per-depth trace: every produced non-win state whose floor rewa...
Definition engine.hpp:683
std::atomic< size_t > _runnerStateLoadThreadRawTime
Summed per-thread state-load time for the step.
Definition engine.hpp:1688
std::mutex _stepBestWinStateLock
Guards updates to _stepBestWinState.
Definition engine.hpp:1613
std::string _winSolutionPath
File the best win-state's input history is written to (see _bestWinSolutionReward).
Definition engine.hpp:1618
std::atomic< size_t > _advanceStateDbAverageTime
State-DB advance time reported for the step.
Definition engine.hpp:1744
std::atomic< size_t > _getAllowedInputsAverageTime
Per-thread-average get-allowed-inputs time for the step.
Definition engine.hpp:1729
bool _winCollectEnabled
Whether win-state collection is enabled (see setWinStateCollection).
Definition engine.hpp:876
auto getManualSaveSolution() const
Returns a copy of the most recent manually saved solution.
Definition engine.hpp:690
inputResult_t runNewInput(Runner &r, const void *baseStateData, const InputSet::inputIndex_t input, threadAccumulator_t &acc, const size_t threadId)
Re-loads the base state, runs a single input, updates per-outcome counters and checkpoint tracking.
Definition engine.hpp:1113
size_t _maxThreadStepTimeThreadId
Id of the thread with the maximum step time.
Definition engine.hpp:1674
auto getStateCount() const
Returns the number of states currently held in the state database.
Definition engine.hpp:694
size_t _stateSizeInDatabase
Size of a single state as stored in the database, in bytes.
Definition engine.hpp:1608
bool _refPinExactVerify
Whether hash-matched pin candidates are byte-verified against captured reference states.
Definition engine.hpp:1509
ssize_t _manualSaveSolutionActiveLastRuleId
Currently active manual-save last-rule id across steps.
Definition engine.hpp:1625
std::shared_ptr< void > _inputHistoryBacking
The one shared input-history backing (e.g.
Definition engine.hpp:1499
std::atomic< size_t > _repeatedStates
Counter for repeated states (detected via hash collision).
Definition engine.hpp:1650
std::unique_ptr< std::atomic< uint8_t >[]> _refPinnedAtDepth
Per reference depth: claimed(1)/unclaimed(0), so exactly one copy is pinned.
Definition engine.hpp:1519
std::atomic< size_t > _runnerStateAdvanceAverageTime
Per-thread-average runner-advance time for the step.
Definition engine.hpp:1684
std::atomic< size_t > _calculateRewardThreadRawTime
Summed per-thread reward-calculation time for the step.
Definition engine.hpp:1723
std::atomic< size_t > _normalStates
Counter for normal states.
Definition engine.hpp:1659
std::atomic< size_t > _returnFreeStateAverageCumulativeTime
Cumulative per-thread-average return-free-state time.
Definition engine.hpp:1720
std::vector< uint8_t > _refVolatileMask
1 = byte is instance-residue (excluded from exact pin verification)
Definition engine.hpp:1602
inputResult_t
Outcome of running a single input on a base state.
Definition engine.hpp:928
@ normal
Resulting state was a normal state and was stored.
Definition engine.hpp:935
@ repeated
Resulting state's hash was already seen.
Definition engine.hpp:929
@ win
Resulting state was a win state.
Definition engine.hpp:936
@ droppedNoStorage
No free state slot was available to store the new state.
Definition engine.hpp:930
@ droppedFailedSerialization
Pushing the state into the database failed (e.g. serialization error).
Definition engine.hpp:931
@ failed
Resulting state was classified as a loss.
Definition engine.hpp:934
@ droppedBelowReference
State's reward fell below the reference trace at its depth (beyond the prune tolerance).
Definition engine.hpp:933
@ droppedCheckpoint
State did not meet the current checkpoint level past the cutoff step.
Definition engine.hpp:932
std::atomic< size_t > _refPinSeenPreDedup
Reference-depth matches seen (diagnostic).
Definition engine.hpp:1518
void runStep()
Runs a single search step: expands all current base states in parallel and advances the databases.
Definition engine.hpp:424
std::atomic< size_t > _failedStates
Counter for failed states (reached a point in the game considered a loss).
Definition engine.hpp:1653
float _refPruneTolerance
Allowed slack below the reference trace before a state is pruned.
Definition engine.hpp:903
size_t _currentStep
Counter for the current step.
Definition engine.hpp:1489
std::atomic< size_t > _returnFreeStateThreadRawTime
Summed per-thread return-free-state time for the step.
Definition engine.hpp:1718
float _refPinBonus
Reward bonus for matching the reference at the state's own depth.
Definition engine.hpp:1510
std::vector< std::vector< uint8_t > > _refStates
Per-depth serialized reference states for EXACT pin verification (empty = hash-only pinning).
Definition engine.hpp:1517
std::atomic< size_t > _checkpointTolerance
Tolerance (in steps) associated with the current checkpoint level.
Definition engine.hpp:1630
std::atomic< size_t > _stepNewStatesProcessed
New states processed during the current step.
Definition engine.hpp:1664
std::atomic< size_t > _getFreeStateAverageCumulativeTime
Cumulative per-thread-average get-free-state time.
Definition engine.hpp:1715
size_t _winCollectMax
Maximum number of distinct win solutions to collect before terminating.
Definition engine.hpp:885
size_t _subTotalAverageCumulativeTime
Sum of all per-operation cumulative average times.
Definition engine.hpp:1753
std::mutex _checkpointMutex
Serializes checkpoint level rises (rare path).
Definition engine.hpp:1632
bool isReferencePruneRequested() const
Whether "Reference Reward Prune" is enabled in the engine configuration (the driver checks this to kn...
Definition engine.hpp:672
std::atomic< size_t > _advanceHashDbAverageCumulativeTime
Cumulative hash-DB advance time.
Definition engine.hpp:1740
std::vector< size_t > _threadStepTime
Per-thread running time of the current step, in microseconds.
Definition engine.hpp:1672
std::atomic< size_t > _runnerStateLoadAverageCumulativeTime
Cumulative per-thread-average state-load time.
Definition engine.hpp:1690
bool isInputHistoryExhausted() const
True if the shared input-history backing (the Trie) has hit its hard node-storage ceiling.
Definition engine.hpp:701
void printInfo()
Logs engine status: timing breakdown, throughput, state counts, checkpoints, databases,...
Definition engine.hpp:706
auto & getStateDb() const
Returns a reference to the owned state database.
Definition engine.hpp:632
size_t getWinCollectionMax() const
Win collection Max Files cap.
Definition engine.hpp:651
float _bestWinSolutionReward
Highest-reward win seen across ALL steps, and the file its input history is written to at detection t...
Definition engine.hpp:1617
bool _refPruneEnabled
Whether reference-reward pruning is armed (requested AND trace supplied).
Definition engine.hpp:897
std::atomic< size_t > _ruleCheckingAverageCumulativeTime
Cumulative per-thread-average rule-checking time.
Definition engine.hpp:1710
std::atomic< size_t > _runnerStateAdvanceThreadRawTime
Summed per-thread runner-advance time for the step.
Definition engine.hpp:1683
const std::vector< uint8_t > & getRefVolatileMask() const
Audit accessor: per-byte volatile mask (1 = instance-dependent byte) over the reference states.
Definition engine.hpp:1553
std::vector< jaffarCommon::hash::hash_t > _refPinHashes
Reference state hash at each search depth.
Definition engine.hpp:1513
size_t _baseStateBatch
Active base-state pull batch size ("Base State Batch Size").
Definition engine.hpp:1503
float _refPinLookaheadBonus
Extra bonus per frame ahead of the reference.
Definition engine.hpp:1512
auto getWinStatesFound() const
Returns the cumulative number of win states found so far.
Definition engine.hpp:692
std::atomic< size_t > _calculateHashThreadRawTime
Summed per-thread hash-calculation time for the step.
Definition engine.hpp:1698
size_t getInputHistoryApproxMemoryBytes() const
Current (approximate) live memory (bytes) of the shared input-history backing; 0 for None/Raw.
Definition engine.hpp:699
std::atomic< size_t > _ruleCheckingAverageTime
Per-thread-average rule-checking time for the step.
Definition engine.hpp:1709
std::atomic< size_t > _droppedStatesNoStorage
Counter for states dropped due to lack of free states.
Definition engine.hpp:1638
Engine(const nlohmann::json &emulatorConfig, const nlohmann::json &gameConfig, const nlohmann::json &runnerConfig, const nlohmann::json &engineConfig)
Constructs the engine, building one runner per worker thread and the state/hash databases.
Definition engine.hpp:71
std::atomic< size_t > _totalNewStatesProcessed
New states processed across all steps so far.
Definition engine.hpp:1665
std::mutex _manualSaveSolutionLock
Guards updates to _manualSaveSolution.
Definition engine.hpp:1622
size_t _refPruneStepGrace
Prune each state vs the reference this many steps earlier (transient-wait allowance)
Definition engine.hpp:904
~Engine()
Frees the best-win and manual-save state buffers allocated in initialize.
Definition engine.hpp:622
bool _compactLog
"Log Verbosity": "Compact" collapses the per-step engine block to essentials.
Definition engine.hpp:1515
std::set< std::string > _winKeysSeen
Dedup keys of the win solutions collected so far.
Definition engine.hpp:888
std::atomic< size_t > _getCandidateInputsThreadRawTime
Summed per-thread get-candidate-inputs time for the step.
Definition engine.hpp:1733
size_t getCheckpointTolerance() const
Returns the current state's checkpoint tolerance.
Definition game.hpp:623
virtual InputSet::inputIndex_t getNullInputIndex() const
The input index representing "no buttons pressed" – used by the engine's Hash Lookahead (advance-with...
Definition game.hpp:614
bool isSaveSolution() const
Indicates whether the current state should trigger a save solution.
Definition game.hpp:629
ssize_t getSaveSolutionCurrentLastRuleIdx() const
Returns the current last rule index that set a save solution.
Definition game.hpp:635
virtual float getFloorReward() const
Reward used for the Reference Reward Floor comparison: the un-biased progress reward,...
Definition game.hpp:610
void updateReward()
Recomputes the current state's reward from the satisfied rules.
Definition game.hpp:459
size_t getCheckpointLevel() const
Returns the current state's checkpoint level.
Definition game.hpp:620
Property * findProperty(const std::string &propertyName)
Finds a registered game property by name.
Definition game.hpp:84
void evaluateRules()
Evaluates the rule set against the current state.
Definition game.hpp:363
float getReward() const
Returns the current state's reward.
Definition game.hpp:596
void updateGameStateType()
Recomputes the state type and checkpoint level from the satisfied rules.
Definition game.hpp:417
stateType_t getStateType() const
Returns the current state type (normal, win or fail).
Definition game.hpp:617
@ normal
No win or fail rule is currently satisfied.
Definition game.hpp:44
@ fail
A fail rule is currently satisfied.
Definition game.hpp:46
@ win
A win rule is currently satisfied.
Definition game.hpp:45
const std::string getSaveSolutionPath() const
Returns the save path of the rule that activated the current save solution.
Definition game.hpp:641
size_t inputIndex_t
Type used to index an input.
Definition inputSet.hpp:29
void * getPointer() const
Returns the raw pointer to the property's value in memory.
Definition property.hpp:192
Owns a Game instance and advances it according to configured inputs.
Definition runner.hpp:38
size_t getStateSize() const
Computes the size in bytes of the serialized runner state.
Definition runner.hpp:384
static std::unique_ptr< Runner > getRunner(const nlohmann::json &emulatorConfig, const nlohmann::json &gameConfig, const nlohmann::json &runnerConfig)
Creates a runner from the emulator, game and runner configurations.
Definition runner.hpp:527
void advanceState(const InputSet::inputIndex_t inputIdx)
Advances the game by one input, then by the configured number of frameskip frames.
Definition runner.hpp:309
void setSearchStep(const size_t searchStep)
Sets the step counter from a search step.
Definition runner.hpp:351
jaffarCommon::hash::hash_t computeHash() const
Computes a hash of the current runner state.
Definition runner.hpp:426
Game * getGame() const
Returns a pointer to the owned game instance.
Definition runner.hpp:516
void serializeState(jaffarCommon::serializer::Base &serializer) const
Serializes the runner state: the game state, the input history, and the input counter.
Definition runner.hpp:360
std::string getInputHistoryString() const
Builds a newline-separated string of the recorded input history.
Definition runner.hpp:456
void deserializeState(jaffarCommon::deserializer::Base &deserializer)
Restores the runner state: the game state, the input history, and the input counter.
Definition runner.hpp:372
size_t getStepCount() const
Returns the current step counter (number of inputs applied / the state's depth).
Definition runner.hpp:354
#define JAFFAR_PROF_ACC(field, var)
Accumulates the microseconds elapsed since timestamp var into field.
Definition engine.hpp:41
#define JAFFAR_PROF_DECL(var)
Declares a timestamp variable var holding the current time (detailed-profiling build).
Definition engine.hpp:39
Abstract base for a JaffarPlus game: wraps an emulator, registers game properties,...
Two-tier (per-domain L1 + shared global L2) hash database used to deduplicate visited search states,...
NUMA topology detection: distance/preference matrices and per-domain delegate-thread selection,...
void initializeNUMA()
Initializes NUMA / core-affinity state.
Definition numa.hpp:44
Drives a Game forward one input at a time, managing the allowed/candidate input sets,...
Per-NUMA-domain database of serialized game states, with reward-ordered queues that feed the search o...
A manually saved solution: its input path, reward, serialized state, and triggering rule index.
Definition engine.hpp:949
ssize_t lastRuleIdx
Index of the last rule active when the state was saved.
Definition engine.hpp:953
float reward
Reward of the saved state.
Definition engine.hpp:951
std::string path
Input sequence (solution path) that reached the saved state.
Definition engine.hpp:950
size_t stepCount
Depth (input count) of the saved state, recorded at capture.
Definition engine.hpp:954
void * stateData
Raw buffer holding the serialized saved state, or nullptr if unset.
Definition engine.hpp:952
A reward value paired with a serialized state buffer.
Definition engine.hpp:941
size_t stepCount
Depth (input count) of the saved state, recorded at capture (the count is not serialized per-state).
Definition engine.hpp:944
void * stateData
Raw buffer holding the serialized state, or nullptr if unset.
Definition engine.hpp:943
float reward
Reward associated with the stored state.
Definition engine.hpp:942
Per-thread accumulator for timing and counters.
Definition engine.hpp:968
size_t normalStates
Number of normal states produced.
Definition engine.hpp:986
size_t runnerStateAdvance
Time spent advancing the runner state with an input.
Definition engine.hpp:970
size_t baseStatesProcessed
Number of base states this thread expanded.
Definition engine.hpp:984
size_t calculateHash
Time spent computing state hashes.
Definition engine.hpp:973
size_t getAllowedInputs
Time spent querying the runner's allowed inputs.
Definition engine.hpp:979
size_t droppedStatesCheckpoint
Number of states dropped for not meeting the checkpoint.
Definition engine.hpp:992
void reset()
Resets all timers and counters to zero.
Definition engine.hpp:996
size_t getCandidateInputs
Time spent querying the runner's candidate inputs.
Definition engine.hpp:980
size_t droppedStatesFailedSerialization
Number of states dropped due to failed serialization.
Definition engine.hpp:991
size_t runnerStateLoad
Time spent loading states into the runner.
Definition engine.hpp:971
size_t repeatedStates
Number of states dropped as repeated.
Definition engine.hpp:987
size_t runnerStateSave
Time spent saving runner states into the state database.
Definition engine.hpp:972
size_t newStatesProcessed
Number of new states this thread produced via inputs.
Definition engine.hpp:985
size_t popBaseStateDb
Time spent popping base-state batches from the state database.
Definition engine.hpp:981
size_t failedStates
Number of states classified as failures.
Definition engine.hpp:988
size_t calculateReward
Time spent computing state rewards.
Definition engine.hpp:978
size_t checkHash
Time spent checking hashes against the hash database.
Definition engine.hpp:974
size_t returnFreeState
Time spent returning state slots to the free queue.
Definition engine.hpp:977
size_t droppedStatesNoStorage
Number of states dropped for lack of free storage.
Definition engine.hpp:990
size_t ruleChecking
Time spent evaluating rules and determining state type.
Definition engine.hpp:975
size_t droppedStatesBelowReference
Number of states dropped for falling below the reference reward trace.
Definition engine.hpp:993
size_t winStates
Number of win states produced.
Definition engine.hpp:989
size_t getFreeState
Time spent acquiring free state slots.
Definition engine.hpp:976