First public commit.

This commit is contained in:
The Dust Council 2026-07-03 19:35:35 -07:00
parent 2a48f52979
commit 4bac9d83ed
288 changed files with 18417 additions and 1076 deletions

View file

@ -0,0 +1 @@
from .assemble import AssemblerError, SOURCES, assemble_stub, have_xa # noqa: F401

View file

@ -0,0 +1,97 @@
"""Assemble the Apple II boot/viewer with `xa` (origin $0800, raw bytes)."""
from __future__ import annotations
import os
import shutil
import subprocess
import tempfile
VIEWER_DIR = os.path.dirname(os.path.abspath(__file__))
SOURCES = {"hgr": "hgr.s", "dhgr": "dhgr.s"}
_cache: dict[tuple, bytes] = {}
# How long the viewer holds the picture (see apple/viewer/awyt.i).
WAIT_MODES = {"forever": 0, "key": 1, "seconds": 2}
SS_WAITMODE = {"key": 1, "seconds": 2, "both": 3}
def build_slideshow_stub(n_images: int, advance: str = "both", seconds: int = 10,
loop: bool = True) -> bytes:
"""Assemble the Apple HGR slideshow boot loader (one 256-byte boot sector).
Reads NIMAGES * 32 sectors into the $4000 buffer and cycles them; must fit a
single boot sector since the Disk II ROM only loads sector 0.
"""
import shutil as _sh
if not _sh.which("xa"):
raise AssemblerError("The 'xa' assembler was not found on PATH.")
end_page = 0x40 + n_images * 0x20
wrapper = (f"#define WAITMODE {SS_WAITMODE[advance]}\n"
f"#define WAITSECS {max(0, min(255, int(seconds)))}\n"
f"#define NIMAGES {n_images}\n"
f"#define LOOPFLAG {1 if loop else 0}\n"
f"#define ENDPAGE ${end_page:02X}\n"
'#include "slideshow.s"\n')
with tempfile.TemporaryDirectory() as td:
out = os.path.join(td, "v.bin")
fd, wrap = tempfile.mkstemp(suffix=".s", prefix="_wrap_", dir=VIEWER_DIR)
try:
with os.fdopen(fd, "w") as f:
f.write(wrapper)
proc = subprocess.run(["xa", "-o", out, os.path.basename(wrap)],
capture_output=True, text=True, cwd=VIEWER_DIR)
if proc.returncode != 0:
raise AssemblerError(f"xa failed:\n{proc.stdout}{proc.stderr}")
with open(out, "rb") as f:
raw = f.read()
finally:
os.unlink(wrap)
if len(raw) > 256:
raise AssemblerError(
f"Apple slideshow boot loader is {len(raw)} bytes, over the 256-byte "
"boot sector")
return raw
class AssemblerError(RuntimeError):
pass
def have_xa() -> bool:
return shutil.which("xa") is not None
def assemble_stub(viewer_key: str, display: str = "forever", seconds: int = 0) -> bytes:
waitmode = WAIT_MODES.get(display, 0)
secs = max(0, min(255, int(seconds))) # 8-bit delay counter
key = (viewer_key, waitmode, secs)
if key in _cache:
return _cache[key]
if not have_xa():
raise AssemblerError("The 'xa' assembler was not found on PATH.")
if not os.path.exists(os.path.join(VIEWER_DIR, SOURCES[viewer_key])):
raise AssemblerError(f"viewer source missing: {SOURCES[viewer_key]}")
# Wrapper sets options then includes the real source; run from VIEWER_DIR so
# the source's #include "awyt.i" resolves (xa looks relative to cwd).
wrapper = (f"#define WAITMODE {waitmode}\n"
f"#define WAITSECS {secs}\n"
f'#include "{SOURCES[viewer_key]}"\n')
with tempfile.TemporaryDirectory() as td:
out = os.path.join(td, "v.bin")
fd, wrap = tempfile.mkstemp(suffix=".s", prefix="_wrap_", dir=VIEWER_DIR)
try:
with os.fdopen(fd, "w") as f:
f.write(wrapper)
proc = subprocess.run(["xa", "-o", out, os.path.basename(wrap)],
capture_output=True, text=True, cwd=VIEWER_DIR)
if proc.returncode != 0:
raise AssemblerError(
f"xa failed for {viewer_key}:\n{proc.stdout}{proc.stderr}")
with open(out, "rb") as f:
raw = f.read()
finally:
os.unlink(wrap)
_cache[key] = raw
return raw

