/** * Looking Glass * Copyright © 2017-2022 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common/array.h" #include "common/debug.h" #include "common/crash.h" #include "common/KVMFR.h" #include "common/stringutils.h" #include "common/thread.h" #include "common/locking.h" #include "common/event.h" #include "common/ivshmem.h" #include "common/time.h" #include "common/version.h" #include "common/paths.h" #include "common/cpuinfo.h" #include "core.h" #include "app.h" #include "audio.h" #include "keybind.h" #include "clipboard.h" #include "kb.h" #include "ll.h" #include "egl_dynprocs.h" #include "gl_dynprocs.h" #include "overlays.h" #include "overlay_utils.h" #include "util.h" // forwards static int renderThread(void * unused); static LGEvent *e_startup = NULL; static LGThread *t_spice = NULL; static LGThread *t_render = NULL; struct AppState g_state = { 0 }; struct CursorState g_cursor; // this structure is initialized in config.c struct AppParams g_params = { 0 }; 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 void preSwapCallback(void * udata) { const uint64_t * renderStart = (const uint64_t *)udata; ringbuffer_push(g_state.renderDuration, &(float) {(nanotime() - *renderStart) * 1e-6f}); } static int renderThread(void * unused) { if (!RENDERER(renderStartup, g_state.useDMA)) { DEBUG_ERROR("EGL render failed to start"); g_state.state = 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; } 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(g_state.state != APP_STATE_SHUTDOWN) { bool forceRender = false; if (g_state.jitRender) forceRender = g_state.ds->waitFrame(); app_handleRenderEvent(microtime()); if (g_state.jitRender) { const uint64_t pending = atomic_load_explicit(&g_state.pendingCount, memory_order_acquire); if (!lgResetEvent(g_state.frameEvent) && !forceRender && !pending && !app_overlayNeedsRender() && !RENDERER(needsRender)) { 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) { float ups = atomic_load_explicit(&g_state.ups, memory_order_relaxed); if (!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, g_state.overlayInput ? g_state.overlayFrameTime : g_state.frameTime); } } int resize = atomic_load(&g_state.lgrResize); if (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, }; g_state.io->FontGlobalScale = 1.0f / g_state.windowScale; ImFontAtlas_Clear(g_state.io->Fonts); ImFontAtlas_AddFontFromFileTTF(g_state.io->Fonts, g_state.fontName, g_params.uiSize * g_state.windowScale, NULL, NULL); g_state.fontLarge = ImFontAtlas_AddFontFromFileTTF(g_state.io->Fonts, g_state.fontName, 1.3f * g_params.uiSize * g_state.windowScale, NULL, NULL); if (!ImFontAtlas_Build(g_state.io->Fonts)) DEBUG_FATAL("Failed to build font atlas: %s (%s)", g_params.uiFont, g_state.fontName); 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); const uint64_t renderStart = nanotime(); LG_LOCK(g_state.lgrLock); if (!RENDERER(render, g_params.winRotate, newFrame, invalidate, preSwapCallback, (void *)&renderStart)) { 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 (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 (!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; } } g_state.state = APP_STATE_SHUTDOWN; lgTimerDestroy(fpsTimer); core_stopCursorThread(); core_stopFrameThread(); RENDERER(deinitialize); g_state.lgr = NULL; LG_LOCK_FREE(g_state.lgrLock); return 0; } int main_cursorThread(void * unused) { LGMP_STATUS status; LG_RendererCursor cursorType = LG_CURSOR_COLOR; KVMFRCursor * cursor = NULL; int cursorSize = 0; lgWaitEvent(e_startup, TIMEOUT_INFINITE); // subscribe to the pointer queue while(g_state.state == APP_STATE_RUNNING) { status = lgmpClientSubscribe(g_state.lgmp, LGMP_Q_POINTER, &g_state.pointerQueue); if (status == LGMP_OK) break; if (status == LGMP_ERR_NO_SUCH_QUEUE) { usleep(1000); continue; } DEBUG_ERROR("lgmpClientSubscribe Failed: %s", lgmpStatusString(status)); g_state.state = APP_STATE_SHUTDOWN; break; } while(g_state.state == APP_STATE_RUNNING && !g_state.stopVideo) { LGMPMessage msg; if ((status = lgmpClientProcess(g_state.pointerQueue, &msg)) != LGMP_OK) { if (status == LGMP_ERR_QUEUE_EMPTY) { 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); } struct timespec req = { .tv_sec = 0, .tv_nsec = g_params.cursorPollInterval * 1000L }; struct timespec rem; while(nanosleep(&req, &rem) < 0) { if (errno != -EINTR) { DEBUG_ERROR("nanosleep failed"); break; } req = rem; } continue; } if (status == LGMP_ERR_INVALID_SESSION) g_state.state = APP_STATE_RESTART; else { DEBUG_ERROR("lgmpClientProcess Failed: %s", lgmpStatusString(status)); g_state.state = APP_STATE_SHUTDOWN; } break; } if (cursor && msg.size > cursorSize) { free(cursor); cursor = NULL; } /* copy and release the message ASAP */ if (!cursor) { cursor = malloc(msg.size); if (!cursor) { DEBUG_ERROR("failed to allocate %d bytes for cursor", msg.size); g_state.state = APP_STATE_SHUTDOWN; break; } cursorSize = msg.size; } memcpy(cursor, msg.mem, msg.size); lgmpClientMessageDone(g_state.pointerQueue); g_cursor.guest.visible = msg.udata & CURSOR_FLAG_VISIBLE; if (msg.udata & CURSOR_FLAG_SHAPE) { switch(cursor->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"); lgmpClientMessageDone(g_state.pointerQueue); continue; } g_cursor.guest.hx = cursor->hx; g_cursor.guest.hy = cursor->hy; const uint8_t * data = (const uint8_t *)(cursor + 1); if (!RENDERER(onMouseShape, cursorType, cursor->width, cursor->height, cursor->pitch, data) ) { DEBUG_ERROR("Failed to update mouse shape"); continue; } } if (msg.udata & CURSOR_FLAG_POSITION) { bool valid = g_cursor.guest.valid; g_cursor.guest.x = cursor->x; g_cursor.guest.y = cursor->y; g_cursor.guest.valid = true; // if the state just became valid if (valid != true && core_inputEnabled()) { core_alignToGuest(); app_resyncMouseBasic(); } // tell the DS there was an update core_handleGuestMouseUpdate(); } 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 && g_cursor.guest.visible && !g_state.stopVideo) lgSignalEvent(g_state.frameEvent); } lgmpClientUnsubscribe(&g_state.pointerQueue); if (cursor) { free(cursor); cursor = NULL; } return 0; } int main_frameThread(void * unused) { struct DMAFrameInfo { KVMFRFrame * frame; size_t dataSize; int fd; }; LGMP_STATUS status; PLGMPClientQueue queue; uint32_t frameSerial = 0; uint32_t formatVer = 0; size_t dataSize = 0; LG_RendererFormat lgrFormat; struct DMAFrameInfo dmaInfo[LGMP_Q_FRAME_LEN] = {0}; if (g_state.useDMA) DEBUG_INFO("Using DMA buffer support"); lgWaitEvent(e_startup, TIMEOUT_INFINITE); if (g_state.state != APP_STATE_RUNNING) return 0; // subscribe to the frame queue while(g_state.state == APP_STATE_RUNNING) { status = lgmpClientSubscribe(g_state.lgmp, LGMP_Q_FRAME, &queue); if (status == LGMP_OK) break; if (status == LGMP_ERR_NO_SUCH_QUEUE) { usleep(1000); continue; } DEBUG_ERROR("lgmpClientSubscribe Failed: %s", lgmpStatusString(status)); g_state.state = APP_STATE_SHUTDOWN; break; } while(g_state.state == APP_STATE_RUNNING && !g_state.stopVideo) { LGMPMessage msg; if ((status = lgmpClientProcess(queue, &msg)) != LGMP_OK) { if (status == LGMP_ERR_QUEUE_EMPTY) { struct timespec req = { .tv_sec = 0, .tv_nsec = g_params.framePollInterval * 1000L }; struct timespec rem; while(nanosleep(&req, &rem) < 0) { if (errno != -EINTR) { DEBUG_ERROR("nanosleep failed"); break; } req = rem; } continue; } if (status == LGMP_ERR_INVALID_SESSION) g_state.state = APP_STATE_RESTART; else { DEBUG_ERROR("lgmpClientProcess Failed: %s", lgmpStatusString(status)); g_state.state = APP_STATE_SHUTDOWN; } break; } KVMFRFrame * frame = (KVMFRFrame *)msg.mem; // ignore any repeated frames, this happens when a new client connects to // the same host application. if (frame->frameSerial == frameSerial && g_state.formatValid) { lgmpClientMessageDone(queue); continue; } frameSerial = frame->frameSerial; struct DMAFrameInfo *dma = NULL; if (!g_state.formatValid || frame->formatVer != formatVer) { // setup the renderer format with the frame format details lgrFormat.type = frame->type; lgrFormat.width = frame->width; lgrFormat.height = frame->height; lgrFormat.stride = frame->stride; lgrFormat.pitch = frame->pitch; if (frame->height != frame->realHeight) { const float needed = ((frame->realHeight * frame->pitch * 2) / 1048576.0f) + 10.0f; const int size = (int)powf(2.0f, ceilf(logf(needed) / logf(2.0f))); DEBUG_BREAK(); DEBUG_WARN("IVSHMEM too small, screen truncated"); DEBUG_WARN("Recommend increase size to %d MiB", size); DEBUG_BREAK(); app_alert(LG_ALERT_ERROR, "IVSHMEM too small, screen truncated\n" "Recommend increasing size to %d MiB", size); } switch(frame->rotation) { case FRAME_ROT_0 : lgrFormat.rotate = LG_ROTATE_0 ; break; case FRAME_ROT_90 : lgrFormat.rotate = LG_ROTATE_90 ; break; case FRAME_ROT_180: lgrFormat.rotate = LG_ROTATE_180; break; case FRAME_ROT_270: lgrFormat.rotate = LG_ROTATE_270; break; } g_state.rotate = lgrFormat.rotate; bool error = false; switch(frame->type) { case FRAME_TYPE_RGBA: case FRAME_TYPE_BGRA: case FRAME_TYPE_RGBA10: dataSize = lgrFormat.height * lgrFormat.pitch; lgrFormat.bpp = 32; break; case FRAME_TYPE_RGBA16F: dataSize = lgrFormat.height * lgrFormat.pitch; lgrFormat.bpp = 64; break; default: DEBUG_ERROR("Unsupported frameType"); error = true; break; } if (error) { lgmpClientMessageDone(queue); g_state.state = APP_STATE_SHUTDOWN; break; } g_state.formatValid = true; formatVer = frame->formatVer; DEBUG_INFO("Format: %s %ux%u stride:%u pitch:%u rotation:%d", FrameTypeStr[frame->type], frame->width, frame->height, frame->stride, frame->pitch, frame->rotation); LG_LOCK(g_state.lgrLock); if (!RENDERER(onFrameFormat, lgrFormat)) { DEBUG_ERROR("renderer failed to configure format"); g_state.state = APP_STATE_SHUTDOWN; LG_UNLOCK(g_state.lgrLock); break; } LG_UNLOCK(g_state.lgrLock); g_state.srcSize.x = lgrFormat.width; g_state.srcSize.y = lgrFormat.height; g_state.haveSrcSize = true; if (g_params.autoResize) g_state.ds->setWindowSize(lgrFormat.width, lgrFormat.height); core_updatePositionInfo(); } if (g_state.useDMA) { /* find the existing dma buffer if it exists */ for(int i = 0; i < ARRAY_LENGTH(dmaInfo); ++i) { if (dmaInfo[i].frame == frame) { dma = &dmaInfo[i]; /* if it's too small close it */ if (dma->dataSize < dataSize) { close(dma->fd); dma->fd = -1; } break; } } /* otherwise find a free buffer for use */ if (!dma) for(int i = 0; i < ARRAY_LENGTH(dmaInfo); ++i) { if (!dmaInfo[i].frame) { dma = &dmaInfo[i]; dma->frame = frame; dma->fd = -1; break; } } /* open the buffer */ if (dma->fd == -1) { const uintptr_t pos = (uintptr_t)msg.mem - (uintptr_t)g_state.shm.mem; const uintptr_t offset = (uintptr_t)frame->offset + FrameBufferStructSize; dma->dataSize = dataSize; dma->fd = ivshmemGetDMABuf(&g_state.shm, pos + offset, dataSize); if (dma->fd < 0) { DEBUG_ERROR("Failed to get the DMA buffer for the frame"); g_state.state = APP_STATE_SHUTDOWN; break; } } } FrameBuffer * fb = (FrameBuffer *)(((uint8_t*)frame) + frame->offset); if (!RENDERER(onFrame, fb, g_state.useDMA ? dma->fd : -1, frame->damageRects, frame->damageRectsCount)) { lgmpClientMessageDone(queue); DEBUG_ERROR("renderer on frame returned failure"); g_state.state = APP_STATE_SHUTDOWN; break; } if (g_params.autoScreensaver && g_state.autoIdleInhibitState != frame->blockScreensaver) { if (frame->blockScreensaver) g_state.ds->inhibitIdle(); else g_state.ds->uninhibitIdle(); g_state.autoIdleInhibitState = frame->blockScreensaver; } const uint64_t t = nanotime(); const uint64_t delta = t - g_state.lastFrameTime; g_state.lastFrameTime = t; if (g_state.lastFrameTimeValid) ringbuffer_push(g_state.uploadTimings, &(float) { delta * 1e-6f }); g_state.lastFrameTimeValid = true; atomic_fetch_add_explicit(&g_state.frameCount, 1, memory_order_relaxed); 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); lgmpClientMessageDone(queue); } lgmpClientUnsubscribe(&queue); RENDERER(onRestart); if (g_state.useDMA) { for(int i = 0; i < ARRAY_LENGTH(dmaInfo); ++i) if (dmaInfo[i].fd >= 0) close(dmaInfo[i].fd); } return 0; } static void checkUUID(void) { if (!g_state.spiceUUIDValid || !g_state.guestUUIDValid) return; if (memcmp(g_state.spiceUUID, g_state.guestUUID, sizeof(g_state.spiceUUID)) == 0) return; DEBUG_ERROR("UUIDs do not match, you have connected SPICE to the wrong guest"); g_state.state = APP_STATE_SHUTDOWN; } void spiceReady(void) { // set the intial mouse mode purespice_mouseMode(true); PSServerInfo info; if (!purespice_getServerInfo(&info)) return; bool uuidValid = false; for(int i = 0; i < sizeof(info.uuid); ++i) if (info.uuid[i]) { uuidValid = true; break; } if (!uuidValid) return; memcpy(g_state.spiceUUID, info.uuid, sizeof(g_state.spiceUUID)); g_state.spiceUUIDValid = true; checkUUID(); } 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, .log = { .info = debug_info, .warn = debug_warn, .error = debug_error, }, .clipboard = { .enable = g_params.useSpiceClipboard, .notice = cb_spiceNotice, .data = cb_spiceData, .release = cb_spiceRelease, .request = cb_spiceRequest }, .playback = { .enable = audio_supportsPlayback(), .start = audio_playbackStart, .volume = audio_playbackVolume, .mute = audio_playbackMute, .stop = audio_playbackStop, .data = audio_playbackData }, .record = { .enable = audio_supportsRecord(), .start = audio_recordStart, .volume = audio_recordVolume, .mute = audio_recordMute, .stop = audio_recordStop } }; if (!purespice_connect(&config)) { DEBUG_ERROR("Failed to connect to spice server"); goto end; } // process all spice messages while(g_state.state != 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; } } end: audio_free(); g_state.state = APP_STATE_SHUTDOWN; return 0; } void intHandler(int sig) { switch(sig) { case SIGINT: case SIGTERM: if (g_state.state != APP_STATE_SHUTDOWN) { DEBUG_INFO("Caught signal, shutting down..."); g_state.state = APP_STATE_SHUTDOWN; } else { DEBUG_INFO("Caught second signal, force quitting..."); 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() { 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 int lg_run(void) { 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(); 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()) { g_state.fontName = util_getUIFont(g_params.uiFont); DEBUG_INFO("Using font: %s", g_state.fontName); } app_initOverlays(); // initialize metrics ringbuffers g_state.renderTimings = ringbuffer_new(256, sizeof(float)); g_state.uploadTimings = ringbuffer_new(256, sizeof(float)); g_state.renderDuration = ringbuffer_new(256, sizeof(float)); overlayGraph_register("FRAME" , g_state.renderTimings , 0.0f, 50.0f); overlayGraph_register("UPLOAD", g_state.uploadTimings , 0.0f, 50.0f); overlayGraph_register("RENDER", g_state.renderDuration, 0.0f, 10.0f); initImGuiKeyMap(g_state.io->KeyMap); // 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; } DEBUG_ASSERT(g_state.ds); 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; } // 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); // try map the shared memory if (!ivshmemOpen(&g_state.shm)) { DEBUG_ERROR("Failed to map memory"); return -1; } if (g_params.useSpiceInput || g_params.useSpiceClipboard || g_params.useSpiceAudio) { if (!lgCreateThread("spiceThread", spiceThread, NULL, &t_spice)) { DEBUG_ERROR("spice create thread failed"); return -1; } } // select and init a renderer bool needsOpenGL; 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_params.allowDMA && ivshmemHasDMA(&g_state.shm); // 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, .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, .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_register(); // 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; } 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); g_state.ds->startup(); g_state.cbAvailable = g_state.ds->cbInit && g_state.ds->cbInit(); if (g_state.cbAvailable) g_state.cbRequestList = ll_new(); LGMP_STATUS status; while(g_state.state == APP_STATE_RUNNING) { if ((status = lgmpClientInit(g_state.shm.mem, g_state.shm.size, &g_state.lgmp)) == LGMP_OK) break; DEBUG_ERROR("lgmpClientInit Failed: %s", lgmpStatusString(status)); return -1; } /* this short timeout is to allow the LGMP host to update the timestamp before * we start checking for a valid session */ g_state.ds->wait(200); if (g_params.captureOnStart) core_setGrab(true); uint32_t udataSize; KVMFR *udata; int waitCount = 0; restart: while(g_state.state == APP_STATE_RUNNING) { if ((status = lgmpClientSessionInit(g_state.lgmp, &udataSize, (uint8_t **)&udata)) == LGMP_OK) break; if (status == LGMP_ERR_INVALID_VERSION) { reportBadVersion(); DEBUG_INFO("Waiting for you to upgrade the host application"); while (g_state.state == APP_STATE_RUNNING && lgmpClientSessionInit(g_state.lgmp, &udataSize, (uint8_t **)&udata) != LGMP_OK) g_state.ds->wait(1000); if (g_state.state != APP_STATE_RUNNING) return -1; break; } if (status != LGMP_ERR_INVALID_SESSION && status != LGMP_ERR_INVALID_MAGIC) { DEBUG_ERROR("lgmpClientSessionInit Failed: %s", lgmpStatusString(status)); return -1; } if (waitCount++ == 0) { DEBUG_BREAK(); DEBUG_INFO("The host application seems to not be running"); DEBUG_INFO("Waiting for the host application to start..."); } if (waitCount == 30) { DEBUG_BREAK(); DEBUG_INFO("Please check the host application is running and is the correct version"); DEBUG_INFO("Check the host log in your guest at %%ProgramData%%\\Looking Glass (host)\\looking-glass-host.txt"); DEBUG_INFO("Continuing to wait..."); } g_state.ds->wait(1000); } if (g_state.state != APP_STATE_RUNNING) return -1; // dont show warnings again after the first startup waitCount = 100; const bool magicMatches = memcmp(udata->magic, KVMFR_MAGIC, sizeof(udata->magic)) == 0; if (udataSize < sizeof(*udata) || !magicMatches || udata->version != KVMFR_VERSION) { reportBadVersion(); if (magicMatches) { DEBUG_ERROR("Expected KVMFR version %d, got %d", KVMFR_VERSION, udata->version); DEBUG_ERROR("Client version: %s", BUILD_VERSION); if (udata->version >= 2) DEBUG_ERROR(" Host version: %s", udata->hostver); } else DEBUG_ERROR("Invalid KVMFR magic"); DEBUG_BREAK(); if (magicMatches) { DEBUG_INFO("Waiting for you to upgrade the host application"); while (g_state.state == APP_STATE_RUNNING && udata->version != KVMFR_VERSION) g_state.ds->wait(1000); if (g_state.state != APP_STATE_RUNNING) return -1; goto restart; } else return -1; } DEBUG_INFO("Guest Information:"); DEBUG_INFO("Version : %s", udata->hostver); /* parse the kvmfr records from the userdata */ udataSize -= sizeof(*udata); uint8_t * p = (uint8_t *)(udata + 1); while(udataSize >= sizeof(KVMFRRecord)) { KVMFRRecord * record = (KVMFRRecord *)p; p += sizeof(*record); udataSize -= sizeof(*record); if (record->size > udataSize) { DEBUG_WARN("KVMFRecord size is invalid, aborting parsing."); break; } switch(record->type) { case KVMFR_RECORD_VMINFO: { KVMFRRecord_VMInfo * vmInfo = (KVMFRRecord_VMInfo *)p; DEBUG_INFO("UUID : " "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", vmInfo->uuid[ 0], vmInfo->uuid[ 1], vmInfo->uuid[ 2], vmInfo->uuid[ 3], vmInfo->uuid[ 4], vmInfo->uuid[ 5], vmInfo->uuid[ 6], vmInfo->uuid[ 7], vmInfo->uuid[ 8], vmInfo->uuid[ 9], vmInfo->uuid[10], vmInfo->uuid[11], vmInfo->uuid[12], vmInfo->uuid[13], vmInfo->uuid[14], vmInfo->uuid[15]); DEBUG_INFO("CPU Model: %s", vmInfo->model); DEBUG_INFO("CPU : %u sockets, %u cores, %u threads", vmInfo->sockets, vmInfo->cores, vmInfo->cpus); DEBUG_INFO("Using : %s", vmInfo->capture); bool uuidValid = false; for(int i = 0; i < sizeof(vmInfo->uuid); ++i) if (vmInfo->uuid[i]) { uuidValid = true; break; } if (!uuidValid) break; memcpy(g_state.guestUUID, vmInfo->uuid, sizeof(g_state.guestUUID)); g_state.guestUUIDValid = true; break; } case KVMFR_RECORD_OSINFO: { KVMFRRecord_OSInfo * osInfo = (KVMFRRecord_OSInfo *)p; static const char * typeStr[] = { "Linux", "BSD", "OSX", "Windows", "Other" }; const char * type; if (osInfo->os >= ARRAY_LENGTH(typeStr)) type = "Unknown"; else type = typeStr[osInfo->os]; DEBUG_INFO("OS : %s", type); if (osInfo->name[0]) DEBUG_INFO("OS Name : %s", osInfo->name); break; } default: DEBUG_WARN("Unhandled KVMFRecord type: %d", record->type); break; } p += record->size; udataSize -= record->size; } checkUUID(); if (g_state.state == APP_STATE_RUNNING) DEBUG_INFO("Starting session"); g_state.kvmfrFeatures = udata->features; if (!core_startCursorThread() || !core_startFrameThread()) return -1; while(g_state.state == APP_STATE_RUNNING) { if (!lgmpClientSessionValid(g_state.lgmp)) { g_state.state = APP_STATE_RESTART; break; } g_state.ds->wait(100); } if (g_state.state == APP_STATE_RESTART) { lgSignalEvent(e_startup); lgSignalEvent(g_state.frameEvent); core_stopFrameThread(); core_stopCursorThread(); g_state.state = APP_STATE_RUNNING; lgInit(); RENDERER(onRestart); DEBUG_INFO("Waiting for the host to restart..."); goto restart; } return 0; } static void lg_shutdown(void) { g_state.state = APP_STATE_SHUTDOWN; 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); } lgmpClientFree(&g_state.lgmp); if (g_state.frameEvent) { lgFreeEvent(g_state.frameEvent); g_state.frameEvent = NULL; } if (e_startup) { lgFreeEvent(e_startup); e_startup = NULL; } // if spice is still connected send key up events for any pressed keys if (g_params.useSpiceInput) { for(int scancode = 0; scancode < KEY_MAX; ++scancode) if (g_state.keyDown[scancode]) { g_state.keyDown[scancode] = false; purespice_keyUp(scancode); } purespice_disconnect(); if (t_spice) lgJoinThread(t_spice, NULL); } if (g_state.ds) g_state.ds->shutdown(); if (g_state.cbRequestList) { ll_free(g_state.cbRequestList); g_state.cbRequestList = NULL; } app_releaseAllKeybinds(); if (g_state.dsInitialized) g_state.ds->free(); if (g_state.overlays) { app_freeOverlays(); ll_free(g_state.overlays); g_state.overlays = NULL; } ivshmemClose(&g_state.shm); // free metrics ringbuffers ringbuffer_free(&g_state.renderTimings); ringbuffer_free(&g_state.uploadTimings); ringbuffer_free(&g_state.renderDuration); 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; } DEBUG_INFO("Looking Glass (%s)", BUILD_VERSION); DEBUG_INFO("Locking Method: " LG_LOCK_MODE); lgDebugCPU(); if (!installCrashHandler("/proc/self/exe")) DEBUG_WARN("Failed to install the crash handler"); lgPathsInit("looking-glass"); config_init(); ivshmemOptionsInit(); egl_dynProcsInit(); gl_dynProcsInit(); g_state.overlays = ll_new(); app_registerOverlay(&LGOverlayConfig, NULL); app_registerOverlay(&LGOverlayAlert , NULL); app_registerOverlay(&LGOverlayFPS , NULL); app_registerOverlay(&LGOverlayGraphs, NULL); app_registerOverlay(&LGOverlayHelp , 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(); if (!config_load(argc, argv)) return -1; const int ret = lg_run(); lg_shutdown(); config_free(); util_freeUIFonts(); cleanupCrashHandler(); return ret; }