Skip to content

Commit 2c7bbbe

Browse files
committed
Speed up rendering with framebuffer delta updates
1 parent 549e596 commit 2c7bbbe

File tree

6 files changed

+30
-32
lines changed

6 files changed

+30
-32
lines changed

scchip/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
# App identification
77
APP_NAME = "SuperChocChip Emulator"
8-
APP_VERSION = "1.3.0"
8+
APP_VERSION = "1.3.1"
99
APP_COPYRIGHT = "Copyright (C) 2022 Gregory Maynard-Hoare, licensed under GNU Affero General Public License v3.0"
1010
APP_INTRO = "{} V{} -- ".format(APP_NAME, APP_VERSION)
1111

scchip/framebuffer.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def __init__(self, renderer, num_planes=1, allow_wrapping=True):
4545
self.vid_size = 0
4646
self.vid_cache = RAM()
4747
self.ram_banks = []
48+
self.frame_delta = {}
4849
self.report_perf()
4950

5051
for _ in range(num_planes):
@@ -85,9 +86,7 @@ def clear(self):
8586
for plane in self.affect_planes:
8687
plane.clear()
8788

88-
for y in range(self.vid_height):
89-
for x in range(self.vid_width):
90-
self._render_pixel(x, y)
89+
self._redraw_all()
9190

9291
def get_affected_planes(self):
9392
return self.affect_planes
@@ -120,7 +119,7 @@ def _render_pixel(self, x, y):
120119
colour += 2 ** plane_num
121120

122121
if self.vid_cache.read(vram_loc) != colour:
123-
self.renderer.set_pixel(x, y, colour)
122+
self.frame_delta[(x, y)] = colour
124123
self.vid_cache.write(vram_loc, colour)
125124

126125
# Half-pixel vertical scrolling is unsupported in 64x32 pixel mode
@@ -137,7 +136,7 @@ def scroll_up(self, rows):
137136
plane.move_mem(-mem_offset) # Usually moves contents up by 1 pixel, or 0.5 on low resolution
138137
plane.zero_block(vid_size - mem_offset, mem_offset) # Erase the bottom strips
139138

140-
self._post_scroll()
139+
self._redraw_all()
141140

142141
def scroll_left(self, cols):
143142
if not self.affect_planes:
@@ -153,7 +152,7 @@ def scroll_left(self, cols):
153152
# Erase 4-pixel block to right of line in high resolution, 2 on low resolution
154153
plane.zero_block(vid_width * y - cols, cols)
155154

156-
self._post_scroll()
155+
self._redraw_all()
157156

158157
def scroll_right(self, cols):
159158
if not self.affect_planes:
@@ -169,7 +168,7 @@ def scroll_right(self, cols):
169168
# Erase 4-pixel block to left of line in high resolution, 2 on low resolution
170169
plane.zero_block(vid_width * y, cols)
171170

172-
self._post_scroll()
171+
self._redraw_all()
173172

174173
def scroll_down(self, rows):
175174
if not self.affect_planes:
@@ -181,16 +180,23 @@ def scroll_down(self, rows):
181180
plane.move_mem(mem_offset) # Usually moves contents down by 1 pixel, 0.5 on low resolution
182181
plane.zero_block(0, mem_offset) # Erase the top strips
183182

184-
self._post_scroll()
183+
self._redraw_all()
185184

186-
def _post_scroll(self):
185+
def _redraw_all(self):
187186
# Redraw whole screen after a scroll. The video cache should take the load off the renderer a bit
188187
for y in range(self.vid_height):
189188
for x in range(self.vid_width):
190189
self._render_pixel(x, y)
191190

192191
def refresh_display(self):
193-
self.renderer.refresh_display()
192+
# Request the renderer updates altered pixels and then refreshes the display. This method results in a huge
193+
# (around 5x) speed up when using PyPy with graphically-intensive games, and a tiny improvement with CPython.
194+
for xy, colour in self.frame_delta.items():
195+
self.renderer.set_pixel(*xy, colour)
196+
197+
content_changed = bool(self.frame_delta)
198+
self.frame_delta.clear()
199+
self.renderer.refresh_display(content_changed)
194200

195201
def switch_planes(self, mask):
196202
try:

scchip/renderers/r_curses.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import curses
2222
import _curses
2323
from .r_null import RendererError, Renderer as RendererBase
24+
from ..constants import APP_NAME
2425

2526

2627
class Renderer(RendererBase):
@@ -92,6 +93,7 @@ def set_resolution(self, width, height):
9293
self.pad.bkgd(" ", curses.color_pair(self.palette_index[0]))
9394

