Skip to content

Initial frame looping#2698

Merged
antonio-lunarg merged 13 commits intoLunarG:devfrom
antonio-lunarg:antonio-frame-loop
Apr 1, 2026
Merged

Initial frame looping#2698
antonio-lunarg merged 13 commits intoLunarG:devfrom
antonio-lunarg:antonio-frame-loop

Conversation

@antonio-lunarg
Copy link
Copy Markdown
Contributor

@antonio-lunarg antonio-lunarg commented Feb 17, 2026

This PR introduces experimental frame looping for replay, so a selected frame can be replayed repeatedly for analysis/profiling.

Changes:

  • Add --loop-frame and --loop-count to replay options on desktop and Android, plus forwarding in android/scripts/gfxrecon.py.
  • Add graphics::FrameLoopInfo to track target frame, remaining iterations, and current looping state.
    -Loop orchestration lives in Application::Run() so loop behavior follows application frame numbering (including trimmed traces).
  • Use PreloadFileProcessor automatically when looping is enabled, preload the target frame, and replay it by controlling advance (SetAdvanceToNextFrame).
  • Skip state blocks during loop iterations to avoid reapplying state setup every repeat.
  • Extend VulkanFrameLoopReplayConsumer to skip recreating already-created objects while looping (instance/device/swapchain/surfaces/AHB path).
  • Default pause_frame to uint32_t max to avoid accidental pause at frame 0 when looping starts.

Notes:

  • This functionality is currently experimental.
  • Implemented in the Vulkan replay path.

@antonio-lunarg antonio-lunarg requested a review from a team as a code owner February 17, 2026 16:34
@antonio-lunarg antonio-lunarg force-pushed the antonio-frame-loop branch 5 times, most recently from f960c13 to 99e019b Compare February 18, 2026 10:41
@antonio-lunarg antonio-lunarg force-pushed the antonio-frame-loop branch 3 times, most recently from 18f0f40 to 4c702b9 Compare February 20, 2026 10:32
@ci-tester-lunarg
Copy link
Copy Markdown
Collaborator

CI gfxreconstruct build queued with queue ID 656236.

@ci-tester-lunarg
Copy link
Copy Markdown
Collaborator

CI gfxreconstruct build # 8901 running.

@ci-tester-lunarg
Copy link
Copy Markdown
Collaborator

CI gfxreconstruct build # 8901 passed.

@antonio-lunarg antonio-lunarg added the approved-to-run-ci Can run CI check on internal LunarG machines label Mar 5, 2026
@antonio-lunarg antonio-lunarg force-pushed the antonio-frame-loop branch 2 times, most recently from 0471206 to fca3ffa Compare March 9, 2026 10:44
@antonio-lunarg antonio-lunarg changed the title Experimental frame looping Initial frame looping Mar 12, 2026
@antonio-lunarg antonio-lunarg force-pushed the antonio-frame-loop branch 2 times, most recently from 603ed4f to fc6ddd4 Compare March 16, 2026 13:17
Copy link
Copy Markdown
Contributor

@jzulauf-lunarg jzulauf-lunarg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preload section looks good, but I'm going to have to integrate this support in PR #2783 ... @antonio-lunarg -- if frame looping is going to change significantly, we should probably carefully align the changes so nothing gets dropped.

error_state_ = CheckFileStatus();
return;
}
if (!advance_to_next_frame_)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels more "contractual". Thinking assert in additional to check. The frame looping support state machine only current loads a single frame and only when advance_to_next_frame_ is enabled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an assertion and integrated with more documentation on top of this function's signature.

// Quit when frame looping has finished.
if (frame_loop_info_->GetLoopIterations() == 0)
{
running_ = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this because

a) we never want to continue past looping frame or
b) because we aren't confident that replay past looping is stable? (or are sure it isn't)

--quit-after-measurement-range controls this behavior for FpsInfo.. don't know if a separate --quit-after-looping would be needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a) I think we don't really care about continuing after looping, at the moment.
b) But I think replay past looping should work.

