diff --git a/idd/LGIdd/CFrameScheduler.cpp b/idd/LGIdd/CFrameScheduler.cpp index 37b88ee2..b7e158d0 100644 --- a/idd/LGIdd/CFrameScheduler.cpp +++ b/idd/LGIdd/CFrameScheduler.cpp @@ -22,10 +22,13 @@ #include "CDebug.h" +#include + static const uint64_t MIN_PERIOD_NS = 2000000ULL; static const uint64_t MAX_PERIOD_NS = 1000000000ULL; static const uint32_t MIN_LEASE_MS = 100; static const uint32_t MAX_LEASE_MS = 5000; +static const uint64_t MIN_SAFETY_NS = 250000ULL; uint64_t CFrameScheduler::Nanotime() { @@ -105,6 +108,9 @@ void CFrameScheduler::ElectOwner(uint64_t now) if (oldClientID != m_schedule.clientID || oldGeneration != m_schedule.generation) { + m_nextDeadline = m_scheduling ? now + m_schedule.period : 0; + m_forceNext = m_scheduling; + if (m_scheduling) DEBUG_INFO("Frame timing owner %u generation %u at %.3f Hz", m_schedule.clientID, m_schedule.generation, @@ -121,6 +127,15 @@ void CFrameScheduler::Reset() client = {}; m_schedule = {}; m_scheduling = false; + m_forceNext = false; + + m_lastArrival = 0; + m_guestPeriod = 0; + m_guestJitter = 0; + m_workEstimate = 0; + m_nextDeadline = 0; + m_arrivalSamples = 0; + m_timingSamples = 0; ReleaseSRWLockExclusive(&m_lock); } @@ -214,3 +229,92 @@ bool CFrameScheduler::GetSchedule(Schedule& schedule) const ReleaseSRWLockShared(&m_lock); return result; } + +void CFrameScheduler::ObserveFrame(uint64_t now) +{ + AcquireSRWLockExclusive(&m_lock); + if (m_lastArrival && now > m_lastArrival) + { + const uint64_t interval = now - m_lastArrival; + if (interval >= MIN_PERIOD_NS && interval <= MAX_PERIOD_NS) + { + if (!m_guestPeriod) + m_guestPeriod = interval; + else + { + const uint64_t error = m_guestPeriod > interval ? + m_guestPeriod - interval : interval - m_guestPeriod; + m_guestPeriod = (m_guestPeriod * 7 + interval) / 8; + m_guestJitter = (m_guestJitter * 7 + error) / 8; + } + + if (m_arrivalSamples < 32) + ++m_arrivalSamples; + } + } + m_lastArrival = now; + ReleaseSRWLockExclusive(&m_lock); +} + +bool CFrameScheduler::SelectFrame(uint64_t now, bool force, + uint32_t& generation) +{ + generation = 0; + AcquireSRWLockExclusive(&m_lock); + if (!m_scheduling) + { + ReleaseSRWLockExclusive(&m_lock); + return true; + } + + generation = m_schedule.generation; + while (m_nextDeadline <= now) + m_nextDeadline += m_schedule.period; + + if (force) + m_forceNext = true; + + if (m_forceNext || m_arrivalSamples < 4 || m_timingSamples < 4) + { + ReleaseSRWLockExclusive(&m_lock); + return true; + } + + const uint64_t safety = + std::max(MIN_SAFETY_NS, + std::max(m_guestJitter * 2, m_workEstimate / 8)); + const uint64_t nextArrival = m_lastArrival + m_guestPeriod; + const bool process = nextArrival <= now || + nextArrival + m_workEstimate + safety > m_nextDeadline; + ReleaseSRWLockExclusive(&m_lock); + return process; +} + +void CFrameScheduler::FramePublished(uint32_t generation, uint64_t now) +{ + AcquireSRWLockExclusive(&m_lock); + if (m_scheduling && generation == m_schedule.generation) + { + m_forceNext = false; + do + m_nextDeadline += m_schedule.period; + while (m_nextDeadline <= now); + } + ReleaseSRWLockExclusive(&m_lock); +} + +void CFrameScheduler::RecordFrameTiming(uint64_t duration) +{ + if (!duration) + return; + + AcquireSRWLockExclusive(&m_lock); + if (!m_workEstimate || duration > m_workEstimate) + m_workEstimate = duration; + else + m_workEstimate = (m_workEstimate * 31 + duration) / 32; + + if (m_timingSamples < 32) + ++m_timingSamples; + ReleaseSRWLockExclusive(&m_lock); +} diff --git a/idd/LGIdd/CFrameScheduler.h b/idd/LGIdd/CFrameScheduler.h index bae9a300..c354f67e 100644 --- a/idd/LGIdd/CFrameScheduler.h +++ b/idd/LGIdd/CFrameScheduler.h @@ -56,6 +56,15 @@ private: Client m_clients[LGMP_MAX_CLIENTS] = {}; Schedule m_schedule = {}; bool m_scheduling = false; + bool m_forceNext = false; + + uint64_t m_lastArrival = 0; + uint64_t m_guestPeriod = 0; + uint64_t m_guestJitter = 0; + uint64_t m_workEstimate = 0; + uint64_t m_nextDeadline = 0; + unsigned m_arrivalSamples = 0; + unsigned m_timingSamples = 0; Client * FindClient(uint32_t clientID); void ElectOwner(uint64_t now); @@ -68,4 +77,8 @@ public: uint64_t now); bool UpdateSchedule(const KVMFRFrameSchedule& schedule, uint64_t now); bool GetSchedule(Schedule& schedule) const; + void ObserveFrame(uint64_t now); + bool SelectFrame(uint64_t now, bool force, uint32_t& generation); + void FramePublished(uint32_t generation, uint64_t now); + void RecordFrameTiming(uint64_t duration); }; diff --git a/idd/LGIdd/CIndirectDeviceContext.cpp b/idd/LGIdd/CIndirectDeviceContext.cpp index 006d8b37..d537df01 100644 --- a/idd/LGIdd/CIndirectDeviceContext.cpp +++ b/idd/LGIdd/CIndirectDeviceContext.cpp @@ -1503,14 +1503,16 @@ CIndirectDeviceContext::PreparedFrameBuffer CIndirectDeviceContext::PrepareFrame return result; } -bool CIndirectDeviceContext::PublishFrameBuffer(unsigned frameIndex) +bool CIndirectDeviceContext::PublishFrameBuffer(unsigned frameIndex, + uint32_t scheduleGeneration) { if (!m_frameQueue || frameIndex >= LGMP_Q_FRAME_LEN) return false; AcquireSRWLockExclusive(&m_framePublishLock); const LGMP_STATUS status = - lgmpHostQueuePost(m_frameQueue, 0, m_frameMemory[frameIndex]); + lgmpHostQueuePost( + m_frameQueue, scheduleGeneration, m_frameMemory[frameIndex]); if (status == LGMP_OK) { m_publishedFrameIndex.store( @@ -1519,6 +1521,10 @@ bool CIndirectDeviceContext::PublishFrameBuffer(unsigned frameIndex) } ReleaseSRWLockExclusive(&m_framePublishLock); + if (status == LGMP_OK) + m_frameScheduler.FramePublished( + scheduleGeneration, CFrameScheduler::Nanotime()); + if (status != LGMP_OK) { DEBUG_ERROR("Failed to publish frame: %s", lgmpStatusString(status)); @@ -1528,6 +1534,22 @@ bool CIndirectDeviceContext::PublishFrameBuffer(unsigned frameIndex) return true; } +void CIndirectDeviceContext::ObserveFrame(uint64_t now) +{ + m_frameScheduler.ObserveFrame(now); +} + +bool CIndirectDeviceContext::SelectFrame(uint64_t now, bool force, + uint32_t& generation) +{ + return m_frameScheduler.SelectFrame(now, force, generation); +} + +void CIndirectDeviceContext::RecordFrameTiming(uint64_t duration) +{ + m_frameScheduler.RecordFrameTiming(duration); +} + void CIndirectDeviceContext::AbortFrameBuffer(unsigned frameIndex) { if (frameIndex >= LGMP_Q_FRAME_LEN) diff --git a/idd/LGIdd/CIndirectDeviceContext.h b/idd/LGIdd/CIndirectDeviceContext.h index e46db882..78081816 100644 --- a/idd/LGIdd/CIndirectDeviceContext.h +++ b/idd/LGIdd/CIndirectDeviceContext.h @@ -225,7 +225,7 @@ public: bool FrameBufferAvailable() const; PreparedFrameBuffer PrepareFrameBuffer(unsigned pitch, const D12FrameFormat& srcFormat, const D12FrameFormat& dstFormat, const RECT * dirtyRects, unsigned nbDirtyRects); - bool PublishFrameBuffer(unsigned frameIndex); + bool PublishFrameBuffer(unsigned frameIndex, uint32_t scheduleGeneration); void AbortFrameBuffer(unsigned frameIndex); void FailFrameBuffer(unsigned frameIndex); void CompleteFrameBuffer(unsigned frameIndex); @@ -234,6 +234,10 @@ public: void WriteFrameBuffer(unsigned frameIndex, void* src, size_t offset, size_t len, bool setWritePos) const; void FinalizeFrameBuffer(unsigned frameIndex) const; + void ObserveFrame(uint64_t now); + bool SelectFrame(uint64_t now, bool force, uint32_t& generation); + void RecordFrameTiming(uint64_t duration); + void SendCursor(const IDARG_OUT_QUERY_HWCURSOR & info, const BYTE * data, UINT sdrWhiteLevel); diff --git a/idd/LGIdd/CSwapChainProcessor.cpp b/idd/LGIdd/CSwapChainProcessor.cpp index 7a0f0f04..62be9dd5 100644 --- a/idd/LGIdd/CSwapChainProcessor.cpp +++ b/idd/LGIdd/CSwapChainProcessor.cpp @@ -31,22 +31,6 @@ static const uint32_t HDR_PQ_MAX_LUMINANCE = 10000; static_assert(LGMP_Q_FRAME_LEN == 2, "IDD damage repair assumes two alternating frame buffers"); -static uint64_t Nanotime() -{ - static const uint64_t frequency = []() - { - LARGE_INTEGER value; - QueryPerformanceFrequency(&value); - return (uint64_t)value.QuadPart; - }(); - - LARGE_INTEGER counter; - QueryPerformanceCounter(&counter); - const uint64_t ticks = (uint64_t)counter.QuadPart; - return ticks / frequency * 1000000000ULL + - ticks % frequency * 1000000000ULL / frequency; -} - static bool FrameMetadataChanged(const D12FrameFormat& previous, const D12FrameFormat& current) { @@ -258,7 +242,7 @@ bool CSwapChainProcessor::SwapChainThreadCore() // path HDR is not available, so default to SDR. DXGI_COLOR_SPACE_TYPE colorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; UINT sdrWhiteLevel = KVMFR_SDR_WHITE_LEVEL_DEFAULT; - const uint64_t captureStart = Nanotime(); + const uint64_t captureStart = CFrameScheduler::Nanotime(); #ifdef HAS_IDDCX_110 if (m_devContext->HasIddCx110DDIs()) @@ -374,7 +358,7 @@ void CSwapChainProcessor::CompletionFunction( // Publish readiness before sampling the endpoint. Timing has its own valid // flag and is published immediately afterwards. sc->m_devContext->FinalizeFrameBuffer(fbRes->GetFrameIndex()); - const uint64_t readyEnd = Nanotime(); + const uint64_t readyEnd = CFrameScheduler::Nanotime(); uint64_t postProcessTime = cpuCopyStart - fbRes->GetPostProcessStart(); uint64_t copyTime = readyEnd - cpuCopyStart; @@ -391,6 +375,8 @@ void CSwapChainProcessor::CompletionFunction( sc->m_postProcessors[fbRes->GetFrameIndex()].RecordTiming( fbRes->GetTimingEffectIndex(), fbRes->GetTimingToken(), fbRes->IsFullCopy(), postProcessTime + copyTime + readyTime); + sc->m_devContext->RecordFrameTiming( + postProcessTime + copyTime + readyTime); sc->m_devContext->SetFrameTiming(fbRes->GetFrameIndex(), fbRes->GetCaptureTime(), postProcessTime, copyTime, readyTime); sc->m_devContext->CompleteFrameBuffer(fbRes->GetFrameIndex()); @@ -643,9 +629,11 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr acquiredBuffer DXGI_COLOR_SPACE_TYPE colorSpace, UINT sdrWhiteLevel, uint64_t captureStart) { - const uint64_t postProcessStart = Nanotime(); + const uint64_t postProcessStart = CFrameScheduler::Nanotime(); const uint64_t captureTime = postProcessStart - captureStart; + m_devContext->ObserveFrame(postProcessStart); + // Preserve the fast drop path: never hold an IddCx frame while waiting for // a slow or disconnected client. We have not read its rectangles, so force // the next published frame to invalidate the entire image. @@ -874,12 +862,21 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr acquiredBuffer SetFullPendingDamage(); // Adaptive effects need comparable full-frame samples until they lock. - if (m_postProcessors[0].RequiresFullDamage()) + const bool requiresFullDamage = + m_postProcessors[0].RequiresFullDamage(); + if (requiresFullDamage) SetFullPendingDamage(); if (noImageUpdate && !m_hasPendingDamage) return true; + uint32_t scheduleGeneration = 0; + if (!m_devContext->SelectFrame(CFrameScheduler::Nanotime(), + needsReconfigure || postProcessFormatChanged || + frameMetadataChanged || requiresFullDamage, + scheduleGeneration)) + return true; + const D12FrameFormat& dstFormat = m_postProcessors[0].GetOutputFormat(); const unsigned pitch = m_postProcessors[0].GetOutputPitch(); @@ -1000,7 +997,7 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr acquiredBuffer ClipDirtyRects(currentDirtyRects, &nbDirtyRects, dstFormat.width, dstFormat.height); - const uint64_t copyStart = Nanotime(); + const uint64_t copyStart = CFrameScheduler::Nanotime(); fbRes->SetTiming(captureTime, postProcessStart, copyStart); copySlot->SetCompletionCallback(&CompletionFunction, this, fbRes); @@ -1070,7 +1067,8 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr acquiredBuffer return false; } - if (!m_devContext->PublishFrameBuffer(buffer.frameIndex)) + if (!m_devContext->PublishFrameBuffer( + buffer.frameIndex, scheduleGeneration)) { SetFullPendingDamage(); return false;