9495
super().set_resolution(width, height)
96+
self.set_title(APP_NAME)
9597

9698
def set_pixel(self, x, y, colour):
9799
if self.palette_index:
@@ -100,14 +102,13 @@ def set_pixel(self, x, y, colour):
100102
curses_colour = curses.A_REVERSE if colour else curses.A_NORMAL
101103

102104
self.pad.addstr(y + 1, x * self.scale, self.pixel_char, curses_colour)
103-
super().set_pixel(x, y, colour)
104105

105-
def refresh_display(self):
106+
def refresh_display(self, content_changed=False):
106107
screen_height, screen_width = self.screen.getmaxyx() # This doesn't seem to ever change/work on Windows?!
107108

108109
if screen_height == self.last_screen_height and screen_width == self.last_screen_width:
109110
# Fast delta update
110-
if self.refresh_needed:
111+
if content_changed:
111112
self.pad.refresh(0, 0, 0, 0, screen_height - 1, screen_width - 1)
112113
else:
113114
# Screen resolution changed, redraw everything
@@ -121,17 +122,13 @@ def refresh_display(self):
121122
self.last_screen_height = screen_height
122123
self.last_screen_width = screen_width
123124

124-
super().refresh_display()
125-
126125
def set_title(self, title):
127126
if self.pad:
128127
title_len = len(title)
129128

130129
if self.width > title_len:
131130
self.pad.addstr(0, 0, "".join((title, " " * (self.width * self.scale - title_len))), curses.A_REVERSE)
132-
self.refresh_needed = True
133-
134-
super().set_title(title)
131+
self.refresh_display(True)
135132

136133
def shutdown(self):
137134
if self.screen:

scchip/renderers/r_null.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,16 @@ def __init__(self, scale=None, use_colour=True, **kwargs): # pylint: disable=un
2222
self.scale = 1 if scale is None else scale
2323
self.use_colour = use_colour
2424
self.set_resolution(0, 0)
25-
self.refresh_needed = False
2625

2726
def set_resolution(self, width, height):
2827
self.width = width
2928
self.height = height
30-
self.refresh_needed = False
3129

3230
def set_pixel(self, x, y, colour): # pylint: disable=unused-argument
33-
self.refresh_needed = True
31+
pass
3432

35-
def refresh_display(self):
36-
self.refresh_needed = False
33+
def refresh_display(self, content_changed=False):
34+
pass
3735

3836
def set_title(self, title):
3937
pass

scchip/renderers/r_pygame.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import pygame
2222
from .r_null import RendererError, Renderer as RendererBase
23+
from ..constants import APP_NAME
2324

2425

2526
class Renderer(RendererBase):
@@ -28,7 +29,7 @@ def __init__(self, scale=None, use_colour=True, pygame_palette=None, smoothing=0
2829
scale = 512 # Default window width if not supplied, or set to default
2930

3031
pygame.display.init()
31-
self.set_title("Starting...") # Perhaps an icon would be nice, too?
32+
self.set_title(APP_NAME) # Perhaps an icon would be nice, too?
3233
self.pixel_array = None
3334
self.scaled_size = (scale, scale // 2)
3435
self.display_surface = pygame.display.set_mode(self.scaled_size, 0, 8)
@@ -72,10 +73,9 @@ def set_resolution(self, width, height):
7273

7374
def set_pixel(self, x, y, colour):
7475
self.pixel_array[x][y] = self.colour_map[colour]
75-
super().set_pixel(x, y, colour)
7676

77-
def refresh_display(self):
78-
if self.refresh_needed and self.pixel_array:
77+
def refresh_display(self, content_changed=False):
78+
if content_changed and self.pixel_array:
7979
self.pixel_array.close()
8080
del self.pixel_array
8181
render_surface = self.render_surface
@@ -89,11 +89,8 @@ def refresh_display(self):
8989
pygame.display.flip()
9090
self._set_pixel_array()
9191

92-
super().refresh_display()
93-
9492
def set_title(self, title):
9593
pygame.display.set_caption(title)
96-
super().set_title(title)
9794

9895
def _set_pixel_array(self):
9996
self.pixel_array = pygame.PixelArray(self.render_surface)

superchocchip.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
__author__ = "Gregory Maynard-Hoare"
44
__copyright__ = "Copyright (C) 2022 Gregory Maynard-Hoare"
55
__license__ = "GNU Affero General Public License v3.0"
6-
__version__ = "1.3.0"
6+
__version__ = "1.3.1"
77

88
from argparse import ArgumentParser
99
from scchip import main

0 commit comments

Comments
 (0)