ULA

ula.py

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