diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 80f58afa..02279b9b 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -188,6 +188,7 @@ set(SOURCES src/evdev.c src/transport.c src/transport_fallback.c + src/sw_surface.c src/input.c src/overlay/splash.c diff --git a/client/include/interface/transport.h b/client/include/interface/transport.h index 6c7dae58..020b21f6 100644 --- a/client/include/interface/transport.h +++ b/client/include/interface/transport.h @@ -113,6 +113,8 @@ typedef enum LG_RecoveryError LG_RECOVERY_ERR_HELPER_UNAVAILABLE, LG_RECOVERY_ERR_TOPOLOGY_FAILED, LG_RECOVERY_ERR_NO_FALLBACK_DISPLAY, + LG_RECOVERY_ERR_BUSY, + LG_RECOVERY_ERR_CAPACITY, } LG_RecoveryError; @@ -175,7 +177,10 @@ typedef uint32_t LG_TransportFrameFlags; typedef struct LG_TransportFrameTiming { - bool valid; /* producer fields below are available and coherent */ + /* The producer fields through readyLeadTime are available and coherent. */ + bool valid; + /* receiveTime and prepareTime are available and coherent. */ + bool providerValid; bool phaseValid; uint32_t scheduleGeneration; uint32_t scheduleEpoch; @@ -186,6 +191,10 @@ typedef struct LG_TransportFrameTiming uint64_t readyTime; uint64_t holdTime; uint64_t readyLeadTime; + /* Non-overlapping work performed while acquiring and preparing the returned + * payload. These do not include status callbacks or renderer import time. */ + uint64_t receiveTime; + uint64_t prepareTime; } LG_TransportFrameTiming; @@ -219,6 +228,9 @@ LG_TransportFrameFormat; typedef struct LG_TransportFrame { uint64_t serial; + /* When setStatusListener is present, matches LG_VideoComponentStatus::epoch + * for the frame endpoint which returned this payload; otherwise may be 0. */ + uint64_t epoch; uint64_t timestamp; uint32_t scheduleEpoch; uint32_t scheduleDeadlineSerial; @@ -253,6 +265,10 @@ typedef uint32_t LG_TransportPointerFlags; typedef struct LG_TransportPointer { + /* When setStatusListener is present, matches LG_VideoComponentStatus::epoch + * for the pointer endpoint which returned this payload; otherwise may be 0. + */ + uint64_t epoch; LG_TransportPointerFlags flags; int16_t x; int16_t y; @@ -268,6 +284,27 @@ typedef struct LG_TransportPointer } LG_TransportPointer; +typedef struct LG_VideoComponentStatus +{ + bool available; + /* Nonzero while available and changed whenever the endpoint is replaced or + * changes availability. */ + uint64_t epoch; + /* LG_TRANSPORT_OK while available; otherwise the latest observed cause. */ + LG_TransportStatus reason; +} +LG_VideoComponentStatus; + +typedef struct LG_VideoStatus +{ + LG_VideoComponentStatus frame; + LG_VideoComponentStatus pointer; +} +LG_VideoStatus; + +typedef void (*LG_VideoStatusFn)(void * opaque, + const LG_VideoStatus * status); + typedef enum LG_TransportControlType { LG_TRANSPORT_CONTROL_SET_CURSOR_POS, @@ -314,6 +351,16 @@ typedef uint64_t LG_TransportControlToken; typedef struct LG_FrameOps { + /* Registration is scoped to a connected session and must synchronously + * report the current status after releasing component state locks. Frame and + * pointer epochs advance independently whenever their endpoint is + * replaced or changes availability. Passing NULL unregisters the listener + * and synchronously quiesces its callbacks. Callbacks must be serialized. + * Providers without a listener are assumed to keep every component they + * expose available for the session. */ + void (*setStatusListener)(LG_Transport * transport, + LG_VideoStatusFn callback, void * callbackOpaque); + bool (*supportsDMA)(LG_Transport * transport); bool (*attachRenderer)(LG_Transport * transport, const LG_RendererInterop * interop); @@ -327,7 +374,7 @@ typedef struct LG_FrameOps void (*getFrameTiming)(LG_Transport * transport, const LG_TransportFrame * frame, LG_TransportFrameTiming * timing); void (*releaseFrame)(LG_Transport * transport, LG_TransportFrame * frame); - /* Optional, thread-safe cancellation of a blocking nextFrame call. This + /* Required, thread-safe cancellation of a blocking nextFrame call. This * does not release a frame already returned to the consumer. */ void (*cancelFrameWait)(LG_Transport * transport); /* Called by the frame consumer as it exits. A backend may release transient @@ -335,11 +382,14 @@ typedef struct LG_FrameOps * restarts. */ void (*stopFrame)(LG_Transport * transport); + /* Pointer operations are optional. When absent, pointer.available must be + * false for providers implementing the status listener. */ LG_TransportStatus (*nextPointer)(LG_Transport * transport, LG_TransportPointer * pointer); void (*releasePointer)(LG_Transport * transport, LG_TransportPointer * pointer); - /* Optional, thread-safe cancellation of a blocking nextPointer call. */ + /* Required whenever nextPointer is present; thread-safe cancellation of a + * blocking nextPointer call. */ void (*cancelPointerWait)(LG_Transport * transport); /* Called by the pointer consumer as it exits. A backend may release * transient stream resources; nextPointer must reacquire them when the @@ -368,7 +418,12 @@ typedef struct LG_SwSurfaceOps bool (*attach)(LG_Transport * transport, const LG_SwSurfaceEventOps * events, void * opaque); void (*detach)(LG_Transport * transport); + /* Activation may wait while the requested component becomes available. + * Deactivation must complete promptly without cancellation. */ bool (*setActive)(LG_Transport * transport, bool active); + /* Required. Interrupts a pending setActive call and causes it to return + * promptly. Safe to invoke repeatedly from another thread. */ + void (*cancelPending)(LG_Transport * transport); } LG_SwSurfaceOps; @@ -400,8 +455,8 @@ typedef struct LG_TransportOps LG_TransportStatus (*connect)(LG_Transport * transport, LG_TransportSession * session); - /* Optional cancellable form used by transports which may wait while - * establishing a session. */ + /* Required session entry point. The callback must be observed promptly + * while establishing a session, and cancellation must bound this call. */ LG_TransportStatus (*connectCancellable)(LG_Transport * transport, LG_TransportSession * session, LG_TransportCancelledFn cancelled, void * opaque); diff --git a/client/src/app.c b/client/src/app.c index 4887b934..93a4ec00 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -27,6 +27,7 @@ #include "render_queue.h" #include "evdev.h" #include "input.h" +#include "sw_surface.h" #include "kb.h" @@ -59,6 +60,13 @@ extern _Atomic(enum RunState) p_appState; +static bool swSurfaceActivationCancelled(void * opaque) +{ + (void)opaque; + return app_getState() != APP_STATE_RUNNING || + atomic_load_explicit(&g_state.stopVideo, memory_order_acquire); +} + enum RunState app_getState(void) { return atomic_load_explicit(&p_appState, memory_order_acquire); @@ -670,6 +678,12 @@ void app_handleRenderEvent(const uint64_t timeUs) app_invalidateWindow(false); } +void app_handleFramePresented(uint64_t frameToken, uint64_t presentTime, + bool valid) +{ + main_framePresented(frameToken, presentTime, valid); +} + void app_setFullscreen(bool fs) { g_state.ds->setFullscreen(fs); @@ -1203,11 +1217,19 @@ void app_stopVideo(bool stop) if (stop) { + if (g_state.fallback) + lgTransportFallback_requestVideoActive(g_state.fallback, false); + if (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME) + { core_stopVideoThreads(); + /* A status callback may have requested fallback before it quiesced. */ + if (g_state.fallback) + lgTransportFallback_requestVideoActive(g_state.fallback, false); + } else - g_state.videoOps->swSurface->setActive( - g_state.transport.handle, false); + lgSwSurface_setActive(g_state.videoOps->swSurface, + g_state.transport.handle, false, NULL, NULL); } else { @@ -1217,14 +1239,19 @@ void app_stopVideo(bool stop) { atomic_store_explicit( &g_state.stopVideo, true, memory_order_release); + if (g_state.fallback) + lgTransportFallback_requestVideoActive(g_state.fallback, false); app_alert(LG_ALERT_ERROR, "Failed to enable the video stream"); } + else + main_videoStreamEnabled(); } else { - g_state.videoOps->swSurface->setActive( - g_state.transport.handle, true); - app_useVideoSource(LG_VIDEO_SOURCE_PRIMARY); + if (lgSwSurface_setActive(g_state.videoOps->swSurface, + g_state.transport.handle, true, + swSurfaceActivationCancelled, NULL)) + app_useVideoSource(LG_VIDEO_SOURCE_PRIMARY); } } } diff --git a/client/src/core.c b/client/src/core.c index c97f1e0d..4b411ffe 100644 --- a/client/src/core.c +++ b/client/src/core.c @@ -627,9 +627,9 @@ static void cancelVideoWaits(void) return; const LG_FrameOps * ops = g_state.videoOps->frame; - if (g_state.frameThread && ops->cancelFrameWait) + if (g_state.frameThread) ops->cancelFrameWait(g_state.transport.handle); - if (g_state.cursorThread && ops->cancelPointerWait) + if (g_state.cursorThread) ops->cancelPointerWait(g_state.transport.handle); } @@ -650,24 +650,26 @@ void core_stopVideoThreads(void) bool core_startVideoThreads(void) { - if (g_state.frameThread && g_state.cursorThread) + const LG_FrameOps * ops = g_state.videoOps->frame; + const bool pointer = ops->nextPointer && ops->releasePointer; + if (g_state.frameThread && (!pointer || g_state.cursorThread)) return true; if (g_state.frameThread || g_state.cursorThread) core_stopVideoThreads(); atomic_store_explicit( &g_state.stopVideoThreads, false, memory_order_release); - if (!lgCreateThread("cursorThread", main_cursorThread, NULL, - &g_state.cursorThread)) - { - DEBUG_ERROR("cursor create thread failed"); - return false; - } - if (!lgCreateThread("frameThread", main_frameThread, NULL, &g_state.frameThread)) { DEBUG_ERROR("frame create thread failed"); + return false; + } + + if (pointer && !lgCreateThread("cursorThread", main_cursorThread, NULL, + &g_state.cursorThread)) + { + DEBUG_ERROR("cursor create thread failed"); core_stopVideoThreads(); return false; } diff --git a/client/src/main.c b/client/src/main.c index d25ca125..e742fcfd 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -70,6 +70,7 @@ #include "evdev.h" #include "frame_scheduler.h" #include "input.h" +#include "sw_surface.h" #ifdef ENABLE_TESTS #include "interface/test_capture.h" @@ -85,6 +86,7 @@ _Static_assert((int)LG_CAPTURE_RGBA32F == (int)LG_TEST_CAPTURE_RGBA32F, #define TRANSPORT_LOST_FALLBACK (1U << 1) #define TRANSPORT_LOST_ALL \ (TRANSPORT_LOST_PRIMARY | TRANSPORT_LOST_FALLBACK) +#define VIDEO_STATUS_ERROR_RETRY_NS 1000000U // forwards static int renderThread(void * unused); @@ -93,6 +95,17 @@ static bool videoSourceInvalidate(LG_VideoSource source); static void videoSourceShowSplashIfNeeded(void); static bool fallbackRequestVideo(void); static void primaryLost(void); +static bool videoComponentPayloadCurrent(const atomic_bool * available, + const atomic_uint_least64_t * epoch, uint64_t payloadEpoch, + bool trackEpoch); +static bool videoPayloadAccept(const atomic_bool * available, + const atomic_uint_least64_t * epoch, uint64_t payloadEpoch, + bool trackEpoch, uint64_t sourceGeneration); +static void videoPayloadRelease(void); +static bool videoFramePayloadAccept(const LG_TransportFrame * frame, + uint64_t currentGeneration, bool trackEpoch, + uint64_t * sourceGeneration, uint64_t * frameSerial, + uint32_t * formatVersion, bool * sourceChanged); static LGEvent *e_startup = NULL; static LGEvent *e_cursorRepaint = NULL; @@ -119,6 +132,20 @@ struct CursorState g_cursor; // this structure is initialized in config.c struct AppParams g_params = { 0 }; +static bool videoStatusRetry(LG_TransportStatus status, + bool statusAware, bool available) +{ + if (status == LG_TRANSPORT_TIMEOUT || + status == LG_TRANSPORT_UNAVAILABLE) + return true; + + if (status != LG_TRANSPORT_ERROR || !statusAware || available) + return false; + + nsleep(VIDEO_STATUS_ERROR_RETRY_NS); + return true; +} + #ifdef ENABLE_TESTS static struct { @@ -243,6 +270,7 @@ struct FrameTimingRecord uint32_t scheduleDeadlineSerial; unsigned readyMask; bool producerValid; + bool providerValid; bool phaseValid; bool transportValid; bool presentValid; @@ -253,6 +281,8 @@ struct FrameTimingRecord uint64_t readyTime; uint64_t holdTime; uint64_t readyLeadTime; + uint64_t receiveTime; + uint64_t providerPrepareTime; uint64_t importTime; uint64_t importWaitTime; uint64_t dispatchTime; @@ -319,6 +349,11 @@ static void frameTimingReset(void) LG_RENDERER_FRAME_TOKEN_NONE, memory_order_release); } +static uint64_t frameTimingAdd(uint64_t lhs, uint64_t rhs) +{ + return rhs > UINT64_MAX - lhs ? UINT64_MAX : lhs + rhs; +} + static struct FrameTimingRecord * frameTimingRecord( LG_RendererFrameToken token) { @@ -326,7 +361,7 @@ static struct FrameTimingRecord * frameTimingRecord( (token - 1) % FRAME_TIMING_RECORD_COUNT]; } -void app_handleFramePresented(uint64_t frameToken, uint64_t presentTime, +void main_framePresented(uint64_t frameToken, uint64_t presentTime, bool valid) { if (!frameToken) @@ -386,7 +421,8 @@ static void frameTimingQueue(LG_RendererFrameToken token, uint64_t frameSerial, { const uint64_t elapsed = queueStart > dispatchStart ? queueStart - dispatchStart : 0; - const uint64_t accounted = importTime + importWaitTime; + const uint64_t accounted = + frameTimingAdd(importTime, importWaitTime); record->importTime = importTime; record->importWaitTime = importWaitTime; record->dispatchTime = elapsed > accounted ? @@ -433,6 +469,9 @@ static void frameTimingFinishFrame(LG_RendererFrameToken token, record->readyTime = timing->readyTime; record->holdTime = timing->holdTime; record->readyLeadTime = timing->readyLeadTime; + record->providerValid = timing->providerValid; + record->receiveTime = timing->receiveTime; + record->providerPrepareTime = timing->prepareTime; if (record->timestamp < timestamp) record->timestamp = timestamp; record->readyMask |= FRAME_TIMING_FRAME_READY; @@ -569,8 +608,13 @@ static void frameTimingPublishReady(void) const uint64_t queueTime = record->prepareStart > record->queueStart ? record->prepareStart - record->queueStart : 0; + const uint64_t providerAccounted = + record->providerValid ? + frameTimingAdd( + record->receiveTime, record->providerPrepareTime) : 0; const uint64_t transportAccounted = - record->importTime + record->dispatchTime + queueTime; + frameTimingAdd(frameTimingAdd(frameTimingAdd(providerAccounted, + record->importTime), record->dispatchTime), queueTime); const uint64_t transportTime = record->readyLeadTime > transportAccounted ? record->readyLeadTime - transportAccounted : 0; @@ -580,6 +624,8 @@ static void frameTimingPublishReady(void) validMask &= ~OVERLAY_FRAME_TIMING_VALID_PRODUCER; if (!record->producerValid || !record->transportValid) validMask &= ~OVERLAY_FRAME_TIMING_VALID_TRANSPORT; + if (!record->providerValid) + validMask &= ~OVERLAY_FRAME_TIMING_VALID_PROVIDER; if (!record->presentValid) validMask &= ~OVERLAY_FRAME_TIMING_VALID_PRESENT; @@ -592,8 +638,10 @@ static void frameTimingPublishReady(void) .ready = record->readyTime * 1e-6f, .hold = record->holdTime * 1e-6f, .transport = transportTime * 1e-6f, - .import = (record->importTime + - (record->producerValid ? 0 : record->importWaitTime)) * 1e-6f, + .receive = record->receiveTime * 1e-6f, + .providerPrepare = record->providerPrepareTime * 1e-6f, + .import = frameTimingAdd(record->importTime, + record->producerValid ? 0 : record->importWaitTime) * 1e-6f, .dispatch = record->dispatchTime * 1e-6f, .queue = queueTime * 1e-6f, .prepare = record->prepareTime * 1e-6f, @@ -752,13 +800,15 @@ static bool queueCursorRedraw(void) const int y = state->cursorY; const int hx = state->cursorHX; const int hy = state->cursorHY; - LG_UNLOCK(g_state.videoSourceLock); - if (!valid) + { + LG_UNLOCK(g_state.videoSourceLock); return false; + } renderQueue_sourceCursorState(renderQueueSource(source), generation, visible, x, y, hx, hy); + LG_UNLOCK(g_state.videoSourceLock); return true; } @@ -1109,8 +1159,8 @@ static int renderThread(void * unused) else if (g_state.videoOps && g_state.videoOps->type == LG_VIDEO_TYPE_SW_SURFACE) { - g_state.videoOps->swSurface->setActive( - g_state.transport.handle, false); + lgSwSurface_setActive(g_state.videoOps->swSurface, + g_state.transport.handle, false, NULL, NULL); g_state.videoOps->swSurface->detach(g_state.transport.handle); } @@ -1125,9 +1175,6 @@ static int renderThread(void * unused) int main_cursorThread(void * unused) { 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]; @@ -1143,7 +1190,10 @@ int main_cursorThread(void * unused) g_state.transport.handle, &pointer); if (status != LG_TRANSPORT_OK) { - if (status == LG_TRANSPORT_TIMEOUT || status == LG_TRANSPORT_UNAVAILABLE) + if (videoStatusRetry(status, + g_state.videoOps->frame->setStatusListener, + atomic_load_explicit( + &g_state.videoPointerAvailable, memory_order_acquire))) { if (!atomic_load_explicit( &g_state.stopVideoThreads, memory_order_acquire) && @@ -1168,11 +1218,22 @@ int main_cursorThread(void * unused) } const bool inputAvailable = lgInput_available(); + if (!videoPayloadAccept( + &g_state.videoPointerAvailable, &g_state.videoPointerEpoch, + pointer.epoch, + g_state.videoOps->frame->setStatusListener != NULL, 0)) + { + g_state.videoOps->frame->releasePointer( + g_state.transport.handle, &pointer); + continue; + } LG_LOCK(g_state.videoSourceLock); - if (atomic_load_explicit(&source->generation, memory_order_acquire) != - sourceGeneration) + const uint64_t sourceGeneration = atomic_load_explicit( + &source->generation, memory_order_acquire); + if (!sourceGeneration) { LG_UNLOCK(g_state.videoSourceLock); + videoPayloadRelease(); g_state.videoOps->frame->releasePointer( g_state.transport.handle, &pointer); continue; @@ -1198,6 +1259,7 @@ int main_cursorThread(void * unused) case CURSOR_TYPE_MASKED_COLOR: cursorType = LG_CURSOR_MASKED_COLOR; break; default: LG_UNLOCK(g_state.videoSourceLock); + videoPayloadRelease(); DEBUG_ERROR("Invalid cursor type"); g_state.videoOps->frame->releasePointer( g_state.transport.handle, &pointer); @@ -1260,8 +1322,6 @@ int main_cursorThread(void * unused) 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, @@ -1278,6 +1338,7 @@ int main_cursorThread(void * unused) if (valid) renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, sourceGeneration, visible, x, y, hx, hy); + LG_UNLOCK(g_state.videoSourceLock); if (sourceApplied) { @@ -1298,6 +1359,7 @@ int main_cursorThread(void * unused) (g_params.mouseRedraw || contentChanged)))) cursorRepaintRequest(); + videoPayloadRelease(); g_state.videoOps->frame->releasePointer( g_state.transport.handle, &pointer); } @@ -1313,9 +1375,7 @@ 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); + uint64_t sourceGeneration = 0; if (g_state.useDMA) DEBUG_INFO("Using DMA buffer support"); @@ -1337,7 +1397,10 @@ int main_frameThread(void * unused) g_state.transport.handle, g_state.useDMA, &frame); if (status != LG_TRANSPORT_OK) { - if (status == LG_TRANSPORT_TIMEOUT || status == LG_TRANSPORT_UNAVAILABLE) + if (videoStatusRetry(status, + g_state.videoOps->frame->setStatusListener, + atomic_load_explicit( + &g_state.videoFrameAvailable, memory_order_acquire))) { if (g_state.lgr->ops.onFramePoll) RENDERER(onFramePoll); @@ -1360,24 +1423,30 @@ int main_frameThread(void * unused) break; } - if (frame.serial == frameSerial && g_state.formatValid && - !frame.scheduleOwner) + const uint64_t currentGeneration = atomic_load_explicit( + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation, + memory_order_acquire); + bool sourceChanged; + if (!videoFramePayloadAccept(&frame, currentGeneration, + g_state.videoOps->frame->setStatusListener != NULL, + &sourceGeneration, &frameSerial, &formatVersion, &sourceChanged)) { - g_state.videoOps->frame->releaseFrame(g_state.transport.handle, &frame); + g_state.videoOps->frame->releaseFrame( + g_state.transport.handle, &frame); continue; } - frameSerial = frame.serial; const uint64_t dispatchStart = nanotime(); const LG_TransportFrameFormat * format = frame.format; if (!format) { + videoPayloadRelease(); DEBUG_ERROR("Transport returned a frame without format metadata"); g_state.videoOps->frame->releaseFrame(g_state.transport.handle, &frame); app_setState(APP_STATE_SHUTDOWN); break; } - const bool formatChanged = + const bool formatChanged = sourceChanged || !g_state.formatValid || format->version != formatVersion; bool rendererSupportsNativeHDR = false; if (formatChanged) @@ -1455,6 +1524,7 @@ int main_frameThread(void * unused) if (invalid) { + videoPayloadRelease(); DEBUG_ERROR("Unsupported frame type"); g_state.videoOps->frame->releaseFrame( g_state.transport.handle, &frame); @@ -1462,6 +1532,9 @@ int main_frameThread(void * unused) break; } + } + if (formatChanged) + { 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, @@ -1472,6 +1545,7 @@ int main_frameThread(void * unused) if (!RENDERER(onFrameFormat, rendererFormat)) { LG_UNLOCK(g_state.lgrLock); + videoPayloadRelease(); DEBUG_ERROR("Renderer failed to configure format"); g_state.videoOps->frame->releaseFrame( g_state.transport.handle, &frame); @@ -1523,13 +1597,13 @@ int main_frameThread(void * unused) { if (formatChanged) LG_UNLOCK(g_state.lgrLock); + videoPayloadRelease(); frameTimingCancel(frameToken); g_state.videoOps->frame->releaseFrame(g_state.transport.handle, &frame); DEBUG_ERROR("Renderer onFrame returned failure"); app_setState(APP_STATE_SHUTDOWN); break; } - /* A DMA snapshot can complete on the render thread immediately after it * is signalled below, so sample producer timing while this lease is still * unambiguously owned by the frame thread. */ @@ -1563,10 +1637,17 @@ int main_frameThread(void * unused) memory_order_release); #endif - atomic_store_explicit( - &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].ready, true, - memory_order_release); - app_useVideoSource(LG_VIDEO_SOURCE_PRIMARY); + if (atomic_load_explicit( + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation, + memory_order_acquire) == sourceGeneration && + atomic_load_explicit( + &g_state.videoFrameAvailable, memory_order_acquire)) + { + atomic_store_explicit( + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].ready, true, + memory_order_release); + app_useVideoSource(LG_VIDEO_SOURCE_PRIMARY); + } if (formatChanged) { @@ -1602,6 +1683,7 @@ int main_frameThread(void * unused) } frameTimingFinishFrame(frameToken, &timing); + videoPayloadRelease(); if (!rendererOwnsFrame) g_state.videoOps->frame->releaseFrame(g_state.transport.handle, &frame); @@ -1741,9 +1823,251 @@ static void videoSourceClearCursor(LG_VideoSource source) 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)); + LG_UNLOCK(g_state.videoSourceLock); +} + +static bool videoComponentStatusChanged(atomic_bool * available, + atomic_uint_least64_t * epoch, atomic_int * reason, + const LG_VideoComponentStatus * status) +{ + /* Close the component before changing epochs so a consumer can never pair + * a newly available state with a payload from the prior endpoint. */ + const bool oldAvailable = atomic_exchange_explicit( + available, false, memory_order_acq_rel); + const uint64_t oldEpoch = atomic_exchange_explicit( + epoch, status->epoch, memory_order_acq_rel); + atomic_store_explicit(reason, status->reason, memory_order_release); + atomic_store_explicit( + available, status->available, memory_order_release); + return oldAvailable != status->available || + oldEpoch != status->epoch; +} + +static bool videoComponentPayloadCurrent(const atomic_bool * available, + const atomic_uint_least64_t * epoch, uint64_t payloadEpoch, + bool trackEpoch) +{ + if (!atomic_load_explicit(available, memory_order_acquire)) + return false; + return !trackEpoch || (payloadEpoch && atomic_load_explicit( + epoch, memory_order_acquire) == payloadEpoch); +} + +/* Success retains the payload gate. The caller must release it after every + * externally visible effect derived from the accepted payload is complete. */ +static bool videoPayloadAccept(const atomic_bool * available, + const atomic_uint_least64_t * epoch, uint64_t payloadEpoch, + bool trackEpoch, uint64_t sourceGeneration) +{ + LG_LOCK(g_state.videoPayloadLock); + const bool current = videoComponentPayloadCurrent( + available, epoch, payloadEpoch, trackEpoch) && + (!sourceGeneration || atomic_load_explicit( + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation, + memory_order_acquire) == sourceGeneration); + if (!current) + LG_UNLOCK(g_state.videoPayloadLock); + return current; +} + +static void videoPayloadRelease(void) +{ + LG_UNLOCK(g_state.videoPayloadLock); +} + +static bool videoFramePayloadAccept(const LG_TransportFrame * frame, + uint64_t currentGeneration, bool trackEpoch, + uint64_t * sourceGeneration, uint64_t * frameSerial, + uint32_t * formatVersion, bool * sourceChanged) +{ + if (!currentGeneration || !videoPayloadAccept( + &g_state.videoFrameAvailable, &g_state.videoFrameEpoch, + frame->epoch, trackEpoch, currentGeneration)) + return false; + + *sourceChanged = *sourceGeneration != currentGeneration; + if (*sourceChanged) + { + *sourceGeneration = currentGeneration; + *frameSerial = 0; + *formatVersion = 0; + } + + if (!*sourceChanged && frame->serial == *frameSerial && + g_state.formatValid && !frame->scheduleOwner) + { + videoPayloadRelease(); + return false; + } + + *frameSerial = frame->serial; + return true; +} + +static void primaryVideoStatusChanged(void * opaque, + const LG_VideoStatus * status) +{ + (void)opaque; + if (!status || !atomic_load_explicit( + &g_state.videoStatusActive, memory_order_acquire) || + app_getState() != APP_STATE_RUNNING) + return; + + LG_LOCK(g_state.videoPayloadLock); + if (!atomic_load_explicit( + &g_state.videoStatusActive, memory_order_acquire) || + app_getState() != APP_STATE_RUNNING) + { + LG_UNLOCK(g_state.videoPayloadLock); + return; + } + + const bool frameWasAvailable = atomic_load_explicit( + &g_state.videoFrameAvailable, memory_order_acquire); + const bool first = !atomic_exchange_explicit( + &g_state.videoStatusKnown, true, memory_order_acq_rel); + const bool frameChanged = videoComponentStatusChanged( + &g_state.videoFrameAvailable, &g_state.videoFrameEpoch, + &g_state.videoFrameReason, &status->frame) || first; + const bool pointerChanged = videoComponentStatusChanged( + &g_state.videoPointerAvailable, &g_state.videoPointerEpoch, + &g_state.videoPointerReason, &status->pointer) || first; + + if (pointerChanged) + videoSourceClearCursor(LG_VIDEO_SOURCE_PRIMARY); + + if (frameChanged) + { + const uint64_t sourceGeneration = atomic_load_explicit( + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation, + memory_order_acquire); + if (!status->frame.available || frameWasAvailable || !sourceGeneration) + { + videoSourceInvalidate(LG_VIDEO_SOURCE_PRIMARY); + /* Keep an inactive generation while frames are unavailable so an + * independent pointer component can continue updating its retained + * state. */ + videoSourceBegin(LG_VIDEO_SOURCE_PRIMARY); + } + } + LG_UNLOCK(g_state.videoPayloadLock); + + if (frameChanged) + g_state.videoOps->frame->cancelFrameWait(g_state.transport.handle); + if (pointerChanged && g_state.videoOps->frame->nextPointer) + g_state.videoOps->frame->cancelPointerWait(g_state.transport.handle); + + if (!frameChanged) + return; + + if (status->frame.available) + return; + + if (atomic_load_explicit(&g_state.stopVideo, memory_order_acquire)) + return; + + const bool fallbackReady = fallbackRequestVideo(); + if (atomic_load_explicit(&g_state.stopVideo, memory_order_acquire)) + { + if (g_state.fallback) + lgTransportFallback_requestVideoActive(g_state.fallback, false); + return; + } + + if (!fallbackReady) + videoSourceShowSplashIfNeeded(); +} + +static void primaryVideoStatusStart(void) +{ + if (!g_state.videoOps || g_state.videoOps->type != LG_VIDEO_TYPE_FRAME) + return; + + atomic_store_explicit( + &g_state.videoFrameAvailable, false, memory_order_relaxed); + atomic_store_explicit( + &g_state.videoPointerAvailable, false, memory_order_relaxed); + atomic_store_explicit( + &g_state.videoStatusKnown, false, memory_order_relaxed); + atomic_store_explicit( + &g_state.videoFrameEpoch, 0, memory_order_relaxed); + atomic_store_explicit( + &g_state.videoPointerEpoch, 0, memory_order_relaxed); + atomic_store_explicit( + &g_state.videoFrameReason, LG_TRANSPORT_UNAVAILABLE, + memory_order_relaxed); + atomic_store_explicit( + &g_state.videoPointerReason, LG_TRANSPORT_UNAVAILABLE, + memory_order_relaxed); + atomic_store_explicit( + &g_state.videoStatusActive, true, memory_order_release); + + if (g_state.videoOps->frame->setStatusListener) + { + g_state.videoOps->frame->setStatusListener(g_state.transport.handle, + primaryVideoStatusChanged, NULL); + return; + } + + const LG_VideoStatus status = + { + .frame = + { + .available = true, + .epoch = 1, + .reason = LG_TRANSPORT_OK, + }, + .pointer = + { + .available = g_state.videoOps->frame->nextPointer != NULL, + .epoch = g_state.videoOps->frame->nextPointer ? 1 : 0, + .reason = g_state.videoOps->frame->nextPointer ? + LG_TRANSPORT_OK : LG_TRANSPORT_UNAVAILABLE, + }, + }; + primaryVideoStatusChanged(NULL, &status); +} + +static void primaryVideoStatusStop(void) +{ + if (!atomic_exchange_explicit( + &g_state.videoStatusActive, false, memory_order_acq_rel)) + return; + + if (g_state.videoOps && g_state.videoOps->type == LG_VIDEO_TYPE_FRAME && + g_state.videoOps->frame->setStatusListener) + g_state.videoOps->frame->setStatusListener( + g_state.transport.handle, NULL, NULL); + + LG_LOCK(g_state.videoPayloadLock); + atomic_store_explicit( + &g_state.videoFrameAvailable, false, memory_order_release); + atomic_store_explicit( + &g_state.videoPointerAvailable, false, memory_order_release); + atomic_store_explicit( + &g_state.videoFrameReason, LG_TRANSPORT_DISCONNECTED, + memory_order_release); + atomic_store_explicit( + &g_state.videoPointerReason, LG_TRANSPORT_DISCONNECTED, + memory_order_release); + atomic_store_explicit( + &g_state.videoStatusKnown, false, memory_order_release); + LG_UNLOCK(g_state.videoPayloadLock); +} + +void main_videoStreamEnabled(void) +{ + if (!g_state.videoOps || g_state.videoOps->type != LG_VIDEO_TYPE_FRAME || + !g_state.videoOps->frame->setStatusListener || + !atomic_load_explicit( + &g_state.videoStatusKnown, memory_order_acquire) || + atomic_load_explicit( + &g_state.videoFrameAvailable, memory_order_acquire)) + return; + + if (!fallbackRequestVideo()) + videoSourceShowSplashIfNeeded(); } static void videoSourceReject(void * opaque, RenderQueueSource queueSource, @@ -1914,10 +2238,10 @@ static void videoSourceApplied(void * opaque, RenderQueueSource queueSource, app_refreshVideoSource(); } -static bool swSurfaceEventAdmitted(LG_VideoSource source) +static bool swSurfaceEventAccepted(LG_VideoSource source) { return source != LG_VIDEO_SOURCE_FALLBACK || - lgTransportFallback_admitted(g_state.fallback); + lgTransportFallback_acceptsVideoEvents(g_state.fallback); } static void swSurfaceConfigure(LG_VideoSource source, @@ -1926,7 +2250,7 @@ static void swSurfaceConfigure(LG_VideoSource source, struct VideoSourceState * state = &g_state.videoSource[source]; uint64_t generation; LG_LOCK(g_state.videoSourceLock); - if (!swSurfaceEventAdmitted(source)) + if (!swSurfaceEventAccepted(source)) { LG_UNLOCK(g_state.videoSourceLock); return; @@ -1984,12 +2308,12 @@ static void swSurfaceDrawFill(LG_VideoSource source, int x, int y, int width, int height, uint32_t color) { LG_LOCK(g_state.videoSourceLock); - const bool admitted = swSurfaceEventAdmitted(source); - const uint64_t generation = admitted ? atomic_load_explicit( + const bool accepted = swSurfaceEventAccepted(source); + const uint64_t generation = accepted ? atomic_load_explicit( &g_state.videoSource[source].generation, memory_order_acquire) : 0; LG_UNLOCK(g_state.videoSourceLock); - if (!admitted) + if (!accepted) return; renderQueue_sourceSwSurfaceDrawFill(renderQueueSource(source), generation, @@ -2000,12 +2324,12 @@ static void swSurfaceDrawBitmap(LG_VideoSource source, bool topDown, int x, int y, int width, int height, int stride, const void * data) { LG_LOCK(g_state.videoSourceLock); - const bool admitted = swSurfaceEventAdmitted(source); - const uint64_t generation = admitted ? atomic_load_explicit( + const bool accepted = swSurfaceEventAccepted(source); + const uint64_t generation = accepted ? atomic_load_explicit( &g_state.videoSource[source].generation, memory_order_acquire) : 0; LG_UNLOCK(g_state.videoSourceLock); - if (!admitted) + if (!accepted) return; renderQueue_sourceSwSurfaceDrawBitmap(renderQueueSource(source), generation, @@ -2039,7 +2363,7 @@ static void swSurfacePointer(LG_VideoSource source, const bool drawCursor = source != LG_VIDEO_SOURCE_PRIMARY || g_cursor.draw || !lgInput_available(); LG_LOCK(g_state.videoSourceLock); - if (!swSurfaceEventAdmitted(source)) + if (!swSurfaceEventAccepted(source)) { LG_UNLOCK(g_state.videoSourceLock); return; @@ -2163,7 +2487,9 @@ static void swSurfaceEventConfigure(void * opaque, static void swSurfaceEventDestroy(void * opaque) { - swSurfaceDestroy(swSurfaceSource(opaque)); + const LG_VideoSource source = swSurfaceSource(opaque); + if (swSurfaceEventAccepted(source)) + swSurfaceDestroy(source); } static void swSurfaceEventDrawFill(void * opaque, int x, int y, @@ -2196,14 +2522,29 @@ static const LG_SwSurfaceEventOps swSurfaceEvents = static bool fallbackRequestVideo(void) { - if (!g_state.fallback) + if (!g_state.fallback || atomic_load_explicit( + &g_state.stopVideo, memory_order_acquire)) return false; lgTransportFallback_requestVideoActive(g_state.fallback, true); - if (!lgTransportFallback_admitted(g_state.fallback)) + if (atomic_load_explicit(&g_state.stopVideo, memory_order_acquire)) + { + lgTransportFallback_requestVideoActive(g_state.fallback, false); + return false; + } + + if (!lgTransportFallback_usable(g_state.fallback) || + !lgTransportFallback_ready(g_state.fallback)) return false; - return app_useVideoSource(LG_VIDEO_SOURCE_FALLBACK); + const bool active = app_useVideoSource(LG_VIDEO_SOURCE_FALLBACK); + if (atomic_load_explicit(&g_state.stopVideo, memory_order_acquire)) + { + lgTransportFallback_requestVideoActive(g_state.fallback, false); + return false; + } + + return active; } static void fallbackConnected(void * opaque, @@ -2235,6 +2576,23 @@ static void fallbackDisconnected(void * opaque) videoSourceShowSplashIfNeeded(); } +static void fallbackVideoStateChanged(void * opaque, bool ready) +{ + (void)opaque; + if (app_getState() == APP_STATE_SHUTDOWN) + return; + + if (ready && lgTransportFallback_videoRequested(g_state.fallback)) + { + fallbackRequestVideo(); + return; + } + + swSurfaceDestroy(LG_VIDEO_SOURCE_FALLBACK); + videoSourceClearCursor(LG_VIDEO_SOURCE_FALLBACK); + app_refreshVideoSource(); +} + static void fallbackLost(void * opaque) { (void)opaque; @@ -2248,14 +2606,14 @@ static void fallbackLost(void * opaque) } } -static void fallbackUUIDMismatch(void * opaque, const uint8_t primary[16], +static void fallbackEndpointMismatch(void * opaque, const uint8_t primary[16], const uint8_t fallback[16]) { (void)opaque; (void)primary; (void)fallback; atomic_store_explicit( - &g_state.fallbackUUIDMismatch, true, memory_order_release); + &g_state.fallbackEndpointMismatch, true, memory_order_release); app_invalidateWindow(false); } @@ -2264,7 +2622,8 @@ static const LG_TransportFallbackEventOps fallbackEvents = .connected = fallbackConnected, .lost = fallbackLost, .disconnected = fallbackDisconnected, - .uuidMismatch = fallbackUUIDMismatch, + .videoStateChanged = fallbackVideoStateChanged, + .endpointMismatch = fallbackEndpointMismatch, }; static void primaryLost(void) @@ -2276,7 +2635,7 @@ static void primaryLost(void) for (;;) { unsigned int next = lost | TRANSPORT_LOST_PRIMARY; - if (lgTransportFallback_ready(g_state.fallback)) + if (lgTransportFallback_usable(g_state.fallback)) next &= ~TRANSPORT_LOST_FALLBACK; if (atomic_compare_exchange_weak_explicit(&g_state.transportLost, &lost, next, memory_order_acq_rel, memory_order_acquire)) @@ -2319,7 +2678,7 @@ static void fallbackStop(void) static void fallbackHandleEvents(void) { if (!atomic_exchange_explicit( - &g_state.fallbackUUIDMismatch, false, memory_order_acq_rel)) + &g_state.fallbackEndpointMismatch, false, memory_order_acq_rel)) return; app_msgBox( @@ -2572,6 +2931,12 @@ static const char * recoveryErrorText(LG_RecoveryError error) case LG_RECOVERY_ERR_NO_FALLBACK_DISPLAY: return "No fallback display became active"; + case LG_RECOVERY_ERR_BUSY: + return "A conflicting recovery request is already in progress"; + + case LG_RECOVERY_ERR_CAPACITY: + return "Too many recovery requests are pending"; + case LG_RECOVERY_ERR_UNSUPPORTED: return "Recovery is not supported by the guest driver"; @@ -2919,17 +3284,34 @@ static MsgBoxHandle showSpiceInputHelp(void) struct TransportSessionProbe { - LG_Transport * transport; + LG_Transport * transport; const LG_TransportOps * ops; - LG_TransportSession session; - LG_TransportStatus status; - atomic_bool done; + LG_TransportCancelledFn cancelled; + void * cancelOpaque; + LG_TransportSession session; + LG_TransportStatus status; + atomic_bool done; }; +static bool transportSessionCancelled(void * opaque) +{ + (void)opaque; + return app_getState() != APP_STATE_RUNNING; +} + +static bool primarySurfaceActivationCancelled(void * opaque) +{ + (void)opaque; + return app_getState() != APP_STATE_RUNNING || + atomic_load_explicit(&g_state.stopVideo, memory_order_acquire); +} + static int transportSessionProbe(void * opaque) { struct TransportSessionProbe * probe = opaque; - probe->status = probe->ops->connect(probe->transport, &probe->session); + probe->status = probe->ops->connectCancellable( + probe->transport, &probe->session, + probe->cancelled, probe->cancelOpaque); atomic_store_explicit(&probe->done, true, memory_order_release); return 0; } @@ -3059,18 +3441,26 @@ static int lg_run(void) LG_RecoveryInfo initialRecovery = { 0 }; const bool initialRecoveryValid = recoveryGetInfo(&initialRecovery); - g_state.videoOps = - g_state.transport.ops->getVideoOps(g_state.transport.handle); + g_state.videoOps = g_state.transport.ops->getVideoOps ? + g_state.transport.ops->getVideoOps(g_state.transport.handle) : NULL; 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->frame || + !g_state.videoOps->frame->nextFrame || + !g_state.videoOps->frame->releaseFrame || + !g_state.videoOps->frame->cancelFrameWait || + ((g_state.videoOps->frame->nextPointer != NULL) != + (g_state.videoOps->frame->releasePointer != NULL)) || + (g_state.videoOps->frame->nextPointer && + !g_state.videoOps->frame->cancelPointerWait))) || (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))) + !g_state.videoOps->swSurface->setActive || + !g_state.videoOps->swSurface->cancelPending))) { DEBUG_ERROR("Transport does not provide a usable video source"); return -1; @@ -3092,6 +3482,7 @@ static int lg_run(void) } //setup the render command queue + LG_LOCK_INIT(g_state.videoPayloadLock); LG_LOCK_INIT(g_state.videoSourceLock); LG_LOCK_INIT(g_state.videoSplashLock); renderQueue_init(); @@ -3296,9 +3687,11 @@ restart: } struct TransportSessionProbe probe = { - .transport = g_state.transport.handle, - .ops = g_state.transport.ops, - .done = false, + .transport = g_state.transport.handle, + .ops = g_state.transport.ops, + .cancelled = transportSessionCancelled, + .cancelOpaque = NULL, + .done = false, }; LGThread * probeThread; if (!lgCreateThread("transportSession", transportSessionProbe, &probe, @@ -3490,15 +3883,19 @@ restart: if (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME) { - videoSourceBegin(LG_VIDEO_SOURCE_PRIMARY); + primaryVideoStatusStart(); if (!atomic_load_explicit(&g_state.stopVideo, memory_order_acquire) && !core_startVideoThreads()) + { + primaryVideoStatusStop(); return recoveryExit(&recoveryPrompt, -1); + } } else { - if (!g_state.videoOps->swSurface->setActive( - g_state.transport.handle, true)) + if (!lgSwSurface_setActive(g_state.videoOps->swSurface, + g_state.transport.handle, true, + primarySurfaceActivationCancelled, NULL)) { DEBUG_ERROR("Failed to activate the primary software surface"); return recoveryExit(&recoveryPrompt, -1); @@ -3509,7 +3906,6 @@ restart: while(likely(app_getState() == APP_STATE_RUNNING)) { fallbackHandleEvents(); - if (unlikely(!g_state.transport.ops->sessionValid( g_state.transport.handle))) { @@ -3525,6 +3921,7 @@ restart: } frameScheduler_stop(); + primaryVideoStatusStop(); if (app_getState() == APP_STATE_RESTART) { @@ -3538,8 +3935,8 @@ restart: if (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME) core_stopVideoThreads(); else - g_state.videoOps->swSurface->setActive( - g_state.transport.handle, false); + lgSwSurface_setActive(g_state.videoOps->swSurface, + g_state.transport.handle, false, NULL, NULL); videoSourceInvalidate(LG_VIDEO_SOURCE_PRIMARY); videoSourceClearCursor(LG_VIDEO_SOURCE_PRIMARY); lgInput_dropTransport(); @@ -3651,6 +4048,7 @@ static void lg_shutdown(void) renderQueue_setSourceFns(NULL, NULL, NULL, NULL); renderQueue_free(); + LG_LOCK_FREE(g_state.videoPayloadLock); LG_LOCK_FREE(g_state.videoSourceLock); LG_LOCK_FREE(g_state.videoSplashLock); frameScheduler_free(); diff --git a/client/src/main.h b/client/src/main.h index 933b41a8..11a5ac97 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -101,11 +101,20 @@ struct AppState atomic_int videoSourceRequested; atomic_int videoSourceApplied; atomic_uint_least64_t videoSourceAppliedGeneration; + atomic_bool videoStatusActive; + atomic_bool videoStatusKnown; + atomic_bool videoFrameAvailable; + atomic_bool videoPointerAvailable; + atomic_uint_least64_t videoFrameEpoch; + atomic_uint_least64_t videoPointerEpoch; + atomic_int videoFrameReason; + atomic_int videoPointerReason; + LG_Lock videoPayloadLock; LG_Lock videoSourceLock; LG_Lock videoSplashLock; bool videoGeometryDirty; - atomic_bool fallbackUUIDMismatch; + atomic_bool fallbackEndpointMismatch; atomic_uint transportLost; uint8_t guestUUID[16]; @@ -357,5 +366,8 @@ extern struct AppParams g_params; int main_cursorThread(void * unused); int main_frameThread(void * unused); +void main_videoStreamEnabled(void); +void main_framePresented(uint64_t frameToken, uint64_t presentTime, + bool valid); #define RENDERER(fn, ...) g_state.lgr->ops.fn(g_state.lgr, ##__VA_ARGS__) diff --git a/client/src/overlay/graphs.c b/client/src/overlay/graphs.c index 8b5992b9..54b0c6c0 100644 --- a/client/src/overlay/graphs.c +++ b/client/src/overlay/graphs.c @@ -238,6 +238,8 @@ static const char * const frameTimingLabels[FRAME_TIMING_STAGE_COUNT] = { "Ready", "Hold", "Transport", + "Receive", + "Provider Prep", "Import", "Dispatch", "Queue", @@ -310,6 +312,8 @@ static bool accumulateFrameTimingSample(int index, void * value_, void * udata_) timing->ready, timing->hold, timing->transport, + timing->receive, + timing->providerPrepare, timing->import, timing->dispatch, timing->queue, diff --git a/client/src/overlays.h b/client/src/overlays.h index df118415..91bbb903 100644 --- a/client/src/overlays.h +++ b/client/src/overlays.h @@ -54,6 +54,8 @@ typedef struct OverlayFrameTiming float ready; float hold; float transport; + float receive; + float providerPrepare; float import; float dispatch; float queue; @@ -75,6 +77,8 @@ enum OverlayFrameTimingStage OVERLAY_FRAME_TIMING_READY, OVERLAY_FRAME_TIMING_HOLD, OVERLAY_FRAME_TIMING_TRANSPORT, + OVERLAY_FRAME_TIMING_RECEIVE, + OVERLAY_FRAME_TIMING_PROVIDER_PREPARE, OVERLAY_FRAME_TIMING_IMPORT, OVERLAY_FRAME_TIMING_DISPATCH, OVERLAY_FRAME_TIMING_QUEUE, @@ -94,6 +98,9 @@ enum OverlayFrameTimingStage ((1U << OVERLAY_FRAME_TIMING_TRANSPORT) - 1U) #define OVERLAY_FRAME_TIMING_VALID_TRANSPORT \ (1U << OVERLAY_FRAME_TIMING_TRANSPORT) +#define OVERLAY_FRAME_TIMING_VALID_PROVIDER \ + ((1U << OVERLAY_FRAME_TIMING_RECEIVE) | \ + (1U << OVERLAY_FRAME_TIMING_PROVIDER_PREPARE)) #define OVERLAY_FRAME_TIMING_VALID_PRESENT \ (1U << OVERLAY_FRAME_TIMING_PRESENT) diff --git a/client/src/render_queue.c b/client/src/render_queue.c index 681f914f..474679cf 100644 --- a/client/src/render_queue.c +++ b/client/src/render_queue.c @@ -36,6 +36,7 @@ typedef struct RenderCommand RenderQueueSource source; uint64_t generation; + uint64_t cursorGeneration; uint64_t transitionSerial; enum @@ -47,6 +48,7 @@ typedef struct RenderCommand CURSOR_OP_IMAGE, CURSOR_OP_COLOR_TRANSFORM, CURSOR_OP_WHITE_LEVEL, + CURSOR_OP_CLEAR, SOURCE_OP_TRANSITION, } op; @@ -172,6 +174,7 @@ static bool l_rendererSupportsNativeHDR; static LG_RendererFormat l_surfaceFormat; static _Atomic(uint64_t) l_sourceGeneration[RENDER_QUEUE_SOURCE_COUNT]; +static _Atomic(uint64_t) l_cursorGeneration[RENDER_QUEUE_SOURCE_COUNT]; static _Atomic(uint64_t) l_transitionSerial; static LG_Lock l_sourceLock; static LG_Lock l_transitionLock; @@ -257,6 +260,15 @@ static bool transitionCommand(const RenderCommand * cmd) cmd->op == SW_SURFACE_OP_CONFIGURE_TRANSITION; } +static bool cursorCommand(const RenderCommand * cmd) +{ + return cmd->op == CURSOR_OP_STATE || + cmd->op == CURSOR_OP_IMAGE || + cmd->op == CURSOR_OP_COLOR_TRANSFORM || + cmd->op == CURSOR_OP_WHITE_LEVEL || + cmd->op == CURSOR_OP_CLEAR; +} + static bool commandValid(const RenderCommand * cmd) { if (transitionCommand(cmd) && @@ -266,8 +278,15 @@ static bool commandValid(const RenderCommand * cmd) if (cmd->source == RENDER_QUEUE_SOURCE_NONE) return cmd->op == SOURCE_OP_TRANSITION; + if (!sourceValid(cmd->source)) + return false; - return generationValid(cmd->source, cmd->generation); + const bool cursorCurrent = !cursorCommand(cmd) || atomic_load_explicit( + &l_cursorGeneration[cmd->source], memory_order_acquire) == + cmd->cursorGeneration; + if (cmd->op == CURSOR_OP_CLEAR) + return cursorCurrent; + return generationValid(cmd->source, cmd->generation) && cursorCurrent; } static uint64_t swSurfaceDamageArea(const FrameDamageRect * rect) @@ -469,6 +488,8 @@ static void setCommandSource(RenderCommand * cmd, RenderQueueSource source, { cmd->source = source; cmd->generation = generation; + cmd->cursorGeneration = sourceValid(source) ? atomic_load_explicit( + &l_cursorGeneration[source], memory_order_acquire) : 0; } static bool copyCursorImage(RenderCommand * cmd, const void * data) @@ -567,6 +588,7 @@ void renderQueue_init(void) for (int i = 0; i < RENDER_QUEUE_SOURCE_COUNT; ++i) { atomic_store_explicit(&l_sourceGeneration[i], 0, memory_order_relaxed); + atomic_store_explicit(&l_cursorGeneration[i], 1, memory_order_relaxed); LG_LOCK_INIT(l_swSurface[i].lock); } atomic_store_explicit(&l_transitionSerial, 0, memory_order_relaxed); @@ -686,9 +708,26 @@ void renderQueue_sourceClearCursor(RenderQueueSource source) return; LG_LOCK(l_sourceLock); + uint64_t cursorGeneration = atomic_load_explicit( + &l_cursorGeneration[source], memory_order_relaxed) + 1; + if (!cursorGeneration) + ++cursorGeneration; + atomic_store_explicit(&l_cursorGeneration[source], cursorGeneration, + memory_order_release); free(l_cursor[source].data); memset(&l_cursor[source], 0, sizeof(l_cursor[source])); + const uint64_t sourceGeneration = atomic_load_explicit( + &l_sourceGeneration[source], memory_order_acquire); + RenderCommand * cmd = malloc(sizeof(*cmd)); + bool wake = false; + if (cmd) + { + setCommandSource(cmd, source, sourceGeneration); + cmd->op = CURSOR_OP_CLEAR; + wake = queueCommand(cmd, RENDER_QUEUE_INVALIDATE_PARTIAL); + } LG_UNLOCK(l_sourceLock); + wakeQueue(wake); } static bool configureSwSurface(RenderQueueSource source, @@ -1454,6 +1493,19 @@ bool renderQueue_process(void) break; } + case CURSOR_OP_CLEAR: + if (l_appliedSource == cmd->source) + { + RENDERER(onMouseEvent, false, 0, 0, 0, 0); + if (g_state.lgr->ops.onMouseColorTransform) + g_state.lgr->ops.onMouseColorTransform( + g_state.lgr, &l_identityColorTransform); + if (g_state.lgr->ops.onMouseWhiteLevel) + g_state.lgr->ops.onMouseWhiteLevel( + g_state.lgr, LG_SDR_WHITE_LEVEL_DEFAULT); + } + break; + case SOURCE_OP_TRANSITION: { const bool swSurface = diff --git a/client/src/sw_surface.c b/client/src/sw_surface.c new file mode 100644 index 00000000..ca6aaece --- /dev/null +++ b/client/src/sw_surface.c @@ -0,0 +1,84 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + */ + +#include "sw_surface.h" + +#include "common/debug.h" +#include "common/thread.h" +#include "common/time.h" + +#include + +struct SwSurfaceCall +{ + const LG_SwSurfaceOps * ops; + LG_Transport * transport; + bool active; + bool result; + atomic_bool done; +}; + +static int swSurfaceCallThread(void * opaque) +{ + struct SwSurfaceCall * call = opaque; + call->result = call->ops->setActive(call->transport, call->active); + atomic_store_explicit(&call->done, true, memory_order_release); + return 0; +} + +bool lgSwSurface_setActive(const LG_SwSurfaceOps * ops, + LG_Transport * transport, bool active, + LG_TransportCancelledFn cancelled, void * opaque) +{ + if (!ops || !ops->setActive || !ops->cancelPending || !transport) + return false; + + struct SwSurfaceCall call = + { + .ops = ops, + .transport = transport, + .active = active, + }; + atomic_init(&call.done, false); + + LGThread * thread; + if (!lgCreateThread("surfaceState", swSurfaceCallThread, &call, &thread)) + return false; + + bool cancelRequested = false; + while (!atomic_load_explicit(&call.done, memory_order_acquire)) + { + if (active && cancelled && cancelled(opaque)) + { + cancelRequested = true; + ops->cancelPending(transport); + } + nsleep(1000000U); + } + + if (active && cancelled && cancelled(opaque)) + cancelRequested = true; + + if (!lgJoinThread(thread, NULL)) + { + DEBUG_ERROR("Failed to join software surface state worker"); + return false; + } + if (active && cancelRequested) + { + /* The backend may complete successfully at the same instant cancellation + * is observed. Leave it inactive before reporting the cancelled result. */ + if (!ops->setActive(transport, false)) + DEBUG_ERROR("Failed to deactivate a cancelled software surface"); + return false; + } + return call.result; +} diff --git a/client/src/sw_surface.h b/client/src/sw_surface.h new file mode 100644 index 00000000..18989e53 --- /dev/null +++ b/client/src/sw_surface.h @@ -0,0 +1,24 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + */ + +#ifndef _H_LG_CLIENT_SW_SURFACE_ +#define _H_LG_CLIENT_SW_SURFACE_ + +#include "interface/transport.h" + +/* Runs setActive on a worker and joins it before returning. Cancellation is + * observed only while activating; deactivation is allowed to complete unless + * an owner invokes cancelPending from another thread. */ +bool lgSwSurface_setActive(const LG_SwSurfaceOps * ops, + LG_Transport * transport, bool active, + LG_TransportCancelledFn cancelled, void * opaque); + +#endif diff --git a/client/src/transport.c b/client/src/transport.c index ceaa6bd4..e13e8244 100644 --- a/client/src/transport.c +++ b/client/src/transport.c @@ -21,6 +21,8 @@ #include "interface/transport.h" #include "dynamic/transports.h" +#include "common/debug.h" + #include void lgTransport_setup(void) @@ -51,10 +53,39 @@ bool lgTransport_create(const char * name, LG_TransportInstance * instance) if (strcmp(LG_Transports[i]->name, name) != 0) continue; - if (!LG_Transports[i]->create(&instance->handle)) + const LG_TransportOps * ops = LG_Transports[i]; + if (!ops->create || !ops->destroy || !ops->connectCancellable || + !ops->disconnect || !ops->sessionValid) + { + DEBUG_ERROR("Transport %s has an incomplete session interface", name); + return false; + } + + if (!ops->create(&instance->handle)) return false; - instance->ops = LG_Transports[i]; + const LG_VideoOps * video = ops->getVideoOps ? + ops->getVideoOps(instance->handle) : NULL; + const bool badFrame = video && video->type == LG_VIDEO_TYPE_FRAME && + (!video->frame || !video->frame->nextFrame || + !video->frame->releaseFrame || !video->frame->cancelFrameWait || + ((video->frame->nextPointer != NULL) != + (video->frame->releasePointer != NULL)) || + (video->frame->nextPointer && !video->frame->cancelPointerWait)); + const bool badSurface = video && video->type == LG_VIDEO_TYPE_SW_SURFACE && + (!video->swSurface || !video->swSurface->attach || + !video->swSurface->detach || !video->swSurface->setActive || + !video->swSurface->cancelPending); + if (video && (badFrame || badSurface || + (video->type != LG_VIDEO_TYPE_FRAME && + video->type != LG_VIDEO_TYPE_SW_SURFACE))) + { + DEBUG_ERROR("Transport %s has an incomplete video interface", name); + ops->destroy(&instance->handle); + return false; + } + + instance->ops = ops; return true; } diff --git a/client/src/transport_fallback.c b/client/src/transport_fallback.c index 3873f46a..c3a71a2c 100644 --- a/client/src/transport_fallback.c +++ b/client/src/transport_fallback.c @@ -23,6 +23,7 @@ #include "audio.h" #include "clipboard.h" #include "input.h" +#include "sw_surface.h" #include "common/debug.h" #include "common/event.h" @@ -44,6 +45,7 @@ struct LG_TransportFallback LG_SwSurfaceEventOps surfaceEvents; void * surfaceOpaque; + bool surfaceRequested; LG_TransportFallbackEventOps eventOps; void * eventOpaque; @@ -55,7 +57,8 @@ struct LG_TransportFallback atomic_bool stop; atomic_bool ready; - atomic_bool admitted; + atomic_bool usable; + atomic_bool videoAcceptingEvents; LG_TransportInstance transport; LG_TransportSession session; @@ -80,28 +83,48 @@ struct LG_TransportFallback static bool applyVideoRequest(LG_TransportFallback * fallback); +struct VideoCallCancellation +{ + LG_TransportFallback * fallback; + uint64_t connectionSerial; + bool requested; +}; + static bool connectCancelled(void * opaque) { const LG_TransportFallback * fallback = opaque; return atomic_load_explicit(&fallback->stop, memory_order_acquire); } -static bool uuidMismatchLocked(const LG_TransportFallback * fallback) +static bool videoCallCancelled(void * opaque) +{ + const struct VideoCallCancellation * cancellation = opaque; + LG_TransportFallback * fallback = cancellation->fallback; + LG_LOCK_SHARED(fallback->lock); + const bool cancelled = atomic_load_explicit( + &fallback->stop, memory_order_acquire) || fallback->closing || + fallback->connectionSerial != cancellation->connectionSerial || + fallback->videoRequested != cancellation->requested; + LG_UNLOCK_SHARED(fallback->lock); + return cancelled; +} + +static bool endpointMismatchLocked(const LG_TransportFallback * fallback) { return fallback->primaryUUIDValid && fallback->session.uuidValid && memcmp(fallback->primaryUUID, fallback->session.uuid, sizeof(fallback->primaryUUID)) != 0; } -static bool uuidAdmittedLocked(const LG_TransportFallback * fallback) +static bool endpointMatchesLocked(const LG_TransportFallback * fallback) { - return !uuidMismatchLocked(fallback); + return !endpointMismatchLocked(fallback); } static bool recordMismatchLocked(LG_TransportFallback * fallback, uint8_t primary[16], uint8_t remote[16]) { - if (!uuidMismatchLocked(fallback)) + if (!endpointMismatchLocked(fallback)) return false; const bool duplicate = fallback->mismatchReported && @@ -133,9 +156,8 @@ static void notifyConnected(LG_TransportFallback * fallback) LG_LOCK(fallback->eventLock); LG_LOCK_EXCLUSIVE(fallback->lock); const bool report = - atomic_load_explicit(&fallback->ready, memory_order_acquire) && - atomic_load_explicit(&fallback->admitted, memory_order_acquire) && - uuidAdmittedLocked(fallback) && !fallback->closing && + atomic_load_explicit(&fallback->usable, memory_order_acquire) && + endpointMatchesLocked(fallback) && !fallback->closing && !fallback->disconnectedReported; fallback->connectedReported = report; const LG_TransportSession session = fallback->session; @@ -174,6 +196,26 @@ static void notifyLost(LG_TransportFallback * fallback, bool requested) LG_UNLOCK(fallback->eventLock); } +static void notifyVideoState(LG_TransportFallback * fallback, + uint64_t connectionSerial, bool ready) +{ + if (!fallback->eventOps.videoStateChanged) + return; + + LG_LOCK(fallback->eventLock); + LG_LOCK_SHARED(fallback->lock); + const bool current = fallback->connectionSerial == connectionSerial && + atomic_load_explicit(&fallback->ready, memory_order_acquire) == ready && + (!ready || (atomic_load_explicit( + &fallback->usable, memory_order_acquire) && + !fallback->closing && endpointMatchesLocked(fallback) && + fallback->videoRequested)); + LG_UNLOCK_SHARED(fallback->lock); + if (current) + fallback->eventOps.videoStateChanged(fallback->eventOpaque, ready); + LG_UNLOCK(fallback->eventLock); +} + static void unpublishProviders(LG_TransportFallback * fallback, bool live) { LG_LOCK(fallback->providerLock); @@ -199,11 +241,13 @@ static void unpublishProviders(LG_TransportFallback * fallback, bool live) LG_UNLOCK(fallback->providerLock); } -static void closeVideoAdmission(LG_TransportFallback * fallback) +static void closeConnection(LG_TransportFallback * fallback) { LG_LOCK_EXCLUSIVE(fallback->lock); atomic_store_explicit(&fallback->ready, false, memory_order_release); - atomic_store_explicit(&fallback->admitted, false, memory_order_release); + atomic_store_explicit(&fallback->usable, false, memory_order_release); + atomic_store_explicit( + &fallback->videoAcceptingEvents, false, memory_order_release); fallback->closing = true; LG_UNLOCK_EXCLUSIVE(fallback->lock); } @@ -211,7 +255,7 @@ static void closeVideoAdmission(LG_TransportFallback * fallback) static bool cleanupConnection(LG_TransportFallback * fallback, bool knownDead) { - closeVideoAdmission(fallback); + closeConnection(fallback); LG_LOCK_EXCLUSIVE(fallback->lock); const bool reportDisconnected = fallback->connectedReported; @@ -224,9 +268,11 @@ static bool cleanupConnection(LG_TransportFallback * fallback, if (fallback->attached) { if (live && fallback->videoActive) - fallback->videoOps->swSurface->setActive( - fallback->transport.handle, false); + lgSwSurface_setActive(fallback->videoOps->swSurface, + fallback->transport.handle, false, NULL, NULL); + LG_LOCK_EXCLUSIVE(fallback->lock); fallback->videoActive = false; + LG_UNLOCK_EXCLUSIVE(fallback->lock); fallback->videoOps->swSurface->detach(fallback->transport.handle); fallback->attached = false; } @@ -249,10 +295,10 @@ static bool publishProviders(LG_TransportFallback * fallback) { LG_LOCK(fallback->providerLock); LG_LOCK_SHARED(fallback->lock); - const bool admitted = atomic_load_explicit( - &fallback->admitted, memory_order_acquire) && - uuidAdmittedLocked(fallback) && !fallback->closing; - if (!admitted) + const bool usable = atomic_load_explicit( + &fallback->usable, memory_order_acquire) && + endpointMatchesLocked(fallback) && !fallback->closing; + if (!usable) { LG_UNLOCK_SHARED(fallback->lock); LG_UNLOCK(fallback->providerLock); @@ -283,75 +329,47 @@ static bool publishProviders(LG_TransportFallback * fallback) return true; } -static bool admitConnection(LG_TransportFallback * fallback, +static bool markConnectionUsable(LG_TransportFallback * fallback, bool * reportMismatch, uint8_t primaryUUID[16], uint8_t fallbackUUID[16]) { LG_LOCK_EXCLUSIVE(fallback->lock); - const bool reject = uuidMismatchLocked(fallback); - if (reject) + const bool mismatch = endpointMismatchLocked(fallback); + if (mismatch) *reportMismatch = recordMismatchLocked( fallback, primaryUUID, fallbackUUID); const bool stop = atomic_load_explicit( &fallback->stop, memory_order_acquire); - const bool admitted = !reject && !stop && !fallback->closing; - if (admitted) + const bool usable = !mismatch && !stop && !fallback->closing; + if (usable) atomic_store_explicit( - &fallback->admitted, true, memory_order_release); + &fallback->usable, true, memory_order_release); LG_UNLOCK_EXCLUSIVE(fallback->lock); - return admitted; + return usable; } static bool publishConnection(LG_TransportFallback * fallback, bool * reportMismatch, uint8_t primaryUUID[16], uint8_t fallbackUUID[16]) { - if (!admitConnection(fallback, reportMismatch, + if (!markConnectionUsable(fallback, reportMismatch, primaryUUID, fallbackUUID)) return false; if (!publishProviders(fallback)) return false; - for (;;) - { - LG_LOCK_EXCLUSIVE(fallback->lock); - const bool reject = uuidMismatchLocked(fallback); - if (reject) - *reportMismatch = recordMismatchLocked( - fallback, primaryUUID, fallbackUUID); - const bool admitted = uuidAdmittedLocked(fallback); - const bool stop = atomic_load_explicit( - &fallback->stop, memory_order_acquire); - const bool closing = fallback->closing; - const bool requested = fallback->videoRequested; - LG_UNLOCK_EXCLUSIVE(fallback->lock); - - if (reject || !admitted || stop || closing) - return false; - if (requested == fallback->videoActive) - break; - - if (!fallback->videoOps->swSurface->setActive( - fallback->transport.handle, requested)) - break; - fallback->videoActive = requested; - } - LG_LOCK_EXCLUSIVE(fallback->lock); - const bool finalReject = uuidMismatchLocked(fallback); - if (finalReject) + const bool finalMismatch = endpointMismatchLocked(fallback); + if (finalMismatch) *reportMismatch = recordMismatchLocked( fallback, primaryUUID, fallbackUUID); - const bool finalAdmitted = uuidAdmittedLocked(fallback); + const bool finalMatches = endpointMatchesLocked(fallback); const bool finalStop = atomic_load_explicit( &fallback->stop, memory_order_acquire); - if (!finalReject && finalAdmitted && !finalStop && !fallback->closing) - { + if (!finalMismatch && finalMatches && !finalStop && !fallback->closing) fallback->mismatchReported = false; - atomic_store_explicit(&fallback->ready, true, memory_order_release); - } - const bool published = !finalReject && finalAdmitted && !finalStop && + const bool published = !finalMismatch && finalMatches && !finalStop && !fallback->closing; LG_UNLOCK_EXCLUSIVE(fallback->lock); return published; @@ -367,37 +385,13 @@ static bool connectFallback(LG_TransportFallback * fallback) return false; } - const LG_VideoOps * videoOps = transport.ops->getVideoOps ? - transport.ops->getVideoOps(transport.handle) : NULL; - if (!videoOps || videoOps->type != LG_VIDEO_TYPE_SW_SURFACE || - !videoOps->swSurface || !videoOps->swSurface->attach || - !videoOps->swSurface->detach || !videoOps->swSurface->setActive) - { - DEBUG_ERROR("Fallback transport %s does not provide a software surface", - fallback->transportName); - lgTransport_destroy(&transport); - return false; - } - - if (!videoOps->swSurface->attach(transport.handle, - &fallback->surfaceEvents, fallback->surfaceOpaque)) - { - DEBUG_ERROR("Failed to attach fallback software surface"); - lgTransport_destroy(&transport); - return false; - } - LG_LOCK_EXCLUSIVE(fallback->lock); fallback->transport = transport; - fallback->videoOps = videoOps; - fallback->attached = true; LG_UNLOCK_EXCLUSIVE(fallback->lock); LG_TransportSession session = { 0 }; - const LG_TransportStatus status = transport.ops->connectCancellable ? - transport.ops->connectCancellable(transport.handle, &session, - connectCancelled, fallback) : - transport.ops->connect(transport.handle, &session); + const LG_TransportStatus status = transport.ops->connectCancellable( + transport.handle, &session, connectCancelled, fallback); if (status != LG_TRANSPORT_OK) { @@ -415,6 +409,26 @@ static bool connectFallback(LG_TransportFallback * fallback) ++fallback->connectionSerial; LG_UNLOCK_EXCLUSIVE(fallback->lock); + const LG_VideoOps * videoOps = transport.ops->getVideoOps ? + transport.ops->getVideoOps(transport.handle) : NULL; + bool attached = false; + if (fallback->surfaceRequested && videoOps && + videoOps->type == LG_VIDEO_TYPE_SW_SURFACE && + videoOps->swSurface && videoOps->swSurface->attach && + videoOps->swSurface->detach && videoOps->swSurface->setActive && + videoOps->swSurface->cancelPending) + { + attached = videoOps->swSurface->attach(transport.handle, + &fallback->surfaceEvents, fallback->surfaceOpaque); + if (!attached) + DEBUG_WARN("Failed to attach fallback video source; continuing without it"); + } + + LG_LOCK_EXCLUSIVE(fallback->lock); + fallback->videoOps = attached ? videoOps : NULL; + fallback->attached = attached; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + bool reportMismatch = false; uint8_t primaryUUID[16]; uint8_t fallbackUUID[16]; @@ -422,13 +436,14 @@ static bool connectFallback(LG_TransportFallback * fallback) primaryUUID, fallbackUUID)) { cleanupConnection(fallback, false); - if (reportMismatch && fallback->eventOps.uuidMismatch) - fallback->eventOps.uuidMismatch( + if (reportMismatch && fallback->eventOps.endpointMismatch) + fallback->eventOps.endpointMismatch( fallback->eventOpaque, primaryUUID, fallbackUUID); return false; } notifyConnected(fallback); + applyVideoRequest(fallback); bool lost = false; while (!atomic_load_explicit(&fallback->stop, memory_order_acquire)) @@ -439,14 +454,14 @@ static bool connectFallback(LG_TransportFallback * fallback) applyVideoRequest(fallback); LG_LOCK_EXCLUSIVE(fallback->lock); - const bool reject = uuidMismatchLocked(fallback); - if (reject) + const bool mismatch = endpointMismatchLocked(fallback); + if (mismatch) reportMismatch = recordMismatchLocked( fallback, primaryUUID, fallbackUUID); - const bool admitted = uuidAdmittedLocked(fallback); + const bool matches = endpointMatchesLocked(fallback); const bool closing = fallback->closing; LG_UNLOCK_EXCLUSIVE(fallback->lock); - if (reject || !admitted || closing) + if (mismatch || !matches || closing) break; if (!sessionLive(fallback)) { @@ -458,8 +473,8 @@ static bool connectFallback(LG_TransportFallback * fallback) const bool reportDisconnect = cleanupConnection(fallback, lost); - if (reportMismatch && fallback->eventOps.uuidMismatch) - fallback->eventOps.uuidMismatch( + if (reportMismatch && fallback->eventOps.endpointMismatch) + fallback->eventOps.endpointMismatch( fallback->eventOpaque, primaryUUID, fallbackUUID); const bool reportLost = lost && reportDisconnect && !atomic_load_explicit( &fallback->stop, memory_order_acquire); @@ -500,7 +515,7 @@ bool lgTransportFallback_start(const char * transportName, return false; *result = NULL; - if (!transportName || !*transportName || !surfaceEvents || + if (!transportName || !*transportName || !lgTransport_isValid(transportName)) return false; @@ -513,13 +528,18 @@ bool lgTransportFallback_start(const char * transportName, LG_LOCK_INIT(fallback->eventLock); atomic_init(&fallback->stop, false); atomic_init(&fallback->ready, false); - atomic_init(&fallback->admitted, false); + atomic_init(&fallback->usable, false); + atomic_init(&fallback->videoAcceptingEvents, false); fallback->transportName = strdup(transportName); if (!fallback->transportName) goto fail; - fallback->surfaceEvents = *surfaceEvents; + if (surfaceEvents) + { + fallback->surfaceEvents = *surfaceEvents; + fallback->surfaceRequested = true; + } fallback->surfaceOpaque = surfaceOpaque; if (eventOps) fallback->eventOps = *eventOps; @@ -587,10 +607,17 @@ bool lgTransportFallback_ready(const LG_TransportFallback * fallback) &fallback->ready, memory_order_acquire); } -bool lgTransportFallback_admitted(const LG_TransportFallback * fallback) +bool lgTransportFallback_usable(const LG_TransportFallback * fallback) { return fallback && atomic_load_explicit( - &fallback->admitted, memory_order_acquire); + &fallback->usable, memory_order_acquire); +} + +bool lgTransportFallback_acceptsVideoEvents( + const LG_TransportFallback * fallback) +{ + return fallback && atomic_load_explicit( + &fallback->videoAcceptingEvents, memory_order_acquire); } bool lgTransportFallback_videoRequested( @@ -615,8 +642,8 @@ static bool applyVideoRequest(LG_TransportFallback * fallback) bool requested; LG_LOCK_EXCLUSIVE(fallback->lock); - if (!atomic_load_explicit(&fallback->ready, memory_order_acquire) || - fallback->closing) + if (!atomic_load_explicit(&fallback->usable, memory_order_acquire) || + !fallback->attached || !fallback->videoOps || fallback->closing) { LG_UNLOCK_EXCLUSIVE(fallback->lock); return false; @@ -632,19 +659,49 @@ static bool applyVideoRequest(LG_TransportFallback * fallback) connectionSerial = fallback->connectionSerial; transport = fallback->transport.handle; surfaceOps = fallback->videoOps->swSurface; + atomic_store_explicit( + &fallback->ready, false, memory_order_release); + atomic_store_explicit(&fallback->videoAcceptingEvents, + requested, memory_order_release); LG_UNLOCK_EXCLUSIVE(fallback->lock); - const bool result = surfaceOps->setActive(transport, requested); + const struct VideoCallCancellation cancellation = + { + .fallback = fallback, + .connectionSerial = connectionSerial, + .requested = requested, + }; + const bool result = lgSwSurface_setActive(surfaceOps, transport, + requested, videoCallCancelled, (void *)&cancellation); 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 desiredCurrent = current && + fallback->videoRequested == requested; + const bool usable = desiredCurrent && result && + atomic_load_explicit(&fallback->usable, memory_order_acquire) && + !atomic_load_explicit(&fallback->stop, memory_order_acquire) && + !fallback->closing && endpointMatchesLocked(fallback); + if (current) + { + if (result) + fallback->videoActive = requested; + else if (requested) + fallback->videoActive = false; + const bool ready = usable && requested; + atomic_store_explicit( + &fallback->ready, ready, memory_order_release); + atomic_store_explicit(&fallback->videoAcceptingEvents, + ready, memory_order_release); + } + const bool accepted = usable; const bool retry = current && !fallback->closing && fallback->videoRequested != requested; LG_UNLOCK_EXCLUSIVE(fallback->lock); + if (current) + notifyVideoState(fallback, connectionSerial, accepted && requested); + if (!retry) return accepted; } @@ -662,16 +719,19 @@ void lgTransportFallback_requestVideoActive( lgSignalEvent(fallback->wakeEvent); } -static bool revokeAdmissionLocked(LG_TransportFallback * fallback) +static bool clearUsableLocked(LG_TransportFallback * fallback) { atomic_store_explicit(&fallback->ready, false, memory_order_release); - atomic_store_explicit(&fallback->admitted, false, memory_order_release); + atomic_store_explicit(&fallback->usable, false, memory_order_release); + atomic_store_explicit( + &fallback->videoAcceptingEvents, false, memory_order_release); if (!fallback->connected) return false; + const bool reportDisconnected = fallback->connectedReported; fallback->connectedReported = false; fallback->closing = true; - return true; + return reportDisconnected; } void lgTransportFallback_setPrimaryUUID( @@ -680,7 +740,7 @@ void lgTransportFallback_setPrimaryUUID( if (!fallback || !uuid) return; - bool revoked = false; + bool mismatchTransition = false; bool notifyDisconnect = false; bool reportMismatch = false; uint8_t primaryUUID[16]; @@ -694,10 +754,10 @@ void lgTransportFallback_setPrimaryUUID( { memcpy(fallback->primaryUUID, uuid, sizeof(fallback->primaryUUID)); fallback->primaryUUIDValid = true; - if (uuidMismatchLocked(fallback)) + if (endpointMismatchLocked(fallback)) { - revoked = true; - notifyDisconnect = revokeAdmissionLocked(fallback); + mismatchTransition = true; + notifyDisconnect = clearUsableLocked(fallback); reportMismatch = recordMismatchLocked( fallback, primaryUUID, fallbackUUID); } @@ -706,11 +766,11 @@ void lgTransportFallback_setPrimaryUUID( if (changed) lgSignalEvent(fallback->wakeEvent); - if (revoked) + if (mismatchTransition) unpublishProviders(fallback, false); notifyDisconnected(fallback, notifyDisconnect); - if (reportMismatch && fallback->eventOps.uuidMismatch) - fallback->eventOps.uuidMismatch( + if (reportMismatch && fallback->eventOps.endpointMismatch) + fallback->eventOps.endpointMismatch( fallback->eventOpaque, primaryUUID, fallbackUUID); } diff --git a/client/src/transport_fallback.h b/client/src/transport_fallback.h index 51180760..66382964 100644 --- a/client/src/transport_fallback.h +++ b/client/src/transport_fallback.h @@ -32,10 +32,12 @@ typedef struct LG_TransportFallbackEventOps { /* The session is valid only for the duration of this callback. */ void (*connected)(void * opaque, const LG_TransportSession * session); - /* Called only when an admitted session is unexpectedly lost. */ + /* Called only when a usable session is unexpectedly lost. */ void (*lost)(void * opaque); void (*disconnected)(void * opaque); - void (*uuidMismatch)(void * opaque, const uint8_t primary[16], + /* Reports completion of an optional video state request. */ + void (*videoStateChanged)(void * opaque, bool ready); + void (*endpointMismatch)(void * opaque, const uint8_t primary[16], const uint8_t fallback[16]); } LG_TransportFallbackEventOps; @@ -47,8 +49,13 @@ bool lgTransportFallback_start(const char * transportName, LG_TransportFallback ** result); void lgTransportFallback_stop(LG_TransportFallback ** fallback); +/* ready reports optional video availability; usable reports session services. */ bool lgTransportFallback_ready(const LG_TransportFallback * fallback); -bool lgTransportFallback_admitted(const LG_TransportFallback * fallback); +bool lgTransportFallback_usable(const LG_TransportFallback * fallback); +/* True while video callbacks belong to the current activation attempt or + * confirmed active state. */ +bool lgTransportFallback_acceptsVideoEvents( + const LG_TransportFallback * fallback); bool lgTransportFallback_videoRequested( LG_TransportFallback * fallback); /* The requested state is retained across reconnects. */ diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 0a6c8c43..3c80bc4b 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -4,6 +4,20 @@ add_executable(font-tests font_test.c ../src/font.c ) + +add_executable(sw-surface-tests + sw_surface_test.c + ../src/sw_surface.c +) +target_include_directories(sw-surface-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../src" +) +target_link_libraries(sw-surface-tests + ${EXE_FLAGS} + lg_common +) +add_test(NAME sw-surface-tests COMMAND sw-surface-tests) +set_tests_properties(sw-surface-tests PROPERTIES TIMEOUT 10) target_compile_definitions(font-tests PRIVATE FONT_TEST_FILE="${PROJECT_TOP}/repos/gui/cimgui/imgui/misc/fonts/DroidSans.ttf" ) @@ -427,6 +441,7 @@ endforeach() add_executable(transport-fallback-tests transport_fallback_test.c ../src/transport_fallback.c + ../src/sw_surface.c ) target_include_directories(transport-fallback-tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../src" @@ -444,9 +459,18 @@ set(TRANSPORT_FALLBACK_CASES graceful dead mismatch - revoke + missing-uuid + late-mismatch retry cancel + no-video + attach-fail + active-fail + activation-cancel + activation-request-cancel + deactivation-completes + activation-mismatch + pre-requested-activation ) foreach(name IN LISTS TRANSPORT_FALLBACK_CASES) add_test(NAME transport-fallback-${name} @@ -482,6 +506,7 @@ set(SPICE_CASES ready-drop session-order surface-active + surface-cancel surface-events surface-detach ) @@ -494,6 +519,30 @@ foreach(name IN LISTS SPICE_CASES) ) endforeach() +add_executable(purespice-connect-cancel-tests + purespice_connect_cancel_test.c +) +target_compile_definitions(purespice PRIVATE PURESPICE_TESTING) +target_compile_definitions(purespice-connect-cancel-tests PRIVATE + PURESPICE_TESTING +) +target_include_directories(purespice-connect-cancel-tests PRIVATE + "${PROJECT_TOP}/repos/PureSpice/include" + "${PROJECT_TOP}/repos/PureSpice/src" + "$" +) +target_link_libraries(purespice-connect-cancel-tests + ${EXE_FLAGS} + purespice + pthread +) +add_test(NAME purespice-connect-cancel + COMMAND purespice-connect-cancel-tests +) +set_tests_properties(purespice-connect-cancel PROPERTIES + TIMEOUT 5 +) + add_executable(frame-scheduler-tests frame_scheduler_test.c ../src/frame_scheduler.c @@ -534,6 +583,7 @@ add_executable(frame-timing-tests target_compile_definitions(frame-timing-tests PRIVATE CIMGUI_DEFINE_ENUMS_AND_STRUCTS=1 ) +target_compile_options(frame-timing-tests PRIVATE -O1) target_include_directories(frame-timing-tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../src" "${PROJECT_TOP}/repos/gui/cimgui" @@ -547,11 +597,18 @@ set(FRAME_TIMING_CASES lifecycle order untracked + accounting + provider-unavailable timeout fifo retire feedback wrap + component-epoch + payload-gate + frame-payload-bookkeeping + connect-cancel + recovery-errors ) foreach(name IN LISTS FRAME_TIMING_CASES) add_test(NAME frame-timing-${name} @@ -662,6 +719,7 @@ set(RENDER_QUEUE_CASES payload validation cache + cursor-replacement ) foreach(name IN LISTS RENDER_QUEUE_CASES) add_test(NAME render-queue-${name} diff --git a/client/tests/frame_timing_test.c b/client/tests/frame_timing_test.c index 398fb95c..59d77ef5 100644 --- a/client/tests/frame_timing_test.c +++ b/client/tests/frame_timing_test.c @@ -25,6 +25,7 @@ #undef main #include +#include #define TEST_ALARM_S 5U @@ -39,6 +40,202 @@ struct Feedback }; static struct Feedback feed; +static unsigned int rawConnectCalls; +static unsigned int cancellableConnectCalls; +static bool connectSawCancellation; + +uint64_t renderQueue_sourceBegin(RenderQueueSource source) +{ + (void)source; + return 1; +} + +void renderQueue_sourceInvalidate(RenderQueueSource source, + uint64_t generation) +{ + (void)source; + (void)generation; +} + +void renderQueue_sourceClearCursor(RenderQueueSource source) +{ + (void)source; +} + +bool renderQueue_sourceSwSurfaceConfigure(RenderQueueSource source, + uint64_t generation, int width, int height) +{ + (void)source; + (void)generation; + (void)width; + (void)height; + return true; +} + +void renderQueue_sourceSwSurfaceDrawFill(RenderQueueSource source, + uint64_t generation, int x, int y, int width, int height, + uint32_t color) +{ + (void)source; + (void)generation; + (void)x; + (void)y; + (void)width; + (void)height; + (void)color; +} + +void renderQueue_sourceSwSurfaceDrawBitmap(RenderQueueSource source, + uint64_t generation, int x, int y, int width, int height, int stride, + const void * data, bool topDown) +{ + (void)source; + (void)generation; + (void)x; + (void)y; + (void)width; + (void)height; + (void)stride; + (void)data; + (void)topDown; +} + +void renderQueue_sourceCursorState(RenderQueueSource source, + uint64_t generation, bool visible, int x, int y, int hx, int hy) +{ + (void)source; + (void)generation; + (void)visible; + (void)x; + (void)y; + (void)hx; + (void)hy; +} + +void renderQueue_sourceCursorImage(RenderQueueSource source, + uint64_t generation, LG_RendererCursor type, + int width, int height, int pitch, const void * data) +{ + (void)source; + (void)generation; + (void)type; + (void)width; + (void)height; + (void)pitch; + (void)data; +} + +void renderQueue_sourceCursorColorTransform(RenderQueueSource source, + uint64_t generation, const LGColorTransform * transform) +{ + (void)source; + (void)generation; + (void)transform; +} + +void renderQueue_sourceCursorWhiteLevel(RenderQueueSource source, + uint64_t generation, uint32_t sdrWhiteLevel) +{ + (void)source; + (void)generation; + (void)sdrWhiteLevel; +} + +void overlaySplash_show(bool show) +{ + (void)show; +} + +bool lgTransportFallback_usable(const LG_TransportFallback * fallback) +{ + (void)fallback; + return false; +} + +bool lgTransportFallback_ready(const LG_TransportFallback * fallback) +{ + (void)fallback; + return false; +} + +bool lgTransportFallback_acceptsVideoEvents( + const LG_TransportFallback * fallback) +{ + (void)fallback; + return false; +} + +bool lgTransportFallback_videoRequested(LG_TransportFallback * fallback) +{ + (void)fallback; + return false; +} + +void lgTransportFallback_requestVideoActive( + LG_TransportFallback * fallback, bool active) +{ + (void)fallback; + (void)active; +} + +bool lgInput_available(void) +{ + return false; +} + +bool core_inputEnabled(void) +{ + return false; +} + +void core_alignToGuest(void) {} +void core_handleGuestMouseUpdate(void) {} +void core_setTitle(const char * title) { (void)title; } + +enum RunState app_getState(void) +{ + return atomic_load_explicit(&p_appState, memory_order_acquire); +} + +void app_setState(enum RunState state) +{ + atomic_store_explicit(&p_appState, state, memory_order_release); +} + +void app_invalidateWindow(bool full) +{ + (void)full; +} + +void app_refreshVideoSource(void) {} +void app_updateMouseState(void) {} + +static bool cancelled(void * opaque) +{ + (void)opaque; + return true; +} + +static LG_TransportStatus rawConnect(LG_Transport * transport, + LG_TransportSession * session) +{ + (void)transport; + (void)session; + ++rawConnectCalls; + return LG_TRANSPORT_ERROR; +} + +static LG_TransportStatus cancellableConnect(LG_Transport * transport, + LG_TransportSession * session, LG_TransportCancelledFn cancelled, + void * opaque) +{ + (void)transport; + (void)session; + ++cancellableConnectCalls; + connectSawCancellation = cancelled && cancelled(opaque); + return connectSawCancellation ? LG_TRANSPORT_DISCONNECTED : + LG_TRANSPORT_OK; +} void frameScheduler_feedback(uint64_t frameSerial, uint32_t generation, uint32_t scheduleEpoch, uint32_t deadlineSerial, @@ -57,6 +254,7 @@ static LG_TransportFrameTiming srcTiming(bool valid, bool phase) return (LG_TransportFrameTiming) { .valid = valid, + .providerValid = true, .phaseValid = phase, .scheduleGeneration = 2, .scheduleEpoch = 3, @@ -66,7 +264,9 @@ static LG_TransportFrameTiming srcTiming(bool valid, bool phase) .copyTime = 13, .readyTime = 14, .holdTime = 15, - .readyLeadTime = 100, + .readyLeadTime = 110, + .receiveTime = 2, + .prepareTime = 3, }; } @@ -130,7 +330,7 @@ static void testLifecycle(void) CHECK(frameTimingQueuedToken() == first); CHECK(frameTimingRecord(first)->dispatchTime == 35); frameTimingCancel(first); - app_handleFramePresented(first, 200, true); + main_framePresented(first, 200, true); CHECK(frameTimingRecord(first)->token == LG_RENDERER_FRAME_TOKEN_NONE); const LG_RendererFrameToken second = frameTimingReserve(); @@ -149,7 +349,7 @@ static void testOrder(void) const LG_RendererFrameToken token = frameTimingReserve(); queue(token, &timing, 55); - app_handleFramePresented(token, 500, true); + main_framePresented(token, 500, true); const LG_RendererFrameTiming render = dstTiming(token, true); frameTimingFinishRender(&render, 200, 7, 300, token); frameTimingPublishReady(); @@ -161,12 +361,15 @@ static void testOrder(void) const OverlayFrameTiming out = take(); CHECK(out.validMask == OVERLAY_FRAME_TIMING_VALID_ALL); CHECK(near(out.capture, 0.000011f)); + CHECK(near(out.transport, 0.000010f)); + CHECK(near(out.receive, 0.000002f)); + CHECK(near(out.providerPrepare, 0.000003f)); CHECK(near(out.dispatch, 0.000035f)); CHECK(near(out.queue, 0.000050f)); CHECK(near(out.present, 0.000500f)); frameTimingFinishFrame(token, &timing); - app_handleFramePresented(token, 600, true); + main_framePresented(token, 600, true); frameTimingFinishRender(&render, 200, 7, 300, token); frameTimingPublishReady(); CHECK(ringbuffer_getCount(g_state.frameLatency) == 0); @@ -188,11 +391,59 @@ static void testUntracked(void) const OverlayFrameTiming out = take(); CHECK(!(out.validMask & OVERLAY_FRAME_TIMING_VALID_PRODUCER)); CHECK(!(out.validMask & OVERLAY_FRAME_TIMING_VALID_TRANSPORT)); + CHECK((out.validMask & OVERLAY_FRAME_TIMING_VALID_PROVIDER) == + OVERLAY_FRAME_TIMING_VALID_PROVIDER); CHECK(!(out.validMask & OVERLAY_FRAME_TIMING_VALID_PRESENT)); + CHECK(near(out.receive, 0.000002f)); + CHECK(near(out.providerPrepare, 0.000003f)); CHECK(out.present == 0.0f); finish(); } +static void testAccounting(void) +{ + start(); + LG_TransportFrameTiming timing = srcTiming(true, true); + timing.readyLeadTime = 1; + timing.receiveTime = UINT64_MAX; + timing.prepareTime = UINT64_MAX; + const LG_RendererFrameToken token = frameTimingReserve(); + frameTimingQueue(token, 67, &timing, UINT64_MAX, UINT64_MAX, 100, 150); + frameTimingFinishFrame(token, &timing); + const LG_RendererFrameTiming render = dstTiming(token, false); + frameTimingFinishRender(&render, 200, 7, 300, token); + frameTimingPublishReady(); + CHECK(ringbuffer_getCount(g_state.frameLatency) == 1); + const OverlayFrameTiming out = take(); + CHECK(out.validMask & OVERLAY_FRAME_TIMING_VALID_TRANSPORT); + CHECK((out.validMask & OVERLAY_FRAME_TIMING_VALID_PROVIDER) == + OVERLAY_FRAME_TIMING_VALID_PROVIDER); + CHECK(out.transport == 0.0f); + CHECK(out.import == + (float)frameTimingAdd(UINT64_MAX, UINT64_MAX) * 1e-6f); + finish(); +} + +static void testProviderUnavailable(void) +{ + start(); + LG_TransportFrameTiming timing = srcTiming(true, true); + timing.providerValid = false; + const LG_RendererFrameToken token = frameTimingReserve(); + queue(token, &timing, 68); + frameTimingFinishFrame(token, &timing); + const LG_RendererFrameTiming render = dstTiming(token, false); + frameTimingFinishRender(&render, 200, 7, 300, token); + frameTimingPublishReady(); + CHECK(ringbuffer_getCount(g_state.frameLatency) == 1); + const OverlayFrameTiming out = take(); + CHECK(out.validMask & OVERLAY_FRAME_TIMING_VALID_PRODUCER); + CHECK(out.validMask & OVERLAY_FRAME_TIMING_VALID_TRANSPORT); + CHECK(!(out.validMask & OVERLAY_FRAME_TIMING_VALID_PROVIDER)); + CHECK(near(out.transport, 0.000015f)); + finish(); +} + static void testTimeout(void) { start(); @@ -321,6 +572,187 @@ static void testWrap(void) finish(); } +static void testComponentEpoch(void) +{ + atomic_bool available = true; + atomic_uint_least64_t epoch = 7; + atomic_int reason = LG_TRANSPORT_OK; + CHECK(videoComponentPayloadCurrent(&available, &epoch, 7, true)); + CHECK(!videoComponentPayloadCurrent(&available, &epoch, 6, true)); + CHECK(videoComponentPayloadCurrent(&available, &epoch, 0, false)); + + const LG_VideoComponentStatus unavailable = + { + .available = false, + .epoch = 8, + .reason = LG_TRANSPORT_ERROR, + }; + CHECK(videoComponentStatusChanged( + &available, &epoch, &reason, &unavailable)); + CHECK(!videoComponentPayloadCurrent(&available, &epoch, 7, true)); + CHECK(!videoComponentPayloadCurrent(&available, &epoch, 0, false)); + CHECK(atomic_load(&reason) == LG_TRANSPORT_ERROR); + + const LG_VideoComponentStatus replacement = + { + .available = true, + .epoch = 9, + .reason = LG_TRANSPORT_OK, + }; + CHECK(videoComponentStatusChanged( + &available, &epoch, &reason, &replacement)); + CHECK(!videoComponentPayloadCurrent(&available, &epoch, 7, true)); + CHECK(videoComponentPayloadCurrent(&available, &epoch, 9, true)); +} + +struct PayloadGateTrace +{ + const atomic_bool * available; + const atomic_uint_least64_t * epoch; + uint64_t payloadEpoch; + uint64_t sourceGeneration; + atomic_bool entered; + atomic_bool release; + atomic_bool accepted; + atomic_bool changed; +}; + +static void * holdPayload(void * opaque) +{ + struct PayloadGateTrace * trace = opaque; + const bool accepted = videoPayloadAccept(trace->available, trace->epoch, + trace->payloadEpoch, true, trace->sourceGeneration); + atomic_store_explicit(&trace->accepted, accepted, memory_order_release); + if (!accepted) + return NULL; + + atomic_store_explicit(&trace->entered, true, memory_order_release); + while (!atomic_load_explicit(&trace->release, memory_order_acquire)) + usleep(1000); + videoPayloadRelease(); + return NULL; +} + +static void * changePayload(void * opaque) +{ + struct PayloadGateTrace * trace = opaque; + LG_LOCK(g_state.videoPayloadLock); + atomic_store_explicit((atomic_bool *)trace->available, + false, memory_order_release); + atomic_store_explicit((atomic_uint_least64_t *)trace->epoch, + trace->payloadEpoch + 1, memory_order_release); + LG_UNLOCK(g_state.videoPayloadLock); + atomic_store_explicit(&trace->changed, true, memory_order_release); + return NULL; +} + +static void testPayloadGate(void) +{ + memset(&g_state, 0, sizeof(g_state)); + LG_LOCK_INIT(g_state.videoPayloadLock); + atomic_store_explicit( + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation, + 5, memory_order_relaxed); + atomic_bool available = true; + atomic_uint_least64_t epoch = 7; + struct PayloadGateTrace trace = + { + .available = &available, + .epoch = &epoch, + .payloadEpoch = 7, + .sourceGeneration = 5, + }; + pthread_t payloadThread; + pthread_t statusThread; + CHECK(pthread_create(&payloadThread, NULL, holdPayload, &trace) == 0); + while (!atomic_load_explicit(&trace.entered, memory_order_acquire)) + usleep(1000); + CHECK(pthread_create(&statusThread, NULL, changePayload, &trace) == 0); + usleep(10000); + CHECK(!atomic_load_explicit(&trace.changed, memory_order_acquire)); + atomic_store_explicit(&trace.release, true, memory_order_release); + CHECK(pthread_join(payloadThread, NULL) == 0); + CHECK(pthread_join(statusThread, NULL) == 0); + CHECK(atomic_load_explicit(&trace.accepted, memory_order_acquire)); + CHECK(atomic_load_explicit(&trace.changed, memory_order_acquire)); + CHECK(!videoPayloadAccept(&available, &epoch, 7, true, 5)); + LG_LOCK_FREE(g_state.videoPayloadLock); +} + +static void testFramePayloadBookkeeping(void) +{ + memset(&g_state, 0, sizeof(g_state)); + LG_LOCK_INIT(g_state.videoPayloadLock); + atomic_store_explicit(&g_state.videoFrameAvailable, + true, memory_order_relaxed); + atomic_store_explicit(&g_state.videoFrameEpoch, 7, memory_order_relaxed); + atomic_store_explicit( + &g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation, + 5, memory_order_relaxed); + g_state.formatValid = true; + + uint64_t sourceGeneration = 4; + uint64_t frameSerial = 41; + uint32_t formatVersion = 3; + bool sourceChanged = false; + LG_TransportFrame frame = + { + .serial = 42, + .epoch = 6, + }; + + CHECK(!videoFramePayloadAccept(&frame, 5, true, + &sourceGeneration, &frameSerial, &formatVersion, &sourceChanged)); + CHECK(sourceGeneration == 4); + CHECK(frameSerial == 41); + CHECK(formatVersion == 3); + + frame.epoch = 7; + CHECK(videoFramePayloadAccept(&frame, 5, true, + &sourceGeneration, &frameSerial, &formatVersion, &sourceChanged)); + CHECK(sourceChanged); + CHECK(sourceGeneration == 5); + CHECK(frameSerial == 42); + CHECK(formatVersion == 0); + videoPayloadRelease(); + + CHECK(!videoFramePayloadAccept(&frame, 5, true, + &sourceGeneration, &frameSerial, &formatVersion, &sourceChanged)); + LG_LOCK_FREE(g_state.videoPayloadLock); +} + +static void testConnectCancellation(void) +{ + rawConnectCalls = 0; + cancellableConnectCalls = 0; + connectSawCancellation = false; + const LG_TransportOps ops = + { + .connect = rawConnect, + .connectCancellable = cancellableConnect, + }; + struct TransportSessionProbe probe = + { + .ops = &ops, + .cancelled = cancelled, + .done = false, + }; + CHECK(transportSessionProbe(&probe) == 0); + CHECK(atomic_load_explicit(&probe.done, memory_order_acquire)); + CHECK(probe.status == LG_TRANSPORT_DISCONNECTED); + CHECK(rawConnectCalls == 0); + CHECK(cancellableConnectCalls == 1); + CHECK(connectSawCancellation); +} + +static void testRecoveryErrors(void) +{ + CHECK(strcmp(recoveryErrorText(LG_RECOVERY_ERR_BUSY), + "A conflicting recovery request is already in progress") == 0); + CHECK(strcmp(recoveryErrorText(LG_RECOVERY_ERR_CAPACITY), + "Too many recovery requests are pending") == 0); +} + struct Test { const char * name; @@ -332,11 +764,18 @@ static const struct Test tests[] = { "lifecycle", testLifecycle }, { "order" , testOrder }, { "untracked", testUntracked }, + { "accounting", testAccounting }, + { "provider-unavailable", testProviderUnavailable }, { "timeout" , testTimeout }, { "fifo" , testFifo }, { "retire" , testRetire }, { "feedback" , testFeedback }, { "wrap" , testWrap }, + { "component-epoch", testComponentEpoch }, + { "payload-gate", testPayloadGate }, + { "frame-payload-bookkeeping", testFramePayloadBookkeeping }, + { "connect-cancel", testConnectCancellation }, + { "recovery-errors", testRecoveryErrors }, }; int main(int argc, char ** argv) diff --git a/client/tests/lgmp_transport_test.c b/client/tests/lgmp_transport_test.c index 6ee9b91a..0b5ba7c9 100644 --- a/client/tests/lgmp_transport_test.c +++ b/client/tests/lgmp_transport_test.c @@ -21,6 +21,7 @@ #include "interface/transport.h" #include "common/KVMFR.h" +#include "common/KVMFRRecovery.h" #include "common/LGMPConfig.h" #include "common/debug.h" #include "common/option.h" @@ -40,8 +41,31 @@ #define TEST_SHM_SIZE (2U * 1024U * 1024U) #define TEST_TIMEOUT 20U #define WAIT_TIMEOUT (TEST_TIMEOUT + 100U) +#define POLL_INTERVAL 50000U extern const LG_TransportOps LGT_LGMP; +extern void lgmp_testSetVideoStatusDispatchGate( + LG_Transport * target, atomic_bool * waiting, atomic_bool * hold); +extern LG_RecoveryError lgmp_testRecoveryError(uint32_t error); +extern bool lgmp_testRecoveryProbeCancellation( + LG_TransportCancelledFn cancelled, void * opaque); + +static bool cancelled(void * opaque) +{ + return *(const bool *)opaque; +} + +struct CancelAfter +{ + unsigned int calls; + unsigned int limit; +}; + +static bool cancelAfter(void * opaque) +{ + struct CancelAfter * state = opaque; + return ++state->calls >= state->limit; +} #define CHECK(x) \ do \ @@ -70,14 +94,164 @@ static bool waitForQueuesEmpty(PLGMPHost host, PLGMPHostQueue frameQueue, return false; } +struct VideoStatusTrace; + struct WaitState { LG_Transport * transport; const LG_FrameOps * ops; LG_TransportStatus status; + atomic_bool done; bool frame; }; +struct UnregisterState +{ + LG_Transport * transport; + const LG_FrameOps * ops; + atomic_bool started; + atomic_bool done; +}; + +struct VideoStatusTrace +{ + atomic_uint count; + atomic_uint_fast64_t frameEpoch; + atomic_uint_fast64_t pointerEpoch; + atomic_int frameReason; + atomic_int pointerReason; + atomic_bool frameAvailable; + atomic_bool pointerAvailable; + atomic_bool active; + atomic_bool overlap; + atomic_bool block; + atomic_bool entered; + atomic_bool release; + atomic_bool * waitBeforeReplace; + atomic_bool unregisterDone; + atomic_bool replaceDone; + LG_Transport * unregisterTransport; + LG_Transport ** destroyTransport; + const LG_FrameOps * unregisterOps; + struct VideoStatusTrace * registerTrace; + struct VideoStatusTrace * replaceTrace; + unsigned int registerAtCount; + LG_Transport * transport; + const LG_FrameOps * ops; +}; + +static void videoStatusChanged(void * opaque, + const LG_VideoStatus * status) +{ + struct VideoStatusTrace * trace = opaque; + if (atomic_exchange_explicit(&trace->active, true, + memory_order_acq_rel)) + atomic_store_explicit(&trace->overlap, true, memory_order_release); + + atomic_store_explicit(&trace->frameEpoch, + status->frame.epoch, memory_order_relaxed); + atomic_store_explicit(&trace->pointerEpoch, + status->pointer.epoch, memory_order_relaxed); + atomic_store_explicit(&trace->frameReason, + status->frame.reason, memory_order_relaxed); + atomic_store_explicit(&trace->pointerReason, + status->pointer.reason, memory_order_relaxed); + atomic_store_explicit(&trace->frameAvailable, + status->frame.available, memory_order_relaxed); + atomic_store_explicit(&trace->pointerAvailable, + status->pointer.available, memory_order_relaxed); + const unsigned int count = atomic_fetch_add_explicit( + &trace->count, 1, memory_order_release) + 1; + + if (trace->unregisterTransport) + { + trace->unregisterOps->setStatusListener( + trace->unregisterTransport, NULL, NULL); + atomic_store_explicit( + &trace->unregisterDone, true, memory_order_release); + } + + if (trace->destroyTransport) + { + LGT_LGMP.destroy(trace->destroyTransport); + atomic_store_explicit( + &trace->unregisterDone, true, memory_order_release); + } + + if (trace->registerTrace && + (!trace->registerAtCount || count >= trace->registerAtCount)) + trace->registerTrace->ops->setStatusListener( + trace->registerTrace->transport, videoStatusChanged, + trace->registerTrace); + + if (atomic_load_explicit(&trace->block, memory_order_acquire)) + { + atomic_store_explicit(&trace->entered, true, memory_order_release); + while (!atomic_load_explicit(&trace->release, memory_order_acquire)) + usleep(1000); + } + + if (trace->replaceTrace) + { + if (trace->waitBeforeReplace) + while (!atomic_load_explicit( + trace->waitBeforeReplace, memory_order_acquire)) + usleep(1000); + trace->replaceTrace->ops->setStatusListener( + trace->replaceTrace->transport, NULL, NULL); + trace->replaceTrace->ops->setStatusListener( + trace->replaceTrace->transport, videoStatusChanged, + trace->replaceTrace); + atomic_store_explicit( + &trace->replaceDone, true, memory_order_release); + } + + atomic_store_explicit(&trace->active, false, memory_order_release); +} + +static bool waitStatusCount( + const struct VideoStatusTrace * trace, unsigned int count) +{ + for (unsigned i = 0; i < WAIT_TIMEOUT; ++i) + { + if (atomic_load_explicit(&trace->count, memory_order_acquire) >= count) + return true; + usleep(1000); + } + return false; +} + +static void * disconnectVideo(void * opaque) +{ + struct VideoStatusTrace * trace = opaque; + LGT_LGMP.disconnect(trace->transport); + return NULL; +} + +static void * disconnectVideoSignaled(void * opaque) +{ + struct VideoStatusTrace * trace = opaque; + LGT_LGMP.disconnect(trace->transport); + return NULL; +} + +static void * registerVideoStatus(void * opaque) +{ + struct VideoStatusTrace * trace = opaque; + trace->ops->setStatusListener( + trace->transport, videoStatusChanged, trace); + return NULL; +} + +static void * unregisterVideoStatus(void * opaque) +{ + struct UnregisterState * state = opaque; + atomic_store_explicit(&state->started, true, memory_order_release); + state->ops->setStatusListener(state->transport, NULL, NULL); + atomic_store_explicit(&state->done, true, memory_order_release); + return NULL; +} + static void * waitForVideo(void * opaque) { struct WaitState * state = opaque; @@ -92,6 +266,7 @@ static void * waitForVideo(void * opaque) LG_TransportPointer pointer; state->status = state->ops->nextPointer(state->transport, &pointer); } + atomic_store_explicit(&state->done, true, memory_order_release); return NULL; } @@ -152,12 +327,31 @@ int main(void) PLGMPHostQueue frameQueue = NULL; PLGMPHostQueue pointerQueue = NULL; PLGMPMemory frameMemory = NULL; + PLGMPMemory malformedFrameMemory = NULL; PLGMPMemory pointerMemory = NULL; + PLGMPMemory malformedPointerMemory = NULL; LG_Transport * transport = NULL; + LG_Transport * secondTransport = NULL; + LG_Transport * thirdTransport = NULL; + LG_Transport * nestedTransport = NULL; + LG_Transport * admittedTransport = NULL; const LG_FrameOps * frameOps = NULL; + struct VideoStatusTrace videoTrace; + struct VideoStatusTrace secondVideoTrace; + memset(&videoTrace, 0, sizeof(videoTrace)); + memset(&secondVideoTrace, 0, sizeof(secondVideoTrace)); debug_init(); + CHECK(lgmp_testRecoveryError(KVMFR_R_ERR_BUSY) == + LG_RECOVERY_ERR_BUSY); + CHECK(lgmp_testRecoveryError(KVMFR_R_ERR_CAPACITY) == + LG_RECOVERY_ERR_CAPACITY); + struct CancelAfter cancelDuringProbe = { .limit = 3 }; + CHECK(!lgmp_testRecoveryProbeCancellation( + cancelAfter, &cancelDuringProbe)); + CHECK(cancelDuringProbe.calls >= cancelDuringProbe.limit); + fd = mkstemp(path); CHECK(fd >= 0); pathExists = true; @@ -185,9 +379,6 @@ int main(void) .numMessages = LGMP_Q_POINTER_LEN, .subTimeout = TEST_TIMEOUT, }; - CHECK(lgmpHostQueueNew(host, frameConfig, &frameQueue) == LGMP_OK); - CHECK(lgmpHostQueueNew(host, pointerConfig, &pointerQueue) == LGMP_OK); - const uint32_t frameSize = sizeof(KVMFRFrame) + sizeof(FrameBuffer) + sizeof(uint32_t); CHECK(lgmpHostMemAlloc(host, frameSize, &frameMemory) == LGMP_OK); @@ -215,9 +406,16 @@ int main(void) FrameBuffer * framebuffer = (FrameBuffer *)((uint8_t *)wireFrame + wireFrame->offset); atomic_store(&framebuffer->wp, sizeof(uint32_t)); + CHECK(lgmpHostMemAlloc(host, sizeof(KVMFRFrame) - 1, + &malformedFrameMemory) == LGMP_OK); CHECK(lgmpHostMemAlloc(host, sizeof(KVMFRCursor), &pointerMemory) == LGMP_OK); + CHECK(lgmpHostMemAlloc(host, sizeof(KVMFRCursor), + &malformedPointerMemory) == LGMP_OK); + KVMFRCursor * malformedPointer = lgmpHostMemPtr(malformedPointerMemory); + malformedPointer->height = 2; + malformedPointer->pitch = UINT32_MAX; KVMFRCursor * wirePointer = lgmpHostMemPtr(pointerMemory); wirePointer->x = 1; wirePointer->y = 1; @@ -225,8 +423,8 @@ int main(void) LGT_LGMP.setup(); option_set_string("lgmp", "shmDevice", path); option_set_bool("lgmp", "allowDMA", false); - option_set_int("lgmp", "framePollInterval", 5000000); - option_set_int("lgmp", "cursorPollInterval", 5000000); + option_set_int("lgmp", "framePollInterval", POLL_INTERVAL); + option_set_int("lgmp", "cursorPollInterval", POLL_INTERVAL); CHECK(LGT_LGMP.create(&transport)); const LG_VideoOps * videoOps = LGT_LGMP.getVideoOps(transport); CHECK(videoOps); @@ -235,20 +433,92 @@ int main(void) frameOps = videoOps->frame; CHECK(frameOps->cancelFrameWait); CHECK(frameOps->cancelPointerWait); + CHECK(frameOps->setStatusListener); - CHECK(unlink(path) == 0); - pathExists = false; + CHECK(LGT_LGMP.create(&secondTransport)); + const LG_VideoOps * secondVideoOps = + LGT_LGMP.getVideoOps(secondTransport); + CHECK(secondVideoOps && secondVideoOps->type == LG_VIDEO_TYPE_FRAME); + CHECK(secondVideoOps->frame && secondVideoOps->frame->setStatusListener); + + CHECK(LGT_LGMP.create(&thirdTransport)); + const LG_VideoOps * thirdVideoOps = + LGT_LGMP.getVideoOps(thirdTransport); + CHECK(thirdVideoOps && thirdVideoOps->type == LG_VIDEO_TYPE_FRAME); + CHECK(thirdVideoOps->frame && thirdVideoOps->frame->setStatusListener); + + CHECK(LGT_LGMP.create(&nestedTransport)); + const LG_VideoOps * nestedVideoOps = + LGT_LGMP.getVideoOps(nestedTransport); + CHECK(nestedVideoOps && nestedVideoOps->type == LG_VIDEO_TYPE_FRAME); + CHECK(nestedVideoOps->frame && nestedVideoOps->frame->setStatusListener); + + CHECK(LGT_LGMP.create(&admittedTransport)); + const LG_VideoOps * admittedVideoOps = + LGT_LGMP.getVideoOps(admittedTransport); + CHECK(admittedVideoOps && admittedVideoOps->type == LG_VIDEO_TYPE_FRAME); + CHECK(admittedVideoOps->frame && + admittedVideoOps->frame->setStatusListener); usleep(300000); CHECK(lgmpHostProcess(host) == LGMP_OK); LG_TransportSession transportSession; - CHECK(LGT_LGMP.connect(transport, &transportSession) == LG_TRANSPORT_OK); + bool cancelConnect = true; + CHECK(LGT_LGMP.connectCancellable(transport, &transportSession, + cancelled, &cancelConnect) == LG_TRANSPORT_DISCONNECTED); + CHECK(!LGT_LGMP.sessionValid(transport)); + CHECK(LGT_LGMP.connectCancellable( + transport, &transportSession, NULL, NULL) == LG_TRANSPORT_OK); + LG_TransportSession secondSession; + CHECK(LGT_LGMP.connectCancellable( + secondTransport, &secondSession, NULL, NULL) == LG_TRANSPORT_OK); + LG_TransportSession thirdSession; + CHECK(LGT_LGMP.connectCancellable( + thirdTransport, &thirdSession, NULL, NULL) == LG_TRANSPORT_OK); + LG_TransportSession nestedSession; + CHECK(LGT_LGMP.connectCancellable( + nestedTransport, &nestedSession, NULL, NULL) == LG_TRANSPORT_OK); + LG_TransportSession admittedSession; + CHECK(LGT_LGMP.connectCancellable( + admittedTransport, &admittedSession, NULL, NULL) == LG_TRANSPORT_OK); + videoTrace.transport = transport; + videoTrace.ops = frameOps; + frameOps->setStatusListener( + transport, videoStatusChanged, &videoTrace); + CHECK(atomic_load_explicit(&videoTrace.count, memory_order_acquire) == 1); + CHECK(!atomic_load_explicit( + &videoTrace.frameAvailable, memory_order_acquire)); + CHECK(!atomic_load_explicit( + &videoTrace.pointerAvailable, memory_order_acquire)); + CHECK(atomic_load_explicit( + &videoTrace.frameReason, memory_order_acquire) == + LG_TRANSPORT_UNAVAILABLE); + CHECK(atomic_load_explicit( + &videoTrace.pointerReason, memory_order_acquire) == + LG_TRANSPORT_UNAVAILABLE); + + CHECK(lgmpHostQueueNew(host, frameConfig, &frameQueue) == LGMP_OK); + CHECK(lgmpHostQueueNew(host, pointerConfig, &pointerQueue) == LGMP_OK); LG_TransportFrame frame; LG_TransportPointer pointer; CHECK(checkWaitCancellation( transport, frameOps, frameQueue, pointerQueue)); + CHECK(waitStatusCount(&videoTrace, 3)); + CHECK(atomic_load_explicit( + &videoTrace.frameAvailable, memory_order_acquire)); + CHECK(atomic_load_explicit( + &videoTrace.pointerAvailable, memory_order_acquire)); + CHECK(atomic_load_explicit( + &videoTrace.frameReason, memory_order_acquire) == LG_TRANSPORT_OK); + CHECK(atomic_load_explicit( + &videoTrace.pointerReason, memory_order_acquire) == LG_TRANSPORT_OK); + CHECK(atomic_load_explicit( + &videoTrace.frameEpoch, memory_order_acquire) != 0); + CHECK(atomic_load_explicit( + &videoTrace.pointerEpoch, memory_order_acquire) != 0); + CHECK(!atomic_load_explicit(&videoTrace.overlap, memory_order_acquire)); CHECK(lgmpHostQueueHasSubs(frameQueue)); CHECK(lgmpHostQueueHasSubs(pointerQueue)); CHECK(lgmpHostQueueNewSubs(frameQueue) == 1); @@ -258,14 +528,130 @@ int main(void) CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION, pointerMemory) == LGMP_OK); CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_OK); + CHECK(frame.epoch == atomic_load_explicit( + &videoTrace.frameEpoch, memory_order_acquire)); LG_TransportFrameTiming timing; frameOps->getFrameTiming(transport, &frame, &timing); CHECK(timing.captureTime == wireFrame->captureTime); CHECK(timing.postProcessTime == wireFrame->postProcessTime); CHECK(timing.copyTime == wireFrame->copyTime); CHECK(timing.readyTime == wireFrame->readyTime); + CHECK(timing.providerValid); + frameOps->releaseFrame(transport, &frame); + + wireFrame->frameSerial = 2; + wireFrame->timingSerial = wireFrame->frameSerial; + wireFrame->timingValid = 0; + CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK); + CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_OK); + /* Work which began before producer timing became coherent is not reported + * as an independent provider stage. */ + wireFrame->timingValid = 1; + frameOps->getFrameTiming(transport, &frame, &timing); + CHECK(timing.valid); + CHECK(!timing.providerValid); + CHECK(timing.receiveTime == 0); + CHECK(timing.prepareTime == 0); frameOps->releaseFrame(transport, &frame); CHECK(frameOps->nextPointer(transport, &pointer) == LG_TRANSPORT_OK); + CHECK(pointer.epoch == atomic_load_explicit( + &videoTrace.pointerEpoch, memory_order_acquire)); + frameOps->releasePointer(transport, &pointer); + + const uint64_t frameEpoch = atomic_load_explicit( + &videoTrace.frameEpoch, memory_order_acquire); + const uint64_t pointerEpoch = atomic_load_explicit( + &videoTrace.pointerEpoch, memory_order_acquire); + CHECK(lgmpHostQueuePost(frameQueue, 0, malformedFrameMemory) == LGMP_OK); + frameOps->cancelFrameWait(transport); + CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_ERROR); + CHECK(!atomic_load_explicit( + &videoTrace.frameAvailable, memory_order_acquire)); + CHECK(atomic_load_explicit( + &videoTrace.pointerAvailable, memory_order_acquire)); + CHECK(atomic_load_explicit( + &videoTrace.frameEpoch, memory_order_acquire) != frameEpoch); + CHECK(atomic_load_explicit( + &videoTrace.pointerEpoch, memory_order_acquire) == pointerEpoch); + CHECK(atomic_load_explicit( + &videoTrace.frameReason, memory_order_acquire) == + LG_TRANSPORT_ERROR); + + CHECK(lgmpHostQueuePost(frameQueue, 0, malformedFrameMemory) == LGMP_OK); + struct WaitState frameErrorWait = + { + .transport = transport, + .ops = frameOps, + .status = LG_TRANSPORT_OK, + .frame = true, + }; + pthread_t frameErrorThread; + CHECK(pthread_create(&frameErrorThread, NULL, + waitForVideo, &frameErrorWait) == 0); + CHECK(waitForQueuesEmpty(host, frameQueue, pointerQueue)); + usleep(POLL_INTERVAL / 10U); + CHECK(!atomic_load_explicit( + &frameErrorWait.done, memory_order_acquire)); + frameOps->cancelFrameWait(transport); + CHECK(pthread_join(frameErrorThread, NULL) == 0); + CHECK(atomic_load_explicit(&frameErrorWait.done, memory_order_acquire)); + CHECK(frameErrorWait.status == LG_TRANSPORT_ERROR); + + wireFrame->frameSerial = 3; + wireFrame->timingSerial = wireFrame->frameSerial; + CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK); + CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_OK); + CHECK(atomic_load_explicit( + &videoTrace.frameAvailable, memory_order_acquire)); + frameOps->releaseFrame(transport, &frame); + + const uint64_t frameEpochAfterRecovery = atomic_load_explicit( + &videoTrace.frameEpoch, memory_order_acquire); + const uint64_t pointerEpochBeforeLoss = atomic_load_explicit( + &videoTrace.pointerEpoch, memory_order_acquire); + CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_SHAPE, + malformedPointerMemory) == LGMP_OK); + frameOps->cancelPointerWait(transport); + CHECK(frameOps->nextPointer(transport, &pointer) == LG_TRANSPORT_ERROR); + CHECK(atomic_load_explicit( + &videoTrace.frameAvailable, memory_order_acquire)); + CHECK(!atomic_load_explicit( + &videoTrace.pointerAvailable, memory_order_acquire)); + CHECK(atomic_load_explicit( + &videoTrace.frameEpoch, memory_order_acquire) == + frameEpochAfterRecovery); + CHECK(atomic_load_explicit( + &videoTrace.pointerEpoch, memory_order_acquire) != + pointerEpochBeforeLoss); + CHECK(atomic_load_explicit( + &videoTrace.pointerReason, memory_order_acquire) == + LG_TRANSPORT_ERROR); + + CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_SHAPE, + malformedPointerMemory) == LGMP_OK); + struct WaitState pointerErrorWait = + { + .transport = transport, + .ops = frameOps, + .status = LG_TRANSPORT_OK, + }; + pthread_t pointerErrorThread; + CHECK(pthread_create(&pointerErrorThread, NULL, + waitForVideo, &pointerErrorWait) == 0); + CHECK(waitForQueuesEmpty(host, frameQueue, pointerQueue)); + usleep(POLL_INTERVAL / 10U); + CHECK(!atomic_load_explicit( + &pointerErrorWait.done, memory_order_acquire)); + frameOps->cancelPointerWait(transport); + CHECK(pthread_join(pointerErrorThread, NULL) == 0); + CHECK(atomic_load_explicit(&pointerErrorWait.done, memory_order_acquire)); + CHECK(pointerErrorWait.status == LG_TRANSPORT_ERROR); + + CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION, + pointerMemory) == LGMP_OK); + CHECK(frameOps->nextPointer(transport, &pointer) == LG_TRANSPORT_OK); + CHECK(atomic_load_explicit( + &videoTrace.pointerAvailable, memory_order_acquire)); frameOps->releasePointer(transport, &pointer); /* @@ -306,7 +692,7 @@ int main(void) * Also recover when the host has already marked the cached handles bad. * LGMP leaves a timed-out handle non-NULL when unsubscribe fails. */ - wireFrame->frameSerial = 2; + wireFrame->frameSerial = 4; wireFrame->timingSerial = wireFrame->frameSerial; CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK); CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION, @@ -319,9 +705,19 @@ int main(void) frameOps->stopFrame(transport); frameOps->stopPointer(transport); frameOps->cancelFrameWait(transport); + const uint64_t oldFrameEpoch = atomic_load_explicit( + &videoTrace.frameEpoch, memory_order_acquire); + const uint64_t oldPointerEpoch = atomic_load_explicit( + &videoTrace.pointerEpoch, memory_order_acquire); CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_TIMEOUT); + CHECK(atomic_load_explicit( + &videoTrace.frameEpoch, memory_order_acquire) != oldFrameEpoch); + CHECK(atomic_load_explicit( + &videoTrace.pointerEpoch, memory_order_acquire) == oldPointerEpoch); frameOps->cancelPointerWait(transport); CHECK(frameOps->nextPointer(transport, &pointer) == LG_TRANSPORT_TIMEOUT); + CHECK(atomic_load_explicit( + &videoTrace.pointerEpoch, memory_order_acquire) != oldPointerEpoch); CHECK(lgmpHostQueueNewSubs(frameQueue) == 1); CHECK(lgmpHostQueueNewSubs(pointerQueue) == 1); @@ -329,18 +725,306 @@ int main(void) CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK); CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION, pointerMemory) == LGMP_OK); + const bool pointerBeforeFrameRecovery = atomic_load_explicit( + &videoTrace.pointerAvailable, memory_order_acquire); CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_OK); + CHECK(atomic_load_explicit( + &videoTrace.frameAvailable, memory_order_acquire)); + CHECK(atomic_load_explicit( + &videoTrace.pointerAvailable, memory_order_acquire) == + pointerBeforeFrameRecovery); frameOps->releaseFrame(transport, &frame); CHECK(frameOps->nextPointer(transport, &pointer) == LG_TRANSPORT_OK); + CHECK(atomic_load_explicit( + &videoTrace.pointerAvailable, memory_order_acquire)); frameOps->releasePointer(transport, &pointer); + atomic_store_explicit(&videoTrace.block, true, memory_order_release); + pthread_t disconnectThread; + pthread_t crossUnregisterThread; + CHECK(pthread_create(&disconnectThread, NULL, + disconnectVideo, &videoTrace) == 0); + for (unsigned i = 0; i < WAIT_TIMEOUT && !atomic_load_explicit( + &videoTrace.entered, memory_order_acquire); ++i) + usleep(1000); + CHECK(atomic_load_explicit(&videoTrace.entered, memory_order_acquire)); + secondVideoTrace.transport = secondTransport; + secondVideoTrace.ops = secondVideoOps->frame; + secondVideoTrace.unregisterTransport = transport; + secondVideoTrace.unregisterOps = frameOps; + CHECK(pthread_create(&crossUnregisterThread, NULL, + registerVideoStatus, &secondVideoTrace) == 0); + usleep(POLL_INTERVAL); + CHECK(atomic_load_explicit( + &secondVideoTrace.count, memory_order_acquire) == 0); + CHECK(!atomic_load_explicit(&secondVideoTrace.unregisterDone, + memory_order_acquire)); + atomic_store_explicit(&videoTrace.release, true, memory_order_release); + CHECK(pthread_join(disconnectThread, NULL) == 0); + CHECK(pthread_join(crossUnregisterThread, NULL) == 0); + CHECK(waitStatusCount(&secondVideoTrace, 1)); + CHECK(atomic_load_explicit(&secondVideoTrace.unregisterDone, + memory_order_acquire)); + CHECK(!atomic_load_explicit(&videoTrace.overlap, memory_order_acquire)); + CHECK(!atomic_load_explicit( + &secondVideoTrace.overlap, memory_order_acquire)); + frameOps->setStatusListener(transport, NULL, NULL); + secondVideoOps->frame->setStatusListener(secondTransport, NULL, NULL); + + /* A callback can unregister another instance while a callback for that + * instance is waiting to dispatch. The queued callback revalidates its + * listener and is skipped. */ + struct VideoStatusTrace queuedTrace; + struct VideoStatusTrace nestedTrace; + struct VideoStatusTrace replacementTrace; + memset(&queuedTrace, 0, sizeof(queuedTrace)); + memset(&nestedTrace, 0, sizeof(nestedTrace)); + memset(&replacementTrace, 0, sizeof(replacementTrace)); + queuedTrace.transport = transport; + queuedTrace.ops = frameOps; + nestedTrace.transport = secondTransport; + nestedTrace.ops = secondVideoOps->frame; + nestedTrace.unregisterTransport = transport; + nestedTrace.unregisterOps = frameOps; + replacementTrace.transport = secondTransport; + replacementTrace.ops = secondVideoOps->frame; + nestedTrace.registerTrace = &replacementTrace; + frameOps->setStatusListener( + transport, videoStatusChanged, &queuedTrace); + const unsigned int queuedCount = atomic_load_explicit( + &queuedTrace.count, memory_order_acquire); + secondVideoOps->frame->setStatusListener( + secondTransport, videoStatusChanged, &nestedTrace); + CHECK(atomic_load_explicit( + &nestedTrace.unregisterDone, memory_order_acquire)); + CHECK(atomic_load_explicit( + &replacementTrace.count, memory_order_acquire) == 1); + LGT_LGMP.disconnect(transport); + CHECK(atomic_load_explicit( + &queuedTrace.count, memory_order_acquire) == queuedCount); + + /* Re-registering the identical callback pair creates a new listener + * lifetime. A publication queued for the prior lifetime must not be + * delivered after that replacement. */ + struct VideoStatusTrace gateTrace; + struct VideoStatusTrace abaTrace; + memset(&gateTrace, 0, sizeof(gateTrace)); + memset(&abaTrace, 0, sizeof(abaTrace)); + gateTrace.transport = transport; + gateTrace.ops = frameOps; + abaTrace.transport = secondTransport; + abaTrace.ops = secondVideoOps->frame; + frameOps->setStatusListener(transport, NULL, NULL); + secondVideoOps->frame->setStatusListener( + secondTransport, videoStatusChanged, &abaTrace); + const unsigned int abaCount = atomic_load_explicit( + &abaTrace.count, memory_order_acquire); + gateTrace.block = true; + atomic_bool abaWaiting = false; + gateTrace.replaceTrace = &abaTrace; + gateTrace.waitBeforeReplace = &abaWaiting; + pthread_t gateThread; + CHECK(pthread_create(&gateThread, NULL, + registerVideoStatus, &gateTrace) == 0); + for (unsigned i = 0; i < WAIT_TIMEOUT && !atomic_load_explicit( + &gateTrace.entered, memory_order_acquire); ++i) + usleep(1000); + CHECK(atomic_load_explicit(&gateTrace.entered, memory_order_acquire)); + atomic_bool abaHold = true; + lgmp_testSetVideoStatusDispatchGate( + secondTransport, &abaWaiting, &abaHold); + pthread_t abaPublishThread; + CHECK(pthread_create(&abaPublishThread, NULL, + disconnectVideoSignaled, &abaTrace) == 0); + for (unsigned i = 0; i < WAIT_TIMEOUT && !atomic_load_explicit( + &abaWaiting, memory_order_acquire); ++i) + usleep(1000); + CHECK(atomic_load_explicit(&abaWaiting, memory_order_acquire)); + atomic_store_explicit(&gateTrace.release, true, memory_order_release); + usleep(POLL_INTERVAL); + CHECK(atomic_load_explicit( + &gateTrace.replaceDone, memory_order_acquire)); + + /* External unregister waits for the stale accepted dispatch to drain. */ + struct UnregisterState unregisterState = + { + .transport = secondTransport, + .ops = secondVideoOps->frame, + }; + pthread_t unregisterThread; + CHECK(pthread_create(&unregisterThread, NULL, + unregisterVideoStatus, &unregisterState) == 0); + for (unsigned i = 0; i < WAIT_TIMEOUT && !atomic_load_explicit( + &unregisterState.started, memory_order_acquire); ++i) + usleep(1000); + CHECK(atomic_load_explicit( + &unregisterState.started, memory_order_acquire)); + usleep(POLL_INTERVAL); + CHECK(!atomic_load_explicit( + &unregisterState.done, memory_order_acquire)); + atomic_store_explicit(&abaHold, false, memory_order_release); + CHECK(pthread_join(gateThread, NULL) == 0); + CHECK(pthread_join(abaPublishThread, NULL) == 0); + CHECK(pthread_join(unregisterThread, NULL) == 0); + CHECK(atomic_load_explicit(&gateTrace.replaceDone, memory_order_acquire)); + CHECK(atomic_load_explicit(&unregisterState.done, memory_order_acquire)); + lgmp_testSetVideoStatusDispatchGate(NULL, NULL, NULL); + CHECK(atomic_load_explicit( + &abaTrace.count, memory_order_acquire) == abaCount + 1); + + /* Symmetric cross-instance unregisters are safe when both registrations + * start together. Both callbacks run sequentially and complete their + * cross-instance unregister. */ + struct VideoStatusTrace symmetricA; + struct VideoStatusTrace symmetricB; + memset(&symmetricA, 0, sizeof(symmetricA)); + memset(&symmetricB, 0, sizeof(symmetricB)); + symmetricA.transport = transport; + symmetricA.ops = frameOps; + symmetricA.unregisterTransport = secondTransport; + symmetricA.unregisterOps = secondVideoOps->frame; + symmetricB.transport = secondTransport; + symmetricB.ops = secondVideoOps->frame; + symmetricB.unregisterTransport = transport; + symmetricB.unregisterOps = frameOps; + pthread_t symmetricThreadA; + pthread_t symmetricThreadB; + CHECK(pthread_create(&symmetricThreadA, NULL, + registerVideoStatus, &symmetricA) == 0); + CHECK(pthread_create(&symmetricThreadB, NULL, + registerVideoStatus, &symmetricB) == 0); + CHECK(pthread_join(symmetricThreadA, NULL) == 0); + CHECK(pthread_join(symmetricThreadB, NULL) == 0); + CHECK(atomic_load_explicit( + &symmetricA.count, memory_order_acquire) == 1); + CHECK(atomic_load_explicit( + &symmetricB.count, memory_order_acquire) == 1); + CHECK(atomic_load_explicit( + &symmetricA.unregisterDone, memory_order_acquire)); + CHECK(atomic_load_explicit( + &symmetricB.unregisterDone, memory_order_acquire)); + + /* A callback may destroy the instance on which an admitted callback is + * active. Physical teardown is deferred until that callback retires. */ + frameOps->setStatusListener(transport, NULL, NULL); + secondVideoOps->frame->setStatusListener(secondTransport, NULL, NULL); + admittedVideoOps->frame->setStatusListener(admittedTransport, NULL, NULL); + struct VideoStatusTrace admittedDestroy; + struct VideoStatusTrace admittedOuter; + memset(&admittedDestroy, 0, sizeof(admittedDestroy)); + memset(&admittedOuter, 0, sizeof(admittedOuter)); + admittedDestroy.transport = secondTransport; + admittedDestroy.ops = secondVideoOps->frame; + admittedDestroy.destroyTransport = &admittedTransport; + admittedOuter.transport = admittedTransport; + admittedOuter.ops = admittedVideoOps->frame; + admittedOuter.registerTrace = &admittedDestroy; + admittedOuter.registerAtCount = 2; + admittedVideoOps->frame->setStatusListener( + admittedTransport, videoStatusChanged, &admittedOuter); + CHECK(atomic_load_explicit( + &admittedOuter.count, memory_order_acquire) == 1); + LGT_LGMP.disconnect(admittedTransport); + CHECK(!admittedTransport); + CHECK(atomic_load_explicit( + &admittedOuter.count, memory_order_acquire) == 2); + CHECK(atomic_load_explicit( + &admittedDestroy.count, memory_order_acquire) == 1); + + /* A synchronous callback stays pinned while it drops serialization to + * drain a different instance, including when a third callback destroys it. + */ + thirdVideoOps->frame->setStatusListener(thirdTransport, NULL, NULL); + nestedVideoOps->frame->setStatusListener(nestedTransport, NULL, NULL); + struct VideoStatusTrace drainTrace; + struct VideoStatusTrace pinnedTrace; + struct VideoStatusTrace thirdTrace; + memset(&drainTrace, 0, sizeof(drainTrace)); + memset(&pinnedTrace, 0, sizeof(pinnedTrace)); + memset(&thirdTrace, 0, sizeof(thirdTrace)); + drainTrace.transport = thirdTransport; + drainTrace.ops = thirdVideoOps->frame; + thirdVideoOps->frame->setStatusListener( + thirdTransport, videoStatusChanged, &drainTrace); + atomic_bool drainWaiting = false; + atomic_bool drainHold = true; + lgmp_testSetVideoStatusDispatchGate( + thirdTransport, &drainWaiting, &drainHold); + pthread_t drainThread; + CHECK(pthread_create(&drainThread, NULL, + disconnectVideoSignaled, &drainTrace) == 0); + for (unsigned i = 0; i < WAIT_TIMEOUT && !atomic_load_explicit( + &drainWaiting, memory_order_acquire); ++i) + usleep(1000); + CHECK(atomic_load_explicit(&drainWaiting, memory_order_acquire)); + + pinnedTrace.transport = transport; + pinnedTrace.ops = frameOps; + pinnedTrace.unregisterTransport = thirdTransport; + pinnedTrace.unregisterOps = thirdVideoOps->frame; + pthread_t pinnedThread; + CHECK(pthread_create(&pinnedThread, NULL, + registerVideoStatus, &pinnedTrace) == 0); + CHECK(waitStatusCount(&pinnedTrace, 1)); + + thirdTrace.transport = nestedTransport; + thirdTrace.ops = nestedVideoOps->frame; + nestedVideoOps->frame->setStatusListener( + nestedTransport, videoStatusChanged, &thirdTrace); + thirdTrace.destroyTransport = &transport; + pthread_t thirdThread; + CHECK(pthread_create(&thirdThread, NULL, + disconnectVideoSignaled, &thirdTrace) == 0); + CHECK(waitStatusCount(&thirdTrace, 2)); + atomic_store_explicit(&drainHold, false, memory_order_release); + CHECK(pthread_join(drainThread, NULL) == 0); + CHECK(pthread_join(pinnedThread, NULL) == 0); + CHECK(pthread_join(thirdThread, NULL) == 0); + lgmp_testSetVideoStatusDispatchGate(NULL, NULL, NULL); + CHECK(!transport); + CHECK(atomic_load_explicit( + &pinnedTrace.unregisterDone, memory_order_acquire)); + CHECK(atomic_load_explicit( + &thirdTrace.unregisterDone, memory_order_acquire)); + + /* The same lifetime rule applies when nested synchronous callbacks destroy + * the instance belonging to the enclosing callback. */ + secondVideoOps->frame->setStatusListener(secondTransport, NULL, NULL); + struct VideoStatusTrace syncDestroy; + struct VideoStatusTrace syncOuter; + memset(&syncDestroy, 0, sizeof(syncDestroy)); + memset(&syncOuter, 0, sizeof(syncOuter)); + syncDestroy.transport = secondTransport; + syncDestroy.ops = secondVideoOps->frame; + syncDestroy.destroyTransport = &thirdTransport; + syncOuter.transport = thirdTransport; + syncOuter.ops = thirdVideoOps->frame; + syncOuter.registerTrace = &syncDestroy; + thirdVideoOps->frame->setStatusListener( + thirdTransport, videoStatusChanged, &syncOuter); + CHECK(!thirdTransport); + CHECK(atomic_load_explicit( + &syncOuter.count, memory_order_acquire) == 1); + CHECK(atomic_load_explicit( + &syncDestroy.count, memory_order_acquire) == 1); + result = 0; cleanup: + if (admittedTransport) + LGT_LGMP.destroy(&admittedTransport); + if (nestedTransport) + LGT_LGMP.destroy(&nestedTransport); + if (thirdTransport) + LGT_LGMP.destroy(&thirdTransport); + if (secondTransport) + LGT_LGMP.destroy(&secondTransport); if (transport) LGT_LGMP.destroy(&transport); option_free(); lgmpHostMemFree(&pointerMemory); + lgmpHostMemFree(&malformedPointerMemory); + lgmpHostMemFree(&malformedFrameMemory); lgmpHostMemFree(&frameMemory); lgmpHostFree(&host); if (hostMemory != MAP_FAILED) diff --git a/client/tests/purespice_connect_cancel_test.c b/client/tests/purespice_connect_cancel_test.c new file mode 100644 index 00000000..917dba0d --- /dev/null +++ b/client/tests/purespice_connect_cancel_test.c @@ -0,0 +1,303 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + */ + +#include "test.h" + +#include +#include "ps.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ACCEPT_TIMEOUT_MS 2000U +#define CANCEL_BOUND_MS 500U + +struct StallPeer +{ + int listenFd; + atomic_bool accepted; + atomic_bool stop; +}; + +struct ConnectTask +{ + PSConfig config; + atomic_bool done; + bool result; +}; + +struct ChannelConnectTask +{ + atomic_bool done; + bool result; +}; + +struct PublishGate +{ + atomic_bool entered; + atomic_bool release; +}; + +static void quietLog(const char * file, unsigned int line, + const char * function, const char * format, ...) +{ + (void)file; + (void)line; + (void)function; + (void)format; +} + +static uint64_t monotonicMs(void) +{ + struct timespec time; + CHECK(clock_gettime(CLOCK_MONOTONIC, &time) == 0); + return (uint64_t)time.tv_sec * UINT64_C(1000) + + (uint64_t)time.tv_nsec / UINT64_C(1000000); +} + +static void * stallPeerThread(void * opaque) +{ + struct StallPeer * peer = opaque; + const int client = accept(peer->listenFd, NULL, NULL); + if (client < 0) + return NULL; + + atomic_store_explicit(&peer->accepted, true, memory_order_release); + while (!atomic_load_explicit(&peer->stop, memory_order_acquire)) + usleep(1000); + + close(client); + return NULL; +} + +static void * connectThread(void * opaque) +{ + struct ConnectTask * task = opaque; + task->result = purespice_connect(&task->config); + atomic_store_explicit(&task->done, true, memory_order_release); + return NULL; +} + +static void * channelConnectThread(void * opaque) +{ + struct ChannelConnectTask * task = opaque; + task->result = purespice_connectChannel(PS_CHANNEL_DISPLAY); + atomic_store_explicit(&task->done, true, memory_order_release); + return NULL; +} + +static void connectPublished(void * opaque) +{ + struct PublishGate * gate = opaque; + atomic_store_explicit(&gate->entered, true, memory_order_release); + while (!atomic_load_explicit(&gate->release, memory_order_acquire)) + usleep(1000); +} + +static void waitFor(atomic_bool * value, unsigned int timeoutMs) +{ + const uint64_t deadline = monotonicMs() + timeoutMs; + while (!atomic_load_explicit(value, memory_order_acquire) && + monotonicMs() < deadline) + usleep(1000); + CHECK(atomic_load_explicit(value, memory_order_acquire)); +} + +static void testStalledHandshake(void) +{ + const PSInit init = + { + .log = + { + .info = quietLog, + .warn = quietLog, + .error = quietLog, + }, + }; + purespice_init(&init); + + const int listenFd = socket(AF_UNIX, SOCK_STREAM, 0); + CHECK(listenFd >= 0); + + struct sockaddr_un address = + { + .sun_family = AF_UNIX, + }; + const int pathLength = snprintf(address.sun_path, + sizeof(address.sun_path), "/tmp/lg-ps-cancel-%ld", (long)getpid()); + CHECK(pathLength > 0 && (size_t)pathLength < sizeof(address.sun_path)); + (void)unlink(address.sun_path); + const socklen_t addressSize = sizeof(address); + CHECK(bind(listenFd, (const struct sockaddr *)&address, + addressSize) == 0); + CHECK(listen(listenFd, 1) == 0); + + struct StallPeer peer = + { + .listenFd = listenFd, + }; + atomic_init(&peer.accepted, false); + atomic_init(&peer.stop, false); + + pthread_t peerThread; + CHECK(pthread_create(&peerThread, NULL, stallPeerThread, &peer) == 0); + + struct ConnectTask connect = + { + .config = + { + .host = address.sun_path, + .port = 0, + .password = "", + }, + }; + atomic_init(&connect.done, false); + + purespice_beginConnect(); + pthread_t connectThreadId; + CHECK(pthread_create( + &connectThreadId, NULL, connectThread, &connect) == 0); + waitFor(&peer.accepted, ACCEPT_TIMEOUT_MS); + + const uint64_t start = monotonicMs(); + purespice_disconnect(); + const uint64_t disconnectElapsed = monotonicMs() - start; + + waitFor(&connect.done, CANCEL_BOUND_MS); + const uint64_t elapsed = monotonicMs() - start; + CHECK(pthread_join(connectThreadId, NULL) == 0); + CHECK(!connect.result); + CHECK(disconnectElapsed < CANCEL_BOUND_MS); + CHECK(elapsed < CANCEL_BOUND_MS); + + atomic_store_explicit(&peer.stop, true, memory_order_release); + CHECK(pthread_join(peerThread, NULL) == 0); + close(listenFd); + CHECK(unlink(address.sun_path) == 0); +} + +static void testCancelBeforeConnect(void) +{ + struct PublishGate gate; + atomic_init(&gate.entered, false); + atomic_init(&gate.release, false); + purespice_testSetConnectPublishedHook(connectPublished, &gate); + + struct ConnectTask connect = + { + .config = + { + .host = "/tmp/lg-ps-unreachable", + .port = 0, + .password = "", + }, + }; + atomic_init(&connect.done, false); + purespice_beginConnect(); + + pthread_t connectThreadId; + CHECK(pthread_create( + &connectThreadId, NULL, connectThread, &connect) == 0); + waitFor(&gate.entered, ACCEPT_TIMEOUT_MS); + + const uint64_t start = monotonicMs(); + purespice_cancelConnect(); + atomic_store_explicit(&gate.release, true, memory_order_release); + waitFor(&connect.done, CANCEL_BOUND_MS); + const uint64_t elapsed = monotonicMs() - start; + + CHECK(pthread_join(connectThreadId, NULL) == 0); + CHECK(!connect.result); + CHECK(elapsed < CANCEL_BOUND_MS); + purespice_testSetConnectPublishedHook(NULL, NULL); +} + +static void testStalledChannelHandshake(void) +{ + const int listenFd = socket(AF_UNIX, SOCK_STREAM, 0); + CHECK(listenFd >= 0); + + struct sockaddr_un address = + { + .sun_family = AF_UNIX, + }; + const int pathLength = snprintf(address.sun_path, + sizeof(address.sun_path), "/tmp/lg-ps-channel-%ld", (long)getpid()); + CHECK(pathLength > 0 && (size_t)pathLength < sizeof(address.sun_path)); + (void)unlink(address.sun_path); + CHECK(bind(listenFd, (const struct sockaddr *)&address, + sizeof(address)) == 0); + CHECK(listen(listenFd, 1) == 0); + + struct StallPeer peer = + { + .listenFd = listenFd, + }; + atomic_init(&peer.accepted, false); + atomic_init(&peer.stop, false); + pthread_t peerThread; + CHECK(pthread_create(&peerThread, NULL, stallPeerThread, &peer) == 0); + + g_ps.family = AF_UNIX; + memset(&g_ps.addr, 0, sizeof(g_ps.addr)); + g_ps.addr.un.sun_family = AF_UNIX; + CHECK(snprintf(g_ps.addr.un.sun_path, sizeof(g_ps.addr.un.sun_path), + "%s", address.sun_path) > 0); + g_ps.epollfd = epoll_create1(0); + CHECK(g_ps.epollfd >= 0); + atomic_store_explicit(&g_ps.channels[PS_CHANNEL_DISPLAY].available, + true, memory_order_release); + + struct ChannelConnectTask connect; + atomic_init(&connect.done, false); + connect.result = true; + purespice_beginConnect(); + purespice_beginChannelConnect(PS_CHANNEL_DISPLAY); + pthread_t connectThreadId; + CHECK(pthread_create(&connectThreadId, NULL, + channelConnectThread, &connect) == 0); + waitFor(&peer.accepted, ACCEPT_TIMEOUT_MS); + + const uint64_t start = monotonicMs(); + purespice_cancelChannelConnect(PS_CHANNEL_DISPLAY); + waitFor(&connect.done, CANCEL_BOUND_MS); + const uint64_t elapsed = monotonicMs() - start; + + CHECK(pthread_join(connectThreadId, NULL) == 0); + CHECK(!connect.result); + CHECK(elapsed < CANCEL_BOUND_MS); + + atomic_store_explicit(&peer.stop, true, memory_order_release); + CHECK(pthread_join(peerThread, NULL) == 0); + close(listenFd); + CHECK(unlink(address.sun_path) == 0); + close(g_ps.epollfd); + g_ps.epollfd = -1; + atomic_store_explicit(&g_ps.channels[PS_CHANNEL_DISPLAY].available, + false, memory_order_release); +} + +int main(void) +{ + testStalledHandshake(); + testCancelBeforeConnect(); + testStalledChannelHandshake(); + puts("PureSpice cancellable connect test passed"); + return 0; +} diff --git a/client/tests/render_queue_test.c b/client/tests/render_queue_test.c index 3e554efd..2353eefb 100644 --- a/client/tests/render_queue_test.c +++ b/client/tests/render_queue_test.c @@ -611,17 +611,129 @@ static void testCache(void) clear, false, NULL) != 0); renderQueue_process(); CHECK(f.shapeCount == 2); - CHECK(f.cursorCount == 3); + CHECK(f.cursorCount == 4); CHECK(!f.cursorVisible); - CHECK(f.colorCount == 3); + CHECK(f.colorCount == 4); CHECK(f.colorFlags == 0); CHECK(f.colorScalar == 0.0f); - CHECK(f.whiteCount == 3); + CHECK(f.whiteCount == 4); CHECK(f.whiteLevel == LG_SDR_WHITE_LEVEL_DEFAULT); stop(); } +static void testCursorReplacement(void) +{ + start(); + + const uint64_t generation = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + const uint8_t shape[] = { 1, 2, 3, 4 }; + LGColorTransform color = + { + .flags = LG_COLOR_TRANSFORM_LUT, + .scalar = 2.0f, + }; + CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY, + generation, false, NULL) != 0); + renderQueue_process(); + renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation, + LG_CURSOR_COLOR, 1, 1, 4, shape); + renderQueue_sourceCursorColorTransform(RENDER_QUEUE_SOURCE_PRIMARY, + generation, &color); + renderQueue_sourceCursorWhiteLevel(RENDER_QUEUE_SOURCE_PRIMARY, + generation, 250); + renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation, + true, 5, 6, 1, 2); + renderQueue_process(); + CHECK(f.shapeCount == 1); + CHECK(f.cursorVisible); + f.shapeCount = 0; + f.colorCount = 0; + f.whiteCount = 0; + f.cursorCount = 0; + renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation, + LG_CURSOR_COLOR, 1, 1, 4, shape); + renderQueue_sourceCursorColorTransform(RENDER_QUEUE_SOURCE_PRIMARY, + generation, &color); + renderQueue_sourceCursorWhiteLevel(RENDER_QUEUE_SOURCE_PRIMARY, + generation, 250); + renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation, + true, 10, 11, 1, 2); + + renderQueue_sourceClearCursor(RENDER_QUEUE_SOURCE_PRIMARY); + CHECK(f.shapeCount == 0); + CHECK(f.colorCount == 0); + CHECK(f.whiteCount == 0); + CHECK(f.cursorCount == 0); + renderQueue_process(); + CHECK(f.shapeCount == 0); + CHECK(f.colorCount == 1); + CHECK(f.colorFlags == 0); + CHECK(f.whiteCount == 1); + CHECK(f.whiteLevel == LG_SDR_WHITE_LEVEL_DEFAULT); + CHECK(f.cursorCount == 1); + CHECK(!f.cursorVisible); + f.shapeCount = 0; + f.colorCount = 0; + f.whiteCount = 0; + f.cursorCount = 0; + renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation, + LG_CURSOR_COLOR, 1, 1, 4, shape); + renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation, + true, 12, 13, 1, 2); + renderQueue_process(); + CHECK(f.shapeCount == 1); + CHECK(f.cursorCount == 1); + CHECK(f.cursorVisible); + + f.shapeCount = 0; + f.colorCount = 0; + f.whiteCount = 0; + f.cursorCount = 0; + renderQueue_sourceClearCursor(RENDER_QUEUE_SOURCE_PRIMARY); + renderQueue_sourceInvalidate(RENDER_QUEUE_SOURCE_PRIMARY, generation); + const uint64_t replacement = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + CHECK(replacement != generation); + CHECK(f.shapeCount == 0); + CHECK(f.colorCount == 0); + CHECK(f.whiteCount == 0); + CHECK(f.cursorCount == 0); + renderQueue_process(); + CHECK(f.shapeCount == 0); + CHECK(f.colorCount == 1); + CHECK(f.colorFlags == 0); + CHECK(f.whiteCount == 1); + CHECK(f.whiteLevel == LG_SDR_WHITE_LEVEL_DEFAULT); + CHECK(f.cursorCount == 1); + CHECK(!f.cursorVisible); + + f.shapeCount = 0; + f.colorCount = 0; + f.whiteCount = 0; + f.cursorCount = 0; + CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY, + replacement, false, NULL) != 0); + renderQueue_process(); + f.shapeCount = 0; + f.colorCount = 0; + f.whiteCount = 0; + f.cursorCount = 0; + renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, replacement, + LG_CURSOR_COLOR, 1, 1, 4, shape); + renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, replacement, + true, 20, 21, 3, 4); + renderQueue_process(); + CHECK(f.shapeCount == 1); + CHECK(f.cursorCount == 1); + CHECK(f.cursorVisible); + CHECK(f.cursorX == 20); + CHECK(f.cursorY == 21); + + stop(); +} + struct Test { const char * name; @@ -637,6 +749,7 @@ static const struct Test tests[] = { "payload" , testPayload }, { "validation", testValidation }, { "cache" , testCache }, + { "cursor-replacement", testCursorReplacement }, }; int main(int argc, char ** argv) diff --git a/client/tests/spice_test.c b/client/tests/spice_test.c index c2420016..16b729c1 100644 --- a/client/tests/spice_test.c +++ b/client/tests/spice_test.c @@ -63,6 +63,11 @@ struct Pure atomic_bool connectOK; atomic_bool callReady; atomic_bool blockConnect; + atomic_bool enterConnect; + atomic_bool allowConnect; + atomic_bool blockChannelConnect; + atomic_bool enterChannelConnect; + atomic_bool channelCancel[PS_CHANNEL_MAX]; atomic_bool cancel; atomic_bool drop; PSStatus dropStatus; @@ -136,8 +141,18 @@ void purespice_init(const PSInit * init) (void)init; } +void purespice_beginConnect(void) +{ + atomic_store_explicit(&ps.cancel, false, memory_order_release); +} + bool purespice_connect(const PSConfig * config) { + while (!atomic_load_explicit(&ps.allowConnect, memory_order_acquire) && + !atomic_load_explicit(&ps.cancel, memory_order_acquire)) + usleep(1000); + atomic_store_explicit(&ps.enterConnect, true, memory_order_release); + ps.config = *config; ps.connectOrder = nextOrder(); atomic_fetch_add(&ps.connectN, 1); @@ -152,6 +167,23 @@ bool purespice_connect(const PSConfig * config) return true; } +void purespice_cancelConnect(void) +{ + atomic_store_explicit(&ps.cancel, true, memory_order_release); +} + +void purespice_beginChannelConnect(PSChannelType channel) +{ + atomic_store_explicit( + &ps.channelCancel[channel], false, memory_order_release); +} + +void purespice_cancelChannelConnect(PSChannelType channel) +{ + atomic_store_explicit( + &ps.channelCancel[channel], true, memory_order_release); +} + void purespice_disconnect(void) { ps.disconnectOrder = nextOrder(); @@ -201,6 +233,18 @@ bool purespice_channelConnected(PSChannelType channel) bool purespice_connectChannel(PSChannelType channel) { ++ps.connectChannelN[channel]; + atomic_store_explicit( + &ps.enterChannelConnect, true, memory_order_release); + while (atomic_load_explicit( + &ps.blockChannelConnect, memory_order_acquire) && + !atomic_load_explicit(&ps.cancel, memory_order_acquire) && + !atomic_load_explicit( + &ps.channelCancel[channel], memory_order_acquire)) + usleep(1000); + if (atomic_load_explicit(&ps.cancel, memory_order_acquire) || + atomic_load_explicit( + &ps.channelCancel[channel], memory_order_acquire)) + return false; if (!ps.connectResult[channel]) return false; ps.connected[channel] = true; @@ -335,6 +379,12 @@ static void resetPure(void) atomic_init(&ps.connectOK, true); atomic_init(&ps.callReady, true); atomic_init(&ps.blockConnect, false); + atomic_init(&ps.enterConnect, false); + atomic_init(&ps.allowConnect, true); + atomic_init(&ps.blockChannelConnect, false); + atomic_init(&ps.enterChannelConnect, false); + for(unsigned int i = 0; i < PS_CHANNEL_MAX; ++i) + atomic_init(&ps.channelCancel[i], false); atomic_init(&ps.cancel, false); atomic_init(&ps.drop, false); atomic_init(&ps.connectN, 0); @@ -485,10 +535,16 @@ static bool cancel(void * opaque) { unsigned int * calls = opaque; ++*calls; - atomic_store(&ps.cancel, true); return true; } +static bool cancelBeforeEntry(void * opaque) +{ + unsigned int * calls = opaque; + ++*calls; + return !atomic_load_explicit(&ps.enterConnect, memory_order_acquire); +} + static void testConnectFail(void) { resetPure(); @@ -560,6 +616,19 @@ static void testCancel(void) CHECK(f.clipboard.offN == 1); CHECK(!atomic_load(&cbTarget)); freeFixture(&f); + + resetPure(); + atomic_store_explicit(&ps.allowConnect, false, memory_order_release); + atomic_store_explicit(&ps.enterConnect, false, memory_order_release); + initFixture(&f); + cancelN = 0; + CHECK(LGT_SPICE.connectCancellable(&f.transport, &session, + cancelBeforeEntry, &cancelN) == LG_TRANSPORT_DISCONNECTED); + CHECK(cancelN == 1); + CHECK(!f.transport.thread); + CHECK(!LGT_SPICE.sessionValid(&f.transport)); + CHECK(atomic_load_explicit(&ps.cancel, memory_order_acquire)); + freeFixture(&f); } static void testReadyDrop(void) @@ -704,6 +773,148 @@ static void testSurfaceActive(void) freeFixture(&f); } +struct ActiveCall +{ + const LG_SwSurfaceOps * ops; + LG_Transport * transport; + atomic_bool done; + bool result; +}; + +static void * activateSurface(void * opaque) +{ + struct ActiveCall * call = opaque; + call->result = call->ops->setActive(call->transport, true); + atomic_store_explicit(&call->done, true, memory_order_release); + return NULL; +} + +static void * holdActivationLock(void * opaque) +{ + LG_Transport * transport = opaque; + spiceSurface_testLockActivation(transport->surface); + atomic_store_explicit( + &ps.enterChannelConnect, true, memory_order_release); + while (atomic_load_explicit( + &ps.blockChannelConnect, memory_order_acquire)) + usleep(1000); + spiceSurface_testUnlockActivation(transport->surface); + return NULL; +} + +static void testSurfaceCancel(void) +{ + resetPure(); + struct Fixture f; + initFixture(&f); + const LG_SwSurfaceOps * sw = spiceSurface_getVideoOps()->swSurface; + CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log)); + atomic_store_explicit( + &f.transport.sessionValid, true, memory_order_release); + atomic_store_explicit( + &ps.blockChannelConnect, true, memory_order_release); + + struct ActiveCall call = + { + .ops = sw, + .transport = &f.transport, + }; + atomic_init(&call.done, false); + pthread_t thread; + CHECK(pthread_create(&thread, NULL, activateSurface, &call) == 0); + waitBool(&ps.enterChannelConnect); + + sw->cancelPending(&f.transport); + waitBool(&call.done); + CHECK(pthread_join(thread, NULL) == 0); + CHECK(!call.result); + CHECK(atomic_load_explicit( + &ps.channelCancel[PS_CHANNEL_DISPLAY], memory_order_acquire)); + CHECK(!atomic_load_explicit(&ps.cancel, memory_order_acquire)); + CHECK(ps.connectChannelN[PS_CHANNEL_DISPLAY] == 1); + CHECK(ps.connectChannelN[PS_CHANNEL_CURSOR] == 0); + CHECK(LGT_SPICE.sessionValid(&f.transport)); + + atomic_store_explicit( + &ps.blockChannelConnect, false, memory_order_release); + CHECK(sw->setActive(&f.transport, true)); + CHECK(LGT_SPICE.sessionValid(&f.transport)); + CHECK(sw->setActive(&f.transport, false)); + + sw->detach(&f.transport); + freeFixture(&f); + + resetPure(); + initFixture(&f); + sw = spiceSurface_getVideoOps()->swSurface; + CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log)); + atomic_store_explicit( + &f.transport.sessionValid, true, memory_order_release); + atomic_store_explicit( + &ps.blockChannelConnect, true, memory_order_release); + + pthread_t holdThread; + CHECK(pthread_create( + &holdThread, NULL, holdActivationLock, &f.transport) == 0); + waitBool(&ps.enterChannelConnect); + + call = (struct ActiveCall) + { + .ops = sw, + .transport = &f.transport, + }; + atomic_init(&call.done, false); + CHECK(pthread_create(&thread, NULL, activateSurface, &call) == 0); + usleep(10000); + sw->cancelPending(&f.transport); + atomic_store_explicit( + &ps.blockChannelConnect, false, memory_order_release); + CHECK(pthread_join(holdThread, NULL) == 0); + waitBool(&call.done); + CHECK(pthread_join(thread, NULL) == 0); + CHECK(!call.result); + CHECK(ps.connectChannelN[PS_CHANNEL_DISPLAY] == 0); + + sw->detach(&f.transport); + freeFixture(&f); + + resetPure(); + initFixture(&f); + sw = spiceSurface_getVideoOps()->swSurface; + CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log)); + atomic_store_explicit( + &f.transport.sessionValid, true, memory_order_release); + atomic_store_explicit( + &ps.blockChannelConnect, true, memory_order_release); + + call = (struct ActiveCall) + { + .ops = sw, + .transport = &f.transport, + }; + atomic_init(&call.done, false); + CHECK(pthread_create(&thread, NULL, activateSurface, &call) == 0); + waitBool(&ps.enterChannelConnect); + + atomic_store_explicit( + &f.transport.sessionValid, false, memory_order_release); + spiceSurface_sessionStopped(f.transport.surface); + waitBool(&call.done); + CHECK(pthread_join(thread, NULL) == 0); + CHECK(!call.result); + CHECK(atomic_load_explicit( + &ps.channelCancel[PS_CHANNEL_DISPLAY], memory_order_acquire)); + CHECK(atomic_load_explicit( + &ps.channelCancel[PS_CHANNEL_CURSOR], memory_order_acquire)); + CHECK(!atomic_load_explicit(&ps.cancel, memory_order_acquire)); + CHECK(sw->setActive(&f.transport, false)); + + atomic_store_explicit( + &ps.blockChannelConnect, false, memory_order_release); + sw->detach(&f.transport); + freeFixture(&f); +} + static void testSurfaceEvents(void) { resetPure(); @@ -864,6 +1075,7 @@ static const struct Test tests[] = { "ready-drop" , testReadyDrop }, { "session-order" , testSessionOrder }, { "surface-active", testSurfaceActive }, + { "surface-cancel", testSurfaceCancel }, { "surface-events", testSurfaceEvents }, { "surface-detach", testSurfaceDetach }, }; diff --git a/client/tests/sw_surface_test.c b/client/tests/sw_surface_test.c new file mode 100644 index 00000000..5ef731a8 --- /dev/null +++ b/client/tests/sw_surface_test.c @@ -0,0 +1,84 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + */ + +#include "sw_surface.h" +#include "test.h" + +#include +#include +#include + +struct LG_Transport +{ + atomic_bool entered; + atomic_bool armed; + atomic_bool release; + atomic_uint cancelCount; + atomic_uint on; + atomic_uint off; +}; + +static bool setActive(LG_Transport * transport, bool active) +{ + atomic_fetch_add(active ? &transport->on : &transport->off, 1); + atomic_store(&transport->entered, true); + if (active) + { + /* Lose the first cancellation edge before arming the wait. */ + while (!atomic_load(&transport->cancelCount)) + usleep(1000); + atomic_store(&transport->release, false); + atomic_store(&transport->armed, true); + while (!atomic_load(&transport->release)) + usleep(1000); + } + return !active || atomic_load(&transport->release); +} + +static void cancelPending(LG_Transport * transport) +{ + atomic_fetch_add(&transport->cancelCount, 1); + if (atomic_load(&transport->armed)) + atomic_store(&transport->release, true); +} + +static bool cancelled(void * opaque) +{ + return *(const bool *)opaque; +} + +static const LG_SwSurfaceOps ops = +{ + .setActive = setActive, + .cancelPending = cancelPending, +}; + +int main(void) +{ + LG_Transport transport = { 0 }; + const bool stop = true; + CHECK(!lgSwSurface_setActive( + &ops, &transport, true, cancelled, (void *)&stop)); + CHECK(atomic_load(&transport.entered)); + CHECK(atomic_load(&transport.cancelCount) >= 2); + CHECK(atomic_load(&transport.on) == 1); + CHECK(atomic_load(&transport.off) == 1); + + atomic_store(&transport.entered, false); + atomic_store(&transport.release, false); + atomic_store(&transport.cancelCount, 0); + CHECK(lgSwSurface_setActive( + &ops, &transport, false, cancelled, (void *)&stop)); + CHECK(atomic_load(&transport.entered)); + CHECK(atomic_load(&transport.cancelCount) == 0); + CHECK(atomic_load(&transport.off) == 2); + return EXIT_SUCCESS; +} diff --git a/client/tests/transport_fallback_test.c b/client/tests/transport_fallback_test.c index fa53cda1..9b090d76 100644 --- a/client/tests/transport_fallback_test.c +++ b/client/tests/transport_fallback_test.c @@ -63,6 +63,16 @@ struct TransportTrace atomic_bool release; atomic_bool cancel; atomic_bool badProvider; + atomic_bool noVideo; + atomic_bool failAttach; + atomic_bool failActive; + atomic_bool blockActive; + atomic_bool activeEntered; + atomic_bool activeCancelled; + atomic_bool holdActiveReturn; + atomic_uint cancelPending; + const LG_SwSurfaceEventOps * surfaceEvents; + void * surfaceOpaque; atomic_uint_fast64_t connTime[4]; unsigned int failConn; bool uuidValid; @@ -74,6 +84,11 @@ struct EventTrace atomic_uint conn; atomic_uint disc; atomic_uint mismatch; + atomic_uint videoReady; + atomic_uint videoUnavailable; + atomic_uint configureAccepted; + atomic_uint surfaceCleanup; + atomic_bool surfaceRetained; LG_TransportSession session; uint8_t primary[16]; uint8_t fallback[16]; @@ -86,6 +101,7 @@ static struct ProviderTrace pInput; static struct ProviderTrace pAudio; #endif static struct ProviderTrace pClipboard; +static _Atomic(LG_TransportFallback *) surfaceFallback; static const uint8_t UUID_A[16] = { 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, @@ -125,6 +141,18 @@ static bool waitReady(const LG_TransportFallback * fallback) return false; } +static bool waitUsable(const LG_TransportFallback * fallback) +{ + for (unsigned int i = 0; i < TEST_WAIT_MS; ++i) + { + if (lgTransportFallback_usable(fallback)) + return true; + usleep(1000); + } + + return false; +} + static void setRemote(const uint8_t uuid[16]) { t.uuidValid = true; @@ -203,8 +231,10 @@ static bool fakeAttach(LG_Transport * transport, { CHECK(transport); CHECK(events); + t.surfaceEvents = events; + t.surfaceOpaque = opaque; atomic_fetch_add(&t.attach, 1); - return true; + return !atomic_load(&t.failAttach); } static void fakeDetach(LG_Transport * transport) @@ -217,7 +247,20 @@ static bool fakeActive(LG_Transport * transport, bool active) { CHECK(transport); atomic_fetch_add(active ? &t.on : &t.off, 1); - return true; + atomic_store(&t.activeEntered, true); + while (atomic_load(&t.blockActive) && + !atomic_load(&t.activeCancelled)) + usleep(1000); + while (active && atomic_load(&t.holdActiveReturn)) + usleep(1000); + return !atomic_load(&t.failActive); +} + +static void fakeCancelPending(LG_Transport * transport) +{ + CHECK(transport); + atomic_fetch_add(&t.cancelPending, 1); + atomic_store(&t.activeCancelled, true); } static const LG_InputOps inputOps = @@ -264,6 +307,7 @@ static const LG_SwSurfaceOps surfaceOps = .attach = fakeAttach, .detach = fakeDetach, .setActive = fakeActive, + .cancelPending = fakeCancelPending, }; static const LG_VideoOps videoOps = @@ -276,7 +320,7 @@ static const LG_VideoOps videoOps = static const LG_VideoOps * fakeGetVideo(LG_Transport * transport) { CHECK(transport); - return &videoOps; + return atomic_load(&t.noVideo) ? NULL : &videoOps; } static const LG_TransportOps transportOps = @@ -389,20 +433,48 @@ static void onMismatch(void * opaque, const uint8_t primary[16], atomic_fetch_add(&e.mismatch, 1); } +static void onVideoState(void * opaque, bool ready) +{ + CHECK(opaque == &e); + atomic_fetch_add(ready ? &e.videoReady : &e.videoUnavailable, 1); + if (!ready && atomic_exchange(&e.surfaceRetained, false)) + atomic_fetch_add(&e.surfaceCleanup, 1); +} + +static void onSurfaceConfigure(void * opaque, + unsigned int width, unsigned int height) +{ + CHECK(opaque == &e); + CHECK(width == 1920); + CHECK(height == 1080); + LG_TransportFallback * fallback = atomic_load(&surfaceFallback); + CHECK(fallback); + if (!lgTransportFallback_acceptsVideoEvents(fallback)) + return; + + atomic_store(&e.surfaceRetained, true); + atomic_fetch_add(&e.configureAccepted, 1); +} + static LG_TransportFallback * start(const uint8_t primary[16]) { - static const LG_SwSurfaceEventOps surfaceEvents = { 0 }; + static const LG_SwSurfaceEventOps surfaceEvents = + { + .configure = onSurfaceConfigure, + }; static const LG_TransportFallbackEventOps eventOps = { .connected = onConnected, .disconnected = onDisconnected, - .uuidMismatch = onMismatch, + .videoStateChanged = onVideoState, + .endpointMismatch = onMismatch, }; LG_TransportFallback * fallback = NULL; - CHECK(lgTransportFallback_start("fake", &surfaceEvents, NULL, + CHECK(lgTransportFallback_start("fake", &surfaceEvents, &e, &eventOps, &e, primary, &fallback)); CHECK(fallback); + atomic_store(&surfaceFallback, fallback); return fallback; } @@ -438,8 +510,9 @@ static void testGraceful(void) atomic_store(&t.release, true); CHECK(waitReady(fallback)); CHECK(waitCount(&e.conn, 1)); + CHECK(waitCount(&e.videoReady, 1)); - CHECK(lgTransportFallback_admitted(fallback)); + CHECK(lgTransportFallback_usable(fallback)); CHECK(atomic_load(&t.on) == 1); CHECK(memcmp(e.session.uuid, UUID_A, sizeof(UUID_A)) == 0); checkPublished(1, 0, 0); @@ -459,17 +532,17 @@ static void testDead(void) { setRemote(UUID_A); LG_TransportFallback * fallback = start(UUID_A); - CHECK(waitReady(fallback)); CHECK(waitCount(&e.conn, 1)); lgTransportFallback_requestVideoActive(fallback, true); CHECK(waitCount(&t.on, 1)); + CHECK(waitReady(fallback)); atomic_store(&t.alive, false); lgTransportFallback_requestVideoActive(fallback, true); CHECK(waitCount(&e.disc, 1)); CHECK(!lgTransportFallback_ready(fallback)); - CHECK(!lgTransportFallback_admitted(fallback)); + CHECK(!lgTransportFallback_usable(fallback)); CHECK(atomic_load(&t.off) == 0); checkPublished(1, 0, 1); @@ -487,7 +560,7 @@ static void testMismatch(void) CHECK(waitCount(&t.free, 2)); CHECK(!lgTransportFallback_ready(fallback)); - CHECK(!lgTransportFallback_admitted(fallback)); + CHECK(!lgTransportFallback_usable(fallback)); CHECK(atomic_load(&e.conn) == 0); CHECK(atomic_load(&e.disc) == 0); CHECK(atomic_load(&e.mismatch) == 1); @@ -503,11 +576,27 @@ static void testMismatch(void) CHECK(atomic_load(&t.free) == 2); } -static void testRevoke(void) +static void testMissingUUID(void) +{ + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitCount(&e.conn, 1)); + CHECK(lgTransportFallback_usable(fallback)); + CHECK(atomic_load(&e.mismatch) == 0); + lgTransportFallback_stop(&fallback); + + setRemote(UUID_B); + fallback = start(NULL); + CHECK(waitCount(&e.conn, 2)); + CHECK(lgTransportFallback_usable(fallback)); + CHECK(atomic_load(&e.mismatch) == 0); + lgTransportFallback_stop(&fallback); + checkPublished(2, 2, 0); +} + +static void testLateMismatch(void) { setRemote(UUID_A); LG_TransportFallback * fallback = start(UUID_A); - CHECK(waitReady(fallback)); CHECK(waitCount(&e.conn, 1)); lgTransportFallback_setPrimaryUUID(fallback, UUID_B); @@ -515,7 +604,7 @@ static void testRevoke(void) CHECK(waitCount(&t.free, 2)); CHECK(!lgTransportFallback_ready(fallback)); - CHECK(!lgTransportFallback_admitted(fallback)); + CHECK(!lgTransportFallback_usable(fallback)); CHECK(atomic_load(&e.conn) == 1); CHECK(atomic_load(&e.disc) == 1); CHECK(atomic_load(&e.mismatch) == 1); @@ -535,7 +624,6 @@ static void testRetry(void) setRemote(UUID_A); t.failConn = 2; LG_TransportFallback * fallback = start(UUID_A); - CHECK(waitReady(fallback)); CHECK(waitCount(&e.conn, 1)); const uint64_t first = atomic_load(&t.connTime[0]); @@ -549,8 +637,8 @@ static void testRetry(void) lgTransportFallback_stop(&fallback); CHECK(atomic_load(&e.conn) == 1); CHECK(atomic_load(&e.disc) == 1); - CHECK(atomic_load(&t.attach) == 3); - CHECK(atomic_load(&t.detach) == 3); + CHECK(atomic_load(&t.attach) == 1); + CHECK(atomic_load(&t.detach) == 1); CHECK(atomic_load(&t.disc) == 3); CHECK(atomic_load(&t.free) == 3); checkPublished(1, 1, 0); @@ -568,13 +656,191 @@ static void testCancel(void) CHECK(atomic_load(&t.cancel)); CHECK(atomic_load(&e.conn) == 0); CHECK(atomic_load(&e.disc) == 0); - CHECK(atomic_load(&t.attach) == 1); - CHECK(atomic_load(&t.detach) == 1); + CHECK(atomic_load(&t.attach) == 0); + CHECK(atomic_load(&t.detach) == 0); CHECK(atomic_load(&t.disc) == 1); CHECK(atomic_load(&t.free) == 1); checkPublished(0, 0, 0); } +static void testWithoutVideo(void) +{ + setRemote(UUID_A); + atomic_store(&t.noVideo, true); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitUsable(fallback)); + CHECK(waitCount(&e.conn, 1)); + CHECK(!lgTransportFallback_ready(fallback)); + checkPublished(1, 0, 0); + + lgTransportFallback_requestVideoActive(fallback, true); + usleep(50000); + CHECK(atomic_load(&t.on) == 0); + CHECK(lgTransportFallback_usable(fallback)); + + lgTransportFallback_stop(&fallback); + CHECK(atomic_load(&t.attach) == 0); + CHECK(atomic_load(&t.detach) == 0); + checkPublished(1, 1, 0); +} + +static void testAttachFailure(void) +{ + setRemote(UUID_A); + atomic_store(&t.failAttach, true); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitUsable(fallback)); + CHECK(waitCount(&e.conn, 1)); + CHECK(!lgTransportFallback_ready(fallback)); + CHECK(atomic_load(&t.attach) == 1); + CHECK(atomic_load(&t.detach) == 0); + checkPublished(1, 0, 0); + lgTransportFallback_stop(&fallback); + checkPublished(1, 1, 0); +} + +static void testActiveFailure(void) +{ + setRemote(UUID_A); + atomic_store(&t.failActive, true); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitCount(&e.conn, 1)); + lgTransportFallback_requestVideoActive(fallback, true); + CHECK(waitCount(&t.on, 1)); + CHECK(waitCount(&e.videoUnavailable, 1)); + CHECK(lgTransportFallback_usable(fallback)); + CHECK(!lgTransportFallback_ready(fallback)); + checkPublished(1, 0, 0); + lgTransportFallback_stop(&fallback); + CHECK(atomic_load(&t.detach) == 1); + checkPublished(1, 1, 0); +} + +static void testActivationCancel(void) +{ + setRemote(UUID_A); + atomic_store(&t.blockActive, true); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitCount(&e.conn, 1)); + lgTransportFallback_requestVideoActive(fallback, true); + CHECK(waitCount(&t.on, 1)); + CHECK(atomic_load(&t.activeEntered)); + lgTransportFallback_stop(&fallback); + CHECK(!fallback); + CHECK(atomic_load(&t.cancelPending) > 0); + CHECK(atomic_load(&t.detach) == 1); + CHECK(atomic_load(&t.disc) == 1); + CHECK(atomic_load(&t.free) == 1); +} + +static void testActivationRequestCancel(void) +{ + setRemote(UUID_A); + atomic_store(&t.blockActive, true); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitCount(&e.conn, 1)); + + lgTransportFallback_requestVideoActive(fallback, true); + CHECK(waitCount(&t.on, 1)); + CHECK(atomic_load(&t.activeEntered)); + CHECK(lgTransportFallback_acceptsVideoEvents(fallback)); + CHECK(t.surfaceEvents); + CHECK(t.surfaceEvents->configure); + t.surfaceEvents->configure(t.surfaceOpaque, 1920, 1080); + CHECK(atomic_load(&e.configureAccepted) == 1); + CHECK(atomic_load(&e.surfaceRetained)); + + lgTransportFallback_requestVideoActive(fallback, false); + CHECK(waitCount(&t.cancelPending, 1)); + CHECK(waitCount(&t.off, 1)); + CHECK(waitCount(&e.videoUnavailable, 1)); + usleep(250000); + + CHECK(atomic_load(&e.videoUnavailable) == 1); + CHECK(atomic_load(&e.videoReady) == 0); + CHECK(atomic_load(&e.configureAccepted) == 1); + CHECK(atomic_load(&e.surfaceCleanup) == 1); + CHECK(!atomic_load(&e.surfaceRetained)); + CHECK(!lgTransportFallback_ready(fallback)); + CHECK(!lgTransportFallback_acceptsVideoEvents(fallback)); + CHECK(lgTransportFallback_usable(fallback)); + checkPublished(1, 0, 0); + + lgTransportFallback_stop(&fallback); + CHECK(!fallback); + CHECK(atomic_load(&t.off) == 1); + CHECK(atomic_load(&t.detach) == 1); + CHECK(atomic_load(&t.disc) == 1); + CHECK(atomic_load(&t.free) == 1); + checkPublished(1, 1, 0); +} + +static void testDeactivationCompletes(void) +{ + setRemote(UUID_A); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitCount(&e.conn, 1)); + lgTransportFallback_requestVideoActive(fallback, true); + CHECK(waitReady(fallback)); + atomic_store(&t.activeEntered, false); + lgTransportFallback_stop(&fallback); + CHECK(!fallback); + CHECK(atomic_load(&t.off) == 1); + CHECK(atomic_load(&t.cancelPending) == 0); + CHECK(atomic_load(&t.detach) == 1); + CHECK(atomic_load(&t.disc) == 1); + CHECK(atomic_load(&t.free) == 1); +} + +static void testMismatchDuringActivation(void) +{ + setRemote(UUID_A); + atomic_store(&t.holdActiveReturn, true); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitCount(&e.conn, 1)); + lgTransportFallback_requestVideoActive(fallback, true); + CHECK(waitCount(&t.on, 1)); + CHECK(atomic_load(&t.activeEntered)); + lgTransportFallback_setPrimaryUUID(fallback, UUID_B); + CHECK(waitCount(&e.disc, 1)); + atomic_store(&t.holdActiveReturn, false); + CHECK(waitCount(&t.free, 1)); + CHECK(!lgTransportFallback_ready(fallback)); + CHECK(!lgTransportFallback_usable(fallback)); + CHECK(!lgTransportFallback_acceptsVideoEvents(fallback)); + CHECK(atomic_load(&e.videoReady) == 0); + lgTransportFallback_stop(&fallback); +} + +static void testPreRequestedActivation(void) +{ + setRemote(UUID_A); + atomic_store(&t.hold, true); + atomic_store(&t.blockActive, true); + LG_TransportFallback * fallback = start(UUID_A); + CHECK(waitCount(&t.conn, 1)); + lgTransportFallback_requestVideoActive(fallback, true); + atomic_store(&t.release, true); + CHECK(waitCount(&e.conn, 1)); + CHECK(waitCount(&t.on, 1)); + CHECK(atomic_load(&t.activeEntered)); + CHECK(!lgTransportFallback_ready(fallback)); + + atomic_store(&t.blockActive, false); + CHECK(waitReady(fallback)); + CHECK(waitCount(&e.videoReady, 1)); + lgTransportFallback_setPrimaryUUID(fallback, UUID_B); + CHECK(waitCount(&e.disc, 1)); + CHECK(waitCount(&t.free, 1)); + CHECK(atomic_load(&e.conn) == 1); + CHECK(atomic_load(&e.disc) == 1); + CHECK(atomic_load(&e.mismatch) == 1); + CHECK(!lgTransportFallback_usable(fallback)); + CHECK(!lgTransportFallback_ready(fallback)); + lgTransportFallback_stop(&fallback); +} + + struct Test { const char * name; @@ -586,9 +852,18 @@ static const struct Test tests[] = { "graceful", testGraceful }, { "dead" , testDead }, { "mismatch", testMismatch }, - { "revoke" , testRevoke }, + { "missing-uuid", testMissingUUID }, + { "late-mismatch", testLateMismatch }, { "retry" , testRetry }, { "cancel" , testCancel }, + { "no-video", testWithoutVideo }, + { "attach-fail", testAttachFailure }, + { "active-fail", testActiveFailure }, + { "activation-cancel", testActivationCancel }, + { "activation-request-cancel", testActivationRequestCancel }, + { "deactivation-completes", testDeactivationCompletes }, + { "activation-mismatch", testMismatchDuringActivation }, + { "pre-requested-activation", testPreRequestedActivation }, }; int main(int argc, char ** argv) diff --git a/client/transports/LGMP/lgmp.c b/client/transports/LGMP/lgmp.c index 9c301551..17cdfaa2 100644 --- a/client/transports/LGMP/lgmp.c +++ b/client/transports/LGMP/lgmp.c @@ -65,10 +65,42 @@ struct LGMPFrameLease LG_TransportFrameFormat format; uint64_t handle; uint32_t generation; + uint64_t receiveTime; + uint64_t prepareTime; + bool providerValid; bool releaseRequested; bool active; }; +typedef struct LGMPVideoStatusUpdate +{ + bool frame; + bool frameAvailable; + bool frameReplaced; + LG_TransportStatus frameReason; + bool pointer; + bool pointerAvailable; + bool pointerReplaced; + LG_TransportStatus pointerReason; +} +LGMPVideoStatusUpdate; + +typedef struct LGMPVideoStatusDispatch +{ + LG_VideoStatusFn callback; + void * opaque; + uint64_t listenerSerial; + bool pending; +} +LGMPVideoStatusDispatch; + +typedef struct LGMPVideoCallbackContext +{ + struct LGMPVideoCallbackContext * previous; + LG_Transport * transport; +} +LGMPVideoCallbackContext; + struct LG_Transport { struct IVSHMEM shm; @@ -79,16 +111,28 @@ struct LG_Transport LGMPInput * input; LG_Lock frameLock; LG_Lock pointerLock; + LG_RWLock videoStatusLock; LGEvent * frameWake; LGEvent * pointerWake; + LG_VideoStatusFn videoStatusCallback; + void * videoStatusOpaque; + atomic_uint videoStatusCallbacks; + uint64_t videoStatusListenerSerial; + uint64_t videoFrameEpoch; + uint64_t videoPointerEpoch; + LG_TransportStatus videoFrameReason; + LG_TransportStatus videoPointerReason; + bool videoFrameAvailable; + bool videoPointerAvailable; + unsigned cursorPollInterval; unsigned framePollInterval; bool allowDMA; - bool connected; + atomic_bool connected; bool frameStopRequested; bool frameScheduleSupported; - bool inputSupported; + atomic_bool inputSupported; uint64_t frameLeaseHandle; uint32_t frameGeneration; uint32_t clientID; @@ -111,8 +155,249 @@ struct LG_Transport uint32_t recoveryCandidateHeartbeat; uint32_t recoveryLGMPVersion; bool recoveryLive; + atomic_bool destroyPending; }; +static LG_Lock l_videoStatusCallbackLock = ATOMIC_FLAG_INIT; +static _Thread_local LGMPVideoCallbackContext * l_videoStatusContext; + +#ifdef ENABLE_TESTS +static atomic_bool * l_videoStatusDispatchWaiting; +static atomic_bool * l_videoStatusDispatchHold; +static LG_Transport * l_videoStatusDispatchTarget; +static LG_Lock l_videoStatusTestLock = ATOMIC_FLAG_INIT; + +void lgmp_testSetVideoStatusDispatchGate( + LG_Transport * target, atomic_bool * waiting, atomic_bool * hold) +{ + LG_LOCK(l_videoStatusTestLock); + l_videoStatusDispatchTarget = target; + l_videoStatusDispatchWaiting = waiting; + l_videoStatusDispatchHold = hold; + LG_UNLOCK(l_videoStatusTestLock); +} +#endif + +static void lgmp_setVideoStatusListener(LG_Transport * this, + LG_VideoStatusFn callback, void * callbackOpaque); +static void lgmp_destroyNow(LG_Transport * this); +static void lgmp_releaseFrame(LG_Transport * this, + LG_TransportFrame * frame); + +static unsigned lgmp_videoStatusOwnCallbacks(const LG_Transport * transport) +{ + unsigned count = 0; + for (const LGMPVideoCallbackContext * context = l_videoStatusContext; + context; context = context->previous) + count += context->transport == transport; + return count; +} + +static bool lgmp_videoStatusContextContains(const LG_Transport * transport) +{ + for (const LGMPVideoCallbackContext * context = l_videoStatusContext; + context; context = context->previous) + if (context->transport == transport) + return true; + return false; +} + +static void lgmp_waitVideoStatusCallbacks( + LG_Transport * transport, unsigned ownCallbacks) +{ + while (atomic_load_explicit( + &transport->videoStatusCallbacks, memory_order_acquire) > + ownCallbacks) + nsleep(1000000U); + + /* A retiring callback publishes the count while holding this lock. Taking + * it here ensures that callback has completed every access to the instance + * before an external caller tears it down. */ + LG_LOCK_EXCLUSIVE(transport->videoStatusLock); + LG_UNLOCK_EXCLUSIVE(transport->videoStatusLock); +} + +static bool lgmp_releaseVideoStatusCallback(LG_Transport * transport) +{ + /* Linearize the final reference and a nested destroy request. Whichever + * side acquires the lock first either observes the pending request or leaves + * a zero count for the destroy path to observe. */ + LG_LOCK_EXCLUSIVE(transport->videoStatusLock); + const bool destroy = atomic_load_explicit( + &transport->destroyPending, memory_order_acquire); + const unsigned previous = atomic_fetch_sub_explicit( + &transport->videoStatusCallbacks, 1, memory_order_acq_rel); + DEBUG_ASSERT(previous != 0); + LG_UNLOCK_EXCLUSIVE(transport->videoStatusLock); + return destroy && previous == 1; +} + +static void lgmp_retainVideoStatusLifetime(LG_Transport * transport) +{ + atomic_fetch_add_explicit( + &transport->videoStatusCallbacks, 1, memory_order_acq_rel); +} + +static bool lgmp_destroyRequested(const LG_Transport * transport) +{ + return atomic_load_explicit( + &transport->destroyPending, memory_order_acquire); +} + +static void lgmp_releaseVideoStatusLifetime(LG_Transport * transport) +{ + if (lgmp_releaseVideoStatusCallback(transport)) + lgmp_destroyNow(transport); +} + +static uint64_t lgmp_nextEpoch(uint64_t epoch) +{ + if (++epoch == 0) + ++epoch; + return epoch; +} + +static void lgmp_callVideoStatusCallback(LG_VideoStatusFn callback, + void * opaque, + LG_Transport * transport, + const LG_VideoStatus * status) +{ + LGMPVideoCallbackContext context = + { + .previous = l_videoStatusContext, + .transport = transport, + }; + l_videoStatusContext = &context; + callback(opaque, status); + l_videoStatusContext = context.previous; +} + +static LG_VideoStatus lgmp_videoStatusLocked(const LG_Transport * this) +{ + return (LG_VideoStatus) + { + .frame = + { + .available = this->videoFrameAvailable, + .epoch = this->videoFrameEpoch, + .reason = this->videoFrameAvailable ? LG_TRANSPORT_OK : + this->videoFrameReason, + }, + .pointer = + { + .available = this->videoPointerAvailable, + .epoch = this->videoPointerEpoch, + .reason = this->videoPointerAvailable ? LG_TRANSPORT_OK : + this->videoPointerReason, + }, + }; +} + +static LG_VideoStatus lgmp_updateVideoStatus(LG_Transport * this, + const LGMPVideoStatusUpdate update, + LGMPVideoStatusDispatch * dispatch) +{ + *dispatch = (LGMPVideoStatusDispatch) { 0 }; + LG_VideoStatus published; + + LG_LOCK_EXCLUSIVE(this->videoStatusLock); + const LG_TransportStatus frameReason = update.frameAvailable ? + LG_TRANSPORT_OK : update.frameReason == LG_TRANSPORT_OK ? + LG_TRANSPORT_UNAVAILABLE : update.frameReason; + const LG_TransportStatus pointerReason = update.pointerAvailable ? + LG_TRANSPORT_OK : update.pointerReason == LG_TRANSPORT_OK ? + LG_TRANSPORT_UNAVAILABLE : update.pointerReason; + const bool frameChanged = update.frame && + this->videoFrameAvailable != update.frameAvailable; + const bool pointerChanged = update.pointer && + this->videoPointerAvailable != update.pointerAvailable; + const bool frameReasonChanged = update.frame && + this->videoFrameReason != frameReason; + const bool pointerReasonChanged = update.pointer && + this->videoPointerReason != pointerReason; + if (frameChanged || update.frameReplaced) + this->videoFrameEpoch = lgmp_nextEpoch(this->videoFrameEpoch); + if (pointerChanged || update.pointerReplaced) + this->videoPointerEpoch = lgmp_nextEpoch(this->videoPointerEpoch); + if (update.frame) + { + this->videoFrameAvailable = update.frameAvailable; + this->videoFrameReason = frameReason; + } + if (update.pointer) + { + this->videoPointerAvailable = update.pointerAvailable; + this->videoPointerReason = pointerReason; + } + published = lgmp_videoStatusLocked(this); + + dispatch->callback = this->videoStatusCallback; + dispatch->opaque = this->videoStatusOpaque; + dispatch->listenerSerial = this->videoStatusListenerSerial; + if (!dispatch->callback || (!frameChanged && !pointerChanged && + !frameReasonChanged && !pointerReasonChanged && + !update.frameReplaced && !update.pointerReplaced)) + dispatch->callback = NULL; + if (dispatch->callback) + atomic_fetch_add_explicit( + &this->videoStatusCallbacks, 1, memory_order_acq_rel); + dispatch->pending = dispatch->callback != NULL; + LG_UNLOCK_EXCLUSIVE(this->videoStatusLock); + return published; +} + +static void lgmp_dispatchVideoStatus(LG_Transport * this, + const LGMPVideoStatusDispatch * dispatch) +{ + if (dispatch->pending) + { + const bool serialized = l_videoStatusContext == NULL; + if (serialized) + { +#ifdef ENABLE_TESTS + LG_LOCK(l_videoStatusTestLock); + LG_Transport * target = l_videoStatusDispatchTarget; + atomic_bool * waiting = l_videoStatusDispatchWaiting; + atomic_bool * hold = l_videoStatusDispatchHold; + LG_UNLOCK(l_videoStatusTestLock); + if ((!target || target == this) && waiting) + atomic_store_explicit( + waiting, true, memory_order_release); + if ((!target || target == this) && hold) + while (atomic_load_explicit( + hold, memory_order_acquire)) + nsleep(1000000U); +#endif + LG_LOCK(l_videoStatusCallbackLock); + } + LG_LOCK_SHARED(this->videoStatusLock); + const bool current = + this->videoStatusCallback == dispatch->callback && + this->videoStatusOpaque == dispatch->opaque && + this->videoStatusListenerSerial == dispatch->listenerSerial; + const LG_VideoStatus status = lgmp_videoStatusLocked(this); + LG_UNLOCK_SHARED(this->videoStatusLock); + if (current) + lgmp_callVideoStatusCallback( + dispatch->callback, dispatch->opaque, this, &status); + const bool destroy = lgmp_releaseVideoStatusCallback(this); + if (serialized) + LG_UNLOCK(l_videoStatusCallbackLock); + if (destroy) + lgmp_destroyNow(this); + } +} + +static LG_VideoStatus lgmp_publishVideoStatus(LG_Transport * this, + const LGMPVideoStatusUpdate update) +{ + LGMPVideoStatusDispatch dispatch; + const LG_VideoStatus published = + lgmp_updateVideoStatus(this, update, &dispatch); + lgmp_dispatchVideoStatus(this, &dispatch); + return published; +} + static bool lgmp_deviceValidator(struct Option * opt, const char ** error) { const char * transport = option_get_string("app", "transport"); @@ -238,13 +523,23 @@ static bool lgmp_recoverySnapshot(const struct LG_Transport * this, return true; } -static bool lgmp_refreshRecoveryLocked(struct LG_Transport * this, bool wait) +static bool lgmp_cancelled(LG_TransportCancelledFn cancelled, void * opaque) +{ + return cancelled && cancelled(opaque); +} + +static bool lgmp_refreshRecoveryLockedCancellable( + struct LG_Transport * this, bool wait, + LG_TransportCancelledFn cancelled, void * opaque) { const uint64_t deadline = wait ? microtime() + LGMP_RECOVERY_PROBE_TIMEOUT_US : 0; do { + if (lgmp_cancelled(cancelled, opaque)) + break; + KVMFRRHeader header; KVMFRRInfo info; const bool valid = lgmp_recoverySnapshot(this, &header, &info); @@ -293,6 +588,12 @@ static bool lgmp_refreshRecoveryLocked(struct LG_Transport * this, bool wait) return false; } +static bool lgmp_refreshRecoveryLocked(struct LG_Transport * this, bool wait) +{ + return lgmp_refreshRecoveryLockedCancellable( + this, wait, NULL, NULL); +} + static bool lgmp_recoveryVersions(struct LG_Transport * this, uint32_t * lgmpVersion, uint32_t * kvmfrVersion) { @@ -347,6 +648,9 @@ static bool lgmp_create(LG_Transport ** result) } this->framePollInterval = framePoll; this->cursorPollInterval = cursorPoll; + atomic_init(&this->connected, false); + atomic_init(&this->inputSupported, false); + atomic_init(&this->destroyPending, false); this->frameWake = lgCreateEvent(true, 0); this->pointerWake = lgCreateEvent(true, 0); @@ -365,7 +669,9 @@ static bool lgmp_create(LG_Transport ** result) LG_LOCK_INIT(this->frameLock); LG_LOCK_INIT(this->pointerLock); + LG_RWLOCK_INIT(this->videoStatusLock); LG_LOCK_INIT(this->recoveryLock); + atomic_init(&this->videoStatusCallbacks, 0); this->frameGeneration = 1; this->frameLease[0].subscription = &this->frameQueue; @@ -377,6 +683,7 @@ static bool lgmp_create(LG_Transport ** result) { LG_LOCK_FREE(this->frameLock); LG_LOCK_FREE(this->pointerLock); + LG_RWLOCK_FREE(this->videoStatusLock); LG_LOCK_FREE(this->recoveryLock); lgFreeEvent(this->frameWake); lgFreeEvent(this->pointerWake); @@ -415,6 +722,7 @@ static bool lgmp_create(LG_Transport ** result) ivshmemClose(&this->shm); LG_LOCK_FREE(this->frameLock); LG_LOCK_FREE(this->pointerLock); + LG_RWLOCK_FREE(this->videoStatusLock); LG_LOCK_FREE(this->recoveryLock); lgFreeEvent(this->frameWake); lgFreeEvent(this->pointerWake); @@ -527,6 +835,13 @@ static void lgmp_closeQueues(struct LG_Transport * this) { lgmp_stopFrame(this); lgmp_stopPointer(this); + lgmp_publishVideoStatus(this, (LGMPVideoStatusUpdate) + { + .frame = true, + .frameReason = LG_TRANSPORT_DISCONNECTED, + .pointer = true, + .pointerReason = LG_TRANSPORT_DISCONNECTED, + }); } static void lgmp_closeDMA(struct LG_Transport * this) @@ -541,12 +856,8 @@ static void lgmp_closeDMA(struct LG_Transport * this) } } -static void lgmp_destroy(LG_Transport ** transport) +static void lgmp_destroyNow(LG_Transport * this) { - if (!transport || !*transport) - return; - - struct LG_Transport * this = *transport; if (this->client) { lgmpInput_destroy(&this->input); @@ -558,11 +869,36 @@ static void lgmp_destroy(LG_Transport ** transport) ivshmemClose(&this->shm); LG_LOCK_FREE(this->frameLock); LG_LOCK_FREE(this->pointerLock); + LG_RWLOCK_FREE(this->videoStatusLock); LG_LOCK_FREE(this->recoveryLock); lgFreeEvent(this->frameWake); lgFreeEvent(this->pointerWake); free(this); +} + +static void lgmp_destroy(LG_Transport ** transport) +{ + if (!transport || !*transport) + return; + + struct LG_Transport * this = *transport; *transport = NULL; + const bool nested = l_videoStatusContext != NULL; + lgmp_setVideoStatusListener(this, NULL, NULL); + if (nested) + { + LG_LOCK_EXCLUSIVE(this->videoStatusLock); + atomic_store_explicit( + &this->destroyPending, true, memory_order_release); + const bool deferred = lgmp_videoStatusContextContains(this) || + atomic_load_explicit( + &this->videoStatusCallbacks, memory_order_acquire) != 0; + LG_UNLOCK_EXCLUSIVE(this->videoStatusLock); + if (deferred) + return; + } + + lgmp_destroyNow(this); } static void lgmp_setVersionMismatch(LG_TransportSession * session, @@ -675,19 +1011,26 @@ static bool lgmp_parseSession(struct LG_Transport * this, return true; } -static LG_TransportStatus lgmp_connect(LG_Transport * this, - LG_TransportSession * session) +static LG_TransportStatus lgmp_connectInternal(LG_Transport * this, + LG_TransportSession * session, LG_TransportCancelledFn cancelled, + void * opaque) { memset(session, 0, sizeof(*session)); session->os = LG_TRANSPORT_OS_OTHER; + if (lgmp_cancelled(cancelled, opaque)) + return LG_TRANSPORT_DISCONNECTED; + if (!this->client) { LG_LOCK(this->recoveryLock); - lgmp_refreshRecoveryLocked(this, - lgmp_recoveryMagic(this->recovery)); + lgmp_refreshRecoveryLockedCancellable(this, + lgmp_recoveryMagic(this->recovery), cancelled, opaque); LG_UNLOCK(this->recoveryLock); + if (lgmp_cancelled(cancelled, opaque)) + return LG_TRANSPORT_DISCONNECTED; + const LGMP_STATUS status = lgmp_initializeClient(this); if (status != LGMP_OK) { @@ -709,6 +1052,9 @@ static LG_TransportStatus lgmp_connect(LG_Transport * this, } } + if (lgmp_cancelled(cancelled, opaque)) + return LG_TRANSPORT_DISCONNECTED; + uint32_t size; uint8_t * data; uint32_t remoteVersion = 0; @@ -720,19 +1066,28 @@ static LG_TransportStatus lgmp_connect(LG_Transport * this, if (!lgmp_parseSession(this, data, size, session)) return LG_TRANSPORT_INVALID_VERSION; + LGMPVideoStatusDispatch dispatch; LG_LOCK(this->frameLock); if (++this->frameGeneration == 0) ++this->frameGeneration; - this->connected = true; this->frameStopRequested = false; this->frameScheduleSupported = session->features & LG_TRANSPORT_FEATURE_FRAME_SCHEDULE; - this->inputSupported = - session->features & LG_TRANSPORT_FEATURE_INPUT; + atomic_store_explicit(&this->inputSupported, + session->features & LG_TRANSPORT_FEATURE_INPUT, + memory_order_release); this->frameSerial = 0; this->frameSerialValid = false; this->formatValid = false; + lgmp_updateVideoStatus(this, (LGMPVideoStatusUpdate) + { + .frame = true, + .pointer = true, + }, &dispatch); + atomic_store_explicit( + &this->connected, true, memory_order_release); LG_UNLOCK(this->frameLock); + lgmp_dispatchVideoStatus(this, &dispatch); return LG_TRANSPORT_OK; case LGMP_ERR_INVALID_VERSION: @@ -761,32 +1116,68 @@ static LG_TransportStatus lgmp_connect(LG_Transport * this, } } +static LG_TransportStatus lgmp_connect(LG_Transport * this, + LG_TransportSession * session) +{ + lgmp_retainVideoStatusLifetime(this); + LG_TransportStatus status = + lgmp_connectInternal(this, session, NULL, NULL); + if (lgmp_destroyRequested(this)) + status = LG_TRANSPORT_DISCONNECTED; + lgmp_releaseVideoStatusLifetime(this); + return status; +} + static void lgmp_disconnect(LG_Transport * this) { if (!this->client) return; + lgmp_retainVideoStatusLifetime(this); + lgmpInput_disconnect(this->input); - lgmp_closeQueues(this); LG_LOCK(this->frameLock); if (++this->frameGeneration == 0) ++this->frameGeneration; - this->frameQueue = NULL; - for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) - this->ownerFrameQueue[i] = NULL; - this->connected = false; + atomic_store_explicit( + &this->connected, false, memory_order_release); this->frameScheduleSupported = false; - this->inputSupported = false; + atomic_store_explicit( + &this->inputSupported, false, memory_order_release); this->clientID = 0; LG_UNLOCK(this->frameLock); + lgmp_closeQueues(this); lgmp_closeDMA(this); + lgmp_releaseVideoStatusLifetime(this); +} + +static LG_TransportStatus lgmp_connectCancellable(LG_Transport * this, + LG_TransportSession * session, LG_TransportCancelledFn cancelled, + void * opaque) +{ + lgmp_retainVideoStatusLifetime(this); + const LG_TransportStatus status = lgmp_connectInternal( + this, session, cancelled, opaque); + LG_TransportStatus result = status; + if (lgmp_destroyRequested(this)) + result = LG_TRANSPORT_DISCONNECTED; + else if (lgmp_cancelled(cancelled, opaque)) + { + if (status == LG_TRANSPORT_OK) + lgmp_disconnect(this); + result = LG_TRANSPORT_DISCONNECTED; + } + + lgmp_releaseVideoStatusLifetime(this); + return result; } static bool lgmp_sessionValid(LG_Transport * this) { - return this->client && this->connected && + return this->client && atomic_load_explicit( + &this->connected, memory_order_acquire) && lgmpClientSessionValid(this->client); } @@ -834,11 +1225,36 @@ static LG_RecoveryError lgmp_recoveryError(uint32_t error) return LG_RECOVERY_ERR_TOPOLOGY_FAILED; case KVMFR_R_ERR_NO_FALLBACK_DISPLAY: return LG_RECOVERY_ERR_NO_FALLBACK_DISPLAY; + case KVMFR_R_ERR_BUSY: + return LG_RECOVERY_ERR_BUSY; + case KVMFR_R_ERR_CAPACITY: + return LG_RECOVERY_ERR_CAPACITY; default: return LG_RECOVERY_ERR_UNSUPPORTED; } } +#ifdef ENABLE_TESTS +LG_RecoveryError lgmp_testRecoveryError(uint32_t error) +{ + return lgmp_recoveryError(error); +} + +bool lgmp_testRecoveryProbeCancellation( + LG_TransportCancelledFn cancelled, void * opaque) +{ + struct LG_Transport transport = + { + .lgmpSize = 1, + }; + LG_LOCK_INIT(transport.recoveryLock); + const bool result = lgmp_refreshRecoveryLockedCancellable( + &transport, true, cancelled, opaque); + LG_LOCK_FREE(transport.recoveryLock); + return result; +} +#endif + static bool lgmp_recoveryRequestSnapshot(const KVMFRRRequest * source, KVMFRRRequest * result) { @@ -1380,13 +1796,14 @@ static int lgmp_getDMA(struct LG_Transport * this, const KVMFRFrame * frame, } static void lgmp_releaseFrameLease(void * opaque, uint64_t handle); +static bool lgmp_frameTimingReady(const KVMFRFrame * frame); static LG_TransportStatus lgmp_nextFrameLocked(LG_Transport * this, bool useDMA, LG_TransportFrame * result) { lgmp_drainFrameLeasesLocked(this); - if (!this->connected) + if (!atomic_load_explicit(&this->connected, memory_order_acquire)) return LG_TRANSPORT_DISCONNECTED; this->frameStopRequested = false; @@ -1513,6 +1930,9 @@ static LG_TransportStatus lgmp_nextFrameLocked(LG_Transport * this, return done == LG_TRANSPORT_OK ? LG_TRANSPORT_TIMEOUT : done; } + const bool providerValid = lgmp_frameTimingReady(frame); + const uint64_t providerStart = providerValid ? nanotime() : 0; + const bool fullDamage = !this->frameSerialValid || equalSerial || frame->frameSerial != this->frameSerial + 1; this->frameSerial = frame->frameSerial; @@ -1602,6 +2022,9 @@ static LG_TransportStatus lgmp_nextFrameLocked(LG_Transport * this, ++this->frameLeaseHandle; lease->handle = this->frameLeaseHandle; lease->generation = this->frameGeneration; + lease->providerValid = providerValid; + lease->receiveTime = 0; + lease->prepareTime = providerValid ? nanotime() - providerStart : 0; lease->releaseRequested = false; lease->active = true; result->releaseFn = lgmp_releaseFrameLease; @@ -1613,14 +2036,57 @@ static LG_TransportStatus lgmp_nextFrameLocked(LG_Transport * this, static LG_TransportStatus lgmp_nextFrame(LG_Transport * this, bool useDMA, LG_TransportFrame * result) { + lgmp_retainVideoStatusLifetime(this); LG_LOCK(this->frameLock); + bool subscribed[LGMP_FRAME_LEASE_COUNT]; + subscribed[0] = this->frameQueue != NULL; + for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) + subscribed[i + 1] = this->ownerFrameQueue[i] != NULL; const LG_TransportStatus status = lgmp_nextFrameLocked(this, useDMA, result); + bool available = this->frameQueue != NULL; + bool replaced = subscribed[0] != available; + for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) + { + const bool ownerAvailable = this->ownerFrameQueue[i] != NULL; + available |= ownerAvailable; + replaced |= subscribed[i + 1] != ownerAvailable; + } + if (status == LG_TRANSPORT_OK) + { + struct LGMPFrameLease * lease = + lgmp_findFrameLeaseLocked(this, result->releaseHandle); + DEBUG_ASSERT(lease); + } + if (status == LG_TRANSPORT_DISCONNECTED || status == LG_TRANSPORT_ERROR) + available = false; + LGMPVideoStatusDispatch dispatch; + const LG_VideoStatus published = lgmp_updateVideoStatus(this, + (LGMPVideoStatusUpdate) + { + .frame = true, + .frameAvailable = available, + .frameReplaced = replaced, + .frameReason = status, + }, &dispatch); + if (status == LG_TRANSPORT_OK) + result->epoch = published.frame.epoch; LG_UNLOCK(this->frameLock); - if ((status == LG_TRANSPORT_TIMEOUT || - status == LG_TRANSPORT_UNAVAILABLE) && this->framePollInterval) + lgmp_dispatchVideoStatus(this, &dispatch); + LG_TransportStatus resultStatus = status; + if (lgmp_destroyRequested(this)) + { + if (status == LG_TRANSPORT_OK) + lgmp_releaseFrame(this, result); + resultStatus = LG_TRANSPORT_DISCONNECTED; + } + else if ((status == LG_TRANSPORT_TIMEOUT || + status == LG_TRANSPORT_UNAVAILABLE || + (status == LG_TRANSPORT_ERROR && !available)) && + this->framePollInterval) lgmp_waitPoll(this->frameWake, this->framePollInterval); - return status; + lgmp_releaseVideoStatusLifetime(this); + return resultStatus; } static void lgmp_cancelFrameWait(LG_Transport * this) @@ -1651,6 +2117,13 @@ static void lgmp_getFrameTiming(LG_Transport * this, return; } + timing->providerValid = lease->providerValid; + if (lease->providerValid) + { + timing->receiveTime = lease->receiveTime; + timing->prepareTime = lease->prepareTime; + } + /* The producer writes these immediately after publishing FrameBuffer::wp. * nextFrame can observe the header earlier, so briefly observe the * publication tail after onFrame consumes the framebuffer without sleeping @@ -1719,8 +2192,17 @@ static void lgmp_releaseFrame(LG_Transport * this, LG_TransportFrame * frame) static LG_TransportStatus lgmp_nextPointer(LG_Transport * this, LG_TransportPointer * result) { - if (!this->connected) + lgmp_retainVideoStatusLifetime(this); + if (!atomic_load_explicit(&this->connected, memory_order_acquire)) + { + lgmp_publishVideoStatus(this, (LGMPVideoStatusUpdate) + { + .pointer = true, + .pointerReason = LG_TRANSPORT_DISCONNECTED, + }); + lgmp_releaseVideoStatusLifetime(this); return LG_TRANSPORT_DISCONNECTED; + } uint32_t pointerFlags = 0; size_t shapeSize = 0; @@ -1728,6 +2210,7 @@ static LG_TransportStatus lgmp_nextPointer(LG_Transport * this, LG_TransportStatus status = LG_TRANSPORT_OK; PLGMPClientQueue pointerQueue; LG_LOCK(this->pointerLock); + const bool wasAvailable = this->pointerQueue != NULL; if (!this->pointerQueue) { status = lgmp_subscribe(this->client, LGMP_Q_POINTER, @@ -1798,10 +2281,58 @@ static LG_TransportStatus lgmp_nextPointer(LG_Transport * this, } } if (status != LG_TRANSPORT_OK) - return status; + { + LGMPVideoStatusDispatch dispatch; + LG_LOCK(this->pointerLock); + bool available = atomic_load_explicit( + &this->connected, memory_order_acquire) && + this->pointerQueue != NULL; + if (status == LG_TRANSPORT_DISCONNECTED || status == LG_TRANSPORT_ERROR) + available = false; + lgmp_updateVideoStatus(this, (LGMPVideoStatusUpdate) + { + .pointer = true, + .pointerAvailable = available, + .pointerReplaced = !wasAvailable && available, + .pointerReason = status, + }, &dispatch); + LG_UNLOCK(this->pointerLock); + lgmp_dispatchVideoStatus(this, &dispatch); + LG_TransportStatus resultStatus = status; + if (lgmp_destroyRequested(this)) + resultStatus = LG_TRANSPORT_DISCONNECTED; + else if (status == LG_TRANSPORT_ERROR && !available && + this->cursorPollInterval) + lgmp_waitPoll(this->pointerWake, this->cursorPollInterval); + lgmp_releaseVideoStatusLifetime(this); + return resultStatus; + } + + LGMPVideoStatusDispatch dispatch; + LG_LOCK(this->pointerLock); + const bool current = + atomic_load_explicit(&this->connected, memory_order_acquire) && + this->pointerQueue == pointerQueue; + const LG_VideoStatus published = lgmp_updateVideoStatus(this, + (LGMPVideoStatusUpdate) + { + .pointer = true, + .pointerAvailable = current, + .pointerReplaced = current && !wasAvailable, + .pointerReason = current ? LG_TRANSPORT_OK : + LG_TRANSPORT_DISCONNECTED, + }, &dispatch); + LG_UNLOCK(this->pointerLock); + lgmp_dispatchVideoStatus(this, &dispatch); + if (!current || lgmp_destroyRequested(this)) + { + lgmp_releaseVideoStatusLifetime(this); + return LG_TRANSPORT_DISCONNECTED; + } const KVMFRCursor * cursor = (const KVMFRCursor *)this->pointerData; memset(result, 0, sizeof(*result)); + result->epoch = published.pointer.epoch; if (pointerFlags & CURSOR_FLAG_POSITION) result->flags |= LG_TRANSPORT_POINTER_POSITION; if (pointerFlags & CURSOR_FLAG_VISIBLE) @@ -1825,6 +2356,7 @@ static LG_TransportStatus lgmp_nextPointer(LG_Transport * this, if (transformSize) result->colorTransform = (const LGColorTransform *)(result->shape + shapeSize); + lgmp_releaseVideoStatusLifetime(this); return LG_TRANSPORT_OK; } @@ -1933,7 +2465,8 @@ static const LG_InputOps * lgmp_getInputOps(LG_Transport * this, void ** opaque) { *opaque = NULL; - if (!this->connected || !this->inputSupported || + if (!atomic_load_explicit(&this->connected, memory_order_acquire) || + !atomic_load_explicit(&this->inputSupported, memory_order_acquire) || !lgmpInput_connect(this->input, this->clientID)) return NULL; @@ -1941,8 +2474,111 @@ static const LG_InputOps * lgmp_getInputOps(LG_Transport * this, return lgmpInput_getOps(); } +static void lgmp_probeVideoStatus(LG_Transport * this) +{ + bool frameAvailable = false; + bool pointerAvailable = false; + LGMPVideoStatusDispatch frameDispatch; + LGMPVideoStatusDispatch pointerDispatch; + + LG_LOCK(this->frameLock); + const bool frameConnected = atomic_load_explicit( + &this->connected, memory_order_acquire); + if (frameConnected) + { + lgmp_subscribe(this->client, LGMP_Q_FRAME, &this->frameQueue); + frameAvailable = this->frameQueue != NULL; + if (this->frameScheduleSupported) + for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) + { + lgmp_subscribe(this->client, LGMP_Q_FRAME_OWNER + i, + &this->ownerFrameQueue[i]); + frameAvailable |= this->ownerFrameQueue[i] != NULL; + } + } + lgmp_updateVideoStatus(this, (LGMPVideoStatusUpdate) + { + .frame = true, + .frameAvailable = frameAvailable, + .frameReason = frameConnected ? LG_TRANSPORT_UNAVAILABLE : + LG_TRANSPORT_DISCONNECTED, + }, &frameDispatch); + LG_UNLOCK(this->frameLock); + lgmp_dispatchVideoStatus(this, &frameDispatch); + + LG_LOCK(this->pointerLock); + const bool pointerConnected = atomic_load_explicit( + &this->connected, memory_order_acquire); + if (pointerConnected) + { + lgmp_subscribe(this->client, LGMP_Q_POINTER, &this->pointerQueue); + pointerAvailable = this->pointerQueue != NULL; + } + lgmp_updateVideoStatus(this, (LGMPVideoStatusUpdate) + { + .pointer = true, + .pointerAvailable = pointerAvailable, + .pointerReason = pointerConnected ? LG_TRANSPORT_UNAVAILABLE : + LG_TRANSPORT_DISCONNECTED, + }, &pointerDispatch); + LG_UNLOCK(this->pointerLock); + lgmp_dispatchVideoStatus(this, &pointerDispatch); +} + +static void lgmp_setVideoStatusListener(LG_Transport * this, + LG_VideoStatusFn callback, void * callbackOpaque) +{ + LG_VideoStatus status; + const bool nested = l_videoStatusContext != NULL; + const unsigned ownCallbacks = + lgmp_videoStatusOwnCallbacks(this); + + if (!nested) + LG_LOCK(l_videoStatusCallbackLock); + LG_LOCK_EXCLUSIVE(this->videoStatusLock); + ++this->videoStatusListenerSerial; + this->videoStatusCallback = NULL; + this->videoStatusOpaque = NULL; + LG_UNLOCK_EXCLUSIVE(this->videoStatusLock); + + if (callback) + lgmp_probeVideoStatus(this); + + LG_LOCK_EXCLUSIVE(this->videoStatusLock); + this->videoStatusCallback = callback; + this->videoStatusOpaque = callbackOpaque; + status = lgmp_videoStatusLocked(this); + LG_UNLOCK_EXCLUSIVE(this->videoStatusLock); + + if (callback) + { + atomic_fetch_add_explicit( + &this->videoStatusCallbacks, 1, memory_order_acq_rel); + lgmp_callVideoStatusCallback( + callback, callbackOpaque, this, &status); + + /* Releasing this callback's lifetime reference is the final access to the + * instance unless this thread becomes the deferred destroy owner. */ + const bool destroy = lgmp_releaseVideoStatusCallback(this); + if (!nested) + LG_UNLOCK(l_videoStatusCallbackLock); + if (destroy) + lgmp_destroyNow(this); + return; + } + + if (nested) + return; + + /* Publications accepted before unregistration retain lifetime references, + * but will skip the cleared listener when they acquire serialization. */ + LG_UNLOCK(l_videoStatusCallbackLock); + lgmp_waitVideoStatusCallbacks(this, ownCallbacks); +} + static const LG_FrameOps lgmpFrameOps = { + .setStatusListener = lgmp_setVideoStatusListener, .supportsDMA = lgmp_supportsDMA, .attachRenderer = lgmp_attachRenderer, .detachRenderer = lgmp_detachRenderer, @@ -1975,8 +2611,9 @@ const LG_TransportOps LGT_LGMP = .setup = lgmp_setup, .create = lgmp_create, .destroy = lgmp_destroy, - .connect = lgmp_connect, - .disconnect = lgmp_disconnect, + .connect = lgmp_connect, + .connectCancellable = lgmp_connectCancellable, + .disconnect = lgmp_disconnect, .sessionValid = lgmp_sessionValid, .getVideoOps = lgmp_getVideoOps, .getInputOps = lgmp_getInputOps, diff --git a/client/transports/SPICE/spice.c b/client/transports/SPICE/spice.c index c35dd919..4310aae8 100644 --- a/client/transports/SPICE/spice.c +++ b/client/transports/SPICE/spice.c @@ -237,6 +237,7 @@ static LG_TransportStatus spiceConnectCancellable(LG_Transport * transport, atomic_store_explicit( &transport->sessionValid, false, memory_order_release); lgResetEvent(transport->connectEvent); + purespice_beginConnect(); if (!lgCreateThread( "spiceProcess", spiceSession_thread, transport, &transport->thread)) @@ -248,6 +249,7 @@ static LG_TransportStatus spiceConnectCancellable(LG_Transport * transport, { cancel = true; atomic_store_explicit(&transport->stop, true, memory_order_release); + purespice_cancelConnect(); lgWaitEvent(transport->connectEvent, TIMEOUT_INFINITE); break; } @@ -255,6 +257,7 @@ static LG_TransportStatus spiceConnectCancellable(LG_Transport * transport, if (cancel || (cancelled && cancelled(opaque))) { atomic_store_explicit(&transport->stop, true, memory_order_release); + purespice_cancelConnect(); lgJoinThread(transport->thread, NULL); transport->thread = NULL; return LG_TRANSPORT_DISCONNECTED; @@ -283,6 +286,7 @@ static void spiceDisconnect(LG_Transport * transport) return; atomic_store_explicit(&transport->stop, true, memory_order_release); + purespice_cancelConnect(); lgJoinThread(transport->thread, NULL); transport->thread = NULL; } diff --git a/client/transports/SPICE/surface.c b/client/transports/SPICE/surface.c index d6fa029e..982ea53f 100644 --- a/client/transports/SPICE/surface.c +++ b/client/transports/SPICE/surface.c @@ -32,6 +32,7 @@ struct SpiceSurface { LG_RWLock eventsLock; LG_Lock activationLock; + atomic_uint_fast64_t activationCancel; const LG_SwSurfaceEventOps * events; void * eventOpaque; @@ -68,8 +69,17 @@ static void surfaceDetach(LG_Transport * transport) static bool surfaceSetActive(LG_Transport * transport, bool active) { SpiceSurface * surface = transport->surface; + const uint64_t cancel = atomic_load_explicit( + &surface->activationCancel, memory_order_acquire); LG_LOCK(surface->activationLock); + if (cancel != atomic_load_explicit( + &surface->activationCancel, memory_order_acquire)) + { + LG_UNLOCK(surface->activationLock); + return false; + } + if (surface->active == active) { LG_UNLOCK(surface->activationLock); @@ -88,8 +98,15 @@ static bool surfaceSetActive(LG_Transport * transport, bool active) if (!atomic_load_explicit( &transport->sessionValid, memory_order_acquire) || !purespice_hasChannel(PS_CHANNEL_DISPLAY) || - !purespice_hasChannel(PS_CHANNEL_CURSOR) || - !purespice_connectChannel(PS_CHANNEL_DISPLAY)) + !purespice_hasChannel(PS_CHANNEL_CURSOR)) + goto done; + + purespice_beginChannelConnect(PS_CHANNEL_DISPLAY); + purespice_beginChannelConnect(PS_CHANNEL_CURSOR); + if (cancel != atomic_load_explicit( + &surface->activationCancel, memory_order_acquire)) + goto done; + if (!purespice_connectChannel(PS_CHANNEL_DISPLAY)) goto done; if (!purespice_connectChannel(PS_CHANNEL_CURSOR)) @@ -105,11 +122,23 @@ static bool surfaceSetActive(LG_Transport * transport, bool active) if (!purespice_disconnectChannel(PS_CHANNEL_CURSOR)) { + purespice_beginChannelConnect(PS_CHANNEL_DISPLAY); + if (cancel != atomic_load_explicit( + &surface->activationCancel, memory_order_acquire)) + goto done; purespice_connectChannel(PS_CHANNEL_DISPLAY); goto done; } } + if (active && cancel != atomic_load_explicit( + &surface->activationCancel, memory_order_acquire)) + { + purespice_disconnectChannel(PS_CHANNEL_CURSOR); + purespice_disconnectChannel(PS_CHANNEL_DISPLAY); + goto done; + } + surface->active = active; result = true; @@ -118,11 +147,25 @@ done: return result; } +static void surfaceCancelActivation(SpiceSurface * surface) +{ + atomic_fetch_add_explicit(&surface->activationCancel, + 1, memory_order_acq_rel); + purespice_cancelChannelConnect(PS_CHANNEL_DISPLAY); + purespice_cancelChannelConnect(PS_CHANNEL_CURSOR); +} + +static void surfaceCancelPending(LG_Transport * transport) +{ + surfaceCancelActivation(transport->surface); +} + static const LG_SwSurfaceOps swSurfaceOps = { - .attach = surfaceAttach, - .detach = surfaceDetach, - .setActive = surfaceSetActive, + .attach = surfaceAttach, + .detach = surfaceDetach, + .setActive = surfaceSetActive, + .cancelPending = surfaceCancelPending, }; static const LG_VideoOps videoOps = @@ -143,6 +186,7 @@ bool spiceSurface_init(SpiceSurface ** result) LG_RWLOCK_INIT(surface->eventsLock); LG_LOCK_INIT(surface->activationLock); + atomic_init(&surface->activationCancel, 0); *result = surface; return true; } @@ -165,12 +209,25 @@ const LG_VideoOps * spiceSurface_getVideoOps(void) void spiceSurface_sessionStopped(SpiceSurface * surface) { + surfaceCancelActivation(surface); LG_LOCK(surface->activationLock); surface->active = false; surface->primaryValid = false; LG_UNLOCK(surface->activationLock); } +#ifdef ENABLE_TESTS +void spiceSurface_testLockActivation(SpiceSurface * surface) +{ + LG_LOCK(surface->activationLock); +} + +void spiceSurface_testUnlockActivation(SpiceSurface * surface) +{ + LG_UNLOCK(surface->activationLock); +} +#endif + void spiceSurface_create(SpiceSurface * surface, unsigned int surfaceId, PSSurfaceFormat format, unsigned int width, unsigned int height) { diff --git a/client/transports/SPICE/surface.h b/client/transports/SPICE/surface.h index 05e0c4e9..1ef21d44 100644 --- a/client/transports/SPICE/surface.h +++ b/client/transports/SPICE/surface.h @@ -33,6 +33,11 @@ void spiceSurface_free(SpiceSurface ** surface); const LG_VideoOps * spiceSurface_getVideoOps(void); void spiceSurface_sessionStopped(SpiceSurface * surface); +#ifdef ENABLE_TESTS +void spiceSurface_testLockActivation(SpiceSurface * surface); +void spiceSurface_testUnlockActivation(SpiceSurface * surface); +#endif + void spiceSurface_create(SpiceSurface * surface, unsigned int surfaceId, PSSurfaceFormat format, unsigned int width, unsigned int height); void spiceSurface_destroy(SpiceSurface * surface, unsigned int surfaceId); diff --git a/client/transports/Test/test.c b/client/transports/Test/test.c index af3ff031..cdf0b8d0 100644 --- a/client/transports/Test/test.c +++ b/client/transports/Test/test.c @@ -86,6 +86,7 @@ struct LG_Transport bool framePending; uint64_t serial; uint64_t nextFrameTime; + uint64_t framePrepareTime; unsigned bufferIndex; struct TestBuffer buffers[TEST_BUFFER_COUNT]; FrameDamageRect damage[LG_TRANSPORT_MAX_DAMAGE_RECTS]; @@ -416,6 +417,23 @@ static LG_TransportStatus test_connect(LG_Transport * this, return LG_TRANSPORT_OK; } +static void test_disconnect(LG_Transport * this); + +static LG_TransportStatus test_connectCancellable(LG_Transport * this, + LG_TransportSession * session, LG_TransportCancelledFn cancelled, + void * opaque) +{ + if (cancelled && cancelled(opaque)) + return LG_TRANSPORT_DISCONNECTED; + + const LG_TransportStatus status = test_connect(this, session); + if (!cancelled || !cancelled(opaque)) + return status; + + test_disconnect(this); + return LG_TRANSPORT_DISCONNECTED; +} + static void test_disconnect(LG_Transport * this) { this->connected = false; @@ -581,6 +599,7 @@ static void test_generateFrame(struct LG_Transport * this, FrameBuffer * fb) static LG_TransportStatus test_nextFrame(LG_Transport * this, bool useDMA, LG_TransportFrame * frame) { + const uint64_t prepareStart = nanotime(); if (!this->connected) return LG_TRANSPORT_DISCONNECTED; if (this->framePending) @@ -614,6 +633,7 @@ static LG_TransportStatus test_nextFrame(LG_Transport * this, bool useDMA, memset(frame, 0, sizeof(*frame)); frame->serial = this->serial; + frame->epoch = 1; frame->timestamp = this->realtime ? nanotime() : this->serial * (1000000000ULL / this->frameRate); frame->format = &this->format; @@ -688,16 +708,33 @@ static LG_TransportStatus test_nextFrame(LG_Transport * this, bool useDMA, } this->framePending = true; + this->framePrepareTime = nanotime() - prepareStart; this->nextFrameTime += 1000000000ULL / this->frameRate; return LG_TRANSPORT_OK; } +static void test_getFrameTiming(LG_Transport * this, + const LG_TransportFrame * frame, LG_TransportFrameTiming * timing) +{ + memset(timing, 0, sizeof(*timing)); + if (!this->framePending || frame->serial != this->serial) + return; + + timing->providerValid = true; + timing->prepareTime = this->framePrepareTime; +} + static void test_releaseFrame(LG_Transport * this, LG_TransportFrame * frame) { this->framePending = false; memset(frame, 0, sizeof(*frame)); } +static void test_cancelFrameWait(LG_Transport * this) +{ + (void)this; +} + static LG_TransportStatus test_nextPointer(LG_Transport * this, LG_TransportPointer * pointer) { @@ -712,6 +749,11 @@ static void test_releasePointer(LG_Transport * this, { } +static void test_cancelPointerWait(LG_Transport * this) +{ + (void)this; +} + static LG_TransportStatus test_sendControl(LG_Transport * this, const LG_TransportControl * control, LG_TransportControlToken * token) { @@ -740,9 +782,12 @@ static const LG_FrameOps testFrameOps = .attachRenderer = test_attachRenderer, .detachRenderer = test_detachRenderer, .nextFrame = test_nextFrame, + .getFrameTiming = test_getFrameTiming, .releaseFrame = test_releaseFrame, + .cancelFrameWait = test_cancelFrameWait, .nextPointer = test_nextPointer, .releasePointer = test_releasePointer, + .cancelPointerWait = test_cancelPointerWait, }; static const LG_VideoOps testVideoOps = @@ -764,6 +809,7 @@ const LG_TransportOps LGT_Test = .create = test_create, .destroy = test_destroy, .connect = test_connect, + .connectCancellable = test_connectCancellable, .disconnect = test_disconnect, .sessionValid = test_sessionValid, .getVideoOps = test_getVideoOps, diff --git a/repos/PureSpice b/repos/PureSpice index b21f8b20..1c0b0820 160000 --- a/repos/PureSpice +++ b/repos/PureSpice @@ -1 +1 @@ -Subproject commit b21f8b20e2ed00c5f29ce5ae28f8dd9156fc4d22 +Subproject commit 1c0b08204ba614c861f1e253293b065cfb2093dd