View file

@ -0,0 +1,44 @@
; Shared display-duration epilogue for the Apple II viewers (HGR already on).
; WAITMODE 0 forever, 1 until a key, 2 about WAITSECS seconds.
; key and seconds exit to Applesoft BASIC ($E000 cold start), a real prompt with
; no DOS needed. The II and II+ have no timer, so seconds is a calibrated delay
; loop near 1 MHz. WAITSECS is clamped to 255 by the assembler.
#if WAITMODE == 0
awhang:
jmp awhang
#endif
#if WAITMODE == 1
awhang:
lda $c000 ; keyboard; bit7 set = a key is down
bpl awhang
bit $c010 ; clear strobe
lda $c051 ; switch to text so the BASIC prompt is visible
lda $c054 ; page 1
jmp $e000 ; Applesoft cold start
#endif
#if WAITMODE == 2
lda #WAITSECS
sta $fd ; seconds remaining
aw_o:
lda #$03 ; ~1 second = 3 * (256*256 inner) at ~1 MHz
sta $fc
aw_m:
ldx #$00
aw_x:
ldy #$00
aw_y:
dey
bne aw_y
dex
bne aw_x
dec $fc
bne aw_m
dec $fd
bne aw_o
lda $c051 ; switch to text so the BASIC prompt is visible
lda $c054 ; page 1
jmp $e000 ; Applesoft cold start
#endif

128
lenser/apple/viewer/dhgr.s Normal file
View file

@ -0,0 +1,128 @@
; lenser -- Apple //e Double Hi-Res boot loader + viewer (self-contained)
;
; Loads 16K to main $2000-$5FFF with ordinary reads (main half at $2000, aux half
; at $4000), then block-copies $4000-$5FFF into auxiliary $2000-$3FFF (RAMWRT on
; only for that clean copy, never during the boot-ROM reads), then turns on DHGR.
; ROM read at $C65C reads sector $3D into page $27 and re-enters $0801.
;
; assembled by apple/viewer/assemble.py via xa
* = $0800
.byte $01
entry: ; $0801, re-entered after each ROM read
lda dpage
cmp #$60 ; loaded $2000-$5FFF (64 pages)?
bcs done
lda psec
cmp #$10
bcc readit
jsr seeknext
lda #$00
sta psec
readit:
lda psec
sta $3d ; desired sector
lda curtrk
sta $41 ; desired track (the ROM read verifies BOTH)
lda #$00
sta $26
lda dpage
sta $27
inc psec
inc dpage
ldx $2b
jmp $c65c
done:
; copy main $4000-$5FFF -> aux $2000-$3FFF
lda #$00
sta $06
lda #$40
sta $07 ; src = $4000
lda #$00
sta $08
lda #$20
sta $09 ; dst = $2000
sta $c005 ; RAMWRT on (writes go to aux)
ldx #$20 ; 32 pages
cpl:
ldy #$00
cp1:
lda ($06),y
sta ($08),y
iny
bne cp1
inc $07
inc $09
dex
bne cpl
sta $c004 ; RAMWRT off
; turn on Double Hi-Res
lda $c050 ; graphics
lda $c052 ; full screen
lda $c054 ; page 1
lda $c057 ; hi-res
sta $c00d ; SET80VID (write-triggered switch -- must STA)
sta $c05e ; SETDHIRES (write-triggered)
#include "awyt.i"
; advance the head one track (two half-steps) with the standard phase-overlap
; seek. energize the NEXT phase while the current one is still on (this pulls
; the head smoothly), then release the old phase, using on/off settle delays
; from an acceleration table indexed by step number ($0a). the final phase is
; released before reading.
seeknext:
inc curtrk ; now on the next track
lda #$00
sta $0a ; step index for the timing table
jsr onestep
jsr onestep
lda halftrk ; release final phase before the read
and #$03
asl
ora $2b
tax
lda $c080,x
rts
onestep:
lda halftrk ; energize NEXT phase (halftrk+1), old still on
clc
adc #$01
and #$03
asl
ora $2b
tax
lda $c081,x
ldx $0a
lda ontable,x
jsr wait
lda halftrk ; release OLD phase (halftrk)
and #$03
asl
ora $2b
tax
lda $c080,x
ldx $0a
lda offtable,x
jsr wait
inc halftrk
inc $0a
rts
; Apple seek timing tables (head accelerates over a move, slow first step).
ontable: .byte $13,$0a,$08,$06,$05,$04,$04,$03
offtable: .byte $46,$1a,$10,$0c,$0a,$09,$08,$08
wait: ; delay loop ~ proportional to A
tay
w1:
ldx #$00
w2:
dex
bne w2
dey
bne w1
rts
psec: .byte $01
dpage: .byte $20
halftrk: .byte $00
curtrk: .byte $00

