ULA

ula.py

12:0ac34bc9f117
2011-12-10 Paul Boddie Made the ULA's buffer use element types consistently. Changed class attribute accesses to be explicit for Shedskin. Changed the test program to use data less likely to be optimised by Shedskin or similar compilers.
     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     return array.array("B", itertools.repeat(0, MAX_MEMORY))   275    276 def fill(memory, start, end, value):   277     for i in xrange(start, end):   278         memory[i] = value   279    280 # Test program providing coverage (necessary for compilers like Shedskin).   281 # NOTE: Running this will actually cause an IndexError.   282    283 if __name__ == "__main__":   284     memory = get_memory()   285     ula = ULA(memory)   286     ula.set_mode(6)   287     fill(memory, 0x6000, 0x8000, encode((1, 0, 1, 0, 1, 0, 1, 0), 1))   288    289     # Make a simple two-dimensional array of tuples (three-dimensional in pygame   290     # terminology).   291    292     a = [   293         [(0, 0, 0), (0, 0, 0), (0, 0, 0)],   294         [(0, 0, 0), (0, 0, 0), (0, 0, 0)],   295         [(0, 0, 0), (0, 0, 0), (0, 0, 0)]   296         ]   297     update(a, ula)   298    299 # vim: tabstop=4 expandtab shiftwidth=4