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 INTENSITY = 255 14 15 LINES_PER_ROW = 8 16 MAX_HEIGHT = 256 17 SCREEN_LIMIT = 0x8000 18 MAX_MEMORY = 0x10000 19 BLANK = (0, 0, 0) 20 21 def update(screen, ula): 22 23 """ 24 Update the 'screen' array by reading from the 'ula'. 25 """ 26 27 ula.vsync() 28 y = 0 29 while y < HEIGHT: 30 x = 0 31 while x < WIDTH: 32 colour = ula.get_pixel_colour() 33 pixel = tuple(map(lambda x: x * INTENSITY, colour)) 34 screen[x][y] = pixel 35 x += 1 36 ula.hsync() 37 y += 1 38 39 class ULA: 40 41 "The ULA functionality." 42 43 modes = [ 44 (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) 45 (640, 1, 25), (320, 1, 32), (160, 2, 32), 46 (320, 1, 25) 47 ] 48 49 palette = range(0, 8) * 2 50 51 def __init__(self, memory): 52 53 "Initialise the ULA with the given 'memory'." 54 55 self.memory = memory 56 self.set_mode(6) 57 58 # Internal state. 59 60 self.buffer = [0] * 8 61 62 def set_mode(self, mode): 63 64 """ 65 For the given 'mode', initialise the... 66 67 * width in pixels 68 * colour depth in bits per pixel 69 * number of character rows 70 * character row size in bytes 71 * screen size in bytes 72 * default screen start address 73 * horizontal pixel scaling factor 74 * vertical pixel scaling factor 75 * line spacing in pixels 76 * number of entries in the pixel buffer 77 """ 78 79 self.width, self.depth, rows = self.modes[mode] 80 81 row_size = (self.width * self.depth * LINES_PER_ROW) / 8 # bits per row -> bytes per row 82 83 # Memory access configuration. 84 85 self.screen_size = row_size * rows 86 self.screen_start = (SCREEN_LIMIT - self.screen_size) & 0xff00 # limitation on positioning 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 = 0 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(self.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 def get_physical_colour(value): 221 222 """ 223 Return the physical colour as an RGB triple for the given 'value'. 224 """ 225 226 return value & 1, value >> 1 & 1, value >> 2 & 1 227 228 def decode(value, depth): 229 230 """ 231 Decode the given byte 'value' according to the 'depth' in bits per pixel, 232 returning a sequence of pixel values. 233 """ 234 235 if depth == 1: 236 return (value >> 7, value >> 6 & 1, value >> 5 & 1, value >> 4 & 1, 237 value >> 3 & 1, value >> 2 & 1, value >> 1 & 1, value & 1) 238 elif depth == 2: 239 return (value >> 6 & 2 | value >> 3 & 1, value >> 5 & 2 | value >> 2 & 1, 240 value >> 4 & 2 | value >> 1 & 1, value >> 3 & 2 | value & 1) 241 elif depth == 4: 242 return (value >> 4 & 8 | value >> 3 & 4 | value >> 2 & 2 | value >> 1 & 1, 243 value >> 3 & 8 | value >> 2 & 4 | value >> 1 & 2 | value & 1) 244 else: 245 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 246 247 # Convenience functions. 248 249 def encode(values, depth): 250 251 """ 252 Encode the given 'values' according to the 'depth' in bits per pixel, 253 returning a byte value for the pixels. 254 """ 255 256 result = 0 257 258 if depth == 1: 259 for value in values: 260 result = result << 1 | (value & 1) 261 elif depth == 2: 262 for value in values: 263 result = result << 1 | (value & 2) << 3 | (value & 1) 264 elif depth == 4: 265 for value in values: 266 result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) 267 else: 268 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 269 270 return result 271 272 def fill(memory, start, end, value): 273 for i in xrange(start, end): 274 memory[i] = value 275 276 def mainloop(): 277 while 1: 278 pygame.display.flip() 279 event = pygame.event.wait() 280 if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: 281 break 282 283 if __name__ == "__main__": 284 pygame.init() 285 screen = pygame.display.set_mode((WIDTH, HEIGHT), 0) 286 287 memory = array.array("B", itertools.repeat(0, MAX_MEMORY)) 288 a = pygame.surfarray.pixels3d(screen) 289 290 ula = ULA(memory) 291 292 # Test MODE 2. 293 294 ula.set_mode(2) 295 296 fill(memory, 0x3000, 0x5800 - 320, encode((1, 6), 4)) 297 fill(memory, 0x5800 - 320, 0x8000, encode((2, 7), 4)) 298 update(a, ula) 299 mainloop() 300 301 ula.screen_start = 0x5800 - 320 302 update(a, ula) 303 mainloop() 304 305 # Test MODE 6. 306 307 ula.set_mode(6) 308 309 fill(memory, 0x6000, 0x6f00 + 160, encode((1, 0, 1, 1, 0, 0, 1, 1), 1)) 310 fill(memory, 0x6f00 + 160, 0x7f40, encode((1, 0, 1, 0, 1, 0, 1, 0), 1)) 311 update(a, ula) 312 mainloop() 313 314 ula.screen_start = 0x6f00 + 160 315 update(a, ula) 316 mainloop() 317 318 # vim: tabstop=4 expandtab shiftwidth=4