mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-04 06:12:04 +00:00
1944 lines
54 KiB
C
1944 lines
54 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 "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"
|
|
|
|
#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
|
|
|
|
// forwards
|
|
static int renderThread(void * unused);
|
|
|
|
static LGEvent *e_startup = NULL;
|
|
static LGEvent *e_spice = NULL;
|
|
static LGThread *t_spice = NULL;
|
|
static LGThread *t_render = NULL;
|
|
|
|
_Atomic(enum RunState) p_appState = APP_STATE_RUNNING;
|
|
|
|
struct AppState g_state =
|
|
{
|
|
.cbRemoteType = SPICE_DATA_NONE,
|
|
.cbWriteType = SPICE_DATA_NONE,
|
|
};
|
|
struct CursorState g_cursor;
|
|
|
|
// this structure is initialized in config.c
|
|
struct AppParams g_params = { 0 };
|
|
|
|
#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.guest.valid = false;
|
|
|
|
// if spice is not in use, hide the local cursor
|
|
if ((!g_params.useSpiceInput && 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;
|
|
}
|
|
|
|
struct RenderTiming
|
|
{
|
|
uint64_t renderStart;
|
|
uint64_t captureTime;
|
|
uint64_t postProcessTime;
|
|
uint64_t copyTime;
|
|
uint64_t importTime;
|
|
};
|
|
|
|
static struct RenderTiming frameTimingLoad(void)
|
|
{
|
|
struct RenderTiming timing = {};
|
|
|
|
for (;;)
|
|
{
|
|
const unsigned sequence = atomic_load_explicit(
|
|
&g_state.frameTimingSequence, memory_order_seq_cst);
|
|
if (sequence & 1)
|
|
continue;
|
|
|
|
timing.captureTime = atomic_load_explicit(
|
|
&g_state.producerCaptureTime, memory_order_seq_cst);
|
|
timing.postProcessTime = atomic_load_explicit(
|
|
&g_state.producerPostProcessTime, memory_order_seq_cst);
|
|
timing.copyTime = atomic_load_explicit(
|
|
&g_state.producerCopyTime, memory_order_seq_cst);
|
|
timing.importTime = atomic_load_explicit(
|
|
&g_state.clientImportTime, memory_order_seq_cst);
|
|
|
|
if (sequence == atomic_load_explicit(
|
|
&g_state.frameTimingSequence, memory_order_seq_cst))
|
|
return timing;
|
|
}
|
|
}
|
|
|
|
static void frameTimingStore(const LG_TransportFrameTiming * timing,
|
|
uint64_t importTime)
|
|
{
|
|
atomic_fetch_add_explicit(
|
|
&g_state.frameTimingSequence, 1, memory_order_seq_cst);
|
|
atomic_store_explicit(&g_state.producerCaptureTime,
|
|
timing->captureTime, memory_order_seq_cst);
|
|
atomic_store_explicit(&g_state.producerPostProcessTime,
|
|
timing->postProcessTime, memory_order_seq_cst);
|
|
atomic_store_explicit(&g_state.producerCopyTime,
|
|
timing->copyTime, memory_order_seq_cst);
|
|
atomic_store_explicit(&g_state.clientImportTime,
|
|
importTime, memory_order_seq_cst);
|
|
atomic_fetch_add_explicit(
|
|
&g_state.frameTimingSequence, 1, memory_order_seq_cst);
|
|
}
|
|
|
|
static void preSwapCallback(void * udata)
|
|
{
|
|
const struct RenderTiming * timing = (const struct RenderTiming *)udata;
|
|
const uint64_t timestamp = nanotime();
|
|
const uint64_t renderTime = timestamp - timing->renderStart;
|
|
if (timing->captureTime || timing->postProcessTime || timing->copyTime ||
|
|
timing->importTime)
|
|
{
|
|
const OverlayFrameTiming frameTiming = {
|
|
.timestamp = timestamp,
|
|
.capture = timing->captureTime * 1e-6f,
|
|
.postProcess = timing->postProcessTime * 1e-6f,
|
|
.copy = timing->copyTime * 1e-6f,
|
|
.import = timing->importTime * 1e-6f,
|
|
.render = renderTime * 1e-6f,
|
|
};
|
|
ringbuffer_push(g_state.frameLatency, &frameTiming);
|
|
}
|
|
|
|
#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 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))
|
|
{
|
|
if (g_state.jitRender)
|
|
{
|
|
const bool forceRender = g_state.ds->waitFrame();
|
|
app_handleRenderEvent(microtime());
|
|
|
|
const uint64_t pending =
|
|
atomic_load_explicit(&g_state.pendingCount, memory_order_acquire);
|
|
|
|
if (!lgResetEvent(g_state.frameEvent)
|
|
&& !forceRender
|
|
&& !pending
|
|
&& !app_overlayNeedsRender())
|
|
{
|
|
if (g_state.ds->skipFrame)
|
|
g_state.ds->skipFrame();
|
|
continue;
|
|
}
|
|
|
|
if (pending > 0)
|
|
atomic_fetch_sub(&g_state.pendingCount, 1);
|
|
}
|
|
else if (g_params.fpsMin != 0)
|
|
{
|
|
app_handleRenderEvent(microtime());
|
|
|
|
float ups = atomic_load_explicit(&g_state.ups, memory_order_relaxed);
|
|
|
|
if (unlikely(
|
|
!lgWaitEventAbs(g_state.frameEvent, &time) ||
|
|
ups > g_params.fpsMin))
|
|
{
|
|
/* only update the time if we woke up early */
|
|
clock_gettime(CLOCK_MONOTONIC, &time);
|
|
tsAdd(&time, app_isOverlayMode() ?
|
|
g_state.overlayFrameTime : g_state.frameTime);
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (unlikely(fontDirty || g_state.fontScale != g_state.windowScale))
|
|
{
|
|
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))
|
|
{
|
|
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);
|
|
}
|
|
|
|
static uint64_t lastFrameCount = 0;
|
|
const uint64_t frameCount =
|
|
atomic_load_explicit(&g_state.frameCount, memory_order_relaxed);
|
|
const bool newFrame = frameCount != lastFrameCount;
|
|
lastFrameCount = frameCount;
|
|
|
|
const bool invalidate = atomic_exchange(&g_state.invalidateWindow, false);
|
|
|
|
struct RenderTiming renderTiming =
|
|
newFrame ? frameTimingLoad() : (struct RenderTiming) {};
|
|
renderTiming.renderStart = nanotime();
|
|
LG_LOCK(g_state.lgrLock);
|
|
|
|
renderQueue_process();
|
|
|
|
if (unlikely(!RENDERER(render, g_params.winRotate, newFrame, invalidate,
|
|
preSwapCallback, (void *)&renderTiming)))
|
|
{
|
|
LG_UNLOCK(g_state.lgrLock);
|
|
break;
|
|
}
|
|
LG_UNLOCK(g_state.lgrLock);
|
|
|
|
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 (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);
|
|
|
|
if (g_state.overlays)
|
|
{
|
|
app_freeOverlays();
|
|
ll_free(g_state.overlays);
|
|
g_state.overlays = NULL;
|
|
}
|
|
|
|
lgTimerDestroy(tickTimer);
|
|
lgTimerDestroy(fpsTimer);
|
|
|
|
core_stopCursorThread();
|
|
core_stopFrameThread();
|
|
|
|
if (g_state.transportOps && g_state.transportOps->detachRenderer)
|
|
g_state.transportOps->detachRenderer(g_state.transport);
|
|
|
|
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;
|
|
|
|
lgWaitEvent(e_startup, TIMEOUT_INFINITE);
|
|
|
|
// subscribe to the pointer queue
|
|
while(app_getState() == APP_STATE_RUNNING && !g_state.stopVideo)
|
|
{
|
|
LG_TransportPointer pointer;
|
|
const LG_TransportStatus status = g_state.transportOps->nextPointer(
|
|
g_state.transport, &pointer);
|
|
if (status != LG_TRANSPORT_OK)
|
|
{
|
|
if (status == LG_TRANSPORT_TIMEOUT || status == LG_TRANSPORT_UNAVAILABLE)
|
|
{
|
|
if (g_cursor.redraw && g_cursor.guest.valid)
|
|
{
|
|
g_cursor.redraw = false;
|
|
RENDERER(onMouseEvent,
|
|
g_cursor.guest.visible &&
|
|
(g_cursor.draw || !g_params.useSpiceInput),
|
|
g_cursor.guest.x, g_cursor.guest.y,
|
|
g_cursor.guest.hx, g_cursor.guest.hy);
|
|
if (!g_state.stopVideo)
|
|
lgSignalEvent(g_state.frameEvent);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
app_setState(status == LG_TRANSPORT_DISCONNECTED ?
|
|
APP_STATE_RESTART : APP_STATE_SHUTDOWN);
|
|
if (status != LG_TRANSPORT_DISCONNECTED)
|
|
DEBUG_ERROR("Pointer transport failed with status %d", status);
|
|
break;
|
|
}
|
|
|
|
if (pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID)
|
|
g_cursor.guest.visible =
|
|
pointer.flags & LG_TRANSPORT_POINTER_VISIBLE;
|
|
|
|
if (pointer.flags & LG_TRANSPORT_POINTER_SHAPE)
|
|
{
|
|
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:
|
|
DEBUG_ERROR("Invalid cursor type");
|
|
g_state.transportOps->releasePointer(g_state.transport, &pointer);
|
|
continue;
|
|
}
|
|
|
|
g_cursor.guest.hx = pointer.hx;
|
|
g_cursor.guest.hy = pointer.hy;
|
|
if (!RENDERER(onMouseShape, cursorType, pointer.width, pointer.height,
|
|
pointer.pitch, pointer.shape))
|
|
{
|
|
DEBUG_ERROR("Failed to update mouse shape");
|
|
g_state.transportOps->releasePointer(g_state.transport, &pointer);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ((pointer.flags & LG_TRANSPORT_POINTER_COLOR_TRANSFORM) &&
|
|
g_state.lgr->ops.onMouseColorTransform)
|
|
g_state.lgr->ops.onMouseColorTransform(g_state.lgr,
|
|
pointer.colorTransform);
|
|
|
|
if ((pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID) &&
|
|
pointer.sdrWhiteLevel && g_state.lgr->ops.onMouseWhiteLevel)
|
|
g_state.lgr->ops.onMouseWhiteLevel(g_state.lgr,
|
|
pointer.sdrWhiteLevel);
|
|
|
|
if (pointer.flags & LG_TRANSPORT_POINTER_POSITION)
|
|
{
|
|
const bool wasValid = g_cursor.guest.valid;
|
|
g_cursor.guest.x = pointer.x;
|
|
g_cursor.guest.y = pointer.y;
|
|
g_cursor.guest.valid = true;
|
|
if (!wasValid && core_inputEnabled())
|
|
{
|
|
core_alignToGuest();
|
|
app_resyncMouseBasic();
|
|
}
|
|
core_handleGuestMouseUpdate();
|
|
}
|
|
|
|
app_updateMouseState();
|
|
g_cursor.redraw = false;
|
|
RENDERER(onMouseEvent,
|
|
g_cursor.guest.visible && (g_cursor.draw || !g_params.useSpiceInput),
|
|
g_cursor.guest.x, g_cursor.guest.y,
|
|
g_cursor.guest.hx, g_cursor.guest.hy);
|
|
|
|
if ((g_params.mouseRedraw ||
|
|
(pointer.flags & LG_TRANSPORT_POINTER_COLOR_TRANSFORM)) &&
|
|
g_cursor.guest.visible && !g_state.stopVideo)
|
|
lgSignalEvent(g_state.frameEvent);
|
|
|
|
g_state.transportOps->releasePointer(g_state.transport, &pointer);
|
|
}
|
|
|
|
if (g_state.transportOps->stopPointer)
|
|
g_state.transportOps->stopPointer(g_state.transport);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int main_frameThread(void * unused)
|
|
{
|
|
uint64_t frameSerial = 0;
|
|
uint32_t formatVersion = 0;
|
|
LG_RendererFormat rendererFormat;
|
|
|
|
if (g_state.useDMA)
|
|
DEBUG_INFO("Using DMA buffer support");
|
|
|
|
lgWaitEvent(e_startup, TIMEOUT_INFINITE);
|
|
if (app_getState() != APP_STATE_RUNNING)
|
|
{
|
|
if (g_state.transportOps->stopFrame)
|
|
g_state.transportOps->stopFrame(g_state.transport);
|
|
return 0;
|
|
}
|
|
|
|
while(app_getState() == APP_STATE_RUNNING && !g_state.stopVideo)
|
|
{
|
|
LG_TransportFrame frame;
|
|
const LG_TransportStatus status = g_state.transportOps->nextFrame(
|
|
g_state.transport, g_state.useDMA, &frame);
|
|
if (status != LG_TRANSPORT_OK)
|
|
{
|
|
if (status == LG_TRANSPORT_TIMEOUT || status == LG_TRANSPORT_UNAVAILABLE)
|
|
continue;
|
|
if (status == LG_TRANSPORT_DISCONNECTED)
|
|
app_setState(APP_STATE_RESTART);
|
|
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;
|
|
}
|
|
|
|
if (frame.serial == frameSerial && g_state.formatValid)
|
|
{
|
|
g_state.transportOps->releaseFrame(g_state.transport, &frame);
|
|
continue;
|
|
}
|
|
frameSerial = frame.serial;
|
|
|
|
const LG_TransportFrameFormat * format = frame.format;
|
|
if (!format)
|
|
{
|
|
DEBUG_ERROR("Transport returned a frame without format metadata");
|
|
g_state.transportOps->releaseFrame(g_state.transport, &frame);
|
|
app_setState(APP_STATE_SHUTDOWN);
|
|
break;
|
|
}
|
|
if (!g_state.formatValid || format->version != formatVersion)
|
|
{
|
|
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;
|
|
}
|
|
g_state.rotate = rendererFormat.rotate;
|
|
|
|
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)
|
|
{
|
|
DEBUG_ERROR("Unsupported frame type");
|
|
g_state.transportOps->releaseFrame(g_state.transport, &frame);
|
|
app_setState(APP_STATE_SHUTDOWN);
|
|
break;
|
|
}
|
|
|
|
g_state.formatValid = true;
|
|
formatVersion = format->version;
|
|
#ifdef ENABLE_TESTS
|
|
atomic_store_explicit(&l_testFrameType, format->type,
|
|
memory_order_release);
|
|
#endif
|
|
DEBUG_INFO("Format: %s %ux%u (%ux%u) stride:%u pitch:%u rotation:%d hdr:%d pq:%d sdrWhite:%u nits",
|
|
FrameTypeStr[format->type], format->frameWidth, format->frameHeight,
|
|
format->dataWidth, format->dataHeight, format->stride, format->pitch,
|
|
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);
|
|
DEBUG_ERROR("Renderer failed to configure format");
|
|
g_state.transportOps->releaseFrame(g_state.transport, &frame);
|
|
app_setState(APP_STATE_SHUTDOWN);
|
|
break;
|
|
}
|
|
const bool rendererSupportsNativeHDR = !rendererFormat.hdr ||
|
|
!g_state.lgr->ops.supports || RENDERER(supports,
|
|
rendererFormat.hdrPQ ? LG_SUPPORTS_HDR_PQ : LG_SUPPORTS_HDR_SCRGB);
|
|
// Publish the matching surface format before allowing the render thread
|
|
// to consume the renderer format. Otherwise it can present one frame in
|
|
// the new encoding while the display server still has the old image
|
|
// description attached.
|
|
renderQueue_surfaceFormat(rendererFormat, rendererSupportsNativeHDR);
|
|
LG_UNLOCK(g_state.lgrLock);
|
|
|
|
g_state.srcSize.x = rendererFormat.screenWidth;
|
|
g_state.srcSize.y = rendererFormat.screenHeight;
|
|
g_state.haveSrcSize = true;
|
|
if (g_params.autoResize)
|
|
g_state.ds->setWindowSize(rendererFormat.frameWidth,
|
|
rendererFormat.frameHeight);
|
|
core_updatePositionInfo();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
g_state.frameImportTime = 0;
|
|
if (!RENDERER(onFrame, frame.framebuffer, frame.dmaFD,
|
|
frame.damageRects, damageCount))
|
|
{
|
|
g_state.transportOps->releaseFrame(g_state.transport, &frame);
|
|
DEBUG_ERROR("Renderer onFrame returned failure");
|
|
app_setState(APP_STATE_SHUTDOWN);
|
|
break;
|
|
}
|
|
|
|
LG_TransportFrameTiming timing = {};
|
|
if (g_state.transportOps->getFrameTiming)
|
|
g_state.transportOps->getFrameTiming(
|
|
g_state.transport, &frame, &timing);
|
|
|
|
frameTimingStore(&timing, g_state.frameImportTime);
|
|
|
|
overlaySplash_show(false);
|
|
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;
|
|
}
|
|
|
|
atomic_fetch_add_explicit(&g_state.frameCount, 1, memory_order_relaxed);
|
|
#ifdef ENABLE_TESTS
|
|
atomic_store_explicit(&l_testFrameSerial, frame.serial,
|
|
memory_order_release);
|
|
#endif
|
|
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);
|
|
|
|
g_state.transportOps->releaseFrame(g_state.transport, &frame);
|
|
app_useSpiceDisplay(false);
|
|
}
|
|
|
|
if (g_state.transportOps->stopFrame)
|
|
g_state.transportOps->stopFrame(g_state.transport);
|
|
|
|
RENDERER(onRestart);
|
|
|
|
if (app_getState() != APP_STATE_SHUTDOWN)
|
|
if (!app_useSpiceDisplay(true))
|
|
overlaySplash_show(true);
|
|
return 0;
|
|
}
|
|
|
|
static void checkUUID(void)
|
|
{
|
|
if (!atomic_load_explicit(&g_state.spiceReady, memory_order_acquire) ||
|
|
!g_state.guestUUIDValid)
|
|
return;
|
|
|
|
if (memcmp(g_state.spiceUUID, g_state.guestUUID,
|
|
sizeof(g_state.spiceUUID)) == 0)
|
|
return;
|
|
|
|
app_msgBox(
|
|
"SPICE Configuration Error",
|
|
"You have connected SPICE to the wrong guest.\n"
|
|
"Input will not function until this is corrected.");
|
|
|
|
g_params.useSpiceInput = false;
|
|
atomic_store_explicit(&g_state.spiceClose, true, memory_order_release);
|
|
purespice_disconnect();
|
|
}
|
|
|
|
void spiceReady(void)
|
|
{
|
|
atomic_store_explicit(&g_state.spiceReady, true, memory_order_release);
|
|
if (atomic_load_explicit(&g_state.spiceDisplayRequested,
|
|
memory_order_acquire))
|
|
app_useSpiceDisplay(true);
|
|
|
|
// set the intial mouse mode
|
|
purespice_mouseMode(true);
|
|
|
|
PSServerInfo info;
|
|
if (purespice_getServerInfo(&info))
|
|
{
|
|
core_setTitle(info.name);
|
|
|
|
bool uuidValid = false;
|
|
for(int i = 0; i < sizeof(info.uuid); ++i)
|
|
if (info.uuid[i])
|
|
{
|
|
uuidValid = true;
|
|
break;
|
|
}
|
|
|
|
if (uuidValid)
|
|
{
|
|
memcpy(g_state.spiceUUID, info.uuid, sizeof(g_state.spiceUUID));
|
|
checkUUID();
|
|
}
|
|
purespice_freeServerInfo(&info);
|
|
}
|
|
else
|
|
DEBUG_WARN("Failed to obtain SPICE server information");
|
|
|
|
if (g_params.useSpiceInput)
|
|
keybind_spiceRegister();
|
|
|
|
lgSignalEvent(e_spice);
|
|
}
|
|
|
|
static void spice_surfaceCreate(unsigned int surfaceId, PSSurfaceFormat format,
|
|
unsigned int width, unsigned int height)
|
|
{
|
|
if (surfaceId != 0)
|
|
{
|
|
DEBUG_INFO("Ignoring secondary SPICE surface: id: %u, size: %ux%u",
|
|
surfaceId, width, height);
|
|
return;
|
|
}
|
|
|
|
switch(format)
|
|
{
|
|
case PS_SURFACE_FMT_32_xRGB:
|
|
case PS_SURFACE_FMT_32_ARGB:
|
|
break;
|
|
|
|
default:
|
|
DEBUG_ERROR("Unsupported primary SPICE surface format: %d", format);
|
|
g_state.spicePrimarySurfaceValid = false;
|
|
app_useSpiceDisplay(false);
|
|
return;
|
|
}
|
|
|
|
DEBUG_INFO("Create primary SPICE surface: id: %u, size: %ux%u",
|
|
surfaceId, width, height);
|
|
|
|
g_state.spicePrimarySurfaceValid = true;
|
|
|
|
g_state.srcSize.x = width;
|
|
g_state.srcSize.y = height;
|
|
g_state.haveSrcSize = true;
|
|
core_updatePositionInfo();
|
|
|
|
renderQueue_spiceConfigure(width, height);
|
|
renderQueue_spiceDrawFill(0, 0, width, height, 0x0);
|
|
}
|
|
|
|
static void spice_surfaceDestroy(unsigned int surfaceId)
|
|
{
|
|
if (!g_state.spicePrimarySurfaceValid || surfaceId != 0)
|
|
{
|
|
DEBUG_INFO("Ignoring destruction of inactive or secondary SPICE surface %u",
|
|
surfaceId);
|
|
return;
|
|
}
|
|
|
|
DEBUG_INFO("Destroy primary SPICE surface %u", surfaceId);
|
|
g_state.spicePrimarySurfaceValid = false;
|
|
app_useSpiceDisplay(false);
|
|
}
|
|
|
|
static void spice_drawFill(unsigned int surfaceId, int x, int y, int width,
|
|
int height, uint32_t color)
|
|
{
|
|
if (!g_state.spicePrimarySurfaceValid || surfaceId != 0)
|
|
return;
|
|
|
|
renderQueue_spiceDrawFill(x, y, width, height, color);
|
|
}
|
|
|
|
static void spice_drawBitmap(unsigned int surfaceId, PSBitmapFormat format,
|
|
bool topDown, int x, int y, int width, int height, int stride, void * data)
|
|
{
|
|
if (!g_state.spicePrimarySurfaceValid || surfaceId != 0)
|
|
return;
|
|
|
|
switch(format)
|
|
{
|
|
case PS_BITMAP_FMT_32BIT:
|
|
case PS_BITMAP_FMT_RGBA:
|
|
break;
|
|
|
|
default:
|
|
DEBUG_ERROR("Unsupported SPICE bitmap format: %d", format);
|
|
return;
|
|
}
|
|
|
|
renderQueue_spiceDrawBitmap(x, y, width, height, stride, data, topDown);
|
|
}
|
|
|
|
static void spice_setCursorRGBAImage(int width, int height, int hx, int hy,
|
|
const void * data)
|
|
{
|
|
g_state.spiceHotX = hx;
|
|
g_state.spiceHotY = hy;
|
|
|
|
const uint8_t * rgba = data;
|
|
uint8_t * bgra = malloc(width * height * 4);
|
|
for (int i = 0; i < width * height; ++i)
|
|
{
|
|
bgra[i * 4 + 0] = rgba[i * 4 + 2];
|
|
bgra[i * 4 + 1] = rgba[i * 4 + 1];
|
|
bgra[i * 4 + 2] = rgba[i * 4 + 0];
|
|
bgra[i * 4 + 3] = rgba[i * 4 + 3];
|
|
}
|
|
renderQueue_cursorImage(
|
|
LG_CURSOR_COLOR, width, height, width * 4, bgra);
|
|
}
|
|
|
|
static void spice_setCursorMonoImage(int width, int height, int hx, int hy,
|
|
const void * xorMask, const void * andMask)
|
|
{
|
|
g_state.spiceHotX = hx;
|
|
g_state.spiceHotY = hy;
|
|
|
|
int stride = (width + 7) / 8;
|
|
uint8_t * buffer = malloc(stride * height * 2);
|
|
memcpy(buffer, andMask, stride * height);
|
|
memcpy(buffer + stride * height, xorMask, stride * height);
|
|
renderQueue_cursorImage(
|
|
LG_CURSOR_MONOCHROME, width, height * 2, stride, buffer);
|
|
}
|
|
|
|
static void spice_setCursorColorImage(int width, int height, int hx, int hy,
|
|
const void * data, const void * maskData)
|
|
{
|
|
g_state.spiceHotX = hx;
|
|
g_state.spiceHotY = hy;
|
|
|
|
const uint8_t * rgba = data;
|
|
const uint8_t * andMask = maskData;
|
|
const int maskStride = (width + 7) / 8;
|
|
uint8_t * bgra = malloc(width * height * 4);
|
|
for (int y = 0; y < height; ++y)
|
|
{
|
|
for (int x = 0; x < width; ++x)
|
|
{
|
|
const int i = y * width + x;
|
|
bgra[i * 4 + 0] = rgba[i * 4 + 2];
|
|
bgra[i * 4 + 1] = rgba[i * 4 + 1];
|
|
bgra[i * 4 + 2] = rgba[i * 4 + 0];
|
|
bgra[i * 4 + 3] =
|
|
andMask[y * maskStride + x / 8] & (0x80U >> (x % 8)) ? 255 : 0;
|
|
}
|
|
}
|
|
|
|
renderQueue_cursorImage(
|
|
LG_CURSOR_MASKED_COLOR, width, height, width * 4, bgra);
|
|
}
|
|
|
|
static void spice_setCursorState(bool visible, int x, int y)
|
|
{
|
|
renderQueue_cursorState(visible, x, y, g_state.spiceHotX, g_state.spiceHotY);
|
|
}
|
|
|
|
int spiceThread(void * arg)
|
|
{
|
|
if (g_params.useSpiceAudio)
|
|
audio_init();
|
|
|
|
const struct PSConfig config =
|
|
{
|
|
.host = g_params.spiceHost,
|
|
.port = g_params.spicePort,
|
|
.password = "",
|
|
.ready = spiceReady,
|
|
.inputs =
|
|
{
|
|
.enable = g_params.useSpiceInput,
|
|
.autoConnect = true
|
|
},
|
|
.clipboard =
|
|
{
|
|
.enable = g_params.useSpiceClipboard,
|
|
.notice = cb_spiceNotice,
|
|
.data = cb_spiceData,
|
|
.release = cb_spiceRelease,
|
|
.request = cb_spiceRequest
|
|
},
|
|
.display =
|
|
{
|
|
.enable = true,
|
|
.autoConnect = false,
|
|
.surfaceCreate = spice_surfaceCreate,
|
|
.surfaceDestroy = spice_surfaceDestroy,
|
|
.drawFill = spice_drawFill,
|
|
.drawBitmap = spice_drawBitmap
|
|
},
|
|
.cursor =
|
|
{
|
|
.enable = true,
|
|
.autoConnect = false,
|
|
.setRGBAImage = spice_setCursorRGBAImage,
|
|
.setMonoImage = spice_setCursorMonoImage,
|
|
.setColorImage = spice_setCursorColorImage,
|
|
.setState = spice_setCursorState,
|
|
},
|
|
#if ENABLE_AUDIO
|
|
.playback =
|
|
{
|
|
.enable = audio_supportsPlayback(),
|
|
.autoConnect = true,
|
|
.start = audio_playbackStart,
|
|
.volume = audio_playbackVolume,
|
|
.mute = audio_playbackMute,
|
|
.stop = audio_playbackStop,
|
|
.data = audio_playbackData
|
|
},
|
|
.record =
|
|
{
|
|
.enable = audio_supportsRecord(),
|
|
.autoConnect = true,
|
|
.start = audio_recordStart,
|
|
.volume = audio_recordVolume,
|
|
.mute = audio_recordMute,
|
|
.stop = audio_recordStop
|
|
}
|
|
#endif
|
|
};
|
|
|
|
if (!purespice_connect(&config))
|
|
{
|
|
DEBUG_ERROR("Failed to connect to spice server");
|
|
lgSignalEvent(e_spice);
|
|
goto end;
|
|
}
|
|
|
|
// process all spice messages
|
|
while(app_getState() != APP_STATE_SHUTDOWN)
|
|
{
|
|
PSStatus status;
|
|
if ((status = purespice_process(100)) != PS_STATUS_RUN)
|
|
{
|
|
if (status != PS_STATUS_SHUTDOWN)
|
|
DEBUG_ERROR("failed to process spice messages");
|
|
goto end;
|
|
}
|
|
}
|
|
|
|
// send key up events for any pressed keys
|
|
if (g_params.useSpiceInput)
|
|
{
|
|
for(int scancode = 0; scancode < KEY_MAX; ++scancode)
|
|
if (atomic_load_explicit(
|
|
&g_state.keyDown[scancode], memory_order_relaxed))
|
|
{
|
|
const uint32_t ps2 = linux_to_ps2[scancode];
|
|
if (ps2 && purespice_keyUp(ps2))
|
|
atomic_store_explicit(
|
|
&g_state.keyDown[scancode], false, memory_order_relaxed);
|
|
else
|
|
DEBUG_ERROR("Failed to release key %d during SPICE shutdown",
|
|
scancode);
|
|
}
|
|
}
|
|
|
|
purespice_disconnect();
|
|
|
|
end:
|
|
|
|
audio_free();
|
|
|
|
// if the connection was disconnected intentionally we don't want to shutdown
|
|
// so that the user can see the message box and take action
|
|
if (!atomic_load_explicit(&g_state.spiceClose, memory_order_acquire))
|
|
app_setState(APP_STATE_SHUTDOWN);
|
|
|
|
lgSignalEvent(e_spice);
|
|
return 0;
|
|
}
|
|
|
|
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(void)
|
|
{
|
|
DEBUG_BREAK();
|
|
DEBUG_ERROR("The host application is not compatible with this client");
|
|
DEBUG_ERROR("This is not a Looking Glass error, do not report this");
|
|
DEBUG_ERROR("Please install the matching host application for this client");
|
|
}
|
|
|
|
static MsgBoxHandle showSpiceInputHelp(void)
|
|
{
|
|
static bool done = false;
|
|
if (!g_params.useSpiceInput || 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_TransportSession session;
|
|
LG_TransportStatus status;
|
|
atomic_bool done;
|
|
};
|
|
|
|
static int transportSessionProbe(void * opaque)
|
|
{
|
|
struct TransportSessionProbe * probe = opaque;
|
|
probe->status = probe->ops->connect(probe->transport, &probe->session);
|
|
atomic_store_explicit(&probe->done, true, memory_order_release);
|
|
return 0;
|
|
}
|
|
|
|
static int lg_run(void)
|
|
{
|
|
#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,
|
|
&g_state.transportOps))
|
|
{
|
|
DEBUG_ERROR("Failed to create transport: %s", g_params.transport);
|
|
return -1;
|
|
}
|
|
DEBUG_INFO("Using Transport: %s", g_state.transportOps->name);
|
|
|
|
// setup the spice startup condition
|
|
if (!(e_spice = lgCreateEvent(false, 0)))
|
|
{
|
|
DEBUG_ERROR("failed to create the spice startup event");
|
|
return -1;
|
|
}
|
|
|
|
// 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
|
|
renderQueue_init();
|
|
|
|
const PSInit psInit =
|
|
{
|
|
.log =
|
|
{
|
|
.info = debug_info,
|
|
.warn = debug_warn,
|
|
.error = debug_error,
|
|
}
|
|
};
|
|
purespice_init(&psInit);
|
|
|
|
g_state.micDefaultState = g_params.micDefaultState;
|
|
|
|
if (g_params.useSpice)
|
|
{
|
|
if (!lgCreateThread("spiceThread", spiceThread, NULL, &t_spice))
|
|
{
|
|
DEBUG_ERROR("spice create thread failed");
|
|
return -1;
|
|
}
|
|
|
|
lgWaitEvent(e_spice, TIMEOUT_INFINITE);
|
|
if (!atomic_load_explicit(&g_state.spiceReady, memory_order_acquire))
|
|
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.transportOps->supportsDMA(g_state.transport);
|
|
|
|
// 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);
|
|
|
|
keybind_commonRegister();
|
|
|
|
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;
|
|
if (g_state.transportOps->attachRenderer &&
|
|
!g_state.transportOps->attachRenderer(g_state.transport, interopPtr))
|
|
{
|
|
DEBUG_ERROR("Failed to attach the renderer to the transport");
|
|
return -1;
|
|
}
|
|
|
|
g_state.ds->startup();
|
|
g_state.cbAvailable = g_state.ds->cbInit && g_state.ds->cbInit();
|
|
if (g_state.cbAvailable)
|
|
g_state.cbRequestList = ll_new();
|
|
|
|
if (g_params.captureOnStart)
|
|
core_setGrab(true);
|
|
|
|
int waitCount = 0;
|
|
LG_TransportSession session;
|
|
MsgBoxHandle msgs[10];
|
|
int msgsCount;
|
|
|
|
restart:
|
|
msgsCount = 0;
|
|
memset(msgs, 0, sizeof(msgs));
|
|
|
|
uint64_t initialSpiceEnable = microtime() + 1000 * 1000;
|
|
|
|
while(app_getState() == APP_STATE_RUNNING)
|
|
{
|
|
if (initialSpiceEnable && microtime() > initialSpiceEnable)
|
|
{
|
|
app_useSpiceDisplay(true);
|
|
initialSpiceEnable = 0;
|
|
}
|
|
|
|
struct TransportSessionProbe probe = {
|
|
.transport = g_state.transport,
|
|
.ops = g_state.transportOps,
|
|
.done = false,
|
|
};
|
|
LGThread * probeThread;
|
|
if (!lgCreateThread("transportSession", transportSessionProbe, &probe,
|
|
&probeThread))
|
|
{
|
|
DEBUG_ERROR("Failed to create transport session probe thread");
|
|
return -1;
|
|
}
|
|
|
|
while (app_getState() == APP_STATE_RUNNING &&
|
|
!atomic_load_explicit(&probe.done, memory_order_acquire))
|
|
g_state.ds->wait(100);
|
|
|
|
if (!lgJoinThread(probeThread, NULL))
|
|
{
|
|
DEBUG_ERROR("Failed to join transport session probe thread");
|
|
return -1;
|
|
}
|
|
|
|
if (app_getState() != APP_STATE_RUNNING)
|
|
return -1;
|
|
|
|
if (probe.status == LG_TRANSPORT_OK)
|
|
{
|
|
session = probe.session;
|
|
initialSpiceEnable = 0;
|
|
break;
|
|
}
|
|
|
|
if (probe.status == LG_TRANSPORT_INVALID_VERSION)
|
|
{
|
|
if (waitCount++ == 0)
|
|
{
|
|
reportBadVersion();
|
|
msgs[msgsCount++] = app_msgBox(
|
|
"Incompatible Transport Version",
|
|
"The selected transport source is not compatible with this client.\n"
|
|
"Please install matching versions.");
|
|
DEBUG_INFO("Remote transport version: %u", probe.session.remoteVersion);
|
|
}
|
|
|
|
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)
|
|
{
|
|
msgs[msgsCount++] = app_msgBox(
|
|
"Transport Source Not Available",
|
|
"The selected transport source is not available.\n"
|
|
"Continuing to wait...");
|
|
msgs[msgsCount++] = showSpiceInputHelp();
|
|
}
|
|
g_state.ds->wait(1000);
|
|
continue;
|
|
}
|
|
|
|
DEBUG_ERROR("Transport connection failed with status %d", probe.status);
|
|
return -1;
|
|
}
|
|
|
|
if (app_getState() != APP_STATE_RUNNING)
|
|
return -1;
|
|
|
|
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));
|
|
g_state.transportFeatures = session.features;
|
|
|
|
if (g_state.spiceReady && g_params.useSpiceInput)
|
|
keybind_spiceRegister();
|
|
checkUUID();
|
|
DEBUG_INFO("Starting session");
|
|
atomic_store_explicit(
|
|
&g_state.lgHostConnected, true, memory_order_release);
|
|
|
|
g_state.lgHostConnected = true;
|
|
|
|
if (!core_startCursorThread() || !core_startFrameThread())
|
|
return -1;
|
|
|
|
while(likely(app_getState() == APP_STATE_RUNNING))
|
|
{
|
|
if (unlikely(!g_state.transportOps->sessionValid(g_state.transport)))
|
|
{
|
|
atomic_store_explicit(
|
|
&g_state.lgHostConnected, false, memory_order_release);
|
|
DEBUG_INFO("Waiting for the host to restart...");
|
|
app_setState(APP_STATE_RESTART);
|
|
break;
|
|
}
|
|
lgMessage_process();
|
|
g_state.ds->wait(100);
|
|
}
|
|
|
|
if (app_getState() == APP_STATE_RESTART)
|
|
{
|
|
lgSignalEvent(e_startup);
|
|
lgSignalEvent(g_state.frameEvent);
|
|
|
|
core_stopFrameThread();
|
|
core_stopCursorThread();
|
|
g_state.transportOps->disconnect(g_state.transport);
|
|
|
|
app_setState(APP_STATE_RUNNING);
|
|
lgInit();
|
|
goto restart;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void lg_shutdown(void)
|
|
{
|
|
app_setState(APP_STATE_SHUTDOWN);
|
|
|
|
if (t_spice)
|
|
lgJoinThread(t_spice, NULL);
|
|
|
|
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);
|
|
}
|
|
|
|
if (g_state.transportOps)
|
|
g_state.transportOps->destroy(&g_state.transport);
|
|
|
|
if (g_state.frameEvent)
|
|
{
|
|
lgFreeEvent(g_state.frameEvent);
|
|
g_state.frameEvent = NULL;
|
|
}
|
|
|
|
if (e_startup)
|
|
{
|
|
lgFreeEvent(e_startup);
|
|
e_startup = NULL;
|
|
}
|
|
|
|
if (e_spice)
|
|
{
|
|
lgFreeEvent(e_spice);
|
|
e_startup = NULL;
|
|
}
|
|
|
|
if (g_state.ds)
|
|
g_state.ds->shutdown();
|
|
|
|
if (g_state.cbRequestList)
|
|
{
|
|
ll_free(g_state.cbRequestList);
|
|
g_state.cbRequestList = NULL;
|
|
}
|
|
|
|
app_releaseAllKeybinds();
|
|
ll_free(g_state.bindings);
|
|
|
|
if (g_state.ds && g_state.dsInitialized)
|
|
g_state.ds->free();
|
|
|
|
renderQueue_free();
|
|
|
|
// free metrics ringbuffers
|
|
ringbuffer_free(&g_state.renderTimings);
|
|
ringbuffer_free(&g_state.frameLatency);
|
|
|
|
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;
|
|
}
|