[host] measure ready time at framebuffer publication

Exclude command queue reset and cleanup from the ready stage, and keep
the wall-clock residual only as the legacy timing fallback.
This commit is contained in:
Geoffrey McRae
2026-08-03 16:22:07 +10:00
parent aa289fa022
commit 24be0a8869
16 changed files with 157 additions and 67 deletions

View File

@@ -56,7 +56,7 @@ bool CD3D12CommandQueue::InitTiming(ID3D12Device3 * device,
D3D12_QUERY_HEAP_DESC queryDesc = {};
queryDesc.Type = D3D12_QUERY_HEAP_TYPE_COPY_QUEUE_TIMESTAMP;
queryDesc.Count = 1;
queryDesc.Count = 2;
hr = device->CreateQueryHeap(&queryDesc, IID_PPV_ARGS(&m_timestampHeap));
if (FAILED(hr))
@@ -71,7 +71,7 @@ bool CD3D12CommandQueue::InitTiming(ID3D12Device3 * device,
D3D12_RESOURCE_DESC resourceDesc = {};
resourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
resourceDesc.Width = sizeof(UINT64);
resourceDesc.Width = sizeof(UINT64) * 2;
resourceDesc.Height = 1;
resourceDesc.DepthOrArraySize = 1;
resourceDesc.MipLevels = 1;
@@ -91,7 +91,7 @@ bool CD3D12CommandQueue::InitTiming(ID3D12Device3 * device,
return false;
}
D3D12_RANGE readRange = { 0, sizeof(UINT64) };
D3D12_RANGE readRange = { 0, sizeof(UINT64) * 2 };
void * timestampMap = nullptr;
hr = m_timestampReadback->Map(0, &readRange, &timestampMap);
if (FAILED(hr))
@@ -298,37 +298,47 @@ void CD3D12CommandQueue::EndTiming()
if (!m_timingActive)
return;
m_gfxList->EndQuery(
m_timestampHeap.Get(), D3D12_QUERY_TYPE_TIMESTAMP, 1);
m_gfxList->ResolveQueryData(
m_timestampHeap.Get(), D3D12_QUERY_TYPE_TIMESTAMP,
0, 1, m_timestampReadback.Get(), 0);
0, 2, m_timestampReadback.Get(), 0);
}
bool CD3D12CommandQueue::GetGPUStartTime(uint64_t& start)
bool CD3D12CommandQueue::ConvertGPUTimestamp(
UINT64 timestamp, uint64_t& result) const
{
if (!m_timingActive)
return false;
const UINT64 gpuStart = m_timestampMap[0];
UINT64 cpuStart;
if (gpuStart < m_calibrationGPU)
UINT64 cpuTimestamp;
if (timestamp < m_calibrationGPU)
{
const UINT64 delta = ScaleTicks(
m_calibrationGPU - gpuStart, m_qpcFrequency, m_timestampFrequency);
m_calibrationGPU - timestamp, m_qpcFrequency, m_timestampFrequency);
if (delta > m_calibrationCPU)
return false;
cpuStart = m_calibrationCPU - delta;
cpuTimestamp = m_calibrationCPU - delta;
}
else
{
const UINT64 delta = ScaleTicks(
gpuStart - m_calibrationGPU, m_qpcFrequency, m_timestampFrequency);
timestamp - m_calibrationGPU, m_qpcFrequency, m_timestampFrequency);
if (UINT64_MAX - m_calibrationCPU < delta)
return false;
cpuStart = m_calibrationCPU + delta;
cpuTimestamp = m_calibrationCPU + delta;
}
start = TicksToNanoseconds(cpuStart, m_qpcFrequency);
result = TicksToNanoseconds(cpuTimestamp, m_qpcFrequency);
return true;
}
bool CD3D12CommandQueue::GetGPUTimes(
uint64_t& start, uint64_t& end) const
{
if (!m_timingActive ||
!ConvertGPUTimestamp(m_timestampMap[0], start) ||
!ConvertGPUTimestamp(m_timestampMap[1], end) ||
end < start)
return false;
return true;
}

View File

