1 #!/usr/bin/env python 2 3 """ 4 Acorn Electron ULA simulation. 5 """ 6 7 import pygame 8 import array 9 import itertools 10 11 WIDTH = 640 12 HEIGHT = 512 13 14 LINES_PER_ROW = 8 15 MAX_HEIGHT = 256 16 SCREEN_LIMIT = 0x8000 17 MAX_MEMORY = 0x10000 18 INTENSITY = 255 19 20 palette = range(0, 8) * 2 21 22 def get_mode((width, depth, rows)): 23 24 """ 25 Return the 'width', 'depth', 'rows', calculated character row size in 26 bytes, screen size in bytes, horizontal pixel scaling, vertical pixel 27 spacing, and line spacing between character rows, as elements of a tuple for 28 a particular screen mode. 29 """ 30 31 return (width, depth, rows, 32 (width * depth * LINES_PER_ROW) / 8, # bits per row -> bytes per row 33 (width * depth * MAX_HEIGHT) / 8, # bits per screen -> bytes per screen 34 WIDTH / width, # pixel width in display pixels 35 HEIGHT / (rows * LINES_PER_ROW), # pixel height in display pixels 36 MAX_HEIGHT / rows - LINES_PER_ROW) # pixels between rows 37 38 modes = map(get_mode, [ 39 (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) 40 (640, 1, 24), (320, 1, 32), (160, 2, 32), 41 (320, 1, 24) 42 ]) 43 44 def update(screen, memory, start, mode): 45 46 """ 47 Update the 'screen' array by reading from 'memory' at the given 'start' 48 address for the given 'mode'. 49 """ 50 51 # Get the width in pixels, colour depth in bits per pixel, number of 52 # character rows, character row size in bytes, screen size in bytes, 53 # horizontal pixel scaling factor, vertical pixel scaling factor, and line 54 # spacing in pixels. 55 56 width, depth, rows, row_size, screen_size, xscale, yscale, spacing = modes[mode] 57 58 address = start 59 y = 0 60 row = 0 61 62 while row < rows: 63 row_start = address 64 line = 0 65 66 # Emit each character row. 67 68 while line < LINES_PER_ROW: 69 line_start = address 70 ysub = 0 71 72 # Scale each row of pixels vertically. 73 74 while ysub < yscale: 75 x = 0 76 77 # Emit each row of pixels. 78 79 while x < WIDTH: 80 byte_value = memory[address] 81 82 for colour in decode(byte_value, depth): 83 colour = get_physical_colour(palette[colour]) 84 pixel = tuple(map(lambda x: x * INTENSITY, colour)) 85 86 # Scale the pixels horizontally. 87 88 xsub = 0 89 while xsub < xscale: 90 screen[x][y] = pixel 91 xsub += 1 92 x += 1 93 94 # Advance to the next column. 95 96 address += LINES_PER_ROW 97 address = wrap_address(address, screen_size) 98 99 ysub += 1 100 y += 1 101 102 # Return to the address at the start of the line in order to be 103 # able to repeat the line. 104 105 address = line_start 106 107 # Move on to the next line in the row. 108 109 line += 1 110 address += 1 111 112 # Skip spacing between rows. 113 114 if spacing: 115 y += spacing * yscale 116 117 # Move on to the next row. 118 119 address = row_start + row_size 120 address = wrap_address(address, screen_size) 121 122 row += 1 123 124 def wrap_address(address, screen_size): 125 if address >= SCREEN_LIMIT: 126 address -= screen_size 127 return address 128 129 def get_physical_colour(value): 130 131 """ 132 Return the physical colour as an RGB triple for the given 'value'. 133 """ 134 135 return value & 1, value >> 1 & 1, value >> 2 & 1 136 137 def decode(value, depth): 138 139 """ 140 Decode the given byte 'value' according to the 'depth' in bits per pixel, 141 returning a sequence of pixel values. 142 """ 143 144 if depth == 1: 145 return (value >> 7, value >> 6 & 1, value >> 5 & 1, value >> 4 & 1, 146 value >> 3 & 1, value >> 2 & 1, value >> 1 & 1, value & 1) 147 elif depth == 2: 148 return (value >> 6 & 2 | value >> 3 & 1, value >> 5 & 2 | value >> 2 & 1, 149 value >> 4 & 2 | value >> 1 & 1, value >> 3 & 2 | value & 1) 150 elif depth == 4: 151 return (value >> 4 & 8 | value >> 3 & 4 | value >> 2 & 2 | value >> 1 & 1, 152 value >> 3 & 8 | value >> 2 & 4 | value >> 1 & 2 | value & 1) 153 else: 154 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 155 156 # Convenience functions. 157 158 def encode(values, depth): 159 160 """ 161 Encode the given 'values' according to the 'depth' in bits per pixel, 162 returning a byte value for the pixels. 163 """ 164 165 result = 0 166 167 if depth == 1: 168 for value in values: 169 result = result << 1 | (value & 1) 170 elif depth == 2: 171 for value in values: 172 result = result << 1 | (value & 2) << 3 | (value & 1) 173 elif depth == 4: 174 for value in values: 175 result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) 176 else: 177 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 178 179 return result 180 181 def fill(memory, start, end, value): 182 for i in xrange(start, end): 183 memory[i] = value 184 185 def mainloop(): 186 while 1: 187 pygame.display.flip() 188 event = pygame.event.wait() 189 if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: 190 break 191 192 if __name__ == "__main__": 193 pygame.init() 194 screen = pygame.display.set_mode((WIDTH, HEIGHT), 0) 195 196 memory = array.array("B", itertools.repeat(0, MAX_MEMORY)) 197 a = pygame.surfarray.pixels3d(screen) 198 199 # Test MODE 2. 200 201 fill(memory, 0x3000, 0x8000, encode((1, 6), 4)) 202 update(a, memory, 0x3000, 2) 203 mainloop() 204 205 # vim: tabstop=4 expandtab shiftwidth=4