From 36b33033a10be117ec08c2519041fe4bda176157 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Mon, 20 Jul 2026 05:09:43 +0000 Subject: [PATCH] [client] transport: introduce pluggable frame transports Move shared-memory ownership, LGMP session handling, queue access, and DMA setup behind a transport interface. The LGMP backend preserves the existing zero-copy frame and DMA paths while owning its lgmp:* options. Expose the initialized EGL context through a versioned renderer interop record for future accelerated decode backends. Add an LGMP-independent, deterministic test transport for graphics-pipeline validation. --- client/CMakeLists.txt | 8 +- client/include/interface/renderer.h | 36 +- client/include/interface/transport.h | 222 +++++ client/renderers/EGL/cursor.c | 12 +- client/renderers/EGL/cursor.h | 2 +- client/renderers/EGL/damage.c | 3 +- client/renderers/EGL/damage.h | 4 +- client/renderers/EGL/desktop_rects.c | 1 - client/renderers/EGL/egl.c | 27 +- client/renderers/EGL/texture_framebuffer.c | 7 +- client/src/app.c | 10 +- client/src/config.c | 39 +- client/src/core.c | 68 +- client/src/keybind.c | 2 +- client/src/main.c | 1001 ++++++-------------- client/src/main.h | 18 +- client/src/transport.c | 59 ++ client/transports/CMakeLists.txt | 44 + client/transports/LGMP/CMakeLists.txt | 11 + client/transports/LGMP/lgmp.c | 766 +++++++++++++++ client/transports/Test/CMakeLists.txt | 10 + client/transports/Test/test.c | 607 ++++++++++++ 22 files changed, 2138 insertions(+), 819 deletions(-) create mode 100644 client/include/interface/transport.h create mode 100644 client/src/transport.c create mode 100644 client/transports/CMakeLists.txt create mode 100644 client/transports/LGMP/CMakeLists.txt create mode 100644 client/transports/LGMP/lgmp.c create mode 100644 client/transports/Test/CMakeLists.txt create mode 100644 client/transports/Test/test.c diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 85b9c559..90885359 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -33,6 +33,10 @@ add_feature_info(ENABLE_OPENGL ENABLE_OPENGL "Legacy OpenGL renderer.") option(ENABLE_EGL "Enable the EGL renderer" ON) add_feature_info(ENABLE_EGL ENABLE_EGL "EGL renderer.") +option(ENABLE_TEST_TRANSPORT "Enable the test transport" OFF) +add_feature_info(ENABLE_TEST_TRANSPORT ENABLE_TEST_TRANSPORT + "Synthetic frames for graphics-pipeline validation.") + option(ENABLE_BACKTRACE "Enable backtrace support on crash" ON) add_feature_info(ENABLE_BACKTRACE ENABLE_BACKTRACE "Backtrace support.") @@ -134,6 +138,7 @@ set(SOURCES src/overlay_utils.c src/render_queue.c src/evdev.c + src/transport.c src/overlay/splash.c src/overlay/alert.c @@ -158,6 +163,7 @@ add_subdirectory("${PROJECT_TOP}/repos/cimgui" "${CMAKE_BINARY_DIR}/cimgui" E add_subdirectory(displayservers) add_subdirectory(renderers) +add_subdirectory(transports) configure_file("${PROJECT_TOP}/resources/looking-glass-client.desktop.in" "${CMAKE_BINARY_DIR}/resources/looking-glass-client.desktop" @ONLY) add_executable(looking-glass-client ${SOURCES}) @@ -191,9 +197,9 @@ target_link_libraries(looking-glass-client lg_resources lg_common displayservers - lgmp purespice renderers + transports cimgui ) diff --git a/client/include/interface/renderer.h b/client/include/interface/renderer.h index 33d7d04c..818d7c66 100644 --- a/client/include/interface/renderer.h +++ b/client/include/interface/renderer.h @@ -23,7 +23,7 @@ #include #include "app.h" -#include "common/KVMFR.h" +#include "common/types.h" #include "common/framebuffer.h" #define IS_LG_RENDERER_VALID(x) \ @@ -88,7 +88,7 @@ typedef struct LG_RendererFormat unsigned int bpp; // bits per pixel (zero if compressed) LG_RendererRotate rotate; // guest rotation - // HDR static metadata (from KVMFRFrame, valid when hdrMetadata is true) + // HDR static metadata, valid when hdrMetadata is true uint16_t hdrDisplayPrimary[3][2]; uint16_t hdrWhitePoint[2]; uint32_t hdrMaxDisplayLuminance; @@ -119,6 +119,31 @@ LG_RendererCursor; typedef struct LG_Renderer LG_Renderer; +typedef enum LG_RendererInteropType +{ + LG_RENDERER_INTEROP_NONE, + LG_RENDERER_INTEROP_EGL, +} +LG_RendererInteropType; + +#define LG_RENDERER_INTEROP_VERSION 1 + +typedef struct LG_RendererInterop +{ + uint32_t version; + uint32_t size; + LG_RendererInteropType type; + struct + { + EGLDisplay display; + EGLConfig config; + EGLContext shareContext; + bool dmaBufImport; + } + egl; +} +LG_RendererInterop; + typedef struct LG_RendererOps { /* returns the friendly name of the renderer */ @@ -145,6 +170,11 @@ typedef struct LG_RendererOps * Context: renderThread */ bool (*supports)(LG_Renderer * renderer, LG_RendererSupport support); + /* optionally exposes the initialized renderer device context to a transport. + * The transport must create its own shared context and release it before the + * renderer is deinitialized. Context: lg_run */ + bool (*getInterop)(LG_Renderer * renderer, LG_RendererInterop * interop); + /* called when the renderer is to reset it's state * Context: lg_run & frameThread */ void (*onRestart)(LG_Renderer * renderer); @@ -163,7 +193,7 @@ typedef struct LG_RendererOps /* optional display calibration update for hardware cursor composition * Context: cursorThread */ void (*onMouseColorTransform)(LG_Renderer * renderer, - const KVMFRColorTransform * transform); + const LGColorTransform * transform); /* updates the cursor-specific SDR white level reported by IddCx * Context: cursorThread */ diff --git a/client/include/interface/transport.h b/client/include/interface/transport.h new file mode 100644 index 00000000..445b4706 --- /dev/null +++ b/client/include/interface/transport.h @@ -0,0 +1,222 @@ +/** + * 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 + */ + +#ifndef _H_LG_CLIENT_TRANSPORT_ +#define _H_LG_CLIENT_TRANSPORT_ + +#include +#include +#include + +#include "common/framebuffer.h" +#include "common/types.h" + +#define LG_TRANSPORT_MAX_DAMAGE_RECTS LG_MAX_FRAME_DAMAGE_RECTS + +typedef struct LG_Transport LG_Transport; +typedef struct LG_RendererInterop LG_RendererInterop; + +typedef enum LG_TransportStatus +{ + LG_TRANSPORT_OK, + LG_TRANSPORT_TIMEOUT, + LG_TRANSPORT_UNAVAILABLE, + LG_TRANSPORT_INVALID_VERSION, + LG_TRANSPORT_DISCONNECTED, + LG_TRANSPORT_END, + LG_TRANSPORT_ERROR, +} +LG_TransportStatus; + +typedef enum LG_TransportGuestOS +{ + LG_TRANSPORT_OS_LINUX, + LG_TRANSPORT_OS_BSD, + LG_TRANSPORT_OS_OSX, + LG_TRANSPORT_OS_WINDOWS, + LG_TRANSPORT_OS_OTHER, +} +LG_TransportGuestOS; + +enum +{ + LG_TRANSPORT_FEATURE_SET_CURSOR_POS = 0x1, + LG_TRANSPORT_FEATURE_WINDOW_SIZE = 0x2, +}; + +typedef uint32_t LG_TransportFeatureFlags; + +typedef struct LG_TransportSession +{ + char version[32]; + LG_TransportFeatureFlags features; + + bool uuidValid; + uint8_t uuid[16]; + + LG_TransportGuestOS os; + char osName[64]; + char capture[32]; + char cpuModel[256]; + uint8_t cpus; + uint8_t cores; + uint8_t sockets; + + uint32_t remoteVersion; +} +LG_TransportSession; + +enum +{ + LG_TRANSPORT_FRAME_BLOCK_SCREENSAVER = 0x1, + LG_TRANSPORT_FRAME_REQUEST_ACTIVATION = 0x2, + LG_TRANSPORT_FRAME_TRUNCATED = 0x4, +}; + +typedef uint32_t LG_TransportFrameFlags; + +typedef struct LG_TransportFrameFormat +{ + uint32_t version; + FrameType type; + uint32_t screenWidth; + uint32_t screenHeight; + uint32_t dataWidth; + uint32_t dataHeight; + uint32_t frameWidth; + uint32_t frameHeight; + FrameRotation rotation; + uint32_t stride; + uint32_t pitch; + + bool hdr; + bool hdrPQ; + bool hdrMetadata; + uint16_t hdrDisplayPrimary[3][2]; + uint16_t hdrWhitePoint[2]; + uint32_t hdrMaxDisplayLuminance; + uint32_t hdrMinDisplayLuminance; + uint32_t hdrMaxContentLightLevel; + uint32_t hdrMaxFrameAverageLightLevel; + uint32_t sdrWhiteLevel; +} +LG_TransportFrameFormat; + +typedef struct LG_TransportFrame +{ + uint64_t serial; + uint64_t timestamp; + LG_TransportFrameFlags flags; + // Backend-owned immutable metadata, valid until releaseFrame. + const LG_TransportFrameFormat * format; + + const FrameBuffer * framebuffer; + int dmaFD; + const FrameDamageRect * damageRects; + uint32_t damageRectsCount; +} +LG_TransportFrame; + +enum +{ + LG_TRANSPORT_POINTER_POSITION = 0x1, + LG_TRANSPORT_POINTER_VISIBLE = 0x2, + LG_TRANSPORT_POINTER_SHAPE = 0x4, + LG_TRANSPORT_POINTER_COLOR_TRANSFORM = 0x8, + LG_TRANSPORT_POINTER_VISIBLE_VALID = 0x10, +}; + +typedef uint32_t LG_TransportPointerFlags; + +typedef struct LG_TransportPointer +{ + LG_TransportPointerFlags flags; + int16_t x; + int16_t y; + CursorType type; + int8_t hx; + int8_t hy; + uint32_t width; + uint32_t height; + uint32_t pitch; + uint32_t sdrWhiteLevel; + const uint8_t * shape; + const LGColorTransform * colorTransform; +} +LG_TransportPointer; + +typedef enum LG_TransportControlType +{ + LG_TRANSPORT_CONTROL_SET_CURSOR_POS, + LG_TRANSPORT_CONTROL_WINDOW_SIZE, +} +LG_TransportControlType; + +typedef struct LG_TransportControl +{ + LG_TransportControlType type; + union + { + struct { int32_t x, y; } cursorPos; + struct { uint32_t width, height; } windowSize; + }; +} +LG_TransportControl; + +typedef uint64_t LG_TransportControlToken; + +typedef struct LG_TransportOps +{ + const char * name; + void (*setup)(void); + bool (*create)(LG_Transport ** transport); + void (*destroy)(LG_Transport ** transport); + + LG_TransportStatus (*connect)(LG_Transport * transport, + LG_TransportSession * session); + void (*disconnect)(LG_Transport * transport); + bool (*sessionValid)(LG_Transport * transport); + bool (*supportsDMA)(LG_Transport * transport); + bool (*attachRenderer)(LG_Transport * transport, + const LG_RendererInterop * interop); + void (*detachRenderer)(LG_Transport * transport); + + LG_TransportStatus (*nextFrame)(LG_Transport * transport, bool useDMA, + LG_TransportFrame * frame); + void (*releaseFrame)(LG_Transport * transport, LG_TransportFrame * frame); + + LG_TransportStatus (*nextPointer)(LG_Transport * transport, + LG_TransportPointer * pointer); + void (*releasePointer)(LG_Transport * transport, + LG_TransportPointer * pointer); + + LG_TransportStatus (*sendControl)(LG_Transport * transport, + const LG_TransportControl * control, LG_TransportControlToken * token); + LG_TransportStatus (*controlStatus)(LG_Transport * transport, + LG_TransportControlToken token); +} +LG_TransportOps; + +void lgTransport_setup(void); +bool lgTransport_isValid(const char * name); +bool lgTransport_create(const char * name, LG_Transport ** transport, + const LG_TransportOps ** ops); + +#endif diff --git a/client/renderers/EGL/cursor.c b/client/renderers/EGL/cursor.c index 9b5ea14e..4577bd21 100644 --- a/client/renderers/EGL/cursor.c +++ b/client/renderers/EGL/cursor.c @@ -101,8 +101,8 @@ struct EGL_Cursor _Atomic(float) mapHDRContentPeak; bool colorTransformUpdate; - KVMFRColorTransform pendingColorTransform; - KVMFRColorTransform activeColorTransform; + LGColorTransform pendingColorTransform; + LGColorTransform activeColorTransform; GLuint colorLUT; struct CursorTex norm; @@ -239,7 +239,7 @@ bool egl_cursorInit(EGL_Cursor ** cursor) atomic_init(&(*cursor)->scale , 1.0f ); atomic_init(&(*cursor)->sourceTransfer , CURSOR_TRANSFER_SRGB); atomic_init(&(*cursor)->sdrWhiteLevel , - KVMFR_SDR_WHITE_LEVEL_DEFAULT); + LG_SDR_WHITE_LEVEL_DEFAULT); atomic_init(&(*cursor)->mapHDRtoSDR , false); atomic_init(&(*cursor)->mapHDRGain , 1.0f ); atomic_init(&(*cursor)->mapHDRContentPeak, 1.0f ); @@ -301,7 +301,7 @@ bool egl_cursorSetShape(EGL_Cursor * cursor, const LG_RendererCursor type, } void egl_cursorSetColorTransform(EGL_Cursor * cursor, - const KVMFRColorTransform * transform) + const LGColorTransform * transform) { LG_LOCK(cursor->lock); cursor->pendingColorTransform = *transform; @@ -351,7 +351,7 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor, cursor->activeColorTransform = cursor->pendingColorTransform; cursor->colorTransformUpdate = false; - if (cursor->activeColorTransform.flags & KVMFR_COLOR_TRANSFORM_LUT) + if (cursor->activeColorTransform.flags & LG_COLOR_TRANSFORM_LUT) { if (!cursor->colorLUT) glGenTextures(1, &cursor->colorLUT); @@ -568,5 +568,5 @@ void egl_cursorSetHDRState(EGL_Cursor * cursor, bool sourceHDR, void egl_cursorSetSDRWhiteLevel(EGL_Cursor * cursor, float sdrWhiteLevel) { atomic_store(&cursor->sdrWhiteLevel, sdrWhiteLevel > 0.0f ? - sdrWhiteLevel : KVMFR_SDR_WHITE_LEVEL_DEFAULT); + sdrWhiteLevel : LG_SDR_WHITE_LEVEL_DEFAULT); } diff --git a/client/renderers/EGL/cursor.h b/client/renderers/EGL/cursor.h index cf89fbe9..4b194b1d 100644 --- a/client/renderers/EGL/cursor.h +++ b/client/renderers/EGL/cursor.h @@ -44,7 +44,7 @@ bool egl_cursorSetShape( const uint8_t * data); void egl_cursorSetColorTransform(EGL_Cursor * cursor, - const KVMFRColorTransform * transform); + const LGColorTransform * transform); void egl_cursorSetSize(EGL_Cursor * cursor, const float x, const float y); diff --git a/client/renderers/EGL/damage.c b/client/renderers/EGL/damage.c index dcba36c5..b4479741 100644 --- a/client/renderers/EGL/damage.c +++ b/client/renderers/EGL/damage.c @@ -20,7 +20,6 @@ #include "damage.h" #include "common/debug.h" -#include "common/KVMFR.h" #include "common/locking.h" #include "app.h" @@ -89,7 +88,7 @@ bool egl_damageInit(EGL_Damage ** damage) return false; } - if (!egl_desktopRectsInit(&(*damage)->mesh, KVMFR_MAX_DAMAGE_RECTS)) + if (!egl_desktopRectsInit(&(*damage)->mesh, LG_MAX_FRAME_DAMAGE_RECTS)) { DEBUG_ERROR("Failed to initialize the mesh"); return false; diff --git a/client/renderers/EGL/damage.h b/client/renderers/EGL/damage.h index f1215d6a..afd2ecbc 100644 --- a/client/renderers/EGL/damage.h +++ b/client/renderers/EGL/damage.h @@ -21,14 +21,14 @@ #pragma once #include -#include "common/KVMFR.h" +#include "common/types.h" #include "interface/renderer.h" #include "desktop_rects.h" struct DesktopDamage { int count; - FrameDamageRect rects[KVMFR_MAX_DAMAGE_RECTS]; + FrameDamageRect rects[LG_MAX_FRAME_DAMAGE_RECTS]; }; typedef struct EGL_Damage EGL_Damage; diff --git a/client/renderers/EGL/desktop_rects.c b/client/renderers/EGL/desktop_rects.c index 2c2069b8..74568da6 100644 --- a/client/renderers/EGL/desktop_rects.c +++ b/client/renderers/EGL/desktop_rects.c @@ -21,7 +21,6 @@ #include "desktop_rects.h" #include "state.h" #include "common/debug.h" -#include "common/KVMFR.h" #include "common/locking.h" #include diff --git a/client/renderers/EGL/egl.c b/client/renderers/EGL/egl.c index e27aca94..474d7aa5 100644 --- a/client/renderers/EGL/egl.c +++ b/client/renderers/EGL/egl.c @@ -23,7 +23,6 @@ #include "common/util.h" #include "common/array.h" #include "common/debug.h" -#include "common/KVMFR.h" #include "common/option.h" #include "common/sysinfo.h" #include "common/rects.h" @@ -57,7 +56,7 @@ #define MAX_BUFFER_AGE 3 #define DESKTOP_DAMAGE_COUNT 4 #define DESKTOP_DAMAGE_MARGIN 1 -#define MAX_ACCUMULATED_DAMAGE ((KVMFR_MAX_DAMAGE_RECTS + MAX_OVERLAY_RECTS + 2) * MAX_BUFFER_AGE) +#define MAX_ACCUMULATED_DAMAGE ((LG_MAX_FRAME_DAMAGE_RECTS + MAX_OVERLAY_RECTS + 2) * MAX_BUFFER_AGE) #define IDX_AGO(counter, i, total) (((counter) + (total) - (i)) % (total)) struct Options @@ -361,6 +360,23 @@ static bool egl_supports(LG_Renderer * renderer, LG_RendererSupport flag) } } +static bool egl_getInterop(LG_Renderer * renderer, + LG_RendererInterop * interop) +{ + struct Inst * this = UPCAST(struct Inst, renderer); + if (!this->display || !this->context || !this->configs || + interop->size < sizeof(*interop)) + return false; + + interop->version = LG_RENDERER_INTEROP_VERSION; + interop->type = LG_RENDERER_INTEROP_EGL; + interop->egl.display = this->display; + interop->egl.config = this->configs; + interop->egl.shareContext = this->context; + interop->egl.dmaBufImport = this->dmaSupport; + return true; +} + static void egl_onRestart(LG_Renderer * renderer) { struct Inst * this = UPCAST(struct Inst, renderer); @@ -598,7 +614,7 @@ static bool egl_onMouseShape(LG_Renderer * renderer, const LG_RendererCursor cur } static void egl_onMouseColorTransform(LG_Renderer * renderer, - const KVMFRColorTransform * transform) + const LGColorTransform * transform) { struct Inst * this = UPCAST(struct Inst, renderer); egl_cursorSetColorTransform(this->cursor, transform); @@ -753,7 +769,7 @@ static bool egl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame, int d if (unlikely( damage->count == -1 || damageRectsCount == 0 || - damage->count + damageRectsCount >= KVMFR_MAX_DAMAGE_RECTS)) + damage->count + damageRectsCount >= LG_MAX_FRAME_DAMAGE_RECTS)) { damage->count = -1; } @@ -1441,7 +1457,7 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, egl_damageRender(this->damage, rotate, newFrame ? desktopDamage : NULL) | invalidateWindow; - struct Rect damage[KVMFR_MAX_DAMAGE_RECTS + MAX_OVERLAY_RECTS + 2]; + struct Rect damage[LG_MAX_FRAME_DAMAGE_RECTS + MAX_OVERLAY_RECTS + 2]; int damageIdx = app_renderOverlay(damage, MAX_OVERLAY_RECTS); if (unlikely(damageIdx != 0)) { @@ -1631,6 +1647,7 @@ struct LG_RendererOps LGR_EGL = .initialize = egl_initialize, .deinitialize = egl_deinitialize, .supports = egl_supports, + .getInterop = egl_getInterop, .onRestart = egl_onRestart, .onResize = egl_onResize, .onMouseShape = egl_onMouseShape, diff --git a/client/renderers/EGL/texture_framebuffer.c b/client/renderers/EGL/texture_framebuffer.c index 84ce4257..eb434b44 100644 --- a/client/renderers/EGL/texture_framebuffer.c +++ b/client/renderers/EGL/texture_framebuffer.c @@ -22,13 +22,12 @@ #include "texture_buffer.h" #include "common/debug.h" -#include "common/KVMFR.h" #include "common/rects.h" struct TexDamage { int count; - FrameDamageRect rects[KVMFR_MAX_DAMAGE_RECTS]; + FrameDamageRect rects[LG_MAX_FRAME_DAMAGE_RECTS]; }; typedef struct TexFB @@ -90,7 +89,7 @@ static bool egl_texFBUpdate(EGL_Texture * texture, const EGL_TexUpdate * update) struct TexDamage * damage = this->damage + parent->bufIndex; bool damageAll = !update->rects || update->rectCount == 0 || damage->count < 0 || - damage->count + update->rectCount > KVMFR_MAX_DAMAGE_RECTS; + damage->count + update->rectCount > LG_MAX_FRAME_DAMAGE_RECTS; bool complete; if (damageAll) @@ -168,7 +167,7 @@ static bool egl_texFBUpdate(EGL_Texture * texture, const EGL_TexUpdate * update) if (i == parent->bufIndex) damage->count = 0; else if (update->rects && update->rectCount > 0 && damage->count >= 0 && - damage->count + update->rectCount <= KVMFR_MAX_DAMAGE_RECTS) + damage->count + update->rectCount <= LG_MAX_FRAME_DAMAGE_RECTS) { memcpy(damage->rects + damage->count, update->rects, update->rectCount * sizeof(FrameDamageRect)); diff --git a/client/src/app.c b/client/src/app.c index f03ffd43..18700f5e 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -1174,27 +1174,27 @@ void app_invalidateOverlay(bool renderTwice) bool app_guestIsLinux(void) { - return g_state.guestOS == KVMFR_OS_LINUX; + return g_state.guestOS == LG_TRANSPORT_OS_LINUX; } bool app_guestIsWindows(void) { - return g_state.guestOS == KVMFR_OS_WINDOWS; + return g_state.guestOS == LG_TRANSPORT_OS_WINDOWS; } bool app_guestIsOSX(void) { - return g_state.guestOS == KVMFR_OS_OSX; + return g_state.guestOS == LG_TRANSPORT_OS_OSX; } bool app_guestIsBSD(void) { - return g_state.guestOS == KVMFR_OS_BSD; + return g_state.guestOS == LG_TRANSPORT_OS_BSD; } bool app_guestIsOther(void) { - return g_state.guestOS == KVMFR_OS_OTHER; + return g_state.guestOS == LG_TRANSPORT_OS_OTHER; } void app_stopVideo(bool stop) diff --git a/client/src/config.c b/client/src/config.c index b0845f13..611dd6d7 100644 --- a/client/src/config.c +++ b/client/src/config.c @@ -47,6 +47,7 @@ static StringList optScancodeValues (struct Option * opt); static bool optScancodeValidate (struct Option * opt, const char ** error); static char * optScancodeToString (struct Option * opt); static bool optRotateValidate (struct Option * opt, const char ** error); +static bool optTransportValidate (struct Option * opt, const char ** error); static bool optMicDefaultParse (struct Option * opt, const char * str); static StringList optMicDefaultValues (struct Option * opt); static char * optMicDefaultToString(struct Option * opt); @@ -87,25 +88,12 @@ static struct Option options[] = .value.x_bool = false, }, { - .module = "app", - .name = "cursorPollInterval", - .description = "How often to check for a cursor update in microseconds", - .type = OPTION_TYPE_INT, - .value.x_int = 1000 - }, - { - .module = "app", - .name = "framePollInterval", - .description = "How often to check for a frame update in microseconds", - .type = OPTION_TYPE_INT, - .value.x_int = 1000 - }, - { - .module = "app", - .name = "allowDMA", - .description = "Allow direct DMA transfers if supported (see `README.md` in the `module` dir)", - .type = OPTION_TYPE_BOOL, - .value.x_bool = true + .module = "app", + .name = "transport", + .description = "Transport backend to use", + .type = OPTION_TYPE_STRING, + .value.x_string = "lgmp", + .validator = optTransportValidate, }, // window options @@ -686,9 +674,7 @@ bool config_load(int argc, char * argv[]) } // setup the application params for the basic types - g_params.cursorPollInterval = option_get_int ("app" , "cursorPollInterval"); - g_params.framePollInterval = option_get_int ("app" , "framePollInterval" ); - g_params.allowDMA = option_get_bool ("app" , "allowDMA" ); + g_params.transport = option_get_string("app", "transport" ); g_params.windowTitle = option_get_string("win", "title" ); g_params.appId = option_get_string("win", "appId" ); @@ -1025,6 +1011,15 @@ static bool optRotateValidate(struct Option * opt, const char ** error) return false; } +static bool optTransportValidate(struct Option * opt, const char ** error) +{ + if (lgTransport_isValid(opt->value.x_string)) + return true; + + *error = "Unknown transport (expected lgmp or test)"; + return false; +} + static bool optMicDefaultParse(struct Option * opt, const char * str) { if (!str) diff --git a/client/src/core.c b/client/src/core.c index e19cbe03..582c018c 100644 --- a/client/src/core.c +++ b/client/src/core.c @@ -192,29 +192,30 @@ bool core_warpPointer(int x, int y, bool exiting) void core_onWindowSizeChanged(unsigned width, unsigned height) { - if (!g_state.pointerQueue) + if (!g_state.transport || + !(g_state.transportFeatures & LG_TRANSPORT_FEATURE_WINDOW_SIZE)) return; if (g_state.srcSize.x == width && g_state.srcSize.y == height) return; - const KVMFRWindowSize msg = + const LG_TransportControl control = { - .msg.type = KVMFR_MESSAGE_WINDOWSIZE, - .w = width, - .h = height + .type = LG_TRANSPORT_CONTROL_WINDOW_SIZE, + .windowSize = { .width = width, .height = height }, }; - uint32_t serial; - LGMP_STATUS status; - if ((status = lgmpClientSendData(g_state.pointerQueue, - &msg, sizeof(msg), &serial)) != LGMP_OK) - DEBUG_WARN("Message send failed: %s", lgmpStatusString(status)); + LG_TransportControlToken token; + const LG_TransportStatus status = g_state.transportOps->sendControl( + g_state.transport, &control, &token); + if (status != LG_TRANSPORT_OK && status != LG_TRANSPORT_UNAVAILABLE) + DEBUG_WARN("Window-size control failed with status %d", status); } void core_updatePositionInfo(void) { - if (g_params.setGuestRes && g_state.kvmfrFeatures & KVMFR_FEATURE_WINDOWSIZE) + if (g_params.setGuestRes && + g_state.transportFeatures & LG_TRANSPORT_FEATURE_WINDOW_SIZE) { LGMsg msg = { @@ -535,20 +536,19 @@ void core_handleMouseNormal(double ex, double ey) util_localCurToGuest(&guest); if (!g_state.stopVideo && - g_state.kvmfrFeatures & KVMFR_FEATURE_SETCURSORPOS) + g_state.transportFeatures & LG_TRANSPORT_FEATURE_SET_CURSOR_POS) { - const KVMFRSetCursorPos msg = { - .msg.type = KVMFR_MESSAGE_SETCURSORPOS, - .x = round(guest.x), - .y = round(guest.y) + const LG_TransportControl control = { + .type = LG_TRANSPORT_CONTROL_SET_CURSOR_POS, + .cursorPos = { .x = round(guest.x), .y = round(guest.y) }, }; - uint32_t setPosSerial; - LGMP_STATUS status; - if ((status = lgmpClientSendData(g_state.pointerQueue, - &msg, sizeof(msg), &setPosSerial)) != LGMP_OK) + LG_TransportControlToken token; + LG_TransportStatus status = g_state.transportOps->sendControl( + g_state.transport, &control, &token); + if (status != LG_TRANSPORT_OK) { - DEBUG_WARN("Message send failed: %s", lgmpStatusString(status)); + DEBUG_WARN("Cursor-position control failed with status %d", status); goto fallback; } else @@ -558,32 +558,24 @@ void core_handleMouseNormal(double ex, double ey) unsigned timeout = 200; do { - LG_LOCK(g_state.pointerQueueLock); - if (!g_state.pointerQueue) + status = g_state.transportOps->controlStatus(g_state.transport, + token); + if (status == LG_TRANSPORT_DISCONNECTED) { - /* the queue is nolonger valid, assume complete */ g_cursor.realigning = false; - LG_UNLOCK(g_state.pointerQueueLock); break; } - - uint32_t hostSerial; - if (lgmpClientGetSerial(g_state.pointerQueue, &hostSerial) != LGMP_OK) + if (status == LG_TRANSPORT_OK) + break; + if (status != LG_TRANSPORT_UNAVAILABLE) { g_cursor.realigning = false; - LG_UNLOCK(g_state.pointerQueueLock); return; } - LG_UNLOCK(g_state.pointerQueueLock); - - if (hostSerial >= setPosSerial) - break; if (--timeout == 0) { - DEBUG_ERROR( - "pointerQueue serial not updating, expected %u but stuck at %u", - setPosSerial, hostSerial); + DEBUG_ERROR("Cursor-position control was not acknowledged"); break; } @@ -591,8 +583,8 @@ void core_handleMouseNormal(double ex, double ey) } while(app_isRunning()); - g_cursor.guest.x = msg.x; - g_cursor.guest.y = msg.y; + g_cursor.guest.x = control.cursorPos.x; + g_cursor.guest.y = control.cursorPos.y; g_cursor.realign = false; g_cursor.realigning = false; g_cursor.redraw = true; diff --git a/client/src/keybind.c b/client/src/keybind.c index 6530a5ac..79e56ceb 100644 --- a/client/src/keybind.c +++ b/client/src/keybind.c @@ -128,7 +128,7 @@ static void bind_toggleKey(int sc, void * opaque) static void bind_setGuestRes(int sc, void * opaque) { - if (!(g_state.kvmfrFeatures & KVMFR_FEATURE_WINDOWSIZE)) + if (!(g_state.transportFeatures & LG_TRANSPORT_FEATURE_WINDOW_SIZE)) { app_alert(LG_ALERT_INFO, "The guest doesn't support this feature"); return; diff --git a/client/src/main.c b/client/src/main.c index f5e80482..061728ce 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -40,12 +40,10 @@ #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" @@ -349,6 +347,9 @@ static int renderThread(void * unused) 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); @@ -356,204 +357,109 @@ static int renderThread(void * unused) return 0; } + int main_cursorThread(void * unused) { - LGMP_STATUS status; - LG_RendererCursor cursorType = LG_CURSOR_COLOR; - KVMFRCursor * cursor = NULL; - int cursorSize = 0; + LG_RendererCursor cursorType = LG_CURSOR_COLOR; lgWaitEvent(e_startup, TIMEOUT_INFINITE); // subscribe to the pointer queue - while(app_getState() == 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)); - app_setState(APP_STATE_SHUTDOWN); - break; - } - while(app_getState() == APP_STATE_RUNNING && !g_state.stopVideo) { - LGMPMessage msg; - if ((status = lgmpClientProcess(g_state.pointerQueue, &msg)) != LGMP_OK) + LG_TransportPointer pointer; + const LG_TransportStatus status = g_state.transportOps->nextPointer( + g_state.transport, &pointer); + if (status != LG_TRANSPORT_OK) { - if (status == LGMP_ERR_QUEUE_EMPTY) + 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 - ); - + 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) - app_setState(APP_STATE_RESTART); - else - { - DEBUG_ERROR("lgmpClientProcess Failed: %s", lgmpStatusString(status)); - app_setState(APP_STATE_SHUTDOWN); - } + 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; } - KVMFRCursor * tmp = (KVMFRCursor *)msg.mem; - const int shapeSize = msg.udata & CURSOR_FLAG_SHAPE ? - tmp->height * tmp->pitch : 0; - const int neededSize = sizeof(*tmp) + shapeSize + - (msg.udata & CURSOR_FLAG_COLOR_TRANSFORM ? - sizeof(KVMFRColorTransform) : 0); + if (pointer.flags & LG_TRANSPORT_POINTER_VISIBLE_VALID) + g_cursor.guest.visible = + pointer.flags & LG_TRANSPORT_POINTER_VISIBLE; - if (cursor && neededSize > cursorSize) + if (pointer.flags & LG_TRANSPORT_POINTER_SHAPE) { - free(cursor); - cursor = NULL; - } - - /* copy and release the message ASAP */ - if (!cursor) - { - cursor = malloc(neededSize); - if (!cursor) - { - DEBUG_ERROR("failed to allocate %d bytes for cursor", neededSize); - app_setState(APP_STATE_SHUTDOWN); - break; - } - cursorSize = neededSize; - } - - memcpy(cursor, msg.mem, neededSize); - lgmpClientMessageDone(g_state.pointerQueue); - - if (msg.udata & CURSOR_FLAG_VISIBLE_VALID) - g_cursor.guest.visible = msg.udata & CURSOR_FLAG_VISIBLE; - - if (msg.udata & CURSOR_FLAG_SHAPE) - { - switch(cursor->type) + 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 = 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) - ) + 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 ((msg.udata & CURSOR_FLAG_COLOR_TRANSFORM) && + if ((pointer.flags & LG_TRANSPORT_POINTER_COLOR_TRANSFORM) && g_state.lgr->ops.onMouseColorTransform) - { - const KVMFRColorTransform * transform = - (const KVMFRColorTransform *)((const uint8_t *)(cursor + 1) + - shapeSize); - g_state.lgr->ops.onMouseColorTransform(g_state.lgr, transform); - } + g_state.lgr->ops.onMouseColorTransform(g_state.lgr, + pointer.colorTransform); - if ((msg.udata & CURSOR_FLAG_VISIBLE_VALID) && - cursor->sdrWhiteLevel && g_state.lgr->ops.onMouseWhiteLevel) - g_state.lgr->ops.onMouseWhiteLevel( - g_state.lgr, cursor->sdrWhiteLevel); + 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 (msg.udata & CURSOR_FLAG_POSITION) + if (pointer.flags & LG_TRANSPORT_POINTER_POSITION) { - bool valid = g_cursor.guest.valid; - g_cursor.guest.x = cursor->x; - g_cursor.guest.y = cursor->y; + 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 the state just became valid - if (valid != true && core_inputEnabled()) + if (!wasValid && core_inputEnabled()) { core_alignToGuest(); app_resyncMouseBasic(); } - - // tell the DS there was an update 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 - ); + g_cursor.guest.x, g_cursor.guest.y, + g_cursor.guest.hx, g_cursor.guest.hy); if ((g_params.mouseRedraw || - (msg.udata & CURSOR_FLAG_COLOR_TRANSFORM)) && + (pointer.flags & LG_TRANSPORT_POINTER_COLOR_TRANSFORM)) && g_cursor.guest.visible && !g_state.stopVideo) lgSignalEvent(g_state.frameEvent); - } - LG_LOCK(g_state.pointerQueueLock); - lgmpClientUnsubscribe(&g_state.pointerQueue); - LG_UNLOCK(g_state.pointerQueueLock); - - if (cursor) - { - free(cursor); - cursor = NULL; + g_state.transportOps->releasePointer(g_state.transport, &pointer); } return 0; @@ -561,22 +467,10 @@ int main_cursorThread(void * unused) int main_frameThread(void * unused) { - struct DMAFrameInfo - { - KVMFRFrame * frame; - size_t dataSize; - int fd; - }; + uint64_t frameSerial = 0; + uint32_t formatVersion = 0; + LG_RendererFormat rendererFormat; - 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"); @@ -584,313 +478,197 @@ int main_frameThread(void * unused) if (app_getState() != APP_STATE_RUNNING) return 0; - // subscribe to the frame queue - while(app_getState() == 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)); - app_setState(APP_STATE_SHUTDOWN); - break; - } - while(app_getState() == APP_STATE_RUNNING && !g_state.stopVideo) { - LGMPMessage msg; - if ((status = lgmpClientProcess(queue, &msg)) != LGMP_OK) + 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 == 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; - } - + if (status == LG_TRANSPORT_TIMEOUT || status == LG_TRANSPORT_UNAVAILABLE) continue; - } - - if (status == LGMP_ERR_INVALID_SESSION) + if (status == LG_TRANSPORT_DISCONNECTED) app_setState(APP_STATE_RESTART); + else if (status == LG_TRANSPORT_END) + app_setState(APP_STATE_SHUTDOWN); else { - DEBUG_ERROR("lgmpClientProcess Failed: %s", lgmpStatusString(status)); + DEBUG_ERROR("Frame transport failed with status %d", status); app_setState(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) + if (frame.serial == frameSerial && g_state.formatValid) { - lgmpClientMessageDone(queue); + g_state.transportOps->releaseFrame(g_state.transport, &frame); continue; } - frameSerial = frame->frameSerial; + frameSerial = frame.serial; - struct DMAFrameInfo *dma = NULL; - - if (!g_state.formatValid || frame->formatVer != formatVer) + const LG_TransportFrameFormat * format = frame.format; + if (!format) { - // setup the renderer format with the frame format details - lgrFormat.type = frame->type; - lgrFormat.screenWidth = frame->screenWidth; - lgrFormat.screenHeight = frame->screenHeight; - lgrFormat.dataWidth = frame->dataWidth; - lgrFormat.dataHeight = frame->dataHeight; - lgrFormat.frameWidth = frame->frameWidth; - lgrFormat.frameHeight = frame->frameHeight; - lgrFormat.stride = frame->stride; - lgrFormat.pitch = frame->pitch; - lgrFormat.hdr = frame->flags & FRAME_FLAG_HDR; - lgrFormat.hdrPQ = frame->flags & FRAME_FLAG_HDR_PQ; - lgrFormat.hdrMetadata = frame->flags & FRAME_FLAG_HDR_METADATA; - lgrFormat.sdrWhiteLevel = frame->sdrWhiteLevel ? - frame->sdrWhiteLevel : KVMFR_SDR_WHITE_LEVEL_DEFAULT; + 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 (lgrFormat.hdrMetadata) + if (rendererFormat.hdrMetadata) { - memcpy(lgrFormat.hdrDisplayPrimary, frame->hdrDisplayPrimary, sizeof(lgrFormat.hdrDisplayPrimary)); - memcpy(lgrFormat.hdrWhitePoint , frame->hdrWhitePoint , sizeof(lgrFormat.hdrWhitePoint )); - lgrFormat.hdrMaxDisplayLuminance = frame->hdrMaxDisplayLuminance; - lgrFormat.hdrMinDisplayLuminance = frame->hdrMinDisplayLuminance; - lgrFormat.hdrMaxContentLightLevel = frame->hdrMaxContentLightLevel; - lgrFormat.hdrMaxFrameAverageLightLevel = frame->hdrMaxFrameAverageLightLevel; - } - else - { - memset(lgrFormat.hdrDisplayPrimary, 0, sizeof(lgrFormat.hdrDisplayPrimary)); - memset(lgrFormat.hdrWhitePoint , 0, sizeof(lgrFormat.hdrWhitePoint )); - lgrFormat.hdrMaxDisplayLuminance = 0; - lgrFormat.hdrMinDisplayLuminance = 0; - lgrFormat.hdrMaxContentLightLevel = 0; - lgrFormat.hdrMaxFrameAverageLightLevel = 0; + 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 & FRAME_FLAG_TRUNCATED) + if (frame.flags & LG_TRANSPORT_FRAME_TRUNCATED) { - const float needed = - ((frame->screenHeight * 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_WARN("Transport buffer too small, screen truncated"); DEBUG_BREAK(); - - app_msgBox( - "IVSHMEM too small", - "IVSHMEM too small\n" - "Please increase the size to %d MiB", - size); + app_msgBox("Transport buffer too small", + "The transport buffer is too small for this frame."); } - switch(frame->rotation) + switch (format->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; - + 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/invalid frame rotation"); - lgrFormat.rotate = LG_ROTATE_0; + DEBUG_ERROR("Unsupported frame rotation"); + rendererFormat.rotate = LG_ROTATE_0; break; } - g_state.rotate = lgrFormat.rotate; + g_state.rotate = rendererFormat.rotate; - dataSize = lgrFormat.dataHeight * lgrFormat.pitch; - - bool error = false; - switch(frame->type) + bool invalid = false; + switch (format->type) { case FRAME_TYPE_RGBA: case FRAME_TYPE_BGRA: case FRAME_TYPE_RGBA10: - lgrFormat.bpp = 32; + rendererFormat.bpp = 32; break; - case FRAME_TYPE_RGBA16F: - lgrFormat.bpp = 64; + rendererFormat.bpp = 64; break; - case FRAME_TYPE_BGR_32: case FRAME_TYPE_RGB_24: - lgrFormat.bpp = 24; + rendererFormat.bpp = 24; break; - default: - DEBUG_ERROR("Unsupported frameType"); - error = true; + invalid = true; break; } - if (error) + if (invalid) { - lgmpClientMessageDone(queue); + DEBUG_ERROR("Unsupported frame type"); + g_state.transportOps->releaseFrame(g_state.transport, &frame); app_setState(APP_STATE_SHUTDOWN); break; } g_state.formatValid = true; - formatVer = frame->formatVer; - + formatVersion = format->version; DEBUG_INFO("Format: %s %ux%u (%ux%u) stride:%u pitch:%u rotation:%d hdr:%d pq:%d sdrWhite:%u nits", - FrameTypeStr[frame->type], - frame->frameWidth, frame->frameHeight, - frame->dataWidth , frame->dataHeight , - frame->stride, frame->pitch, - frame->rotation, - frame->flags & FRAME_FLAG_HDR ? 1 : 0, - frame->flags & FRAME_FLAG_HDR_PQ ? 1 : 0, - lgrFormat.sdrWhiteLevel); + 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, lgrFormat)) + if (!RENDERER(onFrameFormat, rendererFormat)) { - DEBUG_ERROR("renderer failed to configure format"); - app_setState(APP_STATE_SHUTDOWN); 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; } - - // Renderers which expose HDR surface capabilities must match the wire - // encoding. Legacy renderers do not advertise this interface, so retain - // their existing display-server behaviour rather than silently forcing - // them through a conversion path they may not implement. - const bool rendererSupportsNativeHDR = !lgrFormat.hdr || + const bool rendererSupportsNativeHDR = !rendererFormat.hdr || !g_state.lgr->ops.supports || RENDERER(supports, - lgrFormat.hdrPQ ? LG_SUPPORTS_HDR_PQ : LG_SUPPORTS_HDR_SCRGB); + 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(lgrFormat, rendererSupportsNativeHDR); + renderQueue_surfaceFormat(rendererFormat, rendererSupportsNativeHDR); LG_UNLOCK(g_state.lgrLock); - g_state.srcSize.x = lgrFormat.screenWidth; - g_state.srcSize.y = lgrFormat.screenHeight; + g_state.srcSize.x = rendererFormat.screenWidth; + g_state.srcSize.y = rendererFormat.screenHeight; g_state.haveSrcSize = true; if (g_params.autoResize) - g_state.ds->setWindowSize(lgrFormat.frameWidth, lgrFormat.frameHeight); - + g_state.ds->setWindowSize(rendererFormat.frameWidth, + rendererFormat.frameHeight); core_updatePositionInfo(); } - if (g_state.useDMA) + 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) { - /* 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; - } - } - - if (!dma) - { - DEBUG_ERROR("Failed to obtain a free DMA buffer for use"); - app_setState(APP_STATE_SHUTDOWN); - 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 + sizeof(FrameBuffer); - - 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"); - app_setState(APP_STATE_SHUTDOWN); - break; - } - } + 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; } - - uint32_t damageRectsCount = - frame->damageRectsCount <= KVMFR_MAX_DAMAGE_RECTS ? - frame->damageRectsCount : 0; - bool invalidDamage = damageRectsCount != frame->damageRectsCount; - for (uint32_t i = 0; !invalidDamage && i < damageRectsCount; ++i) - { - const FrameDamageRect * rect = frame->damageRects + i; - invalidDamage = - rect->x > frame->frameWidth || - rect->y > frame->frameHeight || - rect->width > frame->frameWidth - rect->x || - rect->height > frame->frameHeight - rect->y; - } - if (unlikely(invalidDamage)) + if (invalidDamage) { DEBUG_WARN("Invalid damage rectangles, forcing a full update"); - damageRectsCount = 0; + damageCount = 0; } - FrameBuffer * fb = (FrameBuffer *)(((uint8_t*)frame) + frame->offset); - if (!RENDERER(onFrame, fb, g_state.useDMA ? dma->fd : -1, - frame->damageRects, damageRectsCount)) + if (!RENDERER(onFrame, frame.framebuffer, frame.dmaFD, + frame.damageRects, damageCount)) { - lgmpClientMessageDone(queue); - DEBUG_ERROR("renderer on frame returned failure"); + g_state.transportOps->releaseFrame(g_state.transport, &frame); + DEBUG_ERROR("Renderer onFrame returned failure"); app_setState(APP_STATE_SHUTDOWN); break; } overlaySplash_show(false); - - if (frame->flags & FRAME_FLAG_REQUEST_ACTIVATION && + if ((frame.flags & LG_TRANSPORT_FRAME_REQUEST_ACTIVATION) && g_params.requestActivation) g_state.ds->requestActivation(); - const bool blockScreensaver = frame->flags & FRAME_FLAG_BLOCK_SCREENSAVER; - if (g_params.autoScreensaver && g_state.autoIdleInhibitState != blockScreensaver) + const bool blockScreensaver = + frame.flags & LG_TRANSPORT_FRAME_BLOCK_SCREENSAVER; + if (g_params.autoScreensaver && + g_state.autoIdleInhibitState != blockScreensaver) { if (blockScreensaver) g_state.ds->inhibitIdle(); @@ -899,10 +677,9 @@ int main_frameThread(void * unused) g_state.autoIdleInhibitState = blockScreensaver; } - const uint64_t t = nanotime(); - const uint64_t delta = t - g_state.lastFrameTime; - g_state.lastFrameTime = t; - + const uint64_t now = nanotime(); + const uint64_t delta = now - g_state.lastFrameTime; + g_state.lastFrameTime = now; if (g_state.lastFrameTimeValid) ringbuffer_push(g_state.uploadTimings, &(float) { delta * 1e-6f }); g_state.lastFrameTimeValid = true; @@ -917,29 +694,15 @@ int main_frameThread(void * unused) else lgSignalEvent(g_state.frameEvent); - lgmpClientMessageDone(queue); - - // switch over to the LG stream + g_state.transportOps->releaseFrame(g_state.transport, &frame); app_useSpiceDisplay(false); } - lgmpClientUnsubscribe(&queue); - RENDERER(onRestart); if (app_getState() != APP_STATE_SHUTDOWN) - { if (!app_useSpiceDisplay(true)) overlaySplash_show(true); - } - - if (g_state.useDMA) - { - for(int i = 0; i < ARRAY_LENGTH(dmaInfo); ++i) - if (dmaInfo[i].fd >= 0) - close(dmaInfo[i].fd); - } - return 0; } @@ -1302,21 +1065,19 @@ static MsgBoxHandle showSpiceInputHelp(void) "through SPICE if you press the capture key."); } -struct LGMPClientSessionProbe +struct TransportSessionProbe { - PLGMPClient client; - uint32_t udataSize; - uint8_t * udata; - uint32_t remoteVersion; - LGMP_STATUS status; + LG_Transport * transport; + const LG_TransportOps * ops; + LG_TransportSession session; + LG_TransportStatus status; atomic_bool done; }; -static int lgmpClientSessionProbe(void * opaque) +static int transportSessionProbe(void * opaque) { - struct LGMPClientSessionProbe * probe = opaque; - probe->status = lgmpClientSessionInit(probe->client, &probe->udataSize, - &probe->udata, NULL, &probe->remoteVersion); + struct TransportSessionProbe * probe = opaque; + probe->status = probe->ops->connect(probe->transport, &probe->session); atomic_store_explicit(&probe->done, true, memory_order_release); return 0; } @@ -1365,7 +1126,7 @@ static int lg_run(void) overlayGraph_register("RENDER", g_state.renderDuration, 0.0f, 10.0f, NULL); // unknown guest OS at this time - g_state.guestOS = KVMFR_OS_OTHER; + g_state.guestOS = LG_TRANSPORT_OS_OTHER; // search for the best displayserver ops to use for(int i = 0; i < LG_DISPLAYSERVER_COUNT; ++i) @@ -1408,12 +1169,13 @@ static int lg_run(void) signal(SIGINT , intHandler); signal(SIGTERM, intHandler); - // try map the shared memory - if (!ivshmemOpen(&g_state.shm)) + if (!lgTransport_create(g_params.transport, &g_state.transport, + &g_state.transportOps)) { - DEBUG_ERROR("Failed to map memory"); + 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))) @@ -1495,9 +1257,7 @@ static int lg_run(void) return -1; } - g_state.useDMA = - g_params.allowDMA && - ivshmemHasDMA(&g_state.shm); + g_state.useDMA = g_state.transportOps->supportsDMA(g_state.transport); // initialize the window dimensions at init for renderers g_state.windowW = g_params.w; @@ -1570,43 +1330,35 @@ static int lg_run(void) // 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(); - LGMP_STATUS status; - - while(app_getState() == 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 = 0; - uint32_t remoteVersion = 0; - KVMFR * udata = NULL; - bool sessionReady = false; - int waitCount = 0; - + int waitCount = 0; + LG_TransportSession session; MsgBoxHandle msgs[10]; int msgsCount; restart: - udataSize = 0; - remoteVersion = 0; - udata = NULL; - sessionReady = false; msgsCount = 0; memset(msgs, 0, sizeof(msgs)); @@ -1616,23 +1368,20 @@ restart: { if (initialSpiceEnable && microtime() > initialSpiceEnable) { - /* use the spice display until we get frames from the LG host application - * it is safe to call this before connect as it will be delayed until - * spiceReady is called */ app_useSpiceDisplay(true); initialSpiceEnable = 0; } - struct LGMPClientSessionProbe probe = - { - .client = g_state.lgmp, - .done = false, + struct TransportSessionProbe probe = { + .transport = g_state.transport, + .ops = g_state.transportOps, + .done = false, }; LGThread * probeThread; - if (!lgCreateThread("lgmpSession", lgmpClientSessionProbe, &probe, + if (!lgCreateThread("transportSession", transportSessionProbe, &probe, &probeThread)) { - DEBUG_ERROR("Failed to create LGMP session probe thread"); + DEBUG_ERROR("Failed to create transport session probe thread"); return -1; } @@ -1642,315 +1391,136 @@ restart: if (!lgJoinThread(probeThread, NULL)) { - DEBUG_ERROR("Failed to join LGMP session probe thread"); + DEBUG_ERROR("Failed to join transport session probe thread"); return -1; } if (app_getState() != APP_STATE_RUNNING) return -1; - status = probe.status; - udataSize = probe.udataSize; - udata = (KVMFR *)probe.udata; - remoteVersion = probe.remoteVersion; - switch(status) + if (probe.status == LG_TRANSPORT_OK) { - case LGMP_OK: - initialSpiceEnable = 0; - break; - - case LGMP_ERR_INVALID_VERSION: - { - if (waitCount++ == 0) - { - reportBadVersion(); - msgs[msgsCount++] = app_msgBox( - "Incompatible LGMP Version", - "The host application is not compatible with this client.\n" - "Please download and install the matching version." - ); - - DEBUG_INFO("LGMP host:%u client:%u", - remoteVersion, LGMP_PROTOCOL_VERSION); - DEBUG_INFO("Waiting for you to upgrade the host application"); - } - - g_state.ds->wait(1000); - continue; - } - - case LGMP_ERR_INVALID_SESSION: - case LGMP_ERR_INVALID_MAGIC: - { - 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(); - if (!g_params.disableWaitingMessage) - { - msgs[msgsCount++] = app_msgBox( - "Host Application Not Running", - "It seems the host application is not running or your\n" - "virtual machine is still starting up\n" - "\n" - "If the the VM is running and booted please check the\n" - "host application log for errors. You can find the\n" - "log through the shortcut in your start menu\n" - "\n" - "Continuing to wait..."); - - msgs[msgsCount++] = showSpiceInputHelp(); - } - - 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); - continue; - } - - default: - DEBUG_ERROR("lgmpClientSessionInit Failed: %s", lgmpStatusString(status)); - return -1; + session = probe.session; + initialSpiceEnable = 0; + break; } - if (app_getState() != APP_STATE_RUNNING) - return -1; - - // dont show warnings again after the first successful startup - waitCount = 100; - - const bool headerValid = udata && udataSize >= sizeof(*udata); - const bool magicMatches = headerValid && - memcmp(udata->magic, KVMFR_MAGIC, sizeof(udata->magic)) == 0; - if (!headerValid || !magicMatches || udata->version != KVMFR_VERSION) + if (probe.status == LG_TRANSPORT_INVALID_VERSION) { - static bool alertsDone = false; - if (alertsDone) - { - if (app_getState() == APP_STATE_RUNNING) - g_state.ds->wait(1000); - continue; - } - - reportBadVersion(); - if (magicMatches) + if (waitCount++ == 0) { + reportBadVersion(); msgs[msgsCount++] = app_msgBox( - "Incompatible KVMFR Version", - "The host application is not compatible with this client.\n" - "Please download and install the matching version.\n" - "\n" - "Client Version: %s\n" - "Host Version: %s", - BUILD_VERSION, - udata->version >= 2 ? udata->hostver : NULL - ); - - 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); + "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); } - else - DEBUG_ERROR("Invalid KVMFR magic"); - - DEBUG_BREAK(); - - msgs[msgsCount++] = showSpiceInputHelp(); - - DEBUG_INFO("Waiting for you to upgrade the host application"); - - alertsDone = true; - if (app_getState() == APP_STATE_RUNNING) - g_state.ds->wait(1000); + g_state.ds->wait(1000); continue; } - sessionReady = true; - break; + 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 (!sessionReady || app_getState() != APP_STATE_RUNNING) + if (app_getState() != APP_STATE_RUNNING) return -1; - /* close any informational message boxes from above as we now connected - * successfully */ - - for(int i = 0; i < msgsCount; ++i) + waitCount = 100; + for (int i = 0; i < msgsCount; ++i) if (msgs[i]) app_msgBoxClose(msgs[i]); DEBUG_INFO("Guest Information:"); - DEBUG_INFO("Version : %s", udata->hostver); + 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); - /* parse the kvmfr records from the userdata */ - udataSize -= sizeof(*udata); - uint8_t * p = (uint8_t *)(udata + 1); - while(udataSize >= sizeof(KVMFRRecord)) + if (session.cpuModel[0] && session.cpus && session.cores) { - KVMFRRecord * record = (KVMFRRecord *)p; - p += sizeof(*record); - udataSize -= sizeof(*record); - if (record->size > udataSize) + char hostModel[1024] = {}; + int hostProcs = 0; + int hostCores = 0; + int hostSockets = 0; + if (cpuInfo_get(hostModel, sizeof(hostModel), &hostProcs, &hostCores, + &hostSockets)) { - DEBUG_WARN("KVMFRecord size is invalid, aborting parsing."); - break; - } - - switch(record->type) - { - case KVMFR_RECORD_VMINFO: + if (hostProcs > hostCores && session.cpus <= session.cores) { - 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 hostHasSMP = false; - bool guestHasSMP = false; - bool guestCPUPt = false; - bool hostIsAMD = false; - { - char model[1024]; - int procs; - int cores; - int sockets; - if (cpuInfo_get(model, sizeof model, &procs, &cores, &sockets) && - procs > cores) - hostHasSMP = true; - - if (vmInfo->cpus > vmInfo->cores) - guestHasSMP = true; - - if (strncmp(model, "AMD ", 4) == 0) - hostIsAMD = true; - - if (strcmp(vmInfo->model, model) == 0) - guestCPUPt = true; - } - - if (hostHasSMP && !guestHasSMP) - { - DEBUG_BREAK(); - DEBUG_WARN( - "I have detected you have a CPU with hyperthreads but your guest" - " is not aware of this"); - DEBUG_WARN( - "This will result in a degredation of latency sensitive tasks" - " including the use of Looking Glass"); - - if (hostIsAMD) - DEBUG_WARN("As you have an AMD CPU, please be sure you have enabled" - " the `topoext` cpu feature flag for your virtual machine"); - DEBUG_BREAK(); - } - - if (!guestCPUPt) - { - DEBUG_BREAK(); - DEBUG_WARN( - "Your guest is unaware of the acceleration features your CPU has"); - DEBUG_WARN( - "Please be sure to set your CPU type to `host-passthrough` in" - " your VM configuration"); - DEBUG_BREAK(); - } - - 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; + 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(); } - case KVMFR_RECORD_OSINFO: + if (strcmp(session.cpuModel, hostModel) != 0) { - 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); - - g_state.guestOS = osInfo->os; - - if (atomic_load_explicit(&g_state.spiceReady, memory_order_acquire) && - g_params.useSpiceInput) - keybind_spiceRegister(); - break; + 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(); } - - default: - DEBUG_WARN("Unhandled KVMFRecord type: %d", record->type); - break; } - - p += record->size; - udataSize -= record->size; } + 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); - if (app_getState() == APP_STATE_RUNNING) - { - DEBUG_INFO("Starting session"); - atomic_store_explicit( - &g_state.lgHostConnected, true, memory_order_release); - } + g_state.lgHostConnected = true; - g_state.kvmfrFeatures = udata->features; - - LG_LOCK_INIT(g_state.pointerQueueLock); if (!core_startCursorThread() || !core_startFrameThread()) - { - LG_LOCK_FREE(g_state.pointerQueueLock); return -1; - } while(likely(app_getState() == APP_STATE_RUNNING)) { - if (unlikely(!lgmpClientSessionValid(g_state.lgmp))) + if (unlikely(!g_state.transportOps->sessionValid(g_state.transport))) { atomic_store_explicit( &g_state.lgHostConnected, false, memory_order_release); @@ -1969,13 +1539,13 @@ restart: core_stopFrameThread(); core_stopCursorThread(); + g_state.transportOps->disconnect(g_state.transport); app_setState(APP_STATE_RUNNING); lgInit(); goto restart; } - LG_LOCK_FREE(g_state.pointerQueueLock); return 0; } @@ -1995,7 +1565,8 @@ static void lg_shutdown(void) lgJoinThread(t_render, NULL); } - lgmpClientFree(&g_state.lgmp); + if (g_state.transportOps) + g_state.transportOps->destroy(&g_state.transport); if (g_state.frameEvent) { @@ -2030,8 +1601,6 @@ static void lg_shutdown(void) if (g_state.ds && g_state.dsInitialized) g_state.ds->free(); - ivshmemClose(&g_state.shm); - renderQueue_free(); // free metrics ringbuffers @@ -2071,7 +1640,7 @@ int main(int argc, char * argv[]) lgPathsInit("looking-glass"); config_init(); - ivshmemOptionsInit(); + lgTransport_setup(); egl_dynProcsInit(); gl_dynProcsInit(); diff --git a/client/src/main.h b/client/src/main.h index 40b65da7..dcfc4d55 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -28,16 +28,14 @@ #include "common/thread.h" #include "common/types.h" -#include "common/ivshmem.h" #include "common/locking.h" #include "common/ringbuffer.h" #include "common/event.h" #include "common/ll.h" #include -#include - #include "cimgui.h" +#include "interface/transport.h" enum RunState { @@ -91,7 +89,7 @@ struct AppState uint8_t guestUUID[16]; bool guestUUIDValid; - KVMFROS guestOS; + LG_TransportGuestOS guestOS; atomic_bool lgHostConnected; @@ -133,11 +131,9 @@ struct AppState size_t cbXfer; struct ll * cbRequestList; - struct IVSHMEM shm; - PLGMPClient lgmp; - PLGMPClientQueue pointerQueue; - LG_Lock pointerQueueLock; - KVMFRFeatureFlags kvmfrFeatures; + LG_Transport * transport; + const LG_TransportOps * transportOps; + LG_TransportFeatureFlags transportFeatures; LGThread * cursorThread; LGThread * frameThread; @@ -223,9 +219,7 @@ struct AppParams bool requestActivation; bool disableWaitingMessage; - unsigned int cursorPollInterval; - unsigned int framePollInterval; - bool allowDMA; + const char * transport; bool forceRenderer; unsigned int forceRendererIndex; diff --git a/client/src/transport.c b/client/src/transport.c new file mode 100644 index 00000000..38a9f472 --- /dev/null +++ b/client/src/transport.c @@ -0,0 +1,59 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "interface/transport.h" +#include "dynamic/transports.h" + +#include + +void lgTransport_setup(void) +{ + for (unsigned i = 0; i < LG_TRANSPORT_COUNT; ++i) + if (LG_Transports[i]->setup) + LG_Transports[i]->setup(); +} + +bool lgTransport_isValid(const char * name) +{ + if (!name) + return false; + for (unsigned i = 0; i < LG_TRANSPORT_COUNT; ++i) + if (strcmp(LG_Transports[i]->name, name) == 0) + return true; + return false; +} + +bool lgTransport_create(const char * name, LG_Transport ** transport, + const LG_TransportOps ** ops) +{ + for (unsigned i = 0; i < LG_TRANSPORT_COUNT; ++i) + { + if (strcmp(LG_Transports[i]->name, name) != 0) + continue; + + if (!LG_Transports[i]->create(transport)) + return false; + + *ops = LG_Transports[i]; + return true; + } + + return false; +} diff --git a/client/transports/CMakeLists.txt b/client/transports/CMakeLists.txt new file mode 100644 index 00000000..1a40eb32 --- /dev/null +++ b/client/transports/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.10) +project(transports LANGUAGES C) + +set(TRANSPORT_H "${CMAKE_BINARY_DIR}/include/dynamic/transports.h") +set(TRANSPORT_C "${CMAKE_BINARY_DIR}/src/transports.c") + +file(WRITE ${TRANSPORT_H} "#include \"interface/transport.h\"\n\n") +file(APPEND ${TRANSPORT_H} "extern const LG_TransportOps * LG_Transports[];\n\n") + +file(WRITE ${TRANSPORT_C} "#include \"interface/transport.h\"\n\n") +file(APPEND ${TRANSPORT_C} "#include \n\n") + +set(TRANSPORTS "_") +set(TRANSPORTS_LINK "_") +function(add_transport name) + set(TRANSPORTS "${TRANSPORTS};${name}" PARENT_SCOPE) + set(TRANSPORTS_LINK "${TRANSPORTS_LINK};transport_${name}" PARENT_SCOPE) + add_subdirectory(${name}) +endfunction() + +# Add/remove transports here! +add_transport(LGMP) +if(ENABLE_TEST_TRANSPORT) + add_transport(Test) +endif() + +list(REMOVE_AT TRANSPORTS 0) +list(REMOVE_AT TRANSPORTS_LINK 0) + +list(LENGTH TRANSPORTS TRANSPORT_COUNT) +file(APPEND ${TRANSPORT_H} "#define LG_TRANSPORT_COUNT ${TRANSPORT_COUNT}\n") + +foreach(transport ${TRANSPORTS}) + file(APPEND ${TRANSPORT_C} "extern const LG_TransportOps LGT_${transport};\n") +endforeach() + +file(APPEND ${TRANSPORT_C} "\nconst LG_TransportOps * LG_Transports[] =\n{\n") +foreach(transport ${TRANSPORTS}) + file(APPEND ${TRANSPORT_C} " &LGT_${transport},\n") +endforeach() +file(APPEND ${TRANSPORT_C} " NULL\n};") + +add_library(transports STATIC ${TRANSPORT_C}) +target_link_libraries(transports ${TRANSPORTS_LINK}) diff --git a/client/transports/LGMP/CMakeLists.txt b/client/transports/LGMP/CMakeLists.txt new file mode 100644 index 00000000..b0d97672 --- /dev/null +++ b/client/transports/LGMP/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.10) +project(transport_LGMP LANGUAGES C) + +add_library(transport_LGMP STATIC + lgmp.c +) + +target_link_libraries(transport_LGMP + lg_common + lgmp +) diff --git a/client/transports/LGMP/lgmp.c b/client/transports/LGMP/lgmp.c new file mode 100644 index 00000000..18634fb0 --- /dev/null +++ b/client/transports/LGMP/lgmp.c @@ -0,0 +1,766 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "interface/transport.h" + +#include "common/KVMFR.h" +#include "common/LGMPConfig.h" +#include "common/debug.h" +#include "common/ivshmem.h" +#include "common/locking.h" +#include "common/option.h" +#include "common/stringutils.h" + +#include + +#include +#include +#include +#include +#include + +struct DMAFrameInfo +{ + const KVMFRFrame * frame; + size_t dataSize; + int fd; +}; + +struct LG_Transport +{ + struct IVSHMEM shm; + PLGMPClient client; + PLGMPClientQueue frameQueue; + PLGMPClientQueue pointerQueue; + LG_Lock pointerLock; + + unsigned cursorPollInterval; + unsigned framePollInterval; + bool allowDMA; + bool connected; + bool framePending; + uint32_t frameSerial; + bool formatValid; + LG_TransportFrameFormat format; + + struct DMAFrameInfo dma[LGMP_Q_FRAME_LEN]; + uint8_t * pointerData; + size_t pointerDataSize; +}; + +static bool lgmp_deviceValidator(struct Option * opt, const char ** error) +{ + if (strlen(opt->value.x_string) > 3 && + memcmp(opt->value.x_string, "kvmfr", 5) != 0) + { + struct stat st; + if (stat(opt->value.x_string, &st) != 0) + { + *error = "Invalid path to the shared memory file"; + return false; + } + } + return true; +} + +static StringList lgmp_deviceValues(struct Option * option) +{ + StringList values = stringlist_new(true); + DIR * dir = opendir("/sys/class/kvmfr"); + if (!dir) + return values; + + struct dirent * entry; + while ((entry = readdir(dir))) + { + if (entry->d_name[0] == '.') + continue; + char * name; + alloc_sprintf(&name, "/dev/%s", entry->d_name); + stringlist_push(values, name); + } + closedir(dir); + return values; +} + +static void lgmp_setup(void) +{ + struct stat st; + const char * defaultDevice = stat("/dev/kvmfr0", &st) == 0 ? + "/dev/kvmfr0" : "/dev/shm/looking-glass"; + + static struct Option options[] = + { + { + .module = "lgmp", + .name = "shmDevice", + .shortopt = 'f', + .description = "Shared memory file or KVMFR device path", + .type = OPTION_TYPE_STRING, + .value.x_string = NULL, + .validator = lgmp_deviceValidator, + .getValues = lgmp_deviceValues, + }, + { + .module = "lgmp", + .name = "allowDMA", + .description = "Allow direct DMA transfers when supported", + .type = OPTION_TYPE_BOOL, + .value.x_bool = true, + }, + { + .module = "lgmp", + .name = "framePollInterval", + .description = "Frame queue polling interval in microseconds", + .type = OPTION_TYPE_INT, + .value.x_int = 1000, + }, + { + .module = "lgmp", + .name = "cursorPollInterval", + .description = "Pointer queue polling interval in microseconds", + .type = OPTION_TYPE_INT, + .value.x_int = 1000, + }, + {0} + }; + + options[0].value.x_string = (char *)defaultDevice; + option_register(options); +} + +static bool lgmp_create(LG_Transport ** result) +{ + struct LG_Transport * this = calloc(1, sizeof(*this)); + if (!this) + return false; + + this->allowDMA = option_get_bool("lgmp", "allowDMA"); + const int framePoll = option_get_int("lgmp", "framePollInterval"); + const int cursorPoll = option_get_int("lgmp", "cursorPollInterval"); + if (framePoll < 0 || cursorPoll < 0) + { + DEBUG_ERROR("LGMP polling intervals cannot be negative"); + free(this); + return false; + } + this->framePollInterval = framePoll; + this->cursorPollInterval = cursorPoll; + + for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) + this->dma[i].fd = -1; + + LG_LOCK_INIT(this->pointerLock); + + if (!ivshmemOpenDev(&this->shm, + option_get_string("lgmp", "shmDevice"))) + { + LG_LOCK_FREE(this->pointerLock); + free(this); + return false; + } + + LGMP_STATUS status = lgmpClientInit(this->shm.mem, this->shm.size, + &this->client); + if (status != LGMP_OK) + { + DEBUG_ERROR("lgmpClientInit failed: %s", lgmpStatusString(status)); + ivshmemClose(&this->shm); + LG_LOCK_FREE(this->pointerLock); + free(this); + return false; + } + + *result = this; + return true; +} + +static void lgmp_closeQueues(struct LG_Transport * this) +{ + if (this->framePending && this->frameQueue) + { + lgmpClientMessageDone(this->frameQueue); + this->framePending = false; + } + lgmpClientUnsubscribe(&this->frameQueue); + + LG_LOCK(this->pointerLock); + lgmpClientUnsubscribe(&this->pointerQueue); + LG_UNLOCK(this->pointerLock); +} + +static void lgmp_closeDMA(struct LG_Transport * this) +{ + for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) + { + if (this->dma[i].fd >= 0) + close(this->dma[i].fd); + this->dma[i].fd = -1; + this->dma[i].frame = NULL; + this->dma[i].dataSize = 0; + } +} + +static void lgmp_destroy(LG_Transport ** transport) +{ + if (!transport || !*transport) + return; + + struct LG_Transport * this = *transport; + lgmp_closeQueues(this); + lgmp_closeDMA(this); + free(this->pointerData); + lgmpClientFree(&this->client); + ivshmemClose(&this->shm); + LG_LOCK_FREE(this->pointerLock); + free(this); + *transport = NULL; +} + +static bool lgmp_parseSession(const uint8_t * data, uint32_t size, + LG_TransportSession * session) +{ + if (size < sizeof(KVMFR)) + return false; + + const KVMFR * header = (const KVMFR *)data; + if (memcmp(header->magic, KVMFR_MAGIC, sizeof(header->magic)) != 0 || + header->version != KVMFR_VERSION) + { + session->remoteVersion = header->version; + return false; + } + + str_copy(session->version, sizeof(session->version), header->hostver, + sizeof(header->hostver)); + if (header->features & KVMFR_FEATURE_SETCURSORPOS) + session->features |= LG_TRANSPORT_FEATURE_SET_CURSOR_POS; + if (header->features & KVMFR_FEATURE_WINDOWSIZE) + session->features |= LG_TRANSPORT_FEATURE_WINDOW_SIZE; + + data += sizeof(*header); + size -= sizeof(*header); + while (size >= sizeof(KVMFRRecord)) + { + const KVMFRRecord * record = (const KVMFRRecord *)data; + data += sizeof(*record); + size -= sizeof(*record); + if (record->size > size) + break; + + switch (record->type) + { + case KVMFR_RECORD_VMINFO: + if (record->size >= sizeof(KVMFRRecord_VMInfo)) + { + const KVMFRRecord_VMInfo * info = + (const KVMFRRecord_VMInfo *)data; + memcpy(session->uuid, info->uuid, sizeof(session->uuid)); + for (unsigned i = 0; i < sizeof(session->uuid); ++i) + session->uuidValid |= session->uuid[i] != 0; + str_copy(session->capture, sizeof(session->capture), info->capture, + sizeof(info->capture)); + session->cpus = info->cpus; + session->cores = info->cores; + session->sockets = info->sockets; + const size_t fixed = offsetof(KVMFRRecord_VMInfo, model); + str_copy(session->cpuModel, sizeof(session->cpuModel), info->model, + record->size - fixed); + } + break; + + case KVMFR_RECORD_OSINFO: + if (record->size >= sizeof(KVMFRRecord_OSInfo)) + { + const KVMFRRecord_OSInfo * info = + (const KVMFRRecord_OSInfo *)data; + session->os = info->os <= KVMFR_OS_OTHER ? + (LG_TransportGuestOS)info->os : LG_TRANSPORT_OS_OTHER; + const size_t fixed = offsetof(KVMFRRecord_OSInfo, name); + str_copy(session->osName, sizeof(session->osName), info->name, + record->size - fixed); + } + break; + } + + data += record->size; + size -= record->size; + } + + return true; +} + +static LG_TransportStatus lgmp_connect(LG_Transport * this, + LG_TransportSession * session) +{ + memset(session, 0, sizeof(*session)); + session->os = LG_TRANSPORT_OS_OTHER; + + uint32_t size; + uint8_t * data; + uint32_t remoteVersion; + LGMP_STATUS status = lgmpClientSessionInit(this->client, &size, &data, NULL, + &remoteVersion); + session->remoteVersion = remoteVersion; + switch (status) + { + case LGMP_OK: + if (!lgmp_parseSession(data, size, session)) + return LG_TRANSPORT_INVALID_VERSION; + this->connected = true; + this->frameSerial = 0; + this->formatValid = false; + return LG_TRANSPORT_OK; + + case LGMP_ERR_INVALID_VERSION: + return LG_TRANSPORT_INVALID_VERSION; + + case LGMP_ERR_INVALID_SESSION: + case LGMP_ERR_INVALID_MAGIC: + return LG_TRANSPORT_UNAVAILABLE; + + default: + DEBUG_ERROR("lgmpClientSessionInit failed: %s", lgmpStatusString(status)); + return LG_TRANSPORT_ERROR; + } +} + +static void lgmp_disconnect(LG_Transport * this) +{ + lgmp_closeQueues(this); + lgmp_closeDMA(this); + this->connected = false; + this->frameSerial = 0; + this->formatValid = false; +} + +static bool lgmp_sessionValid(LG_Transport * this) +{ + return this->connected && lgmpClientSessionValid(this->client); +} + +static bool lgmp_supportsDMA(LG_Transport * this) +{ + return this->allowDMA && ivshmemHasDMA(&this->shm); +} + +static bool lgmp_attachRenderer(LG_Transport * this, + const LG_RendererInterop * interop) +{ + return true; +} + +static void lgmp_detachRenderer(LG_Transport * this) +{ +} + +static LG_TransportStatus lgmp_subscribe(PLGMPClient client, uint32_t id, + PLGMPClientQueue * queue) +{ + if (*queue) + return LG_TRANSPORT_OK; + + LGMP_STATUS status = lgmpClientSubscribe(client, id, queue); + switch (status) + { + case LGMP_OK: return LG_TRANSPORT_OK; + case LGMP_ERR_NO_SUCH_QUEUE: return LG_TRANSPORT_TIMEOUT; + case LGMP_ERR_INVALID_SESSION: return LG_TRANSPORT_DISCONNECTED; + default: + DEBUG_ERROR("lgmpClientSubscribe failed: %s", lgmpStatusString(status)); + return LG_TRANSPORT_ERROR; + } +} + +static LG_TransportStatus lgmp_process(PLGMPClientQueue queue, + unsigned interval, LGMPMessage * message) +{ + LGMP_STATUS status = lgmpClientProcess(queue, message); + switch (status) + { + case LGMP_OK: + return LG_TRANSPORT_OK; + case LGMP_ERR_QUEUE_EMPTY: + if (interval) + usleep(interval); + return LG_TRANSPORT_TIMEOUT; + case LGMP_ERR_INVALID_SESSION: + return LG_TRANSPORT_DISCONNECTED; + default: + DEBUG_ERROR("lgmpClientProcess failed: %s", lgmpStatusString(status)); + return LG_TRANSPORT_ERROR; + } +} + +static int lgmp_getDMA(struct LG_Transport * this, const KVMFRFrame * frame, + size_t dataSize) +{ + struct DMAFrameInfo * dma = NULL; + for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) + if (this->dma[i].frame == frame) + { + dma = &this->dma[i]; + if (dma->dataSize < dataSize && dma->fd >= 0) + { + close(dma->fd); + dma->fd = -1; + } + break; + } + + if (!dma) + for (unsigned i = 0; i < LGMP_Q_FRAME_LEN; ++i) + if (!this->dma[i].frame) + { + dma = &this->dma[i]; + dma->frame = frame; + break; + } + + if (!dma) + return -1; + if (dma->fd >= 0) + return dma->fd; + + const uintptr_t position = (uintptr_t)frame - (uintptr_t)this->shm.mem; + const uintptr_t offset = frame->offset + sizeof(FrameBuffer); + dma->dataSize = dataSize; + dma->fd = ivshmemGetDMABuf(&this->shm, position + offset, dataSize); + return dma->fd; +} + +static LG_TransportStatus lgmp_nextFrame(LG_Transport * this, bool useDMA, + LG_TransportFrame * result) +{ + if (!this->connected) + return LG_TRANSPORT_DISCONNECTED; + if (this->framePending) + return LG_TRANSPORT_ERROR; + + LG_TransportStatus status = lgmp_subscribe(this->client, LGMP_Q_FRAME, + &this->frameQueue); + if (status != LG_TRANSPORT_OK) + { + if (status == LG_TRANSPORT_TIMEOUT) + usleep(1000); + return status; + } + + LGMPMessage message; + status = lgmp_process(this->frameQueue, this->framePollInterval, &message); + if (status != LG_TRANSPORT_OK) + return status; + + if (message.size < sizeof(KVMFRFrame)) + { + lgmpClientMessageDone(this->frameQueue); + DEBUG_ERROR("LGMP frame payload is too small"); + return LG_TRANSPORT_ERROR; + } + + const KVMFRFrame * frame = (const KVMFRFrame *)message.mem; + const size_t frameDataSize = (size_t)frame->dataHeight * frame->pitch; + if (frame->type <= FRAME_TYPE_INVALID || frame->type >= FRAME_TYPE_MAX || + frame->offset > message.size - sizeof(FrameBuffer) || + frameDataSize > message.size - frame->offset - sizeof(FrameBuffer)) + { + lgmpClientMessageDone(this->frameQueue); + DEBUG_ERROR("LGMP frame payload contains invalid dimensions or offsets"); + return LG_TRANSPORT_ERROR; + } + + if (frame->frameSerial == this->frameSerial && this->frameSerial) + { + lgmpClientMessageDone(this->frameQueue); + return LG_TRANSPORT_TIMEOUT; + } + this->frameSerial = frame->frameSerial; + + memset(result, 0, sizeof(*result)); + result->serial = frame->frameSerial; + if (frame->flags & FRAME_FLAG_BLOCK_SCREENSAVER) + result->flags |= LG_TRANSPORT_FRAME_BLOCK_SCREENSAVER; + if (frame->flags & FRAME_FLAG_REQUEST_ACTIVATION) + result->flags |= LG_TRANSPORT_FRAME_REQUEST_ACTIVATION; + if (frame->flags & FRAME_FLAG_TRUNCATED) + result->flags |= LG_TRANSPORT_FRAME_TRUNCATED; + + LG_TransportFrameFormat * format = &this->format; + if (!this->formatValid || format->version != frame->formatVer) + { + memset(format, 0, sizeof(*format)); + format->version = frame->formatVer; + format->type = frame->type; + format->screenWidth = frame->screenWidth; + format->screenHeight = frame->screenHeight; + format->dataWidth = frame->dataWidth; + format->dataHeight = frame->dataHeight; + format->frameWidth = frame->frameWidth; + format->frameHeight = frame->frameHeight; + format->rotation = frame->rotation; + format->stride = frame->stride; + format->pitch = frame->pitch; + format->hdr = frame->flags & FRAME_FLAG_HDR; + format->hdrPQ = frame->flags & FRAME_FLAG_HDR_PQ; + format->hdrMetadata = frame->flags & FRAME_FLAG_HDR_METADATA; + format->sdrWhiteLevel = frame->sdrWhiteLevel ? frame->sdrWhiteLevel : + KVMFR_SDR_WHITE_LEVEL_DEFAULT; + if (format->hdrMetadata) + { + memcpy(format->hdrDisplayPrimary, frame->hdrDisplayPrimary, + sizeof(format->hdrDisplayPrimary)); + memcpy(format->hdrWhitePoint, frame->hdrWhitePoint, + sizeof(format->hdrWhitePoint)); + format->hdrMaxDisplayLuminance = frame->hdrMaxDisplayLuminance; + format->hdrMinDisplayLuminance = frame->hdrMinDisplayLuminance; + format->hdrMaxContentLightLevel = frame->hdrMaxContentLightLevel; + format->hdrMaxFrameAverageLightLevel = + frame->hdrMaxFrameAverageLightLevel; + } + this->formatValid = true; + } + result->format = format; + + result->framebuffer = (const FrameBuffer *)((const uint8_t *)frame + + frame->offset); + result->dmaFD = -1; + if (useDMA) + { + const size_t dataSize = (size_t)format->dataHeight * format->pitch; + result->dmaFD = lgmp_getDMA(this, frame, dataSize); + if (result->dmaFD < 0) + { + lgmpClientMessageDone(this->frameQueue); + DEBUG_ERROR("Failed to obtain a DMA buffer for the frame"); + return LG_TRANSPORT_ERROR; + } + } + + if (frame->damageRectsCount <= KVMFR_MAX_DAMAGE_RECTS) + { + result->damageRects = frame->damageRects; + result->damageRectsCount = frame->damageRectsCount; + } + else + DEBUG_WARN("Invalid damage rectangles, forcing a full update"); + + this->framePending = true; + return LG_TRANSPORT_OK; +} + +static void lgmp_releaseFrame(LG_Transport * this, LG_TransportFrame * frame) +{ + if (this->framePending && this->frameQueue) + lgmpClientMessageDone(this->frameQueue); + this->framePending = false; + memset(frame, 0, sizeof(*frame)); +} + +static LG_TransportStatus lgmp_nextPointer(LG_Transport * this, + LG_TransportPointer * result) +{ + if (!this->connected) + return LG_TRANSPORT_DISCONNECTED; + + uint32_t pointerFlags = 0; + size_t shapeSize = 0; + size_t transformSize = 0; + LG_LOCK(this->pointerLock); + LG_TransportStatus status = lgmp_subscribe(this->client, LGMP_Q_POINTER, + &this->pointerQueue); + LG_UNLOCK(this->pointerLock); + if (status == LG_TRANSPORT_OK) + { + LGMPMessage message; + status = lgmp_process(this->pointerQueue, this->cursorPollInterval, + &message); + if (status == LG_TRANSPORT_OK) + { + if (message.size < sizeof(KVMFRCursor)) + { + lgmpClientMessageDone(this->pointerQueue); + status = LG_TRANSPORT_ERROR; + } + else + { + const KVMFRCursor * cursor = (const KVMFRCursor *)message.mem; + shapeSize = message.udata & CURSOR_FLAG_SHAPE ? + (size_t)cursor->height * cursor->pitch : 0; + transformSize = + message.udata & CURSOR_FLAG_COLOR_TRANSFORM ? + sizeof(KVMFRColorTransform) : 0; + if (shapeSize > message.size - sizeof(*cursor) || + transformSize > message.size - sizeof(*cursor) - shapeSize) + { + lgmpClientMessageDone(this->pointerQueue); + status = LG_TRANSPORT_ERROR; + } + else + { + const size_t needed = sizeof(*cursor) + shapeSize + transformSize; + if (needed > this->pointerDataSize) + { + void * data = realloc(this->pointerData, needed); + if (!data) + status = LG_TRANSPORT_ERROR; + else + { + this->pointerData = data; + this->pointerDataSize = needed; + } + } + if (status == LG_TRANSPORT_OK) + { + memcpy(this->pointerData, message.mem, needed); + pointerFlags = message.udata; + } + lgmpClientMessageDone(this->pointerQueue); + } + } + } + } + if (status != LG_TRANSPORT_OK) + { + if (status == LG_TRANSPORT_TIMEOUT) + usleep(1000); + return status; + } + + const KVMFRCursor * cursor = (const KVMFRCursor *)this->pointerData; + memset(result, 0, sizeof(*result)); + if (pointerFlags & CURSOR_FLAG_POSITION) + result->flags |= LG_TRANSPORT_POINTER_POSITION; + if (pointerFlags & CURSOR_FLAG_VISIBLE) + result->flags |= LG_TRANSPORT_POINTER_VISIBLE; + if (pointerFlags & CURSOR_FLAG_SHAPE) + result->flags |= LG_TRANSPORT_POINTER_SHAPE; + if (pointerFlags & CURSOR_FLAG_COLOR_TRANSFORM) + result->flags |= LG_TRANSPORT_POINTER_COLOR_TRANSFORM; + if (pointerFlags & CURSOR_FLAG_VISIBLE_VALID) + result->flags |= LG_TRANSPORT_POINTER_VISIBLE_VALID; + result->x = cursor->x; + result->y = cursor->y; + result->type = cursor->type; + result->hx = cursor->hx; + result->hy = cursor->hy; + result->width = cursor->width; + result->height = cursor->height; + result->pitch = cursor->pitch; + result->sdrWhiteLevel = cursor->sdrWhiteLevel; + result->shape = (const uint8_t *)(cursor + 1); + if (transformSize) + result->colorTransform = (const LGColorTransform *)(result->shape + + shapeSize); + return LG_TRANSPORT_OK; +} + +static void lgmp_releasePointer(LG_Transport * this, + LG_TransportPointer * pointer) +{ + memset(pointer, 0, sizeof(*pointer)); +} + +static LG_TransportStatus lgmp_sendControl(LG_Transport * this, + const LG_TransportControl * control, LG_TransportControlToken * token) +{ + uint8_t buffer[sizeof(KVMFRWindowSize)]; + uint32_t size; + switch (control->type) + { + case LG_TRANSPORT_CONTROL_SET_CURSOR_POS: + { + const KVMFRSetCursorPos message = { + .msg.type = KVMFR_MESSAGE_SETCURSORPOS, + .x = control->cursorPos.x, + .y = control->cursorPos.y, + }; + memcpy(buffer, &message, sizeof(message)); + size = sizeof(message); + break; + } + case LG_TRANSPORT_CONTROL_WINDOW_SIZE: + { + const KVMFRWindowSize message = { + .msg.type = KVMFR_MESSAGE_WINDOWSIZE, + .w = control->windowSize.width, + .h = control->windowSize.height, + }; + memcpy(buffer, &message, sizeof(message)); + size = sizeof(message); + break; + } + default: + return LG_TRANSPORT_ERROR; + } + + LG_LOCK(this->pointerLock); + if (!this->pointerQueue) + { + LG_UNLOCK(this->pointerLock); + return LG_TRANSPORT_UNAVAILABLE; + } + uint32_t serial; + LGMP_STATUS status = lgmpClientSendData(this->pointerQueue, buffer, size, + &serial); + LG_UNLOCK(this->pointerLock); + if (status != LGMP_OK) + return status == LGMP_ERR_INVALID_SESSION ? LG_TRANSPORT_DISCONNECTED : + LG_TRANSPORT_ERROR; + *token = serial; + return LG_TRANSPORT_OK; +} + +static LG_TransportStatus lgmp_controlStatus(LG_Transport * this, + LG_TransportControlToken token) +{ + LG_LOCK(this->pointerLock); + if (!this->pointerQueue) + { + LG_UNLOCK(this->pointerLock); + return LG_TRANSPORT_DISCONNECTED; + } + uint32_t serial; + LGMP_STATUS status = lgmpClientGetSerial(this->pointerQueue, &serial); + LG_UNLOCK(this->pointerLock); + if (status != LGMP_OK) + return status == LGMP_ERR_INVALID_SESSION ? LG_TRANSPORT_DISCONNECTED : + LG_TRANSPORT_ERROR; + return serial >= token ? LG_TRANSPORT_OK : LG_TRANSPORT_UNAVAILABLE; +} + +const LG_TransportOps LGT_LGMP = +{ + .name = "lgmp", + .setup = lgmp_setup, + .create = lgmp_create, + .destroy = lgmp_destroy, + .connect = lgmp_connect, + .disconnect = lgmp_disconnect, + .sessionValid = lgmp_sessionValid, + .supportsDMA = lgmp_supportsDMA, + .attachRenderer = lgmp_attachRenderer, + .detachRenderer = lgmp_detachRenderer, + .nextFrame = lgmp_nextFrame, + .releaseFrame = lgmp_releaseFrame, + .nextPointer = lgmp_nextPointer, + .releasePointer = lgmp_releasePointer, + .sendControl = lgmp_sendControl, + .controlStatus = lgmp_controlStatus, +}; diff --git a/client/transports/Test/CMakeLists.txt b/client/transports/Test/CMakeLists.txt new file mode 100644 index 00000000..d5ecce55 --- /dev/null +++ b/client/transports/Test/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.10) +project(transport_Test LANGUAGES C) + +add_library(transport_Test STATIC + test.c +) + +target_link_libraries(transport_Test + lg_common +) diff --git a/client/transports/Test/test.c b/client/transports/Test/test.c new file mode 100644 index 00000000..28d01f00 --- /dev/null +++ b/client/transports/Test/test.c @@ -0,0 +1,607 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "interface/transport.h" + +#include "common/array.h" +#include "common/debug.h" +#include "common/option.h" +#include "common/time.h" + +#include +#include +#include +#include +#include + +#define TEST_BUFFER_COUNT 2 +#define TEST_PQ_LUT_SIZE 4096 + +struct TestFormat +{ + const char * name; + FrameType type; + unsigned bytesPerPixel; + bool hdr; + bool hdrPQ; +}; + +static const struct TestFormat testFormats[] = +{ + { "bgra" , FRAME_TYPE_BGRA , 4, false, false }, + { "rgba" , FRAME_TYPE_RGBA , 4, false, false }, + { "bgr32" , FRAME_TYPE_BGR_32 , 4, false, false }, + { "rgb24" , FRAME_TYPE_RGB_24 , 3, false, false }, + { "rgba10" , FRAME_TYPE_RGBA10 , 4, true , true }, + { "rgba16f", FRAME_TYPE_RGBA16F, 8, true , false }, +}; + +struct TestBuffer +{ + FrameBuffer * framebuffer; +}; + +struct LG_Transport +{ + unsigned width; + unsigned height; + unsigned frameRate; + unsigned frameCount; + bool realtime; + bool cycleFormats; + unsigned formatIndex; + + bool connected; + bool framePending; + uint64_t serial; + uint64_t nextFrameTime; + unsigned bufferIndex; + struct TestBuffer buffers[TEST_BUFFER_COUNT]; + FrameDamageRect damage[2]; + LG_TransportFrameFormat format; + uint16_t pqLUT[TEST_PQ_LUT_SIZE + 1]; +}; + +static void test_setup(void) +{ + static struct Option options[] = + { + { + .module = "test", + .name = "width", + .description = "Generated frame width", + .type = OPTION_TYPE_INT, + .value.x_int = 1920, + }, + { + .module = "test", + .name = "height", + .description = "Generated frame height", + .type = OPTION_TYPE_INT, + .value.x_int = 1080, + }, + { + .module = "test", + .name = "frameRate", + .description = "Generated frames per second", + .type = OPTION_TYPE_INT, + .value.x_int = 60, + }, + { + .module = "test", + .name = "frameCount", + .description = "Stop after this many frames, or zero to run forever", + .type = OPTION_TYPE_INT, + .value.x_int = 0, + }, + { + .module = "test", + .name = "realtime", + .description = "Pace generated frames in real time", + .type = OPTION_TYPE_BOOL, + .value.x_bool = true, + }, + { + .module = "test", + .name = "format", + .description = "Generated frame format: bgra, rgba, bgr32, rgb24, " + "rgba10, rgba16f, or cycle", + .type = OPTION_TYPE_STRING, + .value.x_string = "bgra", + }, + {0} + }; + + option_register(options); +} + +static int test_findFormat(const char * name) +{ + for (unsigned i = 0; i < ARRAY_LENGTH(testFormats); ++i) + if (strcmp(name, testFormats[i].name) == 0) + return (int)i; + + return -1; +} + +static float test_linearToPQ(float linear) +{ + const float m1 = 2610.0f / 16384.0f; + const float m2 = 2523.0f / 32.0f; + const float c1 = 3424.0f / 4096.0f; + const float c2 = 2413.0f / 128.0f; + const float c3 = 2392.0f / 128.0f; + const float p = powf(linear, m1); + return powf((c1 + c2 * p) / (1.0f + c3 * p), m2); +} + +static void test_initPQLUT(struct LG_Transport * this) +{ + for (unsigned i = 0; i <= TEST_PQ_LUT_SIZE; ++i) + { + /* scRGB uses 1.0 for 80 nits; PQ uses 1.0 for 10000 nits. */ + const float scRGB = (float)i * 4.0f / TEST_PQ_LUT_SIZE; + const float pq = test_linearToPQ(scRGB / 125.0f); + this->pqLUT[i] = (uint16_t)(pq * 1023.0f + 0.5f); + } +} + +static uint16_t test_scRGBToPQ(const struct LG_Transport * this, float scRGB) +{ + const unsigned index = scRGB <= 0.0f ? 0 : scRGB >= 4.0f ? + TEST_PQ_LUT_SIZE : + (unsigned)(scRGB * (TEST_PQ_LUT_SIZE / 4.0f) + 0.5f); + return this->pqLUT[index]; +} + +static void test_setHDRMetadata(LG_TransportFrameFormat * format) +{ + if (format->hdrPQ) + { + format->hdrDisplayPrimary[0][0] = 35400; + format->hdrDisplayPrimary[0][1] = 14600; + format->hdrDisplayPrimary[1][0] = 8500; + format->hdrDisplayPrimary[1][1] = 39850; + format->hdrDisplayPrimary[2][0] = 6550; + format->hdrDisplayPrimary[2][1] = 2300; + format->hdrMaxDisplayLuminance = 1000; + format->hdrMinDisplayLuminance = 1; + } + else + { + format->hdrDisplayPrimary[0][0] = 13250; + format->hdrDisplayPrimary[0][1] = 34500; + format->hdrDisplayPrimary[1][0] = 7500; + format->hdrDisplayPrimary[1][1] = 30000; + format->hdrDisplayPrimary[2][0] = 34000; + format->hdrDisplayPrimary[2][1] = 16000; + format->hdrMaxDisplayLuminance = 80; + format->hdrMinDisplayLuminance = 50; + } + + format->hdrWhitePoint[0] = 15635; + format->hdrWhitePoint[1] = 16450; + format->hdrMaxContentLightLevel = 1000; + format->hdrMaxFrameAverageLightLevel = 400; +} + +static bool test_setFormat(struct LG_Transport * this, unsigned index) +{ + if (this->format.version && this->formatIndex == index) + return false; + + const struct TestFormat * testFormat = &testFormats[index]; + const uint32_t version = this->format.version + 1; + const bool packedBGR = + testFormat->type == FRAME_TYPE_BGR_32; + const unsigned pitch = packedBGR ? + ALIGN_PAD(this->width * 3, 4) : + this->width * testFormat->bytesPerPixel; + + this->formatIndex = index; + this->format = (LG_TransportFrameFormat) { + .version = version, + .type = testFormat->type, + .screenWidth = this->width, + .screenHeight = this->height, + .dataWidth = packedBGR ? pitch / 4 : this->width, + .dataHeight = this->height, + .frameWidth = this->width, + .frameHeight = this->height, + .rotation = FRAME_ROT_0, + .stride = packedBGR ? pitch / 4 : this->width, + .pitch = pitch, + .hdr = testFormat->hdr, + .hdrPQ = testFormat->hdrPQ, + .hdrMetadata = testFormat->hdr, + .sdrWhiteLevel = LG_SDR_WHITE_LEVEL_DEFAULT, + }; + + if (this->format.hdrMetadata) + test_setHDRMetadata(&this->format); + + return true; +} + +static bool test_create(LG_Transport ** result) +{ + struct LG_Transport * this = calloc(1, sizeof(*this)); + if (!this) + return false; + + const int width = option_get_int("test", "width"); + const int height = option_get_int("test", "height"); + const int frameRate = option_get_int("test", "frameRate"); + const int frameCount = option_get_int("test", "frameCount"); + const char * format = option_get_string("test", "format"); + const bool cycleFormats = strcmp(format, "cycle") == 0; + const int formatIndex = cycleFormats ? 0 : test_findFormat(format); + const unsigned maxBytesPerPixel = formatIndex < 0 ? 0 : + cycleFormats ? 8 : testFormats[formatIndex].bytesPerPixel; + if (width < 1 || height < 1 || !maxBytesPerPixel || + width > (int)(UINT32_MAX / maxBytesPerPixel) || + frameRate < 1 || frameRate > 1000000000 || frameCount < 0 || + (size_t)width > (SIZE_MAX - sizeof(FrameBuffer)) / + maxBytesPerPixel / (size_t)height) + { + DEBUG_ERROR( + "Invalid test transport dimensions, rate, frame count, or format"); + free(this); + return false; + } + + this->width = width; + this->height = height; + this->frameRate = frameRate; + this->frameCount = frameCount; + this->realtime = option_get_bool("test", "realtime"); + this->cycleFormats = cycleFormats; + this->formatIndex = (unsigned)formatIndex; + test_initPQLUT(this); + + const size_t dataSize = + (size_t)this->width * this->height * maxBytesPerPixel; + for (unsigned i = 0; i < TEST_BUFFER_COUNT; ++i) + { + this->buffers[i].framebuffer = malloc(sizeof(FrameBuffer) + dataSize); + if (!this->buffers[i].framebuffer) + { + for (unsigned j = 0; j < i; ++j) + free(this->buffers[j].framebuffer); + free(this); + return false; + } + } + + *result = this; + return true; +} + +static void test_destroy(LG_Transport ** transport) +{ + if (!transport || !*transport) + return; + + struct LG_Transport * this = *transport; + for (unsigned i = 0; i < TEST_BUFFER_COUNT; ++i) + free(this->buffers[i].framebuffer); + free(this); + *transport = NULL; +} + +static LG_TransportStatus test_connect(LG_Transport * this, + LG_TransportSession * session) +{ + memset(session, 0, sizeof(*session)); + memcpy(session->version, "test", 5); + session->os = LG_TRANSPORT_OS_OTHER; + memcpy(session->capture, "test", 5); + + this->connected = true; + this->framePending = false; + this->serial = 0; + this->bufferIndex = 0; + this->nextFrameTime = nanotime(); + this->format.version = 0; + test_setFormat(this, this->formatIndex); + return LG_TRANSPORT_OK; +} + +static void test_disconnect(LG_Transport * this) +{ + this->connected = false; + this->framePending = false; +} + +static bool test_sessionValid(LG_Transport * this) +{ + return this->connected; +} + +static bool test_supportsDMA(LG_Transport * this) +{ + return false; +} + +static bool test_attachRenderer(LG_Transport * this, + const LG_RendererInterop * interop) +{ + return true; +} + +static void test_detachRenderer(LG_Transport * this) +{ +} + +static uint16_t test_floatToHalf(float value) +{ + union + { + float value; + uint32_t bits; + } + input = { .value = value }; + + const uint16_t sign = (input.bits >> 16) & 0x8000; + int exponent = ((input.bits >> 23) & 0xff) - 127 + 15; + uint32_t mantissa = input.bits & 0x7fffff; + if (exponent <= 0) + return sign; + if (exponent >= 31) + return sign | 0x7c00; + + mantissa += 0x1000; + if (mantissa & 0x800000) + { + mantissa = 0; + if (++exponent >= 31) + return sign | 0x7c00; + } + + return sign | ((uint16_t)exponent << 10) | (mantissa >> 13); +} + +static void test_getColor(const struct LG_Transport * this, + unsigned x, unsigned y, uint8_t * r, uint8_t * g, uint8_t * b) +{ + const unsigned widthRange = this->width > 1 ? this->width - 1 : 1; + const unsigned heightRange = this->height > 1 ? this->height - 1 : 1; + *r = (uint64_t)x * 255 / widthRange; + *g = (uint64_t)y * 255 / heightRange; + *b = ((x / 32) ^ (y / 32)) & 1 ? 0x30 : 0x90; + + unsigned boxSize = this->width < this->height ? + this->width : this->height; + if (boxSize > 64) + boxSize = 64; + const unsigned rangeX = this->width > boxSize ? this->width - boxSize : 1; + const unsigned rangeY = this->height > boxSize ? + this->height - boxSize : 1; + const unsigned boxX = (this->serial * 7) % rangeX; + const unsigned boxY = (this->serial * 5) % rangeY; + if (x < boxX || y < boxY || x >= boxX + boxSize || y >= boxY + boxSize) + return; + + const uint32_t color = this->serial * 2654435761U; + *r = color >> 16; + *g = color >> 8; + *b = color; +} + +static void test_generateFrame(struct LG_Transport * this, FrameBuffer * fb) +{ + uint8_t * data = framebuffer_get_data(fb); + if (this->format.type == FRAME_TYPE_BGR_32) + memset(data, 0, (size_t)this->format.pitch * this->height); + + for (unsigned y = 0; y < this->height; ++y) + for (unsigned x = 0; x < this->width; ++x) + { + uint8_t r, g, b; + test_getColor(this, x, y, &r, &g, &b); + const size_t pixel = (size_t)y * this->width + x; + switch (this->format.type) + { + case FRAME_TYPE_BGRA: + ((uint32_t *)data)[pixel] = + 0xff000000U | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; + break; + + case FRAME_TYPE_BGR_32: + { + uint8_t * bgr = data + (size_t)y * this->format.pitch + x * 3; + bgr[0] = b; + bgr[1] = g; + bgr[2] = r; + break; + } + + case FRAME_TYPE_RGBA: + ((uint32_t *)data)[pixel] = + 0xff000000U | ((uint32_t)b << 16) | ((uint32_t)g << 8) | r; + break; + + case FRAME_TYPE_RGB_24: + data[pixel * 3 ] = r; + data[pixel * 3 + 1] = g; + data[pixel * 3 + 2] = b; + break; + + case FRAME_TYPE_RGBA10: + { + const float r709 = (float)r * 4.0f / 255.0f; + const float g709 = (float)g * 4.0f / 255.0f; + const float b709 = (float)b * 4.0f / 255.0f; + const float r2020 = + r709 * 0.6274039f + g709 * 0.3292830f + b709 * 0.0433131f; + const float g2020 = + r709 * 0.0690973f + g709 * 0.9195404f + b709 * 0.0113623f; + const float b2020 = + r709 * 0.0163914f + g709 * 0.0880133f + b709 * 0.8955953f; + ((uint32_t *)data)[pixel] = + ((uint32_t)test_scRGBToPQ(this, r2020) ) | + ((uint32_t)test_scRGBToPQ(this, g2020) << 10) | + ((uint32_t)test_scRGBToPQ(this, b2020) << 20) | + (3U << 30); + break; + } + + case FRAME_TYPE_RGBA16F: + { + uint16_t * rgba = (uint16_t *)data + pixel * 4; + rgba[0] = test_floatToHalf((float)r * 4.0f / 255.0f); + rgba[1] = test_floatToHalf((float)g * 4.0f / 255.0f); + rgba[2] = test_floatToHalf((float)b * 4.0f / 255.0f); + rgba[3] = test_floatToHalf(1.0f); + break; + } + + default: + DEBUG_UNREACHABLE(); + } + } + + framebuffer_set_write_ptr(fb, (size_t)this->format.pitch * this->height); +} + +static LG_TransportStatus test_nextFrame(LG_Transport * this, bool useDMA, + LG_TransportFrame * frame) +{ + if (!this->connected) + return LG_TRANSPORT_DISCONNECTED; + if (this->framePending) + return LG_TRANSPORT_ERROR; + if (this->frameCount && this->serial >= this->frameCount) + return LG_TRANSPORT_END; + + if (this->realtime) + { + const uint64_t now = nanotime(); + if (now < this->nextFrameTime) + { + const uint64_t remaining = this->nextFrameTime - now; + usleep((remaining > 1000000 ? 1000000 : remaining) / 1000); + return LG_TRANSPORT_TIMEOUT; + } + } + + const bool formatChanged = this->cycleFormats && test_setFormat(this, + this->serial % ARRAY_LENGTH(testFormats)); + ++this->serial; + struct TestBuffer * buffer = &this->buffers[this->bufferIndex]; + this->bufferIndex = (this->bufferIndex + 1) % TEST_BUFFER_COUNT; + framebuffer_prepare(buffer->framebuffer); + test_generateFrame(this, buffer->framebuffer); + + memset(frame, 0, sizeof(*frame)); + frame->serial = this->serial; + frame->timestamp = this->realtime ? nanotime() : + this->serial * (1000000000ULL / this->frameRate); + frame->format = &this->format; + frame->framebuffer = buffer->framebuffer; + frame->dmaFD = -1; + + if (this->serial == 1 || formatChanged) + frame->damageRectsCount = 0; + else + { + unsigned boxSize = this->width < this->height ? + this->width : this->height; + if (boxSize > 64) + boxSize = 64; + const unsigned rangeX = this->width > boxSize ? + this->width - boxSize : 1; + const unsigned rangeY = this->height > boxSize ? + this->height - boxSize : 1; + this->damage[0] = (FrameDamageRect) { + .x = ((this->serial - 1) * 7) % rangeX, + .y = ((this->serial - 1) * 5) % rangeY, + .width = boxSize, + .height = boxSize, + }; + this->damage[1] = (FrameDamageRect) { + .x = (this->serial * 7) % rangeX, + .y = (this->serial * 5) % rangeY, + .width = boxSize, + .height = boxSize, + }; + frame->damageRects = this->damage; + frame->damageRectsCount = 2; + } + + this->framePending = true; + this->nextFrameTime += 1000000000ULL / this->frameRate; + return LG_TRANSPORT_OK; +} + +static void test_releaseFrame(LG_Transport * this, LG_TransportFrame * frame) +{ + this->framePending = false; + memset(frame, 0, sizeof(*frame)); +} + +static LG_TransportStatus test_nextPointer(LG_Transport * this, + LG_TransportPointer * pointer) +{ + if (!this->connected) + return LG_TRANSPORT_DISCONNECTED; + usleep(1000); + return LG_TRANSPORT_TIMEOUT; +} + +static void test_releasePointer(LG_Transport * this, + LG_TransportPointer * pointer) +{ +} + +static LG_TransportStatus test_sendControl(LG_Transport * this, + const LG_TransportControl * control, LG_TransportControlToken * token) +{ + if (!this->connected) + return LG_TRANSPORT_DISCONNECTED; + return LG_TRANSPORT_UNAVAILABLE; +} + +static LG_TransportStatus test_controlStatus(LG_Transport * this, + LG_TransportControlToken token) +{ + return this->connected ? LG_TRANSPORT_UNAVAILABLE : + LG_TRANSPORT_DISCONNECTED; +} + +const LG_TransportOps LGT_Test = +{ + .name = "test", + .setup = test_setup, + .create = test_create, + .destroy = test_destroy, + .connect = test_connect, + .disconnect = test_disconnect, + .sessionValid = test_sessionValid, + .supportsDMA = test_supportsDMA, + .attachRenderer = test_attachRenderer, + .detachRenderer = test_detachRenderer, + .nextFrame = test_nextFrame, + .releaseFrame = test_releaseFrame, + .nextPointer = test_nextPointer, + .releasePointer = test_releasePointer, + .sendControl = test_sendControl, + .controlStatus = test_controlStatus, +};