/** * 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 "cursor.h" #include "common/debug.h" #include "common/locking.h" #include "common/option.h" #include "texture.h" #include "shader.h" #include "state.h" #include "model.h" #include "util.h" #include #include #include // these headers are auto generated by cmake #include "cursor.vert.h" #include "cursor_rgb.frag.h" #include "cursor_mono.frag.h" enum CursorTransfer { CURSOR_TRANSFER_SRGB = 0, CURSOR_TRANSFER_SCRGB = 1, CURSOR_TRANSFER_PQ = 2, }; struct CursorTex { struct EGL_Texture * texture; struct EGL_Shader * shader; EGL_Uniform * uMousePos; EGL_Uniform * uScale; EGL_Uniform * uRotate; EGL_Uniform * uCBMode; EGL_Uniform * uSourceTransfer; EGL_Uniform * uSDRWhiteLevel; EGL_Uniform * uMapHDRtoSDR; EGL_Uniform * uMapHDRGain; EGL_Uniform * uMapHDRContentPeak; EGL_Uniform * uColorTransformFlags; EGL_Uniform * uColorMatrix; EGL_Uniform * uColorScalar; EGL_Uniform * uLinearComposition; }; struct CursorPos { float x, y; }; struct CursorSize { float w, h; }; struct EGL_Cursor { LG_Lock lock; LG_RendererCursor type; int width; int height; int stride; uint8_t * data; size_t dataSize; uint32_t * normData; uint32_t * monoData; size_t decodedCapacity; bool update; // cursor state bool visible; LG_RendererRotate rotate; int cbMode; _Atomic(struct CursorPos) pos; _Atomic(struct CursorPos) hs; _Atomic(struct CursorSize) size; _Atomic(float) scale; _Atomic(enum CursorTransfer) sourceTransfer; _Atomic(float) sdrWhiteLevel; _Atomic(bool) mapHDRtoSDR; _Atomic(float) mapHDRGain; _Atomic(float) mapHDRContentPeak; bool colorTransformUpdate; LGColorTransform pendingColorTransform; LGColorTransform activeColorTransform; GLuint colorLUT; struct CursorTex norm; struct CursorTex mono; struct EGL_Model * model; }; static bool cursorTexInit(struct CursorTex * t, const char * vertex_code , size_t vertex_size, const char * fragment_code, size_t fragment_size) { if (!egl_textureInit(&t->texture, NULL, EGL_TEXTYPE_BUFFER)) { DEBUG_ERROR("Failed to initialize the cursor texture"); return false; } if (!egl_shaderInit(&t->shader)) { DEBUG_ERROR("Failed to initialize the cursor shader"); return false; } if (!egl_shaderCompile(t->shader, vertex_code, vertex_size, fragment_code, fragment_size, false, NULL)) { DEBUG_ERROR("Failed to compile the cursor shader"); return false; } t->uMousePos = egl_shaderGetUniform(t->shader, "mouse" ); t->uScale = egl_shaderGetUniform(t->shader, "scale" ); t->uRotate = egl_shaderGetUniform(t->shader, "rotate" ); t->uCBMode = egl_shaderGetUniform(t->shader, "cbMode" ); t->uSourceTransfer = egl_shaderGetUniform(t->shader, "sourceTransfer"); t->uSDRWhiteLevel = egl_shaderGetUniform(t->shader, "sdrWhiteLevel"); t->uMapHDRtoSDR = egl_shaderGetUniform(t->shader, "mapHDRtoSDR" ); t->uMapHDRGain = egl_shaderGetUniform(t->shader, "mapHDRGain" ); t->uMapHDRContentPeak = egl_shaderGetUniform(t->shader, "mapHDRContentPeak"); t->uColorTransformFlags = egl_shaderGetUniform(t->shader, "colorTransformFlags"); t->uColorMatrix = egl_shaderGetUniform(t->shader, "colorMatrix[0]"); t->uColorScalar = egl_shaderGetUniform(t->shader, "colorTransformScalar"); t->uLinearComposition = egl_shaderGetUniform(t->shader, "linearComposition"); egl_shaderAssocTextures(t->shader, 2); return true; } static inline void setCursorTexUniforms(EGL_Cursor * cursor, struct CursorTex * t, bool mono, float x, float y, float w, float h, float scale, bool linearComposition) { const enum CursorTransfer sourceTransfer = mono ? CURSOR_TRANSFER_SRGB : atomic_load(&cursor->sourceTransfer); egl_uniform4f (t->uMousePos , x, y, w, mono ? h / 2 : h); egl_uniform1f (t->uScale , scale); egl_uniform1i (t->uRotate , cursor->rotate); // The colour-blind transform is defined for SDR signals. The desktop // disables it while passing native HDR through, so keep the cursor on the // same path rather than altering encoded PQ or linear scRGB independently. egl_uniform1i (t->uCBMode , (sourceTransfer == CURSOR_TRANSFER_SRGB || atomic_load(&cursor->mapHDRtoSDR)) ? cursor->cbMode : 0); egl_uniform1i (t->uSourceTransfer , sourceTransfer); egl_uniform1f (t->uSDRWhiteLevel , atomic_load(&cursor->sdrWhiteLevel)); egl_uniform1i (t->uMapHDRtoSDR , !mono && atomic_load(&cursor->mapHDRtoSDR)); egl_uniform1f (t->uMapHDRGain , atomic_load(&cursor->mapHDRGain)); egl_uniform1f (t->uMapHDRContentPeak , atomic_load(&cursor->mapHDRContentPeak)); egl_uniform1ui(t->uColorTransformFlags , mono ? 0 : cursor->activeColorTransform.flags); egl_uniformSet (t->uColorMatrix , EGL_UNIFORM_TYPE_4FV, 3, GL_FALSE, cursor->activeColorTransform.matrix); egl_uniform1f (t->uColorScalar , cursor->activeColorTransform.scalar); egl_uniform1i (t->uLinearComposition , !mono && linearComposition); if (!mono && cursor->colorLUT) egl_stateBindTexture(1, GL_TEXTURE_2D, cursor->colorLUT); } static void cursorTexFree(struct CursorTex * t) { egl_textureFree(&t->texture); egl_shaderFree (&t->shader ); }; bool egl_cursorInit(EGL_Cursor ** cursor) { *cursor = malloc(sizeof(**cursor)); if (!*cursor) { DEBUG_ERROR("Failed to malloc EGL_Cursor"); return false; } memset(*cursor, 0, sizeof(**cursor)); LG_LOCK_INIT((*cursor)->lock); if (!cursorTexInit(&(*cursor)->norm, b_shader_cursor_vert , b_shader_cursor_vert_size, b_shader_cursor_rgb_frag, b_shader_cursor_rgb_frag_size)) return false; if (!cursorTexInit(&(*cursor)->mono, b_shader_cursor_vert , b_shader_cursor_vert_size, b_shader_cursor_mono_frag, b_shader_cursor_mono_frag_size)) return false; if (!egl_modelInit(&(*cursor)->model)) { DEBUG_ERROR("Failed to initialize the cursor model"); return false; } egl_modelSetDefault((*cursor)->model, true); (*cursor)->cbMode = option_get_int("egl", "cbMode"); struct CursorPos pos = { .x = 0, .y = 0 }; struct CursorPos hs = { .x = 0, .y = 0 }; struct CursorSize size = { .w = 0, .h = 0 }; atomic_init(&(*cursor)->pos , pos ); atomic_init(&(*cursor)->hs , hs ); atomic_init(&(*cursor)->size , size ); atomic_init(&(*cursor)->scale , 1.0f ); atomic_init(&(*cursor)->sourceTransfer , CURSOR_TRANSFER_SRGB); atomic_init(&(*cursor)->sdrWhiteLevel , LG_SDR_WHITE_LEVEL_DEFAULT); atomic_init(&(*cursor)->mapHDRtoSDR , false); atomic_init(&(*cursor)->mapHDRGain , 1.0f ); atomic_init(&(*cursor)->mapHDRContentPeak, 1.0f ); return true; } void egl_cursorFree(EGL_Cursor ** cursor) { if (!*cursor) return; LG_LOCK_FREE((*cursor)->lock); if ((*cursor)->data) free((*cursor)->data); free((*cursor)->normData); free((*cursor)->monoData); cursorTexFree(&(*cursor)->norm); cursorTexFree(&(*cursor)->mono); if ((*cursor)->colorLUT) glDeleteTextures(1, &(*cursor)->colorLUT); egl_modelFree(&(*cursor)->model); free(*cursor); *cursor = NULL; } static bool cursorEnsureDecoded(EGL_Cursor * cursor, size_t pixels) { if (pixels <= cursor->decodedCapacity) return true; const size_t size = pixels * sizeof(*cursor->normData); uint32_t * normData = malloc(size); uint32_t * monoData = malloc(size); if (!normData || !monoData) { free(normData); free(monoData); DEBUG_ERROR("Failed to allocate cursor conversion buffers"); return false; } free(cursor->normData); free(cursor->monoData); cursor->normData = normData; cursor->monoData = monoData; cursor->decodedCapacity = pixels; return true; } bool egl_cursorSetShape(EGL_Cursor * cursor, const LG_RendererCursor type, const int width, const int height, const int stride, const uint8_t * data) { size_t size; if (!data || !lg_rendererCursorValidate( type, width, height, stride, &size)) { DEBUG_ERROR("Invalid cursor shape geometry"); return false; } const int decodedHeight = type == LG_CURSOR_MONOCHROME ? height / 2 : height; const size_t pixels = (size_t)width * (size_t)decodedHeight; LG_LOCK(cursor->lock); if (!cursorEnsureDecoded(cursor, pixels)) { LG_UNLOCK(cursor->lock); return false; } if (size > cursor->dataSize) { uint8_t * resized = realloc(cursor->data, size); if (!resized) { DEBUG_ERROR("Failed to malloc buffer for cursor shape"); LG_UNLOCK(cursor->lock); return false; } cursor->data = resized; cursor->dataSize = size; } memcpy(cursor->data, data, size); cursor->type = type; cursor->width = width; cursor->height = decodedHeight; cursor->stride = stride; cursor->update = true; LG_UNLOCK(cursor->lock); return true; } void egl_cursorSetColorTransform(EGL_Cursor * cursor, const LGColorTransform * transform) { LG_LOCK(cursor->lock); cursor->pendingColorTransform = *transform; cursor->colorTransformUpdate = true; LG_UNLOCK(cursor->lock); } void egl_cursorSetSize(EGL_Cursor * cursor, const float w, const float h) { struct CursorSize size = { .w = w, .h = h }; atomic_store(&cursor->size, size); } void egl_cursorSetScale(EGL_Cursor * cursor, const float scale) { atomic_store(&cursor->scale, scale); } void egl_cursorSetState(EGL_Cursor * cursor, const bool visible, const float x, const float y, const float hx, const float hy) { cursor->visible = visible; struct CursorPos pos = { .x = x , .y = y }; struct CursorPos hs = { .x = hx, .y = hy }; atomic_store(&cursor->pos, pos); atomic_store(&cursor->hs , hs); } struct CursorState egl_cursorRender(EGL_Cursor * cursor, LG_RendererRotate rotate, int width, int height, bool linearComposition, bool * logicalDeferred) { if (logicalDeferred) *logicalDeferred = false; if (!cursor->visible) return (struct CursorState) { .visible = false }; if (cursor->update || cursor->colorTransformUpdate) { LG_LOCK(cursor->lock); const bool updateShape = cursor->update; cursor->update = false; if (cursor->colorTransformUpdate) { cursor->activeColorTransform = cursor->pendingColorTransform; cursor->colorTransformUpdate = false; if (cursor->activeColorTransform.flags & LG_COLOR_TRANSFORM_LUT) { if (!cursor->colorLUT) glGenTextures(1, &cursor->colorLUT); egl_stateBindTexture(1, GL_TEXTURE_2D, cursor->colorLUT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 4096, 1, 0, GL_RGBA, GL_FLOAT, cursor->activeColorTransform.lut); } } if (updateShape) { const uint8_t * data = cursor->data; switch(cursor->type) { case LG_CURSOR_MASKED_COLOR: { for(int y = 0; y < cursor->height; ++y) { const uint8_t * row = data + (size_t)cursor->stride * (size_t)y; for(int x = 0; x < cursor->width; ++x) { uint32_t src; memcpy(&src, row + (size_t)x * sizeof(src), sizeof(src)); const size_t index = (size_t)y * (size_t)cursor->width + (size_t)x; const bool masked = (src & 0xFF000000) != 0; if (masked) cursor->normData[index] = cursor->monoData[index] = src & 0x00FFFFFF; else { cursor->normData[index] = src | 0xFF000000; cursor->monoData[index] = 0xFF000000; } } } egl_textureSetup(cursor->mono.texture, EGL_PF_BGRA, cursor->width, cursor->height, cursor->width, (size_t)cursor->width * sizeof(*cursor->monoData)); egl_textureUpdate(cursor->mono.texture, (const uint8_t *)cursor->monoData, true); egl_textureSetup(cursor->norm.texture, EGL_PF_BGRA, cursor->width, cursor->height, cursor->width, (size_t)cursor->width * sizeof(*cursor->normData)); egl_textureUpdate(cursor->norm.texture, (const uint8_t *)cursor->normData, true); break; } case LG_CURSOR_COLOR: for(int y = 0; y < cursor->height; ++y) memcpy(cursor->normData + (size_t)y * (size_t)cursor->width, data + (size_t)y * (size_t)cursor->stride, (size_t)cursor->width * sizeof(*cursor->normData)); egl_textureSetup(cursor->norm.texture, EGL_PF_BGRA, cursor->width, cursor->height, cursor->width, (size_t)cursor->width * sizeof(*cursor->normData)); egl_textureUpdate(cursor->norm.texture, (const uint8_t *)cursor->normData, true); break; case LG_CURSOR_MONOCHROME: { for(int y = 0; y < cursor->height; ++y) { const uint8_t * srcAnd = data + (size_t)cursor->stride * (size_t)y; const uint8_t * srcXor = data + (size_t)cursor->stride * (size_t)(y + cursor->height); for(int x = 0; x < cursor->width; ++x) { const uint8_t mask = 0x80 >> (x % 8); const uint32_t andMask = (srcAnd[x / 8] & mask) ? 0xFFFFFFFF : 0xFF000000; const uint32_t xorMask = (srcXor[x / 8] & mask) ? 0x00FFFFFF : 0x00000000; const size_t index = (size_t)y * (size_t)cursor->width + (size_t)x; cursor->normData[index] = andMask; cursor->monoData[index] = xorMask; } } // Monochrome cursors use AND/XOR mask textures - never convert to PQ egl_textureSetup(cursor->norm.texture, EGL_PF_BGRA, cursor->width, cursor->height, cursor->width, (size_t)cursor->width * sizeof(*cursor->normData)); egl_textureSetup(cursor->mono.texture, EGL_PF_BGRA, cursor->width, cursor->height, cursor->width, (size_t)cursor->width * sizeof(*cursor->monoData)); egl_textureUpdate(cursor->norm.texture, (const uint8_t *)cursor->normData, true); egl_textureUpdate(cursor->mono.texture, (const uint8_t *)cursor->monoData, true); break; } } } LG_UNLOCK(cursor->lock); } cursor->rotate = rotate; struct CursorPos pos = atomic_load(&cursor->pos ); float scale = atomic_load(&cursor->scale); struct CursorPos hs = atomic_load(&cursor->hs ); struct CursorSize size = atomic_load(&cursor->size ); pos.x -= hs.x * scale; pos.y -= hs.y * scale; size.w *= scale; size.h *= scale; struct CursorState state = { .visible = true, }; switch (rotate) { case LG_ROTATE_0: state.rect.x = (pos.x * width + width) / 2; state.rect.y = (-pos.y * height + height) / 2 - size.h * height; state.rect.w = size.w * width + 3; state.rect.h = size.h * height + 3; break; case LG_ROTATE_90: state.rect.x = (-pos.y * width + width) / 2 - size.h * width; state.rect.y = (-pos.x * height + height) / 2 - size.w * height; state.rect.w = size.h * width + 3; state.rect.h = size.w * height + 3; break; case LG_ROTATE_180: state.rect.x = (-pos.x * width + width) / 2 - size.w * width; state.rect.y = (pos.y * height + height) / 2; state.rect.w = size.w * width + 3; state.rect.h = size.h * height + 3; break; case LG_ROTATE_270: state.rect.x = (pos.y * width + width) / 2; state.rect.y = (pos.x * height + height) / 2; state.rect.w = size.h * width + 3; state.rect.h = size.w * height + 3; break; default: DEBUG_UNREACHABLE(); } state.rect.x = max(0, state.rect.x - 1); state.rect.y = max(0, state.rect.y - 1); if (linearComposition && cursor->type != LG_CURSOR_COLOR) { if (logicalDeferred) *logicalDeferred = true; return state; } egl_stateBlend(true); switch(cursor->type) { case LG_CURSOR_MONOCHROME: { setCursorTexUniforms(cursor, &cursor->norm, true, pos.x, pos.y, size.w, size.h, scale, false); egl_shaderUse(cursor->norm.shader); egl_stateBlendFunc(GL_ZERO, GL_SRC_COLOR); egl_modelSetTexture(cursor->model, cursor->norm.texture); egl_modelRender(cursor->model); setCursorTexUniforms(cursor, &cursor->mono, true, pos.x, pos.y, size.w, size.h, scale, false); egl_shaderUse(cursor->mono.shader); egl_stateBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO); egl_modelSetTexture(cursor->model, cursor->mono.texture); egl_modelRender(cursor->model); break; } case LG_CURSOR_MASKED_COLOR: { setCursorTexUniforms(cursor, &cursor->norm, false, pos.x, pos.y, size.w, size.h, scale, linearComposition); egl_shaderUse(cursor->norm.shader); egl_stateBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); egl_modelSetTexture(cursor->model, cursor->norm.texture); egl_modelRender(cursor->model); setCursorTexUniforms(cursor, &cursor->mono, false, pos.x, pos.y, size.w, size.h, scale, linearComposition); egl_shaderUse(cursor->mono.shader); egl_stateBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO); egl_modelSetTexture(cursor->model, cursor->mono.texture); egl_modelRender(cursor->model); break; } case LG_CURSOR_COLOR: { setCursorTexUniforms(cursor, &cursor->norm, false, pos.x, pos.y, size.w, size.h, scale, linearComposition); egl_shaderUse(cursor->norm.shader); egl_stateBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); egl_modelSetTexture(cursor->model, cursor->norm.texture); egl_modelRender(cursor->model); break; } } egl_stateBlend(false); return state; } void egl_cursorSetHDRState(EGL_Cursor * cursor, bool sourceHDR, bool nativeHDR, bool mapHDRtoSDR, bool hdrPQ, float mapGain, float mapContentPeak) { // The cursor's white level is supplied independently by the pointer queue. // Do not replace it with the frame-level fallback during every render. atomic_store(&cursor->sourceTransfer, !sourceHDR ? CURSOR_TRANSFER_SRGB : (hdrPQ ? CURSOR_TRANSFER_PQ : CURSOR_TRANSFER_SCRGB)); atomic_store(&cursor->mapHDRtoSDR, sourceHDR && !nativeHDR && mapHDRtoSDR); atomic_store(&cursor->mapHDRGain, mapGain); atomic_store(&cursor->mapHDRContentPeak, mapContentPeak); } void egl_cursorSetSDRWhiteLevel(EGL_Cursor * cursor, float sdrWhiteLevel) { atomic_store(&cursor->sdrWhiteLevel, sdrWhiteLevel > 0.0f ? sdrWhiteLevel : LG_SDR_WHITE_LEVEL_DEFAULT); }