Skip to content

improve: reuse GPU layer pipelines across export preview frames; coalesce export progress updates - #2482

Merged
richiemcilroy merged 14 commits into
CapSoftware:mainfrom
Wraient:improve/preview-layers-pool-and-export-progress-coalesce
Oct 24, 2026
Merged

richiemcilroy merged 14 commits into
CapSoftware:mainfrom
Wraient:improve/preview-layers-pool-and-export-progress-coalesce

Conversation

@Wraient

@Wraient Wraient commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two independent, zero-behavior-change performance wins in the GPUI app's export surface:

# Change Where Expected win
1 Reuse GPU layer pipelines across preview frames instead of rebuilding them per scrub crates/export/src/preview.rs, crates/rendering/src/lib.rs ~250–500 ms saved per scrub frame after the first
2 Coalesce export progress updates: drain to latest message, one UI update per 50 ms wake apps/desktop-gpui/src/editor_export.rs (3 drain loops) 3x fewer model updates per 50 ms wake (each avoided update skips a render-tree rebuild; render cost itself not measured)

1. Editor-owned preview layers

render_preview_frame built a fresh RendererLayers on every call, paying full shader/pipeline compilation each time (~0.25–0.5 s per the note at crates/rendering/src/lib.rs). The layers now live on the editor instance (EditorInstance, as Mutex<Option<RendererLayers>>): taken and put back around each preview render, reused across scrub frames, and freed when the editor closes. The config path still creates fresh layers per call.

Also fixed a latent bug this exposed: CursorLayer::prepare returned early without clearing bind_group when a frame had no cursor, which would have drawn a stale cursor after pooling. Now cleared on both early-return paths.

2. Coalesced progress drain

The file-export, link-export render, and link-export upload loops each did a full model update + cx.notify() (entire editor render-tree rebuild) for every progress message (~60/s), while only the latest value survives each 50 ms wake. All three now drain to the latest message and update once per wake.

Risk

Area Risk Why it's safe
Preview output None Same pipelines, built once; pixel-identical frames
Cross-project leakage None Preview layers belong to one editor instance; never shared across editors or projects
Stale per-frame state None reset_frame_state() on every take; cursor bind_group cleared when absent
Progress UI None Same latest values at the same 20 Hz cadence; no phase/completion change

Test plan

  • cargo fmt --all -- --check clean
  • Repo-wide search: no remaining references to the removed helpers or examples
  • CI suites on this head, once the runs are approved

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; all previous findings are addressed and no new blocking issues were found.

Summary

The PR keeps GPU layers on each editor for reuse across export previews and applies only the latest progress value on each wake.

  • Editor previews reuse GPU layers from frame to frame.
  • Export progress updates show the latest value from each queue drain.

Reviews (11) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." · Reviewed by Greptile

…esce export progress updates

Preview frames rebuilt RendererLayers (full shader/pipeline compilation,
~0.25-0.5s) on every scrub. Preview now takes pooled layers via the same
LAYERS_POOL mechanism the export path uses, keyed per project, returning
them after a clean render.

The three export progress drain loops issued a full model update +
cx.notify() per progress message (~60/s) while only the latest value
survives each 50ms wake. They now drain to the latest message and update
once per wake: 3x fewer render-tree rebuilds, identical visible values.

Benchmarks: crates/export/examples/preview-layers-benchmark.rs,
apps/desktop-gpui/examples/export-progress-coalesce-benchmark.rs
Comment thread crates/export/src/preview.rs Outdated
Comment on lines +292 to +294
let mut layers = layers_reuse_key
.as_deref()
.and_then(|key| cap_rendering::take_reusable_layers(&render_constants.device, key))

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.

P1 Old cursor survives preview changes

After previewing a clip with a cursor, scrubbing to a clip without cursor events can draw the old cursor. The pooled layers keep CursorLayer.bind_group, but CursorLayer.prepare returns without clearing it when interpolated_cursor is None. Previously, each preview started with fresh layers. Clear the cursor’s draw state when taking layers or before returning from that branch.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/export/src/preview.rs
Line: 292-294

