1use anyhow::Result;
2use std::collections::HashMap;
3use wgpu::*;
4
5use crate::gpu_utils;
6use par_term_config::ImageScalingMode;
7
8#[repr(C)]
10#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
11struct SixelInstance {
12 position: [f32; 2], tex_coords: [f32; 4], size: [f32; 2], alpha: f32, _padding: f32, }
18
19struct SixelTextureInfo {
21 #[allow(dead_code)]
22 texture: Texture,
23 #[allow(dead_code)]
24 view: TextureView,
25 bind_group: BindGroup,
26 #[allow(dead_code)]
27 width: u32,
28 #[allow(dead_code)]
29 height: u32,
30}
31
32pub struct GraphicsRenderer {
34 pipeline: RenderPipeline,
36 bind_group_layout: BindGroupLayout,
37 sampler: Sampler,
38
39 instance_buffer: Buffer,
41 instance_capacity: usize,
42
43 texture_cache: HashMap<u64, SixelTextureInfo>,
45
46 cell_width: f32,
48 cell_height: f32,
49 window_padding: f32,
50 content_offset_y: f32,
52 content_offset_x: f32,
54
55 preserve_aspect_ratio: bool,
57}
58
59impl GraphicsRenderer {
60 pub fn new(
62 device: &Device,
63 surface_format: TextureFormat,
64 cell_width: f32,
65 cell_height: f32,
66 window_padding: f32,
67 scaling_mode: ImageScalingMode,
68 preserve_aspect_ratio: bool,
69 ) -> Result<Self> {
70 let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
72 label: Some("Sixel Bind Group Layout"),
73 entries: &[
74 BindGroupLayoutEntry {
76 binding: 0,
77 visibility: ShaderStages::FRAGMENT,
78 ty: BindingType::Texture {
79 sample_type: TextureSampleType::Float { filterable: true },
80 view_dimension: TextureViewDimension::D2,
81 multisampled: false,
82 },
83 count: None,
84 },
85 BindGroupLayoutEntry {
87 binding: 1,
88 visibility: ShaderStages::FRAGMENT,
89 ty: BindingType::Sampler(SamplerBindingType::Filtering),
90 count: None,
91 },
92 ],
93 });
94
95 let sampler = gpu_utils::create_sampler_with_filter(
97 device,
98 scaling_mode.to_filter_mode(),
99 Some("Sixel Sampler"),
100 );
101
102 let pipeline = Self::create_pipeline(device, surface_format, &bind_group_layout)?;
104
105 let initial_capacity = 32;
107 let instance_buffer = device.create_buffer(&BufferDescriptor {
108 label: Some("Sixel Instance Buffer"),
109 size: (initial_capacity * std::mem::size_of::<SixelInstance>()) as u64,
110 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
111 mapped_at_creation: false,
112 });
113
114 Ok(Self {
115 pipeline,
116 bind_group_layout,
117 sampler,
118 instance_buffer,
119 instance_capacity: initial_capacity,
120 texture_cache: HashMap::new(),
121 cell_width,
122 cell_height,
123 window_padding,
124 content_offset_y: 0.0,
125 content_offset_x: 0.0,
126 preserve_aspect_ratio,
127 })
128 }
129
130 fn create_pipeline(
132 device: &Device,
133 format: TextureFormat,
134 bind_group_layout: &BindGroupLayout,
135 ) -> Result<RenderPipeline> {
136 let shader = device.create_shader_module(ShaderModuleDescriptor {
137 label: Some("Sixel Shader"),
138 source: ShaderSource::Wgsl(include_str!("shaders/sixel.wgsl").into()),
139 });
140
141 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
142 label: Some("Sixel Pipeline Layout"),
143 bind_group_layouts: &[bind_group_layout],
144 push_constant_ranges: &[],
145 });
146
147 Ok(device.create_render_pipeline(&RenderPipelineDescriptor {
148 label: Some("Sixel Pipeline"),
149 layout: Some(&pipeline_layout),
150 vertex: VertexState {
151 module: &shader,
152 entry_point: Some("vs_main"),
153 buffers: &[VertexBufferLayout {
154 array_stride: std::mem::size_of::<SixelInstance>() as u64,
155 step_mode: VertexStepMode::Instance,
156 attributes: &vertex_attr_array![
157 0 => Float32x2, 1 => Float32x4, 2 => Float32x2, 3 => Float32, ],
162 }],
163 compilation_options: Default::default(),
164 },
165 fragment: Some(FragmentState {
166 module: &shader,
167 entry_point: Some("fs_main"),
168 targets: &[Some(ColorTargetState {
169 format,
170 blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
172 write_mask: ColorWrites::ALL,
173 })],
174 compilation_options: Default::default(),
175 }),
176 primitive: PrimitiveState {
177 topology: PrimitiveTopology::TriangleStrip,
178 ..Default::default()
179 },
180 depth_stencil: None,
181 multisample: MultisampleState::default(),
182 multiview: None,
183 cache: None,
184 }))
185 }
186
187 pub fn get_or_create_texture(
197 &mut self,
198 device: &Device,
199 queue: &Queue,
200 id: u64,
201 rgba_data: &[u8],
202 width: u32,
203 height: u32,
204 ) -> Result<()> {
205 if let Some(tex_info) = self.texture_cache.get(&id) {
208 let expected_size = (width * height * 4) as usize;
211 if rgba_data.len() != expected_size {
212 return Err(anyhow::anyhow!(
213 "Invalid RGBA data size: expected {}, got {}",
214 expected_size,
215 rgba_data.len()
216 ));
217 }
218
219 queue.write_texture(
221 TexelCopyTextureInfo {
222 texture: &tex_info.texture,
223 mip_level: 0,
224 origin: Origin3d::ZERO,
225 aspect: TextureAspect::All,
226 },
227 rgba_data,
228 TexelCopyBufferLayout {
229 offset: 0,
230 bytes_per_row: Some(4 * width),
231 rows_per_image: Some(height),
232 },
233 Extent3d {
234 width,
235 height,
236 depth_or_array_layers: 1,
237 },
238 );
239
240 return Ok(());
241 }
242
243 let expected_size = (width * height * 4) as usize;
245 if rgba_data.len() != expected_size {
246 return Err(anyhow::anyhow!(
247 "Invalid RGBA data size: expected {}, got {}",
248 expected_size,
249 rgba_data.len()
250 ));
251 }
252
253 let texture = device.create_texture(&TextureDescriptor {
255 label: Some(&format!("Sixel Texture {}", id)),
256 size: Extent3d {
257 width,
258 height,
259 depth_or_array_layers: 1,
260 },
261 mip_level_count: 1,
262 sample_count: 1,
263 dimension: TextureDimension::D2,
264 format: TextureFormat::Rgba8Unorm,
265 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
266 view_formats: &[],
267 });
268
269 queue.write_texture(
271 TexelCopyTextureInfo {
272 texture: &texture,
273 mip_level: 0,
274 origin: Origin3d::ZERO,
275 aspect: TextureAspect::All,
276 },
277 rgba_data,
278 TexelCopyBufferLayout {
279 offset: 0,
280 bytes_per_row: Some(4 * width),
281 rows_per_image: Some(height),
282 },
283 Extent3d {
284 width,
285 height,
286 depth_or_array_layers: 1,
287 },
288 );
289
290 let view = texture.create_view(&TextureViewDescriptor::default());
291
292 let bind_group = device.create_bind_group(&BindGroupDescriptor {
294 label: Some(&format!("Sixel Bind Group {}", id)),
295 layout: &self.bind_group_layout,
296 entries: &[
297 BindGroupEntry {
298 binding: 0,
299 resource: BindingResource::TextureView(&view),
300 },
301 BindGroupEntry {
302 binding: 1,
303 resource: BindingResource::Sampler(&self.sampler),
304 },
305 ],
306 });
307
308 self.texture_cache.insert(
310 id,
311 SixelTextureInfo {
312 texture,
313 view,
314 bind_group,
315 width,
316 height,
317 },
318 );
319
320 log::debug!(
321 "[GRAPHICS] Created sixel texture: id={}, size={}x{}, cache_size={}",
322 id,
323 width,
324 height,
325 self.texture_cache.len()
326 );
327
328 Ok(())
329 }
330
331 pub fn render(
342 &mut self,
343 device: &Device,
344 queue: &Queue,
345 render_pass: &mut RenderPass,
346 graphics: &[(u64, isize, usize, usize, usize, f32, usize)],
347 window_width: f32,
348 window_height: f32,
349 ) -> Result<()> {
350 if graphics.is_empty() {
351 return Ok(());
352 }
353
354 let mut instances = Vec::with_capacity(graphics.len());
356 for &(id, row, col, _width_cells, _height_cells, alpha, scroll_offset_rows) in graphics {
357 if let Some(tex_info) = self.texture_cache.get(&id) {
359 let adjusted_row = row + scroll_offset_rows as isize;
364 let x =
365 (self.window_padding + self.content_offset_x + col as f32 * self.cell_width)
366 / window_width;
367 let y = (self.window_padding
368 + self.content_offset_y
369 + adjusted_row as f32 * self.cell_height)
370 / window_height;
371
372 let tex_v_start = if scroll_offset_rows > 0 && tex_info.height > 0 {
376 let pixels_scrolled = scroll_offset_rows as f32 * self.cell_height;
377 (pixels_scrolled / tex_info.height as f32).min(0.99)
378 } else {
379 0.0
380 };
381 let tex_v_height = 1.0 - tex_v_start;
382
383 let (width, height) = if self.preserve_aspect_ratio {
385 let visible_height_pixels = if scroll_offset_rows > 0 {
388 (tex_info.height as f32 * tex_v_height).max(1.0)
389 } else {
390 tex_info.height as f32
391 };
392 (
393 tex_info.width as f32 / window_width,
394 visible_height_pixels / window_height,
395 )
396 } else {
397 let cell_w = _width_cells as f32 * self.cell_width / window_width;
399 let visible_cell_rows = if scroll_offset_rows > 0 {
400 (_height_cells as f32 * tex_v_height).max(0.0)
401 } else {
402 _height_cells as f32
403 };
404 let cell_h = visible_cell_rows * self.cell_height / window_height;
405 (cell_w, cell_h)
406 };
407
408 instances.push(SixelInstance {
409 position: [x, y],
410 tex_coords: [0.0, tex_v_start, 1.0, tex_v_height], size: [width, height],
412 alpha,
413 _padding: 0.0,
414 });
415 }
416 }
417
418 if instances.is_empty() {
419 return Ok(());
420 }
421
422 log::debug!(
424 "[GRAPHICS] Rendering {} sixel graphics (from {} total graphics provided)",
425 instances.len(),
426 graphics.len()
427 );
428
429 let required_capacity = instances.len();
431 if required_capacity > self.instance_capacity {
432 let new_capacity = (required_capacity * 2).max(32);
433 self.instance_buffer = device.create_buffer(&BufferDescriptor {
434 label: Some("Sixel Instance Buffer"),
435 size: (new_capacity * std::mem::size_of::<SixelInstance>()) as u64,
436 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
437 mapped_at_creation: false,
438 });
439 self.instance_capacity = new_capacity;
440 }
441
442 queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
444
445 render_pass.set_pipeline(&self.pipeline);
447
448 render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
450
451 let mut instance_idx = 0u32;
453 for &(id, _, _, _, _, _, _) in graphics {
454 if let Some(tex_info) = self.texture_cache.get(&id) {
455 render_pass.set_bind_group(0, &tex_info.bind_group, &[]);
456 render_pass.draw(0..4, instance_idx..(instance_idx + 1));
457 instance_idx += 1;
458 }
459 }
460
461 Ok(())
462 }
463
464 #[allow(dead_code)]
466 pub fn remove_texture(&mut self, id: u64) {
467 self.texture_cache.remove(&id);
468 }
469
470 #[allow(dead_code)]
472 pub fn clear_cache(&mut self) {
473 self.texture_cache.clear();
474 }
475
476 #[allow(dead_code)]
478 pub fn cache_size(&self) -> usize {
479 self.texture_cache.len()
480 }
481
482 pub fn update_cell_dimensions(
484 &mut self,
485 cell_width: f32,
486 cell_height: f32,
487 window_padding: f32,
488 ) {
489 self.cell_width = cell_width;
490 self.cell_height = cell_height;
491 self.window_padding = window_padding;
492 }
493
494 pub fn set_content_offset_y(&mut self, offset: f32) {
496 self.content_offset_y = offset;
497 }
498
499 pub fn set_content_offset_x(&mut self, offset: f32) {
501 self.content_offset_x = offset;
502 }
503
504 pub fn set_preserve_aspect_ratio(&mut self, preserve: bool) {
506 self.preserve_aspect_ratio = preserve;
507 }
508
509 pub fn update_scaling_mode(&mut self, device: &Device, scaling_mode: ImageScalingMode) {
514 self.sampler = gpu_utils::create_sampler_with_filter(
515 device,
516 scaling_mode.to_filter_mode(),
517 Some("Sixel Sampler"),
518 );
519 self.texture_cache.clear();
521 }
522}