pcunningham.works
Stars, Again: Real Perspective in RG6
Two days ago I posted a starfield. It looked fine. Then some people who really know the CoCo told me what it actually was: a zoom, not a flight. And the 60 fps on that page? When we finally measured it, it was 10.3.
So we rebuilt it.
Left, the original at 10 fps. Right, the rebuild, 24 stars at 60 fps. Both captured from XRoar emulating a stock 32K Tandy Color Computer with the original MC6847.
What makes stars look real?
Is it the maths, the perspective, or the frame rate? All three, but they do different jobs. Perspective is the look. The maths is what lets a 1980 CPU afford it. The frame rate is what keeps you believing it.
Perspective: crawl, then rush
Fly through a real starfield and a distant star barely moves for most of its approach, then streaks past you in the last moment. That is 1/z: almost nothing for a long time, then everything at once.
The old version gave each star a velocity that grew 3% a frame. Exponential growth looks the same at every scale, so every star did the same thing all the way out, and your eye reads that as the picture getting bigger, not as you moving.
The maths: 1/z without dividing
The 6809 has no divide instruction, and doing it in software for every star is out of the question. The trick, from Jonathan David Gilbert, is to store p = 1/z instead of z, and step it:
p = p + p * p
That is the exact step one unit closer, with the tiny terms dropped. The error only grows when the star is about to pass the camera anyway. Two multiplies, no division, and the screen position is just the star's fixed (X, Y) times p.
Two bonuses fall out. p is a 16-bit fraction, so when a star reaches the camera, p hits 1 and the CPU's carry flag goes up: the "respawn this star" test costs nothing. And one catch: p × p has to be rounded, not truncated, or distant stars round down to no movement at all and freeze forever. The fix is one instruction.
The frame rate: honest this time
Every number here comes from a cycle-exact XRoar trace, not a guess. Getting from 10 fps to 60 took four changes:
- Erase with one AND. Each star remembers the video address and bit mask it drew, so erasing is a single instruction instead of three subroutine calls.
- Most stars did not move. About half the time a far star lands on the same pixel it already occupies, so it just gets re-plotted.
- The joystick was eating half the frame. Reading it through the shared Forth library cost about 8,000 CPU cycles a frame. In assembly it is a few hundred.
- Take turns. Even then, a star costs about 490 cycles and a frame has 14,917. So each frame moves the next 24 stars, and a star that waited takes several steps at once.
That last one creates a choice, and the title screen lets you make it:
| Key | Stars | Each star moves | Measured |
|---|---|---|---|
| 1 | 24 | every frame | 59.9 fps |
| 2 | 48 | every other frame | 59.9 fps |
| 3 | 64 | as fast as it can | 26.5 fps |
| 4 | 200 | every tenth frame | 59.9 fps |
Watch all four and the real answer to the frame rate question shows up. What matters is not how often the screen refreshes, it is how often each star moves. Option 4 has the most stars and the same 60 fps screen as option 1, and it is the least convincing, because every star only moves six times a second.
Option 4 first tried 240 stars and measured 53.5 fps. Ten steps at a time almost never leaves a star on the pixel it was on, so nearly every update is a full redraw. Dropping to 200 brought it back to 59.9.
We also tried being clever: move near stars every frame and let far ones take turns. It hit 60 fps and looked wrong. A star visibly changed character the moment it switched from slow updates to fast ones. Consistent motion beats fast motion, so every star in an option moves at the same rate.
All four options in 74 seconds: option 1 at 0:05, option 2 at 0:21, option 3 at 0:38 and option 4 at 0:59.
- ModeRG6, CSS=1
- Grid256 x 192
- Colours3, artifact
- Stars24 to 200
- Best rate59.9 fps
- App size1,836 bytes
Steering
The left joystick moves the vanishing point. Only new stars launch from where you are pointing; the ones already in flight keep the heading they were born with. So the old stream carries on past you while the new one opens up ahead, which is what turning looks like, rather than the whole sky swinging round at once.
Option 4, 200 stars at 60 fps, steered with the left joystick.
The colours are still free, from NTSC artifacting on a one-bit-per-pixel screen. The original page explains how.
The source
The .bin runs as it is: LOADM it from Disk BASIC, or xroar -run stars-perspective.bin. The .fs is 6809 Forth and assembly for the cross-compiler in ugufru/coco; keep the three library files in a lib/ directory beside it. The rounded square in the depth step is commented bit by bit.
Show stars-perspective.fs (660 lines)
\ src/stars/stars.fs: Perspective warp starfield in RG6
\
\ Up to 200 stars fly at the camera through a 3D field. Each holds a
\ fixed (X, Y) and a projection coefficient p = 1/z, and is drawn at
\ centre + 128 * (X, Y) * p. Stars crawl while far away and rush past
\ in the last few frames, which is what perspective actually looks like.
\
\ Distant stars are born ALL OVER the screen, not just at the centre.
\ The field is wide enough that its far cross-section fills the view,
\ so a new star can appear anywhere and streams outward from there.
\
\ RG6: 256x192 SQUARE pixels, 1 bit per pixel, 6144 bytes at $0400.
\ Single buffer, erase-then-draw per star.
\
\ ── Projection without division (issue 66) ────────────────────────
\ Stepping z -> z-1 takes p = 1/z to 1/(z-1) = p/(1-p), which needs a
\ divide. p' = p * (1 + p) = p + p^2 is the same series cut after two
\ terms, erring by order p^3. Idea from Jonathan David Gilbert.
\
\ p is a 16-bit unsigned fraction, so p >= 1 is simply the CARRY out
\ of p + p^2, and that carry is the respawn test. p^2 must be ROUNDED,
\ not truncated: truncation makes far stars crawl, and below 256/65536
\ it rounds p^2 to zero and freezes the star forever. See
\ projection_sim.py for the numbers behind that decision.
\
\ ── Four ways to spend the frame (issue 69) ───────────────────────
\ Updating 64 stars every frame would need ~230 cycles a star, and one
\ costs ~500. Each frame therefore updates the next PER-FRAME stars,
\ round robin over the stars in use, and each update takes as many
\ depth steps as frames have passed, so every star moves at the same
\ rate and flight time in frames is the same in every option. Giving
\ near stars a faster rate than far ones was tried and rejected: a star
\ visibly changed character as it crossed from one rate to the other.
\ The title screen picks the trade:
\
\ 1 24 stars, 24 a frame, 1 step every star at 60 Hz
\ 2 48 stars, 24 a frame, 2 steps every star at 30 Hz
\ 3 64 stars, 64 a frame, 1 step free-running, about 26 Hz
\ 4 200 stars, 20 a frame, 10 steps every star at 6 Hz
\
\ Option 3 overruns the frame on purpose: vsync finds its flag already
\ set and returns at once, which is how the demo ran before issue 69.
\
\ ── Why RG6 and not SG4 ───────────────────────────────────────────
\ This demo was attempted once before (38f4c6e) on a 64x32 SG4 grid
\ and reverted (0778b95) for "visible oscillation and zigzag". 64x32
\ is simply too coarse for a starfield: a slow star sits in one cell
\ for a dozen frames and then jumps, and there is no room for the
\ near/far spread that sells the effect. 256x192 gives 48 times the
\ addressable points, square pixels, and a black background.
\
\ The LEFT joystick steers the vanishing point. Only new stars are
\ launched from the moved centre; the ones already in flight keep
\ their birth centre, which is exactly how a real course change looks.
\
\ 1 to 4 on the title screen pick an option, SPACE a random one. In
\ flight SPACE returns to the title. There is no exit.
\ ── Libraries ─────────────────────────────────────────────────────
INCLUDE ../../../coco/lib/screen.fs
INCLUDE ../../../coco/lib/rng.fs
INCLUDE ../../../coco/lib/vdg.fs
\ ── Memory map ────────────────────────────────────────────────────
\ $0400-$1BFF RG6 VRAM, 6144 bytes, 32 per scanline, SAM V=6, F=2
\ $5000-$5EFF 240 star records x 16 bytes: the title ages all 240, and the
\ options use the first 24, 48, 64 or 200 (moved from $4000:
\ 240 would reach the scratch bytes at $4400)
\ $4404-$4405 scratch word for the multiplies
\ $4407 scratch byte for the multiplies
\ $4408 centre x, set from the left joystick each frame
\ $4409 centre y
\ $440A quiet: nonzero = advance stars but draw nothing
\ $440B projected x this frame
\ $440C projected y this frame
\ $440D joystick: result so far
\ $440E joystick: bit under test
\ $440F frame number, +1 per tick (joystick read one frame in 4)
\ $4412-$4413 record the next frame's updates start from
\ $4417-$4418 scratch: 2 * round(p^2), for ten steps
\ $441A stars in use: 24, 48, 64, 200, or 240 while ageing
\ $441B PER-FRAME, stars updated each frame
\ $441D-$441E end of the records in use: $5000 + 16 * stars in use
\ $441F step mode: 0, 1, 2, 3 = 1, 2, 4, 10 depth steps per update
\ $4420 preset, copied at startup; nonzero makes space? ignore SPACE
\ $4424-$4425 address of this frame's step handler, from the step mode
\
\ Star record:
\ +0 |X| 0..127 field offset +8,9 VRAM address last drawn
\ +1 |Y| 0..127 +10 size: 0 = point, 1 = 2 tall
\ +2,3 p 0.16 fraction, = 1/z +11 drawn flag
\ +4 cx centre at birth +12 colour as a pair mask at bit 7:
\ +5 cy $80 blue, $40 orange, $C0 white
\ +6 signs: bit 7 = X negative +13 mask last drawn
\ bit 6 = Y negative +14 pair x last drawn
\ +15 y last drawn, $FF = not on screen
\
\ RG6 pixel: addr = $0400 + y*32 + (x>>3), bit = $80 >> (x and 7).
\ A 2-tall star is the same mask one line down, at addr + 32.
\ ── The frame (CODE) ──────────────────────────────────────────────
\ Erase and redraw happen at the TOP of the frame, before the beam has
\ scanned far. Writing moving markers late in the frame puts them into
\ VRAM after the beam has already passed, so they never appear.
CODE tick-stars ( -- )
PSHS X
INC $440F ; frame number
; ---- the joystick, one frame in four ----
LDA $440F
BITA #3
BNE @nojoy
LBSR @joy
@nojoy
; ---- this frame's step handler, chosen once rather than per star ----
; PC-relative, not LDX #@s1: a CODE word is assembled at 0 and moved,
; so an absolute label is its offset in the word, not its address.
; Branches survive the move; LDX #@label jumped into low memory.
LEAX @s1,PCR
LDA $441F
BEQ @mset
LEAX @s2,PCR
DECA
BEQ @mset
LEAX @s4,PCR
DECA
BEQ @mset
LEAX @s10,PCR
@mset STX $4424
; ---- the next PER-FRAME stars, round robin ----
LDY $4412 ; carry on from last frame
LDA $441B
PSHS A
@loop
; ---- one step nearer: p += round(p * p) ----
; With p = a:b, p^2 = a*a<<16 + 2*a*b<<8 + b*b. In units of the
; result: a*a, plus the high 9 bits of 2ab, plus whatever carries
; out of the fraction byte, which is lo(2ab) + hi(b*b) + $80.
; The $80 is the rounding. lo(b*b) cannot reach the result.
LDA 2,Y
LDB 3,Y
MUL ; D = a*b
ASLB
ROLA ; D = low 16 bits of 2ab, C = bit 16
STD $4404
LDA #0 ; LDA, not CLRA: keep the carry
ROLA
STA $4407 ; bit 16 of 2ab
LDA 3,Y
TFR A,B
MUL ; A = hi(b*b)
CLRB
ADDA #$80 ; round
ADCB #0
ADDA $4405 ; + lo(2ab)
ADCB #0 ; B = carries out of the fraction byte
LDA $4407 ; D = bit16 * 256 + carries
ADDB $4404 ; + hi(2ab)
ADCA #0
STD $4404
LDA 2,Y
TFR A,B
MUL ; D = a*a
ADDD $4404 ; D = round(p^2), cannot overflow: p < 1
; ---- a star updated every S frames takes S steps at once ----
; p += S * p^2 is the same series to first order, erring by order
; S*p, so flight time in frames is the same in every option. S is
; 1, 2, 4 or 10, done by shifts and one add, never a multiply:
; measured, two MULs cost ~100 cycles a star and pushed option 2 to
; 112% of a frame. Each handler falls into the next smaller one.
; A carry out of any shift or add means the star reached the camera.
LDX $4424
JMP ,X
@s10 ASLB
ROLA
LBCS @respawn
STD $4417 ; 2R
ASLB
ROLA
LBCS @respawn
ASLB
ROLA
LBCS @respawn ; 8R
ADDD $4417 ; 10R
LBCS @respawn
BRA @s1
@s4 ASLB
ROLA
LBCS @respawn
@s2 ASLB
ROLA
LBCS @respawn
@s1 ADDD 2,Y
LBCS @respawn ; carry: p reached 1, the star is here
STD 2,Y
; ---- project x: centre + 128 * X * p ----
; T = |X| * p >> 8 from two MULs (the low partial is only used
; for its high byte). The offset on screen is T >> 1. If that
; leaves anything in the high byte the offset is 256 or more,
; off screen whatever the centre.
LDA ,Y
LDB 3,Y
MUL
STA $4404
LDA ,Y
LDB 2,Y
MUL
ADDB $4404
ADCA #0 ; D = T
LSRA ; T >> 1
RORB
TSTA
LBNE @respawn
TFR B,A
LDB 6,Y
BMI @xneg
ADDA 4,Y
LBCS @respawn ; past x = 255
BRA @xset
@xneg PSHS A
LDA 4,Y
SUBA ,S+
LBCS @respawn ; past x = 0
@xset STA $440B
; ---- project y, the same way ----
LDA 1,Y
LDB 3,Y
MUL
STA $4404
LDA 1,Y
LDB 2,Y
MUL
ADDB $4404
ADCA #0
LSRA
RORB
TSTA
LBNE @respawn
TFR B,A
LDB 6,Y
BITB #$40
BNE @yneg
ADDA 5,Y
LBCS @respawn
BRA @yset
@yneg PSHS A
LDA 5,Y
SUBA ,S+
LBCS @respawn
@yset CMPA #192
LBHS @respawn
STA $440C
; ---- quiet ticks age the field without touching the screen ----
TST $440A
LBNE @next
; ---- same pair and same line as last frame? Then it did not move ----
; Measured (#69): about two updates in three land exactly where the
; star already is. Those skip the size, mask, address and erase, and
; only re-OR the saved mask at the saved address. Re-plotting instead
; of skipping outright repairs a pixel that another star's erase
; knocked out where the two overlap, which in the centre cluster they
; do. Size is deliberately NOT compared: it changes once in a star's
; life, and a star that crosses the threshold while standing still
; just grows when it next moves. A star not on screen has $FF as its
; saved line, which no projection produces, so it always redraws.
LDA $440C
CMPA 15,Y
BNE @redraw
LDA $440B
ANDA #$FE
CMPA 14,Y
BNE @redraw
LDX 8,Y
BRA @plot
; ---- it moved. Near stars (p >= 1/64, z <= 64) are two tall ----
; except on line 191, where the second line would be past VRAM.
@redraw CLRA
LDB 2,Y
CMPB #4
BLO @szst
LDB $440C
CMPB #191
BEQ @szst
INCA
@szst STA $4407 ; size for this draw
; ---- erase where we drew last frame ----
; The draw saved its VRAM address and mask, so erasing is just an AND.
TST 11,Y
BEQ @noerase
LDX 8,Y
LDB 13,Y
COMB
TFR B,A
ANDA ,X
STA ,X
TST 10,Y
BEQ @noerase
ANDB 32,X
STB 32,X
@noerase
LDA $4407
STA 10,Y
; ---- draw at the new position ----
; An artifact colour comes from the pixel's X PARITY, so a star
; drawn at its exact x flips blue/red every time it crosses a
; pixel. Snap to the containing pair and use the pair mask this
; star was born with: horizontal motion quantises to two pixels,
; and the colour is then stable for the star's whole life. White
; lights BOTH pixels of the pair. Both pixels of a pair are
; always in the same byte, so one mask covers the star.
LDB $440B
ANDB #6 ; pair offset within the byte: 0, 2, 4, 6
LDA 12,Y
@msh TSTB
BEQ @mst
LSRA
LSRA
SUBB #2
BRA @msh
@mst STA 13,Y
LDA $440C
LDB #32
MUL
ADDD #$0400
TFR D,X
LDB $440B
LSRB
LSRB
LSRB
ABX
STX 8,Y
LDA $440B
ANDA #$FE
STA 14,Y ; where it is drawn now, for next frame's test
LDA $440C
STA 15,Y
LDA #1
STA 11,Y
@plot LDA 13,Y
ORA ,X
STA ,X
TST 10,Y
BEQ @next
LDA 13,Y
ORA 32,X
STA 32,X
@next LEAY 16,Y
CMPY $441D ; past the last star in use: wrap
BNE @nwrap
LDY #$5000
@nwrap DEC ,S
LBNE @loop
STY $4412 ; next frame carries on from here
LEAS 1,S
PULS X
;NEXT
; ---- respawn far away, at a fresh point in the field ----
; Its old pixels come off first, through @erase. HALF of new stars
; take X and Y in -127..127, which at this depth lands them anywhere
; within about 100 pixels of the centre: the whole screen. The other
; half take -15..15, a cluster within about 13 pixels, so the vanishing
; point stays busy and those are the stars that come close enough to
; rush past. Spawns below the screen or past a joystick-moved edge
; simply respawn again next frame. Depth is
; z = 121..158 (p = 415..542): no deeper, because rounded 16-bit p^2
; starts to lag the true curve past z = 158. The spread in depth stops
; stars arriving in waves.
@respawn
LBSR @erase ; it may still be on screen from last frame
LBSR @rng
STA 6,Y ; bits 7, 6 are the signs
LDB #$7F ; bit 5 picks wide field or centre cluster
BITA #$20
BEQ @wide
LDB #$0F
@wide STB $4407
LBSR @rng
ANDA $4407
STA ,Y
LBSR @rng
ANDA $4407
STA 1,Y
LBSR @rng
ANDA #$7F
TFR A,B
CLRA
ADDD #415
STD 2,Y
LDA $4408 ; launch from where we point NOW
STA 4,Y
LDA $4409
STA 5,Y
CLR 10,Y
LBSR @rng ; colour for this life: blue, orange or white
ANDA #7
CMPA #3
BLO @cb
CMPA #6
BLO @co
LDA #$C0 ; white: both pixels of the pair
BRA @cst
@cb LDA #$80 ; blue: even pixel
BRA @cst
@co LDA #$40 ; orange: odd pixel
@cst STA 12,Y
CLR 11,Y ; nothing drawn yet
LDA #$FF
STA 15,Y ; so the unchanged test cannot match
LBRA @next
; ---- erase a star's saved pixels, if it has any (respawn path) ----
; The main path inlines the same AND. The respawn reaches here straight
; from the depth step or a bounds test, before any erase has happened.
@erase TST 11,Y
BEQ @erx
LDB 13,Y
LDX 8,Y
COMB
TFR B,A
ANDA ,X
STA ,X
TST 10,Y
BEQ @erc
ANDB 32,X
STB 32,X
@erc CLR 11,Y
@erx RTS
; ---- Inline LCG: seed = seed * 25173 + 13849 ----
@rng LDA VAR_SEED+1
LDB #$55
MUL
PSHS D
LDA VAR_SEED
LDB #$55
MUL
ADDB ,S
STB ,S
LDA VAR_SEED+1
LDB #$62
MUL
ADDB ,S
STB ,S
PULS D
ADDD #$3619
STD VAR_SEED
RTS
; ---- the left joystick sets the centre new stars launch from ----
; CODE, not coco/lib/dac.fs. The library's joy-sample is threaded Forth
; and measured ~8,000 cycles a frame. Each axis samples 0..63: X scales
; by 4 and Y by 3 to span 256x192, at most 252 and 189, both on screen.
; Axis select is SEL1 on PIA0 CA2 ($FF01) and SEL2 on CB2 ($FF03): $34
; drives the line low, $3C high. Left X is (0, 1), left Y (1, 1).
@joy LDA #$34
STA $FF01
LDA #$3C
STA $FF03
BSR @samp
ASLA
ASLA
STA $4408
LDA #$3C
STA $FF01
BSR @samp
TFR A,B
ASLA
PSHS B
ADDA ,S+
STA $4409
RTS
; ---- one axis, 0..63: binary search the DAC against the comparator ----
@samp CLR $440D
LDA #32
STA $440E
@sbit LDA $440D
ORA $440E
ASLA ; DAC is the top six bits of $FF20
ASLA
STA $FF20
LDA $FF00 ; bit 7 set: stick is above the DAC
BPL @snext
LDA $440D
ORA $440E
STA $440D
@snext LSR $440E
BNE @sbit
LDA $440D
RTS
;CODE
\ ── Title screen ──────────────────────────────────────────────────
: vemit ( char -- )
$3F AND kvar-cur @ $0400 + C! kvar-cur @ 1 + kvar-cur ! ;
: vtype ( addr len -- )
0 DO DUP I + C@ vemit LOOP DROP ;
: .title ( -- )
11 1 AT S" STARFIELD" vtype
5 4 AT S" 1 24 STARS 60 HZ" vtype
5 6 AT S" 2 48 STARS 30 HZ" vtype
5 8 AT S" 3 64 STARS FREE RUN" vtype
5 10 AT S" 4 200 STARS 6 HZ" vtype
5 13 AT S" SPACE RANDOM" vtype ;
\ ── Choosing the variant ──────────────────────────────────────────
\ preset: 0 means ask on the title screen. measure_budget.py patches
\ the low byte of this LDD to 1 to 4 in a copy of the binary, so it
\ can measure a variant with no key press; $5A marks it for the search.
\ It applies to the first title screen only.
CODE preset ( -- n )
LDD #$5A00
CLRA
STD ,--U
;NEXT
;CODE
: variant! ( stars per-frame step-mode -- )
$441F C! $441B C! $441A C! ;
\ Options 1 to 4, and 5: the title's quiet ageing, 240 stars at 60 a
\ frame and 4 steps, so all 240 records are spread through depth
\ whichever option is then picked.
: configure ( n -- )
DUP 1 = IF 24 24 0 variant! THEN
DUP 2 = IF 48 24 1 variant! THEN
DUP 3 = IF 64 64 0 variant! THEN
DUP 4 = IF 200 20 3 variant! THEN
5 = IF 240 60 2 variant! THEN
$441A C@ 16 * $5000 + $441D !
$5000 $4412 ! ;
VARIABLE cycle \ 1, 2, 3, 4, 1, ... once per title frame
VARIABLE started \ nonzero once the first title screen is past
\ pick: 1 to 4 for those keys, SPACE for whichever option the cycle
\ is on (random, because nobody can time it), else 0. There is no
\ division in the kernel to pick one arithmetically. Every other key
\ is ignored.
: pick ( -- n )
cycle @ 1 + DUP 4 > IF DROP 1 THEN cycle !
KEY? DUP 32 = IF
DROP cycle @
ELSE
48 - DUP 1 < OVER 4 > OR IF DROP 0 THEN
THEN ;
\ Wait for the key to come up, so the SPACE that left one screen does not
\ also act on the next. The field keeps ageing, but QUIET: this runs in
\ text mode, where a drawn star would land on the title.
: released ( -- )
1 $440A C!
BEGIN tick-stars vsync KEY? 0= UNTIL ;
\ ── Main ──────────────────────────────────────────────────────────
\ The field runs QUIET behind the title screen, as configuration 5 so
\ all 240 records age. Whichever option is picked then uses the first
\ 24, 48, 64 or 200 of them, already spread through depth instead of
\ launching as one wave. There is no division to scatter starting depths with, so
\ they are simply flown there.
\
\ There is no way out: SPACE moves between the title and flight, and the
\ demo runs until the machine is reset.
: new-field ( -- )
$5000 3840 0 FILL
240 0 DO $FFFF $5000 I 16 * + 2 + ! LOOP \ p = 1: respawn on first tick
0 $440F C!
128 $4408 C! 96 $4409 C! \ centred until the first joystick read
5 configure
1 $440A C! ;
: choose ( -- n )
started @ IF 0 ELSE preset THEN 1 started !
DUP IF
150 0 DO tick-stars LOOP \ preset: no key to wait for, age and go
ELSE
DROP
BEGIN tick-stars vsync pick ?DUP UNTIL
released
THEN ;
: graphics ( -- )
$0400 6144 0 FILL \ blank first: this FILL is slow enough to
6 set-sam-v \ show as garbage if the mode is set first
2 set-sam-f
$F8 set-pia ; \ RG6, CSS=1: black background, artifact colour
\ space?: nonzero while SPACE is down. It reads only that key, straight
\ from the matrix (column 7, row 3 in the kernel's KEY_TABLE, active
\ low). Measured, filtering KEY? in Forth cost ~450 cycles a frame and
\ pushed option 2 to 103% of a frame; this is a few dozen. A measurement
\ copy (preset set) never sees it, so a stray keystroke from XRoar's
\ -run cannot end the trace.
CODE space? ( -- flag )
LDD #0
TST $4420 ; preset, copied at startup: nonzero = no keys
BNE @spdone
LDA #$7F ; column 7
STA $FF02
LDA $FF00
BITA #$08 ; row 3: SPACE
BNE @spoff
LDB #1
@spoff LDA #$FF
STA $FF02 ; deselect all columns
CLRA
@spdone STD ,--U
;NEXT
;CODE
: fly ( -- )
0 $440A C!
BEGIN tick-stars vsync space? UNTIL ;
\ SAM R=1 ($FFD7), "address-dependent" fast mode. It does not speed up a
\ single RAM or VRAM access, but the 6809's internal dead cycles put $FFFF
\ on the address bus and those run at double speed: 9 of MUL's 11 cycles.
\ Measured on option 3, 24.6 -> 26.3 fps (#69, #71). The picture is
\ unaffected, unlike R=2 ($FFD9).
: stars ( -- )
0 seed !
preset $4420 C!
$FF $FFD7 C! \ SAM R=1: see above
BEGIN
reset-text cls-black .title
new-field
choose configure
graphics
fly
reset-text released
AGAIN ;
stars