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