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;