1use anyhow::Result;
2use std::collections::HashMap;
3use wgpu::*;
4
5#[repr(C)]
7#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
8struct SixelInstance {
9 position: [f32; 2], tex_coords: [f32; 4], size: [f32; 2], alpha: f32, _padding: f32, }
15
16struct SixelTextureInfo {
18 #[allow(dead_code)]
19 texture: Texture,
20 #[allow(dead_code)]
21 view: TextureView,
22 bind_group: BindGroup,
23 #[allow(dead_code)]
24 width: u32,
25 #[allow(dead_code)]
26 height: u32,
27}
28
29pub struct GraphicsRenderer {
31 pipeline: RenderPipeline,
33 bind_group_layout: BindGroupLayout,
34 sampler: Sampler,
35
36 instance_buffer: Buffer,
38 instance_capacity: usize,
39
40 texture_cache: HashMap<u64, SixelTextureInfo>,
42
43 cell_width: f32,
45 cell_height: f32,
46 window_padding: f32,
47
48 #[allow(dead_code)]
50 surface_format: TextureFormat,
51}
52
53impl GraphicsRenderer {
54 pub fn new(
56 device: &Device,
57 surface_format: TextureFormat,
58 cell_width: f32,
59 cell_height: f32,
60 window_padding: f32,
61 ) -> Result<Self> {
62 let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
64 label: Some("Sixel Bind Group Layout"),
65 entries: &[
66 BindGroupLayoutEntry {
68 binding: 0,
69 visibility: ShaderStages::FRAGMENT,
70 ty: BindingType::Texture {
71 sample_type: TextureSampleType::Float { filterable: true },
72 view_dimension: TextureViewDimension::D2,
73 multisampled: false,
74 },
75 count: None,
76 },
77 BindGroupLayoutEntry {
79 binding: 1,
80 visibility: ShaderStages::FRAGMENT,
81 ty: BindingType::Sampler(SamplerBindingType::Filtering),
82 count: None,
83 },
84 ],
85 });
86
87 let sampler = device.create_sampler(&SamplerDescriptor {
89 label: Some("Sixel Sampler"),
90 address_mode_u: AddressMode::ClampToEdge,
91 address_mode_v: AddressMode::ClampToEdge,
92 address_mode_w: AddressMode::ClampToEdge,
93 mag_filter: FilterMode::Linear,
94 min_filter: FilterMode::Linear,
95 mipmap_filter: FilterMode::Nearest,
96 ..Default::default()
97 });
98
99 let pipeline = Self::create_pipeline(device, surface_format, &bind_group_layout)?;
101
102 let initial_capacity = 32;
104 let instance_buffer = device.create_buffer(&BufferDescriptor {
105 label: Some("Sixel Instance Buffer"),
106 size: (initial_capacity * std::mem::size_of::<SixelInstance>()) as u64,
107 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
108 mapped_at_creation: false,
109 });
110
111 Ok(Self {
112 pipeline,
113 bind_group_layout,
114 sampler,
115 instance_buffer,
116 instance_capacity: initial_capacity,
117 texture_cache: HashMap::new(),
118 cell_width,
119 cell_height,
120 window_padding,
121 surface_format,
122 })
123 }
124
125 fn create_pipeline(
127 device: &Device,
128 format: TextureFormat,
129 bind_group_layout: &BindGroupLayout,
130 ) -> Result<RenderPipeline> {
131 let shader = device.create_shader_module(ShaderModuleDescriptor {
132 label: Some("Sixel Shader"),
133 source: ShaderSource::Wgsl(include_str!("shaders/sixel.wgsl").into()),
134 });
135
136 let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
137 label: Some("Sixel Pipeline Layout"),
138 bind_group_layouts: &[bind_group_layout],
139 push_constant_ranges: &[],
140 });
141
142 Ok(device.create_render_pipeline(&RenderPipelineDescriptor {
143 label: Some("Sixel Pipeline"),
144 layout: Some(&pipeline_layout),
145 vertex: VertexState {
146 module: &shader,
147 entry_point: Some("vs_main"),
148 buffers: &[VertexBufferLayout {
149 array_stride: std::mem::size_of::<SixelInstance>() as u64,
150 step_mode: VertexStepMode::Instance,
151 attributes: &vertex_attr_array![
152 0 => Float32x2, 1 => Float32x4, 2 => Float32x2, 3 => Float32, ],
157 }],
158 compilation_options: Default::default(),
159 },
160 fragment: Some(FragmentState {
161 module: &shader,
162 entry_point: Some("fs_main"),
163 targets: &[Some(ColorTargetState {
164 format,
165 blend: Some(BlendState::ALPHA_BLENDING),
166 write_mask: ColorWrites::ALL,
167 })],
168 compilation_options: Default::default(),
169 }),
170 primitive: PrimitiveState {
171 topology: PrimitiveTopology::TriangleStrip,
172 ..Default::default()
173 },
174 depth_stencil: None,
175 multisample: MultisampleState::default(),
176 multiview: None,
177 cache: None,
178 }))
179 }
180
181 pub fn get_or_create_texture(
191 &mut self,
192 device: &Device,
193 queue: &Queue,
194 id: u64,
195 rgba_data: &[u8],
196 width: u32,
197 height: u32,
198 ) -> Result<()> {
199 if let Some(tex_info) = self.texture_cache.get(&id) {
202 let expected_size = (width * height * 4) as usize;
205 if rgba_data.len() != expected_size {
206 return Err(anyhow::anyhow!(
207 "Invalid RGBA data size: expected {}, got {}",
208 expected_size,
209 rgba_data.len()
210 ));
211 }
212
213 queue.write_texture(
215 TexelCopyTextureInfo {
216 texture: &tex_info.texture,
217 mip_level: 0,
218 origin: Origin3d::ZERO,
219 aspect: TextureAspect::All,
220 },
221 rgba_data,
222 TexelCopyBufferLayout {
223 offset: 0,
224 bytes_per_row: Some(4 * width),
225 rows_per_image: Some(height),
226 },
227 Extent3d {
228 width,
229 height,
230 depth_or_array_layers: 1,
231 },
232 );
233
234 return Ok(());
235 }
236
237 let expected_size = (width * height * 4) as usize;
239 if rgba_data.len() != expected_size {
240 return Err(anyhow::anyhow!(
241 "Invalid RGBA data size: expected {}, got {}",
242 expected_size,
243 rgba_data.len()
244 ));
245 }
246
247 let texture = device.create_texture(&TextureDescriptor {
249 label: Some(&format!("Sixel Texture {}", id)),
250 size: Extent3d {
251 width,
252 height,
253 depth_or_array_layers: 1,
254 },
255 mip_level_count: 1,
256 sample_count: 1,
257 dimension: TextureDimension::D2,
258 format: TextureFormat::Rgba8Unorm,
259 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
260 view_formats: &[],
261 });
262
263 queue.write_texture(
265 TexelCopyTextureInfo {
266 texture: &texture,
267 mip_level: 0,
268 origin: Origin3d::ZERO,
269 aspect: TextureAspect::All,
270 },
271 rgba_data,
272 TexelCopyBufferLayout {
273 offset: 0,
274 bytes_per_row: Some(4 * width),
275 rows_per_image: Some(height),
276 },
277 Extent3d {
278 width,
279 height,
280 depth_or_array_layers: 1,
281 },
282 );
283
284 let view = texture.create_view(&TextureViewDescriptor::default());
285
286 let bind_group = device.create_bind_group(&BindGroupDescriptor {
288 label: Some(&format!("Sixel Bind Group {}", id)),
289 layout: &self.bind_group_layout,
290 entries: &[
291 BindGroupEntry {
292 binding: 0,
293 resource: BindingResource::TextureView(&view),
294 },
295 BindGroupEntry {
296 binding: 1,
297 resource: BindingResource::Sampler(&self.sampler),
298 },
299 ],
300 });
301
302 self.texture_cache.insert(
304 id,
305 SixelTextureInfo {
306 texture,
307 view,
308 bind_group,
309 width,
310 height,
311 },
312 );
313
314 debug_log!(
315 "GRAPHICS",
316 "Created sixel texture: id={}, size={}x{}, cache_size={}",
317 id,
318 width,
319 height,
320 self.texture_cache.len()
321 );
322
323 Ok(())
324 }
325
326 pub fn render(
337 &mut self,
338 device: &Device,
339 queue: &Queue,
340 render_pass: &mut RenderPass,
341 graphics: &[(u64, isize, usize, usize, usize, f32, usize)],
342 window_width: f32,
343 window_height: f32,
344 ) -> Result<()> {
345 if graphics.is_empty() {
346 return Ok(());
347 }
348
349 let mut instances = Vec::with_capacity(graphics.len());
351 for &(id, row, col, _width_cells, _height_cells, alpha, scroll_offset_rows) in graphics {
352 if let Some(tex_info) = self.texture_cache.get(&id) {
354 let x = (self.window_padding + col as f32 * self.cell_width) / window_width;
356 let y = (self.window_padding + row as f32 * self.cell_height) / window_height;
357
358 let tex_v_start = if scroll_offset_rows > 0 && tex_info.height > 0 {
362 let pixels_scrolled = scroll_offset_rows as f32 * self.cell_height;
363 (pixels_scrolled / tex_info.height as f32).min(0.99)
364 } else {
365 0.0
366 };
367 let tex_v_height = 1.0 - tex_v_start;
368
369 let visible_height_pixels = if scroll_offset_rows > 0 {
372 (tex_info.height as f32 * tex_v_height).max(1.0)
374 } else {
375 tex_info.height as f32
376 };
377
378 let width = tex_info.width as f32 / window_width;
381 let height = visible_height_pixels / window_height;
382
383 instances.push(SixelInstance {
384 position: [x, y],
385 tex_coords: [0.0, tex_v_start, 1.0, tex_v_height], size: [width, height],
387 alpha,
388 _padding: 0.0,
389 });
390 }
391 }
392
393 if instances.is_empty() {
394 return Ok(());
395 }
396
397 debug_log!(
399 "GRAPHICS",
400 "Rendering {} sixel graphics (from {} total graphics provided)",
401 instances.len(),
402 graphics.len()
403 );
404
405 let required_capacity = instances.len();
407 if required_capacity > self.instance_capacity {
408 let new_capacity = (required_capacity * 2).max(32);
409 self.instance_buffer = device.create_buffer(&BufferDescriptor {
410 label: Some("Sixel Instance Buffer"),
411 size: (new_capacity * std::mem::size_of::<SixelInstance>()) as u64,
412 usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
413 mapped_at_creation: false,
414 });
415 self.instance_capacity = new_capacity;
416 }
417
418 queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
420
421 render_pass.set_pipeline(&self.pipeline);
423
424 render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..));
426
427 let mut instance_idx = 0u32;
429 for &(id, _, _, _, _, _, _) in graphics {
430 if let Some(tex_info) = self.texture_cache.get(&id) {
431 render_pass.set_bind_group(0, &tex_info.bind_group, &[]);
432 render_pass.draw(0..4, instance_idx..(instance_idx + 1));
433 instance_idx += 1;
434 }
435 }
436
437 Ok(())
438 }
439
440 #[allow(dead_code)]
442 pub fn remove_texture(&mut self, id: u64) {
443 self.texture_cache.remove(&id);
444 }
445
446 #[allow(dead_code)]
448 pub fn clear_cache(&mut self) {
449 self.texture_cache.clear();
450 }
451
452 #[allow(dead_code)]
454 pub fn cache_size(&self) -> usize {
455 self.texture_cache.len()
456 }
457
458 pub fn update_cell_dimensions(
460 &mut self,
461 cell_width: f32,
462 cell_height: f32,
463 window_padding: f32,
464 ) {
465 self.cell_width = cell_width;
466 self.cell_height = cell_height;
467 self.window_padding = window_padding;
468 }
469}