From a19d0eb73bfb43f7c9444f55cc4f08cb9d1b7bf1 Mon Sep 17 00:00:00 2001 From: soul Date: Thu, 22 Oct 2026 20:17:23 +0200 Subject: [PATCH] Arena: sim-rate map and target section 7 (sim rate cut, step part of A-pure) Co-Authored-By: Claude Sonnet 5.5 --- docs/aetheria-arena-sim-rate-map.md | 200 ++++++++++++++++++++++++++++ docs/aetheria-arena-target.md | 16 ++- 2 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 docs/aetheria-arena-sim-rate-map.md diff --git a/docs/aetheria-arena-sim-rate-map.md b/docs/aetheria-arena-sim-rate-map.md new file mode 100644 index 000000000..11646654a --- /dev/null +++ b/docs/aetheria-arena-sim-rate-map.md @@ -0,0 +1,200 @@ +# Aetheria arena, cut sim-rate: one game tick, lowered for game and arena alike + +Date: 2026-10-22. Status: map, drafted by Imagination for Self to commit beside `docs/aetheria-arena-target.md`. +Body pinned at `GameCult/Aetheria` origin/master `0267a191` (view-seam merged). Every claim below was probed on that tree. +Spec: `aetheria-arena:cut_spec:cut-sim-rate.r1`. Question: `aetheria-arena:question:sim-tick-rate`. + +## 1. What the operator said + +On 2026-10-21, while we were talking about Colosseum throughput (session transcript `53e9c70e`, lines 4811-4879): + +- "So we can expect 160x realtime for the Colosseum? That's not bad, we can gather a few thousand matches per hour per + thread on Yggdrasil, then?" +- "I think we can fudge the time step just a little, as long as there's enough resolution for all the relevant decisions" +- "Projectiles are roll based, their flight is pure presentation, I made that decision partly with this in mind" +- "Physics definitely doesn't need 60, not at the pace we're setting. I don't think it even needs 30." +- Answering Self's proposal (lower the whole game's step, chosen by a timer check, an equivalence test and her playtest, + landing before the runner): "Go for it." + +That is ruling `aetheria-arena:ruling:lower-game-sim-rate`. Her words decide two things: physics needs less than 60 Hz, +and probably less than 30. The proposal she approved decides the rest: the game's own rate is lowered (`A-one-sim` holds +unchanged), and the rate is chosen by the timer check, the equivalence test and her playtest. + +Rulings already in force on the same clock (2026-10-07, campaign `aetheria-release`): +- `sim-speed-knob`: the sim's own clock owns pace; Unity's time scale is not touched. +- `sim-speed-surface`: a design speed in game data times the player's accessibility slow-down. +- `sim-speed-tenth`: the design speed will be about a tenth of today's; the tick rate stays hidden; the player's ship may be + extrapolated. +- `tick-rate-follows-design-speed`: the step follows the design speed, so the real-time tick rate is a chosen constant. + "6hz is the kind of rate players notice. But we definitely don't *need* 60." +- `tick-30hz-floor-half`: "30 Hz with 0.5 floor". TickRate is 30 Hz at design speed. The player's factor ranges from 0.5 to 1, + so the slowest tick is 15 Hz. + +The design-speed and accessibility cut (`cut-sim-speed-scale.r2`) was withdrawn under `map-only-what-runs`. So on master the +game still runs at design speed 1 and 60 ticks per real second. The 30 Hz in `tick-30hz-floor-half` never landed. Her +2026-10-21 words ("I don't think it even needs 30") go below it, which is the fork in section 6. + +## 2. Body facts + +### Where the rate lives + +- **Two hidden 60s.** `ActionGameManager.Clock = new SimClock(1f / 60f)` (`ActionGameManager.cs:251`) is the only clock + that steps the game. `SimClock`'s constructor defaults `stepsPerRealSecond = 60` (`SimClock.cs:21`). The two are + independent: changing the step to 1/20 while leaving the default would run 60 steps of 1/20 s per real second, which + makes the game three times faster. The 60 is decided twice, by two writers that do not know about each other. +- **ServerShared has no game rate.** `Zone.Update(float deltaTime)` (`Zone.cs:356`) steps by whatever it is given. Every + sim rule integrates the `dt` it is handed. Nothing in ServerShared knows the game's step. +- **The headless drivers choose their own step.** There is no runner yet. The tests that pin game histories step with + literals: the two-AI digest (`DeterminismLedgerTests.cs:217`, `arena.Update(1f / 60f)`, 3600 steps) and the Duel history + (`SimClockTests.OneHistoryTwice`, `ClockFrame = 1f / 60f`, `:134`). The tests contain 64 `1f / 60` literals, 31 of them + `Update(1f / 60f)` calls. Most are physics fixtures that need some small step, not the game's step. +- **Unity's fixed timestep is not the sim.** No `FixedUpdate` or `Rigidbody` exists in `Assets/Scripts`. The sim steps + only through `Clock.Advance(Time.deltaTime, ...)` (`ActionGameManager.cs:1333`). `ProjectSettings/TimeManager.asset` + decides nothing about the sim and is left alone. + +### What assumes 60 Hz + +The question is not whether a rule mentions 60. It is whether the rule's per-second meaning depends on the step. + +- **Timers that drop their overshoot.** These rules restart a normalized countdown at 1 and discard what the last step + overshot: + - `InstantWeapon.Execute` (`InstantWeapon.cs:227-265`): the cooldown ends when `_cooldown < 0`. The next burst starts + only when a pilot triggers it again on a later step. + - `Cooldown.cs:51,60`. + - `Sensor` ping cooldown (`Sensor.cs:144-148,183`). + - `ConstantWeapon` ammo interval (`ConstantWeapon.cs:148-151`). + + For the cooldown this works out to `floor(C/dt) + 1` steps per cycle, not `C/dt`. A 0.25 s cooldown fires every 16 + steps at 60 Hz (0.267 s, 6.7% slow) and every 6 steps at 20 Hz (0.30 s, 20% slow). Damage per second is therefore a + function of the tick rate today, at 60 Hz as well. This is the rule that would make an equivalence test fail for the + wrong reason. +- **Timers that already carry.** Two rules keep their overshoot, so their per-second rates survive any step: + - the beam's roll cadence (`ConstantWeapon.cs:185-196`, `while (_beamTimer >= interval) _beamTimer -= interval`); + - the burst's round cadence (`InstantWeapon.cs:243-270`, several rounds in one step if the interval is shorter). + + Transfer-verb's give budget (`CargoTransfer.cs:80-94` on its branch) carries too. +- **Passive sensing is a forward-Euler decay.** `Sensor.Execute` applies `next = saturate(previous + G dt)` and then + `next *= 1 - TargetInfoDecay * dt` (`Sensor.cs:225-231`). Its fixed point is `G (1 - k dt) / k`, not the `G / k` that + `Entity.PassiveInfo` documents and returns (`Entity.cs:518-532`). The bias is `k dt`: detection ranges shrink as the + step grows. The exact form, `x e^{-k dt} + (G/k)(1 - e^{-k dt})`, settles at `G / k` at every step. A ping is an impulse + with no `dt` (`Sensor.cs:251-254`) and is step-free. +- **Rules that are step-free already.** + - Drag is `decay(v, drag, delta)` (`Ship.cs:132`). + - Shots are rolls: commit and arrival are sim times checked each step (`FireControl.Step`, `CommitHorizon = .5 s`), so a + resolution lands up to one step late, never differently. + - Tunnelling does not exist for rounds (ruling `projectile-flight-is-presentation`). + - One-tick notifications (paints, ping crossings, `IncomingPaints`) are defined per tick and read per tick + (`ActionGameManager.cs:1336-1340`). +- **Integration error that remains.** Ship motion is stepped explicitly. At a larger step, a hard-turning ship's arc and the + in-arc and in-range test at commit move by O(dt). This is the residual her "fudge" admits. The equivalence test measures + it. +- **Hull contact.** `Contacts.Step` tests overlap after every move. A closing speed times the step that exceeds the + smallest hull's radius can pass through. The cut checks the catalog's fastest ship against its smallest hull at the + chosen step. + +### How the render covers the step + +`DrawAhead` (`DrawAhead.cs:13-19,114-139`) draws every moving thing at `T = Zone.ExactTime + Clock.Lead`: +- closed-form poses (orbits, belts, the field) at T; +- integrated poses (ships, bodies) as the last step plus step velocity times `Lead`; +- the ship's heading turned by `TurnRate * Lead`. + +This is extrapolation (dead reckoning), not interpolation. It adds no latency, and presenters store no previous state, by +rule. At 20 Hz, `Lead` reaches 50 ms. The visible cost is a snap whenever velocity or turn rate changes between steps: +about `Δω * Lead` in heading (a full turn-rate reversal of 3 rad/s gives 0.15 rad at 20 Hz, against 0.05 at 60). Positions +move by `½ a Lead²`, which is negligible. + +Input reaches the sim through `PilotCommand`. Held fields persist, and one-shots queue in order until the next step applies +them (`Pilot.cs:43-102`). A tap shorter than a step still fires, because a pull and then a release in one step both apply, +in order. The step costs the player up to one step of input lag: 33 ms at 30 Hz, 50 ms at 20 Hz. + +### What the digest pins + +`TwoAiDigestIsPinned` pins one SHA-256 over 3600 per-step `Snapshot`s of the two-AI fight at `1/60`, for host class +`linux-x64` (`DeterminismLedgerTests.cs:31-34,186-249`). It has been repinned twice, each for a stated cause. This cut +repins it once, for two causes landing together: the game step (the history has a different number of steps of a +different length), and the carried timers and exact sensor decay (cadence and detection change at any step). It still +pins the same simulated length: 60 s, which is `60 * TickRate` steps. `OneHistoryTwice` and `TwoAiFightIsOneHistory` +compare two runs and need no pin. + +### Measured on Yggdrasil + +**Not measured yet.** +- **The probe is ready.** `SimRateProbe.cs` sits beside this map. It runs a catalog timer survey and the two-AI fight over + 120 simulated seconds at 60, 30, 20, 15 and 10 Hz, three reps each after a warm-up, and reports the best wall time, + shots, hits, first hull loss, first death and survivors. +- **Why it has not run.** It waited about an hour for a Yggdrasil slot. Then three runs failed in the fixture: the test + catalog now loads the first-party mod `aetheria.nakovalnya`, whose `ship.cc` and `ship.glb` are LFS objects the old + probe recipe did not resolve. With every pointer under `GameData` resolved, one is still missing. +- **The recipe to use.** transfer-verb's job script resolves LFS for current master + (`F:\Projects\eureka-handoff-aux-2026-10-19\imag-nak-r2\job.sh`). +- **What the earlier probe showed.** At d52b50f9 the Duel ran about 9,700 steps/s at 60 Hz, mostly with one ship alive. +- **Expectation, not a measurement.** The cost per step does not depend on dt, so cost per simulated second scales with + the tick rate: 2x at 30, 3x at 20, 4x at 15. + +## 3. Prior art + +- **Glenn Fiedler, "Fix Your Timestep!" (2004).** Use a fixed `dt`, an accumulator fed by real time, a clamp against the + spiral of death, and then draw either by interpolating the previous and current states (smooth, one step of added + latency) or by extrapolating. `SimClock` already is the accumulator and the clamp (`MaxFrameSeconds = .1`), and + `DrawAhead` is the extrapolating branch. +- **Supreme Commander (2007)** runs its sim at 10 ticks per second and draws interpolated, for fleets of hundreds. It is + the closest precedent for a large-unit, non-twitch game at a low tick. +- **StarCraft II** runs 16 game loops per sim second (22.4 per real second at Faster), with deterministic lockstep and + interpolated rendering. +- **Factorio** is fixed at 60 UPS, with every timer counted in ticks and lockstep multiplayer, and no decoupling of sim and + render rate. It is the counter-example: it keeps 60 because its belts and inserters are tick-quantised content. Ours are + per-second content. +- **EVE Online** runs its Destiny ball physics on a 1 Hz server tick, and clients interpolate and predict. It shows that + tactical space combat at a slow pace tolerates very coarse ticks when the player commands intent rather than twitch. +- **X4** steps the sectors the player is in at frame rate and the others at a much coarser, abstracted rate. That is the + living-world campaign's concern, not this cut's. + +The lesson that carries over: content authored in seconds survives a step change only if every timer and integrator is +step-exact. Fiedler's accumulator gives that for the clock, and section 2's two families (timers that drop overshoot, +Euler decay) are the places where Aetheria does not have it yet. + +## 4. Authority map + +- **Owner.** `SimClock` owns the game's tick: `SimClock.TickRate` (steps per real second at design speed 1) and + `SimClock.GameStep = 1f / TickRate` (sim seconds per step). `SimClock.ForGame()` builds the game's clock from both. +- **Inputs.** None at run time. It is a code constant, so it is part of the code revision in `A-pure`'s tuple. +- **Outputs.** The `dt` every game-rate driver hands to `Zone.Update`, and `Lead`. +- **Derived.** + - `ActionGameManager.Clock` is `SimClock.ForGame()`. + - The digest, the Duel history and later the runner step by `SimClock.GameStep`. + - A timer's cadence and a sensor's settling level are functions of seconds, not of steps. +- **Demoted.** + - `SimClock`'s `stepsPerRealSecond` default is no longer an owner. The parameter becomes required, and the game's value is + derived from `TickRate`. + - `1f / 60f` in `ActionGameManager` is no longer an owner; it is derived from `SimClock.GameStep`. + - Physics fixtures may keep a literal step of their own, because they test physics, not the game's history. +- **Forbidden writers.** + - Any `new SimClock(` in `Assets/Scripts` other than `ForGame`. + - Any literal step in a driver of a pinned game history. + - A timer that resets to 1 and discards its overshoot. + - A per-step decay written as `1 - k * dt`. +- **Shared paths.** The player's game, the digest and Duel histories, and the future runner all step `Zone.Update` by + `SimClock.GameStep`. The design-speed cut, when re-mapped, sets `Step = DesignSpeed / TickRate` inside `ForGame` and + touches nothing else. +- **Deletion line.** The default `= 60` and the `1f / 60f` in `ActionGameManager` go first. The overshoot-dropping resets go + before the rate changes, so the rate change is measured on step-exact rules. + +## 5. Not this cut + +- **Brains deciding below the physics step.** The ruling allows it, and `PilotCommand`'s held fields already make it + possible. No brain-rate machinery lands here. +- **Design speed and the player's slow-down** (`sim-speed-scale`, withdrawn). `ForGame` is the one place where it will + land. +- **Render interpolation.** Extrapolation is ruled (`sim-speed-tenth`, DrawAhead's no-stored-state rule). Her playtest is + where a heading snap would show. If it does, interpolating the player's ship only is the follow-up, at one step of + latency. +- **Unity's `TimeManager` fixed timestep**, which the sim does not read. + +## 6. The fork + +`aetheria-arena:question:sim-tick-rate`: which tick rate Hands lands for her playtest, given `tick-30hz-floor-half`. +Options and recommendation are in the question. In short: +- 30 Hz keeps the in-force ruling and halves the cost. +- 20 Hz follows her 2026-10-21 words and costs a third, but at the 0.5 accessibility floor the real tick is 10 Hz. +- 15 Hz is a quarter of the cost and 7.5 Hz at the floor, near the 6 Hz she called noticeable. diff --git a/docs/aetheria-arena-target.md b/docs/aetheria-arena-target.md index d6f344501..bbd027a2a 100644 --- a/docs/aetheria-arena-target.md +++ b/docs/aetheria-arena-target.md @@ -85,7 +85,8 @@ Harness: - `A-pure`: A match is a pure function of (code revision, catalog revision, scenario id, match seed, brain ids per side, host determinism class). The same inputs give the same per-tick state digest, in any process, run alone or - in a batch. + in a batch. The step is part of the code revision: `SimClock.GameStep`, never a + driver's own literal. - `A-no-clock`: No input to a match comes from the wall clock, process identity, hash-code ordering of reference types, or a stream another subsystem advances. Every draw a match reaches comes from `SimulationDice` (or a successor owner) keyed by a structured seed. A missing seed is refused, never defaulted to the clock. @@ -169,10 +170,17 @@ No cell is empty. The Scenario row exposes a gap: `Name` is today's only identit 3. **View seam.** Add a `PilotView` built from sensors and book. Brains lose their live `Ship`, `Zone` and `ItemManager` references. `Flight` and `Minion` become `DoctrineBrain` behind an `IBrain` registry. Aiming moves to the view if `observation-fidelity` rules for it. -4. **Replay.** The recorder writes inputs, commands and digests every N ticks as CultCache. The replayer reproduces +4. **Sim rate** (ruling `lower-game-sim-rate`, map `docs/aetheria-arena-sim-rate-map.md`, spec `cut-sim-rate.r1`). + - One game tick, `SimClock.TickRate`, lowered for game and arena alike, so `A-one-sim` holds unchanged. + - Before it, timers carry their overshoot and passive sensing decays exactly, so content authored in seconds means the + same thing at any tick. + - The two-AI digest is repinned once, at the game step, for those two causes. + - It lands before replay and the runner, so recorded digests and measured throughput are at the real rate. + - The rate is the one question `sim-tick-rate` decides, then the equivalence test, then her playtest. +5. **Replay.** The recorder writes inputs, commands and digests every N ticks as CultCache. The replayer reproduces them and names the first divergent tick. Replay tests read an existing match. -5. **Runner.** `AetherDb arena`: a scenario, two brain ids and a seed range produce results and replays in a given +6. **Runner.** `AetherDb arena`: a scenario, two brain ids and a seed range produce results and replays in a given directory. It runs in Yggdrasil's dotnet container, with two-ship throughput measured and recorded. -6. **First tournament.** Round-robin over two or more brains (`DoctrineBrain` plus one variant) and the combat +7. **First tournament.** Round-robin over two or more brains (`DoctrineBrain` plus one variant) and the combat scenarios, with enough seeds per pairing. Ratings are derived with confidence intervals, and the results document is ready to feed `aetheria-matchups`.