I suppose --quit-after-looping might be added later if someone wants it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the someone adding --quit-after-looping would also wrap running_ = false; and break with e..g. if(frame_loop_info_->quit_after_looping)?

// as StartBlock does it lazily, and thus the last will be present.
// NOTE: This must be done before SetBatchSinkProc, or we'll record it to preload
block_parser_->GetBlockAllocator().FlushBatch();
block_parser_->SetOperationMode(BlockParser::OperationMode::kEnqueued);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mode means that we store the minimum needed. For uncompressed block, we store the "raw block" information and have the DispatchArgs directly reference it. However for compressed block (combined with BlockParser::DecompressionPolicy::kAlways below) we only the decompressed data and not the raw block data from the file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For frame looping, I think we only care about being able to replay blocks. For compressed blocks, I would be happy do uncompress once and use the decompressed data later again and again.


// Use kAlways decompression policy to move the maximum amount of work outside the measurement loop
auto save_decompression_policy = block_parser_->GetDecompressionPolicy();
block_parser_->SetDecompressionPolicy(BlockParser::DecompressionPolicy::kAlways);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to support frame looping BlockParser::DecompressionPolicy::kAlways isn't just a performance optimization for accurate measurement required to prevent the the issue of having dispatch_args data members pointing at the most recently decompressed data (or stale pointers) on replay.

Probably needs some commentary about this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment.

@antonio-lunarg antonio-lunarg force-pushed the antonio-frame-loop branch 3 times, most recently from 505606c to 2fc373a Compare March 19, 2026 10:34
preload_processor->SkipStateBlocks();

GFXRECON_LOG_INFO("Looping frame (%i iterations remaining)", frame_loop_info_->GetLoopIterations());
frame_loop_info_->DecrementLoopIterations();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the loop iterations abstracted behind methods as a design principle or do you have an expectation here that this may be more than just -- at some point?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can already wrap a couple of these lines within a frame_loop_info->EndFrame().
Or we might even wrap all of them by passing the file_processor as a parameter of EndFrame(..).
What do you think?

decode::FileProcessor* file_processor_; ///< The FileProcessor object responsible for decoding and processing capture file data.
bool running_; ///< Indicates that the application is actively processing system events for playback.
bool paused_; ///< Indicates that the playback has been paused. When paused the application will stop rendering, but will continue processing system events.
graphics::FrameLoopInfo* frame_loop_info_; ///< Indicates that playback wishes to loop a certain frame
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to cause this PR to be retested, so don't change it for this PR, but for changes in the future and for a possible edit to looping in a future PR, I think this would be better (since it's just a few uint32's and there are only 1 Applications) to embed frame looping info or other Application parameters directly in Application with defaults that mean "no frame looping" to avoid a potential bug in future PRs not checking "frame_loop_info != nullptr".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually have been talking with @jzulauf-lunarg about that. While FrameLoopInfo takes inspiration from FpsInfo, I noticed FpsInfo is quite useful for things related to frame-looping and I was also considering merging these two together into something like FrameInfo, focused on frames rather than the concept of frames-per-second.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created an issue for this: #2810

Copy link
Copy Markdown
Contributor

@bradgrantham-lunarg bradgrantham-lunarg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool, thank you!