Comment:
**Old cursor survives preview changes**

After previewing a clip with a cursor, scrubbing to a clip without cursor events can draw the old cursor. The pooled layers keep `CursorLayer.bind_group`, but `CursorLayer.prepare` returns without clearing it when `interpolated_cursor` is `None`. Previously, each preview started with fresh layers. Clear the cursor’s draw state when taking layers or before returning from that branch.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

println!("old updates: {old_updates}");
println!("new updates: {new_updates}");
println!(
"update reduction: {:.1}x (each avoided update skips a full render-tree rebuild)",

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.

P2 Benchmark overstates render savings

The benchmark reports fewer full editor renders, but it only counts mock_update calls. It never creates a GPUI view or calls cx.notify(), so it cannot measure that saving. Maintainers could mistake fewer field writes for fewer editor renders. Report fewer model updates instead, or measure actual renders in GPUI.

Suggested change
"update reduction: {:.1}x (each avoided update skips a full render-tree rebuild)",
"model update reduction: {:.1}x (editor renders are not measured)",
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/examples/export-progress-coalesce-benchmark.rs
Line: 122

Comment:
**Benchmark overstates render savings**

The benchmark reports fewer full editor renders, but it only counts `mock_update` calls. It never creates a GPUI view or calls `cx.notify()`, so it cannot measure that saving. Maintainers could mistake fewer field writes for fewer editor renders. Report fewer model updates instead, or measure actual renders in GPUI.

```suggestion
        "model update reduction: {:.1}x (editor renders are not measured)",
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread crates/rendering/src/lib.rs Outdated
Comment on lines +1158 to +1162
/// Take a set of GPU layer pipelines previously returned to the pool for
/// `key` on this device, with per-frame state reset. Returns `None` when the
/// pool holds nothing reusable, in which case the caller should build a fresh
/// [`RendererLayers`] and [`return_reusable_layers`] it afterwards.
///

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.

P2 New comments repeat the code

The opening documentation for take_reusable_layers narrates the function and its return type. AGENTS.md explicitly bans comments that narrate code or paraphrase parameters. Remove this routine description and keep only the non-obvious reuse and failure-state requirements. This repository requirement must be satisfied before merging.

Suggested change
/// Take a set of GPU layer pipelines previously returned to the pool for
/// `key` on this device, with per-frame state reset. Returns `None` when the
/// pool holds nothing reusable, in which case the caller should build a fresh
/// [`RendererLayers`] and [`return_reusable_layers`] it afterwards.
///

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/rendering/src/lib.rs
Line: 1158-1162

Comment:
**New comments repeat the code**

The opening documentation for `take_reusable_layers` narrates the function and its return type. `AGENTS.md` explicitly bans comments that narrate code or paraphrase parameters. Remove this routine description and keep only the non-obvious reuse and failure-state requirements. This repository requirement must be satisfied before merging.

```suggestion

```

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…onest benchmark

Greptile review follow-ups:
- CursorLayer::prepare returned early without clearing bind_group when
  interpolated_cursor was None or the cursor texture failed to load. Harmless
  with fresh layers per frame, but pooled layers would draw a stale cursor
  after scrubbing to a cursorless clip. Now cleared on both paths.
- Trimmed take_reusable_layers docs to the non-obvious pool-key invariant
  per AGENTS.md.
- Benchmark now states exactly what it measures (update invocations) vs
  what it does not (cx.notify render cost).

Copy link
Copy Markdown
Member

Please get the current head to 5/5 with Greptile before we merge. I can see the cursor-state reset fix in 89a68d6; please have Greptile re-review that change, verify cursor-present → cursor-absent preview transitions, and correct the benchmark claims so model-update counts are not presented as measured editor renders.

@Wraient

Wraient commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all three Greptile items in 89a68d6:

Cursor state (P1): verified the full transition. CursorLayer::prepare has exactly 4 early returns, and all 4 now set bind_group = None before returning (hidden cursor, no interpolated_cursor, non-finite coords, cursor texture not found). The normal path sets Some(...), and render() only issues draw calls when bind_group is Some. So after prepare, draw state always reflects the current frame: cursor-present -> cursor-absent transitions clear correctly on pooled layers. The two previously-uncleared paths were harmless with fresh layers per frame, which is why this only surfaced with pooling.

Benchmark claims (P2): the example now states exactly what it measures (update invocations, exact at 3.0x) vs what it does not (cx.notify() render cost). PR body updated the same way.

Doc comments (P2): trimmed to the non-obvious pool-key invariant per AGENTS.md.

@greptile please re-review the current head.

Comment on lines +3 to +6
//! Baseline mirrors what `render_preview_frame` did before pooling:
//! `RendererLayers::new_with_options` on every frame, paying full
//! shader/pipeline compilation each time. Improved measures the
//! `take_reusable_layers` / `return_reusable_layers` round trip used now.

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.

P2 Benchmark comments repeat the code

This header describes the old and new implementation, as does the header in apps/desktop-gpui/examples/export-progress-coalesce-benchmark.rs. AGENTS.md explicitly bans comments that narrate code or describe the change. Remove those summaries; keep useful measurement limits and setup requirements. This repository requirement must be satisfied before merging.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/export/examples/preview-layers-benchmark.rs
Line: 3-6

Comment:
**Benchmark comments repeat the code**

This header describes the old and new implementation, as does the header in `apps/desktop-gpui/examples/export-progress-coalesce-benchmark.rs`. `AGENTS.md` explicitly bans comments that narrate code or describe the change. Remove those summaries; keep useful measurement limits and setup requirements. This repository requirement must be satisfied before merging.

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@Wraient

Wraient commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Trimmed both benchmark headers to measurement-only (what it measures + how to run it), no old/new narrative, per AGENTS.md. @greptile please re-review.

@vercel

vercel Bot commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

@Wraient is attempting to deploy a commit to the Cap Software Inc Team on Vercel.

A member of the Team first needs to authorize it.

@richiemcilroy

Copy link
Copy Markdown
Member

Thanks @Wraient, the cursor reset and the coalesced progress loops look right. One thing before we can take it: render_preview_frame now hands its layers to the process-wide LAYERS_POOL (crates/export/src/preview.rs:400), and nothing removes the preview:<path> entry when the editor closes, so a full RendererLayers (frame, background and cursor textures) stays alive in the app after the editor window is gone until another project is previewed. Could you keep the reusable layers on the EditorInstance instead (for example a Mutex<Option<RendererLayers>> taken and put back around the preview render), so they are freed with the editor? Please also merge current main into the branch (it is about 1000 commits behind) so the full CI suites run on it.

…e pool

Richie's review: render_preview_frame handed layers to the process-wide
LAYERS_POOL keyed by preview:<path>, and nothing removed the entry when
the editor closed, leaking a full RendererLayers after the window was gone.

Keep the reusable layers on the EditorInstance as Mutex<Option<RendererLayers>>,
taken and put back around the preview render, so they are freed with the
editor. The config path (fresh device per call) is unaffected.
@Wraient

Wraient commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

@greptile please re-review

@Wraient

Wraient commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@greptile please re-review

Comment thread crates/export/src/preview.rs Outdated
Comment on lines +410 to +414
if let Some(cache) = layers_cache {
if let Ok(mut guard) = cache.lock() {
*guard = Some(layers);
}
}

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.

P2 Nested checks break Rust rules

The cache write-back nests two if let checks. AGENTS.md explicitly requires combining collapsible checks. Use if let Some(cache) = layers_cache && let Ok(mut guard) = cache.lock() instead. This repository requirement must be satisfied before merging.

Suggested change
if let Some(cache) = layers_cache {
if let Ok(mut guard) = cache.lock() {
*guard = Some(layers);
}
}
if let Some(cache) = layers_cache
&& let Ok(mut guard) = cache.lock()
{
*guard = Some(layers);
}

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/export/src/preview.rs
Line: 410-414

Comment:
**Nested checks break Rust rules**

The cache write-back nests two `if let` checks. `AGENTS.md` explicitly requires combining collapsible checks. Use `if let Some(cache) = layers_cache && let Ok(mut guard) = cache.lock()` instead. This repository requirement must be satisfied before merging.

```suggestion
    if let Some(cache) = layers_cache
        && let Ok(mut guard) = cache.lock()
    {
        *guard = Some(layers);
    }
```

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…e-back

Addresses Greptile P2 on the current head: AGENTS.md requires combining
collapsible checks. No behavior change.
@Wraient

Wraient commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@greptile please re-review

@Wraient

Wraient commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@greptile please re-review

@Wraient

Wraient commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@greptile please re-review

@Wraient

Wraient commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Merged current main in (e1b5ff4) and fmt is clean. The CI runs on this head are queued behind first-time-contributor approval (@richiemcilroy). Once approved, Greptile is already re-requested on the new head.

@Wraient

Wraient commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Both asks addressed on the current head:

  1. Preview layers now live on EditorInstance (Mutex<Option>, taken and put back around the preview render), so they are freed when the editor closes; the config path still creates fresh layers per call, and the process-wide pool no longer serves the editor preview path.
  2. Current main is merged in, so the full CI suites run on the merged tree.

Greptile is green on this head and CI passes. Ready for another look whenever you are.

@Wraient

Wraient commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my earlier note: the CI workflows (CI, Perf budget, A/V Sync Tests, Verify recording, Engine verify) on this head are still sitting at "action required" behind the first-time-contributor approval gate, so they have not actually run yet, I should not have said CI passes. @richiemcilroy, the runs need your approval before they go. Greptile's review check did pass on this head (16:12 UTC) with no new findings, and its open P2 (nested if-let) is addressed in 81cf678 on this head.

@richiemcilroy

Copy link
Copy Markdown
Member

Thanks @Wraient, moving the preview layers onto EditorInstance is exactly what we were after, and thanks for merging main. One cleanup left from the pool version: take_reusable_layers and return_reusable_layers in crates/rendering/src/lib.rs (around line 1355) are only used by crates/export/examples/preview-layers-benchmark.rs, and that example measures the process-wide pool that the editor preview no longer uses. Could you remove both functions (with their doc comments) and that example? Please also drop apps/desktop-gpui/examples/export-progress-coalesce-benchmark.rs, since it models the drain loop rather than measuring the app. That keeps the PR to the cursor reset, the editor-owned layers and the coalesced progress loops, and it also clears Greptile's remaining note about the benchmark headers.

take_reusable_layers and return_reusable_layers were only used by the
preview-layers benchmark, which measured the process-wide pool the
editor preview no longer uses. Drop both examples as well.
@Wraient

Wraient commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Done in 159ebdc: removed take_reusable_layers and return_reusable_layers (with their doc comments) from crates/rendering/src/lib.rs, and deleted both examples, crates/export/examples/preview-layers-benchmark.rs and apps/desktop-gpui/examples/export-progress-coalesce-benchmark.rs. The internal pool helpers the render path still uses (take_pooled_layers, prebuild_spare_layers, set_layers_reuse_key) are untouched.

Also updated the PR body: the Benchmarks section and the old pool description are gone, and section 1 now describes the editor-owned layers as they stand. cargo fmt --all -- --check is clean, and a repo-wide search shows no remaining references to the removed helpers or examples. One honest limit: I could not run cargo check locally for this push because my environment is missing the ffmpeg dev libraries its build scripts need, so the compile check lands with CI once the runs are approved.

@greptile please re-review

@Wraient

Wraient commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

@greptile please re-review

@Wraient

Wraient commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Merged current main into the branch (3e91d0c), no conflicts, and the merge does not touch the PR's preview/rendering/export files. @greptile please re-review

@Wraient

Wraient commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

@greptile please re-review

@greptile-apps

greptile-apps Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

@richiemcilroy
richiemcilroy merged commit 06ed090 into CapSoftware:main Oct 24, 2026
46 of 50 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants