Last active
September 8, 2017 17:29
-
-
Save ursetto/f96a84d9ad1e649208dfc0d675cdd3d6 to your computer and use it in GitHub Desktop.
Apple 2 screen holes saving
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
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
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.