[client] expose EGL stages in frame latency

Correlate received frames with the texture update actually consumed by
the renderer and retain damage until its token becomes renderable.

Split client latency into dispatch, import, queue, preparation, setup,
effects, desktop, composition, and swap stages in FRAME LATENCY.
Exclude diagnostic overlay work from composition and preserve client
wait time when producer timing is unavailable.

Publish joined samples through a bounded token queue, avoid sleeping
while producer timing is finalized, and correct Wayland photon units.
This commit is contained in:
Geoffrey McRae
2026-08-03 23:25:25 +10:00
parent 6109501eed
commit fbe08d00ce
21 changed files with 897 additions and 349 deletions

View File

@@ -63,7 +63,8 @@ static void presentationFeedbackPresented(void * opaque,
struct timespec delta; struct timespec delta;
tsDiff(&delta, &present, &data->sent); tsDiff(&delta, &present, &data->sent);
ringbuffer_push(wlWm.photonTimings, &(float){ delta.tv_sec + delta.tv_nsec * 1e-6f }); ringbuffer_push(wlWm.photonTimings,
&(float){ delta.tv_sec * 1e3f + delta.tv_nsec * 1e-6f });
free(data); free(data);
wp_presentation_feedback_destroy(feedback); wp_presentation_feedback_destroy(feedback);
} }

View File

