ltk/gles_render/
surface_cache.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Pre-rendered themed surfaces. A card drawn with a Gaussian drop shadow,
//! a gradient fill and several inset shadows costs half a dozen full-quad
//! shader passes; nothing in that chrome changes between frames, so it is
//! rendered once into a texture per (size, style) and blitted afterwards.
//! The textures live in the per-context [`super::image::ImageCache`].

use glow::HasContext;

use crate::types::Rect;

use super::GlesCanvas;
use super::helpers::{ alloc_fbo_tex, ortho_rect };

/// Canvas state saved while drawing into an offscreen texture.
pub( crate ) struct OffscreenTarget
{
	fbo:          glow::Framebuffer,
	saved_fbo:    glow::Framebuffer,
	saved_tex:    glow::Texture,
	saved_size:   ( u32, u32 ),
	saved_clip:   Option<Rect>,
	saved_alpha:  f32,
	saved_layer:  bool,
}

impl GlesCanvas
{
	pub( crate ) fn cached_surface( &self, key: u64 ) -> Option<( glow::Texture, u32, u32 )>
	{
		self.image_cache.borrow_mut().surface( key )
	}

	pub( crate ) fn insert_cached_surface( &self, key: u64, tex: glow::Texture, w: u32, h: u32 )
	{
		self.image_cache.borrow_mut().insert_surface( &self.gl, key, tex, w, h );
	}

	/// Redirect every draw to a fresh transparent `w × h` texture until
	/// [`Self::end_offscreen`]. Clip, opacity and path-clip layer are
	/// suspended meanwhile and restored afterwards.
	pub( crate ) fn begin_offscreen( &mut self, w: u32, h: u32 ) -> OffscreenTarget
	{
		self.flush_clip_layer();
		// SAFETY: the context is current (canvas invariant); the FBO and
		// texture are created through `self.gl` and released in
		// `end_offscreen` / by the cache eviction.
		let ( fbo, tex ) = unsafe
		{
			let fbo = self.gl.create_framebuffer().expect( "create_framebuffer" );
			let tex = alloc_fbo_tex( &self.gl, self.version, w, h );
			self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( fbo ) );
			self.gl.framebuffer_texture_2d( glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some( tex ), 0 );
			( fbo, tex )
		};
		let target = OffscreenTarget
		{
			fbo,
			saved_fbo:   self.fbo,
			saved_tex:   self.fbo_tex,
			saved_size:  ( self.width, self.height ),
			saved_clip:  self.clip_scissor,
			saved_alpha: self.global_alpha,
			saved_layer: self.clip_layer_active,
		};
		self.fbo               = fbo;
		self.fbo_tex           = tex;
		self.width             = w;
		self.height            = h;
		self.clip_scissor      = None;
		self.global_alpha      = 1.0;
		self.clip_layer_active = false;
		self.clear();
		target
	}

	/// Restore the canvas target and hand back the offscreen texture.
	pub( crate ) fn end_offscreen( &mut self, target: OffscreenTarget ) -> glow::Texture
	{
		let tex = self.fbo_tex;
		self.fbo               = target.saved_fbo;
		self.fbo_tex           = target.saved_tex;
		self.width             = target.saved_size.0;
		self.height            = target.saved_size.1;
		self.clip_scissor      = target.saved_clip;
		self.global_alpha      = target.saved_alpha;
		self.clip_layer_active = target.saved_layer;
		// SAFETY: `target.fbo` was created in `begin_offscreen` and is no
		// longer bound after `activate_target` below.
		unsafe { self.gl.delete_framebuffer( target.fbo ); }
		self.activate_target();
		tex
	}

	/// Composite a premultiplied offscreen texture at `( x, y )` with the
	/// canvas opacity, through the same path a sub-canvas blit takes.
	pub( crate ) fn blit_texture( &mut self, tex: glow::Texture, w: u32, h: u32, x: f32, y: f32 )
	{
		let dest = Rect { x, y, width: w as f32, height: h as f32 };
		if let Some( clip ) = self.clip_scissor
		{
			let outside = dest.x >= clip.x + clip.width || dest.x + dest.width <= clip.x
				|| dest.y >= clip.y + clip.height || dest.y + dest.height <= clip.y;
			if outside { return; }
		}
		self.activate_target();
		let mvp   = ortho_rect( self.width, self.height, dest );
		let alpha = self.global_alpha;
		// SAFETY: `tex` belongs to the shared cache of this context and
		// outlives the call; the blend state is the premultiplied over
		// composite the sub-blit shader expects.
		unsafe
		{
			self.gl.use_program( Some( self.sub_blit_program ) );
			self.gl.uniform_matrix_4_f32_slice( Some( &self.u_subblit_mvp ), false, &mvp );
			self.gl.uniform_1_f32( Some( &self.u_subblit_opacity ), alpha );
			self.gl.uniform_1_f32( Some( &self.u_subblit_fade_bottom ), 0.0 );
			self.gl.uniform_1_f32( Some( &self.u_subblit_height_px ), h as f32 );
			self.gl.active_texture( glow::TEXTURE0 );
			self.gl.bind_texture( glow::TEXTURE_2D, Some( tex ) );
			self.gl.uniform_1_i32( Some( &self.u_subblit_sampler ), 0 );
			self.gl.bind_vertex_array( Some( self.quad_vao ) );
			self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
			self.gl.bind_vertex_array( None );
			self.gl.bind_texture( glow::TEXTURE_2D, None );
		}
	}
}