mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-09 08:41:31 +00:00
2002 lines
63 KiB
C++
2002 lines
63 KiB
C++
/**
|
|
* 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 "CPlatformInfo.h"
|
|
|
|
#include <avrt.h>
|
|
#include <new>
|
|
#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_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,
|
|
LUID renderAdapter, std::shared_ptr<CD3D11Device> dx11Device,
|
|
HANDLE newFrameEvent) :
|
|
m_monitorContext(monitorContext),
|
|
m_assignmentGeneration(assignmentGeneration),
|
|
m_monitor(monitor),
|
|
m_devContext(devContext),
|
|
m_hSwapChain(hSwapChain),
|
|
m_renderAdapter(renderAdapter),
|
|
m_dx11Device(dx11Device),
|
|
m_newFrameEvent(newFrameEvent)
|
|
{
|
|
// 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 (std::nothrow) BYTE[512 * 512 * 4];
|
|
}
|
|
|
|
bool CSwapChainProcessor::Start()
|
|
{
|
|
if (!m_terminateEvent.Get() || !m_candidateEvent.Get() ||
|
|
!m_candidateAvailableEvent.Get() || !m_publishTimer.Get() ||
|
|
!m_cursorDataEvent.Get() || !m_shapeBuffer)
|
|
{
|
|
DEBUG_ERROR("Failed to initialize swap chain worker resources");
|
|
return false;
|
|
}
|
|
|
|
// Bind the swap chain before initializing the expensive transport pipeline.
|
|
m_thread[0].Attach(CreateThread(
|
|
nullptr, 0, _SwapChainThread, this, 0, nullptr));
|
|
if (!m_thread[0].Get())
|
|
{
|
|
DEBUG_ERROR_HR(GetLastError(), "Failed to create swap chain worker");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CSwapChainProcessor::InitializePipeline()
|
|
{
|
|
for (;;)
|
|
{
|
|
if (!m_monitorContext->IsAssignmentCurrent(m_assignmentGeneration) ||
|
|
WaitForSingleObject(m_terminateEvent.Get(), 0) == WAIT_OBJECT_0)
|
|
return false;
|
|
|
|
UINT64 alignSize = CPlatformInfo::GetPageSize();
|
|
auto dx12Device = std::make_shared<CD3D12Device>(m_renderAdapter);
|
|
const CD3D12Device::InitResult result = dx12Device->Init(
|
|
m_devContext->GetIVSHMEM(), alignSize, !m_dx11Device->IsSoftware());
|
|
if (result == CD3D12Device::RETRY)
|
|
{
|
|
const HRESULT deviceStatus =
|
|
m_dx11Device->GetDevice()->GetDeviceRemovedReason();
|
|
if (FAILED(deviceStatus))
|
|
{
|
|
DEBUG_ERROR_HR(deviceStatus,
|
|
"D3D11 device removed during D3D12 initialization");
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
if (result == CD3D12Device::FAILURE)
|
|
return false;
|
|
|
|
if (!m_devContext->SetupLGMP(alignSize))
|
|
{
|
|
DEBUG_ERROR("SetupLGMP failed");
|
|
return false;
|
|
}
|
|
|
|
m_dx12Device = std::move(dx12Device);
|
|
break;
|
|
}
|
|
|
|
if (!m_monitorContext->IsAssignmentCurrent(m_assignmentGeneration) ||
|
|
WaitForSingleObject(m_terminateEvent.Get(), 0) == WAIT_OBJECT_0)
|
|
return false;
|
|
|
|
m_resPool.Init(m_dx11Device, m_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(m_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(m_dx12Device, false))
|
|
DEBUG_ERROR("Failed to initialize post processor copy support");
|
|
}
|
|
DEBUG_WARN(
|
|
"Failed to initialize post-processing effects; effects disabled");
|
|
}
|
|
|
|
if (!m_monitorContext->IsAssignmentCurrent(m_assignmentGeneration) ||
|
|
WaitForSingleObject(m_terminateEvent.Get(), 0) == WAIT_OBJECT_0)
|
|
return false;
|
|
|
|
m_thread[2].Attach(CreateThread(
|
|
nullptr, 0, _PublisherThread, this, 0, nullptr));
|
|
if (!m_thread[2].Get())
|
|
{
|
|
DEBUG_ERROR_HR(GetLastError(), "Failed to create publisher thread");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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.
|
|
if (m_dx12Device)
|
|
{
|
|
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<CSwapChainProcessor*>(arg)->SwapChainThread();
|
|
return 0;
|
|
}
|
|
|
|
static bool ArmPublishTimer(HANDLE timer, uint64_t delay)
|
|
{
|
|
if (!timer)
|
|
return false;
|
|
|
|
LARGE_INTEGER due = {};
|
|
due.QuadPart = -static_cast<LONGLONG>((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<CSwapChainProcessor *>(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);
|
|
|
|
const bool ready = HasReadyCandidate();
|
|
if (!ready)
|
|
{
|
|
m_devContext->ProcessFrameQueue();
|
|
if (HasReadyCandidate())
|
|
continue;
|
|
|
|
if (republish && m_devContext->HasPublishedFrame())
|
|
{
|
|
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;
|
|
}
|
|
|
|
const uint64_t replayNow = CFrameScheduler::Nanotime();
|
|
uint64_t cadenceTarget = 0;
|
|
if (schedule.deliveryDeadlineSerial && periodic &&
|
|
schedule.deadline <= replayNow)
|
|
{
|
|
m_devContext->FrameMissed(schedule, replayNow, periodic);
|
|
continue;
|
|
}
|
|
if (schedule.deliveryDeadlineSerial && periodic)
|
|
cadenceTarget = schedule.deadline;
|
|
|
|
uint64_t replayTarget;
|
|
if (m_devContext->GetSharedFrameTarget(replayNow, replayTarget))
|
|
{
|
|
bool retry = false;
|
|
if (replayTarget <= replayNow)
|
|
{
|
|
if (m_devContext->ReplaySharedFrame(replayNow, retry))
|
|
continue;
|
|
if (retry)
|
|
replayTarget = replayNow + PUBLISH_RETRY_NS;
|
|
else
|
|
{
|
|
if (cadenceTarget)
|
|
replayTarget = cadenceTarget;
|
|
else
|
|
{
|
|
if (m_publishTimer.Get())
|
|
CancelWaitableTimer(m_publishTimer.Get());
|
|
if (WaitForMultipleObjects(
|
|
ARRAYSIZE(idleHandles), idleHandles, FALSE, INFINITE) ==
|
|
WAIT_OBJECT_0)
|
|
break;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cadenceTarget && cadenceTarget < replayTarget)
|
|
replayTarget = cadenceTarget;
|
|
|
|
const uint64_t delay = replayTarget > replayNow ?
|
|
replayTarget - replayNow : PUBLISH_RETRY_NS;
|
|
ArmPublishTimer(m_publishTimer.Get(), delay);
|
|
if (WaitForMultipleObjects(
|
|
ARRAYSIZE(timerHandles), timerHandles, FALSE, INFINITE) ==
|
|
WAIT_OBJECT_0)
|
|
break;
|
|
continue;
|
|
}
|
|
|
|
if (cadenceTarget)
|
|
{
|
|
ArmPublishTimer(
|
|
m_publishTimer.Get(), cadenceTarget - replayNow);
|
|
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;
|
|
}
|
|
|
|
uint64_t replayTarget;
|
|
if (m_devContext->GetSharedFrameTarget(now, replayTarget) &&
|
|
replayTarget < target)
|
|
{
|
|
if (replayTarget <= now)
|
|
{
|
|
m_devContext->ProcessFrameQueue();
|
|
bool retry = false;
|
|
if (m_devContext->ReplaySharedFrame(
|
|
CFrameScheduler::Nanotime(), retry))
|
|
continue;
|
|
|
|
if (retry)
|
|
replayTarget = now + PUBLISH_RETRY_NS;
|
|
else
|
|
replayTarget = target;
|
|
}
|
|
|
|
const uint64_t delay = replayTarget > now ?
|
|
replayTarget - now : PUBLISH_RETRY_NS;
|
|
ArmPublishTimer(m_publishTimer.Get(), delay);
|
|
if (WaitForMultipleObjects(
|
|
ARRAYSIZE(timerHandles), timerHandles, 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);
|
|
|
|
SwapChainThreadCore();
|
|
|
|
// Returning success from EvtIddCxMonitorAssignSwapChain transfers ownership
|
|
// to the driver, regardless of whether SetDevice or later initialization
|
|
// succeeds. Release it on every worker exit.
|
|
WdfObjectDelete((WDFOBJECT)m_hSwapChain);
|
|
m_hSwapChain = nullptr;
|
|
|
|
AvRevertMmThreadCharacteristics(avTaskHandle);
|
|
}
|
|
|
|
void CSwapChainProcessor::SwapChainThreadCore()
|
|
{
|
|
ComPtr<IDXGIDevice> dxgiDevice;
|
|
HRESULT hr = m_dx11Device->GetDevice().As(&dxgiDevice);
|
|
if (FAILED(hr))
|
|
{
|
|
DEBUG_ERROR_HR(hr, "Failed to get the dxgiDevice");
|
|
return;
|
|
}
|
|
|
|
IDARG_IN_SWAPCHAINSETDEVICE setDevice = {};
|
|
setDevice.pDevice = dxgiDevice.Get();
|
|
|
|
// IddCx can unassign a swap chain before its worker binds the device. Avoid
|
|
// using an invalidated handle; the worker epilogue still releases the
|
|
// driver-owned swap chain.
|
|
if (!m_monitorContext->IsAssignmentCurrent(m_assignmentGeneration) ||
|
|
WaitForSingleObject(m_terminateEvent.Get(), 0) == WAIT_OBJECT_0)
|
|
return;
|
|
|
|
// 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;
|
|
}
|
|
DEBUG_INFO("Swap chain device set");
|
|
|
|
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);
|
|
}
|
|
|
|
if (!InitializePipeline())
|
|
return;
|
|
|
|
if (!m_monitorContext->IsAssignmentCurrent(m_assignmentGeneration) ||
|
|
WaitForSingleObject(m_terminateEvent.Get(), 0) == WAIT_OBJECT_0)
|
|
return;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
bool hasLastFrameNumber = false;
|
|
for (;;)
|
|
{
|
|
if (WaitForSingleObject(m_terminateEvent.Get(), 0) == WAIT_OBJECT_0)
|
|
break;
|
|
|
|
UINT frameNumber = 0;
|
|
UINT dirtyRectCount = 0;
|
|
UINT moveRegionCount = 0;
|
|
ComPtr<IDXGIResource> 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))
|
|
{
|
|
const bool duplicateFrame =
|
|
hasLastFrameNumber && frameNumber == lastFrameNumber;
|
|
if (!duplicateFrame)
|
|
{
|
|
lastFrameNumber = frameNumber;
|
|
hasLastFrameNumber = true;
|
|
}
|
|
if (!SwapChainNewFrame(surface, dirtyRectCount, moveRegionCount,
|
|
colorSpace, sdrWhiteLevel, captureStart, duplicateFrame))
|
|
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;
|
|
}
|
|
|
|
}
|
|
|
|
void CSwapChainProcessor::CandidateCompletionFunction(
|
|
CD3D12CommandSlot * slot, bool result, void * param1, void * param2)
|
|
{
|
|
auto sc = static_cast<CSwapChainProcessor *>(param1);
|
|
auto candidate = static_cast<FrameCandidate *>(param2);
|
|
|
|
uint64_t gpuStart = 0;
|
|
uint64_t gpuEnd = 0;
|
|
const bool timingValid = result && slot->GetGPUTimes(gpuStart, gpuEnd);
|
|
|
|
bool forceFrame = false;
|
|
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;
|
|
forceFrame = result && candidate->timingToken != 0;
|
|
}
|
|
ReleaseSRWLockExclusive(&sc->m_candidateLock);
|
|
|
|
if (!result)
|
|
{
|
|
sc->SetFullPendingDamage();
|
|
sc->m_devContext->ForceFrame();
|
|
}
|
|
else if (forceFrame)
|
|
sc->m_devContext->ForceFrame();
|
|
sc->SignalCandidateState();
|
|
}
|
|
|
|
void CSwapChainProcessor::CompletionFunction(
|
|
CD3D12CommandSlot * slot, bool result, void * param1, void * param2)
|
|
{
|
|
auto sc = static_cast<CSwapChainProcessor *>(param1);
|
|
auto fbRes = static_cast<CFrameBufferResource *>(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;
|
|
uint64_t timingStart;
|
|
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;
|
|
timingStart = candidate.timingStart;
|
|
prepareTimingValid = candidate.prepareTimingValid;
|
|
ReleaseSRWLockShared(&sc->m_candidateLock);
|
|
|
|
const uint64_t publishStart = fbRes->GetCopyStart();
|
|
uint64_t gpuCopyStart = 0;
|
|
uint64_t gpuCopyEnd = 0;
|
|
uint64_t indirectCopyTime = 0;
|
|
if (sc->m_dx12Device->IsIndirectCopy())
|
|
{
|
|
// GPU timestamps end at the readback copy. Track the following CPU copy
|
|
// separately for frame metrics; benchmark wall time includes it directly.
|
|
const uint64_t indirectCopyStart = CFrameScheduler::Nanotime();
|
|
sc->m_devContext->WriteFrameBuffer(
|
|
fbRes->GetFrameIndex(), fbRes->GetMap(), 0, fbRes->GetFrameSize(), false);
|
|
indirectCopyTime = CFrameScheduler::Nanotime() - indirectCopyStart;
|
|
}
|
|
|
|
// 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);
|
|
|
|
const uint64_t copyReady = 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 = copyReady - publishStart;
|
|
if (gpuTimingValid && gpuCopyStart >= publishStart &&
|
|
gpuCopyEnd >= gpuCopyStart && gpuCopyEnd <= copyReady)
|
|
publishCopyTime = gpuCopyEnd - gpuCopyStart + indirectCopyTime;
|
|
|
|
const uint64_t copyTime = prepareCopyTime + publishCopyTime;
|
|
|
|
// Make the framebuffer readable before phase bookkeeping. If the scheduler
|
|
// lock is busy, the frame is still delivered and only this phase sample is
|
|
// discarded.
|
|
sc->m_devContext->FinalizeFrameBuffer(fbRes->GetFrameIndex());
|
|
const uint64_t publishedAt = CFrameScheduler::Nanotime();
|
|
const uint64_t prepareElapsed = prepareReady >= postProcessStart ?
|
|
prepareReady - postProcessStart : 0;
|
|
const uint64_t prepareMeasured = postProcessTime + prepareCopyTime;
|
|
const uint64_t prepareReadyTime = prepareElapsed > prepareMeasured ?
|
|
prepareElapsed - prepareMeasured : 0;
|
|
const uint64_t publishElapsed = publishedAt >= publishStart ?
|
|
publishedAt - publishStart : 0;
|
|
const uint64_t publishReadyTime = publishElapsed > publishCopyTime ?
|
|
publishElapsed - publishCopyTime : 0;
|
|
const uint64_t readyTime = prepareReadyTime + publishReadyTime;
|
|
const uint64_t holdTime = publishStart >= prepareReady ?
|
|
publishStart - prepareReady : 0;
|
|
|
|
sc->m_devContext->SetFrameTiming(fbRes->GetFrameIndex(),
|
|
fbRes->GetCaptureTime(), postProcessTime, copyTime, readyTime, holdTime,
|
|
fbRes->GetSchedule(), publishedAt);
|
|
sc->m_devContext->TryRecordFrameTiming(
|
|
publishedAt - publishStart);
|
|
|
|
// Use matching wall-clock boundaries for both modes. The split excludes the
|
|
// cadence hold while including the indirect CPU copy only when it occurs.
|
|
const uint64_t timingToken = fbRes->GetTimingToken();
|
|
if (timingToken && timingStart && prepareReady >= timingStart &&
|
|
copyReady >= publishStart)
|
|
{
|
|
const uint64_t totalTime =
|
|
(prepareReady - timingStart) + (copyReady - publishStart);
|
|
sc->m_postProcessors[candidateIndex].RecordTiming(
|
|
fbRes->GetTimingEffectIndex(), timingToken,
|
|
fbRes->IsFullCopy(), totalTime);
|
|
}
|
|
|
|
sc->m_devContext->CompleteFrameBuffer(fbRes->GetFrameIndex(), true);
|
|
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;
|
|
}
|
|
}
|
|
|
|
static void AccumulatePendingDamage(
|
|
RECT pendingDirtyRects[], unsigned * nbPendingDirtyRects,
|
|
bool * hasPendingDamage, const RECT dirtyRects[], unsigned nbDirtyRects)
|
|
{
|
|
if (nbDirtyRects > LG_MAX_DIRTY_RECTS)
|
|
nbDirtyRects = 0;
|
|
|
|
if (!*hasPendingDamage)
|
|
{
|
|
*hasPendingDamage = true;
|
|
*nbPendingDirtyRects = nbDirtyRects;
|
|
if (nbDirtyRects)
|
|
memcpy(pendingDirtyRects, dirtyRects,
|
|
nbDirtyRects * sizeof(*pendingDirtyRects));
|
|
return;
|
|
}
|
|
|
|
// Zero dirty rectangles represents full-frame damage. Once an accumulated
|
|
// set is full, no later rectangles can narrow that same set again.
|
|
if (*nbPendingDirtyRects == 0 || nbDirtyRects == 0 ||
|
|
*nbPendingDirtyRects + nbDirtyRects > LG_MAX_DIRTY_RECTS)
|
|
{
|
|
*nbPendingDirtyRects = 0;
|
|
return;
|
|
}
|
|
|
|
memcpy(pendingDirtyRects + *nbPendingDirtyRects, dirtyRects,
|
|
nbDirtyRects * sizeof(*pendingDirtyRects));
|
|
*nbPendingDirtyRects += nbDirtyRects;
|
|
}
|
|
|
|
void CSwapChainProcessor::SetFullPendingDamage()
|
|
{
|
|
AcquireSRWLockExclusive(&m_damageLock);
|
|
m_hasPendingDamage = true;
|
|
m_nbPendingDirtyRects = 0;
|
|
for (CandidateDamageTail& tail : m_candidateDamageTail)
|
|
if (tail.active)
|
|
{
|
|
tail.hasDamage = true;
|
|
tail.nbDirtyRects = 0;
|
|
}
|
|
ReleaseSRWLockExclusive(&m_damageLock);
|
|
}
|
|
|
|
void CSwapChainProcessor::AccumulateFrameDamage(
|
|
const RECT * dirtyRects, unsigned nbDirtyRects)
|
|
{
|
|
AcquireSRWLockExclusive(&m_damageLock);
|
|
AccumulatePendingDamage(
|
|
m_pendingDirtyRects, &m_nbPendingDirtyRects, &m_hasPendingDamage,
|
|
dirtyRects, nbDirtyRects);
|
|
for (CandidateDamageTail& tail : m_candidateDamageTail)
|
|
if (tail.active)
|
|
AccumulatePendingDamage(
|
|
tail.dirtyRects, &tail.nbDirtyRects, &tail.hasDamage,
|
|
dirtyRects, nbDirtyRects);
|
|
ReleaseSRWLockExclusive(&m_damageLock);
|
|
}
|
|
|
|
int CSwapChainProcessor::AcquireCandidate(
|
|
bool exclusiveSample, bool allowSupersede)
|
|
{
|
|
int selected = -1;
|
|
uint64_t oldest = UINT64_MAX;
|
|
bool superseded = false;
|
|
bool idle = true;
|
|
bool publishing = false;
|
|
|
|
AcquireSRWLockExclusive(&m_candidateLock);
|
|
for (unsigned i = 0; i < ARRAYSIZE(m_candidates); ++i)
|
|
{
|
|
if (m_candidates[i].state != CANDIDATE_FREE)
|
|
{
|
|
idle = false;
|
|
if (m_candidates[i].state == CANDIDATE_PUBLISHING)
|
|
publishing = true;
|
|
}
|
|
else if (selected < 0)
|
|
selected = static_cast<int>(i);
|
|
}
|
|
|
|
// Effect timing samples must not queue behind work which can later be
|
|
// superseded, otherwise that discarded work contaminates the sample.
|
|
if (exclusiveSample && !idle)
|
|
selected = -1;
|
|
|
|
unsigned readyCount = 0;
|
|
for (const FrameCandidate& candidate : m_candidates)
|
|
if (candidate.state == CANDIDATE_READY)
|
|
++readyCount;
|
|
|
|
// Preserve one completed fallback unless another candidate is already
|
|
// publishing. In that case its peer must remain available for new source
|
|
// frames instead of being frozen for the duration of the transport copy.
|
|
if (allowSupersede && !exclusiveSample && selected < 0 &&
|
|
readyCount > (publishing ? 0U : 1U))
|
|
for (unsigned i = 0; i < ARRAYSIZE(m_candidates); ++i)
|
|
if (m_candidates[i].state == CANDIDATE_READY &&
|
|
m_candidates[i].sequence < oldest)
|
|
{
|
|
selected = static_cast<int>(i);
|
|
oldest = m_candidates[i].sequence;
|
|
}
|
|
|
|
if (selected >= 0)
|
|
{
|
|
FrameCandidate& candidate =
|
|
m_candidates[static_cast<unsigned>(selected)];
|
|
superseded = candidate.state == CANDIDATE_READY;
|
|
candidate.state = CANDIDATE_PREPARING;
|
|
candidate.sequence = ++m_candidateSequence;
|
|
}
|
|
ReleaseSRWLockExclusive(&m_candidateLock);
|
|
|
|
if (superseded)
|
|
m_devContext->FrameSuperseded();
|
|
return selected;
|
|
}
|
|
|
|
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)
|
|
{
|
|
// Alignment is allocation metadata. GetDesc may report the resolved value
|
|
// when the creation descriptor requested automatic alignment.
|
|
return
|
|
left.Dimension == right.Dimension &&
|
|
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, size_t frameSize)
|
|
{
|
|
FrameCandidate& candidate = m_candidates[candidateIndex];
|
|
|
|
// Keep the transport layout in local GPU memory so publication does not
|
|
// combine texture detiling with the IVSHMEM or readback transfer.
|
|
D3D12_RESOURCE_DESC desc = {};
|
|
desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
|
|
desc.Width = frameSize;
|
|
desc.Height = 1;
|
|
desc.DepthOrArraySize = 1;
|
|
desc.MipLevels = 1;
|
|
desc.Format = DXGI_FORMAT_UNKNOWN;
|
|
desc.SampleDesc.Count = 1;
|
|
desc.SampleDesc.Quality = 0;
|
|
desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
|
|
desc.Flags = D3D12_RESOURCE_FLAG_NONE;
|
|
|
|
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);
|
|
|
|
AcquireSRWLockExclusive(&m_damageLock);
|
|
for (CandidateDamageTail& tail : m_candidateDamageTail)
|
|
tail = {};
|
|
ReleaseSRWLockExclusive(&m_damageLock);
|
|
SignalCandidateState();
|
|
}
|
|
|
|
void CSwapChainProcessor::SignalCandidateState()
|
|
{
|
|
SetEvent(m_candidateEvent.Get());
|
|
SetEvent(m_candidateAvailableEvent.Get());
|
|
}
|
|
|
|
bool CSwapChainProcessor::PublishNewestCandidate(
|
|
const CFrameScheduler::Schedule& schedule, bool periodic,
|
|
uint64_t publishStart)
|
|
{
|
|
// Once a deadline is due, submit transport work before allowing another
|
|
// preparation to enqueue on the same physical copy queue.
|
|
CSRWExclusiveLock pipelineLock(&m_pipelineLock);
|
|
|
|
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<int>(i);
|
|
newestSequence = m_candidates[i].sequence;
|
|
}
|
|
|
|
if (selectedCandidate >= 0)
|
|
m_candidates[static_cast<unsigned>(selectedCandidate)].state =
|
|
CANDIDATE_PUBLISHING;
|
|
ReleaseSRWLockExclusive(&m_candidateLock);
|
|
|
|
if (selectedCandidate < 0)
|
|
return false;
|
|
const unsigned candidateIndex =
|
|
static_cast<unsigned>(selectedCandidate);
|
|
|
|
const auto restoreCandidates = [this, candidateIndex]()
|
|
{
|
|
AcquireSRWLockExclusive(&m_candidateLock);
|
|
if (m_candidates[candidateIndex].state == CANDIDATE_PUBLISHING)
|
|
m_candidates[candidateIndex].state = CANDIDATE_READY;
|
|
ReleaseSRWLockExclusive(&m_candidateLock);
|
|
SignalCandidateState();
|
|
};
|
|
|
|
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];
|
|
const uint64_t candidateSequence = candidate.sequence;
|
|
|
|
auto buffer = m_devContext->PrepareFrameBuffer(
|
|
candidate.pitch,
|
|
candidate.srcFormat,
|
|
candidate.dstFormat,
|
|
candidate.dirtyRects,
|
|
candidate.nbDirtyRects,
|
|
schedule);
|
|
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.CopyFromCandidate(
|
|
copySlot->GetGfxList(), fbRes->Get().Get(), candidate.resource.Get(),
|
|
copyDirtyRects, nbCopyDirtyRects, fullCopy);
|
|
copySlot->EndTiming();
|
|
|
|
// Reserve the LGMP delivery or retained-frame slot before submitting the
|
|
// copy. This makes failure recoverable without racing a very fast GPU
|
|
// completion callback.
|
|
bool deliveredToOwner;
|
|
if (!m_devContext->PublishFrameBuffer(
|
|
buffer.frameIndex, schedule, deliveredToOwner))
|
|
{
|
|
copySlot->Cancel();
|
|
m_devContext->AbortFrameBuffer(buffer.frameIndex);
|
|
restoreCandidates();
|
|
return false;
|
|
}
|
|
CFrameScheduler::Schedule frameSchedule = schedule;
|
|
// Phase accounting must never hold up D3D submission. Keep the immutable
|
|
// delivery identity and discard only this feedback sample on contention.
|
|
if (!deliveredToOwner ||
|
|
!m_devContext->TryFrameSubmitted(buffer.frameIndex, schedule))
|
|
frameSchedule.phaseEligible = false;
|
|
fbRes->SetSchedule(frameSchedule);
|
|
|
|
// Retire the candidate damage before submission. The completion callback
|
|
// may run before Execute returns and make this candidate reusable.
|
|
AcquireSRWLockExclusive(&m_damageLock);
|
|
if (candidate.nbDirtyRects)
|
|
memcpy(m_dirtyRects, candidate.dirtyRects,
|
|
candidate.nbDirtyRects * sizeof(*m_dirtyRects));
|
|
m_nbDirtyRects = candidate.nbDirtyRects;
|
|
CandidateDamageTail& tail = m_candidateDamageTail[candidateIndex];
|
|
if (tail.active && tail.ownerSequence == candidateSequence)
|
|
{
|
|
m_hasPendingDamage = tail.hasDamage;
|
|
m_nbPendingDirtyRects = tail.nbDirtyRects;
|
|
if (tail.hasDamage && tail.nbDirtyRects)
|
|
memcpy(m_pendingDirtyRects, tail.dirtyRects,
|
|
tail.nbDirtyRects * sizeof(*m_pendingDirtyRects));
|
|
tail.ownerSequence = 0;
|
|
tail.active = false;
|
|
}
|
|
ReleaseSRWLockExclusive(&m_damageLock);
|
|
|
|
if (!copySlot->Execute())
|
|
{
|
|
// The logical damage state was advanced before submission. Force a full
|
|
// repair whether submission failed or its callback reported the failure.
|
|
SetFullPendingDamage();
|
|
AcquireSRWLockShared(&m_candidateLock);
|
|
const bool callbackPending =
|
|
candidate.state == CANDIDATE_PUBLISHING;
|
|
ReleaseSRWLockShared(&m_candidateLock);
|
|
if (callbackPending && !copySlot->HasSubmittedWork())
|
|
{
|
|
m_devContext->FailFrameBuffer(buffer.frameIndex);
|
|
ReleaseCandidate(candidateIndex);
|
|
}
|
|
m_devContext->ForceFrame();
|
|
|
|
SignalCandidateState();
|
|
return false;
|
|
}
|
|
|
|
m_devContext->CommitFrameBuffer(
|
|
buffer.frameIndex, schedule, periodic, deliveredToOwner);
|
|
|
|
unsigned superseded = 0;
|
|
AcquireSRWLockExclusive(&m_candidateLock);
|
|
for (FrameCandidate& ready : m_candidates)
|
|
if (ready.state == CANDIDATE_READY &&
|
|
ready.sequence < candidateSequence)
|
|
{
|
|
ready.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<unsigned>(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<IDXGIResource> acquiredBuffer,
|
|
unsigned dirtyRectCount, unsigned moveRegionCount,
|
|
DXGI_COLOR_SPACE_TYPE colorSpace, UINT sdrWhiteLevel,
|
|
uint64_t captureStart, bool duplicateFrame)
|
|
{
|
|
const uint64_t postProcessStart = CFrameScheduler::Nanotime();
|
|
const uint64_t captureTime = postProcessStart - captureStart;
|
|
|
|
RECT dirtyRects[LG_MAX_DIRTY_RECTS] = {0};
|
|
unsigned resolvedDirtyRectCount = 0;
|
|
bool fullDamage = false;
|
|
bool noImageUpdate = false;
|
|
HRESULT hr;
|
|
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.
|
|
fullDamage = true;
|
|
}
|
|
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");
|
|
fullDamage = true;
|
|
}
|
|
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;
|
|
}
|
|
else
|
|
resolvedDirtyRectCount = dirtyOut.DirtyRectOutCount;
|
|
}
|
|
|
|
// Reencode frames reuse the preceding presentation number. Inspect their
|
|
// empty dirty rectangle above, but suppress every ordinary duplicate.
|
|
if (duplicateFrame && !noImageUpdate)
|
|
return true;
|
|
|
|
ComPtr<ID3D11Texture2D> texture;
|
|
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;
|
|
}
|
|
|
|
if (fullDamage)
|
|
srcRes->SetFullDamage();
|
|
else
|
|
srcRes->SetDirtyRects(dirtyRects, resolvedDirtyRectCount);
|
|
|
|
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;
|
|
unsigned timingEffectIndex = 0;
|
|
uint64_t timingToken = 0;
|
|
{
|
|
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();
|
|
|
|
m_postProcessors[0].GetTimingToken(
|
|
&timingEffectIndex, &timingToken);
|
|
}
|
|
|
|
if (needsReconfigure || postProcessFormatChanged || frameMetadataChanged)
|
|
m_devContext->ForceFrame();
|
|
|
|
// Always prepare the requested static-desktop re-encode. An older
|
|
// publication can still fail after this frame is acquired, so deciding
|
|
// solely from the current pending-damage state can lose the final update.
|
|
int selectedCandidate = AcquireCandidate(timingToken != 0, !noImageUpdate);
|
|
while (selectedCandidate < 0 && noImageUpdate)
|
|
{
|
|
HANDLE waitHandles[] =
|
|
{
|
|
m_terminateEvent.Get(),
|
|
m_candidateAvailableEvent.Get(),
|
|
};
|
|
const DWORD waitResult = WaitForMultipleObjects(
|
|
ARRAYSIZE(waitHandles), waitHandles, FALSE, INFINITE);
|
|
if (waitResult == WAIT_OBJECT_0)
|
|
return true;
|
|
if (waitResult != WAIT_OBJECT_0 + 1)
|
|
{
|
|
DEBUG_ERROR_HR(HRESULT_FROM_WIN32(GetLastError()),
|
|
"Failed while waiting for a frame candidate");
|
|
return false;
|
|
}
|
|
|
|
selectedCandidate = AcquireCandidate(timingToken != 0, false);
|
|
}
|
|
if (selectedCandidate < 0)
|
|
{
|
|
m_devContext->FrameSuperseded();
|
|
return true;
|
|
}
|
|
const unsigned candidateIndex =
|
|
static_cast<unsigned>(selectedCandidate);
|
|
|
|
FrameCandidate& candidate = m_candidates[candidateIndex];
|
|
|
|
CSRWExclusiveLock pipelineLock(&m_pipelineLock);
|
|
CPostProcessor& postProcessor = m_postProcessors[candidateIndex];
|
|
const D12FrameFormat& dstFormat = postProcessor.GetOutputFormat();
|
|
|
|
RECT currentDirtyRects[LG_MAX_DIRTY_RECTS] = {};
|
|
unsigned nbDirtyRects = 0;
|
|
AcquireSRWLockExclusive(&m_damageLock);
|
|
if (m_hasPendingDamage)
|
|
{
|
|
nbDirtyRects = m_nbPendingDirtyRects;
|
|
if (nbDirtyRects)
|
|
memcpy(currentDirtyRects, m_pendingDirtyRects,
|
|
nbDirtyRects * sizeof(*currentDirtyRects));
|
|
}
|
|
CandidateDamageTail& tail = m_candidateDamageTail[candidateIndex];
|
|
tail.ownerSequence = candidate.sequence;
|
|
tail.nbDirtyRects = 0;
|
|
tail.hasDamage = false;
|
|
tail.active = true;
|
|
ReleaseSRWLockExclusive(&m_damageLock);
|
|
|
|
CD3D12CommandSlot * copySlot =
|
|
m_dx12Device->GetCopySlot(candidateIndex);
|
|
if (!copySlot)
|
|
{
|
|
ReleaseCandidate(candidateIndex);
|
|
DEBUG_ERROR("Failed to get a copy CommandSlot");
|
|
SetFullPendingDamage();
|
|
return false;
|
|
}
|
|
// Candidate and copy-slot acquisition are common to both benchmark modes.
|
|
const uint64_t timingStart = timingToken ?
|
|
CFrameScheduler::Nanotime() : 0;
|
|
|
|
ComPtr<ID3D12Resource> 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 IddCx has finished updating it.
|
|
*/
|
|
if (!srcRes->Signal())
|
|
{
|
|
if (computeSlot)
|
|
computeSlot->Cancel();
|
|
copySlot->Cancel();
|
|
ReleaseCandidate(candidateIndex);
|
|
SetFullPendingDamage();
|
|
return false;
|
|
}
|
|
|
|
if (computeSlot)
|
|
{
|
|
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);
|
|
|
|
const size_t frameSize = postProcessor.GetOutputSize();
|
|
if (!EnsureCandidateResource(candidateIndex, frameSize))
|
|
{
|
|
copySlot->Cancel();
|
|
if (computeSlot)
|
|
m_dx12Device->WaitForIdle();
|
|
ReleaseCandidate(candidateIndex);
|
|
SetFullPendingDamage();
|
|
return false;
|
|
}
|
|
|
|
candidate.srcFormat = srcFormat;
|
|
candidate.dstFormat = dstFormat;
|
|
candidate.nbDirtyRects = nbDirtyRects;
|
|
candidate.pitch = postProcessor.GetOutputPitch();
|
|
candidate.frameSize = frameSize;
|
|
candidate.captureTime = captureTime;
|
|
candidate.postProcessStart = postProcessStart;
|
|
candidate.prepareCopyStart = CFrameScheduler::Nanotime();
|
|
candidate.prepareReady = 0;
|
|
candidate.prepareGPUStart = 0;
|
|
candidate.prepareGPUEnd = 0;
|
|
candidate.timingStart = timingStart;
|
|
candidate.prepareTimingValid = false;
|
|
if (nbDirtyRects)
|
|
memcpy(candidate.dirtyRects, currentDirtyRects,
|
|
nbDirtyRects * sizeof(*candidate.dirtyRects));
|
|
candidate.timingEffectIndex = timingEffectIndex;
|
|
candidate.timingToken = timingToken;
|
|
|
|
copySlot->SetCompletionCallback(
|
|
&CandidateCompletionFunction, this, &candidate);
|
|
copySlot->BeginTiming();
|
|
postProcessor.CopyToCandidate(
|
|
copySlot->GetGfxList(), candidate.resource.Get(),
|
|
copySrcResource.Get());
|
|
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<CSwapChainProcessor*>(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;
|
|
}
|
|
}
|
|
}
|