From 1a6f85eae26e06b4f1dc6c751f99ac84ea8752ba Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Tue, 11 Aug 2026 18:09:10 +1000 Subject: [PATCH] [client] video: switch transport sources Track primary and fallback video with source-specific generations, and publish a source only after its first frame or software surface is ready. Route SPICE surfaces and cursors through the generic render queue. Keep the current image across handoffs, and switch input only after the new source has been presented. Move fallback activation to its worker so display callbacks cannot block or deadlock the render path. --- client/include/app.h | 14 +- client/src/app.c | 220 +++++--- client/src/core.c | 2 + client/src/main.c | 935 ++++++++++++++++++++++++++------ client/src/main.h | 38 +- client/src/render_queue.c | 705 +++++++++++++++++++++--- client/src/render_queue.h | 101 +++- client/src/transport_fallback.c | 98 ++-- client/src/transport_fallback.h | 2 +- client/tests/mouse_test.c | 5 + 10 files changed, 1728 insertions(+), 392 deletions(-) diff --git a/client/include/app.h b/client/include/app.h index de8b072c..d0b150f1 100644 --- a/client/include/app.h +++ b/client/include/app.h @@ -227,8 +227,18 @@ bool app_guestIsOther(void); void app_stopVideo(bool stop); /** - * Enable/disable the spice display + * Select the primary or fallback video source */ -bool app_useSpiceDisplay(bool enable); +typedef enum LG_VideoSource +{ + LG_VIDEO_SOURCE_NONE, + LG_VIDEO_SOURCE_PRIMARY, + LG_VIDEO_SOURCE_FALLBACK, + LG_VIDEO_SOURCE_COUNT, +} +LG_VideoSource; + +bool app_useVideoSource(LG_VideoSource source); +void app_refreshVideoSource(void); #endif diff --git a/client/src/app.c b/client/src/app.c index 8719c1b5..e904848f 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -306,6 +306,7 @@ void app_handleEnterEvent(bool entered) if (!g_params.alwaysShowCursor) g_cursor.draw = false; g_cursor.redraw = true; + app_invalidateWindow(false); } } @@ -1139,81 +1140,166 @@ void app_stopVideo(bool stop) if (stop) { - core_stopCursorThread(); - core_stopFrameThread(); + if (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME) + { + core_stopCursorThread(); + core_stopFrameThread(); + } + else + g_state.videoOps->swSurface->setActive( + g_state.transport.handle, false); } else { - core_startCursorThread(); - core_startFrameThread(); + if (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME) + { + core_startCursorThread(); + core_startFrameThread(); + } + else + { + g_state.videoOps->swSurface->setActive( + g_state.transport.handle, true); + app_useVideoSource(LG_VIDEO_SOURCE_PRIMARY); + } } } -bool app_useSpiceDisplay(bool enable) +static RenderQueueSource renderSource(LG_VideoSource source) { - if (!g_params.useSpice || !g_state.fallback) - return false; - - atomic_store_explicit(&g_state.fallbackDisplayRequested, enable, - memory_order_release); - - // if the fallback is not yet ready, retain the requested state - if (!lgTransportFallback_ready(g_state.fallback)) - return false; - - bool active = atomic_load_explicit(&g_state.fallbackDisplayActive, - memory_order_acquire); - if (active == enable) - return active; - - // do not allow stopping of the host app if not connected - if (!enable && !atomic_load_explicit( - &g_state.lgHostConnected, memory_order_acquire)) + switch(source) { - atomic_store_explicit(&g_state.fallbackDisplayRequested, active, - memory_order_release); - return false; + case LG_VIDEO_SOURCE_NONE: + return RENDER_QUEUE_SOURCE_NONE; + + case LG_VIDEO_SOURCE_PRIMARY: + return RENDER_QUEUE_SOURCE_PRIMARY; + + case LG_VIDEO_SOURCE_FALLBACK: + return RENDER_QUEUE_SOURCE_FALLBACK; + + case LG_VIDEO_SOURCE_COUNT: + break; } - bool expected = false; - if (!atomic_compare_exchange_strong_explicit( - &g_state.fallbackDisplayTransition, &expected, true, - memory_order_acquire, memory_order_relaxed)) - return atomic_load_explicit(&g_state.fallbackDisplayActive, - memory_order_acquire); - - active = atomic_load_explicit(&g_state.fallbackDisplayActive, - memory_order_relaxed); - if (active == enable) - goto done; - - if (!lgTransportFallback_setVideoActive(g_state.fallback, enable)) - goto fail; - - renderQueue_swSurfaceShow(enable); - - active = enable; - atomic_store_explicit(&g_state.fallbackDisplayActive, active, - memory_order_release); - lgInput_useTransport(!active); - overlayStatus_set(LG_USER_STATUS_SPICE, enable); - -done: - atomic_store_explicit(&g_state.fallbackDisplayTransition, false, - memory_order_release); - - enable = atomic_load_explicit(&g_state.fallbackDisplayRequested, - memory_order_acquire); - if (enable != active) - return app_useSpiceDisplay(enable); - - return active; - -fail: - DEBUG_ERROR("Failed to %s the SPICE display", - enable ? "enable" : "disable"); - lgTransportFallback_setVideoActive(g_state.fallback, active); - atomic_store_explicit(&g_state.fallbackDisplayRequested, active, - memory_order_release); - goto done; + return RENDER_QUEUE_SOURCE_NONE; +} + +static bool useVideoSourceLocked(LG_VideoSource source, + LG_VideoSource previousRequested, bool inputAvailable) +{ + const RenderQueueSource queueSource = renderSource(source); + if (source == LG_VIDEO_SOURCE_NONE) + return renderQueue_sourceTransition( + queueSource, 0, false, NULL) != 0; + + struct VideoSourceState * state = &g_state.videoSource[source]; + if (previousRequested != source) + { + atomic_store_explicit( + &state->transitionSerial, 0, memory_order_release); + atomic_store_explicit( + &state->transitionPending, false, memory_order_release); + } + + const uint64_t generation = atomic_load_explicit( + &state->generation, memory_order_acquire); + if (!generation || !atomic_load_explicit( + &state->ready, memory_order_acquire)) + return false; + + const LG_VideoSource applied = atomic_load_explicit( + &g_state.videoSourceApplied, memory_order_acquire); + const uint64_t appliedGeneration = atomic_load_explicit( + &g_state.videoSourceAppliedGeneration, memory_order_acquire); + const int width = atomic_load_explicit( + &state->width, memory_order_relaxed); + const int height = atomic_load_explicit( + &state->height, memory_order_relaxed); + const LG_RendererRotate rotate = atomic_load_explicit( + &state->rotate, memory_order_relaxed); + if (width <= 0 || height <= 0) + return false; + + const LG_RendererRotate appliedRotate = atomic_load_explicit( + &state->appliedRotate, memory_order_acquire); + const bool geometryApplied = + atomic_load_explicit(&state->appliedWidth, memory_order_relaxed) == + (unsigned int)width && + atomic_load_explicit(&state->appliedHeight, memory_order_relaxed) == + (unsigned int)height && + appliedRotate == rotate; + if (applied == source && appliedGeneration == generation && + previousRequested == source && geometryApplied) + return true; + + if (previousRequested == source && atomic_load_explicit( + &state->transitionPending, memory_order_acquire)) + return true; + + if (state->cursorStateValid) + renderQueue_sourceCursorState(queueSource, generation, + state->cursorVisible && + (source == LG_VIDEO_SOURCE_FALLBACK || + g_cursor.draw || !inputAvailable), + state->cursorX, state->cursorY, + state->cursorHX, state->cursorHY); + + const bool configure = state->swSurface && state->configurePending; + atomic_store_explicit( + &state->transitionPending, true, memory_order_release); + const uint64_t serial = configure ? + renderQueue_sourceSwSurfaceConfigureTransition( + queueSource, generation, width, height, + &state->transitionSerial) : + renderQueue_sourceTransition( + queueSource, generation, state->swSurface, + &state->transitionSerial); + if (!serial) + { + atomic_store_explicit( + &state->transitionPending, false, memory_order_release); + return false; + } + return true; +} + +bool app_useVideoSource(LG_VideoSource source) +{ + if (source < LG_VIDEO_SOURCE_NONE || source >= LG_VIDEO_SOURCE_COUNT) + return false; + + if (source == LG_VIDEO_SOURCE_PRIMARY && !atomic_load_explicit( + &g_state.lgHostConnected, memory_order_acquire)) + return false; + + if (source == LG_VIDEO_SOURCE_FALLBACK && !g_state.fallback) + return false; + + const bool inputAvailable = lgInput_available(); + LG_LOCK(g_state.videoSourceLock); + const LG_VideoSource previousRequested = atomic_load_explicit( + &g_state.videoSourceRequested, memory_order_acquire); + atomic_store_explicit( + &g_state.videoSourceRequested, source, memory_order_release); + if (source == LG_VIDEO_SOURCE_FALLBACK) + lgTransportFallback_requestVideoActive(g_state.fallback, true); + const bool result = + useVideoSourceLocked(source, previousRequested, inputAvailable); + LG_UNLOCK(g_state.videoSourceLock); + return result; +} + +void app_refreshVideoSource(void) +{ + const bool inputAvailable = lgInput_available(); + LG_LOCK(g_state.videoSourceLock); + const LG_VideoSource source = atomic_load_explicit( + &g_state.videoSourceRequested, memory_order_acquire); + if (source != LG_VIDEO_SOURCE_NONE && + (source != LG_VIDEO_SOURCE_PRIMARY || atomic_load_explicit( + &g_state.lgHostConnected, memory_order_acquire)) && + (source != LG_VIDEO_SOURCE_FALLBACK || g_state.fallback)) + useVideoSourceLocked(source, source, inputAvailable); + LG_UNLOCK(g_state.videoSourceLock); } diff --git a/client/src/core.c b/client/src/core.c index c8618321..9fae5f79 100644 --- a/client/src/core.c +++ b/client/src/core.c @@ -187,6 +187,7 @@ static void applyView(bool active, bool force) (g_params.alwaysShowCursor || g_params.captureInputOnly) ? true : g_cursor.inView; g_cursor.redraw = true; + app_invalidateWindow(false); g_cursor.motionValid = false; g_cursor.autoCaptureActive = active && g_params.autoCapture && !g_cursor.grab && !g_params.captureInputOnly; @@ -984,6 +985,7 @@ void core_handleMouseNormal(double ex, double ey) g_cursor.realign = false; g_cursor.realigning = false; g_cursor.redraw = true; + app_invalidateWindow(false); if (!g_cursor.inWindow) return; diff --git a/client/src/main.c b/client/src/main.c index 8e0dd972..231c1369 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -83,6 +83,9 @@ _Static_assert((int)LG_CAPTURE_RGBA32F == (int)LG_TEST_CAPTURE_RGBA32F, // forwards static int renderThread(void * unused); +static RenderQueueSource renderQueueSource(LG_VideoSource source); +static bool videoSourceInvalidate(LG_VideoSource source); +static void videoSourceShowSplashIfNeeded(void); static LGEvent *e_startup = NULL; static LGEvent *e_cursorRepaint = NULL; @@ -707,6 +710,51 @@ static void cursorRepaintRequest(void) lgSignalEvent(e_cursorRepaint); } +static bool queueCursorRedraw(void) +{ + if (!atomic_load_explicit(&g_cursor.redraw, memory_order_acquire)) + return false; + + const bool inputAvailable = lgInput_available(); + LG_LOCK(g_state.videoSourceLock); + if (!atomic_exchange_explicit( + &g_cursor.redraw, false, memory_order_acq_rel)) + { + LG_UNLOCK(g_state.videoSourceLock); + return false; + } + + const LG_VideoSource source = atomic_load_explicit( + &g_state.videoSourceApplied, memory_order_acquire); + const uint64_t generation = atomic_load_explicit( + &g_state.videoSourceAppliedGeneration, memory_order_acquire); + if (source <= LG_VIDEO_SOURCE_NONE || source >= LG_VIDEO_SOURCE_COUNT) + { + LG_UNLOCK(g_state.videoSourceLock); + return false; + } + + const struct VideoSourceState * state = &g_state.videoSource[source]; + const bool valid = state->cursorStateValid && + atomic_load_explicit(&state->generation, memory_order_acquire) == + generation; + const bool visible = state->cursorVisible && + (source == LG_VIDEO_SOURCE_FALLBACK || + g_cursor.draw || !inputAvailable); + const int x = state->cursorX; + const int y = state->cursorY; + const int hx = state->cursorHX; + const int hy = state->cursorHY; + LG_UNLOCK(g_state.videoSourceLock); + + if (!valid) + return false; + + renderQueue_sourceCursorState(renderQueueSource(source), generation, + visible, x, y, hx, hy); + return true; +} + static uint64_t cursorRepaintRenderBegin(bool * cursorWake) { LG_LOCK(l_cursorRepaint.lock); @@ -930,13 +978,33 @@ static int renderThread(void * unused) atomic_compare_exchange_weak(&g_state.lgrResize, &resize, 0); } - const LG_RendererFrameToken frameTokenLimit = frameTimingQueuedToken(); - const uint64_t prepareStart = nanotime(); + const uint64_t prepareStart = nanotime(); + + queueCursorRedraw(); LG_LOCK(g_state.lgrLock); renderQueue_process(); + if (g_state.videoGeometryDirty) + { + g_state.videoGeometryDirty = false; + LG_UNLOCK(g_state.lgrLock); + core_updatePositionInfo(); + LG_LOCK(g_state.lgrLock); + } + + int sourceResize = atomic_load(&g_state.lgrResize); + if (unlikely(sourceResize)) + { + RENDERER(onResize, g_state.windowW, g_state.windowH, + g_state.windowScale, g_state.dstRect, g_params.winRotate); + atomic_compare_exchange_weak( + &g_state.lgrResize, &sourceResize, 0); + } + + const LG_RendererFrameToken frameTokenLimit = frameTimingQueuedToken(); + const bool windowInvalid = atomic_exchange(&g_state.invalidateWindow, false); const bool overlayFull = app_overlayNeedsFullRender(); @@ -956,6 +1024,7 @@ static int renderThread(void * unused) } const uint64_t renderEnd = nanotime(); LG_UNLOCK(g_state.lgrLock); + renderQueue_presented(); cursorRepaintRenderEnd(cursorSerial, renderStart, true, cursorWake && rendererTiming.frameToken == LG_RENDERER_FRAME_TOKEN_NONE); @@ -1035,8 +1104,16 @@ static int renderThread(void * unused) core_stopCursorThread(); core_stopFrameThread(); - if (g_state.videoOps && g_state.videoOps->frame->detachRenderer) + if (g_state.videoOps && g_state.videoOps->type == LG_VIDEO_TYPE_FRAME && + g_state.videoOps->frame->detachRenderer) g_state.videoOps->frame->detachRenderer(g_state.transport.handle); + else if (g_state.videoOps && + g_state.videoOps->type == LG_VIDEO_TYPE_SW_SURFACE) + { + g_state.videoOps->swSurface->setActive( + g_state.transport.handle, false); + g_state.videoOps->swSurface->detach(g_state.transport.handle); + } RENDERER(deinitialize); g_state.lgr = NULL; @@ -1048,8 +1125,12 @@ static int renderThread(void * unused) int main_cursorThread(void * unused) { - LG_RendererCursor cursorType = LG_CURSOR_COLOR; - uint32_t cursorWhiteLevel = 0; + LG_RendererCursor cursorType = LG_CURSOR_COLOR; + const uint64_t sourceGeneration = atomic_load_explicit( + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation, + memory_order_acquire); + struct VideoSourceState * source = + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY]; lgWaitEvent(e_startup, TIMEOUT_INFINITE); @@ -1063,17 +1144,8 @@ int main_cursorThread(void * unused) { if (status == LG_TRANSPORT_TIMEOUT || status == LG_TRANSPORT_UNAVAILABLE) { - if (g_cursor.redraw && g_cursor.guest.valid) - { - g_cursor.redraw = false; - RENDERER(onMouseEvent, - g_cursor.guest.visible && - (g_cursor.draw || !lgInput_available()), - g_cursor.guest.x, g_cursor.guest.y, - g_cursor.guest.hx, g_cursor.guest.hy); - if (!g_state.stopVideo) - cursorRepaintRequest(); - } + if (!g_state.stopVideo && queueCursorRedraw()) + cursorRepaintRequest(); continue; } @@ -1090,18 +1162,29 @@ int main_cursorThread(void * unused) break; } - const bool wasRendered = g_cursor.guest.visible && - (g_cursor.draw || !lgInput_available()); + const bool inputAvailable = lgInput_available(); + LG_LOCK(g_state.videoSourceLock); + if (atomic_load_explicit(&source->generation, memory_order_acquire) != + sourceGeneration) + { + LG_UNLOCK(g_state.videoSourceLock); + g_state.videoOps->frame->releasePointer( + g_state.transport.handle, &pointer); + continue; + } + + const bool wasRendered = source->cursorStateValid && + source->cursorVisible && (g_cursor.draw || !inputAvailable); bool hotspotChanged = false; if (pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID) - g_cursor.guest.visible = + source->cursorVisible = pointer.flags & LG_TRANSPORT_POINTER_VISIBLE; if (pointer.flags & LG_TRANSPORT_POINTER_SHAPE) { - const int oldHX = g_cursor.guest.hx; - const int oldHY = g_cursor.guest.hy; + const int oldHX = source->cursorHX; + const int oldHY = source->cursorHY; switch (pointer.type) { @@ -1109,6 +1192,7 @@ int main_cursorThread(void * unused) case CURSOR_TYPE_MONOCHROME : cursorType = LG_CURSOR_MONOCHROME ; break; case CURSOR_TYPE_MASKED_COLOR: cursorType = LG_CURSOR_MASKED_COLOR; break; default: + LG_UNLOCK(g_state.videoSourceLock); DEBUG_ERROR("Invalid cursor type"); g_state.videoOps->frame->releasePointer( g_state.transport.handle, &pointer); @@ -1116,68 +1200,93 @@ int main_cursorThread(void * unused) } hotspotChanged = - g_cursor.guest.hx != pointer.hx || g_cursor.guest.hy != pointer.hy; - if (!RENDERER(onMouseShape, cursorType, pointer.width, pointer.height, - pointer.pitch, pointer.shape)) - { - DEBUG_ERROR("Failed to update mouse shape"); - g_state.videoOps->frame->releasePointer( - g_state.transport.handle, &pointer); - continue; - } - g_cursor.guest.hx = pointer.hx; - g_cursor.guest.hy = pointer.hy; + source->cursorHX != pointer.hx || source->cursorHY != pointer.hy; + source->cursorHX = pointer.hx; + source->cursorHY = pointer.hy; - if (hotspotChanged && g_cursor.guest.valid && + if (hotspotChanged && source->cursorStateValid && !(pointer.flags & LG_TRANSPORT_POINTER_POSITION)) { - g_cursor.guest.x += oldHX - pointer.hx; - g_cursor.guest.y += oldHY - pointer.hy; + source->cursorX += oldHX - pointer.hx; + source->cursorY += oldHY - pointer.hy; } } - if ((pointer.flags & LG_TRANSPORT_POINTER_COLOR_TRANSFORM) && - g_state.lgr->ops.onMouseColorTransform) - g_state.lgr->ops.onMouseColorTransform(g_state.lgr, - pointer.colorTransform); - const bool whiteLevelChanged = (pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID) && - pointer.sdrWhiteLevel && pointer.sdrWhiteLevel != cursorWhiteLevel && - g_state.lgr->ops.onMouseWhiteLevel; + pointer.sdrWhiteLevel && + pointer.sdrWhiteLevel != source->cursorWhiteLevel; if (whiteLevelChanged) - { - g_state.lgr->ops.onMouseWhiteLevel(g_state.lgr, - pointer.sdrWhiteLevel); - cursorWhiteLevel = pointer.sdrWhiteLevel; - } + source->cursorWhiteLevel = pointer.sdrWhiteLevel; if (pointer.flags & LG_TRANSPORT_POINTER_POSITION) { - const bool wasValid = g_cursor.guest.valid; - g_cursor.guest.x = pointer.x; - g_cursor.guest.y = pointer.y; - g_cursor.guest.valid = true; - if (!wasValid && core_inputEnabled()) - core_alignToGuest(); + source->cursorX = pointer.x; + source->cursorY = pointer.y; + source->cursorStateValid = true; } - if ((pointer.flags & LG_TRANSPORT_POINTER_POSITION) || hotspotChanged) - core_handleGuestMouseUpdate(); + const bool sourceApplied = atomic_load_explicit( + &g_state.videoSourceApplied, memory_order_acquire) == + LG_VIDEO_SOURCE_PRIMARY && atomic_load_explicit( + &g_state.videoSourceAppliedGeneration, memory_order_acquire) == + sourceGeneration; + if (sourceApplied) + atomic_exchange_explicit( + &g_cursor.redraw, false, memory_order_acq_rel); - app_updateMouseState(); - g_cursor.redraw = false; - RENDERER(onMouseEvent, - g_cursor.guest.visible && (g_cursor.draw || !lgInput_available()), - g_cursor.guest.x, g_cursor.guest.y, - g_cursor.guest.hx, g_cursor.guest.hy); + const bool valid = source->cursorStateValid; + const bool guestVisible = source->cursorVisible; + const bool visible = guestVisible && + (g_cursor.draw || !inputAvailable); + const int x = source->cursorX; + const int y = source->cursorY; + const int hx = source->cursorHX; + const int hy = source->cursorHY; + const bool isRendered = valid && visible; + bool wasValid = false; + if (sourceApplied) + { + wasValid = g_cursor.guest.valid; + g_cursor.guest.visible = guestVisible; + g_cursor.guest.x = x; + g_cursor.guest.y = y; + g_cursor.guest.hx = hx; + g_cursor.guest.hy = hy; + g_cursor.guest.valid = valid; + } + LG_UNLOCK(g_state.videoSourceLock); + + if (pointer.flags & LG_TRANSPORT_POINTER_SHAPE) + renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, + sourceGeneration, cursorType, pointer.width, pointer.height, + pointer.pitch, pointer.shape); + + if (pointer.flags & LG_TRANSPORT_POINTER_COLOR_TRANSFORM) + renderQueue_sourceCursorColorTransform(RENDER_QUEUE_SOURCE_PRIMARY, + sourceGeneration, pointer.colorTransform); + + if (whiteLevelChanged) + renderQueue_sourceCursorWhiteLevel(RENDER_QUEUE_SOURCE_PRIMARY, + sourceGeneration, pointer.sdrWhiteLevel); + + if (valid) + renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, + sourceGeneration, visible, x, y, hx, hy); + + if (sourceApplied) + { + if (!wasValid && valid && core_inputEnabled()) + core_alignToGuest(); + if ((pointer.flags & LG_TRANSPORT_POINTER_POSITION) || hotspotChanged) + core_handleGuestMouseUpdate(); + app_updateMouseState(); + } - const bool isRendered = g_cursor.guest.visible && - (g_cursor.draw || !lgInput_available()); const bool contentChanged = (pointer.flags & (LG_TRANSPORT_POINTER_SHAPE | LG_TRANSPORT_POINTER_COLOR_TRANSFORM)) || whiteLevelChanged; - if (!g_state.stopVideo && + if (sourceApplied && !g_state.stopVideo && (wasRendered != isRendered || ((wasRendered || isRendered) && (g_params.mouseRedraw || contentChanged)))) @@ -1198,6 +1307,9 @@ int main_frameThread(void * unused) uint64_t frameSerial = 0; uint32_t formatVersion = 0; LG_RendererFormat rendererFormat; + const uint64_t sourceGeneration = atomic_load_explicit( + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation, + memory_order_acquire); if (g_state.useDMA) DEBUG_INFO("Using DMA buffer support"); @@ -1257,7 +1369,10 @@ int main_frameThread(void * unused) app_setState(APP_STATE_SHUTDOWN); break; } - if (!g_state.formatValid || format->version != formatVersion) + const bool formatChanged = + !g_state.formatValid || format->version != formatVersion; + bool rendererSupportsNativeHDR = false; + if (formatChanged) { memset(&rendererFormat, 0, sizeof(rendererFormat)); rendererFormat.type = format->type; @@ -1310,8 +1425,6 @@ int main_frameThread(void * unused) rendererFormat.rotate = LG_ROTATE_0; break; } - g_state.rotate = rendererFormat.rotate; - bool invalid = false; switch (format->type) { @@ -1341,12 +1454,6 @@ int main_frameThread(void * unused) break; } - g_state.formatValid = true; - formatVersion = format->version; -#ifdef ENABLE_TESTS - atomic_store_explicit(&l_testFrameType, format->type, - memory_order_release); -#endif DEBUG_INFO("Format: %s %ux%u (%ux%u) stride:%u pitch:%u rotation:%d hdr:%d pq:%d sdrWhite:%u nits", FrameTypeStr[format->type], format->frameWidth, format->frameHeight, format->dataWidth, format->dataHeight, format->stride, format->pitch, @@ -1363,23 +1470,18 @@ int main_frameThread(void * unused) app_setState(APP_STATE_SHUTDOWN); break; } - const bool rendererSupportsNativeHDR = !rendererFormat.hdr || + rendererSupportsNativeHDR = !rendererFormat.hdr || !g_state.lgr->ops.supports || RENDERER(supports, rendererFormat.hdrPQ ? LG_SUPPORTS_HDR_PQ : LG_SUPPORTS_HDR_SCRGB); - // Publish the matching surface format before allowing the render thread - // to consume the renderer format. Otherwise it can present one frame in - // the new encoding while the display server still has the old image - // description attached. - renderQueue_surfaceFormat(rendererFormat, rendererSupportsNativeHDR); - LG_UNLOCK(g_state.lgrLock); - g_state.srcSize.x = rendererFormat.screenWidth; - g_state.srcSize.y = rendererFormat.screenHeight; - g_state.haveSrcSize = true; - if (g_params.autoResize) - g_state.ds->setWindowSize(rendererFormat.frameWidth, - rendererFormat.frameHeight); - core_updatePositionInfo(); + struct VideoSourceState * source = + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY]; + atomic_store_explicit( + &source->width, rendererFormat.screenWidth, memory_order_relaxed); + atomic_store_explicit( + &source->height, rendererFormat.screenHeight, memory_order_relaxed); + atomic_store_explicit( + &source->rotate, rendererFormat.rotate, memory_order_release); } uint32_t damageCount = frame.damageRectsCount <= @@ -1411,6 +1513,8 @@ int main_frameThread(void * unused) rendererOwnsFrame ? frame.releaseOpaque : NULL, rendererOwnsFrame ? frame.releaseHandle : 0)) { + if (formatChanged) + LG_UNLOCK(g_state.lgrLock); frameTimingCancel(frameToken); g_state.videoOps->frame->releaseFrame(g_state.transport.handle, &frame); DEBUG_ERROR("Renderer onFrame returned failure"); @@ -1428,26 +1532,50 @@ int main_frameThread(void * unused) const uint64_t queueStart = nanotime(); atomic_fetch_add_explicit(&g_state.frameCount, 1, memory_order_relaxed); + frameTimingQueue(frameToken, frame.serial, &timing, + g_state.frameImportTime, g_state.frameImportWaitTime, + dispatchStart, queueStart); + + if (formatChanged) + { + g_state.formatValid = true; + formatVersion = format->version; +#ifdef ENABLE_TESTS + atomic_store_explicit(&l_testFrameType, format->type, + memory_order_release); +#endif + /* Publish the format only after accepting and timing its first frame. + * lgrLock prevents the render thread from consuming either update until + * both are ready. */ + renderQueue_sourceSurfaceFormat(RENDER_QUEUE_SOURCE_PRIMARY, + sourceGeneration, rendererFormat, rendererSupportsNativeHDR); + } #ifdef ENABLE_TESTS atomic_store_explicit(&l_testFrameSerial, frame.serial, memory_order_release); #endif - frameTimingQueue(frameToken, frame.serial, &timing, - g_state.frameImportTime, g_state.frameImportWaitTime, - dispatchStart, queueStart); + + atomic_store_explicit( + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].ready, true, + memory_order_release); + app_useVideoSource(LG_VIDEO_SOURCE_PRIMARY); + + if (formatChanged) + { + LG_UNLOCK(g_state.lgrLock); + if (g_params.autoResize) + g_state.ds->setWindowSize(rendererFormat.frameWidth, + rendererFormat.frameHeight); + } if (g_state.jitRender) { 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); - } if ((frame.flags & LG_TRANSPORT_FRAME_REQUEST_ACTIVATION) && g_params.requestActivation) @@ -1469,16 +1597,24 @@ int main_frameThread(void * unused) if (!rendererOwnsFrame) g_state.videoOps->frame->releaseFrame(g_state.transport.handle, &frame); - app_useSpiceDisplay(false); } if (app_getState() != APP_STATE_SHUTDOWN) - if (!app_useSpiceDisplay(true)) - overlaySplash_show(true); + { + if (app_getState() == APP_STATE_RESTART) + videoSourceInvalidate(LG_VIDEO_SOURCE_PRIMARY); + else + atomic_store_explicit( + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].ready, false, + memory_order_release); - /* Queue the fallback source before clearing desktop snapshots. The render - * thread processes this transition under lgrLock before its next render, so - * restart cannot expose an empty desktop frame between the two operations. */ + if (!app_useVideoSource(LG_VIDEO_SOURCE_FALLBACK)) + videoSourceShowSplashIfNeeded(); + } + + /* Queue the replacement before releasing desktop snapshots. The existing + * front buffer remains visible until the render thread draws it or the + * splash requested above. */ LG_LOCK(g_state.lgrLock); RENDERER(onRestart); LG_UNLOCK(g_state.lgrLock); @@ -1491,55 +1627,329 @@ int main_frameThread(void * unused) return 0; } -static void fallbackSurfaceConfigure(void * opaque, +static RenderQueueSource renderQueueSource(LG_VideoSource source) +{ + switch(source) + { + case LG_VIDEO_SOURCE_PRIMARY: + return RENDER_QUEUE_SOURCE_PRIMARY; + + case LG_VIDEO_SOURCE_FALLBACK: + return RENDER_QUEUE_SOURCE_FALLBACK; + + case LG_VIDEO_SOURCE_NONE: + case LG_VIDEO_SOURCE_COUNT: + break; + } + + return RENDER_QUEUE_SOURCE_NONE; +} + +static void videoSourceBegin(LG_VideoSource source) +{ + struct VideoSourceState * state = &g_state.videoSource[source]; + const uint64_t generation = + renderQueue_sourceBegin(renderQueueSource(source)); + + LG_LOCK(g_state.videoSourceLock); + atomic_store_explicit(&state->ready, false, memory_order_release); + atomic_store_explicit( + &state->transitionSerial, 0, memory_order_relaxed); + atomic_store_explicit( + &state->transitionPending, false, memory_order_relaxed); + atomic_store_explicit(&state->appliedWidth, 0, memory_order_relaxed); + atomic_store_explicit(&state->appliedHeight, 0, memory_order_relaxed); + atomic_store_explicit( + &state->appliedRotate, LG_ROTATE_0, memory_order_relaxed); + atomic_store_explicit( + &state->generation, generation, memory_order_release); + state->configurePending = false; + LG_UNLOCK(g_state.videoSourceLock); +} + +static bool videoSourceInvalidate(LG_VideoSource source) +{ + struct VideoSourceState * state = &g_state.videoSource[source]; + LG_LOCK(g_state.videoSourceLock); + const uint64_t generation = atomic_exchange_explicit( + &state->generation, 0, memory_order_acq_rel); + atomic_store_explicit(&state->ready, false, memory_order_release); + LG_UNLOCK(g_state.videoSourceLock); + if (generation) + renderQueue_sourceInvalidate(renderQueueSource(source), generation); + return generation != 0; +} + +static void videoSourceShowSplashIfNeeded(void) +{ + LG_LOCK(g_state.videoSplashLock); + bool available; + for (;;) + { + available = false; + const LG_VideoSource source = atomic_load_explicit( + &g_state.videoSourceRequested, memory_order_acquire); + if (source <= LG_VIDEO_SOURCE_NONE || source >= LG_VIDEO_SOURCE_COUNT) + break; + + const struct VideoSourceState * state = &g_state.videoSource[source]; + const uint64_t generation = atomic_load_explicit( + &state->generation, memory_order_acquire); + const bool transitionPending = atomic_load_explicit( + &state->transitionPending, memory_order_acquire); + const LG_VideoSource applied = atomic_load_explicit( + &g_state.videoSourceApplied, memory_order_acquire); + const uint64_t appliedGeneration = atomic_load_explicit( + &g_state.videoSourceAppliedGeneration, memory_order_acquire); + available = generation && + ((applied == source && appliedGeneration == generation) || + (atomic_load_explicit(&state->ready, memory_order_acquire) && + transitionPending)); + + if (source == atomic_load_explicit( + &g_state.videoSourceRequested, memory_order_acquire) && + generation == atomic_load_explicit( + &state->generation, memory_order_acquire)) + break; + } + + if (!available) + overlaySplash_show(true); + LG_UNLOCK(g_state.videoSplashLock); +} + +static void videoSourceClearCursor(LG_VideoSource source) +{ + struct VideoSourceState * state = &g_state.videoSource[source]; + LG_LOCK(g_state.videoSourceLock); + state->cursorStateValid = false; + state->cursorVisible = false; + state->cursorWhiteLevel = 0; + if (atomic_load_explicit( + &g_state.videoSourceApplied, memory_order_acquire) == source) + g_cursor.guest.valid = false; + LG_UNLOCK(g_state.videoSourceLock); + + renderQueue_sourceClearCursor(renderQueueSource(source)); +} + +static bool videoSourcePrepare(void * opaque, RenderQueueSource queueSource, + uint64_t generation, uint64_t serial) +{ + (void)opaque; + + LG_VideoSource source = LG_VIDEO_SOURCE_NONE; + if (queueSource == RENDER_QUEUE_SOURCE_PRIMARY) + source = LG_VIDEO_SOURCE_PRIMARY; + else if (queueSource == RENDER_QUEUE_SOURCE_FALLBACK) + source = LG_VIDEO_SOURCE_FALLBACK; + + if (source == LG_VIDEO_SOURCE_NONE) + return true; + + struct VideoSourceState * state = &g_state.videoSource[source]; + if (atomic_load_explicit(&state->generation, memory_order_acquire) != + generation || + !atomic_load_explicit(&state->ready, memory_order_acquire) || + atomic_load_explicit( + &g_state.videoSourceRequested, memory_order_acquire) != source) + goto reject; + + const int width = atomic_load_explicit( + &state->width, memory_order_relaxed); + const int height = atomic_load_explicit( + &state->height, memory_order_relaxed); + const LG_RendererRotate rotate = atomic_load_explicit( + &state->rotate, memory_order_relaxed); + if (width <= 0 || height <= 0) + goto reject; + + LG_LOCK(g_state.videoSplashLock); + if (atomic_load_explicit(&state->generation, memory_order_acquire) != + generation || + !atomic_load_explicit(&state->ready, memory_order_acquire) || + atomic_load_explicit( + &g_state.videoSourceRequested, memory_order_acquire) != source) + { + LG_UNLOCK(g_state.videoSplashLock); + goto reject; + } + + if (!g_state.haveSrcSize || g_state.srcSize.x != width || + g_state.srcSize.y != height || g_state.rotate != rotate) + { + g_state.srcSize.x = width; + g_state.srcSize.y = height; + g_state.rotate = rotate; + g_state.haveSrcSize = true; + g_state.videoGeometryDirty = true; + } + + atomic_store_explicit( + &g_state.videoSource[source].appliedWidth, width, memory_order_relaxed); + atomic_store_explicit( + &g_state.videoSource[source].appliedHeight, height, + memory_order_relaxed); + atomic_store_explicit( + &g_state.videoSource[source].appliedRotate, rotate, + memory_order_release); + + overlaySplash_show(false); + LG_UNLOCK(g_state.videoSplashLock); + + return true; + +reject: + if (atomic_compare_exchange_strong_explicit(&state->transitionSerial, + &serial, 0, memory_order_acq_rel, memory_order_relaxed)) + atomic_store_explicit( + &state->transitionPending, false, memory_order_release); + return false; +} + +static void videoSourceApplied(void * opaque, RenderQueueSource queueSource, + uint64_t generation, uint64_t serial, bool swSurface) +{ + (void)opaque; + (void)swSurface; + + LG_VideoSource source = LG_VIDEO_SOURCE_NONE; + if (queueSource == RENDER_QUEUE_SOURCE_PRIMARY) + source = LG_VIDEO_SOURCE_PRIMARY; + else if (queueSource == RENDER_QUEUE_SOURCE_FALLBACK) + source = LG_VIDEO_SOURCE_FALLBACK; + + bool cursorStateValid = false; + bool cursorVisible = false; + int cursorX = 0; + int cursorY = 0; + int cursorHX = 0; + int cursorHY = 0; + + LG_LOCK(g_state.videoSourceLock); + atomic_store_explicit( + &g_state.videoSourceApplied, source, memory_order_release); + atomic_store_explicit( + &g_state.videoSourceAppliedGeneration, generation, + memory_order_release); + + if (source != LG_VIDEO_SOURCE_NONE) + { + struct VideoSourceState * state = &g_state.videoSource[source]; + if (atomic_load_explicit(&state->generation, memory_order_acquire) == + generation) + { + cursorStateValid = state->cursorStateValid; + cursorVisible = state->cursorVisible; + cursorX = state->cursorX; + cursorY = state->cursorY; + cursorHX = state->cursorHX; + cursorHY = state->cursorHY; + + uint64_t pendingSerial = serial; + if (atomic_compare_exchange_strong_explicit(&state->transitionSerial, + &pendingSerial, 0, memory_order_acq_rel, memory_order_relaxed)) + { + atomic_store_explicit( + &state->transitionPending, false, memory_order_release); + state->configurePending = false; + } + } + } + + const LG_VideoSource requested = atomic_load_explicit( + &g_state.videoSourceRequested, memory_order_acquire); + if (source == LG_VIDEO_SOURCE_PRIMARY && + requested == LG_VIDEO_SOURCE_PRIMARY && g_state.fallback) + lgTransportFallback_requestVideoActive(g_state.fallback, false); + + g_cursor.guest.valid = cursorStateValid; + if (cursorStateValid) + { + g_cursor.guest.visible = cursorVisible; + g_cursor.guest.x = cursorX; + g_cursor.guest.y = cursorY; + g_cursor.guest.hx = cursorHX; + g_cursor.guest.hy = cursorHY; + } + LG_UNLOCK(g_state.videoSourceLock); + + const bool fallback = source == LG_VIDEO_SOURCE_FALLBACK; + lgInput_useTransport(!fallback); + overlayStatus_set(LG_USER_STATUS_SPICE, fallback); + atomic_store_explicit(&g_cursor.redraw, true, memory_order_release); + app_invalidateWindow(false); + app_updateMouseState(); + app_refreshVideoSource(); +} + +static void swSurfaceConfigure(LG_VideoSource source, unsigned int width, unsigned int height) { - (void)opaque; - g_state.fallbackSurfaceValid = true; - g_state.srcSize.x = width; - g_state.srcSize.y = height; - g_state.haveSrcSize = true; - core_updatePositionInfo(); + struct VideoSourceState * state = &g_state.videoSource[source]; + videoSourceBegin(source); + atomic_store_explicit(&state->width, width, memory_order_relaxed); + atomic_store_explicit(&state->height, height, memory_order_relaxed); + atomic_store_explicit(&state->rotate, LG_ROTATE_0, memory_order_release); + LG_LOCK(g_state.videoSourceLock); + state->configurePending = true; + atomic_store_explicit(&state->ready, true, memory_order_release); + LG_UNLOCK(g_state.videoSourceLock); - renderQueue_swSurfaceConfigure(width, height); + if (source == LG_VIDEO_SOURCE_PRIMARY && g_params.autoResize) + g_state.ds->setWindowSize(width, height); + + app_refreshVideoSource(); } -static void fallbackSurfaceDestroy(void * opaque) +static void swSurfaceDestroy(LG_VideoSource source) { - (void)opaque; - g_state.fallbackSurfaceValid = false; - if (atomic_exchange_explicit(&g_state.fallbackDisplayActive, false, - memory_order_acq_rel)) - { - renderQueue_swSurfaceShow(false); - lgInput_useTransport(true); - overlayStatus_set(LG_USER_STATUS_SPICE, false); - } + if (!videoSourceInvalidate(source)) + return; + if (app_getState() == APP_STATE_SHUTDOWN) + return; + + const LG_VideoSource applied = atomic_load_explicit( + &g_state.videoSourceApplied, memory_order_acquire); + const LG_VideoSource requested = atomic_load_explicit( + &g_state.videoSourceRequested, memory_order_acquire); + if (applied != source && requested != source) + return; + + /* Retain the last presented surface until this source reconnects or a + * replacement becomes ready. */ + videoSourceShowSplashIfNeeded(); } -static void fallbackSurfaceDrawFill(void * opaque, int x, int y, +static void swSurfaceDrawFill(LG_VideoSource source, int x, int y, int width, int height, uint32_t color) { - (void)opaque; - renderQueue_swSurfaceDrawFill(x, y, width, height, color); + const uint64_t generation = atomic_load_explicit( + &g_state.videoSource[source].generation, memory_order_acquire); + renderQueue_sourceSwSurfaceDrawFill(renderQueueSource(source), generation, + x, y, width, height, color); } -static void fallbackSurfaceDrawBitmap(void * opaque, bool topDown, +static void swSurfaceDrawBitmap(LG_VideoSource source, bool topDown, int x, int y, int width, int height, int stride, const void * data) { - (void)opaque; - renderQueue_swSurfaceDrawBitmap( + const uint64_t generation = atomic_load_explicit( + &g_state.videoSource[source].generation, memory_order_acquire); + renderQueue_sourceSwSurfaceDrawBitmap(renderQueueSource(source), generation, x, y, width, height, stride, data, topDown); } -static void fallbackSurfacePointer(void * opaque, +static void swSurfacePointer(LG_VideoSource source, const LG_TransportPointer * pointer) { - (void)opaque; + struct VideoSourceState * state = &g_state.videoSource[source]; + const uint64_t generation = atomic_load_explicit( + &state->generation, memory_order_acquire); + if (!generation) + return; + LG_RendererCursor type = LG_CURSOR_COLOR; if (pointer->flags & LG_TRANSPORT_POINTER_SHAPE) - { - LG_RendererCursor type; switch (pointer->type) { case CURSOR_TYPE_COLOR: @@ -1558,50 +1968,180 @@ static void fallbackSurfacePointer(void * opaque, return; } - const size_t size = (size_t)pointer->pitch * pointer->height; - uint8_t * shape = malloc(size); - if (!shape) - return; - memcpy(shape, pointer->shape, size); - renderQueue_cursorImage(type, pointer->width, pointer->height, - pointer->pitch, shape); + const bool drawCursor = source != LG_VIDEO_SOURCE_PRIMARY || + g_cursor.draw || !lgInput_available(); + LG_LOCK(g_state.videoSourceLock); + if (atomic_load_explicit(&state->generation, memory_order_acquire) != + generation) + { + LG_UNLOCK(g_state.videoSourceLock); + return; } - if ((pointer->flags & LG_TRANSPORT_POINTER_POSITION) && - (pointer->flags & LG_TRANSPORT_POINTER_VISIBLE_VALID)) - renderQueue_cursorState( - pointer->flags & LG_TRANSPORT_POINTER_VISIBLE, - pointer->x, pointer->y, pointer->hx, pointer->hy); + const bool wasRendered = state->cursorStateValid && + state->cursorVisible && drawCursor; + bool hotspotChanged = false; + + if (pointer->flags & LG_TRANSPORT_POINTER_SHAPE) + { + const int oldHX = state->cursorHX; + const int oldHY = state->cursorHY; + hotspotChanged = + state->cursorHX != pointer->hx || state->cursorHY != pointer->hy; + state->cursorHX = pointer->hx; + state->cursorHY = pointer->hy; + if (hotspotChanged && state->cursorStateValid && + !(pointer->flags & LG_TRANSPORT_POINTER_POSITION)) + { + state->cursorX += oldHX - pointer->hx; + state->cursorY += oldHY - pointer->hy; + } + } + + if (pointer->flags & LG_TRANSPORT_POINTER_POSITION) + { + state->cursorX = pointer->x; + state->cursorY = pointer->y; + state->cursorStateValid = true; + } + + if (pointer->flags & LG_TRANSPORT_POINTER_VISIBLE_VALID) + state->cursorVisible = pointer->flags & LG_TRANSPORT_POINTER_VISIBLE; + + const bool whiteLevelChanged = pointer->sdrWhiteLevel && + pointer->sdrWhiteLevel != state->cursorWhiteLevel; + if (whiteLevelChanged) + state->cursorWhiteLevel = pointer->sdrWhiteLevel; + + const bool valid = state->cursorStateValid; + const bool guestVisible = state->cursorVisible; + const bool visible = guestVisible && drawCursor; + const int x = state->cursorX; + const int y = state->cursorY; + const int hx = state->cursorHX; + const int hy = state->cursorHY; + const bool isRendered = valid && visible; + const bool sourceApplied = atomic_load_explicit( + &g_state.videoSourceApplied, memory_order_acquire) == source && + atomic_load_explicit(&g_state.videoSourceAppliedGeneration, + memory_order_acquire) == generation; + bool wasValid = false; + if (sourceApplied) + { + wasValid = g_cursor.guest.valid; + g_cursor.guest.visible = guestVisible; + g_cursor.guest.x = x; + g_cursor.guest.y = y; + g_cursor.guest.hx = hx; + g_cursor.guest.hy = hy; + g_cursor.guest.valid = valid; + } + LG_UNLOCK(g_state.videoSourceLock); + + if (pointer->flags & LG_TRANSPORT_POINTER_SHAPE) + renderQueue_sourceCursorImage(renderQueueSource(source), generation, + type, pointer->width, pointer->height, pointer->pitch, + pointer->shape); + + if (pointer->flags & LG_TRANSPORT_POINTER_COLOR_TRANSFORM) + renderQueue_sourceCursorColorTransform(renderQueueSource(source), + generation, pointer->colorTransform); + + if (whiteLevelChanged) + renderQueue_sourceCursorWhiteLevel(renderQueueSource(source), + generation, pointer->sdrWhiteLevel); + + if (valid && (pointer->flags & (LG_TRANSPORT_POINTER_POSITION | + LG_TRANSPORT_POINTER_VISIBLE_VALID | LG_TRANSPORT_POINTER_SHAPE))) + renderQueue_sourceCursorState(renderQueueSource(source), generation, + visible, x, y, hx, hy); + + if (sourceApplied) + { + if (!wasValid && valid && core_inputEnabled()) + core_alignToGuest(); + if (source == LG_VIDEO_SOURCE_PRIMARY && + ((pointer->flags & LG_TRANSPORT_POINTER_POSITION) || hotspotChanged)) + core_handleGuestMouseUpdate(); + app_updateMouseState(); + } + + const bool contentChanged = + (pointer->flags & (LG_TRANSPORT_POINTER_SHAPE | + LG_TRANSPORT_POINTER_COLOR_TRANSFORM)) || whiteLevelChanged; + if (sourceApplied && (wasRendered != isRendered || + ((wasRendered || isRendered) && + (g_params.mouseRedraw || contentChanged)))) + cursorRepaintRequest(); } -static const LG_SwSurfaceEventOps fallbackSurfaceEvents = +static LG_VideoSource swSurfaceSource(void * opaque) { - .configure = fallbackSurfaceConfigure, - .destroy = fallbackSurfaceDestroy, - .drawFill = fallbackSurfaceDrawFill, - .drawBitmap = fallbackSurfaceDrawBitmap, - .pointer = fallbackSurfacePointer, + return (LG_VideoSource)(uintptr_t)opaque; +} + +static void swSurfaceEventConfigure(void * opaque, + unsigned int width, unsigned int height) +{ + swSurfaceConfigure(swSurfaceSource(opaque), width, height); +} + +static void swSurfaceEventDestroy(void * opaque) +{ + swSurfaceDestroy(swSurfaceSource(opaque)); +} + +static void swSurfaceEventDrawFill(void * opaque, int x, int y, + int width, int height, uint32_t color) +{ + swSurfaceDrawFill( + swSurfaceSource(opaque), x, y, width, height, color); +} + +static void swSurfaceEventDrawBitmap(void * opaque, bool topDown, + int x, int y, int width, int height, int stride, const void * data) +{ + swSurfaceDrawBitmap(swSurfaceSource(opaque), topDown, + x, y, width, height, stride, data); +} + +static void swSurfaceEventPointer(void * opaque, + const LG_TransportPointer * pointer) +{ + swSurfacePointer(swSurfaceSource(opaque), pointer); +} + +static const LG_SwSurfaceEventOps swSurfaceEvents = +{ + .configure = swSurfaceEventConfigure, + .destroy = swSurfaceEventDestroy, + .drawFill = swSurfaceEventDrawFill, + .drawBitmap = swSurfaceEventDrawBitmap, + .pointer = swSurfaceEventPointer, }; static void fallbackConnected(void * opaque, const LG_TransportSession * session) { (void)opaque; + if (app_getState() == APP_STATE_SHUTDOWN) + return; + if (session->name[0] && !atomic_load_explicit( &g_state.lgHostConnected, memory_order_acquire)) core_setTitle(session->name); - if (atomic_load_explicit(&g_state.fallbackDisplayRequested, - memory_order_acquire)) - app_useSpiceDisplay(true); + app_refreshVideoSource(); } static void fallbackDisconnected(void * opaque) { - fallbackSurfaceDestroy(opaque); - if (!atomic_load_explicit( + (void)opaque; + swSurfaceDestroy(LG_VIDEO_SOURCE_FALLBACK); + videoSourceClearCursor(LG_VIDEO_SOURCE_FALLBACK); + if (app_getState() != APP_STATE_SHUTDOWN && !atomic_load_explicit( &g_state.lgHostConnected, memory_order_acquire)) - overlaySplash_show(true); + videoSourceShowSplashIfNeeded(); } static void fallbackUUIDMismatch(void * opaque, const uint8_t primary[16], @@ -1627,7 +2167,8 @@ static bool fallbackStart(void) if (!g_params.useSpice || strcmp(g_params.transport, "spice") == 0) return true; - if (lgTransportFallback_start("spice", &fallbackSurfaceEvents, NULL, + if (lgTransportFallback_start("spice", &swSurfaceEvents, + (void *)(uintptr_t)LG_VIDEO_SOURCE_FALLBACK, &fallbackEvents, NULL, &g_state.fallback)) return true; @@ -1871,10 +2412,18 @@ static int lg_run(void) g_state.videoOps = g_state.transport.ops->getVideoOps(g_state.transport.handle); - if (!g_state.videoOps || g_state.videoOps->type != LG_VIDEO_TYPE_FRAME || - !g_state.videoOps->frame) + if (!g_state.videoOps || + (g_state.videoOps->type != LG_VIDEO_TYPE_FRAME && + g_state.videoOps->type != LG_VIDEO_TYPE_SW_SURFACE) || + (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME && + !g_state.videoOps->frame) || + (g_state.videoOps->type == LG_VIDEO_TYPE_SW_SURFACE && + (!g_state.videoOps->swSurface || + !g_state.videoOps->swSurface->attach || + !g_state.videoOps->swSurface->detach || + !g_state.videoOps->swSurface->setActive))) { - DEBUG_ERROR("Transport does not provide a frame source"); + DEBUG_ERROR("Transport does not provide a usable video source"); return -1; } DEBUG_INFO("Using Video: %s", g_state.videoOps->name); @@ -1894,7 +2443,17 @@ static int lg_run(void) } //setup the render command queue + LG_LOCK_INIT(g_state.videoSourceLock); + LG_LOCK_INIT(g_state.videoSplashLock); renderQueue_init(); + renderQueue_setSourceFns(videoSourcePrepare, videoSourceApplied, NULL); + atomic_store_explicit(&g_state.videoSourceRequested, + LG_VIDEO_SOURCE_PRIMARY, memory_order_relaxed); + atomic_store_explicit(&g_state.videoSourceApplied, + LG_VIDEO_SOURCE_NONE, memory_order_relaxed); + g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].swSurface = + g_state.videoOps->type == LG_VIDEO_TYPE_SW_SURFACE; + g_state.videoSource[LG_VIDEO_SOURCE_FALLBACK].swSurface = true; frameScheduler_init(); g_state.micDefaultState = g_params.micDefaultState; @@ -1932,7 +2491,7 @@ static int lg_run(void) return -1; } - g_state.useDMA = + g_state.useDMA = g_state.videoOps->type == LG_VIDEO_TYPE_FRAME && g_state.videoOps->frame->supportsDMA(g_state.transport.handle); // initialize the window dimensions at init for renderers @@ -2032,11 +2591,19 @@ static int lg_run(void) if (g_state.lgr->ops.getInterop && g_state.lgr->ops.getInterop(g_state.lgr, &interop)) interopPtr = &interop; - if (g_state.videoOps->frame->attachRenderer && - !g_state.videoOps->frame->attachRenderer( - g_state.transport.handle, interopPtr)) + bool videoAttached; + if (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME) + videoAttached = !g_state.videoOps->frame->attachRenderer || + g_state.videoOps->frame->attachRenderer( + g_state.transport.handle, interopPtr); + else + videoAttached = g_state.videoOps->swSurface->attach( + g_state.transport.handle, &swSurfaceEvents, + (void *)(uintptr_t)LG_VIDEO_SOURCE_PRIMARY); + + if (!videoAttached) { - DEBUG_ERROR("Failed to attach the renderer to the transport"); + DEBUG_ERROR("Failed to attach the video source"); return -1; } @@ -2058,16 +2625,17 @@ restart: msgsCount = 0; memset(msgs, 0, sizeof(msgs)); - uint64_t initialSpiceEnable = microtime() + 1000 * 1000; + uint64_t initialFallbackEnable = g_state.fallback ? + microtime() + 1000 * 1000 : 0; while(app_getState() == APP_STATE_RUNNING) { fallbackHandleEvents(); - if (initialSpiceEnable && microtime() > initialSpiceEnable) + if (initialFallbackEnable && microtime() > initialFallbackEnable) { - app_useSpiceDisplay(true); - initialSpiceEnable = 0; + app_useVideoSource(LG_VIDEO_SOURCE_FALLBACK); + initialFallbackEnable = 0; } struct TransportSessionProbe probe = { @@ -2102,7 +2670,7 @@ restart: if (probe.status == LG_TRANSPORT_OK) { session = probe.session; - initialSpiceEnable = 0; + initialFallbackEnable = 0; break; } @@ -2237,10 +2805,22 @@ restart: atomic_store_explicit( &g_state.lgHostConnected, true, memory_order_release); - g_state.lgHostConnected = true; - - if (!core_startCursorThread() || !core_startFrameThread()) - return -1; + if (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME) + { + videoSourceBegin(LG_VIDEO_SOURCE_PRIMARY); + if (!core_startCursorThread() || !core_startFrameThread()) + return -1; + } + else + { + if (!g_state.videoOps->swSurface->setActive( + g_state.transport.handle, true)) + { + DEBUG_ERROR("Failed to activate the primary software surface"); + return -1; + } + app_useVideoSource(LG_VIDEO_SOURCE_PRIMARY); + } while(likely(app_getState() == APP_STATE_RUNNING)) { @@ -2275,8 +2855,16 @@ restart: lgSignalEvent(e_startup); lgSignalEvent(g_state.frameEvent); - core_stopFrameThread(); - core_stopCursorThread(); + if (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME) + { + core_stopFrameThread(); + core_stopCursorThread(); + } + else + g_state.videoOps->swSurface->setActive( + g_state.transport.handle, false); + videoSourceInvalidate(LG_VIDEO_SOURCE_PRIMARY); + videoSourceClearCursor(LG_VIDEO_SOURCE_PRIMARY); lgInput_dropTransport(); lgAudio_dropTransport(); lgClipboard_dropTransport(); @@ -2365,7 +2953,10 @@ static void lg_shutdown(void) if (g_state.ds && g_state.dsInitialized) g_state.ds->free(); + renderQueue_setSourceFns(NULL, NULL, NULL); renderQueue_free(); + LG_LOCK_FREE(g_state.videoSourceLock); + LG_LOCK_FREE(g_state.videoSplashLock); frameScheduler_free(); // free metrics ringbuffers diff --git a/client/src/main.h b/client/src/main.h index ec658dcb..5657bfea 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -57,6 +57,30 @@ enum AudioResampler { AUDIO_RESAMPLER_BACKEND }; +struct VideoSourceState +{ + atomic_uint_least64_t generation; + atomic_uint_least64_t transitionSerial; + atomic_bool transitionPending; + atomic_uint_least32_t width; + atomic_uint_least32_t height; + atomic_int rotate; + atomic_uint_least32_t appliedWidth; + atomic_uint_least32_t appliedHeight; + atomic_int appliedRotate; + atomic_bool ready; + bool swSurface; + bool configurePending; + + bool cursorStateValid; + bool cursorVisible; + int cursorX; + int cursorY; + int cursorHX; + int cursorHY; + uint32_t cursorWhiteLevel; +}; + struct AppState { ImGuiIO * io; @@ -79,11 +103,15 @@ struct AppState bool dsInitialized; bool jitRender; - atomic_bool fallbackDisplayRequested; - atomic_bool fallbackDisplayActive; - atomic_bool fallbackDisplayTransition; + struct VideoSourceState videoSource[LG_VIDEO_SOURCE_COUNT]; + atomic_int videoSourceRequested; + atomic_int videoSourceApplied; + atomic_uint_least64_t videoSourceAppliedGeneration; + LG_Lock videoSourceLock; + LG_Lock videoSplashLock; + bool videoGeometryDirty; + atomic_bool fallbackUUIDMismatch; - bool fallbackSurfaceValid; uint8_t guestUUID[16]; bool guestUUIDValid; @@ -296,7 +324,7 @@ struct CursorState bool realigning; /* true if the cursor needs re-drawing/updating */ - bool redraw; + atomic_bool redraw; /* true if the cursor movements should be scaled */ bool useScale; diff --git a/client/src/render_queue.c b/client/src/render_queue.c index 53353215..443da401 100644 --- a/client/src/render_queue.c +++ b/client/src/render_queue.c @@ -20,20 +20,167 @@ #include "render_queue.h" +#include #include #include #include "common/debug.h" #include "common/ll.h" #include "main.h" -#include "overlays.h" struct ll * l_renderQueue = NULL; -static bool l_showSwSurface; -static bool l_surfaceFormatValid; -static bool l_rendererSupportsNativeHDR; +static bool l_showSwSurface; +static bool l_surfaceFormatValid; +static bool l_rendererSupportsNativeHDR; static LG_RendererFormat l_surfaceFormat; +static _Atomic(uint64_t) l_sourceGeneration[RENDER_QUEUE_SOURCE_COUNT]; +static _Atomic(uint64_t) l_transitionSerial; +static LG_Lock l_sourceLock; +static LG_Lock l_transitionLock; + +static RenderQueueSource l_appliedSource; +static uint64_t l_appliedGeneration; +static bool l_appliedSwSurface; + +static RenderQueueSourcePrepareFn l_sourcePrepareFn; +static RenderQueueSourceAppliedFn l_sourceAppliedFn; +static void * l_sourceCallbackOpaque; + +typedef struct RenderQueueTransition +{ + RenderQueueSource source; + uint64_t generation; + uint64_t serial; + bool swSurface; + bool valid; +} +RenderQueueTransition; + +static RenderQueueTransition l_pendingTransition; + +typedef struct RenderQueueCursor +{ + uint64_t stateGeneration; + bool stateValid; + bool visible; + int x; + int y; + int hx; + int hy; + + uint64_t imageGeneration; + bool imageValid; + LG_RendererCursor type; + int width; + int height; + int pitch; + uint8_t * data; + + uint64_t colorGeneration; + bool colorValid; + LGColorTransform colorTransform; + + uint64_t whiteGeneration; + bool whiteValid; + uint32_t sdrWhiteLevel; +} +RenderQueueCursor; + +typedef struct RenderQueueFormat +{ + uint64_t generation; + bool valid; + LG_RendererFormat format; + bool rendererSupportsNativeHDR; +} +RenderQueueFormat; + +static RenderQueueCursor l_cursor[RENDER_QUEUE_SOURCE_COUNT]; +static RenderQueueFormat l_format[RENDER_QUEUE_SOURCE_COUNT]; +static const LGColorTransform l_identityColorTransform; + +static bool sourceValid(RenderQueueSource source) +{ + return source > RENDER_QUEUE_SOURCE_NONE && + source < RENDER_QUEUE_SOURCE_COUNT; +} + +static bool generationValid(RenderQueueSource source, uint64_t generation) +{ + return sourceValid(source) && generation && + atomic_load_explicit(&l_sourceGeneration[source], + memory_order_acquire) == generation; +} + +static bool transitionCommand(const RenderCommand * cmd) +{ + return cmd->op == SOURCE_OP_TRANSITION || + cmd->op == SW_SURFACE_OP_CONFIGURE_TRANSITION; +} + +static bool commandValid(const RenderCommand * cmd) +{ + if (transitionCommand(cmd) && + atomic_load_explicit(&l_transitionSerial, memory_order_acquire) != + cmd->transitionSerial) + return false; + + if (cmd->source == RENDER_QUEUE_SOURCE_NONE) + return cmd->op == SOURCE_OP_TRANSITION; + + return generationValid(cmd->source, cmd->generation); +} + +static void freeCommand(RenderCommand * cmd) +{ + switch (cmd->op) + { + case SW_SURFACE_OP_DRAW_BITMAP: + free(cmd->swSurfaceDrawBitmap.data); + break; + + case CURSOR_OP_IMAGE: + free(cmd->cursorImage.data); + break; + + case CURSOR_OP_COLOR_TRANSFORM: + free(cmd->cursorColorTransform.data); + break; + + default: + break; + } + + free(cmd); +} + +static void setCommandSource(RenderCommand * cmd, RenderQueueSource source, + uint64_t generation) +{ + cmd->source = source; + cmd->generation = generation; +} + +static bool copyCursorImage(RenderCommand * cmd, const void * data) +{ + if (!data || cmd->cursorImage.height <= 0 || cmd->cursorImage.pitch <= 0) + return false; + + if ((size_t)cmd->cursorImage.height > + SIZE_MAX / (size_t)cmd->cursorImage.pitch) + return false; + + const size_t size = + (size_t)cmd->cursorImage.height * (size_t)cmd->cursorImage.pitch; + cmd->cursorImage.data = malloc(size); + if (!cmd->cursorImage.data) + return false; + + memcpy(cmd->cursorImage.data, data, size); + return true; +} + static void updateSurfaceFormat(void) { if (!g_state.ds->setHDRImageDesc) @@ -56,7 +203,11 @@ static void updateSurfaceFormat(void) } if (!l_surfaceFormatValid) + { + g_state.ds->setHDRImageDesc(&format); + atomic_store(&g_state.hdrDescFailed, false); return; + } if (format.hdr && !l_rendererSupportsNativeHDR) { @@ -83,7 +234,22 @@ void renderQueue_init(void) l_showSwSurface = false; l_surfaceFormatValid = false; l_rendererSupportsNativeHDR = false; + l_appliedSource = RENDER_QUEUE_SOURCE_NONE; + l_appliedGeneration = 0; + l_appliedSwSurface = false; + l_sourcePrepareFn = NULL; + l_sourceAppliedFn = NULL; + l_sourceCallbackOpaque = NULL; + l_pendingTransition = (RenderQueueTransition) {}; memset(&l_surfaceFormat, 0, sizeof(l_surfaceFormat)); + memset(l_cursor, 0, sizeof(l_cursor)); + memset(l_format, 0, sizeof(l_format)); + + for (int i = 0; i < RENDER_QUEUE_SOURCE_COUNT; ++i) + atomic_store_explicit(&l_sourceGeneration[i], 0, memory_order_relaxed); + atomic_store_explicit(&l_transitionSerial, 0, memory_order_relaxed); + LG_LOCK_INIT(l_sourceLock); + LG_LOCK_INIT(l_transitionLock); } void renderQueue_free(void) @@ -92,47 +258,194 @@ void renderQueue_free(void) return; renderQueue_clear(); + + for (int i = 0; i < RENDER_QUEUE_SOURCE_COUNT; ++i) + { + free(l_cursor[i].data); + l_cursor[i].data = NULL; + } + ll_free(l_renderQueue); + l_renderQueue = NULL; + LG_LOCK_FREE(l_sourceLock); + LG_LOCK_FREE(l_transitionLock); } void renderQueue_clear(void) { RenderCommand * cmd; while(ll_shift(l_renderQueue, (void **)&cmd)) - { - if (cmd->op == SW_SURFACE_OP_DRAW_BITMAP) - free(cmd->swSurfaceDrawBitmap.data); - free(cmd); - } + freeCommand(cmd); } -void renderQueue_swSurfaceConfigure(int width, int height) +void renderQueue_setSourceFns(RenderQueueSourcePrepareFn prepare, + RenderQueueSourceAppliedFn applied, void * opaque) { - RenderCommand * cmd = malloc(sizeof(*cmd)); - cmd->op = SW_SURFACE_OP_CONFIGURE; - cmd->swSurfaceConfigure.width = width; - cmd->swSurfaceConfigure.height = height; - ll_push(l_renderQueue, cmd); - app_invalidateWindow(true); + l_sourcePrepareFn = prepare; + l_sourceAppliedFn = applied; + l_sourceCallbackOpaque = opaque; } -void renderQueue_swSurfaceDrawFill(int x, int y, int width, int height, +uint64_t renderQueue_sourceBegin(RenderQueueSource source) +{ + if (!sourceValid(source)) + return 0; + + LG_LOCK(l_sourceLock); + const uint64_t previous = atomic_load_explicit( + &l_sourceGeneration[source], memory_order_relaxed); + const uint64_t generation = previous + 1; + atomic_store_explicit( + &l_sourceGeneration[source], generation, memory_order_release); + + RenderQueueCursor * cursor = &l_cursor[source]; + if (cursor->stateValid && cursor->stateGeneration == previous) + cursor->stateGeneration = generation; + if (cursor->imageValid && cursor->imageGeneration == previous) + cursor->imageGeneration = generation; + if (cursor->colorValid && cursor->colorGeneration == previous) + cursor->colorGeneration = generation; + if (cursor->whiteValid && cursor->whiteGeneration == previous) + cursor->whiteGeneration = generation; + LG_UNLOCK(l_sourceLock); + return generation; +} + +void renderQueue_sourceInvalidate(RenderQueueSource source, + uint64_t generation) +{ + if (!sourceValid(source) || !generation) + return; + LG_LOCK(l_sourceLock); + const uint64_t invalidGeneration = generation + 1; + if (atomic_compare_exchange_strong_explicit(&l_sourceGeneration[source], + &generation, invalidGeneration, memory_order_acq_rel, + memory_order_relaxed)) + { + RenderQueueCursor * cursor = &l_cursor[source]; + if (cursor->stateValid && cursor->stateGeneration == generation) + cursor->stateGeneration = invalidGeneration; + if (cursor->imageValid && cursor->imageGeneration == generation) + cursor->imageGeneration = invalidGeneration; + if (cursor->colorValid && cursor->colorGeneration == generation) + cursor->colorGeneration = invalidGeneration; + if (cursor->whiteValid && cursor->whiteGeneration == generation) + cursor->whiteGeneration = invalidGeneration; + } + LG_UNLOCK(l_sourceLock); +} + +void renderQueue_sourceClearCursor(RenderQueueSource source) +{ + if (!sourceValid(source)) + return; + + LG_LOCK(l_sourceLock); + free(l_cursor[source].data); + memset(&l_cursor[source], 0, sizeof(l_cursor[source])); + LG_UNLOCK(l_sourceLock); +} + +static uint64_t enqueueTransition(RenderCommand * cmd, + atomic_uint_least64_t * publishedSerial) +{ + LG_LOCK(l_transitionLock); + const uint64_t serial = atomic_load_explicit( + &l_transitionSerial, memory_order_relaxed) + 1; + cmd->transitionSerial = serial; + if (!ll_push(l_renderQueue, cmd)) + { + LG_UNLOCK(l_transitionLock); + free(cmd); + return 0; + } + atomic_store_explicit( + &l_transitionSerial, serial, memory_order_release); + if (publishedSerial) + atomic_store_explicit(publishedSerial, serial, memory_order_release); + LG_UNLOCK(l_transitionLock); + + app_invalidateWindow(true); + return serial; +} + +uint64_t renderQueue_sourceTransition(RenderQueueSource source, + uint64_t generation, bool swSurface, + atomic_uint_least64_t * publishedSerial) +{ + if (source != RENDER_QUEUE_SOURCE_NONE && + !generationValid(source, generation)) + return 0; + + if (source == RENDER_QUEUE_SOURCE_NONE) + { + generation = 0; + swSurface = false; + } + + RenderCommand * cmd = malloc(sizeof(*cmd)); + if (!cmd) + return 0; + + setCommandSource(cmd, source, generation); + cmd->op = SOURCE_OP_TRANSITION; + cmd->sourceTransition.swSurface = swSurface; + return enqueueTransition(cmd, publishedSerial); +} + +uint64_t renderQueue_sourceSwSurfaceConfigureTransition( + RenderQueueSource source, uint64_t generation, int width, int height, + atomic_uint_least64_t * publishedSerial) +{ + if (!generationValid(source, generation)) + return 0; + + RenderCommand * cmd = malloc(sizeof(*cmd)); + if (!cmd) + return 0; + + setCommandSource(cmd, source, generation); + cmd->op = + SW_SURFACE_OP_CONFIGURE_TRANSITION; + cmd->swSurfaceConfigureTransition.width = width; + cmd->swSurfaceConfigureTransition.height = height; + return enqueueTransition(cmd, publishedSerial); +} + +void renderQueue_sourceSwSurfaceDrawFill(RenderQueueSource source, + uint64_t generation, int x, int y, int width, int height, uint32_t color) { + if (!generationValid(source, generation)) + return; + RenderCommand * cmd = malloc(sizeof(*cmd)); + if (!cmd) + return; + + setCommandSource(cmd, source, generation); cmd->op = SW_SURFACE_OP_DRAW_FILL; cmd->swSurfaceDrawFill.x = x; cmd->swSurfaceDrawFill.y = y; cmd->swSurfaceDrawFill.width = width; cmd->swSurfaceDrawFill.height = height; cmd->swSurfaceDrawFill.color = color; - ll_push(l_renderQueue, cmd); + if (!ll_push(l_renderQueue, cmd)) + { + free(cmd); + return; + } + app_invalidateWindow(true); } -void renderQueue_swSurfaceDrawBitmap(int x, int y, int width, int height, - int stride, const void * data, bool topDown) +void renderQueue_sourceSwSurfaceDrawBitmap(RenderQueueSource source, + uint64_t generation, int x, int y, int width, int height, int stride, + const void * data, bool topDown) { + if (!generationValid(source, generation)) + return; + if (width <= 0 || height <= 0 || stride <= 0) { if (width < 0 || height < 0 || stride < 0) @@ -175,6 +488,7 @@ void renderQueue_swSurfaceDrawBitmap(int x, int y, int width, int height, memcpy(copy, data, size); + setCommandSource(cmd, source, generation); cmd->op = SW_SURFACE_OP_DRAW_BITMAP; cmd->swSurfaceDrawBitmap.x = x; cmd->swSurfaceDrawBitmap.y = y; @@ -186,7 +500,31 @@ void renderQueue_swSurfaceDrawBitmap(int x, int y, int width, int height, if (!ll_push(l_renderQueue, cmd)) { - free(copy); + freeCommand(cmd); + return; + } + + app_invalidateWindow(true); +} + +void renderQueue_sourceSurfaceFormat(RenderQueueSource source, + uint64_t generation, const LG_RendererFormat format, + bool rendererSupportsNativeHDR) +{ + if (!generationValid(source, generation)) + return; + + RenderCommand * cmd = malloc(sizeof(*cmd)); + if (!cmd) + return; + + setCommandSource(cmd, source, generation); + cmd->op = SURFACE_OP_FORMAT; + cmd->surfaceFormat.format = format; + cmd->surfaceFormat.rendererSupportsNativeHDR = + rendererSupportsNativeHDR; + if (!ll_push(l_renderQueue, cmd)) + { free(cmd); return; } @@ -194,50 +532,161 @@ void renderQueue_swSurfaceDrawBitmap(int x, int y, int width, int height, app_invalidateWindow(true); } -void renderQueue_swSurfaceShow(bool show) +void renderQueue_sourceCursorState(RenderQueueSource source, + uint64_t generation, bool visible, int x, int y, int hx, int hy) { - RenderCommand * cmd = malloc(sizeof(*cmd)); - cmd->op = SW_SURFACE_OP_SHOW; - cmd->swSurfaceShow.show = show; - ll_push(l_renderQueue, cmd); - app_invalidateWindow(true); -} + if (!generationValid(source, generation)) + return; -void renderQueue_surfaceFormat(const LG_RendererFormat format, - bool rendererSupportsNativeHDR) -{ RenderCommand * cmd = malloc(sizeof(*cmd)); - cmd->op = SURFACE_OP_FORMAT; - cmd->surfaceFormat.format = format; - cmd->surfaceFormat.rendererSupportsNativeHDR = - rendererSupportsNativeHDR; - ll_push(l_renderQueue, cmd); - app_invalidateWindow(true); -} + if (!cmd) + return; -void renderQueue_cursorState(bool visible, int x, int y, int hx, int hy) -{ - RenderCommand * cmd = malloc(sizeof(*cmd)); + setCommandSource(cmd, source, generation); cmd->op = CURSOR_OP_STATE; cmd->cursorState.visible = visible; cmd->cursorState.x = x; cmd->cursorState.y = y; cmd->cursorState.hx = hx; cmd->cursorState.hy = hy; - ll_push(l_renderQueue, cmd); + if (!ll_push(l_renderQueue, cmd)) + free(cmd); } -void renderQueue_cursorImage(LG_RendererCursor type, - int width, int height, int pitch, uint8_t * data) +void renderQueue_sourceCursorImage(RenderQueueSource source, + uint64_t generation, LG_RendererCursor type, + int width, int height, int pitch, const void * data) { - RenderCommand * cmd = malloc(sizeof(*cmd)); + if (!generationValid(source, generation)) + return; + + RenderCommand * cmd = malloc(sizeof(*cmd)); + if (!cmd) + return; + + setCommandSource(cmd, source, generation); cmd->op = CURSOR_OP_IMAGE; cmd->cursorImage.type = type; cmd->cursorImage.width = width; cmd->cursorImage.height = height; cmd->cursorImage.pitch = pitch; - cmd->cursorImage.data = data; - ll_push(l_renderQueue, cmd); + cmd->cursorImage.data = NULL; + if (!copyCursorImage(cmd, data) || !ll_push(l_renderQueue, cmd)) + freeCommand(cmd); +} + +void renderQueue_sourceCursorColorTransform(RenderQueueSource source, + uint64_t generation, const LGColorTransform * transform) +{ + if (!generationValid(source, generation) || !transform) + return; + + RenderCommand * cmd = malloc(sizeof(*cmd)); + if (!cmd) + return; + + LGColorTransform * copy = malloc(sizeof(*copy)); + if (!copy) + { + free(cmd); + return; + } + *copy = *transform; + + setCommandSource(cmd, source, generation); + cmd->op = CURSOR_OP_COLOR_TRANSFORM; + cmd->cursorColorTransform.data = copy; + if (!ll_push(l_renderQueue, cmd)) + freeCommand(cmd); +} + +void renderQueue_sourceCursorWhiteLevel(RenderQueueSource source, + uint64_t generation, uint32_t sdrWhiteLevel) +{ + if (!generationValid(source, generation)) + return; + + RenderCommand * cmd = malloc(sizeof(*cmd)); + if (!cmd) + return; + + setCommandSource(cmd, source, generation); + cmd->op = CURSOR_OP_WHITE_LEVEL; + cmd->cursorWhiteLevel.value = sdrWhiteLevel; + if (!ll_push(l_renderQueue, cmd)) + free(cmd); +} + +static void applyCursor(RenderQueueSource source, uint64_t generation) +{ + RenderQueueCursor * cursor = &l_cursor[source]; + + if (cursor->imageValid && cursor->imageGeneration == generation) + RENDERER(onMouseShape, cursor->type, cursor->width, cursor->height, + cursor->pitch, cursor->data); + + if (cursor->imageValid && cursor->imageGeneration == generation && + cursor->stateValid && cursor->stateGeneration == generation) + RENDERER(onMouseEvent, cursor->visible, cursor->x, cursor->y, + cursor->hx, cursor->hy); + else + RENDERER(onMouseEvent, false, 0, 0, 0, 0); + + if (g_state.lgr->ops.onMouseColorTransform) + g_state.lgr->ops.onMouseColorTransform(g_state.lgr, + cursor->colorValid && cursor->colorGeneration == generation ? + &cursor->colorTransform : &l_identityColorTransform); + + if (g_state.lgr->ops.onMouseWhiteLevel) + g_state.lgr->ops.onMouseWhiteLevel(g_state.lgr, + cursor->whiteValid && cursor->whiteGeneration == generation ? + cursor->sdrWhiteLevel : LG_SDR_WHITE_LEVEL_DEFAULT); +} + +static void applySourceTransition(const RenderCommand * cmd, + bool showSwSurface) +{ + if (l_showSwSurface != showSwSurface) + { + RENDERER(swSurfaceShow, showSwSurface); + l_showSwSurface = showSwSurface; + } + + l_surfaceFormatValid = false; + l_rendererSupportsNativeHDR = false; + if (sourceValid(cmd->source)) + { + const RenderQueueFormat * format = &l_format[cmd->source]; + if (format->valid && format->generation == cmd->generation) + { + l_surfaceFormat = format->format; + l_surfaceFormatValid = true; + l_rendererSupportsNativeHDR = + format->rendererSupportsNativeHDR; + } + } + updateSurfaceFormat(); + + l_appliedSource = cmd->source; + l_appliedGeneration = cmd->generation; + l_appliedSwSurface = showSwSurface; + + if (sourceValid(cmd->source)) + applyCursor(cmd->source, cmd->generation); + else + RENDERER(onMouseEvent, false, 0, 0, 0, 0); + + l_pendingTransition.source = cmd->source; + l_pendingTransition.generation = cmd->generation; + l_pendingTransition.serial = cmd->transitionSerial; + l_pendingTransition.swSurface = l_appliedSwSurface; + l_pendingTransition.valid = true; +} + +static bool prepareSourceTransition(const RenderCommand * cmd) +{ + return !l_sourcePrepareFn || l_sourcePrepareFn(l_sourceCallbackOpaque, + cmd->source, cmd->generation, cmd->transitionSerial); } void renderQueue_process(void) @@ -245,12 +694,32 @@ void renderQueue_process(void) RenderCommand * cmd; while(ll_shift(l_renderQueue, (void **)&cmd)) { + const bool transition = transitionCommand(cmd); + if (transition) + LG_LOCK(l_transitionLock); + LG_LOCK(l_sourceLock); + + if (!commandValid(cmd)) + { + LG_UNLOCK(l_sourceLock); + if (transition) + LG_UNLOCK(l_transitionLock); + freeCommand(cmd); + continue; + } + switch(cmd->op) { - case SW_SURFACE_OP_CONFIGURE: + case SW_SURFACE_OP_CONFIGURE_TRANSITION: + if (!prepareSourceTransition(cmd)) + break; RENDERER(swSurfaceConfigure, - cmd->swSurfaceConfigure.width, - cmd->swSurfaceConfigure.height); + cmd->swSurfaceConfigureTransition.width, + cmd->swSurfaceConfigureTransition.height); + RENDERER(swSurfaceDrawFill, 0, 0, + cmd->swSurfaceConfigureTransition.width, + cmd->swSurfaceConfigureTransition.height, 0); + applySourceTransition(cmd, true); break; case SW_SURFACE_OP_DRAW_FILL: @@ -267,36 +736,132 @@ void renderQueue_process(void) cmd->swSurfaceDrawBitmap.stride, cmd->swSurfaceDrawBitmap.data, cmd->swSurfaceDrawBitmap.topDown); free(cmd->swSurfaceDrawBitmap.data); - break; - - case SW_SURFACE_OP_SHOW: - l_showSwSurface = cmd->swSurfaceShow.show; - RENDERER(swSurfaceShow, cmd->swSurfaceShow.show); - updateSurfaceFormat(); - if (cmd->swSurfaceShow.show) - overlaySplash_show(false); + cmd->swSurfaceDrawBitmap.data = NULL; break; case SURFACE_OP_FORMAT: - l_surfaceFormat = cmd->surfaceFormat.format; - l_surfaceFormatValid = true; - l_rendererSupportsNativeHDR = + { + RenderQueueFormat * format = &l_format[cmd->source]; + format->generation = cmd->generation; + format->valid = true; + format->format = cmd->surfaceFormat.format; + format->rendererSupportsNativeHDR = cmd->surfaceFormat.rendererSupportsNativeHDR; - updateSurfaceFormat(); + + if (l_appliedSource == cmd->source && + l_appliedGeneration == cmd->generation && + !l_appliedSwSurface) + { + l_surfaceFormat = format->format; + l_surfaceFormatValid = true; + l_rendererSupportsNativeHDR = + format->rendererSupportsNativeHDR; + updateSurfaceFormat(); + } break; + } case CURSOR_OP_STATE: - RENDERER(onMouseEvent, cmd->cursorState.visible, cmd->cursorState.x, - cmd->cursorState.y, cmd->cursorState.hx, cmd->cursorState.hy); + { + RenderQueueCursor * cursor = &l_cursor[cmd->source]; + cursor->stateGeneration = cmd->generation; + cursor->stateValid = true; + cursor->visible = cmd->cursorState.visible; + cursor->x = cmd->cursorState.x; + cursor->y = cmd->cursorState.y; + cursor->hx = cmd->cursorState.hx; + cursor->hy = cmd->cursorState.hy; + + if (l_appliedSource == cmd->source && + l_appliedGeneration == cmd->generation) + RENDERER(onMouseEvent, + cursor->imageValid && + cursor->imageGeneration == cmd->generation && + cursor->visible, + cursor->x, cursor->y, cursor->hx, cursor->hy); break; + } case CURSOR_OP_IMAGE: - RENDERER(onMouseShape, - cmd->cursorImage.type, - cmd->cursorImage.width, cmd->cursorImage.height, - cmd->cursorImage.pitch, cmd->cursorImage.data); - free(cmd->cursorImage.data); + { + RenderQueueCursor * cursor = &l_cursor[cmd->source]; + free(cursor->data); + cursor->imageGeneration = cmd->generation; + cursor->imageValid = true; + cursor->type = cmd->cursorImage.type; + cursor->width = cmd->cursorImage.width; + cursor->height = cmd->cursorImage.height; + cursor->pitch = cmd->cursorImage.pitch; + cursor->data = cmd->cursorImage.data; + cmd->cursorImage.data = NULL; + + if (l_appliedSource == cmd->source && + l_appliedGeneration == cmd->generation) + { + RENDERER(onMouseShape, cursor->type, cursor->width, + cursor->height, cursor->pitch, cursor->data); + if (cursor->stateValid && + cursor->stateGeneration == cmd->generation) + RENDERER(onMouseEvent, cursor->visible, cursor->x, cursor->y, + cursor->hx, cursor->hy); + } + break; + } + + case CURSOR_OP_COLOR_TRANSFORM: + { + RenderQueueCursor * cursor = &l_cursor[cmd->source]; + cursor->colorGeneration = cmd->generation; + cursor->colorValid = true; + cursor->colorTransform = *cmd->cursorColorTransform.data; + + if (l_appliedSource == cmd->source && + l_appliedGeneration == cmd->generation && + g_state.lgr->ops.onMouseColorTransform) + g_state.lgr->ops.onMouseColorTransform(g_state.lgr, + &cursor->colorTransform); + break; + } + + case CURSOR_OP_WHITE_LEVEL: + { + RenderQueueCursor * cursor = &l_cursor[cmd->source]; + cursor->whiteGeneration = cmd->generation; + cursor->whiteValid = true; + cursor->sdrWhiteLevel = cmd->cursorWhiteLevel.value; + + if (l_appliedSource == cmd->source && + l_appliedGeneration == cmd->generation && + g_state.lgr->ops.onMouseWhiteLevel) + g_state.lgr->ops.onMouseWhiteLevel(g_state.lgr, + cursor->sdrWhiteLevel); + break; + } + + case SOURCE_OP_TRANSITION: + if (!prepareSourceTransition(cmd)) + break; + applySourceTransition(cmd, + cmd->source != RENDER_QUEUE_SOURCE_NONE && + cmd->sourceTransition.swSurface); + break; } - free(cmd); + LG_UNLOCK(l_sourceLock); + if (transition) + LG_UNLOCK(l_transitionLock); + freeCommand(cmd); } } + +void renderQueue_presented(void) +{ + if (!l_pendingTransition.valid) + return; + + const RenderQueueTransition pending = l_pendingTransition; + l_pendingTransition.valid = false; + + if (l_sourceAppliedFn) + l_sourceAppliedFn(l_sourceCallbackOpaque, pending.source, + pending.generation, pending.serial, pending.swSurface); +} diff --git a/client/src/render_queue.h b/client/src/render_queue.h index a4dde5f0..ce2a6deb 100644 --- a/client/src/render_queue.h +++ b/client/src/render_queue.h @@ -21,17 +21,40 @@ #include "common/ll.h" #include "interface/renderer.h" +#include + +typedef enum RenderQueueSource +{ + RENDER_QUEUE_SOURCE_NONE, + RENDER_QUEUE_SOURCE_PRIMARY, + RENDER_QUEUE_SOURCE_FALLBACK, + RENDER_QUEUE_SOURCE_COUNT, +} +RenderQueueSource; + +typedef void (*RenderQueueSourceAppliedFn)(void * opaque, + RenderQueueSource source, uint64_t generation, uint64_t serial, + bool swSurface); +typedef bool (*RenderQueueSourcePrepareFn)(void * opaque, + RenderQueueSource source, uint64_t generation, uint64_t serial); + typedef struct { + RenderQueueSource source; + uint64_t generation; + uint64_t transitionSerial; + enum { - SW_SURFACE_OP_CONFIGURE, + SW_SURFACE_OP_CONFIGURE_TRANSITION, SW_SURFACE_OP_DRAW_FILL, SW_SURFACE_OP_DRAW_BITMAP, - SW_SURFACE_OP_SHOW, SURFACE_OP_FORMAT, CURSOR_OP_STATE, CURSOR_OP_IMAGE, + CURSOR_OP_COLOR_TRANSFORM, + CURSOR_OP_WHITE_LEVEL, + SOURCE_OP_TRANSITION, } op; @@ -41,7 +64,7 @@ typedef struct { int width, height; } - swSurfaceConfigure; + swSurfaceConfigureTransition; struct { @@ -61,12 +84,6 @@ typedef struct } swSurfaceDrawBitmap; - struct - { - bool show; - } - swSurfaceShow; - struct { LG_RendererFormat format; @@ -93,6 +110,24 @@ typedef struct uint8_t * data; } cursorImage; + + struct + { + LGColorTransform * data; + } + cursorColorTransform; + + struct + { + uint32_t value; + } + cursorWhiteLevel; + + struct + { + bool swSurface; + } + sourceTransition; }; } RenderCommand; @@ -101,22 +136,48 @@ void renderQueue_init(void); void renderQueue_free(void); void renderQueue_clear(void); void renderQueue_process(void); +void renderQueue_presented(void); -void renderQueue_swSurfaceConfigure(int width, int height); +void renderQueue_setSourceFns(RenderQueueSourcePrepareFn prepare, + RenderQueueSourceAppliedFn applied, void * opaque); -void renderQueue_swSurfaceDrawFill(int x, int y, int width, int height, +/* Starting a source lifecycle makes commands from its prior generation stale. + * After a successful render, call renderQueue_presented on the render thread + * to acknowledge the newest applied transition. */ +uint64_t renderQueue_sourceBegin(RenderQueueSource source); +void renderQueue_sourceInvalidate(RenderQueueSource source, + uint64_t generation); +void renderQueue_sourceClearCursor(RenderQueueSource source); +uint64_t renderQueue_sourceTransition(RenderQueueSource source, + uint64_t generation, bool swSurface, + atomic_uint_least64_t * publishedSerial); + +uint64_t renderQueue_sourceSwSurfaceConfigureTransition( + RenderQueueSource source, uint64_t generation, int width, int height, + atomic_uint_least64_t * publishedSerial); + +void renderQueue_sourceSwSurfaceDrawFill(RenderQueueSource source, + uint64_t generation, int x, int y, int width, int height, uint32_t color); -void renderQueue_swSurfaceDrawBitmap(int x, int y, int width, int height, - int stride, const void * data, bool topDown); +void renderQueue_sourceSwSurfaceDrawBitmap(RenderQueueSource source, + uint64_t generation, int x, int y, int width, int height, int stride, + const void * data, bool topDown); -void renderQueue_swSurfaceShow(bool show); - -void renderQueue_surfaceFormat(const LG_RendererFormat format, +void renderQueue_sourceSurfaceFormat(RenderQueueSource source, + uint64_t generation, const LG_RendererFormat format, bool rendererSupportsNativeHDR); -void renderQueue_cursorState(bool visible, int x, int y, int hx, int hy); +void renderQueue_sourceCursorState(RenderQueueSource source, + uint64_t generation, bool visible, int x, int y, int hx, int hy); -void renderQueue_cursorImage(LG_RendererCursor type, - int width, int height, int pitch, - uint8_t * data); +/* Cursor payloads are copied before these calls return. */ +void renderQueue_sourceCursorImage(RenderQueueSource source, + uint64_t generation, LG_RendererCursor type, + int width, int height, int pitch, const void * data); + +void renderQueue_sourceCursorColorTransform(RenderQueueSource source, + uint64_t generation, const LGColorTransform * transform); + +void renderQueue_sourceCursorWhiteLevel(RenderQueueSource source, + uint64_t generation, uint32_t sdrWhiteLevel); diff --git a/client/src/transport_fallback.c b/client/src/transport_fallback.c index 58cdaca5..2152cea0 100644 --- a/client/src/transport_fallback.c +++ b/client/src/transport_fallback.c @@ -49,7 +49,6 @@ struct LG_TransportFallback LGThread * thread; LGEvent * wakeEvent; - LGEvent * videoIdleEvent; LG_RWLock lock; atomic_bool stop; @@ -66,7 +65,6 @@ struct LG_TransportFallback bool closing; bool videoRequested; bool videoActive; - bool videoBusy; bool primaryUUIDValid; uint8_t primaryUUID[16]; @@ -76,6 +74,8 @@ struct LG_TransportFallback uint8_t mismatchFallback[16]; }; +static bool applyVideoRequest(LG_TransportFallback * fallback); + static bool connectCancelled(void * opaque) { const LG_TransportFallback * fallback = opaque; @@ -139,26 +139,18 @@ static void unpublishProviders(LG_TransportFallback * fallback, bool live) fallback->providersPublished = false; } -static void waitForVideoIdle(LG_TransportFallback * fallback) +static void closeVideoAdmission(LG_TransportFallback * fallback) { - for (;;) - { - LG_LOCK_EXCLUSIVE(fallback->lock); - atomic_store_explicit(&fallback->ready, false, memory_order_release); - fallback->closing = true; - const bool busy = fallback->videoBusy; - LG_UNLOCK_EXCLUSIVE(fallback->lock); - - if (!busy) - return; - lgWaitEvent(fallback->videoIdleEvent, TIMEOUT_INFINITE); - } + LG_LOCK_EXCLUSIVE(fallback->lock); + atomic_store_explicit(&fallback->ready, false, memory_order_release); + fallback->closing = true; + LG_UNLOCK_EXCLUSIVE(fallback->lock); } static bool cleanupConnection(LG_TransportFallback * fallback, bool knownDead) { - waitForVideoIdle(fallback); + closeVideoAdmission(fallback); LG_LOCK_EXCLUSIVE(fallback->lock); const bool reportDisconnected = fallback->connectedReported; @@ -352,6 +344,7 @@ static bool connectFallback(LG_TransportFallback * fallback) while (!atomic_load_explicit(&fallback->stop, memory_order_acquire)) { lgWaitEvent(fallback->wakeEvent, SESSION_POLL_MS); + applyVideoRequest(fallback); LG_LOCK_EXCLUSIVE(fallback->lock); const bool reject = uuidMismatchLocked(fallback); @@ -435,10 +428,6 @@ bool lgTransportFallback_start(const char * transportName, if (!fallback->wakeEvent) goto fail; - fallback->videoIdleEvent = lgCreateEvent(true, 0); - if (!fallback->videoIdleEvent) - goto fail; - *result = fallback; if (!lgCreateThread("transportFallback", fallbackThread, fallback, &fallback->thread)) @@ -448,8 +437,6 @@ bool lgTransportFallback_start(const char * transportName, fail: *result = NULL; - if (fallback->videoIdleEvent) - lgFreeEvent(fallback->videoIdleEvent); if (fallback->wakeEvent) lgFreeEvent(fallback->wakeEvent); LG_RWLOCK_FREE(fallback->lock); @@ -477,7 +464,6 @@ void lgTransportFallback_stop(LG_TransportFallback ** fallbackPtr) return; } - lgFreeEvent(fallback->videoIdleEvent); lgFreeEvent(fallback->wakeEvent); LG_RWLOCK_FREE(fallback->lock); free(fallback->transportName); @@ -490,20 +476,16 @@ bool lgTransportFallback_ready(const LG_TransportFallback * fallback) &fallback->ready, memory_order_acquire); } -bool lgTransportFallback_setVideoActive( - LG_TransportFallback * fallback, bool active) +static bool applyVideoRequest(LG_TransportFallback * fallback) { - if (!fallback) - return false; - - uint64_t connectionSerial; - LG_Transport * transport; - const LG_SwSurfaceOps * surfaceOps; - for (;;) { + uint64_t connectionSerial; + LG_Transport * transport; + const LG_SwSurfaceOps * surfaceOps; + bool requested; + LG_LOCK_EXCLUSIVE(fallback->lock); - fallback->videoRequested = active; if (!atomic_load_explicit(&fallback->ready, memory_order_acquire) || fallback->closing) { @@ -511,38 +493,44 @@ bool lgTransportFallback_setVideoActive( return false; } - if (fallback->videoActive == active) + requested = fallback->videoRequested; + if (fallback->videoActive == requested) { LG_UNLOCK_EXCLUSIVE(fallback->lock); return true; } - if (!fallback->videoBusy) - { - fallback->videoBusy = true; - connectionSerial = fallback->connectionSerial; - transport = fallback->transport.handle; - surfaceOps = fallback->videoOps->swSurface; - LG_UNLOCK_EXCLUSIVE(fallback->lock); - break; - } - + connectionSerial = fallback->connectionSerial; + transport = fallback->transport.handle; + surfaceOps = fallback->videoOps->swSurface; LG_UNLOCK_EXCLUSIVE(fallback->lock); - lgWaitEvent(fallback->videoIdleEvent, TIMEOUT_INFINITE); - } - lgResetEvent(fallback->videoIdleEvent); - const bool result = surfaceOps->setActive(transport, active); + const bool result = surfaceOps->setActive(transport, requested); + + LG_LOCK_EXCLUSIVE(fallback->lock); + const bool current = fallback->connectionSerial == connectionSerial; + if (current && result) + fallback->videoActive = requested; + const bool accepted = current && !fallback->closing && result; + const bool retry = current && !fallback->closing && + fallback->videoRequested != requested; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + + if (!retry) + return accepted; + } +} + +void lgTransportFallback_requestVideoActive( + LG_TransportFallback * fallback, bool active) +{ + if (!fallback) + return; LG_LOCK_EXCLUSIVE(fallback->lock); - const bool current = fallback->connectionSerial == connectionSerial; - if (current && result) - fallback->videoActive = active; - const bool accepted = current && !fallback->closing && result; - fallback->videoBusy = false; + fallback->videoRequested = active; LG_UNLOCK_EXCLUSIVE(fallback->lock); - lgSignalEvent(fallback->videoIdleEvent); - return accepted; + lgSignalEvent(fallback->wakeEvent); } void lgTransportFallback_setPrimaryUUID( diff --git a/client/src/transport_fallback.h b/client/src/transport_fallback.h index 32d00c51..2d228c9a 100644 --- a/client/src/transport_fallback.h +++ b/client/src/transport_fallback.h @@ -46,7 +46,7 @@ void lgTransportFallback_stop(LG_TransportFallback ** fallback); bool lgTransportFallback_ready(const LG_TransportFallback * fallback); /* The requested state is retained across reconnects. */ -bool lgTransportFallback_setVideoActive( +void lgTransportFallback_requestVideoActive( LG_TransportFallback * fallback, bool active); void lgTransportFallback_setPrimaryUUID( diff --git a/client/tests/mouse_test.c b/client/tests/mouse_test.c index a9b802df..f17d846f 100644 --- a/client/tests/mouse_test.c +++ b/client/tests/mouse_test.c @@ -262,6 +262,11 @@ bool app_isRunning(void) return false; } +void app_invalidateWindow(bool full) +{ + (void)full; +} + uint64_t app_mouseSeq(void) { return 0;