1 #!/usr/bin/env python 2 3 """ 4 Acorn Electron ULA simulation. 5 """ 6 7 import array 8 import itertools 9 10 WIDTH = 640 11 HEIGHT = 512 12 INTENSITY = 255 13 14 LINES_PER_ROW = 8 15 MAX_HEIGHT = 256 16 SCREEN_LIMIT = 0x8000 17 MAX_MEMORY = 0x10000 18 BLANK = (0, 0, 0) 19 20 def update(screen, ula): 21 22 """ 23 Update the 'screen' array by reading from the 'ula'. 24 """ 25 26 ula.vsync() 27 y = 0 28 while y < HEIGHT: 29 x = 0 30 while x < WIDTH: 31 colour = ula.get_pixel_colour() 32 pixel = tuple(map(lambda x: x * INTENSITY, colour)) 33 screen[x][y] = pixel 34 x += 1 35 ula.hsync() 36 y += 1 37 38 class ULA: 39 40 "The ULA functionality." 41 42 modes = [ 43 (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) 44 (640, 1, 25), (320, 1, 32), (160, 2, 32), 45 (320, 1, 25) 46 ] 47 48 palette = range(0, 8) * 2 49 50 def __init__(self, memory): 51 52 "Initialise the ULA with the given 'memory'." 53 54 self.memory = memory 55 self.set_mode(6) 56 57 # Internal state. 58 59 self.buffer = [(0, 0, 0)] * 8 60 61 def set_mode(self, mode): 62 63 """ 64 For the given 'mode', initialise the... 65 66 * width in pixels 67 * colour depth in bits per pixel 68 * number of character rows 69 * character row size in bytes 70 * screen size in bytes 71 * default screen start address 72 * horizontal pixel scaling factor 73 * vertical pixel scaling factor 74 * line spacing in pixels 75 * number of entries in the pixel buffer 76 """ 77 78 self.width, self.depth, rows = ULA.modes[mode] 79 80 row_size = (self.width * self.depth * LINES_PER_ROW) / 8 # bits per row -> bytes per row 81 82 # Memory access configuration. 83 # Note the limitation on positioning the screen start. 84 85 screen_size = row_size * rows 86 self.screen_start = (SCREEN_LIMIT - screen_size) & 0xff00 87 self.screen_size = SCREEN_LIMIT - self.screen_start 88 89 # Scanline configuration. 90 91 self.xscale = WIDTH / self.width # pixel width in display pixels 92 self.yscale = HEIGHT / (rows * LINES_PER_ROW) # pixel height in display pixels 93 94 self.spacing = MAX_HEIGHT / rows - LINES_PER_ROW # pixels between rows 95 96 # Start of unused region. 97 98 self.footer = rows * LINES_PER_ROW 99 self.margin = MAX_HEIGHT - rows * (LINES_PER_ROW + self.spacing) + self.spacing 100 101 # Internal pixel buffer size. 102 103 self.buffer_limit = 8 / self.depth 104 105 def vsync(self): 106 107 "Signal the start of a frame." 108 109 self.line_start = self.address = self.screen_start 110 self.line = self.line_start % LINES_PER_ROW 111 self.ysub = 0 112 self.ssub = 0 113 self.reset_horizontal() 114 115 def reset_horizontal(self): 116 117 "Reset horizontal state." 118 119 self.xsub = 0 120 self.buffer_index = self.buffer_limit # need refill 121 122 def hsync(self): 123 124 "Signal the end of a line." 125 126 # Support spacing between character rows. 127 128 if self.ssub: 129 self.ssub -= 1 130 return 131 132 self.reset_horizontal() 133 134 # Scale pixels vertically. 135 136 self.ysub += 1 137 138 # Re-read the current line if appropriate. 139 140 if self.ysub < self.yscale: 141 self.address = self.line_start 142 return 143 144 # Otherwise, move on to the next line. 145 146 self.ysub = 0 147 self.line += 1 148 149 # If not on a row boundary, move to the next line. 150 151 if self.line % LINES_PER_ROW: 152 self.address = self.line_start + 1 153 self.wrap_address() 154 155 # After the end of the last line in a row, the address should already 156 # have been positioned on the last line of the next column. 157 158 else: 159 self.address -= LINES_PER_ROW - 1 160 self.wrap_address() 161 162 # Test for the footer region. 163 164 if self.spacing and self.line == self.footer: 165 self.ssub = self.margin * self.yscale 166 return 167 168 # Support spacing between character rows. 169 170 self.ssub = self.spacing * self.yscale 171 172 self.line_start = self.address 173 174 def get_pixel_colour(self): 175 176 """ 177 Return a pixel colour by reading from the pixel buffer. 178 """ 179 180 # Detect spacing between character rows. 181 182 if self.ssub: 183 return BLANK 184 185 # Scale pixels horizontally. 186 187 if self.xsub == self.xscale: 188 self.xsub = 0 189 self.buffer_index += 1 190 191 if self.buffer_index == self.buffer_limit: 192 self.buffer_index = 0 193 self.fill_pixel_buffer() 194 195 self.xsub += 1 196 return self.buffer[self.buffer_index] 197 198 def fill_pixel_buffer(self): 199 200 """ 201 Fill the pixel buffer by translating memory content for the current 202 mode. 203 """ 204 205 byte_value = self.memory[self.address] 206 207 i = 0 208 for colour in decode(byte_value, self.depth): 209 self.buffer[i] = get_physical_colour(ULA.palette[colour]) 210 i += 1 211 212 # Advance to the next column. 213 214 self.address += LINES_PER_ROW 215 self.wrap_address() 216 217 def wrap_address(self): 218 if self.address >= SCREEN_LIMIT: 219 self.address -= self.screen_size 220 221 def get_physical_colour(value): 222 223 """ 224 Return the physical colour as an RGB triple for the given 'value'. 225 """ 226 227 return value & 1, value >> 1 & 1, value >> 2 & 1 228 229 def decode(value, depth): 230 231 """ 232 Decode the given byte 'value' according to the 'depth' in bits per pixel, 233 returning a sequence of pixel values. 234 """ 235 236 if depth == 1: 237 return (value >> 7, value >> 6 & 1, value >> 5 & 1, value >> 4 & 1, 238 value >> 3 & 1, value >> 2 & 1, value >> 1 & 1, value & 1) 239 elif depth == 2: 240 return (value >> 6 & 2 | value >> 3 & 1, value >> 5 & 2 | value >> 2 & 1, 241 value >> 4 & 2 | value >> 1 & 1, value >> 3 & 2 | value & 1) 242 elif depth == 4: 243 return (value >> 4 & 8 | value >> 3 & 4 | value >> 2 & 2 | value >> 1 & 1, 244 value >> 3 & 8 | value >> 2 & 4 | value >> 1 & 2 | value & 1) 245 else: 246 raise ValueError("Only depths of 1, 2 and 4 are supported, not %d." % depth) 247 248 # Convenience functions. 249 250 def encode(values, depth): 251 252 """ 253 Encode the given 'values' according to the 'depth' in bits per pixel, 254 returning a byte value for the pixels. 255 """ 256 257 result = 0 258 259 if depth == 1: 260 for value in values: 261 result = result << 1 | (value & 1) 262 elif depth == 2: 263 for value in values: 264 result = result << 1 | (value & 2) << 3 | (value & 1) 265 elif depth == 4: 266 for value in values: 267 result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) 268 else: 269 raise ValueError("Only depths of 1, 2 and 4 are supported, not %d." % depth) 270 271 return result 272 273 def get_memory(): 274 275 "Return an array representing the computer's memory." 276 277 return array.array("B", itertools.repeat(0, MAX_MEMORY)) 278 279 def get_screen(): 280 281 "Return a list of arrays representing the display." 282 283 x = 0 284 screen = [] 285 while x < WIDTH: 286 y = 0 287 column = [] 288 while y < HEIGHT: 289 column.append((0, 0, 0)) 290 y += 1 291 screen.append(column) 292 x += 1 293 return screen 294 295 def fill(memory, start, end, value): 296 for i in xrange(start, end): 297 memory[i] = value 298 299 # Test program providing coverage (necessary for compilers like Shedskin). 300 # NOTE: Running this will actually cause an IndexError. 301 302 if __name__ == "__main__": 303 memory = get_memory() 304 ula = ULA(memory) 305 ula.set_mode(6) 306 fill(memory, 0x6000, 0x8000, encode((1, 0, 1, 0, 1, 0, 1, 0), 1)) 307 308 # Make a simple two-dimensional array of tuples (three-dimensional in pygame 309 # terminology). 310 311 a = get_screen() 312 update(a, ula) 313 314 # vim: tabstop=4 expandtab shiftwidth=4