Skip to content

Commit f7f4213

Browse files
authored
Make sure the frame data is written all at once (#36)
1 parent 861bb6d commit f7f4213

1 file changed

Lines changed: 37 additions & 21 deletions

File tree

gambaterm/run.py

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ def get_ref(width: int, height: int, console: Console) -> tuple[int, int]:
3636
return refx, refy
3737

3838

39-
def write_bytes(term: Terminal, video_data: bytes) -> None:
39+
def write_frame(term: Terminal, frame_data: bytes) -> None:
4040
# Fix code page issue on windows:
4141
# `sys.stdout.buffer.raw` is a `WindowsConsoleIO` that always support UTF-8
4242
# regardless of the configured codepage
4343
if sys.platform == "win32" and term.stream.fileno() == sys.stdout.fileno():
44-
sys.stdout.buffer.write(video_data)
44+
sys.stdout.buffer.write(frame_data)
4545
sys.stdout.buffer.flush()
4646
else:
47-
os.write(term.stream.fileno(), video_data)
47+
os.write(term.stream.fileno(), frame_data)
4848

4949

5050
def run(
@@ -92,6 +92,8 @@ def run(
9292
new_frame = False
9393
screen_ready = True
9494
frame_start_time = None
95+
frame_data = bytearray()
96+
current_title_sequence = b""
9597

9698
# Loop over emulator frames
9799
for i in count():
@@ -134,52 +136,67 @@ def run(
134136

135137
# Render video
136138
with timing(video_deltas):
137-
# Send the frame
139+
# Re-use the same buffer to accumulate frame data and avoid unnecessary allocations.
140+
frame_data.clear()
141+
142+
# Detect if a shift is currently happening
138143
shift = shifting and shifting[-1] > 1 / fps
144+
145+
# Render a new frame only if:
146+
# - it is the right time according to frame_advance
147+
# - a new frame is available from the emulator
148+
# - the screen is ready for a new frame (either CPR sync is disabled, or enabled and we received the CPR response)
149+
# - we are not currently shifting (to prevent flooding the terminal with new frames when the rendering is too slow)
139150
if i % frame_advance == 0 and new_frame and screen_ready and not shift:
140151
new_frame = False
141-
# Check terminal size
152+
153+
# Detect terminal resize and color mode change
142154
new_height = term.height or 24
143155
new_width = term.width or 80
144-
maybe_clear_seq = b""
156+
maybe_clear_sequence = b""
145157
if (new_height, new_width) != (
146158
height,
147159
width,
148160
) or new_color_mode != color_mode:
149-
maybe_clear_seq = b"\033[H\033[2J"
161+
maybe_clear_sequence = b"\033[H\033[2J"
150162
height, width = new_height, new_width
151163
refx, refy = get_ref(width, height, console)
152164
color_mode = new_color_mode
153165
last_frame.fill(0)
166+
154167
# Render frame with synchronized output mode (DEC 2026) to prevent flickering
155168
# when the screen is cleared, or an artificial CRT-like "rolling band" side-effects
156169
# from fast "sprite blinking" meant to cause "transparency" effect on original HW,
157170
# https://zladx.github.io/posts/links-awakening-partial-translucency
158-
video_data = (
159-
b"\033[?2026h"
160-
+ maybe_clear_seq
161-
+ blit(video, last_frame, refx, refy, width - 1, height, color_mode)
162-
+ b"\033[?2026l"
171+
frame_data += b"\033[?2026h"
172+
frame_data += maybe_clear_sequence
173+
frame_data += blit(
174+
video, last_frame, refx, refy, width - 1, height, color_mode
163175
)
176+
frame_data += b"\033[?2026l"
164177
last_frame = video.copy()
178+
165179
# Update reporting
166-
data_length.append(len(video_data))
180+
data_length.append(len(frame_data))
167181
shown_frames.append(True)
182+
168183
# Ignore this video frame
169184
else:
170-
video_data = None
171185
data_length.append(0)
172186
shown_frames.append(False)
173187

188+
# Pacing and synchronization
174189
with timing(sync_deltas):
175190
# Video sync
176-
if video_data:
177-
write_bytes(term, video_data)
191+
if frame_data:
178192
# Send CPR request
179193
if use_cpr_sync:
180-
term.stream.write("\033[1;1H\033[6n")
181-
term.stream.flush()
194+
frame_data += b"\033[1;1H\033[6n"
182195
screen_ready = False
196+
# Add the current title
197+
frame_data += current_title_sequence
198+
# Write the entire frame in one go to avoid fragmentation
199+
write_frame(term, frame_data)
183200
# Timing sync
184201
increment = samples / console.TICKS_IN_FRAME
185202
deadline = start + increment / fps
@@ -190,7 +207,7 @@ def run(
190207
shifting.append(time.time() - deadline)
191208
start = deadline
192209

193-
# Reporting
210+
# Prepare title for the next frame
194211
if i % average_over == 1:
195212
tps = fps * console.TICKS_IN_FRAME
196213
emu_fps = tps * len(ticks) / sum(ticks)
@@ -207,5 +224,4 @@ def run(
207224
title += f"{data_rate:.0f} KB/s | "
208225
title += f"Audio: {audio_percent:.0f}% CPU | "
209226
title += f"{color_mode.report()} mode"
210-
term.stream.write(term.set_window_title(title))
211-
term.stream.flush()
227+
current_title_sequence = term.set_window_title(title).encode("utf-8")

0 commit comments

Comments
 (0)