use super::DEPTH_PICK_SAMPLE_SIZE;
use super::passes;
use crate::ecs::generational_registry::*;
use crate::ecs::loading::{
loading_pipeline_is_active, loading_pipeline_mark_completed, loading_pipeline_pop,
loading_pipeline_release_recipe_sources, loading_pipeline_reset_source_arena,
loading_pipeline_store_decoded,
};
use crate::render::wgpu::rendergraph;
#[cfg(all(not(target_arch = "wasm32"), feature = "screenshot"))]
use crate::render::wgpu::rendergraph::render_graph_get_texture;
use crate::render::wgpu::rendergraph::{
render_graph_add_pass, render_graph_compile, render_graph_execute, render_graph_get_pass_mut,
render_graph_get_texture_view, render_graph_restore_pass_enabled_states,
render_graph_save_pass_enabled_states, render_graph_set_external_texture,
render_graph_set_pass_enabled,
};
fn fit_constrained_aspect(tile_width: u32, tile_height: u32, aspect: f32) -> (u32, u32) {
let aspect = aspect.max(0.0001);
let tile_aspect = tile_width as f32 / tile_height.max(1) as f32;
if tile_aspect > aspect {
let width = (tile_height as f32 * aspect).round() as u32;
(width.max(1), tile_height.max(1))
} else {
let height = (tile_width as f32 / aspect).round() as u32;
(tile_width.max(1), height.max(1))
}
}
struct WindowDispatchContext<'a> {
cameras: Vec<freecs::Entity>,
target_size: (u32, u32),
dirty_world_spheres: &'a [(nalgebra_glm::Vec3, f32)],
frame_settings_version: u64,
original_active_camera: Option<freecs::Entity>,
focus_policy: crate::ecs::graphics::resources::ViewportFocusPolicy,
mouse_position: nalgebra_glm::Vec2,
global_dirty_signal: bool,
}
impl super::WgpuRenderer {
pub fn render_frame(
&mut self,
world: &mut crate::ecs::world::World,
) -> Result<(), Box<dyn std::error::Error>> {
let _span = tracing::info_span!("render_frame").entered();
self.frame_index = self.frame_index.wrapping_add(1);
let current_settings_signature = world.resources.graphics.settings_signature();
if self.last_settings_signature != Some(current_settings_signature) {
world.resources.graphics.settings_version =
world.resources.graphics.settings_version.wrapping_add(1);
self.last_settings_signature = Some(current_settings_signature);
}
let requested_anisotropy = world.resources.graphics.material_anisotropy_filtering;
if self
.material_texture_arrays
.set_anisotropy(&self.device, requested_anisotropy)
{
let srgb_view = self.material_texture_arrays.srgb_view().clone();
let linear_view = self.material_texture_arrays.linear_view().clone();
let sampler = self.material_texture_arrays.sampler().clone();
if let Some(pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
&& let Some(mesh_pass) =
(pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
{
mesh_pass.set_material_array_views(
&self.device,
srgb_view.clone(),
linear_view.clone(),
sampler.clone(),
);
}
if let Some(pass) = render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
&& let Some(skinned) = (pass as &mut dyn std::any::Any)
.downcast_mut::<passes::geometry::SkinnedMeshPass>()
{
skinned.set_material_array_views(&self.device, srgb_view, linear_view, sampler);
}
}
let frame_time_ms = world.resources.window.timing.delta_time * 1000.0;
if frame_time_ms > 0.0 {
world
.resources
.graphics
.adaptive_sampling
.record_frame_time(frame_time_ms);
}
if self.depth_pick_pending {
let _ = self.device.poll(wgpu::PollType::Poll);
if self
.depth_pick_map_complete
.load(std::sync::atomic::Ordering::Relaxed)
{
let buffer_slice = self.depth_pick_staging_buffer.slice(..);
let data = buffer_slice.get_mapped_range();
let mut depth_values = Vec::new();
let mut entity_id_values = Vec::new();
for chunk in data.chunks_exact(8) {
let depth = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
let entity_id = u32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]);
depth_values.push(depth);
entity_id_values.push(entity_id);
}
drop(data);
self.depth_pick_staging_buffer.unmap();
world.resources.gpu_picking.set_depth_samples(
depth_values,
entity_id_values,
DEPTH_PICK_SAMPLE_SIZE,
DEPTH_PICK_SAMPLE_SIZE,
self.depth_pick_center.0,
self.depth_pick_center.1,
);
if let Some(camera_entity) = self.depth_pick_camera
&& let Some(matrices) =
crate::ecs::camera::queries::query_camera_matrices(world, camera_entity)
{
let (texture_width, texture_height) = self.depth_pick_texture_size;
let inverse_view_proj = (matrices.projection * matrices.view)
.try_inverse()
.unwrap_or_else(nalgebra_glm::Mat4::identity);
world.resources.gpu_picking.compute_result(
&inverse_view_proj,
texture_width as f32,
texture_height as f32,
);
}
self.depth_pick_pending = false;
self.depth_pick_map_complete
.store(false, std::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "screenshot"))]
if self.screenshot_pending {
let _ = self.device.poll(wgpu::PollType::Poll);
if self
.screenshot_map_complete
.load(std::sync::atomic::Ordering::Relaxed)
{
let buffer_slice = self.screenshot_staging_buffer.slice(..);
let data = buffer_slice.get_mapped_range();
let width = self.screenshot_width;
let height = self.screenshot_height;
let bytes_per_pixel = 4u32;
let unpadded_bytes_per_row = width * bytes_per_pixel;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
let mut pixels = Vec::with_capacity((width * height * bytes_per_pixel) as usize);
for row in 0..height {
let start = (row * padded_bytes_per_row) as usize;
let end = start + (unpadded_bytes_per_row) as usize;
pixels.extend_from_slice(&data[start..end]);
}
drop(data);
self.screenshot_staging_buffer.unmap();
let path = self.screenshot_path.take().unwrap_or_else(|| {
let screenshots_dir = std::path::PathBuf::from("screenshots");
if !screenshots_dir.exists() {
let _ = std::fs::create_dir_all(&screenshots_dir);
}
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
screenshots_dir.join(format!("screenshot_{}.png", timestamp))
});
let save_width = width;
let save_height = height;
let max_dimension = self.screenshot_max_dimension.take();
std::thread::spawn(move || {
let Some(mut image) =
image::RgbaImage::from_raw(save_width, save_height, pixels)
else {
tracing::error!("Failed to create image from screenshot data");
return;
};
if let Some(limit) = max_dimension {
image = crop_center_square(image);
let (w, h) = image.dimensions();
let longest = w.max(h);
if longest > limit {
let scale = limit as f32 / longest as f32;
let new_w = ((w as f32 * scale).round() as u32).max(1);
let new_h = ((h as f32 * scale).round() as u32).max(1);
image = image::imageops::resize(
&image,
new_w,
new_h,
image::imageops::FilterType::Lanczos3,
);
}
}
if let Err(error) = image.save(&path) {
tracing::error!("Failed to save screenshot: {}", error);
} else {
tracing::info!("Screenshot saved to: {}", path.display());
}
});
self.screenshot_pending = false;
self.screenshot_map_complete
.store(false, std::sync::atomic::Ordering::Relaxed);
}
}
self.resize_render_buffers(self.surface_config.width, self.surface_config.height);
let mut ui_image_uploads = Vec::new();
let mut texture_reload_commands = Vec::new();
let mut hdr_skybox_commands = Vec::new();
let mut procedural_ibl_commands = Vec::new();
let mut ibl_snapshot_commands = Vec::new();
#[cfg(not(target_arch = "wasm32"))]
let mut screenshot_path: Option<Option<std::path::PathBuf>> = None;
#[cfg(not(target_arch = "wasm32"))]
let mut screenshot_max_dimension: Option<u32> = None;
let render_commands = std::mem::take(&mut world.resources.commands.render);
for cmd in render_commands {
match cmd {
crate::ecs::world::RenderCommand::UploadUiImageLayer {
layer,
rgba_data,
width,
height,
} => {
ui_image_uploads.push((layer, rgba_data, width, height));
}
crate::ecs::world::RenderCommand::ReloadTexture {
name,
rgba_data,
width,
height,
} => {
texture_reload_commands.push((name, rgba_data, width, height));
}
crate::ecs::world::RenderCommand::LoadHdrSkybox { hdr_data } => {
hdr_skybox_commands.push(hdr_data);
}
crate::ecs::world::RenderCommand::LoadHdrSkyboxFromPath { path } => {
match std::fs::read(&path) {
Ok(bytes) => {
tracing::info!("Successfully read HDR file: {}", path.display());
hdr_skybox_commands.push(bytes);
}
Err(e) => {
tracing::error!("Failed to read HDR file {}: {}", path.display(), e);
}
}
}
crate::ecs::world::RenderCommand::CaptureProceduralAtmosphereIBL {
atmosphere,
time,
} => {
procedural_ibl_commands.push((atmosphere, time));
}
crate::ecs::world::RenderCommand::CaptureIblSnapshots { atmosphere, hours } => {
ibl_snapshot_commands.push((atmosphere, hours));
}
crate::ecs::world::RenderCommand::CaptureScreenshot {
path,
max_dimension,
} => {
#[cfg(not(target_arch = "wasm32"))]
{
screenshot_path = Some(path);
screenshot_max_dimension = max_dimension;
}
#[cfg(target_arch = "wasm32")]
{
let _ = path;
let _ = max_dimension;
}
}
}
}
if !ui_image_uploads.is_empty() {
for (layer, rgba_data, width, height) in &ui_image_uploads {
self.ui_texture_array
.upload_layer(&self.queue, *layer, rgba_data, *width, *height);
}
}
self.drain_loading_tasks(world);
if !texture_reload_commands.is_empty() {
use crate::render::wgpu::texture_cache::{TextureReloadResult, texture_cache_reload};
for (name, rgba_data, width, height) in texture_reload_commands {
let result = texture_cache_reload(
&mut world.resources.texture_cache,
&self.device,
&self.queue,
&name,
&rgba_data,
width,
height,
);
if matches!(result, TextureReloadResult::Recreated) {
let usage_opt =
registry_entry_by_name(&world.resources.texture_cache.registry, &name).map(
|entry| match entry.texture.format() {
wgpu::TextureFormat::Rgba8UnormSrgb
| wgpu::TextureFormat::Bgra8UnormSrgb => {
crate::render::wgpu::texture_cache::TextureUsage::Color
}
_ => crate::render::wgpu::texture_cache::TextureUsage::Linear,
},
);
if let Some(usage) = usage_opt {
self.material_texture_arrays.upload(
&self.device,
&self.queue,
&self.mip_generator,
crate::render::wgpu::material_texture_arrays::MaterialTextureUpload {
name: name.clone(),
rgba_data: &rgba_data,
width,
height,
usage,
wrap_u: crate::render::wgpu::texture_cache::SamplerWrap::Repeat,
wrap_v: crate::render::wgpu::texture_cache::SamplerWrap::Repeat,
},
);
}
if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
&& let Some(texture_entry) =
registry_entry_by_name(&world.resources.texture_cache.registry, &name)
{
mesh_pass.register_texture_with_data(
name.clone(),
texture_entry.view.clone(),
texture_entry.sampler.clone(),
);
}
if let Some(skinned_mesh_pass) =
render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass
as &mut dyn std::any::Any)
.downcast_mut::<passes::geometry::SkinnedMeshPass>()
&& let Some(texture_entry) =
registry_entry_by_name(&world.resources.texture_cache.registry, &name)
{
skinned_mesh_pass.register_texture(
name.clone(),
texture_entry.view.clone(),
texture_entry.sampler.clone(),
);
}
if let Some(decal_pass) =
render_graph_get_pass_mut(&mut self.graph, "decal_pass")
&& let Some(decal_pass) = (decal_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::DecalPass>()
&& let Some(texture_entry) =
registry_entry_by_name(&world.resources.texture_cache.registry, &name)
{
decal_pass.register_texture(
name,
texture_entry.view.clone(),
texture_entry.sampler.clone(),
);
}
}
}
}
#[cfg(feature = "assets")]
if !hdr_skybox_commands.is_empty() {
let mut ibl_views = None;
if let Some(sky_pass) = render_graph_get_pass_mut(&mut self.graph, "sky_pass")
&& let Some(sky_pass) =
(sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
{
for hdr_data in hdr_skybox_commands {
ibl_views =
Some(sky_pass.load_hdr_skybox(&self.device, &self.queue, &hdr_data));
}
}
if let Some((irradiance_view, prefiltered_view)) = ibl_views {
if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
{
mesh_pass.update_ibl_textures(
self.brdf_lut_view.clone(),
irradiance_view.clone(),
prefiltered_view.clone(),
);
}
if let Some(skinned_mesh_pass) =
render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::geometry::SkinnedMeshPass>(
)
{
skinned_mesh_pass.set_ibl_textures(
self.brdf_lut_view.clone(),
irradiance_view.clone(),
prefiltered_view.clone(),
);
}
world.resources.ibl_views.brdf_lut_view = Some(self.brdf_lut_view.clone());
world.resources.ibl_views.irradiance_view = Some(irradiance_view);
world.resources.ibl_views.prefiltered_view = Some(prefiltered_view);
}
}
#[cfg(feature = "assets")]
if !procedural_ibl_commands.is_empty() {
let mut ibl_views = None;
if let Some(sky_pass) = render_graph_get_pass_mut(&mut self.graph, "sky_pass")
&& let Some(sky_pass) =
(sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
{
for (atmosphere, time) in procedural_ibl_commands {
ibl_views = sky_pass.capture_procedural_atmosphere(
&self.device,
&self.queue,
atmosphere,
time,
);
}
}
if let Some((irradiance_view, prefiltered_view)) = ibl_views {
if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
{
mesh_pass.update_ibl_textures(
self.brdf_lut_view.clone(),
irradiance_view.clone(),
prefiltered_view.clone(),
);
}
if let Some(skinned_mesh_pass) =
render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::geometry::SkinnedMeshPass>(
)
{
skinned_mesh_pass.set_ibl_textures(
self.brdf_lut_view.clone(),
irradiance_view.clone(),
prefiltered_view.clone(),
);
}
world.resources.ibl_views.brdf_lut_view = Some(self.brdf_lut_view.clone());
world.resources.ibl_views.irradiance_view = Some(irradiance_view);
world.resources.ibl_views.prefiltered_view = Some(prefiltered_view);
}
}
#[cfg(feature = "assets")]
if !ibl_snapshot_commands.is_empty()
&& let Some(sky_pass) = render_graph_get_pass_mut(&mut self.graph, "sky_pass")
&& let Some(sky_pass) =
(sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
{
for (atmosphere, hours) in ibl_snapshot_commands {
sky_pass.capture_ibl_snapshots(&self.device, &self.queue, atmosphere, &hours);
}
}
let mut frame_any_camera_wants_lines = false;
let mut frame_any_camera_wants_wireframe = false;
for camera in world.resources.user_interface.required_cameras.iter() {
let shading = world
.core
.get_viewport_shading(*camera)
.copied()
.unwrap_or_default();
let post_process = world.core.get_camera_post_process(*camera).copied();
let culling_mask = world.core.get_camera_culling_mask(*camera).copied();
let environment = world.core.get_camera_environment(*camera).copied();
let effective = crate::ecs::camera::components::EffectiveShading::for_camera(
&world.resources.graphics,
&shading,
post_process.as_ref(),
culling_mask.as_ref(),
environment.as_ref(),
);
if effective.show_normals || effective.show_bounding_volumes {
frame_any_camera_wants_lines = true;
}
if effective.show_wireframe {
frame_any_camera_wants_wireframe = true;
}
}
let upload_lines = world.resources.graphics.show_normals
|| world.resources.graphics.show_bounding_volumes
|| frame_any_camera_wants_lines;
if let Some(lines_pass) = render_graph_get_pass_mut(&mut self.graph, "lines_pass")
&& let Some(lines_pass) =
(lines_pass as &mut dyn std::any::Any).downcast_mut::<passes::LinesPass>()
{
passes::sync_lines_data(
lines_pass,
&self.device,
&self.queue,
world,
frame_any_camera_wants_wireframe,
);
passes::sync_bounding_volume_data(
lines_pass,
&self.device,
&self.queue,
world,
upload_lines,
world.resources.graphics.show_selected_bounding_volume,
world.resources.graphics.bounding_volume_selected_entity,
);
passes::sync_normal_data(
lines_pass,
&self.device,
&self.queue,
world,
upload_lines,
world.resources.graphics.normal_line_length,
world.resources.graphics.normal_line_color,
);
}
let point_shadow_view = render_graph_get_pass_mut(&mut self.graph, "shadow_depth_pass")
.and_then(|pass| {
(pass as &dyn std::any::Any)
.downcast_ref::<passes::ShadowDepthPass>()
.map(|shadow_pass| shadow_pass.point_light_cubemap_view().clone())
});
if let Some(view) = point_shadow_view {
if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
{
mesh_pass.update_point_shadow_cubemap(view.clone());
}
if let Some(skinned_mesh_pass) =
render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::SkinnedMeshPass>()
{
skinned_mesh_pass.update_point_shadow_cubemap(view);
}
}
{
let raw_hour = world.resources.graphics.day_night.hour;
let hour = raw_hour - (raw_hour / 24.0).floor() * 24.0;
let world_id = world.resources.world_id;
let mut bracket_views: Option<(
wgpu::TextureView,
wgpu::TextureView,
wgpu::TextureView,
wgpu::TextureView,
f32,
)> = None;
if let Some(sky_pass) = render_graph_get_pass_mut(&mut self.graph, "sky_pass")
&& let Some(sky_pass) =
(sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
&& let Some((index_a, index_b, blend)) =
sky_pass.select_ibl_bracket(world.resources.graphics.atmosphere, hour)
{
let irr_a =
sky_pass.ibl_snapshots[index_a]
.1
.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::Cube),
..Default::default()
});
let pref_a =
sky_pass.ibl_snapshots[index_a]
.2
.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::Cube),
..Default::default()
});
let irr_b =
sky_pass.ibl_snapshots[index_b]
.1
.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::Cube),
..Default::default()
});
let pref_b =
sky_pass.ibl_snapshots[index_b]
.2
.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::Cube),
..Default::default()
});
bracket_views = Some((irr_a, pref_a, irr_b, pref_b, blend));
}
if let Some((irr_a, pref_a, irr_b, pref_b, blend)) = bracket_views {
world.resources.graphics.ibl_blend_factor = blend;
if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
{
mesh_pass.update_ibl_textures_blended(
self.brdf_lut_view.clone(),
irr_a.clone(),
pref_a.clone(),
irr_b.clone(),
pref_b.clone(),
blend,
);
}
if let Some(skinned_mesh_pass) =
render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::geometry::SkinnedMeshPass>(
)
{
skinned_mesh_pass.update_ibl_textures_blended_for_world(
world_id,
passes::geometry::BlendedIblViews {
brdf_lut: self.brdf_lut_view.clone(),
irradiance_a: irr_a.clone(),
prefiltered_a: pref_a.clone(),
irradiance_b: irr_b,
prefiltered_b: pref_b,
blend_factor: blend,
},
);
}
world.resources.ibl_views.brdf_lut_view = Some(self.brdf_lut_view.clone());
world.resources.ibl_views.irradiance_view = Some(irr_a);
world.resources.ibl_views.prefiltered_view = Some(pref_a);
}
}
let surface_texture = match self.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(frame)
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
wgpu::CurrentSurfaceTexture::Outdated => {
self.surface.configure(&self.device, &self.surface_config);
match self.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(frame)
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
other => {
panic!("Failed to get surface texture after reconfiguration: {other:?}")
}
}
}
_ => {
return Ok(());
}
};
let surface_texture_view =
surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor {
label: wgpu::Label::default(),
aspect: wgpu::TextureAspect::default(),
format: Some(self.surface_format),
dimension: None,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
usage: None,
});
render_graph_set_external_texture(
&mut self.graph,
self.swapchain_id,
Some(surface_texture.texture.clone()),
surface_texture_view,
self.surface_config.width,
self.surface_config.height,
);
let active_camera_entities: std::collections::HashSet<freecs::Entity> = world
.resources
.user_interface
.required_cameras
.iter()
.copied()
.collect();
self.cleanup_unused_camera_viewports(&active_camera_entities);
world
.resources
.user_interface
.viewport_texture_sizes
.clear();
let dirty_world_spheres: Vec<(nalgebra_glm::Vec3, f32)> = {
let mut spheres = Vec::new();
let dirty_entities: Vec<_> = world
.resources
.mesh_render_state
.dirty_entities_for_culling()
.collect();
for entity in dirty_entities {
let Some(global_transform) = world.core.get_global_transform(entity) else {
continue;
};
let Some(bounding_volume) = world.core.get_bounding_volume(entity) else {
continue;
};
let world_center = global_transform.0
* nalgebra_glm::Vec4::new(
bounding_volume.obb.center.x,
bounding_volume.obb.center.y,
bounding_volume.obb.center.z,
1.0,
);
let world_center =
nalgebra_glm::Vec3::new(world_center.x, world_center.y, world_center.z);
let scale = nalgebra_glm::length(&nalgebra_glm::Vec3::new(
global_transform.0[(0, 0)],
global_transform.0[(1, 0)],
global_transform.0[(2, 0)],
));
let world_radius = bounding_volume.sphere_radius * scale;
spheres.push((world_center, world_radius));
}
spheres
};
let global_dirty_signal = world
.resources
.mesh_render_state
.requires_full_invalidation();
let frame_settings_version = world.resources.graphics.settings_version;
let mouse_position = world.resources.input.mouse.position;
let original_active_camera = world.resources.active_camera;
let focus_policy = world.resources.graphics.focus_policy;
let primary_cameras = world.resources.user_interface.required_cameras.clone();
self.dispatch_window_renders(
world,
WindowDispatchContext {
cameras: primary_cameras,
target_size: (self.surface_config.width, self.surface_config.height),
dirty_world_spheres: &dirty_world_spheres,
frame_settings_version,
original_active_camera,
focus_policy,
mouse_position,
global_dirty_signal,
},
)?;
#[cfg(not(target_arch = "wasm32"))]
{
if !self.screenshot_pending
&& let Some(path) = screenshot_path
&& let Some(compute_output_texture) =
render_graph_get_texture(&self.graph, self.compute_output_id)
{
let width = self.surface_config.width;
let height = self.surface_config.height;
let bytes_per_pixel = 4u32;
let unpadded_bytes_per_row = width * bytes_per_pixel;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
let required_buffer_size = (padded_bytes_per_row * height) as u64;
if self.screenshot_staging_buffer.size() < required_buffer_size {
self.screenshot_staging_buffer =
self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Screenshot Staging Buffer"),
size: required_buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
}
let mut encoder =
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Screenshot Encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: compute_output_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &self.screenshot_staging_buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
self.queue.submit(std::iter::once(encoder.finish()));
self.screenshot_path = path;
self.screenshot_width = width;
self.screenshot_height = height;
self.screenshot_max_dimension = screenshot_max_dimension;
self.screenshot_pending = true;
let map_complete = self.screenshot_map_complete.clone();
self.screenshot_staging_buffer.slice(..).map_async(
wgpu::MapMode::Read,
move |_| {
map_complete.store(true, std::sync::atomic::Ordering::Relaxed);
},
);
}
}
surface_texture.present();
Ok(())
}
pub fn resize_surface(
&mut self,
width: u32,
height: u32,
) -> Result<(), Box<dyn std::error::Error>> {
if width == 0 || height == 0 {
return Ok(());
}
self.surface_config.width = width;
self.surface_config.height = height;
self.surface.configure(&self.device, &self.surface_config);
self.resize_render_buffers(width, height);
Ok(())
}
pub fn configure_with_state(
&mut self,
state: &mut dyn crate::run::State,
) -> Result<(), Box<dyn std::error::Error>> {
let mut text_pass = passes::TextPass::new(
&self.device,
wgpu::TextureFormat::Rgba16Float,
wgpu::TextureFormat::Depth32Float,
);
text_pass.update_glyph_atlas(&self.device, &self.glyph_atlas.texture_view);
render_graph_add_pass(
&mut self.graph,
Box::new(text_pass),
&[("color", self.scene_color_id), ("depth", self.depth_id)],
)?;
state.configure_render_graph(
&mut self.graph,
&self.device,
self.surface_format,
crate::run::RenderResources {
scene_color: self.scene_color_id,
depth: self.depth_id,
compute_output: self.compute_output_id,
swapchain: self.swapchain_id,
view_normals: self.view_normals_id,
ssao_raw: self.ssao_raw_id,
ssao: self.ssao_id,
ssgi_raw: self.ssgi_raw_id,
ssgi: self.ssgi_id,
ssr_raw: self.ssr_raw_id,
ssr: self.ssr_id,
surface_width: self.surface_config.width,
surface_height: self.surface_config.height,
},
);
let viewport_blit_pass = passes::BlitPass::new(&self.device, self.surface_format)
.with_name("viewport_blit_pass");
render_graph_add_pass(
&mut self.graph,
Box::new(viewport_blit_pass),
&[
("input", self.fxaa_output_id),
("output", self.viewport_resource_id),
],
)?;
let cached_viewport_preload_pass = passes::BlitPass::new(&self.device, self.surface_format)
.with_name("cached_viewport_preload_pass")
.with_runs_in_full(false)
.with_runs_in_compose_only(true);
render_graph_add_pass(
&mut self.graph,
Box::new(cached_viewport_preload_pass),
&[
("input", self.viewport_resource_id),
("output", self.fxaa_output_id),
],
)?;
let swapchain_compose_pass =
passes::ViewportComposePass::new(&self.device, self.surface_format);
render_graph_add_pass(
&mut self.graph,
Box::new(swapchain_compose_pass),
&[
("input", self.fxaa_output_id),
("output", self.swapchain_id),
],
)?;
let mut ui_pass = passes::UiPass::new(&self.device, self.surface_format);
ui_pass.update_glyph_atlas(&self.device, &self.glyph_atlas.texture_view);
render_graph_add_pass(
&mut self.graph,
Box::new(ui_pass),
&[("color", self.swapchain_id), ("depth", self.ui_depth_id)],
)?;
if let Some(ui_image_pass) = self.ui_image_pass.take() {
render_graph_add_pass(
&mut self.graph,
ui_image_pass,
&[("color", self.swapchain_id), ("depth", self.ui_depth_id)],
)?;
}
let pick_keepalive_pass = passes::PickKeepalivePass::new();
render_graph_add_pass(
&mut self.graph,
Box::new(pick_keepalive_pass),
&[("entity_id", self.entity_id_id), ("depth", self.depth_id)],
)?;
render_graph_compile(&mut self.graph)?;
Ok(())
}
pub fn initialize_fonts(&mut self, _world: &mut crate::ecs::world::World) {
self.glyph_atlas_initialized = true;
}
pub fn update_with_state(
&mut self,
state: &mut dyn crate::run::State,
world: &mut crate::ecs::world::World,
) -> Result<(), Box<dyn std::error::Error>> {
self.initialize_fonts(world);
for pending_load in world.resources.loading.pending_font_loads.drain(..) {
world
.resources
.text
.font_engine
.load_font(pending_load.font_data);
}
state.update_render_graph(&mut self.graph, world);
Ok(())
}
pub fn copy_fonts_to_world(&self, _world: &mut crate::ecs::world::World) {}
pub fn register_render_texture(&mut self, name: &str, view: wgpu::TextureView) {
let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(&format!("{}_sampler", name)),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
{
mesh_pass.register_texture(name.to_string(), view, sampler);
}
}
}
struct MaterialTextureUploadRequest<'a> {
texture: crate::ecs::asset_id::TextureId,
rgba_data: &'a [u8],
width: u32,
height: u32,
usage: crate::render::wgpu::texture_cache::TextureUsage,
sampler: crate::render::wgpu::texture_cache::SamplerSettings,
}
impl super::WgpuRenderer {
fn dispatch_pick_compute_if_pending(&mut self, world: &mut crate::ecs::world::World) {
if self.depth_pick_pending {
return;
}
let Some(request) = world.resources.gpu_picking.take_pending_request() else {
return;
};
let Some(depth_texture_view) = render_graph_get_texture_view(&self.graph, self.depth_id)
else {
return;
};
let Some(entity_id_texture_view) =
render_graph_get_texture_view(&self.graph, self.entity_id_id)
else {
return;
};
let uniform_data: [u32; 4] = [
request.screen_x,
request.screen_y,
DEPTH_PICK_SAMPLE_SIZE,
0,
];
self.queue.write_buffer(
&self.depth_pick_uniform_buffer,
0,
bytemuck::cast_slice(&uniform_data),
);
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Depth Pick Bind Group"),
layout: &self.depth_pick_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(depth_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: self.depth_pick_storage_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.depth_pick_uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(entity_id_texture_view),
},
],
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Depth Pick Encoder"),
});
{
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Depth Pick Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.depth_pick_compute_pipeline);
compute_pass.set_bind_group(0, &bind_group, &[]);
compute_pass.dispatch_workgroups(DEPTH_PICK_SAMPLE_SIZE, DEPTH_PICK_SAMPLE_SIZE, 1);
}
encoder.copy_buffer_to_buffer(
&self.depth_pick_storage_buffer,
0,
&self.depth_pick_staging_buffer,
0,
(DEPTH_PICK_SAMPLE_SIZE * DEPTH_PICK_SAMPLE_SIZE * 8) as u64,
);
self.queue.submit(std::iter::once(encoder.finish()));
self.depth_pick_bind_group = Some(bind_group);
self.depth_pick_pending = true;
self.depth_pick_center = (request.screen_x, request.screen_y);
self.depth_pick_texture_size = self.render_buffer_size;
self.depth_pick_camera = world.resources.active_camera;
let map_complete = self.depth_pick_map_complete.clone();
self.depth_pick_staging_buffer
.slice(..)
.map_async(wgpu::MapMode::Read, move |_| {
map_complete.store(true, std::sync::atomic::Ordering::Relaxed);
});
}
fn clear_swapchain_for_window_ui(&mut self, world: &crate::ecs::world::World) {
let Some(view) = rendergraph::render_graph_get_texture_view(&self.graph, self.swapchain_id)
else {
return;
};
let clear = world
.resources
.retained_ui
.background_color
.map(|color| wgpu::Color {
r: color.x as f64,
g: color.y as f64,
b: color.z as f64,
a: color.w as f64,
})
.unwrap_or(wgpu::Color {
r: 0.04,
g: 0.04,
b: 0.06,
a: 1.0,
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Window UI Swapchain Clear"),
});
{
let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Window UI Swapchain Clear Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(clear),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
}
self.queue.submit(std::iter::once(encoder.finish()));
}
fn dispatch_window_renders(
&mut self,
world: &mut crate::ecs::world::World,
ctx: WindowDispatchContext<'_>,
) -> Result<(), Box<dyn std::error::Error>> {
let _span =
tracing::info_span!("dispatch_window_renders", cameras = ctx.cameras.len(),).entered();
let WindowDispatchContext {
cameras,
target_size,
dirty_world_spheres,
frame_settings_version,
original_active_camera,
focus_policy,
mouse_position,
global_dirty_signal,
} = ctx;
let (surface_width, surface_height) = target_size;
let has_viewports = !cameras.is_empty();
let _ = render_graph_set_pass_enabled(&mut self.graph, "viewport_blit_pass", has_viewports);
let _ = render_graph_set_pass_enabled(&mut self.graph, "viewport_compose_pass", true);
if !has_viewports {
self.resize_render_buffers_for_window(surface_width, surface_height);
}
world.resources.window.camera_tile_render_iteration = 0;
let ui_pass_states_before_loop = if has_viewports {
let saved_states = render_graph_save_pass_enabled_states(&self.graph);
let saved = (
saved_states.get("ui_pass").copied().unwrap_or(true),
saved_states.get("ui_image_pass").copied().unwrap_or(true),
);
let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_pass", false);
let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_image_pass", false);
Some(saved)
} else {
None
};
let mouse_focused_camera = world
.resources
.window
.camera_tile_rects
.iter()
.find(|(_, rect)| rect.contains(mouse_position))
.map(|(entity, _)| *entity);
let focused_camera = mouse_focused_camera.or(original_active_camera);
for (iteration_index, camera_entity) in cameras.iter().enumerate() {
let (tile_width, tile_height) = world
.resources
.window
.camera_tile_rects
.get(camera_entity)
.map(|rect| {
(
(rect.width.round() as u32).max(1),
(rect.height.round() as u32).max(1),
)
})
.unwrap_or((surface_width.max(1), surface_height.max(1)));
let constrained = world.core.get_constrained_aspect(*camera_entity).copied();
let (display_width, display_height) = if let Some(aspect) = constrained {
fit_constrained_aspect(tile_width, tile_height, aspect.0)
} else {
(tile_width, tile_height)
};
let render_scale = world.resources.graphics.render_scale.clamp(0.25, 4.0);
let is_main_viewport_camera = Some(*camera_entity) == original_active_camera;
let render_width = ((display_width as f32 * render_scale).round() as u32).max(1);
let render_height = ((display_height as f32 * render_scale).round() as u32).max(1);
self.ensure_camera_viewport(*camera_entity, render_width, render_height);
self.resize_render_buffers_for_window(render_width, render_height);
let viewport_cloned = self.camera_viewports.get(camera_entity).map(|viewport| {
(
viewport.texture.clone(),
viewport.view.clone(),
viewport.size,
viewport.has_rendered_at_least_once,
viewport.last_active_view,
viewport.last_settings_version,
viewport.last_camera_world_transform,
)
});
if let Some((
texture,
view,
size,
has_rendered_once,
last_active_view,
last_settings_version,
last_camera_world_transform,
)) = viewport_cloned
{
render_graph_set_external_texture(
&mut self.graph,
self.viewport_resource_id,
Some(texture),
view,
render_width,
render_height,
);
world.resources.active_camera = Some(*camera_entity);
world.resources.window.camera_tile_render_iteration = iteration_index as u32;
let viewport_shading = world
.core
.get_viewport_shading(*camera_entity)
.copied()
.unwrap_or_default();
let update_mode = world
.core
.get_viewport_update_mode(*camera_entity)
.copied()
.unwrap_or_default();
let camera_post_process =
world.core.get_camera_post_process(*camera_entity).copied();
let camera_culling_mask =
world.core.get_camera_culling_mask(*camera_entity).copied();
let camera_environment = world.core.get_camera_environment(*camera_entity).copied();
let effective = crate::ecs::camera::components::EffectiveShading::for_camera(
&world.resources.graphics,
&viewport_shading,
camera_post_process.as_ref(),
camera_culling_mask.as_ref(),
camera_environment.as_ref(),
);
let is_focused_view = Some(*camera_entity) == focused_camera;
let focus_forces_render = matches!(
focus_policy,
crate::ecs::graphics::resources::ViewportFocusPolicy::FocusedAlways
) && is_focused_view;
let view_changed = last_active_view != Some(effective);
let settings_changed = last_settings_version != frame_settings_version;
let camera_matrices =
crate::ecs::camera::queries::query_camera_matrices(world, *camera_entity);
let camera_world_transform = world
.core
.get_global_transform(*camera_entity)
.map(|gt| gt.0);
let camera_self_dirty = match (last_camera_world_transform, camera_world_transform)
{
(Some(prev), Some(curr)) => prev != curr,
(None, _) => true,
(_, None) => false,
};
let frustum_dirty = if let Some(matrices) = camera_matrices.as_ref() {
let view_proj = matrices.projection * matrices.view;
let frustum_planes = passes::geometry::extract_frustum_planes(&view_proj);
dirty_world_spheres.iter().any(|(center, radius)| {
passes::geometry::sphere_in_frustum(center, *radius, &frustum_planes)
})
} else {
!dirty_world_spheres.is_empty()
};
let pick_forces_render =
is_main_viewport_camera && world.resources.gpu_picking.has_pending_request();
let should_render = pick_forces_render
|| match update_mode {
crate::ecs::camera::components::ViewportUpdateMode::Always => true,
crate::ecs::camera::components::ViewportUpdateMode::WhenVisible => true,
crate::ecs::camera::components::ViewportUpdateMode::WhenDirty => {
focus_forces_render
|| !has_rendered_once
|| global_dirty_signal
|| settings_changed
|| view_changed
|| camera_self_dirty
|| frustum_dirty
}
crate::ecs::camera::components::ViewportUpdateMode::Once => {
!has_rendered_once
}
crate::ecs::camera::components::ViewportUpdateMode::Disabled => {
!has_rendered_once
}
};
world.resources.graphics.active_view = effective;
if self.extract_dirty_done_for_frame != self.frame_index {
let _span = tracing::info_span!("extract_frame_dirty_state").entered();
self.extract_frame_dirty_state(world);
self.extract_dirty_done_for_frame = self.frame_index;
}
if self.text_meshes_prepared_for_frame != self.frame_index {
let _span = tracing::info_span!("prepare_text_meshes").entered();
self.prepare_text_meshes(world);
self.text_meshes_prepared_for_frame = self.frame_index;
}
let phase = if should_render {
rendergraph::ExecutePhase::Full
} else {
rendergraph::ExecutePhase::ComposeOnly
};
let command_buffers = {
let _span =
tracing::info_span!("render_graph_execute", phase = ?phase).entered();
rendergraph::render_graph_execute_with_phase(
&mut self.graph,
&self.device,
&self.queue,
world,
phase,
)?
};
{
let _span = tracing::info_span!("queue_submit").entered();
self.queue.submit(command_buffers);
}
if should_render
&& let Some(viewport) = self.camera_viewports.get_mut(camera_entity)
{
viewport.has_rendered_at_least_once = true;
viewport.last_active_view = Some(effective);
viewport.last_settings_version = frame_settings_version;
viewport.last_render_frame = self.frame_index;
viewport.last_camera_world_transform = camera_world_transform;
}
if is_main_viewport_camera {
self.dispatch_pick_compute_if_pending(world);
}
world
.resources
.user_interface
.viewport_texture_sizes
.push(size);
}
}
world.resources.active_camera = original_active_camera;
world.resources.graphics.active_view =
crate::ecs::camera::components::EffectiveShading::from_graphics(
&world.resources.graphics,
);
if let Some((ui_was_enabled, ui_image_was_enabled)) = ui_pass_states_before_loop {
let _span = tracing::info_span!("ui_composite").entered();
let _ = rendergraph::render_graph_resize_transient_resource(
&mut self.graph,
&self.device,
self.ui_depth_id,
surface_width.max(1),
surface_height.max(1),
);
let swapped_ui = swap_window_ui_frame(world, target_size);
let saved_pass_states = render_graph_save_pass_enabled_states(&self.graph);
for name in saved_pass_states.keys() {
let _ = render_graph_set_pass_enabled(&mut self.graph, name, false);
}
let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_pass", ui_was_enabled);
let _ = render_graph_set_pass_enabled(
&mut self.graph,
"ui_image_pass",
ui_image_was_enabled,
);
self.extract_frame_dirty_state(world);
self.prepare_text_meshes(world);
let command_buffers = {
let _span = tracing::info_span!("ui_composite_execute").entered();
render_graph_execute(&mut self.graph, &self.device, &self.queue, world)?
};
self.queue.submit(command_buffers);
restore_window_ui_frame(world, swapped_ui);
render_graph_restore_pass_enabled_states(&mut self.graph, &saved_pass_states);
let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_pass", ui_was_enabled);
let _ = render_graph_set_pass_enabled(
&mut self.graph,
"ui_image_pass",
ui_image_was_enabled,
);
}
if cameras.is_empty() {
world.resources.window.camera_tile_render_iteration = 0;
if world.resources.graphics.render_world_to_swapchain {
self.extract_frame_dirty_state(world);
self.prepare_text_meshes(world);
let command_buffers =
render_graph_execute(&mut self.graph, &self.device, &self.queue, world)?;
self.queue.submit(command_buffers);
} else {
let swapped_ui = swap_window_ui_frame(world, target_size);
self.clear_swapchain_for_window_ui(world);
let _ = rendergraph::render_graph_resize_transient_resource(
&mut self.graph,
&self.device,
self.ui_depth_id,
surface_width.max(1),
surface_height.max(1),
);
let saved_pass_states = render_graph_save_pass_enabled_states(&self.graph);
for name in saved_pass_states.keys() {
let _ = render_graph_set_pass_enabled(&mut self.graph, name, false);
}
let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_pass", true);
let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_image_pass", true);
self.extract_frame_dirty_state(world);
self.prepare_text_meshes(world);
let command_buffers =
render_graph_execute(&mut self.graph, &self.device, &self.queue, world)?;
self.queue.submit(command_buffers);
restore_window_ui_frame(world, swapped_ui);
render_graph_restore_pass_enabled_states(&mut self.graph, &saved_pass_states);
let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_pass", true);
let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_image_pass", true);
}
}
let dispatched_settings_version = world.resources.graphics.settings_version;
self.record_window_dispatch(dispatched_settings_version);
Ok(())
}
fn upload_material_texture(
&mut self,
world: &mut crate::ecs::world::World,
request: MaterialTextureUploadRequest<'_>,
) {
let MaterialTextureUploadRequest {
texture,
rgba_data,
width,
height,
usage,
sampler,
} = request;
let Some(name) = registry_name_for(&world.resources.texture_cache.registry, texture.index)
.map(str::to_string)
else {
tracing::error!(
"upload_material_texture: no name registered for texture id {}",
texture.index
);
return;
};
if let Err(e) =
crate::render::wgpu::texture_cache::texture_cache_load_from_raw_rgba_with_format(
&mut world.resources.texture_cache,
&self.device,
&self.queue,
&self.mip_generator,
crate::render::wgpu::texture_cache::TextureUploadRequest {
name: name.clone(),
rgba_data,
dimensions: (width, height),
spec: crate::render::wgpu::texture_cache::TextureUploadSpec {
format: usage.wgpu_format(),
sampler,
},
},
)
{
tracing::error!("Failed to load texture: {}", e);
return;
}
let layer_index = self.material_texture_arrays.upload(
&self.device,
&self.queue,
&self.mip_generator,
crate::render::wgpu::material_texture_arrays::MaterialTextureUpload {
name: name.clone(),
rgba_data,
width,
height,
usage,
wrap_u: sampler.wrap_u,
wrap_v: sampler.wrap_v,
},
);
if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
&& let Some(texture_entry) =
registry_entry_by_name(&world.resources.texture_cache.registry, &name)
{
mesh_pass.register_texture_with_data(
name.clone(),
texture_entry.view.clone(),
texture_entry.sampler.clone(),
);
if let Some(layer) = layer_index {
mesh_pass.add_material_layer_mapping(
texture,
crate::render::wgpu::material_texture_arrays::MaterialTextureLayer {
usage,
layer,
wrap_u: sampler.wrap_u,
wrap_v: sampler.wrap_v,
},
);
}
}
if let Some(skinned_mesh_pass) =
render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::geometry::SkinnedMeshPass>()
&& let Some(texture_entry) =
registry_entry_by_name(&world.resources.texture_cache.registry, &name)
{
skinned_mesh_pass.register_texture(
name.clone(),
texture_entry.view.clone(),
texture_entry.sampler.clone(),
);
if let Some(layer) = layer_index {
skinned_mesh_pass.add_material_layer_mapping(
texture,
crate::render::wgpu::material_texture_arrays::MaterialTextureLayer {
usage,
layer,
wrap_u: sampler.wrap_u,
wrap_v: sampler.wrap_v,
},
);
}
}
if let Some(decal_pass) = render_graph_get_pass_mut(&mut self.graph, "decal_pass")
&& let Some(decal_pass) =
(decal_pass as &mut dyn std::any::Any).downcast_mut::<passes::DecalPass>()
&& let Some(texture_entry) =
registry_entry_by_name(&world.resources.texture_cache.registry, &name)
{
decal_pass.register_texture(
name,
texture_entry.view.clone(),
texture_entry.sampler.clone(),
);
}
}
pub(super) fn drain_loading_tasks(&mut self, world: &mut crate::ecs::world::World) {
let budget = world.resources.loading.pipeline.tasks_per_frame.max(1);
let mut uploaded_textures: Vec<crate::ecs::asset_id::TextureId> = Vec::new();
for _ in 0..budget {
let Some(task) = loading_pipeline_pop(&mut world.resources.loading.pipeline) else {
break;
};
let category = task.category();
let label = match &task {
crate::ecs::loading::LoadingTask::UploadDecodedTexture { texture, .. }
| crate::ecs::loading::LoadingTask::MaterializeTexture { texture, .. } => {
registry_name_for(&world.resources.texture_cache.registry, texture.index)
.map(str::to_string)
.unwrap_or_else(|| format!("texture {}", texture.index))
}
crate::ecs::loading::LoadingTask::DecodeImage { image, .. } => {
format!("image {}", image.0)
}
};
match task {
crate::ecs::loading::LoadingTask::UploadDecodedTexture {
texture,
rgba_data,
width,
height,
usage,
sampler,
} => {
self.upload_material_texture(
world,
MaterialTextureUploadRequest {
texture,
rgba_data: &rgba_data,
width,
height,
usage,
sampler,
},
);
uploaded_textures.push(texture);
}
crate::ecs::loading::LoadingTask::DecodeImage {
image,
encoded_bytes,
} => match crate::ecs::loading::decode_to_rgba8(&encoded_bytes) {
Ok(decoded) => {
loading_pipeline_store_decoded(
&mut world.resources.loading.pipeline,
image,
decoded,
);
}
Err(error) => {
tracing::warn!("decode failed for source image {}: {}", image.0, error);
}
},
crate::ecs::loading::LoadingTask::MaterializeTexture {
texture,
recipe,
usage,
sampler,
} => {
let produced = crate::ecs::loading::execute_texture_recipe(
&recipe,
&world.resources.loading.pipeline.decoded_images,
);
loading_pipeline_release_recipe_sources(
&mut world.resources.loading.pipeline,
&recipe,
);
if let Some(decoded) = produced {
let texture_name = world
.resources
.texture_cache
.registry
.index_to_name
.get(texture.index as usize)
.and_then(|slot| slot.clone());
if let Some(name) = texture_name {
world
.resources
.assets
.texture_sources
.entry(name)
.or_insert_with(|| crate::ecs::asset_state::TextureSourceBytes {
data: crate::ecs::asset_state::TextureSourceData::Rgba {
rgba: decoded.rgba.clone(),
width: decoded.width,
height: decoded.height,
},
usage,
sampler,
});
}
self.upload_material_texture(
world,
MaterialTextureUploadRequest {
texture,
rgba_data: &decoded.rgba,
width: decoded.width,
height: decoded.height,
usage,
sampler,
},
);
uploaded_textures.push(texture);
} else {
tracing::warn!("texture recipe produced no data for '{}'", label);
}
}
}
loading_pipeline_mark_completed(&mut world.resources.loading.pipeline, label, category);
}
let needs_resolve = !uploaded_textures.is_empty()
|| !world
.resources
.assets
.material_registry
.pending_resolve
.is_empty();
if needs_resolve {
crate::ecs::material::resources::material_registry_resolve_uploaded_textures(
&mut world.resources.assets.material_registry,
&world.resources.texture_cache,
&uploaded_textures,
);
world.resources.mesh_render_state.request_full_rebuild();
}
if !loading_pipeline_is_active(&world.resources.loading.pipeline) {
loading_pipeline_reset_source_arena(&mut world.resources.loading.pipeline);
}
let evicted = crate::render::wgpu::texture_cache::texture_cache_remove_unused(
&mut world.resources.texture_cache,
);
if !evicted.is_empty() {
for name in &evicted {
self.material_texture_arrays.release(name);
world.resources.assets.texture_sources.remove(name);
}
if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
&& let Some(mesh_pass) =
(mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
{
for name in &evicted {
mesh_pass.unregister_texture(name);
}
}
if let Some(skinned_mesh_pass) =
render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
&& let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
.downcast_mut::<passes::geometry::SkinnedMeshPass>(
)
{
for name in &evicted {
skinned_mesh_pass.unregister_texture(name);
}
}
if let Some(decal_pass) = render_graph_get_pass_mut(&mut self.graph, "decal_pass")
&& let Some(decal_pass) =
(decal_pass as &mut dyn std::any::Any).downcast_mut::<passes::DecalPass>()
{
for name in &evicted {
decal_pass.unregister_texture(name);
}
}
world.resources.mesh_render_state.request_full_rebuild();
}
}
}
struct SwappedWindowUi;
fn swap_window_ui_frame(
_world: &mut crate::ecs::world::World,
_target_size: (u32, u32),
) -> SwappedWindowUi {
SwappedWindowUi
}
fn restore_window_ui_frame(_world: &mut crate::ecs::world::World, _saved: SwappedWindowUi) {}
#[cfg(not(target_arch = "wasm32"))]
fn crop_center_square(image: image::RgbaImage) -> image::RgbaImage {
let (width, height) = image.dimensions();
if width == height {
return image;
}
let side = width.min(height);
let x = (width - side) / 2;
let y = (height - side) / 2;
image::imageops::crop_imm(&image, x, y, side, side).to_image()
}