[client] cursor: pace composited redraws

Coalesce pointer updates to the nominal output cadence instead of
waking the renderer for every transport packet. Keep request and
presented generations separate so updates racing a render receive a
trailing swap.

Let JIT consume cursor changes on display callbacks to preserve VRR
cadence, and use a deadline worker for non-JIT rendering. Give normal
source frames a short grace period so an on-rate feed absorbs the
cursor update instead of producing an extra swap.

Repaint visibility and cursor-content changes even when movement
redraws are disabled, including the transition that hides the cursor.
This commit is contained in:
Geoffrey McRae
2026-08-06 18:52:46 +10:00
parent 604b3082f3
commit 5cc7f06185
3 changed files with 208 additions and 19 deletions

View File

@@ -398,7 +398,7 @@ static struct Option options[] =
{ {
.module = "input", .module = "input",
.name = "mouseRedraw", .name = "mouseRedraw",
.description = "Mouse movements trigger redraws (ignores FPS minimum)", .description = "Mouse movements trigger display-rate redraws",
.type = OPTION_TYPE_BOOL, .type = OPTION_TYPE_BOOL,
.value.x_bool = true, .value.x_bool = true,
}, },

View File

@@ -83,10 +83,24 @@ _Static_assert((int)LG_CAPTURE_RGBA32F == (int)LG_TEST_CAPTURE_RGBA32F,
// forwards // forwards
static int renderThread(void * unused); static int renderThread(void * unused);
static LGEvent *e_startup = NULL; static LGEvent *e_startup = NULL;
static LGEvent *e_spice = NULL; static LGEvent *e_spice = NULL;
static LGThread *t_spice = NULL; static LGEvent *e_cursorRepaint = NULL;
static LGThread *t_render = NULL; static LGThread *t_spice = NULL;
static LGThread *t_render = NULL;
static LGThread *t_cursorRepaint = NULL;
static struct
{
LG_Lock lock;
uint64_t requestedSerial;
uint64_t presentedSerial;
uint64_t lastRenderTime;
bool rendering;
bool wakeArmed;
bool lastRenderCursorOnly;
}
l_cursorRepaint;
_Atomic(enum RunState) p_appState = APP_STATE_RUNNING; _Atomic(enum RunState) p_appState = APP_STATE_RUNNING;
@@ -660,6 +674,125 @@ static void preSwapCallback(void * udata)
#endif #endif
} }
static uint64_t cursorRepaintPeriod(void)
{
uint64_t period = g_state.overlayFrameTime;
uint64_t outputPeriod;
if (g_state.ds->getFramePeriod &&
g_state.ds->getFramePeriod(&outputPeriod) && outputPeriod)
period = outputPeriod;
if (overlaySplash_isFading())
period = max(period, g_state.overlayFrameTime);
return period;
}
static bool cursorRepaintNeeded(void)
{
LG_LOCK(l_cursorRepaint.lock);
const bool result =
l_cursorRepaint.requestedSerial != l_cursorRepaint.presentedSerial &&
!overlaySplash_isFading();
LG_UNLOCK(l_cursorRepaint.lock);
return result;
}
static void cursorRepaintRequest(void)
{
LG_LOCK(l_cursorRepaint.lock);
const bool wasDirty =
l_cursorRepaint.requestedSerial != l_cursorRepaint.presentedSerial;
++l_cursorRepaint.requestedSerial;
LG_UNLOCK(l_cursorRepaint.lock);
if (!g_state.jitRender && !wasDirty)
lgSignalEvent(e_cursorRepaint);
}
static uint64_t cursorRepaintRenderBegin(bool * cursorWake)
{
LG_LOCK(l_cursorRepaint.lock);
const uint64_t serial = l_cursorRepaint.requestedSerial;
l_cursorRepaint.rendering = true;
*cursorWake = l_cursorRepaint.wakeArmed;
if (l_cursorRepaint.wakeArmed)
l_cursorRepaint.wakeArmed = false;
LG_UNLOCK(l_cursorRepaint.lock);
return serial;
}
static void cursorRepaintRenderEnd(
uint64_t serial, uint64_t renderTime, bool success, bool cursorOnly)
{
LG_LOCK(l_cursorRepaint.lock);
if (success)
{
l_cursorRepaint.presentedSerial = serial;
l_cursorRepaint.lastRenderTime = renderTime;
l_cursorRepaint.lastRenderCursorOnly = cursorOnly;
}
l_cursorRepaint.rendering = false;
const bool wake = success && !g_state.jitRender &&
l_cursorRepaint.requestedSerial != l_cursorRepaint.presentedSerial;
LG_UNLOCK(l_cursorRepaint.lock);
if (wake)
lgSignalEvent(e_cursorRepaint);
}
static int cursorRepaintThread(void * unused)
{
while (app_getState() != APP_STATE_SHUTDOWN)
{
lgWaitEvent(e_cursorRepaint, TIMEOUT_INFINITE);
while (app_getState() != APP_STATE_SHUTDOWN)
{
uint64_t waitTime = 0;
LG_LOCK(l_cursorRepaint.lock);
if (l_cursorRepaint.requestedSerial ==
l_cursorRepaint.presentedSerial ||
l_cursorRepaint.rendering || l_cursorRepaint.wakeArmed)
{
LG_UNLOCK(l_cursorRepaint.lock);
break;
}
const uint64_t now = nanotime();
const uint64_t period = cursorRepaintPeriod();
/* Give an on-rate source frame enough time to absorb this cursor update.
* Cursor-only rendering retains the full output cadence once needed. */
const uint64_t grace =
l_cursorRepaint.lastRenderCursorOnly ? 0 : period / 4;
const uint64_t deadline =
l_cursorRepaint.lastRenderTime + period + grace;
if (l_cursorRepaint.lastRenderTime && now < deadline)
waitTime = deadline - now;
else
{
l_cursorRepaint.wakeArmed = true;
app_invalidateWindow(false);
}
LG_UNLOCK(l_cursorRepaint.lock);
if (!waitTime)
break;
struct timespec deadlineTime;
clock_gettime(CLOCK_MONOTONIC, &deadlineTime);
tsAdd(&deadlineTime, waitTime);
lgWaitEventAbs(e_cursorRepaint, &deadlineTime);
}
}
return 0;
}
static int renderThread(void * unused) static int renderThread(void * unused)
{ {
if (!RENDERER(renderStartup, g_state.useDMA)) if (!RENDERER(renderStartup, g_state.useDMA))
@@ -731,8 +864,10 @@ static int renderThread(void * unused)
(!g_state.lastRenderTimeValid || !periodKnown || (!g_state.lastRenderTimeValid || !periodKnown ||
elapsed + outputPeriod >= g_state.overlayFrameTime); elapsed + outputPeriod >= g_state.overlayFrameTime);
const bool clientWake = lgResetEvent(g_state.frameEvent); const bool clientWake = lgResetEvent(g_state.frameEvent);
const bool cursorRender = cursorRepaintNeeded();
if (!clientWake && !forceRender && !pending && !overlayRender) if (!clientWake && !forceRender && !pending && !overlayRender &&
!cursorRender)
{ {
if (g_state.ds->skipFrame) if (g_state.ds->skipFrame)
g_state.ds->skipFrame(); g_state.ds->skipFrame();
@@ -752,6 +887,10 @@ static int renderThread(void * unused)
lgWaitEventAbs(g_state.frameEvent, &time); lgWaitEventAbs(g_state.frameEvent, &time);
} }
bool cursorWake;
const uint64_t cursorSerial = cursorRepaintRenderBegin(&cursorWake);
const uint64_t renderStart = nanotime();
frameTimingPublishReady(); frameTimingPublishReady();
int resize = atomic_load(&g_state.lgrResize); int resize = atomic_load(&g_state.lgrResize);
@@ -815,10 +954,14 @@ static int renderThread(void * unused)
invalidate, preSwapCallback, NULL, &rendererTiming))) invalidate, preSwapCallback, NULL, &rendererTiming)))
{ {
LG_UNLOCK(g_state.lgrLock); LG_UNLOCK(g_state.lgrLock);
cursorRepaintRenderEnd(cursorSerial, renderStart, false, false);
break; break;
} }
const uint64_t renderEnd = nanotime(); const uint64_t renderEnd = nanotime();
LG_UNLOCK(g_state.lgrLock); LG_UNLOCK(g_state.lgrLock);
cursorRepaintRenderEnd(cursorSerial, renderStart, true,
cursorWake &&
rendererTiming.frameToken == LG_RENDERER_FRAME_TOKEN_NONE);
if (!g_state.jitRender) if (!g_state.jitRender)
frameScheduler_observeCadence(); frameScheduler_observeCadence();
@@ -894,7 +1037,8 @@ static int renderThread(void * unused)
int main_cursorThread(void * unused) int main_cursorThread(void * unused)
{ {
LG_RendererCursor cursorType = LG_CURSOR_COLOR; LG_RendererCursor cursorType = LG_CURSOR_COLOR;
uint32_t cursorWhiteLevel = 0;
lgWaitEvent(e_startup, TIMEOUT_INFINITE); lgWaitEvent(e_startup, TIMEOUT_INFINITE);
@@ -916,10 +1060,8 @@ int main_cursorThread(void * unused)
(g_cursor.draw || !g_params.useSpiceInput), (g_cursor.draw || !g_params.useSpiceInput),
g_cursor.guest.x, g_cursor.guest.y, g_cursor.guest.x, g_cursor.guest.y,
g_cursor.guest.hx, g_cursor.guest.hy); g_cursor.guest.hx, g_cursor.guest.hy);
/* The active fade already schedules a render. Coalesce pointer if (!g_state.stopVideo)
* changes into it instead of exposing rapid single-buffer redraws. */ cursorRepaintRequest();
if (!g_state.stopVideo && !overlaySplash_isFading())
lgSignalEvent(g_state.frameEvent);
} }
continue; continue;
} }
@@ -931,6 +1073,9 @@ int main_cursorThread(void * unused)
break; break;
} }
const bool wasRendered = g_cursor.guest.visible &&
(g_cursor.draw || !g_params.useSpiceInput);
if (pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID) if (pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID)
g_cursor.guest.visible = g_cursor.guest.visible =
pointer.flags & LG_TRANSPORT_POINTER_VISIBLE; pointer.flags & LG_TRANSPORT_POINTER_VISIBLE;
@@ -964,10 +1109,16 @@ int main_cursorThread(void * unused)
g_state.lgr->ops.onMouseColorTransform(g_state.lgr, g_state.lgr->ops.onMouseColorTransform(g_state.lgr,
pointer.colorTransform); pointer.colorTransform);
if ((pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID) && const bool whiteLevelChanged =
pointer.sdrWhiteLevel && g_state.lgr->ops.onMouseWhiteLevel) (pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID) &&
pointer.sdrWhiteLevel && pointer.sdrWhiteLevel != cursorWhiteLevel &&
g_state.lgr->ops.onMouseWhiteLevel;
if (whiteLevelChanged)
{
g_state.lgr->ops.onMouseWhiteLevel(g_state.lgr, g_state.lgr->ops.onMouseWhiteLevel(g_state.lgr,
pointer.sdrWhiteLevel); pointer.sdrWhiteLevel);
cursorWhiteLevel = pointer.sdrWhiteLevel;
}
if (pointer.flags & LG_TRANSPORT_POINTER_POSITION) if (pointer.flags & LG_TRANSPORT_POINTER_POSITION)
{ {
@@ -990,11 +1141,16 @@ int main_cursorThread(void * unused)
g_cursor.guest.x, g_cursor.guest.y, g_cursor.guest.x, g_cursor.guest.y,
g_cursor.guest.hx, g_cursor.guest.hy); g_cursor.guest.hx, g_cursor.guest.hy);
if ((g_params.mouseRedraw || const bool isRendered = g_cursor.guest.visible &&
(pointer.flags & LG_TRANSPORT_POINTER_COLOR_TRANSFORM)) && (g_cursor.draw || !g_params.useSpiceInput);
g_cursor.guest.visible && !g_state.stopVideo && const bool contentChanged =
!overlaySplash_isFading()) (pointer.flags & (LG_TRANSPORT_POINTER_SHAPE |
lgSignalEvent(g_state.frameEvent); LG_TRANSPORT_POINTER_COLOR_TRANSFORM)) || whiteLevelChanged;
if (!g_state.stopVideo &&
(wasRendered != isRendered ||
((wasRendered || isRendered) &&
(g_params.mouseRedraw || contentChanged))))
cursorRepaintRequest();
g_state.transportOps->releasePointer(g_state.transport, &pointer); g_state.transportOps->releasePointer(g_state.transport, &pointer);
} }
@@ -1713,6 +1869,7 @@ static int transportSessionProbe(void * opaque)
static int lg_run(void) static int lg_run(void)
{ {
LG_LOCK_INIT(l_cursorRepaint.lock);
frameTimingInit(); frameTimingInit();
#ifdef ENABLE_TESTS #ifdef ENABLE_TESTS
@@ -1967,6 +2124,22 @@ static int lg_run(void)
// interactivity. // interactivity.
g_state.overlayFrameTime = min(g_state.frameTime, 1000000000ULL / 60ULL); g_state.overlayFrameTime = min(g_state.frameTime, 1000000000ULL / 60ULL);
if (!g_state.jitRender)
{
if (!(e_cursorRepaint = lgCreateEvent(true, 0)))
{
DEBUG_ERROR("failed to create the cursor repaint event");
return -1;
}
if (!lgCreateThread("cursorRepaint", cursorRepaintThread, NULL,
&t_cursorRepaint))
{
DEBUG_ERROR("cursor repaint thread creation failed");
return -1;
}
}
keybind_commonRegister(); keybind_commonRegister();
if (g_state.jitRender) if (g_state.jitRender)
@@ -2214,6 +2387,9 @@ static void lg_shutdown(void)
app_setState(APP_STATE_SHUTDOWN); app_setState(APP_STATE_SHUTDOWN);
frameScheduler_stop(); frameScheduler_stop();
if (e_cursorRepaint)
lgSignalEvent(e_cursorRepaint);
if (t_spice) if (t_spice)
lgJoinThread(t_spice, NULL); lgJoinThread(t_spice, NULL);
@@ -2226,6 +2402,19 @@ static void lg_shutdown(void)
lgJoinThread(t_render, NULL); lgJoinThread(t_render, NULL);
} }
if (t_cursorRepaint)
{
lgJoinThread(t_cursorRepaint, NULL);
t_cursorRepaint = NULL;
}
if (e_cursorRepaint)
{
lgFreeEvent(e_cursorRepaint);
e_cursorRepaint = NULL;
}
LG_LOCK_FREE(l_cursorRepaint.lock);
if (g_state.transportOps) if (g_state.transportOps)
g_state.transportOps->destroy(&g_state.transport); g_state.transportOps->destroy(&g_state.transport);

View File

@@ -324,7 +324,7 @@ All command line options
+------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+ +------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+
| input:rawMouse | | no | Use RAW mouse input when in capture mode (good for gaming) | | input:rawMouse | | no | Use RAW mouse input when in capture mode (good for gaming) |
+------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+ +------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+
| input:mouseRedraw | | yes | Mouse movements trigger redraws (ignores FPS minimum) | | input:mouseRedraw | | yes | Mouse movements trigger display-rate redraws |
+------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+ +------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+
| input:mouseTrace | | no | Enable mouse input diagnostics | | input:mouseTrace | | no | Enable mouse input diagnostics |
+------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+ +------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+