Files
marathon/crates/libmarathon/src/debug_ui/render/systems.rs
2026-02-07 14:11:12 +00:00

508 lines
18 KiB
Rust

// Copyright (c) 2021 Vladyslav Batyrenko
// SPDX-License-Identifier: MIT
//
// This code is vendored from bevy_egui: https://github.com/vladbat00/bevy_egui
// Original author: Vladyslav Batyrenko <vladyslav.batyrenko@gmail.com>
use crate::debug_ui::{
EguiContextSettings, EguiManagedTextures, EguiRenderOutput, EguiUserTextures,
RenderComputedScaleFactor,
render::{
DrawCommand, DrawPrimitive, EguiBevyPaintCallback, EguiCameraView, EguiDraw, EguiPipeline,
EguiPipelineKey, EguiViewTarget, PaintCallbackDraw,
},
};
use bevy::asset::prelude::*;
use bevy::prelude::{Deref, DerefMut};
use bevy::ecs::{prelude::*, system::SystemParam};
use bevy::image::Image;
use bevy::log;
use bevy::math::{URect, UVec2, Vec2};
use bevy::platform::collections::HashMap;
use bevy::render::{
camera::ExtractedCamera,
extract_resource::ExtractResource,
render_asset::RenderAssets,
render_resource::{
BindGroup, BindGroupEntry, BindingResource, Buffer, BufferDescriptor, BufferId,
CachedRenderPipelineId, DynamicUniformBuffer, PipelineCache, SpecializedRenderPipelines,
},
renderer::{RenderDevice, RenderQueue},
sync_world::{MainEntity, RenderEntity},
texture::GpuImage,
view::ExtractedView,
};
use bytemuck::cast_slice;
use itertools::Itertools;
use wgpu_types::{BufferAddress, BufferUsages};
/// Extracted Egui settings.
#[derive(Resource, Deref, DerefMut, Default)]
pub struct ExtractedEguiSettings(pub EguiContextSettings);
/// The extracted version of [`EguiManagedTextures`].
#[derive(Debug, Resource)]
pub struct ExtractedEguiManagedTextures(pub HashMap<(Entity, u64), Handle<Image>>);
impl ExtractResource for ExtractedEguiManagedTextures {
type Source = EguiManagedTextures;
fn extract_resource(source: &Self::Source) -> Self {
Self(source.iter().map(|(k, v)| (*k, v.handle.clone())).collect())
}
}
/// Corresponds to Egui's [`egui::TextureId`].
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum EguiTextureId {
/// Textures allocated via Egui.
Managed(MainEntity, u64),
/// Textures allocated via Bevy.
User(u64),
}
/// Extracted Egui textures.
#[derive(SystemParam)]
pub struct ExtractedEguiTextures<'w> {
/// Maps Egui managed texture ids to Bevy image handles.
pub egui_textures: Res<'w, ExtractedEguiManagedTextures>,
/// Maps Bevy managed texture handles to Egui user texture ids.
pub user_textures: Res<'w, EguiUserTextures>,
}
impl ExtractedEguiTextures<'_> {
/// Returns an iterator over all textures (both Egui and Bevy managed).
pub fn handles(&self) -> impl Iterator<Item = (EguiTextureId, AssetId<Image>)> + '_ {
self.egui_textures
.0
.iter()
.map(|(&(window, texture_id), managed_tex)| {
(
EguiTextureId::Managed(MainEntity::from(window), texture_id),
managed_tex.id(),
)
})
.chain(
self.user_textures
.textures
.iter()
.map(|(handle, (_, id))| (EguiTextureId::User(*id), *handle)),
)
}
}
/// Describes the transform buffer.
#[derive(Resource, Default)]
pub struct EguiTransforms {
/// Uniform buffer.
pub buffer: DynamicUniformBuffer<EguiTransform>,
/// The Entity is from the main world.
pub offsets: HashMap<MainEntity, u32>,
/// Bind group.
pub bind_group: Option<(BufferId, BindGroup)>,
}
/// Scale and translation for rendering Egui shapes. Is needed to transform Egui coordinates from
/// the screen space with the center at (0, 0) to the normalised viewport space.
#[derive(bevy::render::render_resource::ShaderType, Default)]
pub struct EguiTransform {
/// Is affected by render target size, scale factor and [`EguiContextSettings::scale_factor`].
pub scale: Vec2,
/// Normally equals `[-1.0, 1.0]`.
pub translation: Vec2,
}
impl EguiTransform {
/// Calculates the transform from target size and target scale factor multiplied by [`EguiContextSettings::scale_factor`].
pub fn new(target_size: Vec2, scale_factor: f32) -> Self {
EguiTransform {
scale: Vec2::new(
2.0 / (target_size.x / scale_factor),
-2.0 / (target_size.y / scale_factor),
),
translation: Vec2::new(-1.0, 1.0),
}
}
}
/// Prepares Egui transforms.
pub fn prepare_egui_transforms_system(
mut egui_transforms: ResMut<EguiTransforms>,
views: Query<&RenderComputedScaleFactor>,
render_targets: Query<(&ExtractedView, &ExtractedCamera, &EguiCameraView)>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
egui_pipeline: Res<EguiPipeline>,
) -> Result {
egui_transforms.buffer.clear();
egui_transforms.offsets.clear();
for (view, camera, egui_camera_view) in render_targets.iter() {
let Some(target_size) = camera.physical_target_size else {
continue;
};
let &RenderComputedScaleFactor { scale_factor } = views.get(egui_camera_view.0)?;
let transform = EguiTransform::new(target_size.as_vec2(), scale_factor);
let offset = egui_transforms
.buffer
.push(&transform);
egui_transforms
.offsets
.insert(view.retained_view_entity.main_entity, offset);
}
egui_transforms
.buffer
.write_buffer(&render_device, &render_queue);
if let Some(buffer) = egui_transforms.buffer.buffer() {
match egui_transforms.bind_group {
Some((id, _)) if buffer.id() == id => {}
_ => {
let transform_bind_group = render_device.create_bind_group(
Some("egui transform bind group"),
&egui_pipeline.transform_bind_group_layout,
&[BindGroupEntry {
binding: 0,
resource: egui_transforms.buffer.binding().unwrap(),
}],
);
egui_transforms.bind_group = Some((buffer.id(), transform_bind_group));
}
};
}
Ok(())
}
/// Maps Egui textures to bind groups.
#[derive(Resource, Deref, DerefMut, Default)]
pub struct EguiTextureBindGroups(pub HashMap<EguiTextureId, (BindGroup, Option<u32>)>);
/// Queues bind groups.
pub fn queue_bind_groups_system(
mut commands: Commands,
egui_textures: ExtractedEguiTextures,
render_device: Res<RenderDevice>,
gpu_images: Res<RenderAssets<GpuImage>>,
egui_pipeline: Res<EguiPipeline>,
) {
let egui_texture_iterator = egui_textures.handles().filter_map(|(texture, handle_id)| {
let gpu_image = gpu_images.get(handle_id)?;
Some((texture, gpu_image))
});
let bind_groups = if let Some(bindless) = egui_pipeline.bindless {
let bindless = u32::from(bindless) as usize;
let mut bind_groups = HashMap::new();
let mut texture_array = Vec::new();
let mut sampler_array = Vec::new();
let mut egui_texture_ids = Vec::new();
for textures in egui_texture_iterator.chunks(bindless).into_iter() {
texture_array.clear();
sampler_array.clear();
egui_texture_ids.clear();
for (egui_texture_id, gpu_image) in textures {
egui_texture_ids.push(egui_texture_id);
// Dereference needed to convert from bevy to wgpu type
texture_array.push(&*gpu_image.texture_view);
sampler_array.push(&*gpu_image.sampler);
}
let bind_group = render_device.create_bind_group(
None,
&egui_pipeline.texture_bind_group_layout,
&[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureViewArray(texture_array.as_slice()),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::SamplerArray(sampler_array.as_slice()),
},
],
);
// Simply assign bind group to egui texture
// Additional code is not needed because bevy RenderPass set_bind_group
// removes redundant switching between bind groups
for (offset, egui_texture_id) in egui_texture_ids.drain(..).enumerate() {
bind_groups.insert(egui_texture_id, (bind_group.clone(), Some(offset as u32)));
}
}
bind_groups
} else {
egui_texture_iterator
.map(|(texture, gpu_image)| {
let bind_group = render_device.create_bind_group(
None,
&egui_pipeline.texture_bind_group_layout,
&[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(&gpu_image.texture_view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(&gpu_image.sampler),
},
],
);
(texture, (bind_group, None::<u32>))
})
.collect()
};
commands.insert_resource(EguiTextureBindGroups(bind_groups))
}
/// Cached Pipeline IDs for the specialized instances of `EguiPipeline`.
#[derive(Resource)]
pub struct EguiPipelines(pub HashMap<MainEntity, CachedRenderPipelineId>);
/// Queue [`EguiPipeline`] instances.
pub fn queue_pipelines_system(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut specialized_pipelines: ResMut<SpecializedRenderPipelines<EguiPipeline>>,
egui_pipeline: Res<EguiPipeline>,
egui_views: Query<&EguiViewTarget, With<ExtractedView>>,
camera_views: Query<(&MainEntity, &ExtractedCamera)>,
) {
let pipelines: HashMap<MainEntity, CachedRenderPipelineId> = egui_views
.iter()
.filter_map(|egui_camera_view| {
let (main_entity, extracted_camera) = camera_views.get(egui_camera_view.0).ok()?;
let pipeline_id = specialized_pipelines.specialize(
&pipeline_cache,
&egui_pipeline,
EguiPipelineKey {
hdr: extracted_camera.hdr,
},
);
Some((*main_entity, pipeline_id))
})
.collect();
commands.insert_resource(EguiPipelines(pipelines));
}
/// Cached Pipeline IDs for the specialized instances of `EguiPipeline`.
#[derive(Default, Resource)]
pub struct EguiRenderData(pub(crate) HashMap<MainEntity, EguiRenderTargetData>);
pub(crate) struct EguiRenderTargetData {
keep: bool,
pub(crate) render_entity: RenderEntity,
pub(crate) vertex_data: Vec<u8>,
pub(crate) vertex_buffer_capacity: usize,
pub(crate) vertex_buffer: Option<Buffer>,
pub(crate) index_data: Vec<u32>,
pub(crate) index_buffer_capacity: usize,
pub(crate) index_buffer: Option<Buffer>,
pub(crate) draw_commands: Vec<DrawCommand>,
pub(crate) postponed_updates: Vec<(egui::Rect, PaintCallbackDraw)>,
pub(crate) pixels_per_point: f32,
pub(crate) target_size: UVec2,
pub(crate) key: Option<EguiPipelineKey>,
}
impl Default for EguiRenderTargetData {
fn default() -> Self {
Self {
keep: false,
render_entity: RenderEntity::from(Entity::PLACEHOLDER),
vertex_data: Vec::new(),
vertex_buffer_capacity: 0,
vertex_buffer: None,
index_data: Vec::new(),
index_buffer_capacity: 0,
index_buffer: None,
draw_commands: Vec::new(),
postponed_updates: Vec::new(),
pixels_per_point: 0.0,
target_size: UVec2::ZERO,
key: None,
}
}
}
/// Prepares Egui transforms.
pub fn prepare_egui_render_target_data_system(
mut render_data: ResMut<EguiRenderData>,
render_targets: Query<(
Entity,
&ExtractedView,
&RenderComputedScaleFactor,
&EguiViewTarget,
&EguiRenderOutput,
)>,
extracted_cameras: Query<&ExtractedCamera>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
) {
let render_data = &mut render_data.0;
render_data.retain(|_, data| {
let keep = data.keep;
data.keep = false;
keep
});
for (render_entity, view, computed_scale_factor, egui_view_target, render_output) in
render_targets.iter()
{
let data = render_data
.entry(view.retained_view_entity.main_entity)
.or_default();
data.keep = true;
data.render_entity = render_entity.into();
// Construct a pipeline key based on a render target.
let Ok(extracted_camera) = extracted_cameras.get(egui_view_target.0) else {
// This is ok when a window is minimized.
log::trace!("ExtractedCamera entity doesn't exist for the Egui view");
continue;
};
data.key = Some(EguiPipelineKey {
hdr: extracted_camera.hdr,
});
data.pixels_per_point = computed_scale_factor.scale_factor;
if extracted_camera
.physical_viewport_size
.is_none_or(|size| size.x < 1 || size.y < 1)
{
continue;
}
let mut index_offset = 0;
data.draw_commands.clear();
data.vertex_data.clear();
data.index_data.clear();
data.postponed_updates.clear();
for egui::epaint::ClippedPrimitive {
clip_rect,
primitive,
} in render_output.paint_jobs.as_slice()
{
let clip_rect = *clip_rect;
let clip_urect = URect {
min: UVec2 {
x: (clip_rect.min.x * data.pixels_per_point).round() as u32,
y: (clip_rect.min.y * data.pixels_per_point).round() as u32,
},
max: UVec2 {
x: (clip_rect.max.x * data.pixels_per_point).round() as u32,
y: (clip_rect.max.y * data.pixels_per_point).round() as u32,
},
};
if clip_urect
.intersect(URect::new(
view.viewport.x,
view.viewport.y,
view.viewport.x + view.viewport.z,
view.viewport.y + view.viewport.w,
))
.is_empty()
{
continue;
}
let mesh = match primitive {
egui::epaint::Primitive::Mesh(mesh) => mesh,
egui::epaint::Primitive::Callback(paint_callback) => {
let callback = match paint_callback
.callback
.clone()
.downcast::<EguiBevyPaintCallback>()
{
Ok(callback) => callback,
Err(err) => {
log::error!("Unsupported Egui paint callback type: {err:?}");
continue;
}
};
data.postponed_updates.push((
clip_rect,
PaintCallbackDraw {
callback: callback.clone(),
rect: paint_callback.rect,
},
));
data.draw_commands.push(DrawCommand {
primitive: DrawPrimitive::PaintCallback(PaintCallbackDraw {
callback,
rect: paint_callback.rect,
}),
clip_rect,
});
continue;
}
};
data.vertex_data
.extend_from_slice(cast_slice::<_, u8>(mesh.vertices.as_slice()));
data.index_data
.extend(mesh.indices.iter().map(|i| i + index_offset));
index_offset += mesh.vertices.len() as u32;
let texture_handle = match mesh.texture_id {
egui::TextureId::Managed(id) => {
EguiTextureId::Managed(view.retained_view_entity.main_entity, id)
}
egui::TextureId::User(id) => EguiTextureId::User(id),
};
data.draw_commands.push(DrawCommand {
primitive: DrawPrimitive::Egui(EguiDraw {
vertices_count: mesh.indices.len(),
egui_texture: texture_handle,
}),
clip_rect,
});
}
if data.vertex_data.len() > data.vertex_buffer_capacity {
data.vertex_buffer_capacity = data.vertex_data.len().next_power_of_two();
data.vertex_buffer = Some(render_device.create_buffer(&BufferDescriptor {
label: Some("egui vertex buffer"),
size: data.vertex_buffer_capacity as BufferAddress,
usage: BufferUsages::COPY_DST | BufferUsages::VERTEX,
mapped_at_creation: false,
}));
}
let index_data_size = data.index_data.len() * std::mem::size_of::<u32>();
if index_data_size > data.index_buffer_capacity {
data.index_buffer_capacity = index_data_size.next_power_of_two();
data.index_buffer = Some(render_device.create_buffer(&BufferDescriptor {
label: Some("egui index buffer"),
size: data.index_buffer_capacity as BufferAddress,
usage: BufferUsages::COPY_DST | BufferUsages::INDEX,
mapped_at_creation: false,
}));
}
let (vertex_buffer, index_buffer) = match (&data.vertex_buffer, &data.index_buffer) {
(Some(vertex), Some(index)) => (vertex, index),
_ => {
continue;
}
};
render_queue.write_buffer(vertex_buffer, 0, &data.vertex_data);
render_queue.write_buffer(index_buffer, 0, cast_slice(&data.index_data));
}
}