Skip to content

native: throw the imp's fireball from the tic that reaches it - #480

Draft
MarcusKainth wants to merge 1 commit into
mainfrom
native/troop-fireball
Draft

native: throw the imp's fireball from the tic that reaches it#480
MarcusKainth wants to merge 1 commit into
mainfrom
native/troop-fireball

Conversation

@MarcusKainth

Copy link
Copy Markdown
Owner

What this changes, and why

A_TroopAttack's missile branch reached P_SpawnMissile and the tic said
it could not be produced. It now throws.

The strike builds a missile::throwing ask out of the attack's own
answer — the imp, its target, MT_TROOPSHOT, and the draws the tic and
the attack already made — and folds missile::spawn over it. What comes
back goes on the end of the mobj arrays the way a spawn does:
missile::born_column for every column but the two the caller assigns,
the next link order for m_linkseq, the slot the compaction leaves for
m_id, and one of each counter. The pointer the fireball carries back to
the imp is renumbered with every other pointer the compaction moves.

missile::spawn_fold is new, for the same reason inter::damage_fold is:
a map runs every function in its body once even on an empty list, and this
body is P_SpawnMissile with the move test under it.

Evidence

The oracle, and what it does not reach

The first divergence stays at gametic 169 and moves off prndindex.

$ clickdoom native diff 400 --probe refemu/reference_traces/demo3/probe.9a6a47d01119.tsv
# native diff elapsed=41.3s tics=377 tics/s=9.1
clickdoom: error: tic 169 mobj slot 265 m_x: 21639119 against the probe's 21353709

Every number the throw draws is now counted, so prndindex at 169 reads
194 against the probe's 194. What is left is the fireball's own thinker.

P_AddThinker appends to the thinker list and P_RunThinkers walks that
list to its end, so the engine runs a thing's own thinker on the tic it was
spawned. The probe's fireball at 169 has already taken one full
P_XYMovement step and one tic off its clock. This tic has not, because
the missile's own thinker is still refused. The arithmetic is exact:

imp at 169            x = 21781824      y = -9198912
fireball momentum         -285410           589940
spawn plus half step   21639119          -8903942   <- what this leaves
plus one full step     21353709          -8314002   <- what the probe holds

So the divergence cannot move past 169 until the missile's thinker runs,
which is the pull request after this one. That is the oracle this misses,
and the figure above is what it costs.

Gametic 169 also still says it could not be produced, but not for the
missile any more. Breaking now_unresolved into its terms shows
cw_crowded and tx_crowded at 169, tx_unrun and tz_unrun from 170
on. The last two are the missile's own thinker.

The parity fixture

sim_parity_live gains the fireball's row at 169: the list grows from 264
things to 265, and the last one's type, z, momentum, angle, target, state
and flags are read against the probe. Its x, y and tics are read as one
momentum step and one tic behind the probe's, which is the thinker this
does not run, and the prndindex check is what says the draws are right.

$ cargo test -p clickdoom-native --features clickhouse-tests --test sim_parity_live
test the_tic_matches_the_engine_where_the_fixture_reaches ... ok
test result: ok. 1 passed; 0 failed; finished in 46.32s

The seeded throw

sim_troop_live's fireball arm no longer reads a refusal. It reads a thing
appended to the list, its type, and the pointer it carries back.

$ cargo test -p clickdoom-native --features clickhouse-tests --test sim_troop_live
test a_tic_carries_the_imps_attack_through ... ok
test result: ok. 1 passed; 0 failed; finished in 131.82s

The seeded arms fail when the wiring breaks

Three breaks, each applied on its own and reverted.

$ break: the throw is gated on the claw instead
thread 'a_tic_carries_the_imps_attack_through' panicked at native/tests/sim_troop_live.rs:203:5:
assertion `left == right` failed: the claw is a branch this runs
  left: 1
 right: 0

$ break: the fireball forgets what threw it        # moved_slot(&value) -> moved_slot("0")
thread 'a_tic_carries_the_imps_attack_through' panicked at native/tests/sim_troop_live.rs:246:5:
assertion `left == right` failed: carrying a pointer back at the imp that threw it
  left: 0
 right: 116

$ break: the draws the throw makes are not counted
thread 'the_tic_matches_the_engine_where_the_fixture_reaches' panicked at sim_parity_live.rs:
assertion `left == right` failed: the tic draws what the engine draws
  left: 192
 right: 194

Cost

Measured on a throwaway ClickHouse 26.8.2.7 of this lane's own, started
from docker-compose.yml's digest with the same config.d and users.d,
against f2818a7 in the same session, three rounds interleaved, the
machine lock taken and given back around each measurement. The 60 tic
window is idle; the 240 tic window runs the chase, the shot and the
attack.

arm    window   tic (median)   analysis (median)   FunctionExecute/tic
main    60 tics     44.19 ms         27.60 s              13423
troop   60 tics     46.19 ms         30.41 s              13553
main   240 tics     36.06 ms         28.33 s              21074
troop  240 tics     36.96 ms         30.97 s              21323

The resident statement goes from 1,255,576 to 1,327,228 bytes.

The idle tic rises 2.0 ms by arm medians and 1.9 ms paired; the active tic
rises 0.9 ms. Both are over the 0.5 ms gate, and both are smaller than the
attack wiring's own. The idle gate miss is tracked as its own issue and
nothing here narrows where it sits.

The demo gate

Relative to main, in the same session.

main    tics/s=34.9  late=25
troop   tics/s=34.9  late=15

Suites

$ make lint
lint exit=0

$ cargo nextest run -p clickdoom-native --features clickhouse-tests
Summary [ 937.203s] 281 tests run: 281 passed, 0 skipped

$ make native-smoke
native-smoke exit=0

sql::sim::tick::tests::each_caller_of_the_move_test_holds_one counts the
move test's callers so nobody adds one by accident. The spawn's half-step
is a real new caller, so the count goes from four to five.

Invariants

None.

Spec impact

  • None. No contract in SPEC.md is touched

Checks

  • make gates. Not run whole. make lint, the whole of
    clickdoom-native's test set against a live server, and
    make native-smoke ran instead, all by exit code
  • make native-smoke, if native/ or driver/src/native/ changed
  • No AI attribution trailers in the commits

Anything else

The oracle is the part worth attention: the divergence stays at gametic
169 because the engine runs a newly spawned thing's own thinker on the tic
it was spawned, and this does not.

Written mostly by Claude Opus 5.

A_TroopAttack's missile branch reached P_SpawnMissile and the tic
refused it. The strike now builds a throwing ask from the attack's own
answer and folds missile::spawn over it, and the thing it makes goes on
the end of the mobj arrays the way a spawn does: born_column for every
column but the two the caller assigns, the next link order for
m_linkseq, the slot the compaction leaves for m_id, and one of each
counter.

missile::spawn_fold is new, for the same reason inter::damage_fold is:
a map runs every function in its body once even on an empty list, and
the body here is P_SpawnMissile and the move test under it.

The fireball goes at the end because P_AddThinker appends, and the
pointer it carries to the imp that threw it is renumbered with every
other pointer the compaction moves.
@github-actions github-actions Bot added the area: native Native mode: the tic simulation and renderer as SQL, and the WAD loader label Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: native Native mode: the tic simulation and renderer as SQL, and the WAD loader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant