1use crate::{
2 assets::upload::Asset,
3 ecs::system::Res,
4 wgpu::{backend::WGPUBackend, gpu_context::GpuContext, mipmap::MipmapGenerator},
5};
6
7pub struct TextureDescriptor {
11 pub file: Option<&'static str>,
14 pub width: u32,
16 pub height: u32,
18 pub format: wgpu::TextureFormat,
20 pub data: Option<Vec<u8>>,
22 pub generate_mips: bool,
24}
25
26impl TextureDescriptor {
27 pub fn from_file(path: &'static str) -> Self {
30 Self {
31 file: Some(path),
32 width: 0,
33 height: 0,
34 format: wgpu::TextureFormat::Rgba8UnormSrgb,
35 data: None,
36 generate_mips: false,
37 }
38 }
39
40 pub fn from_data(width: u32, height: u32, format: wgpu::TextureFormat, data: Vec<u8>) -> Self {
42 Self {
43 file: None,
44 width,
45 height,
46 format,
47 data: Some(data),
48 generate_mips: false,
49 }
50 }
51
52 pub fn with_format(mut self, format: wgpu::TextureFormat) -> Self {
53 self.format = format;
54 self
55 }
56
57 pub fn with_mips(mut self) -> Self {
58 self.generate_mips = true;
59 self
60 }
61}
62
63pub struct GPUTexture {
70 texture: wgpu::Texture,
71 view: wgpu::TextureView,
72 width: u32,
73 height: u32,
74 format: wgpu::TextureFormat,
75 ctx: GpuContext,
76}
77
78impl GPUTexture {
79 pub fn write(&self, pixels: &[u8]) {
84 write_texture_level0(self.ctx.queue(), &self.texture, 0, self.format, self.width, self.height, pixels);
85 }
86
87 pub fn width(&self) -> u32 {
88 self.width
89 }
90
91 pub fn height(&self) -> u32 {
92 self.height
93 }
94
95 pub(crate) fn view(&self) -> &wgpu::TextureView {
96 &self.view
97 }
98}
99
100pub(crate) fn write_texture_level0(
107 queue: &wgpu::Queue,
108 texture: &wgpu::Texture,
109 origin_z: u32,
110 format: wgpu::TextureFormat,
111 width: u32,
112 height: u32,
113 pixels: &[u8],
114) {
115 queue.write_texture(
116 wgpu::TexelCopyTextureInfo {
117 texture,
118 mip_level: 0,
119 origin: wgpu::Origin3d { x: 0, y: 0, z: origin_z },
120 aspect: wgpu::TextureAspect::All,
121 },
122 pixels,
123 wgpu::TexelCopyBufferLayout {
124 offset: 0,
125 bytes_per_row: Some(bytes_per_pixel(format) * width),
126 rows_per_image: Some(height),
127 },
128 wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
129 );
130}
131
132pub(crate) fn bytes_per_pixel(format: wgpu::TextureFormat) -> u32 {
134 match format {
135 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => 4,
136 wgpu::TextureFormat::Rgba16Float => 8,
137 wgpu::TextureFormat::Rgba32Float => 16,
138 other => panic!("unsupported texture format for GPUTexture: {other:?}"),
139 }
140}
141
142pub(crate) fn decode_file(path: &str, format: wgpu::TextureFormat) -> Option<(u32, u32, Vec<u8>)> {
149 let img = match image::open(path) {
150 Ok(img) => img,
151 Err(e) => {
152 tracing::error!("failed to load texture '{path}': {e}");
153 return None;
154 }
155 };
156
157 Some(match format {
158 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => {
159 let img = img.to_rgba8();
160 let (w, h) = img.dimensions();
161 (w, h, img.into_raw())
162 }
163 wgpu::TextureFormat::Rgba32Float => {
164 let img = img.to_rgba32f();
165 let (w, h) = img.dimensions();
166 let bytes = bytemuck::cast_slice(img.into_raw().as_slice()).to_vec();
167 (w, h, bytes)
168 }
169 wgpu::TextureFormat::Rgba16Float => {
170 let img = img.to_rgba32f();
171 let (w, h) = img.dimensions();
172 let bytes = img
173 .into_raw()
174 .into_iter()
175 .flat_map(|c| half::f16::from_f32(c).to_le_bytes())
176 .collect();
177 (w, h, bytes)
178 }
179 other => panic!("unsupported texture format for GPUTexture: {other:?}"),
180 })
181}
182
183impl Asset<WGPUBackend> for GPUTexture {
184 type Source = TextureDescriptor;
185 type Deps<'a> = Res<'a, MipmapGenerator>;
186
187 fn upload<'a>(
188 source: &TextureDescriptor,
189 backend: &WGPUBackend,
190 mipmap_generator: &Res<'a, MipmapGenerator>,
191 ) -> Option<Self> {
192 let (width, height, data) = if let Some(path) = source.file {
194 decode_file(path, source.format)?
195 } else if let Some(data) = &source.data {
196 (source.width, source.height, data.clone())
197 } else {
198 tracing::error!("TextureSpec has neither `file` nor `data` set");
199 return None;
200 };
201
202 let mip_count = super::mipmap::mip_count(width.max(height), source.generate_mips);
203
204 let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
205 label: None,
206 size: wgpu::Extent3d {
207 width,
208 height,
209 depth_or_array_layers: 1,
210 },
211 mip_level_count: mip_count, sample_count: 1,
213 dimension: wgpu::TextureDimension::D2,
214 format: source.format,
215 usage: super::mipmap::texture_usage(mip_count),
216 view_formats: &[],
217 });
218
219 backend.queue.write_texture(
221 wgpu::TexelCopyTextureInfo {
222 texture: &texture,
223 mip_level: 0,
224 origin: wgpu::Origin3d::default(),
225 aspect: wgpu::TextureAspect::All,
226 },
227 &data,
228 wgpu::TexelCopyBufferLayout {
229 offset: 0,
230 bytes_per_row: Some(bytes_per_pixel(source.format) * width),
231 rows_per_image: Some(height),
232 },
233 wgpu::Extent3d {
234 width,
235 height,
236 depth_or_array_layers: 1,
237 },
238 );
239
240 if mip_count > 1 {
241 mipmap_generator.generate_mips(
242 &backend.device,
243 &backend.queue,
244 &texture,
245 source.format,
246 mip_count,
247 1,
248 );
249 }
250
251 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
252 Some(Self {
253 texture,
254 view,
255 width,
256 height,
257 format: source.format,
258 ctx: GpuContext::from_backend(backend),
259 })
260 }
261}
262
263crate::wgpu::plugin_macros::mipmap_asset_plugin! {
264 TexturePlugin, GPUTexture
270}