First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
1
lenser/a5200/__init__.py
Normal file
1
lenser/a5200/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Atari 5200 SuperSystem target for lenser."""
|
||||
23
lenser/a5200/convert/__init__.py
Normal file
23
lenser/a5200/convert/__init__.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"""Atari 5200 conversion dispatch.
|
||||
|
||||
The 5200 has the same ANTIC + GTIA graphics hardware (and the same 256-colour GTIA
|
||||
palette) as the Atari 400/800, so it reuses the Atari 8-bit encoders unchanged.
|
||||
GR.9 doubles as the monochrome / tinted-mono mode (16 real luminance shades of one
|
||||
hue). GR.15+DLI is omitted: per-scanline DLI colour changes need OS NMI vectoring
|
||||
the 5200 lacks.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from ...atari.convert import convert_image as _atari_convert
|
||||
|
||||
MODES = ["gr15", "gr9", "gr8", "mono"]
|
||||
|
||||
|
||||
def convert_image(path_or_img, mode="gr15", palette_name="ntsc",
|
||||
dither_mode="floyd", intensive=False, prep_opt=None,
|
||||
base_color=None):
|
||||
if mode not in MODES:
|
||||
mode = "gr15"
|
||||
return _atari_convert(path_or_img, mode=mode, palette_name="ntsc",
|
||||
dither_mode=dither_mode, intensive=intensive,
|
||||
prep_opt=prep_opt, base_color=base_color)
|
||||
17
lenser/a5200/exporter.py
Normal file
17
lenser/a5200/exporter.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
"""Build an Atari 5200 cartridge (.a52) from a conversion."""
|
||||
from __future__ import annotations
|
||||
|
||||
from .viewer import assemble
|
||||
|
||||
_EXTS = (".a52", ".bin", ".car", ".rom")
|
||||
|
||||
|
||||
def export_a52(conv, output_path, source_path=None, display="forever",
|
||||
seconds=0, video="ntsc"):
|
||||
if not output_path.lower().endswith(_EXTS):
|
||||
output_path += ".a52"
|
||||
rom = assemble.build_cart(conv.mode, bytes(conv.data), display=display,
|
||||
seconds=seconds, video=video)
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(rom)
|
||||
return output_path
|
||||
1
lenser/a5200/viewer/__init__.py
Normal file
1
lenser/a5200/viewer/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Atari 5200 6502 viewer (assembled by xa)."""
|
||||
131
lenser/a5200/viewer/assemble.py
Normal file
131
lenser/a5200/viewer/assemble.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"""Assemble the Atari 5200 viewer with `xa` and lay out the 32K cartridge.
|
||||
|
||||
The cartridge fills $4000-$BFFF. The 5200 BIOS jumps to the address at $BFFE-F
|
||||
(duplicated at $BFE8-9, with $BFFC=0 / $BFFD=$FF) -- verified in MAME a5200.
|
||||
|
||||
ANTIC DMAs the bitmap and display list straight from cartridge ROM, so we only
|
||||
place them at fixed cart addresses and point ANTIC at them:
|
||||
$4000 viewer code
|
||||
$4100 GTIA register script (offset, value pairs, $FF-terminated)
|
||||
$4200 display list (ANTIC reads it here)
|
||||
$6000 bitmap (the converter's 4K-split blob -> $6000 / $7000)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
VIEWER_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
CART_BASE = 0x4000
|
||||
CART_SIZE = 0x8000
|
||||
SCRIPT_ADDR = 0x4100
|
||||
DLIST_ADDR = 0x4200
|
||||
BITMAP_ADDR = 0x6000 # 4K-aligned: blob $4000/$5000 -> $6000/$7000
|
||||
SPLIT_LINE = 102 # matches atari _common.split_screen
|
||||
LINES = 192
|
||||
|
||||
# ANTIC mode byte + GTIA register script per display mode. Script entries are
|
||||
# (GTIA offset from $C000, colour value); colours come from the converter blob.
|
||||
ANTIC_MODE = {"gr15": 0x0E, "gr8": 0x0F, "gr9": 0x0F}
|
||||
|
||||
WAIT_MODES = {"forever": 0, "key": 1, "seconds": 2}
|
||||
|
||||
|
||||
def _gtia_script(mode: str, colors) -> bytes:
|
||||
c = list(colors)
|
||||
if mode == "gr15": # 4 colours: COLBK, COLPF0, COLPF1, COLPF2
|
||||
regs = [(0x1A, c[0]), (0x16, c[1]), (0x17, c[2]), (0x18, c[3]), (0x1B, 0x00)]
|
||||
elif mode == "gr8": # 2 colours: background (COLPF2+COLBK), fg (COLPF1)
|
||||
regs = [(0x18, c[0]), (0x1A, c[0]), (0x17, c[1]), (0x1B, 0x00)]
|
||||
else: # gr9: hue in COLBK, GTIA mode 9 via PRIOR
|
||||
regs = [(0x1A, c[0]), (0x1B, 0x40)]
|
||||
out = bytearray()
|
||||
for off, val in regs:
|
||||
out += bytes([off & 0xFF, val & 0xFF])
|
||||
out.append(0xFF) # terminator
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def _dlist(mode: str) -> bytes:
|
||||
m = ANTIC_MODE[mode]
|
||||
dl = bytearray([0x70, 0x70, 0x70]) # 24 blank scan lines
|
||||
dl += bytes([0x40 | m, 0x00, 0x60]) # LMS -> $6000
|
||||
dl += bytes([m] * (SPLIT_LINE - 1)) # 102 lines from $6000
|
||||
dl += bytes([0x40 | m, 0x00, 0x70]) # LMS -> $7000
|
||||
dl += bytes([m] * (LINES - SPLIT_LINE - 1)) # 90 lines from $7000
|
||||
dl += bytes([0x41, DLIST_ADDR & 0xFF, DLIST_ADDR >> 8]) # JVB -> dlist (loop)
|
||||
return bytes(dl)
|
||||
|
||||
|
||||
class AssemblerError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def have_xa() -> bool:
|
||||
return shutil.which("xa") is not None
|
||||
|
||||
|
||||
def _assemble(display: str = "forever", seconds: int = 0,
|
||||
video: str = "ntsc") -> 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)
|
||||
rate = 50 if video == "pal" else 60
|
||||
wrapper = (f"#define SCRIPT ${SCRIPT_ADDR:04X}\n"
|
||||
f"#define DLIST ${DLIST_ADDR:04X}\n"
|
||||
f"#define WAITMODE {waitmode}\n"
|
||||
f"#define WAITSECS {max(1, int(seconds))}\n"
|
||||
f"#define RATE {rate}\n"
|
||||
'#include "viewer.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:
|
||||
return f.read()
|
||||
finally:
|
||||
os.unlink(wrap)
|
||||
|
||||
|
||||
def build_cart(mode: str, data: bytes, display: str = "forever",
|
||||
seconds: int = 0, video: str = "ntsc") -> bytes:
|
||||
"""data = the atari converter blob (bitmap 4K-split at offset 0/0x1000, colour
|
||||
register bytes from offset 0x2000)."""
|
||||
if mode not in ANTIC_MODE:
|
||||
raise ValueError(f"unsupported 5200 mode {mode}")
|
||||
code = _assemble(display, seconds, video)
|
||||
bitmap = data[:0x2000]
|
||||
colors = data[0x2000:]
|
||||
script = _gtia_script(mode, colors)
|
||||
dlist = _dlist(mode)
|
||||
|
||||
rom = bytearray(b"\xff" * CART_SIZE)
|
||||
def place(addr, blob):
|
||||
off = addr - CART_BASE
|
||||
rom[off:off + len(blob)] = blob
|
||||
place(CART_BASE, code)
|
||||
place(SCRIPT_ADDR, script)
|
||||
place(DLIST_ADDR, dlist)
|
||||
place(BITMAP_ADDR, bitmap)
|
||||
|
||||
# cartridge header (verified in MAME a5200)
|
||||
def put16(addr, val):
|
||||
off = addr - CART_BASE
|
||||
rom[off] = val & 0xFF
|
||||
rom[off + 1] = (val >> 8) & 0xFF
|
||||
put16(0xBFFE, CART_BASE) # start address -- BIOS jumps here
|
||||
put16(0xBFE8, CART_BASE) # duplicate (validation)
|
||||
rom[0xBFFC - CART_BASE] = 0x00
|
||||
rom[0xBFFD - CART_BASE] = 0xFF
|
||||
return bytes(rom)
|
||||
53
lenser/a5200/viewer/awyt5200.i
Normal file
53
lenser/a5200/viewer/awyt5200.i
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
; Atari 5200 "how long to show the picture" epilogue (no OS, pure hardware).
|
||||
; Selected at assembly time by WAITMODE (set by a5200/viewer/assemble.py):
|
||||
; 0 forever -- just loop
|
||||
; 1 until a button -- poll the keypad (POKEY) and triggers (GTIA), then reset
|
||||
; 2 WAITSECS secs -- count frames via ANTIC VCOUNT, then reset
|
||||
; "Exit" jumps through the BIOS reset vector, which re-runs the cartridge and so
|
||||
; re-displays the picture (a cartridge console has no OS to return to).
|
||||
|
||||
#if WAITMODE == 0
|
||||
awloop:
|
||||
jmp awloop
|
||||
#endif
|
||||
|
||||
#if WAITMODE == 1
|
||||
lda #$03
|
||||
sta $e80f ; POKEY SKCTL -- enable keypad scanning
|
||||
awloop:
|
||||
lda $c010 ; GTIA TRIG0 (fire button); 0 = pressed
|
||||
and #$01
|
||||
beq awexit
|
||||
lda $e80f ; POKEY SKSTAT; bit2 = 0 while a keypad key is down
|
||||
and #$04
|
||||
bne awloop
|
||||
awexit:
|
||||
jmp ($fffc) ; BIOS reset -> re-display
|
||||
#endif
|
||||
|
||||
#if WAITMODE == 2
|
||||
lda #<(WAITSECS*RATE)
|
||||
sta $80
|
||||
lda #>(WAITSECS*RATE)
|
||||
sta $81
|
||||
awloop:
|
||||
lda $80
|
||||
ora $81
|
||||
beq awexit ; counted all the frames
|
||||
vw1:
|
||||
lda $d40b ; ANTIC VCOUNT
|
||||
bne vw1 ; wait for top of frame (VCOUNT = 0)
|
||||
vw2:
|
||||
lda $d40b
|
||||
beq vw2 ; wait until it leaves the top -> one frame elapsed
|
||||
lda $80
|
||||
sec
|
||||
sbc #$01
|
||||
sta $80
|
||||
lda $81
|
||||
sbc #$00
|
||||
sta $81
|
||||
jmp awloop
|
||||
awexit:
|
||||
jmp ($fffc) ; BIOS reset -> re-display
|
||||
#endif
|
||||
51
lenser/a5200/viewer/viewer.s
Normal file
51
lenser/a5200/viewer/viewer.s
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
; Atari 5200 image viewer -- self-contained, no OS.
|
||||
;
|
||||
; The 5200 has no operating system (only a small BIOS that jumps straight to the
|
||||
; cartridge), so this code programmes ANTIC and GTIA hardware registers directly.
|
||||
; ANTIC DMAs the bitmap and display list straight out of cartridge ROM, so nothing
|
||||
; needs copying to RAM -- the viewer only applies the GTIA colour registers, points
|
||||
; ANTIC at the display list, enables DMA, and holds.
|
||||
;
|
||||
; GTIA is at $C000 on the 5200 (not $D000 as on the 400/800); ANTIC is at $D400.
|
||||
;
|
||||
; #defines from viewer/assemble.py --
|
||||
; DLIST cartridge address of the display list
|
||||
; SCRIPT cartridge address of the GTIA register script -- (offset, value)
|
||||
; byte pairs applied as $C000+offset = value, terminated by $FF
|
||||
|
||||
* = $4000
|
||||
|
||||
start:
|
||||
sei
|
||||
cld
|
||||
ldx #$ff
|
||||
txs
|
||||
lda #$00
|
||||
sta $d40e ; NMIEN = 0 -- no interrupts
|
||||
sta $d400 ; DMACTL = 0 while we set up
|
||||
|
||||
; ---- apply the GTIA register script (offset, value pairs) ----
|
||||
ldx #$00
|
||||
sloop:
|
||||
lda SCRIPT,x
|
||||
cmp #$ff
|
||||
beq sdone
|
||||
tay ; Y = register offset within GTIA
|
||||
inx
|
||||
lda SCRIPT,x
|
||||
sta $c000,y ; GTIA register = value
|
||||
inx
|
||||
jmp sloop
|
||||
sdone:
|
||||
; ---- point ANTIC at the display list ----
|
||||
lda #<DLIST
|
||||
sta $d402 ; DLISTL
|
||||
lda #>DLIST
|
||||
sta $d403 ; DLISTH
|
||||
|
||||
; ---- enable DMA -- normal playfield + display-list DMA ----
|
||||
lda #$22
|
||||
sta $d400 ; DMACTL
|
||||
|
||||
; ---- hold the picture (forever / until a button / N seconds) ----
|
||||
#include "awyt5200.i"
|
||||
Loading…
Add table
Add a link
Reference in a new issue