ltk/gles_render/
framebuffer.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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! FBO / framebuffer management for [`GlesCanvas`]: sub-canvas blit,
//! main-FBO ⇄ default-framebuffer present, lazy auxiliary FBO for
//! snapshot-based effects (Overlay blend inset shadow), and the
//! externally-exposed borrowed-texture view.
//!
//! See `primitives.rs` module doc for the canvas-wide `unsafe` contract
//! shared by every block in this file. Per-block notes below only call
//! out what is specific to the operation.

use glow::HasContext;

use crate::types::Rect;

use super::helpers::{ alloc_fbo_tex, native_framebuffer_id, native_texture_id, ortho_rect };
use super::raii::{ FboBinding, ProgramBinding };
use super::{ BorrowedGlesTexture, GlesCanvas };

impl GlesCanvas
{
	pub fn blit( &mut self, src: &GlesCanvas, dest_x: i32, dest_y: i32 )
	{
		self.blit_fade_bottom( src, dest_x, dest_y, 0.0 );
	}

	/// Blit `src` into this canvas at `( dest_x, dest_y )`, optionally feathering
	/// the last `fade_bottom_px` source rows so the bottom edge dissolves into
	/// transparency instead of cutting off cleanly. Used by viewports whose
	/// bottom edge is the leading edge of a slide-down animation, where a hard
	/// cut against the underlying layer reads as a knife. With `fade_bottom_px
	/// == 0.0` this matches [`Self::blit`] exactly.
	pub fn blit_fade_bottom( &mut self, src: &GlesCanvas, dest_x: i32, dest_y: i32, fade_bottom_px: f32 )
	{
		self.activate_target();
		let dest = Rect
		{
			x:      dest_x as f32,
			y:      dest_y as f32,
			width:  src.width  as f32,
			height: src.height as f32,
		};
		let mvp        = ortho_rect( self.width, self.height, dest );
		let alpha      = self.global_alpha;
		let height_px  = src.height as f32;
		let fade_clamp = fade_bottom_px.max( 0.0 ).min( height_px );
		// SAFETY: `src.fbo_tex` is owned by `src` (a `&GlesCanvas` argument)
		// and outlives the call. `src` and `self` share the same `Arc<glow::Context>`
		// — verified by construction (sub-canvases are built via `sub_canvas`,
		// which clones `Arc::clone(&self.gl)`) — so sampling `src`'s texture
		// from `self`'s FBO is well-defined.
		unsafe
		{
			// Both the main canvas and the sub-canvas FBO hold premultiplied
			// colour, and the global blend is `(ONE, ONE_MINUS_SRC_ALPHA)` —
			// the premul over-composite formula this blit needs. No temporary
			// blend switch necessary.
			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 ), fade_clamp );
			self.gl.uniform_1_f32( Some( &self.u_subblit_height_px ), height_px );
			self.gl.active_texture( glow::TEXTURE0 );
			self.gl.bind_texture( glow::TEXTURE_2D, Some( src.fbo_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 );
		}
	}

	/// Re-bind our FBO + viewport + scissor as the active GL state. Cheap and
	/// idempotent, called at the top of every draw / clear / clip method so
	/// that switching between canvases (e.g. main → sub-canvas → main) leaves
	/// each one's state correct without explicit "make active" calls.
	///
	/// Why this exists: GL state (FBO binding, scissor box, viewport) is
	/// global — there is no implicit per-canvas state. When rendering
	/// switches between targets, every method on the active canvas must
	/// reassert its own FBO + viewport, plus re-enable its own scissor (or
	/// disable scissor when the canvas has no clip).
	pub( super ) fn activate_target( &self )
	{
		// SAFETY: rebinds canvas-owned FBO + viewport + scissor. All values
		// (`self.fbo`, `self.width`, `self.height`, `self.clip_scissor`)
		// live as long as `&self`, and the bind is idempotent.
		unsafe
		{
			self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( self.fbo ) );
			self.gl.viewport( 0, 0, self.width as i32, self.height as i32 );
			match self.clip_scissor
			{
				Some( r ) =>
				{
					let ( x, y, w, h ) = self.scissor_pixels( r );
					self.gl.enable( glow::SCISSOR_TEST );
					self.gl.scissor( x, y, w, h );
				}
				None =>
				{
					self.gl.disable( glow::SCISSOR_TEST );
				}
			}
		}
	}

	/// Return a borrowed descriptor for the FBO color texture
	/// containing the latest rendered pixels.
	///
	/// `y_inverted` is `true`: the FBO uses GL's native lower-left
	/// origin, so row 0 in texture memory is the bottom of the
	/// rendered image. Consumers that follow the same convention flip
	/// during sampling when this flag is set, producing a correctly-
	/// oriented result. The CPU-side counterpart
	/// [`Self::read_rgba_pixels`] does the same flip inline so the
	/// byte buffer is top-down.
	pub fn borrowed_texture( &self ) -> BorrowedGlesTexture
	{
		BorrowedGlesTexture
		{
			texture_id:     native_texture_id( self.fbo_tex ),
			framebuffer_id: native_framebuffer_id( self.fbo ),
			texture:        self.fbo_tex,
			framebuffer:    self.fbo,
			width:          self.width,
			height:         self.height,
			premultiplied:  true,
			y_inverted:     true,
		}
	}

	/// Read the FBO color attachment into `out` as tightly packed RGBA8,
	/// top-left row first.
	///
	/// This is a compatibility escape hatch. It forces a GPU→CPU sync and
	/// should not be used in steady-state hot paths.
	pub fn read_rgba_pixels( &self, out: &mut [u8] ) -> Result<(), String>
	{
		let needed = self.width as usize * self.height as usize * 4;
		if out.len() < needed
		{
			return Err( format!(
				"read_rgba_pixels needs {needed} bytes, got {}",
				out.len(),
			) );
		}

		let mut raw = vec![ 0_u8; needed ];
		// SAFETY: `raw.len() == needed == width * height * 4` and PACK_ALIGNMENT
		// is set to 1, so `read_pixels` writes exactly `needed` bytes into a
		// buffer of exactly that size. `RGBA + UNSIGNED_BYTE` is the only
		// guaranteed-readable format on every GLES2/3 driver.
		unsafe
		{
			self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( self.fbo ) );
			self.gl.pixel_store_i32( glow::PACK_ALIGNMENT, 1 );
			self.gl.read_pixels(
				0,
				0,
				self.width as i32,
				self.height as i32,
				glow::RGBA,
				glow::UNSIGNED_BYTE,
				glow::PixelPackData::Slice( Some( &mut raw ) ),
			);
		}
		let stride = self.width as usize * 4;
		for y in 0..self.height as usize
		{
			let src = ( self.height as usize - 1 - y ) * stride;
			let dst = y * stride;
			out[ dst..dst + stride ].copy_from_slice( &raw[ src..src + stride ] );
		}
		Ok( () )
	}

	/// Lazily allocate the auxiliary FBO+texture pair used as a snapshot of
	/// `fbo` for framebuffer-fetch-style effects (Overlay blend,
	/// backdrop-blur source). The pair is sized to match the canvas so
	/// `gl_FragCoord.xy / canvas_size` samples the right texel.
	///
	/// Returns the texture handle of `aux_a`. The FBO is only needed for
	/// the backdrop blur passes that write into `aux_b`; Overlay only
	/// reads from `aux_a`, so this helper keeps the blur-only `aux_b`
	/// allocation deferred until it is actually needed.
	fn ensure_aux_a( &mut self ) -> glow::Texture
	{
		if self.aux_a.is_none()
		{
			// SAFETY: `alloc_fbo_tex` is `unsafe fn`; its requirement (current
			// GL context) holds. Same FBO build / completeness assertion as
			// `setup.rs::new`. We deliberately leave `aux_a`'s FBO as the live
			// binding — the next draw goes through `activate_target` which
			// rebinds `self.fbo`.
			unsafe
			{
				let fbo = self.gl.create_framebuffer().expect( "aux_a FBO" );
				let tex = alloc_fbo_tex( &self.gl, self.version, self.width, self.height );
				self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( fbo ) );
				self.gl.framebuffer_texture_2d
				(
					glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0,
					glow::TEXTURE_2D, Some( tex ), 0,
				);
				let status = self.gl.check_framebuffer_status( glow::FRAMEBUFFER );
				assert_eq!( status, glow::FRAMEBUFFER_COMPLETE, "aux_a FBO incomplete: 0x{status:x}" );
				self.aux_a = Some( ( fbo, tex ) );
			}
		}
		self.aux_a.expect( "just allocated" ).1
	}

	/// Snapshot variant that additionally clamps `region` to the active
	/// scissor. Safe only for shaders that sample the snapshot at
	/// exactly one point per fragment.
	pub( super ) fn snapshot_fbo_region_tight( &mut self, region: Rect )
	{
		self.snapshot_fbo_region_impl( region, true )
	}

	fn snapshot_fbo_region_impl( &mut self, region: Rect, intersect_scissor: bool )
	{
		let aux_tex = self.ensure_aux_a();
		// Clamp `region` to canvas bounds. `copy_tex_sub_image_2d` would
		// generate `INVALID_VALUE` (or undefined behaviour on some
		// drivers) if the source rect extends outside the framebuffer.
		let cw = self.width  as f32;
		let ch = self.height as f32;
		let mut x0 = region.x.max( 0.0 );
		let mut y0_top = region.y.max( 0.0 );
		let mut x1 = ( region.x + region.width  ).min( cw );
		let mut y1_top = ( region.y + region.height ).min( ch );
		if intersect_scissor
		{
			if let Some( clip ) = self.clip_scissor
			{
				x0     = x0.max( clip.x );
				y0_top = y0_top.max( clip.y );
				x1     = x1.min( clip.x + clip.width );
				y1_top = y1_top.min( clip.y + clip.height );
			}
		}
		let w = ( x1 - x0 ).floor() as i32;
		let h = ( y1_top - y0_top ).floor() as i32;
		if w <= 0 || h <= 0 { return; }
		// GL framebuffer origin is bottom-left; our rect is top-left.
		let src_x = x0.floor() as i32;
		let src_y = self.height as i32 - y0_top.floor() as i32 - h;
		// SAFETY: the four bounds checks above guarantee `(src_x, src_y, w, h)`
		// lies fully inside `self.fbo`'s colour attachment, so
		// `copy_tex_sub_image_2d` will not raise INVALID_VALUE. `aux_tex` was
		// allocated through `ensure_aux_a` to canvas dimensions, so the
		// destination region is also in-bounds.
		unsafe
		{
			self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( self.fbo ) );
			self.gl.bind_texture( glow::TEXTURE_2D, Some( aux_tex ) );
			self.gl.copy_tex_sub_image_2d
			(
				glow::TEXTURE_2D, 0,
				src_x, src_y,
				src_x, src_y,
				w, h,
			);
			self.gl.bind_texture( glow::TEXTURE_2D, None );
		}
	}

	/// Drop both auxiliary FBO+texture pairs if allocated. Called from
	/// [`Self::resize`] so the next effect re-allocates at the new size.
	pub( super ) fn invalidate_aux( &mut self )
	{
		// SAFETY: each (fbo, tex) pair was created through `self.gl` in
		// `ensure_aux_a` / `ensure_aux_b`, so deleting through the same
		// context is well-defined. `take()` ensures we never double-delete.
		unsafe
		{
			if let Some( ( fbo, tex ) ) = self.aux_a.take()
			{
				self.gl.delete_framebuffer( fbo );
				self.gl.delete_texture( tex );
			}
			if let Some( ( fbo, tex ) ) = self.aux_b.take()
			{
				self.gl.delete_framebuffer( fbo );
				self.gl.delete_texture( tex );
			}
		}
	}

	/// Blit the FBO color attachment onto the default framebuffer (the EGL
	/// window). Caller is responsible for the `eglSwapBuffers` that
	/// publishes the result. After present, the FBO is rebound so the next
	/// frame's draws keep accumulating into the shadow canvas.
	///
	/// The blit always covers the full surface — partial-redraw still saves
	/// work upstream (only changed widget pixels are repainted into the
	/// FBO), but the FBO→FB0 transfer itself is a single cheap fullscreen
	/// op.
	pub fn present( &mut self )
	{
		// Scoped guards: bind the default framebuffer (id 0) and the
		// blit program for the duration of this fn. On Drop they restore
		// `self.fbo` and "no program", so any future early-return / panic
		// in the blit body cannot leave the canvas pointing at the wrong
		// FBO or program. The viewport, blend and scissor are restored
		// inline below — they are non-resource state that does not need
		// the guard treatment because `activate_target` rewrites them on
		// every subsequent draw.
		//
		// SAFETY: GL context is current (canvas invariant). `self.fbo` is
		// the canvas-owned FBO from `setup.rs::new`. `self.blit_program`
		// was linked in `setup.rs::new`. Restoring "no program active"
		// (`None`) is always sound.
		let _fbo  = unsafe { FboBinding::scoped( &self.gl, None, Some( self.fbo ) ) };
		let _prog = unsafe { ProgramBinding::scoped( &self.gl, Some( self.blit_program ), None ) };
		// SAFETY: see above. The block sets up the blit pipeline state,
		// draws a fullscreen quad sampling `fbo_tex`, then restores blend
		// and viewport so the next frame's draws inherit the canvas-wide
		// defaults. FBO + program are restored by the guards on scope exit.
		unsafe
		{
			self.gl.viewport( 0, 0, self.width as i32, self.height as i32 );
			self.gl.disable( glow::SCISSOR_TEST );
			self.gl.disable( glow::BLEND );
			self.gl.active_texture( glow::TEXTURE0 );
			self.gl.bind_texture( glow::TEXTURE_2D, Some( self.fbo_tex ) );
			self.gl.uniform_1_i32( Some( &self.u_blit_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 );
			self.gl.enable( glow::BLEND );
			self.gl.viewport( 0, 0, self.width as i32, self.height as i32 );
		}
		// Scissor was disabled above; reflect that in our cached state.
		self.clip_scissor = None;
		// Guards drop here: FBO → self.fbo, program → None.
	}
}