Files
LookingGlass/client/src/render_queue.c
2026-08-13 04:28:09 +10:00

1554 lines
43 KiB
C

/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "render_queue.h"
#include <limits.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "common/debug.h"
#include "common/locking.h"
#include "main.h"
typedef struct RenderCommand
{
struct RenderCommand * next;
RenderQueueSource source;
uint64_t generation;
uint64_t cursorGeneration;
uint64_t transitionSerial;
enum
{
SW_SURFACE_OP_CONFIGURE_TRANSITION,
SW_SURFACE_OP_UPDATE,
SURFACE_OP_FORMAT,
CURSOR_OP_STATE,
CURSOR_OP_IMAGE,
CURSOR_OP_COLOR_TRANSFORM,
CURSOR_OP_WHITE_LEVEL,
CURSOR_OP_CLEAR,
SOURCE_OP_TRANSITION,
}
op;
union
{
struct
{
int width, height;
uint64_t epoch;
}
swSurfaceConfigureTransition;
struct
{
uint64_t epoch;
}
swSurfaceUpdate;
struct
{
LG_RendererFormat format;
bool rendererSupportsNativeHDR;
}
surfaceFormat;
struct
{
bool visible;
int x;
int y;
int hx;
int hy;
}
cursorState;
struct
{
LG_RendererCursor type;
int width;
int height;
int pitch;
uint8_t * data;
}
cursorImage;
struct
{
LGColorTransform * data;
}
cursorColorTransform;
struct
{
uint32_t value;
}
cursorWhiteLevel;
struct
{
bool swSurface;
}
sourceTransition;
};
}
RenderCommand;
typedef enum RenderQueueInvalidate
{
RENDER_QUEUE_INVALIDATE_NONE,
RENDER_QUEUE_INVALIDATE_PARTIAL,
RENDER_QUEUE_INVALIDATE_FULL,
}
RenderQueueInvalidate;
static RenderCommand * l_renderQueueHead;
static RenderCommand * l_renderQueueTail;
static LG_Lock l_renderQueueLock;
static bool l_renderQueueInitialized;
static RenderQueueInvalidate l_renderQueueInvalidate;
typedef struct RenderQueueSwSurface
{
LG_Lock lock;
/* Transport bitmap storage is borrowed, so callbacks compose it into this
* persistent image before returning. The renderer consumes it synchronously
* while lock is held. */
uint64_t generation;
uint64_t epoch;
int width;
int height;
int pitch;
uint8_t * data;
bool damageFull;
int damageCount;
FrameDamageRect damage[LG_MAX_FRAME_DAMAGE_RECTS];
bool updateQueued;
RenderCommand updateCommand;
}
RenderQueueSwSurface;
static RenderQueueSwSurface l_swSurface[RENDER_QUEUE_SOURCE_COUNT];
static RenderQueueSource l_swSurfaceResidentSource;
static uint64_t l_swSurfaceResidentGeneration;
static uint64_t l_swSurfaceResidentEpoch;
static RenderQueueSwSurface * swSurfaceFromUpdateCommand(
const RenderCommand * cmd)
{
for (int i = 0; i < RENDER_QUEUE_SOURCE_COUNT; ++i)
if (cmd == &l_swSurface[i].updateCommand)
return &l_swSurface[i];
return NULL;
}
static bool l_showSwSurface;
static bool l_surfaceFormatValid;
static bool l_rendererSupportsNativeHDR;
static LG_RendererFormat l_surfaceFormat;
static _Atomic(uint64_t) l_sourceGeneration[RENDER_QUEUE_SOURCE_COUNT];
static _Atomic(uint64_t) l_cursorGeneration[RENDER_QUEUE_SOURCE_COUNT];
static _Atomic(uint64_t) l_transitionSerial;
static LG_Lock l_sourceLock;
static LG_Lock l_transitionLock;
static RenderQueueSource l_appliedSource;
static uint64_t l_appliedGeneration;
static bool l_appliedSwSurface;
static RenderQueueSourcePrepareFn l_sourcePrepareFn;
static RenderQueueSourceRejectFn l_sourceRejectFn;
static RenderQueueSourceAppliedFn l_sourceAppliedFn;
static void * l_sourceCallbackOpaque;
typedef struct RenderQueueTransition
{
RenderQueueSource source;
uint64_t generation;
uint64_t serial;
bool swSurface;
bool valid;
}
RenderQueueTransition;
static RenderQueueTransition l_pendingTransition;
typedef struct RenderQueueCursor
{
uint64_t stateGeneration;
bool stateValid;
bool visible;
int x;
int y;
int hx;
int hy;
uint64_t imageGeneration;
bool imageValid;
LG_RendererCursor type;
int width;
int height;
int pitch;
uint8_t * data;
uint64_t colorGeneration;
bool colorValid;
LGColorTransform colorTransform;
uint64_t whiteGeneration;
bool whiteValid;
uint32_t sdrWhiteLevel;
}
RenderQueueCursor;
typedef struct RenderQueueFormat
{
uint64_t generation;
bool valid;
LG_RendererFormat format;
bool rendererSupportsNativeHDR;
}
RenderQueueFormat;
static RenderQueueCursor l_cursor[RENDER_QUEUE_SOURCE_COUNT];
static RenderQueueFormat l_format[RENDER_QUEUE_SOURCE_COUNT];
static const LGColorTransform l_identityColorTransform;
static bool sourceValid(RenderQueueSource source)
{
return source > RENDER_QUEUE_SOURCE_NONE &&
source < RENDER_QUEUE_SOURCE_COUNT;
}
static bool generationValid(RenderQueueSource source, uint64_t generation)
{
return sourceValid(source) && generation &&
atomic_load_explicit(&l_sourceGeneration[source],
memory_order_acquire) == generation;
}
static bool transitionCommand(const RenderCommand * cmd)
{
return cmd->op == SOURCE_OP_TRANSITION ||
cmd->op == SW_SURFACE_OP_CONFIGURE_TRANSITION;
}
static bool cursorCommand(const RenderCommand * cmd)
{
return cmd->op == CURSOR_OP_STATE ||
cmd->op == CURSOR_OP_IMAGE ||
cmd->op == CURSOR_OP_COLOR_TRANSFORM ||
cmd->op == CURSOR_OP_WHITE_LEVEL ||
cmd->op == CURSOR_OP_CLEAR;
}
static bool commandValid(const RenderCommand * cmd)
{
if (transitionCommand(cmd) &&
atomic_load_explicit(&l_transitionSerial, memory_order_acquire) !=
cmd->transitionSerial)
return false;
if (cmd->source == RENDER_QUEUE_SOURCE_NONE)
return cmd->op == SOURCE_OP_TRANSITION;
if (!sourceValid(cmd->source))
return false;
const bool cursorCurrent = !cursorCommand(cmd) || atomic_load_explicit(
&l_cursorGeneration[cmd->source], memory_order_acquire) ==
cmd->cursorGeneration;
if (cmd->op == CURSOR_OP_CLEAR)
return cursorCurrent;
return generationValid(cmd->source, cmd->generation) && cursorCurrent;
}
static uint64_t swSurfaceDamageArea(const FrameDamageRect * rect)
{
return (uint64_t)rect->width * rect->height;
}
static FrameDamageRect swSurfaceDamageUnion(
const FrameDamageRect * a, const FrameDamageRect * b)
{
const uint32_t left = min(a->x, b->x);
const uint32_t top = min(a->y, b->y);
const uint32_t right = max(a->x + a->width, b->x + b->width);
const uint32_t bottom = max(a->y + a->height, b->y + b->height);
return (FrameDamageRect)
{
.x = left,
.y = top,
.width = right - left,
.height = bottom - top,
};
}
static bool swSurfaceDamageTouches(
const FrameDamageRect * a, const FrameDamageRect * b)
{
return a->x <= b->x + b->width && b->x <= a->x + a->width &&
a->y <= b->y + b->height && b->y <= a->y + a->height;
}
static void swSurfaceDamageReset(RenderQueueSwSurface * surface)
{
surface->damageFull = false;
surface->damageCount = 0;
}
static bool swSurfaceDamagePending(const RenderQueueSwSurface * surface)
{
return surface->damageFull || surface->damageCount;
}
static void swSurfaceDamageAdd(RenderQueueSwSurface * surface,
const FrameDamageRect * damage)
{
if (surface->damageFull)
return;
if (damage->x == 0 && damage->y == 0 &&
damage->width == (uint32_t)surface->width &&
damage->height == (uint32_t)surface->height)
{
surface->damageFull = true;
surface->damageCount = 0;
return;
}
FrameDamageRect rect = *damage;
for (;;)
{
for (int i = 0; i < surface->damageCount;)
{
if (!swSurfaceDamageTouches(&rect, &surface->damage[i]))
{
++i;
continue;
}
rect = swSurfaceDamageUnion(&rect, &surface->damage[i]);
surface->damage[i] = surface->damage[--surface->damageCount];
i = 0;
}
if (surface->damageCount < LG_MAX_FRAME_DAMAGE_RECTS)
break;
int best = 0;
uint64_t bestCost = UINT64_MAX;
for (int i = 0; i < surface->damageCount; ++i)
{
const FrameDamageRect merged =
swSurfaceDamageUnion(&rect, &surface->damage[i]);
const uint64_t cost = swSurfaceDamageArea(&merged) -
swSurfaceDamageArea(&surface->damage[i]);
if (cost < bestCost)
{
best = i;
bestCost = cost;
}
}
rect = swSurfaceDamageUnion(&rect, &surface->damage[best]);
surface->damage[best] = surface->damage[--surface->damageCount];
}
surface->damage[surface->damageCount++] = rect;
uint64_t area = 0;
for (int i = 0; i < surface->damageCount; ++i)
area += swSurfaceDamageArea(&surface->damage[i]);
const uint64_t fullArea =
(uint64_t)surface->width * surface->height;
if (area >= fullArea - fullArea / 4)
{
surface->damageFull = true;
surface->damageCount = 0;
}
}
static void freeCommand(RenderCommand * cmd)
{
if (cmd->op == SW_SURFACE_OP_UPDATE)
return;
switch (cmd->op)
{
case CURSOR_OP_IMAGE:
free(cmd->cursorImage.data);
break;
case CURSOR_OP_COLOR_TRANSFORM:
free(cmd->cursorColorTransform.data);
break;
default:
break;
}
free(cmd);
}
static bool queueCommand(RenderCommand * cmd,
RenderQueueInvalidate invalidate)
{
bool wake = false;
cmd->next = NULL;
LG_LOCK(l_renderQueueLock);
if (l_renderQueueTail)
l_renderQueueTail->next = cmd;
else
l_renderQueueHead = cmd;
l_renderQueueTail = cmd;
if (invalidate > l_renderQueueInvalidate)
{
l_renderQueueInvalidate = invalidate;
wake = true;
}
LG_UNLOCK(l_renderQueueLock);
return wake;
}
/* The surface lock must be held while its embedded command is queued. */
static bool queueSwSurfaceUpdate(RenderQueueSource source,
RenderQueueSwSurface * surface)
{
if (surface->updateQueued || !surface->data ||
!swSurfaceDamagePending(surface))
return false;
RenderCommand * cmd = &surface->updateCommand;
cmd->source = source;
cmd->generation = surface->generation;
cmd->op = SW_SURFACE_OP_UPDATE;
cmd->swSurfaceUpdate.epoch = surface->epoch;
surface->updateQueued = true;
return queueCommand(cmd, RENDER_QUEUE_INVALIDATE_PARTIAL);
}
static void wakeQueue(bool wake)
{
if (wake)
app_invalidateWindow(false);
}
static void enqueueCommand(RenderCommand * cmd,
RenderQueueInvalidate invalidate)
{
wakeQueue(queueCommand(cmd, invalidate));
}
static RenderCommand * detachCommands(RenderQueueInvalidate * invalidate)
{
LG_LOCK(l_renderQueueLock);
RenderCommand * head = l_renderQueueHead;
if (invalidate)
*invalidate = l_renderQueueInvalidate;
l_renderQueueHead = NULL;
l_renderQueueTail = NULL;
l_renderQueueInvalidate = RENDER_QUEUE_INVALIDATE_NONE;
LG_UNLOCK(l_renderQueueLock);
return head;
}
static void setCommandSource(RenderCommand * cmd, RenderQueueSource source,
uint64_t generation)
{
cmd->source = source;
cmd->generation = generation;
cmd->cursorGeneration = sourceValid(source) ? atomic_load_explicit(
&l_cursorGeneration[source], memory_order_acquire) : 0;
}
static bool copyCursorImage(RenderCommand * cmd, const void * data)
{
if (!data || cmd->cursorImage.width <= 0 ||
cmd->cursorImage.height <= 0 || cmd->cursorImage.pitch <= 0)
return false;
if ((size_t)cmd->cursorImage.height >
SIZE_MAX / (size_t)cmd->cursorImage.pitch)
return false;
const size_t size =
(size_t)cmd->cursorImage.height * (size_t)cmd->cursorImage.pitch;
cmd->cursorImage.data = malloc(size);
if (!cmd->cursorImage.data)
return false;
memcpy(cmd->cursorImage.data, data, size);
return true;
}
static void updateSurfaceFormat(void)
{
if (!g_state.ds->setHDRImageDesc)
return;
LG_RendererFormat format = {};
if (l_surfaceFormatValid)
format = l_surfaceFormat;
if (l_showSwSurface)
{
// The software surface is always 8-bit SDR, regardless of the last frame
// format received from the active frame provider.
format.hdr = false;
format.hdrPQ = false;
format.hdrMetadata = false;
g_state.ds->setHDRImageDesc(&format);
atomic_store(&g_state.hdrDescFailed, false);
return;
}
if (!l_surfaceFormatValid)
{
g_state.ds->setHDRImageDesc(&format);
atomic_store(&g_state.hdrDescFailed, false);
return;
}
if (format.hdr && !l_rendererSupportsNativeHDR)
{
format.hdr = false;
format.hdrPQ = false;
format.hdrMetadata = false;
g_state.ds->setHDRImageDesc(&format);
DEBUG_WARN("Renderer surface cannot represent the native HDR encoding; "
"using software HDR mapping");
atomic_store(&g_state.hdrDescFailed, true);
}
else if (!g_state.ds->setHDRImageDesc(&format))
{
DEBUG_WARN("Display server failed to apply HDR image description");
atomic_store(&g_state.hdrDescFailed, true);
}
else
atomic_store(&g_state.hdrDescFailed, false);
}
void renderQueue_init(void)
{
l_renderQueueHead = NULL;
l_renderQueueTail = NULL;
l_renderQueueInvalidate = RENDER_QUEUE_INVALIDATE_NONE;
l_showSwSurface = false;
l_surfaceFormatValid = false;
l_rendererSupportsNativeHDR = false;
l_appliedSource = RENDER_QUEUE_SOURCE_NONE;
l_appliedGeneration = 0;
l_appliedSwSurface = false;
l_swSurfaceResidentSource = RENDER_QUEUE_SOURCE_NONE;
l_swSurfaceResidentGeneration = 0;
l_swSurfaceResidentEpoch = 0;
l_sourcePrepareFn = NULL;
l_sourceRejectFn = NULL;
l_sourceAppliedFn = NULL;
l_sourceCallbackOpaque = NULL;
l_pendingTransition = (RenderQueueTransition) {};
memset(&l_surfaceFormat, 0, sizeof(l_surfaceFormat));
memset(l_cursor, 0, sizeof(l_cursor));
memset(l_format, 0, sizeof(l_format));
memset(l_swSurface, 0, sizeof(l_swSurface));
for (int i = 0; i < RENDER_QUEUE_SOURCE_COUNT; ++i)
{
atomic_store_explicit(&l_sourceGeneration[i], 0, memory_order_relaxed);
atomic_store_explicit(&l_cursorGeneration[i], 1, memory_order_relaxed);
LG_LOCK_INIT(l_swSurface[i].lock);
}
atomic_store_explicit(&l_transitionSerial, 0, memory_order_relaxed);
LG_LOCK_INIT(l_renderQueueLock);
LG_LOCK_INIT(l_sourceLock);
LG_LOCK_INIT(l_transitionLock);
l_renderQueueInitialized = true;
}
void renderQueue_free(void)
{
if (!l_renderQueueInitialized)
return;
renderQueue_clear();
for (int i = 0; i < RENDER_QUEUE_SOURCE_COUNT; ++i)
{
free(l_cursor[i].data);
l_cursor[i].data = NULL;
free(l_swSurface[i].data);
l_swSurface[i].data = NULL;
LG_LOCK_FREE(l_swSurface[i].lock);
}
l_renderQueueInitialized = false;
LG_LOCK_FREE(l_renderQueueLock);
LG_LOCK_FREE(l_sourceLock);
LG_LOCK_FREE(l_transitionLock);
}
void renderQueue_clear(void)
{
RenderCommand * cmd = detachCommands(NULL);
while(cmd)
{
RenderCommand * next = cmd->next;
RenderQueueSwSurface * surface = swSurfaceFromUpdateCommand(cmd);
if (surface)
{
LG_LOCK(surface->lock);
surface->updateQueued = false;
LG_UNLOCK(surface->lock);
}
else
freeCommand(cmd);
cmd = next;
}
}
void renderQueue_setSourceFns(RenderQueueSourcePrepareFn prepare,
RenderQueueSourceRejectFn reject,
RenderQueueSourceAppliedFn applied, void * opaque)
{
l_sourcePrepareFn = prepare;
l_sourceRejectFn = reject;
l_sourceAppliedFn = applied;
l_sourceCallbackOpaque = opaque;
}
uint64_t renderQueue_sourceBegin(RenderQueueSource source)
{
if (!sourceValid(source))
return 0;
LG_LOCK(l_sourceLock);
LG_LOCK(l_swSurface[source].lock);
const uint64_t previous = atomic_load_explicit(
&l_sourceGeneration[source], memory_order_relaxed);
const uint64_t generation = previous + 1;
atomic_store_explicit(
&l_sourceGeneration[source], generation, memory_order_release);
RenderQueueCursor * cursor = &l_cursor[source];
if (cursor->stateValid && cursor->stateGeneration == previous)
cursor->stateGeneration = generation;
if (cursor->imageValid && cursor->imageGeneration == previous)
cursor->imageGeneration = generation;
if (cursor->colorValid && cursor->colorGeneration == previous)
cursor->colorGeneration = generation;
if (cursor->whiteValid && cursor->whiteGeneration == previous)
cursor->whiteGeneration = generation;
LG_UNLOCK(l_swSurface[source].lock);
LG_UNLOCK(l_sourceLock);
return generation;
}
void renderQueue_sourceInvalidate(RenderQueueSource source,
uint64_t generation)
{
if (!sourceValid(source) || !generation)
return;
LG_LOCK(l_sourceLock);
LG_LOCK(l_swSurface[source].lock);
const uint64_t invalidGeneration = generation + 1;
if (atomic_compare_exchange_strong_explicit(&l_sourceGeneration[source],
&generation, invalidGeneration, memory_order_acq_rel,
memory_order_relaxed))
{
RenderQueueCursor * cursor = &l_cursor[source];
if (cursor->stateValid && cursor->stateGeneration == generation)
cursor->stateGeneration = invalidGeneration;
if (cursor->imageValid && cursor->imageGeneration == generation)
cursor->imageGeneration = invalidGeneration;
if (cursor->colorValid && cursor->colorGeneration == generation)
cursor->colorGeneration = invalidGeneration;
if (cursor->whiteValid && cursor->whiteGeneration == generation)
cursor->whiteGeneration = invalidGeneration;
}
LG_UNLOCK(l_swSurface[source].lock);
LG_UNLOCK(l_sourceLock);
}
void renderQueue_sourceClearCursor(RenderQueueSource source)
{
if (!sourceValid(source))
return;
LG_LOCK(l_sourceLock);
uint64_t cursorGeneration = atomic_load_explicit(
&l_cursorGeneration[source], memory_order_relaxed) + 1;
if (!cursorGeneration)
++cursorGeneration;
atomic_store_explicit(&l_cursorGeneration[source], cursorGeneration,
memory_order_release);
free(l_cursor[source].data);
memset(&l_cursor[source], 0, sizeof(l_cursor[source]));
const uint64_t sourceGeneration = atomic_load_explicit(
&l_sourceGeneration[source], memory_order_acquire);
RenderCommand * cmd = malloc(sizeof(*cmd));
bool wake = false;
if (cmd)
{
setCommandSource(cmd, source, sourceGeneration);
cmd->op = CURSOR_OP_CLEAR;
wake = queueCommand(cmd, RENDER_QUEUE_INVALIDATE_PARTIAL);
}
LG_UNLOCK(l_sourceLock);
wakeQueue(wake);
}
static bool configureSwSurface(RenderQueueSource source,
uint64_t generation, int width, int height, uint64_t * epoch)
{
if (width <= 0 || height <= 0 || width > INT_MAX / 4)
{
DEBUG_ERROR("Invalid software surface dimensions: %dx%d", width, height);
return false;
}
const int pitch = width * 4;
if ((size_t)height > SIZE_MAX / (size_t)pitch)
{
DEBUG_ERROR("Software surface size overflows: %dx%d", width, height);
return false;
}
RenderQueueSwSurface * surface = &l_swSurface[source];
LG_LOCK(surface->lock);
if (generationValid(source, generation) && surface->data &&
surface->generation == generation &&
surface->width == width && surface->height == height)
{
*epoch = surface->epoch;
LG_UNLOCK(surface->lock);
return true;
}
LG_UNLOCK(surface->lock);
uint8_t * data = calloc((size_t)height, (size_t)pitch);
if (!data)
{
DEBUG_ERROR("Failed to allocate software surface: %dx%d", width, height);
return false;
}
LG_LOCK(surface->lock);
if (!generationValid(source, generation))
{
LG_UNLOCK(surface->lock);
free(data);
return false;
}
if (surface->data && surface->generation == generation &&
surface->width == width && surface->height == height)
{
*epoch = surface->epoch;
LG_UNLOCK(surface->lock);
free(data);
return true;
}
uint64_t nextEpoch = surface->epoch + 1;
if (!nextEpoch)
++nextEpoch;
free(surface->data);
surface->generation = generation;
surface->epoch = nextEpoch;
surface->width = width;
surface->height = height;
surface->pitch = pitch;
surface->data = data;
swSurfaceDamageReset(surface);
*epoch = nextEpoch;
LG_UNLOCK(surface->lock);
return true;
}
bool renderQueue_sourceSwSurfaceConfigure(RenderQueueSource source,
uint64_t generation, int width, int height)
{
if (!generationValid(source, generation))
return false;
uint64_t epoch;
return configureSwSurface(source, generation, width, height, &epoch);
}
static bool getSwSurfaceConfiguration(RenderQueueSource source,
uint64_t generation, int width, int height, uint64_t * epoch)
{
RenderQueueSwSurface * surface = &l_swSurface[source];
LG_LOCK(surface->lock);
const bool valid = generationValid(source, generation) && surface->data &&
surface->generation == generation &&
surface->width == width && surface->height == height;
if (valid)
*epoch = surface->epoch;
LG_UNLOCK(surface->lock);
return valid;
}
static uint64_t enqueueTransition(RenderCommand * cmd,
atomic_uint_least64_t * publishedSerial)
{
LG_LOCK(l_transitionLock);
const uint64_t serial = atomic_load_explicit(
&l_transitionSerial, memory_order_relaxed) + 1;
cmd->transitionSerial = serial;
const bool wake = queueCommand(cmd, RENDER_QUEUE_INVALIDATE_FULL);
atomic_store_explicit(
&l_transitionSerial, serial, memory_order_release);
if (publishedSerial)
atomic_store_explicit(publishedSerial, serial, memory_order_release);
LG_UNLOCK(l_transitionLock);
wakeQueue(wake);
return serial;
}
uint64_t renderQueue_sourceTransition(RenderQueueSource source,
uint64_t generation, bool swSurface,
atomic_uint_least64_t * publishedSerial)
{
if (source != RENDER_QUEUE_SOURCE_NONE &&
!generationValid(source, generation))
return 0;
if (source == RENDER_QUEUE_SOURCE_NONE)
{
generation = 0;
swSurface = false;
}
RenderCommand * cmd = malloc(sizeof(*cmd));
if (!cmd)
return 0;
setCommandSource(cmd, source, generation);
cmd->op = SOURCE_OP_TRANSITION;
cmd->sourceTransition.swSurface = swSurface;
return enqueueTransition(cmd, publishedSerial);
}
uint64_t renderQueue_sourceSwSurfaceConfigureTransition(
RenderQueueSource source, uint64_t generation, int width, int height,
atomic_uint_least64_t * publishedSerial)
{
if (!generationValid(source, generation))
return 0;
RenderCommand * cmd = malloc(sizeof(*cmd));
if (!cmd)
return 0;
uint64_t epoch;
if (!getSwSurfaceConfiguration(
source, generation, width, height, &epoch))
{
free(cmd);
return 0;
}
setCommandSource(cmd, source, generation);
cmd->op =
SW_SURFACE_OP_CONFIGURE_TRANSITION;
cmd->swSurfaceConfigureTransition.width = width;
cmd->swSurfaceConfigureTransition.height = height;
cmd->swSurfaceConfigureTransition.epoch = epoch;
return enqueueTransition(cmd, publishedSerial);
}
static bool clipSwSurfaceRect(const RenderQueueSwSurface * surface,
int * x, int * y, int * width, int * height,
int * sourceX, int * sourceY)
{
const int64_t left = max((int64_t)*x, 0);
const int64_t top = max((int64_t)*y, 0);
const int64_t right = min((int64_t)*x + *width,
(int64_t)surface->width);
const int64_t bottom = min((int64_t)*y + *height,
(int64_t)surface->height);
if (right <= left || bottom <= top)
return false;
if (sourceX)
*sourceX = (int)(left - *x);
if (sourceY)
*sourceY = (int)(top - *y);
*x = (int)left;
*y = (int)top;
*width = (int)(right - left);
*height = (int)(bottom - top);
return true;
}
void renderQueue_sourceSwSurfaceDrawFill(RenderQueueSource source,
uint64_t generation, int x, int y, int width, int height,
uint32_t color)
{
if (!generationValid(source, generation) || width <= 0 || height <= 0)
return;
RenderQueueSwSurface * surface = &l_swSurface[source];
LG_LOCK(surface->lock);
if (surface->generation != generation ||
!generationValid(source, generation) ||
!clipSwSurfaceRect(surface,
&x, &y, &width, &height, NULL, NULL))
{
LG_UNLOCK(surface->lock);
return;
}
uint8_t * row = surface->data +
(size_t)y * surface->pitch + (size_t)x * 4;
for (int dy = 0; dy < height; ++dy)
{
uint32_t * dst = (uint32_t *)row;
for (int dx = 0; dx < width; ++dx)
dst[dx] = color;
row += surface->pitch;
}
const FrameDamageRect damage =
{
.x = (uint32_t)x,
.y = (uint32_t)y,
.width = (uint32_t)width,
.height = (uint32_t)height,
};
swSurfaceDamageAdd(surface, &damage);
const bool wake = queueSwSurfaceUpdate(source, surface);
LG_UNLOCK(surface->lock);
wakeQueue(wake);
}
void renderQueue_sourceSwSurfaceDrawBitmap(RenderQueueSource source,
uint64_t generation, int x, int y, int width, int height, int stride,
const void * data, bool topDown)
{
if (!generationValid(source, generation))
return;
if (width <= 0 || height <= 0 || stride <= 0)
{
if (width < 0 || height < 0 || stride < 0)
DEBUG_ERROR("Invalid software surface bitmap dimensions: "
"%dx%d, stride: %d",
width, height, stride);
return;
}
if (!data)
{
DEBUG_ERROR("Software surface bitmap data is NULL");
return;
}
if (width > stride / 4)
{
DEBUG_ERROR("Software surface bitmap stride is too small: "
"width: %d, stride: %d", width, stride);
return;
}
if ((size_t)height > SIZE_MAX / (size_t)stride)
{
DEBUG_ERROR("Software surface bitmap size overflows: "
"height: %d, stride: %d", height, stride);
return;
}
RenderQueueSwSurface * surface = &l_swSurface[source];
LG_LOCK(surface->lock);
int sourceX;
int sourceY;
const int sourceHeight = height;
if (surface->generation != generation ||
!generationValid(source, generation) ||
!clipSwSurfaceRect(surface,
&x, &y, &width, &height, &sourceX, &sourceY))
{
LG_UNLOCK(surface->lock);
return;
}
const size_t copySize = (size_t)width * 4;
uint8_t * dst = surface->data +
(size_t)y * surface->pitch + (size_t)x * 4;
for (int dy = 0; dy < height; ++dy)
{
const int sourceRow = topDown ?
sourceY + dy : sourceHeight - sourceY - dy - 1;
const uint8_t * src = (const uint8_t *)data +
(size_t)sourceRow * stride + (size_t)sourceX * 4;
memcpy(dst, src, copySize);
dst += surface->pitch;
}
const FrameDamageRect damage =
{
.x = (uint32_t)x,
.y = (uint32_t)y,
.width = (uint32_t)width,
.height = (uint32_t)height,
};
swSurfaceDamageAdd(surface, &damage);
const bool wake = queueSwSurfaceUpdate(source, surface);
LG_UNLOCK(surface->lock);
wakeQueue(wake);
}
void renderQueue_sourceSurfaceFormat(RenderQueueSource source,
uint64_t generation, const LG_RendererFormat format,
bool rendererSupportsNativeHDR)
{
if (!generationValid(source, generation))
return;
RenderCommand * cmd = malloc(sizeof(*cmd));
if (!cmd)
return;
setCommandSource(cmd, source, generation);
cmd->op = SURFACE_OP_FORMAT;
cmd->surfaceFormat.format = format;
cmd->surfaceFormat.rendererSupportsNativeHDR =
rendererSupportsNativeHDR;
enqueueCommand(cmd, RENDER_QUEUE_INVALIDATE_FULL);
}
void renderQueue_sourceCursorState(RenderQueueSource source,
uint64_t generation, bool visible, int x, int y, int hx, int hy)
{
if (!generationValid(source, generation))
return;
RenderCommand * cmd = malloc(sizeof(*cmd));
if (!cmd)
return;
setCommandSource(cmd, source, generation);
cmd->op = CURSOR_OP_STATE;
cmd->cursorState.visible = visible;
cmd->cursorState.x = x;
cmd->cursorState.y = y;
cmd->cursorState.hx = hx;
cmd->cursorState.hy = hy;
enqueueCommand(cmd, RENDER_QUEUE_INVALIDATE_NONE);
}
void renderQueue_sourceCursorImage(RenderQueueSource source,
uint64_t generation, LG_RendererCursor type,
int width, int height, int pitch, const void * data)
{
if (!generationValid(source, generation))
return;
RenderCommand * cmd = malloc(sizeof(*cmd));
if (!cmd)
return;
setCommandSource(cmd, source, generation);
cmd->op = CURSOR_OP_IMAGE;
cmd->cursorImage.type = type;
cmd->cursorImage.width = width;
cmd->cursorImage.height = height;
cmd->cursorImage.pitch = pitch;
cmd->cursorImage.data = NULL;
if (!copyCursorImage(cmd, data))
freeCommand(cmd);
else
enqueueCommand(cmd, RENDER_QUEUE_INVALIDATE_NONE);
}
void renderQueue_sourceCursorColorTransform(RenderQueueSource source,
uint64_t generation, const LGColorTransform * transform)
{
if (!generationValid(source, generation) || !transform)
return;
RenderCommand * cmd = malloc(sizeof(*cmd));
if (!cmd)
return;
LGColorTransform * copy = malloc(sizeof(*copy));
if (!copy)
{
free(cmd);
return;
}
*copy = *transform;
setCommandSource(cmd, source, generation);
cmd->op = CURSOR_OP_COLOR_TRANSFORM;
cmd->cursorColorTransform.data = copy;
enqueueCommand(cmd, RENDER_QUEUE_INVALIDATE_NONE);
}
void renderQueue_sourceCursorWhiteLevel(RenderQueueSource source,
uint64_t generation, uint32_t sdrWhiteLevel)
{
if (!generationValid(source, generation))
return;
RenderCommand * cmd = malloc(sizeof(*cmd));
if (!cmd)
return;
setCommandSource(cmd, source, generation);
cmd->op = CURSOR_OP_WHITE_LEVEL;
cmd->cursorWhiteLevel.value = sdrWhiteLevel;
enqueueCommand(cmd, RENDER_QUEUE_INVALIDATE_NONE);
}
static bool swSurfaceCommandMatches(const RenderQueueSwSurface * surface,
const RenderCommand * cmd, uint64_t epoch)
{
return surface->data && surface->generation == cmd->generation &&
surface->epoch == epoch;
}
static void uploadSwSurfaceFull(RenderQueueSource source,
RenderQueueSwSurface * surface)
{
RENDERER(swSurfaceConfigure, surface->width, surface->height);
RENDERER(swSurfaceDrawBitmap,
0, 0, surface->width, surface->height,
surface->pitch, surface->data, true);
swSurfaceDamageReset(surface);
l_swSurfaceResidentSource = source;
l_swSurfaceResidentGeneration = surface->generation;
l_swSurfaceResidentEpoch = surface->epoch;
}
static bool swSurfaceResident(RenderQueueSource source,
const RenderQueueSwSurface * surface)
{
return l_swSurfaceResidentSource == source &&
l_swSurfaceResidentGeneration == surface->generation &&
l_swSurfaceResidentEpoch == surface->epoch;
}
static void uploadSwSurfaceDamage(RenderQueueSource source,
RenderQueueSwSurface * surface)
{
if (!swSurfaceResident(source, surface))
{
uploadSwSurfaceFull(source, surface);
return;
}
if (surface->damageFull)
RENDERER(swSurfaceDrawBitmap,
0, 0, surface->width, surface->height,
surface->pitch, surface->data, true);
else
for (int i = 0; i < surface->damageCount; ++i)
{
const FrameDamageRect * damage = &surface->damage[i];
uint8_t * data = surface->data +
(size_t)damage->y * surface->pitch + (size_t)damage->x * 4;
RENDERER(swSurfaceDrawBitmap,
damage->x, damage->y, damage->width, damage->height,
surface->pitch, data, true);
}
swSurfaceDamageReset(surface);
}
static bool swSurfaceTransitionMatches(
const RenderQueueSwSurface * surface, const RenderCommand * cmd,
bool configure)
{
const uint64_t epoch = configure ?
cmd->swSurfaceConfigureTransition.epoch : surface->epoch;
return swSurfaceCommandMatches(surface, cmd, epoch) &&
(!configure ||
(surface->width == cmd->swSurfaceConfigureTransition.width &&
surface->height == cmd->swSurfaceConfigureTransition.height));
}
static bool prepareSwSurfaceTransition(const RenderCommand * cmd,
bool configure)
{
RenderQueueSwSurface * surface = &l_swSurface[cmd->source];
LG_LOCK(surface->lock);
const bool valid = swSurfaceTransitionMatches(surface, cmd, configure);
if (valid)
{
if (configure || !swSurfaceResident(cmd->source, surface))
uploadSwSurfaceFull(cmd->source, surface);
else
uploadSwSurfaceDamage(cmd->source, surface);
}
LG_UNLOCK(surface->lock);
return valid;
}
static bool swSurfaceTransitionReady(const RenderCommand * cmd,
bool configure)
{
RenderQueueSwSurface * surface = &l_swSurface[cmd->source];
LG_LOCK(surface->lock);
const bool ready = swSurfaceTransitionMatches(surface, cmd, configure);
LG_UNLOCK(surface->lock);
return ready;
}
/* Called with l_sourceLock held. The marker may be recycled into the live
* queue after an older generation was detached from it. */
static bool processSwSurfaceUpdate(RenderCommand * cmd, bool commandValid)
{
RenderQueueSwSurface * surface = &l_swSurface[cmd->source];
LG_LOCK(surface->lock);
if (!surface->updateQueued)
{
LG_UNLOCK(surface->lock);
return false;
}
surface->updateQueued = false;
const bool matches = commandValid &&
swSurfaceCommandMatches(surface, cmd, cmd->swSurfaceUpdate.epoch);
const bool active = l_appliedSwSurface &&
l_appliedSource == cmd->source &&
l_appliedGeneration == cmd->generation;
if (matches && active)
uploadSwSurfaceDamage(cmd->source, surface);
bool wake = false;
if (swSurfaceDamagePending(surface) &&
l_appliedSwSurface && l_appliedSource == cmd->source &&
l_appliedGeneration == surface->generation &&
generationValid(cmd->source, surface->generation))
wake = queueSwSurfaceUpdate(cmd->source, surface);
LG_UNLOCK(surface->lock);
return wake;
}
static void applyCursor(RenderQueueSource source, uint64_t generation)
{
RenderQueueCursor * cursor = &l_cursor[source];
if (cursor->imageValid && cursor->imageGeneration == generation)
RENDERER(onMouseShape, cursor->type, cursor->width, cursor->height,
cursor->pitch, cursor->data);
if (cursor->imageValid && cursor->imageGeneration == generation &&
cursor->stateValid && cursor->stateGeneration == generation)
RENDERER(onMouseEvent, cursor->visible, cursor->x, cursor->y,
cursor->hx, cursor->hy);
else
RENDERER(onMouseEvent, false, 0, 0, 0, 0);
if (g_state.lgr->ops.onMouseColorTransform)
g_state.lgr->ops.onMouseColorTransform(g_state.lgr,
cursor->colorValid && cursor->colorGeneration == generation ?
&cursor->colorTransform : &l_identityColorTransform);
if (g_state.lgr->ops.onMouseWhiteLevel)
g_state.lgr->ops.onMouseWhiteLevel(g_state.lgr,
cursor->whiteValid && cursor->whiteGeneration == generation ?
cursor->sdrWhiteLevel : LG_SDR_WHITE_LEVEL_DEFAULT);
}
static void applySourceTransition(const RenderCommand * cmd,
bool showSwSurface)
{
if (l_showSwSurface != showSwSurface)
{
RENDERER(swSurfaceShow, showSwSurface);
l_showSwSurface = showSwSurface;
}
l_surfaceFormatValid = false;
l_rendererSupportsNativeHDR = false;
if (sourceValid(cmd->source))
{
const RenderQueueFormat * format = &l_format[cmd->source];
if (format->valid && format->generation == cmd->generation)
{
l_surfaceFormat = format->format;
l_surfaceFormatValid = true;
l_rendererSupportsNativeHDR =
format->rendererSupportsNativeHDR;
}
}
updateSurfaceFormat();
l_appliedSource = cmd->source;
l_appliedGeneration = cmd->generation;
l_appliedSwSurface = showSwSurface;
if (sourceValid(cmd->source))
applyCursor(cmd->source, cmd->generation);
else
RENDERER(onMouseEvent, false, 0, 0, 0, 0);
l_pendingTransition.source = cmd->source;
l_pendingTransition.generation = cmd->generation;
l_pendingTransition.serial = cmd->transitionSerial;
l_pendingTransition.swSurface = l_appliedSwSurface;
l_pendingTransition.valid = true;
}
static bool prepareSourceTransition(const RenderCommand * cmd)
{
return !l_sourcePrepareFn || l_sourcePrepareFn(l_sourceCallbackOpaque,
cmd->source, cmd->generation, cmd->transitionSerial);
}
static void rejectSourceTransition(const RenderCommand * cmd)
{
if (l_sourceRejectFn)
l_sourceRejectFn(l_sourceCallbackOpaque,
cmd->source, cmd->transitionSerial);
}
bool renderQueue_process(void)
{
RenderQueueInvalidate invalidate;
RenderCommand * cmd = detachCommands(&invalidate);
while(cmd)
{
RenderCommand * next = cmd->next;
const bool transition = transitionCommand(cmd);
bool wake = false;
if (transition)
LG_LOCK(l_transitionLock);
LG_LOCK(l_sourceLock);
const bool validCommand = commandValid(cmd);
if (!validCommand)
{
if (cmd->op == SW_SURFACE_OP_UPDATE)
wake = processSwSurfaceUpdate(cmd, false);
LG_UNLOCK(l_sourceLock);
if (transition)
LG_UNLOCK(l_transitionLock);
freeCommand(cmd);
wakeQueue(wake);
cmd = next;
continue;
}
switch(cmd->op)
{
case SW_SURFACE_OP_CONFIGURE_TRANSITION:
if (!swSurfaceTransitionReady(cmd, true))
{
DEBUG_ERROR("Software surface configuration is unavailable");
rejectSourceTransition(cmd);
break;
}
if (!prepareSourceTransition(cmd))
break;
if (!prepareSwSurfaceTransition(cmd, true))
{
rejectSourceTransition(cmd);
break;
}
applySourceTransition(cmd, true);
break;
case SW_SURFACE_OP_UPDATE:
wake = processSwSurfaceUpdate(cmd, true);
break;
case SURFACE_OP_FORMAT:
{
RenderQueueFormat * format = &l_format[cmd->source];
format->generation = cmd->generation;
format->valid = true;
format->format = cmd->surfaceFormat.format;
format->rendererSupportsNativeHDR =
cmd->surfaceFormat.rendererSupportsNativeHDR;
if (l_appliedSource == cmd->source &&
l_appliedGeneration == cmd->generation &&
!l_appliedSwSurface)
{
l_surfaceFormat = format->format;
l_surfaceFormatValid = true;
l_rendererSupportsNativeHDR =
format->rendererSupportsNativeHDR;
updateSurfaceFormat();
}
break;
}
case CURSOR_OP_STATE:
{
RenderQueueCursor * cursor = &l_cursor[cmd->source];
cursor->stateGeneration = cmd->generation;
cursor->stateValid = true;
cursor->visible = cmd->cursorState.visible;
cursor->x = cmd->cursorState.x;
cursor->y = cmd->cursorState.y;
cursor->hx = cmd->cursorState.hx;
cursor->hy = cmd->cursorState.hy;
if (l_appliedSource == cmd->source &&
l_appliedGeneration == cmd->generation)
RENDERER(onMouseEvent,
cursor->imageValid &&
cursor->imageGeneration == cmd->generation &&
cursor->visible,
cursor->x, cursor->y, cursor->hx, cursor->hy);
break;
}
case CURSOR_OP_IMAGE:
{
RenderQueueCursor * cursor = &l_cursor[cmd->source];
free(cursor->data);
cursor->imageGeneration = cmd->generation;
cursor->imageValid = true;
cursor->type = cmd->cursorImage.type;
cursor->width = cmd->cursorImage.width;
cursor->height = cmd->cursorImage.height;
cursor->pitch = cmd->cursorImage.pitch;
cursor->data = cmd->cursorImage.data;
cmd->cursorImage.data = NULL;
if (l_appliedSource == cmd->source &&
l_appliedGeneration == cmd->generation)
{
RENDERER(onMouseShape, cursor->type, cursor->width,
cursor->height, cursor->pitch, cursor->data);
if (cursor->stateValid &&
cursor->stateGeneration == cmd->generation)
RENDERER(onMouseEvent, cursor->visible, cursor->x, cursor->y,
cursor->hx, cursor->hy);
}
break;
}
case CURSOR_OP_COLOR_TRANSFORM:
{
RenderQueueCursor * cursor = &l_cursor[cmd->source];
cursor->colorGeneration = cmd->generation;
cursor->colorValid = true;
cursor->colorTransform = *cmd->cursorColorTransform.data;
if (l_appliedSource == cmd->source &&
l_appliedGeneration == cmd->generation &&
g_state.lgr->ops.onMouseColorTransform)
g_state.lgr->ops.onMouseColorTransform(g_state.lgr,
&cursor->colorTransform);
break;
}
case CURSOR_OP_WHITE_LEVEL:
{
RenderQueueCursor * cursor = &l_cursor[cmd->source];
cursor->whiteGeneration = cmd->generation;
cursor->whiteValid = true;
cursor->sdrWhiteLevel = cmd->cursorWhiteLevel.value;
if (l_appliedSource == cmd->source &&
l_appliedGeneration == cmd->generation &&
g_state.lgr->ops.onMouseWhiteLevel)
g_state.lgr->ops.onMouseWhiteLevel(g_state.lgr,
cursor->sdrWhiteLevel);
break;
}
case CURSOR_OP_CLEAR:
if (l_appliedSource == cmd->source)
{
RENDERER(onMouseEvent, false, 0, 0, 0, 0);
if (g_state.lgr->ops.onMouseColorTransform)
g_state.lgr->ops.onMouseColorTransform(
g_state.lgr, &l_identityColorTransform);
if (g_state.lgr->ops.onMouseWhiteLevel)
g_state.lgr->ops.onMouseWhiteLevel(
g_state.lgr, LG_SDR_WHITE_LEVEL_DEFAULT);
}
break;
case SOURCE_OP_TRANSITION:
{
const bool swSurface =
cmd->source != RENDER_QUEUE_SOURCE_NONE &&
cmd->sourceTransition.swSurface;
if (swSurface && !swSurfaceTransitionReady(cmd, false))
{
DEBUG_ERROR("Software surface source is unavailable");
rejectSourceTransition(cmd);
break;
}
if (!prepareSourceTransition(cmd))
break;
if (swSurface && !prepareSwSurfaceTransition(cmd, false))
{
rejectSourceTransition(cmd);
break;
}
applySourceTransition(cmd, swSurface);
break;
}
}
LG_UNLOCK(l_sourceLock);
if (transition)
LG_UNLOCK(l_transitionLock);
freeCommand(cmd);
wakeQueue(wake);
cmd = next;
}
return invalidate == RENDER_QUEUE_INVALIDATE_FULL;
}
void renderQueue_presented(void)
{
if (!l_pendingTransition.valid)
return;
const RenderQueueTransition pending = l_pendingTransition;
l_pendingTransition.valid = false;
if (l_sourceAppliedFn)
l_sourceAppliedFn(l_sourceCallbackOpaque, pending.source,
pending.generation, pending.serial, pending.swSurface);
}