if (at_end)
bool PreloadFileProcessor::AdvanceToNextFrame(ProcessBlockState process_result)
{
if (advance_to_next_frame_)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like "whether to advance to next frame" is an application preference, but it's reaching its fingers into PreloadFileProcessor to do it. That is to say, AdvanceToNextFrame and maybe "move replay cursor" primitive make sense, but what would it mean to call AdvanceToNextFrame but have advance_to_next_frame_ be false? Why not have advance_to_next_frame_ be in the caller, and then whether to call PreloadNextFrames and AdvanceToNextFrame and "move replay cursor" would be up to the caller of FilePreloadProcessor?

The precedent for this is that there is already a SkipStateBlocks that controls playback and the cursor that is a primitive an application may choose to call, and then whether to skip isn't tracked by FilePreloadProcessor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please have a look at the last two commits: I replaced the advance_to_next_frame_ mode bit with two explicit functions: ReplayCurrentPreloadedFrame() and AdvancePreloadedFrame(). When looping, Application calls only the replay function and skips the advance, keeping the cursor on the same preloaded frame for every iteration.

@antonio-lunarg antonio-lunarg force-pushed the antonio-frame-loop branch 2 times, most recently from 4160f3c to 5777fec Compare March 24, 2026 11:21
Copy link
Copy Markdown
Contributor

@MarkY-LunarG MarkY-LunarG left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Great first step!

@antonio-lunarg antonio-lunarg force-pushed the antonio-frame-loop branch 2 times, most recently from c3f7ba2 to 4eacee5 Compare April 1, 2026 15:38
antonio-lunarg and others added 13 commits April 1, 2026 18:33
Add experimental frame looping by not advancing the preloaded frame
after replaying a target frame.

- Add `--loop-frame` and `--loop-count` replay options on desktop,
  Android, and in the launcher script.
- Add `VulkanReplayFrameLoopConsumer` to enter loop mode at frame end,
  idle devices, not advancing to the next frame, and replay it.
- Extend `PreloadFileProcessor` with `SetAdvanceToNextFrame()` to skip
  advancing to the next frame.
- Allow Application to keep preloading while frame looping is active.

Also, default `pause_frame` to uint32_t max. This is needed to avoid pausing
the replay when frame looping is enabled for the first frame. In this case
the frame counter is not incremented, therefore it is still 0 when it is
checked against `pause_frame` (if default is 0) effectively matching its
value which pauses replay. Setting pause_frame default to uint32_t max
solves this edge case.

Co-authored-by: Nick Driscoll <nick@lunarg.com>
When looping a frame, some objects have already been created during the
first iteration of the frame, hence the corresponding vkCreate* commands
should be skipped.
Use 0 as first frame index and uint32_t max as default number of loop
iterations.

Co-authored-by: Antonio Caggiano <antonio@lunarg.com>
Set advance to next frame even when not at loop frame.
When looping, make sure to skip any state blocks to avoid reappling them
on each loop iteration.
Rejects empty, non-numeric, and zero values so replay settings only accept
meaningful frame targets. Returns success only after the parsed value
passes all checks, which makes invalid input handling more predictable and
avoids treating frame zero as a usable loop point.
Track the current replay frame in FrameLoopInfo and use it to drive loop-frame
preload and replay handling from Application::Run(). This removes the ad hoc
local loop state and keeps loop decisions attached to the frame-loop state
object.
Require frame-advance mode when preloading frames and document it.
Fix IsFileValid() to require both a queued preload head and replay
cursor before replaying preloaded frames.
Have ReplayOneFrame report the replay result and next cursor instead of
mutating replay_cursor_ directly. Keep cursor advancement and frame-number
updates in AdvanceToNextFrame() so preload replay state changes stay in
one place.
Replace SetAdvanceToNextFrame() mode bit with explicit
ReplayCurrentPreloadedFrame() and AdvancePreloadedFrame() primitives.
Whether to advance is now the caller's decision: Application calls
ReplayCurrentPreloadedFrame() directly when looping, and the normal
ProcessNextFrame() path calls both in sequence.

Remove the IsFileValid() override that existed solely to support the
advance_to_next_frame_=false mode.
Centralize the "infinite iterations" sentinel for clarity and
maintainability, and prevents accidental decrement of the infinite
sentinel while preserving current finite-loop behavior.
@antonio-lunarg antonio-lunarg merged commit 5cbaa50 into LunarG:dev Apr 1, 2026
7 checks passed
@antonio-lunarg antonio-lunarg deleted the antonio-frame-loop branch April 2, 2026 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved-to-run-ci Can run CI check on internal LunarG machines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants