diff --git a/client/displayservers/Wayland/presentation.c b/client/displayservers/Wayland/presentation.c index 4664260e..22cd658e 100644 --- a/client/displayservers/Wayland/presentation.c +++ b/client/displayservers/Wayland/presentation.c @@ -63,7 +63,8 @@ static void presentationFeedbackPresented(void * opaque, struct timespec delta; 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); wp_presentation_feedback_destroy(feedback); } diff --git a/client/include/app.h b/client/include/app.h index 2fbbaae3..3e29c33a 100644 --- a/client/include/app.h +++ b/client/include/app.h @@ -146,7 +146,9 @@ GraphHandle app_registerGraph(const char * name, RingBuffer buffer, void app_unregisterGraph(GraphHandle handle); void app_invalidateGraph(GraphHandle handle); 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 (*callback)(void * udata, int * id), void * udata); diff --git a/client/include/interface/renderer.h b/client/include/interface/renderer.h index 4285bd04..7b95741e 100644 --- a/client/include/interface/renderer.h +++ b/client/include/interface/renderer.h @@ -133,6 +133,25 @@ typedef struct 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 { LG_CURSOR_COLOR , @@ -237,20 +256,24 @@ typedef struct LG_RendererOps bool (*onFrameFormat)(LG_Renderer * renderer, const LG_RendererFormat format); - /* called when there is a new frame - * Context: frameThread */ + /* called when there is a new frame. frameToken must remain attached to the + * 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, - const FrameDamageRect * damage, int damageCount); + const FrameDamageRect * damage, int damageCount, + LG_RendererFrameToken frameToken); /* called when the rederer is to startup * Context: renderThread */ bool (*renderStartup)(LG_Renderer * renderer, bool useDMA); - /* called to render the scene - * Context: renderThread */ + /* called to render the scene. The renderer must not consume a received + * 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, - const bool newFrame, const bool invalidateWindow, - void (*preSwap)(void * udata), void * udata); + LG_RendererFrameToken frameTokenLimit, const bool invalidateWindow, + void (*preSwap)(void * udata), void * udata, + LG_RendererFrameTiming * timing); /* Optional test/diagnostic readback of the fully composed framebuffer. * Called on the render thread before swap while the graphics context is diff --git a/client/include/interface/transport.h b/client/include/interface/transport.h index a7fc10ce..e05887e0 100644 --- a/client/include/interface/transport.h +++ b/client/include/interface/transport.h @@ -94,6 +94,7 @@ typedef uint32_t LG_TransportFrameFlags; typedef struct LG_TransportFrameTiming { + bool valid; /* producer fields below are available and coherent */ uint64_t captureTime; uint64_t postProcessTime; uint64_t copyTime; diff --git a/client/renderers/EGL/damage.h b/client/renderers/EGL/damage.h index afd2ecbc..4cd02d9a 100644 --- a/client/renderers/EGL/damage.h +++ b/client/renderers/EGL/damage.h @@ -27,8 +27,9 @@ struct DesktopDamage { - int count; - FrameDamageRect rects[LG_MAX_FRAME_DAMAGE_RECTS]; + LG_RendererFrameToken frameToken; + int count; + FrameDamageRect rects[LG_MAX_FRAME_DAMAGE_RECTS]; }; typedef struct EGL_Damage EGL_Damage; diff --git a/client/renderers/EGL/desktop.c b/client/renderers/EGL/desktop.c index 44a358b8..fe59cc02 100644 --- a/client/renderers/EGL/desktop.c +++ b/client/renderers/EGL/desktop.c @@ -24,6 +24,7 @@ #include "common/debug.h" #include "common/option.h" #include "common/locking.h" +#include "common/time.h" #include "app.h" #include "texture.h" @@ -390,14 +391,15 @@ bool egl_desktopSetup(EGL_Desktop * desktop, const LG_RendererFormat format) 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, uint64_t * waitTimeNs) { if (likely(desktop->useDMA && dmaFd >= 0)) { if (likely(egl_textureUpdateFromDMA( - desktop->texture, frame, dmaFd, waitTimeNs))) + desktop->texture, frame, frameToken, dmaFd, waitTimeNs))) { atomic_store(&desktop->processFrame, 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 * previous texture contents instead. */ - if (likely(egl_textureUpdateFromFrame(desktop->texture, frame, + if (likely(egl_textureUpdateFromFrame(desktop->texture, frame, frameToken, damageRects, damageRectsCount, waitTimeNs))) return true; @@ -454,7 +456,10 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, unsigned int outputHeight, const float x, const float y, const float scaleX, const float scaleY, enum EGL_DesktopScaleType scaleType, 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; int width, height; @@ -478,21 +483,33 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, if (unlikely(outputWidth == 0 || outputHeight == 0)) DEBUG_FATAL("outputWidth || outputHeight == 0"); - const enum EGL_TexStatus status = egl_textureProcess(tex); - const bool textureUpdated = status == EGL_TEX_STATUS_UPDATED; + LG_RendererFrameToken frameToken; + 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 (status != EGL_TEX_STATUS_NOTREADY) 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; egl_desktopRectsMatrix(desktop->matrix, width, height, x, y, scaleX, scaleY, rotate); egl_desktopRectsUpdate(desktop->mesh, rects, width, height); - *fullFrame = false; const bool hdr = desktop->hdr && !desktop->useSpice; uint32_t hdrPeak = 0; if (hdr) @@ -509,16 +526,22 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, bool processFrame = textureUpdated; processFrame |= atomic_exchange(&desktop->processFrame, false); processFrame |= egl_postProcessConfigModified(desktop->pp); - if (processFrame && - egl_postProcessRun(desktop->pp, tex, desktop->mesh, - width, height, outputWidth, outputHeight, dma, - hdr && desktop->hdrPQ, (float)hdrPeak) && - egl_postProcessNeedsFullFrame(desktop->pp)) + if (processFrame) { - /* 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; + const uint64_t effectsStart = nanotime(); + const bool postProcessed = egl_postProcessRun( + desktop->pp, tex, desktop->mesh, + 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; diff --git a/client/renderers/EGL/desktop.h b/client/renderers/EGL/desktop.h index 2142ff65..093b7c17 100644 --- a/client/renderers/EGL/desktop.h +++ b/client/renderers/EGL/desktop.h @@ -48,7 +48,8 @@ void egl_desktopSetNativeHDR(EGL_Desktop * desktop, bool nativeHDR, void egl_desktopGetHDRMapping(EGL_Desktop * desktop, bool * enabled, float * gain, float * contentPeak); 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, uint64_t * waitTimeNs); 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, const float scaleX, const float scaleY, enum EGL_DesktopScaleType scaleType, 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_desktopSpiceDrawFill(EGL_Desktop * desktop, int x, int y, int width, diff --git a/client/renderers/EGL/egl.c b/client/renderers/EGL/egl.c index 167bff18..a3fe1c6c 100644 --- a/client/renderers/EGL/egl.c +++ b/client/renderers/EGL/egl.c @@ -378,6 +378,8 @@ static void egl_onRestart(LG_Renderer * renderer) this->frameContext = NULL; INTERLOCKED_SECTION(this->desktopDamageLock, { + this->desktopDamage[this->desktopDamageIdx].frameToken = + LG_RENDERER_FRAME_TOKEN_NONE; 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); } -static bool egl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame, int dmaFd, - const FrameDamageRect * damageRects, int damageRectsCount) +static bool egl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame, + int dmaFd, const FrameDamageRect * damageRects, int damageRectsCount, + LG_RendererFrameToken frameToken) { struct Inst * this = UPCAST(struct Inst, renderer); egl_stateCheckShared(); @@ -751,21 +754,23 @@ static bool egl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame, int d const uint64_t start = nanotime(); uint64_t waitTimeNs = 0; if (unlikely(!egl_desktopUpdate( - this->desktop, frame, dmaFd, damageRects, damageRectsCount, - &waitTimeNs))) + this->desktop, frame, frameToken, dmaFd, + damageRects, damageRectsCount, &waitTimeNs))) { DEBUG_INFO("Failed to to update the desktop"); return false; } + const uint64_t elapsed = nanotime() - start; /* Producer Copy already covers the interval before FrameBuffer::wp becomes * ready. Exclude that overlapping wait from the client Import stage. */ - app_setFrameImportTime( - elapsed > waitTimeNs ? elapsed - waitTimeNs : 0); + app_setFrameImportTiming( + elapsed > waitTimeNs ? elapsed - waitTimeNs : 0, waitTimeNs); INTERLOCKED_SECTION(this->desktopDamageLock, { - struct DesktopDamage * damage = this->desktopDamage + this->desktopDamageIdx; + struct DesktopDamage * damage = + this->desktopDamage + this->desktopDamageIdx; if (unlikely( damage->count == -1 || damageRectsCount == 0 || @@ -779,6 +784,7 @@ static bool egl_onFrame(LG_Renderer * renderer, const FrameBuffer * frame, int d damageRectsCount * sizeof(FrameDamageRect)); damage->count += damageRectsCount; } + damage->frameToken = frameToken; }); return true; @@ -1334,10 +1340,14 @@ inline static void renderLetterBox(struct Inst * this) } static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, - const bool newFrame, 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); + *timing = (LG_RendererFrameTiming) {}; + + const uint64_t setupStart = nanotime(); egl_stateCheckShared(); EGLint bufferAge = egl_bufferAge(this); 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 || this->showSpice; - bool hasOverlay = false; - struct CursorState cursorState = { .visible = false }; - struct DesktopDamage * desktopDamage; + bool hasOverlay = false; + struct CursorState cursorState = { .visible = false }; + struct DesktopDamage noDesktopDamage = {}; + struct DesktopDamage * desktopDamage = NULL; struct DamageRects * accumulated = (struct DamageRects *)alloca( sizeof(struct DamageRects) + @@ -1365,12 +1376,20 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, accumulated->count = 0; 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)) { for (int i = 0; i < bufferAge; ++i) { 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)) { @@ -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; - this->desktopDamage[this->desktopDamageIdx].count = 0; + + if (damageQueued) + { + 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) @@ -1427,17 +1455,46 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, } ++this->overlayHistoryIdx; - bool fullFrame = false; - const bool linearHDRComposition = egl_hdrComposeBegin(this->hdrCompose); - bool deferredLogicalCursor = false; - if (likely(this->destRect.w > 0 && this->destRect.h > 0)) + bool fullFrame = false; + const bool linearHDRComposition = egl_hdrComposeBegin( + this->hdrCompose); + 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->translateX, this->translateY, this->scaleX , this->scaleY , 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, (this->format.rotate + rotate) % LG_ROTATE_MAX, @@ -1456,9 +1513,13 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, renderLetterBox(this); hasOverlay |= - egl_damageRender(this->damage, rotate, newFrame ? desktopDamage : NULL) | + egl_damageRender( + this->damage, rotate, frameConsumed ? desktopDamage : NULL) | 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]; int damageIdx = app_renderOverlay(damage, MAX_OVERLAY_RECTS); 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) damage[i].y = this->height - damage[i].y - damage[i].h; } + const uint64_t postOverlayStart = nanotime(); if (likely(damageIdx >= 0 && cursorState.visible)) damage[damageIdx++] = cursorState.rect; @@ -1559,13 +1621,19 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, egl_cursorRender(this->cursor, (this->format.rotate + rotate) % LG_ROTATE_MAX, this->width, this->height, false, NULL); - this->hadOverlay = hasOverlay; this->cursorLast = cursorState; preSwap(udata); + + const uint64_t composeEnd = nanotime(); + timing->composeTime += composeEnd - postOverlayStart; + + const uint64_t swapStart = nanotime(); app_eglSwapBuffers(this->display, this->surface, damage, this->noSwapDamage ? 0 : damageIdx); + const uint64_t swapEnd = nanotime(); + timing->swapTime = swapEnd - swapStart; return true; } diff --git a/client/renderers/EGL/texture.c b/client/renderers/EGL/texture.c index d0a093d4..d97bdb9c 100644 --- a/client/renderers/EGL/texture.c +++ b/client/renderers/EGL/texture.c @@ -160,12 +160,14 @@ bool egl_textureUpdateRect(EGL_Texture * this, } bool egl_textureUpdateFromFrame(EGL_Texture * this, - const FrameBuffer * frame, const FrameDamageRect * damageRects, - int damageRectsCount, uint64_t * waitTimeNs) + const FrameBuffer * frame, LG_RendererFrameToken frameToken, + const FrameDamageRect * damageRects, int damageRectsCount, + uint64_t * waitTimeNs) { const struct EGL_TexUpdate update = { .type = EGL_TEXTYPE_FRAMEBUFFER, + .frameToken = frameToken, .x = 0, .y = 0, .width = this->format.width, @@ -182,18 +184,20 @@ bool egl_textureUpdateFromFrame(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 = { - .type = EGL_TEXTYPE_DMABUF, - .x = 0, - .y = 0, - .width = this->format.width, - .height = this->format.height, - .pitch = this->format.pitch, - .stride = this->format.stride, - .dmaFD = dmaFd + .type = EGL_TEXTYPE_DMABUF, + .frameToken = frameToken, + .x = 0, + .y = 0, + .width = this->format.width, + .height = this->format.height, + .pitch = this->format.pitch, + .stride = this->format.stride, + .dmaFD = dmaFd }; /* wait for completion */ @@ -204,9 +208,18 @@ bool egl_textureUpdateFromDMA(EGL_Texture * this, 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) diff --git a/client/renderers/EGL/texture.h b/client/renderers/EGL/texture.h index 09e7a055..02b35a89 100644 --- a/client/renderers/EGL/texture.h +++ b/client/renderers/EGL/texture.h @@ -27,6 +27,7 @@ #include "model.h" #include "common/framebuffer.h" #include "common/types.h" +#include "interface/renderer.h" #include "util.h" @@ -40,8 +41,9 @@ typedef struct EGL_Model EGL_Model; typedef struct EGL_TexUpdate { /* the type of this update */ - EGL_TexType type; - uint64_t * waitTimeNs; + EGL_TexType type; + LG_RendererFrameToken frameToken; + uint64_t * waitTimeNs; int x, y, width, height; @@ -90,7 +92,8 @@ typedef struct EGL_TextureOps bool (*update)(EGL_Texture * texture, const EGL_TexUpdate * update); /* 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 */ enum EGL_TexStatus (*get)(EGL_Texture * texture, GLuint * tex, @@ -104,10 +107,11 @@ EGL_TextureOps; struct EGL_Texture { struct EGL_TextureOps ops; - EGL_TexType type; - GLuint sampler; + EGL_TexType type; + GLuint sampler; + LG_RendererFrameToken frameToken; - EGL_TexFormat format; + EGL_TexFormat format; }; bool egl_textureInit(EGL_Texture ** texture, EGLDisplay * display, @@ -125,13 +129,24 @@ bool egl_textureUpdateRect(EGL_Texture * texture, const uint8_t * buffer, bool topDown); bool egl_textureUpdateFromFrame(EGL_Texture * texture, - const FrameBuffer * frame, const FrameDamageRect * damageRects, - int damageRectsCount, uint64_t * waitTimeNs); + const FrameBuffer * frame, LG_RendererFrameToken frameToken, + const FrameDamageRect * damageRects, int damageRectsCount, + uint64_t * waitTimeNs); 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, unsigned int * sizeX, unsigned int * sizeY, EGL_PixelFormat * fmt) diff --git a/client/renderers/EGL/texture_buffer.c b/client/renderers/EGL/texture_buffer.c index 6d17d427..0033720a 100644 --- a/client/renderers/EGL/texture_buffer.c +++ b/client/renderers/EGL/texture_buffer.c @@ -108,11 +108,15 @@ bool egl_texBufferSetup(EGL_Texture * texture, const EGL_TexSetup * setup) NULL); } - this->bufIndex = 0; - this->rIndex = -1; + this->bufIndex = 0; + this->rIndex = -1; + texture->frameToken = LG_RENDERER_FRAME_TOKEN_NONE; for (int i = 0; i < this->texCount; ++i) + { this->buf[i].updated = false; + this->slotToken[i] = LG_RENDERER_FRAME_TOKEN_NONE; + } return true; } @@ -138,8 +142,10 @@ static bool egl_texBufferUpdate(EGL_Texture * texture, const EGL_TexUpdate * upd 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; } @@ -260,13 +266,15 @@ static bool egl_texBufferStreamUpdate(EGL_Texture * texture, } } + this->slotToken[this->bufIndex] = update->frameToken; this->buf[this->bufIndex].updated = true; LG_UNLOCK(this->copyLock); return true; } -EGL_TexStatus egl_texBufferStreamProcess(EGL_Texture * texture) +EGL_TexStatus egl_texBufferStreamProcess(EGL_Texture * texture, + LG_RendererFrameToken frameTokenLimit) { TextureBuffer * this = UPCAST(TextureBuffer, texture); @@ -276,7 +284,8 @@ EGL_TexStatus egl_texBufferStreamProcess(EGL_Texture * texture) GLuint tex = this->tex[index]; EGL_TexBuffer * buffer = &this->buf[index]; - if (!buffer->updated) + if (!buffer->updated || + !egl_textureFrameAllowed(this->slotToken[index], frameTokenLimit)) { LG_UNLOCK(this->copyLock); return EGL_TEX_STATUS_OK; @@ -284,7 +293,8 @@ EGL_TexStatus egl_texBufferStreamProcess(EGL_Texture * texture) DEBUG_ASSERT(!this->sync[index]); - this->rIndex = index; + this->rIndex = index; + texture->frameToken = this->slotToken[index]; if (++this->bufIndex == this->texCount) this->bufIndex = 0; buffer->updated = false; diff --git a/client/renderers/EGL/texture_buffer.h b/client/renderers/EGL/texture_buffer.h index 7327aad8..ff41947d 100644 --- a/client/renderers/EGL/texture_buffer.h +++ b/client/renderers/EGL/texture_buffer.h @@ -31,14 +31,15 @@ typedef struct TextureBuffer EGL_Texture base; bool free; - int texCount; - GLuint tex[EGL_TEX_BUFFER_MAX]; - EGL_TexBuffer buf[EGL_TEX_BUFFER_MAX]; - int bufFree; - GLsync sync[EGL_TEX_BUFFER_MAX]; - LG_Lock copyLock; - int bufIndex; - int rIndex; + int texCount; + GLuint tex[EGL_TEX_BUFFER_MAX]; + EGL_TexBuffer buf[EGL_TEX_BUFFER_MAX]; + int bufFree; + GLsync sync[EGL_TEX_BUFFER_MAX]; + LG_RendererFrameToken slotToken[EGL_TEX_BUFFER_MAX]; + LG_Lock copyLock; + int bufIndex; + int rIndex; } TextureBuffer; @@ -46,7 +47,8 @@ bool egl_texBufferInit(EGL_Texture ** texture_, EGL_TexType type, EGLDisplay * display); void egl_texBufferFree(EGL_Texture * texture_); 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_PixelFormat * fmt); EGL_TexStatus egl_texBufferBind(EGL_Texture * texture_, GLuint unit); @@ -57,6 +59,7 @@ bool egl_texBufferStreamSetup(EGL_Texture * texture_, const EGL_TexSetup * setup); /* Returns with copyLock held when the current upload buffer is safe to write. */ 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_PixelFormat * fmt); diff --git a/client/renderers/EGL/texture_dmabuf.c b/client/renderers/EGL/texture_dmabuf.c index 37f4b0e9..0140383e 100644 --- a/client/renderers/EGL/texture_dmabuf.c +++ b/client/renderers/EGL/texture_dmabuf.c @@ -29,10 +29,11 @@ struct FdImage { - int fd; - EGLImage image; - GLsync sync; - int texIndex; + int fd; + EGLImage image; + GLsync sync; + int texIndex; + LG_RendererFrameToken frameToken; }; typedef struct TexDMABUF @@ -43,6 +44,7 @@ typedef struct TexDMABUF struct FdImage images[2]; int lastIndex; + int renderIndex; EGL_PixelFormat pixFmt; unsigned fourcc; @@ -76,10 +78,16 @@ static void egl_texDMABUFCleanup(EGL_Texture * texture) glDeleteSync(this->images[i].sync); this->images[i].sync = 0; } - this->images[i].fd = -1; - this->images[i].texIndex = -1; + this->images[i].fd = -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); 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) { - this->images[i].fd = -1; - this->images[i].image = EGL_NO_IMAGE; - this->images[i].sync = 0; - this->images[i].texIndex = -1; + this->images[i].fd = -1; + this->images[i].image = EGL_NO_IMAGE; + this->images[i].sync = 0; + 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; if (!egl_texBufferStreamInit(&parent, type, display)) @@ -276,15 +286,45 @@ static bool egl_texDMABUFUpdate(EGL_Texture * texture, 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; } -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, @@ -299,7 +339,7 @@ static EGL_TexStatus egl_texDMABUFGet(EGL_Texture * texture, GLuint * tex, INTERLOCKED_SECTION(parent->copyLock, { - index = this->lastIndex; + index = this->renderIndex; if (index >= 0) { struct FdImage * cur = &this->images[index]; diff --git a/client/renderers/EGL/texture_framebuffer.c b/client/renderers/EGL/texture_framebuffer.c index c99737f5..811e91c9 100644 --- a/client/renderers/EGL/texture_framebuffer.c +++ b/client/renderers/EGL/texture_framebuffer.c @@ -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 * caller recovers, and never expose this slot to the render context. */ - damage->count = -1; + damage->count = -1; parent->buf[parent->bufIndex].updated = false; + parent->slotToken[parent->bufIndex] = LG_RENDERER_FRAME_TOKEN_NONE; LG_UNLOCK(parent->copyLock); return false; } + parent->slotToken[parent->bufIndex] = update->frameToken; parent->buf[parent->bufIndex].updated = true; for (int i = 0; i < EGL_TEX_BUFFER_MAX; ++i) diff --git a/client/renderers/OpenGL/opengl.c b/client/renderers/OpenGL/opengl.c index a4a9506b..72d18586 100644 --- a/client/renderers/OpenGL/opengl.c +++ b/client/renderers/OpenGL/opengl.c @@ -107,44 +107,46 @@ struct Inst LG_RendererParams params; struct OpenGL_Options opt; - bool amdPinnedMemSupport; - bool renderStarted; - bool configured; - bool reconfigure; - LG_DSGLContext glContext; + bool amdPinnedMemSupport; + bool renderStarted; + bool configured; + bool reconfigure; + LG_DSGLContext glContext; - struct IntPoint window; - float uiScale; - _Atomic(bool) frameUpdate; + struct IntPoint window; + float uiScale; + _Atomic(bool) frameUpdate; - LG_Lock formatLock; - LG_RendererFormat format; - GLuint intFormat; - GLuint vboFormat; - GLuint dataFormat; - size_t texSize; - size_t texPos; - float scaleX, scaleY; - const FrameBuffer * frame; + LG_Lock formatLock; + LG_RendererFormat format; + GLuint intFormat; + GLuint vboFormat; + GLuint dataFormat; + size_t texSize; + size_t texPos; + float scaleX, scaleY; + const FrameBuffer * frame; + LG_RendererFrameToken pendingFrameToken; - uint64_t drawStart; - bool hasBuffers; - GLuint vboID[BUFFER_COUNT]; - uint8_t * texPixels[BUFFER_COUNT]; - LG_Lock frameLock; - bool texReady; - int texWIndex, texRIndex; - int texList; - int mouseList; - int spiceList; - LG_RendererRect destRect; - struct IntPoint spiceSize; - bool spiceShow; + uint64_t drawStart; + bool hasBuffers; + GLuint vboID[BUFFER_COUNT]; + uint8_t * texPixels[BUFFER_COUNT]; + LG_Lock frameLock; + bool texReady; + int texWIndex, texRIndex; + int texList; + int mouseList; + int spiceList; + LG_RendererRect destRect; + struct IntPoint spiceSize; + bool spiceShow; - bool hasTextures, hasFrames; - GLuint frames[BUFFER_COUNT]; - GLsync fences[BUFFER_COUNT]; - GLuint textures[TEXTURE_COUNT]; + bool hasTextures, hasFrames; + GLuint frames[BUFFER_COUNT]; + GLsync fences[BUFFER_COUNT]; + LG_RendererFrameToken frameToken[BUFFER_COUNT]; + GLuint textures[TEXTURE_COUNT]; LG_Lock mouseLock; LG_RendererCursor mouseCursor; @@ -174,7 +176,9 @@ enum ConfigStatus static void deconfigure(struct Inst * this); static enum ConfigStatus configure(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); 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, - const FrameDamageRect * damage, int damageCount) + const FrameDamageRect * damage, int damageCount, + LG_RendererFrameToken frameToken) { struct Inst * this = UPCAST(struct Inst, renderer); LG_LOCK(this->frameLock); - this->frame = frame; + this->frame = frame; + this->pendingFrameToken = frameToken; atomic_store_explicit(&this->frameUpdate, true, memory_order_release); LG_UNLOCK(this->frameLock); @@ -486,10 +492,13 @@ bool opengl_renderStartup(LG_Renderer * renderer, bool useDMA) return true; } -bool opengl_render(LG_Renderer * renderer, LG_RendererRotate rotate, const bool newFrame, - const bool invalidateWindow, void (*preSwap)(void * udata), void * udata) +bool opengl_render(LG_Renderer * renderer, LG_RendererRotate rotate, + LG_RendererFrameToken frameTokenLimit, const bool invalidateWindow, + void (*preSwap)(void * udata), void * udata, + LG_RendererFrameTiming * timing) { struct Inst * this = UPCAST(struct Inst, renderer); + *timing = (LG_RendererFrameTiming) {}; setupModelView(this); @@ -501,7 +510,7 @@ bool opengl_render(LG_Renderer * renderer, LG_RendererRotate rotate, const bool case CONFIG_STATUS_NOOP : case CONFIG_STATUS_OK : - if (!drawFrame(this)) + if (!drawFrame(this, frameTokenLimit, &timing->frameToken)) return false; } @@ -1024,16 +1033,17 @@ static void deconfigure(struct Inst * this) 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]) { free(this->texPixels[i]); @@ -1208,8 +1218,12 @@ static bool opengl_bufferFn(void * opaque, const void * data, size_t size) 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])) { 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->texRIndex = this->texWIndex; + *consumedFrameToken = this->frameToken[this->texRIndex]; + this->frameToken[this->texRIndex] = LG_RENDERER_FRAME_TOKEN_NONE; if (++this->texWIndex == BUFFER_COUNT) this->texWIndex = 0; } 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); return true; } + atomic_store_explicit(&this->frameUpdate, false, memory_order_release); + const LG_RendererFrameToken pendingFrameToken = this->pendingFrameToken; LG_LOCK(this->formatLock); 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 this->fences[this->texWIndex] = g_gl_dynProcs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + this->frameToken[this->texWIndex] = pendingFrameToken; glFlush(); LG_UNLOCK(this->formatLock); diff --git a/client/src/app.c b/client/src/app.c index 6d71855a..e45f2f88 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -922,9 +922,10 @@ void app_setGraphCompact(GraphHandle handle, bool 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) diff --git a/client/src/main.c b/client/src/main.c index 2e1f8633..f830c811 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -199,82 +199,296 @@ static bool tickTimerFn(void * unused) 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 postProcessTime; uint64_t copyTime; uint64_t readyTime; 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 (;;) - { - const unsigned sequence = atomic_load_explicit( - &g_state.frameTimingSequence, memory_order_seq_cst); - if (sequence & 1) - continue; +/* The frame and render threads complete records independently. A token is + * published only after both halves have arrived, while queuedToken prevents a + * renderer that woke for unrelated work from consuming an unqueued frame. */ - timing.captureTime = atomic_load_explicit( - &g_state.producerCaptureTime, memory_order_seq_cst); - timing.postProcessTime = atomic_load_explicit( - &g_state.producerPostProcessTime, memory_order_seq_cst); - timing.copyTime = atomic_load_explicit( - &g_state.producerCopyTime, memory_order_seq_cst); - timing.readyTime = atomic_load_explicit( - &g_state.producerReadyTime, memory_order_seq_cst); - timing.importTime = atomic_load_explicit( - &g_state.clientImportTime, memory_order_seq_cst); - - if (sequence == atomic_load_explicit( - &g_state.frameTimingSequence, memory_order_seq_cst)) - return timing; - } +static void frameTimingInit(void) +{ + LG_LOCK_INIT(l_frameTiming.lock); + atomic_store_explicit(&l_frameTiming.queuedToken, + LG_RENDERER_FRAME_TOKEN_NONE, memory_order_relaxed); + l_frameTiming.nextToken = LG_RENDERER_FRAME_TOKEN_NONE; + l_frameTiming.retireToken = 1; + l_frameTiming.publishRead = 0; + l_frameTiming.publishWrite = 0; + l_frameTiming.publishCount = 0; + l_frameTiming.overflowWarning = false; + memset(l_frameTiming.record, 0, sizeof(l_frameTiming.record)); } -static void frameTimingStore(const LG_TransportFrameTiming * timing, - uint64_t importTime) +static void frameTimingReset(void) { - atomic_fetch_add_explicit( - &g_state.frameTimingSequence, 1, memory_order_seq_cst); - atomic_store_explicit(&g_state.producerCaptureTime, - timing->captureTime, memory_order_seq_cst); - atomic_store_explicit(&g_state.producerPostProcessTime, - timing->postProcessTime, memory_order_seq_cst); - atomic_store_explicit(&g_state.producerCopyTime, - timing->copyTime, memory_order_seq_cst); - atomic_store_explicit(&g_state.producerReadyTime, - timing->readyTime, memory_order_seq_cst); - atomic_store_explicit(&g_state.clientImportTime, - importTime, memory_order_seq_cst); - atomic_fetch_add_explicit( - &g_state.frameTimingSequence, 1, memory_order_seq_cst); + INTERLOCKED_SECTION(l_frameTiming.lock, { + memset(l_frameTiming.record, 0, sizeof(l_frameTiming.record)); + l_frameTiming.retireToken = l_frameTiming.nextToken + 1; + l_frameTiming.publishRead = 0; + l_frameTiming.publishWrite = 0; + l_frameTiming.publishCount = 0; + l_frameTiming.overflowWarning = false; + }); + /* Tokens remain monotonic across reconnects so stale renderer state can + * never alias a new frame. No frame is consumable until it is queued. */ + atomic_store_explicit(&l_frameTiming.queuedToken, + LG_RENDERER_FRAME_TOKEN_NONE, memory_order_release); +} + +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) { - const struct RenderTiming * timing = (const struct RenderTiming *)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); - } + (void)udata; #ifdef ENABLE_TESTS if (!l_testCapture.enabled || l_testCapture.complete) @@ -425,6 +639,8 @@ static int renderThread(void * unused) } } + frameTimingPublishReady(); + int resize = atomic_load(&g_state.lgrResize); if (unlikely(resize)) { @@ -460,29 +676,32 @@ static int renderThread(void * unused) 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); - struct RenderTiming renderTiming = - newFrame ? frameTimingLoad() : (struct RenderTiming) {}; - renderTiming.renderStart = nanotime(); + const LG_RendererFrameToken frameTokenLimit = frameTimingQueuedToken(); + const uint64_t prepareStart = nanotime(); + LG_LOCK(g_state.lgrLock); renderQueue_process(); - if (unlikely(!RENDERER(render, g_params.winRotate, newFrame, invalidate, - preSwapCallback, (void *)&renderTiming))) + const uint64_t prepareTime = nanotime() - prepareStart; + + LG_RendererFrameTiming rendererTiming = {}; + if (unlikely(!RENDERER(render, g_params.winRotate, frameTokenLimit, + invalidate, preSwapCallback, NULL, &rendererTiming))) { LG_UNLOCK(g_state.lgrLock); break; } + const uint64_t renderEnd = nanotime(); 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 delta = t - g_state.lastRenderTime; @@ -692,6 +911,7 @@ int main_frameThread(void * unused) continue; } frameSerial = frame.serial; + const uint64_t dispatchStart = nanotime(); const LG_TransportFrameFormat * format = frame.format; if (!format) @@ -842,23 +1062,42 @@ int main_frameThread(void * unused) 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, - frame.damageRects, damageCount)) + frame.damageRects, damageCount, frameToken)) { + frameTimingCancel(frameToken); g_state.transportOps->releaseFrame(g_state.transport, &frame); DEBUG_ERROR("Renderer onFrame returned failure"); app_setState(APP_STATE_SHUTDOWN); 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 = {}; if (g_state.transportOps->getFrameTiming) g_state.transportOps->getFrameTiming( g_state.transport, &frame, &timing); - frameTimingStore(&timing, g_state.frameImportTime); - overlaySplash_show(false); if ((frame.flags & LG_TRANSPORT_FRAME_REQUEST_ACTIVATION) && g_params.requestActivation) @@ -876,19 +1115,7 @@ int main_frameThread(void * unused) g_state.autoIdleInhibitState = blockScreensaver; } - 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 - 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); + frameTimingFinishFrame(frameToken, &timing); g_state.transportOps->releaseFrame(g_state.transport, &frame); app_useSpiceDisplay(false); @@ -1322,6 +1549,8 @@ static int transportSessionProbe(void * opaque) static int lg_run(void) { + frameTimingInit(); + #ifdef ENABLE_TESTS memset(&l_testCapture, 0, sizeof(l_testCapture)); atomic_store_explicit(&l_testFrameSerial, 0, memory_order_relaxed); @@ -1620,6 +1849,7 @@ static int lg_run(void) int msgsCount; restart: + frameTimingReset(); msgsCount = 0; memset(msgs, 0, sizeof(msgs)); @@ -1867,6 +2097,7 @@ static void lg_shutdown(void) // free metrics ringbuffers ringbuffer_free(&g_state.renderTimings); ringbuffer_free(&g_state.frameLatency); + LG_LOCK_FREE(l_frameTiming.lock); free(g_state.fontName); igDestroyContext(NULL); diff --git a/client/src/main.h b/client/src/main.h index eb220430..bf416b99 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -148,14 +148,9 @@ struct AppState RingBuffer renderTimings; RingBuffer frameLatency; uint64_t frameImportTime; + uint64_t frameImportWaitTime; 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(float) fps, ups; diff --git a/client/src/overlay/graphs.c b/client/src/overlay/graphs.c index 83fdb8af..9a8e8d67 100644 --- a/client/src/overlay/graphs.c +++ b/client/src/overlay/graphs.c @@ -64,6 +64,49 @@ struct OverlayGraph 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) { @@ -127,10 +170,7 @@ static void graphs_free(void * udata) { struct OverlayGraph * graph; while(ll_shift(gs.graphs, (void **)&graph)) - { - free(graph->name); - free(graph); - } + graphFree(graph); ll_free(gs.graphs); gs.graphs = NULL; @@ -175,20 +215,37 @@ static bool rbCalcMetrics(int index, void * value_, void * udata_) return true; } -#define TIMING_PLOT_BUCKETS 100 -#define TIMING_PLOT_WINDOW_NS 20000000000ULL -#define TIMING_PLOT_BUCKET_NS \ +#define TIMING_PLOT_BUCKETS 100 +#define TIMING_PLOT_WINDOW_NS 20000000000ULL +#define TIMING_PLOT_BUCKET_NS \ (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 { uint64_t windowStart; uint64_t windowEnd; - float stageMin[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; - float stageMax[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; - float stageSum[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; - unsigned count[TIMING_PLOT_BUCKETS]; + float stageMin[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; + float stageMax[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; + float stageSum[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; + unsigned stageSamples[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; + unsigned samples[TIMING_PLOT_BUCKETS]; }; static float roundTimingScale(float value) @@ -223,8 +280,10 @@ static float graphRowWeight(const struct OverlayGraph * graph) 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_; const OverlayFrameTiming * timing = value_; if (timing->timestamp < data->windowStart) @@ -232,29 +291,44 @@ static bool accumulateTimingSample(int index, void * value_, void * udata_) if (timing->timestamp >= data->windowEnd) return false; - const uint64_t offset = timing->timestamp - data->windowStart; - const int bucket = min(offset / TIMING_PLOT_BUCKET_NS, - TIMING_PLOT_BUCKETS - 1); - const float values[TIMING_STAGE_COUNT] = { + const float values[FRAME_TIMING_STAGE_COUNT] = { timing->capture, timing->postProcess, timing->copy, timing->ready, 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]) - data->stageMin[i][bucket] = data->stageMax[i][bucket] = values[i]; + if (!(timing->validMask & (1U << stage))) + continue; + + const float value = values[stage]; + if (!data->stageSamples[stage][bucket]) + data->stageMin[stage][bucket] = data->stageMax[stage][bucket] = value; else { - data->stageMin[i][bucket] = min(data->stageMin[i][bucket], values[i]); - data->stageMax[i][bucket] = max(data->stageMax[i][bucket], values[i]); + data->stageMin[stage][bucket] = + 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; } @@ -315,20 +389,24 @@ static void renderLineGraph(struct OverlayGraph * graph, bool interactive, static void renderTimingStatistic(struct OverlayGraph * graph, 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) { - float cumulative[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; - float zero[TIMING_PLOT_BUCKETS] = {}; + float cumulative[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; + float zero[TIMING_PLOT_BUCKETS]; float xValues[TIMING_PLOT_BUCKETS]; float peak = 0.0f; for (int bucket = 0; bucket < TIMING_PLOT_BUCKETS; ++bucket) { + const bool populated = isfinite(values[0][bucket]); xValues[bucket] = bucket; - for (int stage = 0; stage < TIMING_STAGE_COUNT; ++stage) - cumulative[stage][bucket] = values[stage][bucket] + - (stage ? cumulative[stage - 1][bucket] : 0.0f); - peak = max(peak, cumulative[TIMING_STAGE_COUNT - 1][bucket]); + zero[bucket] = populated ? 0.0f : NAN; + for (int stage = 0; stage < FRAME_TIMING_STAGE_COUNT; ++stage) + cumulative[stage][bucket] = populated ? values[stage][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); @@ -346,26 +424,20 @@ static void renderTimingStatistic(struct OverlayGraph * graph, ImPlot_SetupLegend(ImPlotLocation_South, ImPlotLegendFlags_Outside | ImPlotLegendFlags_Horizontal); - const char * labels[TIMING_STAGE_COUNT] = { - "Capture", - "Post", - "Copy", - "Ready", - "Import", - "Render", - }; - gs.plotSpec->Offset = 0; 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 = - ImPlot_GetColormapColor(stage, ImPlotColormap_Deep); + const ImVec4 color = stage < colorCount ? + ImPlot_GetColormapColor(stage, ImPlotColormap_Paired) : + (ImVec4) {0.75f, 0.75f, 0.75f, 1.0f}; gs.plotSpec->FillColor = color; gs.plotSpec->FillAlpha = 0.35f; gs.plotSpec->LineWeight = 1.0f; gs.plotSpec->Flags = ImPlotShadedFlags_None; - ImPlot_PlotShaded_FloatPtrFloatPtrFloatPtr(labels[stage], xValues, + ImPlot_PlotShaded_FloatPtrFloatPtrFloatPtr( + frameTimingLabels[stage], xValues, stage ? cumulative[stage - 1] : zero, cumulative[stage], TIMING_PLOT_BUCKETS, *gs.plotSpec); @@ -373,7 +445,7 @@ static void renderTimingStatistic(struct OverlayGraph * graph, gs.plotSpec->LineWeight = 1.0f; gs.plotSpec->FillAlpha = 1.0f; 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); } @@ -383,31 +455,39 @@ static void renderTimingStatistic(struct OverlayGraph * graph, ImPlot_EndPlot(); } -static void renderTimingGraph(struct OverlayGraph * graph, bool interactive, - ImVec2 size) +static void renderFrameTimingGraph(struct OverlayGraph * graph, + bool interactive, ImVec2 size) { 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.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 maximum[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; - float average[TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; + float minimum[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; + float maximum[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; + float average[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; for (int bucket = 0; bucket < TIMING_PLOT_BUCKETS; ++bucket) - { - if (!data.count[bucket]) - continue; - - for (int stage = 0; stage < TIMING_STAGE_COUNT; ++stage) + for (int stage = 0; stage < FRAME_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]; maximum[stage][bucket] = data.stageMax[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 height = (size.y - spacing * 2.0f) / 3.0f; @@ -477,7 +557,8 @@ static int graphs_render(void * udata, bool interactive, igPushID_Ptr(graph); const float height = rowHeight * graphRowWeight(graph); if (graph->type == OVERLAY_GRAPH_FRAME_TIMING) - renderTimingGraph(graph, interactive, (ImVec2) {winSize.x, height}); + renderFrameTimingGraph(graph, interactive, + (ImVec2) {winSize.x, height}); else renderLineGraph(graph, interactive, (ImVec2) {winSize.x, height}); igPopID(); @@ -502,47 +583,26 @@ struct LG_OverlayOps LGOverlayGraphs = GraphHandle overlayGraph_register(const char * name, RingBuffer buffer, float min, float max, GraphFormatFn formatFn) { - if (!name || !*name) - { - DEBUG_ERROR("graph name must not be empty"); - return NULL; - } - - struct OverlayGraph * graph = malloc(sizeof(*graph)); + struct OverlayGraph * graph = graphNew(name, buffer); if (!graph) - { - DEBUG_ERROR("out of memory"); 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->min = min; graph->max = max; - for (int i = 0; i < TIMING_STATISTIC_COUNT; ++i) - graph->yScale[i] = 0.0f; graph->formatFn = formatFn; - ll_push(gs.graphs, graph); - return graph; + return graphPublish(graph); } GraphHandle overlayGraph_registerFrameTiming(const char * name, RingBuffer buffer) { - GraphHandle graph = overlayGraph_register(name, buffer, 0.0f, 0.0f, NULL); - if (graph) - graph->type = OVERLAY_GRAPH_FRAME_TIMING; - return graph; + struct OverlayGraph * graph = graphNew(name, buffer); + if (!graph) + return NULL; + + graph->type = OVERLAY_GRAPH_FRAME_TIMING; + return graphPublish(graph); } void overlayGraph_unregister(GraphHandle handle) @@ -551,8 +611,7 @@ void overlayGraph_unregister(GraphHandle handle) return; ll_removeData(gs.graphs, handle); - free(handle->name); - free(handle); + graphFree(handle); if (gs.show) app_invalidateWindow(false); diff --git a/client/src/overlays.h b/client/src/overlays.h index 223284c6..17d8dab8 100644 --- a/client/src/overlays.h +++ b/client/src/overlays.h @@ -47,15 +47,46 @@ void overlayAlert_show(LG_MsgAlert type, const char * fmt, va_list args); typedef struct OverlayFrameTiming { uint64_t timestamp; + uint32_t validMask; float capture; float postProcess; float copy; float ready; float import; - float render; + float dispatch; + float queue; + float prepare; + float setup; + float effects; + float desktop; + float compose; + float swap; } 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, float min, float max, GraphFormatFn formatFn); GraphHandle overlayGraph_registerFrameTiming(const char * name, diff --git a/client/transports/LGMP/lgmp.c b/client/transports/LGMP/lgmp.c index 29ba8873..78299a93 100644 --- a/client/transports/LGMP/lgmp.c +++ b/client/transports/LGMP/lgmp.c @@ -36,6 +36,8 @@ #include #include +#define LGMP_TIMING_SPIN_COUNT 4096 + struct DMAFrameInfo { const KVMFRFrame * frame; @@ -622,18 +624,21 @@ static void lgmp_getFrameTiming(LG_Transport * this, this->pendingFrame->frameSerial != frame->serial) return; - /* The producer writes these immediately after completing the framebuffer. - * nextFrame can observe the header earlier, so sample them only after the - * renderer's onFrame call has consumed the framebuffer. */ + /* The producer writes these immediately after publishing FrameBuffer::wp. + * nextFrame can observe the header earlier, so briefly observe the + * publication tail after onFrame consumes the framebuffer without sleeping + * the frame-acquisition thread. */ for (unsigned i = 0; !lgmp_frameTimingReady(this->pendingFrame) && - i < 1000; + i < LGMP_TIMING_SPIN_COUNT; ++i) - usleep(1); + { + } if (!lgmp_frameTimingReady(this->pendingFrame)) return; + timing->valid = true; timing->captureTime = this->pendingFrame->captureTime; timing->postProcessTime = this->pendingFrame->postProcessTime; timing->copyTime = this->pendingFrame->copyTime;