1use crate::{
2 assets::{plugin::AssetPlugin, singleton_asset::LazyResourcePlugin, upload::Asset},
3 ecs::{plugin::Plugin, system::Res},
4 wgpu::{backend::WGPUBackend, mipmap::MipmapGenerator},
5};
6
7pub struct TextureDescriptor {
8 pub file: Option<&'static str>,
9 pub width: u32,
10 pub height: u32,
11 pub format: wgpu::TextureFormat,
12 pub data: Option<Vec<u8>>,
13 pub generate_mips: bool,
14}
15
16impl TextureDescriptor {
17 pub fn with_format(mut self, format: wgpu::TextureFormat) -> Self {
18 self.format = format;
19 self
20 }
21}
22
23pub struct GPUTexture {
24 pub texture: wgpu::Texture,
25 pub view: wgpu::TextureView,
26}
27
28pub(crate) fn bytes_per_pixel(format: wgpu::TextureFormat) -> u32 {
30 match format {
31 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => 4,
32 wgpu::TextureFormat::Rgba16Float => 8,
33 wgpu::TextureFormat::Rgba32Float => 16,
34 other => panic!("unsupported texture format for GPUTexture: {other:?}"),
35 }
36}
37
38pub(crate) fn decode_file(path: &str, format: wgpu::TextureFormat) -> (u32, u32, Vec<u8>) {
45 let img = image::open(path).unwrap_or_else(|e| panic!("failed to load texture '{path}': {e}"));
46
47 match format {
48 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => {
49 let img = img.to_rgba8();
50 let (w, h) = img.dimensions();
51 (w, h, img.into_raw())
52 }
53 wgpu::TextureFormat::Rgba32Float => {
54 let img = img.to_rgba32f();
55 let (w, h) = img.dimensions();
56 let bytes = bytemuck::cast_slice(img.into_raw().as_slice()).to_vec();
57 (w, h, bytes)
58 }
59 wgpu::TextureFormat::Rgba16Float => {
60 let img = img.to_rgba32f();
61 let (w, h) = img.dimensions();
62 let bytes = img
63 .into_raw()
64 .into_iter()
65 .flat_map(|c| half::f16::from_f32(c).to_le_bytes())
66 .collect();
67 (w, h, bytes)
68 }
69 other => panic!("unsupported texture format for GPUTexture: {other:?}"),
70 }
71}
72
73impl Asset<WGPUBackend> for GPUTexture {
74 type Source = TextureDescriptor;
75 type Deps<'a> = Res<'a, MipmapGenerator>;
76
77 fn upload<'a>(
78 source: &TextureDescriptor,
79 backend: &WGPUBackend,
80 mipmap_generator: &Res<'a, MipmapGenerator>,
81 ) -> Option<Self> {
82 let (width, height, data) = if let Some(path) = source.file {
84 decode_file(path, source.format)
85 } else if let Some(data) = &source.data {
86 (source.width, source.height, data.clone())
87 } else {
88 tracing::error!("TextureSpec has neither `file` nor `data` set");
89 return None;
90 };
91
92 let mip_count = if source.generate_mips {
93 (width.max(height) as f32).log2().floor() as u32 + 1
94 } else {
95 1
96 };
97
98 let texture = backend.device.create_texture(&wgpu::TextureDescriptor {
99 label: None,
100 size: wgpu::Extent3d {
101 width,
102 height,
103 depth_or_array_layers: 1,
104 },
105 mip_level_count: mip_count, sample_count: 1,
107 dimension: wgpu::TextureDimension::D2,
108 format: source.format,
109 usage: if mip_count > 1 {
110 wgpu::TextureUsages::TEXTURE_BINDING
113 | wgpu::TextureUsages::COPY_DST
114 | wgpu::TextureUsages::RENDER_ATTACHMENT
115 } else {
116 wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST
117 },
118 view_formats: &[],
119 });
120
121 backend.queue.write_texture(
123 wgpu::TexelCopyTextureInfo {
124 texture: &texture,
125 mip_level: 0,
126 origin: wgpu::Origin3d::default(),
127 aspect: wgpu::TextureAspect::All,
128 },
129 &data,
130 wgpu::TexelCopyBufferLayout {
131 offset: 0,
132 bytes_per_row: Some(bytes_per_pixel(source.format) * width),
133 rows_per_image: Some(height),
134 },
135 wgpu::Extent3d {
136 width,
137 height,
138 depth_or_array_layers: 1,
139 },
140 );
141
142 if mip_count > 1 {
143 mipmap_generator.generate_mips(
144 &backend.device,
145 &backend.queue,
146 &texture,
147 source.format,
148 mip_count,
149 1,
150 );
151 }
152
153 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
154 Some(Self { texture, view })
155 }
156}
157
158pub struct TexturePlugin;
159impl Plugin for TexturePlugin {
160 fn build(&self, app: &mut crate::prelude::App) {
161 app.add_plugin(LazyResourcePlugin::<WGPUBackend, MipmapGenerator>::new());
162 app.add_plugin(AssetPlugin::<WGPUBackend, GPUTexture>::new());
163 }
164}