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:
+147
-27
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user