First commit. Version 0.1.0
This commit is contained in:
342
src/gles_render/framebuffer.rs
Normal file
342
src/gles_render/framebuffer.rs
Normal file
@@ -0,0 +1,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.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user