1.1 --- a/ULA.txt Sun Dec 11 01:11:35 2011 +0100
1.2 +++ b/ULA.txt Mon Dec 12 01:31:51 2011 +0100
1.3 @@ -1,3 +1,16 @@
1.4 +Timing
1.5 +------
1.6 +
1.7 +According to the above (15.3.2 in the AUG), there are 312 scanlines, 256 of
1.8 +which are used to generate pixel data. At 50Hz, this means that 128 cycles are
1.9 +used to produce pixel data (2000000 / 50 = 40000; 40000 / 312 ~= 128). This is
1.10 +consistent with the observation that each scanline requires at most 80 bytes
1.11 +of data, and that the ULA is apparently busy for 40 out of 64 microseconds in
1.12 +each scanline.
1.13 +
1.14 +See: The Advanced User Guide for the Acorn Electron
1.15 +See: http://mdfs.net/Docs/Comp/Electron/Techinfo.htm
1.16 +
1.17 Hardware Scrolling
1.18 ------------------
1.19
2.1 --- a/main.py Sun Dec 11 01:11:35 2011 +0100
2.2 +++ b/main.py Mon Dec 12 01:31:51 2011 +0100
2.3 @@ -7,9 +7,32 @@
2.4 from ula import *
2.5 import pygame
2.6
2.7 -def update_surface(surface_array, screen_array):
2.8 - surface_array[:] = screen_array
2.9 - surface_array *= INTENSITY
2.10 +WIDTH = 640
2.11 +HEIGHT = 512
2.12 +INTENSITY = 255
2.13 +
2.14 +def update_screen(screen, screen_array):
2.15 +
2.16 + """
2.17 + Update the host's 'screen' surface with the contents of the 'screen_array'
2.18 + containing the pixel content of the screen.
2.19 + """
2.20 +
2.21 + surface = pygame.Surface((WIDTH, MAX_HEIGHT), 0, screen)
2.22 + a = pygame.surfarray.pixels3d(surface)
2.23 +
2.24 + # Copy the array to a surface and apply a pixel intensity.
2.25 +
2.26 + try:
2.27 + a[:] = screen_array
2.28 + a *= INTENSITY
2.29 + finally:
2.30 + del a
2.31 +
2.32 + # Scale the surface to the dimensions of the host's screen and copy the
2.33 + # result to the host's screen.
2.34 +
2.35 + screen.blit(pygame.transform.scale(surface, (WIDTH, HEIGHT)), (0, 0))
2.36
2.37 def mainloop():
2.38 while 1:
2.39 @@ -21,7 +44,6 @@
2.40 if __name__ == "__main__":
2.41 pygame.init()
2.42 screen = pygame.display.set_mode((WIDTH, HEIGHT), 0)
2.43 - a = pygame.surfarray.pixels3d(screen)
2.44
2.45 ula = get_ula()
2.46 ula_screen = get_screen()
2.47 @@ -33,12 +55,12 @@
2.48 ula.fill(0x3000, 0x5800 - 320, encode((1, 6), 4))
2.49 ula.fill(0x5800 - 320, 0x8000, encode((2, 7), 4))
2.50 update(ula_screen, ula)
2.51 - update_surface(a, ula_screen)
2.52 + update_screen(screen, ula_screen)
2.53 mainloop()
2.54
2.55 ula.screen_start = 0x3000 + 2
2.56 update(ula_screen, ula)
2.57 - update_surface(a, ula_screen)
2.58 + update_screen(screen, ula_screen)
2.59 mainloop()
2.60
2.61 # Test MODE 6.
2.62 @@ -48,12 +70,12 @@
2.63 ula.fill(0x6000, 0x6f00 + 160, encode((1, 0, 1, 1, 0, 0, 1, 1), 1))
2.64 ula.fill(0x6f00 + 160, 0x7f40, encode((1, 0, 1, 0, 1, 0, 1, 0), 1))
2.65 update(ula_screen, ula)
2.66 - update_surface(a, ula_screen)
2.67 + update_screen(screen, ula_screen)
2.68 mainloop()
2.69
2.70 ula.screen_start = 0x6f00 + 160
2.71 update(ula_screen, ula)
2.72 - update_surface(a, ula_screen)
2.73 + update_screen(screen, ula_screen)
2.74 mainloop()
2.75
2.76 # vim: tabstop=4 expandtab shiftwidth=4
3.1 --- a/ula.py Sun Dec 11 01:11:35 2011 +0100
3.2 +++ b/ula.py Mon Dec 12 01:31:51 2011 +0100
3.3 @@ -4,29 +4,31 @@
3.4 Acorn Electron ULA simulation.
3.5 """
3.6
3.7 -WIDTH = 640
3.8 -HEIGHT = 512
3.9 -INTENSITY = 255
3.10 -
3.11 -LINES_PER_ROW = 8
3.12 -MAX_HEIGHT = 256
3.13 -SCREEN_LIMIT = 0x8000
3.14 -MAX_MEMORY = 0x10000
3.15 +LINES_PER_ROW = 8 # the number of pixel lines per character row
3.16 +MAX_HEIGHT = 256 # the height of the screen in pixels
3.17 +MAX_SCANLINE = 312 # the number of scanlines in each frame
3.18 +MAX_WIDTH = 640 # the width of the screen in pixels
3.19 +MAX_SCANPOS = 1024 # the number of positions in each scanline
3.20 +SCREEN_LIMIT = 0x8000 # the first address after the screen memory
3.21 +MAX_MEMORY = 0x10000 # the number of addressable memory locations
3.22 BLANK = (0, 0, 0)
3.23
3.24 def update(screen, ula):
3.25
3.26 """
3.27 - Update the 'screen' array by reading from the 'ula'.
3.28 + Update the 'screen' array by reading from the 'ula'. This function
3.29 + effectively has the role of the video circuit, but also provides the clock
3.30 + signal to the ULA.
3.31 """
3.32
3.33 ula.vsync()
3.34 y = 0
3.35 - while y < HEIGHT:
3.36 + while y < MAX_SCANLINE:
3.37 x = 0
3.38 - while x < WIDTH:
3.39 + while x < MAX_SCANPOS:
3.40 colour = ula.get_pixel_colour()
3.41 - screen[x][y] = colour
3.42 + if x < MAX_WIDTH and y < MAX_HEIGHT:
3.43 + screen[x][y] = colour
3.44 x += 1
3.45 ula.hsync()
3.46 y += 1
3.47 @@ -66,14 +68,14 @@
3.48 * screen size in bytes
3.49 * default screen start address
3.50 * horizontal pixel scaling factor
3.51 - * vertical pixel scaling factor
3.52 * line spacing in pixels
3.53 * number of entries in the pixel buffer
3.54 """
3.55
3.56 self.width, self.depth, rows = self.modes[mode]
3.57
3.58 - row_size = (self.width * self.depth * LINES_PER_ROW) / 8 # bits per row -> bytes per row
3.59 + self.columns = (self.width * self.depth) / 8 # bits read -> bytes read
3.60 + row_size = self.columns * LINES_PER_ROW
3.61
3.62 # Memory access configuration.
3.63 # Note the limitation on positioning the screen start.
3.64 @@ -84,15 +86,13 @@
3.65
3.66 # Scanline configuration.
3.67
3.68 - self.xscale = WIDTH / self.width # pixel width in display pixels
3.69 - self.yscale = HEIGHT / (rows * LINES_PER_ROW) # pixel height in display pixels
3.70 -
3.71 + self.xscale = MAX_WIDTH / self.width # pixel width in display pixels
3.72 self.spacing = MAX_HEIGHT / rows - LINES_PER_ROW # pixels between rows
3.73
3.74 # Start of unused region.
3.75
3.76 self.footer = rows * LINES_PER_ROW
3.77 - self.margin = MAX_HEIGHT - rows * (LINES_PER_ROW + self.spacing) + self.spacing
3.78 + self.margin = MAX_SCANLINE - rows * (LINES_PER_ROW + self.spacing) + self.spacing
3.79
3.80 # Internal pixel buffer size.
3.81
3.82 @@ -104,7 +104,6 @@
3.83
3.84 self.line_start = self.address = self.screen_start
3.85 self.line = self.line_start % LINES_PER_ROW
3.86 - self.ysub = 0
3.87 self.ssub = 0
3.88 self.reset_horizontal()
3.89
3.90 @@ -113,6 +112,7 @@
3.91 "Reset horizontal state."
3.92
3.93 self.xsub = 0
3.94 + self.column = 0
3.95 self.buffer_index = self.buffer_limit # need refill
3.96
3.97 def hsync(self):
3.98 @@ -126,20 +126,6 @@
3.99 return
3.100
3.101 self.reset_horizontal()
3.102 -
3.103 - # Scale pixels vertically.
3.104 -
3.105 - self.ysub += 1
3.106 -
3.107 - # Re-read the current line if appropriate.
3.108 -
3.109 - if self.ysub < self.yscale:
3.110 - self.address = self.line_start
3.111 - return
3.112 -
3.113 - # Otherwise, move on to the next line.
3.114 -
3.115 - self.ysub = 0
3.116 self.line += 1
3.117
3.118 # If not on a row boundary, move to the next line.
3.119 @@ -158,12 +144,12 @@
3.120 # Test for the footer region.
3.121
3.122 if self.spacing and self.line == self.footer:
3.123 - self.ssub = self.margin * self.yscale
3.124 + self.ssub = self.margin
3.125 return
3.126
3.127 # Support spacing between character rows.
3.128
3.129 - self.ssub = self.spacing * self.yscale
3.130 + self.ssub = self.spacing
3.131
3.132 self.line_start = self.address
3.133
3.134 @@ -186,6 +172,13 @@
3.135
3.136 if self.buffer_index == self.buffer_limit:
3.137 self.buffer_index = 0
3.138 + self.column += 1
3.139 +
3.140 + # Detect the end of the scanline.
3.141 +
3.142 + if self.column > self.columns:
3.143 + return BLANK
3.144 +
3.145 self.fill_pixel_buffer()
3.146
3.147 self.xsub += 1
3.148 @@ -289,8 +282,8 @@
3.149
3.150 x = 0
3.151 screen = []
3.152 - while x < WIDTH:
3.153 - screen.append([(0, 0, 0)] * HEIGHT)
3.154 + while x < MAX_WIDTH:
3.155 + screen.append([(0, 0, 0)] * MAX_HEIGHT)
3.156 x += 1
3.157 return screen
3.158