From 73b887e2f7052a2abf48daa3555da314579d12af Mon Sep 17 00:00:00 2001 From: ARIA Date: Thu, 23 Jul 2026 06:43:48 +0000 Subject: [PATCH] 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. --- tools/intro_sequence.py | 174 +++++++++++++++++++++++++++++++++------- 1 file changed, 147 insertions(+), 27 deletions(-) diff --git a/tools/intro_sequence.py b/tools/intro_sequence.py index dc46259..a5ccd91 100644 --- a/tools/intro_sequence.py +++ b/tools/intro_sequence.py @@ -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()