@@ -146,7 +146,9 @@ GraphHandle app_registerGraph(const char * name, RingBuffer buffer,
void app_unregisterGraph(GraphHandle handle); void app_unregisterGraph(GraphHandle handle);
void app_invalidateGraph(GraphHandle handle); void app_invalidateGraph(GraphHandle handle);
void app_setGraphCompact(GraphHandle handle, bool compact); void app_setGraphCompact(GraphHandle handle, bool compact);
void app_setFrameImportTime(uint64_t time); /* Records nanosecond durations for the current frame's renderer import. The
* wait component overlaps producer work and is accounted for separately. */
void app_setFrameImportTiming(uint64_t importTime, uint64_t importWaitTime);
void app_overlayConfigRegister(const char * title, void app_overlayConfigRegister(const char * title,
void (*callback)(void * udata, int * id), void * udata); void (*callback)(void * udata, int * id), void * udata);

View File

@@ -133,6 +133,25 @@ typedef struct LG_RendererCapture
} }
LG_RendererCapture; LG_RendererCapture;
#define LG_RENDERER_FRAME_TOKEN_NONE 0
typedef uint64_t LG_RendererFrameToken;
/* Renderer timings are client-clock durations in nanoseconds. frameToken is
* the received frame actually consumed by this render, or
* LG_RENDERER_FRAME_TOKEN_NONE when the render only refreshed existing
* content. */
typedef struct LG_RendererFrameTiming
{
LG_RendererFrameToken frameToken; /* frame consumed by this render */
uint64_t setupTime; /* render entry to desktop processing */
uint64_t effectsTime; /* post-processing work */
uint64_t desktopTime; /* desktop work, excluding effects */
uint64_t composeTime; /* composition, excluding UI overlay */
uint64_t swapTime; /* display-server buffer swap */
}
LG_RendererFrameTiming;
typedef enum LG_RendererCursor typedef enum LG_RendererCursor
{ {
LG_CURSOR_COLOR , LG_CURSOR_COLOR ,
@@ -237,20 +256,24 @@ typedef struct LG_RendererOps
bool (*onFrameFormat)(LG_Renderer * renderer, bool (*onFrameFormat)(LG_Renderer * renderer,
const LG_RendererFormat format); const LG_RendererFormat format);
/* called when there is a new frame /* called when there is a new frame. frameToken must remain attached to the
* Context: frameThread */ * update if the renderer coalesces it, and must be reported by render only
* when that update is consumed. Context: frameThread */
bool (*onFrame)(LG_Renderer * renderer, const FrameBuffer * frame, int dmaFD, bool (*onFrame)(LG_Renderer * renderer, const FrameBuffer * frame, int dmaFD,
const FrameDamageRect * damage, int damageCount); const FrameDamageRect * damage, int damageCount,
LG_RendererFrameToken frameToken);
/* called when the rederer is to startup /* called when the rederer is to startup
* Context: renderThread */ * Context: renderThread */
bool (*renderStartup)(LG_Renderer * renderer, bool useDMA); bool (*renderStartup)(LG_Renderer * renderer, bool useDMA);
/* called to render the scene /* called to render the scene. The renderer must not consume a received
* Context: renderThread */ * frame newer than frameTokenLimit, and reports the token it did consume in
* timing (or LG_RENDERER_FRAME_TOKEN_NONE). Context: renderThread */
bool (*render)(LG_Renderer * renderer, LG_RendererRotate rotate, bool (*render)(LG_Renderer * renderer, LG_RendererRotate rotate,
const bool newFrame, const bool invalidateWindow, LG_RendererFrameToken frameTokenLimit, const bool invalidateWindow,
void (*preSwap)(void * udata), void * udata); void (*preSwap)(void * udata), void * udata,
LG_RendererFrameTiming * timing);
/* Optional test/diagnostic readback of the fully composed framebuffer. /* Optional test/diagnostic readback of the fully composed framebuffer.
* Called on the render thread before swap while the graphics context is * Called on the render thread before swap while the graphics context is

View File

@@ -94,6 +94,7 @@ typedef uint32_t LG_TransportFrameFlags;
typedef struct LG_TransportFrameTiming typedef struct LG_TransportFrameTiming
{ {
bool valid; /* producer fields below are available and coherent */
uint64_t captureTime; uint64_t captureTime;
uint64_t postProcessTime; uint64_t postProcessTime;
uint64_t copyTime; uint64_t copyTime;

View File

@@ -27,8 +27,9 @@
struct DesktopDamage struct DesktopDamage
{ {
int count; LG_RendererFrameToken frameToken;
FrameDamageRect rects[LG_MAX_FRAME_DAMAGE_RECTS]; int count;
FrameDamageRect rects[LG_MAX_FRAME_DAMAGE_RECTS];
}; };
typedef struct EGL_Damage EGL_Damage; typedef struct EGL_Damage EGL_Damage;

View File

@@ -24,6 +24,7 @@
#include "common/debug.h" #include "common/debug.h"
#include "common/option.h" #include "common/option.h"
#include "common/locking.h" #include "common/locking.h"
#include "common/time.h"
#include "app.h" #include "app.h"
#include "texture.h" #include "texture.h"
@@ -390,14 +391,15 @@ bool egl_desktopSetup(EGL_Desktop * desktop, const LG_RendererFormat format)
return true; return true;
} }
bool egl_desktopUpdate(EGL_Desktop * desktop, const FrameBuffer * frame, int dmaFd, bool egl_desktopUpdate(EGL_Desktop * desktop, const FrameBuffer * frame,
LG_RendererFrameToken frameToken, int dmaFd,
const FrameDamageRect * damageRects, int damageRectsCount, const FrameDamageRect * damageRects, int damageRectsCount,
uint64_t * waitTimeNs) uint64_t * waitTimeNs)
{ {
if (likely(desktop->useDMA && dmaFd >= 0)) if (likely(desktop->useDMA && dmaFd >= 0))
{ {
if (likely(egl_textureUpdateFromDMA( if (likely(egl_textureUpdateFromDMA(
desktop->texture, frame, dmaFd, waitTimeNs))) desktop->texture, frame, frameToken, dmaFd, waitTimeNs)))
{ {
atomic_store(&desktop->processFrame, true); atomic_store(&desktop->processFrame, true);
return true; return true;
@@ -438,7 +440,7 @@ bool egl_desktopUpdate(EGL_Desktop * desktop, const FrameBuffer * frame, int dma
* the upload. Signalling here can race with that submission and process the * the upload. Signalling here can race with that submission and process the
* previous texture contents instead. * previous texture contents instead.
*/ */
if (likely(egl_textureUpdateFromFrame(desktop->texture, frame, if (likely(egl_textureUpdateFromFrame(desktop->texture, frame, frameToken,
damageRects, damageRectsCount, waitTimeNs))) damageRects, damageRectsCount, waitTimeNs)))
return true; return true;
@@ -454,7 +456,10 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth,
unsigned int outputHeight, const float x, const float y, unsigned int outputHeight, const float x, const float y,
const float scaleX, const float scaleY, enum EGL_DesktopScaleType scaleType, const float scaleX, const float scaleY, enum EGL_DesktopScaleType scaleType,
LG_RendererRotate rotate, const struct DamageRects * rects, LG_RendererRotate rotate, const struct DamageRects * rects,
bool * fullFrame, EGL_Framebuffer * target) LG_RendererFrameToken damageFrameToken,
LG_RendererFrameToken frameTokenLimit, bool * fullFrame,
LG_RendererFrameToken * consumedFrameToken,
uint64_t * effectsTime, EGL_Framebuffer * target)
{ {
EGL_Texture * tex; EGL_Texture * tex;
int width, height; int width, height;
@@ -478,21 +483,33 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth,
if (unlikely(outputWidth == 0 || outputHeight == 0)) if (unlikely(outputWidth == 0 || outputHeight == 0))
DEBUG_FATAL("outputWidth || outputHeight == 0"); DEBUG_FATAL("outputWidth || outputHeight == 0");
const enum EGL_TexStatus status = egl_textureProcess(tex); LG_RendererFrameToken frameToken;
const bool textureUpdated = status == EGL_TEX_STATUS_UPDATED; const enum EGL_TexStatus status =
egl_textureProcess(tex, frameTokenLimit, &frameToken);
const bool textureUpdated = status == EGL_TEX_STATUS_UPDATED;
*consumedFrameToken = frameToken;
*effectsTime = 0;
if (unlikely(status != EGL_TEX_STATUS_OK && !textureUpdated)) if (unlikely(status != EGL_TEX_STATUS_OK && !textureUpdated))
{ {
if (status != EGL_TEX_STATUS_NOTREADY) if (status != EGL_TEX_STATUS_NOTREADY)
DEBUG_ERROR("Failed to process the desktop texture"); DEBUG_ERROR("Failed to process the desktop texture");
} }
*fullFrame = false;
if (frameToken != LG_RENDERER_FRAME_TOKEN_NONE &&
frameToken != damageFrameToken)
{
rects = NULL;
*fullFrame = true;
}
int scaleAlgo = EGL_SCALE_NEAREST; int scaleAlgo = EGL_SCALE_NEAREST;
egl_desktopRectsMatrix(desktop->matrix, egl_desktopRectsMatrix(desktop->matrix,
width, height, x, y, scaleX, scaleY, rotate); width, height, x, y, scaleX, scaleY, rotate);
egl_desktopRectsUpdate(desktop->mesh, rects, width, height); egl_desktopRectsUpdate(desktop->mesh, rects, width, height);
*fullFrame = false;
const bool hdr = desktop->hdr && !desktop->useSpice; const bool hdr = desktop->hdr && !desktop->useSpice;
uint32_t hdrPeak = 0; uint32_t hdrPeak = 0;
if (hdr) if (hdr)
@@ -509,16 +526,22 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth,
bool processFrame = textureUpdated; bool processFrame = textureUpdated;
processFrame |= atomic_exchange(&desktop->processFrame, false); processFrame |= atomic_exchange(&desktop->processFrame, false);
processFrame |= egl_postProcessConfigModified(desktop->pp); processFrame |= egl_postProcessConfigModified(desktop->pp);
if (processFrame && if (processFrame)
egl_postProcessRun(desktop->pp, tex, desktop->mesh,
width, height, outputWidth, outputHeight, dma,
hdr && desktop->hdrPQ, (float)hdrPeak) &&
egl_postProcessNeedsFullFrame(desktop->pp))
{ {
/* The filter output may have changed everywhere, but this only applies to const uint64_t effectsStart = nanotime();
* the render that actually evaluated the filter. */ const bool postProcessed = egl_postProcessRun(
egl_desktopRectsUpdate(desktop->mesh, NULL, width, height); desktop->pp, tex, desktop->mesh,
*fullFrame = true; width, height, outputWidth, outputHeight, dma,
hdr && desktop->hdrPQ, (float)hdrPeak);
*effectsTime = nanotime() - effectsStart;
if (postProcessed && egl_postProcessNeedsFullFrame(desktop->pp))
{
/* The filter output may have changed everywhere, but this only applies
* to the render that actually evaluated the filter. */
egl_desktopRectsUpdate(desktop->mesh, NULL, width, height);
*fullFrame = true;
}
} }
unsigned int finalSizeX, finalSizeY; unsigned int finalSizeX, finalSizeY;

View File

@@ -48,7 +48,8 @@ void egl_desktopSetNativeHDR(EGL_Desktop * desktop, bool nativeHDR,
void egl_desktopGetHDRMapping(EGL_Desktop * desktop, bool * enabled, void egl_desktopGetHDRMapping(EGL_Desktop * desktop, bool * enabled,
float * gain, float * contentPeak); float * gain, float * contentPeak);
bool egl_desktopSetup (EGL_Desktop * desktop, const LG_RendererFormat format); bool egl_desktopSetup (EGL_Desktop * desktop, const LG_RendererFormat format);
bool egl_desktopUpdate(EGL_Desktop * desktop, const FrameBuffer * frame, int dmaFd, bool egl_desktopUpdate(EGL_Desktop * desktop, const FrameBuffer * frame,
LG_RendererFrameToken frameToken, int dmaFd,
const FrameDamageRect * damageRects, int damageRectsCount, const FrameDamageRect * damageRects, int damageRectsCount,
uint64_t * waitTimeNs); uint64_t * waitTimeNs);
void egl_desktopResize(EGL_Desktop * desktop, int width, int height); void egl_desktopResize(EGL_Desktop * desktop, int width, int height);
@@ -56,7 +57,10 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth,
unsigned int outputHeight, const float x, const float y, unsigned int outputHeight, const float x, const float y,
const float scaleX, const float scaleY, enum EGL_DesktopScaleType scaleType, const float scaleX, const float scaleY, enum EGL_DesktopScaleType scaleType,
LG_RendererRotate rotate, const struct DamageRects * rects, LG_RendererRotate rotate, const struct DamageRects * rects,
bool * fullFrame, EGL_Framebuffer * target); LG_RendererFrameToken damageFrameToken,
LG_RendererFrameToken frameTokenLimit, bool * fullFrame,
LG_RendererFrameToken * consumedFrameToken,
uint64_t * effectsTime, EGL_Framebuffer * target);
void egl_desktopSpiceConfigure(EGL_Desktop * desktop, int width, int height); void egl_desktopSpiceConfigure(EGL_Desktop * desktop, int width, int height);
void egl_desktopSpiceDrawFill(EGL_Desktop * desktop, int x, int y, int width, void egl_desktopSpiceDrawFill(EGL_Desktop * desktop, int x, int y, int width,

View File

@@ -378,6 +378,8 @@ static void egl_onRestart(LG_Renderer * renderer)
this->frameContext = NULL; this->frameContext = NULL;
INTERLOCKED_SECTION(this->desktopDamageLock, { INTERLOCKED_SECTION(this->desktopDamageLock, {
this->desktopDamage[this->desktopDamageIdx].frameToken =
LG_RENDERER_FRAME_TOKEN_NONE;
this->desktopDamage[this->desktopDamageIdx].count = -1; this->desktopDamage[this->desktopDamageIdx].count = -1;
}); });
} }
@@ -742,8 +744,9 @@ static bool egl_onFrameFormat(LG_Renderer * renderer, const LG_RendererFormat fo
return egl_desktopSetup(this->desktop, format); return egl_desktopSetup(this->desktop, format);
} }
static bool egl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame, int dmaFd, static bool egl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame,
const FrameDamageRect * damageRects, int damageRectsCount) int dmaFd, const FrameDamageRect * damageRects, int damageRectsCount,
LG_RendererFrameToken frameToken)
{ {
struct Inst * this = UPCAST(struct Inst, renderer); struct Inst * this = UPCAST(struct Inst, renderer);
egl_stateCheckShared(); egl_stateCheckShared();
@@ -751,21 +754,23 @@ static bool egl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame, int d
const uint64_t start = nanotime(); const uint64_t start = nanotime();
uint64_t waitTimeNs = 0; uint64_t waitTimeNs = 0;
if (unlikely(!egl_desktopUpdate( if (unlikely(!egl_desktopUpdate(
this->desktop, frame, dmaFd, damageRects, damageRectsCount, this->desktop, frame, frameToken, dmaFd,
&waitTimeNs))) damageRects, damageRectsCount, &waitTimeNs)))
{ {
DEBUG_INFO("Failed to to update the desktop"); DEBUG_INFO("Failed to to update the desktop");
return false; return false;
} }
const uint64_t elapsed = nanotime() - start; const uint64_t elapsed = nanotime() - start;
/* Producer Copy already covers the interval before FrameBuffer::wp becomes /* Producer Copy already covers the interval before FrameBuffer::wp becomes
* ready. Exclude that overlapping wait from the client Import stage. */ * ready. Exclude that overlapping wait from the client Import stage. */
app_setFrameImportTime( app_setFrameImportTiming(
elapsed > waitTimeNs ? elapsed - waitTimeNs : 0); elapsed > waitTimeNs ? elapsed - waitTimeNs : 0, waitTimeNs);
INTERLOCKED_SECTION(this->desktopDamageLock, { INTERLOCKED_SECTION(this->desktopDamageLock, {
struct DesktopDamage * damage = this->desktopDamage + this->desktopDamageIdx; struct DesktopDamage * damage =
this->desktopDamage + this->desktopDamageIdx;
if (unlikely( if (unlikely(
damage->count == -1 || damage->count == -1 ||
damageRectsCount == 0 || damageRectsCount == 0 ||
@@ -779,6 +784,7 @@ static bool egl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame, int d
damageRectsCount * sizeof(FrameDamageRect)); damageRectsCount * sizeof(FrameDamageRect));
damage->count += damageRectsCount; damage->count += damageRectsCount;
} }
damage->frameToken = frameToken;
}); });
return true; return true;
@@ -1334,10 +1340,14 @@ inline static void renderLetterBox(struct Inst * this)
} }
static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate,
const bool newFrame, const bool invalidateWindow, LG_RendererFrameToken frameTokenLimit, const bool invalidateWindow,
void (*preSwap)(void * udata), void * udata) void (*preSwap)(void * udata), void * udata,
LG_RendererFrameTiming * timing)
{ {
struct Inst * this = UPCAST(struct Inst, renderer); struct Inst * this = UPCAST(struct Inst, renderer);
*timing = (LG_RendererFrameTiming) {};
const uint64_t setupStart = nanotime();
egl_stateCheckShared(); egl_stateCheckShared();
EGLint bufferAge = egl_bufferAge(this); EGLint bufferAge = egl_bufferAge(this);
const bool hdrStateChanged = egl_updateHDRState(this, false); const bool hdrStateChanged = egl_updateHDRState(this, false);
@@ -1354,9 +1364,10 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate,
bufferAge <= 0 || bufferAge > MAX_BUFFER_AGE || bufferAge <= 0 || bufferAge > MAX_BUFFER_AGE ||
this->showSpice; this->showSpice;
bool hasOverlay = false; bool hasOverlay = false;
struct CursorState cursorState = { .visible = false }; struct CursorState cursorState = { .visible = false };
struct DesktopDamage * desktopDamage; struct DesktopDamage noDesktopDamage = {};
struct DesktopDamage * desktopDamage = NULL;
struct DamageRects * accumulated = (struct DamageRects *)alloca( struct DamageRects * accumulated = (struct DamageRects *)alloca(
sizeof(struct DamageRects) + sizeof(struct DamageRects) +
@@ -1365,12 +1376,20 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate,
accumulated->count = 0; accumulated->count = 0;
INTERLOCKED_SECTION(this->desktopDamageLock, { INTERLOCKED_SECTION(this->desktopDamageLock, {
struct DesktopDamage * pendingDamage =
this->desktopDamage + this->desktopDamageIdx;
const bool damageQueued =
pendingDamage->frameToken == LG_RENDERER_FRAME_TOKEN_NONE ||
pendingDamage->frameToken <= frameTokenLimit;
const int historyOffset = damageQueued ? 0 : 1;
if (likely(!renderAll)) if (likely(!renderAll))
{ {
for (int i = 0; i < bufferAge; ++i) for (int i = 0; i < bufferAge; ++i)
{ {
struct DesktopDamage * damage = this->desktopDamage + struct DesktopDamage * damage = this->desktopDamage +
IDX_AGO(this->desktopDamageIdx, i, DESKTOP_DAMAGE_COUNT); IDX_AGO(this->desktopDamageIdx, i + historyOffset,
DESKTOP_DAMAGE_COUNT);
if (unlikely(damage->count < 0)) if (unlikely(damage->count < 0))
{ {
@@ -1387,9 +1406,18 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate,
} }
} }
} }
desktopDamage = this->desktopDamage + this->desktopDamageIdx;
this->desktopDamageIdx = (this->desktopDamageIdx + 1) % DESKTOP_DAMAGE_COUNT; if (damageQueued)
this->desktopDamage[this->desktopDamageIdx].count = 0; {
desktopDamage = pendingDamage;
this->desktopDamageIdx =
(this->desktopDamageIdx + 1) % DESKTOP_DAMAGE_COUNT;
this->desktopDamage[this->desktopDamageIdx].frameToken =
LG_RENDERER_FRAME_TOKEN_NONE;
this->desktopDamage[this->desktopDamageIdx].count = 0;
}
else
desktopDamage = &noDesktopDamage;
}); });
if (hdrStateChanged) if (hdrStateChanged)
@@ -1427,17 +1455,46 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate,
} }
++this->overlayHistoryIdx; ++this->overlayHistoryIdx;
bool fullFrame = false; bool fullFrame = false;
const bool linearHDRComposition = egl_hdrComposeBegin(this->hdrCompose); const bool linearHDRComposition = egl_hdrComposeBegin(
bool deferredLogicalCursor = false; this->hdrCompose);
if (likely(this->destRect.w > 0 && this->destRect.h > 0)) bool deferredLogicalCursor = false;
const bool haveDesktop = likely(
this->destRect.w > 0 && this->destRect.h > 0);
bool desktopRendered = false;
const uint64_t desktopStart = nanotime();
uint64_t effectsTime = 0;
LG_RendererFrameToken frameToken = LG_RENDERER_FRAME_TOKEN_NONE;
timing->setupTime = desktopStart - setupStart;
if (haveDesktop)
{ {
if (egl_desktopRender(this->desktop, desktopRendered = egl_desktopRender(this->desktop,
this->destRect.w, this->destRect.h, this->destRect.w, this->destRect.h,
this->translateX, this->translateY, this->translateX, this->translateY,
this->scaleX , this->scaleY , this->scaleX , this->scaleY ,
this->scaleType , rotate, renderAll ? NULL : accumulated, this->scaleType , rotate, renderAll ? NULL : accumulated,
&fullFrame, egl_hdrComposeGetFramebuffer(this->hdrCompose))) desktopDamage->frameToken, frameTokenLimit, &fullFrame, &frameToken,
&effectsTime,
egl_hdrComposeGetFramebuffer(this->hdrCompose));
}
const uint64_t composeStart = nanotime();
if (haveDesktop)
{
const uint64_t totalDesktopTime = composeStart - desktopStart;
timing->effectsTime = effectsTime;
timing->desktopTime = totalDesktopTime > effectsTime ?
totalDesktopTime - effectsTime : 0;
}
else
timing->setupTime += composeStart - desktopStart;
timing->frameToken = frameToken;
const bool frameConsumed = frameToken != LG_RENDERER_FRAME_TOKEN_NONE;
if (haveDesktop)
{
if (desktopRendered)
{ {
cursorState = egl_cursorRender(this->cursor, cursorState = egl_cursorRender(this->cursor,
(this->format.rotate + rotate) % LG_ROTATE_MAX, (this->format.rotate + rotate) % LG_ROTATE_MAX,
@@ -1456,9 +1513,13 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate,
renderLetterBox(this); renderLetterBox(this);
hasOverlay |= hasOverlay |=
egl_damageRender(this->damage, rotate, newFrame ? desktopDamage : NULL) | egl_damageRender(
this->damage, rotate, frameConsumed ? desktopDamage : NULL) |
invalidateWindow; invalidateWindow;
/* The diagnostics being displayed must not contribute to Compose. */
timing->composeTime = nanotime() - composeStart;
struct Rect damage[LG_MAX_FRAME_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); int damageIdx = app_renderOverlay(damage, MAX_OVERLAY_RECTS);
if (unlikely(damageIdx != 0)) if (unlikely(damageIdx != 0))
@@ -1482,6 +1543,7 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate,
for (int i = 0; i < damageIdx; ++i) for (int i = 0; i < damageIdx; ++i)
damage[i].y = this->height - damage[i].y - damage[i].h; damage[i].y = this->height - damage[i].y - damage[i].h;
} }
const uint64_t postOverlayStart = nanotime();
if (likely(damageIdx >= 0 && cursorState.visible)) if (likely(damageIdx >= 0 && cursorState.visible))
damage[damageIdx++] = cursorState.rect; damage[damageIdx++] = cursorState.rect;
@@ -1559,13 +1621,19 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate,
egl_cursorRender(this->cursor, egl_cursorRender(this->cursor,
(this->format.rotate + rotate) % LG_ROTATE_MAX, (this->format.rotate + rotate) % LG_ROTATE_MAX,
this->width, this->height, false, NULL); this->width, this->height, false, NULL);
this->hadOverlay = hasOverlay; this->hadOverlay = hasOverlay;
this->cursorLast = cursorState; this->cursorLast = cursorState;
preSwap(udata); preSwap(udata);
const uint64_t composeEnd = nanotime();
timing->composeTime += composeEnd - postOverlayStart;
const uint64_t swapStart = nanotime();
app_eglSwapBuffers(this->display, this->surface, damage, app_eglSwapBuffers(this->display, this->surface, damage,
this->noSwapDamage ? 0 : damageIdx); this->noSwapDamage ? 0 : damageIdx);
const uint64_t swapEnd = nanotime();
timing->swapTime = swapEnd - swapStart;
return true; return true;
} }

View File

@@ -160,12 +160,14 @@ bool egl_textureUpdateRect(EGL_Texture * this,
} }
bool egl_textureUpdateFromFrame(EGL_Texture * this, bool egl_textureUpdateFromFrame(EGL_Texture * this,
const FrameBuffer * frame, const FrameDamageRect * damageRects, const FrameBuffer * frame, LG_RendererFrameToken frameToken,
int damageRectsCount, uint64_t * waitTimeNs) const FrameDamageRect * damageRects, int damageRectsCount,
uint64_t * waitTimeNs)
{ {
const struct EGL_TexUpdate update = const struct EGL_TexUpdate update =
{ {
.type = EGL_TEXTYPE_FRAMEBUFFER, .type = EGL_TEXTYPE_FRAMEBUFFER,
.frameToken = frameToken,
.x = 0, .x = 0,
.y = 0, .y = 0,
.width = this->format.width, .width = this->format.width,
@@ -182,18 +184,20 @@ bool egl_textureUpdateFromFrame(EGL_Texture * this,
} }
bool egl_textureUpdateFromDMA(EGL_Texture * this, bool egl_textureUpdateFromDMA(EGL_Texture * this,
const FrameBuffer * frame, const int dmaFd, uint64_t * waitTimeNs) const FrameBuffer * frame, LG_RendererFrameToken frameToken,
const int dmaFd, uint64_t * waitTimeNs)
{ {
const struct EGL_TexUpdate update = const struct EGL_TexUpdate update =
{ {
.type = EGL_TEXTYPE_DMABUF, .type = EGL_TEXTYPE_DMABUF,
.x = 0, .frameToken = frameToken,
.y = 0, .x = 0,
.width = this->format.width, .y = 0,
.height = this->format.height, .width = this->format.width,
.pitch = this->format.pitch, .height = this->format.height,
.stride = this->format.stride, .pitch = this->format.pitch,
.dmaFD = dmaFd .stride = this->format.stride,
.dmaFD = dmaFd
}; };
/* wait for completion */ /* wait for completion */
@@ -204,9 +208,18 @@ bool egl_textureUpdateFromDMA(EGL_Texture * this,
return this->ops.update(this, &update); return this->ops.update(this, &update);
} }
enum EGL_TexStatus egl_textureProcess(EGL_Texture * this) enum EGL_TexStatus egl_textureProcess(EGL_Texture * this,
LG_RendererFrameToken frameTokenLimit,
LG_RendererFrameToken * consumedFrameToken)
{ {
return this->ops.process(this); if (consumedFrameToken)
*consumedFrameToken = LG_RENDERER_FRAME_TOKEN_NONE;
const enum EGL_TexStatus status =
this->ops.process(this, frameTokenLimit);
if (status == EGL_TEX_STATUS_UPDATED && consumedFrameToken)
*consumedFrameToken = this->frameToken;
return status;
} }
enum EGL_TexStatus egl_textureBind(EGL_Texture * this) enum EGL_TexStatus egl_textureBind(EGL_Texture * this)

View File

@@ -27,6 +27,7 @@
#include "model.h" #include "model.h"
#include "common/framebuffer.h" #include "common/framebuffer.h"
#include "common/types.h" #include "common/types.h"
#include "interface/renderer.h"
#include "util.h" #include "util.h"
@@ -40,8 +41,9 @@ typedef struct EGL_Model EGL_Model;
typedef struct EGL_TexUpdate typedef struct EGL_TexUpdate
{ {
/* the type of this update */ /* the type of this update */
EGL_TexType type; EGL_TexType type;
uint64_t * waitTimeNs; LG_RendererFrameToken frameToken;
uint64_t * waitTimeNs;
int x, y, width, height; int x, y, width, height;
@@ -90,7 +92,8 @@ typedef struct EGL_TextureOps
bool (*update)(EGL_Texture * texture, const EGL_TexUpdate * update); bool (*update)(EGL_Texture * texture, const EGL_TexUpdate * update);
/* called from a background job to prepare the texture for use before bind */ /* called from a background job to prepare the texture for use before bind */
enum EGL_TexStatus (*process)(EGL_Texture * texture); enum EGL_TexStatus (*process)(EGL_Texture * texture,
LG_RendererFrameToken frameTokenLimit);
/* get the texture for use */ /* get the texture for use */
enum EGL_TexStatus (*get)(EGL_Texture * texture, GLuint * tex, enum EGL_TexStatus (*get)(EGL_Texture * texture, GLuint * tex,
@@ -104,10 +107,11 @@ EGL_TextureOps;
struct EGL_Texture struct EGL_Texture
{ {
struct EGL_TextureOps ops; struct EGL_TextureOps ops;
EGL_TexType type; EGL_TexType type;
GLuint sampler; GLuint sampler;
LG_RendererFrameToken frameToken;
EGL_TexFormat format; EGL_TexFormat format;
}; };
bool egl_textureInit(EGL_Texture ** texture, EGLDisplay * display, bool egl_textureInit(EGL_Texture ** texture, EGLDisplay * display,
@@ -125,13 +129,24 @@ bool egl_textureUpdateRect(EGL_Texture * texture,
const uint8_t * buffer, bool topDown); const uint8_t * buffer, bool topDown);
bool egl_textureUpdateFromFrame(EGL_Texture * texture, bool egl_textureUpdateFromFrame(EGL_Texture * texture,
const FrameBuffer * frame, const FrameDamageRect * damageRects, const FrameBuffer * frame, LG_RendererFrameToken frameToken,
int damageRectsCount, uint64_t * waitTimeNs); const FrameDamageRect * damageRects, int damageRectsCount,
uint64_t * waitTimeNs);
bool egl_textureUpdateFromDMA(EGL_Texture * texture, bool egl_textureUpdateFromDMA(EGL_Texture * texture,
const FrameBuffer * frame, const int dmaFd, uint64_t * waitTimeNs); const FrameBuffer * frame, LG_RendererFrameToken frameToken,
const int dmaFd, uint64_t * waitTimeNs);
enum EGL_TexStatus egl_textureProcess(EGL_Texture * texture); enum EGL_TexStatus egl_textureProcess(EGL_Texture * texture,
LG_RendererFrameToken frameTokenLimit,
LG_RendererFrameToken * consumedFrameToken);
static inline bool egl_textureFrameAllowed(LG_RendererFrameToken frameToken,
LG_RendererFrameToken frameTokenLimit)
{
return frameToken == LG_RENDERER_FRAME_TOKEN_NONE ||
frameToken <= frameTokenLimit;
}
static inline EGL_TexStatus egl_textureGet(EGL_Texture * texture, GLuint * tex, static inline EGL_TexStatus egl_textureGet(EGL_Texture * texture, GLuint * tex,
unsigned int * sizeX, unsigned int * sizeY, EGL_PixelFormat * fmt) unsigned int * sizeX, unsigned int * sizeY, EGL_PixelFormat * fmt)

View File

@@ -108,11 +108,15 @@ bool egl_texBufferSetup(EGL_Texture * texture, const EGL_TexSetup * setup)
NULL); NULL);
} }
this->bufIndex = 0; this->bufIndex = 0;
this->rIndex = -1; this->rIndex = -1;
texture->frameToken = LG_RENDERER_FRAME_TOKEN_NONE;
for (int i = 0; i < this->texCount; ++i) for (int i = 0; i < this->texCount; ++i)
{
this->buf[i].updated = false; this->buf[i].updated = false;
this->slotToken[i] = LG_RENDERER_FRAME_TOKEN_NONE;
}
return true; return true;
} }
@@ -138,8 +142,10 @@ static bool egl_texBufferUpdate(EGL_Texture * texture, const EGL_TexUpdate * upd
return true; return true;
} }
EGL_TexStatus egl_texBufferProcess(EGL_Texture * texture) EGL_TexStatus egl_texBufferProcess(EGL_Texture * texture,
LG_RendererFrameToken frameTokenLimit)
{ {
(void)frameTokenLimit;
return EGL_TEX_STATUS_OK; return EGL_TEX_STATUS_OK;
} }
@@ -260,13 +266,15 @@ static bool egl_texBufferStreamUpdate(EGL_Texture * texture,
} }
} }
this->slotToken[this->bufIndex] = update->frameToken;
this->buf[this->bufIndex].updated = true; this->buf[this->bufIndex].updated = true;
LG_UNLOCK(this->copyLock); LG_UNLOCK(this->copyLock);
return true; return true;
} }
EGL_TexStatus egl_texBufferStreamProcess(EGL_Texture * texture) EGL_TexStatus egl_texBufferStreamProcess(EGL_Texture * texture,
LG_RendererFrameToken frameTokenLimit)
{ {
TextureBuffer * this = UPCAST(TextureBuffer, texture); TextureBuffer * this = UPCAST(TextureBuffer, texture);
@@ -276,7 +284,8 @@ EGL_TexStatus egl_texBufferStreamProcess(EGL_Texture * texture)
GLuint tex = this->tex[index]; GLuint tex = this->tex[index];
EGL_TexBuffer * buffer = &this->buf[index]; EGL_TexBuffer * buffer = &this->buf[index];
if (!buffer->updated) if (!buffer->updated ||
!egl_textureFrameAllowed(this->slotToken[index], frameTokenLimit))
{ {
LG_UNLOCK(this->copyLock); LG_UNLOCK(this->copyLock);
return EGL_TEX_STATUS_OK; return EGL_TEX_STATUS_OK;
@@ -284,7 +293,8 @@ EGL_TexStatus egl_texBufferStreamProcess(EGL_Texture * texture)
DEBUG_ASSERT(!this->sync[index]); DEBUG_ASSERT(!this->sync[index]);
this->rIndex = index; this->rIndex = index;
texture->frameToken = this->slotToken[index];
if (++this->bufIndex == this->texCount) if (++this->bufIndex == this->texCount)
this->bufIndex = 0; this->bufIndex = 0;
buffer->updated = false; buffer->updated = false;

View File

@@ -31,14 +31,15 @@ typedef struct TextureBuffer
EGL_Texture base; EGL_Texture base;
bool free; bool free;
int texCount; int texCount;
GLuint tex[EGL_TEX_BUFFER_MAX]; GLuint tex[EGL_TEX_BUFFER_MAX];
EGL_TexBuffer buf[EGL_TEX_BUFFER_MAX]; EGL_TexBuffer buf[EGL_TEX_BUFFER_MAX];
int bufFree; int bufFree;
GLsync sync[EGL_TEX_BUFFER_MAX]; GLsync sync[EGL_TEX_BUFFER_MAX];
LG_Lock copyLock; LG_RendererFrameToken slotToken[EGL_TEX_BUFFER_MAX];
int bufIndex; LG_Lock copyLock;
int rIndex; int bufIndex;
int rIndex;
} }
TextureBuffer; TextureBuffer;
@@ -46,7 +47,8 @@ bool egl_texBufferInit(EGL_Texture ** texture_, EGL_TexType type,
EGLDisplay * display); EGLDisplay * display);
void egl_texBufferFree(EGL_Texture * texture_); void egl_texBufferFree(EGL_Texture * texture_);
bool egl_texBufferSetup(EGL_Texture * texture_, const EGL_TexSetup * setup); bool egl_texBufferSetup(EGL_Texture * texture_, const EGL_TexSetup * setup);
EGL_TexStatus egl_texBufferProcess(EGL_Texture * texture_); EGL_TexStatus egl_texBufferProcess(EGL_Texture * texture_,
LG_RendererFrameToken frameTokenLimit);
EGL_TexStatus egl_texBufferGet(EGL_Texture * texture_, GLuint * tex, EGL_TexStatus egl_texBufferGet(EGL_Texture * texture_, GLuint * tex,
EGL_PixelFormat * fmt); EGL_PixelFormat * fmt);
EGL_TexStatus egl_texBufferBind(EGL_Texture * texture_, GLuint unit); EGL_TexStatus egl_texBufferBind(EGL_Texture * texture_, GLuint unit);
@@ -57,6 +59,7 @@ bool egl_texBufferStreamSetup(EGL_Texture * texture_,
const EGL_TexSetup * setup); const EGL_TexSetup * setup);
/* Returns with copyLock held when the current upload buffer is safe to write. */ /* Returns with copyLock held when the current upload buffer is safe to write. */
bool egl_texBufferStreamLock(TextureBuffer * texture); bool egl_texBufferStreamLock(TextureBuffer * texture);
EGL_TexStatus egl_texBufferStreamProcess(EGL_Texture * texture_); EGL_TexStatus egl_texBufferStreamProcess(EGL_Texture * texture_,
LG_RendererFrameToken frameTokenLimit);
EGL_TexStatus egl_texBufferStreamGet(EGL_Texture * texture_, GLuint * tex, EGL_TexStatus egl_texBufferStreamGet(EGL_Texture * texture_, GLuint * tex,
EGL_PixelFormat * fmt); EGL_PixelFormat * fmt);

View File

@@ -29,10 +29,11 @@
struct FdImage struct FdImage
{ {
int fd; int fd;
EGLImage image; EGLImage image;
GLsync sync; GLsync sync;
int texIndex; int texIndex;
LG_RendererFrameToken frameToken;
}; };
typedef struct TexDMABUF typedef struct TexDMABUF
@@ -43,6 +44,7 @@ typedef struct TexDMABUF
struct FdImage images[2]; struct FdImage images[2];
int lastIndex; int lastIndex;
int renderIndex;
EGL_PixelFormat pixFmt; EGL_PixelFormat pixFmt;
unsigned fourcc; unsigned fourcc;
@@ -76,10 +78,16 @@ static void egl_texDMABUFCleanup(EGL_Texture * texture)
glDeleteSync(this->images[i].sync); glDeleteSync(this->images[i].sync);
this->images[i].sync = 0; this->images[i].sync = 0;
} }
this->images[i].fd = -1; this->images[i].fd = -1;
this->images[i].texIndex = -1; this->images[i].texIndex = -1;
this->images[i].frameToken = LG_RENDERER_FRAME_TOKEN_NONE;
} }
this->lastIndex = -1;
this->renderIndex = -1;
parent->rIndex = -1;
texture->frameToken = LG_RENDERER_FRAME_TOKEN_NONE;
egl_texUtilFreeBuffers(parent->buf, parent->texCount); egl_texUtilFreeBuffers(parent->buf, parent->texCount);
if (parent->tex[0]) if (parent->tex[0])
@@ -99,12 +107,14 @@ static bool egl_texDMABUFInit(EGL_Texture ** texture, EGL_TexType type,
for(int i = 0; i < ARRAY_LENGTH(this->images); ++i) for(int i = 0; i < ARRAY_LENGTH(this->images); ++i)
{ {
this->images[i].fd = -1; this->images[i].fd = -1;
this->images[i].image = EGL_NO_IMAGE; this->images[i].image = EGL_NO_IMAGE;
this->images[i].sync = 0; this->images[i].sync = 0;
this->images[i].texIndex = -1; this->images[i].texIndex = -1;
this->images[i].frameToken = LG_RENDERER_FRAME_TOKEN_NONE;
} }
this->lastIndex = -1; this->lastIndex = -1;
this->renderIndex = -1;
EGL_Texture * parent = &this->base.base; EGL_Texture * parent = &this->base.base;
if (!egl_texBufferStreamInit(&parent, type, display)) if (!egl_texBufferStreamInit(&parent, type, display))
@@ -276,15 +286,45 @@ static bool egl_texDMABUFUpdate(EGL_Texture * texture,
INTERLOCKED_SECTION(parent->copyLock, INTERLOCKED_SECTION(parent->copyLock,
{ {
this->lastIndex = (fdImage == &this->images[0]) ? 0 : 1; fdImage->frameToken = update->frameToken;
this->lastIndex = (fdImage == &this->images[0]) ? 0 : 1;
}); });
return true; return true;
} }
static EGL_TexStatus egl_texDMABUFProcess(EGL_Texture * texture) static EGL_TexStatus egl_texDMABUFProcess(EGL_Texture * texture,
LG_RendererFrameToken frameTokenLimit)
{ {
return EGL_TEX_STATUS_OK; TextureBuffer * parent = UPCAST(TextureBuffer, texture);
TexDMABUF * this = UPCAST(TexDMABUF , parent);
int index = -1;
bool haveImage = false;
bool updated = false;
INTERLOCKED_SECTION(parent->copyLock,
{
index = this->lastIndex;
haveImage = this->renderIndex >= 0;
if (index >= 0 && egl_textureFrameAllowed(
this->images[index].frameToken, frameTokenLimit))
{
const struct FdImage * pending = &this->images[index];
if (this->renderIndex != index ||
texture->frameToken != pending->frameToken)
{
this->renderIndex = index;
parent->rIndex = pending->texIndex;
texture->frameToken = pending->frameToken;
updated = true;
haveImage = true;
}
}
});
if (unlikely(!haveImage))
return EGL_TEX_STATUS_NOTREADY;
return updated ? EGL_TEX_STATUS_UPDATED : EGL_TEX_STATUS_OK;
} }
static EGL_TexStatus egl_texDMABUFGet(EGL_Texture * texture, GLuint * tex, static EGL_TexStatus egl_texDMABUFGet(EGL_Texture * texture, GLuint * tex,
@@ -299,7 +339,7 @@ static EGL_TexStatus egl_texDMABUFGet(EGL_Texture * texture, GLuint * tex,
INTERLOCKED_SECTION(parent->copyLock, INTERLOCKED_SECTION(parent->copyLock,
{ {
index = this->lastIndex; index = this->renderIndex;
if (index >= 0) if (index >= 0)
{ {
struct FdImage * cur = &this->images[index]; struct FdImage * cur = &this->images[index];

View File

@@ -156,12 +156,14 @@ static bool egl_texFBUpdate(EGL_Texture * texture, const EGL_TexUpdate * update)
{ {
/* The mapped PBO may contain a partial copy. Force a full refresh if the /* The mapped PBO may contain a partial copy. Force a full refresh if the
* caller recovers, and never expose this slot to the render context. */ * caller recovers, and never expose this slot to the render context. */
damage->count = -1; damage->count = -1;
parent->buf[parent->bufIndex].updated = false; parent->buf[parent->bufIndex].updated = false;
parent->slotToken[parent->bufIndex] = LG_RENDERER_FRAME_TOKEN_NONE;
LG_UNLOCK(parent->copyLock); LG_UNLOCK(parent->copyLock);
return false; return false;
} }
parent->slotToken[parent->bufIndex] = update->frameToken;
parent->buf[parent->bufIndex].updated = true; parent->buf[parent->bufIndex].updated = true;
for (int i = 0; i < EGL_TEX_BUFFER_MAX; ++i) for (int i = 0; i < EGL_TEX_BUFFER_MAX; ++i)

View File

@@ -107,44 +107,46 @@ struct Inst
LG_RendererParams params; LG_RendererParams params;
struct OpenGL_Options opt; struct OpenGL_Options opt;
bool amdPinnedMemSupport; bool amdPinnedMemSupport;
bool renderStarted; bool renderStarted;
bool configured; bool configured;
bool reconfigure; bool reconfigure;
LG_DSGLContext glContext; LG_DSGLContext glContext;
struct IntPoint window; struct IntPoint window;
float uiScale; float uiScale;
_Atomic(bool) frameUpdate; _Atomic(bool) frameUpdate;
LG_Lock formatLock; LG_Lock formatLock;
LG_RendererFormat format; LG_RendererFormat format;
GLuint intFormat; GLuint intFormat;
GLuint vboFormat; GLuint vboFormat;
GLuint dataFormat; GLuint dataFormat;
size_t texSize; size_t texSize;
size_t texPos; size_t texPos;
float scaleX, scaleY; float scaleX, scaleY;
const FrameBuffer * frame; const FrameBuffer * frame;
LG_RendererFrameToken pendingFrameToken;
uint64_t drawStart; uint64_t drawStart;
bool hasBuffers; bool hasBuffers;
GLuint vboID[BUFFER_COUNT]; GLuint vboID[BUFFER_COUNT];
uint8_t * texPixels[BUFFER_COUNT]; uint8_t * texPixels[BUFFER_COUNT];
LG_Lock frameLock; LG_Lock frameLock;
bool texReady; bool texReady;
int texWIndex, texRIndex; int texWIndex, texRIndex;
int texList; int texList;
int mouseList; int mouseList;
int spiceList; int spiceList;
LG_RendererRect destRect; LG_RendererRect destRect;
struct IntPoint spiceSize; struct IntPoint spiceSize;
bool spiceShow; bool spiceShow;
bool hasTextures, hasFrames; bool hasTextures, hasFrames;
GLuint frames[BUFFER_COUNT]; GLuint frames[BUFFER_COUNT];
GLsync fences[BUFFER_COUNT]; GLsync fences[BUFFER_COUNT];
GLuint textures[TEXTURE_COUNT]; LG_RendererFrameToken frameToken[BUFFER_COUNT];
GLuint textures[TEXTURE_COUNT];
LG_Lock mouseLock; LG_Lock mouseLock;
LG_RendererCursor mouseCursor; LG_RendererCursor mouseCursor;
@@ -174,7 +176,9 @@ enum ConfigStatus
static void deconfigure(struct Inst * this); static void deconfigure(struct Inst * this);
static enum ConfigStatus configure(struct Inst * this); static enum ConfigStatus configure(struct Inst * this);
static void updateMouseShape(struct Inst * this); static void updateMouseShape(struct Inst * this);
static bool drawFrame(struct Inst * this); static bool drawFrame(struct Inst * this,
LG_RendererFrameToken frameTokenLimit,
LG_RendererFrameToken * consumedFrameToken);
static void drawMouse(struct Inst * this); static void drawMouse(struct Inst * this);
const char * opengl_getName(void) const char * opengl_getName(void)
@@ -391,12 +395,14 @@ bool opengl_onFrameFormat(LG_Renderer * renderer, const LG_RendererFormat format
} }
bool opengl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame, int dmaFd, bool opengl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame, int dmaFd,
const FrameDamageRect * damage, int damageCount) const FrameDamageRect * damage, int damageCount,
LG_RendererFrameToken frameToken)
{ {
struct Inst * this = UPCAST(struct Inst, renderer); struct Inst * this = UPCAST(struct Inst, renderer);
LG_LOCK(this->frameLock); LG_LOCK(this->frameLock);
this->frame = frame; this->frame = frame;
this->pendingFrameToken = frameToken;
atomic_store_explicit(&this->frameUpdate, true, memory_order_release); atomic_store_explicit(&this->frameUpdate, true, memory_order_release);
LG_UNLOCK(this->frameLock); LG_UNLOCK(this->frameLock);
@@ -486,10 +492,13 @@ bool opengl_renderStartup(LG_Renderer * renderer, bool useDMA)
return true; return true;
} }
bool opengl_render(LG_Renderer * renderer, LG_RendererRotate rotate, const bool newFrame, bool opengl_render(LG_Renderer * renderer, LG_RendererRotate rotate,
const bool invalidateWindow, void (*preSwap)(void * udata), void * udata) LG_RendererFrameToken frameTokenLimit, const bool invalidateWindow,
void (*preSwap)(void * udata), void * udata,
LG_RendererFrameTiming * timing)
{ {
struct Inst * this = UPCAST(struct Inst, renderer); struct Inst * this = UPCAST(struct Inst, renderer);
*timing = (LG_RendererFrameTiming) {};
setupModelView(this); setupModelView(this);
@@ -501,7 +510,7 @@ bool opengl_render(LG_Renderer * renderer, LG_RendererRotate rotate, const bool
case CONFIG_STATUS_NOOP : case CONFIG_STATUS_NOOP :
case CONFIG_STATUS_OK : case CONFIG_STATUS_OK :
if (!drawFrame(this)) if (!drawFrame(this, frameTokenLimit, &timing->frameToken))
return false; return false;
} }
@@ -1024,16 +1033,17 @@ static void deconfigure(struct Inst * this)
this->hasBuffers = false; this->hasBuffers = false;
} }
if (this->amdPinnedMemSupport) for(int i = 0; i < BUFFER_COUNT; ++i)
{ {
for(int i = 0; i < BUFFER_COUNT; ++i) if (this->fences[i])
{ {
if (this->fences[i]) g_gl_dynProcs.glDeleteSync(this->fences[i]);
{ this->fences[i] = NULL;
g_gl_dynProcs.glDeleteSync(this->fences[i]); }
this->fences[i] = NULL; this->frameToken[i] = LG_RENDERER_FRAME_TOKEN_NONE;
}
if (this->amdPinnedMemSupport)
{
if (this->texPixels[i]) if (this->texPixels[i])
{ {
free(this->texPixels[i]); free(this->texPixels[i]);
@@ -1208,8 +1218,12 @@ static bool opengl_bufferFn(void * opaque, const void * data, size_t size)
return true; return true;
} }
static bool drawFrame(struct Inst * this) static bool drawFrame(struct Inst * this,
LG_RendererFrameToken frameTokenLimit,
LG_RendererFrameToken * consumedFrameToken)
{ {
*consumedFrameToken = LG_RENDERER_FRAME_TOKEN_NONE;
if (g_gl_dynProcs.glIsSync(this->fences[this->texWIndex])) if (g_gl_dynProcs.glIsSync(this->fences[this->texWIndex]))
{ {
switch(g_gl_dynProcs.glClientWaitSync(this->fences[this->texWIndex], 0, GL_TIMEOUT_IGNORED)) switch(g_gl_dynProcs.glClientWaitSync(this->fences[this->texWIndex], 0, GL_TIMEOUT_IGNORED))
@@ -1234,16 +1248,21 @@ static bool drawFrame(struct Inst * this)
this->fences[this->texWIndex] = NULL; this->fences[this->texWIndex] = NULL;
this->texRIndex = this->texWIndex; this->texRIndex = this->texWIndex;
*consumedFrameToken = this->frameToken[this->texRIndex];
this->frameToken[this->texRIndex] = LG_RENDERER_FRAME_TOKEN_NONE;
if (++this->texWIndex == BUFFER_COUNT) if (++this->texWIndex == BUFFER_COUNT)
this->texWIndex = 0; this->texWIndex = 0;
} }
LG_LOCK(this->frameLock); LG_LOCK(this->frameLock);
if (!atomic_exchange_explicit(&this->frameUpdate, false, memory_order_acquire)) if (!atomic_load_explicit(&this->frameUpdate, memory_order_acquire) ||
this->pendingFrameToken > frameTokenLimit)
{ {
LG_UNLOCK(this->frameLock); LG_UNLOCK(this->frameLock);
return true; return true;
} }
atomic_store_explicit(&this->frameUpdate, false, memory_order_release);
const LG_RendererFrameToken pendingFrameToken = this->pendingFrameToken;
LG_LOCK(this->formatLock); LG_LOCK(this->formatLock);
glBindTexture(GL_TEXTURE_2D, this->frames[this->texWIndex]); glBindTexture(GL_TEXTURE_2D, this->frames[this->texWIndex]);
@@ -1317,6 +1336,7 @@ static bool drawFrame(struct Inst * this)
// set a fence so we don't overwrite a buffer in use // set a fence so we don't overwrite a buffer in use
this->fences[this->texWIndex] = this->fences[this->texWIndex] =
g_gl_dynProcs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); g_gl_dynProcs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
this->frameToken[this->texWIndex] = pendingFrameToken;
glFlush(); glFlush();
LG_UNLOCK(this->formatLock); LG_UNLOCK(this->formatLock);

View File

@@ -922,9 +922,10 @@ void app_setGraphCompact(GraphHandle handle, bool compact)
overlayGraph_setCompact(handle, compact); overlayGraph_setCompact(handle, compact);
} }
void app_setFrameImportTime(uint64_t time) void app_setFrameImportTiming(uint64_t importTime, uint64_t importWaitTime)
{ {
g_state.frameImportTime = time; g_state.frameImportTime = importTime;
g_state.frameImportWaitTime = importWaitTime;
} }
void app_registerOverlay(const struct LG_OverlayOps * ops, const void * params) void app_registerOverlay(const struct LG_OverlayOps * ops, const void * params)

View File

@@ -199,82 +199,296 @@ static bool tickTimerFn(void * unused)
return true; return true;
} }
struct RenderTiming #define FRAME_TIMING_RECORD_COUNT 1024
#define FRAME_TIMING_PUBLISH_BATCH_SIZE 32
enum FrameTimingReady
{ {
uint64_t renderStart; FRAME_TIMING_FRAME_READY = 1 << 0,
FRAME_TIMING_RENDER_READY = 1 << 1,
};
struct FrameTimingRecord
{
LG_RendererFrameToken token;
unsigned readyMask;
bool producerValid;
uint64_t captureTime; uint64_t captureTime;
uint64_t postProcessTime; uint64_t postProcessTime;
uint64_t copyTime; uint64_t copyTime;
uint64_t readyTime; uint64_t readyTime;
uint64_t importTime; uint64_t importTime;
uint64_t importWaitTime;
uint64_t dispatchTime;
uint64_t queueStart;
uint64_t prepareStart;
uint64_t prepareTime;
uint64_t timestamp;
uint64_t setupTime;
uint64_t effectsTime;
uint64_t desktopTime;
uint64_t composeTime;
uint64_t swapTime;
}; };
static struct RenderTiming frameTimingLoad(void) static struct
{ {
struct RenderTiming timing = {}; LG_Lock lock;
_Atomic(LG_RendererFrameToken) queuedToken;
LG_RendererFrameToken nextToken;
LG_RendererFrameToken retireToken;
unsigned publishRead;
unsigned publishWrite;
unsigned publishCount;
bool overflowWarning;
LG_RendererFrameToken publishToken[FRAME_TIMING_RECORD_COUNT];
struct FrameTimingRecord record[FRAME_TIMING_RECORD_COUNT];
}
l_frameTiming;
for (;;) /* The frame and render threads complete records independently. A token is
{ * published only after both halves have arrived, while queuedToken prevents a
const unsigned sequence = atomic_load_explicit( * renderer that woke for unrelated work from consuming an unqueued frame. */
&g_state.frameTimingSequence, memory_order_seq_cst);
if (sequence & 1)
continue;
timing.captureTime = atomic_load_explicit( static void frameTimingInit(void)
&g_state.producerCaptureTime, memory_order_seq_cst); {
timing.postProcessTime = atomic_load_explicit( LG_LOCK_INIT(l_frameTiming.lock);
&g_state.producerPostProcessTime, memory_order_seq_cst); atomic_store_explicit(&l_frameTiming.queuedToken,
timing.copyTime = atomic_load_explicit( LG_RENDERER_FRAME_TOKEN_NONE, memory_order_relaxed);
&g_state.producerCopyTime, memory_order_seq_cst); l_frameTiming.nextToken = LG_RENDERER_FRAME_TOKEN_NONE;
timing.readyTime = atomic_load_explicit( l_frameTiming.retireToken = 1;
&g_state.producerReadyTime, memory_order_seq_cst); l_frameTiming.publishRead = 0;
timing.importTime = atomic_load_explicit( l_frameTiming.publishWrite = 0;
&g_state.clientImportTime, memory_order_seq_cst); l_frameTiming.publishCount = 0;
l_frameTiming.overflowWarning = false;
if (sequence == atomic_load_explicit( memset(l_frameTiming.record, 0, sizeof(l_frameTiming.record));
&g_state.frameTimingSequence, memory_order_seq_cst))
return timing;
}
} }
static void frameTimingStore(const LG_TransportFrameTiming * timing, static void frameTimingReset(void)
uint64_t importTime)
{ {
atomic_fetch_add_explicit( INTERLOCKED_SECTION(l_frameTiming.lock, {
&g_state.frameTimingSequence, 1, memory_order_seq_cst); memset(l_frameTiming.record, 0, sizeof(l_frameTiming.record));
atomic_store_explicit(&g_state.producerCaptureTime, l_frameTiming.retireToken = l_frameTiming.nextToken + 1;
timing->captureTime, memory_order_seq_cst); l_frameTiming.publishRead = 0;
atomic_store_explicit(&g_state.producerPostProcessTime, l_frameTiming.publishWrite = 0;
timing->postProcessTime, memory_order_seq_cst); l_frameTiming.publishCount = 0;
atomic_store_explicit(&g_state.producerCopyTime, l_frameTiming.overflowWarning = false;
timing->copyTime, memory_order_seq_cst); });
atomic_store_explicit(&g_state.producerReadyTime, /* Tokens remain monotonic across reconnects so stale renderer state can
timing->readyTime, memory_order_seq_cst); * never alias a new frame. No frame is consumable until it is queued. */
atomic_store_explicit(&g_state.clientImportTime, atomic_store_explicit(&l_frameTiming.queuedToken,
importTime, memory_order_seq_cst); LG_RENDERER_FRAME_TOKEN_NONE, memory_order_release);
atomic_fetch_add_explicit( }
&g_state.frameTimingSequence, 1, memory_order_seq_cst);
static struct FrameTimingRecord * frameTimingRecord(
LG_RendererFrameToken token)
{
return &l_frameTiming.record[
(token - 1) % FRAME_TIMING_RECORD_COUNT];
}
static LG_RendererFrameToken frameTimingReserve(void)
{
LG_RendererFrameToken token;
LG_LOCK(l_frameTiming.lock);
token = ++l_frameTiming.nextToken;
if (unlikely(token == LG_RENDERER_FRAME_TOKEN_NONE))
token = ++l_frameTiming.nextToken;
struct FrameTimingRecord * record = frameTimingRecord(token);
const bool recordActive =
record->token != LG_RENDERER_FRAME_TOKEN_NONE &&
((record->readyMask & FRAME_TIMING_RENDER_READY) ||
record->token >= l_frameTiming.retireToken);
if (unlikely(recordActive && !l_frameTiming.overflowWarning))
{
DEBUG_WARN("Frame timing record pool exhausted; samples will be lost");
l_frameTiming.overflowWarning = true;
}
*record = (struct FrameTimingRecord) { .token = token };
LG_UNLOCK(l_frameTiming.lock);
return token;
}
static void frameTimingCancel(LG_RendererFrameToken token)
{
INTERLOCKED_SECTION(l_frameTiming.lock, {
struct FrameTimingRecord * record = frameTimingRecord(token);
if (record->token == token)
*record = (struct FrameTimingRecord) {};
});
}
static void frameTimingQueue(LG_RendererFrameToken token, uint64_t importTime,
uint64_t importWaitTime, uint64_t dispatchStart, uint64_t queueStart)
{
INTERLOCKED_SECTION(l_frameTiming.lock, {
struct FrameTimingRecord * record = frameTimingRecord(token);
if (record->token == token)
{
const uint64_t elapsed = queueStart > dispatchStart ?
queueStart - dispatchStart : 0;
const uint64_t accounted = importTime + importWaitTime;
record->importTime = importTime;
record->importWaitTime = importWaitTime;
record->dispatchTime = elapsed > accounted ? elapsed - accounted : 0;
record->queueStart = queueStart;
if (record->timestamp < queueStart)
record->timestamp = queueStart;
}
});
atomic_store_explicit(
&l_frameTiming.queuedToken, token, memory_order_release);
}
static LG_RendererFrameToken frameTimingQueuedToken(void)
{
return atomic_load_explicit(
&l_frameTiming.queuedToken, memory_order_acquire);
}
static void frameTimingFinishFrame(LG_RendererFrameToken token,
const LG_TransportFrameTiming * timing)
{
const uint64_t timestamp = nanotime();
INTERLOCKED_SECTION(l_frameTiming.lock, {
struct FrameTimingRecord * record = frameTimingRecord(token);
if (record->token == token)
{
if (token < l_frameTiming.retireToken &&
!(record->readyMask & FRAME_TIMING_RENDER_READY))
*record = (struct FrameTimingRecord) {};
else
{
record->producerValid = timing->valid;
record->captureTime = timing->captureTime;
record->postProcessTime = timing->postProcessTime;
record->copyTime = timing->copyTime;
record->readyTime = timing->readyTime;
if (record->timestamp < timestamp)
record->timestamp = timestamp;
record->readyMask |= FRAME_TIMING_FRAME_READY;
}
}
});
}
static void frameTimingFinishRender(const LG_RendererFrameTiming * timing,
uint64_t prepareStart, uint64_t prepareTime, uint64_t timestamp)
{
INTERLOCKED_SECTION(l_frameTiming.lock, {
if (l_frameTiming.retireToken <= timing->frameToken)
l_frameTiming.retireToken = timing->frameToken + 1;
struct FrameTimingRecord * record = frameTimingRecord(timing->frameToken);
if (record->token == timing->frameToken)
{
record->prepareStart = prepareStart;
record->prepareTime = prepareTime;
if (record->timestamp < timestamp)
record->timestamp = timestamp;
record->setupTime = timing->setupTime;
record->effectsTime = timing->effectsTime;
record->desktopTime = timing->desktopTime;
record->composeTime = timing->composeTime;
record->swapTime = timing->swapTime;
record->readyMask |= FRAME_TIMING_RENDER_READY;
if (unlikely(
l_frameTiming.publishCount == FRAME_TIMING_RECORD_COUNT))
{
if (!l_frameTiming.overflowWarning)
{
DEBUG_WARN("Frame timing publish queue exhausted; sample lost");
l_frameTiming.overflowWarning = true;
}
*record = (struct FrameTimingRecord) {};
}
else
{
l_frameTiming.publishToken[l_frameTiming.publishWrite] =
timing->frameToken;
l_frameTiming.publishWrite =
(l_frameTiming.publishWrite + 1) % FRAME_TIMING_RECORD_COUNT;
++l_frameTiming.publishCount;
}
}
});
}
static void frameTimingPublishReady(void)
{
struct FrameTimingRecord ready[FRAME_TIMING_PUBLISH_BATCH_SIZE];
unsigned readyCount = 0;
LG_LOCK(l_frameTiming.lock);
while (readyCount < FRAME_TIMING_PUBLISH_BATCH_SIZE &&
l_frameTiming.publishCount)
{
const LG_RendererFrameToken token =
l_frameTiming.publishToken[l_frameTiming.publishRead];
struct FrameTimingRecord * record = frameTimingRecord(token);
if (record->token != token ||
!(record->readyMask & FRAME_TIMING_RENDER_READY))
{
l_frameTiming.publishRead =
(l_frameTiming.publishRead + 1) % FRAME_TIMING_RECORD_COUNT;
--l_frameTiming.publishCount;
continue;
}
if (!(record->readyMask & FRAME_TIMING_FRAME_READY))
break;
ready[readyCount++] = *record;
*record = (struct FrameTimingRecord) {};
l_frameTiming.publishRead =
(l_frameTiming.publishRead + 1) % FRAME_TIMING_RECORD_COUNT;
--l_frameTiming.publishCount;
}
LG_UNLOCK(l_frameTiming.lock);
for (unsigned i = 0; i < readyCount; ++i)
{
const struct FrameTimingRecord * record = &ready[i];
const uint64_t queueTime =
record->prepareStart > record->queueStart ?
record->prepareStart - record->queueStart : 0;
const uint32_t validMask =
OVERLAY_FRAME_TIMING_VALID_ALL &
(record->producerValid ? UINT32_MAX :
~OVERLAY_FRAME_TIMING_VALID_PRODUCER);
const OverlayFrameTiming timing = {
.timestamp = record->timestamp,
.validMask = validMask,
.capture = record->captureTime * 1e-6f,
.postProcess = record->postProcessTime * 1e-6f,
.copy = record->copyTime * 1e-6f,
.ready = record->readyTime * 1e-6f,
.import = (record->importTime +
(record->producerValid ? 0 : record->importWaitTime)) * 1e-6f,
.dispatch = record->dispatchTime * 1e-6f,
.queue = queueTime * 1e-6f,
.prepare = record->prepareTime * 1e-6f,
.setup = record->setupTime * 1e-6f,
.effects = record->effectsTime * 1e-6f,
.desktop = record->desktopTime * 1e-6f,
.compose = record->composeTime * 1e-6f,
.swap = record->swapTime * 1e-6f,
};
ringbuffer_push(g_state.frameLatency, &timing);
}
} }
static void preSwapCallback(void * udata) static void preSwapCallback(void * udata)
{ {
const struct RenderTiming * timing = (const struct RenderTiming *)udata; (void)udata;
const uint64_t timestamp = nanotime();
const uint64_t renderTime = timestamp - timing->renderStart;
if (timing->captureTime || timing->postProcessTime || timing->copyTime ||
timing->readyTime || timing->importTime)
{
const OverlayFrameTiming frameTiming = {
.timestamp = timestamp,
.capture = timing->captureTime * 1e-6f,
.postProcess = timing->postProcessTime * 1e-6f,
.copy = timing->copyTime * 1e-6f,
.ready = timing->readyTime * 1e-6f,
.import = timing->importTime * 1e-6f,
.render = renderTime * 1e-6f,
};
ringbuffer_push(g_state.frameLatency, &frameTiming);
}
#ifdef ENABLE_TESTS #ifdef ENABLE_TESTS
if (!l_testCapture.enabled || l_testCapture.complete) if (!l_testCapture.enabled || l_testCapture.complete)
@@ -425,6 +639,8 @@ static int renderThread(void * unused)
} }
} }
frameTimingPublishReady();
int resize = atomic_load(&g_state.lgrResize); int resize = atomic_load(&g_state.lgrResize);
if (unlikely(resize)) if (unlikely(resize))
{ {
@@ -460,29 +676,32 @@ static int renderThread(void * unused)
atomic_compare_exchange_weak(&g_state.lgrResize, &resize, 0); atomic_compare_exchange_weak(&g_state.lgrResize, &resize, 0);
} }
static uint64_t lastFrameCount = 0;
const uint64_t frameCount =
atomic_load_explicit(&g_state.frameCount, memory_order_relaxed);
const bool newFrame = frameCount != lastFrameCount;
lastFrameCount = frameCount;
const bool invalidate = atomic_exchange(&g_state.invalidateWindow, false); const bool invalidate = atomic_exchange(&g_state.invalidateWindow, false);
struct RenderTiming renderTiming = const LG_RendererFrameToken frameTokenLimit = frameTimingQueuedToken();
newFrame ? frameTimingLoad() : (struct RenderTiming) {}; const uint64_t prepareStart = nanotime();
renderTiming.renderStart = nanotime();
LG_LOCK(g_state.lgrLock); LG_LOCK(g_state.lgrLock);
renderQueue_process(); renderQueue_process();
if (unlikely(!RENDERER(render, g_params.winRotate, newFrame, invalidate, const uint64_t prepareTime = nanotime() - prepareStart;
preSwapCallback, (void *)&renderTiming)))
LG_RendererFrameTiming rendererTiming = {};
if (unlikely(!RENDERER(render, g_params.winRotate, frameTokenLimit,
invalidate, preSwapCallback, NULL, &rendererTiming)))
{ {
LG_UNLOCK(g_state.lgrLock); LG_UNLOCK(g_state.lgrLock);
break; break;
} }
const uint64_t renderEnd = nanotime();
LG_UNLOCK(g_state.lgrLock); LG_UNLOCK(g_state.lgrLock);
if (rendererTiming.frameToken != LG_RENDERER_FRAME_TOKEN_NONE)
frameTimingFinishRender(
&rendererTiming, prepareStart, prepareTime, renderEnd);
frameTimingPublishReady();
const uint64_t t = nanotime(); const uint64_t t = nanotime();
const uint64_t delta = t - g_state.lastRenderTime; const uint64_t delta = t - g_state.lastRenderTime;
@@ -692,6 +911,7 @@ int main_frameThread(void * unused)
continue; continue;
} }
frameSerial = frame.serial; frameSerial = frame.serial;
const uint64_t dispatchStart = nanotime();
const LG_TransportFrameFormat * format = frame.format; const LG_TransportFrameFormat * format = frame.format;
if (!format) if (!format)
@@ -842,23 +1062,42 @@ int main_frameThread(void * unused)
damageCount = 0; damageCount = 0;
} }
g_state.frameImportTime = 0; const LG_RendererFrameToken frameToken = frameTimingReserve();
g_state.frameImportTime = 0;
g_state.frameImportWaitTime = 0;
if (!RENDERER(onFrame, frame.framebuffer, frame.dmaFD, if (!RENDERER(onFrame, frame.framebuffer, frame.dmaFD,
frame.damageRects, damageCount)) frame.damageRects, damageCount, frameToken))
{ {
frameTimingCancel(frameToken);
g_state.transportOps->releaseFrame(g_state.transport, &frame); g_state.transportOps->releaseFrame(g_state.transport, &frame);
DEBUG_ERROR("Renderer onFrame returned failure"); DEBUG_ERROR("Renderer onFrame returned failure");
app_setState(APP_STATE_SHUTDOWN); app_setState(APP_STATE_SHUTDOWN);
break; break;
} }
const uint64_t queueStart = nanotime();
atomic_fetch_add_explicit(&g_state.frameCount, 1, memory_order_relaxed);
#ifdef ENABLE_TESTS
atomic_store_explicit(&l_testFrameSerial, frame.serial,
memory_order_release);
#endif
frameTimingQueue(frameToken, g_state.frameImportTime,
g_state.frameImportWaitTime, dispatchStart, queueStart);
if (g_state.jitRender)
{
if (atomic_load_explicit(&g_state.pendingCount, memory_order_acquire) < 10)
atomic_fetch_add_explicit(&g_state.pendingCount, 1,
memory_order_release);
}
else
lgSignalEvent(g_state.frameEvent);
LG_TransportFrameTiming timing = {}; LG_TransportFrameTiming timing = {};
if (g_state.transportOps->getFrameTiming) if (g_state.transportOps->getFrameTiming)
g_state.transportOps->getFrameTiming( g_state.transportOps->getFrameTiming(
g_state.transport, &frame, &timing); g_state.transport, &frame, &timing);
frameTimingStore(&timing, g_state.frameImportTime);
overlaySplash_show(false); overlaySplash_show(false);
if ((frame.flags & LG_TRANSPORT_FRAME_REQUEST_ACTIVATION) && if ((frame.flags & LG_TRANSPORT_FRAME_REQUEST_ACTIVATION) &&
g_params.requestActivation) g_params.requestActivation)
@@ -876,19 +1115,7 @@ int main_frameThread(void * unused)
g_state.autoIdleInhibitState = blockScreensaver; g_state.autoIdleInhibitState = blockScreensaver;
} }
atomic_fetch_add_explicit(&g_state.frameCount, 1, memory_order_relaxed); frameTimingFinishFrame(frameToken, &timing);
#ifdef ENABLE_TESTS
atomic_store_explicit(&l_testFrameSerial, frame.serial,
memory_order_release);
#endif
if (g_state.jitRender)
{
if (atomic_load_explicit(&g_state.pendingCount, memory_order_acquire) < 10)
atomic_fetch_add_explicit(&g_state.pendingCount, 1,
memory_order_release);
}
else
lgSignalEvent(g_state.frameEvent);
g_state.transportOps->releaseFrame(g_state.transport, &frame); g_state.transportOps->releaseFrame(g_state.transport, &frame);
app_useSpiceDisplay(false); app_useSpiceDisplay(false);
@@ -1322,6 +1549,8 @@ static int transportSessionProbe(void * opaque)
static int lg_run(void) static int lg_run(void)
{ {
frameTimingInit();
#ifdef ENABLE_TESTS #ifdef ENABLE_TESTS
memset(&l_testCapture, 0, sizeof(l_testCapture)); memset(&l_testCapture, 0, sizeof(l_testCapture));
atomic_store_explicit(&l_testFrameSerial, 0, memory_order_relaxed); atomic_store_explicit(&l_testFrameSerial, 0, memory_order_relaxed);
@@ -1620,6 +1849,7 @@ static int lg_run(void)
int msgsCount; int msgsCount;
restart: restart:
frameTimingReset();
msgsCount = 0; msgsCount = 0;
memset(msgs, 0, sizeof(msgs)); memset(msgs, 0, sizeof(msgs));
@@ -1867,6 +2097,7 @@ static void lg_shutdown(void)
// free metrics ringbuffers // free metrics ringbuffers
ringbuffer_free(&g_state.renderTimings); ringbuffer_free(&g_state.renderTimings);
ringbuffer_free(&g_state.frameLatency); ringbuffer_free(&g_state.frameLatency);
LG_LOCK_FREE(l_frameTiming.lock);
free(g_state.fontName); free(g_state.fontName);
igDestroyContext(NULL); igDestroyContext(NULL);

View File

@@ -148,14 +148,9 @@ struct AppState
RingBuffer renderTimings; RingBuffer renderTimings;
RingBuffer frameLatency; RingBuffer frameLatency;
uint64_t frameImportTime; uint64_t frameImportTime;
uint64_t frameImportWaitTime;
atomic_uint_least64_t pendingCount; atomic_uint_least64_t pendingCount;
atomic_uint frameTimingSequence;
atomic_uint_least64_t producerCaptureTime;
atomic_uint_least64_t producerPostProcessTime;
atomic_uint_least64_t producerCopyTime;
atomic_uint_least64_t producerReadyTime;
atomic_uint_least64_t clientImportTime;
atomic_uint_least64_t renderCount, frameCount; atomic_uint_least64_t renderCount, frameCount;
_Atomic(float) fps, ups; _Atomic(float) fps, ups;

View File

@@ -64,6 +64,49 @@ struct OverlayGraph
GraphFormatFn formatFn; GraphFormatFn formatFn;
}; };
static void graphFree(struct OverlayGraph * graph)
{
free(graph->name);
free(graph);
}
static struct OverlayGraph * graphNew(const char * name, RingBuffer buffer)
{
if (!name || !*name)
{
DEBUG_ERROR("graph name must not be empty");
return NULL;
}
struct OverlayGraph * graph = calloc(1, sizeof(*graph));
if (!graph)
{
DEBUG_ERROR("out of memory");
return NULL;
}
graph->name = lg_strdup(name);
if (!graph->name)
{
DEBUG_ERROR("out of memory");
graphFree(graph);
return NULL;
}
graph->buffer = buffer;
graph->enabled = true;
return graph;
}
static GraphHandle graphPublish(struct OverlayGraph * graph)
{
if (!ll_push(gs.graphs, graph))
{
graphFree(graph);
return NULL;
}
return graph;
}
static void configCallback(void * udata, int * id) static void configCallback(void * udata, int * id)
{ {
@@ -127,10 +170,7 @@ static void graphs_free(void * udata)
{ {
struct OverlayGraph * graph; struct OverlayGraph * graph;
while(ll_shift(gs.graphs, (void **)&graph)) while(ll_shift(gs.graphs, (void **)&graph))
{ graphFree(graph);
free(graph->name);
free(graph);
}
ll_free(gs.graphs); ll_free(gs.graphs);
gs.graphs = NULL; gs.graphs = NULL;
@@ -175,20 +215,37 @@ static bool rbCalcMetrics(int index, void * value_, void * udata_)
return true; return true;
} }
#define TIMING_PLOT_BUCKETS 100 #define TIMING_PLOT_BUCKETS 100
#define TIMING_PLOT_WINDOW_NS 20000000000ULL #define TIMING_PLOT_WINDOW_NS 20000000000ULL
#define TIMING_PLOT_BUCKET_NS \ #define TIMING_PLOT_BUCKET_NS \
(TIMING_PLOT_WINDOW_NS / TIMING_PLOT_BUCKETS) (TIMING_PLOT_WINDOW_NS / TIMING_PLOT_BUCKETS)
#define TIMING_STAGE_COUNT 6 #define FRAME_TIMING_STAGE_COUNT OVERLAY_FRAME_TIMING_COUNT
static const char * const frameTimingLabels[FRAME_TIMING_STAGE_COUNT] = {
"Capture",
"Post",
"Copy",
"Ready",
"Import",
"Dispatch",
"Queue",
"Prepare",
"Setup",
"Effects",
"Desktop",
"Compose",
"Swap",
};
struct TimingPlotData struct TimingPlotData
{ {
uint64_t windowStart; uint64_t windowStart;
uint64_t windowEnd; uint64_t windowEnd;
float stageMin[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; float stageMin[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS];
float stageMax[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; float stageMax[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS];
float stageSum[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; float stageSum[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS];
unsigned count[TIMING_PLOT_BUCKETS]; unsigned stageSamples[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS];
unsigned samples[TIMING_PLOT_BUCKETS];
}; };
static float roundTimingScale(float value) static float roundTimingScale(float value)
@@ -223,8 +280,10 @@ static float graphRowWeight(const struct OverlayGraph * graph)
return graph->compact ? 0.5f : 1.0f; return graph->compact ? 0.5f : 1.0f;
} }
static bool accumulateTimingSample(int index, void * value_, void * udata_) static bool accumulateFrameTimingSample(int index, void * value_, void * udata_)
{ {
(void)index;
struct TimingPlotData * data = udata_; struct TimingPlotData * data = udata_;
const OverlayFrameTiming * timing = value_; const OverlayFrameTiming * timing = value_;
if (timing->timestamp < data->windowStart) if (timing->timestamp < data->windowStart)
@@ -232,29 +291,44 @@ static bool accumulateTimingSample(int index, void * value_, void * udata_)
if (timing->timestamp >= data->windowEnd) if (timing->timestamp >= data->windowEnd)
return false; return false;
const uint64_t offset = timing->timestamp - data->windowStart; const float values[FRAME_TIMING_STAGE_COUNT] = {
const int bucket = min(offset / TIMING_PLOT_BUCKET_NS,
TIMING_PLOT_BUCKETS - 1);
const float values[TIMING_STAGE_COUNT] = {
timing->capture, timing->capture,
timing->postProcess, timing->postProcess,
timing->copy, timing->copy,
timing->ready, timing->ready,
timing->import, timing->import,
timing->render, timing->dispatch,
timing->queue,
timing->prepare,
timing->setup,
timing->effects,
timing->desktop,
timing->compose,
timing->swap,
}; };
for (int i = 0; i < TIMING_STAGE_COUNT; ++i)
const uint64_t offset = timing->timestamp - data->windowStart;
const int bucket = min(offset / TIMING_PLOT_BUCKET_NS,
TIMING_PLOT_BUCKETS - 1);
for (int stage = 0; stage < FRAME_TIMING_STAGE_COUNT; ++stage)
{ {
if (!data->count[bucket]) if (!(timing->validMask & (1U << stage)))
data->stageMin[i][bucket] = data->stageMax[i][bucket] = values[i]; continue;
const float value = values[stage];
if (!data->stageSamples[stage][bucket])
data->stageMin[stage][bucket] = data->stageMax[stage][bucket] = value;
else else
{ {
data->stageMin[i][bucket] = min(data->stageMin[i][bucket], values[i]); data->stageMin[stage][bucket] =
data->stageMax[i][bucket] = max(data->stageMax[i][bucket], values[i]); min(data->stageMin[stage][bucket], value);
data->stageMax[stage][bucket] =
max(data->stageMax[stage][bucket], value);
} }
data->stageSum[i][bucket] += values[i]; data->stageSum[stage][bucket] += value;
++data->stageSamples[stage][bucket];
} }
++data->count[bucket]; ++data->samples[bucket];
return true; return true;
} }
@@ -315,20 +389,24 @@ static void renderLineGraph(struct OverlayGraph * graph, bool interactive,
static void renderTimingStatistic(struct OverlayGraph * graph, static void renderTimingStatistic(struct OverlayGraph * graph,
const char * statistic, int statisticId, const char * statistic, int statisticId,
const float values[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS], const float values[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS],
bool interactive, ImVec2 size) bool interactive, ImVec2 size)
{ {
float cumulative[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; float cumulative[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {};
float zero[TIMING_PLOT_BUCKETS] = {}; float zero[TIMING_PLOT_BUCKETS];
float xValues[TIMING_PLOT_BUCKETS]; float xValues[TIMING_PLOT_BUCKETS];
float peak = 0.0f; float peak = 0.0f;
for (int bucket = 0; bucket < TIMING_PLOT_BUCKETS; ++bucket) for (int bucket = 0; bucket < TIMING_PLOT_BUCKETS; ++bucket)
{ {
const bool populated = isfinite(values[0][bucket]);
xValues[bucket] = bucket; xValues[bucket] = bucket;
for (int stage = 0; stage < TIMING_STAGE_COUNT; ++stage) zero[bucket] = populated ? 0.0f : NAN;
cumulative[stage][bucket] = values[stage][bucket] + for (int stage = 0; stage < FRAME_TIMING_STAGE_COUNT; ++stage)
(stage ? cumulative[stage - 1][bucket] : 0.0f); cumulative[stage][bucket] = populated ? values[stage][bucket] +
peak = max(peak, cumulative[TIMING_STAGE_COUNT - 1][bucket]); (stage ? cumulative[stage - 1][bucket] : 0.0f) : NAN;
if (populated)
peak = max(peak, cumulative[FRAME_TIMING_STAGE_COUNT - 1][bucket]);
} }
const float valueMax = graphScale(graph, statisticId, peak); const float valueMax = graphScale(graph, statisticId, peak);
@@ -346,26 +424,20 @@ static void renderTimingStatistic(struct OverlayGraph * graph,
ImPlot_SetupLegend(ImPlotLocation_South, ImPlot_SetupLegend(ImPlotLocation_South,
ImPlotLegendFlags_Outside | ImPlotLegendFlags_Horizontal); ImPlotLegendFlags_Outside | ImPlotLegendFlags_Horizontal);
const char * labels[TIMING_STAGE_COUNT] = {
"Capture",
"Post",
"Copy",
"Ready",
"Import",
"Render",
};
gs.plotSpec->Offset = 0; gs.plotSpec->Offset = 0;
gs.plotSpec->Stride = sizeof(float); gs.plotSpec->Stride = sizeof(float);
for (int stage = 0; stage < TIMING_STAGE_COUNT; ++stage) const int colorCount = ImPlot_GetColormapSize(ImPlotColormap_Paired);
for (int stage = 0; stage < FRAME_TIMING_STAGE_COUNT; ++stage)
{ {
const ImVec4 color = const ImVec4 color = stage < colorCount ?
ImPlot_GetColormapColor(stage, ImPlotColormap_Deep); ImPlot_GetColormapColor(stage, ImPlotColormap_Paired) :
(ImVec4) {0.75f, 0.75f, 0.75f, 1.0f};
gs.plotSpec->FillColor = color; gs.plotSpec->FillColor = color;
gs.plotSpec->FillAlpha = 0.35f; gs.plotSpec->FillAlpha = 0.35f;
gs.plotSpec->LineWeight = 1.0f; gs.plotSpec->LineWeight = 1.0f;
gs.plotSpec->Flags = ImPlotShadedFlags_None; gs.plotSpec->Flags = ImPlotShadedFlags_None;
ImPlot_PlotShaded_FloatPtrFloatPtrFloatPtr(labels[stage], xValues, ImPlot_PlotShaded_FloatPtrFloatPtrFloatPtr(
frameTimingLabels[stage], xValues,
stage ? cumulative[stage - 1] : zero, cumulative[stage], stage ? cumulative[stage - 1] : zero, cumulative[stage],
TIMING_PLOT_BUCKETS, *gs.plotSpec); TIMING_PLOT_BUCKETS, *gs.plotSpec);
@@ -373,7 +445,7 @@ static void renderTimingStatistic(struct OverlayGraph * graph,
gs.plotSpec->LineWeight = 1.0f; gs.plotSpec->LineWeight = 1.0f;
gs.plotSpec->FillAlpha = 1.0f; gs.plotSpec->FillAlpha = 1.0f;
gs.plotSpec->Flags = ImPlotLineFlags_None; gs.plotSpec->Flags = ImPlotLineFlags_None;
ImPlot_PlotLine_FloatPtrInt(labels[stage], cumulative[stage], ImPlot_PlotLine_FloatPtrInt(frameTimingLabels[stage], cumulative[stage],
TIMING_PLOT_BUCKETS, 1.0, 0.0, *gs.plotSpec); TIMING_PLOT_BUCKETS, 1.0, 0.0, *gs.plotSpec);
} }
@@ -383,31 +455,39 @@ static void renderTimingStatistic(struct OverlayGraph * graph,
ImPlot_EndPlot(); ImPlot_EndPlot();
} }
static void renderTimingGraph(struct OverlayGraph * graph, bool interactive, static void renderFrameTimingGraph(struct OverlayGraph * graph,
ImVec2 size) bool interactive, ImVec2 size)
{ {
struct TimingPlotData data = {}; struct TimingPlotData data = {};
data.windowEnd = nanotime() / TIMING_PLOT_BUCKET_NS * TIMING_PLOT_BUCKET_NS; data.windowEnd =
nanotime() / TIMING_PLOT_BUCKET_NS * TIMING_PLOT_BUCKET_NS;
data.windowStart = data.windowEnd > TIMING_PLOT_WINDOW_NS ? data.windowStart = data.windowEnd > TIMING_PLOT_WINDOW_NS ?
data.windowEnd - TIMING_PLOT_WINDOW_NS : 0; data.windowEnd - TIMING_PLOT_WINDOW_NS : 0;
ringbuffer_forEach(graph->buffer, accumulateTimingSample, &data, false); ringbuffer_forEach(
graph->buffer, accumulateFrameTimingSample, &data, false);
float minimum[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; float minimum[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {};
float maximum[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; float maximum[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {};
float average[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; float average[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {};
for (int bucket = 0; bucket < TIMING_PLOT_BUCKETS; ++bucket) for (int bucket = 0; bucket < TIMING_PLOT_BUCKETS; ++bucket)
{ for (int stage = 0; stage < FRAME_TIMING_STAGE_COUNT; ++stage)
if (!data.count[bucket])
continue;
for (int stage = 0; stage < TIMING_STAGE_COUNT; ++stage)
{ {
if (!data.samples[bucket])
{
minimum[stage][bucket] = NAN;
maximum[stage][bucket] = NAN;
average[stage][bucket] = NAN;
continue;
}
if (!data.stageSamples[stage][bucket])
continue;
minimum[stage][bucket] = data.stageMin[stage][bucket]; minimum[stage][bucket] = data.stageMin[stage][bucket];
maximum[stage][bucket] = data.stageMax[stage][bucket]; maximum[stage][bucket] = data.stageMax[stage][bucket];
average[stage][bucket] = average[stage][bucket] =
data.stageSum[stage][bucket] / data.count[bucket]; data.stageSum[stage][bucket] / data.stageSamples[stage][bucket];
} }
}
const float spacing = igGetStyle()->ItemSpacing.y; const float spacing = igGetStyle()->ItemSpacing.y;
const float height = (size.y - spacing * 2.0f) / 3.0f; const float height = (size.y - spacing * 2.0f) / 3.0f;
@@ -477,7 +557,8 @@ static int graphs_render(void * udata, bool interactive,
igPushID_Ptr(graph); igPushID_Ptr(graph);
const float height = rowHeight * graphRowWeight(graph); const float height = rowHeight * graphRowWeight(graph);
if (graph->type == OVERLAY_GRAPH_FRAME_TIMING) if (graph->type == OVERLAY_GRAPH_FRAME_TIMING)
renderTimingGraph(graph, interactive, (ImVec2) {winSize.x, height}); renderFrameTimingGraph(graph, interactive,
(ImVec2) {winSize.x, height});
else else
renderLineGraph(graph, interactive, (ImVec2) {winSize.x, height}); renderLineGraph(graph, interactive, (ImVec2) {winSize.x, height});
igPopID(); igPopID();
@@ -502,47 +583,26 @@ struct LG_OverlayOps LGOverlayGraphs =
GraphHandle overlayGraph_register(const char * name, RingBuffer buffer, GraphHandle overlayGraph_register(const char * name, RingBuffer buffer,
float min, float max, GraphFormatFn formatFn) float min, float max, GraphFormatFn formatFn)
{ {
if (!name || !*name) struct OverlayGraph * graph = graphNew(name, buffer);
{
DEBUG_ERROR("graph name must not be empty");
return NULL;
}
struct OverlayGraph * graph = malloc(sizeof(*graph));
if (!graph) if (!graph)
{
DEBUG_ERROR("out of memory");
return NULL; return NULL;
}
graph->name = lg_strdup(name);
if (!graph->name)
{
DEBUG_ERROR("out of memory");
free(graph);
return NULL;
}
graph->buffer = buffer;
graph->enabled = true;
graph->compact = false;
graph->type = OVERLAY_GRAPH_LINE; graph->type = OVERLAY_GRAPH_LINE;
graph->min = min; graph->min = min;
graph->max = max; graph->max = max;
for (int i = 0; i < TIMING_STATISTIC_COUNT; ++i)
graph->yScale[i] = 0.0f;
graph->formatFn = formatFn; graph->formatFn = formatFn;
ll_push(gs.graphs, graph); return graphPublish(graph);
return graph;
} }
GraphHandle overlayGraph_registerFrameTiming(const char * name, GraphHandle overlayGraph_registerFrameTiming(const char * name,
RingBuffer buffer) RingBuffer buffer)
{ {
GraphHandle graph = overlayGraph_register(name, buffer, 0.0f, 0.0f, NULL); struct OverlayGraph * graph = graphNew(name, buffer);
if (graph) if (!graph)
graph->type = OVERLAY_GRAPH_FRAME_TIMING; return NULL;
return graph;
graph->type = OVERLAY_GRAPH_FRAME_TIMING;
return graphPublish(graph);
} }
void overlayGraph_unregister(GraphHandle handle) void overlayGraph_unregister(GraphHandle handle)
@@ -551,8 +611,7 @@ void overlayGraph_unregister(GraphHandle handle)
return; return;
ll_removeData(gs.graphs, handle); ll_removeData(gs.graphs, handle);
free(handle->name); graphFree(handle);
free(handle);
if (gs.show) if (gs.show)
app_invalidateWindow(false); app_invalidateWindow(false);

View File

@@ -47,15 +47,46 @@ void overlayAlert_show(LG_MsgAlert type, const char * fmt, va_list args);
typedef struct OverlayFrameTiming typedef struct OverlayFrameTiming
{ {
uint64_t timestamp; uint64_t timestamp;
uint32_t validMask;
float capture; float capture;
float postProcess; float postProcess;
float copy; float copy;
float ready; float ready;
float import; float import;
float render; float dispatch;
float queue;
float prepare;
float setup;
float effects;
float desktop;
float compose;
float swap;
} }
OverlayFrameTiming; OverlayFrameTiming;
enum OverlayFrameTimingStage
{
OVERLAY_FRAME_TIMING_CAPTURE,
OVERLAY_FRAME_TIMING_POST_PROCESS,
OVERLAY_FRAME_TIMING_COPY,
OVERLAY_FRAME_TIMING_READY,
OVERLAY_FRAME_TIMING_IMPORT,
OVERLAY_FRAME_TIMING_DISPATCH,
OVERLAY_FRAME_TIMING_QUEUE,
OVERLAY_FRAME_TIMING_PREPARE,
OVERLAY_FRAME_TIMING_SETUP,
OVERLAY_FRAME_TIMING_EFFECTS,
OVERLAY_FRAME_TIMING_DESKTOP,
OVERLAY_FRAME_TIMING_COMPOSE,
OVERLAY_FRAME_TIMING_SWAP,
OVERLAY_FRAME_TIMING_COUNT,
};
#define OVERLAY_FRAME_TIMING_VALID_ALL \
((1U << OVERLAY_FRAME_TIMING_COUNT) - 1U)
#define OVERLAY_FRAME_TIMING_VALID_PRODUCER \
((1U << OVERLAY_FRAME_TIMING_IMPORT) - 1U)
GraphHandle overlayGraph_register(const char * name, RingBuffer buffer, GraphHandle overlayGraph_register(const char * name, RingBuffer buffer,
float min, float max, GraphFormatFn formatFn); float min, float max, GraphFormatFn formatFn);
GraphHandle overlayGraph_registerFrameTiming(const char * name, GraphHandle overlayGraph_registerFrameTiming(const char * name,

View File

@@ -36,6 +36,8 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#define LGMP_TIMING_SPIN_COUNT 4096
struct DMAFrameInfo struct DMAFrameInfo
{ {
const KVMFRFrame * frame; const KVMFRFrame * frame;
@@ -622,18 +624,21 @@ static void lgmp_getFrameTiming(LG_Transport * this,
this->pendingFrame->frameSerial != frame->serial) this->pendingFrame->frameSerial != frame->serial)
return; return;
/* The producer writes these immediately after completing the framebuffer. /* The producer writes these immediately after publishing FrameBuffer::wp.
* nextFrame can observe the header earlier, so sample them only after the * nextFrame can observe the header earlier, so briefly observe the
* renderer's onFrame call has consumed the framebuffer. */ * publication tail after onFrame consumes the framebuffer without sleeping
* the frame-acquisition thread. */
for (unsigned i = 0; for (unsigned i = 0;
!lgmp_frameTimingReady(this->pendingFrame) && !lgmp_frameTimingReady(this->pendingFrame) &&
i < 1000; i < LGMP_TIMING_SPIN_COUNT;
++i) ++i)
usleep(1); {
}
if (!lgmp_frameTimingReady(this->pendingFrame)) if (!lgmp_frameTimingReady(this->pendingFrame))
return; return;
timing->valid = true;
timing->captureTime = this->pendingFrame->captureTime; timing->captureTime = this->pendingFrame->captureTime;
timing->postProcessTime = this->pendingFrame->postProcessTime; timing->postProcessTime = this->pendingFrame->postProcessTime;
timing->copyTime = this->pendingFrame->copyTime; timing->copyTime = this->pendingFrame->copyTime;