Skip to main content

bevy_render/view/visibility/
range.rs

1//! Specific distances from the camera in which entities are visible, also known
2//! as *hierarchical levels of detail* or *HLOD*s.
3
4use super::VisibilityRange;
5use bevy_app::{App, Plugin};
6use bevy_ecs::{
7    entity::Entity,
8    lifecycle::RemovedComponents,
9    query::Changed,
10    resource::Resource,
11    schedule::IntoScheduleConfigs as _,
12    system::{Query, Res, ResMut},
13};
14use bevy_log::warn_once;
15use bevy_math::{vec4, Vec4};
16use bevy_platform::collections::HashMap;
17use bevy_utils::prelude::default;
18use nonmax::NonMaxU16;
19use wgpu::{BufferBindingType, BufferUsages};
20
21use crate::{
22    render_resource::BufferVec,
23    renderer::{RenderDevice, RenderQueue},
24    sync_world::{MainEntity, MainEntityHashMap},
25    Extract, ExtractSchedule, GpuResourceAppExt, Render, RenderApp, RenderSystems,
26};
27
28/// We need at least 4 storage buffer bindings available to enable the
29/// visibility range buffer.
30///
31/// Even though we only use one storage buffer, the first 3 available storage
32/// buffers will go to various light-related buffers. We will grab the fourth
33/// buffer slot.
34pub const VISIBILITY_RANGES_STORAGE_BUFFER_COUNT: u32 = 4;
35
36/// The size of the visibility ranges buffer in elements (not bytes) when fewer
37/// than 6 storage buffers are available and we're forced to use a uniform
38/// buffer instead (most notably, on WebGL 2).
39const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: usize = 64;
40
41/// A plugin that enables [`RenderVisibilityRanges`]s, which allow entities to be
42/// hidden or shown based on distance to the camera.
43pub struct RenderVisibilityRangePlugin;
44
45impl Plugin for RenderVisibilityRangePlugin {
46    fn build(&self, app: &mut App) {
47        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
48            return;
49        };
50
51        render_app
52            .init_gpu_resource::<RenderVisibilityRanges>()
53            .add_systems(ExtractSchedule, extract_visibility_ranges)
54            .add_systems(
55                Render,
56                write_render_visibility_ranges.in_set(RenderSystems::PrepareResourcesFlush),
57            );
58    }
59}
60
61/// Stores information related to [`VisibilityRange`]s in the render world.
62#[derive(impl bevy_ecs::resource::Resource for RenderVisibilityRanges where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {}Resource)]
63pub struct RenderVisibilityRanges {
64    /// Information corresponding to each entity.
65    entities: MainEntityHashMap<RenderVisibilityEntityInfo>,
66
67    /// Maps a [`VisibilityRange`] to its index within the `buffer`.
68    ///
69    /// This map allows us to deduplicate identical visibility ranges, which
70    /// saves GPU memory.
71    range_to_index: HashMap<VisibilityRange, NonMaxU16>,
72
73    /// The GPU buffer that stores [`VisibilityRange`]s.
74    ///
75    /// Each [`Vec4`] contains the start margin start, start margin end, end
76    /// margin start, and end margin end distances, in that order.
77    buffer: BufferVec<Vec4>,
78
79    /// True if the buffer has been changed since the last frame and needs to be
80    /// reuploaded to the GPU.
81    buffer_dirty: bool,
82}
83
84/// Per-entity information related to [`VisibilityRange`]s.
85struct RenderVisibilityEntityInfo {
86    /// The index of the range within the GPU buffer.
87    buffer_index: NonMaxU16,
88    /// True if the range is abrupt: i.e. has no crossfade.
89    is_abrupt: bool,
90}
91
92impl Default for RenderVisibilityRanges {
93    fn default() -> Self {
94        Self {
95            entities: default(),
96            range_to_index: default(),
97            buffer: BufferVec::new(
98                BufferUsages::STORAGE | BufferUsages::UNIFORM | BufferUsages::VERTEX,
99            ),
100            buffer_dirty: true,
101        }
102    }
103}
104
105impl RenderVisibilityRanges {
106    /// Clears the per-frame entity table in preparation for a new frame.
107    ///
108    /// `range_to_index` and `buffer` are deliberately *not* cleared: the
109    /// GPU-driven path bakes a range's buffer index into the mesh's
110    /// `MeshInputUniform` at extraction and only refreshes it on re-extraction,
111    /// so a range must keep the same index for the lifetime of the app or
112    /// still-visible meshes would end up pointing at the wrong slot.
113    fn clear(&mut self) {
114        self.entities.clear();
115    }
116
117    /// Inserts a new entity into the [`RenderVisibilityRanges`].
118    fn insert(&mut self, entity: MainEntity, visibility_range: &VisibilityRange) {
119        // Reuse this range's slot, or append a new one. Indices are never
120        // reused, so any index already baked into a mesh stays valid.
121        let buffer_index = match self.range_to_index.get(visibility_range) {
122            Some(index) => *index,
123            None => {
124                // `try_from` errors instead of wrapping past `NonMaxU16`'s
125                // range, so overflow warns rather than silently aliasing onto
126                // slot 0.
127                let index = u16::try_from(self.range_to_index.len())
128                    .ok()
129                    .and_then(|next| NonMaxU16::try_from(next).ok())
130                    .unwrap_or_else(|| {
131                        {
    {
        static SHOULD_FIRE: ::bevy_utils::OnceFlag =
            ::bevy_utils::OnceFlag::new();
        if SHOULD_FIRE.set() {
            {
                use ::tracing::__macro_support::Callsite as _;
                static __CALLSITE: ::tracing::callsite::DefaultCallsite =
                    {
                        static META: ::tracing::Metadata<'static> =
                            {
                                ::tracing_core::metadata::Metadata::new("event src/view/visibility/range.rs:131",
                                    "bevy_render::view::visibility::range",
                                    ::tracing::Level::WARN,
                                    ::tracing_core::__macro_support::Option::Some("src/view/visibility/range.rs"),
                                    ::tracing_core::__macro_support::Option::Some(131u32),
                                    ::tracing_core::__macro_support::Option::Some("bevy_render::view::visibility::range"),
                                    ::tracing_core::field::FieldSet::new(&["message"],
                                        ::tracing_core::callsite::Identifier(&__CALLSITE)),
                                    ::tracing::metadata::Kind::EVENT)
                            };
                        ::tracing::callsite::DefaultCallsite::new(&META)
                    };
                let enabled =
                    ::tracing::Level::WARN <=
                                ::tracing::level_filters::STATIC_MAX_LEVEL &&
                            ::tracing::Level::WARN <=
                                ::tracing::level_filters::LevelFilter::current() &&
                        {
                            let interest = __CALLSITE.interest();
                            !interest.is_never() &&
                                ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                                    interest)
                        };
                if enabled {
                    (|value_set: ::tracing::field::ValueSet|
                                {
                                    let meta = __CALLSITE.metadata();
                                    ::tracing::Event::dispatch(meta, &value_set);
                                    ;
                                })({
                            #[allow(unused_imports)]
                            use ::tracing::field::{debug, display, Value};
                            __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&format_args!("More than {0} distinct `VisibilityRange`s are in use; additional ranges will share GPU slot 0 and may be culled or crossfaded incorrectly.",
                                                                u16::MAX) as &dyn ::tracing::field::Value))])
                        });
                } else { ; }
            };
        }
    }
};warn_once!(
132                            "More than {} distinct `VisibilityRange`s are in use; \
133                             additional ranges will share GPU slot 0 and may be \
134                             culled or crossfaded incorrectly.",
135                            u16::MAX
136                        );
137                        NonMaxU16::default()
138                    });
139                self.range_to_index.insert(visibility_range.clone(), index);
140                self.buffer_dirty = true;
141                index
142            }
143        };
144
145        self.entities.insert(
146            entity,
147            RenderVisibilityEntityInfo {
148                buffer_index,
149                is_abrupt: visibility_range.is_abrupt(),
150            },
151        );
152    }
153
154    /// Rebuilds the GPU buffer from `range_to_index` in index order. Only runs
155    /// when a new range was added this frame, and leaves `buffer_dirty` set for
156    /// [`write_render_visibility_ranges`] to consume.
157    fn rebuild_buffer_if_dirty(&mut self) {
158        if !self.buffer_dirty {
159            return;
160        }
161        let mut ordered = ::alloc::vec::from_elem(Vec4::ZERO, self.range_to_index.len())vec![Vec4::ZERO; self.range_to_index.len()];
162        for (range, index) in &self.range_to_index {
163            ordered[index.get() as usize] = vec4(
164                range.start_margin.start,
165                range.start_margin.end,
166                range.end_margin.start,
167                range.end_margin.end,
168            );
169        }
170        self.buffer.clear();
171        for value in ordered {
172            self.buffer.push(value);
173        }
174    }
175
176    /// Returns the index in the GPU buffer corresponding to the visible range
177    /// for the given entity.
178    ///
179    /// If the entity has no visible range, returns `None`.
180    #[inline]
181    pub fn lod_index_for_entity(&self, entity: MainEntity) -> Option<NonMaxU16> {
182        self.entities.get(&entity).map(|info| info.buffer_index)
183    }
184
185    /// Returns true if the entity has a visibility range and it isn't abrupt:
186    /// i.e. if it has a crossfade.
187    #[inline]
188    pub fn entity_has_crossfading_visibility_ranges(&self, entity: MainEntity) -> bool {
189        self.entities
190            .get(&entity)
191            .is_some_and(|info| !info.is_abrupt)
192    }
193
194    /// Returns a reference to the GPU buffer that stores visibility ranges.
195    #[inline]
196    pub fn buffer(&self) -> &BufferVec<Vec4> {
197        &self.buffer
198    }
199}
200
201/// Extracts all [`VisibilityRange`] components from the main world to the
202/// render world and inserts them into [`RenderVisibilityRanges`].
203pub fn extract_visibility_ranges(
204    mut render_visibility_ranges: ResMut<RenderVisibilityRanges>,
205    visibility_ranges_query: Extract<Query<(Entity, &VisibilityRange)>>,
206    changed_ranges_query: Extract<Query<Entity, Changed<VisibilityRange>>>,
207    mut removed_visibility_ranges: Extract<RemovedComponents<VisibilityRange>>,
208) {
209    if changed_ranges_query.is_empty() && removed_visibility_ranges.read().next().is_none() {
210        return;
211    }
212
213    render_visibility_ranges.clear();
214    for (entity, visibility_range) in visibility_ranges_query.iter() {
215        render_visibility_ranges.insert(entity.into(), visibility_range);
216    }
217    render_visibility_ranges.rebuild_buffer_if_dirty();
218}
219
220/// Writes the [`RenderVisibilityRanges`] table to the GPU.
221pub fn write_render_visibility_ranges(
222    render_device: Res<RenderDevice>,
223    render_queue: Res<RenderQueue>,
224    mut render_visibility_ranges: ResMut<RenderVisibilityRanges>,
225) {
226    // If there haven't been any changes, early out.
227    if !render_visibility_ranges.buffer_dirty {
228        return;
229    }
230
231    // Mess with the length of the buffer to meet API requirements if necessary.
232    match render_device.get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT)
233    {
234        // If we're using a uniform buffer, we must have *exactly*
235        // `VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE` elements.
236        BufferBindingType::Uniform
237            if render_visibility_ranges.buffer.len() > VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE =>
238        {
239            render_visibility_ranges
240                .buffer
241                .truncate(VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE);
242        }
243        BufferBindingType::Uniform
244            if render_visibility_ranges.buffer.len() < VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE =>
245        {
246            while render_visibility_ranges.buffer.len() < VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE {
247                render_visibility_ranges.buffer.push(default());
248            }
249        }
250
251        // Otherwise, if we're using a storage buffer, just ensure there's
252        // something in the buffer, or else it won't get allocated.
253        BufferBindingType::Storage { .. } if render_visibility_ranges.buffer.is_empty() => {
254            render_visibility_ranges.buffer.push(default());
255        }
256
257        _ => {}
258    }
259
260    // Schedule the write.
261    render_visibility_ranges
262        .buffer
263        .write_buffer(&render_device, &render_queue);
264    render_visibility_ranges.buffer_dirty = false;
265}