Xenon 2

How it works · write-up

Level 4 tongue and Drone rendering

xenondoc/LEVEL4_SPRITE_RENDERING.md · 14 KB · updated 2026-10-05

Reproduction and root causes

Reported recording: E:/xenon_runs/level4-full-after-level3-20261003-r278.validation/level4-full-after-level3-20261003-r278.x2events, game frame 37337, Sprite AVI VBL 141808. Compare the original AVI at VBL 141810, compensating for its two-VBL display latency. Both AVIs are 640×400: multiply logical screen coordinates by two.

  • Tongue: follower #127245 uses update $50126, draw $4FE34, sprite $59AF2, and screen anchor (180,175). The leader uses $500D8; the tail uses $50152. $4FE34 tests object word +$54 and draws only when nonzero, through $0E24 → $10A2. $4FE40 is the flash variant, through $0F1A → $159C. The capture dispatch whitelist rejected these conditional wrappers, so no tongue quads were emitted despite the objects being observed.
  • Drone body: attachment $002C, object $03A046, identity #96156, update $454A, ordinary draw $10A2, sprite $27B2C, anchor (42,161). It already appears in both reported AVIs: the aligned logical crop (30,145,24,30) has zero differing pixels. The replay selection failure comes from unused interaction fields: collision bounds (138,-7,5,6) are off screen, while actual render bounds (35,154,15,15) surround the Drone. Its update procedure was also classified as unknown equipment.
  • Drone bullets: $454A emits into 80 eight-byte slots at $3CF5C, containing signed x,y,dx,dy; negative X marks an inactive slot. $6A1C updates them, retires collisions and out-of-bounds shots, and draws surviving points directly into the ST framebuffer. These are not MyObjectEntry objects and do not enter the object draw dispatcher or sprite atlas.

These are separate capture and inspection defects, not an object-count limit. The reported frame contains 64 captured objects, below the 256-object limit.

The Level 4 checkpoint RAM was checked against ReVa project /level4-live/mydumpat0. Its bytes match the routines above. Some inherited overlay function names/comments describe Level 3 code and must not be trusted without checking the live bytes.

Implemented repair

DrawCommandStream_OnInstructionFetch retains dispatcher observations for world state, but captures ordinary sprites at the executed object-blitter entries $10A2/$159C/$1912/$1CD6. This covers conditional wrappers automatically, preserves terrain-masked and flash drawing, emits nothing for hidden objects, and avoids capturing the same blit at both dispatcher and callee.

Drone point capture runs after actual framebuffer writes at $6AC8 or $6B56. It reads the touched pixels' final palette indices and emits short flat-color runs using synthetic IDs $FFF80000 | slot<<5 | run. The original OR/AND operations preserve some background planes, and the wide branch writes a few extra spill pixels; reproducing a generic solid 3×3 square would be inaccurate. No shader, atlas, event-format change or artificial world object is needed.

The C world classifies $454A as a friendly player attachment. Python changes are limited to symbols, tests and replay inspection: equipment names appear in the object table, scene clicks use captured render bounds, and attachment outlines use those bounds. Collision geometry remains separately inspectable. The Python autopilot was not changed or used for gameplay.

Recorded verification

Official Release rebuild of Hatari and the native replay DLL, followed by visible resident-C validation with both AVIs, fast-forward and checkpoints every 150 frames:

E:/xenon_runs/level4-sprite-tongue-drone-20261003-r280.validation/level4-sprite-tongue-drone-20261003-r280.x2events

Started from R278's checkpoints/f37317-periodic.sav. All 600 aligned frames, 37319–37918, were compared in the captured bounds of the tongue, Drone body and Drone point runs. Each category has zero differing pixels. There were 20,512 point runs, including 531 wide Drone draws. All 8,240 visible tongue observations were drawn; all 1,360 hidden observations remained hidden. Ordinary object sprites have no duplicate captures. Shield stayed 35 and lives 3.

Whole-frame differences remain at the existing side-cannon left-edge clipping and endpoint/body color overlap. The entire AVIs are therefore not claimed to be pixel-identical. Detailed counts: validation.json.

Fixed Sprite AVI, frame 37337

Aligned original frame

Repeat the comparison from the repository root:

