use super::DEPTH_PICK_SAMPLE_SIZE;
use super::PassMode;
use super::font_atlas;
use super::passes;
use super::texture_cache::texture_cache_load_from_raw_rgba;
use crate::ecs::generational_registry::registry_entry_by_name;
impl crate::ecs::world::Render for super::WgpuRenderer {
fn render_frame(
&mut self,
world: &mut crate::ecs::world::World,
ui_output: Option<crate::ecs::world::UiOutput>,
ui_primitives: Option<crate::ecs::world::UiPrimitives>,
) -> Result<(), Box<dyn std::error::Error>> {
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) = world.resources.active_camera
&& let Some(camera) = world.core.get_camera(camera_entity)
&& let Some(global_transform) = world.core.get_global_transform(camera_entity)
{
let (viewport_width, viewport_height) = (
self.surface_config.width as f32,
self.surface_config.height as f32,
);
let aspect_ratio = viewport_width / viewport_height;
let view_matrix = global_transform
.0
.try_inverse()
.unwrap_or_else(nalgebra_glm::Mat4::identity);
let projection_matrix = camera.projection.matrix_with_aspect(aspect_ratio);
let inverse_view_proj = (projection_matrix * view_matrix)
.try_inverse()
.unwrap_or_else(nalgebra_glm::Mat4::identity);
world.resources.gpu_picking.compute_result(
&inverse_view_proj,
viewport_width,
viewport_height,
);
}
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;
std::thread::spawn(move || {
match image::RgbaImage::from_raw(save_width, save_height, pixels) {
Some(image) => {
if let Err(error) = image.save(&path) {
tracing::error!("Failed to save screenshot: {}", error);
} else {
tracing::info!("Screenshot saved to: {}", path.display());
}
}
None => {
tracing::error!("Failed to create image from screenshot data");
}
}
});
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 sprite_commands = Vec::new();
let mut texture_load_commands = 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();
for cmd in world.resources.command_queue.iter() {
match cmd {
crate::ecs::world::WorldCommand::UploadSpriteTexture {
slot,
rgba_data,
width,
height,
} => {
sprite_commands.push((*slot, rgba_data.clone(), *width, *height));
}
crate::ecs::world::WorldCommand::LoadTexture {
name,
rgba_data,
width,
height,
} => {
texture_load_commands.push((name.clone(), rgba_data.clone(), *width, *height));
}
crate::ecs::world::WorldCommand::ReloadTexture {
name,
rgba_data,
width,
height,
} => {
texture_reload_commands.push((
name.clone(),
rgba_data.clone(),
*width,
*height,
));
}
crate::ecs::world::WorldCommand::LoadHdrSkybox { hdr_data } => {
hdr_skybox_commands.push(hdr_data.clone());
}
crate::ecs::world::WorldCommand::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::WorldCommand::CaptureProceduralAtmosphereIBL {
atmosphere,
time,
} => {
procedural_ibl_commands.push((*atmosphere, *time));
}
crate::ecs::world::WorldCommand::CaptureIblSnapshots { atmosphere, hours } => {
ibl_snapshot_commands.push((*atmosphere, hours.clone()));
}
_ => {}
}
}
if !sprite_commands.is_empty() {
if let Some(sprite_pass) = self.graph.get_pass_mut("sprite_pass")
&& let Some(sprite_pass) =
(sprite_pass as &mut dyn std::any::Any).downcast_mut::<passes::SpritePass>()
{
for (slot, rgba_data, width, height) in sprite_commands {
sprite_pass.upload_texture_to_atlas(
&self.queue,
slot,
&rgba_data,
width,
height,
);
}
}
world.resources.command_queue.retain(|cmd| {
!matches!(
cmd,
crate::ecs::world::WorldCommand::UploadSpriteTexture { .. }
)
});
}
if !texture_load_commands.is_empty() {
for (name, rgba_data, width, height) in texture_load_commands {
if let Err(e) = texture_cache_load_from_raw_rgba(
&mut world.resources.texture_cache,
&self.device,
&self.queue,
name.clone(),
&rgba_data,
(width, height),
) {
tracing::error!("Failed to load texture: {}", e);
continue;
}
if let Some(mesh_pass) = self.graph.get_pass_mut("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(),
rgba_data.clone(),
width,
height,
);
}
if let Some(skinned_mesh_pass) = self.graph.get_pass_mut("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) = self.graph.get_pass_mut("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(),
);
}
}
world
.resources
.command_queue
.retain(|cmd| !matches!(cmd, crate::ecs::world::WorldCommand::LoadTexture { .. }));
}
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) {
if let Some(mesh_pass) = self.graph.get_pass_mut("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(),
rgba_data.clone(),
width,
height,
);
}
if let Some(skinned_mesh_pass) = self.graph.get_pass_mut("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) = self.graph.get_pass_mut("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(),
);
}
}
}
world.resources.command_queue.retain(|cmd| {
!matches!(cmd, crate::ecs::world::WorldCommand::ReloadTexture { .. })
});
}
#[cfg(feature = "assets")]
if !hdr_skybox_commands.is_empty() {
let mut ibl_views = None;
if let Some(sky_pass) = self.graph.get_pass_mut("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) = self.graph.get_pass_mut("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) = self.graph.get_pass_mut("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);
}
world.resources.command_queue.retain(|cmd| {
!matches!(
cmd,
crate::ecs::world::WorldCommand::LoadHdrSkybox { .. }
| crate::ecs::world::WorldCommand::LoadHdrSkyboxFromPath { .. }
)
});
}
#[cfg(feature = "assets")]
if !procedural_ibl_commands.is_empty() {
let mut ibl_views = None;
if let Some(sky_pass) = self.graph.get_pass_mut("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) = self.graph.get_pass_mut("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) = self.graph.get_pass_mut("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);
}
world.resources.command_queue.retain(|cmd| {
!matches!(
cmd,
crate::ecs::world::WorldCommand::CaptureProceduralAtmosphereIBL { .. }
)
});
}
#[cfg(feature = "assets")]
if !ibl_snapshot_commands.is_empty() {
if let Some(sky_pass) = self.graph.get_pass_mut("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);
}
}
world.resources.command_queue.retain(|cmd| {
!matches!(
cmd,
crate::ecs::world::WorldCommand::CaptureIblSnapshots { .. }
)
});
}
if let Some(lines_pass) = self.graph.get_pass_mut("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);
passes::sync_bounding_volume_data(
lines_pass,
&self.device,
&self.queue,
world,
world.resources.graphics.show_bounding_volumes,
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,
world.resources.graphics.show_normals,
world.resources.graphics.normal_line_length,
world.resources.graphics.normal_line_color,
);
}
#[cfg(feature = "egui")]
if let Some(ref ui_output) = ui_output
&& let Some(ref ui_primitives) = ui_primitives
{
self.prepare_egui_pass(ui_output, ui_primitives);
}
#[cfg(not(feature = "egui"))]
let _ = (ui_output, ui_primitives);
let point_shadow_view = self
.graph
.get_pass_mut("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) = self.graph.get_pass_mut("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) = self.graph.get_pass_mut("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) = self.graph.get_pass_mut("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) = self.graph.get_pass_mut("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) = self.graph.get_pass_mut("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,
});
self.graph.set_external_texture(
self.swapchain_id,
Some(surface_texture.texture.clone()),
surface_texture_view,
self.surface_config.width,
self.surface_config.height,
);
let required_cameras = world.resources.user_interface.required_cameras.clone();
self.cleanup_unused_camera_viewports(&required_cameras);
#[cfg(feature = "egui")]
{
world.resources.user_interface.viewport_textures.clear();
world
.resources
.user_interface
.viewport_texture_sizes
.clear();
}
let has_viewports = !required_cameras.is_empty();
let _ = self
.graph
.set_pass_enabled("viewport_blit_pass", has_viewports);
if !has_viewports {
self.resize_render_buffers(self.surface_config.width, self.surface_config.height);
}
let graphics = &world.resources.graphics;
let _ = self
.graph
.set_pass_enabled("bloom_pass", graphics.bloom_enabled);
let _ = self
.graph
.set_pass_enabled("ssao_pass", graphics.ssao_enabled);
let _ = self
.graph
.set_pass_enabled("ssao_blur_pass", graphics.ssao_enabled);
let _ = self
.graph
.set_pass_enabled("ssr_pass", graphics.ssr_enabled);
let _ = self
.graph
.set_pass_enabled("ssr_blur_pass", graphics.ssr_enabled);
let _ = self
.graph
.set_pass_enabled("ssgi_pass", graphics.ssgi_enabled);
let _ = self
.graph
.set_pass_enabled("ssgi_blur_pass", graphics.ssgi_enabled);
let original_active_camera = world.resources.active_camera;
for camera_entity in &required_cameras {
#[cfg(feature = "egui")]
self.ensure_camera_viewport(
*camera_entity,
self.surface_config.width,
self.surface_config.height,
);
let viewport_cloned = self
.camera_viewports
.get(camera_entity)
.map(|viewport| (viewport.texture.clone(), viewport.view.clone()));
#[cfg(feature = "egui")]
let viewport_egui = self
.camera_viewports
.get(camera_entity)
.map(|viewport| (viewport.texture_id, viewport.size));
if let Some((texture, view)) = viewport_cloned {
self.graph.set_external_texture(
self.viewport_resource_id,
Some(texture),
view,
self.surface_config.width,
self.surface_config.height,
);
world.resources.active_camera = Some(*camera_entity);
self.extract_frame_dirty_state(world);
let command_buffers = self.graph.execute(&self.device, &self.queue, world)?;
self.queue.submit(command_buffers);
#[cfg(feature = "egui")]
if let Some((texture_id, size)) = viewport_egui {
world
.resources
.user_interface
.viewport_textures
.push(texture_id);
world
.resources
.user_interface
.viewport_texture_sizes
.push(size);
}
}
}
world.resources.active_camera = original_active_camera;
if required_cameras.is_empty() {
self.extract_frame_dirty_state(world);
let command_buffers = self.graph.execute(&self.device, &self.queue, world)?;
self.queue.submit(command_buffers);
}
#[cfg(not(target_arch = "wasm32"))]
{
let screenshot_command = world.resources.command_queue.iter().find_map(|cmd| {
if let crate::ecs::world::WorldCommand::CaptureScreenshot { path } = cmd {
Some(path.clone())
} else {
None
}
});
if !self.screenshot_pending
&& let Some(path) = screenshot_command
&& let Some(compute_output_texture) = self.graph.get_texture(self.compute_output_id)
{
world.resources.command_queue.retain(|cmd| {
!matches!(
cmd,
crate::ecs::world::WorldCommand::CaptureScreenshot { .. }
)
});
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_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);
},
);
}
}
if !self.depth_pick_pending
&& let Some(request) = world.resources.gpu_picking.take_pending_request()
&& let Some(depth_texture_view) = self.graph.get_texture_view(self.depth_id)
&& let Some(entity_id_texture_view) = self.graph.get_texture_view(self.entity_id_id)
{
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);
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);
});
}
surface_texture.present();
Ok(())
}
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(())
}
fn surface_size(&self) -> (u32, u32) {
(self.surface_config.width, self.surface_config.height)
}
fn configure_with_state(
&mut self,
state: &mut dyn crate::run::State,
) -> Result<(), Box<dyn std::error::Error>> {
let msdf_atlas = font_atlas::generate_font_atlas_msdf(
&self.device,
&self.queue,
font_atlas::DEFAULT_FONT_DATA,
32.0,
)?;
self.msdf_font_textures
.push((msdf_atlas.texture, msdf_atlas.texture_view));
let msdf_texture_index = (self.msdf_font_textures.len() - 1) as u32;
let msdf_font_data = crate::ecs::text::resources::FontAtlasData {
texture_index: msdf_texture_index,
glyphs: msdf_atlas.glyphs,
kerning: msdf_atlas.kerning.clone(),
width: msdf_atlas.width,
height: msdf_atlas.height,
font_size: msdf_atlas.font_size,
sdf_range: msdf_atlas.sdf_range,
};
self.font_data.push(msdf_font_data);
for (source, base_size) in [
(font_atlas::DEFAULT_FONT_DATA, 18.0_f32),
(font_atlas::DEFAULT_FONT_DATA, 28.0_f32),
(font_atlas::DEFAULT_MONO_FONT_DATA, 18.0_f32),
(font_atlas::DEFAULT_MONO_FONT_DATA, 28.0_f32),
] {
let bitmap_atlas = font_atlas::generate_font_atlas_bitmap(
&self.device,
&self.queue,
source,
base_size,
1.0,
self.bitmap_atlas_subpixel,
)?;
self.bitmap_font_sources.push((source.to_vec(), base_size));
self.bitmap_font_textures
.push((bitmap_atlas.texture, bitmap_atlas.texture_view));
let bitmap_texture_index = (self.bitmap_font_textures.len() - 1) as u32;
let bitmap_font_data = crate::ecs::text::resources::FontAtlasData {
texture_index: bitmap_texture_index,
glyphs: bitmap_atlas.glyphs,
kerning: bitmap_atlas.kerning,
width: bitmap_atlas.width,
height: bitmap_atlas.height,
font_size: bitmap_atlas.font_size,
sdf_range: bitmap_atlas.sdf_range,
};
self.bitmap_font_data.push(bitmap_font_data);
}
let mut text_pass = passes::TextPass::new(
&self.device,
wgpu::TextureFormat::Rgba16Float,
wgpu::TextureFormat::Depth32Float,
);
text_pass.update_texture_bind_groups(&self.device, &self.msdf_font_textures);
self.graph.add_pass(
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");
self.graph.add_pass(
Box::new(viewport_blit_pass),
&[
("input", self.compute_output_id),
("output", self.viewport_resource_id),
],
)?;
if let Some(ui_image_pass) = self.ui_image_pass.take() {
self.graph.add_pass(
ui_image_pass,
&[("color", self.swapchain_id), ("depth", self.ui_depth_id)],
)?;
}
let mut ui_pass = passes::UiPass::new(&self.device, self.surface_format);
ui_pass.update_texture_bind_groups(&self.device, &self.bitmap_font_textures);
self.graph.add_pass(
Box::new(ui_pass),
&[("color", self.swapchain_id), ("depth", self.ui_depth_id)],
)?;
#[cfg(feature = "egui")]
if let Some(egui_pass) = self.egui_pass.take() {
self.graph
.add_pass(egui_pass, &[("swapchain", self.swapchain_id)])?;
}
self.graph.compile()?;
Ok(())
}
fn initialize_fonts(&mut self, world: &mut crate::ecs::world::World) {
if !self.font_initialized {
world.resources.retained_ui.subpixel_text_rendering = !cfg!(target_arch = "wasm32");
for font_data in &self.font_data {
let font_index = world
.resources
.text_cache
.font_manager
.add_font(font_data.clone());
if world.resources.text_cache.font_manager.default_font == 0 {
world.resources.text_cache.font_manager.default_font = font_index;
}
}
for bitmap_data in &self.bitmap_font_data {
world
.resources
.text_cache
.font_manager
.add_bitmap_font(bitmap_data.clone());
}
self.font_initialized = true;
}
}
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.pending_font_loads.drain(..) {
let msdf_atlas = match font_atlas::generate_font_atlas_msdf(
&self.device,
&self.queue,
&pending_load.font_data,
pending_load.font_size,
) {
Ok(atlas) => atlas,
Err(e) => {
tracing::error!("Failed to load MSDF font: {}", e);
continue;
}
};
self.msdf_font_textures
.push((msdf_atlas.texture, msdf_atlas.texture_view));
let msdf_texture_index = (self.msdf_font_textures.len() - 1) as u32;
let msdf_font_data = crate::ecs::text::resources::FontAtlasData {
texture_index: msdf_texture_index,
glyphs: msdf_atlas.glyphs,
kerning: msdf_atlas.kerning.clone(),
width: msdf_atlas.width,
height: msdf_atlas.height,
font_size: msdf_atlas.font_size,
sdf_range: msdf_atlas.sdf_range,
};
let font_index = world
.resources
.text_cache
.font_manager
.add_font(msdf_font_data);
let mut bitmap_load_failed = false;
for base_size in [pending_load.font_size, 28.0_f32] {
let bitmap_atlas = match font_atlas::generate_font_atlas_bitmap(
&self.device,
&self.queue,
&pending_load.font_data,
base_size,
self.bitmap_atlas_scale,
self.bitmap_atlas_subpixel,
) {
Ok(atlas) => atlas,
Err(e) => {
tracing::error!("Failed to load bitmap font: {}", e);
bitmap_load_failed = true;
break;
}
};
self.bitmap_font_sources
.push((pending_load.font_data.clone(), base_size));
self.bitmap_font_textures
.push((bitmap_atlas.texture, bitmap_atlas.texture_view));
let bitmap_texture_index = (self.bitmap_font_textures.len() - 1) as u32;
let bitmap_font_data = crate::ecs::text::resources::FontAtlasData {
texture_index: bitmap_texture_index,
glyphs: bitmap_atlas.glyphs,
kerning: bitmap_atlas.kerning,
width: bitmap_atlas.width,
height: bitmap_atlas.height,
font_size: bitmap_atlas.font_size,
sdf_range: bitmap_atlas.sdf_range,
};
world
.resources
.text_cache
.font_manager
.add_bitmap_font(bitmap_font_data);
}
if bitmap_load_failed {
continue;
}
if let Some(text_pass) = self.graph.get_pass_mut("text_pass")
&& let Some(text_pass) =
(text_pass as &mut dyn std::any::Any).downcast_mut::<passes::TextPass>()
{
text_pass.update_texture_bind_groups(&self.device, &self.msdf_font_textures);
}
if let Some(ui_pass) = self.graph.get_pass_mut("ui_pass")
&& let Some(ui_pass) =
(ui_pass as &mut dyn std::any::Any).downcast_mut::<passes::UiPass>()
{
ui_pass.update_texture_bind_groups(&self.device, &self.bitmap_font_textures);
}
tracing::info!("Loaded custom font at index {}", font_index);
}
let new_scale = world.resources.window.cached_scale_factor;
let new_subpixel = world.resources.retained_ui.subpixel_text_rendering;
if (new_scale - self.bitmap_atlas_scale).abs() > f32::EPSILON
|| new_subpixel != self.bitmap_atlas_subpixel
{
self.regenerate_bitmap_atlases(new_scale, new_subpixel, world);
}
state.update_render_graph(&mut self.graph, world);
Ok(())
}
fn copy_fonts_to_world(&self, world: &mut crate::ecs::world::World) {
if world.resources.text_cache.font_manager.font_count() == 0 {
for font_data in &self.font_data {
let font_index = world
.resources
.text_cache
.font_manager
.add_font(font_data.clone());
if world.resources.text_cache.font_manager.default_font == 0 {
world.resources.text_cache.font_manager.default_font = font_index;
}
}
for bitmap_data in &self.bitmap_font_data {
world
.resources
.text_cache
.font_manager
.add_bitmap_font(bitmap_data.clone());
}
}
}
fn render_world_to_texture(
&mut self,
world: &mut crate::ecs::world::World,
target_texture: Option<&wgpu::Texture>,
target_view: &wgpu::TextureView,
width: u32,
height: u32,
) -> Result<(), Box<dyn std::error::Error>> {
super::WgpuRenderer::render_world_to_texture(
self,
world,
target_texture,
target_view,
width,
height,
)
}
fn device(&self) -> &wgpu::Device {
&self.device
}
fn queue(&self) -> &wgpu::Queue {
&self.queue
}
fn surface_format(&self) -> wgpu::TextureFormat {
self.surface_format
}
#[cfg(feature = "egui")]
fn register_egui_texture(&mut self, view: &wgpu::TextureView) -> Option<egui::TextureId> {
if let Some(egui_pass) = self.graph.get_pass_mut("egui_pass")
&& let Some(egui_pass) =
(egui_pass as &mut dyn std::any::Any).downcast_mut::<passes::EguiPass>()
{
Some(egui_pass.register_texture(&self.device, view))
} else {
None
}
}
#[cfg(feature = "egui")]
fn register_secondary_egui_texture(
&mut self,
id: usize,
view: &wgpu::TextureView,
) -> Option<egui::TextureId> {
let secondary = self.secondary_surfaces.get_mut(&id)?;
let renderer = secondary.egui_renderer.as_mut()?;
Some(renderer.register_native_texture(&self.device, view, wgpu::FilterMode::Linear))
}
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) = self.graph.get_pass_mut("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);
}
}
fn create_secondary_surface(
&mut self,
id: usize,
window: std::sync::Arc<winit::window::Window>,
width: u32,
height: u32,
) -> Result<(), Box<dyn std::error::Error>> {
let surface = self.instance.create_surface(window)?;
let capabilities = surface.get_capabilities(&self.adapter);
let format = capabilities
.formats
.iter()
.copied()
.find(|f| !f.is_srgb())
.unwrap_or(capabilities.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: width.max(1),
height: height.max(1),
present_mode: capabilities.present_modes[0],
alpha_mode: capabilities.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&self.device, &config);
self.secondary_surfaces.insert(
id,
super::SecondarySurface {
surface,
config,
format,
width: width.max(1),
height: height.max(1),
#[cfg(feature = "egui")]
egui_renderer: None,
#[cfg(feature = "egui")]
egui_depth_texture: None,
#[cfg(feature = "egui")]
egui_depth_view: None,
#[cfg(feature = "egui")]
egui_depth_size: (0, 0),
},
);
Ok(())
}
fn remove_secondary_surface(&mut self, id: usize) {
self.secondary_surfaces.remove(&id);
#[cfg(feature = "egui")]
self.secondary_camera_viewport_cache.remove(&id);
}
fn resize_secondary_surface(
&mut self,
id: usize,
width: u32,
height: u32,
) -> Result<(), Box<dyn std::error::Error>> {
if width == 0 || height == 0 {
return Ok(());
}
if let Some(secondary) = self.secondary_surfaces.get_mut(&id) {
secondary.config.width = width;
secondary.config.height = height;
secondary.width = width;
secondary.height = height;
secondary.surface.configure(&self.device, &secondary.config);
}
Ok(())
}
fn render_world_to_secondary_surface(
&mut self,
id: usize,
world: &mut crate::ecs::world::World,
) -> Result<(), Box<dyn std::error::Error>> {
super::WgpuRenderer::render_to_secondary_surface(
self,
id,
world,
PassMode::World { enable_ui: true },
)
}
fn render_retained_ui_to_secondary_surface(
&mut self,
id: usize,
world: &mut crate::ecs::world::World,
) -> Result<(), Box<dyn std::error::Error>> {
super::WgpuRenderer::render_to_secondary_surface(self, id, world, PassMode::UiOnly)
}
fn secondary_surface_size(&self, id: usize) -> Option<(u32, u32)> {
self.secondary_surfaces
.get(&id)
.map(|secondary| (secondary.width, secondary.height))
}
fn initialize_secondary_egui(&mut self, id: usize) {
#[cfg(not(feature = "egui"))]
let _ = id;
#[cfg(feature = "egui")]
self.initialize_secondary_egui_impl(id);
}
fn render_egui_to_secondary_surface(
&mut self,
id: usize,
ui_output: Option<crate::ecs::world::UiOutput>,
ui_primitives: Option<crate::ecs::world::UiPrimitives>,
) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "egui")]
return self.render_egui_to_secondary_surface_impl(id, ui_output, ui_primitives);
#[cfg(not(feature = "egui"))]
{
let _ = (id, ui_output, ui_primitives);
Ok(())
}
}
}