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@29 | 7 | from array import array |
paul@29 | 8 | from itertools import repeat |
paul@29 | 9 | |
paul@22 | 10 | LINES_PER_ROW = 8 # the number of pixel lines per character row |
paul@22 | 11 | MAX_HEIGHT = 256 # the height of the screen in pixels |
paul@40 | 12 | MAX_WIDTH = 640 # the width of the screen in pixels |
paul@40 | 13 | |
paul@40 | 14 | MAX_CSYNC = 2 # the scanline during which vsync ends |
paul@40 | 15 | MIN_PIXELLINE = 38 # the first scanline involving pixel generation |
paul@22 | 16 | MAX_SCANLINE = 312 # the number of scanlines in each frame |
paul@40 | 17 | |
paul@40 | 18 | MAX_PIXELLINE = MIN_PIXELLINE + MAX_HEIGHT |
paul@40 | 19 | |
paul@40 | 20 | MAX_HSYNC = 75 # the number of cycles in each hsync period |
paul@42 | 21 | MIN_PIXELPOS = 256 # the first cycle involving pixel generation |
paul@40 | 22 | MAX_SCANPOS = 1024 # the number of cycles in each scanline |
paul@40 | 23 | |
paul@40 | 24 | MAX_PIXELPOS = MIN_PIXELPOS + MAX_WIDTH |
paul@40 | 25 | |
paul@22 | 26 | SCREEN_LIMIT = 0x8000 # the first address after the screen memory |
paul@22 | 27 | MAX_MEMORY = 0x10000 # the number of addressable memory locations |
paul@40 | 28 | MAX_RAM = 0x10000 # the number of addressable RAM locations (64Kb in each IC) |
paul@3 | 29 | BLANK = (0, 0, 0) |
paul@1 | 30 | |
paul@29 | 31 | def update(ula): |
paul@1 | 32 | |
paul@1 | 33 | """ |
paul@31 | 34 | Update the 'ula' for one frame. Return the resulting screen. |
paul@31 | 35 | """ |
paul@31 | 36 | |
paul@31 | 37 | video = ula.video |
paul@31 | 38 | |
paul@31 | 39 | i = 0 |
paul@31 | 40 | limit = MAX_SCANLINE * MAX_SCANPOS |
paul@31 | 41 | while i < limit: |
paul@31 | 42 | ula.update() |
paul@31 | 43 | video.update() |
paul@31 | 44 | i += 1 |
paul@40 | 45 | |
paul@31 | 46 | return video.screen |
paul@31 | 47 | |
paul@31 | 48 | class Video: |
paul@31 | 49 | |
paul@31 | 50 | """ |
paul@31 | 51 | A class representing the video circuitry. |
paul@1 | 52 | """ |
paul@1 | 53 | |
paul@31 | 54 | def __init__(self): |
paul@31 | 55 | self.screen = array("B", repeat(0, MAX_WIDTH * 3 * MAX_HEIGHT)) |
paul@31 | 56 | self.colour = BLANK |
paul@31 | 57 | self.csync = 1 |
paul@31 | 58 | self.hs = 1 |
paul@40 | 59 | self.x = 0 |
paul@40 | 60 | self.y = 0 |
paul@1 | 61 | |
paul@40 | 62 | def set_csync(self, value): |
paul@40 | 63 | if self.csync and not value: |
paul@40 | 64 | self.y = 0 |
paul@40 | 65 | self.pos = 0 |
paul@40 | 66 | self.csync = value |
paul@40 | 67 | |
paul@40 | 68 | def set_hs(self, value): |
paul@40 | 69 | if self.hs and not value: |
paul@40 | 70 | self.x = 0 |
paul@40 | 71 | self.y += 1 |
paul@40 | 72 | self.hs = value |
paul@31 | 73 | |
paul@31 | 74 | def update(self): |
paul@40 | 75 | if MIN_PIXELLINE <= self.y < MAX_PIXELLINE: |
paul@42 | 76 | if MIN_PIXELPOS + 8 <= self.x < MAX_PIXELPOS + 8: |
paul@31 | 77 | self.screen[self.pos] = self.colour[0]; self.pos += 1 |
paul@31 | 78 | self.screen[self.pos] = self.colour[1]; self.pos += 1 |
paul@31 | 79 | self.screen[self.pos] = self.colour[2]; self.pos += 1 |
paul@40 | 80 | self.x += 1 |
paul@40 | 81 | |
paul@40 | 82 | class RAM: |
paul@40 | 83 | |
paul@40 | 84 | """ |
paul@40 | 85 | A class representing the RAM circuits (IC4 to IC7). Each circuit |
paul@48 | 86 | traditionally holds 64 kilobits, with each access obtaining 1 bit from each |
paul@48 | 87 | IC, and thus two accesses being required to obtain a whole byte. Here, we |
paul@48 | 88 | model the circuits with a list of 65536 half-bytes with each bit in a |
paul@48 | 89 | half-byte representing a bit stored on a separate IC. |
paul@40 | 90 | """ |
paul@40 | 91 | |
paul@40 | 92 | def __init__(self): |
paul@40 | 93 | |
paul@40 | 94 | "Initialise the RAM circuits." |
paul@40 | 95 | |
paul@40 | 96 | self.memory = [0] * MAX_RAM |
paul@40 | 97 | self.row_address = 0 |
paul@40 | 98 | self.column_address = 0 |
paul@40 | 99 | self.data = 0 |
paul@40 | 100 | |
paul@40 | 101 | def row_select(self, address): |
paul@40 | 102 | self.row_address = address |
paul@40 | 103 | |
paul@40 | 104 | def row_deselect(self): |
paul@40 | 105 | pass |
paul@40 | 106 | |
paul@40 | 107 | def column_select(self, address): |
paul@40 | 108 | self.column_address = address |
paul@40 | 109 | |
paul@40 | 110 | # Read the data. |
paul@40 | 111 | |
paul@40 | 112 | self.data = self.memory[self.row_address << 8 | self.column_address] |
paul@40 | 113 | |
paul@40 | 114 | def column_deselect(self): |
paul@40 | 115 | pass |
paul@40 | 116 | |
paul@40 | 117 | # Convenience methods. |
paul@40 | 118 | |
paul@40 | 119 | def fill(self, start, end, value): |
paul@40 | 120 | for i in xrange(start, end): |
paul@40 | 121 | self.memory[i << 1] = value >> 4 |
paul@40 | 122 | self.memory[i << 1 | 0x1] = value & 0xf |
paul@29 | 123 | |
paul@2 | 124 | class ULA: |
paul@2 | 125 | |
paul@31 | 126 | """ |
paul@31 | 127 | A class providing the ULA functionality. Instances of this class refer to |
paul@31 | 128 | the system memory, maintain internal state (such as information about the |
paul@31 | 129 | current screen mode), and provide outputs (such as the current pixel |
paul@31 | 130 | colour). |
paul@31 | 131 | """ |
paul@1 | 132 | |
paul@2 | 133 | modes = [ |
paul@2 | 134 | (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) |
paul@3 | 135 | (640, 1, 25), (320, 1, 32), (160, 2, 32), |
paul@3 | 136 | (320, 1, 25) |
paul@2 | 137 | ] |
paul@2 | 138 | |
paul@2 | 139 | palette = range(0, 8) * 2 |
paul@2 | 140 | |
paul@40 | 141 | def __init__(self, ram, video): |
paul@1 | 142 | |
paul@40 | 143 | "Initialise the ULA with the given 'ram' and 'video' instances." |
paul@2 | 144 | |
paul@40 | 145 | self.ram = ram |
paul@31 | 146 | self.video = video |
paul@2 | 147 | self.set_mode(6) |
paul@1 | 148 | |
paul@31 | 149 | self.reset() |
paul@31 | 150 | |
paul@31 | 151 | def reset(self): |
paul@31 | 152 | |
paul@31 | 153 | "Reset the ULA." |
paul@31 | 154 | |
paul@43 | 155 | # General state. |
paul@43 | 156 | |
paul@43 | 157 | self.nmi = 0 # no NMI asserted initially |
paul@43 | 158 | self.irq_vsync = 0 # no IRQ asserted initially |
paul@43 | 159 | |
paul@40 | 160 | # Internal state. |
paul@40 | 161 | |
paul@50 | 162 | self.cycle = [0]*8 # counter within each 2MHz period represented by 8 latches |
paul@40 | 163 | self.access = 0 # counter used to determine whether a byte needs reading |
paul@40 | 164 | self.ram_address = 0 # address given to the RAM |
paul@40 | 165 | self.data = 0 # data read from the RAM |
paul@42 | 166 | self.have_pixels = 0 # whether pixel data has been read |
paul@42 | 167 | self.writing_pixels = 0 # whether pixel data can be written |
paul@45 | 168 | self.buffer = [BLANK]*8 # pixel buffer for decoded RAM data |
paul@40 | 169 | |
paul@50 | 170 | self.cycle[7] = 1 # assert the final latch (asserting the first on update) |
paul@50 | 171 | |
paul@40 | 172 | self.reset_vertical() |
paul@31 | 173 | |
paul@2 | 174 | def set_mode(self, mode): |
paul@1 | 175 | |
paul@2 | 176 | """ |
paul@2 | 177 | For the given 'mode', initialise the... |
paul@1 | 178 | |
paul@2 | 179 | * width in pixels |
paul@2 | 180 | * colour depth in bits per pixel |
paul@2 | 181 | * number of character rows |
paul@2 | 182 | * character row size in bytes |
paul@2 | 183 | * screen size in bytes |
paul@2 | 184 | * default screen start address |
paul@2 | 185 | * horizontal pixel scaling factor |
paul@2 | 186 | * line spacing in pixels |
paul@2 | 187 | * number of entries in the pixel buffer |
paul@31 | 188 | |
paul@31 | 189 | The ULA should be reset after a mode switch in order to cleanly display |
paul@31 | 190 | a full screen. |
paul@2 | 191 | """ |
paul@1 | 192 | |
paul@3 | 193 | self.width, self.depth, rows = self.modes[mode] |
paul@3 | 194 | |
paul@31 | 195 | columns = (self.width * self.depth) / 8 # bits read -> bytes read |
paul@40 | 196 | self.access_frequency = 80 / columns # cycle frequency for reading bytes |
paul@31 | 197 | row_size = columns * LINES_PER_ROW |
paul@2 | 198 | |
paul@3 | 199 | # Memory access configuration. |
paul@4 | 200 | # Note the limitation on positioning the screen start. |
paul@3 | 201 | |
paul@4 | 202 | screen_size = row_size * rows |
paul@4 | 203 | self.screen_start = (SCREEN_LIMIT - screen_size) & 0xff00 |
paul@4 | 204 | self.screen_size = SCREEN_LIMIT - self.screen_start |
paul@3 | 205 | |
paul@3 | 206 | # Scanline configuration. |
paul@1 | 207 | |
paul@22 | 208 | self.xscale = MAX_WIDTH / self.width # pixel width in display pixels |
paul@3 | 209 | self.spacing = MAX_HEIGHT / rows - LINES_PER_ROW # pixels between rows |
paul@3 | 210 | |
paul@3 | 211 | # Start of unused region. |
paul@3 | 212 | |
paul@3 | 213 | self.footer = rows * LINES_PER_ROW |
paul@22 | 214 | self.margin = MAX_SCANLINE - rows * (LINES_PER_ROW + self.spacing) + self.spacing |
paul@3 | 215 | |
paul@3 | 216 | # Internal pixel buffer size. |
paul@3 | 217 | |
paul@2 | 218 | self.buffer_limit = 8 / self.depth |
paul@1 | 219 | |
paul@40 | 220 | def vsync(self, value=0): |
paul@40 | 221 | |
paul@40 | 222 | "Signal the start of a frame." |
paul@40 | 223 | |
paul@40 | 224 | self.csync = value |
paul@40 | 225 | self.video.set_csync(value) |
paul@40 | 226 | |
paul@40 | 227 | def hsync(self, value=0): |
paul@40 | 228 | |
paul@40 | 229 | "Signal the end of a scanline." |
paul@40 | 230 | |
paul@40 | 231 | self.hs = value |
paul@40 | 232 | self.video.set_hs(value) |
paul@40 | 233 | |
paul@40 | 234 | def reset_vertical(self): |
paul@2 | 235 | |
paul@2 | 236 | "Signal the start of a frame." |
paul@1 | 237 | |
paul@2 | 238 | self.line_start = self.address = self.screen_start |
paul@5 | 239 | self.line = self.line_start % LINES_PER_ROW |
paul@3 | 240 | self.ssub = 0 |
paul@31 | 241 | self.y = 0 |
paul@40 | 242 | self.x = 0 |
paul@2 | 243 | |
paul@40 | 244 | def reset_horizontal(self): |
paul@1 | 245 | |
paul@40 | 246 | "Reset horizontal state within the active region of the frame." |
paul@31 | 247 | |
paul@31 | 248 | self.y += 1 |
paul@40 | 249 | self.x = 0 |
paul@40 | 250 | |
paul@40 | 251 | if not self.inside_frame(): |
paul@40 | 252 | return |
paul@2 | 253 | |
paul@3 | 254 | # Support spacing between character rows. |
paul@3 | 255 | |
paul@3 | 256 | if self.ssub: |
paul@3 | 257 | self.ssub -= 1 |
paul@3 | 258 | return |
paul@3 | 259 | |
paul@2 | 260 | self.line += 1 |
paul@2 | 261 | |
paul@3 | 262 | # If not on a row boundary, move to the next line. |
paul@3 | 263 | |
paul@3 | 264 | if self.line % LINES_PER_ROW: |
paul@2 | 265 | self.address = self.line_start + 1 |
paul@2 | 266 | self.wrap_address() |
paul@2 | 267 | |
paul@2 | 268 | # After the end of the last line in a row, the address should already |
paul@2 | 269 | # have been positioned on the last line of the next column. |
paul@1 | 270 | |
paul@2 | 271 | else: |
paul@2 | 272 | self.address -= LINES_PER_ROW - 1 |
paul@2 | 273 | self.wrap_address() |
paul@1 | 274 | |
paul@3 | 275 | # Test for the footer region. |
paul@3 | 276 | |
paul@3 | 277 | if self.spacing and self.line == self.footer: |
paul@22 | 278 | self.ssub = self.margin |
paul@3 | 279 | return |
paul@1 | 280 | |
paul@3 | 281 | # Support spacing between character rows. |
paul@2 | 282 | |
paul@22 | 283 | self.ssub = self.spacing |
paul@3 | 284 | |
paul@3 | 285 | self.line_start = self.address |
paul@1 | 286 | |
paul@40 | 287 | def in_frame(self): return MIN_PIXELLINE <= self.y < MAX_PIXELLINE |
paul@40 | 288 | def inside_frame(self): return MIN_PIXELLINE < self.y < MAX_PIXELLINE |
paul@42 | 289 | def read_pixels(self): return MIN_PIXELPOS <= self.x < MAX_PIXELPOS and self.in_frame() |
paul@31 | 290 | |
paul@31 | 291 | def update(self): |
paul@1 | 292 | |
paul@2 | 293 | """ |
paul@40 | 294 | Update the state of the ULA for each clock cycle. This involves updating |
paul@40 | 295 | the pixel colour by reading from the pixel buffer. |
paul@2 | 296 | """ |
paul@2 | 297 | |
paul@40 | 298 | # Detect the end of the scanline. |
paul@40 | 299 | |
paul@40 | 300 | if self.x == MAX_SCANPOS: |
paul@40 | 301 | self.reset_horizontal() |
paul@40 | 302 | |
paul@40 | 303 | # Detect the end of the frame. |
paul@40 | 304 | |
paul@40 | 305 | if self.y == MAX_SCANLINE: |
paul@40 | 306 | self.reset_vertical() |
paul@40 | 307 | |
paul@40 | 308 | |
paul@40 | 309 | |
paul@40 | 310 | # Clock management. |
paul@40 | 311 | |
paul@43 | 312 | access_ram = not self.nmi and self.access == 0 and self.read_pixels() and not self.ssub |
paul@40 | 313 | |
paul@50 | 314 | # Update the state of the device. |
paul@50 | 315 | # NOTE: This is not meant to be "nice" Python, but instead models the |
paul@50 | 316 | # NOTE: propagation of state through the latches. |
paul@50 | 317 | |
paul@50 | 318 | self.cycle[0], self.cycle[1], self.cycle[2], self.cycle[3], \ |
paul@50 | 319 | self.cycle[4], self.cycle[5], self.cycle[6], self.cycle[7] = \ |
paul@50 | 320 | self.cycle[7], self.cycle[0], self.cycle[1], self.cycle[2], \ |
paul@50 | 321 | self.cycle[3], self.cycle[4], self.cycle[5], self.cycle[6] |
paul@50 | 322 | |
paul@40 | 323 | # Set row address (for ULA access only). |
paul@40 | 324 | |
paul@50 | 325 | if self.cycle[0]: |
paul@40 | 326 | |
paul@40 | 327 | # NOTE: Propagate CPU address here. |
paul@40 | 328 | |
paul@40 | 329 | if access_ram: |
paul@40 | 330 | self.ram_address = (self.address & 0xff80) >> 7 |
paul@40 | 331 | |
paul@42 | 332 | # Initialise the pixel buffer if appropriate. |
paul@42 | 333 | |
paul@42 | 334 | if not self.writing_pixels and self.have_pixels: |
paul@42 | 335 | self.xcounter = self.xscale |
paul@42 | 336 | self.buffer_index = 0 |
paul@42 | 337 | self.fill_pixel_buffer() |
paul@42 | 338 | self.writing_pixels = 1 |
paul@42 | 339 | |
paul@40 | 340 | # Latch row address, set column address (for ULA access only). |
paul@40 | 341 | |
paul@50 | 342 | elif self.cycle[1]: |
paul@40 | 343 | |
paul@40 | 344 | # NOTE: Permit CPU access here. |
paul@31 | 345 | |
paul@40 | 346 | if access_ram: |
paul@40 | 347 | self.ram.row_select(self.ram_address) |
paul@40 | 348 | |
paul@40 | 349 | # NOTE: Propagate CPU address here. |
paul@40 | 350 | |
paul@40 | 351 | if access_ram: |
paul@40 | 352 | self.ram_address = (self.address & 0x7f) << 1 |
paul@40 | 353 | |
paul@40 | 354 | # Latch column address. |
paul@40 | 355 | |
paul@50 | 356 | elif self.cycle[2]: |
paul@40 | 357 | |
paul@40 | 358 | # NOTE: Permit CPU access here. |
paul@31 | 359 | |
paul@40 | 360 | if access_ram: |
paul@40 | 361 | self.ram.column_select(self.ram_address) |
paul@40 | 362 | |
paul@40 | 363 | # Read 4 bits (for ULA access only). |
paul@40 | 364 | # NOTE: Perhaps map alternate bits, not half-bytes. |
paul@40 | 365 | |
paul@50 | 366 | elif self.cycle[3]: |
paul@40 | 367 | |
paul@40 | 368 | # NOTE: Propagate CPU data here. |
paul@40 | 369 | |
paul@40 | 370 | if access_ram: |
paul@40 | 371 | self.data = self.ram.data << 4 |
paul@40 | 372 | |
paul@40 | 373 | # Set column address (for ULA access only). |
paul@40 | 374 | |
paul@50 | 375 | elif self.cycle[4]: |
paul@40 | 376 | self.ram.column_deselect() |
paul@31 | 377 | |
paul@40 | 378 | # NOTE: Propagate CPU address here. |
paul@40 | 379 | |
paul@40 | 380 | if access_ram: |
paul@40 | 381 | self.ram_address = (self.address & 0x7f) << 1 | 0x1 |
paul@40 | 382 | |
paul@40 | 383 | # Latch column address. |
paul@40 | 384 | |
paul@50 | 385 | elif self.cycle[5]: |
paul@40 | 386 | |
paul@40 | 387 | # NOTE: Permit CPU access here. |
paul@40 | 388 | |
paul@40 | 389 | if access_ram: |
paul@40 | 390 | self.ram.column_select(self.ram_address) |
paul@31 | 391 | |
paul@40 | 392 | # Read 4 bits (for ULA access only). |
paul@40 | 393 | # NOTE: Perhaps map alternate bits, not half-bytes. |
paul@40 | 394 | |
paul@50 | 395 | elif self.cycle[6]: |
paul@40 | 396 | |
paul@40 | 397 | # NOTE: Propagate CPU data here. |
paul@40 | 398 | |
paul@40 | 399 | if access_ram: |
paul@40 | 400 | self.data = self.data | self.ram.data |
paul@42 | 401 | self.have_pixels = 1 |
paul@40 | 402 | |
paul@40 | 403 | # Advance to the next column. |
paul@40 | 404 | |
paul@40 | 405 | self.address += LINES_PER_ROW |
paul@40 | 406 | self.wrap_address() |
paul@40 | 407 | |
paul@40 | 408 | # Reset addresses. |
paul@31 | 409 | |
paul@50 | 410 | elif self.cycle[7]: |
paul@40 | 411 | self.ram.column_deselect() |
paul@40 | 412 | self.ram.row_deselect() |
paul@40 | 413 | |
paul@40 | 414 | # Update the RAM access controller. |
paul@40 | 415 | |
paul@40 | 416 | self.access = (self.access + 1) % self.access_frequency |
paul@40 | 417 | |
paul@40 | 418 | |
paul@40 | 419 | |
paul@40 | 420 | # Video signalling. |
paul@40 | 421 | |
paul@40 | 422 | # Detect any sync conditions. |
paul@31 | 423 | |
paul@40 | 424 | if self.x == 0: |
paul@40 | 425 | self.hsync() |
paul@40 | 426 | if self.y == 0: |
paul@40 | 427 | self.vsync() |
paul@43 | 428 | self.irq_vsync = 0 |
paul@43 | 429 | elif self.y == MAX_PIXELLINE: |
paul@43 | 430 | self.irq_vsync = 1 |
paul@40 | 431 | |
paul@40 | 432 | # Detect the end of hsync. |
paul@31 | 433 | |
paul@40 | 434 | elif self.x == MAX_HSYNC: |
paul@40 | 435 | self.hsync(1) |
paul@40 | 436 | |
paul@40 | 437 | # Detect the end of vsync. |
paul@40 | 438 | |
paul@40 | 439 | elif self.y == MAX_CSYNC and self.x == MAX_SCANPOS / 2: |
paul@40 | 440 | self.vsync(1) |
paul@40 | 441 | |
paul@40 | 442 | |
paul@40 | 443 | |
paul@40 | 444 | # Pixel production. |
paul@31 | 445 | |
paul@3 | 446 | # Detect spacing between character rows. |
paul@3 | 447 | |
paul@42 | 448 | if not self.writing_pixels or self.ssub: |
paul@31 | 449 | self.video.colour = BLANK |
paul@3 | 450 | |
paul@31 | 451 | # For pixels within the frame, obtain and output the value. |
paul@31 | 452 | |
paul@31 | 453 | else: |
paul@40 | 454 | |
paul@42 | 455 | self.xcounter -= 1 |
paul@42 | 456 | self.video.colour = self.buffer[self.buffer_index] |
paul@1 | 457 | |
paul@31 | 458 | # Scale pixels horizontally, only accessing the next pixel value |
paul@31 | 459 | # after the required number of scan positions. |
paul@22 | 460 | |
paul@42 | 461 | if self.xcounter == 0: |
paul@40 | 462 | self.xcounter = self.xscale |
paul@31 | 463 | self.buffer_index += 1 |
paul@31 | 464 | |
paul@42 | 465 | # Handle the buffer empty condition. |
paul@22 | 466 | |
paul@40 | 467 | if self.buffer_index >= self.buffer_limit: |
paul@42 | 468 | self.writing_pixels = 0 |
paul@2 | 469 | |
paul@31 | 470 | self.x += 1 |
paul@2 | 471 | |
paul@2 | 472 | def fill_pixel_buffer(self): |
paul@1 | 473 | |
paul@2 | 474 | """ |
paul@2 | 475 | Fill the pixel buffer by translating memory content for the current |
paul@2 | 476 | mode. |
paul@2 | 477 | """ |
paul@1 | 478 | |
paul@40 | 479 | byte_value = self.data # which should have been read automatically |
paul@1 | 480 | |
paul@2 | 481 | i = 0 |
paul@2 | 482 | for colour in decode(byte_value, self.depth): |
paul@2 | 483 | self.buffer[i] = get_physical_colour(self.palette[colour]) |
paul@2 | 484 | i += 1 |
paul@2 | 485 | |
paul@2 | 486 | def wrap_address(self): |
paul@2 | 487 | if self.address >= SCREEN_LIMIT: |
paul@2 | 488 | self.address -= self.screen_size |
paul@1 | 489 | |
paul@1 | 490 | def get_physical_colour(value): |
paul@1 | 491 | |
paul@1 | 492 | """ |
paul@1 | 493 | Return the physical colour as an RGB triple for the given 'value'. |
paul@1 | 494 | """ |
paul@1 | 495 | |
paul@1 | 496 | return value & 1, value >> 1 & 1, value >> 2 & 1 |
paul@1 | 497 | |
paul@1 | 498 | def decode(value, depth): |
paul@1 | 499 | |
paul@1 | 500 | """ |
paul@1 | 501 | Decode the given byte 'value' according to the 'depth' in bits per pixel, |
paul@1 | 502 | returning a sequence of pixel values. |
paul@1 | 503 | """ |
paul@1 | 504 | |
paul@1 | 505 | if depth == 1: |
paul@1 | 506 | return (value >> 7, value >> 6 & 1, value >> 5 & 1, value >> 4 & 1, |
paul@1 | 507 | value >> 3 & 1, value >> 2 & 1, value >> 1 & 1, value & 1) |
paul@1 | 508 | elif depth == 2: |
paul@1 | 509 | return (value >> 6 & 2 | value >> 3 & 1, value >> 5 & 2 | value >> 2 & 1, |
paul@1 | 510 | value >> 4 & 2 | value >> 1 & 1, value >> 3 & 2 | value & 1) |
paul@1 | 511 | elif depth == 4: |
paul@1 | 512 | return (value >> 4 & 8 | value >> 3 & 4 | value >> 2 & 2 | value >> 1 & 1, |
paul@1 | 513 | value >> 3 & 8 | value >> 2 & 4 | value >> 1 & 2 | value & 1) |
paul@1 | 514 | else: |
paul@1 | 515 | raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth |
paul@1 | 516 | |
paul@1 | 517 | # Convenience functions. |
paul@1 | 518 | |
paul@1 | 519 | def encode(values, depth): |
paul@1 | 520 | |
paul@1 | 521 | """ |
paul@1 | 522 | Encode the given 'values' according to the 'depth' in bits per pixel, |
paul@1 | 523 | returning a byte value for the pixels. |
paul@1 | 524 | """ |
paul@1 | 525 | |
paul@1 | 526 | result = 0 |
paul@1 | 527 | |
paul@1 | 528 | if depth == 1: |
paul@1 | 529 | for value in values: |
paul@1 | 530 | result = result << 1 | (value & 1) |
paul@1 | 531 | elif depth == 2: |
paul@1 | 532 | for value in values: |
paul@1 | 533 | result = result << 1 | (value & 2) << 3 | (value & 1) |
paul@1 | 534 | elif depth == 4: |
paul@1 | 535 | for value in values: |
paul@1 | 536 | result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) |
paul@1 | 537 | else: |
paul@1 | 538 | raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth |
paul@1 | 539 | |
paul@1 | 540 | return result |
paul@1 | 541 | |
paul@11 | 542 | def get_ula(): |
paul@11 | 543 | |
paul@31 | 544 | "Return a ULA initialised with a memory array and video." |
paul@31 | 545 | |
paul@40 | 546 | return ULA(get_ram(), get_video()) |
paul@11 | 547 | |
paul@31 | 548 | def get_video(): |
paul@31 | 549 | |
paul@31 | 550 | "Return a video circuit." |
paul@31 | 551 | |
paul@31 | 552 | return Video() |
paul@11 | 553 | |
paul@40 | 554 | def get_ram(): |
paul@10 | 555 | |
paul@40 | 556 | "Return an instance representing the computer's RAM hardware." |
paul@7 | 557 | |
paul@40 | 558 | return RAM() |
paul@1 | 559 | |
paul@7 | 560 | # Test program providing coverage (necessary for compilers like Shedskin). |
paul@7 | 561 | |
paul@7 | 562 | if __name__ == "__main__": |
paul@11 | 563 | ula = get_ula() |
paul@7 | 564 | ula.set_mode(2) |
paul@40 | 565 | ula.reset() |
paul@40 | 566 | ula.ram.fill(0x5800 - 320, 0x8000, encode((2, 7), 4)) |
paul@7 | 567 | |
paul@7 | 568 | # Make a simple two-dimensional array of tuples (three-dimensional in pygame |
paul@7 | 569 | # terminology). |
paul@7 | 570 | |
paul@29 | 571 | a = update(ula) |
paul@7 | 572 | |
paul@1 | 573 | # vim: tabstop=4 expandtab shiftwidth=4 |