[client/host/idd] correct frame timing attribution

Use calibrated copy-queue timestamps to separate source and effect
waits from the actual framebuffer copy.

Exclude producer readiness waits from client import timing so the
same interval is not counted in both Copy and Import.
This commit is contained in:
Geoffrey McRae
2026-08-03 15:58:41 +10:00
parent c8e4b97c3c
commit aa289fa022
17 changed files with 630 additions and 92 deletions

View File

@@ -21,7 +21,125 @@
#include "CD3D12CommandQueue.h"
#include "CDebug.h"
bool CD3D12CommandQueue::Init(ID3D12Device3 * device, D3D12_COMMAND_LIST_TYPE type, const WCHAR* name, CallbackMode callbackMode)
static uint64_t ScaleTicks(uint64_t ticks, uint64_t targetFrequency,
uint64_t sourceFrequency)
{
return ticks / sourceFrequency * targetFrequency +
ticks % sourceFrequency * targetFrequency / sourceFrequency;
}
static uint64_t TicksToNanoseconds(uint64_t ticks, uint64_t frequency)
{
return ScaleTicks(ticks, 1000000000ULL, frequency);
}
bool CD3D12CommandQueue::InitTiming(ID3D12Device3 * device,
D3D12_COMMAND_LIST_TYPE type)
{
if (type != D3D12_COMMAND_LIST_TYPE_COPY)
return false;
D3D12_FEATURE_DATA_D3D12_OPTIONS3 options = {};
HRESULT hr = device->CheckFeatureSupport(
D3D12_FEATURE_D3D12_OPTIONS3, &options, sizeof(options));
if (FAILED(hr) || !options.CopyQueueTimestampQueriesSupported)
return false;
hr = m_queue->GetTimestampFrequency(&m_timestampFrequency);
if (FAILED(hr) || !m_timestampFrequency)
return false;
LARGE_INTEGER qpcFrequency;
if (!QueryPerformanceFrequency(&qpcFrequency))
return false;
m_qpcFrequency = (UINT64)qpcFrequency.QuadPart;
D3D12_QUERY_HEAP_DESC queryDesc = {};
queryDesc.Type = D3D12_QUERY_HEAP_TYPE_COPY_QUEUE_TIMESTAMP;
queryDesc.Count = 1;
hr = device->CreateQueryHeap(&queryDesc, IID_PPV_ARGS(&m_timestampHeap));
if (FAILED(hr))
return false;
D3D12_HEAP_PROPERTIES heapProps = {};
heapProps.Type = D3D12_HEAP_TYPE_READBACK;
heapProps.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapProps.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heapProps.CreationNodeMask = 1;
heapProps.VisibleNodeMask = 1;
D3D12_RESOURCE_DESC resourceDesc = {};
resourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
resourceDesc.Width = sizeof(UINT64);
resourceDesc.Height = 1;
resourceDesc.DepthOrArraySize = 1;
resourceDesc.MipLevels = 1;
resourceDesc.SampleDesc.Count = 1;
resourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
hr = device->CreateCommittedResource(
&heapProps,
D3D12_HEAP_FLAG_NONE,
&resourceDesc,
D3D12_RESOURCE_STATE_COPY_DEST,
NULL,
IID_PPV_ARGS(&m_timestampReadback));
if (FAILED(hr))
{
m_timestampHeap.Reset();
return false;
}
D3D12_RANGE readRange = { 0, sizeof(UINT64) };
void * timestampMap = nullptr;
hr = m_timestampReadback->Map(0, &readRange, &timestampMap);
if (FAILED(hr))
{
m_timestampReadback.Reset();
m_timestampHeap.Reset();
return false;
}
m_timestampMap = static_cast<UINT64 *>(timestampMap);
hr = m_queue->GetClockCalibration(&m_calibrationGPU, &m_calibrationCPU);
if (FAILED(hr))
{
D3D12_RANGE writeRange = { 0, 0 };
m_timestampReadback->Unmap(0, &writeRange);
m_timestampMap = nullptr;
m_timestampReadback.Reset();
m_timestampHeap.Reset();
return false;
}
m_timingSupported = true;
return true;
}
void CD3D12CommandQueue::UpdateClockCalibration()
{
if (!m_timingSupported)
return;
LARGE_INTEGER now;
if (QueryPerformanceCounter(&now) &&
(UINT64)now.QuadPart >= m_calibrationCPU &&
(UINT64)now.QuadPart - m_calibrationCPU < m_qpcFrequency)
return;
UINT64 gpu;
UINT64 cpu;
if (SUCCEEDED(m_queue->GetClockCalibration(&gpu, &cpu)))
{
m_calibrationGPU = gpu;
m_calibrationCPU = cpu;
}
}
bool CD3D12CommandQueue::Init(ID3D12Device3 * device,
D3D12_COMMAND_LIST_TYPE type, const WCHAR * name,
CallbackMode callbackMode)
{
HRESULT hr;
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
@@ -79,7 +197,7 @@ bool CD3D12CommandQueue::Init(ID3D12Device3 * device, D3D12_COMMAND_LIST_TYPE ty
ULONG flags = (callbackMode == FAST) ?
WT_EXECUTEINWAITTHREAD : WT_EXECUTEINPERSISTENTTHREAD;
RegisterWaitForSingleObject(
if (!RegisterWaitForSingleObject(
&m_waitHandle,
m_event.Get(),
[](PVOID param, BOOLEAN timeout){
@@ -90,9 +208,19 @@ bool CD3D12CommandQueue::Init(ID3D12Device3 * device, D3D12_COMMAND_LIST_TYPE ty
},
this,
INFINITE,
flags);
flags))
{
DEBUG_ERROR_HR(GetLastError(),
"Failed to register the completion wait (%ls)", name);
m_waitHandle = INVALID_HANDLE_VALUE;
return false;
}
}
if (callbackMode != DISABLED &&
type == D3D12_COMMAND_LIST_TYPE_COPY && !InitTiming(device, type))
DEBUG_WARN("GPU timing is unavailable for CommandQueue(%ls)", name);
m_name = name;
m_fenceValue = 0;
DEBUG_INFO("Created CD3D12CommandQueue(%ls)", name);
@@ -103,15 +231,25 @@ void CD3D12CommandQueue::DeInit()
{
if (m_waitHandle != INVALID_HANDLE_VALUE)
{
// Queue owners drain callbacks before destruction. Keep this unregister
// non-blocking so a removed or hung device cannot stall teardown.
UnregisterWait(m_waitHandle);
m_waitHandle = INVALID_HANDLE_VALUE;
}
if (m_timestampMap)
{
D3D12_RANGE writeRange = { 0, 0 };
m_timestampReadback->Unmap(0, &writeRange);
m_timestampMap = nullptr;
}
}
bool CD3D12CommandQueue::Execute()
{
m_needsReset = true;
m_completionResult = true;
m_pending = m_waitHandle != INVALID_HANDLE_VALUE;
HRESULT hr = m_gfxList->Close();
if (FAILED(hr))
@@ -137,11 +275,63 @@ bool CD3D12CommandQueue::Execute()
return false;
}
m_pending = true;
m_queue->Signal(m_fence.Get(), m_fenceValue);
return true;
}
bool CD3D12CommandQueue::BeginTiming()
{
m_timingActive = m_timingSupported;
if (!m_timingActive)
return false;
// Refresh after an idle period; active queues are calibrated by their
// completion path without adding work to frame submission.
UpdateClockCalibration();
m_gfxList->EndQuery(
m_timestampHeap.Get(), D3D12_QUERY_TYPE_TIMESTAMP, 0);
return true;
}
void CD3D12CommandQueue::EndTiming()
{
if (!m_timingActive)
return;
m_gfxList->ResolveQueryData(
m_timestampHeap.Get(), D3D12_QUERY_TYPE_TIMESTAMP,
0, 1, m_timestampReadback.Get(), 0);
}
bool CD3D12CommandQueue::GetGPUStartTime(uint64_t& start)
{
if (!m_timingActive)
return false;
const UINT64 gpuStart = m_timestampMap[0];
UINT64 cpuStart;
if (gpuStart < m_calibrationGPU)
{
const UINT64 delta = ScaleTicks(
m_calibrationGPU - gpuStart, m_qpcFrequency, m_timestampFrequency);
if (delta > m_calibrationCPU)
return false;
cpuStart = m_calibrationCPU - delta;
}
else
{
const UINT64 delta = ScaleTicks(
gpuStart - m_calibrationGPU, m_qpcFrequency, m_timestampFrequency);
if (UINT64_MAX - m_calibrationCPU < delta)
return false;
cpuStart = m_calibrationCPU + delta;
}
start = TicksToNanoseconds(cpuStart, m_qpcFrequency);
return true;
}
#if 0
void CD3D12CommandQueue::Wait()
{
@@ -159,6 +349,7 @@ void CD3D12CommandQueue::Wait()
bool CD3D12CommandQueue::Reset()
{
m_timingActive = false;
if (!m_needsReset)
return true;

View File

@@ -25,6 +25,7 @@
#include <wrl.h>
#include <d3d12.h>
#include <atomic>
#include <stdint.h>
using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
@@ -41,11 +42,21 @@ class CD3D12CommandQueue
ComPtr<ID3D12CommandList > m_cmdList;
ComPtr<ID3D12Fence > m_fence;
std::atomic<bool> m_pending = false;
ComPtr<ID3D12QueryHeap> m_timestampHeap;
ComPtr<ID3D12Resource > m_timestampReadback;
UINT64 * m_timestampMap = nullptr;
UINT64 m_timestampFrequency = 0;
UINT64 m_calibrationGPU = 0;
UINT64 m_calibrationCPU = 0;
UINT64 m_qpcFrequency = 0;
bool m_timingSupported = false;
bool m_timingActive = false;
std::atomic<bool> m_pending = false;
HandleT<HANDLENullTraits> m_event;
HANDLE m_waitHandle = INVALID_HANDLE_VALUE;
UINT64 m_fenceValue = 0;
bool m_needsReset = false;
HANDLE m_waitHandle = INVALID_HANDLE_VALUE;
UINT64 m_fenceValue = 0;
bool m_needsReset = false;
typedef void (*CompletionFunction)(CD3D12CommandQueue * queue,
bool result, void * param1, void * param2);
@@ -54,6 +65,9 @@ class CD3D12CommandQueue
void * m_completionParams[2];
bool m_completionResult = true;
bool InitTiming(ID3D12Device3 * device, D3D12_COMMAND_LIST_TYPE type);
void UpdateClockCalibration();
void OnCompletion()
{
if (m_completionCallback)
@@ -62,6 +76,7 @@ class CD3D12CommandQueue
m_completionResult,
m_completionParams[0],
m_completionParams[1]);
UpdateClockCalibration();
m_pending = false;
}
@@ -90,6 +105,12 @@ class CD3D12CommandQueue
bool Reset();
bool Execute();
bool BeginTiming();
void EndTiming();
// Return the command-list start in QueryPerformanceCounter-domain ns.
bool GetGPUStartTime(uint64_t& start);
//void Wait();
bool IsReady () const { return !m_pending ; }
HANDLE GetEvent() const { return m_event.Get(); }

View File

@@ -33,16 +33,16 @@ using namespace Microsoft::WRL;
class CFrameBufferResource
{
private:
bool m_valid = false;
unsigned m_frameIndex = 0;
uint8_t * m_base = nullptr;
size_t m_size = 0;
size_t m_frameSize = 0;
uint64_t m_captureTime = 0;
uint64_t m_postProcessTime = 0;
uint64_t m_copyStart = 0;
bool m_valid = false;
unsigned m_frameIndex = 0;
uint8_t * m_base = nullptr;
size_t m_size = 0;
size_t m_frameSize = 0;
uint64_t m_captureTime = 0;
uint64_t m_postProcessStart = 0;
uint64_t m_copyStart = 0;
ComPtr<ID3D12Resource> m_res;
void * m_map = nullptr;
void * m_map = nullptr;
public:
bool Init(CSwapChainProcessor * swapChain, unsigned frameIndex, uint8_t * base, size_t size);
@@ -55,16 +55,16 @@ class CFrameBufferResource
size_t GetFrameSize() { return m_frameSize; }
void * GetMap() { return m_map; }
void SetTiming(uint64_t captureTime, uint64_t postProcessTime,
void SetTiming(uint64_t captureTime, uint64_t postProcessStart,
uint64_t copyStart)
{
m_captureTime = captureTime;
m_postProcessTime = postProcessTime;
m_copyStart = copyStart;
m_captureTime = captureTime;
m_postProcessStart = postProcessStart;
m_copyStart = copyStart;
}
uint64_t GetCaptureTime () const { return m_captureTime; }
uint64_t GetPostProcessTime() const { return m_postProcessTime; }
uint64_t GetCopyStart () const { return m_copyStart; }
uint64_t GetCaptureTime () const { return m_captureTime; }
uint64_t GetPostProcessStart() const { return m_postProcessStart; }
uint64_t GetCopyStart () const { return m_copyStart; }
ComPtr<ID3D12Resource> Get() { return m_res; }
};

View File

@@ -289,7 +289,7 @@ bool CSwapChainProcessor::SwapChainThreadCore()
{
lastFrameNumber = frameNumber;
if (!SwapChainNewFrame(surface, dirtyRectCount, moveRegionCount,
colorSpace, sdrWhiteLevel, Nanotime() - captureStart))
colorSpace, sdrWhiteLevel, captureStart))
DEBUG_WARN("Failed to submit frame");
}
@@ -315,29 +315,35 @@ bool CSwapChainProcessor::SwapChainThreadCore()
void CSwapChainProcessor::CompletionFunction(
CD3D12CommandQueue * queue, bool result, void * param1, void * param2)
{
UNREFERENCED_PARAMETER(queue);
auto sc = (CSwapChainProcessor *)param1;
auto fbRes = (CFrameBufferResource *)param2;
auto sc = (CSwapChainProcessor *)param1;
auto fbRes = (CFrameBufferResource*)param2;
uint64_t copyStart = fbRes->GetCopyStart();
uint64_t gpuCopyStart = 0;
// fail gracefully
if (!result)
{
sc->m_devContext->SetFrameTiming(fbRes->GetFrameIndex(),
fbRes->GetCaptureTime(), fbRes->GetPostProcessTime(),
Nanotime() - fbRes->GetCopyStart());
sc->m_devContext->FinalizeFrameBuffer(fbRes->GetFrameIndex());
return;
}
if (sc->m_dx12Device->IsIndirectCopy())
if (result && sc->m_dx12Device->IsIndirectCopy())
sc->m_devContext->WriteFrameBuffer(
fbRes->GetFrameIndex(), fbRes->GetMap(), 0, fbRes->GetFrameSize(), false);
const uint64_t copyTime = Nanotime() - fbRes->GetCopyStart();
sc->m_devContext->SetFrameTiming(fbRes->GetFrameIndex(),
fbRes->GetCaptureTime(), fbRes->GetPostProcessTime(), copyTime);
// Queue waits execute before this timestamp. Use it as the boundary so the
// source fence and effects are charged to Post, while Copy retains the full
// time through buffer readiness and any indirect memcpy.
const bool gpuTimingValid = result && queue->GetGPUStartTime(gpuCopyStart);
// 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 copyEnd = Nanotime();
if (gpuTimingValid &&
gpuCopyStart >= fbRes->GetPostProcessStart() &&
gpuCopyStart <= copyEnd)
copyStart = gpuCopyStart;
const uint64_t postProcessTime = copyStart -
fbRes->GetPostProcessStart();
const uint64_t copyTime = copyEnd - copyStart;
sc->m_devContext->SetFrameTiming(fbRes->GetFrameIndex(),
fbRes->GetCaptureTime(), postProcessTime, copyTime);
}
@@ -515,8 +521,11 @@ bool CSwapChainProcessor::GetContentHDRMetadata(D12FrameFormat& format) const
bool CSwapChainProcessor::SwapChainNewFrame(ComPtr<IDXGIResource> acquiredBuffer,
unsigned dirtyRectCount, unsigned moveRegionCount,
DXGI_COLOR_SPACE_TYPE colorSpace, UINT sdrWhiteLevel,
uint64_t captureTime)
uint64_t captureStart)
{
const uint64_t postProcessStart = Nanotime();
const uint64_t captureTime = postProcessStart - captureStart;
// Preserve the fast drop path: never hold an IddCx frame while waiting for
// a slow or disconnected client. We have not read its rectangles, so force
// the next published frame to invalidate the entire image.
@@ -526,8 +535,6 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr<IDXGIResource> acquiredBuffer
return true;
}
const uint64_t postProcessStart = Nanotime();
ComPtr<ID3D11Texture2D> texture;
HRESULT hr = acquiredBuffer.As(&texture);
if (FAILED(hr))
@@ -775,7 +782,7 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr<IDXGIResource> acquiredBuffer
}
const uint64_t copyStart = Nanotime();
fbRes->SetTiming(captureTime, copyStart - postProcessStart, copyStart);
fbRes->SetTiming(captureTime, postProcessStart, copyStart);
copyQueue->SetCompletionCallback(&CompletionFunction, this, fbRes);
@@ -789,6 +796,9 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr<IDXGIResource> acquiredBuffer
dstLoc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
dstLoc.PlacedFootprint = layout;
// The source/compute waits were inserted directly on the queue above. The
// command-list timestamp therefore marks the first actual copy operation.
copyQueue->BeginTiming();
if (IsFullDamage(currentDirtyRects, nbDirtyRects, dstFormat.desc) ||
nbDirtyRects > KVMFR_MAX_DAMAGE_RECTS || m_nbDirtyRects == 0)
{
@@ -812,6 +822,7 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr<IDXGIResource> acquiredBuffer
for (const RECT * rect = currentDirtyRects; rect < currentDirtyRects + nbDirtyRects; ++rect)
CopyDirtyRect(copyQueue->GetGfxList(), &dstLoc, &srcLoc, *rect);
}
copyQueue->EndTiming();
if (!copyQueue->Execute())
{

View File

@@ -104,7 +104,7 @@ private:
bool SwapChainNewFrame(ComPtr<IDXGIResource> acquiredBuffer,
unsigned dirtyRectCount, unsigned moveRegionCount,
DXGI_COLOR_SPACE_TYPE colorSpace, UINT sdrWhiteLevel,
uint64_t captureTime);
uint64_t captureStart);
public:
CSwapChainProcessor(CIndirectMonitorContext * monitorContext, UINT64 assignmentGeneration,