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