"""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)