python xenon_tools\compare_renderer_avis.py E:\xenon_runs\level4-sprite-tongue-drone-20261003-r280.validation\level4-sprite-tongue-drone-20261003-r280.x2events --stride 1 --top 4 --output work\level4-sprite-comparison

Shared-path regression recording: E:/xenon_runs/level2-sprite-blitter-regression-20261003-r281.validation/level2-sprite-blitter-regression-20261003-r281.x2events. All 6,565 ordinary draws over 164 frames match the former dispatcher contract for sprite ID, anchor-minus-origin coordinates and flash flag, with no missing or duplicate entries. All 358 terrain-masked worm observations are captured. Its AVI residuals include host control overlays, the previously documented terrain-mask top-edge arithmetic and one overlapping-shot pixel; this regression check does not claim whole-frame equality.

109 applicable native and replay tests passed, including Drone classification, visible-object selection and object-table naming. Three existing legacy checks were reported separately: Python/C handler parity (the frozen Python classifier differs), symbol-catalog parity (an older missing Level 2 diver symbol), and a legacy controller's no-export assertion (its Python Level 4 probe exports objects). Those checks do not exercise the rendering hooks and were not rewritten to hide their failures. Older AVIs retain their originally recorded omissions.

Pools and equipment coverage

The Drone pool is separate storage, not an extra 80 MyObjectEntry entries. The resident initializer $7406 initializes 158 general object entries of 98 bytes from $3927E, the player at $3921C, and the 80 Drone point slots at $3CF5C. Ordinary bullets/attachments/enemies share the general object allocator $2BE4. The 48 six-byte star records at $3D1DC are another known separate array, already captured in both vertical and perspective modes. This inventory does not prove that every direct framebuffer writer has been found; an unfamiliar draw-wrapper audit cannot discover a writer that never enters object dispatch.

An equipment rendering matrix should equip each attachment through its real installer in isolated test snapshots, exercise all supported power levels, both compatible wing slots, flashing/animation, screen edges and pool saturation. Check Forward/Double/Side Shot, Laser, Cannon, Drone, Rear Shot, Missile Launcher, Homing Missile, Flamer, Electro Ball, both Mine variants and Bomb; also test Dive/Nashwan transitions. Merely changing a slot code is insufficient: installers create an object, link lists and initialize private fields. Compare authentic and Sprite AVIs with the two-VBL correction. This exhaustive patched-loadout matrix has not yet been run. R278/R280 cover Double Shot, Cannon, Laser, Side Shot and Drone, not every loadout.

Unfamiliar draw audit and additional prepared sprites

Protocol 17 adds optional trailer $5843: count followed by 24-byte records level:u16, captured_draws:u16, object_id:u32, identity_id:u32, draw_proc:u32, update_proc:u32, sprite_id:u32, all big-endian. Decoders preserve recordings from versions 6–16. Python publishes these records as frame["unrecognized_draw_routines"]; C replay inspection exposes the same array inside its frame section, including for browser inspection.

An unfamiliar installed wrapper is sampled once per (level, draw_proc) per recording. Common resident routines with verified contracts are excluded. Capture counts come from the draw-command ring between dispatcher $3EE4 and return $3EE6. A nonzero count means that this particular invocation used supported capture; zero can mean a conditional hidden/no-op branch, and is not proof of missing rendering. Seen state is reset when a new recording begins; a skipped warm-up frame cannot consume its first sample. Bounds are 256 new samples/frame and 1,024 distinct pairs; capture flag bit 3 explicitly reports audit overflow. No audit metadata is collected during ordinary resident-C play without a recorder or frame subscriber.

Reusable audit (also inventories older recordings and equipment coverage):

python xenon_tools\audit_draw_routines.py RECORDING.x2events --output work\draw-audit.json

R282 recorded five distinct Level 4 wrappers, including the tongue, head and eyes, without duplicates or overflow. They were unfamiliar wrapper addresses but already used captured shared blitters.

ReVa additionally verified $51166 and flash $5113A (update $50F18): they replace A0 with SpriteData and call $0EA2 -> $10AE or $0F26 -> $15A8. Their prepared sprites were omitted even though later tile composites were captured. Shared thunk hooks now capture them using the active dispatcher's owner, preserving flash and draw order. Side-cannon endpoints retain their synthetic IDs and are excluded to prevent duplicate capture. The old $5085E fallback is used only outside an active dispatch. $50D8A and $51442 invoke already-supported tile/run-mask renderers.

