@@ -13,7 +13,6 @@ use glam::UVec2;
1313use graphene_application_io:: { ApplicationIo , EditorApi , SurfaceHandle , SurfaceId } ;
1414use std:: sync:: Arc ;
1515use vello:: { AaConfig , AaSupport , RenderParams , Renderer , RendererOptions , Scene } ;
16- use wgpu:: util:: TextureBlitter ;
1716use wgpu:: { Origin3d , TextureAspect } ;
1817
1918pub use context:: Context as WgpuContext ;
@@ -48,10 +47,236 @@ pub type WgpuWindow = Arc<SurfaceHandle<WindowHandle>>;
4847pub struct Surface {
4948 pub inner : wgpu:: Surface < ' static > ,
5049 pub target_texture : Mutex < Option < TargetTexture > > ,
51- pub blitter : TextureBlitter ,
50+ pub blitter : CachedBlitter ,
5251 pub format : wgpu:: TextureFormat ,
5352}
5453
54+ /// A texture blitter that caches its bind group to avoid recreating it every frame.
55+ ///
56+ /// The standard wgpu `TextureBlitter` creates a new bind group on every `copy()` call,
57+ /// which causes excessive GPU resource allocation during viewport panning. This blitter
58+ /// maintains a persistent intermediate texture (recreated only on size change) and a cached
59+ /// bind group bound to it. Each frame, the source is copied into the persistent texture
60+ /// via `copy_texture_to_texture` (same format, no bind groups), then the cached bind group
61+ /// is used for the format-converting render pass.
62+ pub struct CachedBlitter {
63+ pipeline : wgpu:: RenderPipeline ,
64+ bind_group_layout : wgpu:: BindGroupLayout ,
65+ sampler : wgpu:: Sampler ,
66+ cache : std:: sync:: Mutex < Option < BlitCache > > ,
67+ }
68+
69+ struct BlitCache {
70+ source_texture : wgpu:: Texture ,
71+ bind_group : wgpu:: BindGroup ,
72+ size : wgpu:: Extent3d ,
73+ }
74+
75+ const BLIT_SHADER : & str = r"
76+ struct VertexOutput {
77+ @builtin(position) position: vec4<f32>,
78+ @location(0) tex_coords: vec2<f32>,
79+ }
80+
81+ @vertex
82+ fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput {
83+ var out: VertexOutput;
84+ out.tex_coords = vec2<f32>(
85+ f32((vi << 1u) & 2u),
86+ f32(vi & 2u),
87+ );
88+ out.position = vec4<f32>(out.tex_coords * 2.0 - 1.0, 0.0, 1.0);
89+ out.tex_coords.y = 1.0 - out.tex_coords.y;
90+ return out;
91+ }
92+
93+ @group(0) @binding(0)
94+ var src_texture: texture_2d<f32>;
95+ @group(0) @binding(1)
96+ var src_sampler: sampler;
97+
98+ @fragment
99+ fn fs_main(vs: VertexOutput) -> @location(0) vec4<f32> {
100+ return textureSample(src_texture, src_sampler, vs.tex_coords);
101+ }
102+ " ;
103+
104+ impl CachedBlitter {
105+ pub fn new ( device : & wgpu:: Device , format : wgpu:: TextureFormat ) -> Self {
106+ let sampler = device. create_sampler ( & wgpu:: SamplerDescriptor {
107+ label : Some ( "CachedBlitter::sampler" ) ,
108+ address_mode_u : wgpu:: AddressMode :: ClampToEdge ,
109+ address_mode_v : wgpu:: AddressMode :: ClampToEdge ,
110+ address_mode_w : wgpu:: AddressMode :: ClampToEdge ,
111+ mag_filter : wgpu:: FilterMode :: Nearest ,
112+ ..Default :: default ( )
113+ } ) ;
114+
115+ let bind_group_layout = device. create_bind_group_layout ( & wgpu:: BindGroupLayoutDescriptor {
116+ label : Some ( "CachedBlitter::bind_group_layout" ) ,
117+ entries : & [
118+ wgpu:: BindGroupLayoutEntry {
119+ binding : 0 ,
120+ visibility : wgpu:: ShaderStages :: FRAGMENT ,
121+ ty : wgpu:: BindingType :: Texture {
122+ sample_type : wgpu:: TextureSampleType :: Float { filterable : false } ,
123+ view_dimension : wgpu:: TextureViewDimension :: D2 ,
124+ multisampled : false ,
125+ } ,
126+ count : None ,
127+ } ,
128+ wgpu:: BindGroupLayoutEntry {
129+ binding : 1 ,
130+ visibility : wgpu:: ShaderStages :: FRAGMENT ,
131+ ty : wgpu:: BindingType :: Sampler ( wgpu:: SamplerBindingType :: NonFiltering ) ,
132+ count : None ,
133+ } ,
134+ ] ,
135+ } ) ;
136+
137+ let pipeline_layout = device. create_pipeline_layout ( & wgpu:: PipelineLayoutDescriptor {
138+ label : Some ( "CachedBlitter::pipeline_layout" ) ,
139+ bind_group_layouts : & [ & bind_group_layout] ,
140+ push_constant_ranges : & [ ] ,
141+ } ) ;
142+
143+ let shader = device. create_shader_module ( wgpu:: ShaderModuleDescriptor {
144+ label : Some ( "CachedBlitter::shader" ) ,
145+ source : wgpu:: ShaderSource :: Wgsl ( BLIT_SHADER . into ( ) ) ,
146+ } ) ;
147+
148+ let pipeline = device. create_render_pipeline ( & wgpu:: RenderPipelineDescriptor {
149+ label : Some ( "CachedBlitter::pipeline" ) ,
150+ layout : Some ( & pipeline_layout) ,
151+ vertex : wgpu:: VertexState {
152+ module : & shader,
153+ entry_point : Some ( "vs_main" ) ,
154+ compilation_options : wgpu:: PipelineCompilationOptions :: default ( ) ,
155+ buffers : & [ ] ,
156+ } ,
157+ primitive : wgpu:: PrimitiveState {
158+ topology : wgpu:: PrimitiveTopology :: TriangleList ,
159+ ..Default :: default ( )
160+ } ,
161+ depth_stencil : None ,
162+ multisample : wgpu:: MultisampleState :: default ( ) ,
163+ fragment : Some ( wgpu:: FragmentState {
164+ module : & shader,
165+ entry_point : Some ( "fs_main" ) ,
166+ compilation_options : wgpu:: PipelineCompilationOptions :: default ( ) ,
167+ targets : & [ Some ( wgpu:: ColorTargetState {
168+ format,
169+ blend : None ,
170+ write_mask : wgpu:: ColorWrites :: ALL ,
171+ } ) ] ,
172+ } ) ,
173+ multiview : None ,
174+ cache : None ,
175+ } ) ;
176+
177+ Self {
178+ pipeline,
179+ bind_group_layout,
180+ sampler,
181+ cache : std:: sync:: Mutex :: new ( None ) ,
182+ }
183+ }
184+
185+ /// Copies the source texture to the target with format conversion, using a cached bind group.
186+ ///
187+ /// Internally maintains a persistent intermediate texture. Each frame:
188+ /// 1. Copies `source` → intermediate via `copy_texture_to_texture` (same format, no bind groups)
189+ /// 2. Blits intermediate → `target` via a render pass with the cached bind group
190+ ///
191+ /// The bind group and intermediate texture are only recreated when the source size changes.
192+ pub fn copy (
193+ & self ,
194+ device : & wgpu:: Device ,
195+ encoder : & mut wgpu:: CommandEncoder ,
196+ source : & wgpu:: Texture ,
197+ target : & wgpu:: TextureView ,
198+ ) {
199+ let size = source. size ( ) ;
200+
201+ // Take cache out of mutex to avoid holding the lock during GPU operations
202+ let mut cache = self . cache . lock ( ) . unwrap ( ) . take ( ) ;
203+
204+ // Recreate the persistent texture and bind group if size changed
205+ if !matches ! ( & cache, Some ( c) if c. size == size) {
206+ let texture = device. create_texture ( & wgpu:: TextureDescriptor {
207+ label : Some ( "CachedBlitter::intermediate" ) ,
208+ size,
209+ mip_level_count : 1 ,
210+ sample_count : 1 ,
211+ dimension : wgpu:: TextureDimension :: D2 ,
212+ format : VELLO_SURFACE_FORMAT ,
213+ usage : wgpu:: TextureUsages :: COPY_DST | wgpu:: TextureUsages :: TEXTURE_BINDING ,
214+ view_formats : & [ ] ,
215+ } ) ;
216+ let view = texture. create_view ( & wgpu:: TextureViewDescriptor :: default ( ) ) ;
217+ let bind_group = device. create_bind_group ( & wgpu:: BindGroupDescriptor {
218+ label : Some ( "CachedBlitter::bind_group" ) ,
219+ layout : & self . bind_group_layout ,
220+ entries : & [
221+ wgpu:: BindGroupEntry {
222+ binding : 0 ,
223+ resource : wgpu:: BindingResource :: TextureView ( & view) ,
224+ } ,
225+ wgpu:: BindGroupEntry {
226+ binding : 1 ,
227+ resource : wgpu:: BindingResource :: Sampler ( & self . sampler ) ,
228+ } ,
229+ ] ,
230+ } ) ;
231+ cache = Some ( BlitCache { source_texture : texture, bind_group, size } ) ;
232+ }
233+
234+ let c = cache. as_ref ( ) . unwrap ( ) ;
235+
236+ // Copy source → persistent intermediate texture (same format, no bind group creation)
237+ encoder. copy_texture_to_texture (
238+ wgpu:: TexelCopyTextureInfoBase {
239+ texture : source,
240+ mip_level : 0 ,
241+ origin : Default :: default ( ) ,
242+ aspect : Default :: default ( ) ,
243+ } ,
244+ wgpu:: TexelCopyTextureInfoBase {
245+ texture : & c. source_texture ,
246+ mip_level : 0 ,
247+ origin : Default :: default ( ) ,
248+ aspect : Default :: default ( ) ,
249+ } ,
250+ size,
251+ ) ;
252+
253+ // Blit intermediate → target with format conversion using the cached bind group
254+ {
255+ let mut pass = encoder. begin_render_pass ( & wgpu:: RenderPassDescriptor {
256+ label : Some ( "CachedBlitter::pass" ) ,
257+ color_attachments : & [ Some ( wgpu:: RenderPassColorAttachment {
258+ view : target,
259+ depth_slice : None ,
260+ resolve_target : None ,
261+ ops : wgpu:: Operations {
262+ load : wgpu:: LoadOp :: Load ,
263+ store : wgpu:: StoreOp :: Store ,
264+ } ,
265+ } ) ] ,
266+ depth_stencil_attachment : None ,
267+ timestamp_writes : None ,
268+ occlusion_query_set : None ,
269+ } ) ;
270+ pass. set_pipeline ( & self . pipeline ) ;
271+ pass. set_bind_group ( 0 , & c. bind_group , & [ ] ) ;
272+ pass. draw ( 0 ..3 , 0 ..1 ) ;
273+ }
274+
275+ // Put cache back for next frame
276+ * self . cache . lock ( ) . unwrap ( ) = cache;
277+ }
278+ }
279+
55280#[ derive( Clone , Debug ) ]
56281pub struct TargetTexture {
57282 texture : wgpu:: Texture ,
@@ -182,7 +407,7 @@ impl WgpuExecutor {
182407 // Use the surface's preferred format (Firefox prefers Bgra8Unorm, Chrome prefers Rgba8Unorm)
183408 let surface_caps = surface. get_capabilities ( & self . context . adapter ) ;
184409 let surface_format = surface_caps. formats [ 0 ] ;
185- let blitter = TextureBlitter :: new ( & self . context . device , surface_format) ;
410+ let blitter = CachedBlitter :: new ( & self . context . device , surface_format) ;
186411 Ok ( SurfaceHandle {
187412 window_id,
188413 surface : Surface {
0 commit comments