102
lenser/apple/viewer/hgr.s Normal file
View file

@ -0,0 +1,102 @@
; lenser -- Apple II HGR boot loader + viewer (self-contained, no DOS)
;
; The Disk II boot ROM loads track 0 sector 0 to $0800 and JMPs $0801. This
; re-entrant loader reads the 32 sectors of the 8K HGR bitmap into $2000-$3FFF
; (track 0 sectors 1-15, then tracks 1 and 2), seeking the head between tracks
; with the standard phase-overlap step, then switches on HGR and loops. The ROM
; read at $Cn5C verifies the address field against BOTH the sector ($3D) and the
; track ($41), then reads into page $27 and JMPs back to $0801.
;
; assembled by apple/viewer/assemble.py via xa
* = $0800
.byte $01 ; ROM scratch (boot sector byte 0)
entry: ; $0801, (re)entered after every ROM read
lda dpage
cmp #$40
bcs done ; loaded $2000..$3FFF -> show it
lda psec
cmp #$10
bcc readit ; still sectors left on this track
jsr seeknext ; finished a track -> step to the next
lda #$00
sta psec
readit:
lda psec
sta $3d ; desired sector
lda curtrk
sta $41 ; desired track (ROM read verifies both)
lda #$00
sta $26 ; buffer lo
lda dpage
sta $27 ; buffer hi
inc psec
inc dpage
ldx $2b ; slot*16 (set by boot ROM)
jmp $c65c ; slot 6 ROM read; reads sector then JMP $0801
done:
lda $c050 ; graphics
lda $c052 ; full screen (not mixed)
lda $c054 ; page 1
lda $c057 ; hi-res
#include "awyt.i"
; advance the head one track (two half-steps) with the standard phase-overlap
; step. energize the next phase while the current one is still on, then release
; the old phase, using on/off settle delays from an acceleration table.
seeknext:
inc curtrk
lda #$00
sta $0a
jsr onestep
jsr onestep
lda halftrk ; release final phase before reading
and #$03
asl
ora $2b
tax
lda $c080,x
rts
onestep:
lda halftrk ; energize NEXT phase (old still on -> overlap)
clc
adc #$01
and #$03
asl
ora $2b
tax
lda $c081,x
ldx $0a
lda ontable,x
jsr wait
lda halftrk ; release OLD phase
and #$03
asl
ora $2b
tax
lda $c080,x
ldx $0a
lda offtable,x
jsr wait
inc halftrk
inc $0a
rts
ontable: .byte $13,$0a,$08,$06
offtable: .byte $46,$1a,$10,$0c
wait:
tay
w1:
ldx #$00
w2:
dex
bne w2
dey
bne w1
rts
; ---- loader state (initial values; updated in place during the load) ----
psec: .byte $01 ; next physical sector (track 0 starts at 1)
dpage: .byte $20 ; next destination page ($2000)
halftrk: .byte $00 ; current half-track
curtrk: .byte $00 ; current track

View file

