par_term_render/graphics_renderer/
upload.rs1use crate::error::RenderError;
7use std::time::Instant;
8use wgpu::*;
9
10const MAX_TEXTURE_CACHE_SIZE: usize = 100;
13
14const MAX_TEXTURE_CACHE_BYTES: u64 = 256 * 1024 * 1024;
18
19pub(super) fn texture_bytes(info: &SixelTextureInfo) -> u64 {
21 u64::from(info.width) * u64::from(info.height) * 4
22}
23
24fn plan_lru_evictions(
29 entries: &[(u64, Instant, u64)],
30 incoming_bytes: u64,
31 total_bytes: u64,
32) -> Vec<u64> {
33 let mut order: Vec<(Instant, u64, u64)> = entries
34 .iter()
35 .map(|(id, last_used, bytes)| (*last_used, *id, *bytes))
36 .collect();
37 order.sort_unstable_by_key(|(last_used, _, _)| *last_used);
38 let mut evict = Vec::new();
39 let mut bytes = total_bytes;
40 let mut count = entries.len();
41 let mut cursor = 0;
42 while cursor < order.len()
43 && (bytes + incoming_bytes > MAX_TEXTURE_CACHE_BYTES || count >= MAX_TEXTURE_CACHE_SIZE)
44 {
45 let (_, id, entry_bytes) = order[cursor];
46 bytes -= entry_bytes;
47 count -= 1;
48 evict.push(id);
49 cursor += 1;
50 }
51 evict
52}
53
54pub(super) struct SixelTextureInfo {
56 pub(super) texture: Texture,
57 #[allow(dead_code)] view: TextureView,
59 pub(super) bind_group: BindGroup,
60 pub(super) width: u32,
61 pub(super) height: u32,
62}
63
64pub(super) struct CachedTexture {
66 pub(super) texture: SixelTextureInfo,
67 pub(super) last_used: Instant,
69}
70
71impl super::GraphicsRenderer {
72 pub fn get_or_create_texture(
82 &mut self,
83 device: &Device,
84 queue: &Queue,
85 id: u64,
86 rgba_data: &[u8],
87 width: u32,
88 height: u32,
89 ) -> Result<(), RenderError> {
90 if let Some(cached) = self.texture_cache.get_mut(&id) {
93 cached.last_used = Instant::now();
95
96 const VIRTUAL_PLACEMENT_ID_FLAG: u64 = 1u64 << 63;
104 if id & VIRTUAL_PLACEMENT_ID_FLAG != 0 {
105 return Ok(());
106 }
107
108 let expected_size = (width * height * 4) as usize;
111 if rgba_data.len() != expected_size {
112 return Err(RenderError::InvalidTextureData {
113 expected: expected_size,
114 actual: rgba_data.len(),
115 });
116 }
117
118 queue.write_texture(
120 TexelCopyTextureInfo {
121 texture: &cached.texture.texture,
122 mip_level: 0,
123 origin: Origin3d::ZERO,
124 aspect: TextureAspect::All,
125 },
126 rgba_data,
127 TexelCopyBufferLayout {
128 offset: 0,
129 bytes_per_row: Some(4 * width),
130 rows_per_image: Some(height),
131 },
132 Extent3d {
133 width,
134 height,
135 depth_or_array_layers: 1,
136 },
137 );
138
139 return Ok(());
140 }
141
142 let expected_size = (width * height * 4) as usize;
144 if rgba_data.len() != expected_size {
145 return Err(RenderError::InvalidTextureData {
146 expected: expected_size,
147 actual: rgba_data.len(),
148 });
149 }
150
151 let incoming_bytes = expected_size as u64;
154 let entries: Vec<(u64, Instant, u64)> = self
155 .texture_cache
156 .iter()
157 .map(|(id, cached)| (*id, cached.last_used, texture_bytes(&cached.texture)))
158 .collect();
159 for lru_id in plan_lru_evictions(&entries, incoming_bytes, self.texture_cache_bytes) {
160 if let Some(evicted) = self.texture_cache.remove(&lru_id) {
161 self.texture_cache_bytes -= texture_bytes(&evicted.texture);
162 log::debug!(
163 "[GRAPHICS] Evicting LRU texture: id={}, freed={} bytes, cache_bytes={}",
164 lru_id,
165 texture_bytes(&evicted.texture),
166 self.texture_cache_bytes
167 );
168 }
169 }
170
171 let texture = device.create_texture(&TextureDescriptor {
173 label: Some(&format!("Sixel Texture {}", id)),
174 size: Extent3d {
175 width,
176 height,
177 depth_or_array_layers: 1,
178 },
179 mip_level_count: 1,
180 sample_count: 1,
181 dimension: TextureDimension::D2,
182 format: TextureFormat::Rgba8Unorm,
183 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
184 view_formats: &[],
185 });
186
187 queue.write_texture(
189 TexelCopyTextureInfo {
190 texture: &texture,
191 mip_level: 0,
192 origin: Origin3d::ZERO,
193 aspect: TextureAspect::All,
194 },
195 rgba_data,
196 TexelCopyBufferLayout {
197 offset: 0,
198 bytes_per_row: Some(4 * width),
199 rows_per_image: Some(height),
200 },
201 Extent3d {
202 width,
203 height,
204 depth_or_array_layers: 1,
205 },
206 );
207
208 let view = texture.create_view(&TextureViewDescriptor::default());
209
210 let bind_group = device.create_bind_group(&BindGroupDescriptor {
212 label: Some(&format!("Sixel Bind Group {}", id)),
213 layout: &self.bind_group_layout,
214 entries: &[
215 BindGroupEntry {
216 binding: 0,
217 resource: BindingResource::TextureView(&view),
218 },
219 BindGroupEntry {
220 binding: 1,
221 resource: BindingResource::Sampler(&self.sampler),
222 },
223 ],
224 });
225
226 self.texture_cache.insert(
228 id,
229 CachedTexture {
230 texture: SixelTextureInfo {
231 texture,
232 view,
233 bind_group,
234 width,
235 height,
236 },
237 last_used: Instant::now(),
238 },
239 );
240
241 log::debug!(
242 "[GRAPHICS] Created sixel texture: id={}, size={}x{}, cache_size={}/{}",
243 id,
244 width,
245 height,
246 self.texture_cache.len(),
247 MAX_TEXTURE_CACHE_SIZE
248 );
249
250 Ok(())
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use std::time::Duration;
258
259 fn mb(n: u64) -> u64 {
260 n * 1024 * 1024
261 }
262
263 fn entry(id: u64, age_secs: u64, w: u32, h: u32) -> (u64, Instant, u64) {
264 (
265 id,
266 Instant::now() - Duration::from_secs(age_secs),
267 u64::from(w) * u64::from(h) * 4,
268 )
269 }
270
271 #[test]
272 fn evicts_oldest_until_byte_budget_fits() {
273 let entries = vec![
276 entry(1, 30, 5120, 5120),
277 entry(2, 20, 5120, 5120),
278 entry(3, 10, 5120, 5120),
279 ];
280 assert_eq!(plan_lru_evictions(&entries, mb(100), mb(300)), vec![1, 2]);
281 }
282
283 #[test]
284 fn evicts_exactly_one_for_count_cap() {
285 let entries: Vec<_> = (1..=100u64).map(|i| entry(i, 100 - i, 16, 16)).collect();
288 assert_eq!(
289 plan_lru_evictions(&entries, 16 * 16 * 4, 100 * 16 * 16 * 4),
290 vec![1]
291 );
292 }
293
294 #[test]
295 fn oversized_incoming_on_empty_cache_proceeds_unbudgeted() {
296 assert!(plan_lru_evictions(&[], mb(300), 0).is_empty());
299 }
300
301 #[test]
302 fn no_eviction_when_within_budget() {
303 let entries = vec![entry(1, 5, 100, 100), entry(2, 3, 100, 100)];
304 assert!(plan_lru_evictions(&entries, 100 * 100 * 4, 2 * 100 * 100 * 4).is_empty());
305 }
306}