//! Window → Rendering: active camera, project defaults, and volume list. use bevy::prelude::*; use bevy_egui::egui; use game::rendering::{ camera_auto_exposure_active, effective_viewport_render_stack, hdr_enabled_profile, SolariRaytracingSceneStats, }; use settings::{ ActiveCameraRenderProfile, ActiveVolumeContribution, ExposureMode, GiPath, ProjectSettings, RenderFallbackReason, RenderingCapabilities, }; use shared::{ ActorKind, AuthoringLightKind, LevelObject, LightDesc, PostProcessVolumeDesc, ProjectSun, }; use crate::history::{spawn_with_history, EditorEntitySnapshot}; use crate::project::settings_ui::{open_project_settings_panel, ProjectSettingsPanel}; use crate::selection::SelectedEntity; use crate::ui::helpers::focus_editor_camera_on_selection; use crate::viewport::EditorViewportMode; #[derive(Resource, Default)] pub struct RenderingDiagnosticsPanel { pub open: bool, tab: RenderingPanelTab, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] enum RenderingPanelTab { #[default] Active, Project, Volumes, } pub fn rendering_diagnostics_window(world: &mut World, ctx: &egui::Context, open: &mut bool) { if !*open { return; } egui::Window::new("Rendering") .open(open) .default_width(440.0) .show(ctx, |ui| { rendering_diagnostics_ui(world, ui); }); } pub fn rendering_diagnostics_ui(world: &mut World, ui: &mut egui::Ui) { let mut tab = world.resource::().tab; ui.horizontal(|ui| { ui.selectable_value(&mut tab, RenderingPanelTab::Active, "Active Camera"); ui.selectable_value(&mut tab, RenderingPanelTab::Project, "Project"); ui.selectable_value(&mut tab, RenderingPanelTab::Volumes, "Volumes"); }); world.resource_mut::().tab = tab; ui.separator(); match tab { RenderingPanelTab::Active => active_camera_tab(world, ui), RenderingPanelTab::Project => project_tab(world, ui), RenderingPanelTab::Volumes => volumes_tab(world, ui), } } fn active_camera_tab(world: &mut World, ui: &mut egui::Ui) { let profile = world.resource::().clone(); let caps = world.resource::().clone(); let contributions = world.resource::().clone(); let solari_stats = world.get_resource::().copied(); let local_shadow_lights = game::rendering::world_has_local_shadow_lights(world); let stack = effective_viewport_render_stack( profile.gi_path, &caps, solari_stats.as_ref(), local_shadow_lights, ); let gi_label = match ( stack.requested_gi_path, stack.effective_gi_path, stack.fallback_reason, ) { (GiPath::Forward, _, _) => "Forward PBR", (GiPath::SolariDeferred, GiPath::SolariDeferred, _) => "Solari deferred", (GiPath::SolariDeferred, _, Some(RenderFallbackReason::RtUnsupported)) => { "Solari requested, Forward (RT unsupported)" } (GiPath::SolariDeferred, _, None) => "Solari requested", }; ui.label(format!("GI path: {gi_label}")); ui.label(format!( "Stack: requested={:?}, effective={:?}, fallback={:?}", stack.requested_gi_path, stack.effective_gi_path, stack.fallback_reason )); if stack.requested_gi_path == GiPath::SolariDeferred && stack.effective_gi_path == GiPath::SolariDeferred { ui.colored_label( egui::Color32::from_rgb(255, 180, 100), "Solari deferred: directional lights and emissive meshes affect lighting; point/spot LightDesc components are disabled in Bevy 0.18.", ); } else if stack.fallback_reason == Some(RenderFallbackReason::RtUnsupported) || (!caps.rt_supported && profile.gi_path == GiPath::Forward) { ui.colored_label( egui::Color32::from_rgb(255, 180, 100), "Forward fallback — Solari RT features unavailable on this GPU.", ); } if stack.requested_gi_path == GiPath::SolariDeferred && !stack.solari_ready { ui.colored_label( egui::Color32::from_rgb(255, 180, 100), "Solari is active/requested but still warming up or missing scene readiness; see Solari raytracing scene below.", ); } runtime_lighting_summary(world, ui); solari_geometry_summary(world, ui); ui.separator(); let auto_active = camera_auto_exposure_active(&profile, &caps); match profile.exposure_mode { ExposureMode::Auto if auto_active => { ui.label(format!( "Exposure: Auto (brighten {:.1} / darken {:.1} f-stops/s)", profile.auto_exposure_speed_brighten, profile.auto_exposure_speed_darken )); } ExposureMode::Auto => { ui.label(format!( "Exposure: Auto requested — using manual {:.2} EV100", profile.exposure_ev100 )); if !profile.hdr { ui.colored_label( egui::Color32::from_rgb(255, 180, 100), "Fallback: HDR is disabled in project settings.", ); } else if !hdr_enabled_profile(&profile) { ui.colored_label( egui::Color32::from_rgb(255, 180, 100), "Fallback: HDR pass off (check BEVY_FPS_HDR or project HDR).", ); } else if !caps.auto_exposure_supported { ui.colored_label( egui::Color32::from_rgb(255, 180, 100), "Fallback: GPU compute shaders unavailable.", ); } } ExposureMode::Manual => { ui.label(format!( "Exposure: Manual ({:.2} EV100)", profile.exposure_ev100 )); let project = world.resource::(); if project.rendering.exposure_mode == ExposureMode::Auto { ui.small("Post-process volume overrides exposure EV100 (manual for this view)."); } } } ui.label(format!( "Fog: rgb({:.2},{:.2},{:.2}) density {:.4}", profile.fog_color[0], profile.fog_color[1], profile.fog_color[2], profile.fog_density )); ui.label(format!( "Post-FX: hdr={} target_hdr={} bloom={} taa={} ssao={} atmosphere={} tonemap={}", profile.hdr, hdr_enabled_profile(&profile) || profile.atmosphere || stack.effective_gi_path == GiPath::SolariDeferred, profile.bloom, profile.taa, profile.ssao, profile.atmosphere, profile.tonemapping_aces )); if let Some(effect) = &profile.fullscreen_effect { ui.label(format!("Fullscreen effect: {effect}")); } ui.separator(); ui.heading("Contributing volumes"); if contributions.entries.is_empty() { ui.label("No volumes at camera position — using project defaults."); } else { for entry in &contributions.entries { ui.label(format!( "{} — {:.0}% weight, {} overrides", entry.label, entry.weight * 100.0, entry.override_count )); } } if ui.button("Select overlapping volumes at camera").clicked() { select_volumes_at_camera(world); } if let Some(mode) = world.get_resource::() { ui.separator(); let mode_label = match *mode { EditorViewportMode::Lit => "Lit", EditorViewportMode::Unlit => "Unlit", EditorViewportMode::Collider => "Collider", }; ui.label(format!("Viewport mode: {mode_label}")); } } fn runtime_lighting_summary(world: &mut World, ui: &mut egui::Ui) { ui.separator(); ui.heading("Runtime lights"); let directional = world.query::<&DirectionalLight>().iter(world).count(); let point = world.query::<&PointLight>().iter(world).count(); let spot = world.query::<&SpotLight>().iter(world).count(); let shadowed_directional = world .query::<&DirectionalLight>() .iter(world) .filter(|light| light.shadows_enabled) .count(); let shadowed_point = world .query::<&PointLight>() .iter(world) .filter(|light| light.shadows_enabled) .count(); let shadowed_spot = world .query::<&SpotLight>() .iter(world) .filter(|light| light.shadows_enabled) .count(); ui.label(format!( "Counts: {directional} directional, {point} point, {spot} spot" )); ui.label(format!( "Shadow casters: {shadowed_directional} directional, {shadowed_point} point, {shadowed_spot} spot" )); let scene_directionals: Vec<(f32, bool)> = world .query_filtered::<&LightDesc, With>() .iter(world) .filter_map(|light| { matches!(light.kind, AuthoringLightKind::Directional) .then_some((light.intensity, light.shadows)) }) .collect(); if scene_directionals.is_empty() { ui.label("Scene sun: none (project sun drives outdoor lighting)"); } else { let (lux, shadows) = scene_directionals[0]; ui.label(format!( "Scene sun override: {lux:.0} lux, shadows={shadows} ({} directional LightDesc in level)", scene_directionals.len() )); } let project_sun_active = world .query_filtered::<&Visibility, With>() .iter(world) .any(|visibility| *visibility != Visibility::Hidden); if project_sun_active { ui.label("Project sun: active"); } else if !scene_directionals.is_empty() { ui.label("Project sun: hidden (scene directional override)"); } else { ui.colored_label( egui::Color32::from_rgb(255, 140, 140), "Project sun: hidden and no scene directional — level may appear black.", ); } if directional + point + spot == 0 { ui.colored_label( egui::Color32::from_rgb(255, 140, 140), "Zero runtime lights in the world.", ); } } fn solari_geometry_summary(world: &World, ui: &mut egui::Ui) { let Some(stats) = world.get_resource::() else { return; }; ui.separator(); ui.heading("Solari raytracing scene"); ui.label(format!( "Project meshes: {} tagged, {} eligible, {} Solari-compatible assets, {} unsupported, {} excluded helpers, {} outside level roots", stats.tagged_meshes, stats.project_meshes, stats.compatible_mesh_assets, stats.unsupported_meshes, stats.excluded_meshes, stats.missing_level_root_meshes )); if stats.incompatible_mesh_assets > 0 { ui.colored_label( egui::Color32::from_rgb(255, 180, 100), format!( "Incompatible mesh assets: {} ({} missing tangents, {} missing U32 indices)", stats.incompatible_mesh_assets, stats.mesh_assets_missing_tangents, stats.mesh_assets_missing_u32_indices ), ); } ui.label(format!( "Render scene: {} instances, {} directional lights, bind group ready={}", stats.render_instances, stats.render_directional_lights, stats.render_bind_group_ready )); ui.label(format!( "Solari-compatible lights: {} directional/emissive", stats.compatible_light_count() )); if stats.solari_ready() { ui.label("Readiness: Solari active"); } else { ui.colored_label( egui::Color32::from_rgb(255, 180, 100), format!("Readiness: {}", solari_readiness_reason(stats)), ); } } fn solari_readiness_reason(stats: &SolariRaytracingSceneStats) -> &'static str { if stats.tagged_meshes == 0 { "no project meshes are tagged for raytracing" } else if stats.compatible_mesh_assets == 0 { "tagged meshes are not Solari-compatible mesh assets" } else if stats.render_instances == 0 { "raytracing meshes have not reached the render world" } else if !stats.render_bind_group_ready { "Bevy has not built the raytracing scene bind group" } else if stats.compatible_light_count() == 0 { "no extracted directional or emissive Solari light source" } else { "waiting for Solari readiness" } } fn project_tab(world: &mut World, ui: &mut egui::Ui) { let rendering = world.resource::().rendering.clone(); ui.label(format!("GiMode: {:?}", rendering.gi_mode)); ui.label(format!("HDR: {}", rendering.hdr)); if rendering.exposure_mode == ExposureMode::Auto { ui.label(format!( "Exposure: Auto (brighten {:.1} / darken {:.1} f-stops/s)", rendering.auto_exposure_speed_brighten, rendering.auto_exposure_speed_darken )); } else { ui.label(format!( "Exposure: Manual ({:.2} EV100)", rendering.exposure_ev100 )); } ui.label(format!( "Ambient brightness: {:.1}", rendering.ambient_brightness )); ui.label(format!( "Sun illuminance: {:.0} lux", rendering.sun_illuminance )); ui.label(format!( "Post-FX: bloom={} taa={} ssao={} atmosphere={}", rendering.bloom, rendering.taa, rendering.ssao, rendering.atmosphere )); if ui.button("Open Project Settings → Rendering").clicked() { let mut panel = world.resource_mut::(); open_project_settings_panel(&mut panel); } if ui.button("Scene → Lighting → Use project sun").clicked() { crate::ui::helpers::use_project_sun(world); } if ui.button("Reset scene lighting to project").clicked() { crate::ui::helpers::reset_scene_lighting_to_project_defaults(world); } } fn volumes_tab(world: &mut World, ui: &mut egui::Ui) { let mut volumes: Vec<(Entity, PostProcessVolumeDesc, String)> = world .query_filtered::<(Entity, &PostProcessVolumeDesc, Option<&Name>), With>() .iter(world) .map(|(entity, desc, name)| { let label = desc .label .clone() .or_else(|| name.map(|n| n.to_string())) .unwrap_or_else(|| format!("Volume {entity:?}")); (entity, desc.clone(), label) }) .collect(); volumes.sort_by(|a, b| b.1.priority.cmp(&a.1.priority).then_with(|| a.0.cmp(&b.0))); if volumes.is_empty() { ui.label("No post-process volumes in this level."); return; } for (entity, desc, label) in volumes { ui.horizontal(|ui| { if ui.button(&label).clicked() { world.resource_mut::().0 = Some(entity); } ui.label(format!("priority {}", desc.priority)); if ui.small_button("Focus").clicked() { world.resource_mut::().0 = Some(entity); focus_editor_camera_on_selection(world); } }); } } pub fn select_volumes_at_camera(world: &mut World) { use game_hot::sample_volumes_at; use settings::ProjectRenderCamera; let camera_pos = world .query_filtered::<&GlobalTransform, With>() .iter(world) .next() .map(|t| t.translation()) .unwrap_or(Vec3::ZERO); let volume_list: Vec<_> = world .query_filtered::<(Entity, &PostProcessVolumeDesc, &GlobalTransform), With>() .iter(world) .collect(); let samples = sample_volumes_at(camera_pos, &volume_list); if !samples.is_empty() { world.resource_mut::().0 = Some(samples[0]); } } pub fn spawn_post_process_volume_at_camera(world: &mut World) { use shared::EditorVisibility; let translation = world .query_filtered::<&GlobalTransform, With>() .iter(world) .next() .map(|t| t.translation()) .unwrap_or(Vec3::new(0.0, 2.0, 0.0)); let snapshot = EditorEntitySnapshot { actor_id: None, actor_kind: ActorKind::PostProcessVolume, actor_name: None, name: Some("Post Process Volume".into()), transform: Transform::from_translation(translation), primitive: None, brush: None, static_mesh_renderer: None, material: None, material_override: None, rigid_body: None, collider: None, physics: None, light: None, player_spawn: false, model: None, prefab: None, prefab_instance: None, weapon_spawn: None, trigger_volume: None, post_process_volume: Some(PostProcessVolumeDesc::default()), team_spawn: None, objective: None, hierarchy_sibling_index: 0, editor_visibility: EditorVisibility::default(), children: Vec::new(), }; spawn_with_history(world, snapshot); }