Files
LookingGlass/client/src/main.c
2026-08-14 19:09:11 +10:00

4143 lines
127 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 "main.h"
#include "config.h"
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <inttypes.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <math.h>
#include <stdlib.h>
#include <stdatomic.h>
#include <linux/input.h>
#include "common/array.h"
#include "common/debug.h"
#include "common/crash.h"
#include "common/stringutils.h"
#include "common/thread.h"
#include "common/locking.h"
#include "common/event.h"
#include "common/time.h"
#include "common/version.h"
#include "common/paths.h"
#include "common/cpuinfo.h"
#include "common/ll.h"
#include "common/option.h"
#include "common/proctitle.h"
#include "message.h"
#include "core.h"
#include "app.h"
#include "audio.h"
#include "keybind.h"
#include "core/clipboard.h"
#include "kb.h"
#include "egl_dynprocs.h"
#include "gl_dynprocs.h"
#include "overlays.h"
#include "overlay_utils.h"
#include "util.h"
#include "render_queue.h"
#include "evdev.h"
#include "frame_scheduler.h"
#include "input.h"
#include "sw_surface.h"
#ifdef ENABLE_TESTS
#include "interface/test_capture.h"
_Static_assert((int)LG_CAPTURE_RGBA8 == (int)LG_TEST_CAPTURE_RGBA8,
"capture format mismatch");
_Static_assert((int)LG_CAPTURE_RGB10_A2 == (int)LG_TEST_CAPTURE_RGB10_A2,
"capture format mismatch");
_Static_assert((int)LG_CAPTURE_RGBA32F == (int)LG_TEST_CAPTURE_RGBA32F,
"capture format mismatch");
#endif
#define TRANSPORT_LOST_PRIMARY (1U << 0)
#define TRANSPORT_LOST_FALLBACK (1U << 1)
#define TRANSPORT_LOST_ALL \
(TRANSPORT_LOST_PRIMARY | TRANSPORT_LOST_FALLBACK)
#define VIDEO_STATUS_ERROR_RETRY_NS 1000000U
// forwards
static int renderThread(void * unused);
static RenderQueueSource renderQueueSource(LG_VideoSource source);
static bool videoSourceInvalidate(LG_VideoSource source);
static void videoSourceShowSplashIfNeeded(void);
static bool fallbackRequestVideo(void);
static void primaryLost(void);
static bool videoComponentPayloadCurrent(const atomic_bool * available,
const atomic_uint_least64_t * epoch, uint64_t payloadEpoch,
bool trackEpoch);
static bool videoPayloadAccept(const atomic_bool * available,
const atomic_uint_least64_t * epoch, uint64_t payloadEpoch,
bool trackEpoch, uint64_t sourceGeneration);
static void videoPayloadRelease(void);
static bool videoFramePayloadAccept(const LG_TransportFrame * frame,
uint64_t currentGeneration, bool trackEpoch,
uint64_t * sourceGeneration, uint64_t * frameSerial,
uint32_t * formatVersion, bool * sourceChanged);
static LGEvent *e_startup = NULL;
static LGEvent *e_cursorRepaint = NULL;
static LGThread *t_render = NULL;
static LGThread *t_cursorRepaint = NULL;
static struct
{
LG_Lock lock;
uint64_t requestedSerial;
uint64_t presentedSerial;
uint64_t lastRenderTime;
bool rendering;
bool wakeArmed;
bool lastRenderCursorOnly;
}
l_cursorRepaint;
_Atomic(enum RunState) p_appState = APP_STATE_RUNNING;
struct AppState g_state = { 0 };
struct CursorState g_cursor;
// this structure is initialized in config.c
struct AppParams g_params = { 0 };
static bool videoStatusRetry(LG_TransportStatus status,
bool statusAware, bool available)
{
if (status == LG_TRANSPORT_TIMEOUT ||
status == LG_TRANSPORT_UNAVAILABLE)
return true;
if (status != LG_TRANSPORT_ERROR || !statusAware || available)
return false;
nsleep(VIDEO_STATUS_ERROR_RETRY_NS);
return true;
}
#ifdef ENABLE_TESTS
static struct
{
const char * path;
uint64_t targetSerial;
uint64_t lastSerial;
unsigned delay;
unsigned stableRenders;
bool enabled;
bool complete;
}
l_testCapture;
static atomic_uint_least64_t l_testFrameSerial;
static _Atomic(FrameType) l_testFrameType;
#endif
static void lgInit(void)
{
g_state.formatValid = false;
g_state.resizeDone = true;
core_setCursorInView(false);
if (g_cursor.grab)
core_setGrab(false);
g_cursor.useScale = false;
g_cursor.scale.x = 1.0;
g_cursor.scale.y = 1.0;
g_cursor.draw = false;
g_cursor.inView = false;
g_cursor.viewReq = false;
g_cursor.exit = false;
g_cursor.surfaceExit = false;
g_cursor.guest.valid = false;
// if guest input is not in use, hide the local cursor
if ((!lgInput_available() && g_params.hideMouse) || !g_params.showCursorDot)
g_state.ds->setPointer(LG_POINTER_NONE);
else
g_state.ds->setPointer(LG_POINTER_SQUARE);
}
static bool fpsTimerFn(void * unused)
{
static uint64_t last;
if (!last)
{
last = nanotime();
return true;
}
const uint64_t renderCount = atomic_exchange_explicit(&g_state.renderCount, 0,
memory_order_acquire);
float fps, ups;
if (renderCount > 0)
{
const uint64_t frameCount = atomic_exchange_explicit(&g_state.frameCount, 0,
memory_order_acquire);
const uint64_t time = nanotime();
const uint64_t elapsedNs = time - last;
const float elapsedMs = (float)elapsedNs / 1e6f;
last = time;
fps = 1e3f / (elapsedMs / (float)renderCount);
ups = 1e3f / (elapsedMs / (float)frameCount);
}
else
{
last = nanotime();
fps = 0.0f;
ups = 0.0f;
}
atomic_store_explicit(&g_state.fps, fps, memory_order_relaxed);
atomic_store_explicit(&g_state.ups, ups, memory_order_relaxed);
return true;
}
static bool tickTimerFn(void * unused)
{
static unsigned long long tickCount = 0;
bool needsRender = false;
struct Overlay * overlay;
ll_lock(g_state.overlays);
ll_forEachNL(g_state.overlays, item, overlay)
{
if (overlay->ops->tick && overlay->ops->tick(overlay->udata, tickCount))
needsRender = true;
}
ll_unlock(g_state.overlays);
if (needsRender)
app_invalidateWindow(false);
++tickCount;
return true;
}
#define FRAME_TIMING_RECORD_COUNT 1024
#define FRAME_TIMING_PUBLISH_BATCH_SIZE 32
#define FRAME_TIMING_PRESENT_TIMEOUT_NS 500000000ULL
#define USB_REDIR_DISCONNECT_TIMEOUT_US 500000ULL
enum FrameTimingReady
{
FRAME_TIMING_FRAME_READY = 1 << 0,
FRAME_TIMING_RENDER_READY = 1 << 1,
FRAME_TIMING_PRESENT_READY = 1 << 2,
};
struct FrameTimingRecord
{
LG_RendererFrameToken token;
uint64_t frameSerial;
uint32_t scheduleGeneration;
uint32_t scheduleEpoch;
uint32_t scheduleDeadlineSerial;
unsigned readyMask;
bool producerValid;
bool providerValid;
bool phaseValid;
bool transportValid;
bool presentValid;
uint64_t captureTime;
uint64_t postProcessTime;
uint64_t copyTime;
uint64_t readyTime;
uint64_t holdTime;
uint64_t readyLeadTime;
uint64_t receiveTime;
uint64_t providerPrepareTime;
uint64_t importTime;
uint64_t importWaitTime;
uint64_t dispatchTime;
uint64_t queueStart;
uint64_t prepareStart;
uint64_t prepareTime;
uint64_t timestamp;
uint64_t setupTime;
uint64_t effectsTime;
uint64_t desktopTime;
uint64_t composeTime;
uint64_t swapTime;
uint64_t presentTime;
uint64_t presentDeadline;
};
static struct
{
LG_Lock lock;
_Atomic(LG_RendererFrameToken) queuedToken;
LG_RendererFrameToken nextToken;
LG_RendererFrameToken retireToken;
unsigned publishRead;
unsigned publishWrite;
unsigned publishCount;
bool overflowWarning;
LG_RendererFrameToken publishToken[FRAME_TIMING_RECORD_COUNT];
struct FrameTimingRecord record[FRAME_TIMING_RECORD_COUNT];
}
l_frameTiming;
/* The frame and render threads complete records independently. A token is
* published only after both halves have arrived, while queuedToken prevents a
* renderer that woke for unrelated work from consuming an unqueued frame. */
static void frameTimingInit(void)
{
LG_LOCK_INIT(l_frameTiming.lock);
atomic_store_explicit(&l_frameTiming.queuedToken,
LG_RENDERER_FRAME_TOKEN_NONE, memory_order_relaxed);
l_frameTiming.nextToken = LG_RENDERER_FRAME_TOKEN_NONE;
l_frameTiming.retireToken = 1;
l_frameTiming.publishRead = 0;
l_frameTiming.publishWrite = 0;
l_frameTiming.publishCount = 0;
l_frameTiming.overflowWarning = false;
memset(l_frameTiming.record, 0, sizeof(l_frameTiming.record));
}
static void frameTimingReset(void)
{
INTERLOCKED_SECTION(l_frameTiming.lock, {
memset(l_frameTiming.record, 0, sizeof(l_frameTiming.record));
l_frameTiming.retireToken = l_frameTiming.nextToken + 1;
l_frameTiming.publishRead = 0;
l_frameTiming.publishWrite = 0;
l_frameTiming.publishCount = 0;
l_frameTiming.overflowWarning = false;
});
/* Tokens remain monotonic across reconnects so stale renderer state can
* never alias a new frame. No frame is consumable until it is queued. */
atomic_store_explicit(&l_frameTiming.queuedToken,
LG_RENDERER_FRAME_TOKEN_NONE, memory_order_release);
}
static uint64_t frameTimingAdd(uint64_t lhs, uint64_t rhs)
{
return rhs > UINT64_MAX - lhs ? UINT64_MAX : lhs + rhs;
}
static struct FrameTimingRecord * frameTimingRecord(
LG_RendererFrameToken token)
{
return &l_frameTiming.record[
(token - 1) % FRAME_TIMING_RECORD_COUNT];
}
void main_framePresented(uint64_t frameToken, uint64_t presentTime,
bool valid)
{
if (!frameToken)
return;
INTERLOCKED_SECTION(l_frameTiming.lock, {
struct FrameTimingRecord * record = frameTimingRecord(frameToken);
if (record->token == frameToken)
{
record->presentTime = presentTime;
record->presentValid = valid;
record->readyMask |= FRAME_TIMING_PRESENT_READY;
}
});
}
static LG_RendererFrameToken frameTimingReserve(void)
{
LG_RendererFrameToken token;
LG_LOCK(l_frameTiming.lock);
token = ++l_frameTiming.nextToken;
if (unlikely(token == LG_RENDERER_FRAME_TOKEN_NONE))
token = ++l_frameTiming.nextToken;
struct FrameTimingRecord * record = frameTimingRecord(token);
const bool recordActive =
record->token != LG_RENDERER_FRAME_TOKEN_NONE &&
((record->readyMask & FRAME_TIMING_RENDER_READY) ||
record->token >= l_frameTiming.retireToken);
if (unlikely(recordActive && !l_frameTiming.overflowWarning))
{
DEBUG_WARN("Frame timing record pool exhausted; samples will be lost");
l_frameTiming.overflowWarning = true;
}
*record = (struct FrameTimingRecord) { .token = token };
LG_UNLOCK(l_frameTiming.lock);
return token;
}
static void frameTimingCancel(LG_RendererFrameToken token)
{
INTERLOCKED_SECTION(l_frameTiming.lock, {
struct FrameTimingRecord * record = frameTimingRecord(token);
if (record->token == token)
*record = (struct FrameTimingRecord) {};
});
}
static void frameTimingQueue(LG_RendererFrameToken token, uint64_t frameSerial,
const LG_TransportFrameTiming * timing, uint64_t importTime,
uint64_t importWaitTime, uint64_t dispatchStart, uint64_t queueStart)
{
INTERLOCKED_SECTION(l_frameTiming.lock, {
struct FrameTimingRecord * record = frameTimingRecord(token);
if (record->token == token)
{
const uint64_t elapsed = queueStart > dispatchStart ?
queueStart - dispatchStart : 0;
const uint64_t accounted =
frameTimingAdd(importTime, importWaitTime);
record->importTime = importTime;
record->importWaitTime = importWaitTime;
record->dispatchTime = elapsed > accounted ?
elapsed - accounted : 0;
record->queueStart = queueStart;
record->frameSerial = frameSerial;
record->scheduleGeneration = timing->scheduleGeneration;
record->scheduleEpoch = timing->scheduleEpoch;
record->scheduleDeadlineSerial = timing->scheduleDeadlineSerial;
record->phaseValid = timing->phaseValid;
if (record->timestamp < queueStart)
record->timestamp = queueStart;
}
});
atomic_store_explicit(
&l_frameTiming.queuedToken, token, memory_order_release);
}
static LG_RendererFrameToken frameTimingQueuedToken(void)
{
return atomic_load_explicit(
&l_frameTiming.queuedToken, memory_order_acquire);
}
static void frameTimingFinishFrame(LG_RendererFrameToken token,
const LG_TransportFrameTiming * timing)
{
const uint64_t timestamp = nanotime();
INTERLOCKED_SECTION(l_frameTiming.lock, {
struct FrameTimingRecord * record = frameTimingRecord(token);
if (record->token == token)
{
if (token < l_frameTiming.retireToken &&
!(record->readyMask & FRAME_TIMING_RENDER_READY))
*record = (struct FrameTimingRecord) {};
else
{
record->producerValid = timing->valid;
record->captureTime = timing->captureTime;
record->postProcessTime = timing->postProcessTime;
record->copyTime = timing->copyTime;
record->readyTime = timing->readyTime;
record->holdTime = timing->holdTime;
record->readyLeadTime = timing->readyLeadTime;
record->providerValid = timing->providerValid;
record->receiveTime = timing->receiveTime;
record->providerPrepareTime = timing->prepareTime;
if (record->timestamp < timestamp)
record->timestamp = timestamp;
record->readyMask |= FRAME_TIMING_FRAME_READY;
}
}
});
}
static void frameTimingFinishRender(const LG_RendererFrameTiming * timing,
uint64_t prepareStart, uint64_t prepareTime, uint64_t timestamp,
LG_RendererFrameToken cadenceToken)
{
uint64_t feedbackFrameSerial = 0;
uint64_t feedbackQueueStart = 0;
uint32_t feedbackGeneration = 0;
uint32_t feedbackEpoch = 0;
uint32_t feedbackDeadline = 0;
bool feedbackValid = false;
LG_LOCK(l_frameTiming.lock);
if (l_frameTiming.retireToken <= timing->frameToken)
l_frameTiming.retireToken = timing->frameToken + 1;
struct FrameTimingRecord * record = frameTimingRecord(timing->frameToken);
if (record->token == timing->frameToken)
{
record->prepareStart = prepareStart;
record->prepareTime = prepareTime;
if (record->timestamp < timestamp)
record->timestamp = timestamp;
record->setupTime = timing->setupTime;
record->effectsTime = timing->effectsTime;
record->desktopTime = timing->desktopTime;
record->composeTime = timing->composeTime;
record->swapTime = timing->swapTime;
record->presentDeadline = timestamp + FRAME_TIMING_PRESENT_TIMEOUT_NS;
if (!timing->presentTracked &&
!(record->readyMask & FRAME_TIMING_PRESENT_READY))
{
record->presentTime = 0;
record->presentValid = false;
record->readyMask |= FRAME_TIMING_PRESENT_READY;
}
record->readyMask |= FRAME_TIMING_RENDER_READY;
record->transportValid = record->phaseValid &&
timing->frameToken == cadenceToken;
feedbackFrameSerial = record->frameSerial;
feedbackGeneration = record->scheduleGeneration;
feedbackEpoch = record->scheduleEpoch;
feedbackDeadline = record->scheduleDeadlineSerial;
feedbackValid = record->transportValid;
feedbackQueueStart = record->queueStart;
if (unlikely(
l_frameTiming.publishCount == FRAME_TIMING_RECORD_COUNT))
{
if (!l_frameTiming.overflowWarning)
{
DEBUG_WARN("Frame timing publish queue exhausted; sample lost");
l_frameTiming.overflowWarning = true;
}
*record = (struct FrameTimingRecord) {};
}
else
{
l_frameTiming.publishToken[l_frameTiming.publishWrite] =
timing->frameToken;
l_frameTiming.publishWrite =
(l_frameTiming.publishWrite + 1) % FRAME_TIMING_RECORD_COUNT;
++l_frameTiming.publishCount;
}
}
LG_UNLOCK(l_frameTiming.lock);
if (g_state.jitRender && feedbackValid && feedbackFrameSerial &&
feedbackGeneration && feedbackEpoch && feedbackDeadline &&
feedbackQueueStart && prepareStart >= feedbackQueueStart)
{
frameScheduler_feedback(
feedbackFrameSerial, feedbackGeneration, feedbackEpoch,
feedbackDeadline, prepareStart - feedbackQueueStart);
}
}
static void frameTimingPublishReady(void)
{
struct FrameTimingRecord ready[FRAME_TIMING_PUBLISH_BATCH_SIZE];
unsigned readyCount = 0;
LG_LOCK(l_frameTiming.lock);
const uint64_t now = nanotime();
while (readyCount < FRAME_TIMING_PUBLISH_BATCH_SIZE &&
l_frameTiming.publishCount)
{
const LG_RendererFrameToken token =
l_frameTiming.publishToken[l_frameTiming.publishRead];
struct FrameTimingRecord * record = frameTimingRecord(token);
if (record->token != token ||
!(record->readyMask & FRAME_TIMING_RENDER_READY))
{
l_frameTiming.publishRead =
(l_frameTiming.publishRead + 1) % FRAME_TIMING_RECORD_COUNT;
--l_frameTiming.publishCount;
continue;
}
if (!(record->readyMask & FRAME_TIMING_FRAME_READY))
break;
if (!(record->readyMask & FRAME_TIMING_PRESENT_READY))
{
if (!record->presentDeadline || now < record->presentDeadline)
break;
record->presentTime = 0;
record->presentValid = false;
record->readyMask |= FRAME_TIMING_PRESENT_READY;
}
ready[readyCount++] = *record;
*record = (struct FrameTimingRecord) {};
l_frameTiming.publishRead =
(l_frameTiming.publishRead + 1) % FRAME_TIMING_RECORD_COUNT;
--l_frameTiming.publishCount;
}
LG_UNLOCK(l_frameTiming.lock);
for (unsigned i = 0; i < readyCount; ++i)
{
const struct FrameTimingRecord * record = &ready[i];
const uint64_t queueTime =
record->prepareStart > record->queueStart ?
record->prepareStart - record->queueStart : 0;
const uint64_t providerAccounted =
record->providerValid ?
frameTimingAdd(
record->receiveTime, record->providerPrepareTime) : 0;
const uint64_t transportAccounted =
frameTimingAdd(frameTimingAdd(frameTimingAdd(providerAccounted,
record->importTime), record->dispatchTime), queueTime);
const uint64_t transportTime =
record->readyLeadTime > transportAccounted ?
record->readyLeadTime - transportAccounted : 0;
uint32_t validMask =
OVERLAY_FRAME_TIMING_VALID_ALL;
if (!record->producerValid)
validMask &= ~OVERLAY_FRAME_TIMING_VALID_PRODUCER;
if (!record->producerValid || !record->transportValid)
validMask &= ~OVERLAY_FRAME_TIMING_VALID_TRANSPORT;
if (!record->providerValid)
validMask &= ~OVERLAY_FRAME_TIMING_VALID_PROVIDER;
if (!record->presentValid)
validMask &= ~OVERLAY_FRAME_TIMING_VALID_PRESENT;
const OverlayFrameTiming timing = {
.timestamp = record->timestamp,
.validMask = validMask,
.capture = record->captureTime * 1e-6f,
.postProcess = record->postProcessTime * 1e-6f,
.copy = record->copyTime * 1e-6f,
.ready = record->readyTime * 1e-6f,
.hold = record->holdTime * 1e-6f,
.transport = transportTime * 1e-6f,
.receive = record->receiveTime * 1e-6f,
.providerPrepare = record->providerPrepareTime * 1e-6f,
.import = frameTimingAdd(record->importTime,
record->producerValid ? 0 : record->importWaitTime) * 1e-6f,
.dispatch = record->dispatchTime * 1e-6f,
.queue = queueTime * 1e-6f,
.prepare = record->prepareTime * 1e-6f,
.setup = record->setupTime * 1e-6f,
.effects = record->effectsTime * 1e-6f,
.desktop = record->desktopTime * 1e-6f,
.compose = record->composeTime * 1e-6f,
.swap = record->swapTime * 1e-6f,
.present = record->presentTime * 1e-6f,
};
ringbuffer_push(g_state.frameLatency, &timing);
}
}
static void preSwapCallback(void * udata)
{
(void)udata;
#ifdef ENABLE_TESTS
if (!l_testCapture.enabled || l_testCapture.complete)
return;
const uint64_t serial =
atomic_load_explicit(&l_testFrameSerial, memory_order_acquire);
if (serial < l_testCapture.targetSerial)
return;
if (l_testCapture.lastSerial != serial)
{
l_testCapture.lastSerial = serial;
l_testCapture.stableRenders = 0;
return;
}
if (l_testCapture.stableRenders++ < l_testCapture.delay)
return;
l_testCapture.complete = true;
LG_RendererCapture capture;
if (!g_state.lgr->ops.capture ||
!RENDERER(capture, &capture))
{
DEBUG_ERROR("The selected renderer cannot capture its framebuffer");
app_setState(APP_STATE_SHUTDOWN);
return;
}
const FrameType sourceType =
atomic_load_explicit(&l_testFrameType, memory_order_acquire);
const LG_TestCaptureHeader header = {
.magic = LG_TEST_CAPTURE_MAGIC,
.version = LG_TEST_CAPTURE_VERSION,
.headerSize = sizeof(header),
.frameSerial = serial,
.sourceType = sourceType,
.captureFormat = capture.format,
.width = capture.width,
.height = capture.height,
.stride = capture.stride,
.flags = (capture.hdr ? LG_TEST_CAPTURE_HDR : 0) |
(capture.hdrPQ ? LG_TEST_CAPTURE_HDR_PQ : 0) |
(capture.nativeHDR ? LG_TEST_CAPTURE_NATIVE_HDR : 0) |
LG_TEST_CAPTURE_BOTTOM_UP,
.dataSize = capture.dataSize,
};
FILE * file = fopen(l_testCapture.path, "wb");
bool written = false;
if (file)
{
written =
fwrite(&header, sizeof(header), 1, file) == 1 &&
fwrite(capture.data, capture.dataSize, 1, file) == 1;
if (fclose(file) != 0)
written = false;
}
free(capture.data);
if (!written)
DEBUG_ERROR("Failed to write test capture to: %s", l_testCapture.path);
else
DEBUG_INFO("Wrote test capture for frame %" PRIu64 " to: %s",
serial, l_testCapture.path);
app_setState(APP_STATE_SHUTDOWN);
#endif
}
static uint64_t cursorRepaintPeriod(void)
{
uint64_t period = g_state.overlayFrameTime;
uint64_t outputPeriod;
if (g_state.ds->getFramePeriod &&
g_state.ds->getFramePeriod(&outputPeriod) && outputPeriod)
period = outputPeriod;
if (overlaySplash_isFading())
period = max(period, g_state.overlayFrameTime);
return period;
}
static bool cursorRepaintNeeded(void)
{
LG_LOCK(l_cursorRepaint.lock);
const bool result =
l_cursorRepaint.requestedSerial != l_cursorRepaint.presentedSerial &&
!overlaySplash_isFading();
LG_UNLOCK(l_cursorRepaint.lock);
return result;
}
static void cursorRepaintRequest(void)
{
LG_LOCK(l_cursorRepaint.lock);
const bool wasDirty =
l_cursorRepaint.requestedSerial != l_cursorRepaint.presentedSerial;
++l_cursorRepaint.requestedSerial;
LG_UNLOCK(l_cursorRepaint.lock);
if (!g_state.jitRender && !wasDirty)
lgSignalEvent(e_cursorRepaint);
}
static bool queueCursorRedraw(void)
{
if (!atomic_load_explicit(&g_cursor.redraw, memory_order_acquire))
return false;
const bool inputAvailable = lgInput_available();
LG_LOCK(g_state.videoSourceLock);
if (!atomic_exchange_explicit(
&g_cursor.redraw, false, memory_order_acq_rel))
{
LG_UNLOCK(g_state.videoSourceLock);
return false;
}
const LG_VideoSource source = atomic_load_explicit(
&g_state.videoSourceApplied, memory_order_acquire);
const uint64_t generation = atomic_load_explicit(
&g_state.videoSourceAppliedGeneration, memory_order_acquire);
if (source <= LG_VIDEO_SOURCE_NONE || source >= LG_VIDEO_SOURCE_COUNT)
{
LG_UNLOCK(g_state.videoSourceLock);
return false;
}
const struct VideoSourceState * state = &g_state.videoSource[source];
const bool valid = state->cursorStateValid &&
atomic_load_explicit(&state->generation, memory_order_acquire) ==
generation;
const bool visible = state->cursorVisible &&
(source == LG_VIDEO_SOURCE_FALLBACK ||
g_cursor.draw || !inputAvailable);
const int x = state->cursorX;
const int y = state->cursorY;
const int hx = state->cursorHX;
const int hy = state->cursorHY;
if (!valid)
{
LG_UNLOCK(g_state.videoSourceLock);
return false;
}
renderQueue_sourceCursorState(renderQueueSource(source), generation,
visible, x, y, hx, hy);
LG_UNLOCK(g_state.videoSourceLock);
return true;
}
static uint64_t cursorRepaintRenderBegin(bool * cursorWake)
{
LG_LOCK(l_cursorRepaint.lock);
const uint64_t serial = l_cursorRepaint.requestedSerial;
l_cursorRepaint.rendering = true;
*cursorWake = l_cursorRepaint.wakeArmed;
if (l_cursorRepaint.wakeArmed)
l_cursorRepaint.wakeArmed = false;
LG_UNLOCK(l_cursorRepaint.lock);
return serial;
}
static void cursorRepaintRenderEnd(
uint64_t serial, uint64_t renderTime, bool success, bool cursorOnly)
{
LG_LOCK(l_cursorRepaint.lock);
if (success)
{
l_cursorRepaint.presentedSerial = serial;
l_cursorRepaint.lastRenderTime = renderTime;
l_cursorRepaint.lastRenderCursorOnly = cursorOnly;
}
l_cursorRepaint.rendering = false;
const bool wake = success && !g_state.jitRender &&
l_cursorRepaint.requestedSerial != l_cursorRepaint.presentedSerial;
LG_UNLOCK(l_cursorRepaint.lock);
if (wake)
lgSignalEvent(e_cursorRepaint);
}
static int cursorRepaintThread(void * unused)
{
while (app_getState() != APP_STATE_SHUTDOWN)
{
lgWaitEvent(e_cursorRepaint, TIMEOUT_INFINITE);
while (app_getState() != APP_STATE_SHUTDOWN)
{
uint64_t waitTime = 0;
LG_LOCK(l_cursorRepaint.lock);
if (l_cursorRepaint.requestedSerial ==
l_cursorRepaint.presentedSerial ||
l_cursorRepaint.rendering || l_cursorRepaint.wakeArmed)
{
LG_UNLOCK(l_cursorRepaint.lock);
break;
}
const uint64_t now = nanotime();
const uint64_t period = cursorRepaintPeriod();
/* Give an on-rate source frame enough time to absorb this cursor update.
* Cursor-only rendering retains the full output cadence once needed. */
const uint64_t grace =
l_cursorRepaint.lastRenderCursorOnly ? 0 : period / 4;
const uint64_t deadline =
l_cursorRepaint.lastRenderTime + period + grace;
if (l_cursorRepaint.lastRenderTime && now < deadline)
waitTime = deadline - now;
else
{
l_cursorRepaint.wakeArmed = true;
app_invalidateWindow(false);
}
LG_UNLOCK(l_cursorRepaint.lock);
if (!waitTime)
break;
struct timespec deadlineTime;
clock_gettime(CLOCK_MONOTONIC, &deadlineTime);
tsAdd(&deadlineTime, waitTime);
lgWaitEventAbs(e_cursorRepaint, &deadlineTime);
}
}
return 0;
}
static int renderThread(void * unused)
{
if (!RENDERER(renderStartup, g_state.useDMA))
{
DEBUG_ERROR("EGL render failed to start");
app_setState(APP_STATE_SHUTDOWN);
/* unblock threads waiting on the condition */
lgSignalEvent(e_startup);
return 1;
}
if (!g_state.lgr->ops.supports ||
!RENDERER(supports, LG_SUPPORTS_DMABUF))
g_state.useDMA = false;
/* start up the fps timer */
LGTimer * fpsTimer;
if (!lgCreateTimer(500, fpsTimerFn, NULL, &fpsTimer))
{
DEBUG_ERROR("Failed to create the fps timer");
return 1;
}
app_initOverlays();
LGTimer * tickTimer;
if (!lgCreateTimer(1000 / TICK_RATE, tickTimerFn, NULL, &tickTimer))
{
lgTimerDestroy(fpsTimer);
DEBUG_ERROR("Failed to create the tick timer");
return 1;
}
LG_LOCK_INIT(g_state.lgrLock);
/* signal to other threads that the renderer is ready */
lgSignalEvent(e_startup);
struct timespec time;
clock_gettime(CLOCK_MONOTONIC, &time);
while(likely(app_getState() != APP_STATE_SHUTDOWN))
{
LG_RendererFrameToken cadenceToken = LG_RENDERER_FRAME_TOKEN_NONE;
if (g_state.jitRender)
{
const LG_DSWaitFrameResult waitResult = g_state.ds->waitFrame();
if (waitResult & LG_DS_WAIT_FRAME_CADENCE)
frameScheduler_observeCadence();
const LG_RendererFrameToken queuedAtWake =
waitResult == LG_DS_WAIT_FRAME_CADENCE ?
frameTimingQueuedToken() : LG_RENDERER_FRAME_TOKEN_NONE;
const bool forceRender =
waitResult & LG_DS_WAIT_FRAME_FORCE_RENDER;
app_handleRenderEvent(microtime());
const uint64_t pending =
atomic_load_explicit(&g_state.pendingCount, memory_order_acquire);
const bool overlayNeeded = app_overlayNeedsRender();
uint64_t outputPeriod = 0;
const bool periodKnown = overlayNeeded &&
g_state.ds->getFramePeriod &&
g_state.ds->getFramePeriod(&outputPeriod);
const uint64_t elapsed = g_state.lastRenderTimeValid ?
nanotime() - g_state.lastRenderTime : 0;
const bool overlayRender = overlayNeeded &&
(!g_state.lastRenderTimeValid || !periodKnown ||
elapsed + outputPeriod >= g_state.overlayFrameTime);
const bool clientWake = lgResetEvent(g_state.frameEvent);
const bool cursorRender = cursorRepaintNeeded();
if (!clientWake && !forceRender && !pending && !overlayRender &&
!cursorRender)
{
if (g_state.ds->skipFrame)
g_state.ds->skipFrame();
continue;
}
if (pending > 0)
{
atomic_fetch_sub(&g_state.pendingCount, 1);
if (!clientWake)
cadenceToken = queuedAtWake;
}
}
else if (g_params.fpsMin != 0)
{
app_handleRenderEvent(microtime());
lgWaitEventAbs(g_state.frameEvent, &time);
}
bool cursorWake;
const uint64_t cursorSerial = cursorRepaintRenderBegin(&cursorWake);
const uint64_t renderStart = nanotime();
frameTimingPublishReady();
int resize = atomic_load(&g_state.lgrResize);
if (unlikely(resize))
{
g_state.io->DisplaySize = (ImVec2) {
.x = g_state.windowW,
.y = g_state.windowH,
};
g_state.io->DisplayFramebufferScale = (ImVec2) {
.x = g_state.windowScale,
.y = g_state.windowScale,
};
igGetStyle()->FontScaleMain = 1.0f / g_state.windowScale;
}
const bool fontDirty = atomic_exchange(&g_state.fontDirty, false);
const bool fontUpdate = fontDirty ||
g_state.fontScale != g_state.windowScale;
if (unlikely(fontUpdate))
{
if (!util_buildUIFontAtlas(g_state.io->Fonts,
g_params.uiSize * g_state.windowScale, &g_state.fontLarge))
DEBUG_FATAL("Failed to build font atlas: %s (%s)", g_params.uiFont, g_state.fontName);
if (g_state.lgr && !RENDERER(onFontUpdate))
DEBUG_FATAL("Failed to upload the ImGui font atlas");
g_state.fontScale = g_state.windowScale;
}
if (unlikely(resize || fontUpdate))
cadenceToken = LG_RENDERER_FRAME_TOKEN_NONE;
if (unlikely(resize))
{
if (g_state.lgr)
RENDERER(onResize, g_state.windowW, g_state.windowH,
g_state.windowScale, g_state.dstRect, g_params.winRotate);
atomic_compare_exchange_weak(&g_state.lgrResize, &resize, 0);
}
const uint64_t prepareStart = nanotime();
queueCursorRedraw();
LG_LOCK(g_state.lgrLock);
const bool queueFull = renderQueue_process();
if (g_state.videoGeometryDirty)
{
g_state.videoGeometryDirty = false;
LG_UNLOCK(g_state.lgrLock);
core_updatePositionInfo();
LG_LOCK(g_state.lgrLock);
}
int sourceResize = atomic_load(&g_state.lgrResize);
if (unlikely(sourceResize))
{
RENDERER(onResize, g_state.windowW, g_state.windowH,
g_state.windowScale, g_state.dstRect, g_params.winRotate);
atomic_compare_exchange_weak(
&g_state.lgrResize, &sourceResize, 0);
}
const LG_RendererFrameToken frameTokenLimit = frameTimingQueuedToken();
const bool windowInvalid =
atomic_exchange(&g_state.invalidateWindow, false);
const bool overlayFull = app_overlayNeedsFullRender();
const bool invalidate = queueFull || windowInvalid || overlayFull;
const uint64_t prepareTime = nanotime() - prepareStart;
if (unlikely(invalidate))
cadenceToken = LG_RENDERER_FRAME_TOKEN_NONE;
LG_RendererFrameTiming rendererTiming = {};
if (unlikely(!RENDERER(render, g_params.winRotate, frameTokenLimit,
invalidate, preSwapCallback, NULL, &rendererTiming)))
{
LG_UNLOCK(g_state.lgrLock);
cursorRepaintRenderEnd(cursorSerial, renderStart, false, false);
break;
}
const uint64_t renderEnd = nanotime();
LG_UNLOCK(g_state.lgrLock);
renderQueue_presented();
cursorRepaintRenderEnd(cursorSerial, renderStart, true,
cursorWake &&
rendererTiming.frameToken == LG_RENDERER_FRAME_TOKEN_NONE);
if (!g_state.jitRender)
frameScheduler_observeCadence();
if (rendererTiming.frameToken != LG_RENDERER_FRAME_TOKEN_NONE)
frameTimingFinishRender(
&rendererTiming, prepareStart, prepareTime, renderEnd,
cadenceToken);
frameTimingPublishReady();
const uint64_t t = nanotime();
const uint64_t delta = t - g_state.lastRenderTime;
g_state.lastRenderTime = t;
atomic_fetch_add_explicit(&g_state.renderCount, 1, memory_order_relaxed);
if (!g_state.jitRender && g_params.fpsMin != 0)
{
/* A frame-driven swap also satisfies the minimum render rate. */
clock_gettime(CLOCK_MONOTONIC, &time);
tsAdd(&time, app_overlayNeedsRender() ?
g_state.overlayFrameTime : g_state.frameTime);
}
if (likely(g_state.lastRenderTimeValid))
{
const float fdelta = (float)delta / 1e6f;
ringbuffer_push(g_state.renderTimings, &fdelta);
}
g_state.lastRenderTimeValid = true;
const uint64_t now = microtime();
if (unlikely(
!g_state.resizeDone &&
g_state.resizeTimeout < now))
{
if (g_params.autoResize)
{
g_state.ds->setWindowSize(
g_state.dstRect.w,
g_state.dstRect.h
);
}
g_state.resizeDone = true;
}
}
app_setState(APP_STATE_SHUTDOWN);
lgTimerDestroy(tickTimer);
lgTimerDestroy(fpsTimer);
if (g_state.transport.handle &&
g_state.transport.ops->sessionValid(g_state.transport.handle))
{
lgInput_setTransport(NULL, NULL);
lgAudio_setTransport(NULL, NULL);
lgClipboard_setTransport(NULL, NULL);
}
else
{
lgInput_dropTransport();
lgAudio_dropTransport();
lgClipboard_dropTransport();
}
core_stopVideoThreads();
if (g_state.videoOps && g_state.videoOps->type == LG_VIDEO_TYPE_FRAME &&
g_state.videoOps->frame->detachRenderer)
g_state.videoOps->frame->detachRenderer(g_state.transport.handle);
else if (g_state.videoOps &&
g_state.videoOps->type == LG_VIDEO_TYPE_SW_SURFACE)
{
lgSwSurface_setActive(g_state.videoOps->swSurface,
g_state.transport.handle, false, NULL, NULL);
g_state.videoOps->swSurface->detach(g_state.transport.handle);
}
RENDERER(deinitialize);
g_state.lgr = NULL;
LG_LOCK_FREE(g_state.lgrLock);
return 0;
}
int main_cursorThread(void * unused)
{
LG_RendererCursor cursorType = LG_CURSOR_COLOR;
struct VideoSourceState * source =
&g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY];
lgWaitEvent(e_startup, TIMEOUT_INFINITE);
// subscribe to the pointer queue
while(app_getState() == APP_STATE_RUNNING &&
!atomic_load_explicit(
&g_state.stopVideoThreads, memory_order_acquire))
{
LG_TransportPointer pointer;
const LG_TransportStatus status = g_state.videoOps->frame->nextPointer(
g_state.transport.handle, &pointer);
if (status != LG_TRANSPORT_OK)
{
if (videoStatusRetry(status,
g_state.videoOps->frame->setStatusListener,
atomic_load_explicit(
&g_state.videoPointerAvailable, memory_order_acquire)))
{
if (!atomic_load_explicit(
&g_state.stopVideoThreads, memory_order_acquire) &&
queueCursorRedraw())
cursorRepaintRequest();
continue;
}
if (status == LG_TRANSPORT_DISCONNECTED)
{
lgInput_dropTransport();
lgAudio_dropTransport();
lgClipboard_dropTransport();
primaryLost();
}
else
{
DEBUG_ERROR("Pointer transport failed with status %d", status);
app_setState(APP_STATE_SHUTDOWN);
}
break;
}
const bool inputAvailable = lgInput_available();
if (!videoPayloadAccept(
&g_state.videoPointerAvailable, &g_state.videoPointerEpoch,
pointer.epoch,
g_state.videoOps->frame->setStatusListener != NULL, 0))
{
g_state.videoOps->frame->releasePointer(
g_state.transport.handle, &pointer);
continue;
}
LG_LOCK(g_state.videoSourceLock);
const uint64_t sourceGeneration = atomic_load_explicit(
&source->generation, memory_order_acquire);
if (!sourceGeneration)
{
LG_UNLOCK(g_state.videoSourceLock);
videoPayloadRelease();
g_state.videoOps->frame->releasePointer(
g_state.transport.handle, &pointer);
continue;
}
const bool wasRendered = source->cursorStateValid &&
source->cursorVisible && (g_cursor.draw || !inputAvailable);
bool hotspotChanged = false;
if (pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID)
source->cursorVisible =
pointer.flags & LG_TRANSPORT_POINTER_VISIBLE;
if (pointer.flags & LG_TRANSPORT_POINTER_SHAPE)
{
const int oldHX = source->cursorHX;
const int oldHY = source->cursorHY;
switch (pointer.type)
{
case CURSOR_TYPE_COLOR : cursorType = LG_CURSOR_COLOR ; break;
case CURSOR_TYPE_MONOCHROME : cursorType = LG_CURSOR_MONOCHROME ; break;
case CURSOR_TYPE_MASKED_COLOR: cursorType = LG_CURSOR_MASKED_COLOR; break;
default:
LG_UNLOCK(g_state.videoSourceLock);
videoPayloadRelease();
DEBUG_ERROR("Invalid cursor type");
g_state.videoOps->frame->releasePointer(
g_state.transport.handle, &pointer);
continue;
}
hotspotChanged =
source->cursorHX != pointer.hx || source->cursorHY != pointer.hy;
source->cursorHX = pointer.hx;
source->cursorHY = pointer.hy;
if (hotspotChanged && source->cursorStateValid &&
!(pointer.flags & LG_TRANSPORT_POINTER_POSITION))
{
source->cursorX += oldHX - pointer.hx;
source->cursorY += oldHY - pointer.hy;
}
}
const bool whiteLevelChanged =
(pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID) &&
pointer.sdrWhiteLevel &&
pointer.sdrWhiteLevel != source->cursorWhiteLevel;
if (whiteLevelChanged)
source->cursorWhiteLevel = pointer.sdrWhiteLevel;
if (pointer.flags & LG_TRANSPORT_POINTER_POSITION)
{
source->cursorX = pointer.x;
source->cursorY = pointer.y;
source->cursorStateValid = true;
}
const bool sourceApplied = atomic_load_explicit(
&g_state.videoSourceApplied, memory_order_acquire) ==
LG_VIDEO_SOURCE_PRIMARY && atomic_load_explicit(
&g_state.videoSourceAppliedGeneration, memory_order_acquire) ==
sourceGeneration;
if (sourceApplied)
atomic_exchange_explicit(
&g_cursor.redraw, false, memory_order_acq_rel);
const bool valid = source->cursorStateValid;
const bool guestVisible = source->cursorVisible;
const bool visible = guestVisible &&
(g_cursor.draw || !inputAvailable);
const int x = source->cursorX;
const int y = source->cursorY;
const int hx = source->cursorHX;
const int hy = source->cursorHY;
const bool isRendered = valid && visible;
bool wasValid = false;
if (sourceApplied)
{
wasValid = g_cursor.guest.valid;
g_cursor.guest.visible = guestVisible;
g_cursor.guest.x = x;
g_cursor.guest.y = y;
g_cursor.guest.hx = hx;
g_cursor.guest.hy = hy;
g_cursor.guest.valid = valid;
}
if (pointer.flags & LG_TRANSPORT_POINTER_SHAPE)
renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY,
sourceGeneration, cursorType, pointer.width, pointer.height,
pointer.pitch, pointer.shape);
if (pointer.flags & LG_TRANSPORT_POINTER_COLOR_TRANSFORM)
renderQueue_sourceCursorColorTransform(RENDER_QUEUE_SOURCE_PRIMARY,
sourceGeneration, pointer.colorTransform);
if (whiteLevelChanged)
renderQueue_sourceCursorWhiteLevel(RENDER_QUEUE_SOURCE_PRIMARY,
sourceGeneration, pointer.sdrWhiteLevel);
if (valid)
renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY,
sourceGeneration, visible, x, y, hx, hy);
LG_UNLOCK(g_state.videoSourceLock);
if (sourceApplied)
{
if (!wasValid && valid && core_inputEnabled())
core_alignToGuest();
if ((pointer.flags & LG_TRANSPORT_POINTER_POSITION) || hotspotChanged)
core_handleGuestMouseUpdate();
app_updateMouseState();
}
const bool contentChanged =
(pointer.flags & (LG_TRANSPORT_POINTER_SHAPE |
LG_TRANSPORT_POINTER_COLOR_TRANSFORM)) || whiteLevelChanged;
if (sourceApplied &&
!atomic_load_explicit(&g_state.stopVideo, memory_order_acquire) &&
(wasRendered != isRendered ||
((wasRendered || isRendered) &&
(g_params.mouseRedraw || contentChanged))))
cursorRepaintRequest();
videoPayloadRelease();
g_state.videoOps->frame->releasePointer(
g_state.transport.handle, &pointer);
}
if (g_state.videoOps->frame->stopPointer)
g_state.videoOps->frame->stopPointer(g_state.transport.handle);
return 0;
}
int main_frameThread(void * unused)
{
uint64_t frameSerial = 0;
uint32_t formatVersion = 0;
LG_RendererFormat rendererFormat;
uint64_t sourceGeneration = 0;
if (g_state.useDMA)
DEBUG_INFO("Using DMA buffer support");
lgWaitEvent(e_startup, TIMEOUT_INFINITE);
if (app_getState() != APP_STATE_RUNNING)
{
if (g_state.videoOps->frame->stopFrame)
g_state.videoOps->frame->stopFrame(g_state.transport.handle);
return 0;
}
while(app_getState() == APP_STATE_RUNNING &&
!atomic_load_explicit(
&g_state.stopVideoThreads, memory_order_acquire))
{
LG_TransportFrame frame;
const LG_TransportStatus status = g_state.videoOps->frame->nextFrame(
g_state.transport.handle, g_state.useDMA, &frame);
if (status != LG_TRANSPORT_OK)
{
if (videoStatusRetry(status,
g_state.videoOps->frame->setStatusListener,
atomic_load_explicit(
&g_state.videoFrameAvailable, memory_order_acquire)))
{
if (g_state.lgr->ops.onFramePoll)
RENDERER(onFramePoll);
continue;
}
if (status == LG_TRANSPORT_DISCONNECTED)
{
lgInput_dropTransport();
lgAudio_dropTransport();
lgClipboard_dropTransport();
primaryLost();
}
else if (status == LG_TRANSPORT_END)
app_setState(APP_STATE_SHUTDOWN);
else
{
DEBUG_ERROR("Frame transport failed with status %d", status);
app_setState(APP_STATE_SHUTDOWN);
}
break;
}
const uint64_t currentGeneration = atomic_load_explicit(
&g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation,
memory_order_acquire);
bool sourceChanged;
if (!videoFramePayloadAccept(&frame, currentGeneration,
g_state.videoOps->frame->setStatusListener != NULL,
&sourceGeneration, &frameSerial, &formatVersion, &sourceChanged))
{
g_state.videoOps->frame->releaseFrame(
g_state.transport.handle, &frame);
continue;
}
const uint64_t dispatchStart = nanotime();
const LG_TransportFrameFormat * format = frame.format;
if (!format)
{
videoPayloadRelease();
DEBUG_ERROR("Transport returned a frame without format metadata");
g_state.videoOps->frame->releaseFrame(g_state.transport.handle, &frame);
app_setState(APP_STATE_SHUTDOWN);
break;
}
const bool formatChanged = sourceChanged ||
!g_state.formatValid || format->version != formatVersion;
bool rendererSupportsNativeHDR = false;
if (formatChanged)
{
memset(&rendererFormat, 0, sizeof(rendererFormat));
rendererFormat.type = format->type;
rendererFormat.screenWidth = format->screenWidth;
rendererFormat.screenHeight = format->screenHeight;
rendererFormat.dataWidth = format->dataWidth;
rendererFormat.dataHeight = format->dataHeight;
rendererFormat.frameWidth = format->frameWidth;
rendererFormat.frameHeight = format->frameHeight;
rendererFormat.stride = format->stride;
rendererFormat.pitch = format->pitch;
rendererFormat.hdr = format->hdr;
rendererFormat.hdrPQ = format->hdrPQ;
rendererFormat.hdrMetadata = format->hdrMetadata;
rendererFormat.sdrWhiteLevel = format->sdrWhiteLevel;
if (rendererFormat.hdrMetadata)
{
memcpy(rendererFormat.hdrDisplayPrimary, format->hdrDisplayPrimary,
sizeof(rendererFormat.hdrDisplayPrimary));
memcpy(rendererFormat.hdrWhitePoint, format->hdrWhitePoint,
sizeof(rendererFormat.hdrWhitePoint));
rendererFormat.hdrMaxDisplayLuminance =
format->hdrMaxDisplayLuminance;
rendererFormat.hdrMinDisplayLuminance =
format->hdrMinDisplayLuminance;
rendererFormat.hdrMaxContentLightLevel =
format->hdrMaxContentLightLevel;
rendererFormat.hdrMaxFrameAverageLightLevel =
format->hdrMaxFrameAverageLightLevel;
}
if (frame.flags & LG_TRANSPORT_FRAME_TRUNCATED)
{
DEBUG_BREAK();
DEBUG_WARN("Transport buffer too small, screen truncated");
DEBUG_BREAK();
app_msgBox("Transport buffer too small",
"The transport buffer is too small for this frame.");
}
switch (format->rotation)
{
case FRAME_ROT_0 : rendererFormat.rotate = LG_ROTATE_0 ; break;
case FRAME_ROT_90 : rendererFormat.rotate = LG_ROTATE_90 ; break;
case FRAME_ROT_180: rendererFormat.rotate = LG_ROTATE_180; break;
case FRAME_ROT_270: rendererFormat.rotate = LG_ROTATE_270; break;
default:
DEBUG_ERROR("Unsupported frame rotation");
rendererFormat.rotate = LG_ROTATE_0;
break;
}
bool invalid = false;
switch (format->type)
{
case FRAME_TYPE_RGBA:
case FRAME_TYPE_BGRA:
case FRAME_TYPE_RGBA10:
rendererFormat.bpp = 32;
break;
case FRAME_TYPE_RGBA16F:
rendererFormat.bpp = 64;
break;
case FRAME_TYPE_BGR_32:
case FRAME_TYPE_RGB_24:
rendererFormat.bpp = 24;
break;
default:
invalid = true;
break;
}
if (invalid)
{
videoPayloadRelease();
DEBUG_ERROR("Unsupported frame type");
g_state.videoOps->frame->releaseFrame(
g_state.transport.handle, &frame);
app_setState(APP_STATE_SHUTDOWN);
break;
}
}
if (formatChanged)
{
DEBUG_INFO("Format: %s %ux%u (%ux%u) stride:%u pitch:%u rotation:%d hdr:%d pq:%d sdrWhite:%u nits",
FrameTypeStr[format->type], format->frameWidth, format->frameHeight,
format->dataWidth, format->dataHeight, format->stride, format->pitch,
format->rotation, format->hdr ? 1 : 0, format->hdrPQ ? 1 : 0,
rendererFormat.sdrWhiteLevel);
LG_LOCK(g_state.lgrLock);
if (!RENDERER(onFrameFormat, rendererFormat))
{
LG_UNLOCK(g_state.lgrLock);
videoPayloadRelease();
DEBUG_ERROR("Renderer failed to configure format");
g_state.videoOps->frame->releaseFrame(
g_state.transport.handle, &frame);
app_setState(APP_STATE_SHUTDOWN);
break;
}
rendererSupportsNativeHDR = !rendererFormat.hdr ||
!g_state.lgr->ops.supports || RENDERER(supports,
rendererFormat.hdrPQ ? LG_SUPPORTS_HDR_PQ : LG_SUPPORTS_HDR_SCRGB);
struct VideoSourceState * source =
&g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY];
atomic_store_explicit(
&source->width, rendererFormat.screenWidth, memory_order_relaxed);
atomic_store_explicit(
&source->height, rendererFormat.screenHeight, memory_order_relaxed);
atomic_store_explicit(
&source->rotate, rendererFormat.rotate, memory_order_release);
}
uint32_t damageCount = frame.damageRectsCount <=
LG_TRANSPORT_MAX_DAMAGE_RECTS ? frame.damageRectsCount : 0;
bool invalidDamage = damageCount != frame.damageRectsCount ||
(damageCount && !frame.damageRects);
for (uint32_t i = 0; !invalidDamage && i < damageCount; ++i)
{
const FrameDamageRect * rect = &frame.damageRects[i];
invalidDamage = rect->x > format->frameWidth ||
rect->y > format->frameHeight ||
rect->width > format->frameWidth - rect->x ||
rect->height > format->frameHeight - rect->y;
}
if (invalidDamage)
{
DEBUG_WARN("Invalid damage rectangles, forcing a full update");
damageCount = 0;
}
const LG_RendererFrameToken frameToken = frameTimingReserve();
g_state.frameImportTime = 0;
g_state.frameImportWaitTime = 0;
const bool rendererOwnsFrame = frame.dmaFD >= 0 && frame.releaseFn;
if (!RENDERER(onFrame, frame.framebuffer, frame.dmaFD,
frame.damageRects, damageCount, frameToken,
rendererOwnsFrame ? frame.releaseFn : NULL,
rendererOwnsFrame ? frame.releaseOpaque : NULL,
rendererOwnsFrame ? frame.releaseHandle : 0))
{
if (formatChanged)
LG_UNLOCK(g_state.lgrLock);
videoPayloadRelease();
frameTimingCancel(frameToken);
g_state.videoOps->frame->releaseFrame(g_state.transport.handle, &frame);
DEBUG_ERROR("Renderer onFrame returned failure");
app_setState(APP_STATE_SHUTDOWN);
break;
}
/* A DMA snapshot can complete on the render thread immediately after it
* is signalled below, so sample producer timing while this lease is still
* unambiguously owned by the frame thread. */
LG_TransportFrameTiming timing = {};
if (g_state.videoOps->frame->getFrameTiming)
g_state.videoOps->frame->getFrameTiming(
g_state.transport.handle, &frame, &timing);
const uint64_t queueStart = nanotime();
atomic_fetch_add_explicit(&g_state.frameCount, 1, memory_order_relaxed);
frameTimingQueue(frameToken, frame.serial, &timing,
g_state.frameImportTime, g_state.frameImportWaitTime,
dispatchStart, queueStart);
if (formatChanged)
{
g_state.formatValid = true;
formatVersion = format->version;
#ifdef ENABLE_TESTS
atomic_store_explicit(&l_testFrameType, format->type,
memory_order_release);
#endif
/* Publish the format only after accepting and timing its first frame.
* lgrLock prevents the render thread from consuming either update until
* both are ready. */
renderQueue_sourceSurfaceFormat(RENDER_QUEUE_SOURCE_PRIMARY,
sourceGeneration, rendererFormat, rendererSupportsNativeHDR);
}
#ifdef ENABLE_TESTS
atomic_store_explicit(&l_testFrameSerial, frame.serial,
memory_order_release);
#endif
if (atomic_load_explicit(
&g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation,
memory_order_acquire) == sourceGeneration &&
atomic_load_explicit(
&g_state.videoFrameAvailable, memory_order_acquire))
{
atomic_store_explicit(
&g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].ready, true,
memory_order_release);
app_useVideoSource(LG_VIDEO_SOURCE_PRIMARY);
}
if (formatChanged)
{
LG_UNLOCK(g_state.lgrLock);
if (g_params.autoResize)
g_state.ds->setWindowSize(rendererFormat.frameWidth,
rendererFormat.frameHeight);
}
if (g_state.jitRender)
{
if (atomic_load_explicit(&g_state.pendingCount, memory_order_acquire) < 10)
atomic_fetch_add_explicit(&g_state.pendingCount, 1,
memory_order_release);
}
else
lgSignalEvent(g_state.frameEvent);
if ((frame.flags & LG_TRANSPORT_FRAME_REQUEST_ACTIVATION) &&
g_params.requestActivation)
g_state.ds->requestActivation();
const bool blockScreensaver =
frame.flags & LG_TRANSPORT_FRAME_BLOCK_SCREENSAVER;
if (g_params.autoScreensaver &&
g_state.autoIdleInhibitState != blockScreensaver)
{
if (blockScreensaver)
g_state.ds->inhibitIdle();
else
g_state.ds->uninhibitIdle();
g_state.autoIdleInhibitState = blockScreensaver;
}
frameTimingFinishFrame(frameToken, &timing);
videoPayloadRelease();
if (!rendererOwnsFrame)
g_state.videoOps->frame->releaseFrame(g_state.transport.handle, &frame);
}
if (app_getState() != APP_STATE_SHUTDOWN)
{
if (app_getState() == APP_STATE_RESTART)
videoSourceInvalidate(LG_VIDEO_SOURCE_PRIMARY);
else
atomic_store_explicit(
&g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].ready, false,
memory_order_release);
if (!fallbackRequestVideo())
videoSourceShowSplashIfNeeded();
}
/* Queue the replacement before releasing desktop snapshots. The existing
* front buffer remains visible until the render thread draws it or the
* splash requested above. */
LG_LOCK(g_state.lgrLock);
RENDERER(onRestart);
LG_UNLOCK(g_state.lgrLock);
/* Renderer reset requests release for every asynchronous DMA snapshot.
* Drain those requests before the transport unsubscribes or reconnects. */
if (g_state.videoOps->frame->stopFrame)
g_state.videoOps->frame->stopFrame(g_state.transport.handle);
return 0;
}
static RenderQueueSource renderQueueSource(LG_VideoSource source)
{
switch(source)
{
case LG_VIDEO_SOURCE_PRIMARY:
return RENDER_QUEUE_SOURCE_PRIMARY;
case LG_VIDEO_SOURCE_FALLBACK:
return RENDER_QUEUE_SOURCE_FALLBACK;
case LG_VIDEO_SOURCE_NONE:
case LG_VIDEO_SOURCE_COUNT:
break;
}
return RENDER_QUEUE_SOURCE_NONE;
}
static void videoSourceBeginLocked(LG_VideoSource source)
{
struct VideoSourceState * state = &g_state.videoSource[source];
const uint64_t generation =
renderQueue_sourceBegin(renderQueueSource(source));
atomic_store_explicit(&state->ready, false, memory_order_release);
atomic_store_explicit(
&state->transitionSerial, 0, memory_order_relaxed);
atomic_store_explicit(
&state->transitionPending, false, memory_order_relaxed);
atomic_store_explicit(&state->appliedWidth, 0, memory_order_relaxed);
atomic_store_explicit(&state->appliedHeight, 0, memory_order_relaxed);
atomic_store_explicit(
&state->appliedRotate, LG_ROTATE_0, memory_order_relaxed);
atomic_store_explicit(
&state->generation, generation, memory_order_release);
state->configurePending = false;
}
static void videoSourceBegin(LG_VideoSource source)
{
LG_LOCK(g_state.videoSourceLock);
videoSourceBeginLocked(source);
LG_UNLOCK(g_state.videoSourceLock);
}
static bool videoSourceInvalidate(LG_VideoSource source)
{
struct VideoSourceState * state = &g_state.videoSource[source];
LG_LOCK(g_state.videoSourceLock);
const uint64_t generation = atomic_exchange_explicit(
&state->generation, 0, memory_order_acq_rel);
atomic_store_explicit(&state->ready, false, memory_order_release);
LG_UNLOCK(g_state.videoSourceLock);
if (generation)
renderQueue_sourceInvalidate(renderQueueSource(source), generation);
return generation != 0;
}
static void videoSourceShowSplashIfNeeded(void)
{
LG_LOCK(g_state.videoSplashLock);
bool available;
for (;;)
{
available = false;
const LG_VideoSource source = atomic_load_explicit(
&g_state.videoSourceRequested, memory_order_acquire);
if (source <= LG_VIDEO_SOURCE_NONE || source >= LG_VIDEO_SOURCE_COUNT)
break;
const struct VideoSourceState * state = &g_state.videoSource[source];
const uint64_t generation = atomic_load_explicit(
&state->generation, memory_order_acquire);
const bool transitionPending = atomic_load_explicit(
&state->transitionPending, memory_order_acquire);
const LG_VideoSource applied = atomic_load_explicit(
&g_state.videoSourceApplied, memory_order_acquire);
const uint64_t appliedGeneration = atomic_load_explicit(
&g_state.videoSourceAppliedGeneration, memory_order_acquire);
available = generation &&
((applied == source && appliedGeneration == generation) ||
(atomic_load_explicit(&state->ready, memory_order_acquire) &&
transitionPending));
if (source == atomic_load_explicit(
&g_state.videoSourceRequested, memory_order_acquire) &&
generation == atomic_load_explicit(
&state->generation, memory_order_acquire))
break;
}
if (!available)
overlaySplash_show(true);
LG_UNLOCK(g_state.videoSplashLock);
}
static void videoSourceClearCursor(LG_VideoSource source)
{
struct VideoSourceState * state = &g_state.videoSource[source];
LG_LOCK(g_state.videoSourceLock);
state->cursorStateValid = false;
state->cursorVisible = false;
state->cursorWhiteLevel = 0;
if (atomic_load_explicit(
&g_state.videoSourceApplied, memory_order_acquire) == source)
g_cursor.guest.valid = false;
renderQueue_sourceClearCursor(renderQueueSource(source));
LG_UNLOCK(g_state.videoSourceLock);
}
static bool videoComponentStatusChanged(atomic_bool * available,
atomic_uint_least64_t * epoch, atomic_int * reason,
const LG_VideoComponentStatus * status)
{
/* Close the component before changing epochs so a consumer can never pair
* a newly available state with a payload from the prior endpoint. */
const bool oldAvailable = atomic_exchange_explicit(
available, false, memory_order_acq_rel);
const uint64_t oldEpoch = atomic_exchange_explicit(
epoch, status->epoch, memory_order_acq_rel);
atomic_store_explicit(reason, status->reason, memory_order_release);
atomic_store_explicit(
available, status->available, memory_order_release);
return oldAvailable != status->available ||
oldEpoch != status->epoch;
}
static bool videoComponentPayloadCurrent(const atomic_bool * available,
const atomic_uint_least64_t * epoch, uint64_t payloadEpoch,
bool trackEpoch)
{
if (!atomic_load_explicit(available, memory_order_acquire))
return false;
return !trackEpoch || (payloadEpoch && atomic_load_explicit(
epoch, memory_order_acquire) == payloadEpoch);
}
/* Success retains the payload gate. The caller must release it after every
* externally visible effect derived from the accepted payload is complete. */
static bool videoPayloadAccept(const atomic_bool * available,
const atomic_uint_least64_t * epoch, uint64_t payloadEpoch,
bool trackEpoch, uint64_t sourceGeneration)
{
LG_LOCK(g_state.videoPayloadLock);
const bool current = videoComponentPayloadCurrent(
available, epoch, payloadEpoch, trackEpoch) &&
(!sourceGeneration || atomic_load_explicit(
&g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation,
memory_order_acquire) == sourceGeneration);
if (!current)
LG_UNLOCK(g_state.videoPayloadLock);
return current;
}
static void videoPayloadRelease(void)
{
LG_UNLOCK(g_state.videoPayloadLock);
}
static bool videoFramePayloadAccept(const LG_TransportFrame * frame,
uint64_t currentGeneration, bool trackEpoch,
uint64_t * sourceGeneration, uint64_t * frameSerial,
uint32_t * formatVersion, bool * sourceChanged)
{
if (!currentGeneration || !videoPayloadAccept(
&g_state.videoFrameAvailable, &g_state.videoFrameEpoch,
frame->epoch, trackEpoch, currentGeneration))
return false;
*sourceChanged = *sourceGeneration != currentGeneration;
if (*sourceChanged)
{
*sourceGeneration = currentGeneration;
*frameSerial = 0;
*formatVersion = 0;
}
if (!*sourceChanged && frame->serial == *frameSerial &&
g_state.formatValid && !frame->scheduleOwner)
{
videoPayloadRelease();
return false;
}
*frameSerial = frame->serial;
return true;
}
static void primaryVideoStatusChanged(void * opaque,
const LG_VideoStatus * status)
{
(void)opaque;
if (!status || !atomic_load_explicit(
&g_state.videoStatusActive, memory_order_acquire) ||
app_getState() != APP_STATE_RUNNING)
return;
LG_LOCK(g_state.videoPayloadLock);
if (!atomic_load_explicit(
&g_state.videoStatusActive, memory_order_acquire) ||
app_getState() != APP_STATE_RUNNING)
{
LG_UNLOCK(g_state.videoPayloadLock);
return;
}
const bool frameWasAvailable = atomic_load_explicit(
&g_state.videoFrameAvailable, memory_order_acquire);
const bool first = !atomic_exchange_explicit(
&g_state.videoStatusKnown, true, memory_order_acq_rel);
const bool frameChanged = videoComponentStatusChanged(
&g_state.videoFrameAvailable, &g_state.videoFrameEpoch,
&g_state.videoFrameReason, &status->frame) || first;
const bool pointerChanged = videoComponentStatusChanged(
&g_state.videoPointerAvailable, &g_state.videoPointerEpoch,
&g_state.videoPointerReason, &status->pointer) || first;
if (pointerChanged)
videoSourceClearCursor(LG_VIDEO_SOURCE_PRIMARY);
if (frameChanged)
{
const uint64_t sourceGeneration = atomic_load_explicit(
&g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].generation,
memory_order_acquire);
if (!status->frame.available || frameWasAvailable || !sourceGeneration)
{
videoSourceInvalidate(LG_VIDEO_SOURCE_PRIMARY);
/* Keep an inactive generation while frames are unavailable so an
* independent pointer component can continue updating its retained
* state. */
videoSourceBegin(LG_VIDEO_SOURCE_PRIMARY);
}
}
LG_UNLOCK(g_state.videoPayloadLock);
if (frameChanged)
g_state.videoOps->frame->cancelFrameWait(g_state.transport.handle);
if (pointerChanged && g_state.videoOps->frame->nextPointer)
g_state.videoOps->frame->cancelPointerWait(g_state.transport.handle);
if (!frameChanged)
return;
if (status->frame.available)
return;
if (atomic_load_explicit(&g_state.stopVideo, memory_order_acquire))
return;
const bool fallbackReady = fallbackRequestVideo();
if (atomic_load_explicit(&g_state.stopVideo, memory_order_acquire))
{
if (g_state.fallback)
lgTransportFallback_requestVideoActive(g_state.fallback, false);
return;
}
if (!fallbackReady)
videoSourceShowSplashIfNeeded();
}
static void primaryVideoStatusStart(void)
{
if (!g_state.videoOps || g_state.videoOps->type != LG_VIDEO_TYPE_FRAME)
return;
atomic_store_explicit(
&g_state.videoFrameAvailable, false, memory_order_relaxed);
atomic_store_explicit(
&g_state.videoPointerAvailable, false, memory_order_relaxed);
atomic_store_explicit(
&g_state.videoStatusKnown, false, memory_order_relaxed);
atomic_store_explicit(
&g_state.videoFrameEpoch, 0, memory_order_relaxed);
atomic_store_explicit(
&g_state.videoPointerEpoch, 0, memory_order_relaxed);
atomic_store_explicit(
&g_state.videoFrameReason, LG_TRANSPORT_UNAVAILABLE,
memory_order_relaxed);
atomic_store_explicit(
&g_state.videoPointerReason, LG_TRANSPORT_UNAVAILABLE,
memory_order_relaxed);
atomic_store_explicit(
&g_state.videoStatusActive, true, memory_order_release);
if (g_state.videoOps->frame->setStatusListener)
{
g_state.videoOps->frame->setStatusListener(g_state.transport.handle,
primaryVideoStatusChanged, NULL);
return;
}
const LG_VideoStatus status =
{
.frame =
{
.available = true,
.epoch = 1,
.reason = LG_TRANSPORT_OK,
},
.pointer =
{
.available = g_state.videoOps->frame->nextPointer != NULL,
.epoch = g_state.videoOps->frame->nextPointer ? 1 : 0,
.reason = g_state.videoOps->frame->nextPointer ?
LG_TRANSPORT_OK : LG_TRANSPORT_UNAVAILABLE,
},
};
primaryVideoStatusChanged(NULL, &status);
}
static void primaryVideoStatusStop(void)
{
if (!atomic_exchange_explicit(
&g_state.videoStatusActive, false, memory_order_acq_rel))
return;
if (g_state.videoOps && g_state.videoOps->type == LG_VIDEO_TYPE_FRAME &&
g_state.videoOps->frame->setStatusListener)
g_state.videoOps->frame->setStatusListener(
g_state.transport.handle, NULL, NULL);
LG_LOCK(g_state.videoPayloadLock);
atomic_store_explicit(
&g_state.videoFrameAvailable, false, memory_order_release);
atomic_store_explicit(
&g_state.videoPointerAvailable, false, memory_order_release);
atomic_store_explicit(
&g_state.videoFrameReason, LG_TRANSPORT_DISCONNECTED,
memory_order_release);
atomic_store_explicit(
&g_state.videoPointerReason, LG_TRANSPORT_DISCONNECTED,
memory_order_release);
atomic_store_explicit(
&g_state.videoStatusKnown, false, memory_order_release);
LG_UNLOCK(g_state.videoPayloadLock);
}
void main_videoStreamEnabled(void)
{
if (!g_state.videoOps || g_state.videoOps->type != LG_VIDEO_TYPE_FRAME ||
!g_state.videoOps->frame->setStatusListener ||
!atomic_load_explicit(
&g_state.videoStatusKnown, memory_order_acquire) ||
atomic_load_explicit(
&g_state.videoFrameAvailable, memory_order_acquire))
return;
if (!fallbackRequestVideo())
videoSourceShowSplashIfNeeded();
}
static void videoSourceReject(void * opaque, RenderQueueSource queueSource,
uint64_t serial)
{
(void)opaque;
LG_VideoSource source = LG_VIDEO_SOURCE_NONE;
if (queueSource == RENDER_QUEUE_SOURCE_PRIMARY)
source = LG_VIDEO_SOURCE_PRIMARY;
else if (queueSource == RENDER_QUEUE_SOURCE_FALLBACK)
source = LG_VIDEO_SOURCE_FALLBACK;
if (source == LG_VIDEO_SOURCE_NONE)
return;
struct VideoSourceState * state = &g_state.videoSource[source];
if (atomic_compare_exchange_strong_explicit(&state->transitionSerial,
&serial, 0, memory_order_acq_rel, memory_order_relaxed))
atomic_store_explicit(
&state->transitionPending, false, memory_order_release);
}
static bool videoSourcePrepare(void * opaque, RenderQueueSource queueSource,
uint64_t generation, uint64_t serial)
{
(void)opaque;
LG_VideoSource source = LG_VIDEO_SOURCE_NONE;
if (queueSource == RENDER_QUEUE_SOURCE_PRIMARY)
source = LG_VIDEO_SOURCE_PRIMARY;
else if (queueSource == RENDER_QUEUE_SOURCE_FALLBACK)
source = LG_VIDEO_SOURCE_FALLBACK;
if (source == LG_VIDEO_SOURCE_NONE)
return true;
struct VideoSourceState * state = &g_state.videoSource[source];
if (atomic_load_explicit(&state->generation, memory_order_acquire) !=
generation ||
!atomic_load_explicit(&state->ready, memory_order_acquire) ||
atomic_load_explicit(
&g_state.videoSourceRequested, memory_order_acquire) != source)
goto reject;
const int width = atomic_load_explicit(
&state->width, memory_order_relaxed);
const int height = atomic_load_explicit(
&state->height, memory_order_relaxed);
const LG_RendererRotate rotate = atomic_load_explicit(
&state->rotate, memory_order_relaxed);
if (width <= 0 || height <= 0)
goto reject;
LG_LOCK(g_state.videoSplashLock);
if (atomic_load_explicit(&state->generation, memory_order_acquire) !=
generation ||
!atomic_load_explicit(&state->ready, memory_order_acquire) ||
atomic_load_explicit(
&g_state.videoSourceRequested, memory_order_acquire) != source)
{
LG_UNLOCK(g_state.videoSplashLock);
goto reject;
}
if (!g_state.haveSrcSize || g_state.srcSize.x != width ||
g_state.srcSize.y != height || g_state.rotate != rotate)
{
g_state.srcSize.x = width;
g_state.srcSize.y = height;
g_state.rotate = rotate;
g_state.haveSrcSize = true;
g_state.videoGeometryDirty = true;
}
atomic_store_explicit(
&g_state.videoSource[source].appliedWidth, width, memory_order_relaxed);
atomic_store_explicit(
&g_state.videoSource[source].appliedHeight, height,
memory_order_relaxed);
atomic_store_explicit(
&g_state.videoSource[source].appliedRotate, rotate,
memory_order_release);
overlaySplash_show(false);
LG_UNLOCK(g_state.videoSplashLock);
return true;
reject:
videoSourceReject(opaque, queueSource, serial);
return false;
}
static void videoSourceApplied(void * opaque, RenderQueueSource queueSource,
uint64_t generation, uint64_t serial, bool swSurface)
{
(void)opaque;
(void)swSurface;
LG_VideoSource source = LG_VIDEO_SOURCE_NONE;
if (queueSource == RENDER_QUEUE_SOURCE_PRIMARY)
source = LG_VIDEO_SOURCE_PRIMARY;
else if (queueSource == RENDER_QUEUE_SOURCE_FALLBACK)
source = LG_VIDEO_SOURCE_FALLBACK;
bool cursorStateValid = false;
bool cursorVisible = false;
int cursorX = 0;
int cursorY = 0;
int cursorHX = 0;
int cursorHY = 0;
LG_LOCK(g_state.videoSourceLock);
atomic_store_explicit(
&g_state.videoSourceApplied, source, memory_order_release);
atomic_store_explicit(
&g_state.videoSourceAppliedGeneration, generation,
memory_order_release);
if (source != LG_VIDEO_SOURCE_NONE)
{
struct VideoSourceState * state = &g_state.videoSource[source];
if (atomic_load_explicit(&state->generation, memory_order_acquire) ==
generation)
{
cursorStateValid = state->cursorStateValid;
cursorVisible = state->cursorVisible;
cursorX = state->cursorX;
cursorY = state->cursorY;
cursorHX = state->cursorHX;
cursorHY = state->cursorHY;
uint64_t pendingSerial = serial;
if (atomic_compare_exchange_strong_explicit(&state->transitionSerial,
&pendingSerial, 0, memory_order_acq_rel, memory_order_relaxed))
{
atomic_store_explicit(
&state->transitionPending, false, memory_order_release);
state->configurePending = false;
}
}
}
const LG_VideoSource requested = atomic_load_explicit(
&g_state.videoSourceRequested, memory_order_acquire);
if (source == LG_VIDEO_SOURCE_PRIMARY &&
requested == LG_VIDEO_SOURCE_PRIMARY && g_state.fallback)
lgTransportFallback_requestVideoActive(g_state.fallback, false);
g_cursor.guest.valid = cursorStateValid;
if (cursorStateValid)
{
g_cursor.guest.visible = cursorVisible;
g_cursor.guest.x = cursorX;
g_cursor.guest.y = cursorY;
g_cursor.guest.hx = cursorHX;
g_cursor.guest.hy = cursorHY;
}
LG_UNLOCK(g_state.videoSourceLock);
const bool fallback = source == LG_VIDEO_SOURCE_FALLBACK;
lgInput_useTransport(!fallback);
overlayStatus_set(LG_USER_STATUS_SPICE, fallback);
atomic_store_explicit(&g_cursor.redraw, true, memory_order_release);
app_invalidateWindow(false);
app_updateMouseState();
app_refreshVideoSource();
}
static bool swSurfaceEventAccepted(LG_VideoSource source)
{
return source != LG_VIDEO_SOURCE_FALLBACK ||
lgTransportFallback_acceptsVideoEvents(g_state.fallback);
}
static void swSurfaceConfigure(LG_VideoSource source,
unsigned int width, unsigned int height)
{
struct VideoSourceState * state = &g_state.videoSource[source];
uint64_t generation;
LG_LOCK(g_state.videoSourceLock);
if (!swSurfaceEventAccepted(source))
{
LG_UNLOCK(g_state.videoSourceLock);
return;
}
videoSourceBeginLocked(source);
generation = atomic_load_explicit(
&state->generation, memory_order_acquire);
atomic_store_explicit(&state->width, width, memory_order_relaxed);
atomic_store_explicit(&state->height, height, memory_order_relaxed);
atomic_store_explicit(&state->rotate, LG_ROTATE_0, memory_order_release);
state->configurePending = true;
LG_UNLOCK(g_state.videoSourceLock);
if (!renderQueue_sourceSwSurfaceConfigure(renderQueueSource(source),
generation, width, height))
return;
LG_LOCK(g_state.videoSourceLock);
if (atomic_load_explicit(
&state->generation, memory_order_acquire) != generation)
{
LG_UNLOCK(g_state.videoSourceLock);
return;
}
atomic_store_explicit(&state->ready, true, memory_order_release);
LG_UNLOCK(g_state.videoSourceLock);
if (source == LG_VIDEO_SOURCE_PRIMARY && g_params.autoResize)
g_state.ds->setWindowSize(width, height);
app_refreshVideoSource();
}
static void swSurfaceDestroy(LG_VideoSource source)
{
if (!videoSourceInvalidate(source))
return;
if (app_getState() == APP_STATE_SHUTDOWN)
return;
const LG_VideoSource applied = atomic_load_explicit(
&g_state.videoSourceApplied, memory_order_acquire);
const LG_VideoSource requested = atomic_load_explicit(
&g_state.videoSourceRequested, memory_order_acquire);
if (applied != source && requested != source)
return;
/* Retain the last presented surface until this source reconnects or a
* replacement becomes ready. */
videoSourceShowSplashIfNeeded();
}
static void swSurfaceDrawFill(LG_VideoSource source, int x, int y,
int width, int height, uint32_t color)
{
LG_LOCK(g_state.videoSourceLock);
const bool accepted = swSurfaceEventAccepted(source);
const uint64_t generation = accepted ? atomic_load_explicit(
&g_state.videoSource[source].generation, memory_order_acquire) : 0;
LG_UNLOCK(g_state.videoSourceLock);
if (!accepted)
return;
renderQueue_sourceSwSurfaceDrawFill(renderQueueSource(source), generation,
x, y, width, height, color);
}
static void swSurfaceDrawBitmap(LG_VideoSource source, bool topDown,
int x, int y, int width, int height, int stride, const void * data)
{
LG_LOCK(g_state.videoSourceLock);
const bool accepted = swSurfaceEventAccepted(source);
const uint64_t generation = accepted ? atomic_load_explicit(
&g_state.videoSource[source].generation, memory_order_acquire) : 0;
LG_UNLOCK(g_state.videoSourceLock);
if (!accepted)
return;
renderQueue_sourceSwSurfaceDrawBitmap(renderQueueSource(source), generation,
x, y, width, height, stride, data, topDown);
}
static void swSurfacePointer(LG_VideoSource source,
const LG_TransportPointer * pointer)
{
struct VideoSourceState * state = &g_state.videoSource[source];
LG_RendererCursor type = LG_CURSOR_COLOR;
if (pointer->flags & LG_TRANSPORT_POINTER_SHAPE)
switch (pointer->type)
{
case CURSOR_TYPE_COLOR:
type = LG_CURSOR_COLOR;
break;
case CURSOR_TYPE_MONOCHROME:
type = LG_CURSOR_MONOCHROME;
break;
case CURSOR_TYPE_MASKED_COLOR:
type = LG_CURSOR_MASKED_COLOR;
break;
default:
return;
}
const bool drawCursor = source != LG_VIDEO_SOURCE_PRIMARY ||
g_cursor.draw || !lgInput_available();
LG_LOCK(g_state.videoSourceLock);
if (!swSurfaceEventAccepted(source))
{
LG_UNLOCK(g_state.videoSourceLock);
return;
}
const uint64_t generation = atomic_load_explicit(
&state->generation, memory_order_acquire);
if (!generation)
{
LG_UNLOCK(g_state.videoSourceLock);
return;
}
const bool wasRendered = state->cursorStateValid &&
state->cursorVisible && drawCursor;
bool hotspotChanged = false;
if (pointer->flags & LG_TRANSPORT_POINTER_SHAPE)
{
const int oldHX = state->cursorHX;
const int oldHY = state->cursorHY;
hotspotChanged =
state->cursorHX != pointer->hx || state->cursorHY != pointer->hy;
state->cursorHX = pointer->hx;
state->cursorHY = pointer->hy;
if (hotspotChanged && state->cursorStateValid &&
!(pointer->flags & LG_TRANSPORT_POINTER_POSITION))
{
state->cursorX += oldHX - pointer->hx;
state->cursorY += oldHY - pointer->hy;
}
}
if (pointer->flags & LG_TRANSPORT_POINTER_POSITION)
{
state->cursorX = pointer->x;
state->cursorY = pointer->y;
state->cursorStateValid = true;
}
if (pointer->flags & LG_TRANSPORT_POINTER_VISIBLE_VALID)
state->cursorVisible = pointer->flags & LG_TRANSPORT_POINTER_VISIBLE;
const bool whiteLevelChanged = pointer->sdrWhiteLevel &&
pointer->sdrWhiteLevel != state->cursorWhiteLevel;
if (whiteLevelChanged)
state->cursorWhiteLevel = pointer->sdrWhiteLevel;
const bool valid = state->cursorStateValid;
const bool guestVisible = state->cursorVisible;
const bool visible = guestVisible && drawCursor;
const int x = state->cursorX;
const int y = state->cursorY;
const int hx = state->cursorHX;
const int hy = state->cursorHY;
const bool isRendered = valid && visible;
const bool sourceApplied = atomic_load_explicit(
&g_state.videoSourceApplied, memory_order_acquire) == source &&
atomic_load_explicit(&g_state.videoSourceAppliedGeneration,
memory_order_acquire) == generation;
bool wasValid = false;
if (sourceApplied)
{
wasValid = g_cursor.guest.valid;
g_cursor.guest.visible = guestVisible;
g_cursor.guest.x = x;
g_cursor.guest.y = y;
g_cursor.guest.hx = hx;
g_cursor.guest.hy = hy;
g_cursor.guest.valid = valid;
}
if (pointer->flags & LG_TRANSPORT_POINTER_SHAPE)
renderQueue_sourceCursorImage(renderQueueSource(source), generation,
type, pointer->width, pointer->height, pointer->pitch,
pointer->shape);
if (pointer->flags & LG_TRANSPORT_POINTER_COLOR_TRANSFORM)
renderQueue_sourceCursorColorTransform(renderQueueSource(source),
generation, pointer->colorTransform);
if (whiteLevelChanged)
renderQueue_sourceCursorWhiteLevel(renderQueueSource(source),
generation, pointer->sdrWhiteLevel);
if (valid && (pointer->flags & (LG_TRANSPORT_POINTER_POSITION |
LG_TRANSPORT_POINTER_VISIBLE_VALID | LG_TRANSPORT_POINTER_SHAPE)))
renderQueue_sourceCursorState(renderQueueSource(source), generation,
visible, x, y, hx, hy);
LG_UNLOCK(g_state.videoSourceLock);
if (sourceApplied)
{
if (!wasValid && valid && core_inputEnabled())
core_alignToGuest();
if (source == LG_VIDEO_SOURCE_PRIMARY &&
((pointer->flags & LG_TRANSPORT_POINTER_POSITION) || hotspotChanged))
core_handleGuestMouseUpdate();
app_updateMouseState();
}
const bool contentChanged =
(pointer->flags & (LG_TRANSPORT_POINTER_SHAPE |
LG_TRANSPORT_POINTER_COLOR_TRANSFORM)) || whiteLevelChanged;
if (sourceApplied && (wasRendered != isRendered ||
((wasRendered || isRendered) &&
(g_params.mouseRedraw || contentChanged))))
cursorRepaintRequest();
}
static LG_VideoSource swSurfaceSource(void * opaque)
{
return (LG_VideoSource)(uintptr_t)opaque;
}
static void swSurfaceEventConfigure(void * opaque,
unsigned int width, unsigned int height)
{
swSurfaceConfigure(swSurfaceSource(opaque), width, height);
}
static void swSurfaceEventDestroy(void * opaque)
{
const LG_VideoSource source = swSurfaceSource(opaque);
if (swSurfaceEventAccepted(source))
swSurfaceDestroy(source);
}
static void swSurfaceEventDrawFill(void * opaque, int x, int y,
int width, int height, uint32_t color)
{
swSurfaceDrawFill(swSurfaceSource(opaque), x, y, width, height, color);
}
static void swSurfaceEventDrawBitmap(void * opaque, bool topDown,
int x, int y, int width, int height, int stride, const void * data)
{
swSurfaceDrawBitmap(swSurfaceSource(opaque), topDown,
x, y, width, height, stride, data);
}
static void swSurfaceEventPointer(void * opaque,
const LG_TransportPointer * pointer)
{
swSurfacePointer(swSurfaceSource(opaque), pointer);
}
static const LG_SwSurfaceEventOps swSurfaceEvents =
{
.configure = swSurfaceEventConfigure,
.destroy = swSurfaceEventDestroy,
.drawFill = swSurfaceEventDrawFill,
.drawBitmap = swSurfaceEventDrawBitmap,
.pointer = swSurfaceEventPointer,
};
static bool fallbackRequestVideo(void)
{
if (!g_state.fallback || atomic_load_explicit(
&g_state.stopVideo, memory_order_acquire))
return false;
lgTransportFallback_requestVideoActive(g_state.fallback, true);
if (atomic_load_explicit(&g_state.stopVideo, memory_order_acquire))
{
lgTransportFallback_requestVideoActive(g_state.fallback, false);
return false;
}
if (!lgTransportFallback_usable(g_state.fallback) ||
!lgTransportFallback_ready(g_state.fallback))
return false;
const bool active = app_useVideoSource(LG_VIDEO_SOURCE_FALLBACK);
if (atomic_load_explicit(&g_state.stopVideo, memory_order_acquire))
{
lgTransportFallback_requestVideoActive(g_state.fallback, false);
return false;
}
return active;
}
static void fallbackConnected(void * opaque,
const LG_TransportSession * session)
{
(void)opaque;
atomic_fetch_and_explicit(&g_state.transportLost,
~TRANSPORT_LOST_FALLBACK, memory_order_acq_rel);
if (app_getState() == APP_STATE_SHUTDOWN)
return;
if (session->name[0] && !atomic_load_explicit(
&g_state.lgHostConnected, memory_order_acquire))
core_setTitle(session->name);
if (lgTransportFallback_videoRequested(g_state.fallback))
fallbackRequestVideo();
else
app_refreshVideoSource();
}
static void fallbackDisconnected(void * opaque)
{
(void)opaque;
swSurfaceDestroy(LG_VIDEO_SOURCE_FALLBACK);
videoSourceClearCursor(LG_VIDEO_SOURCE_FALLBACK);
if (app_getState() != APP_STATE_SHUTDOWN && !atomic_load_explicit(
&g_state.lgHostConnected, memory_order_acquire))
videoSourceShowSplashIfNeeded();
}
static void fallbackVideoStateChanged(void * opaque, bool ready)
{
(void)opaque;
if (app_getState() == APP_STATE_SHUTDOWN)
return;
if (ready && lgTransportFallback_videoRequested(g_state.fallback))
{
fallbackRequestVideo();
return;
}
swSurfaceDestroy(LG_VIDEO_SOURCE_FALLBACK);
videoSourceClearCursor(LG_VIDEO_SOURCE_FALLBACK);
app_refreshVideoSource();
}
static void fallbackLost(void * opaque)
{
(void)opaque;
const unsigned int lost = atomic_fetch_or_explicit(&g_state.transportLost,
TRANSPORT_LOST_FALLBACK, memory_order_acq_rel) |
TRANSPORT_LOST_FALLBACK;
if ((lost & TRANSPORT_LOST_ALL) == TRANSPORT_LOST_ALL)
{
DEBUG_INFO("Primary and fallback transport sessions disconnected");
app_setState(APP_STATE_SHUTDOWN);
}
}
static void fallbackEndpointMismatch(void * opaque, const uint8_t primary[16],
const uint8_t fallback[16])
{
(void)opaque;
(void)primary;
(void)fallback;
atomic_store_explicit(
&g_state.fallbackEndpointMismatch, true, memory_order_release);
app_invalidateWindow(false);
}
static const LG_TransportFallbackEventOps fallbackEvents =
{
.connected = fallbackConnected,
.lost = fallbackLost,
.disconnected = fallbackDisconnected,
.videoStateChanged = fallbackVideoStateChanged,
.endpointMismatch = fallbackEndpointMismatch,
};
static void primaryLost(void)
{
atomic_store_explicit(
&g_state.lgHostConnected, false, memory_order_release);
unsigned int lost = atomic_load_explicit(
&g_state.transportLost, memory_order_acquire);
for (;;)
{
unsigned int next = lost | TRANSPORT_LOST_PRIMARY;
if (lgTransportFallback_usable(g_state.fallback))
next &= ~TRANSPORT_LOST_FALLBACK;
if (atomic_compare_exchange_weak_explicit(&g_state.transportLost,
&lost, next, memory_order_acq_rel, memory_order_acquire))
{
lost = next;
break;
}
}
if ((lost & TRANSPORT_LOST_ALL) == TRANSPORT_LOST_ALL)
{
DEBUG_INFO("Primary and fallback transport sessions disconnected");
app_setState(APP_STATE_SHUTDOWN);
return;
}
DEBUG_INFO("Waiting for the host to restart...");
app_setState(APP_STATE_RESTART);
}
static bool fallbackStart(const uint8_t primaryUUID[16])
{
if (!option_get_bool("spice", "enable") ||
strcmp(g_params.transport, "spice") == 0)
return true;
if (lgTransportFallback_start("spice", &swSurfaceEvents,
(void *)(uintptr_t)LG_VIDEO_SOURCE_FALLBACK,
&fallbackEvents, NULL, primaryUUID, &g_state.fallback))
return true;
DEBUG_ERROR("Failed to start the SPICE fallback transport");
return false;
}
static void fallbackStop(void)
{
lgTransportFallback_stop(&g_state.fallback);
}
static void fallbackHandleEvents(void)
{
if (!atomic_exchange_explicit(
&g_state.fallbackEndpointMismatch, false, memory_order_acq_rel))
return;
app_msgBox(
"SPICE Configuration Error",
"You have connected SPICE to the wrong guest.\n"
"SPICE fallback services will not function until this is corrected.");
}
void intHandler(int sig)
{
switch(sig)
{
case SIGINT:
case SIGTERM:
if (app_getState() != APP_STATE_SHUTDOWN)
{
DEBUG_INFO("Caught signal, shutting down...");
app_setState(APP_STATE_SHUTDOWN);
}
else
{
DEBUG_INFO("Caught second signal, force quitting...");
printAllThreadBacktraces();
signal(sig, SIG_DFL);
raise(sig);
}
break;
}
}
static bool tryRenderer(const int index, const LG_RendererParams lgrParams,
bool * needsOpenGL)
{
const LG_RendererOps *r = LG_Renderers[index];
if (!IS_LG_RENDERER_VALID(r))
{
DEBUG_ERROR("FIXME: Renderer %d is invalid, skipping", index);
return false;
}
// create the renderer
g_state.lgr = NULL;
*needsOpenGL = false;
if (!r->create(&g_state.lgr, lgrParams, needsOpenGL))
{
g_state.lgr = NULL;
return false;
}
// init the ops member
memcpy(&g_state.lgr->ops, r, sizeof(*r));
// initialize the renderer
if (!r->initialize(g_state.lgr))
{
r->deinitialize(g_state.lgr);
g_state.lgr = NULL;
return false;
}
DEBUG_INFO("Using Renderer: %s", r->getName());
return true;
}
static void reportBadVersion(const LG_VersionMismatch * mismatch)
{
DEBUG_BREAK();
if (mismatch->valid)
{
DEBUG_ERROR("Incompatible %s version", mismatch->component);
DEBUG_ERROR("Expected version: %u", mismatch->expectedVersion);
DEBUG_ERROR("Current version : %u", mismatch->currentVersion);
}
else
DEBUG_ERROR("The transport is not compatible with this client");
DEBUG_ERROR("Please install matching Looking Glass components");
}
static const int RECOVERY_PENDING = -1;
static const int RECOVERY_NO = 0;
static const int RECOVERY_YES = 1;
struct RecoveryPrompt
{
atomic_int choice;
atomic_uintptr_t handle;
atomic_uintptr_t message;
atomic_bool messageClosed;
uint64_t instance;
uint32_t serial;
uint32_t failedSerial;
LG_RecoveryState reportedState;
bool shown;
bool requested;
bool owned;
bool retryPrompt;
bool retryDeclined;
};
static bool recoveryGetInfo(LG_RecoveryInfo * info)
{
if (!g_state.transport.ops->getRecoveryInfo)
return false;
return g_state.transport.ops->getRecoveryInfo(
g_state.transport.handle, info) == LG_TRANSPORT_OK;
}
static void recoveryConfirm(bool yes, void * opaque)
{
struct RecoveryPrompt * prompt = opaque;
atomic_store_explicit(&prompt->choice,
yes ? RECOVERY_YES : RECOVERY_NO, memory_order_release);
}
static void recoveryClosePrompt(struct RecoveryPrompt * prompt)
{
if (atomic_load_explicit(
&prompt->choice, memory_order_acquire) != RECOVERY_PENDING)
{
atomic_store_explicit(&prompt->handle, 0, memory_order_relaxed);
return;
}
MsgBoxHandle handle = (MsgBoxHandle)atomic_exchange_explicit(
&prompt->handle, 0, memory_order_acq_rel);
app_msgBoxClose(handle);
}
static void recoveryCloseMessage(struct RecoveryPrompt * prompt)
{
MsgBoxHandle handle = (MsgBoxHandle)atomic_exchange_explicit(
&prompt->message, 0, memory_order_acq_rel);
app_msgBoxClose(handle);
}
static void recoveryMessageClosed(MsgBoxHandle handle, void * opaque)
{
struct RecoveryPrompt * prompt = opaque;
atomic_store_explicit(
&prompt->messageClosed, true, memory_order_release);
uintptr_t expected = (uintptr_t)handle;
atomic_compare_exchange_strong_explicit(&prompt->message,
&expected, 0, memory_order_acq_rel, memory_order_acquire);
}
static void recoveryBeginMessage(struct RecoveryPrompt * prompt)
{
recoveryCloseMessage(prompt);
atomic_store_explicit(
&prompt->messageClosed, false, memory_order_relaxed);
}
static void recoveryStoreMessage(struct RecoveryPrompt * prompt,
MsgBoxHandle handle)
{
atomic_store_explicit(
&prompt->message, (uintptr_t)handle, memory_order_release);
if (!atomic_load_explicit(
&prompt->messageClosed, memory_order_acquire))
return;
uintptr_t expected = (uintptr_t)handle;
atomic_compare_exchange_strong_explicit(&prompt->message,
&expected, 0, memory_order_acq_rel, memory_order_acquire);
}
static void recoveryClose(struct RecoveryPrompt * prompt)
{
recoveryClosePrompt(prompt);
recoveryCloseMessage(prompt);
}
static int recoveryExit(struct RecoveryPrompt * prompt, int result)
{
recoveryClose(prompt);
return result;
}
static void retainMessage(MsgBoxHandle * messages, size_t capacity,
int * count, MsgBoxHandle message)
{
if (!message)
return;
if ((size_t)*count >= capacity)
{
app_msgBoxClose(message);
return;
}
messages[(*count)++] = message;
}
static void recoveryShowPrompt(struct RecoveryPrompt * prompt,
const LG_VersionMismatch * mismatch, bool hasFallback)
{
const char * fallback = hasFallback ?
"The client will then switch to its SPICE fallback." :
"SPICE is disabled in this client, so use another SPICE viewer.";
recoveryCloseMessage(prompt);
prompt->shown = true;
prompt->retryPrompt = false;
atomic_store_explicit(
&prompt->choice, RECOVERY_PENDING, memory_order_relaxed);
MsgBoxHandle handle;
if (mismatch->valid)
{
const char * component = mismatch->component[0] ?
mismatch->component : "transport";
handle = app_confirmMsgBox(
"Incompatible Transport Version", recoveryConfirm, prompt,
"Expected %s version: %u\n"
"Current %s version: %u\n"
"\n"
"Enable recovery mode to restore the guest display topology?\n"
"%s",
component, mismatch->expectedVersion,
component, mismatch->currentVersion, fallback);
}
else
{
handle = app_confirmMsgBox(
"Incompatible Transport Version", recoveryConfirm, prompt,
"The transport is not compatible with this client.\n"
"\n"
"Enable recovery mode to restore the guest display topology?\n"
"%s", fallback);
}
atomic_store_explicit(
&prompt->handle, (uintptr_t)handle, memory_order_release);
if (atomic_load_explicit(
&prompt->choice, memory_order_acquire) != RECOVERY_PENDING)
atomic_store_explicit(&prompt->handle, 0, memory_order_relaxed);
}
static const char * recoveryErrorText(LG_RecoveryError error)
{
switch (error)
{
case LG_RECOVERY_ERR_HELPER_UNAVAILABLE:
return "The IDD helper is unavailable";
case LG_RECOVERY_ERR_TOPOLOGY_FAILED:
return "Windows could not restore the saved display topology";
case LG_RECOVERY_ERR_NO_FALLBACK_DISPLAY:
return "No fallback display became active";
case LG_RECOVERY_ERR_BUSY:
return "A conflicting recovery request is already in progress";
case LG_RECOVERY_ERR_CAPACITY:
return "Too many recovery requests are pending";
case LG_RECOVERY_ERR_UNSUPPORTED:
return "Recovery is not supported by the guest driver";
case LG_RECOVERY_ERR_NONE:
break;
}
return "The guest could not enter recovery mode";
}
static int recoveryTakeChoice(struct RecoveryPrompt * prompt)
{
const int choice = atomic_exchange_explicit(
&prompt->choice, RECOVERY_PENDING, memory_order_acq_rel);
if (choice != RECOVERY_PENDING)
atomic_store_explicit(&prompt->handle, 0, memory_order_relaxed);
return choice;
}
static void recoveryShowRetry(struct RecoveryPrompt * prompt,
const char * reason, uint32_t failedSerial)
{
recoveryClose(prompt);
if (failedSerial)
prompt->failedSerial = failedSerial;
prompt->serial = 0;
prompt->reportedState = LG_RECOVERY_STATE_FAILED;
prompt->shown = true;
prompt->requested = false;
prompt->owned = false;
prompt->retryPrompt = true;
atomic_store_explicit(
&prompt->choice, RECOVERY_PENDING, memory_order_relaxed);
MsgBoxHandle handle = app_confirmMsgBox(
"Recovery Failed", recoveryConfirm, prompt,
"%s.\n\nRetry guest display recovery?", reason);
atomic_store_explicit(
&prompt->handle, (uintptr_t)handle, memory_order_release);
if (atomic_load_explicit(
&prompt->choice, memory_order_acquire) != RECOVERY_PENDING)
atomic_store_explicit(&prompt->handle, 0, memory_order_relaxed);
}
static void recoveryShowPending(struct RecoveryPrompt * prompt,
const LG_VersionMismatch * mismatch)
{
prompt->shown = true;
prompt->reportedState = LG_RECOVERY_STATE_SWITCHING;
prompt->retryPrompt = false;
if (mismatch->valid)
{
const char * component = mismatch->component[0] ?
mismatch->component : "transport";
recoveryBeginMessage(prompt);
recoveryStoreMessage(prompt, app_msgBoxWithClose(
"Incompatible Transport Version",
recoveryMessageClosed, prompt,
"Expected %s version: %u\n"
"Current %s version: %u\n"
"\n"
"Recovery mode is pending. Waiting for the guest driver.",
component, mismatch->expectedVersion,
component, mismatch->currentVersion));
}
else
{
recoveryBeginMessage(prompt);
recoveryStoreMessage(prompt, app_msgBoxWithClose(
"Incompatible Transport Version",
recoveryMessageClosed, prompt,
"The transport is not compatible with this client.\n"
"\n"
"Recovery mode is pending. Waiting for the guest driver."));
}
}
static bool recoverySerialNewer(uint32_t serial, uint32_t reference)
{
const uint32_t difference = serial - reference;
return difference && difference < 0x80000000U;
}
static void recoveryHandleMismatch(struct RecoveryPrompt * prompt,
const LG_VersionMismatch * mismatch)
{
LG_RecoveryInfo info = { 0 };
const bool available = recoveryGetInfo(&info) &&
(info.capabilities & LG_RECOVERY_CAP_DISPLAY);
if (!available)
{
if (prompt->requested && !prompt->retryPrompt &&
!prompt->retryDeclined)
{
recoveryShowRetry(prompt,
"The guest recovery channel is unavailable", 0);
return;
}
if (prompt->shown)
return;
prompt->shown = true;
if (mismatch->valid)
{
const char * component = mismatch->component[0] ?
mismatch->component : "transport";
recoveryBeginMessage(prompt);
recoveryStoreMessage(prompt, app_msgBoxWithClose(
"Incompatible Transport Version",
recoveryMessageClosed, prompt,
"Expected %s version: %u\n"
"Current %s version: %u\n"
"\n"
"This guest driver does not support remote recovery.\n"
"Use a guest console to install matching components.",
component, mismatch->expectedVersion,
component, mismatch->currentVersion));
}
else
{
recoveryBeginMessage(prompt);
recoveryStoreMessage(prompt, app_msgBoxWithClose(
"Incompatible Transport Version",
recoveryMessageClosed, prompt,
"The transport is not compatible with this client.\n"
"\n"
"This guest driver does not support remote recovery.\n"
"Use a guest console to install matching components."));
}
return;
}
if (prompt->instance != info.instance)
{
recoveryClose(prompt);
prompt->instance = info.instance;
prompt->serial = 0;
prompt->failedSerial = 0;
prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN;
prompt->shown = false;
prompt->requested = false;
prompt->owned = false;
prompt->retryPrompt = false;
prompt->retryDeclined = false;
atomic_store_explicit(
&prompt->choice, RECOVERY_PENDING, memory_order_relaxed);
}
if (info.uuidValid)
lgTransportFallback_setPrimaryUUID(g_state.fallback, info.uuid);
else
lgTransportFallback_clearPrimaryUUID(g_state.fallback);
const bool requestNewer = info.requestSerial != 0 &&
(info.ackSerial == 0 ||
recoverySerialNewer(info.requestSerial, info.ackSerial));
const LG_RecoveryRequest globalRequest = requestNewer ?
info.request : info.ackRequest;
const uint32_t globalSerial = requestNewer ?
info.requestSerial : info.ackSerial;
if (prompt->requested && globalSerial &&
globalRequest == LG_RECOVERY_REQ_NORMAL &&
(globalSerial == prompt->serial ||
recoverySerialNewer(globalSerial, prompt->serial)))
{
recoveryClose(prompt);
prompt->serial = 0;
prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN;
prompt->shown = prompt->retryDeclined;
prompt->requested = false;
prompt->owned = false;
prompt->retryPrompt = false;
atomic_store_explicit(
&prompt->choice, RECOVERY_PENDING, memory_order_relaxed);
}
const bool pendingRecovery = requestNewer &&
info.request == LG_RECOVERY_REQ_RECOVERY;
const bool failedRecovery = !requestNewer && info.ackSerial != 0 &&
info.ackRequest == LG_RECOVERY_REQ_RECOVERY &&
info.state == LG_RECOVERY_STATE_FAILED;
const bool statusRecovery = !requestNewer && info.ackSerial != 0 &&
info.ackRequest == LG_RECOVERY_REQ_RECOVERY &&
(info.state == LG_RECOVERY_STATE_SWITCHING ||
info.state == LG_RECOVERY_STATE_ACTIVE);
if (failedRecovery && prompt->requested &&
prompt->failedSerial != info.ackSerial &&
(info.ackSerial == prompt->serial ||
recoverySerialNewer(info.ackSerial, prompt->serial)))
{
if (prompt->owned && info.ackSerial == prompt->serial)
{
recoveryShowRetry(prompt, recoveryErrorText(info.error),
info.ackSerial);
return;
}
recoveryClose(prompt);
prompt->serial = 0;
prompt->failedSerial = info.ackSerial;
prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN;
prompt->shown = prompt->retryDeclined;
prompt->requested = false;
prompt->owned = false;
prompt->retryPrompt = false;
atomic_store_explicit(
&prompt->choice, RECOVERY_PENDING, memory_order_relaxed);
}
if (pendingRecovery)
{
if (!prompt->requested || prompt->serial != info.requestSerial)
{
recoveryClose(prompt);
prompt->requested = true;
prompt->serial = info.requestSerial;
prompt->failedSerial = 0;
prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN;
prompt->shown = false;
prompt->owned = false;
prompt->retryPrompt = false;
atomic_store_explicit(
&prompt->choice, RECOVERY_PENDING, memory_order_relaxed);
}
}
else if (statusRecovery &&
(!prompt->requested || prompt->serial != info.ackSerial))
{
recoveryClose(prompt);
prompt->requested = true;
prompt->serial = info.ackSerial;
prompt->failedSerial = 0;
prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN;
prompt->shown = false;
prompt->owned = false;
prompt->retryPrompt = false;
atomic_store_explicit(
&prompt->choice, RECOVERY_PENDING, memory_order_relaxed);
}
const bool retryChoice = prompt->retryPrompt;
const int choice = recoveryTakeChoice(prompt);
if (choice == RECOVERY_NO)
{
prompt->retryPrompt = false;
if (retryChoice)
prompt->retryDeclined = true;
return;
}
if (choice == RECOVERY_YES)
{
prompt->retryPrompt = false;
prompt->retryDeclined = false;
const LG_TransportStatus status =
g_state.transport.ops->requestRecovery ?
g_state.transport.ops->requestRecovery(g_state.transport.handle,
LG_RECOVERY_REQ_RECOVERY, &prompt->serial) :
LG_TRANSPORT_UNAVAILABLE;
prompt->requested = status == LG_TRANSPORT_OK;
prompt->owned = prompt->requested;
prompt->reportedState = LG_RECOVERY_STATE_UNKNOWN;
if (!prompt->requested)
{
recoveryShowRetry(prompt,
"The recovery request could not be sent to the guest driver", 0);
return;
}
prompt->failedSerial = 0;
fallbackRequestVideo();
recoveryShowPending(prompt, mismatch);
app_alert(LG_ALERT_INFO, "Guest display recovery requested");
return;
}
const bool matchingAck = prompt->requested && statusRecovery &&
info.ackSerial == prompt->serial;
if (matchingAck && info.state == LG_RECOVERY_STATE_ACTIVE)
{
fallbackRequestVideo();
if (prompt->reportedState != LG_RECOVERY_STATE_ACTIVE)
{
prompt->shown = true;
prompt->reportedState = LG_RECOVERY_STATE_ACTIVE;
if (mismatch->valid)
{
const char * component = mismatch->component[0] ?
mismatch->component : "transport";
recoveryBeginMessage(prompt);
recoveryStoreMessage(prompt, app_msgBoxWithClose(
"Incompatible Transport Version",
recoveryMessageClosed, prompt,
"Expected %s version: %u\n"
"Current %s version: %u\n"
"\n"
"Recovery mode is active. SPICE video is available.",
component, mismatch->expectedVersion,
component, mismatch->currentVersion));
}
else
{
recoveryBeginMessage(prompt);
recoveryStoreMessage(prompt, app_msgBoxWithClose(
"Incompatible Transport Version",
recoveryMessageClosed, prompt,
"The transport is not compatible with this client.\n"
"\n"
"Recovery mode is active. SPICE video is available."));
}
app_alert(LG_ALERT_SUCCESS, "Guest display recovery is active");
}
return;
}
const bool pending = prompt->requested &&
((pendingRecovery &&
info.requestSerial == prompt->serial) ||
(matchingAck && info.state == LG_RECOVERY_STATE_SWITCHING));
if (pending)
{
fallbackRequestVideo();
if (prompt->reportedState != LG_RECOVERY_STATE_SWITCHING)
recoveryShowPending(prompt, mismatch);
return;
}
if (!prompt->shown)
recoveryShowPrompt(prompt, mismatch, g_state.fallback != NULL);
}
static MsgBoxHandle showSpiceInputHelp(void)
{
static bool done = false;
if (!option_get_bool("spice", "enable") ||
!option_get_bool("spice", "input") || done)
return NULL;
done = true;
return app_msgBox(
"Information",
"Please note you can still control your guest\n"
"through SPICE if you press the capture key.");
}
struct TransportSessionProbe
{
LG_Transport * transport;
const LG_TransportOps * ops;
LG_TransportCancelledFn cancelled;
void * cancelOpaque;
LG_TransportSession session;
LG_TransportStatus status;
atomic_bool done;
};
static bool transportSessionCancelled(void * opaque)
{
(void)opaque;
return app_getState() != APP_STATE_RUNNING;
}
static bool primarySurfaceActivationCancelled(void * opaque)
{
(void)opaque;
return app_getState() != APP_STATE_RUNNING ||
atomic_load_explicit(&g_state.stopVideo, memory_order_acquire);
}
static int transportSessionProbe(void * opaque)
{
struct TransportSessionProbe * probe = opaque;
probe->status = probe->ops->connectCancellable(
probe->transport, &probe->session,
probe->cancelled, probe->cancelOpaque);
atomic_store_explicit(&probe->done, true, memory_order_release);
return 0;
}
static int lg_run(void)
{
LG_LOCK_INIT(l_cursorRepaint.lock);
frameTimingInit();
lgInput_init();
lgAudio_init();
if (!lgClipboard_init())
return -1;
#ifdef ENABLE_TESTS
memset(&l_testCapture, 0, sizeof(l_testCapture));
atomic_store_explicit(&l_testFrameSerial, 0, memory_order_relaxed);
atomic_store_explicit(&l_testFrameType, FRAME_TYPE_INVALID,
memory_order_relaxed);
if (strcmp(g_params.transport, "test") == 0)
{
const char * capturePath = option_get_string("test", "captureFile");
const int captureFrame = option_get_int("test", "captureFrame");
const int captureDelay = option_get_int("test", "captureDelay");
if (capturePath)
{
if (captureFrame < 1 || captureDelay < 0)
{
DEBUG_ERROR("test capture requires captureFrame >= 1 and "
"captureDelay >= 0");
return -1;
}
l_testCapture.path = capturePath;
l_testCapture.targetSerial = captureFrame;
l_testCapture.delay = captureDelay;
l_testCapture.enabled = true;
}
}
#endif
g_cursor.sens = g_params.mouseSens;
if (g_cursor.sens < -9) g_cursor.sens = -9;
else if (g_cursor.sens > 9) g_cursor.sens = 9;
/* setup imgui */
igCreateContext(NULL);
g_state.io = igGetIO_Nil();
g_state.style = igGetStyle();
g_state.style->Colors[ImGuiCol_ModalWindowDimBg] = (ImVec4) { 0.0f, 0.0f, 0.0f, 0.4f };
alloc_sprintf(&g_state.imGuiIni, "%s/imgui.ini", lgConfigDir());
g_state.io->IniFilename = g_state.imGuiIni;
g_state.io->BackendFlags |= ImGuiBackendFlags_HasMouseCursors;
g_state.windowScale = 1.0;
if (!util_initUIFonts())
{
DEBUG_ERROR("Failed to initialize UI fonts");
return -1;
}
g_state.fontName = util_getUIFont(g_params.uiFont);
if (!g_state.fontName)
{
DEBUG_ERROR("Failed to load UI font: %s", g_params.uiFont);
return -1;
}
DEBUG_INFO("Using font: %s", g_state.fontName);
// initialize metrics ringbuffers
g_state.renderTimings = ringbuffer_new(256, sizeof(float));
g_state.frameLatency = ringbuffer_new(4096, sizeof(OverlayFrameTiming));
overlayGraph_setCompact(overlayGraph_register(
"FRAME", g_state.renderTimings, 0.0f, 50.0f, NULL), true);
overlayGraph_registerFrameTiming("FRAME LATENCY", g_state.frameLatency);
// unknown guest OS at this time
g_state.guestOS = LG_TRANSPORT_OS_OTHER;
// search for the best displayserver ops to use
for(int i = 0; i < LG_DISPLAYSERVER_COUNT; ++i)
if (LG_DisplayServers[i]->probe())
{
g_state.ds = LG_DisplayServers[i];
break;
}
if (!g_state.ds)
{
DEBUG_ERROR("No display servers available, tried:");
for (int i = 0; i < LG_DISPLAYSERVER_COUNT; ++i)
DEBUG_ERROR("* %s", LG_DisplayServers[i]->name);
return -1;
}
ASSERT_LG_DS_VALID(g_state.ds);
if (g_params.jitRender)
{
if (g_state.ds->waitFrame)
g_state.jitRender = true;
else
DEBUG_WARN("JIT render not supported on display server backend, disabled");
}
// init the subsystem
if (!g_state.ds->earlyInit())
{
DEBUG_ERROR("Subsystem early init failed");
return -1;
}
if (evdev_start())
DEBUG_INFO("Using evdev for keyboard capture");
// override the SIGINIT handler so that we can tell the difference between
// SIGINT and the user sending a close event, such as ALT+F4
signal(SIGINT , intHandler);
signal(SIGTERM, intHandler);
if (!lgTransport_create(g_params.transport, &g_state.transport))
{
DEBUG_ERROR("Failed to create transport: %s", g_params.transport);
return -1;
}
DEBUG_INFO("Using Transport: %s", g_state.transport.ops->name);
LG_RecoveryInfo initialRecovery = { 0 };
const bool initialRecoveryValid = recoveryGetInfo(&initialRecovery);
g_state.videoOps = g_state.transport.ops->getVideoOps ?
g_state.transport.ops->getVideoOps(g_state.transport.handle) : NULL;
if (!g_state.videoOps ||
(g_state.videoOps->type != LG_VIDEO_TYPE_FRAME &&
g_state.videoOps->type != LG_VIDEO_TYPE_SW_SURFACE) ||
(g_state.videoOps->type == LG_VIDEO_TYPE_FRAME &&
(!g_state.videoOps->frame ||
!g_state.videoOps->frame->nextFrame ||
!g_state.videoOps->frame->releaseFrame ||
!g_state.videoOps->frame->cancelFrameWait ||
((g_state.videoOps->frame->nextPointer != NULL) !=
(g_state.videoOps->frame->releasePointer != NULL)) ||
(g_state.videoOps->frame->nextPointer &&
!g_state.videoOps->frame->cancelPointerWait))) ||
(g_state.videoOps->type == LG_VIDEO_TYPE_SW_SURFACE &&
(!g_state.videoOps->swSurface ||
!g_state.videoOps->swSurface->attach ||
!g_state.videoOps->swSurface->detach ||
!g_state.videoOps->swSurface->setActive ||
!g_state.videoOps->swSurface->cancelPending)))
{
DEBUG_ERROR("Transport does not provide a usable video source");
return -1;
}
DEBUG_INFO("Using Video: %s", g_state.videoOps->name);
// setup the startup condition
if (!(e_startup = lgCreateEvent(false, 0)))
{
DEBUG_ERROR("failed to create the startup event");
return -1;
}
// setup the new frame event
if (!(g_state.frameEvent = lgCreateEvent(!g_state.jitRender, 0)))
{
DEBUG_ERROR("failed to create the frame event");
return -1;
}
//setup the render command queue
LG_LOCK_INIT(g_state.videoPayloadLock);
LG_LOCK_INIT(g_state.videoSourceLock);
LG_LOCK_INIT(g_state.videoSplashLock);
renderQueue_init();
renderQueue_setSourceFns(
videoSourcePrepare, videoSourceReject, videoSourceApplied, NULL);
atomic_store_explicit(&g_state.videoSourceRequested,
LG_VIDEO_SOURCE_PRIMARY, memory_order_relaxed);
atomic_store_explicit(&g_state.videoSourceApplied,
LG_VIDEO_SOURCE_NONE, memory_order_relaxed);
g_state.videoSource[LG_VIDEO_SOURCE_PRIMARY].swSurface =
g_state.videoOps->type == LG_VIDEO_TYPE_SW_SURFACE;
g_state.videoSource[LG_VIDEO_SOURCE_FALLBACK].swSurface = true;
frameScheduler_init();
g_state.micDefaultState = g_params.micDefaultState;
const uint8_t * fallbackUUID = initialRecoveryValid &&
initialRecovery.uuidValid ? initialRecovery.uuid : NULL;
if (!fallbackStart(fallbackUUID))
return -1;
// select and init a renderer
bool needsOpenGL = false;
LG_RendererParams lgrParams;
lgrParams.quickSplash = g_params.quickSplash;
if (g_params.forceRenderer)
{
DEBUG_INFO("Trying forced renderer");
if (!tryRenderer(g_params.forceRendererIndex, lgrParams, &needsOpenGL))
{
DEBUG_ERROR("Forced renderer failed to iniailize");
return -1;
}
}
else
{
// probe for a a suitable renderer
for(unsigned int i = 0; i < LG_RENDERER_COUNT; ++i)
{
if (tryRenderer(i, lgrParams, &needsOpenGL))
break;
}
}
if (!g_state.lgr)
{
DEBUG_ERROR("Unable to find a suitable renderer");
return -1;
}
g_state.useDMA = g_state.videoOps->type == LG_VIDEO_TYPE_FRAME &&
g_state.videoOps->frame->supportsDMA(g_state.transport.handle);
// initialize the window dimensions at init for renderers
g_state.windowW = g_params.w;
g_state.windowH = g_params.h;
g_state.windowCX = g_params.w / 2;
g_state.windowCY = g_params.h / 2;
core_updatePositionInfo();
const LG_DSInitParams params =
{
.title = g_params.windowTitle,
.appId = g_params.appId,
.x = g_params.x,
.y = g_params.y,
.w = g_params.w,
.h = g_params.h,
.center = g_params.center,
.fullscreen = g_params.fullscreen,
.resizable = g_params.allowResize,
.borderless = g_params.borderless,
.maximize = g_params.maximize,
.largeCursorDot = g_params.largeCursorDot,
.allowNoInput = strcmp(g_params.transport, "test") == 0,
.opengl = needsOpenGL,
.jitRender = g_params.jitRender
};
g_state.dsInitialized = g_state.ds->init(params);
if (!g_state.dsInitialized)
{
DEBUG_ERROR("Failed to initialize the displayserver backend");
return -1;
}
if (g_params.noScreensaver)
g_state.ds->inhibitIdle();
// ensure renderer viewport is aware of the current window size
core_updatePositionInfo();
if (g_params.fpsMin <= 0)
{
// default 30 fps
g_state.frameTime = 1000000000ULL / 30ULL;
}
else
{
DEBUG_INFO("Using the FPS minimum from args: %d", g_params.fpsMin);
g_state.frameTime = 1000000000ULL / (unsigned long long)g_params.fpsMin;
}
// when the overlay is shown we should run at a minimum of 60 fps for
// interactivity.
g_state.overlayFrameTime = min(g_state.frameTime, 1000000000ULL / 60ULL);
if (!g_state.jitRender)
{
if (!(e_cursorRepaint = lgCreateEvent(true, 0)))
{
DEBUG_ERROR("failed to create the cursor repaint event");
return -1;
}
if (!lgCreateThread("cursorRepaint", cursorRepaintThread, NULL,
&t_cursorRepaint))
{
DEBUG_ERROR("cursor repaint thread creation failed");
return -1;
}
}
keybind_commonRegister();
keybind_inputRegister();
if (g_state.jitRender)
DEBUG_INFO("Using JIT render mode");
lgInit();
// start the renderThread so we don't just display junk
if (!lgCreateThread("renderThread", renderThread, NULL, &t_render))
{
DEBUG_ERROR("render create thread failed");
return -1;
}
// wait for startup to complete so that any error messages below are output at
// the end of the output
lgWaitEvent(e_startup, TIMEOUT_INFINITE);
LG_RendererInterop interop = {
.version = LG_RENDERER_INTEROP_VERSION,
.size = sizeof(interop),
};
const LG_RendererInterop * interopPtr = NULL;
if (g_state.lgr->ops.getInterop &&
g_state.lgr->ops.getInterop(g_state.lgr, &interop))
interopPtr = &interop;
bool videoAttached;
if (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME)
videoAttached = !g_state.videoOps->frame->attachRenderer ||
g_state.videoOps->frame->attachRenderer(
g_state.transport.handle, interopPtr);
else
videoAttached = g_state.videoOps->swSurface->attach(
g_state.transport.handle, &swSurfaceEvents,
(void *)(uintptr_t)LG_VIDEO_SOURCE_PRIMARY);
if (!videoAttached)
{
DEBUG_ERROR("Failed to attach the video source");
return -1;
}
g_state.ds->startup();
const bool clipboardAvailable =
g_state.ds->cbInit && g_state.ds->cbInit();
lgClipboard_setLocalAvailable(clipboardAvailable);
if (g_params.captureOnStart)
core_setGrab(true);
int waitCount = 0;
LG_TransportSession session;
MsgBoxHandle msgs[10];
int msgsCount;
struct RecoveryPrompt recoveryPrompt = { 0 };
atomic_init(&recoveryPrompt.choice, RECOVERY_PENDING);
atomic_init(&recoveryPrompt.handle, 0);
atomic_init(&recoveryPrompt.message, 0);
atomic_init(&recoveryPrompt.messageClosed, false);
restart:
frameTimingReset();
msgsCount = 0;
memset(msgs, 0, sizeof(msgs));
uint64_t initialFallbackEnable = g_state.fallback ?
microtime() + 1000 * 1000 : 0;
while(app_getState() == APP_STATE_RUNNING)
{
fallbackHandleEvents();
if (initialFallbackEnable && microtime() > initialFallbackEnable)
{
if (fallbackRequestVideo())
initialFallbackEnable = 0;
}
struct TransportSessionProbe probe = {
.transport = g_state.transport.handle,
.ops = g_state.transport.ops,
.cancelled = transportSessionCancelled,
.cancelOpaque = NULL,
.done = false,
};
LGThread * probeThread;
if (!lgCreateThread("transportSession", transportSessionProbe, &probe,
&probeThread))
{
DEBUG_ERROR("Failed to create transport session probe thread");
return recoveryExit(&recoveryPrompt, -1);
}
while (app_getState() == APP_STATE_RUNNING &&
!atomic_load_explicit(&probe.done, memory_order_acquire))
{
g_state.ds->wait(100);
fallbackHandleEvents();
}
if (!lgJoinThread(probeThread, NULL))
{
DEBUG_ERROR("Failed to join transport session probe thread");
return recoveryExit(&recoveryPrompt, -1);
}
if (app_getState() != APP_STATE_RUNNING)
return recoveryExit(&recoveryPrompt, -1);
if (probe.status == LG_TRANSPORT_OK)
{
session = probe.session;
atomic_fetch_and_explicit(&g_state.transportLost,
~TRANSPORT_LOST_PRIMARY, memory_order_acq_rel);
initialFallbackEnable = 0;
break;
}
if (probe.status == LG_TRANSPORT_INVALID_VERSION)
{
if (waitCount++ == 0)
reportBadVersion(&probe.session.versionMismatch);
recoveryHandleMismatch(
&recoveryPrompt, &probe.session.versionMismatch);
g_state.ds->wait(1000);
continue;
}
if (probe.status == LG_TRANSPORT_UNAVAILABLE ||
probe.status == LG_TRANSPORT_DISCONNECTED)
{
if (waitCount++ == 0)
{
DEBUG_BREAK();
DEBUG_INFO("The transport source is not available");
DEBUG_INFO("Waiting for the source to start...");
}
if (waitCount == 30 && !g_params.disableWaitingMessage)
{
const size_t msgCapacity = sizeof(msgs) / sizeof(*msgs);
retainMessage(msgs, msgCapacity, &msgsCount,
app_msgBox("Transport Source Not Available",
"The selected transport source is not available.\n"
"Continuing to wait..."));
retainMessage(msgs, msgCapacity, &msgsCount,
showSpiceInputHelp());
}
g_state.ds->wait(1000);
continue;
}
DEBUG_ERROR("Transport connection failed with status %d", probe.status);
return recoveryExit(&recoveryPrompt, -1);
}
if (app_getState() != APP_STATE_RUNNING)
return recoveryExit(&recoveryPrompt, -1);
recoveryClose(&recoveryPrompt);
LG_RecoveryInfo recoveryInfo = { 0 };
if (recoveryGetInfo(&recoveryInfo) &&
(recoveryInfo.state == LG_RECOVERY_STATE_ACTIVE ||
recoveryInfo.state == LG_RECOVERY_STATE_SWITCHING ||
recoveryInfo.request == LG_RECOVERY_REQ_RECOVERY ||
recoveryInfo.ackRequest == LG_RECOVERY_REQ_RECOVERY) &&
g_state.transport.ops->requestRecovery)
{
const LG_TransportStatus status =
g_state.transport.ops->requestRecovery(g_state.transport.handle,
LG_RECOVERY_REQ_NORMAL, NULL);
if (status != LG_TRANSPORT_OK)
DEBUG_WARN("Failed to leave recovery mode: %d", status);
}
recoveryPrompt.serial = 0;
recoveryPrompt.failedSerial = 0;
recoveryPrompt.reportedState = LG_RECOVERY_STATE_UNKNOWN;
recoveryPrompt.shown = false;
recoveryPrompt.requested = false;
recoveryPrompt.owned = false;
recoveryPrompt.retryPrompt = false;
recoveryPrompt.retryDeclined = false;
atomic_store_explicit(
&recoveryPrompt.choice, RECOVERY_PENDING, memory_order_relaxed);
waitCount = 100;
for (int i = 0; i < msgsCount; ++i)
if (msgs[i])
app_msgBoxClose(msgs[i]);
DEBUG_INFO("Guest Information:");
DEBUG_INFO("Version : %s", session.version[0] ? session.version : "unknown");
if (session.cpuModel[0])
DEBUG_INFO("CPU Model: %s", session.cpuModel);
if (session.cpus)
DEBUG_INFO("CPU : %u sockets, %u cores, %u threads",
session.sockets, session.cores, session.cpus);
if (session.capture[0])
DEBUG_INFO("Using : %s", session.capture);
if (session.cpuModel[0] && session.cpus && session.cores)
{
char hostModel[1024] = {};
int hostProcs = 0;
int hostCores = 0;
int hostSockets = 0;
if (cpuInfo_get(hostModel, sizeof(hostModel), &hostProcs, &hostCores,
&hostSockets))
{
if (hostProcs > hostCores && session.cpus <= session.cores)
{
DEBUG_BREAK();
DEBUG_WARN("The host CPU has hardware threads but the guest is not aware of them");
DEBUG_WARN("This can degrade latency-sensitive tasks including Looking Glass");
if (strncmp(hostModel, "AMD ", 4) == 0)
DEBUG_WARN("For AMD CPUs, enable the `topoext` CPU feature for the virtual machine");
DEBUG_BREAK();
}
if (strcmp(session.cpuModel, hostModel) != 0)
{
DEBUG_BREAK();
DEBUG_WARN("The guest is unaware of the acceleration features of the host CPU");
DEBUG_WARN("Set the virtual machine CPU type to `host-passthrough`");
DEBUG_BREAK();
}
}
}
static const char * osNames[] = { "Linux", "BSD", "OSX", "Windows", "Other" };
const char * os = session.os < LG_TRANSPORT_OS_OTHER + 1 ?
osNames[session.os] : "Unknown";
DEBUG_INFO("OS : %s", os);
if (session.osName[0])
DEBUG_INFO("OS Name : %s", session.osName);
g_state.guestOS = session.os;
g_state.guestUUIDValid = session.uuidValid;
if (session.uuidValid)
{
memcpy(g_state.guestUUID, session.uuid, sizeof(g_state.guestUUID));
lgTransportFallback_setPrimaryUUID(
g_state.fallback, g_state.guestUUID);
}
else
lgTransportFallback_clearPrimaryUUID(g_state.fallback);
g_state.transportFeatures = session.features;
frameScheduler_start(session.features);
void * inputOpaque = NULL;
const LG_InputOps * inputOps = g_state.transport.ops->getInputOps ?
g_state.transport.ops->getInputOps(
g_state.transport.handle, &inputOpaque) : NULL;
lgInput_setTransport(inputOps, inputOpaque);
void * audioOpaque = NULL;
const LG_AudioOps * audioOps = g_state.transport.ops->getAudioOps ?
g_state.transport.ops->getAudioOps(
g_state.transport.handle, &audioOpaque) : NULL;
lgAudio_setTransport(audioOps, audioOpaque);
void * clipboardOpaque = NULL;
const LG_ClipboardOps * clipboardOps =
g_state.transport.ops->getClipboardOps ?
g_state.transport.ops->getClipboardOps(
g_state.transport.handle, &clipboardOpaque) : NULL;
lgClipboard_setTransport(clipboardOps, clipboardOpaque);
DEBUG_INFO("Starting session");
atomic_store_explicit(
&g_state.lgHostConnected, true, memory_order_release);
if (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME)
{
primaryVideoStatusStart();
if (!atomic_load_explicit(&g_state.stopVideo, memory_order_acquire) &&
!core_startVideoThreads())
{
primaryVideoStatusStop();
return recoveryExit(&recoveryPrompt, -1);
}
}
else
{
if (!lgSwSurface_setActive(g_state.videoOps->swSurface,
g_state.transport.handle, true,
primarySurfaceActivationCancelled, NULL))
{
DEBUG_ERROR("Failed to activate the primary software surface");
return recoveryExit(&recoveryPrompt, -1);
}
app_useVideoSource(LG_VIDEO_SOURCE_PRIMARY);
}
while(likely(app_getState() == APP_STATE_RUNNING))
{
fallbackHandleEvents();
if (unlikely(!g_state.transport.ops->sessionValid(
g_state.transport.handle)))
{
lgInput_dropTransport();
lgAudio_dropTransport();
lgClipboard_dropTransport();
primaryLost();
break;
}
lgMessage_process();
frameScheduler_update();
g_state.ds->wait(100);
}
frameScheduler_stop();
primaryVideoStatusStop();
if (app_getState() == APP_STATE_RESTART)
{
atomic_store_explicit(
&g_state.lgHostConnected, false, memory_order_release);
g_state.guestUUIDValid = false;
lgSignalEvent(e_startup);
lgSignalEvent(g_state.frameEvent);
if (g_state.videoOps->type == LG_VIDEO_TYPE_FRAME)
core_stopVideoThreads();
else
lgSwSurface_setActive(g_state.videoOps->swSurface,
g_state.transport.handle, false, NULL, NULL);
videoSourceInvalidate(LG_VIDEO_SOURCE_PRIMARY);
videoSourceClearCursor(LG_VIDEO_SOURCE_PRIMARY);
lgInput_dropTransport();
lgAudio_dropTransport();
lgClipboard_dropTransport();
g_state.transport.ops->disconnect(g_state.transport.handle);
if (app_transitionState(APP_STATE_RESTART, APP_STATE_RUNNING))
{
lgInit();
goto restart;
}
}
return recoveryExit(&recoveryPrompt, 0);
}
static void lg_shutdown(void)
{
app_setState(APP_STATE_SHUTDOWN);
frameScheduler_stop();
if (e_cursorRepaint)
lgSignalEvent(e_cursorRepaint);
if (t_render)
{
if (g_state.jitRender && g_state.ds->stopWaitFrame)
g_state.ds->stopWaitFrame();
lgSignalEvent(e_startup);
lgSignalEvent(g_state.frameEvent);
lgJoinThread(t_render, NULL);
}
// Stop external input callbacks before tearing down shared client state
evdev_stop();
if (g_state.ds && g_state.dsInitialized)
g_state.ds->shutdown();
fallbackStop();
if (t_cursorRepaint)
{
lgJoinThread(t_cursorRepaint, NULL);
t_cursorRepaint = NULL;
}
if (e_cursorRepaint)
{
lgFreeEvent(e_cursorRepaint);
e_cursorRepaint = NULL;
}
LG_LOCK_FREE(l_cursorRepaint.lock);
if (g_state.transport.ops)
{
lgInput_dropTransport();
if (g_state.transport.handle &&
g_state.transport.ops->sessionValid(g_state.transport.handle))
{
lgAudio_setTransport(NULL, NULL);
lgClipboard_setTransport(NULL, NULL);
}
else
{
lgAudio_dropTransport();
lgClipboard_dropTransport();
}
lgTransport_destroy(&g_state.transport);
}
lgInput_free();
lgAudio_free();
lgClipboard_setLocalAvailable(false);
if (e_startup)
{
lgFreeEvent(e_startup);
e_startup = NULL;
}
app_releaseAllKeybinds();
ll_free(g_state.bindings);
if (g_state.ds && g_state.dsInitialized)
{
g_state.dsInitialized = false;
g_state.ds->free();
}
/* Clipboard sources own FUSE dataset references and are released by the
* display server before the filesystem is unmounted. */
lgClipboard_free();
// Input callbacks have stopped; the evdev state and overlays can go
evdev_free();
if (g_state.overlays)
{
app_freeOverlays();
ll_free(g_state.overlays);
g_state.overlays = NULL;
}
// app_invalidateWindow runs during display server and overlay teardown
if (g_state.frameEvent)
{
lgFreeEvent(g_state.frameEvent);
g_state.frameEvent = NULL;
}
renderQueue_setSourceFns(NULL, NULL, NULL, NULL);
renderQueue_free();
LG_LOCK_FREE(g_state.videoPayloadLock);
LG_LOCK_FREE(g_state.videoSourceLock);
LG_LOCK_FREE(g_state.videoSplashLock);
frameScheduler_free();
// free metrics ringbuffers
ringbuffer_free(&g_state.renderTimings);
ringbuffer_free(&g_state.frameLatency);
LG_LOCK_FREE(l_frameTiming.lock);
free(g_state.fontName);
igDestroyContext(NULL);
free(g_state.imGuiIni);
}
int main(int argc, char * argv[])
{
// initialize for DEBUG_* macros
debug_init();
if (getuid() == 0)
{
DEBUG_ERROR("Do not run looking glass as root!");
return -1;
}
if (getuid() != geteuid())
{
DEBUG_ERROR("Do not run looking glass as setuid!");
return -1;
}
lgInitProcessTitle(argc, &argv);
core_setTitle(NULL);
DEBUG_INFO("Looking Glass (%s)", BUILD_VERSION);
DEBUG_INFO("Locking Method: " LG_LOCK_MODE);
cpuInfo_log();
if (!installCrashHandler("/proc/self/exe", argv[0]))
DEBUG_WARN("Failed to install the crash handler");
lgPathsInit("looking-glass");
config_init();
lgTransport_setup();
egl_dynProcsInit();
gl_dynProcsInit();
if (!lgMessage_init())
return -1;
g_state.bindings = ll_new();
g_state.overlays = ll_new();
app_registerOverlay(&LGOverlaySplash, NULL);
app_registerOverlay(&LGOverlayConfig, NULL);
app_registerOverlay(&LGOverlayAlert , NULL);
app_registerOverlay(&LGOverlayFPS , NULL);
app_registerOverlay(&LGOverlayGraphs, NULL);
app_registerOverlay(&LGOverlayHelp , NULL);
app_registerOverlay(&LGOverlayMsg , NULL);
app_registerOverlay(&LGOverlayStatus, NULL);
// early renderer setup for option registration
for(unsigned int i = 0; i < LG_RENDERER_COUNT; ++i)
LG_Renderers[i]->setup();
for(unsigned int i = 0; i < LG_DISPLAYSERVER_COUNT; ++i)
LG_DisplayServers[i]->setup();
for(unsigned int i = 0; LG_AudioDevs[i]; ++i)
if (LG_AudioDevs[i]->earlyInit)
LG_AudioDevs[i]->earlyInit();
evdev_earlyInit();
if (!config_load(argc, argv))
return -1;
const int ret = lg_run();
lg_shutdown();
lgMessage_deinit();
config_free();
util_freeUIFonts();
cleanupCrashHandler();
return ret;
}