mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-04 06:12:04 +00:00
[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:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user