[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.
This commit is contained in:
Geoffrey McRae
2026-08-05 11:58:32 +10:00
parent 12f6485e52
commit afb80e0ae4
3 changed files with 83 additions and 46 deletions

View File

@@ -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();

View File

@@ -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 <string.h>
#include <math.h>
#include <stdatomic.h>
#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);
}