Files
LookingGlass/client/tests/lgmp_transport_test.c
2026-08-13 04:08:49 +10:00

354 lines
11 KiB
C

/**
* 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.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "interface/transport.h"
#include "common/KVMFR.h"
#include "common/LGMPConfig.h"
#include "common/debug.h"
#include "common/option.h"
#include <lgmp/host.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#define TEST_SHM_SIZE (2U * 1024U * 1024U)
#define TEST_TIMEOUT 20U
#define WAIT_TIMEOUT (TEST_TIMEOUT + 100U)
extern const LG_TransportOps LGT_LGMP;
#define CHECK(x) \
do \
{ \
if (!(x)) \
{ \
fprintf(stderr, "check failed at %s:%d: %s\n", __FILE__, __LINE__, #x); \
goto cleanup; \
} \
} \
while (0)
static bool waitForQueuesEmpty(PLGMPHost host, PLGMPHostQueue frameQueue,
PLGMPHostQueue pointerQueue)
{
for (unsigned i = 0; i < WAIT_TIMEOUT; ++i)
{
if (lgmpHostProcess(host) != LGMP_OK)
return false;
if (lgmpHostQueuePending(frameQueue) == 0 &&
lgmpHostQueuePending(pointerQueue) == 0)
return true;
usleep(1000);
}
return false;
}
struct WaitState
{
LG_Transport * transport;
const LG_FrameOps * ops;
LG_TransportStatus status;
bool frame;
};
static void * waitForVideo(void * opaque)
{
struct WaitState * state = opaque;
if (state->frame)
{
LG_TransportFrame frame;
state->status = state->ops->nextFrame(
state->transport, false, &frame);
}
else
{
LG_TransportPointer pointer;
state->status = state->ops->nextPointer(state->transport, &pointer);
}
return NULL;
}
static bool checkWaitCancellation(LG_Transport * transport,
const LG_FrameOps * ops, PLGMPHostQueue frameQueue,
PLGMPHostQueue pointerQueue)
{
struct WaitState frame = {
.transport = transport,
.ops = ops,
.status = LG_TRANSPORT_ERROR,
.frame = true,
};
struct WaitState pointer = {
.transport = transport,
.ops = ops,
.status = LG_TRANSPORT_ERROR,
};
pthread_t frameThread;
pthread_t pointerThread;
if (pthread_create(&frameThread, NULL, waitForVideo, &frame) != 0)
return false;
if (pthread_create(&pointerThread, NULL, waitForVideo, &pointer) != 0)
{
ops->cancelFrameWait(transport);
pthread_join(frameThread, NULL);
return false;
}
bool subscribed = false;
for (unsigned i = 0; i < WAIT_TIMEOUT; ++i)
{
if (lgmpHostQueueHasSubs(frameQueue) &&
lgmpHostQueueHasSubs(pointerQueue))
{
subscribed = true;
break;
}
usleep(1000);
}
ops->cancelFrameWait(transport);
ops->cancelPointerWait(transport);
const int frameJoin = pthread_join(frameThread, NULL);
const int pointerJoin = pthread_join(pointerThread, NULL);
return subscribed && frameJoin == 0 && pointerJoin == 0 &&
frame.status == LG_TRANSPORT_TIMEOUT &&
pointer.status == LG_TRANSPORT_TIMEOUT;
}
int main(void)
{
int result = 1;
int fd = -1;
char path[] = "/tmp/lgmp-transport-test-XXXXXX";
bool pathExists = false;
void * hostMemory = MAP_FAILED;
PLGMPHost host = NULL;
PLGMPHostQueue frameQueue = NULL;
PLGMPHostQueue pointerQueue = NULL;
PLGMPMemory frameMemory = NULL;
PLGMPMemory pointerMemory = NULL;
LG_Transport * transport = NULL;
const LG_FrameOps * frameOps = NULL;
debug_init();
fd = mkstemp(path);
CHECK(fd >= 0);
pathExists = true;
CHECK(ftruncate(fd, TEST_SHM_SIZE) == 0);
hostMemory = mmap(NULL, TEST_SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
fd, 0);
CHECK(hostMemory != MAP_FAILED);
KVMFR session = { 0 };
memcpy(session.magic, KVMFR_MAGIC, sizeof(session.magic));
session.version = KVMFR_VERSION;
memcpy(session.hostver, "transport-test", sizeof("transport-test"));
CHECK(lgmpHostInit(hostMemory, TEST_SHM_SIZE, &host, sizeof(session),
(uint8_t *)&session) == LGMP_OK);
const struct LGMPQueueConfig frameConfig = {
.queueID = LGMP_Q_FRAME,
.numMessages = LGMP_Q_FRAME_LEN,
.subTimeout = TEST_TIMEOUT,
};
const struct LGMPQueueConfig pointerConfig = {
.queueID = LGMP_Q_POINTER,
.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);
KVMFRFrame * wireFrame = lgmpHostMemPtr(frameMemory);
wireFrame->formatVer = 1;
wireFrame->frameSerial = 1;
wireFrame->type = FRAME_TYPE_BGRA;
wireFrame->screenWidth = 1;
wireFrame->screenHeight = 1;
wireFrame->dataWidth = 1;
wireFrame->dataHeight = 1;
wireFrame->frameWidth = 1;
wireFrame->frameHeight = 1;
wireFrame->rotation = FRAME_ROT_0;
wireFrame->stride = 1;
wireFrame->pitch = sizeof(uint32_t);
wireFrame->offset = sizeof(*wireFrame);
wireFrame->sdrWhiteLevel = KVMFR_SDR_WHITE_LEVEL_DEFAULT;
wireFrame->captureTime = 100;
wireFrame->postProcessTime = 200;
wireFrame->copyTime = 300;
wireFrame->readyTime = 400;
wireFrame->timingSerial = wireFrame->frameSerial;
wireFrame->timingValid = 1;
FrameBuffer * framebuffer = (FrameBuffer *)((uint8_t *)wireFrame +
wireFrame->offset);
atomic_store(&framebuffer->wp, sizeof(uint32_t));
CHECK(lgmpHostMemAlloc(host, sizeof(KVMFRCursor), &pointerMemory) ==
LGMP_OK);
KVMFRCursor * wirePointer = lgmpHostMemPtr(pointerMemory);
wirePointer->x = 1;
wirePointer->y = 1;
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);
CHECK(LGT_LGMP.create(&transport));
const LG_VideoOps * videoOps = LGT_LGMP.getVideoOps(transport);
CHECK(videoOps);
CHECK(videoOps->type == LG_VIDEO_TYPE_FRAME);
CHECK(videoOps->frame);
frameOps = videoOps->frame;
CHECK(frameOps->cancelFrameWait);
CHECK(frameOps->cancelPointerWait);
CHECK(unlink(path) == 0);
pathExists = false;
usleep(300000);
CHECK(lgmpHostProcess(host) == LGMP_OK);
LG_TransportSession transportSession;
CHECK(LGT_LGMP.connect(transport, &transportSession) == LG_TRANSPORT_OK);
LG_TransportFrame frame;
LG_TransportPointer pointer;
CHECK(checkWaitCancellation(
transport, frameOps, frameQueue, pointerQueue));
CHECK(lgmpHostQueueHasSubs(frameQueue));
CHECK(lgmpHostQueueHasSubs(pointerQueue));
CHECK(lgmpHostQueueNewSubs(frameQueue) == 1);
CHECK(lgmpHostQueueNewSubs(pointerQueue) == 1);
CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK);
CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION,
pointerMemory) == LGMP_OK);
CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_OK);
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);
frameOps->releaseFrame(transport, &frame);
CHECK(frameOps->nextPointer(transport, &pointer) == LG_TRANSPORT_OK);
frameOps->releasePointer(transport, &pointer);
/*
* Stop with messages pending. Unsubscribing before the host timeout is the
* lifecycle behavior that was lost when LGMP moved behind the transport API.
*/
CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK);
CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION,
pointerMemory) == LGMP_OK);
CHECK(frameOps->stopFrame);
CHECK(frameOps->stopPointer);
frameOps->stopFrame(transport);
frameOps->stopPointer(transport);
CHECK(!lgmpHostQueueHasSubs(frameQueue));
CHECK(!lgmpHostQueueHasSubs(pointerQueue));
CHECK(waitForQueuesEmpty(host, frameQueue, pointerQueue));
CHECK(lgmpHostQueuePending(frameQueue) == 0);
CHECK(lgmpHostQueuePending(pointerQueue) == 0);
frameOps->cancelFrameWait(transport);
CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_TIMEOUT);
frameOps->cancelPointerWait(transport);
CHECK(frameOps->nextPointer(transport, &pointer) == LG_TRANSPORT_TIMEOUT);
CHECK(lgmpHostQueueNewSubs(frameQueue) == 1);
CHECK(lgmpHostQueueNewSubs(pointerQueue) == 1);
/* The host resends the same serial after a new subscription. */
CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK);
CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION,
pointerMemory) == LGMP_OK);
CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_OK);
frameOps->releaseFrame(transport, &frame);
CHECK(frameOps->nextPointer(transport, &pointer) == LG_TRANSPORT_OK);
frameOps->releasePointer(transport, &pointer);
/*
* 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->timingSerial = wireFrame->frameSerial;
CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK);
CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION,
pointerMemory) == LGMP_OK);
CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_OK);
CHECK(waitForQueuesEmpty(host, frameQueue, pointerQueue));
CHECK(lgmpHostQueueHasSubs(frameQueue));
CHECK(lgmpHostQueueHasSubs(pointerQueue));
frameOps->stopFrame(transport);
frameOps->stopPointer(transport);
frameOps->cancelFrameWait(transport);
CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_TIMEOUT);
frameOps->cancelPointerWait(transport);
CHECK(frameOps->nextPointer(transport, &pointer) == LG_TRANSPORT_TIMEOUT);
CHECK(lgmpHostQueueNewSubs(frameQueue) == 1);
CHECK(lgmpHostQueueNewSubs(pointerQueue) == 1);
/* Resetting the transport serial makes the host's resend visible. */
CHECK(lgmpHostQueuePost(frameQueue, 0, frameMemory) == LGMP_OK);
CHECK(lgmpHostQueuePost(pointerQueue, CURSOR_FLAG_POSITION,
pointerMemory) == LGMP_OK);
CHECK(frameOps->nextFrame(transport, false, &frame) == LG_TRANSPORT_OK);
frameOps->releaseFrame(transport, &frame);
CHECK(frameOps->nextPointer(transport, &pointer) == LG_TRANSPORT_OK);
frameOps->releasePointer(transport, &pointer);
result = 0;
cleanup:
if (transport)
LGT_LGMP.destroy(&transport);
option_free();
lgmpHostMemFree(&pointerMemory);
lgmpHostMemFree(&frameMemory);
lgmpHostFree(&host);
if (hostMemory != MAP_FAILED)
munmap(hostMemory, TEST_SHM_SIZE);
if (fd >= 0)
close(fd);
if (pathExists)
unlink(path);
return result;
}