Skip to content

Commit 496d5ab

Browse files
committed
Much more accurate timing for Present()
1 parent 8f79b2f commit 496d5ab

6 files changed

Lines changed: 67 additions & 26 deletions

File tree

Common/DeviceResources.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ void DX::DeviceResources::CreateWindowSizeDependentResources()
318318
swapChain.As(&m_swapChain)
319319
);
320320

321-
m_swapChain->SetMaximumFrameLatency(2);
321+
m_swapChain->SetMaximumFrameLatency(1);
322322
m_frameLatencyWaitable = m_swapChain->GetFrameLatencyWaitableObject();
323323

324324
// Associate swap chain with SwapChainPanel

Streaming/FFmpegDecoder.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,15 @@ void FFMpegDecoder::WaitForFrame() {
360360
}
361361
}
362362

363-
bool FFMpegDecoder::RenderFrameOnMainThread(std::shared_ptr<VideoRenderer>& sceneRenderer) {
363+
bool FFMpegDecoder::RenderFrameOnMainThread(std::shared_ptr<VideoRenderer> &sceneRenderer) {
364364
if (m_Pacer) {
365-
return m_Pacer->renderOnMainThread(sceneRenderer);
365+
return m_Pacer->renderOnMainThread(sceneRenderer);
366366
}
367367
return false;
368368
}
369+
370+
void FFMpegDecoder::WaitUntilPresentTarget() {
371+
if (m_Pacer) {
372+
m_Pacer->waitUntilPresentTarget();
373+
}
374+
}

Streaming/FFmpegDecoder.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ extern "C" {
1717
#define MAX_BUFFER 1024 * 1024
1818

1919
typedef struct MLFrameData {
20-
uint32_t presentationTimeMs;
21-
int64_t decodeEndQpc;
20+
uint32_t presentationTimeMs; // host's pts
21+
int64_t decodeEndQpc; // when we finished decoding
22+
int64_t presentTargetQpc; // timestamp when frame should be presented
2223
} MLFrameData;
2324

2425
namespace moonlight_xbox_dx {
@@ -36,6 +37,7 @@ class FFMpegDecoder {
3637
int ModifyFrameDropTarget(bool increase);
3738
void WaitForFrame();
3839
bool RenderFrameOnMainThread(std::shared_ptr<VideoRenderer> &sceneRenderer);
40+
void WaitUntilPresentTarget();
3941
static FFMpegDecoder *getInstance();
4042
static DECODER_RENDERER_CALLBACKS getDecoder();
4143
std::recursive_mutex mutex;

Streaming/Pacer.cpp

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,8 @@ void Pacer::vsyncEmulator() {
235235
period.initFromHz(hzNum, hzDen, baseQpc);
236236

237237
if (curDeadlineQpc > 0) {
238-
Utils::Logf("resync to hardware vsync, frame: %d, drift: %lld ticks (%.3f ms), phase: %d\n",
239-
frames, period.nextDeadlineQpc - curDeadlineQpc, QpcToMs(period.nextDeadlineQpc - curDeadlineQpc), phase);
238+
FQLog("resync to hardware vsync, frame: %d, drift: %lld ticks (%.3f ms), phase: %d\n",
239+
frames, period.nextDeadlineQpc - curDeadlineQpc, QpcToMs(period.nextDeadlineQpc - curDeadlineQpc), phase);
240240
}
241241

242242
curDeadlineQpc = baseQpc;
@@ -292,7 +292,6 @@ void Pacer::vsyncHardware() {
292292
}
293293
lastT0 = now;
294294
}
295-
296295
}
297296

298297
void Pacer::backPacer() {
@@ -405,37 +404,49 @@ void Pacer::handleVsync(int64_t nextDeadlineQpc) {
405404
// Place the first frame on the render queue
406405
AVFrame *frame = std::move(m_PacingQueue.front());
407406
m_PacingQueue.pop_front();
408-
lock.unlock();
409407

410-
// try to time this as close to deadline as possible
411-
remainingQpc = nextDeadlineQpc - QpcNow();
412-
if (remainingQpc > 150) {
413-
SleepUntilQpc(nextDeadlineQpc);
408+
// Pass along the target timestamp with the frame metadata
409+
if (frame->opaque_ref) {
410+
auto *data = reinterpret_cast<MLFrameData *>(frame->opaque_ref->data);
411+
data->presentTargetQpc = nextDeadlineQpc;
414412
}
415413

414+
lock.unlock();
415+
416+
// Render frame immediately, it will wait to be presented at presentTargetQpc
416417
enqueueFrameForRenderingAndUnlock(frame);
417418
}
418419

420+
void Pacer::enqueueFrameForRenderingAndUnlock(AVFrame *frame) {
421+
{
422+
std::scoped_lock<std::mutex> lock(m_FrameQueueLock);
423+
dropFrameForEnqueue(m_RenderQueue, PLOT_DROPPED_PACER_FRONT);
424+
m_RenderQueue.push_back(frame);
425+
}
426+
427+
// notify render loop of new frame
428+
m_RenderQueueNotEmpty.notify_one();
429+
}
430+
419431
// Main thread
420432

421433
void Pacer::waitForFrame() {
422434
// Wait for the renderer to be ready for the next frame
423-
LARGE_INTEGER t0, t1;
424-
QueryPerformanceCounter(&t0);
435+
int64_t t0 = QpcNow();
425436

426437
HANDLE flw = m_DeviceResources->GetFrameLatencyWaitable();
427438
WaitForSingleObjectEx(flw, 1000, true);
428439

429-
QueryPerformanceCounter(&t1);
430-
FQLog("waitForFrame part 1 waited %.3f ms\n", QpcToMs(t1.QuadPart - t0.QuadPart));
440+
int64_t t1 = QpcNow();
441+
FQLog("waitForFrame(): FrameLatencyWaitable waited %.3f ms\n", QpcToMs(t1 - t0));
431442

432443
std::unique_lock<std::mutex> lock(m_FrameQueueLock);
433444
m_RenderQueueNotEmpty.wait(lock, [this] {
434445
return stopping() || !m_RenderQueue.empty();
435446
});
436447

437448
int64_t t2 = QpcNow();
438-
FQLog("waitForFrame part 2 waited %.3f ms\n", QpcToMs(t2 - t1.QuadPart));
449+
FQLog("waitForFrame(): m_RenderQueueNotEmpty waited %.3f ms\n", QpcToMs(t2 - t1));
439450
}
440451

441452
bool Pacer::renderOnMainThread(std::shared_ptr<VideoRenderer> &sceneRenderer) {
@@ -453,20 +464,37 @@ bool Pacer::renderOnMainThread(std::shared_ptr<VideoRenderer> &sceneRenderer) {
453464
return false; // no frame, don't Present()
454465
}
455466

467+
// Extract the target present time for this frame
468+
if (frame->opaque_ref) {
469+
auto *data = reinterpret_cast<MLFrameData *>(frame->opaque_ref->data);
470+
m_PresentTargetQpc = data ? data->presentTargetQpc : 0;
471+
} else {
472+
m_PresentTargetQpc = 0;
473+
}
474+
456475
frontPacer(sceneRenderer, frame);
457476
return true; // ok to Present()
458477
}
459478

460-
void Pacer::enqueueFrameForRenderingAndUnlock(AVFrame *frame) {
461-
{
462-
std::scoped_lock<std::mutex> lock(m_FrameQueueLock);
463-
dropFrameForEnqueue(m_RenderQueue, PLOT_DROPPED_PACER_FRONT);
464-
m_RenderQueue.push_back(frame);
479+
void Pacer::waitUntilPresentTarget() {
480+
const int64_t target = m_PresentTargetQpc;
481+
if (target <= 0) {
482+
return;
465483
}
466484

467-
// notify render loop of new frame
468-
FQLog("enqueueFrame notifying m_RenderQueueNotEmpty\n");
469-
m_RenderQueueNotEmpty.notify_one();
485+
const int64_t now = QpcNow();
486+
if (target <= now) {
487+
FQLog("waitUntilPresentTarget(): target was %.3f ms too late\n", QpcToMs(now - target));
488+
return;
489+
}
490+
491+
FQLog("waitUntilPresentTarget(): waiting for %.3f ms\n", QpcToMs(target - now));
492+
493+
SleepUntilQpc(target);
494+
495+
// Measure how well we timed things
496+
// const double skewMs = QpcToMs(QpcNow() - target);
497+
// ImGuiPlots::instance().observeFloat(PLOT_PRESENT_ACCURACY, (float)skewMs);
470498
}
471499

472500
void Pacer::frontPacer(std::shared_ptr<VideoRenderer> &sceneRenderer, AVFrame *frame) {

Streaming/Pacer.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class Pacer {
2424

2525
bool renderOnMainThread(std::shared_ptr<moonlight_xbox_dx::VideoRenderer> &sceneRenderer);
2626

27+
void waitUntilPresentTarget();
28+
2729
int getFrameDropTarget();
2830

2931
int modifyFrameDropTarget(bool increase);
@@ -63,6 +65,7 @@ class Pacer {
6365
std::mutex m_FrameQueueLock;
6466
std::condition_variable m_RenderQueueNotEmpty;
6567
std::condition_variable m_PacingQueueNotEmpty;
68+
int64_t m_PresentTargetQpc;
6669
std::atomic<bool> m_Stopping{false};
6770
int m_StreamFps;
6871
double m_RefreshRate;

Streaming/moonlight_xbox_dxMain.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ void moonlight_xbox_dxMain::StartRenderLoop()
121121

122122
Update();
123123
if (Render()) {
124+
FFMpegDecoder::instance().WaitUntilPresentTarget();
125+
124126
m_deviceResources->Present();
125127

126128
int64_t afterPresent = QpcNow();

0 commit comments

Comments
 (0)