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