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
|
y108-141) -- nicht 100% pixelidentisch zum Original, aber nah dran. Kann bei
|
||||||
Bedarf spaeter nachjustiert werden.
|
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,
|
Aufruf: python3 intro_sequence.py [--once] (--once: nach einem Durchlauf beenden,
|
||||||
sonst loopt die Sequenz weiter wie im Original-Titelbildschirm)
|
sonst loopt die Sequenz weiter wie im Original-Titelbildschirm)
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import threading
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(__file__))
|
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)
|
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):
|
def build_letterboxed(surface, canvas_w=320, canvas_h=240):
|
||||||
'''Zentriert ein kleineres PCC-Bild (z.B. 320x199/200) auf eine 320x240-
|
'''Zentriert ein kleineres PCC-Bild (z.B. 320x199/200) auf eine 320x240-
|
||||||
Leinwand mit schwarzen Balken -- wie im echten DOSBox-Fenster beobachtet.'''
|
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))
|
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
|
'''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)
|
parts = build_rauser_parts(assets)
|
||||||
sting = None
|
sting = None
|
||||||
try:
|
# TITEL2 Song 0 -- der kuerzere der beiden TITEL2-Songs (~2.04s), Song 1
|
||||||
if pygame.mixer.get_init():
|
# ist der Factor5-Sound (siehe Docstring).
|
||||||
# TITEL2 Song 0 -- der kuerzere der beiden TITEL2-Songs (~2.04s),
|
sting_path = audio_paths.get(('TITEL2', 0))
|
||||||
# Song 1 ist der neu zugeordnete Factor5-Sound (siehe Docstring).
|
if pygame.mixer.get_init() and sting_path:
|
||||||
sting_wav = tfmx_audio.render_wav(assets.container, 'TITEL2', song_index=0)
|
try:
|
||||||
sting = pygame.mixer.Sound(sting_wav)
|
sting = pygame.mixer.Sound(sting_path)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f'[intro_sequence] Rauser-Sound-Sting konnte nicht geladen werden: {exc}', file=sys.stderr)
|
print(f'[intro_sequence] Rauser-Sound-Sting konnte nicht geladen werden: {exc}', file=sys.stderr)
|
||||||
|
|
||||||
sting_played = False
|
sting_played = False
|
||||||
|
|
||||||
@@ -532,8 +645,8 @@ def render_title_wipe_frame(quads, t):
|
|||||||
return canvas
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
def run_once(screen, clock, assets):
|
def run_once(screen, clock, assets, audio_paths):
|
||||||
quit_requested = run_rauser(screen, clock, assets)
|
quit_requested = run_rauser(screen, clock, assets, audio_paths)
|
||||||
if quit_requested:
|
if quit_requested:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -551,13 +664,13 @@ def run_once(screen, clock, assets):
|
|||||||
# passt. Screen-Dauer ist jetzt FEST (~5s), nicht mehr an die (falsche)
|
# passt. Screen-Dauer ist jetzt FEST (~5s), nicht mehr an die (falsche)
|
||||||
# 123s-Songlaenge gekoppelt.
|
# 123s-Songlaenge gekoppelt.
|
||||||
factor5 = build_letterboxed(assets.pcc_surface('FACTOR5'))
|
factor5 = build_letterboxed(assets.pcc_surface('FACTOR5'))
|
||||||
try:
|
factor5_path = audio_paths.get(('TITEL2', 1))
|
||||||
if pygame.mixer.get_init():
|
if pygame.mixer.get_init() and factor5_path:
|
||||||
factor5_wav = tfmx_audio.render_wav(assets.container, 'TITEL2', song_index=1)
|
try:
|
||||||
factor5_sound = pygame.mixer.Sound(factor5_wav)
|
factor5_sound = pygame.mixer.Sound(factor5_path)
|
||||||
factor5_sound.play()
|
factor5_sound.play()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f'[intro_sequence] Factor5-Sound konnte nicht geladen werden: {exc}', file=sys.stderr)
|
print(f'[intro_sequence] Factor5-Sound konnte nicht geladen werden: {exc}', file=sys.stderr)
|
||||||
|
|
||||||
def show_factor5(_t):
|
def show_factor5(_t):
|
||||||
blit_scaled_centered(screen, factor5, WINDOW_W, WINDOW_H)
|
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
|
# weiter bis zum Ende der Sequenz (Original hat vermutlich durchgehende
|
||||||
# Titelmusik ueber Logo+Titelbild+Karte -- noch nicht per Referenz-
|
# Titelmusik ueber Logo+Titelbild+Karte -- noch nicht per Referenz-
|
||||||
# Sichtung bestaetigt, aber plausibelste Annahme fuers erste v1).
|
# Sichtung bestaetigt, aber plausibelste Annahme fuers erste v1).
|
||||||
try:
|
titel_path = audio_paths.get(('TITEL', None))
|
||||||
if pygame.mixer.get_init():
|
if pygame.mixer.get_init() and titel_path:
|
||||||
titel_wav = tfmx_audio.render_wav(assets.container, 'TITEL')
|
try:
|
||||||
pygame.mixer.music.load(titel_wav)
|
pygame.mixer.music.load(titel_path)
|
||||||
pygame.mixer.music.play(-1)
|
pygame.mixer.music.play(-1)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f'[intro_sequence] Titelmusik konnte nicht geladen werden: {exc}', file=sys.stderr)
|
print(f'[intro_sequence] Titelmusik konnte nicht geladen werden: {exc}', file=sys.stderr)
|
||||||
|
|
||||||
kw, kh, kidx, kpal = assets.pcc('KELLOGGS')
|
kw, kh, kidx, kpal = assets.pcc('KELLOGGS')
|
||||||
|
|
||||||
@@ -638,9 +751,16 @@ def main():
|
|||||||
|
|
||||||
assets = Assets(DAT_PATH)
|
assets = Assets(DAT_PATH)
|
||||||
|
|
||||||
quit_requested = run_once(screen, clock, assets)
|
# Alle TFMX-Songs EINMALIG vor der eigentlichen Sequenz rendern (mit
|
||||||
while not quit_requested and not once:
|
# sichtbarem, responsivem Ladebildschirm) -- siehe render_audio_with_loading
|
||||||
quit_requested = run_once(screen, clock, assets)
|
# 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():
|
if pygame.mixer.get_init():
|
||||||
pygame.mixer.music.stop()
|
pygame.mixer.music.stop()
|
||||||
|
|||||||
Reference in New Issue
Block a user