Compare commits
No commits in common. "16b1e65fa3db4e2cd7552e8cdb443faa8bbc7646" and "f99488af9f042d1ce654c770de9d94f6037d59ad" have entirely different histories.
16b1e65fa3
...
f99488af9f
8 changed files with 21 additions and 334 deletions
|
|
@ -1,134 +0,0 @@
|
|||
# Lane L3 — predictions, written before the build (method rule 2)
|
||||
|
||||
Lane L3 owns VM144 (`sots-re-win10-144`, `re@192.168.10.144`, a ZFS clone of VM140).
|
||||
Target: manufacture a workload in which the trade-route vector and/or the spy-program
|
||||
vector is **non-empty**, then measure whether the four RNG-bearing tail callees draw.
|
||||
|
||||
Instrument: `watch.mode=cont`, a third mode on lane W3's watchpoint module. Same single
|
||||
arming detour (`StrategyServer::ApplyAllTurnCommands`, RVA `0x0038f6a0`), same VEH, same
|
||||
canary, same hit format. Two differences from `tshn`:
|
||||
|
||||
1. it arms **all four** debug slots on the two containers — `_Myfirst` and `_Mylast` of
|
||||
each — so an allocation (`_Myfirst` moves from 0) and a `push_back` (`_Mylast` moves)
|
||||
are separable;
|
||||
2. it **re-arms and re-logs every End Turn** instead of only the first. Lane W3 printed
|
||||
one count from one turn of one save; this workload is a game played across many turns,
|
||||
and the question "did the container grow on turn N" needs a line per turn.
|
||||
|
||||
---
|
||||
|
||||
## What the static reading says the workload is (before any run)
|
||||
|
||||
Read out of `sots.exe`, `TechTree/MasterTechList.tech`, `Data/Strategy/StrategyVars.txt`,
|
||||
`Species/*/sections/*.shipsection` and the repo's own catalogs. This is the part three
|
||||
previous lanes did not have, and it changes the cost estimate by an order of magnitude.
|
||||
|
||||
### Trade routes
|
||||
|
||||
* The gate is the tech **`CCC_FTLEcon`** (id 10026) — it sets `ServerPlayer+0xff` (`CnTrd`)
|
||||
unless the player is `RebAI` (`ghidra/addresses.json:1510`, `tech-effects.md:64`).
|
||||
* Cost: `CCC_ROOT → CCC_FTLBrdB` (RP 4,000) `→ CCC_FTLEcon` (RP 18,000). **Zuul cannot
|
||||
research it** (`Zuul:0` in the availability line) — so the workload must not be Zuul.
|
||||
* `StrategyServer_RegisterTradeSystems 0x007adc80` registers, for every owned system whose
|
||||
owner has `CnTrd`, `ServerSystem::NumTradeRoutesSupported 0x00819d20` route slots, and
|
||||
that function is `ceil(civ/REQ_CIV) + ceil(imp/REQ_IMP)` with a **floor of 1**.
|
||||
* **Therefore the population thresholds in `StrategyVars.txt`
|
||||
(`TRADE_ROUTE_REQ_CIVPOPULATION 200000000`, `TRADE_ROUTE_REQ_IMPPOPULATION 500000000`)
|
||||
are not a hard gate — they scale the count, and the minimum is 1 per owned system.**
|
||||
* `TRADE_SECTOR_SIZE 10.0` — routes live inside a trade sector, so the endpoints have to be
|
||||
near each other.
|
||||
* A **trade station** (`DNStationTrade`, station type 4) adds `STATION_TRADE_NUMROUTES 2`
|
||||
and `STATION_BONUS_TRADE_INCOME 0.25`. It is a *bonus*, not a prerequisite.
|
||||
|
||||
> **Prediction T1.** The premise carried in `tail-probes.md` §3.2 and repeated in the L3
|
||||
> brief — "trade income needs trade-station construction plus its tech" — is **wrong about
|
||||
> the station**. `CCC_FTLEcon` plus two owned systems inside one trade sector is sufficient
|
||||
> to make the trade-route vector non-empty. No trade station is needed.
|
||||
>
|
||||
> *Falsifier:* a game with `CCC_FTLEcon` researched, ≥2 owned systems within 10 units, and
|
||||
> `trade routes … count=0` still logged after several End Turns.
|
||||
|
||||
> **Prediction T2.** The vector fills during turn processing, not at the moment the tech
|
||||
> completes, so the first non-zero count appears on the End Turn **after** the one that
|
||||
> finished `CCC_FTLEcon`.
|
||||
|
||||
### Spy programs
|
||||
|
||||
Nobody had a click path. The static reading gives one.
|
||||
|
||||
* `Species/<race>/sections/_Spy.shipsection` — `entity_class "SpyShip"`, `section_type
|
||||
mission`, `nodesign 1`, `spy 1`, `cost 20000`, and
|
||||
`requires CCC_SpyBm` + `requires IND_SlvgTech`.
|
||||
* The **spy tender** — the "ship a spy can dock with" in `Can't build spy: Fleet does not
|
||||
have a ship spy can dock with.` — is the section carrying `spytender`. In this image that
|
||||
is exactly one section per race: **`CRRepairandSalvage`** (Cruiser Repair & Salvage
|
||||
mission section, `requires IND_SlvgTech`, cost 60,000). Nothing in the UI says "tender".
|
||||
* Tech cost: `IND_OrbFound → IND_CruisCon` (16,000) `→ IND_SlvgTech` (12,000) for the
|
||||
tender and half the spy; `CCC_ROOT → CCC_FTLBrdB` (4,000) `→ CCC_SpyBm` (12,000) for the
|
||||
other half. `CCC_FTLBrdB` is shared with the trade chain.
|
||||
* The action path is the **`Special` menu on the fleet panel** (`(229,464)` at 1024×768):
|
||||
`SHIPACTION_BUILDSPY` / `DEPLOYSPY` / `PICKUPSPY`, backed by `Game::BuildSpyDialog`,
|
||||
`Game::DeploySpyDialog`, `Game::PickupSpyDialog` and the `Game::CanBuildSpy` functor.
|
||||
* The engine's own refusal strings enumerate the preconditions exactly:
|
||||
build — own system, player has ability, fleet has a tender, tender not already carrying;
|
||||
deploy — **not your own system**, no enemy fleet present, and
|
||||
`Can't deploy spy: Spy requires an asteroid belt to hide.`
|
||||
(`SPYSHIP_NEARBY_ASTEROID_RANGE 500.0`; `STARMAP_ASTEROIDBELT_ODDS 25` %).
|
||||
|
||||
> **Prediction S1.** `ServerSpyManager`'s vector is the spy-**craft** list, and it grows at
|
||||
> **Build Spy**, not at Deploy — the `Game::SpyCraft` record has `atto` (attached-to) and
|
||||
> `tdep` (turn deployed) fields, which only make sense if the object exists while still
|
||||
> docked. If S1 holds, the spy half of this workload needs **no second empire at all**.
|
||||
>
|
||||
> *Falsifier:* Build Spy succeeds (the fleet shows a docked spy) and the next End Turn still
|
||||
> logs `spy programs … count=0`. That would move the growth point to Deploy and make an
|
||||
> enemy colony with an asteroid belt mandatory.
|
||||
|
||||
> **Prediction S2.** The trapped write to the spy vector's `_Mylast` will have an `eip`
|
||||
> inside `ServerSpyManager` and an `ebpret` inside the turn-command applier, not inside a UI
|
||||
> class — because the debug registers are armed on the **turn thread only**
|
||||
> (`ArmCurrentThread(GetCurrentThread())`), and SOTS is lockstep: the UI issues a command
|
||||
> and the server applies it. If the write instead happens on the UI thread, **no trap will
|
||||
> be taken at all** and the count line is the only evidence. This is the rule-20 trap in
|
||||
> this instrument: a zero trap count with a rising count line means *wrong thread*, not
|
||||
> *no write*.
|
||||
|
||||
### Getting there without twenty turns of play
|
||||
|
||||
The `Custom` game-setup screen (`GAMESETUPFE_BTN_CUSTOM`) exposes
|
||||
`GAMESETUP_INITCOLONIES`, `GAMESETUP_INITSAVINGS`, `GAMESETUP_INITTECHS`,
|
||||
`GAMESETUP_MONEY_RESEARCHMOD`, `GAMESETUP_MONEY_INCOMEMOD`, `GAMESETUP_SYSTEM_DISTANCE`,
|
||||
`GAMESETUP_SYSTEM_SIZE`, `GAMESETUP_SYSTEM_SUITABILITY`. All of it is **stock**: no data
|
||||
file is modified, no save is edited, so the resulting save is a legitimate corpus member.
|
||||
|
||||
> **Prediction W1.** With initial colonies, initial savings, initial techs and the research
|
||||
> modifier at maximum on a small close-packed map, both tech chains are reachable inside
|
||||
> ~10 End Turns rather than the "tens of 30–60 s End Turns" the brief budgets.
|
||||
|
||||
---
|
||||
|
||||
## Instrument predictions
|
||||
|
||||
> **Prediction I1 (control, rule 19).** `watch=on watch.mode=cont` is byte-neutral: one End
|
||||
> Turn from `ref-turn2.sav` reproduces the determinism oracle
|
||||
> — `(Autosave EndTurn).sav` `bb4fd9ac89f41e3b…`, `(Autosave).sav` `978041acd168b56e…` —
|
||||
> exactly as `watch=off` does. The two configs differ in one key.
|
||||
|
||||
> **Prediction I2.** On `ref-turn2.sav` the new mode reproduces lane W3's numbers:
|
||||
> both managers non-null, all six pointers zero, both counts 0, zero traps. If it does not,
|
||||
> the mode is wrong and nothing measured with it later counts.
|
||||
|
||||
> **Prediction I3.** The canary self-test passes on **every** turn, not just the first —
|
||||
> the delta form of the counter is new in this mode and is itself a thing that can be wrong.
|
||||
|
||||
---
|
||||
|
||||
## What would make this lane's result worthless
|
||||
|
||||
* Counting a container from the wrong base. `S` is the `ApplyAllTurnCommands` `this`; every
|
||||
`StrategyServer_off_*` is in the **`S+4`** frame (`lane-t.json:172`). A wrong base yields
|
||||
a plausible-looking zero.
|
||||
* Reporting "0 traps" when the writer ran on another thread (S2).
|
||||
* Producing a save whose non-empty container came from a modified data file. Nothing in this
|
||||
lane modifies `sots.gob` or any loose data file; if that ever changes it must be recorded
|
||||
in the save's provenance line.
|
||||
|
|
@ -68,32 +68,3 @@ The second run (turn-1 workload, `watch.mode=modcount`) has **no oracle of its o
|
|||
everything else — the canary, the VEH, the flusher, the hit format — is unchanged. Keep the canary
|
||||
self-test and keep the `ref-turn2` oracle control: together they cost about two minutes and they
|
||||
are what makes the numbers evidence rather than output.
|
||||
|
||||
## Mode `cont` (lane L3)
|
||||
|
||||
A third `watch.mode`, added when lane L3 needed to watch the trade-route and spy-program
|
||||
containers across a whole game played forward rather than one turn of one save.
|
||||
|
||||
* All four debug slots go on the two containers: trade `_Myfirst` (`mgr+0x3c`), trade `_Mylast`
|
||||
(`mgr+0x40`), spy `_Myfirst` (`mgr+0x10`), spy `_Mylast` (`mgr+0x14`). `_Myfirst` is armed as
|
||||
well as `_Mylast` because these vectors are default-constructed with all three pointers zero:
|
||||
the first element writes all three, and watching only `_Mylast` cannot tell "allocated for the
|
||||
first time" from "appended to an existing buffer". Those are different events in the model the
|
||||
lane was trying to falsify.
|
||||
* It **re-arms and re-logs on every End Turn**. `modcount` and `tshn` arm once on purpose - their
|
||||
targets are chosen from one turn's state and re-picking them would move the measurement. `cont`
|
||||
has no such target selection, the two manager pointers are stable for the life of the server,
|
||||
and the per-turn count line is the signal.
|
||||
* The canary self-test therefore runs every turn too, and its counter is read as a **delta**. On
|
||||
the arm-once modes the delta is the old value, so their log lines are unchanged.
|
||||
|
||||
`ReportContainer` is factored out of `ArmTshnSlots` so both modes emit a byte-identical container
|
||||
line; lane W3's published counts are the control every later count is compared against.
|
||||
|
||||
**`hooks=off watch=on` used to arm nothing.** `Shim_Init` returned before `install_watchpoints`
|
||||
whenever the trace mode was `off`, so that config printed `watch=on` in the banner and then
|
||||
measured a confident zero - the exact failure mode method rule 1 exists to catch. The watchpoints
|
||||
are an independent instrument (one arming detour of their own, no trace records), and a long play
|
||||
session wants them without paying 30-45 s per End Turn for template hooks that measure nothing it
|
||||
is asking about. `main.cpp` now initialises MinHook and installs the watchpoint module on the
|
||||
`hooks=off` path when `watch=on`.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
|
||||
// Source: sots-re ghidra/addresses.json @ 7d51767, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Source: sots-re ghidra/addresses.json @ e966a72, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
|
@ -1933,10 +1933,6 @@ constexpr uint32_t SVSOSwarmQueen_off_Hives = 0x00000010;
|
|||
constexpr uint32_t SVSOSwarmQueen_off_Queens = 0x00000020;
|
||||
// field int -- the difficulty tier, wire tag "CDiff". Set to -1 by the ctor 0x0051a820 and written ONLY by SVSOSlaversRefuel_UpdateDifficultyTier, which stores (scanIndex - 1) and only when it differs. Reachable values are 0 (frame 1..49) and 1 (frame 50..99) ONLY: frame <= 0 exits at index 0 and frame >= 100 walks off the end of the 3-entry threshold table, so BOTH edges store nothing and the tier can never reach 2 [verified]
|
||||
constexpr uint32_t SVSOSlaversRefuel_off_CDiff = 0x00000038;
|
||||
// __thiscall void (ShipAction* this, ...) /* THE `Build Spy` SHIP-ACTION HANDLER -- the writer at the top of the only call chain that has ever grown Game::ServerSpyManager's spy vector. Trapped live: with a debug breakpoint on spyManager+0x10/+0x14 and a `Build Spy` order issued from the fleet panel's Special menu, the single allocation of that vector arrived as 0x00789620+0x53 -> 0x008383c0+0x9c -> 0x0059f1a0+0x5f -> 0x00483410+0x7c/+0x82, on the turn thread, inside the End Turn that applied the order. REACHABILITY: same indirection class as its sibling ShipBorneBuildQueue_ProcessTurn 0x00789500 (lane B6) -- 0x120 bytes away, zero direct call sites, no vtable slot; both are entries in the STACK-BUILT function-pointer table the ship-action dispatcher (lane T's 0x007b9c4b site, dispatched from StrategyServer 0x007dc92b/0x007dc952/0x007dcb0c) indexes by action type. NOT READ IN GHIDRA: the body, the action-type index and the argument list are unknown; what is measured is the entry's position in the chain and the fact that no other path grew the container across 7 instrumented turns */ [mapped]
|
||||
constexpr uint32_t ShipAction_BuildSpy = 0x00389620;
|
||||
// __thiscall void (Game::ServerSpyManager* this, ...) /* The spy-craft creator: the only frame between the Build Spy ship action and the push_back that grows the manager's vector at this+0x10..0x18. Trapped at +0x9c calling 0x0059f1a0 with that vector; the element it appends is the Game::SpyCraft whose serialized record then appears under `spymgr`/`spy` with sid=1, sown=<player id>, atto=<tender ship id>, tdep=-1. NOT READ IN GHIDRA: whether it also does the payment, the tender bookkeeping and the event post, or delegates them */ [mapped]
|
||||
constexpr uint32_t ServerSpyManager_CreateSpyCraft = 0x004383c0;
|
||||
// thiscall void __thiscall Game::AITColonize::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. SHARED BODY: Game::AITColonizeGoal::Execute is the same address. 48 bytes; a forwarder to the parameterised worker 0x0068b280 called as worker(ecx = agent, this, pass, this->+0x8, &this->+0x20, &this->+0x10) with `edi = this->+0xc` passed as an IMPLICIT REGISTER ARGUMENT (whole-program-optimised custom convention; a reimplementation that ports only the stack arguments passes garbage). Reaches the colonize order (list 7) at depth 4 via 0x0068b280 -> 0x006930f0 -> 0x00578ff0 -> 0x00769640 [unverified]
|
||||
constexpr uint32_t AITColonize_Execute = 0x0028b400;
|
||||
// thiscall void __thiscall Game::AITEscortGateInvade::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. SHARED BODY: Game::AITEscortGateInvadeGoal::Execute is the same address. 80 bytes; forwards to 0x0068c5d0 as __fastcall(ecx = this->+0xc, edx = agent) plus 8 stack arguments. Reaches list 7 at depth 4 [unverified]
|
||||
|
|
|
|||
|
|
@ -20,14 +20,8 @@ int g_players = 2;
|
|||
char g_outPath[MAX_PATH] = {};
|
||||
|
||||
// Which four addresses the single arming point computes. `modcount` is lane W2's set and is the
|
||||
// default so its run stays reproducible byte for byte; `tshn` is lane W3's; `snlv` is lane L1's;
|
||||
// `cont` is lane L3's.
|
||||
//
|
||||
// `cont` differs from the others in one further respect: it re-arms and re-logs on EVERY End Turn
|
||||
// rather than only the first. The other modes measure one turn of one save, where arming once is
|
||||
// right. L3's workload is a game played forward across many turns and the question is "did the
|
||||
// container grow on turn N", which needs a line per turn.
|
||||
enum class Mode { ModCount, Tshn, Snlv, Cont };
|
||||
// default so its run stays reproducible byte for byte; `tshn` is lane W3's.
|
||||
enum class Mode { ModCount, Tshn, Snlv };
|
||||
Mode g_mode = Mode::ModCount;
|
||||
|
||||
// ---- what is being watched ------------------------------------------------------------------
|
||||
|
|
@ -296,62 +290,6 @@ constexpr std::uint32_t kTradeMgrOffVec = 0x3c; // _Myfirst; _Mylast at +0
|
|||
constexpr std::uint32_t kServerOffSpyMgr = 0x158; // -> ServerSpyManager*
|
||||
constexpr std::uint32_t kSpyMgrOffVec = 0x10; // _Myfirst; _Mylast at +0x14
|
||||
|
||||
// One container report line. Factored out of lane W3's arming function so lane L3's mode prints
|
||||
// the identical line -- the two modes' numbers have to be comparable word for word, and W3's
|
||||
// published counts are the control this campaign compares every later count against.
|
||||
struct ContInfo {
|
||||
std::uintptr_t mgr; // 0 when the manager pointer is null or unreadable
|
||||
int count; // element count, or -1 when the offset does not name a std::vector
|
||||
};
|
||||
|
||||
ContInfo ReportContainer(std::uintptr_t S, std::uint32_t mgrOff, std::uint32_t vecOff,
|
||||
const char* what) {
|
||||
const std::uintptr_t slot = S + 4 + mgrOff;
|
||||
const std::uintptr_t mgr = Readable(slot, 4) ? U32(slot) : 0;
|
||||
std::uint32_t vf = 0, vl = 0, ve = 0;
|
||||
int n = -1;
|
||||
if (Readable(mgr, vecOff + 12)) n = VectorCount(mgr + vecOff, &vf, &vl, &ve, 4);
|
||||
LogF("watch: %s -- manager=0x%08x (S+4+0x%x) vector@mgr+0x%x first=0x%08x last=0x%08x "
|
||||
"end=0x%08x count=%d",
|
||||
what, static_cast<unsigned>(mgr), mgrOff, vecOff, vf, vl, ve, n);
|
||||
return {mgr, n};
|
||||
}
|
||||
|
||||
// Lane L3's four slots: both ends of both containers.
|
||||
//
|
||||
// `_Mylast` alone catches every push_back, which is what lane W3 armed. `_Myfirst` is armed too
|
||||
// because these vectors are default-constructed -- all three pointers zero -- so the FIRST element
|
||||
// writes all three, and watching only `_Mylast` cannot tell "the vector was allocated for the
|
||||
// first time" from "an element was appended to an existing buffer". They are different events in
|
||||
// the model this lane is trying to falsify, so they get different slots.
|
||||
//
|
||||
// The canary self-test then borrows slot 3 for one write and gives it back, exactly as in the
|
||||
// other two modes.
|
||||
void ArmContSlots(std::uintptr_t S) {
|
||||
const ContInfo trade = ReportContainer(S, kServerOffTradeMgr, kTradeMgrOffVec, "trade routes");
|
||||
const ContInfo spy = ReportContainer(S, kServerOffSpyMgr, kSpyMgrOffVec, "spy programs");
|
||||
|
||||
const struct {
|
||||
const ContInfo* c;
|
||||
std::uint32_t vecOff;
|
||||
const char* what;
|
||||
} want[4] = {
|
||||
{&trade, kTradeMgrOffVec, "trade routes _Myfirst"},
|
||||
{&trade, kTradeMgrOffVec + 4, "trade routes _Mylast"},
|
||||
{&spy, kSpyMgrOffVec, "spy programs _Myfirst"},
|
||||
{&spy, kSpyMgrOffVec + 4, "spy programs _Mylast"},
|
||||
};
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (want[i].c->mgr) {
|
||||
g_watchAddr[i] = want[i].c->mgr + want[i].vecOff;
|
||||
std::snprintf(g_slotName[i], sizeof g_slotName[i], "%s (mgr+0x%x, count %d at arm)",
|
||||
want[i].what, want[i].vecOff, want[i].c->count);
|
||||
} else {
|
||||
std::snprintf(g_slotName[i], sizeof g_slotName[i], "%s (manager null)", want[i].what);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lane W3's four slots. Slot 0 is the whole point: the `TShn` word of an NVO record on a system
|
||||
// whose AFlags is zero. Slot 1 is its map's _Mysize, which is what makes slot 0's silence mean
|
||||
// something -- if the node were freed and reallocated, slot 0 would be watching dead memory and
|
||||
|
|
@ -456,12 +394,21 @@ void ArmTshnSlots(std::uintptr_t S) {
|
|||
{kServerOffSpyMgr, kSpyMgrOffVec, "spy programs"},
|
||||
};
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const ContInfo ci = ReportContainer(S, cont[i].mgrOff, cont[i].vecOff, cont[i].what);
|
||||
if (ci.mgr) {
|
||||
g_watchAddr[2 + i] = ci.mgr + cont[i].vecOff + 4;
|
||||
const std::uintptr_t slot = S + 4 + cont[i].mgrOff;
|
||||
const std::uintptr_t mgr = Readable(slot, 4) ? U32(slot) : 0;
|
||||
std::uint32_t vf = 0, vl = 0, ve = 0;
|
||||
int n = -1;
|
||||
if (Readable(mgr, cont[i].vecOff + 12))
|
||||
n = VectorCount(mgr + cont[i].vecOff, &vf, &vl, &ve, 4);
|
||||
LogF("watch: %s -- manager=0x%08x (S+4+0x%x) vector@mgr+0x%x first=0x%08x last=0x%08x "
|
||||
"end=0x%08x count=%d",
|
||||
cont[i].what, static_cast<unsigned>(mgr), cont[i].mgrOff, cont[i].vecOff, vf, vl, ve,
|
||||
n);
|
||||
if (mgr) {
|
||||
g_watchAddr[2 + i] = mgr + cont[i].vecOff + 4;
|
||||
std::snprintf(g_slotName[2 + i], sizeof g_slotName[2 + i],
|
||||
"%s vector _Mylast (mgr+0x%x, count %d at arm)", cont[i].what,
|
||||
cont[i].vecOff + 4, ci.count);
|
||||
cont[i].vecOff + 4, n);
|
||||
} else {
|
||||
std::snprintf(g_slotName[2 + i], sizeof g_slotName[2 + i], "%s (manager null)",
|
||||
cont[i].what);
|
||||
|
|
@ -487,23 +434,17 @@ extern "C" void WatchApplyAllDetour();
|
|||
|
||||
extern "C" void WatchOnApplyAll(void* self) {
|
||||
InterlockedIncrement(&g_mark);
|
||||
// modcount/tshn arm once and never again -- their targets are chosen from one turn's state and
|
||||
// re-picking them would move the measurement. `cont` re-arms every turn: the two manager
|
||||
// pointers are stable for the life of the server, and the count line is the per-turn signal.
|
||||
if (g_armed && g_mode != Mode::Cont) {
|
||||
if (g_armed) {
|
||||
watch_flush(g_log);
|
||||
return;
|
||||
}
|
||||
if (g_armed) watch_flush(g_log);
|
||||
const std::uintptr_t S = reinterpret_cast<std::uintptr_t>(self);
|
||||
g_watchAddr[0] = 0;
|
||||
g_watchAddr[1] = 0;
|
||||
g_watchAddr[2] = 0;
|
||||
g_watchAddr[3] = 0;
|
||||
|
||||
if (g_mode == Mode::Cont) {
|
||||
ArmContSlots(S);
|
||||
} else if (g_mode == Mode::Tshn || g_mode == Mode::Snlv) {
|
||||
if (g_mode == Mode::Tshn || g_mode == Mode::Snlv) {
|
||||
ArmTshnSlots(S);
|
||||
} else {
|
||||
|
||||
|
|
@ -541,9 +482,6 @@ extern "C" void WatchOnApplyAll(void* self) {
|
|||
const std::uintptr_t saved3 = g_watchAddr[3];
|
||||
g_watchAddr[3] = reinterpret_cast<std::uintptr_t>(const_cast<std::uint32_t*>(&g_canary));
|
||||
std::uint32_t dr7 = 0;
|
||||
// The counter is cumulative and `cont` mode runs this test once per End Turn, so take the
|
||||
// delta. On the arm-once modes `before` is 0 and the printed value is unchanged.
|
||||
const LONG canaryBefore = g_canaryHits;
|
||||
g_canaryArmed = true;
|
||||
if (!ArmCurrentThread(g_watchAddr, 4, &dr7)) {
|
||||
LogF("watch: ArmCurrentThread FAILED (err %lu) -- NOTHING IS ARMED", GetLastError());
|
||||
|
|
@ -552,7 +490,7 @@ extern "C" void WatchOnApplyAll(void* self) {
|
|||
return;
|
||||
}
|
||||
g_canary = 0x5a5a5a5a;
|
||||
const LONG canaryHits = g_canaryHits - canaryBefore;
|
||||
const LONG canaryHits = g_canaryHits;
|
||||
g_canaryArmed = false;
|
||||
LogF("watch: SELFTEST canary writes=1 traps=%ld dr7=0x%08x %s", canaryHits, dr7,
|
||||
canaryHits == 1 ? "PASS" : "FAIL -- every count below is unmeasured, not zero");
|
||||
|
|
@ -597,8 +535,7 @@ bool watch_apply_config(const char* key, const char* value, std::string* err) {
|
|||
if (std::strcmp(value, "modcount") == 0) g_mode = Mode::ModCount;
|
||||
else if (std::strcmp(value, "tshn") == 0) g_mode = Mode::Tshn;
|
||||
else if (std::strcmp(value, "snlv") == 0) g_mode = Mode::Snlv;
|
||||
else if (std::strcmp(value, "cont") == 0) g_mode = Mode::Cont;
|
||||
else if (err) *err = "expected modcount|tshn|snlv|cont";
|
||||
else if (err) *err = "expected modcount|tshn|snlv";
|
||||
return true;
|
||||
}
|
||||
if (std::strcmp(key, "watch.out") == 0) {
|
||||
|
|
|
|||
|
|
@ -360,24 +360,7 @@ void Shim_Init(HMODULE self) {
|
|||
|
||||
const Config cfg = ReadConfig();
|
||||
if (!cfg.hooks) {
|
||||
// `hooks=off` turns off the TRACE hooks. It must not turn off the watchpoint module.
|
||||
// Those are two independent instruments: the watchpoints install one arming detour of
|
||||
// their own and write no trace records, and lane L3 wears them for a whole game played
|
||||
// forward, where the template hooks' 30-45 s per End Turn is pure cost and measures
|
||||
// nothing being asked about. Before this, `hooks=off watch=on` silently armed nothing --
|
||||
// a config that reports "watch=on" in the banner and then measures a confident zero,
|
||||
// which is exactly the failure method rule 1 exists to catch.
|
||||
if (shim::hooks::watch_enabled()) {
|
||||
MH_STATUS st = MH_Initialize();
|
||||
Log("hook: trace hooks disabled by config; MH_Initialize (watch only) -> %s",
|
||||
MH_StatusToString(st));
|
||||
if (st == MH_OK) {
|
||||
g_minhookUp = true;
|
||||
shim::hooks::install_watchpoints(exeBase, g_dir, &ShimLogLine);
|
||||
}
|
||||
} else {
|
||||
Log("hook: disabled by config");
|
||||
}
|
||||
Log("hook: disabled by config");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
# Lane L3: the watchpoint module in `cont` mode -- all four debug slots on the two ends of the
|
||||
# trade-route and spy-program vectors, re-armed and re-counted on EVERY End Turn.
|
||||
#
|
||||
# `hooks=off` on purpose. This config is worn for a whole game played forward, not for one turn of
|
||||
# one save: the template hooks cost 30-45 s per End Turn and measure nothing this lane is asking
|
||||
# about. The watchpoint module installs independently of the trace mode.
|
||||
#
|
||||
# Identical to shim.cfg.l3control except for the single key `watch=`, so the pair is a real
|
||||
# rule-19 control.
|
||||
hooks=off
|
||||
watch=on
|
||||
watch.mode=cont
|
||||
watch.out=C:\SOTS\shim.watch.txt
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# Lane L3 CONTROL: the same build as shim.cfg.l3cont with the watchpoints disarmed. The two files
|
||||
# differ in exactly one key (`watch=`), which is what makes the pair a rule-19 control.
|
||||
#
|
||||
# `hooks=off` on purpose. This config is worn for a whole game played forward, not for one turn of
|
||||
# one save: the template hooks cost 30-45 s per End Turn and measure nothing this lane is asking
|
||||
# about. The watchpoint module installs independently of the trace mode.
|
||||
#
|
||||
# Identical to shim.cfg.l3control except for the single key `watch=`, so the pair is a real
|
||||
# rule-19 control.
|
||||
hooks=off
|
||||
watch=off
|
||||
watch.mode=cont
|
||||
watch.out=C:\SOTS\shim.watch.txt
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# Lane L3: lane H's shim.cfg.hp11 verbatim -- the same eleven register-transparent entry probes and
|
||||
# the same six-hook RNG ledger -- plus the watchpoint module in cont mode. The point is to run the
|
||||
# IDENTICAL instrument lane H ran on an empty-container turn against a turn whose trade-route
|
||||
# vector is NON-empty, so the two entry counts are comparable line for line.
|
||||
hook.Shim::SelfTest::Fill=off
|
||||
hook.Mars::GlobalConsts::LoadFile=off
|
||||
hook.Game::WeaponDictionary::Init=off
|
||||
hook.Game::SectionDictionary::SectionDictionary=off
|
||||
hook.Game::ServerPlayer::ComputeBudget=off
|
||||
hook.Game::TechTree::ProcessResearch=off
|
||||
hook.Game::ServerPlayer::OnTechResearched=off
|
||||
hook.Game::ServerSystem::ProcessTurn=off
|
||||
hook.Game::ServerPlayer::ProcessTurn=off
|
||||
hook.Game::ServerSystem::GroupOutput=off
|
||||
hook.Game::ServerSystem::ComputeTotalOutput=off
|
||||
hook.Game::StrategyServer::MoveFleet=off
|
||||
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||
|
||||
hook.Game::StrategyHost::Autosave=trace
|
||||
hook.Game::StrategyServer::ProcessTurn=trace
|
||||
hook.Game::StrategyServer::OnAllCombatDone_Tail=trace
|
||||
hook.Game::StrategyServer::ApplyEncounterResult=trace
|
||||
hook.Game::StrategyServer::NodeLineDecay=trace
|
||||
hook.Game::StrategyServer::ProcessNodeSpaceTravel=trace
|
||||
hook.Game::EncounterDetect::AssignContacts=trace
|
||||
hook.Game::EncounterDetect::ProcessTeamRecord=trace
|
||||
|
||||
fpu.sample_turn=off
|
||||
fpu.sample_ticks=off
|
||||
|
||||
# The generator is 0x9cc bytes and every record carries it twice. Inlining it as hex would make
|
||||
# each record ~5 kB of state nobody reads.
|
||||
trace.inline_max=64
|
||||
trace.path=C:\SOTS\shim.trace.jsonl
|
||||
trace.flush=always
|
||||
probes=11
|
||||
|
||||
watch=on
|
||||
watch.mode=cont
|
||||
watch.out=C:\SOTS\shim.watch.txt
|
||||
Loading…
Add table
Reference in a new issue