From 3142b70aa565325834ed0cc7fdc7cdc3bf9e3c06 Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Tue, 22 Sep 2026 02:24:18 +0200 Subject: [PATCH] Gate virtual-bullet spawn on rack admission: +68% tick rate, selected gun unchanged The default rack is now Pattern-only (31c7c01), but membership filters SELECTION, not SPAWNING - so all 13 unselected guns still ran `predict` + `spawnBullets` every tick to feed fitness tables nobody reads. Measured waste: Tsetlin alone 0.98 ms/tick, KNN 0.30, plus 10 more. This generalises the gate TMPATTERN already had to every gun, behind `TR_VBULLET_ADMIT_ONLY` (default 1 = gate, 0 = old). OFFLINE COST (release build, 400 ticks, min of 2 reps, 13-gun rack): OFF 2.14 ms/tick (implied 467 ticks/s) ON 0.04 ms/tick (implied 27667 ticks/s) -> reclaimed 2.10 ms/tick, ~98% of the virtual-bullet cost. Tsetlin's 0.98 disappears, KNN's 0.30 disappears, only Pattern (0.017) survives. Note the absolute scale is lower than an earlier unoptimised measurement (~6.1 ms/t) because this is a -d:release build; the ON-vs-OFF DELTA is the robust result. LIVE TICK RATE (ONE frozen binary, 4 runs/arm x 4 rounds, all 8 concurrent, gate varied by env only): ON 146.2 ticks/s (142.3, 145.2, 147.9, 149.4) OFF 86.9 ticks/s ( 95.0, 41.4, 99.1, 112.1) NO OVERLAP: ON min 142.3 > OFF max 112.1. Excluding a game-outcome outlier in the OFF arm, OFF max is still 112.1. Outside the noise. **+68% tick rate**, which also makes every future A/B faster. Both arms still pay the fixed 8192-slot ring scan in tickBullets. SAFETY, verified not assumed: Pattern is admitted under the shipped rack, so its own fitness keeps accumulating and the SELECTED gun is unchanged - Pattern 100% in all 4 runs both arms, and Pattern was the ONLY gun with vShots>0 in the ON arm while all 13 had vShots>0 in the OFF arm. So admission is the correct predicate. Mid-round transitions are safe by construction: only predict/spawn are gated, while tickBullets still resolves every active bullet and the feedback case still calls the owning gun's onResult. A TOOLING BUG THIS CAUGHT, and a correction to the task's assumption: `acceptance_offline_vs_online.nim` IS affected (I had assumed it was not). It compares the live per-gun vShots against an offline replay that always spawns all guns, so under the default gate the online non-Pattern vShots are 0 while offline is ~400 - a guaranteed mismatch. Fixed by pinning `TR_VBULLET_ADMIT_ONLY=0` inside that test (same putEnv/defer pattern as TR_RECORD_WORLDSTATE), keeping 12/12. The test is about offline/online METRIC parity, so it needs every gun spawning. Unaffected (verified from source): audit_virtual_guns.nim, measure_cornering_guns.nim, sweep_tm_pattern.nim - all offline, none read GUN_STATS_PATH. Guards: test_vbullet_admit_gate 12 (new, pure), test_gun_harness 39, test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41, test_tfil_ring_weights 24, test_power_policy 26, test_ram_decision 28, test_rack_membership 48, test_selector_tiebreak 19, test_tm_pattern_registration 20, test_tm_pattern_learning 3, acceptance_offline_vs_online 12/12. ModularBot compiles. --- ModularBot_garage/src/ModularBot.nim | 91 +++++--- common_libs/gun_harness/virtual_bullets.nim | 18 ++ .../tests/acceptance_offline_vs_online.nim | 9 + .../tests/measure_vbullet_admit_gate.nim | 208 ++++++++++++++++++ common_libs/tests/test_vbullet_admit_gate.nim | 99 +++++++++ 5 files changed, 392 insertions(+), 33 deletions(-) create mode 100644 common_libs/tests/measure_vbullet_admit_gate.nim create mode 100644 common_libs/tests/test_vbullet_admit_gate.nim diff --git a/ModularBot_garage/src/ModularBot.nim b/ModularBot_garage/src/ModularBot.nim index 1bb66f2..ec09410 100644 --- a/ModularBot_garage/src/ModularBot.nim +++ b/ModularBot_garage/src/ModularBot.nim @@ -90,6 +90,18 @@ let DisabledGuns = s proc gunDisabled(id: int): bool {.inline.} = id in DisabledGuns +## ── virtual-bullet spawn gate (TR_VBULLET_ADMIT_ONLY) ────────────────────── +## Rack membership filters SELECTION; this knob extends it to SPAWNING. With it +## set (default 1) a gun the current rack does not admit is skipped ENTIRELY in +## the live aim block — no `predict()`, no `spawnBullets()` — so 13 unselected +## guns stop computing a fitness table nobody reads (~40-46% of the per-tick +## budget in the shipped onlyPattern rack). Set to 0 to restore the pre-change +## behaviour (every non-TMPATTERN gun spawns) on the SAME frozen binary. +## `onResult` feedback is NEVER gated (see the gated aim block in `run`). +let VBulletAdmitOnly* = + block: + let v = getEnv("TR_VBULLET_ADMIT_ONLY", "1").strip().toLowerAscii() + not (v == "0" or v == "false" or v == "no" or v == "off") ## ── Movement rack (runtime switch) ───────────────────────────────────────── ## `TR_MOVEMENT` selects the movement engine, mirroring the gun rack's ## env-driven construction. Both engines are always constructed, so the switch @@ -995,45 +1007,58 @@ method run*(bot: ModularBot) = var knnPreds: array[len(PowerBins), GunPrediction] var tmselPreds: array[len(PowerBins), GunPrediction] var tmpPreds: array[len(PowerBins), GunPrediction] + # ── TR_VBULLET_ADMIT_ONLY gate ────────────────────────────────────── + # Rack membership used to filter only SELECTION, so every unselected gun + # still ran predict()+spawnBullets() each tick to feed a fitness table the + # selector would never read (~40-46% of the per-tick budget). `admit` + # skips BOTH calls for a gun the current rack does not admit. It is + # recomputed each tick because the rack mode can change mid-round (a melee + # thinning to 1v1). Bullets already in flight are NOT cancelled: they are + # resolved by `tickBullets` and the feedback `case` below still calls the + # owning gun's onResult, so attribution survives a mid-round rack change. + # TMPATTERN keeps its own admission gate even when the knob is 0, so + # TR_VBULLET_ADMIT_ONLY=0 reproduces the exact pre-change rack. + var admit: array[15, bool] + for gi in 0..<15: + admit[gi] = vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership, + VBulletAdmitOnly or gi == TmPatternId) for i in 0..= Warmup and si < Warmup + Measure + let t0 = getMonoTime() + for gi in 0..= Warmup and si < Warmup + Measure + for gi in 0.. 0: acc.pred[gi] /= counts[gi].float + if onCalls[gi] > 0: acc.onRes[gi] /= onCalls[gi].float + result = acc + +proc main() = + randomize(1) + let fx = loadFixtures() + let states = buildStates(fx, MaxTicks) + echo fmt"fixtures={fx.len} ticks={states[0].len} warmup={Warmup} measured={Measure}" + echo "shipped rack: onlyPattern (DefaultRackMembership); predicate vb.vBulletAdmitted" + + # Two reps, take the min (least noise). + var offPs, offRes, onPs, onRes: int64 + var offSpawns, onSpawns, ticksRun: int + for rep in 0..<2: + let off = runArm(states, gateOn = false) + let on = runArm(states, gateOn = true) + if rep == 0: + offPs = off.psNs; offRes = off.resNs; offSpawns = off.spawns + onPs = on.psNs; onRes = on.resNs; onSpawns = on.spawns + ticksRun = off.ticks + else: + offPs = min(offPs, off.psNs); offRes = min(offRes, off.resNs) + onPs = min(onPs, on.psNs); onRes = min(onRes, on.resNs) + offSpawns = min(offSpawns, off.spawns); onSpawns = min(onSpawns, on.spawns) + + let n = max(1, ticksRun).float + let offPSms = offPs.float / 1.0e6 / n # ms/tick + let offRSms = offRes.float / 1.0e6 / n + let onPSms = onPs.float / 1.0e6 / n + let onRSms = onRes.float / 1.0e6 / n + let offTot = offPSms + offRSms + let onTot = onPSms + onRSms + + echo "" + echo "=================== PER-TICK COST: gate ON vs OFF (13-gun live rack) ===================" + echo "arm predict+spawn ms/t resolve+onResult ms/t total ms/t ticks/s spawns/t" + echo fmt"OFF {offPSms:>20.2f} {offRSms:>22.2f} {offTot:>12.2f} {1000.0/offTot:>7.0f} {offSpawns.float/n:>10.1f}" + echo fmt"ON {onPSms:>20.2f} {onRSms:>22.2f} {onTot:>12.2f} {1000.0/onTot:>7.0f} {onSpawns.float/n:>10.1f}" + echo fmt"reclaimed = {offTot - onTot:.2f} ms/tick ({100.0*(offTot-onTot)/offTot:.0f}% of the gate-OFF total)" + echo fmt"implied ticks/s: OFF {1000.0/offTot:.0f} -> ON {1000.0/onTot:.0f} (x{offTot/onTot:.2f})" + echo "(note: tickBullets also scans the fixed 8192-slot ring each tick, present in BOTH arms)" + + let pg = perGunCosts(states) + echo "" + echo "=================== PER-GUN COST (full rack, names what disappears) ===================" + echo "gun predict+spawn ms/t onResult ms/t gateON" + var sumPred, sumOn: float + for gi in 0..24.3f}{pg.onRes[gi]/1.0e6:>18.3f}{($admitted):>10}" + echo fmt"TOTAL {sumPred/1.0e6:>24.3f}{sumOn/1.0e6:>18.3f}" + echo "" + +when isMainModule: + main() diff --git a/common_libs/tests/test_vbullet_admit_gate.nim b/common_libs/tests/test_vbullet_admit_gate.nim new file mode 100644 index 0000000..e21015c --- /dev/null +++ b/common_libs/tests/test_vbullet_admit_gate.nim @@ -0,0 +1,99 @@ +## Pure unit guard for the `TR_VBULLET_ADMIT_ONLY` spawn-gate predicate. +## +## No Java, no battle, no virtual bullets: `vBulletAdmitted` is a pure function of +## (gunId, rack mode, membership table, admitOnly) over the SHIPPED +## `DefaultRackMembership`, so the live gate can be pinned without running the +## bot. Covers: +## * the shipped default rack admits Pattern (5) and nothing else; +## * `admitOnly=false` (TR_VBULLET_ADMIT_ONLY=0) restores "spawn for every gun"; +## * mode-restricted membership (`rmOnly1v1` / `rmOnlyMelee`) flips with mode; +## * an empty membership table filters nothing (pre-change callers); +## * out-of-range ids are admitted, negative ids are not; +## * the TMPATTERN special case: forcing `admitOnly=true` keeps gun 14 gated +## even when the knob is off — the exact composition the live loop uses. +## +## Run with plain: +## nim c -r common_libs/tests/test_vbullet_admit_gate.nim + +import gun_harness/virtual_bullets +import gun_harness/selector + +var failures = 0 +proc check(name: string, ok: bool) = + if ok: echo "PASS: ", name + else: echo "FAIL: ", name; inc failures + +proc testShippedDefaultRack() = + ## The shipped onlyPattern table: Pattern (5) admitted in both modes, every + ## other registered gun (0..14) off. + check "default: Pattern admitted in 1v1", + vBulletAdmitted(5, rm1v1, DefaultRackMembership, true) + check "default: Pattern admitted in melee", + vBulletAdmitted(5, rmMelee, DefaultRackMembership, true) + var allOthersOff = true + for gi in 0.. 0: + echo "\n", failures, " check(s) FAILED" + quit(1) +echo "\nAll vbullet-admit-gate checks passed."