First commit. Version 0.1.0

This commit is contained in:
2026-05-10 09:58:23 +02:00
parent af105b7f7d
commit bbab5e238d
635 changed files with 53627 additions and 175 deletions

View 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.
}
}