First commit. Version 0.1.0
This commit is contained in:
168
src/gles_render/image.rs
Normal file
168
src/gles_render/image.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
//! Raster-image draw path for [`GlesCanvas`]. Uploads the RGBA
|
||||
//! bytes as a premultiplied-alpha texture (cached by content
|
||||
//! fingerprint so repeated draws of the same buffer do not
|
||||
//! re-upload) and composites it through the texture shader,
|
||||
//! honouring the canvas' `global_alpha` via the opacity uniform.
|
||||
//!
|
||||
//! The cache is keyed by `(size, fingerprint)` where the fingerprint
|
||||
//! is a 64-bit hash sampled from the RGBA bytes. This avoids the
|
||||
//! address-reuse trap of a pointer-based key — when an `Arc<Vec<u8>>`
|
||||
//! gets dropped and the allocator hands the same heap address to a
|
||||
//! *different* buffer on the next frame, a pointer-keyed cache would
|
||||
//! serve the stale texture for the new content. Content-keying makes
|
||||
//! that impossible: identical bytes → identical key, regardless of
|
||||
//! where they live in memory.
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{ Hash, Hasher };
|
||||
|
||||
use glow::HasContext;
|
||||
|
||||
use crate::types::Rect;
|
||||
|
||||
use super::helpers::{ ortho_rect, upload_rgba_texture };
|
||||
use super::GlesCanvas;
|
||||
|
||||
/// Compute a 64-bit fingerprint of an RGBA buffer for the texture
|
||||
/// cache. Hashes the full byte slice for small buffers (icons,
|
||||
/// thumbnails — below 16 KB ≈ 64×64 RGBA), and falls back to a
|
||||
/// strided 8 × 512-byte sample for anything larger so a wallpaper
|
||||
/// blit does not pay an 8 MB hash on every frame. Both modes
|
||||
/// distinguish the chevron-icon-style cases that motivated the move
|
||||
/// to content-keying — the SVG-rasterised buffers differ across
|
||||
/// most of their interior bytes, not just at the corners.
|
||||
fn fingerprint_rgba( bytes: &[u8] ) -> u64
|
||||
{
|
||||
const FULL_HASH_THRESHOLD: usize = 16 * 1024;
|
||||
const SAMPLE_CHUNKS: usize = 8;
|
||||
const SAMPLE_CHUNK_BYTES: usize = 512;
|
||||
|
||||
let mut h = DefaultHasher::new();
|
||||
let n = bytes.len();
|
||||
n.hash( &mut h );
|
||||
if n <= FULL_HASH_THRESHOLD
|
||||
{
|
||||
bytes.hash( &mut h );
|
||||
} else {
|
||||
let stride = n / SAMPLE_CHUNKS;
|
||||
for i in 0..SAMPLE_CHUNKS
|
||||
{
|
||||
let pos = ( i * stride ).min( n - SAMPLE_CHUNK_BYTES );
|
||||
bytes[ pos..pos + SAMPLE_CHUNK_BYTES ].hash( &mut h );
|
||||
}
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
|
||||
impl GlesCanvas
|
||||
{
|
||||
/// Blit RGBA image data scaled to dest rect with opacity.
|
||||
///
|
||||
/// Defensive: rejects buffers whose declared `img_w × img_h × 4` does not
|
||||
/// match `rgba_data.len()`. The mismatch path logs a one-line warning
|
||||
/// and returns without uploading or drawing — the same boundary that
|
||||
/// the internal `upload_rgba_texture` helper enforces, raised one
|
||||
/// level so the cache key is never seeded with a bogus mapping.
|
||||
pub fn draw_image_data( &mut self, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
||||
{
|
||||
let expected = ( img_w as usize ).saturating_mul( img_h as usize ).saturating_mul( 4 );
|
||||
if img_w == 0 || img_h == 0 || rgba_data.len() != expected
|
||||
{
|
||||
eprintln!(
|
||||
"[ltk] GlesCanvas::draw_image_data: refusing draw — {}×{} declared, {} bytes provided, expected {}",
|
||||
img_w, img_h, rgba_data.len(), expected,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
self.activate_target();
|
||||
// Content-fingerprint key — see the module doc for the
|
||||
// rationale. The (w, h) prefix means a pathological pair of
|
||||
// buffers with identical bytes but different declared sizes
|
||||
// stays distinct (cannot happen for valid input, defence in
|
||||
// depth).
|
||||
let cache_key = ( img_w, img_h, fingerprint_rgba( rgba_data ) );
|
||||
|
||||
if !self.image_cache.contains_key( &cache_key )
|
||||
{
|
||||
let tex = upload_rgba_texture( &self.gl, self.version, rgba_data, img_w as i32, img_h as i32 );
|
||||
self.image_cache.insert( cache_key, ( tex, img_w, img_h ) );
|
||||
}
|
||||
|
||||
// Snap to integer pixels. With GL_LINEAR sampling, a
|
||||
// fractional `dest.x` / `dest.y` makes every fragment sample
|
||||
// at sub-texel offset — bilinear blends adjacent texels and
|
||||
// the result reads as ~1 px softer than the source. At
|
||||
// integer offset every fragment center maps to a texel
|
||||
// centre and the bilinear collapses to identity, so a 1:1
|
||||
// sampled icon renders crisp.
|
||||
let dest = Rect
|
||||
{
|
||||
x: dest.x.round(),
|
||||
y: dest.y.round(),
|
||||
width: dest.width.round(),
|
||||
height: dest.height.round(),
|
||||
};
|
||||
|
||||
if let Some( ( tex, _, _ ) ) = self.image_cache.get( &cache_key )
|
||||
{
|
||||
let mvp = ortho_rect( self.width, self.height, dest );
|
||||
let alpha = opacity * self.global_alpha;
|
||||
// SAFETY: see `primitives.rs` module doc. `*tex` is owned by
|
||||
// `self.image_cache` so it outlives the call. The image cache
|
||||
// stays valid as long as `&mut self` is held — no eviction
|
||||
// path runs concurrently with the draw.
|
||||
unsafe
|
||||
{
|
||||
self.gl.use_program( Some( self.tex_program ) );
|
||||
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_tex_mvp ), false, &mvp );
|
||||
self.gl.uniform_1_f32( Some( &self.u_tex_opacity ), alpha );
|
||||
self.gl.active_texture( glow::TEXTURE0 );
|
||||
self.gl.bind_texture( glow::TEXTURE_2D, Some( *tex ) );
|
||||
self.gl.uniform_1_i32( Some( &self.u_tex_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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw an externally-owned GL texture into `dest`.
|
||||
///
|
||||
/// The caller owns the texture and is responsible for keeping it valid
|
||||
/// for the duration of this call. No upload, no caching — used to
|
||||
/// composite content rendered by another GL producer (web engine,
|
||||
/// video decoder, …) into the LTK widget tree.
|
||||
pub fn draw_external_texture( &mut self, texture: glow::Texture, dest: Rect, opacity: f32 )
|
||||
{
|
||||
self.activate_target();
|
||||
let dest = Rect
|
||||
{
|
||||
x: dest.x.round(),
|
||||
y: dest.y.round(),
|
||||
width: dest.width.round(),
|
||||
height: dest.height.round(),
|
||||
};
|
||||
let mvp = ortho_rect( self.width, self.height, dest );
|
||||
let alpha = opacity * self.global_alpha;
|
||||
// SAFETY: caller-owned texture must outlive this call. We only
|
||||
// sample it; we never delete or reassign the GL name.
|
||||
unsafe
|
||||
{
|
||||
self.gl.use_program( Some( self.tex_program ) );
|
||||
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_tex_mvp ), false, &mvp );
|
||||
self.gl.uniform_1_f32( Some( &self.u_tex_opacity ), alpha );
|
||||
self.gl.active_texture( glow::TEXTURE0 );
|
||||
self.gl.bind_texture( glow::TEXTURE_2D, Some( texture ) );
|
||||
self.gl.uniform_1_i32( Some( &self.u_tex_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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user