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(ula): 17 18 """ 19 Return a screen array by reading from the 'ula'. This function effectively 20 has the role of the video circuit, but also provides the clock signal to the 21 ULA. 22 """ 23 24 screen = [] 25 26 ula.vsync() 27 y = 0 28 while y < MAX_SCANLINE: 29 row = [] 30 x = 0 31 while x < MAX_SCANPOS: 32 colour = ula.get_pixel_colour() 33 if x < MAX_WIDTH and y < MAX_HEIGHT: 34 row.append(colour) 35 x += 1 36 ula.hsync() 37 if y < MAX_HEIGHT: 38 screen.append(row) 39 y += 1 40 41 return screen 42 43 class ULA: 44 45 "The ULA functionality." 46 47 modes = [ 48 (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) 49 (640, 1, 25), (320, 1, 32), (160, 2, 32), 50 (320, 1, 25) 51 ] 52 53 palette = range(0, 8) * 2 54 55 def __init__(self, memory): 56 57 "Initialise the ULA with the given 'memory'." 58 59 self.memory = memory 60 self.set_mode(6) 61 62 # Internal state. 63 64 self.buffer = [(0, 0, 0)] * 8 65 66 def set_mode(self, mode): 67 68 """ 69 For the given 'mode', initialise the... 70 71 * width in pixels 72 * colour depth in bits per pixel 73 * number of character rows 74 * character row size in bytes 75 * screen size in bytes 76 * default screen start address 77 * horizontal pixel scaling factor 78 * line spacing in pixels 79 * number of entries in the pixel buffer 80 """ 81 82 self.width, self.depth, rows = ULA.modes[mode] 83 84 self.columns = (self.width * self.depth) / 8 # bits read -> bytes read 85 row_size = self.columns * LINES_PER_ROW 86 87 # Memory access configuration. 88 # Note the limitation on positioning the screen start. 89 90 screen_size = row_size * rows 91 self.screen_start = (SCREEN_LIMIT - screen_size) & 0xff00 92 self.screen_size = SCREEN_LIMIT - self.screen_start 93 94 # Scanline configuration. 95 96 self.xscale = MAX_WIDTH / self.width # pixel width in display pixels 97 self.spacing = MAX_HEIGHT / rows - LINES_PER_ROW # pixels between rows 98 99 # Start of unused region. 100 101 self.footer = rows * LINES_PER_ROW 102 self.margin = MAX_SCANLINE - rows * (LINES_PER_ROW + self.spacing) + self.spacing 103 104 # Internal pixel buffer size. 105 106 self.buffer_limit = 8 / self.depth 107 108 def vsync(self): 109 110 "Signal the start of a frame." 111 112 self.line_start = self.address = self.screen_start 113 self.line = self.line_start % LINES_PER_ROW 114 self.ssub = 0 115 self.reset_horizontal() 116 117 def reset_horizontal(self): 118 119 "Reset horizontal state." 120 121 self.xsub = 0 122 self.column = 0 123 self.buffer_index = self.buffer_limit # need refill 124 125 def hsync(self): 126 127 "Signal the end of a line." 128 129 # Support spacing between character rows. 130 131 if self.ssub: 132 self.ssub -= 1 133 return 134 135 self.reset_horizontal() 136 self.line += 1 137 138 # If not on a row boundary, move to the next line. 139 140 if self.line % LINES_PER_ROW: 141 self.address = self.line_start + 1 142 self.wrap_address() 143 144 # After the end of the last line in a row, the address should already 145 # have been positioned on the last line of the next column. 146 147 else: 148 self.address -= LINES_PER_ROW - 1 149 self.wrap_address() 150 151 # Test for the footer region. 152 153 if self.spacing and self.line == self.footer: 154 self.ssub = self.margin 155 return 156 157 # Support spacing between character rows. 158 159 self.ssub = self.spacing 160 161 self.line_start = self.address 162 163 def get_pixel_colour(self): 164 165 """ 166 Return a pixel colour by reading from the pixel buffer. 167 """ 168 169 # Detect spacing between character rows. 170 171 if self.ssub: 172 return BLANK 173 174 # Scale pixels horizontally. 175 176 if self.xsub == self.xscale: 177 self.xsub = 0 178 self.buffer_index += 1 179 180 if self.buffer_index == self.buffer_limit: 181 self.buffer_index = 0 182 self.column += 1 183 184 # Detect the end of the scanline. 185 186 if self.column > self.columns: 187 return BLANK 188 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(ULA.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 fill(memory, start, end, value): 287 i = start 288 while i < end: 289 memory[i] = value 290 i += 1 291 292 # Test program providing coverage (necessary for compilers like Shedskin). 293 294 if __name__ == "__main__": 295 ula = get_ula() 296 ula.set_mode(6) 297 ula.fill(0x6000, 0x8000, encode((1, 0, 1, 0, 1, 0, 1, 0), 1)) 298 299 # Make a simple two-dimensional array of tuples (three-dimensional in pygame 300 # terminology). 301 302 a = update(ula) 303 304 # vim: tabstop=4 expandtab shiftwidth=4