Skip to main content

banished_gfx/
texture.rs

1//! Texture upload helpers.
2
3use banished_formats::texture::decode_rgba;
4use banished_formats::types::ImageBuffer;
5
6/// Upload RGBA8 mip levels as an sRGB texture.
7pub fn upload_rgba(
8    device: &wgpu::Device,
9    queue: &wgpu::Queue,
10    label: &str,
11    width: u32,
12    height: u32,
13    mips: &[Vec<u8>],
14) -> wgpu::TextureView {
15    upload_rgba_as(
16        device,
17        queue,
18        label,
19        width,
20        height,
21        mips,
22        wgpu::TextureFormat::Rgba8UnormSrgb,
23    )
24}
25
26/// Upload 4-byte-per-texel mip levels in `format`.
27pub fn upload_rgba_as(
28    device: &wgpu::Device,
29    queue: &wgpu::Queue,
30    label: &str,
31    width: u32,
32    height: u32,
33    mips: &[Vec<u8>],
34    format: wgpu::TextureFormat,
35) -> wgpu::TextureView {
36    let texture = device.create_texture(&wgpu::TextureDescriptor {
37        label: Some(label),
38        size: wgpu::Extent3d {
39            width,
40            height,
41            depth_or_array_layers: 1,
42        },
43        mip_level_count: mips.len() as u32,
44        sample_count: 1,
45        dimension: wgpu::TextureDimension::D2,
46        format,
47        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
48        view_formats: &[],
49    });
50    for (level, data) in mips.iter().enumerate() {
51        let (w, h) = ((width >> level).max(1), (height >> level).max(1));
52        queue.write_texture(
53            wgpu::TexelCopyTextureInfo {
54                texture: &texture,
55                mip_level: level as u32,
56                origin: wgpu::Origin3d::ZERO,
57                aspect: wgpu::TextureAspect::All,
58            },
59            data,
60            wgpu::TexelCopyBufferLayout {
61                offset: 0,
62                bytes_per_row: Some(w * 4),
63                rows_per_image: Some(h),
64            },
65            wgpu::Extent3d {
66                width: w,
67                height: h,
68                depth_or_array_layers: 1,
69            },
70        );
71    }
72    texture.create_view(&Default::default())
73}
74
75/// Decode and upload an `ImageBuffer` with all its mip levels (sRGB).
76pub fn upload_image(
77    device: &wgpu::Device,
78    queue: &wgpu::Queue,
79    label: &str,
80    img: &ImageBuffer,
81) -> Option<wgpu::TextureView> {
82    let mips: Option<Vec<Vec<u8>>> = img.levels[..img.mip_count()]
83        .iter()
84        .enumerate()
85        .map(|(i, level)| decode_rgba(img.format, img.width >> i, img.height >> i, level))
86        .collect();
87    let mips = mips?;
88    if mips.is_empty() {
89        return None;
90    }
91    Some(upload_rgba(
92        device, queue, label, img.width, img.height, &mips,
93    ))
94}
95
96/// Decode an `ImageBuffer` into RGBA8 levels: `faces × mips` (face-major, as stored).
97pub fn decode_levels(img: &ImageBuffer) -> Option<Vec<Vec<u8>>> {
98    let mips = img.mip_count().max(1);
99    let mut out = Vec::with_capacity(img.faces() * mips);
100    for face in 0..img.faces() {
101        for mip in 0..mips {
102            let level = img.levels.get(face * mips + mip)?;
103            let (w, h) = ((img.width >> mip).max(1), (img.height >> mip).max(1));
104            out.push(decode_rgba(img.format, w, h, level)?);
105        }
106    }
107    Some(out)
108}
109
110/// Upload a 2D `ImageBuffer` (all mips) as linear RGBA8, the way the D3D9 renderer samples it.
111pub fn upload_image_linear(
112    device: &wgpu::Device,
113    queue: &wgpu::Queue,
114    label: &str,
115    img: &ImageBuffer,
116) -> Option<wgpu::TextureView> {
117    let levels = decode_levels(img)?;
118    Some(upload_rgba_as(
119        device,
120        queue,
121        label,
122        img.width,
123        img.height,
124        &levels[..img.mip_count().max(1)],
125        wgpu::TextureFormat::Rgba8Unorm,
126    ))
127}
128
129/// Upload a cube-map `ImageBuffer` (6 faces, D3D order +X −X +Y −Y +Z −Z) as a cube view.
130pub fn upload_cube(
131    device: &wgpu::Device,
132    queue: &wgpu::Queue,
133    label: &str,
134    img: &ImageBuffer,
135) -> Option<wgpu::TextureView> {
136    if img.faces() != 6 {
137        return None;
138    }
139    let levels = decode_levels(img)?;
140    let mips = img.mip_count().max(1);
141    let texture = device.create_texture(&wgpu::TextureDescriptor {
142        label: Some(label),
143        size: wgpu::Extent3d {
144            width: img.width,
145            height: img.height,
146            depth_or_array_layers: 6,
147        },
148        mip_level_count: mips as u32,
149        sample_count: 1,
150        dimension: wgpu::TextureDimension::D2,
151        format: wgpu::TextureFormat::Rgba8Unorm,
152        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
153        view_formats: &[],
154    });
155    for face in 0..6 {
156        for mip in 0..mips {
157            let (w, h) = ((img.width >> mip).max(1), (img.height >> mip).max(1));
158            queue.write_texture(
159                wgpu::TexelCopyTextureInfo {
160                    texture: &texture,
161                    mip_level: mip as u32,
162                    origin: wgpu::Origin3d {
163                        x: 0,
164                        y: 0,
165                        z: face as u32,
166                    },
167                    aspect: wgpu::TextureAspect::All,
168                },
169                &levels[face * mips + mip],
170                wgpu::TexelCopyBufferLayout {
171                    offset: 0,
172                    bytes_per_row: Some(w * 4),
173                    rows_per_image: Some(h),
174                },
175                wgpu::Extent3d {
176                    width: w,
177                    height: h,
178                    depth_or_array_layers: 1,
179                },
180            );
181        }
182    }
183    Some(texture.create_view(&wgpu::TextureViewDescriptor {
184        dimension: Some(wgpu::TextureViewDimension::Cube),
185        ..Default::default()
186    }))
187}