@@ -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
5050def 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