/*
wearechat - generic multiplayer game with voip
Copyright (C) 2025 metamuffin
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3 of the License only.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
use super::{GraphicsConfig, ScenePreparer, TextureIdentityKind};
use anyhow::Result;
use image::ImageReader;
use log::debug;
use std::{io::Cursor, sync::Arc, time::Instant};
use wgpu::{
AddressMode, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindingResource,
Color, ColorTargetState, ColorWrites, CommandEncoderDescriptor, Device, Extent3d, FilterMode,
ImageDataLayout, LoadOp, Operations, Queue, RenderPassColorAttachment, RenderPassDescriptor,
RenderPipeline, SamplerDescriptor, StoreOp, Texture, TextureAspect, TextureDescriptor,
TextureDimension, TextureFormat, TextureUsages, TextureViewDescriptor, include_wgsl,
};
pub struct MipGenerationPipeline {
pipeline: RenderPipeline,
}
impl MipGenerationPipeline {
pub fn load(device: &Device, format: TextureFormat) -> Self {
let shader = device.create_shader_module(include_wgsl!("../shaders/texture_copy.wgsl"));
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("mip generator"),
layout: None,
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(ColorTargetState {
format,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
Self { pipeline }
}
}
impl ScenePreparer {
pub fn update_textures(&self, num_done: &mut usize) -> Result<()> {
for format in self.mip_generation_pipelines.needed() {
self.mip_generation_pipelines.insert(
format,
Arc::new(MipGenerationPipeline::load(&self.device, format)),
0,
);
*num_done += 1
}
for kind in self.placeholder_textures.needed() {
let (linear, color) = match kind {
TextureIdentityKind::Normal => (true, [128, 128, 255, 255]),
TextureIdentityKind::Multiply => (false, [255, 255, 255, 255]),
};
let tex_bg = create_texture(
&self.device,
&self.queue,
&self.layouts.texture,
&color,
1,
1,
if linear {
TextureFormat::Rgba8Unorm
} else {
TextureFormat::Rgba8UnormSrgb
},
None,
&self.config,
);
self.placeholder_textures.insert(kind, tex_bg, 4);
*num_done += 1;
}
for spec in self.textures.needed() {
let start = Instant::now();
let format = if spec.linear {
TextureFormat::Rgba8Unorm
} else {
TextureFormat::Rgba8UnormSrgb
};
if let Some(mipgen) = self.mip_generation_pipelines.try_get(format) {
if let Some(buf) = self.downloader.try_get(spec.data.clone())? {
let image = ImageReader::new(Cursor::new(buf.0)).with_guessed_format()?;
let image = image.decode()?;
let dims = (image.width(), image.height());
let image = image.into_rgba8();
let image = image.into_vec();
let tex_bg = create_texture(
&self.device,
&self.queue,
&self.layouts.texture,
&image,
dims.0,
dims.1,
format,
Some(&mipgen),
&self.config,
);
self.textures.insert(spec, tex_bg, image.len());
debug!(
"texture created (res={}x{}, took {:?})",
dims.0,
dims.1,
start.elapsed()
);
*num_done += 1;
}
}
}
Ok(())
}
}
fn create_texture(
device: &Device,
queue: &Queue,
bgl: &BindGroupLayout,
data: &[u8],
width: u32,
height: u32,
format: TextureFormat,
mipgen: Option<&MipGenerationPipeline>,
config: &GraphicsConfig,
) -> (Arc, Arc) {
let mip_level_count = (width.ilog2().max(4) - 3).min(config.max_mip_count);
let extent = Extent3d {
depth_or_array_layers: 1,
width,
height,
};
let texture = device.create_texture(&TextureDescriptor {
label: None,
size: extent,
mip_level_count,
sample_count: 1,
dimension: TextureDimension::D2,
format,
usage: TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_DST
| TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let textureview = texture.create_view(&TextureViewDescriptor::default());
let sampler = device.create_sampler(&SamplerDescriptor {
address_mode_u: AddressMode::Repeat,
address_mode_v: AddressMode::Repeat,
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: FilterMode::Linear,
anisotropy_clamp: config.max_anisotropy,
..Default::default()
});
let bind_group = device.create_bind_group(&BindGroupDescriptor {
label: None,
layout: &bgl,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&textureview),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&sampler),
},
],
});
let level_views = (0..mip_level_count)
.map(|mip| {
texture.create_view(&TextureViewDescriptor {
label: Some("mip generation level view"),
format: None,
dimension: None,
aspect: TextureAspect::All,
base_mip_level: mip,
mip_level_count: Some(1),
base_array_layer: 0,
array_layer_count: None,
})
})
.collect::>();
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None });
// TODO why does copy_buffer_to_texture have more restrictive alignment requirements?!
// let upload_buffer = device.create_buffer_init(&BufferInitDescriptor {
// label: Some("texture upload"),
// contents: data,
// usage: BufferUsages::COPY_DST | BufferUsages::COPY_SRC,
// });
// encoder.copy_buffer_to_texture(
// ImageCopyBuffer {
// buffer: &upload_buffer,
// layout: ImageDataLayout {
// offset: 0,
// bytes_per_row: Some(width * 4),
// rows_per_image: None,
// },
// },
// texture.as_image_copy(),
// extent,
// );
queue.write_texture(
texture.as_image_copy(),
data,
ImageDataLayout {
bytes_per_row: Some(width * 4),
rows_per_image: None,
offset: 0,
},
extent,
);
for level in 1..mip_level_count {
let mip_pipeline = &mipgen.unwrap().pipeline;
let source_view = &level_views[level as usize - 1];
let target_view = &level_views[level as usize];
let mip_bind_group = device.create_bind_group(&BindGroupDescriptor {
layout: &mip_pipeline.get_bind_group_layout(0),
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(source_view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&sampler),
},
],
label: None,
});
let mut rpass = encoder.begin_render_pass(&RenderPassDescriptor {
label: None,
color_attachments: &[Some(RenderPassColorAttachment {
view: target_view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(Color::WHITE),
store: StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
rpass.set_pipeline(&mip_pipeline);
rpass.set_bind_group(0, &mip_bind_group, &[]);
rpass.draw(0..3, 0..1);
}
queue.submit(Some(encoder.finish()));
(Arc::new(texture), Arc::new(bind_group))
}