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