1 #!/usr/bin/env python 2 3 """ 4 Acorn Electron ULA simulation. 5 """ 6 7 LINES_PER_ROW = 8 # the number of pixel lines per character row 8 MAX_HEIGHT = 256 # the height of the screen in pixels 9 MAX_SCANLINE = 312 # the number of scanlines in each frame 10 MAX_WIDTH = 640 # the width of the screen in pixels 11 MAX_SCANPOS = 1024 # the number of positions in each scanline 12 SCREEN_LIMIT = 0x8000 # the first address after the screen memory 13 MAX_MEMORY = 0x10000 # the number of addressable memory locations 14 BLANK = (0, 0, 0) 15 16 def update(screen, ula): 17 18 """ 19 Update the 'screen' array by reading from the 'ula'. This function 20 effectively has the role of the video circuit, but also provides the clock 21 signal to the ULA. 22 """ 23 24 ula.vsync() 25 y = 0 26 while y < MAX_SCANLINE: 27 x = 0 28 while x < MAX_SCANPOS: 29 colour = ula.get_pixel_colour() 30 if x < MAX_WIDTH and y < MAX_HEIGHT: 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] * 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 * line spacing in pixels 72 * number of entries in the pixel buffer 73 """ 74 75 self.width, self.depth, rows = self.modes[mode] 76 77 self.columns = (self.width * self.depth) / 8 # bits read -> bytes read 78 row_size = self.columns * LINES_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 = MAX_WIDTH / self.width # pixel width in display pixels 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_SCANLINE - 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.ssub = 0 108 self.reset_horizontal() 109 110 def reset_horizontal(self): 111 112 "Reset horizontal state." 113 114 self.xsub = 0 115 self.column = 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 self.line += 1 130 131 # If not on a row boundary, move to the next line. 132 133 if self.line % LINES_PER_ROW: 134 self.address = self.line_start + 1 135 self.wrap_address() 136 137 # After the end of the last line in a row, the address should already 138 # have been positioned on the last line of the next column. 139 140 else: 141 self.address -= LINES_PER_ROW - 1 142 self.wrap_address() 143 144 # Test for the footer region. 145 146 if self.spacing and self.line == self.footer: 147 self.ssub = self.margin 148 return 149 150 # Support spacing between character rows. 151 152 self.ssub = self.spacing 153 154 self.line_start = self.address 155 156 def get_pixel_colour(self): 157 158 """ 159 Return a pixel colour by reading from the pixel buffer. 160 """ 161 162 # Detect spacing between character rows. 163 164 if self.ssub: 165 return BLANK 166 167 # Scale pixels horizontally. 168 169 if self.xsub == self.xscale: 170 self.xsub = 0 171 self.buffer_index += 1 172 173 if self.buffer_index == self.buffer_limit: 174 self.buffer_index = 0 175 self.column += 1 176 177 # Detect the end of the scanline. 178 179 if self.column > self.columns: 180 return BLANK 181 182 self.fill_pixel_buffer() 183 184 self.xsub += 1 185 return self.buffer[self.buffer_index] 186 187 def fill_pixel_buffer(self): 188 189 """ 190 Fill the pixel buffer by translating memory content for the current 191 mode. 192 """ 193 194 byte_value = self.memory[self.address] 195 196 i = 0 197 for colour in decode(byte_value, self.depth): 198 self.buffer[i] = get_physical_colour(self.palette[colour]) 199 i += 1 200 201 # Advance to the next column. 202 203 self.address += LINES_PER_ROW 204 self.wrap_address() 205 206 def wrap_address(self): 207 if self.address >= SCREEN_LIMIT: 208 self.address -= self.screen_size 209 210 # Convenience methods. 211 212 def fill(self, start, end, value): 213 fill(self.memory, start, end, value) 214 215 def get_physical_colour(value): 216 217 """ 218 Return the physical colour as an RGB triple for the given 'value'. 219 """ 220 221 return value & 1, value >> 1 & 1, value >> 2 & 1 222 223 def decode(value, depth): 224 225 """ 226 Decode the given byte 'value' according to the 'depth' in bits per pixel, 227 returning a sequence of pixel values. 228 """ 229 230 if depth == 1: 231 return (value >> 7, value >> 6 & 1, value >> 5 & 1, value >> 4 & 1, 232 value >> 3 & 1, value >> 2 & 1, value >> 1 & 1, value & 1) 233 elif depth == 2: 234 return (value >> 6 & 2 | value >> 3 & 1, value >> 5 & 2 | value >> 2 & 1, 235 value >> 4 & 2 | value >> 1 & 1, value >> 3 & 2 | value & 1) 236 elif depth == 4: 237 return (value >> 4 & 8 | value >> 3 & 4 | value >> 2 & 2 | value >> 1 & 1, 238 value >> 3 & 8 | value >> 2 & 4 | value >> 1 & 2 | value & 1) 239 else: 240 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 241 242 # Convenience functions. 243 244 def encode(values, depth): 245 246 """ 247 Encode the given 'values' according to the 'depth' in bits per pixel, 248 returning a byte value for the pixels. 249 """ 250 251 result = 0 252 253 if depth == 1: 254 for value in values: 255 result = result << 1 | (value & 1) 256 elif depth == 2: 257 for value in values: 258 result = result << 1 | (value & 2) << 3 | (value & 1) 259 elif depth == 4: 260 for value in values: 261 result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) 262 else: 263 raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth 264 265 return result 266 267 def get_ula(): 268 269 "Return a ULA initialised with a memory array." 270 271 return ULA(get_memory()) 272 273 def get_memory(): 274 275 "Return an array representing the computer's memory." 276 277 return [0] * MAX_MEMORY 278 279 def get_screen(): 280 281 "Return a list of arrays representing the display." 282 283 x = 0 284 screen = [] 285 while x < MAX_WIDTH: 286 screen.append([(0, 0, 0)] * MAX_HEIGHT) 287 x += 1 288 return screen 289 290 def fill(memory, start, end, value): 291 for i in xrange(start, end): 292 memory[i] = value 293 294 # Test program providing coverage (necessary for compilers like Shedskin). 295 296 if __name__ == "__main__": 297 ula = get_ula() 298 ula.set_mode(2) 299 ula.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) 300 301 # Make a simple two-dimensional array of tuples (three-dimensional in pygame 302 # terminology). 303 304 a = get_screen() 305 update(a, ula) 306 307 # vim: tabstop=4 expandtab shiftwidth=4