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