# HG changeset patch # User Paul Boddie # Date 1322957860 -3600 # Node ID d6b8cd0ac739c8821f72f108bf3cabc27516baa4 # Parent 9584cf16614a0d2c1ac8c7fecfb9cb221cf88fe8 Added a module which simulates the video functions of the Acorn Electron ULA. diff -r 9584cf16614a -r d6b8cd0ac739 ula.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ula.py Sun Dec 04 01:17:40 2011 +0100 @@ -0,0 +1,205 @@ +#!/usr/bin/env python + +""" +Acorn Electron ULA simulation. +""" + +import pygame +import array +import itertools + +WIDTH = 640 +HEIGHT = 512 + +LINES_PER_ROW = 8 +MAX_HEIGHT = 256 +SCREEN_LIMIT = 0x8000 +MAX_MEMORY = 0x10000 +INTENSITY = 255 + +palette = range(0, 8) * 2 + +def get_mode((width, depth, rows)): + + """ + Return the 'width', 'depth', 'rows', calculated character row size in + bytes, screen size in bytes, horizontal pixel scaling, vertical pixel + spacing, and line spacing between character rows, as elements of a tuple for + a particular screen mode. + """ + + return (width, depth, rows, + (width * depth * LINES_PER_ROW) / 8, # bits per row -> bytes per row + (width * depth * MAX_HEIGHT) / 8, # bits per screen -> bytes per screen + WIDTH / width, # pixel width in display pixels + HEIGHT / (rows * LINES_PER_ROW), # pixel height in display pixels + MAX_HEIGHT / rows - LINES_PER_ROW) # pixels between rows + +modes = map(get_mode, [ + (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) + (640, 1, 24), (320, 1, 32), (160, 2, 32), + (320, 1, 24) + ]) + +def update(screen, memory, start, mode): + + """ + Update the 'screen' array by reading from 'memory' at the given 'start' + address for the given 'mode'. + """ + + # Get the width in pixels, colour depth in bits per pixel, number of + # character rows, character row size in bytes, screen size in bytes, + # horizontal pixel scaling factor, vertical pixel scaling factor, and line + # spacing in pixels. + + width, depth, rows, row_size, screen_size, xscale, yscale, spacing = modes[mode] + + address = start + y = 0 + row = 0 + + while row < rows: + row_start = address + line = 0 + + # Emit each character row. + + while line < LINES_PER_ROW: + line_start = address + ysub = 0 + + # Scale each row of pixels vertically. + + while ysub < yscale: + x = 0 + + # Emit each row of pixels. + + while x < WIDTH: + byte_value = memory[address] + + for colour in decode(byte_value, depth): + colour = get_physical_colour(palette[colour]) + pixel = tuple(map(lambda x: x * INTENSITY, colour)) + + # Scale the pixels horizontally. + + xsub = 0 + while xsub < xscale: + screen[x][y] = pixel + xsub += 1 + x += 1 + + # Advance to the next column. + + address += LINES_PER_ROW + address = wrap_address(address, screen_size) + + ysub += 1 + y += 1 + + # Return to the address at the start of the line in order to be + # able to repeat the line. + + address = line_start + + # Move on to the next line in the row. + + line += 1 + address += 1 + + # Skip spacing between rows. + + if spacing: + y += spacing * yscale + + # Move on to the next row. + + address = row_start + row_size + address = wrap_address(address, screen_size) + + row += 1 + +def wrap_address(address, screen_size): + if address >= SCREEN_LIMIT: + address -= screen_size + return address + +def get_physical_colour(value): + + """ + Return the physical colour as an RGB triple for the given 'value'. + """ + + return value & 1, value >> 1 & 1, value >> 2 & 1 + +def decode(value, depth): + + """ + Decode the given byte 'value' according to the 'depth' in bits per pixel, + returning a sequence of pixel values. + """ + + if depth == 1: + return (value >> 7, value >> 6 & 1, value >> 5 & 1, value >> 4 & 1, + value >> 3 & 1, value >> 2 & 1, value >> 1 & 1, value & 1) + elif depth == 2: + return (value >> 6 & 2 | value >> 3 & 1, value >> 5 & 2 | value >> 2 & 1, + value >> 4 & 2 | value >> 1 & 1, value >> 3 & 2 | value & 1) + elif depth == 4: + return (value >> 4 & 8 | value >> 3 & 4 | value >> 2 & 2 | value >> 1 & 1, + value >> 3 & 8 | value >> 2 & 4 | value >> 1 & 2 | value & 1) + else: + raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth + +# Convenience functions. + +def encode(values, depth): + + """ + Encode the given 'values' according to the 'depth' in bits per pixel, + returning a byte value for the pixels. + """ + + result = 0 + + if depth == 1: + for value in values: + result = result << 1 | (value & 1) + elif depth == 2: + for value in values: + result = result << 1 | (value & 2) << 3 | (value & 1) + elif depth == 4: + for value in values: + result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) + else: + raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth + + return result + +def fill(memory, start, end, value): + for i in xrange(start, end): + memory[i] = value + +def mainloop(): + while 1: + pygame.display.flip() + event = pygame.event.wait() + if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: + break + +if __name__ == "__main__": + pygame.init() + screen = pygame.display.set_mode((WIDTH, HEIGHT), 0) + + memory = array.array("B", itertools.repeat(0, MAX_MEMORY)) + a = pygame.surfarray.pixels3d(screen) + + # Test MODE 2. + + fill(memory, 0x3000, 0x8000, encode((1, 6), 4)) + update(a, memory, 0x3000, 2) + mainloop() + +# vim: tabstop=4 expandtab shiftwidth=4