From afb80e0ae45232f023bc8a9a31d6ce5c420c8e90 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 5 Aug 2026 11:58:32 +1000 Subject: [PATCH] [client] overlay: render splash fade from monotonic time Move splash transitions onto the render thread and derive opacity from elapsed monotonic time. Pace active animation at a display-compatible minimum of 60 Hz while allowing incoming frames to satisfy that demand. Reset the splash cleanly across reconnects and latch state changes with the full-frame restoration decision. --- client/include/interface/overlay.h | 2 +- client/src/main.c | 28 ++++++--- client/src/overlay/splash.c | 99 +++++++++++++++++++----------- 3 files changed, 83 insertions(+), 46 deletions(-) diff --git a/client/include/interface/overlay.h b/client/include/interface/overlay.h index 32c66657..5a0f9664 100644 --- a/client/include/interface/overlay.h +++ b/client/include/interface/overlay.h @@ -41,7 +41,7 @@ struct LG_OverlayOps /* final free */ void (*free)(void * udata); - /* return true if realtime rendering is required when in jitRender mode + /* return true if realtime rendering is required * optional, if omitted assumes false */ bool (*needs_render)(void * udata, bool interactive); diff --git a/client/src/main.c b/client/src/main.c index 716f6da5..e7572a22 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -630,9 +630,16 @@ static int renderThread(void * unused) const uint64_t pending = atomic_load_explicit(&g_state.pendingCount, memory_order_acquire); - const bool overlayRender = app_overlayNeedsRender() && - (!g_state.lastRenderTimeValid || - nanotime() - g_state.lastRenderTime >= g_state.overlayFrameTime); + const bool overlayNeeded = app_overlayNeedsRender(); + uint64_t outputPeriod = 0; + const bool periodKnown = overlayNeeded && + g_state.ds->getFramePeriod && + g_state.ds->getFramePeriod(&outputPeriod); + const uint64_t elapsed = g_state.lastRenderTimeValid ? + nanotime() - g_state.lastRenderTime : 0; + const bool overlayRender = overlayNeeded && + (!g_state.lastRenderTimeValid || !periodKnown || + elapsed + outputPeriod >= g_state.overlayFrameTime); if (!lgResetEvent(g_state.frameEvent) && !forceRender @@ -690,10 +697,6 @@ static int renderThread(void * unused) atomic_compare_exchange_weak(&g_state.lgrResize, &resize, 0); } - const bool invalidate = - atomic_exchange(&g_state.invalidateWindow, false) || - app_overlayNeedsFullRender(); - const LG_RendererFrameToken frameTokenLimit = frameTimingQueuedToken(); const uint64_t prepareStart = nanotime(); @@ -701,6 +704,10 @@ static int renderThread(void * unused) renderQueue_process(); + const bool windowInvalid = + atomic_exchange(&g_state.invalidateWindow, false); + const bool overlayFull = app_overlayNeedsFullRender(); + const bool invalidate = windowInvalid || overlayFull; const uint64_t prepareTime = nanotime() - prepareStart; LG_RendererFrameTiming rendererTiming = {}; @@ -728,7 +735,7 @@ static int renderThread(void * unused) { /* A frame-driven swap also satisfies the minimum render rate. */ clock_gettime(CLOCK_MONOTONIC, &time); - tsAdd(&time, app_isOverlayMode() ? + tsAdd(&time, app_overlayNeedsRender() ? g_state.overlayFrameTime : g_state.frameTime); } @@ -1114,16 +1121,19 @@ int main_frameThread(void * unused) if (atomic_load_explicit(&g_state.pendingCount, memory_order_acquire) < 10) atomic_fetch_add_explicit(&g_state.pendingCount, 1, memory_order_release); + overlaySplash_show(false); } else + { + overlaySplash_show(false); lgSignalEvent(g_state.frameEvent); + } LG_TransportFrameTiming timing = {}; if (g_state.transportOps->getFrameTiming) g_state.transportOps->getFrameTiming( g_state.transport, &frame, &timing); - overlaySplash_show(false); if ((frame.flags & LG_TRANSPORT_FRAME_REQUEST_ACTIVATION) && g_params.requestActivation) g_state.ds->requestActivation(); diff --git a/client/src/overlay/splash.c b/client/src/overlay/splash.c index a90c84da..f6f2d003 100644 --- a/client/src/overlay/splash.c +++ b/client/src/overlay/splash.c @@ -30,21 +30,32 @@ #include "common/appstrings.h" #include "common/stringlist.h" #include "common/stringutils.h" +#include "common/time.h" #include "resources/lg-logo.svg.h" #include #include +#include -#define SEGMENTS 12 +#define SEGMENTS 12 +#define FADE_TIME_NS 1000000000ULL -static bool l_show; -static bool l_fadeDone; -static float l_alpha; -static OverlayImage l_logo; -static float l_vectors[SEGMENTS][2]; -static StringList l_tagline; -static StringList l_footline; +enum SplashState +{ + SPLASH_VISIBLE, + SPLASH_FADING, + SPLASH_HIDDEN, +}; + +static _Atomic(bool) l_requestedShow; +static bool l_appliedShow; +static enum SplashState l_state; +static uint64_t l_fadeStart; +static OverlayImage l_logo; +static float l_vectors[SEGMENTS][2]; +static StringList l_tagline; +static StringList l_footline; static void calcRadialVectors(float vectors[][2], int segments) { @@ -87,9 +98,10 @@ static void drawRadialGradient(ImDrawList * list, int x, int y, int w, int h, static bool splash_init(void ** udata, const void * params) { - l_show = true; - l_fadeDone = false; - l_alpha = 1.0f; + atomic_store_explicit(&l_requestedShow, true, memory_order_relaxed); + l_appliedShow = true; + l_state = SPLASH_VISIBLE; + l_fadeStart = 0; overlayLoadSVG(b_lg_logo_svg, b_lg_logo_svg_size, &l_logo, 200, 200); calcRadialVectors(l_vectors, ARRAY_LENGTH(l_vectors)); @@ -155,10 +167,20 @@ static void renderText(ImDrawList * list, int x, int y, ImU32 color, static int splash_render(void * udata, bool interactive, struct Rect * windowRects, int maxRects) { - if (!l_show && l_fadeDone) + float alpha = 1.0f; + if (l_state == SPLASH_HIDDEN) return 0; + if (l_state == SPLASH_FADING) + { + const uint64_t elapsed = nanotime() - l_fadeStart; + if (elapsed >= FADE_TIME_NS) + { + l_state = SPLASH_HIDDEN; + return 0; + } + alpha -= (float)elapsed / (float)FADE_TIME_NS; + } - const float alpha = l_fadeDone ? 1.0f : l_alpha; ImVec2 * screen = overlayGetScreenSize(); ImDrawList * list = igGetBackgroundDrawList_Nil(); @@ -226,30 +248,36 @@ static int splash_render(void * udata, bool interactive, struct Rect * windowRec return 1; } -static bool splash_tick(void * udata, unsigned long long tickCount) +static bool splash_needsRender(void * udata, bool interactive) { - if (!l_show && !l_fadeDone) - { - if (g_params.quickSplash) - { - l_fadeDone = true; - app_invalidateWindow(true); - return false; - } - - l_alpha -= 1.0f / TICK_RATE; - if (l_alpha <= 0.0f) - l_fadeDone = true; - app_invalidateWindow(true); - return false; - } - - return false; + return l_state == SPLASH_FADING || + atomic_load_explicit(&l_requestedShow, memory_order_acquire) != + l_appliedShow; } static bool splash_needsFullRender(void * udata) { - return !l_show && !l_fadeDone; + const bool show = + atomic_load_explicit(&l_requestedShow, memory_order_acquire); + const bool changed = show != l_appliedShow; + if (changed) + { + l_appliedShow = show; + if (show) + { + l_state = SPLASH_VISIBLE; + l_fadeStart = 0; + } + else if (g_params.quickSplash) + l_state = SPLASH_HIDDEN; + else + { + l_state = SPLASH_FADING; + l_fadeStart = nanotime(); + } + } + + return changed || l_state == SPLASH_FADING; } struct LG_OverlayOps LGOverlaySplash = @@ -257,17 +285,16 @@ struct LG_OverlayOps LGOverlaySplash = .name = "splash", .init = splash_init, .free = splash_free, + .needs_render = splash_needsRender, .needs_full_render = splash_needsFullRender, .render = splash_render, - .tick = splash_tick, }; void overlaySplash_show(bool show) { - if (l_show == show) + if (atomic_exchange_explicit( + &l_requestedShow, show, memory_order_acq_rel) == show) return; - l_show = show; - app_invalidateOverlay(true); app_invalidateWindow(true); }