First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
0
lenser/nes/__init__.py
Normal file
0
lenser/nes/__init__.py
Normal file
65
lenser/nes/cartridge.py
Normal file
65
lenser/nes/cartridge.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""Build an iNES (.nes) NROM cartridge: 6502 PPU-init viewer + image data + CHR.
|
||||
|
||||
Layout: 16-byte iNES header, 16K PRG-ROM (viewer at $C000, 32-byte palette at
|
||||
$F000, 1024-byte nametable+attributes at $F020, vectors at $FFFA), then 8K
|
||||
CHR-ROM (256 background tiles in pattern table 0).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
VIEWER_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
PRG_BASE = 0xC000
|
||||
PRG_SIZE = 0x4000 # 16K
|
||||
PAL_ADDR = 0xF000 # 32-byte palette
|
||||
NT_ADDR = 0xF020 # 1024-byte nametable + attribute
|
||||
|
||||
|
||||
def have_xa() -> bool:
|
||||
return shutil.which("xa") is not None
|
||||
|
||||
|
||||
def _assemble() -> bytes:
|
||||
if not have_xa():
|
||||
raise RuntimeError("The 'xa' (xa65) assembler was not found (apt install xa65).")
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
out = os.path.join(td, "v.bin")
|
||||
proc = subprocess.run(["xa", "-o", out, "viewer.s"],
|
||||
capture_output=True, text=True, cwd=VIEWER_DIR)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"xa failed:\n{proc.stdout}{proc.stderr}")
|
||||
with open(out, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def build_rom(palette: bytes, nametable: bytes, chr_rom: bytes) -> bytes:
|
||||
"""palette: 32 bytes; nametable: 1024 (960 names + 64 attr); chr_rom: 8192."""
|
||||
if len(palette) != 32 or len(nametable) != 1024 or len(chr_rom) != 8192:
|
||||
raise ValueError("bad NES data block sizes")
|
||||
code = _assemble() # 6502 viewer, org $C000
|
||||
if len(code) > PAL_ADDR - PRG_BASE:
|
||||
raise RuntimeError("viewer code overruns the $F000 data area")
|
||||
prg = bytearray(PRG_SIZE)
|
||||
prg[0:len(code)] = code # start == $C000
|
||||
prg[PAL_ADDR - PRG_BASE:PAL_ADDR - PRG_BASE + 32] = palette
|
||||
prg[NT_ADDR - PRG_BASE:NT_ADDR - PRG_BASE + 1024] = nametable
|
||||
# CPU vectors $FFFA/$FFFC/$FFFE all point at start ($C000)
|
||||
for off in (0x3FFA, 0x3FFC, 0x3FFE):
|
||||
prg[off] = PRG_BASE & 0xFF
|
||||
prg[off + 1] = PRG_BASE >> 8
|
||||
|
||||
header = bytes([0x4E, 0x45, 0x53, 0x1A, # "NES\x1A"
|
||||
1, # 16K PRG units
|
||||
1, # 8K CHR units
|
||||
0x00, 0x00] + [0] * 8) # NROM (mapper 0)
|
||||
return header + bytes(prg) + bytes(chr_rom)
|
||||
|
||||
|
||||
def write_nes(rom: bytes, path: str) -> str:
|
||||
with open(path, "wb") as f:
|
||||
f.write(rom)
|
||||
return path
|
||||
19
lenser/nes/convert/__init__.py
Normal file
19
lenser/nes/convert/__init__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
"""Nintendo Entertainment System conversion dispatch."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ... import imageprep
|
||||
from . import bg, mono
|
||||
|
||||
_MODULES = {"bg": bg, "mono": mono}
|
||||
MODES = list(_MODULES.keys())
|
||||
|
||||
|
||||
def convert_image(path_or_img, mode="bg", palette_name="nes",
|
||||
dither_mode="floyd", intensive=False, prep_opt=None,
|
||||
base_color=None):
|
||||
prep_opt = prep_opt or imageprep.PrepOptions()
|
||||
module = _MODULES.get(mode, bg)
|
||||
img_rgb = imageprep.prepare(path_or_img, module.WIDTH, module.HEIGHT,
|
||||
module.PIXEL_ASPECT, prep_opt, border_rgb=(0, 0, 0))
|
||||
return module.convert(img_rgb, palette_name, dither_mode, intensive,
|
||||
base_color=base_color)
|
||||
206
lenser/nes/convert/_common.py
Normal file
206
lenser/nes/convert/_common.py
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
"""NES background encoder: 4 sub-palettes + per-16x16 attribute + 256-tile CHR.
|
||||
|
||||
The NES PPU draws a 32x30 grid of 8x8 tiles (pattern table, <=256 unique), each
|
||||
tile 2bpp. Colour comes from 4 background sub-palettes (each = a shared universal
|
||||
background + 3 colours), one chosen per 16x16 region via the attribute table. So
|
||||
the pipeline is: choose the universal bg, cluster the image into 4 sub-palettes,
|
||||
assign each region its best one, dither, then vector-quantise the 8x8 tile
|
||||
patterns down to 256 CHR tiles.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither, palette as c64pal
|
||||
from ...convert import base
|
||||
from .. import palette as npal
|
||||
|
||||
W, H = 256, 240
|
||||
RCOLS, RROWS = 16, 15 # 16x16-pixel regions (2x2 tiles)
|
||||
TCOLS, TROWS = 32, 30 # 8x8 tiles
|
||||
NTILES = 256
|
||||
|
||||
|
||||
def _best_colors(pix_lab, plab, bg0, n, k_cand=20):
|
||||
"""Best ``n`` NES colours (besides fixed bg0) for a pool of pixels."""
|
||||
mean_d = np.sum((pix_lab.mean(0)[None, :] - plab) ** 2, 1)
|
||||
cand = [c for c in np.argsort(mean_d)[:k_cand] if c != bg0]
|
||||
dist = np.sum((pix_lab[:, None, :] - plab[None, cand, :]) ** 2, 2) # (px, k)
|
||||
d_bg = np.sum((pix_lab - plab[bg0]) ** 2, 1) # (px,)
|
||||
from itertools import combinations
|
||||
best, best_err = None, np.inf
|
||||
for combo in combinations(range(len(cand)), n):
|
||||
m = np.minimum(d_bg, dist[:, combo].min(1)).sum()
|
||||
if m < best_err:
|
||||
best_err, best = m, [cand[i] for i in combo]
|
||||
return best
|
||||
|
||||
|
||||
def _tile_codebook(patterns, k, iters=8):
|
||||
"""k-medoids over 8x8 pen patterns (values 0-3); distance = differing pixels."""
|
||||
uniq, counts = np.unique(patterns, axis=0, return_counts=True)
|
||||
if len(uniq) <= k:
|
||||
code = np.zeros((k, patterns.shape[1]), patterns.dtype)
|
||||
code[:len(uniq)] = uniq
|
||||
lut = {tuple(p): i for i, p in enumerate(uniq)}
|
||||
return code, np.array([lut[tuple(p)] for p in patterns])
|
||||
code = uniq[np.argsort(-counts)[:k]].copy()
|
||||
labels = np.zeros(len(patterns), np.int64)
|
||||
for _ in range(iters):
|
||||
for s in range(0, len(patterns), 2048):
|
||||
blk = patterns[s:s + 2048]
|
||||
labels[s:s + 2048] = (blk[:, None, :] != code[None]).sum(2).argmin(1)
|
||||
moved = False
|
||||
for j in range(k):
|
||||
mem = patterns[labels == j]
|
||||
if len(mem):
|
||||
med = np.array([np.bincount(mem[:, p], minlength=4).argmax()
|
||||
for p in range(mem.shape[1])], patterns.dtype)
|
||||
if not np.array_equal(med, code[j]):
|
||||
code[j] = med; moved = True
|
||||
if not moved:
|
||||
break
|
||||
for s in range(0, len(patterns), 2048):
|
||||
blk = patterns[s:s + 2048]
|
||||
labels[s:s + 2048] = (blk[:, None, :] != code[None]).sum(2).argmin(1)
|
||||
return code, labels
|
||||
|
||||
|
||||
def encode(img_rgb, dither_mode, subpalettes):
|
||||
"""subpalettes: list of 4 lists [bg0,c1,c2,c3] of NES colour indices (the
|
||||
universal bg = subpalettes[*][0], identical across all four)."""
|
||||
plab = npal.palette_lab()
|
||||
prgb = npal.get_palette().astype(np.uint8)
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb)
|
||||
bg0 = subpalettes[0][0]
|
||||
sp = np.array(subpalettes) # (4,4) NES indices
|
||||
|
||||
# assign each 16x16 region to the sub-palette giving least nearest-colour error
|
||||
region_pal = np.zeros((RROWS, RCOLS), np.int64)
|
||||
for ry in range(RROWS):
|
||||
for rx in range(RCOLS):
|
||||
blk = img_lab[ry * 16:ry * 16 + 16, rx * 16:rx * 16 + 16].reshape(-1, 3)
|
||||
errs = []
|
||||
for s in range(4):
|
||||
d = np.sum((blk[:, None, :] - plab[sp[s]][None, :, :]) ** 2, 2)
|
||||
errs.append(d.min(1).sum())
|
||||
region_pal[ry, rx] = int(np.argmin(errs))
|
||||
|
||||
# per-pixel allowed colours = the pixel's region sub-palette; dither
|
||||
allowed = np.zeros((H, W, 4), np.int64)
|
||||
for ry in range(RROWS):
|
||||
for rx in range(RCOLS):
|
||||
allowed[ry * 16:ry * 16 + 16, rx * 16:rx * 16 + 16] = sp[region_pal[ry, rx]]
|
||||
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.int64)
|
||||
|
||||
# pen per pixel = position of its colour within its region's sub-palette
|
||||
pen = np.zeros((H, W), np.uint8)
|
||||
for ry in range(RROWS):
|
||||
for rx in range(RCOLS):
|
||||
ys, xs = slice(ry * 16, ry * 16 + 16), slice(rx * 16, rx * 16 + 16)
|
||||
pal = sp[region_pal[ry, rx]]
|
||||
block = idx[ys, xs]
|
||||
pmap = np.zeros(block.shape, np.uint8)
|
||||
for k, col in enumerate(pal):
|
||||
pmap[block == col] = k
|
||||
pen[ys, xs] = pmap
|
||||
|
||||
# 8x8 tiles -> patterns; vector-quantise to <=256 CHR tiles
|
||||
tiles = pen.reshape(TROWS, 8, TCOLS, 8).transpose(0, 2, 1, 3).reshape(TROWS * TCOLS, 64)
|
||||
code, labels = _tile_codebook(tiles, NTILES)
|
||||
nametable = labels.astype(np.uint8).reshape(TROWS, TCOLS)
|
||||
|
||||
# ---- emit NES data ----
|
||||
chr_rom = bytearray(8192)
|
||||
for t in range(NTILES):
|
||||
pat = code[t].reshape(8, 8)
|
||||
for r in range(8):
|
||||
p0 = p1 = 0
|
||||
for x in range(8):
|
||||
v = int(pat[r, x])
|
||||
p0 |= (v & 1) << (7 - x)
|
||||
p1 |= ((v >> 1) & 1) << (7 - x)
|
||||
chr_rom[t * 16 + r] = p0
|
||||
chr_rom[t * 16 + 8 + r] = p1
|
||||
|
||||
attr = bytearray(64)
|
||||
for ar in range(8):
|
||||
for ac in range(8):
|
||||
b = 0
|
||||
for q, (dy, dx) in enumerate(((0, 0), (0, 1), (1, 0), (1, 1))):
|
||||
ry, rx = ar * 2 + dy, ac * 2 + dx
|
||||
s = int(region_pal[ry, rx]) if ry < RROWS and rx < RCOLS else 0
|
||||
b |= (s & 3) << (q * 2)
|
||||
attr[ar * 8 + ac] = b
|
||||
|
||||
pal32 = bytearray(32)
|
||||
for s in range(4):
|
||||
pal32[s * 4] = bg0
|
||||
pal32[s * 4 + 1] = int(sp[s][1])
|
||||
pal32[s * 4 + 2] = int(sp[s][2])
|
||||
pal32[s * 4 + 3] = int(sp[s][3])
|
||||
pal32[16:32] = pal32[0:16] # sprite palette = mirror
|
||||
nametable_full = bytes(nametable.reshape(-1)) + bytes(attr) # 960 + 64
|
||||
|
||||
# rebuild the displayed image (clustered tiles + region palettes) for preview
|
||||
disp = code[labels].reshape(TROWS, TCOLS, 8, 8).transpose(0, 2, 1, 3).reshape(H, W)
|
||||
final_idx = np.zeros((H, W), np.uint16)
|
||||
for ry in range(RROWS):
|
||||
for rx in range(RCOLS):
|
||||
ys, xs = slice(ry * 16, ry * 16 + 16), slice(rx * 16, rx * 16 + 16)
|
||||
pal = sp[region_pal[ry, rx]]
|
||||
final_idx[ys, xs] = pal[disp[ys, xs]]
|
||||
|
||||
err = base.perceptual_error(final_idx, img_lab, plab)
|
||||
return bytes(pal32), nametable_full, bytes(chr_rom), final_idx, err, plab, prgb
|
||||
|
||||
|
||||
def pick_subpalettes(img_rgb, n_groups=4, mono=False, base_color=None):
|
||||
"""Choose the universal bg + ``n_groups`` sub-palettes (each bg + 3 colours)."""
|
||||
plab = npal.palette_lab()
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb)
|
||||
|
||||
if mono:
|
||||
greys = sorted(npal.GREYS, key=lambda i: plab[i, 0])
|
||||
if base_color in range(64):
|
||||
ramp = sorted({greys[0], int(base_color), greys[-1]}, key=lambda i: plab[i, 0])
|
||||
else:
|
||||
# 4 greys spanning black->white (include the lightest so highlights
|
||||
# actually reach white -- otherwise the image comes out muddy/dark)
|
||||
lums = np.array([plab[i, 0] for i in greys])
|
||||
ramp = [greys[int(np.argmin(np.abs(lums - t)))]
|
||||
for t in np.linspace(lums.min(), lums.max(), 4)]
|
||||
bg0 = ramp[0]
|
||||
others = [c for c in ramp if c != bg0][:3]
|
||||
while len(others) < 3:
|
||||
others.append(others[-1])
|
||||
return [[bg0] + others] * 4
|
||||
|
||||
bg0 = base.best_global_color(img_lab, plab)
|
||||
# cluster regions by mean colour into n_groups, then pick 3 colours per group
|
||||
feats = []
|
||||
for ry in range(RROWS):
|
||||
for rx in range(RCOLS):
|
||||
feats.append(img_lab[ry * 16:ry * 16 + 16, rx * 16:rx * 16 + 16]
|
||||
.reshape(-1, 3).mean(0))
|
||||
feats = np.array(feats)
|
||||
rng = np.random.default_rng(0)
|
||||
cen = feats[rng.choice(len(feats), n_groups, replace=False)]
|
||||
for _ in range(12):
|
||||
lab = np.argmin(np.sum((feats[:, None, :] - cen[None]) ** 2, 2), 1)
|
||||
for g in range(n_groups):
|
||||
if (lab == g).any():
|
||||
cen[g] = feats[lab == g].mean(0)
|
||||
subs = []
|
||||
for g in range(n_groups):
|
||||
members = np.where(lab == g)[0]
|
||||
if len(members) == 0:
|
||||
subs.append([bg0, bg0, bg0, bg0]); continue
|
||||
pool = np.concatenate([
|
||||
img_lab[(m // RCOLS) * 16:(m // RCOLS) * 16 + 16,
|
||||
(m % RCOLS) * 16:(m % RCOLS) * 16 + 16].reshape(-1, 3)
|
||||
for m in members])
|
||||
if len(pool) > 4000:
|
||||
pool = pool[rng.choice(len(pool), 4000, replace=False)]
|
||||
subs.append([bg0] + _best_colors(pool, plab, bg0, 3))
|
||||
return subs
|
||||
21
lenser/nes/convert/bg.py
Normal file
21
lenser/nes/convert/bg.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
"""NES background image: 256x240, 4 sub-palettes (universal bg + 3 each), tiles."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ...convert.base import Conversion
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 256, 240
|
||||
PIXEL_ASPECT = 1.0 # NES pixels are ~square (8:7 really; close enough)
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="nes", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
subs = _common.pick_subpalettes(img_rgb, mono=False)
|
||||
pal32, nt, chr_rom, idx, err, plab, prgb = _common.encode(
|
||||
img_rgb, dither_mode, subs)
|
||||
return Conversion(
|
||||
mode="bg", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx, data={"palette": pal32, "nametable": nt, "chr": chr_rom},
|
||||
data_addr=0, viewer="nes", preview_rgb=prgb[idx], error=err,
|
||||
meta={"palette": "nes", "dither": dither_mode},
|
||||
)
|
||||
22
lenser/nes/convert/mono.py
Normal file
22
lenser/nes/convert/mono.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
"""NES monochrome: 256x240 greyscale using the PPU's grey ramp (tone by
|
||||
dithering). ``--mono-base`` tints the ramp toward a hue."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ...convert.base import Conversion
|
||||
from . import _common
|
||||
|
||||
WIDTH, HEIGHT = 256, 240
|
||||
PIXEL_ASPECT = 1.0
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="nes", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
subs = _common.pick_subpalettes(img_rgb, mono=True, base_color=base_color)
|
||||
pal32, nt, chr_rom, idx, err, plab, prgb = _common.encode(
|
||||
img_rgb, dither_mode, subs)
|
||||
return Conversion(
|
||||
mode="mono", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx, data={"palette": pal32, "nametable": nt, "chr": chr_rom},
|
||||
data_addr=0, viewer="nes", preview_rgb=prgb[idx], error=err,
|
||||
meta={"palette": "nes", "dither": dither_mode},
|
||||
)
|
||||
13
lenser/nes/exporter.py
Normal file
13
lenser/nes/exporter.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"""Build an NES (.nes) cartridge from a conversion."""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import cartridge
|
||||
|
||||
|
||||
def export_nes(conv, output_path, source_path=None, display="forever",
|
||||
seconds=0, video="ntsc"):
|
||||
if not output_path.lower().endswith((".nes", ".unf")):
|
||||
output_path += ".nes"
|
||||
d = conv.data
|
||||
rom = cartridge.build_rom(d["palette"], d["nametable"], d["chr"])
|
||||
return cartridge.write_nes(rom, output_path)
|
||||
58
lenser/nes/palette.py
Normal file
58
lenser/nes/palette.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"""Nintendo Entertainment System (2C02 PPU) master palette.
|
||||
|
||||
The NES has no fixed RGB palette -- the PPU generates an NTSC signal. We
|
||||
reproduce MAME's exact ``nespal_to_RGB`` (YUV->RGB) formula so the encoder
|
||||
matches what the emulator renders. A palette value is a 6-bit index 0-63:
|
||||
high nibble = luminance (0-3), low nibble = hue (0-15, with 0/13/14/15 greyscale).
|
||||
The byte written to PPU palette RAM IS this index, so ``PALETTE``/``GREYS`` are
|
||||
indexed by the hardware value.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..palette import srgb_to_lab
|
||||
|
||||
_TINT, _HUE = 0.22, 287.0
|
||||
_Kr, _Kb, _Ku, _Kv = 0.2989, 0.1145, 2.029, 1.140
|
||||
_BRIGHT = [[0.50, 0.75, 1.0, 1.0],
|
||||
[0.29, 0.45, 0.73, 0.9],
|
||||
[0.0, 0.24, 0.47, 0.77]]
|
||||
|
||||
|
||||
def _rgb(intensity: int, num: int):
|
||||
if num == 0:
|
||||
sat = rad = 0.0; y = _BRIGHT[0][intensity]
|
||||
elif num == 13:
|
||||
sat = rad = 0.0; y = _BRIGHT[2][intensity]
|
||||
elif num in (14, 15):
|
||||
sat = rad = y = 0.0
|
||||
else:
|
||||
sat = _TINT; rad = math.radians(num * 30 + _HUE); y = _BRIGHT[1][intensity]
|
||||
u, v = sat * math.cos(rad), sat * math.sin(rad)
|
||||
R = (y + _Kv * v) * 255.0
|
||||
G = (y - (_Kb * _Ku * u + _Kr * _Kv * v) / (1 - _Kb - _Kr)) * 255.0
|
||||
B = (y + _Ku * u) * 255.0
|
||||
cl = lambda x: max(0, min(255, int(math.floor(x + 0.5))))
|
||||
return (cl(R), cl(G), cl(B))
|
||||
|
||||
|
||||
PALETTE = np.array([_rgb(i >> 4, i & 0x0F) for i in range(64)], dtype=np.float64)
|
||||
|
||||
# Distinct grey ramp (R==G==B), sorted dark->light, deduped by luminance.
|
||||
_grey = {}
|
||||
for _i in range(64):
|
||||
r, g, b = PALETTE[_i]
|
||||
if r == g == b:
|
||||
_grey.setdefault(int(r), _i)
|
||||
GREYS = [_grey[k] for k in sorted(_grey)] # e.g. $0F,$1D,$2D,$10,$20
|
||||
|
||||
|
||||
def get_palette() -> np.ndarray:
|
||||
return PALETTE
|
||||
|
||||
|
||||
def palette_lab() -> np.ndarray:
|
||||
return srgb_to_lab(PALETTE)
|
||||
77
lenser/nes/viewer.s
Normal file
77
lenser/nes/viewer.s
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
; NES background image viewer (6502 / 2C02 PPU). NROM, 16K PRG at $C000.
|
||||
; Waits for PPU warm-up, loads the palette ($3F00, 32 bytes), nametable +
|
||||
; attribute table ($2000, 1024 bytes), enables background, then idles. The
|
||||
; image-specific data lives at fixed PRG addresses written by the builder-
|
||||
; $F000 32-byte palette $F020 1024-byte nametable + attributes
|
||||
; Tiles come from CHR-ROM pattern table 0.
|
||||
|
||||
* = $C000
|
||||
|
||||
PPUCTRL = $2000
|
||||
PPUMASK = $2001
|
||||
PPUSTATUS = $2002
|
||||
PPUSCROLL = $2005
|
||||
PPUADDR = $2006
|
||||
PPUDATA = $2007
|
||||
|
||||
start:
|
||||
sei
|
||||
cld
|
||||
ldx #$ff
|
||||
txs
|
||||
lda #$00
|
||||
sta PPUCTRL ; NMI off
|
||||
sta PPUMASK ; rendering off during setup
|
||||
|
||||
bit PPUSTATUS ; clear latch
|
||||
w1: bit PPUSTATUS
|
||||
bpl w1 ; wait for first vblank
|
||||
w2: bit PPUSTATUS
|
||||
bpl w2 ; wait for second vblank (PPU warmed up)
|
||||
|
||||
; ---- palette- $3F00..$3F1F from $F000 ----
|
||||
lda #$3f
|
||||
sta PPUADDR
|
||||
lda #$00
|
||||
sta PPUADDR
|
||||
ldx #$00
|
||||
pal: lda $f000,x
|
||||
sta PPUDATA
|
||||
inx
|
||||
cpx #$20
|
||||
bne pal
|
||||
|
||||
; ---- nametable + attributes- $2000..$23FF (1024) from $F020 ----
|
||||
lda #$20
|
||||
sta PPUADDR
|
||||
lda #$00
|
||||
sta PPUADDR
|
||||
ldx #$00
|
||||
nt0: lda $f020,x
|
||||
sta PPUDATA
|
||||
inx
|
||||
bne nt0
|
||||
nt1: lda $f120,x
|
||||
sta PPUDATA
|
||||
inx
|
||||
bne nt1
|
||||
nt2: lda $f220,x
|
||||
sta PPUDATA
|
||||
inx
|
||||
bne nt2
|
||||
nt3: lda $f320,x
|
||||
sta PPUDATA
|
||||
inx
|
||||
bne nt3
|
||||
|
||||
; ---- enable background ----
|
||||
lda #$00
|
||||
sta PPUSCROLL
|
||||
sta PPUSCROLL
|
||||
lda #$00
|
||||
sta PPUCTRL ; nametable $2000, bg pattern table $0000, NMI off
|
||||
lda #$0a
|
||||
sta PPUMASK ; show background, no left-column clip
|
||||
loop:
|
||||
jmp loop
|
||||
; the builder writes the $FFFA-$FFFF vectors (all -> start = $C000)
|
||||
Loading…
Add table
Add a link
Reference in a new issue