@@ -67,6 +67,7 @@ class CD3D12CommandQueue
bool InitTiming(ID3D12Device3 * device, D3D12_COMMAND_LIST_TYPE type);
void UpdateClockCalibration();
bool ConvertGPUTimestamp(UINT64 timestamp, uint64_t& result) const;
void OnCompletion()
{
@@ -108,8 +109,8 @@ class CD3D12CommandQueue
bool BeginTiming();
void EndTiming();
// Return the command-list start in QueryPerformanceCounter-domain ns.
bool GetGPUStartTime(uint64_t& start);
// Return the copy boundaries in QueryPerformanceCounter-domain ns.
bool GetGPUTimes(uint64_t& start, uint64_t& end) const;
//void Wait();
bool IsReady () const { return !m_pending ; }

View File

@@ -1302,6 +1302,7 @@ CIndirectDeviceContext::PreparedFrameBuffer CIndirectDeviceContext::PrepareFrame
fi->captureTime = 0;
fi->postProcessTime = 0;
fi->copyTime = 0;
fi->readyTime = 0;
fi->timingSerial = 0;
InterlockedExchange((volatile LONG *)&fi->timingValid, 0);
fi->rotation = FRAME_ROT_0;
@@ -1370,7 +1371,8 @@ bool CIndirectDeviceContext::PublishFrameBuffer(unsigned frameIndex)
}
void CIndirectDeviceContext::SetFrameTiming(unsigned frameIndex,
uint64_t captureTime, uint64_t postProcessTime, uint64_t copyTime)
uint64_t captureTime, uint64_t postProcessTime, uint64_t copyTime,
uint64_t readyTime)
{
if (frameIndex >= LGMP_Q_FRAME_LEN)
return;
@@ -1379,6 +1381,7 @@ void CIndirectDeviceContext::SetFrameTiming(unsigned frameIndex,
frame->captureTime = captureTime;
frame->postProcessTime = postProcessTime;
frame->copyTime = copyTime;
frame->readyTime = readyTime;
frame->timingSerial = frame->frameSerial;
InterlockedExchange((volatile LONG *)&frame->timingValid, 1);
}

View File

@@ -218,7 +218,7 @@ public:
PreparedFrameBuffer PrepareFrameBuffer(unsigned pitch, const D12FrameFormat& srcFormat, const D12FrameFormat& dstFormat, const RECT * dirtyRects, unsigned nbDirtyRects);
bool PublishFrameBuffer(unsigned frameIndex);
void SetFrameTiming(unsigned frameIndex, uint64_t captureTime,
uint64_t postProcessTime, uint64_t copyTime);
uint64_t postProcessTime, uint64_t copyTime, uint64_t readyTime);
void WriteFrameBuffer(unsigned frameIndex, void* src, size_t offset, size_t len, bool setWritePos) const;
void FinalizeFrameBuffer(unsigned frameIndex) const;

View File

@@ -318,32 +318,38 @@ void CSwapChainProcessor::CompletionFunction(
auto sc = (CSwapChainProcessor *)param1;
auto fbRes = (CFrameBufferResource *)param2;
uint64_t copyStart = fbRes->GetCopyStart();
uint64_t gpuCopyStart = 0;
const uint64_t cpuCopyStart = fbRes->GetCopyStart();
uint64_t gpuCopyStart = 0;
uint64_t gpuCopyEnd = 0;
if (result && sc->m_dx12Device->IsIndirectCopy())
sc->m_devContext->WriteFrameBuffer(
fbRes->GetFrameIndex(), fbRes->GetMap(), 0, fbRes->GetFrameSize(), false);
// 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);
// Queue waits execute before the start timestamp. The end timestamp follows
// the last CopyTextureRegion, separating GPU work from readiness dispatch.
const bool gpuTimingValid = result &&
queue->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 copyEnd = Nanotime();
const uint64_t readyEnd = Nanotime();
uint64_t postProcessTime = cpuCopyStart - fbRes->GetPostProcessStart();
uint64_t copyTime = readyEnd - cpuCopyStart;
uint64_t readyTime = 0;
if (gpuTimingValid &&
gpuCopyStart >= fbRes->GetPostProcessStart() &&
gpuCopyStart <= copyEnd)
copyStart = gpuCopyStart;
gpuCopyEnd <= readyEnd)
{
postProcessTime = gpuCopyStart - fbRes->GetPostProcessStart();
copyTime = gpuCopyEnd - gpuCopyStart;
readyTime = readyEnd - gpuCopyEnd;
}
const uint64_t postProcessTime = copyStart -
fbRes->GetPostProcessStart();
const uint64_t copyTime = copyEnd - copyStart;
sc->m_devContext->SetFrameTiming(fbRes->GetFrameIndex(),
fbRes->GetCaptureTime(), postProcessTime, copyTime);
fbRes->GetCaptureTime(), postProcessTime, copyTime, readyTime);
}