ltk/gles_render/
image.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
// 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 );
		}
	}
}