8bitlenser/lenser/bbc/palette.py
2026-07-03 19:35:35 -07:00

70 lines
2.5 KiB
Python

"""BBC Micro (Video ULA + 6845) palette and screen packing.
8 physical colours -- pure digital RGB. Modes map 1/2/4 bits-per-pixel of
*logical* colour through a programmable palette (VDU 19) to these physicals.
Screen memory is character-cell interleaved: 8x8 cells ordered left-to-right then
top-to-bottom; within a cell the bytes go by scanline, and each scanline spans
1/2/4 bytes (2/4/8-colour). Pixel bits within a byte are interleaved, leftmost
pixel in the high bits.
"""
from __future__ import annotations
import numpy as np
from ..palette import srgb_to_lab
# Physical colours 0..7.
PHYS = np.array([
(0, 0, 0), # 0 black
(255, 0, 0), # 1 red
(0, 255, 0), # 2 green
(255, 255, 0), # 3 yellow
(0, 0, 255), # 4 blue
(255, 0, 255), # 5 magenta
(0, 255, 255), # 6 cyan
(255, 255, 255), # 7 white
], dtype=np.float64)
def phys_lab() -> np.ndarray:
return srgb_to_lab(PHYS)
def mono_lab() -> np.ndarray:
return srgb_to_lab(PHYS[[0, 7]]) # black + white
def _byte_for_pixels(vals, bits_per_pixel):
"""Encode the pixels covering one byte into the BBC interleaved layout.
Leftmost pixel uses the highest bit of each bit-plane group."""
n = len(vals) # pixels per byte (8/4/2)
b = 0
for bit in range(bits_per_pixel - 1, -1, -1): # high plane first
for i, v in enumerate(vals): # left pixel first
b = (b << 1) | ((v >> bit) & 1)
return b
def pack(idx: np.ndarray, width: int, bits_per_pixel: int) -> bytes:
"""Pack a (height, width) logical-colour array into BBC screen bytes.
The BBC layout is universal: one byte holds ``ppb`` horizontally-adjacent
pixels, and 8 consecutive bytes step down the 8 raster lines of that
byte-column before moving one byte-column to the right; whole character rows
(8 raster lines) then follow top-to-bottom. So
addr = char_row*(num_byte_cols*8) + byte_col*8 + raster
"""
h, w = idx.shape
ppb = 8 // bits_per_pixel # pixels per byte
num_byte_cols = w // ppb
row_stride = num_byte_cols * 8 # bytes per character row
out = bytearray((w * h) // ppb)
for y in range(h):
base = (y // 8) * row_stride + (y % 8)
for bc in range(num_byte_cols):
x0 = bc * ppb
vals = [int(idx[y, x0 + p]) for p in range(ppb)]
out[base + bc * 8] = _byte_for_pixels(vals, bits_per_pixel)
return bytes(out)