70 lines
2.5 KiB
Python
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)
|