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