First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
1
lenser/a2600/viewer/__init__.py
Normal file
1
lenser/a2600/viewer/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
138
lenser/a2600/viewer/a2600.s
Normal file
138
lenser/a2600/viewer/a2600.s
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
; lenser -- Atari 2600 (VCS) image viewer kernel (6502, "racing the beam").
|
||||
;
|
||||
; No framebuffer -- per visible scanline the kernel feeds the TIA a 40-pixel
|
||||
; asymmetric playfield (left 20 + right 20, rewritten mid-line) plus a shared
|
||||
; playfield colour (COLUPF) and two backgrounds -- COLUBK is set for the left
|
||||
; half during HBLANK then rewritten mid-line for the right half, so each
|
||||
; scanline shows 3 colours (left bg + right bg + foreground). The nine 192-byte
|
||||
; data tables are page-aligned (set by the cartridge builder) so LDA tab,Y never
|
||||
; crosses a page and the kernel stays cycle-exact.
|
||||
;
|
||||
; #defines from the wrapper -- PF0L,PF1L,PF2L,PF0R,PF1R,PF2R,BKL,BKR,PFT (table
|
||||
; bases), WAITMODE (0 forever / 1 fire / 2 seconds), WAITSECS.
|
||||
;
|
||||
; assembled by a2600/viewer/assemble.py via xa
|
||||
|
||||
VSYNC = $00
|
||||
VBLANK = $01
|
||||
WSYNC = $02
|
||||
NUSIZ0 = $04
|
||||
COLUPF = $08
|
||||
COLUBK = $09
|
||||
CTRLPF = $0A
|
||||
PF0 = $0D
|
||||
PF1 = $0E
|
||||
PF2 = $0F
|
||||
INPT4 = $3C
|
||||
FRLO = $80 ; frame counter (seconds mode)
|
||||
FRHI = $81
|
||||
PARITY = $82 ; interlace frame parity (IL mode)
|
||||
|
||||
* = $f000
|
||||
start:
|
||||
sei
|
||||
cld
|
||||
ldx #0
|
||||
txa
|
||||
clr:
|
||||
sta $00,x
|
||||
inx
|
||||
bne clr ; clear $00-$FF (RAM + TIA)
|
||||
ldx #$ff
|
||||
txs
|
||||
|
||||
frame:
|
||||
lda #2
|
||||
sta VSYNC
|
||||
sta WSYNC
|
||||
sta WSYNC
|
||||
sta WSYNC ; 3 VSYNC lines
|
||||
lda #0
|
||||
sta VSYNC
|
||||
lda #2
|
||||
sta VBLANK
|
||||
sta CTRLPF ; reflect off (we draw both halves explicitly)
|
||||
lda #0
|
||||
sta CTRLPF
|
||||
#if IL
|
||||
; temporal interlace -- alternate the two 4K banks (frame A / frame B)
|
||||
; each frame; the kernel is identical in both banks so execution
|
||||
; continues seamlessly and only the data tables differ.
|
||||
lda PARITY
|
||||
eor #1
|
||||
sta PARITY
|
||||
beq selbank0
|
||||
lda $1ff9 ; F8 hotspot -> select bank 1 (AD F9 1F)
|
||||
jmp bankdone
|
||||
selbank0:
|
||||
lda $1ff8 ; F8 hotspot -> select bank 0 (AD F8 1F)
|
||||
bankdone:
|
||||
#endif
|
||||
ldx #36
|
||||
vbl:
|
||||
sta WSYNC
|
||||
dex
|
||||
bne vbl
|
||||
lda #0
|
||||
sta VBLANK
|
||||
|
||||
; ---- visible image, 192 cycle-exact scanlines ----
|
||||
ldy #0
|
||||
kloop:
|
||||
sta WSYNC
|
||||
lda BKL,y ; left background (HBLANK)
|
||||
sta COLUBK
|
||||
lda PFT,y ; shared foreground
|
||||
sta COLUPF
|
||||
lda PF0L,y
|
||||
sta PF0
|
||||
lda PF1L,y
|
||||
sta PF1
|
||||
lda PF2L,y
|
||||
sta PF2
|
||||
lda PF0R,y ; right playfield PF0 (early, before mid-line)
|
||||
sta PF0
|
||||
lda BKR,y ; right background (lands at the half boundary)
|
||||
sta COLUBK
|
||||
lda PF1R,y
|
||||
sta PF1
|
||||
lda PF2R,y
|
||||
sta PF2
|
||||
iny
|
||||
cpy #192
|
||||
bne kloop
|
||||
|
||||
lda #0
|
||||
sta PF0
|
||||
sta PF1
|
||||
sta PF2
|
||||
sta COLUBK
|
||||
|
||||
; ---- hold / exit ----
|
||||
#if WAITMODE == 1
|
||||
lda INPT4
|
||||
bmi over ; bit7 set = fire not pressed
|
||||
jmp start ; pressed -> reset (re-display)
|
||||
#endif
|
||||
#if WAITMODE == 2
|
||||
inc FRLO
|
||||
bne fr_ok
|
||||
inc FRHI
|
||||
fr_ok:
|
||||
lda FRHI
|
||||
cmp #>(WAITSECS*60)
|
||||
bcc over
|
||||
lda FRLO
|
||||
cmp #<(WAITSECS*60)
|
||||
bcc over
|
||||
jmp start
|
||||
#endif
|
||||
over:
|
||||
lda #2
|
||||
sta VBLANK
|
||||
ldx #30
|
||||
ovl:
|
||||
sta WSYNC
|
||||
dex
|
||||
bne ovl
|
||||
jmp frame
|
||||
92
lenser/a2600/viewer/assemble.py
Normal file
92
lenser/a2600/viewer/assemble.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
"""Assemble the Atari 2600 kernel with `xa` and lay out the cartridge.
|
||||
|
||||
A static image is a 4K cart (one kernel + nine 192-byte tables). An interlace
|
||||
image is an 8K F8-bankswitch cart: two 4K banks, each holding the SAME kernel
|
||||
plus one of the two table sets (frame A / frame B). The kernel toggles the bank
|
||||
once per frame, so the two frames alternate at 60Hz and blend to ~4-6 perceived
|
||||
colours per scanline.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
VIEWER_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
WAIT_MODES = {"forever": 0, "fire": 1, "key": 1, "seconds": 2}
|
||||
|
||||
# page-aligned data tables ($F100..$F900)
|
||||
TABLES = {"PF0L": 0xF100, "PF1L": 0xF200, "PF2L": 0xF300, "PF0R": 0xF400,
|
||||
"PF1R": 0xF500, "PF2R": 0xF600, "BKL": 0xF700, "BKR": 0xF800,
|
||||
"PFT": 0xF900}
|
||||
ORDER = ["PF0L", "PF1L", "PF2L", "PF0R", "PF1R", "PF2R", "BKL", "BKR", "PFT"]
|
||||
|
||||
|
||||
class AssemblerError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def have_xa() -> bool:
|
||||
return shutil.which("xa") is not None
|
||||
|
||||
|
||||
def _assemble_kernel(display: str, seconds: int, interlace: bool) -> bytes:
|
||||
if not have_xa():
|
||||
raise AssemblerError("The 'xa' (xa65) assembler was not found on PATH.\n"
|
||||
"Install it with: sudo apt install xa65")
|
||||
waitmode = WAIT_MODES.get(display, 0)
|
||||
defs = "".join(f"#define {n} ${a:04X}\n" for n, a in TABLES.items())
|
||||
wrapper = (defs +
|
||||
f"#define WAITMODE {waitmode}\n"
|
||||
f"#define WAITSECS {max(0, int(seconds))}\n"
|
||||
f"#define IL {1 if interlace else 0}\n"
|
||||
'#include "a2600.s"\n')
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
out = os.path.join(td, "k.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:
|
||||
kernel = f.read()
|
||||
finally:
|
||||
os.unlink(wrap)
|
||||
if len(kernel) > 0x100:
|
||||
raise AssemblerError(f"kernel is {len(kernel)} bytes, overruns the table "
|
||||
"page at $F100")
|
||||
return kernel
|
||||
|
||||
|
||||
def _lay_bank(kernel: bytes, data: bytes) -> bytearray:
|
||||
"""One 4096-byte bank: kernel at $F000, nine tables, reset/IRQ vectors."""
|
||||
rom = bytearray(b"\x00" * 4096)
|
||||
rom[0:len(kernel)] = kernel
|
||||
tables = [data[i * 192:(i + 1) * 192] for i in range(9)]
|
||||
for name, tab in zip(ORDER, tables):
|
||||
off = TABLES[name] - 0xF000
|
||||
rom[off:off + 192] = tab
|
||||
rom[0xFFC] = 0x00 # reset vector lo -> $F000
|
||||
rom[0xFFD] = 0xF0
|
||||
rom[0xFFE] = 0x00 # IRQ/BRK vector
|
||||
rom[0xFFF] = 0xF0
|
||||
return rom
|
||||
|
||||
|
||||
def build_cart(data: bytes, display: str = "forever", seconds: int = 0) -> bytes:
|
||||
"""data = nine 192-byte tables (static, 4K cart) or eighteen (two sets ->
|
||||
interlace 8K F8 cart). Returns the cart image with reset vectors set."""
|
||||
if len(data) == 9 * 192:
|
||||
kernel = _assemble_kernel(display, seconds, interlace=False)
|
||||
return bytes(_lay_bank(kernel, data))
|
||||
if len(data) == 18 * 192:
|
||||
kernel = _assemble_kernel(display, seconds, interlace=True)
|
||||
bank0 = _lay_bank(kernel, data[:9 * 192])
|
||||
bank1 = _lay_bank(kernel, data[9 * 192:])
|
||||
return bytes(bank0 + bank1)
|
||||
raise ValueError(f"expected 1728 or 3456 bytes of tables, got {len(data)}")
|
||||
Loading…
Add table
Add a link
Reference in a new issue