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