From b50876e77c59a6d64e5899de552adbdd4a42c36d Mon Sep 17 00:00:00 2001 From: Marcos Slomp Date: Mon, 23 Mar 2026 16:58:41 -0700 Subject: [PATCH] eliminate NewFrame, and account for "abandoned" and "out-of-order" timestamp queries --- public/tracy/TracyD3D12.hpp | 213 ++++++++++++++++++++++-------------- 1 file changed, 131 insertions(+), 82 deletions(-) diff --git a/public/tracy/TracyD3D12.hpp b/public/tracy/TracyD3D12.hpp index a3594689..095a4a42 100644 --- a/public/tracy/TracyD3D12.hpp +++ b/public/tracy/TracyD3D12.hpp @@ -36,42 +36,48 @@ using TracyD3D12Ctx = void*; #include "../client/TracyProfiler.hpp" #include "../client/TracyCallstack.hpp" +#include +#include +#include +#include #include +#include #include #include #include -#include + +#ifndef TRACY_D3D12_TIMESTAMP_COLLECT_TIMEOUT +#define TRACY_D3D12_TIMESTAMP_COLLECT_TIMEOUT 0.200f +#endif #define TracyD3D12Panic(msg, ...) do { assert(false && "TracyD3D12: " msg); tracy::Profiler::LogString( tracy::MessageSourceType::Tracy, tracy::MessageSeverity::Error, tracy::Color::Red4, 0, msg ); __VA_ARGS__; } while(false); namespace tracy { - struct D3D12QueryPayload - { - uint32_t m_queryIdStart = 0; - uint32_t m_queryCount = 0; - }; - // Command queue context. class D3D12QueueCtx { friend class D3D12ZoneScope; + static constexpr UINT64 InvalidTimestamp = 0; // arbitrary choice (afaik, it's technically a valid timestamp value) + + uint8_t m_contextId = 255; // TODO: apparently, 255 means "invalid id"; is this documented somewhere? + + std::mutex m_collectionMutex; + ID3D12Device* m_device = nullptr; ID3D12CommandQueue* m_queue = nullptr; - uint8_t m_contextId = 255; // TODO: apparently, 255 means "invalid id"; is this documented somewhere? ID3D12QueryHeap* m_queryHeap = nullptr; ID3D12Resource* m_readbackBuffer = nullptr; - // In-progress payload. - uint32_t m_queryLimit = 0; - std::atomic m_queryCounter = 0; - uint32_t m_previousQueryCounter = 0; + using atomic_counter = std::atomic; + atomic_counter m_queryCounter = 0; + atomic_counter m_previousCheckpoint = 0; - uint32_t m_activePayload = 0; - ID3D12Fence* m_payloadFence = nullptr; - std::queue m_payloadQueue; + uint32_t m_queryLimit = 0; + + std::vector m_queryRequestTime; UINT64 m_prevCalibrationTicksCPU = 0; @@ -173,11 +179,23 @@ namespace tracy TracyD3D12Panic("Failed to create query readback buffer.", return); } - if (FAILED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_payloadFence)))) { - TracyD3D12Panic("Failed to create payload fence.", return); + D3D12_RANGE zeroRange{ 0, m_queryLimit * sizeof(UINT64) }; + void* buffer = nullptr; + if (FAILED(m_readbackBuffer->Map(0, &zeroRange, &buffer))) + { + TracyD3D12Panic("Failed to map readback buffer for initialization.", return); + } + UINT64* timestampBuffer = static_cast(buffer); + for (uint64_t i = 0; i < m_queryLimit; ++i) + { + timestampBuffer[i] = InvalidTimestamp; + } + m_readbackBuffer->Unmap(0, &zeroRange); } + m_queryRequestTime.resize(m_queryLimit); + float period = [queue]() { uint64_t timestampFrequency; @@ -205,19 +223,19 @@ namespace tracy cpuTimestamp = Profiler::GetTime(); - // all checked: ready to roll + // All setup/init checks completed: ready to create the context. m_contextId = GetGpuCtxCounter().fetch_add(1); ZoneValue(int64_t(m_contextId)); auto* item = Profiler::QueueSerial(); MemWrite(&item->hdr.type, QueueType::GpuNewContext); - MemWrite(&item->gpuNewContext.cpuTime, cpuTimestamp); - MemWrite(&item->gpuNewContext.gpuTime, gpuTimestamp); - MemWrite(&item->gpuNewContext.thread, decltype(item->gpuNewContext.thread)(0)); // #TODO: why 0 instead of GetThreadHandle()? - MemWrite(&item->gpuNewContext.period, period); - MemWrite(&item->gpuNewContext.context, GetId()); - MemWrite(&item->gpuNewContext.flags, GpuContextCalibration); - MemWrite(&item->gpuNewContext.type, GpuContextType::Direct3D12); + MemWrite(&item->gpuNewContext.cpuTime, static_cast(cpuTimestamp)); + MemWrite(&item->gpuNewContext.gpuTime, static_cast(gpuTimestamp)); + MemWrite(&item->gpuNewContext.thread, static_cast(0)); // zero means the context is not associated with a specific thread + MemWrite(&item->gpuNewContext.period, static_cast(period)); + MemWrite(&item->gpuNewContext.context, static_cast(GetId())); + MemWrite(&item->gpuNewContext.flags, static_cast(GpuContextCalibration)); + MemWrite(&item->gpuNewContext.type, static_cast(GpuContextType::Direct3D12)); SubmitQueueItem(item); } @@ -225,28 +243,14 @@ namespace tracy { ZoneScopedC(Color::Red4); ZoneValue(int64_t(m_contextId)); - // collect all pending timestamps - while (m_payloadFence->GetCompletedValue() != m_activePayload) - /* busy-wait ... */; - Collect(); - m_payloadFence->Release(); - m_readbackBuffer->Release(); - m_queryHeap->Release(); - } - - void NewFrame() - { - uint32_t queryCounter = m_queryCounter.exchange(0); - m_payloadQueue.emplace(D3D12QueryPayload{ m_previousQueryCounter, queryCounter }); - m_previousQueryCounter += queryCounter; - - if (m_previousQueryCounter >= m_queryLimit) + while (m_previousCheckpoint.load() != m_queryCounter.load()) { - m_previousQueryCounter -= m_queryLimit; + Collect(); } - m_queue->Signal(m_payloadFence, ++m_activePayload); + m_readbackBuffer->Release(); + m_queryHeap->Release(); } void Name( const char* name, uint16_t len ) @@ -267,79 +271,123 @@ namespace tracy #ifdef TRACY_ON_DEMAND if (!GetProfiler().IsConnected()) { - m_queryCounter = 0; - + m_previousCheckpoint = m_queryCounter.load(); return; } #endif ZoneScopedC(Color::Red4); ZoneValue(uint64_t(m_contextId)); - // Find out what payloads are available. - const auto newestReadyPayload = m_payloadFence->GetCompletedValue(); - const auto payloadCount = m_payloadQueue.size() - (m_activePayload - newestReadyPayload); - - if (!payloadCount) + // Only one thread is allowed to collect timestamps at any given time + // but there's no need to block contending threads + if (!m_collectionMutex.try_lock()) { - return; // No payloads are available yet, exit out. + return; + } + std::unique_lock lock (m_collectionMutex, std::adopt_lock); + + // Establish a range of queries to collect: + // the range starts at the "previous checkpoint" (known to have been resolved already), + // and can extend up to the last query id emitted so far. + uint64_t begin = m_previousCheckpoint.load(); + uint64_t latestCheckpoint = m_queryCounter.load(); + + uint32_t count = RingCount(begin, latestCheckpoint); + if (count == 0) + { + return; // no pending timestamp queries + } + + // paranoid check... + if (count >= RingSize()) + { + TracyD3D12Panic("Collect: FULL! too many pending timestamp queries.", return); } - D3D12_RANGE mapRange{ 0, m_queryLimit * sizeof(uint64_t) }; - - // Map the readback buffer so we can fetch the query data from the GPU. + D3D12_RANGE mapRange{ 0, m_queryLimit * sizeof(UINT64) }; void* readbackBufferMapping = nullptr; - if (FAILED(m_readbackBuffer->Map(0, &mapRange, &readbackBufferMapping))) { - TracyD3D12Panic("Failed to map readback buffer.", return); + TracyD3D12Panic("Collect: failed to map timestamp buffer.", return); } + UINT64* timestampBuffer = static_cast(readbackBufferMapping); - auto* timestampData = static_cast(readbackBufferMapping); - - for (uint32_t i = 0; i < payloadCount; ++i) + for (uint64_t i = begin; i != latestCheckpoint; ++i) { - const auto& payload = m_payloadQueue.front(); + const uint32_t queryId = RingIndex(i); + UINT64& gpuTimestamp = timestampBuffer[queryId]; - for (uint32_t j = 0; j < payload.m_queryCount; ++j) + if (gpuTimestamp == InvalidTimestamp) { - const auto counter = (payload.m_queryIdStart + j) % m_queryLimit; - const auto timestamp = timestampData[counter]; - const auto queryId = counter; - - auto* item = Profiler::QueueSerial(); - MemWrite(&item->hdr.type, QueueType::GpuTime); - MemWrite(&item->gpuTime.gpuTime, timestamp); - MemWrite(&item->gpuTime.queryId, static_cast(queryId)); - MemWrite(&item->gpuTime.context, GetId()); - - Profiler::QueueSerialFinish(); + // drop the timestamp query if it's been in flight for too long + using Clock = std::chrono::high_resolution_clock; + auto now = Clock::now(); + auto start = m_queryRequestTime[queryId]; + auto timeout = std::chrono::duration{TRACY_D3D12_TIMESTAMP_COLLECT_TIMEOUT}; + if (now - start >= timeout) + { + TracyPlot("TracyD3D12 timeout", int64_t(0)); + TracyPlot("TracyD3D12 timeout", int64_t(1)); + TracyPlot("TracyD3D12 timeout", int64_t(0)); + m_previousCheckpoint.store(i+1, std::memory_order_relaxed); + // TODO: may need to emit a "bogus" GpuTime just to provide + // a "match" for the query ids that have been instrumented + continue; + } + // otherwise, let subsequent Collect() calls handle it + break; } - m_payloadQueue.pop(); + auto* item = Profiler::QueueSerial(); + MemWrite(&item->hdr.type, QueueType::GpuTime); + MemWrite(&item->gpuTime.gpuTime, static_cast(gpuTimestamp)); + MemWrite(&item->gpuTime.queryId, static_cast(queryId)); + MemWrite(&item->gpuTime.context, GetId()); + Profiler::QueueSerialFinish(); + + gpuTimestamp = InvalidTimestamp; + m_previousCheckpoint.store(i+1, std::memory_order_relaxed); } - m_readbackBuffer->Unmap(0, nullptr); + m_readbackBuffer->Unmap(0, &mapRange); - // Recalibrate to account for drift. RecalibrateClocks(); } private: + tracy_force_inline uint32_t RingSize() const + { + return m_queryLimit; + } + + tracy_force_inline uint32_t RingIndex(uint64_t logicalSlot) const + { + return static_cast(logicalSlot % RingSize()); + } + + tracy_force_inline uint32_t RingCount(uint64_t begin, uint64_t end) const + { + return static_cast(end - begin); + } + tracy_force_inline uint32_t NextQueryId() { - uint32_t queryCounter = m_queryCounter.fetch_add(2); - if (queryCounter >= m_queryLimit) + const uint64_t beginSlot = m_queryCounter.fetch_add(2, std::memory_order_relaxed); + if (RingCount(m_previousCheckpoint.load(), beginSlot) >= RingSize()) { ZoneScopedC(Color::Red4); ZoneValue(int64_t(m_contextId)); TracyD3D12Panic("Submitted too many GPU queries!"); - // TODO: get rid of NewFrame() and make collection "circular" - // TODO: decide what to do when "full" (collect, or return an error-id?) + // TODO: decide what to do when "full" (Collect(), or return an arbitrary error-id?) } - const uint32_t id = (m_previousQueryCounter + queryCounter) % m_queryLimit; + const uint32_t r0 = RingIndex(beginSlot); + const uint32_t r1 = RingIndex(beginSlot + 1); + const auto t = std::chrono::high_resolution_clock::now(); + m_queryRequestTime[r0] = t; + m_queryRequestTime[r1] = t; - return id; + return r0; } tracy_force_inline uint8_t GetId() const @@ -479,6 +527,7 @@ namespace tracy } #undef TracyD3D12Panic +#undef TRACY_D3D12_TIMESTAMP_COLLECT_TIMEOUT using TracyD3D12Ctx = tracy::D3D12QueueCtx*; @@ -486,7 +535,7 @@ using TracyD3D12Ctx = tracy::D3D12QueueCtx*; #define TracyD3D12Destroy(ctx) tracy::DestroyD3D12Context(ctx); #define TracyD3D12ContextName(ctx, name, size) ctx->Name(name, size); -#define TracyD3D12NewFrame(ctx) ctx->NewFrame(); +#define TracyD3D12NewFrame(ctx) ((void)(ctx)) #define TracyD3D12UnnamedZone ___tracy_gpu_d3d12_zone #define TracyD3D12SrcLocSymbol TracyConcat(__tracy_d3d12_source_location,TracyLine)