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