ULA

Changeset

2:4460129be79e
2011-12-04 Paul Boddie raw files shortlog changelog graph Changed the program architecture to involve an external screen update loop requesting pixel values from a ULA object which manages its own internal state, maintaining a pixel buffer for decoded memory content.
ula.py (file)
     1.1 --- a/ula.py	Sun Dec 04 01:17:40 2011 +0100
     1.2 +++ b/ula.py	Sun Dec 04 21:43:45 2011 +0100
     1.3 @@ -17,114 +17,171 @@
     1.4  MAX_MEMORY = 0x10000
     1.5  INTENSITY = 255
     1.6  
     1.7 -palette = range(0, 8) * 2
     1.8 -
     1.9 -def get_mode((width, depth, rows)):
    1.10 +def update(screen, ula):
    1.11  
    1.12      """
    1.13 -    Return the 'width', 'depth', 'rows', calculated character row size in
    1.14 -    bytes, screen size in bytes, horizontal pixel scaling, vertical pixel
    1.15 -    spacing, and line spacing between character rows, as elements of a tuple for
    1.16 -    a particular screen mode.
    1.17 -    """
    1.18 -
    1.19 -    return (width, depth, rows,
    1.20 -        (width * depth * LINES_PER_ROW) / 8, # bits per row -> bytes per row
    1.21 -        (width * depth * MAX_HEIGHT) / 8,    # bits per screen -> bytes per screen
    1.22 -        WIDTH / width,                       # pixel width in display pixels
    1.23 -        HEIGHT / (rows * LINES_PER_ROW),     # pixel height in display pixels
    1.24 -        MAX_HEIGHT / rows - LINES_PER_ROW)   # pixels between rows
    1.25 -
    1.26 -modes = map(get_mode, [
    1.27 -    (640, 1, 32), (320, 2, 32), (160, 4, 32),   # (width, depth, rows)
    1.28 -    (640, 1, 24), (320, 1, 32), (160, 2, 32),
    1.29 -    (320, 1, 24)
    1.30 -    ])
    1.31 -
    1.32 -def update(screen, memory, start, mode):
    1.33 -
    1.34 -    """
    1.35 -    Update the 'screen' array by reading from 'memory' at the given 'start'
    1.36 -    address for the given 'mode'.
    1.37 +    Update the 'screen' array by reading from the 'ula'.
    1.38      """
    1.39  
    1.40 -    # Get the width in pixels, colour depth in bits per pixel, number of
    1.41 -    # character rows, character row size in bytes, screen size in bytes,
    1.42 -    # horizontal pixel scaling factor, vertical pixel scaling factor, and line
    1.43 -    # spacing in pixels.
    1.44 +    ula.vsync()
    1.45 +    y = 0
    1.46 +    while y < HEIGHT:
    1.47 +        x = 0
    1.48 +        while x < WIDTH:
    1.49 +            colour = ula.get_pixel_colour()
    1.50 +            pixel = tuple(map(lambda x: x * INTENSITY, colour))
    1.51 +            screen[x][y] = pixel
    1.52 +            x += 1
    1.53 +        ula.hsync()
    1.54 +        y += 1
    1.55  
    1.56 -    width, depth, rows, row_size, screen_size, xscale, yscale, spacing = modes[mode]
    1.57 +class ULA:
    1.58 +
    1.59 +    "The ULA functionality."
    1.60  
    1.61 -    address = start
    1.62 -    y = 0
    1.63 -    row = 0
    1.64 +    modes = [
    1.65 +        (640, 1, 32), (320, 2, 32), (160, 4, 32),   # (width, depth, rows)
    1.66 +        (640, 1, 24), (320, 1, 32), (160, 2, 32),
    1.67 +        (320, 1, 24)
    1.68 +        ]
    1.69 +
    1.70 +    palette = range(0, 8) * 2
    1.71 +
    1.72 +    def __init__(self, memory):
    1.73  
    1.74 -    while row < rows:
    1.75 -        row_start = address
    1.76 -        line = 0
    1.77 +        "Initialise the ULA with the given 'memory'."
    1.78 +
    1.79 +        self.memory = memory
    1.80 +        self.set_mode(6)
    1.81  
    1.82 -        # Emit each character row.
    1.83 +        # Internal state.
    1.84 +
    1.85 +        self.buffer = [0] * 8
    1.86 +
    1.87 +    def set_mode(self, mode):
    1.88  
    1.89 -        while line < LINES_PER_ROW:
    1.90 -            line_start = address
    1.91 -            ysub = 0
    1.92 +        """
    1.93 +        For the given 'mode', initialise the...
    1.94  
    1.95 -            # Scale each row of pixels vertically.
    1.96 +          * width in pixels
    1.97 +          * colour depth in bits per pixel
    1.98 +          * number of character rows
    1.99 +          * character row size in bytes
   1.100 +          * screen size in bytes
   1.101 +          * default screen start address
   1.102 +          * horizontal pixel scaling factor
   1.103 +          * vertical pixel scaling factor
   1.104 +          * line spacing in pixels
   1.105 +          * number of entries in the pixel buffer
   1.106 +        """
   1.107  
   1.108 -            while ysub < yscale:
   1.109 -                x = 0
   1.110 +        self.width, self.depth, self.rows = self.modes[mode]
   1.111 +
   1.112 +        row_size = (self.width * self.depth * LINES_PER_ROW) / 8    # bits per row -> bytes per row
   1.113  
   1.114 -                # Emit each row of pixels.
   1.115 +        self.screen_size = row_size * self.rows
   1.116 +        self.screen_start = SCREEN_LIMIT - self.screen_size
   1.117 +        self.xscale = WIDTH / self.width                            # pixel width in display pixels
   1.118 +        self.yscale = HEIGHT / (self.rows * LINES_PER_ROW)          # pixel height in display pixels
   1.119 +        self.spacing = MAX_HEIGHT / self.rows - LINES_PER_ROW       # pixels between rows
   1.120 +        self.buffer_limit = 8 / self.depth
   1.121  
   1.122 -                while x < WIDTH:
   1.123 -                    byte_value = memory[address]
   1.124 +    def vsync(self):
   1.125 +
   1.126 +        "Signal the start of a frame."
   1.127  
   1.128 -                    for colour in decode(byte_value, depth):
   1.129 -                        colour = get_physical_colour(palette[colour])
   1.130 -                        pixel = tuple(map(lambda x: x * INTENSITY, colour))
   1.131 +        self.line_start = self.address = self.screen_start
   1.132 +        self.line = 0
   1.133 +        self.ysub = 0
   1.134 +        self.reset_horizontal()
   1.135  
   1.136 -                        # Scale the pixels horizontally.
   1.137 +    def reset_horizontal(self):
   1.138 +
   1.139 +        "Reset horizontal state."
   1.140 +
   1.141 +        self.xsub = 0
   1.142 +        self.buffer_index = self.buffer_limit # need refill
   1.143  
   1.144 -                        xsub = 0
   1.145 -                        while xsub < xscale:
   1.146 -                            screen[x][y] = pixel
   1.147 -                            xsub += 1
   1.148 -                            x += 1
   1.149 +    def hsync(self):
   1.150 +
   1.151 +        "Signal the end of a line."
   1.152 +
   1.153 +        self.reset_horizontal()
   1.154 +
   1.155 +        # Scale pixels vertically.
   1.156  
   1.157 -                    # Advance to the next column.
   1.158 +        self.ysub += 1
   1.159 +
   1.160 +        # Re-read the current line if appropriate.
   1.161 +
   1.162 +        if self.ysub < self.yscale:
   1.163 +            self.address = self.line_start
   1.164 +            return
   1.165 +
   1.166 +        # Otherwise, move on to the next line.
   1.167  
   1.168 -                    address += LINES_PER_ROW
   1.169 -                    address = wrap_address(address, screen_size)
   1.170 +        self.ysub = 0
   1.171 +        self.line += 1
   1.172 +
   1.173 +        if self.line < LINES_PER_ROW:
   1.174 +            self.address = self.line_start + 1
   1.175 +            self.wrap_address()
   1.176 +
   1.177 +        # After the end of the last line in a row, the address should already
   1.178 +        # have been positioned on the last line of the next column.
   1.179  
   1.180 -                ysub += 1
   1.181 -                y += 1
   1.182 +        else:
   1.183 +            self.address -= LINES_PER_ROW - 1
   1.184 +            self.wrap_address()
   1.185  
   1.186 -                # Return to the address at the start of the line in order to be
   1.187 -                # able to repeat the line.
   1.188 +        self.line_start = self.address
   1.189  
   1.190 -                address = line_start
   1.191 +        # Move on to the next row if appropriate.
   1.192 +
   1.193 +        if self.line == LINES_PER_ROW:
   1.194 +            self.line = 0
   1.195  
   1.196 -            # Move on to the next line in the row.
   1.197 +    def get_pixel_colour(self):
   1.198  
   1.199 -            line += 1
   1.200 -            address += 1
   1.201 +        """
   1.202 +        Return a pixel colour by reading from the pixel buffer.
   1.203 +        """
   1.204 +
   1.205 +        # Scale pixels horizontally.
   1.206  
   1.207 -        # Skip spacing between rows.
   1.208 +        if self.xsub == self.xscale:
   1.209 +            self.xsub = 0
   1.210 +            self.buffer_index += 1
   1.211  
   1.212 -        if spacing:
   1.213 -            y += spacing * yscale
   1.214 +        if self.buffer_index == self.buffer_limit:
   1.215 +            self.buffer_index = 0
   1.216 +            self.fill_pixel_buffer()
   1.217 +
   1.218 +        self.xsub += 1
   1.219 +        return self.buffer[self.buffer_index]
   1.220 +
   1.221 +    def fill_pixel_buffer(self):
   1.222  
   1.223 -        # Move on to the next row.
   1.224 +        """
   1.225 +        Fill the pixel buffer by translating memory content for the current
   1.226 +        mode.
   1.227 +        """
   1.228  
   1.229 -        address = row_start + row_size
   1.230 -        address = wrap_address(address, screen_size)
   1.231 +        byte_value = self.memory[self.address]
   1.232  
   1.233 -        row += 1
   1.234 +        i = 0
   1.235 +        for colour in decode(byte_value, self.depth):
   1.236 +            self.buffer[i] = get_physical_colour(self.palette[colour])
   1.237 +            i += 1
   1.238 +
   1.239 +        # Advance to the next column.
   1.240  
   1.241 -def wrap_address(address, screen_size):
   1.242 -    if address >= SCREEN_LIMIT:
   1.243 -        address -= screen_size
   1.244 -    return address
   1.245 +        self.address += LINES_PER_ROW
   1.246 +        self.wrap_address()
   1.247 +
   1.248 +    def wrap_address(self):
   1.249 +        if self.address >= SCREEN_LIMIT:
   1.250 +            self.address -= self.screen_size
   1.251  
   1.252  def get_physical_colour(value):
   1.253  
   1.254 @@ -196,10 +253,19 @@
   1.255      memory = array.array("B", itertools.repeat(0, MAX_MEMORY))
   1.256      a = pygame.surfarray.pixels3d(screen)
   1.257  
   1.258 +    ula = ULA(memory)
   1.259 +
   1.260      # Test MODE 2.
   1.261  
   1.262 -    fill(memory, 0x3000, 0x8000, encode((1, 6), 4))
   1.263 -    update(a, memory, 0x3000, 2)
   1.264 +    ula.set_mode(2)
   1.265 +
   1.266 +    fill(memory, 0x3000, 0x5800 - 320, encode((1, 6), 4))
   1.267 +    fill(memory, 0x5800 - 320, 0x8000, encode((2, 7), 4))
   1.268 +    update(a, ula)
   1.269 +    mainloop()
   1.270 +
   1.271 +    ula.screen_start = 0x5800 - 320
   1.272 +    update(a, ula)
   1.273      mainloop()
   1.274  
   1.275  # vim: tabstop=4 expandtab shiftwidth=4