wormhole/
texture.rs

1// Texture System - Loads and manages textures from JXL files
2
3use wgpu::*;
4use std::path::Path;
5use std::fs;
6
7pub struct Texture {
8    pub texture: wgpu::Texture,
9    pub view: TextureView,
10    pub sampler: Sampler,
11    pub width: u32,
12    pub height: u32,
13}
14
15impl Texture {
16    pub fn from_jxl(
17        device: &Device,
18        queue: &Queue,
19        path: &Path,
20    ) -> Result<Self, Box<dyn std::error::Error>> {
21        // Read JXL file
22        let data = fs::read(path)?;
23        
24        // Decode JXL using jxl-oxide crate (simpler API than jxl)
25        use jxl_oxide::JxlImage;
26        
27        // Open the JXL image
28        let mut image = JxlImage::builder()
29            .read(&data[..])
30            .map_err(|e| format!("Failed to read JXL header: {}", e))?;
31        
32        // Get image dimensions from header
33        let header = image.image_header();
34        let width = header.size.width as u32;
35        let height = header.size.height as u32;
36        
37        // Render the first frame - render_frame returns a Render struct
38        let render = image.render_frame(0)
39            .map_err(|e| format!("Failed to render frame: {}", e))?;
40        
41        // Get pixel data - use image_all_channels() to get interleaved data
42        let frame_buffer = render.image_all_channels();
43        let f32_pixels = frame_buffer.buf();
44        
45        // Calculate number of channels: buffer_size / (width * height)
46        // Error shows 786432 bytes = 256*256*12 = RGB f32 (3 channels * 4 bytes per f32)
47        let pixels_per_channel = (width * height) as usize;
48        let num_channels = f32_pixels.len() / pixels_per_channel;
49        
50        // Convert f32 to u8 RGBA
51        let mut rgba8_data = Vec::with_capacity(pixels_per_channel * 4);
52        
53        if num_channels == 3 {
54            // RGB - convert to RGBA by adding opaque alpha channel
55            for i in 0..pixels_per_channel {
56                rgba8_data.push((f32_pixels[i * 3].clamp(0.0, 1.0) * 255.0) as u8); // R
57                rgba8_data.push((f32_pixels[i * 3 + 1].clamp(0.0, 1.0) * 255.0) as u8); // G
58                rgba8_data.push((f32_pixels[i * 3 + 2].clamp(0.0, 1.0) * 255.0) as u8); // B
59                rgba8_data.push(255); // A (opaque)
60            }
61        } else if num_channels >= 4 {
62            // RGBA or more channels - take first 4 channels
63            for i in 0..pixels_per_channel {
64                rgba8_data.push((f32_pixels[i * num_channels].clamp(0.0, 1.0) * 255.0) as u8); // R
65                rgba8_data.push((f32_pixels[i * num_channels + 1].clamp(0.0, 1.0) * 255.0) as u8); // G
66                rgba8_data.push((f32_pixels[i * num_channels + 2].clamp(0.0, 1.0) * 255.0) as u8); // B
67                rgba8_data.push((f32_pixels[i * num_channels + 3].clamp(0.0, 1.0) * 255.0) as u8); // A
68            }
69        } else {
70            return Err(format!("Unsupported channel count: {} (expected 3 or 4)", num_channels).into());
71        }
72        
73        let pixels = rgba8_data.as_slice();
74        
75        // Create texture with decoded pixel data
76        let texture_size = Extent3d {
77            width,
78            height,
79            depth_or_array_layers: 1,
80        };
81        
82        let texture = device.create_texture(&TextureDescriptor {
83            label: Some("JXL Texture"),
84            size: texture_size,
85            mip_level_count: 1,
86            sample_count: 1,
87            dimension: TextureDimension::D2,
88            format: TextureFormat::Rgba8UnormSrgb,
89            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
90            view_formats: &[],
91        });
92        
93        queue.write_texture(
94            ImageCopyTexture {
95                texture: &texture,
96                mip_level: 0,
97                origin: Origin3d::ZERO,
98                aspect: TextureAspect::All,
99            },
100            pixels,
101            ImageDataLayout {
102                offset: 0,
103                bytes_per_row: Some(4 * width),
104                rows_per_image: Some(height),
105            },
106            texture_size,
107        );
108        
109        let view = texture.create_view(&TextureViewDescriptor::default());
110        let sampler = device.create_sampler(&SamplerDescriptor {
111            label: Some("Texture Sampler"),
112            address_mode_u: AddressMode::ClampToEdge,
113            address_mode_v: AddressMode::ClampToEdge,
114            address_mode_w: AddressMode::ClampToEdge,
115            mag_filter: FilterMode::Linear,
116            min_filter: FilterMode::Linear,
117            mipmap_filter: FilterMode::Nearest,
118            ..Default::default()
119        });
120        
121        Ok(Self {
122            texture,
123            view,
124            sampler,
125            width,
126            height,
127        })
128    }
129}