Xenon 2

How it works · write-up

Plan: Capture & render the starfield on GPU

xenondoc/plan-starfield-early-draft.md · 8 KB · updated 2026-09-17

Context

You asked to investigate how the starfield is drawn (guessing it's drawn after the wall/background pass and before enemies, and that it's random-generator-driven), and whether it's feasible to reproduce in a shader. My first pass at this got two things wrong, corrected directly by you (who has been visualizing live write-traces to the frame buffer — more authoritative than my static disassembly reading):

  1. The whole frame is redrawn every frame, in this order: background → stars → masked sprites. The background pass wipes everything drawn on top of it in the previous frame (stars, enemies, ship, bullets), so stars genuinely get redrawn from scratch every frame too — there's no "persists across throttled frames" complexity to design around. My first draft invented a "sticky state" persistence mechanism that isn't needed at all.
  2. Each star is a single pixel, not an 8x8 glyph. Your write-trace observation: each star draw updates one line — 1 pixel tall, 16 pixels wide in terms of the write's addressing, but in practice only 1 of those 16 bits ends up set (the ST screen's bitplane-interleaved nature means a "16px-wide word write" is how you address a single pixel's worth of plane data, not a claim that 16 pixels are actually lit). My "8-scanline glyph" reading of maybe_background_starts_ FUN_00006f36's inner loop was a misread of what the MOVEP-based writes actually produce.

Both corrections make this easier to implement than my first draft, not harder: no persistence state to maintain, and a single-pixel draw is about as simple as a GPU quad gets.

What's still confirmed from the first pass

  • Z-order: confirmed correct — maybe_background_starts_FUN_00006f36 is called at 0x7a8a, right after the background/tile draw and before the object dispatch pass. Matches both your original guess and the corrected "background → stars → sprites" order above.
  • Not a runtime random generator: the function reads from a small fixed table at ST address 0x2c3a2 (10 entries × 32 bytes), gated by a 3-tier distance-accumulator throttle (10/100/1000 unit thresholds) that plausibly corresponds to the 3 (or more) parallax layers you described (farther/dimmer/slower vs closer/brighter/faster) — smaller threshold fires more often, larger fires rarely. This part of the earlier analysis isn't invalidated by your corrections, just the "what does one table entry visually produce" part was wrong.
  • A second, unrelated function, maybe_background_starts2_FUN_00001bd4, is a one-shot flag-triggered effect (not the continuous starfield) — still out of scope for this plan; possibly related to the pre-game/high-score "flying into the starfield" effect you mentioned.

Design

Capture via memory-write observation, not instruction-fetch + manual address decoding. Given you're already directly observing write-traces to the frame buffer for this function, and the exact MOVEP-based addressing turned out to be easy to misread from static disassembly alone, the more reliable and lower-effort approach is to hook into the write side rather than try to fully solve the address formula by hand:

  • Extend the existing per-write chokepoint (ScreenTrace_LogWrite, src/screentrace.c — already calls DrawCommandStream_OnMemoryWrite for buffer-swap detection, so this is a proven, existing hook point, not new infrastructure) with a new filter: only handle writes whose current PC falls within maybe_background_starts_FUN_00006f36's range (0x6f36-0x6ffd). PC during a write callback should be available the same way regs-based register access already works elsewhere in drawCommandStream.c (m68k_areg/m68k_dreg come from the same newcpu.h register state — needs confirming the exact accessor, e.g. m68k_getpc(), during implementation).
  • For each such write, decode (addr, size, value) into a screen pixel position + color: same byteOffset-to-x/y math already established for wall tiles (byteOffset = addr - drawBufferBase; x = ((byteOffset % 160) / 8) * 16; y = byteOffset / 160), refined to pixel (not 16px-block) granularity — for a given write, the specific bit(s) set within the written value, combined with which of the 4 interleaved bitplane byte lanes the write address falls on, determines exactly which single pixel and color index actually gets lit. This decode needs empirical verification against your write-trace tool's actual observed values (see Known risks).
  • Since stars redraw every single frame (confirmed above), this reuses DrawCommandStream exactly like every other format — no ring-buffer bypass, no persistent state table. Each frame's star writes just become more entries in that frame's normal capture, exactly like tiles/objects.

Rendering: a single lit star pixel is a degenerate case of the same quad machinery already used for everything else — either a true 1x1 px quad (cheapest, but likely too small to see clearly at typical display scaling) or a small fixed-size point-sprite quad (e.g. 2x2) centered on the captured position, colored from the captured palette index. No new atlas entries are strictly required if we treat this as "plot a flat-colored pixel," which sidesteps needing to decode the 0x2c3a2 table into the atlas at all — worth deciding once the write-decode confirms whether the table data really is just per-tier/per-position color selection or something more.

Known risks / verify empirically before implementing

  1. PC-filtering a memory-write hook is a new pattern for this project — every existing hook filters on instruction-fetch PC (objects, tiles, background-cursor-read), none currently filter writes by the executing PC. Need to confirm what's available in the write-callback context (ScreenTrace_LogWrite's parameters, plus whatever CPU-state accessor gives the current PC at write time) before assuming this is a drop-in extension of the existing pattern.
  2. Exact bit/color decode from a captured write is unconfirmed. Need a handful of real captured (addr, value) pairs (from your existing write-trace visualization, or from a first pass of the new hook logging raw values) to nail down which bit(s)/byte-lane combination produces the single lit pixel and what determines its color index, before finalizing the decode math.
  3. Tier-to-visual-layer mapping (3 code tiers vs however many parallax layers are actually visible) still isn't independently confirmed beyond the plausible threshold-ordering argument from the first pass.
  4. maybe_background_starts2_FUN_00001bd4 stays out of scope — flagged as a separate, later piece of work.

Verification

  1. Rebuild with the new write-observation hook added but only logging (not yet pushing draw entries) for a short play session; cross-check logged (addr, value, decoded x/y/color) against your own write-trace visualization for the same session to confirm the decode is right before wiring it into actual rendering.
  2. Once confirmed, wire into DrawCommandStream/the sprite-stream vertex builder; open "Sprite Stream" during gameplay with scrolling active.
  3. Confirm: multiple layers of single-pixel stars visible behind ship/enemies, in front of background/tiles, scrolling at different speeds with plausible brightness differences, no flicker (since they're genuinely redrawn every frame now, this should just fall out correctly).