Verification recording: E:/xenon_runs/level4-prepared-sprite-audit-20261003-r288.validation/level4-prepared-sprite-audit-20261003-r288.x2events. Frames 36018–36444: 427 records, three distinct unfamiliar routines, no duplicates/overflow and no reported damage. Prepared object sprites are present. Of 611 aligned sprite crops, 608 match exactly; three flash/overlap crops retain 20 differing physical pixels in total. Whole-frame identity is not claimed. Details: work/level4-prepared-r288-comparison.json.

R278 shop upgrade and boss aiming

R278 collected Side Shot power 1 during gameplay, then bought its next power for 1,000 cash at 38857, raising 1 -> 2 after the Level 4 boss. Installer $549A increments object +$42 up to maximum +$32 when the same attachment is owned. Spawner $6990 selects a larger sprite through +$42 and copies power into the projectile's damage word; $6086 adds the global $7AF4 bonus before the target's hit handler. The purchase therefore changes projectile size and damage, rather than buying an ineffective copy.

The mission already selects Side Shot for active eyes, and a mounted forward Laser/Cannon lane for the remaining head. All mounted weapons fire together; Drone hits during transit are incidental. There is no exact, general maximum DPS optimizer across every attachment and upgrade.

A real aiming mismatch exists for the right eye: at R278 frame 37337 its live inclusive hitbox is (182,56)..(202,76), while the fixed station Y=51–53 sends Side Shot at Y=48–50. The C tactic now enters the upper edge of the actual eye firing row after reaching its right-hand pocket. The established left refuge and upper transfer route are preserved. Applying the lower row to both sides, or directly to the crossing route, exposed the ship; those trial changes were removed.

Focused current-code run: E:/xenon_runs/level4-right-eye-focused-20261003-r287.validation/level4-right-eye-focused-20261003-r287.x2events. From R278 checkpoint f37317-periodic.sav, the right eye disappears at 37542, the head at 38129, and the run reaches the shop with shield 35 -> 35, lives 3 -> 3. Both AVIs are finalized. All 1,215 replayed live statuses match exactly. The right-eye death frame matches R280: the eye was killed during transit, so this run proves continuation without damage but does not demonstrate a DPS gain at the corrected final station.

The earlier checkpoint f36999-periodic.sav still exposes a left-eye stall. The original C Level 4 tactic was rebuilt for an identical-checkpoint control: E:/xenon_runs/level4-eye-old-tactic-baseline-20261003-r286.validation/level4-eye-old-tactic-baseline-20261003-r286.x2events. It takes directional hits at 37197, 37237, 37270, 37349, 37489, 37571, 37729, 37762 and 37798, then loses a life at 37815. The staged right-eye candidate R285 has the identical hit sequence before reaching its new aiming condition. The left eye remains at 18 health after it settles at Y=50 while the ship holds Y=40; its remaining firing alignment/resume problem is not fixed here. R283/R284/R285 are rejected trials, not clean validation runs.

Protocol checks: 21 pass; replay UI: 35 pass; C mission: 144 of 148 pass, including every Level 4 check. Four unchanged Level 2 expectation failures remain in the wider suite; no Python policy was edited to hide them. The additional C/Python audit decoding and truncation test passes, as do 13 native session tests. R288 matches all 426 live statuses in C replay.

Authoritative Web replay support

The Web worker uses the same C decoder through xenon_replay.wasm; it does not have a separate TypeScript frame decoder. Rebuild with the official hatari_dev.ps1 build-replay-wasm, then build-web after protocol changes. The new Draw routine audit card displays protocol-17 samples on their recorded frame, including level, identity, procedures, sprite and captured draw count. Audit overflow is reported in Diagnostics. No audit samples is not proof that every renderer is supported: samples are emitted once per routine and level.

node web/tests/replay-protocol.mjs <recording.x2events> tests the actual built Web WASM module. R288 passed all 427 frames and three samples, rejection of a truncated audit, and decoding after removing only the protocol-17 trailer. Both official Web builds and TypeScript checking passed on 2026-10-04.