@ -0,0 +1,204 @@
; lenser -- Apple II HGR slideshow loader + viewer (self-contained, no DOS).
;
; The Disk II boot ROM loads track 0 sector 0 to $0800 and JMPs $0801. This
; loader reads ALL the slideshow's HGR images (NIMAGES * 32 sectors) contiguously
; into a RAM buffer from $4000 up (image i at $4000 + i*$2000), then cycles --
; copying each image into HGR page 1 ($2000), switching on graphics, and waiting
; (key / seconds / both) before the next. Looping is RAM-only (no re-seek).
;
; #defines from the wrapper --
; WAITMODE 1 key / 2 seconds / 3 both WAITSECS seconds (~) NIMAGES count
; LOOPFLAG 1 wrap / 0 stop ENDPAGE one past the last buffer page ($40+N*$20)
;
; assembled by apple/viewer/assemble.py via xa
src = $06 ; zero-page copy pointers
dst = $08
ssidx = $19
* = $0800
.byte $01 ; ROM scratch (boot sector byte 0)
entry: ; $0801, (re)entered after every ROM read
lda dpage
cmp #ENDPAGE
bcs loaded ; whole buffer read -> start the show
lda psec
cmp #$10
bcc readit
jsr seeknext
lda #$00
sta psec
readit:
lda psec
sta $3d
lda curtrk
sta $41
lda #$00
sta $26
lda dpage
sta $27
inc psec
inc dpage
ldx $2b
jmp $c65c ; slot-6 ROM read; reads a sector then JMP $0801
loaded:
lda #$00
sta ssidx
cyc:
; ---- copy image ssidx ($4000 + ssidx*$2000) -> HGR page 1 ($2000) ----
lda ssidx
asl
asl
asl
asl
asl ; ssidx * $20 pages (carry clear, <=4 images)
adc #$40
sta src+1 ; source hi = $40 + ssidx*$20
lda #$00
sta src
sta dst
lda #$20
sta dst+1 ; dest = $2000
ldx #$20 ; 32 pages
ldy #$00
cpyl:
lda (src),y
sta (dst),y
iny
bne cpyl
inc src+1
inc dst+1
dex
bne cpyl
lda $c050 ; graphics
lda $c054 ; page 1
lda $c057 ; hi-res
jsr sswait
inc ssidx
lda ssidx
cmp #NIMAGES
bcc cyc
#if LOOPFLAG == 1
jmp loaded ; wrap (re-uses the ssidx=0 init at loaded)
#else
lda $c051 ; text
lda $c054
jmp $e000 ; Applesoft cold start
#endif
; ---- wait (returns); clears the key strobe first ----
sswait:
bit $c010
#if WAITMODE == 1
swk:
lda $c000
bpl swk
bit $c010
rts
#endif
#if WAITMODE == 2
lda #WAITSECS
sta $fd
so:
lda #$03
sta $fc
sm:
ldx #$00
sx:
ldy #$00
sy:
dey
bne sy
dex
bne sx
dec $fc
bne sm
dec $fd
bne so
rts
#endif
#if WAITMODE == 3
lda #WAITSECS
sta $fd
bo:
lda #$03
sta $fc
bm:
ldx #$00
bx:
lda $c000
bmi bdone ; a key ends the slide
ldy #$00
by:
dey
bne by
dex
bne bx
dec $fc
bne bm
dec $fd
bne bo
bdone:
bit $c010 ; (also harmless on timeout)
rts
#endif
; advance the head one track (two half-steps), phase-overlap step (from hgr.s)
seeknext:
inc curtrk
lda #$00
sta $0a
jsr onestep
jsr onestep
lda halftrk
and #$03
asl
ora $2b
tax
lda $c080,x
rts
onestep:
lda halftrk
clc
adc #$01
and #$03
asl
ora $2b
tax
lda $c081,x
ldx $0a
lda ontable,x
jsr wait
lda halftrk
and #$03
asl
ora $2b
tax
lda $c080,x
ldx $0a
lda offtable,x
jsr wait
inc halftrk
inc $0a
rts
ontable: .byte $13,$0a,$08,$06
offtable: .byte $46,$1a,$10,$0c
wait:
tay
w1:
ldx #$00
w2:
dex
bne w2
dey
bne w1
rts
psec: .byte $01 ; next physical sector (track 0 starts at 1)
dpage: .byte $40 ; next destination page ($4000)
halftrk: .byte $00
curtrk: .byte $00