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