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