Hang-Bug beim Rauser-Intro gefixt: TFMX-Rendering blockierte die pygame-Eventloop

Stefans Meldung 'nur noch ein schwarzes Bild, verschiebe ich das Fenster
wird der Desktop reingerendert, das Programm haengt' war real (per
Xvfb-Test verifiziert). Ursache: render_wav() rief render_tfmx als
blockierenden subprocess.run() auf -- bei einem frischen, ungecachten Song
(v.a. TITEL, ~109s) dauerte das reproduzierbar ~14s ohne dass die
pygame-Eventloop weiterlief, das OS stufte das Fenster dann als haengend
ein.

Fix: preload_all_audio() rendert alle benoetigten Songs jetzt EINMALIG vor
der eigentlichen Sequenz, per Hintergrund-Thread + responsivem
Ladebildschirm (render_audio_with_loading). run_once()/run_rauser() greifen
danach nur noch auf fertige WAV-Pfade zu, nie mehr Live-Rendering im
Hauptloop. Verifiziert per Xvfb-Testlauf: laeuft jetzt sauber durch.
This commit is contained in:
ARIA
2026-07-23 06:43:48 +00:00
parent af2ead172f
commit 73b887e2f7
+147 -27
View File
@@ -91,12 +91,27 @@ Indizes 34/42/43/44/45 in KELLOGGS.PCC = "praesentiert"-Text, bbox x71-241
y108-141) -- nicht 100% pixelidentisch zum Original, aber nah dran. Kann bei
Bedarf spaeter nachjustiert werden.
HANG-FIX (23.07.2026 -- Stefans Meldung "nur noch ein schwarzes Bild,
verschiebe ich das Fenster wird der Desktop reingerendert, das Programm
haengt"): war real und per Xvfb-Test verifiziert, kein Bedienfehler. Ursache:
tfmx_audio.render_wav() rendert TFMX-Musik per BLOCKIERENDEM subprocess.run()
-- bei einem frischen, ungecachten Song (v.a. TITEL, ~109s) dauert das
reproduzierbar mehrere -zig Sekunden (gemessen: ~14s), waehrend derer die
pygame-Eventloop NICHT lief -- das Betriebssystem haelt das Fenster dann fuer
"nicht reagierend" (schwarz eingefroren, Desktop blitzt beim Verschieben
durch). Fix: alle drei benoetigten Songs werden jetzt VOR der eigentlichen
Sequenz einmalig in preload_all_audio() gerendert, mit sichtbarem, responsivem
Ladebildschirm (render_audio_with_loading() -- Rendering laeuft in einem
Thread, Hauptthread pumpt weiter Events). run_once() greift danach nur noch
auf fertige WAV-Pfade zu, nie mehr Live-Rendering im Hauptloop.
Aufruf: python3 intro_sequence.py [--once] (--once: nach einem Durchlauf beenden,
sonst loopt die Sequenz weiter wie im Original-Titelbildschirm)
"""
import sys
import os
import time
import threading
from collections import deque
sys.path.insert(0, os.path.dirname(__file__))
@@ -307,6 +322,100 @@ def wait_or_skip(clock, seconds, screen_update_fn, fps=30):
clock.tick(fps)
def render_audio_with_loading(screen, clock, container, name, song_index, label):
'''Rendert einen TFMX-Song per tfmx_audio.render_wav in einem Hintergrund-
Thread, waehrend der Hauptthread weiter Events pumpt und einen simplen
Ladebildschirm zeichnet.
FIX (23.07.2026 -- Stefans Meldung "kommt nur noch ein schwarzes Bild,
verschiebe ich das Fenster wird der Desktop reingerendert, das Programm
haengt"): Ursache gefunden und per Test verifiziert, nicht nur vermutet --
render_wav() ruft render_tfmx als BLOCKIERENDEN subprocess.run() auf, ohne
dass die pygame-Eventloop weiterlaeuft. Bei einem frischen, noch nicht
gecachten Song (v.a. TITEL, ~109s Musik) dauert dieser eine Call
reproduzierbar mehrere -zig Sekunden (per Xvfb-Testlauf auf aria-wohnung
gemessen: ~14s Totalblockade direkt nach Factor5, bevor das Kellogg's-Logo
kommt). Ohne pygame.event.get()/display.flip() waehrend dieser Zeit stuft
das Betriebssystem das Fenster als "nicht reagierend" ein -- exakt das
beschriebene Bild (Inhalt bleibt schwarz stehen, beim Verschieben des
Fensters blitzt der darunterliegende Desktop durch, weil nichts neu
gezeichnet wird).
Fix: render_wav laeuft in einem Thread; der Haupt-Thread bleibt die ganze
Zeit responsiv (Events pumpen, Ladehinweis zeichnen, display.flip()) --
egal wie lange das Rendering dauert (auch beim allerersten Start ohne
Cache oder wenn build.sh den render_tfmx-Binary neu kompilieren muss).
Gibt (wav_path_or_None, quit_requested) zurueck. wav_path ist None wenn
das Rendering fehlschlaegt (z.B. kein C-Compiler verfuegbar) -- die
Sequenz laeuft dann wie bisher an dieser Stelle stumm weiter, kein
Absturz.'''
result = {}
def worker():
try:
result['path'] = tfmx_audio.render_wav(container, name, song_index=song_index)
except Exception as exc:
result['error'] = exc
thread = threading.Thread(target=worker, daemon=True)
thread.start()
try:
font = pygame.font.SysFont(None, 28)
except Exception:
font = None
dots = 0
last_dot_tick = time.monotonic()
while thread.is_alive():
for event in pygame.event.get():
if event.type == pygame.QUIT:
return None, True
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
return None, True
now = time.monotonic()
if now - last_dot_tick > 0.4:
dots = (dots + 1) % 4
last_dot_tick = now
screen.fill(BG_COLOR)
if font is not None:
text = font.render(f'Lade {label}{"." * dots}', True, (200, 200, 200))
screen.blit(text, (WINDOW_W // 2 - text.get_width() // 2, WINDOW_H // 2 - text.get_height() // 2))
pygame.display.flip()
clock.tick(30)
thread.join()
if 'error' in result:
print(f'[intro_sequence] {label} konnte nicht gerendert werden: {result["error"]}', file=sys.stderr)
return None, False
return result.get('path'), False
def preload_all_audio(screen, clock, assets):
'''Rendert VOR der eigentlichen Sequenz alle drei benoetigten TFMX-Songs
einmalig (siehe render_audio_with_loading-Docstring) -- danach ist jeder
run_once()-Durchlauf (auch der allererste!) garantiert freeze-frei, weil
zur Laufzeit nur noch aus dem WAV-Cache abgespielt wird statt live zu
rendern. Ueberspringt das Preloading komplett, wenn gar kein Audio-Device
verfuegbar ist (dann bliebe es sowieso stumm, siehe main()).'''
if not pygame.mixer.get_init():
return {}, False
jobs = [
(('TITEL2', 0), 'Rauser-Sound'),
(('TITEL2', 1), 'Factor5-Sound'),
(('TITEL', None), 'Titelmusik'),
]
audio_paths = {}
for key, label in jobs:
name, song_index = key
path, quit_requested = render_audio_with_loading(screen, clock, assets.container, name, song_index, label)
audio_paths[key] = path
if quit_requested:
return audio_paths, True
return audio_paths, False
def build_letterboxed(surface, canvas_w=320, canvas_h=240):
'''Zentriert ein kleineres PCC-Bild (z.B. 320x199/200) auf eine 320x240-
Leinwand mit schwarzen Balken -- wie im echten DOSBox-Fenster beobachtet.'''
@@ -385,19 +494,23 @@ def render_rauser_slide(canvas, parts, slide_t):
canvas.blit(r3, (r3_final_x, r3_y))
def run_rauser(screen, clock, assets):
def run_rauser(screen, clock, assets, audio_paths):
'''Rauser-Karte: weisser Grund, Slide-in, Sound-Sting bei Ankunft, Fade zu
grauem Grund, kurz halten. Gibt True zurueck wenn der User abbrechen will.'''
grauem Grund, kurz halten. Gibt True zurueck wenn der User abbrechen will.
audio_paths kommt aus preload_all_audio() -- das Audio wird hier NICHT
mehr live gerendert (siehe render_audio_with_loading-Docstring zum
Hang-Bug, den das behebt), sondern nur noch aus dem fertigen WAV-Pfad
geladen (schnell, nie blockierend).'''
parts = build_rauser_parts(assets)
sting = None
try:
if pygame.mixer.get_init():
# TITEL2 Song 0 -- der kuerzere der beiden TITEL2-Songs (~2.04s),
# Song 1 ist der neu zugeordnete Factor5-Sound (siehe Docstring).
sting_wav = tfmx_audio.render_wav(assets.container, 'TITEL2', song_index=0)
sting = pygame.mixer.Sound(sting_wav)
except Exception as exc:
print(f'[intro_sequence] Rauser-Sound-Sting konnte nicht geladen werden: {exc}', file=sys.stderr)
# TITEL2 Song 0 -- der kuerzere der beiden TITEL2-Songs (~2.04s), Song 1
# ist der Factor5-Sound (siehe Docstring).
sting_path = audio_paths.get(('TITEL2', 0))
if pygame.mixer.get_init() and sting_path:
try:
sting = pygame.mixer.Sound(sting_path)
except Exception as exc:
print(f'[intro_sequence] Rauser-Sound-Sting konnte nicht geladen werden: {exc}', file=sys.stderr)
sting_played = False
@@ -532,8 +645,8 @@ def render_title_wipe_frame(quads, t):
return canvas
def run_once(screen, clock, assets):
quit_requested = run_rauser(screen, clock, assets)
def run_once(screen, clock, assets, audio_paths):
quit_requested = run_rauser(screen, clock, assets, audio_paths)
if quit_requested:
return True
@@ -551,13 +664,13 @@ def run_once(screen, clock, assets):
# passt. Screen-Dauer ist jetzt FEST (~5s), nicht mehr an die (falsche)
# 123s-Songlaenge gekoppelt.
factor5 = build_letterboxed(assets.pcc_surface('FACTOR5'))
try:
if pygame.mixer.get_init():
factor5_wav = tfmx_audio.render_wav(assets.container, 'TITEL2', song_index=1)
factor5_sound = pygame.mixer.Sound(factor5_wav)
factor5_path = audio_paths.get(('TITEL2', 1))
if pygame.mixer.get_init() and factor5_path:
try:
factor5_sound = pygame.mixer.Sound(factor5_path)
factor5_sound.play()
except Exception as exc:
print(f'[intro_sequence] Factor5-Sound konnte nicht geladen werden: {exc}', file=sys.stderr)
except Exception as exc:
print(f'[intro_sequence] Factor5-Sound konnte nicht geladen werden: {exc}', file=sys.stderr)
def show_factor5(_t):
blit_scaled_centered(screen, factor5, WINDOW_W, WINDOW_H)
@@ -575,13 +688,13 @@ def run_once(screen, clock, assets):
# weiter bis zum Ende der Sequenz (Original hat vermutlich durchgehende
# Titelmusik ueber Logo+Titelbild+Karte -- noch nicht per Referenz-
# Sichtung bestaetigt, aber plausibelste Annahme fuers erste v1).
try:
if pygame.mixer.get_init():
titel_wav = tfmx_audio.render_wav(assets.container, 'TITEL')
pygame.mixer.music.load(titel_wav)
titel_path = audio_paths.get(('TITEL', None))
if pygame.mixer.get_init() and titel_path:
try:
pygame.mixer.music.load(titel_path)
pygame.mixer.music.play(-1)
except Exception as exc:
print(f'[intro_sequence] Titelmusik konnte nicht geladen werden: {exc}', file=sys.stderr)
except Exception as exc:
print(f'[intro_sequence] Titelmusik konnte nicht geladen werden: {exc}', file=sys.stderr)
kw, kh, kidx, kpal = assets.pcc('KELLOGGS')
@@ -638,9 +751,16 @@ def main():
assets = Assets(DAT_PATH)
quit_requested = run_once(screen, clock, assets)
while not quit_requested and not once:
quit_requested = run_once(screen, clock, assets)
# Alle TFMX-Songs EINMALIG vor der eigentlichen Sequenz rendern (mit
# sichtbarem, responsivem Ladebildschirm) -- siehe render_audio_with_loading
# zum Hang-Bug, den das behebt. Danach greifen alle run_once()-Durchlaeufe
# nur noch auf fertige WAV-Pfade zu, nie mehr live-render im Hauptloop.
audio_paths, quit_requested = preload_all_audio(screen, clock, assets)
if not quit_requested:
quit_requested = run_once(screen, clock, assets, audio_paths)
while not quit_requested and not once:
quit_requested = run_once(screen, clock, assets, audio_paths)
if pygame.mixer.get_init():
pygame.mixer.music.stop()