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@1 | 7 | import pygame |
paul@1 | 8 | import array |
paul@1 | 9 | import itertools |
paul@1 | 10 | |
paul@1 | 11 | WIDTH = 640 |
paul@1 | 12 | HEIGHT = 512 |
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@1 | 18 | INTENSITY = 255 |
paul@1 | 19 | |
paul@1 | 20 | palette = range(0, 8) * 2 |
paul@1 | 21 | |
paul@1 | 22 | def get_mode((width, depth, rows)): |
paul@1 | 23 | |
paul@1 | 24 | """ |
paul@1 | 25 | Return the 'width', 'depth', 'rows', calculated character row size in |
paul@1 | 26 | bytes, screen size in bytes, horizontal pixel scaling, vertical pixel |
paul@1 | 27 | spacing, and line spacing between character rows, as elements of a tuple for |
paul@1 | 28 | a particular screen mode. |
paul@1 | 29 | """ |
paul@1 | 30 | |
paul@1 | 31 | return (width, depth, rows, |
paul@1 | 32 | (width * depth * LINES_PER_ROW) / 8, # bits per row -> bytes per row |
paul@1 | 33 | (width * depth * MAX_HEIGHT) / 8, # bits per screen -> bytes per screen |
paul@1 | 34 | WIDTH / width, # pixel width in display pixels |
paul@1 | 35 | HEIGHT / (rows * LINES_PER_ROW), # pixel height in display pixels |
paul@1 | 36 | MAX_HEIGHT / rows - LINES_PER_ROW) # pixels between rows |
paul@1 | 37 | |
paul@1 | 38 | modes = map(get_mode, [ |
paul@1 | 39 | (640, 1, 32), (320, 2, 32), (160, 4, 32), # (width, depth, rows) |
paul@1 | 40 | (640, 1, 24), (320, 1, 32), (160, 2, 32), |
paul@1 | 41 | (320, 1, 24) |
paul@1 | 42 | ]) |
paul@1 | 43 | |
paul@1 | 44 | def update(screen, memory, start, mode): |
paul@1 | 45 | |
paul@1 | 46 | """ |
paul@1 | 47 | Update the 'screen' array by reading from 'memory' at the given 'start' |
paul@1 | 48 | address for the given 'mode'. |
paul@1 | 49 | """ |
paul@1 | 50 | |
paul@1 | 51 | # Get the width in pixels, colour depth in bits per pixel, number of |
paul@1 | 52 | # character rows, character row size in bytes, screen size in bytes, |
paul@1 | 53 | # horizontal pixel scaling factor, vertical pixel scaling factor, and line |
paul@1 | 54 | # spacing in pixels. |
paul@1 | 55 | |
paul@1 | 56 | width, depth, rows, row_size, screen_size, xscale, yscale, spacing = modes[mode] |
paul@1 | 57 | |
paul@1 | 58 | address = start |
paul@1 | 59 | y = 0 |
paul@1 | 60 | row = 0 |
paul@1 | 61 | |
paul@1 | 62 | while row < rows: |
paul@1 | 63 | row_start = address |
paul@1 | 64 | line = 0 |
paul@1 | 65 | |
paul@1 | 66 | # Emit each character row. |
paul@1 | 67 | |
paul@1 | 68 | while line < LINES_PER_ROW: |
paul@1 | 69 | line_start = address |
paul@1 | 70 | ysub = 0 |
paul@1 | 71 | |
paul@1 | 72 | # Scale each row of pixels vertically. |
paul@1 | 73 | |
paul@1 | 74 | while ysub < yscale: |
paul@1 | 75 | x = 0 |
paul@1 | 76 | |
paul@1 | 77 | # Emit each row of pixels. |
paul@1 | 78 | |
paul@1 | 79 | while x < WIDTH: |
paul@1 | 80 | byte_value = memory[address] |
paul@1 | 81 | |
paul@1 | 82 | for colour in decode(byte_value, depth): |
paul@1 | 83 | colour = get_physical_colour(palette[colour]) |
paul@1 | 84 | pixel = tuple(map(lambda x: x * INTENSITY, colour)) |
paul@1 | 85 | |
paul@1 | 86 | # Scale the pixels horizontally. |
paul@1 | 87 | |
paul@1 | 88 | xsub = 0 |
paul@1 | 89 | while xsub < xscale: |
paul@1 | 90 | screen[x][y] = pixel |
paul@1 | 91 | xsub += 1 |
paul@1 | 92 | x += 1 |
paul@1 | 93 | |
paul@1 | 94 | # Advance to the next column. |
paul@1 | 95 | |
paul@1 | 96 | address += LINES_PER_ROW |
paul@1 | 97 | address = wrap_address(address, screen_size) |
paul@1 | 98 | |
paul@1 | 99 | ysub += 1 |
paul@1 | 100 | y += 1 |
paul@1 | 101 | |
paul@1 | 102 | # Return to the address at the start of the line in order to be |
paul@1 | 103 | # able to repeat the line. |
paul@1 | 104 | |
paul@1 | 105 | address = line_start |
paul@1 | 106 | |
paul@1 | 107 | # Move on to the next line in the row. |
paul@1 | 108 | |
paul@1 | 109 | line += 1 |
paul@1 | 110 | address += 1 |
paul@1 | 111 | |
paul@1 | 112 | # Skip spacing between rows. |
paul@1 | 113 | |
paul@1 | 114 | if spacing: |
paul@1 | 115 | y += spacing * yscale |
paul@1 | 116 | |
paul@1 | 117 | # Move on to the next row. |
paul@1 | 118 | |
paul@1 | 119 | address = row_start + row_size |
paul@1 | 120 | address = wrap_address(address, screen_size) |
paul@1 | 121 | |
paul@1 | 122 | row += 1 |
paul@1 | 123 | |
paul@1 | 124 | def wrap_address(address, screen_size): |
paul@1 | 125 | if address >= SCREEN_LIMIT: |
paul@1 | 126 | address -= screen_size |
paul@1 | 127 | return address |
paul@1 | 128 | |
paul@1 | 129 | def get_physical_colour(value): |
paul@1 | 130 | |
paul@1 | 131 | """ |
paul@1 | 132 | Return the physical colour as an RGB triple for the given 'value'. |
paul@1 | 133 | """ |
paul@1 | 134 | |
paul@1 | 135 | return value & 1, value >> 1 & 1, value >> 2 & 1 |
paul@1 | 136 | |
paul@1 | 137 | def decode(value, depth): |
paul@1 | 138 | |
paul@1 | 139 | """ |
paul@1 | 140 | Decode the given byte 'value' according to the 'depth' in bits per pixel, |
paul@1 | 141 | returning a sequence of pixel values. |
paul@1 | 142 | """ |
paul@1 | 143 | |
paul@1 | 144 | if depth == 1: |
paul@1 | 145 | return (value >> 7, value >> 6 & 1, value >> 5 & 1, value >> 4 & 1, |
paul@1 | 146 | value >> 3 & 1, value >> 2 & 1, value >> 1 & 1, value & 1) |
paul@1 | 147 | elif depth == 2: |
paul@1 | 148 | return (value >> 6 & 2 | value >> 3 & 1, value >> 5 & 2 | value >> 2 & 1, |
paul@1 | 149 | value >> 4 & 2 | value >> 1 & 1, value >> 3 & 2 | value & 1) |
paul@1 | 150 | elif depth == 4: |
paul@1 | 151 | return (value >> 4 & 8 | value >> 3 & 4 | value >> 2 & 2 | value >> 1 & 1, |
paul@1 | 152 | value >> 3 & 8 | value >> 2 & 4 | value >> 1 & 2 | value & 1) |
paul@1 | 153 | else: |
paul@1 | 154 | raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth |
paul@1 | 155 | |
paul@1 | 156 | # Convenience functions. |
paul@1 | 157 | |
paul@1 | 158 | def encode(values, depth): |
paul@1 | 159 | |
paul@1 | 160 | """ |
paul@1 | 161 | Encode the given 'values' according to the 'depth' in bits per pixel, |
paul@1 | 162 | returning a byte value for the pixels. |
paul@1 | 163 | """ |
paul@1 | 164 | |
paul@1 | 165 | result = 0 |
paul@1 | 166 | |
paul@1 | 167 | if depth == 1: |
paul@1 | 168 | for value in values: |
paul@1 | 169 | result = result << 1 | (value & 1) |
paul@1 | 170 | elif depth == 2: |
paul@1 | 171 | for value in values: |
paul@1 | 172 | result = result << 1 | (value & 2) << 3 | (value & 1) |
paul@1 | 173 | elif depth == 4: |
paul@1 | 174 | for value in values: |
paul@1 | 175 | result = result << 1 | (value & 8) << 3 | (value & 4) << 2 | (value & 2) << 1 | (value & 1) |
paul@1 | 176 | else: |
paul@1 | 177 | raise ValueError, "Only depths of 1, 2 and 4 are supported, not %d." % depth |
paul@1 | 178 | |
paul@1 | 179 | return result |
paul@1 | 180 | |
paul@1 | 181 | def fill(memory, start, end, value): |
paul@1 | 182 | for i in xrange(start, end): |
paul@1 | 183 | memory[i] = value |
paul@1 | 184 | |
paul@1 | 185 | def mainloop(): |
paul@1 | 186 | while 1: |
paul@1 | 187 | pygame.display.flip() |
paul@1 | 188 | event = pygame.event.wait() |
paul@1 | 189 | if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: |
paul@1 | 190 | break |
paul@1 | 191 | |
paul@1 | 192 | if __name__ == "__main__": |
paul@1 | 193 | pygame.init() |
paul@1 | 194 | screen = pygame.display.set_mode((WIDTH, HEIGHT), 0) |
paul@1 | 195 | |
paul@1 | 196 | memory = array.array("B", itertools.repeat(0, MAX_MEMORY)) |
paul@1 | 197 | a = pygame.surfarray.pixels3d(screen) |
paul@1 | 198 | |
paul@1 | 199 | # Test MODE 2. |
paul@1 | 200 | |
paul@1 | 201 | fill(memory, 0x3000, 0x8000, encode((1, 6), 4)) |
paul@1 | 202 | update(a, memory, 0x3000, 2) |
paul@1 | 203 | mainloop() |
paul@1 | 204 | |
paul@1 | 205 | # vim: tabstop=4 expandtab shiftwidth=4 |