From afcceec986708f950ec0e68fe1273b239182876b Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:26:56 +0200 Subject: [PATCH] Fix skipped realtime analyzer frames --- scripts/test_gui_contracts.mjs | 7 +++++++ www/main.js | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/scripts/test_gui_contracts.mjs b/scripts/test_gui_contracts.mjs index a00a4eb..a9deb03 100644 --- a/scripts/test_gui_contracts.mjs +++ b/scripts/test_gui_contracts.mjs @@ -10,4 +10,11 @@ assert(!/\bopt-w-\d+\b/.test(styleSelect), 'style selector must not have a fixed assert(/adjustStyleDropdownWidth\(\)/.test(main), 'dynamic style selector sizing is missing'); assert(/styleSel\.style\.width\s*=/.test(main), 'dynamic style selector width assignment is missing'); +assert(/currentMetricsSeq\s*=\s*Number\.isFinite\(env\.audio\?\.phoenixMetricsSeq\)/.test(main), + 'render loop must track the metrics sequence used by the RTA'); +assert(/renderRequestGeneration\s*\+=\s*1/.test(main), + 'render requests must carry a generation that survives asynchronous rendering'); +assert(/renderRequestGeneration\s*===\s*renderGeneration\)\s*renderDirty\s*=\s*false/.test(main), + 'an in-flight render must not clear a newer render request'); + console.log('GUI contract regression tests passed'); diff --git a/www/main.js b/www/main.js index b373d27..35fffa5 100644 --- a/www/main.js +++ b/www/main.js @@ -2634,6 +2634,7 @@ let prevSaverActive = false; let audioRecoverInFlight = false; let lastAudioRecoverAt = 0; let renderDirty = true; +let renderRequestGeneration = 0; let lastRenderedAudioSeq = 0; let processingProfileDirty = true; let appliedProcessingProfileView = ''; @@ -2642,6 +2643,7 @@ const DATA_ONLY_RENDER_REASONS = new Set(['audio', 'spectro', 'visuals']); function requestRender(reason = 'ui') { renderDirty = true; + renderRequestGeneration += 1; if (!DATA_ONLY_RENDER_REASONS.has(reason)) processingProfileDirty = true; env.__lastRenderReason = reason; } @@ -2686,7 +2688,14 @@ async function loop(now){ } const targetMs = audioOk ? (1000 / desiredFpsForStyle(renderStyle)) : (1000 / 20); const target = targetMs; - const currentAudioSeq = Number.isFinite(env.audio?.xySeq) ? env.audio.xySeq : 0; + const currentMetricsSeq = Number.isFinite(env.audio?.phoenixMetricsSeq) + ? env.audio.phoenixMetricsSeq + : 0; + const currentVisualsSeq = Number.isFinite(env.audio?.phoenixVisualsSeq) + ? env.audio.phoenixVisualsSeq + : 0; + const currentXySeq = Number.isFinite(env.audio?.xySeq) ? env.audio.xySeq : 0; + const currentAudioSeq = Math.max(currentMetricsSeq, currentVisualsSeq, currentXySeq); const audioDirty = currentAudioSeq > lastRenderedAudioSeq; const shouldRenderFrame = renderDirty || @@ -2714,14 +2723,17 @@ async function loop(now){ } prevSaverActive = saverActive; if (!saverActive && shouldRenderFrame) { + const renderGeneration = renderRequestGeneration; meterFacade.setFrameStamp?.(++frameCounter); drawBG(); if (!currentView || currentViewId !== renderStyle) setView(renderStyle); if (currentView.render) await currentView.render(env, viewState); drawOptionsPanelBackdrop(); drawAudioLostOverlay(); - renderDirty = false; - lastRenderedAudioSeq = currentAudioSeq; + // A packet or UI event can request another frame while the async view + // render is in progress. Do not erase that newer request here. + if (renderRequestGeneration === renderGeneration) renderDirty = false; + lastRenderedAudioSeq = Math.max(lastRenderedAudioSeq, currentAudioSeq); } } catch(e){ showErr('Render error: ' + (e?.message || String(e)));