Skip to content

Conversation

@afrdbaig7
Copy link

Fixes #3611

Problem

Graphite could crash with a WGPU out-of-memory error during normal usage.

The root cause was repeated allocation of overlay textures instead of reusing existing ones.
Each frame or update created new GPU textures, causing GPU memory usage to grow continuously until WGPU failed.

This resulted in:

GPU out-of-memory crashes

Increasing memory usage over time

Unstable rendering behavior during longer sessions

Solution

Overlay textures are now cached and reused instead of being recreated repeatedly.

By reusing existing GPU textures, unnecessary allocations are avoided and GPU memory usage remains stable.
The save behavior and rendering logic remain unchanged — only texture lifecycle management is corrected.

Changes

Introduced caching for overlay textures

Prevented repeated GPU texture allocations

Ensured existing textures are reused where appropriate

Testing

Built the project locally

Reproduced the out-of-memory crash before the fix

Verified GPU memory usage remains stable after the change

Confirmed no regressions in rendering behavior

@timon-schelling
Copy link
Member

Thanks for the contribution.

I was never able to reproduce the issue, but we should do this anyway.
But I would like if TargetTexture was turned into a proper abstraction (not just making the fields pub). It should represent a texture + view that is reusable with a given size. I would suggest a ensure_size function that creates a new texture and view if the new size is != the current size. replacing the logic in render_vello_scene_to_target_texture.

@afrdbaig7 If you feel comfortable/able to do that, that would be great, otherwise I will take this over at some point.

@afrdbaig7
Copy link
Author

@timon-schelling Thanks for the clarification

I’m comfortable taking this on. I’ll refactor TargetTexture into a proper abstraction that owns the texture + view and handles resizing internally via an ensure_size method, and move the size/creation logic out of render_vello_scene_to_target_texture.

I’ll push an updated version once that refactor is done.

@afrdbaig7 afrdbaig7 force-pushed the fix-wgpu-memory-leak branch from 1beb06a to f17644e Compare January 15, 2026 05:48
self.bind_overlays_texture(texture);
}
if let Some(target_texture) = &self.overlays_target_texture {
self.overlays_texture = Some(target_texture.texture().clone());
Copy link
Member

Choose a reason for hiding this comment

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

I think it is possible to remove self.overlays_texture entirely.
You should be able to just use overlay_target_texture.view() in update bind groups directly.

Copy link
Author

@afrdbaig7 afrdbaig7 Jan 16, 2026

Choose a reason for hiding this comment

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

I tested this locally by removing self.overlays_texture and using overlay_target_texture.view() directly when updating the bind groups. Everything worked as expected in my tests, and I didn’t run into any lifetime, resizing, or rendering issues.

I’ll go ahead and push this refactor.

let texture = self.context.device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: size.x,
Copy link
Member

Choose a reason for hiding this comment

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

Why this change?

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for calling this out. This change was to avoid output: Option being None at the moment we call ensure_size(). On the first call we create a minimal 1×1 texture so ensure_size() can run and reallocate it to the requested size.
I agree this is a bit wasteful since the 1×1 texture is usually replaced immediately. I went with this approach to keep the change small and low-risk, without widening the scope of this PR.
If you’d prefer the cleaner design, I’m happy to follow up by either:
making ensure_size() handle the None case directly, or
adding TargetTexture::new(device, size) so we allocate the correct size on the first call.

Copy link
Member

@timon-schelling timon-schelling Jan 23, 2026

Choose a reason for hiding this comment

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

Yes add new for target texture creation.

} else {
pub async fn render_vello_scene_to_target_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Color, output: &mut Option<TargetTexture>) -> Result<()> {
// Initialize with a minimal texture if this is the first call
if output.is_none() {
Copy link
Member

Choose a reason for hiding this comment

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

keep using if let syntax, then you also don't need the unwrap.

Copy link
Author

Choose a reason for hiding this comment

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

Good catch! You're right, I'll refactor to use if let syntax to avoid the unwrap(). Will update shortly.

@timon-schelling timon-schelling marked this pull request as draft January 23, 2026 15:38
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.

[Bug] Crash in desktop RC2 from basic use

2 participants