Path: blob/main/crates/bevy_dev_tools/src/render_debug.rs
9328 views
//! Renderer debugging overlay12use bevy_app::{App, Plugin};3use bevy_asset::{embedded_asset, Handle};4use bevy_core_pipeline::{5mip_generation::experimental::depth::ViewDepthPyramid,6oit::OrderIndependentTransparencySettingsOffset,7schedule::{Core3d, Core3dSystems},8tonemapping::tonemapping,9FullscreenShader,10};11use bevy_ecs::{12component::Component,13entity::Entity,14message::{Message, MessageReader, MessageWriter},15prelude::{Has, ReflectComponent, World},16reflect::ReflectResource,17resource::Resource,18schedule::IntoScheduleConfigs,19system::{Commands, Query, Res, ResMut},20world::FromWorld,21};22use bevy_input::{prelude::KeyCode, ButtonInput};23use bevy_log::info;24use bevy_reflect::{std_traits::ReflectDefault, Reflect};25use bevy_render::{26extract_component::{ExtractComponent, ExtractComponentPlugin},27extract_resource::{ExtractResource, ExtractResourcePlugin},28render_asset::RenderAssets,29render_resource::{30binding_types, BindGroupEntries, BindGroupLayout, BindGroupLayoutDescriptor,31BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, ColorWrites,32DynamicUniformBuffer, FragmentState, Operations, PipelineCache, RenderPassColorAttachment,33RenderPassDescriptor, RenderPipelineDescriptor, Sampler, SamplerDescriptor, ShaderStages,34ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureFormat,35TextureSampleType, VertexState,36},37renderer::{RenderContext, RenderDevice, RenderQueue, ViewQuery},38texture::{FallbackImage, GpuImage},39view::{Msaa, ViewTarget, ViewUniformOffset},40Render, RenderApp, RenderSystems,41};42use bevy_shader::Shader;4344use bevy_pbr::{45Bluenoise, MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup,46ViewContactShadowsUniformOffset, ViewEnvironmentMapUniformOffset, ViewFogUniformOffset,47ViewLightProbesUniformOffset, ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset,48};4950/// Adds a rendering debug overlay to visualize various renderer buffers.51#[derive(Default)]52pub struct RenderDebugOverlayPlugin;5354impl Plugin for RenderDebugOverlayPlugin {55fn build(&self, app: &mut App) {56embedded_asset!(app, "debug_overlay.wgsl");5758app.register_type::<RenderDebugOverlay>()59.init_resource::<RenderDebugOverlay>()60.add_message::<RenderDebugOverlayEvent>()61.add_plugins((62ExtractResourcePlugin::<RenderDebugOverlay>::default(),63ExtractComponentPlugin::<RenderDebugOverlay>::default(),64))65.add_systems(bevy_app::Update, (handle_input, update_overlay).chain());66}6768fn finish(&self, app: &mut App) {69let Some(render_app) = app.get_sub_app_mut(RenderApp) else {70return;71};7273render_app74.init_resource::<RenderDebugOverlayPipeline>()75.init_resource::<SpecializedRenderPipelines<RenderDebugOverlayPipeline>>()76.init_resource::<RenderDebugOverlayUniforms>()77.add_systems(78Render,79(80prepare_debug_overlay_pipelines.in_set(RenderSystems::Prepare),81prepare_debug_overlay_resources.in_set(RenderSystems::PrepareResources),82),83)84.add_systems(85Core3d,86render_debug_overlay87.after(tonemapping)88.in_set(Core3dSystems::PostProcess),89);90}91}9293/// Automatically attach keybinds to make render debug overlays available to users without code94/// changes when the feature is enabled.95pub fn handle_input(96keyboard: Res<ButtonInput<KeyCode>>,97mut events: MessageWriter<RenderDebugOverlayEvent>,98) {99if keyboard.just_pressed(KeyCode::F1) {100events.write(RenderDebugOverlayEvent::CycleMode);101}102if keyboard.just_pressed(KeyCode::F2) {103events.write(RenderDebugOverlayEvent::CycleOpacity);104}105}106107/// Listen to messages to update the debug overlay configuration.108pub fn update_overlay(109mut commands: Commands,110mut events: MessageReader<RenderDebugOverlayEvent>,111mut config_res: ResMut<RenderDebugOverlay>,112cameras: Query<113(114Entity,115Option<&RenderDebugOverlay>,116Has<bevy_core_pipeline::prepass::DepthPrepass>,117Has<bevy_core_pipeline::prepass::NormalPrepass>,118Has<bevy_core_pipeline::prepass::MotionVectorPrepass>,119Has<bevy_core_pipeline::prepass::DeferredPrepass>,120Has<bevy_render::occlusion_culling::OcclusionCulling>,121Has<bevy_pbr::ScreenSpaceReflections>,122),123bevy_ecs::query::With<bevy_camera::Camera>,124>,125) {126let mut changed = false;127128for event in events.read() {129match event {130RenderDebugOverlayEvent::CycleMode => {131let modes = [132RenderDebugMode::Depth,133RenderDebugMode::Normal,134RenderDebugMode::MotionVectors,135RenderDebugMode::Deferred,136RenderDebugMode::DeferredBaseColor,137RenderDebugMode::DeferredEmissive,138RenderDebugMode::DeferredMetallicRoughness,139RenderDebugMode::DepthPyramid { mip_level: 0 },140];141142let is_supported = |mode: &RenderDebugMode| {143cameras.iter().any(144|(_, _, depth, normal, motion, deferred, occlusion, _ssr)| {145match mode {146RenderDebugMode::Depth => depth || deferred,147RenderDebugMode::Normal => normal || deferred,148RenderDebugMode::MotionVectors => motion,149RenderDebugMode::Deferred150| RenderDebugMode::DeferredBaseColor151| RenderDebugMode::DeferredEmissive152| RenderDebugMode::DeferredMetallicRoughness => deferred,153// We don't have a good way to check for DepthPyramid in the main154// world, but it usually depends on DepthPrepass.155// However, we can at least check if OcclusionCulling is present.156RenderDebugMode::DepthPyramid { .. } => depth && occlusion,157}158},159)160};161162if !config_res.enabled {163for mode in modes {164if is_supported(&mode) {165config_res.enabled = true;166config_res.mode = mode;167break;168}169}170} else {171let current_index = modes172.iter()173.position(|m| {174core::mem::discriminant(m) == core::mem::discriminant(&config_res.mode)175})176.unwrap_or(0);177178let mut next_mode = None;179180if let RenderDebugMode::DepthPyramid { mip_level } = config_res.mode181&& mip_level < 7182{183config_res.mode = RenderDebugMode::DepthPyramid {184mip_level: mip_level + 1,185};186next_mode = Some(config_res.mode);187}188189if next_mode.is_none() {190for i in 1..modes.len() {191let idx = (current_index + i) % modes.len();192if is_supported(&modes[idx]) {193next_mode = Some(modes[idx]);194break;195}196}197198if let Some(mode) = next_mode {199let next_index = modes200.iter()201.position(|m| {202core::mem::discriminant(m) == core::mem::discriminant(&mode)203})204.unwrap();205206if next_index <= current_index {207config_res.enabled = false;208} else {209config_res.mode = mode;210}211} else {212config_res.enabled = false;213}214}215}216changed = true;217218if config_res.enabled {219info!("Debug Overlay: {:?}", config_res.mode);220} else {221info!("Debug Overlay Disabled");222}223}224RenderDebugOverlayEvent::CycleOpacity => {225config_res.opacity = if config_res.opacity < 0.5 {2260.5227} else if config_res.opacity < 0.8 {2280.8229} else if config_res.opacity < 1.0 {2301.0231} else {2320.5233};234changed = true;235info!("Debug Overlay Opacity: {}", config_res.opacity);236}237}238}239240for (entity, existing_config, ..) in &cameras {241if existing_config.is_none() || (changed && Some(config_res.as_ref()) != existing_config) {242commands.entity(entity).insert(config_res.clone());243}244}245}246247/// Configure the render debug overlay.248#[derive(Message, Debug, Copy, Clone, PartialEq, Eq, Hash, Reflect)]249#[reflect(Debug, PartialEq, Hash)]250pub enum RenderDebugOverlayEvent {251/// Cycle to the next debug mode.252CycleMode,253/// Cycle to the next opacity level.254CycleOpacity,255}256257/// Configure the render debug overlay.258#[derive(Resource, Component, Clone, ExtractResource, ExtractComponent, Reflect, PartialEq)]259#[reflect(Resource, Component, Default)]260pub struct RenderDebugOverlay {261/// Enables or disables drawing the overlay.262pub enabled: bool,263/// The kind of data to write to the overlay.264pub mode: RenderDebugMode,265/// The opacity of the overlay, to allow seeing the rendered image underneath.266pub opacity: f32,267}268269impl Default for RenderDebugOverlay {270fn default() -> Self {271Self {272enabled: false,273mode: RenderDebugMode::Depth,274opacity: 1.0,275}276}277}278279/// The kind of renderer data to visualize.280#[expect(missing_docs, reason = "Enum variants are self-explanatory")]281#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Reflect)]282pub enum RenderDebugMode {283#[default]284Depth,285Normal,286MotionVectors,287Deferred,288DeferredBaseColor,289DeferredEmissive,290DeferredMetallicRoughness,291DepthPyramid {292mip_level: u32,293},294}295296#[derive(ShaderType)]297struct RenderDebugOverlayUniform {298pub opacity: f32,299pub mip_level: u32,300}301302#[derive(Resource, Default)]303struct RenderDebugOverlayUniforms {304pub uniforms: DynamicUniformBuffer<RenderDebugOverlayUniform>,305}306307#[derive(Component)]308struct RenderDebugOverlayUniformOffset {309pub offset: u32,310}311312#[derive(Resource)]313struct RenderDebugOverlayPipeline {314shader: Handle<Shader>,315mesh_view_layouts: MeshPipelineViewLayouts,316bind_group_layout: BindGroupLayout,317bind_group_layout_descriptor: BindGroupLayoutDescriptor,318sampler: Sampler,319fullscreen_vertex_shader: VertexState,320}321322impl FromWorld for RenderDebugOverlayPipeline {323fn from_world(world: &mut World) -> Self {324let render_device = world.resource::<RenderDevice>();325let asset_server = world.resource::<bevy_asset::AssetServer>();326let mesh_view_layouts = world.resource::<MeshPipelineViewLayouts>().clone();327let fullscreen_vertex_shader = world.resource::<FullscreenShader>().to_vertex_state();328329let sampler = render_device.create_sampler(&SamplerDescriptor::default());330331let bind_group_layout_descriptor = BindGroupLayoutDescriptor::new(332"debug_overlay_bind_group_layout",333&BindGroupLayoutEntries::sequential(334ShaderStages::FRAGMENT,335(336binding_types::uniform_buffer::<RenderDebugOverlayUniform>(true),337binding_types::texture_2d(TextureSampleType::Float { filterable: true }),338binding_types::sampler(339bevy_render::render_resource::SamplerBindingType::Filtering,340),341binding_types::texture_2d(TextureSampleType::Float { filterable: true }),342binding_types::sampler(343bevy_render::render_resource::SamplerBindingType::Filtering,344),345),346),347);348349let bind_group_layout = render_device.create_bind_group_layout(350bind_group_layout_descriptor.label.as_ref(),351&bind_group_layout_descriptor.entries,352);353354Self {355shader: asset_server.load("embedded://bevy_dev_tools/debug_overlay.wgsl"),356mesh_view_layouts,357bind_group_layout,358bind_group_layout_descriptor,359sampler,360fullscreen_vertex_shader,361}362}363}364365#[derive(PartialEq, Eq, Hash, Clone, Copy)]366struct RenderDebugOverlayPipelineKey {367mode: RenderDebugMode,368view_layout_key: MeshPipelineViewLayoutKey,369texture_format: TextureFormat,370}371372impl SpecializedRenderPipeline for RenderDebugOverlayPipeline {373type Key = RenderDebugOverlayPipelineKey;374375fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {376let mut shader_defs = Vec::new();377match key.mode {378RenderDebugMode::Depth => {379shader_defs.push("DEBUG_DEPTH".into());380if key381.view_layout_key382.contains(MeshPipelineViewLayoutKey::DEPTH_PREPASS)383{384shader_defs.push("DEPTH_PREPASS".into());385}386if key387.view_layout_key388.contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS)389{390shader_defs.push("DEFERRED_PREPASS".into());391}392}393RenderDebugMode::Normal => {394shader_defs.push("DEBUG_NORMAL".into());395if key396.view_layout_key397.contains(MeshPipelineViewLayoutKey::NORMAL_PREPASS)398{399shader_defs.push("NORMAL_PREPASS".into());400}401if key402.view_layout_key403.contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS)404{405shader_defs.push("DEFERRED_PREPASS".into());406}407}408RenderDebugMode::MotionVectors => {409shader_defs.push("DEBUG_MOTION_VECTORS".into());410if key411.view_layout_key412.contains(MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS)413{414shader_defs.push("MOTION_VECTOR_PREPASS".into());415}416}417RenderDebugMode::Deferred => {418shader_defs.push("DEBUG_DEFERRED".into());419if key420.view_layout_key421.contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS)422{423shader_defs.push("DEFERRED_PREPASS".into());424}425}426RenderDebugMode::DeferredBaseColor => {427shader_defs.push("DEBUG_DEFERRED_BASE_COLOR".into());428if key429.view_layout_key430.contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS)431{432shader_defs.push("DEFERRED_PREPASS".into());433}434}435RenderDebugMode::DeferredEmissive => {436shader_defs.push("DEBUG_DEFERRED_EMISSIVE".into());437if key438.view_layout_key439.contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS)440{441shader_defs.push("DEFERRED_PREPASS".into());442}443}444RenderDebugMode::DeferredMetallicRoughness => {445shader_defs.push("DEBUG_DEFERRED_METALLIC_ROUGHNESS".into());446if key447.view_layout_key448.contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS)449{450shader_defs.push("DEFERRED_PREPASS".into());451}452}453RenderDebugMode::DepthPyramid { .. } => shader_defs.push("DEBUG_DEPTH_PYRAMID".into()),454}455456if key457.view_layout_key458.contains(MeshPipelineViewLayoutKey::MULTISAMPLED)459{460shader_defs.push("MULTISAMPLED".into());461}462463let mesh_view_layout_descriptor = self464.mesh_view_layouts465.get_view_layout(key.view_layout_key)466.main_layout467.clone();468469RenderPipelineDescriptor {470label: Some("debug_overlay_pipeline".into()),471layout: vec![472mesh_view_layout_descriptor,473self.bind_group_layout_descriptor.clone(),474],475vertex: self.fullscreen_vertex_shader.clone(),476fragment: Some(FragmentState {477shader: self.shader.clone(),478shader_defs,479entry_point: Some("fragment".into()),480targets: vec![Some(ColorTargetState {481format: key.texture_format,482blend: None,483write_mask: ColorWrites::ALL,484})],485}),486primitive: bevy_render::render_resource::PrimitiveState::default(),487depth_stencil: None,488multisample: bevy_render::render_resource::MultisampleState::default(),489immediate_size: 0,490zero_initialize_workgroup_memory: false,491}492}493}494495fn prepare_debug_overlay_pipelines(496mut commands: Commands,497pipeline_cache: Res<PipelineCache>,498mut pipelines: ResMut<SpecializedRenderPipelines<RenderDebugOverlayPipeline>>,499pipeline: Res<RenderDebugOverlayPipeline>,500images: Res<RenderAssets<GpuImage>>,501blue_noise: Res<Bluenoise>,502views: Query<(503Entity,504&ViewTarget,505&RenderDebugOverlay,506&Msaa,507Option<&bevy_core_pipeline::prepass::ViewPrepassTextures>,508Has<bevy_core_pipeline::oit::OrderIndependentTransparencySettings>,509Has<bevy_pbr::ExtractedAtmosphere>,510)>,511) {512for (entity, target, config, msaa, prepass_textures, has_oit, has_atmosphere) in &views {513if !config.enabled {514continue;515}516517let mut view_layout_key = MeshPipelineViewLayoutKey::from(*msaa)518| MeshPipelineViewLayoutKey::from(prepass_textures);519520if has_oit {521view_layout_key |= MeshPipelineViewLayoutKey::OIT_ENABLED;522}523if has_atmosphere {524view_layout_key |= MeshPipelineViewLayoutKey::ATMOSPHERE;525}526527if let Some(gpu_image) = images.get(&blue_noise.texture)528&& gpu_image.texture.depth_or_array_layers() > 1529{530view_layout_key |= MeshPipelineViewLayoutKey::STBN;531}532533let pipeline_id = pipelines.specialize(534&pipeline_cache,535&pipeline,536RenderDebugOverlayPipelineKey {537mode: config.mode,538view_layout_key,539texture_format: target.main_texture_format(),540},541);542543commands544.entity(entity)545.insert(RenderDebugOverlayPipelineId(pipeline_id));546}547}548549#[derive(Component)]550struct RenderDebugOverlayPipelineId(CachedRenderPipelineId);551552fn prepare_debug_overlay_resources(553mut commands: Commands,554render_device: Res<RenderDevice>,555render_queue: Res<RenderQueue>,556mut uniforms: ResMut<RenderDebugOverlayUniforms>,557views: Query<(Entity, &RenderDebugOverlay)>,558) {559let len = views.iter().len();560if len == 0 {561return;562}563564let Some(mut writer) = uniforms565.uniforms566.get_writer(len, &render_device, &render_queue)567else {568return;569};570571for (entity, config) in &views {572let offset = writer.write(&RenderDebugOverlayUniform {573opacity: config.opacity,574mip_level: if let RenderDebugMode::DepthPyramid { mip_level } = config.mode {575mip_level576} else {5770578},579});580581commands582.entity(entity)583.insert(RenderDebugOverlayUniformOffset { offset });584}585}586587fn render_debug_overlay(588view: ViewQuery<(589&ViewTarget,590&RenderDebugOverlay,591&RenderDebugOverlayPipelineId,592&RenderDebugOverlayUniformOffset,593&MeshViewBindGroup,594&ViewUniformOffset,595&ViewLightsUniformOffset,596&ViewFogUniformOffset,597&ViewLightProbesUniformOffset,598&ViewScreenSpaceReflectionsUniformOffset,599&ViewContactShadowsUniformOffset,600&ViewEnvironmentMapUniformOffset,601Has<bevy_core_pipeline::oit::OrderIndependentTransparencySettings>,602Option<&OrderIndependentTransparencySettingsOffset>,603Option<&ViewDepthPyramid>,604)>,605pipeline_cache: Res<PipelineCache>,606pipeline_res: Res<RenderDebugOverlayPipeline>,607uniforms: Res<RenderDebugOverlayUniforms>,608fallback_image: Res<FallbackImage>,609mut ctx: RenderContext,610) {611let (612target,613config,614pipeline_id,615uniform_offset,616mesh_view_bind_group,617view_uniform_offset,618view_lights_offset,619view_fog_offset,620view_light_probes_offset,621view_ssr_offset,622view_contact_shadows_offset,623view_environment_map_offset,624has_oit,625view_oit_offset,626depth_pyramid,627) = view.into_inner();628629if !config.enabled {630return;631}632633let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id.0) else {634return;635};636637let Some(uniform_binding) = uniforms.uniforms.binding() else {638return;639};640641let post_process = target.post_process_write();642643let depth_pyramid_view = if let Some(dp) = depth_pyramid {644&dp.all_mips645} else {646&fallback_image.d2.texture_view647};648649let debug_bind_group = ctx.render_device().create_bind_group(650"debug_buffer_bind_group",651&pipeline_res.bind_group_layout,652&BindGroupEntries::sequential((653uniform_binding,654post_process.source,655&pipeline_res.sampler,656depth_pyramid_view,657&pipeline_res.sampler,658)),659);660661let pass_descriptor = RenderPassDescriptor {662label: Some("debug_buffer_pass"),663color_attachments: &[Some(RenderPassColorAttachment {664view: post_process.destination,665depth_slice: None,666resolve_target: None,667ops: Operations {668load: bevy_render::render_resource::LoadOp::Clear(Default::default()),669store: bevy_render::render_resource::StoreOp::Store,670},671})],672depth_stencil_attachment: None,673timestamp_writes: None,674occlusion_query_set: None,675multiview_mask: None,676};677678let mut render_pass = ctx.command_encoder().begin_render_pass(&pass_descriptor);679680render_pass.set_pipeline(pipeline);681682let mut dynamic_offsets = vec![683view_uniform_offset.offset,684view_lights_offset.offset,685view_fog_offset.offset,686**view_light_probes_offset,687**view_ssr_offset,688**view_contact_shadows_offset,689**view_environment_map_offset,690];691if has_oit && let Some(view_oit_offset) = view_oit_offset {692dynamic_offsets.push(view_oit_offset.offset);693}694695render_pass.set_bind_group(0, &mesh_view_bind_group.main, &dynamic_offsets);696render_pass.set_bind_group(1, &debug_bind_group, &[uniform_offset.offset]);697698render_pass.draw(0..3, 0..1);699}700701702