/** * Looking Glass * Copyright © 2017-2026 The Looking Glass Authors * https://looking-glass.io * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., 59 * Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "CSwapChainProcessor.h" #include "CIndirectMonitorContext.h" #include #include "CDebug.h" #include "CPipeServer.h" #ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION #define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002 #endif static const uint32_t HDR_PQ_MIN_LUMINANCE = 50; static const uint32_t HDR_PQ_MAX_LUMINANCE = 10000; static const uint64_t PUBLISH_RETRY_NS = 1000000ULL; static const DWORD CANDIDATE_WAIT_MS = 2; static_assert(LGMP_Q_FRAME_LEN == 2, "IDD candidate pipeline assumes two slots"); class CSRWExclusiveLock { private: SRWLOCK * m_lock; public: explicit CSRWExclusiveLock(SRWLOCK * lock) : m_lock(lock) { AcquireSRWLockExclusive(m_lock); } ~CSRWExclusiveLock() { ReleaseSRWLockExclusive(m_lock); } }; static bool FrameMetadataChanged(const D12FrameFormat& previous, const D12FrameFormat& current) { return previous.hdrMetadata != current.hdrMetadata || previous.sdrWhiteLevel != current.sdrWhiteLevel || (current.hdrMetadata && (memcmp(previous.displayPrimary, current.displayPrimary, sizeof(current.displayPrimary)) != 0 || memcmp(previous.whitePoint, current.whitePoint, sizeof(current.whitePoint)) != 0 || previous.maxDisplayLuminance != current.maxDisplayLuminance || previous.minDisplayLuminance != current.minDisplayLuminance || previous.maxContentLightLevel != current.maxContentLightLevel || previous.maxFrameAverageLightLevel != current.maxFrameAverageLightLevel)); } CSwapChainProcessor::CSwapChainProcessor(CIndirectMonitorContext * monitorContext, UINT64 assignmentGeneration, IDDCX_MONITOR monitor, CIndirectDeviceContext* devContext, IDDCX_SWAPCHAIN hSwapChain, std::shared_ptr dx11Device, std::shared_ptr dx12Device, HANDLE newFrameEvent) : m_monitorContext(monitorContext), m_assignmentGeneration(assignmentGeneration), m_monitor(monitor), m_devContext(devContext), m_hSwapChain(hSwapChain), m_dx11Device(dx11Device), m_dx12Device(dx12Device), m_newFrameEvent(newFrameEvent) { m_resPool.Init(dx11Device, dx12Device); m_fbPool.Init(this); const bool enableEffects = !m_dx11Device->IsSoftware(); if (!enableEffects) DEBUG_INFO("Software render adapter: post-processing disabled"); bool initialized = true; for (CPostProcessor& postProcessor : m_postProcessors) if (!postProcessor.Init(dx12Device, enableEffects)) { initialized = false; break; } if (initialized) for (unsigned i = 1; i < ARRAYSIZE(m_postProcessors); ++i) if (!m_postProcessors[i].ShareEffectState(m_postProcessors[0])) { DEBUG_ERROR("Post processor effect chains do not match"); initialized = false; break; } if (!initialized) { for (CPostProcessor& postProcessor : m_postProcessors) { postProcessor.Reset(); if (!postProcessor.Init(dx12Device, false)) DEBUG_ERROR("Failed to initialize post processor copy support"); } DEBUG_WARN( "Failed to initialize post-processing effects; effects disabled"); } // Manual-reset: all worker threads wait on this, so it must stay signalled // once set or only one thread would ever observe termination. m_terminateEvent.Attach(CreateEvent(nullptr, TRUE, FALSE, nullptr)); m_candidateEvent.Attach(CreateEvent(nullptr, FALSE, FALSE, nullptr)); m_candidateAvailableEvent.Attach( CreateEvent(nullptr, FALSE, FALSE, nullptr)); m_publishTimer.Attach(CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS)); if (!m_publishTimer.Get()) m_publishTimer.Attach(CreateWaitableTimerExW( nullptr, nullptr, 0, TIMER_ALL_ACCESS)); m_cursorDataEvent.Attach(CreateEvent(nullptr, FALSE, FALSE, nullptr)); m_shapeBuffer = new BYTE[512 * 512 * 4]; // Start the worker only after every object it can access is initialized. m_thread[0].Attach(CreateThread( nullptr, 0, _SwapChainThread, this, 0, nullptr)); m_thread[2].Attach(CreateThread( nullptr, 0, _PublisherThread, this, 0, nullptr)); } CSwapChainProcessor::~CSwapChainProcessor() { SetEvent(m_terminateEvent.Get()); if (m_thread[0].Get()) WaitForSingleObject(m_thread[0].Get(), INFINITE); if (m_thread[1].Get()) WaitForSingleObject(m_thread[1].Get(), INFINITE); if (m_thread[2].Get()) WaitForSingleObject(m_thread[2].Get(), INFINITE); // Drain in-flight GPU work / completion callbacks before releasing the // resources they reference. The swap chain was already released in the // worker epilogue, so this does not hold an IddCx frame. m_dx12Device->WaitForIdle(); ResetCandidates(); for (CPostProcessor& postProcessor : m_postProcessors) postProcessor.Reset(); m_resPool.Reset(); m_fbPool.Reset(); delete[] m_shapeBuffer; } DWORD CALLBACK CSwapChainProcessor::_SwapChainThread(LPVOID arg) { reinterpret_cast(arg)->SwapChainThread(); return 0; } static bool ArmPublishTimer(HANDLE timer, uint64_t delay) { if (!timer) return false; LARGE_INTEGER due = {}; due.QuadPart = -static_cast((delay + 99) / 100); if (!due.QuadPart) due.QuadPart = -1; return SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE) != FALSE; } DWORD CALLBACK CSwapChainProcessor::_PublisherThread(LPVOID arg) { reinterpret_cast(arg)->PublisherThread(); return 0; } bool CSwapChainProcessor::HasReadyCandidate() { bool ready = false; AcquireSRWLockShared(&m_candidateLock); for (const FrameCandidate& candidate : m_candidates) if (candidate.state == CANDIDATE_READY) { ready = true; break; } ReleaseSRWLockShared(&m_candidateLock); return ready; } void CSwapChainProcessor::PublisherThread() { DWORD avTask = 0; HANDLE avTaskHandle = AvSetMmThreadCharacteristicsW(L"Distribution", &avTask); const HANDLE scheduleEvent = m_devContext->GetFrameScheduleEvent(); HANDLE idleHandles[] = { m_terminateEvent.Get(), m_candidateEvent.Get(), scheduleEvent, }; HANDLE timerHandles[] = { m_terminateEvent.Get(), m_candidateEvent.Get(), scheduleEvent, m_publishTimer.Get(), }; for (;;) { const uint64_t now = CFrameScheduler::Nanotime(); uint64_t target; CFrameScheduler::Schedule schedule; bool periodic; bool republish; m_devContext->GetPublishTarget( now, target, schedule, periodic, republish); if (!HasReadyCandidate()) { if (republish && m_devContext->HasPublishedFrame()) { m_devContext->ProcessFrameQueue(); if (m_devContext->RepublishFrameBuffer(schedule)) continue; ArmPublishTimer(m_publishTimer.Get(), PUBLISH_RETRY_NS); if (WaitForMultipleObjects( ARRAYSIZE(timerHandles), timerHandles, FALSE, INFINITE) == WAIT_OBJECT_0) break; continue; } if (m_publishTimer.Get()) CancelWaitableTimer(m_publishTimer.Get()); if (WaitForMultipleObjects( ARRAYSIZE(idleHandles), idleHandles, FALSE, INFINITE) == WAIT_OBJECT_0) break; continue; } if (target > now) { ArmPublishTimer(m_publishTimer.Get(), target - now); if (WaitForMultipleObjects( ARRAYSIZE(timerHandles), timerHandles, FALSE, INFINITE) == WAIT_OBJECT_0) break; continue; } const uint64_t publishStart = CFrameScheduler::Nanotime(); m_devContext->ProcessFrameQueue(); if (!m_devContext->FrameBufferAvailable(schedule) || !PublishNewestCandidate( schedule, periodic, publishStart)) { ArmPublishTimer(m_publishTimer.Get(), PUBLISH_RETRY_NS); if (WaitForMultipleObjects( ARRAYSIZE(timerHandles), timerHandles, FALSE, INFINITE) == WAIT_OBJECT_0) break; } } AvRevertMmThreadCharacteristics(avTaskHandle); } void CSwapChainProcessor::SwapChainThread() { DWORD avTask = 0; HANDLE avTaskHandle = AvSetMmThreadCharacteristicsW(L"Distribution", &avTask); DEBUG_INFO("Start Thread"); // Only delete the swap chain if we took ownership of it (SetDevice // succeeded). If SetDevice failed IddCx still owns and tears it down, so // deleting it here would double-free the WDF object. Releasing it when we do // own it hands the acquired frame back to IddCx promptly. if (SwapChainThreadCore()) WdfObjectDelete((WDFOBJECT)m_hSwapChain); m_hSwapChain = nullptr; AvRevertMmThreadCharacteristics(avTaskHandle); } bool CSwapChainProcessor::SwapChainThreadCore() { ComPtr dxgiDevice; HRESULT hr = m_dx11Device->GetDevice().As(&dxgiDevice); if (FAILED(hr)) { DEBUG_ERROR_HR(hr, "Failed to get the dxgiDevice"); return false; } if (IDD_IS_FUNCTION_AVAILABLE(IddCxSetRealtimeGPUPriority)) { DEBUG_INFO("Using IddCxSetRealtimeGPUPriority"); IDARG_IN_SETREALTIMEGPUPRIORITY arg = {0}; arg.pDevice = dxgiDevice.Get(); hr = IddCxSetRealtimeGPUPriority(m_hSwapChain, &arg); if (FAILED(hr)) DEBUG_ERROR_HR(hr, "Failed to set realtime GPU thread priority"); } else { DEBUG_INFO("Using SetGPUThreadPriority"); dxgiDevice->SetGPUThreadPriority(7); } IDARG_IN_SWAPCHAINSETDEVICE setDevice = {}; setDevice.pDevice = dxgiDevice.Get(); // IddCx can unassign a swap chain while its devices are still being // created. In that case the owner signals termination and IddCx retains // responsibility for the handle because SetDevice has not succeeded. if (!m_monitorContext->IsAssignmentCurrent(m_assignmentGeneration) || WaitForSingleObject(m_terminateEvent.Get(), 0) == WAIT_OBJECT_0) return false; // A failure here (commonly DXGI_ERROR_ACCESS_LOST on the first assignment) // is not recoverable on this handle - IddCx reassigns a fresh swap chain, // which is what actually succeeds. Bail cleanly and let that happen. hr = IddCxSwapChainSetDevice(m_hSwapChain, &setDevice); if (FAILED(hr)) { if (!m_monitorContext->IsAssignmentCurrent(m_assignmentGeneration) || WaitForSingleObject(m_terminateEvent.Get(), 0) == WAIT_OBJECT_0) DEBUG_INFO("Swap chain was unassigned during device setup"); else DEBUG_ERROR_HR(hr, "IddCxSwapChainSetDevice Failed"); return false; } // Past this point SetDevice succeeded: we own the swap chain and are // responsible for deleting it. IDARG_IN_SETUP_HWCURSOR c = {}; c.CursorInfo.Size = sizeof(c.CursorInfo); c.CursorInfo.AlphaCursorSupport = TRUE; c.CursorInfo.ColorXorCursorSupport = IDDCX_XOR_CURSOR_SUPPORT_FULL; c.CursorInfo.MaxX = 512; c.CursorInfo.MaxY = 512; c.hNewCursorDataAvailable = m_cursorDataEvent.Get(); NTSTATUS status = IddCxMonitorSetupHardwareCursor(m_monitor, &c); if (!NT_SUCCESS(status)) { DEBUG_ERROR("IddCxMonitorSetupHardwareCursor Failed (0x%08x)", status); return true; } m_lastShapeId = 0; m_thread[1].Attach(CreateThread(nullptr, 0, _CursorThread, this, 0, nullptr)); // The replacement swap chain is fully initialized and no frame has been // acquired yet, so a coalesced follow-up replug may now proceed safely. m_devContext->OnSwapChainReady(); // postpone sending this to ensure we dont spam messages if we end up in a // restart loop while waiting for a valid configuration g_pipe.SetGPUStatus(m_dx11Device->IsSoftware()); UINT lastFrameNumber = 0; for (;;) { if (WaitForSingleObject(m_terminateEvent.Get(), 0) == WAIT_OBJECT_0) break; UINT frameNumber = 0; UINT dirtyRectCount = 0; UINT moveRegionCount = 0; ComPtr surface; // The surface colour space is the source of truth for the content format. // Only the buffer2 acquisition path (IddCx 1.10+) reports it; on the legacy // 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 = CFrameScheduler::Nanotime(); #ifdef HAS_IDDCX_110 if (m_devContext->HasIddCx110DDIs()) { IDARG_IN_RELEASEANDACQUIREBUFFER2 acquireIn = {}; acquireIn.Size = sizeof(acquireIn); acquireIn.AcquireSystemMemoryBuffer = FALSE; IDARG_OUT_RELEASEANDACQUIREBUFFER2 buffer = {}; buffer.MetaData.Size = sizeof(buffer.MetaData); hr = IddCxSwapChainReleaseAndAcquireBuffer2(m_hSwapChain, &acquireIn, &buffer); if (SUCCEEDED(hr)) { frameNumber = buffer.MetaData.PresentationFrameNumber; dirtyRectCount = buffer.MetaData.DirtyRectCount; surface = buffer.MetaData.pSurface; colorSpace = buffer.MetaData.SurfaceColorSpace; sdrWhiteLevel = buffer.MetaData.SdrWhiteLevel; m_sdrWhiteLevel.store(sdrWhiteLevel, std::memory_order_relaxed); UpdateHDRMetadata(buffer.MetaData); } } else #endif { IDARG_OUT_RELEASEANDACQUIREBUFFER buffer = {}; hr = IddCxSwapChainReleaseAndAcquireBuffer(m_hSwapChain, &buffer); if (SUCCEEDED(hr)) { frameNumber = buffer.MetaData.PresentationFrameNumber; dirtyRectCount = buffer.MetaData.DirtyRectCount; moveRegionCount = buffer.MetaData.MoveRegionCount; surface = buffer.MetaData.pSurface; } } if (hr == E_PENDING) { HANDLE waitHandles[] = { m_newFrameEvent, m_terminateEvent.Get() }; DWORD waitResult = WaitForMultipleObjects(ARRAYSIZE(waitHandles), waitHandles, FALSE, 17); if (waitResult == WAIT_OBJECT_0 || waitResult == WAIT_TIMEOUT) continue; else if (waitResult == WAIT_OBJECT_0 + 1) break; else { hr = HRESULT_FROM_WIN32(waitResult); break; } } else if (SUCCEEDED(hr)) { if (frameNumber != lastFrameNumber) { lastFrameNumber = frameNumber; if (!SwapChainNewFrame(surface, dirtyRectCount, moveRegionCount, colorSpace, sdrWhiteLevel, captureStart)) DEBUG_WARN("Failed to submit frame"); } // Every acquired frame must be finished before the next acquire, even if // its presentation number was a duplicate and no work was submitted. hr = IddCxSwapChainFinishedProcessingFrame(m_hSwapChain); if (FAILED(hr)) { // A lost path is normal (mode change/topology rebuild); Windows // reassigns a fresh swap chain. Just exit and let it. if (hr != STATUS_GRAPHICS_PATH_NOT_IN_TOPOLOGY) DEBUG_ERROR_HR(hr, "IddCxSwapChainFinishedProcessingFrame Failed"); break; } } else break; } return true; } void CSwapChainProcessor::CandidateCompletionFunction( CD3D12CommandSlot * slot, bool result, void * param1, void * param2) { auto sc = static_cast(param1); auto candidate = static_cast(param2); uint64_t gpuStart = 0; uint64_t gpuEnd = 0; const bool timingValid = result && slot->GetGPUTimes(gpuStart, gpuEnd); AcquireSRWLockExclusive(&sc->m_candidateLock); if (candidate->state == CANDIDATE_PREPARING) { candidate->prepareReady = CFrameScheduler::Nanotime(); candidate->prepareGPUStart = gpuStart; candidate->prepareGPUEnd = gpuEnd; candidate->prepareTimingValid = timingValid; candidate->state = result ? CANDIDATE_READY : CANDIDATE_FREE; } ReleaseSRWLockExclusive(&sc->m_candidateLock); if (!result) { sc->SetFullPendingDamage(); sc->m_devContext->ForceFrame(); } sc->SignalCandidateState(); } void CSwapChainProcessor::CompletionFunction( CD3D12CommandSlot * slot, bool result, void * param1, void * param2) { auto sc = static_cast(param1); auto fbRes = static_cast(param2); const unsigned candidateIndex = fbRes->GetCandidateIndex(); if (!result) { // The frame was reserved in LGMP before GPU submission. Make the message // releasable even though its contents failed. sc->m_devContext->FailFrameBuffer(fbRes->GetFrameIndex()); sc->SetFullPendingDamage(); sc->m_devContext->ForceFrame(); sc->ReleaseCandidate(candidateIndex); return; } uint64_t prepareCopyStart; uint64_t prepareReady; uint64_t prepareGPUStart; uint64_t prepareGPUEnd; bool prepareTimingValid; AcquireSRWLockShared(&sc->m_candidateLock); const FrameCandidate& candidate = sc->m_candidates[candidateIndex]; prepareCopyStart = candidate.prepareCopyStart; prepareReady = candidate.prepareReady; prepareGPUStart = candidate.prepareGPUStart; prepareGPUEnd = candidate.prepareGPUEnd; prepareTimingValid = candidate.prepareTimingValid; ReleaseSRWLockShared(&sc->m_candidateLock); const uint64_t publishStart = fbRes->GetCopyStart(); uint64_t gpuCopyStart = 0; uint64_t gpuCopyEnd = 0; if (sc->m_dx12Device->IsIndirectCopy()) sc->m_devContext->WriteFrameBuffer( fbRes->GetFrameIndex(), fbRes->GetMap(), 0, fbRes->GetFrameSize(), false); // Queue waits execute before the start timestamp. The end timestamp follows // the last copy command, separating GPU work from readiness dispatch. const bool gpuTimingValid = slot->GetGPUTimes(gpuCopyStart, gpuCopyEnd); // 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 = CFrameScheduler::Nanotime(); const uint64_t postProcessStart = fbRes->GetPostProcessStart(); uint64_t postProcessTime = prepareCopyStart - postProcessStart; uint64_t prepareCopyTime = prepareReady - prepareCopyStart; if (prepareTimingValid && prepareGPUStart >= postProcessStart && prepareGPUEnd >= prepareGPUStart && prepareGPUEnd <= prepareReady) { postProcessTime = prepareGPUStart - postProcessStart; prepareCopyTime = prepareGPUEnd - prepareGPUStart; } uint64_t publishCopyTime = readyEnd - publishStart; if (gpuTimingValid && gpuCopyStart >= publishStart && gpuCopyEnd >= gpuCopyStart && gpuCopyEnd <= readyEnd) publishCopyTime = gpuCopyEnd - gpuCopyStart; const uint64_t copyTime = prepareCopyTime + publishCopyTime; const uint64_t elapsed = readyEnd - postProcessStart; const uint64_t measured = postProcessTime + copyTime; const uint64_t readyTime = elapsed > measured ? elapsed - measured : 0; sc->m_postProcessors[candidateIndex].RecordTiming( fbRes->GetTimingEffectIndex(), fbRes->GetTimingToken(), fbRes->IsFullCopy(), postProcessTime + copyTime); sc->m_devContext->RecordFrameTiming(readyEnd - publishStart); sc->m_devContext->SetFrameTiming(fbRes->GetFrameIndex(), fbRes->GetCaptureTime(), postProcessTime, copyTime, readyTime); sc->m_devContext->CompleteFrameBuffer(fbRes->GetFrameIndex()); sc->ReleaseCandidate(candidateIndex); } static bool IsFullDamage(const RECT * dirtyRects, unsigned nbDirtyRects, unsigned width, unsigned height) { for (const RECT * rect = dirtyRects; rect < dirtyRects + nbDirtyRects; ++rect) if (rect->left == 0 && rect->top == 0 && rect->right == (LONG)width && rect->bottom == (LONG)height) return true; return false; } static bool DirtyRectContains(const RECT& outer, const RECT& inner) { return outer.left <= inner.left && outer.top <= inner.top && outer.right >= inner.right && outer.bottom >= inner.bottom; } static bool DirtyRectsTouchOrIntersect(const RECT& a, const RECT& b) { return a.left <= b.right && a.right >= b.left && a.top <= b.bottom && a.bottom >= b.top; } static RECT MergeDirtyRects(const RECT& a, const RECT& b) { RECT result; result.left = min(a.left , b.left ); result.top = min(a.top , b.top ); result.right = max(a.right , b.right ); result.bottom = max(a.bottom, b.bottom); return result; } static uint64_t DirtyRectArea(const RECT& rect) { const uint64_t width = (uint64_t)((int64_t)rect.right - rect.left); const uint64_t height = (uint64_t)((int64_t)rect.bottom - rect.top ); return width * height; } static bool AddCopyDirtyRect(RECT dirtyRects[], unsigned capacity, unsigned * nbDirtyRects, const RECT& dirtyRect) { RECT candidate = dirtyRect; for (unsigned i = 0; i < *nbDirtyRects;) { if (DirtyRectContains(dirtyRects[i], candidate)) return true; const RECT merged = MergeDirtyRects(dirtyRects[i], candidate); // Reduce command and overlap cost without copying more pixels than the // two original rectangles would have copied. if (DirtyRectContains(candidate, dirtyRects[i]) || (DirtyRectsTouchOrIntersect(dirtyRects[i], candidate) && DirtyRectArea(merged) <= DirtyRectArea(dirtyRects[i]) + DirtyRectArea(candidate))) { candidate = merged; --(*nbDirtyRects); dirtyRects[i] = dirtyRects[*nbDirtyRects]; i = 0; continue; } ++i; } if (*nbDirtyRects >= capacity) return false; dirtyRects[(*nbDirtyRects)++] = candidate; return true; } static bool CopyAreaCoversFrame(const RECT * dirtyRects, unsigned nbDirtyRects, unsigned width, unsigned height) { const uint64_t frameArea = (uint64_t)width * height; uint64_t copyArea = 0; for (const RECT * rect = dirtyRects; rect < dirtyRects + nbDirtyRects; ++rect) { const uint64_t area = DirtyRectArea(*rect); if (area >= frameArea - copyArea) return true; copyArea += area; } return false; } static bool ClipDirtyRect(RECT& rect, unsigned width, unsigned height) { const LONG maxRight = (LONG)width; const LONG maxBottom = (LONG)height; if (rect.left < 0 ) rect.left = 0; if (rect.top < 0 ) rect.top = 0; if (rect.right > maxRight ) rect.right = maxRight; if (rect.bottom > maxBottom) rect.bottom = maxBottom; return rect.left < rect.right && rect.top < rect.bottom; } static void ClipDirtyRects(RECT dirtyRects[], unsigned * nbDirtyRects, unsigned width, unsigned height) { unsigned out = 0; for (unsigned i = 0; i < *nbDirtyRects; ++i) { RECT rect = dirtyRects[i]; if (ClipDirtyRect(rect, width, height)) dirtyRects[out++] = rect; } *nbDirtyRects = out; } static FrameType GetFrameType(DXGI_FORMAT format) { switch (format) { case DXGI_FORMAT_B8G8R8A8_UNORM : return FRAME_TYPE_BGRA; case DXGI_FORMAT_R8G8B8A8_UNORM : return FRAME_TYPE_RGBA; case DXGI_FORMAT_R10G10B10A2_UNORM : return FRAME_TYPE_RGBA10; case DXGI_FORMAT_R16G16B16A16_FLOAT: return FRAME_TYPE_RGBA16F; default : return FRAME_TYPE_INVALID; } } void CSwapChainProcessor::SetFullPendingDamage() { AcquireSRWLockExclusive(&m_damageLock); m_hasPendingDamage = true; m_nbPendingDirtyRects = 0; ++m_damageGeneration; ReleaseSRWLockExclusive(&m_damageLock); } void CSwapChainProcessor::AccumulateFrameDamage( const RECT * dirtyRects, unsigned nbDirtyRects) { AcquireSRWLockExclusive(&m_damageLock); ++m_damageGeneration; if (nbDirtyRects > LG_MAX_DIRTY_RECTS) nbDirtyRects = 0; if (!m_hasPendingDamage) { m_hasPendingDamage = true; m_nbPendingDirtyRects = nbDirtyRects; if (nbDirtyRects) memcpy(m_pendingDirtyRects, dirtyRects, nbDirtyRects * sizeof(*m_pendingDirtyRects)); ReleaseSRWLockExclusive(&m_damageLock); return; } // Zero dirty rectangles represents full-frame damage. Once any skipped // frame requires a full update, no later rectangles can narrow it again. if (m_nbPendingDirtyRects == 0 || nbDirtyRects == 0) { m_nbPendingDirtyRects = 0; ReleaseSRWLockExclusive(&m_damageLock); return; } if (m_nbPendingDirtyRects + nbDirtyRects > LG_MAX_DIRTY_RECTS) { m_nbPendingDirtyRects = 0; ReleaseSRWLockExclusive(&m_damageLock); return; } memcpy(m_pendingDirtyRects + m_nbPendingDirtyRects, dirtyRects, nbDirtyRects * sizeof(*m_pendingDirtyRects)); m_nbPendingDirtyRects += nbDirtyRects; ReleaseSRWLockExclusive(&m_damageLock); } int CSwapChainProcessor::AcquireCandidate() { HANDLE waitHandles[] = { m_candidateAvailableEvent.Get(), m_terminateEvent.Get(), }; for (;;) { int selected = -1; uint64_t oldest = UINT64_MAX; bool superseded = false; AcquireSRWLockExclusive(&m_candidateLock); for (unsigned i = 0; i < ARRAYSIZE(m_candidates); ++i) if (m_candidates[i].state == CANDIDATE_FREE) { selected = static_cast(i); break; } unsigned readyCount = 0; for (const FrameCandidate& candidate : m_candidates) if (candidate.state == CANDIDATE_READY) ++readyCount; if (selected < 0 && readyCount > 1) for (unsigned i = 0; i < ARRAYSIZE(m_candidates); ++i) if (m_candidates[i].state == CANDIDATE_READY && m_candidates[i].sequence < oldest) { selected = static_cast(i); oldest = m_candidates[i].sequence; } if (selected >= 0) { FrameCandidate& candidate = m_candidates[static_cast(selected)]; superseded = candidate.state == CANDIDATE_READY; candidate.state = CANDIDATE_PREPARING; candidate.sequence = ++m_candidateSequence; } ReleaseSRWLockExclusive(&m_candidateLock); if (selected >= 0) { if (superseded) m_devContext->FrameSuperseded(); return selected; } const DWORD result = WaitForMultipleObjects( ARRAYSIZE(waitHandles), waitHandles, FALSE, CANDIDATE_WAIT_MS); if (result == WAIT_OBJECT_0 + 1) return -1; if (result == WAIT_TIMEOUT) return -1; if (result != WAIT_OBJECT_0) return -1; } } void CSwapChainProcessor::ReleaseCandidate(unsigned candidateIndex) { if (candidateIndex >= ARRAYSIZE(m_candidates)) return; AcquireSRWLockExclusive(&m_candidateLock); m_candidates[candidateIndex].state = CANDIDATE_FREE; ReleaseSRWLockExclusive(&m_candidateLock); SignalCandidateState(); } static bool ResourceDescMatches( const D3D12_RESOURCE_DESC& left, const D3D12_RESOURCE_DESC& right) { return left.Dimension == right.Dimension && left.Alignment == right.Alignment && left.Width == right.Width && left.Height == right.Height && left.DepthOrArraySize == right.DepthOrArraySize && left.MipLevels == right.MipLevels && left.Format == right.Format && left.SampleDesc.Count == right.SampleDesc.Count && left.SampleDesc.Quality == right.SampleDesc.Quality && left.Layout == right.Layout && left.Flags == right.Flags; } bool CSwapChainProcessor::EnsureCandidateResource( unsigned candidateIndex, ID3D12Resource * source) { FrameCandidate& candidate = m_candidates[candidateIndex]; D3D12_RESOURCE_DESC desc = source->GetDesc(); desc.Alignment = 0; desc.Flags = static_cast( static_cast(desc.Flags) & ~static_cast(D3D12_RESOURCE_FLAG_ALLOW_CROSS_ADAPTER)); if (candidate.resource && ResourceDescMatches(candidate.resource->GetDesc(), desc)) return true; candidate.resource.Reset(); D3D12_HEAP_PROPERTIES heapProps = {}; heapProps.Type = D3D12_HEAP_TYPE_DEFAULT; heapProps.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; heapProps.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; heapProps.CreationNodeMask = 1; heapProps.VisibleNodeMask = 1; const HRESULT hr = m_dx12Device->GetDevice()->CreateCommittedResource( &heapProps, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&candidate.resource)); if (FAILED(hr)) { DEBUG_ERROR_HR(hr, "Failed to create retained frame candidate"); return false; } static const WCHAR * names[] = { L"Frame Candidate 0", L"Frame Candidate 1", }; candidate.resource->SetName(names[candidateIndex]); return true; } void CSwapChainProcessor::ResetCandidates() { AcquireSRWLockExclusive(&m_candidateLock); for (FrameCandidate& candidate : m_candidates) candidate = {}; ReleaseSRWLockExclusive(&m_candidateLock); SignalCandidateState(); } void CSwapChainProcessor::SignalCandidateState() { SetEvent(m_candidateEvent.Get()); SetEvent(m_candidateAvailableEvent.Get()); } bool CSwapChainProcessor::PublishNewestCandidate( const CFrameScheduler::Schedule& schedule, bool periodic, uint64_t publishStart) { int selectedCandidate = -1; uint64_t newestSequence = 0; AcquireSRWLockExclusive(&m_candidateLock); for (unsigned i = 0; i < ARRAYSIZE(m_candidates); ++i) if (m_candidates[i].state == CANDIDATE_READY && (selectedCandidate < 0 || m_candidates[i].sequence > newestSequence)) { selectedCandidate = static_cast(i); newestSequence = m_candidates[i].sequence; } if (selectedCandidate >= 0) for (unsigned i = 0; i < ARRAYSIZE(m_candidates); ++i) if (static_cast(i) == selectedCandidate) m_candidates[i].state = CANDIDATE_PUBLISHING; else if (m_candidates[i].state == CANDIDATE_READY) m_candidates[i].state = CANDIDATE_HELD; ReleaseSRWLockExclusive(&m_candidateLock); if (selectedCandidate < 0) return false; const unsigned candidateIndex = static_cast(selectedCandidate); const auto restoreCandidates = [this, candidateIndex]() { AcquireSRWLockExclusive(&m_candidateLock); if (m_candidates[candidateIndex].state == CANDIDATE_PUBLISHING) m_candidates[candidateIndex].state = CANDIDATE_READY; for (FrameCandidate& candidate : m_candidates) if (candidate.state == CANDIDATE_HELD) candidate.state = CANDIDATE_READY; ReleaseSRWLockExclusive(&m_candidateLock); SignalCandidateState(); }; CSRWExclusiveLock pipelineLock(&m_pipelineLock); AcquireSRWLockShared(&m_candidateLock); const bool candidateValid = m_candidates[candidateIndex].state == CANDIDATE_PUBLISHING && m_candidates[candidateIndex].resource.Get(); ReleaseSRWLockShared(&m_candidateLock); if (!candidateValid) { restoreCandidates(); return false; } FrameCandidate& candidate = m_candidates[candidateIndex]; CPostProcessor& postProcessor = m_postProcessors[candidateIndex]; auto buffer = m_devContext->PrepareFrameBuffer( candidate.pitch, candidate.srcFormat, candidate.dstFormat, candidate.dirtyRects, candidate.nbDirtyRects); if (!buffer.mem) { restoreCandidates(); return false; } CFrameBufferResource * fbRes = m_fbPool.Get(buffer, candidate.frameSize); if (!fbRes) { m_devContext->AbortFrameBuffer(buffer.frameIndex); restoreCandidates(); DEBUG_ERROR("Failed to get a CFrameBufferResource from the pool"); SetFullPendingDamage(); return false; } CD3D12CommandSlot * copySlot = m_dx12Device->GetCopySlot(candidateIndex); if (!copySlot) { m_devContext->AbortFrameBuffer(buffer.frameIndex); restoreCandidates(); DEBUG_ERROR("Failed to get a copy CommandSlot for publication"); SetFullPendingDamage(); return false; } RECT previousDirtyRects[LG_MAX_DIRTY_RECTS] = {}; unsigned nbPreviousDirtyRects = 0; AcquireSRWLockShared(&m_damageLock); nbPreviousDirtyRects = m_nbDirtyRects; if (nbPreviousDirtyRects) memcpy(previousDirtyRects, m_dirtyRects, nbPreviousDirtyRects * sizeof(*previousDirtyRects)); ReleaseSRWLockShared(&m_damageLock); RECT copyDirtyRects[LG_MAX_DIRTY_RECTS * 2] = {}; unsigned nbCopyDirtyRects = 0; bool fullCopy = buffer.fullCopy || candidate.nbDirtyRects == 0 || nbPreviousDirtyRects == 0; if (!fullCopy) { for (const RECT * rect = previousDirtyRects; rect < previousDirtyRects + nbPreviousDirtyRects && !fullCopy; ++rect) { RECT clipped = *rect; if (ClipDirtyRect(clipped, candidate.dstFormat.width, candidate.dstFormat.height) && !AddCopyDirtyRect(copyDirtyRects, ARRAYSIZE(copyDirtyRects), &nbCopyDirtyRects, clipped)) fullCopy = true; } for (const RECT * rect = candidate.dirtyRects; rect < candidate.dirtyRects + candidate.nbDirtyRects && !fullCopy; ++rect) if (!AddCopyDirtyRect(copyDirtyRects, ARRAYSIZE(copyDirtyRects), &nbCopyDirtyRects, *rect)) fullCopy = true; if (!fullCopy) fullCopy = IsFullDamage( copyDirtyRects, nbCopyDirtyRects, candidate.dstFormat.width, candidate.dstFormat.height) || CopyAreaCoversFrame( copyDirtyRects, nbCopyDirtyRects, candidate.dstFormat.width, candidate.dstFormat.height); if (!fullCopy) fullCopy = postProcessor.ShouldCopyFully( copyDirtyRects, nbCopyDirtyRects); } fbRes->SetTiming( candidate.captureTime, candidate.postProcessStart, publishStart); fbRes->SetCandidateIndex(candidateIndex); fbRes->SetPostProcessSample( candidate.timingEffectIndex, candidate.timingToken, fullCopy); copySlot->SetCompletionCallback(&CompletionFunction, this, fbRes); copySlot->BeginTiming(); postProcessor.CopyFrame( copySlot->GetGfxList(), fbRes->Get().Get(), candidate.resource.Get(), copyDirtyRects, nbCopyDirtyRects, fullCopy); copySlot->EndTiming(); // Reserve the LGMP message before submitting the copy. This makes post // failure recoverable without racing a very fast GPU completion callback. if (!m_devContext->PublishFrameBuffer( buffer.frameIndex, schedule)) { copySlot->Cancel(); m_devContext->AbortFrameBuffer(buffer.frameIndex); restoreCandidates(); return false; } if (!copySlot->Execute()) { AcquireSRWLockShared(&m_candidateLock); const bool callbackPending = candidate.state == CANDIDATE_PUBLISHING; ReleaseSRWLockShared(&m_candidateLock); if (callbackPending && !copySlot->HasSubmittedWork()) { m_devContext->FailFrameBuffer(buffer.frameIndex); SetFullPendingDamage(); ReleaseCandidate(candidateIndex); } m_devContext->ForceFrame(); AcquireSRWLockExclusive(&m_candidateLock); for (FrameCandidate& held : m_candidates) if (held.state == CANDIDATE_HELD) held.state = CANDIDATE_READY; ReleaseSRWLockExclusive(&m_candidateLock); SignalCandidateState(); return false; } AcquireSRWLockExclusive(&m_damageLock); if (candidate.nbDirtyRects) memcpy(m_dirtyRects, candidate.dirtyRects, candidate.nbDirtyRects * sizeof(*m_dirtyRects)); m_nbDirtyRects = candidate.nbDirtyRects; if (candidate.damageGeneration == m_damageGeneration) { m_hasPendingDamage = false; m_nbPendingDirtyRects = 0; } ReleaseSRWLockExclusive(&m_damageLock); m_devContext->CommitFrameBuffer( buffer.frameIndex, schedule, periodic); unsigned superseded = 0; AcquireSRWLockExclusive(&m_candidateLock); for (FrameCandidate& held : m_candidates) if (held.state == CANDIDATE_HELD) { held.state = CANDIDATE_FREE; ++superseded; } ReleaseSRWLockExclusive(&m_candidateLock); for (unsigned i = 0; i < superseded; ++i) m_devContext->FrameSuperseded(); SignalCandidateState(); return true; } #ifdef HAS_IDDCX_110 void CSwapChainProcessor::UpdateHDRMetadata(const IDDCX_METADATA2& metadata) { if (!(metadata.ValidFlags & IDDCX_METADATA2_VALID_FLAGS_HDR10METADATA)) return; const IDDCX_HDR10_FRAME_METADATA& frame = metadata.Hdr10FrameMetaData; switch (frame.Type) { case IDDCX_HDR10_FRAME_METADATA_TYPE_DEFAULT: if (!m_useDefaultHDRMetadata) DEBUG_TRACE("HDR10 frame metadata switched to the monitor default"); m_useDefaultHDRMetadata = true; m_hasNewHDRMetadata = false; break; case IDDCX_HDR10_FRAME_METADATA_TYPE_UNCHANGED: break; case IDDCX_HDR10_FRAME_METADATA_TYPE_NEW: if (!m_hasNewHDRMetadata || memcmp(&m_newHDRMetadata, &frame.NewMetaData, sizeof(m_newHDRMetadata)) != 0) DEBUG_TRACE("Received new HDR10 frame metadata"); m_newHDRMetadata = frame.NewMetaData; m_useDefaultHDRMetadata = false; m_hasNewHDRMetadata = true; break; default: DEBUG_WARN("Invalid HDR10 frame metadata type %u", static_cast(frame.Type)); break; } } #endif bool CSwapChainProcessor::GetContentHDRMetadata(D12FrameFormat& format) const { #ifdef HAS_IDDCX_110 // The monitor default describes the virtual display, not the content. Only // publish an explicit per-frame metadata block to downstream consumers. if (m_useDefaultHDRMetadata || !m_hasNewHDRMetadata) return false; const IDDCX_HDR10_METADATA& metadata = m_newHDRMetadata; format.displayPrimary[0][0] = metadata.RedPrimary [0]; format.displayPrimary[0][1] = metadata.RedPrimary [1]; format.displayPrimary[1][0] = metadata.GreenPrimary[0]; format.displayPrimary[1][1] = metadata.GreenPrimary[1]; format.displayPrimary[2][0] = metadata.BluePrimary [0]; format.displayPrimary[2][1] = metadata.BluePrimary [1]; format.whitePoint [0] = metadata.WhitePoint [0]; format.whitePoint [1] = metadata.WhitePoint [1]; format.maxDisplayLuminance = metadata.MaxMasteringLuminance; format.minDisplayLuminance = metadata.MinMasteringLuminance; format.maxContentLightLevel = metadata.MaxContentLightLevel; format.maxFrameAverageLightLevel = metadata.MaxFrameAverageLightLevel; return true; #else UNREFERENCED_PARAMETER(format); return false; #endif } bool CSwapChainProcessor::SwapChainNewFrame(ComPtr acquiredBuffer, unsigned dirtyRectCount, unsigned moveRegionCount, DXGI_COLOR_SPACE_TYPE colorSpace, UINT sdrWhiteLevel, uint64_t captureStart) { const uint64_t postProcessStart = CFrameScheduler::Nanotime(); const uint64_t captureTime = postProcessStart - captureStart; ComPtr texture; HRESULT hr = acquiredBuffer.As(&texture); if (FAILED(hr)) { DEBUG_ERROR_HR(hr, "Failed to obtain the ID3D11Texture2D from the acquiredBuffer"); SetFullPendingDamage(); return false; } CInteropResource * srcRes = m_resPool.Get(texture); if (!srcRes) { DEBUG_ERROR("Failed to get a CInteropResource from the pool"); SetFullPendingDamage(); return false; } /** * Even though we have not performed any copy/draw operations we still need to * use a fence. Because we share this texture with DirectX12 it is able to * read from it before the desktop duplication API has finished updating it. */ if (!srcRes->Signal()) { SetFullPendingDamage(); return false; } RECT dirtyRects[LG_MAX_DIRTY_RECTS] = {0}; bool noImageUpdate = false; if (moveRegionCount || dirtyRectCount > ARRAYSIZE(dirtyRects)) { // Move regions are not represented by the dirty rectangle list. Copy the // full surface so the alternating destinations remain coherent. srcRes->SetFullDamage(); } else { IDARG_IN_GETDIRTYRECTS dirtyIn = {}; dirtyIn.DirtyRectInCount = dirtyRectCount; dirtyIn.pDirtyRects = dirtyRects; IDARG_OUT_GETDIRTYRECTS dirtyOut = {}; hr = IddCxSwapChainGetDirtyRects(m_hSwapChain, &dirtyIn, &dirtyOut); if (FAILED(hr)) { DEBUG_ERROR_HR(hr, "IddCxSwapChainGetDirtyRects Failed"); srcRes->SetFullDamage(); } else if (dirtyOut.DirtyRectOutCount == 1 && dirtyRects[0].left == 0 && dirtyRects[0].top == 0 && dirtyRects[0].right == 0 && dirtyRects[0].bottom == 0) { // One empty rectangle is IddCx's static-desktop re-encode marker. It // does not describe an image update and must not become full damage. noImageUpdate = true; srcRes->SetDirtyRects(nullptr, 0); } else srcRes->SetDirtyRects(dirtyRects, dirtyOut.DirtyRectOutCount); } D3D12_RESOURCE_DESC srcDesc = srcRes->GetRes()->GetDesc(); if (!noImageUpdate) { m_devContext->ObserveFrame(postProcessStart); AccumulateFrameDamage( srcRes->GetDirtyRects(), srcRes->GetDirtyRectCount()); } D12FrameFormat srcFormat = {}; srcFormat.desc = srcDesc; srcFormat.width = (unsigned)srcDesc.Width; srcFormat.height = srcDesc.Height; srcFormat.format = GetFrameType(srcDesc.Format); srcFormat.sdrWhiteLevel = sdrWhiteLevel; srcFormat.colorTransform = m_devContext->GetColorTransform(); switch (colorSpace) { case DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020: case DXGI_COLOR_SPACE_RGB_STUDIO_G2084_NONE_P2020: // HDR10: BT.2020 primaries with the PQ (ST.2084) transfer function // already applied to the pixel data. srcFormat.hdr = true; srcFormat.hdrPQ = true; if (!GetContentHDRMetadata(srcFormat)) { // No per-content metadata is active. The pixels are still PQ-encoded, // so keep the PQ flag and use BT.2020/PQ defaults internally rather // than publishing the virtual monitor metadata as content metadata. // BT.2020 primaries (in 0.00002 units): srcFormat.displayPrimary[0][0] = 35400; // Rx srcFormat.displayPrimary[0][1] = 14600; // Ry srcFormat.displayPrimary[1][0] = 8500; // Gx srcFormat.displayPrimary[1][1] = 39850; // Gy srcFormat.displayPrimary[2][0] = 6550; // Bx srcFormat.displayPrimary[2][1] = 2300; // By // D65 white point (in 0.00002 units): srcFormat.whitePoint[0] = 15635; srcFormat.whitePoint[1] = 16450; // Cover the complete PQ signal range. srcFormat.maxDisplayLuminance = HDR_PQ_MAX_LUMINANCE; srcFormat.minDisplayLuminance = HDR_PQ_MIN_LUMINANCE; // Content light levels unknown: srcFormat.maxContentLightLevel = 0; srcFormat.maxFrameAverageLightLevel = 0; } else srcFormat.hdrMetadata = true; break; case DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709: // scRGB: linear (FP16) content with BT.709 primaries. HDR, but the PQ // curve has not been applied. srcFormat.hdr = true; srcFormat.hdrPQ = false; if (!GetContentHDRMetadata(srcFormat)) { // No per-content metadata is active. Use reasonable internal defaults // without publishing the virtual monitor metadata downstream. // BT.709/sRGB primaries (in 0.00002 units): srcFormat.displayPrimary[0][0] = 32000; // Rx srcFormat.displayPrimary[0][1] = 16500; // Ry srcFormat.displayPrimary[1][0] = 15000; // Gx srcFormat.displayPrimary[1][1] = 30000; // Gy srcFormat.displayPrimary[2][0] = 7500; // Bx srcFormat.displayPrimary[2][1] = 3000; // By // D65 white point (in 0.00002 units): srcFormat.whitePoint[0] = 15635; srcFormat.whitePoint[1] = 16450; // Mastering luminances follow SMPTE ST 2086 units: max in whole cd/m², // min in 0.0001 cd/m². 80 cd/m² display, 0.005 cd/m² black: srcFormat.maxDisplayLuminance = 80; srcFormat.minDisplayLuminance = 50; // Content light levels unknown: srcFormat.maxContentLightLevel = 0; srcFormat.maxFrameAverageLightLevel = 0; } else srcFormat.hdrMetadata = true; break; default: // Everything else (e.g. RGB_FULL_G22_NONE_P709) is SDR. srcFormat.hdr = false; srcFormat.hdrPQ = false; break; } bool frameMetadataChanged = false; bool needsReconfigure = false; bool postProcessFormatChanged = false; bool requiresFullDamage = false; { CSRWExclusiveLock pipelineLock(&m_pipelineLock); m_postProcessors[0].Update(srcFormat); frameMetadataChanged = noImageUpdate && FrameMetadataChanged( m_postProcessors[0].GetOutputFormat(), srcFormat); for (const CPostProcessor& postProcessor : m_postProcessors) if (postProcessor.NeedsReconfigure(srcFormat)) { needsReconfigure = true; break; } // A format change can replace resources referenced by either retained // candidate. Stop publication, drain both queues, then invalidate them. if (needsReconfigure) { AcquireSRWLockExclusive(&m_damageLock); m_nbDirtyRects = 0; ReleaseSRWLockExclusive(&m_damageLock); SetFullPendingDamage(); m_dx12Device->WaitForIdle(); ResetCandidates(); } bool configurationStable = false; for (unsigned pass = 0; pass < 2 && !configurationStable; ++pass) { for (unsigned i = 0; i < ARRAYSIZE(m_postProcessors); ++i) { bool formatChanged = false; if (!m_postProcessors[i].Configure(srcFormat, &formatChanged)) { SetFullPendingDamage(); return false; } if (i == 0) postProcessFormatChanged |= formatChanged; } configurationStable = true; for (const CPostProcessor& postProcessor : m_postProcessors) if (postProcessor.NeedsReconfigure(srcFormat)) { configurationStable = false; break; } } if (!configurationStable) { DEBUG_ERROR("Post processor configuration did not stabilize"); SetFullPendingDamage(); return false; } if (postProcessFormatChanged) { AcquireSRWLockExclusive(&m_damageLock); m_nbDirtyRects = 0; ReleaseSRWLockExclusive(&m_damageLock); SetFullPendingDamage(); } else if (frameMetadataChanged) SetFullPendingDamage(); requiresFullDamage = m_postProcessors[0].RequiresFullDamage(); if (requiresFullDamage) SetFullPendingDamage(); } if (needsReconfigure || postProcessFormatChanged || frameMetadataChanged) m_devContext->ForceFrame(); if (noImageUpdate) { AcquireSRWLockShared(&m_damageLock); const bool hasPendingDamage = m_hasPendingDamage; ReleaseSRWLockShared(&m_damageLock); if (!hasPendingDamage) return true; } const int selectedCandidate = AcquireCandidate(); if (selectedCandidate < 0) { m_devContext->FrameSuperseded(); return true; } const unsigned candidateIndex = static_cast(selectedCandidate); CSRWExclusiveLock pipelineLock(&m_pipelineLock); CPostProcessor& postProcessor = m_postProcessors[candidateIndex]; const D12FrameFormat& dstFormat = postProcessor.GetOutputFormat(); RECT currentDirtyRects[LG_MAX_DIRTY_RECTS] = {}; unsigned nbDirtyRects = 0; uint64_t damageGeneration = 0; AcquireSRWLockShared(&m_damageLock); if (m_hasPendingDamage) { nbDirtyRects = m_nbPendingDirtyRects; if (nbDirtyRects) memcpy(currentDirtyRects, m_pendingDirtyRects, nbDirtyRects * sizeof(*currentDirtyRects)); damageGeneration = m_damageGeneration; } ReleaseSRWLockShared(&m_damageLock); CD3D12CommandSlot * copySlot = m_dx12Device->GetCopySlot(candidateIndex); if (!copySlot) { ReleaseCandidate(candidateIndex); DEBUG_ERROR("Failed to get a copy CommandSlot"); SetFullPendingDamage(); return false; } ComPtr copySrcResource = srcRes->GetRes(); CD3D12CommandSlot * computeSlot = nullptr; if (postProcessor.HasActiveEffects()) { computeSlot = m_dx12Device->GetComputeSlot(candidateIndex); if (!computeSlot) { copySlot->Cancel(); ReleaseCandidate(candidateIndex); DEBUG_ERROR("Failed to get a compute CommandSlot"); SetFullPendingDamage(); return false; } if (!srcRes->Sync(*computeSlot)) { computeSlot->Cancel(); copySlot->Cancel(); ReleaseCandidate(candidateIndex); SetFullPendingDamage(); return false; } copySrcResource = postProcessor.Run( computeSlot->GetGfxList(), copySrcResource, currentDirtyRects, &nbDirtyRects); if (!copySrcResource) { computeSlot->Cancel(); copySlot->Cancel(); ReleaseCandidate(candidateIndex); DEBUG_ERROR("Post processor returned no output resource"); SetFullPendingDamage(); return false; } if (!computeSlot->Execute()) { copySlot->Cancel(); m_dx12Device->WaitForIdle(); ReleaseCandidate(candidateIndex); SetFullPendingDamage(); return false; } if (!copySlot->WaitFor(*computeSlot)) { copySlot->Cancel(); m_dx12Device->WaitForIdle(); ReleaseCandidate(candidateIndex); DEBUG_ERROR("Failed to queue compute synchronization"); SetFullPendingDamage(); return false; } } else if (!srcRes->Sync(*copySlot)) { copySlot->Cancel(); ReleaseCandidate(candidateIndex); DEBUG_ERROR("Failed to queue source synchronization"); SetFullPendingDamage(); return false; } ClipDirtyRects(currentDirtyRects, &nbDirtyRects, dstFormat.width, dstFormat.height); if (!EnsureCandidateResource(candidateIndex, copySrcResource.Get())) { copySlot->Cancel(); if (computeSlot) m_dx12Device->WaitForIdle(); ReleaseCandidate(candidateIndex); SetFullPendingDamage(); return false; } FrameCandidate& candidate = m_candidates[candidateIndex]; candidate.srcFormat = srcFormat; candidate.dstFormat = dstFormat; candidate.nbDirtyRects = nbDirtyRects; candidate.pitch = postProcessor.GetOutputPitch(); candidate.frameSize = postProcessor.GetOutputSize(); candidate.damageGeneration = damageGeneration; candidate.captureTime = captureTime; candidate.postProcessStart = postProcessStart; candidate.prepareCopyStart = CFrameScheduler::Nanotime(); candidate.prepareReady = 0; candidate.prepareGPUStart = 0; candidate.prepareGPUEnd = 0; candidate.prepareTimingValid = false; if (nbDirtyRects) memcpy(candidate.dirtyRects, currentDirtyRects, nbDirtyRects * sizeof(*candidate.dirtyRects)); postProcessor.GetTimingToken( &candidate.timingEffectIndex, &candidate.timingToken); copySlot->SetCompletionCallback( &CandidateCompletionFunction, this, &candidate); copySlot->BeginTiming(); const D3D12_RESOURCE_DESC copySrcDesc = copySrcResource->GetDesc(); if (copySrcDesc.Dimension == D3D12_RESOURCE_DIMENSION_BUFFER) copySlot->GetGfxList()->CopyBufferRegion( candidate.resource.Get(), 0, copySrcResource.Get(), 0, copySrcDesc.Width); else { D3D12_TEXTURE_COPY_LOCATION srcLocation = {}; srcLocation.pResource = copySrcResource.Get(); srcLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; srcLocation.SubresourceIndex = 0; D3D12_TEXTURE_COPY_LOCATION dstLocation = {}; dstLocation.pResource = candidate.resource.Get(); dstLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; dstLocation.SubresourceIndex = 0; copySlot->GetGfxList()->CopyTextureRegion( &dstLocation, 0, 0, 0, &srcLocation, nullptr); } copySlot->EndTiming(); if (!copySlot->Execute()) { if (!copySlot->HasSubmittedWork()) { if (computeSlot) m_dx12Device->WaitForIdle(); ReleaseCandidate(candidateIndex); } SetFullPendingDamage(); m_devContext->ForceFrame(); return false; } return true; } DWORD CALLBACK CSwapChainProcessor::_CursorThread(LPVOID arg) { reinterpret_cast(arg)->CursorThread(); return 0; } bool CSwapChainProcessor::QueryHWCursor() { IDARG_IN_QUERY_HWCURSOR in = {}; in.LastShapeId = m_lastShapeId; in.pShapeBuffer = m_shapeBuffer; in.ShapeBufferSizeInBytes = 512 * 512 * 4; IDARG_OUT_QUERY_HWCURSOR out = {}; UINT cursorWhiteLevel = m_sdrWhiteLevel.load(std::memory_order_relaxed); NTSTATUS status; #ifdef HAS_IDDCX_110 if (m_devContext->HasIddCx110DDIs()) { IDARG_OUT_QUERY_HWCURSOR3 out3 = {}; status = IddCxMonitorQueryHardwareCursor3(m_monitor, &in, &out3); out.IsCursorVisible = out3.IsCursorVisible; out.X = out3.X; out.Y = out3.Y; out.IsCursorShapeUpdated = out3.IsCursorShapeUpdated; out.CursorShapeInfo = out3.CursorShapeInfo; if (out3.SdrWhiteLevel) cursorWhiteLevel = out3.SdrWhiteLevel; } else #endif { status = IddCxMonitorQueryHardwareCursor(m_monitor, &in, &out); } if (FAILED(status)) { // this occurs if the display went away (ie, screen blanking or disabled) if (status == STATUS_GRAPHICS_PATH_NOT_IN_TOPOLOGY) { SetEvent(m_terminateEvent.Get()); return false; } DEBUG_ERROR("IddCxMonitorQueryHardwareCursor failed (0x%08x)", status); return false; } if (out.IsCursorShapeUpdated) m_lastShapeId = out.CursorShapeInfo.ShapeId; m_devContext->SendCursor(out, m_shapeBuffer, cursorWhiteLevel); return true; } void CSwapChainProcessor::CursorThread() { HRESULT hr = 0; bool running = true; while (running) { HANDLE waitHandles[] = { m_cursorDataEvent.Get(), m_terminateEvent.Get() }; DWORD waitResult = WaitForMultipleObjects( ARRAYSIZE(waitHandles), waitHandles, FALSE, 100); switch (waitResult) { case WAIT_TIMEOUT: continue; // cursorDataEvent case WAIT_OBJECT_0: if (!QueryHWCursor()) return; continue; // terminateEvent case WAIT_OBJECT_0 + 1: running = false; continue; default: hr = HRESULT_FROM_WIN32(waitResult); DEBUG_ERROR_HR(hr, "WaitForMultipleObjects"); return; } } }