ULA

Change of ula.py

31:579ebc9db48b
ula.py
     1.1 --- a/ula.py	Sun Dec 18 19:29:20 2011 +0100
     1.2 +++ b/ula.py	Tue Dec 20 00:56:33 2011 +0100
     1.3 @@ -16,36 +16,55 @@
     1.4  MAX_MEMORY = 0x10000    # the number of addressable memory locations
     1.5  BLANK = (0, 0, 0)
     1.6  
     1.7 -screen = array("B", repeat(0, MAX_WIDTH * 3 * MAX_HEIGHT))
     1.8 -
     1.9  def update(ula):
    1.10  
    1.11      """
    1.12 -    Return a screen array by reading from the 'ula'. This function effectively
    1.13 -    has the role of the video circuit, but also provides the clock signal to the
    1.14 -    ULA.
    1.15 +    Update the 'ula' for one frame. Return the resulting screen.
    1.16 +    """
    1.17 +
    1.18 +    video = ula.video
    1.19 +
    1.20 +    i = 0
    1.21 +    limit = MAX_SCANLINE * MAX_SCANPOS
    1.22 +    while i < limit:
    1.23 +        ula.update()
    1.24 +        video.update()
    1.25 +        i += 1
    1.26 +    return video.screen
    1.27 +
    1.28 +class Video:
    1.29 +
    1.30 +    """
    1.31 +    A class representing the video circuitry.
    1.32      """
    1.33  
    1.34 -    ula.vsync()
    1.35 -    pos = 0
    1.36 -    y = 0
    1.37 -    while y < MAX_SCANLINE:
    1.38 -        x = 0
    1.39 -        while x < MAX_SCANPOS:
    1.40 -            colour = ula.get_pixel_colour()
    1.41 -            if x < MAX_WIDTH and y < MAX_HEIGHT:
    1.42 -                screen[pos] = colour[0]; pos += 1
    1.43 -                screen[pos] = colour[1]; pos += 1
    1.44 -                screen[pos] = colour[2]; pos += 1
    1.45 -            x += 1
    1.46 -        ula.hsync()
    1.47 -        y += 1
    1.48 +    def __init__(self):
    1.49 +        self.screen = array("B", repeat(0, MAX_WIDTH * 3 * MAX_HEIGHT))
    1.50 +        self.colour = BLANK
    1.51 +        self.csync = 1
    1.52 +        self.hs = 1
    1.53 +        self.reset()
    1.54  
    1.55 -    return screen
    1.56 +    def reset(self):
    1.57 +        self.pos = 0
    1.58 +
    1.59 +    def update(self):
    1.60 +        if self.csync:
    1.61 +            if self.hs:
    1.62 +                self.screen[self.pos] = self.colour[0]; self.pos += 1
    1.63 +                self.screen[self.pos] = self.colour[1]; self.pos += 1
    1.64 +                self.screen[self.pos] = self.colour[2]; self.pos += 1
    1.65 +        else:
    1.66 +            self.pos = 0
    1.67  
    1.68  class ULA:
    1.69  
    1.70 -    "The ULA functionality."
    1.71 +    """
    1.72 +    A class providing the ULA functionality. Instances of this class refer to
    1.73 +    the system memory, maintain internal state (such as information about the
    1.74 +    current screen mode), and provide outputs (such as the current pixel
    1.75 +    colour).
    1.76 +    """
    1.77  
    1.78      modes = [
    1.79          (640, 1, 32), (320, 2, 32), (160, 4, 32),   # (width, depth, rows)
    1.80 @@ -55,17 +74,26 @@
    1.81  
    1.82      palette = range(0, 8) * 2
    1.83  
    1.84 -    def __init__(self, memory):
    1.85 +    def __init__(self, memory, video):
    1.86  
    1.87 -        "Initialise the ULA with the given 'memory'."
    1.88 +        "Initialise the ULA with the given 'memory' and 'video'."
    1.89  
    1.90          self.memory = memory
    1.91 +        self.video = video
    1.92          self.set_mode(6)
    1.93  
    1.94          # Internal state.
    1.95  
    1.96          self.buffer = [0] * 8
    1.97  
    1.98 +        self.reset()
    1.99 +
   1.100 +    def reset(self):
   1.101 +
   1.102 +        "Reset the ULA."
   1.103 +
   1.104 +        self.vsync()
   1.105 +
   1.106      def set_mode(self, mode):
   1.107  
   1.108          """
   1.109 @@ -80,12 +108,15 @@
   1.110            * horizontal pixel scaling factor
   1.111            * line spacing in pixels
   1.112            * number of entries in the pixel buffer
   1.113 +
   1.114 +        The ULA should be reset after a mode switch in order to cleanly display
   1.115 +        a full screen.
   1.116          """
   1.117  
   1.118          self.width, self.depth, rows = self.modes[mode]
   1.119  
   1.120 -        self.columns = (self.width * self.depth) / 8                        # bits read -> bytes read
   1.121 -        row_size = self.columns * LINES_PER_ROW
   1.122 +        columns = (self.width * self.depth) / 8                             # bits read -> bytes read
   1.123 +        row_size = columns * LINES_PER_ROW
   1.124  
   1.125          # Memory access configuration.
   1.126          # Note the limitation on positioning the screen start.
   1.127 @@ -115,19 +146,19 @@
   1.128          self.line_start = self.address = self.screen_start
   1.129          self.line = self.line_start % LINES_PER_ROW
   1.130          self.ssub = 0
   1.131 +        self.y = 0
   1.132          self.reset_horizontal()
   1.133  
   1.134 -    def reset_horizontal(self):
   1.135 -
   1.136 -        "Reset horizontal state."
   1.137 +        # Signal the video circuit.
   1.138  
   1.139 -        self.xsub = 0
   1.140 -        self.column = 0
   1.141 -        self.buffer_index = self.buffer_limit # need refill
   1.142 +        self.csync = self.video.csync = 1
   1.143  
   1.144      def hsync(self):
   1.145  
   1.146 -        "Signal the end of a line."
   1.147 +        "Signal the end of a scanline."
   1.148 +
   1.149 +        self.y += 1
   1.150 +        self.reset_horizontal()
   1.151  
   1.152          # Support spacing between character rows.
   1.153  
   1.154 @@ -135,7 +166,6 @@
   1.155              self.ssub -= 1
   1.156              return
   1.157  
   1.158 -        self.reset_horizontal()
   1.159          self.line += 1
   1.160  
   1.161          # If not on a row boundary, move to the next line.
   1.162 @@ -163,36 +193,73 @@
   1.163  
   1.164          self.line_start = self.address
   1.165  
   1.166 -    def get_pixel_colour(self):
   1.167 +    def reset_horizontal(self):
   1.168 +
   1.169 +        "Reset horizontal state."
   1.170 +
   1.171 +        self.x = 0
   1.172 +        self.buffer_index = self.buffer_limit # need refill
   1.173 +
   1.174 +        # Signal the video circuit.
   1.175 +
   1.176 +        self.hs = self.video.hs = 1
   1.177 +
   1.178 +    def update(self):
   1.179  
   1.180          """
   1.181 -        Return a pixel colour by reading from the pixel buffer.
   1.182 +        Update the pixel colour by reading from the pixel buffer.
   1.183          """
   1.184  
   1.185 +        # Detect the end of the line.
   1.186 +
   1.187 +        if self.x >= MAX_WIDTH:
   1.188 +            if self.x == MAX_WIDTH:
   1.189 +                self.hs = self.video.hs = 0
   1.190 +
   1.191 +            # Detect the end of the scanline.
   1.192 +
   1.193 +            elif self.x == MAX_SCANPOS:
   1.194 +                self.hsync()
   1.195 +
   1.196 +                # Detect the end of the frame.
   1.197 +
   1.198 +                if self.y == MAX_SCANLINE:
   1.199 +                    self.vsync()
   1.200 +
   1.201 +                # Detect the end of the screen.
   1.202 +
   1.203 +                elif self.y == MAX_HEIGHT:
   1.204 +                    self.csync = self.video.csync = 0
   1.205 +
   1.206          # Detect spacing between character rows.
   1.207  
   1.208          if self.ssub:
   1.209 -            return BLANK
   1.210 +            self.video.colour = BLANK
   1.211  
   1.212 -        # Scale pixels horizontally.
   1.213 +        # Detect horizontal and vertical sync conditions.
   1.214  
   1.215 -        if self.xsub == self.xscale:
   1.216 -            self.xsub = 0
   1.217 -            self.buffer_index += 1
   1.218 +        elif not self.hs or not self.csync:
   1.219 +            pass
   1.220 +
   1.221 +        # For pixels within the frame, obtain and output the value.
   1.222 +
   1.223 +        else:
   1.224  
   1.225 -        if self.buffer_index == self.buffer_limit:
   1.226 -            self.buffer_index = 0
   1.227 -            self.column += 1
   1.228 +            # Scale pixels horizontally, only accessing the next pixel value
   1.229 +            # after the required number of scan positions.
   1.230  
   1.231 -            # Detect the end of the scanline.
   1.232 +            if self.x % self.xscale == 0:
   1.233 +                self.buffer_index += 1
   1.234 +
   1.235 +            # Fill the buffer once all values have been read.
   1.236  
   1.237 -            if self.column > self.columns:
   1.238 -                return BLANK
   1.239 +            if self.buffer_index >= self.buffer_limit:
   1.240 +                self.buffer_index = 0
   1.241 +                self.fill_pixel_buffer()
   1.242  
   1.243 -            self.fill_pixel_buffer()
   1.244 +            self.video.colour = self.buffer[self.buffer_index]
   1.245  
   1.246 -        self.xsub += 1
   1.247 -        return self.buffer[self.buffer_index]
   1.248 +        self.x += 1
   1.249  
   1.250      def fill_pixel_buffer(self):
   1.251  
   1.252 @@ -276,9 +343,15 @@
   1.253  
   1.254  def get_ula():
   1.255  
   1.256 -    "Return a ULA initialised with a memory array."
   1.257 +    "Return a ULA initialised with a memory array and video."
   1.258 +
   1.259 +    return ULA(get_memory(), get_video())
   1.260  
   1.261 -    return ULA(get_memory())
   1.262 +def get_video():
   1.263 +
   1.264 +    "Return a video circuit."
   1.265 +
   1.266 +    return Video()
   1.267  
   1.268  def get_memory():
   1.269