[client] transport: track video component availability

This commit is contained in:
Geoffrey McRae
2026-08-12 22:40:07 +10:00
parent c92acb0a34
commit 4768396180
28 changed files with 3965 additions and 284 deletions

View File

@@ -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_PROPERTY:purespice,INCLUDE_DIRECTORIES>"
)
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}

View File

@@ -25,6 +25,7 @@
#undef main
#include <math.h>
#include <pthread.h>
#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)

View File

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

View File

@@ -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 <purespice.h>
#include "ps.h"
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
#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;
}

View File

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

View File

@@ -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 },
};

View File

@@ -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 <stdatomic.h>
#include <stdlib.h>
#include <unistd.h>
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;
}

View File

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