Skip to content

Instantly share code, notes, and snippets.

@ursetto
Last active September 8, 2017 17:29
Show Gist options
  • Save ursetto/f96a84d9ad1e649208dfc0d675cdd3d6 to your computer and use it in GitHub Desktop.
Save ursetto/f96a84d9ad1e649208dfc0d675cdd3d6 to your computer and use it in GitHub Desktop.
Apple 2 screen holes saving
This is an analysis of the screen hole saving code found from 4:20-5:30
in the Kansasfest 2017 talk at
https://archive.org/details/2017_Kfest_Porting_Games_from_Booter_to_ProDOS
In the talk, presenter Peter Ferrie said the game Airheart stored game code in
the screen holes and "in certain configurations" it would cause a crash. The
screen holes are eight 8-byte sections of unused RAM in text page 1
($0400-$07FF) at $478-$47F, $4F8-$4FF, $578-$57F, ... $7F8-$7FF.
By convention hardware in slot N may use the 8 bytes at $0xx8+N
as scratchpad or semi-persistent storage. For examples see
http://www.kreativekorp.com/miscpages/a2info/screenholes.shtml .
The goal of this talk was porting old games to ProDOS to run off "modern"
devices instead of raw Disk ][, and the problem in this case was that the card
was storing current track and sector information in the screen holes and it was
getting corrupted. Presumably this is a problem for other RAM or disk emulator
cards as well; for example CFFA firmware 2.0 stores information in screen holes
and updates it during operation, reportedly rendering some games unplayable.
The solution given swaps the screen hole information into and out of private
storage while calling ProDOS, so the program sees its data when running and the
hardware sees its data while performing disk operations.
Although I might have some details wrong here, it struck me how tightly
optimized the code was, and it wasn't obvious how it even worked at first.
Analysis
--------
Zero page $00-$01 : Screen page base. Initially $0700, then $0600, $0500, $0400.
X register : Index into storage array. Counts downward from $0F to $00.
Y register : Index into screen page. Initialized from A at loop entry.
A register : Index into screen page, hole/array memory value, working register.
* Entry point. Start at hole $07F8 and at end of storage array.
inithole
LDA #$07 ; Init ZP $00-$01, X and A as above.
STA $01
LDA #$00
STA $00
LDX #$0F
LDA #$F8
loop
TAY ; Invariant: A holds screen page index at loop entry
LDA ($00),Y
PHA ; Save current screen hole byte on stack.
LDA array,X ; No effect if next instruction is LDA.
* On first run, the storage array is uninitialized. The first operation
* below should be LDA ($B1), so the invalid array is not copied into the screen holes.
* Afterward, the array contains valid screen hole data. You then update the
* operation to STA ($91), so on subsequent runs the saved data
* will be restored into the screen holes.
initpatch ; STA->LDA
LDA_STA ($00),Y ; If STA, restore screen hole byte from array;
; if LDA, this is a no-op.
PLA
STA array,X ; Copy saved screen hole byte into array.
DEX
BMI done ; Exit when array is full (all 16 bytes are copied, X<0).
TXA ; TXA/LSR tests whether array index is odd or even
LSR ; and sets carry accordingly (1 = odd).
TYA ; Bring screen index into A for manipulation
* Update the screen page index for the next screen hole, and loop.
* The order will be:
* $7F8, $7F8+n, $778, $778+n, $6F8, $6F8+n, ..., $478, $478+n
* where N is the slot patched in below (EOR #$0n). The loop ends when
* all 16 holes are traversed (because the array index is 0).
* It's not clear why EOR #$D1 is shown in the talk. May be error
* or placeholder (otherwise I've made a huge mistake).
* It's also not clear why the code saves screen holes for both
* slot N and slot 0. Was slot 0 affected as well?
slotpatch
; Patch next instruction operand to #$0n, where N is desired slot
EOR #$01 ; Cycle page index between $x8 and $x8+n as long as N in 1..7
BCC loop ; Take branch every other loop, using array index odd/even
; (carry still valid from TXA/LSR)
EOR #$80 ; Cycle page index between $F8 and $78
BPL loop ; If flipping from $78->$F8 (now negative), continue
DEC $01 ; Go to next page counting down ($07->$06, etc.)
BNE loop ; Always -- equiv. to BRA or JMP. Never reaches 0.
; notreached
array
DS 16 ; 16 byte storage array
Usage
-----
You call the code above once at the beginning of the program (after patching
the slot, and the 'initpatch' instruction to LDA) to save the 16 bytes of screen
hole information (your hardware data) to your private array.
You then patch the instruction to STA to activate the "swap" functionality.
Now you surround all disk-related MLI calls to ProDOS with hole swap calls.
This ensures that, during the MLI call, the hardware data is in the screen
holes as the card expects, and outside of the MLI call the program data
is in the screen holes as the software expects.
Below is an untested example.
* save screen hole data to array. On entry, Y = slot #
save
LDA #$91 ; opcode LDA (zpind),Y
STA initpatch
STY slotpatch+1
JSR inithole
LDA #$B1 ; opcode STA (zpind),Y
STA initpatch ; update for future calls
RTS
* issue prodos MLI read call, exchanging
* screen hole data with array data around the call
* all registers destroyed
mliread
JSR inithole ; exchange data
JSR $BF00 ; MLI entry
DB $CA ; MLI call "READ"
DW read_parms ; MLI read parameters ptr
JSR inithole ; exchange data
RTS
@peterferrie
Copy link

peterferrie commented Sep 8, 2017

the EOR #$D1 is a place-holder for the actual slot number which is known only to the caller.
Slot 0 is saved because CFFA (at least) stores things in it temporarily because its PROM space isn't writable, and the real slot space is used for other things.

@peterferrie
Copy link

lines 115 and 119 in your snippet have their opcodes reversed.
The first one should be #$B1, the second one #$91 to match the comments which are correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment