To support frame looping, GfxReconstruct must reset Vulkan device and resource states back to their initial captured states at the boundary of each loop iteration.
Additionally, the primary goal of this refactor is to execute ResetLoopBoundary() outside the GFXR_REPLAY_EVENT_FRAME_BEGIN and GFXR_REPLAY_EVENT_FRAME_END markers reported to benchmark plugins. By moving the reset outside of this window, the frame timings reported to the plugin represent clean frame execution and do not include the significant overhead of resetting loop boundaries (with the exception of the first frame, where initialization/preload overhead is expected).
Previously, this loop boundary reset (ResetLoopBoundary()) was triggered inside either:
ProcessFrameEndMarker()(if the capture contained frame markers).Process_vkQueuePresentKHR()(if no frame markers were present).
This old approach had several drawbacks:
- Timing Inconsistencies & Profiling Overhead: State restoration occurred within the replaying of frame blocks (e.g. inside
vkQueuePresentKHRor frame marker callbacks), which fell inside the active profiling window of the benchmark plugin, polluting the reported frame time. - Tight Coupling: Vulkan-specific API call processing was coupled with the high-level playback/frame-boundary lifecycle.
- Duplicated Logic: The consumer had to track whether the capture utilized frame markers (
uses_frame_markers_) and branch its reset logic accordingly.
To resolve this, we introduced a clean, unified OnFrameBegin() lifecycle hook at the decoder/consumer level, driven directly by the FileProcessor.
We added OnFrameBegin() to the base decoder and consumer interfaces:
// api_decoder.h
class ApiDecoder {
public:
virtual void OnFrameBegin() {}
// ...
};
// common_consumer_base.h
class CommonConsumerBase : public MetadataConsumerBase, public MarkerConsumerBase {
public:
virtual void OnFrameBegin() {}
// ...
};2.2 Pre-Triggering and Dispatching Flow (application.cpp, file_processor.cpp, preload_file_processor.cpp)
To guarantee that ResetLoopBoundary() is executed outside of the plugin profiling window:
Application::PlaySingleFrame()explicitly triggersfile_processor_->OnFrameBegin()before it fires the plugin'sFrameBeginevent.FileProcessor::OnFrameBegin()invokesOnFrameBegin()on all decoders.ProcessNextFrame()in bothFileProcessorandPreloadFileProcessoronly replays frame blocks and does not callOnFrameBegin()inline.- This keeps the design simple, clean, and avoids tracking state variables such as invocation flags.
// application.cpp
bool Application::PlaySingleFrame()
{
if (file_processor_)
{
file_processor_->OnFrameBegin();
if (replay_event_sink_)
{
replay_event_sink_->FrameBegin(file_processor_->GetCurrentFrameNumber());
}
success = file_processor_->ProcessNextFrame();
// ...
}
}
// file_processor.cpp
void FileProcessor::OnFrameBegin()
{
for (auto decoder : decoders_)
{
decoder->OnFrameBegin();
}
}
bool FileProcessor::ProcessNextFrame()
{
bool success = false;
if (AsyncProcessingEnabled())
{
success = ProcessNextFrameAsync();
}
else
{
success = ProcessNextFrameSync();
}
return success;
}
// preload_file_processor.cpp
bool PreloadFileProcessor::ProcessNextFrame()
{
if (!replay_from_queue_)
{
return FileProcessor::ProcessNextFrame();
}
DispatchVisitor dispatch_visitor(*this, decoders_, annotation_handler_);
preload_block_iterator_ = ReplayOneFrame(dispatch_visitor, preload_block_iterator_, BlockIterator());
const ProcessBlocksResult& result = dispatch_visitor.GetReplayResult();
HandleReplayResult(result, preload_block_iterator_);
// ...
return success;
}The Vulkan decoder intercepts OnFrameBegin() and forwards it to all active Vulkan consumers:
// vulkan_decoder_base.h
void OnFrameBegin() override
{
for (auto consumer : consumers_)
{
consumer->OnFrameBegin();
}
}VulkanReplayFrameLoopConsumer implements OnFrameBegin() to reset the loop boundary if it is on a repeated iteration of the loop:
// vulkan_replay_frame_loop_consumer.cpp
void VulkanReplayFrameLoopConsumer::OnFrameBegin()
{
if (IsLoopNotFirstIteration())
{
ResetLoopBoundary();
}
}As a result, we cleaned up vulkan_replay_frame_loop_consumer.cpp and vulkan_replay_frame_loop_consumer.h by removing:
ProcessFrameEndMarker()override.uses_frame_markers_tracking field and constructor initialization.- Loop boundary reset checks inside
Process_vkQueuePresentKHR().
We also modified ClassifyActiveCommandPools() by removing the IsLoopFirstIteration() guard from its check. Since ResetLoopBoundary() is now triggered at the start of iteration 2 (IsLoopNotFirstIteration() is true, but IsLoopFirstIteration() is false), removing this guard ensures command pools classification runs on the first boundary reset, preventing driver segmentation faults (SIGSEGV at vkEndCommandBuffer) from un-recreated loop-recorded command buffers.
- Clean Separations of Concerns: The Vulkan replay consumer does not need to know how frame boundaries are defined (markers vs present calls). It simply responds to the high-level
OnFrameBegin()notification. - Robustness: State restoration is guaranteed to happen exactly before any block of the new frame is parsed/replayed, minimizing validation layer issues and timing mismatches.
- Unified Logic: Removes branching and duplicate reset code paths in the consumer.