Files
marathon/crates/libmarathon/src/debug_ui/render/render_pass.rs
2025-12-14 20:25:55 +00:00

276 lines
10 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::render::{
DrawPrimitive, EguiViewTarget,
systems::{EguiPipelines, EguiRenderData, EguiTextureBindGroups, EguiTransforms},
};
use bevy::camera::Viewport;
use bevy::ecs::{
query::QueryState,
world::{Mut, World},
};
use bevy::math::{URect, UVec2};
use bevy::render::{
camera::ExtractedCamera,
render_graph::{Node, NodeRunError, RenderGraphContext},
render_resource::{PipelineCache, RenderPassDescriptor},
renderer::RenderContext,
sync_world::RenderEntity,
view::{ExtractedView, ViewTarget},
};
use wgpu_types::{IndexFormat, ShaderStages};
/// Egui pass node.
pub struct EguiPassNode {
egui_view_query: QueryState<(&'static ExtractedView, &'static EguiViewTarget)>,
egui_view_target_query: QueryState<(&'static ViewTarget, &'static ExtractedCamera)>,
}
impl EguiPassNode {
/// Creates an Egui pass node.
pub fn new(world: &mut World) -> Self {
Self {
egui_view_query: world.query_filtered(),
egui_view_target_query: world.query(),
}
}
}
impl Node for EguiPassNode {
fn update(&mut self, world: &mut World) {
self.egui_view_query.update_archetypes(world);
self.egui_view_target_query.update_archetypes(world);
world.resource_scope(|world, mut render_data: Mut<EguiRenderData>| {
for (_main_entity, data) in &mut render_data.0 {
let Some(key) = data.key else {
bevy::log::warn!("Failed to retrieve egui node data!");
return;
};
for (clip_rect, command) in data.postponed_updates.drain(..) {
let info = egui::PaintCallbackInfo {
viewport: command.rect,
clip_rect,
pixels_per_point: data.pixels_per_point,
screen_size_px: data.target_size.to_array(),
};
command
.callback
.cb()
.update(info, data.render_entity, key, world);
}
}
});
}
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
world: &'w World,
) -> Result<(), NodeRunError> {
let egui_pipelines = &world.resource::<EguiPipelines>().0;
let pipeline_cache = world.resource::<PipelineCache>();
let render_data = world.resource::<EguiRenderData>();
// Extract the UI view.
let input_view_entity = graph.view_entity();
// Query the UI view components.
let Ok((view, view_target)) = self.egui_view_query.get_manual(world, input_view_entity)
else {
return Ok(());
};
let Ok((target, camera)) = self.egui_view_target_query.get_manual(world, view_target.0)
else {
return Ok(());
};
let Some(data) = render_data.0.get(&view.retained_view_entity.main_entity) else {
return Ok(());
};
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("egui_pass"),
color_attachments: &[Some(target.get_unsampled_color_attachment())],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
let Some(viewport) = camera.viewport.clone().or_else(|| {
camera.physical_viewport_size.map(|size| Viewport {
physical_position: UVec2::ZERO,
physical_size: size,
..Default::default()
})
}) else {
return Ok(());
};
render_pass.set_camera_viewport(&Viewport {
physical_position: UVec2::ZERO,
physical_size: camera.physical_target_size.unwrap(),
..Default::default()
});
let mut requires_reset = true;
let mut last_scissor_rect = None;
let mut last_bindless_offset = None;
let pipeline_id = egui_pipelines
.get(&view.retained_view_entity.main_entity)
.expect("Expected a queued pipeline");
let Some(pipeline) = pipeline_cache.get_render_pipeline(*pipeline_id) else {
return Ok(());
};
let bind_groups = world.resource::<EguiTextureBindGroups>();
let egui_transforms = world.resource::<EguiTransforms>();
let transform_buffer_offset =
egui_transforms.offsets[&view.retained_view_entity.main_entity];
let transform_buffer_bind_group = &egui_transforms
.bind_group
.as_ref()
.expect("Expected a prepared bind group")
.1;
let (vertex_buffer, index_buffer) = match (&data.vertex_buffer, &data.index_buffer) {
(Some(vertex), Some(index)) => (vertex, index),
_ => {
return Ok(());
}
};
let mut index_offset: u32 = 0;
for draw_command in &data.draw_commands {
if requires_reset {
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(
0,
transform_buffer_bind_group,
&[transform_buffer_offset],
);
render_pass.set_camera_viewport(&Viewport {
physical_position: UVec2::ZERO,
physical_size: camera.physical_target_size.unwrap(),
..Default::default()
});
requires_reset = false;
last_bindless_offset = None;
last_scissor_rect = None;
}
let clip_urect = URect {
min: UVec2 {
x: (draw_command.clip_rect.min.x * data.pixels_per_point).round() as u32,
y: (draw_command.clip_rect.min.y * data.pixels_per_point).round() as u32,
},
max: UVec2 {
x: (draw_command.clip_rect.max.x * data.pixels_per_point).round() as u32,
y: (draw_command.clip_rect.max.y * data.pixels_per_point).round() as u32,
},
};
let scissor_rect = clip_urect.intersect(URect {
min: viewport.physical_position,
max: viewport.physical_position + viewport.physical_size,
});
if scissor_rect.is_empty() {
continue;
}
if Some(scissor_rect) != last_scissor_rect {
last_scissor_rect = Some(scissor_rect);
// Bevy TrackedRenderPass doesn't track set_scissor_rect calls,
// so set_scissor_rect is updated only when it is needed.
render_pass.set_scissor_rect(
scissor_rect.min.x,
scissor_rect.min.y,
scissor_rect.width(),
scissor_rect.height(),
);
}
let Some(pipeline_key) = data.key else {
continue;
};
match &draw_command.primitive {
DrawPrimitive::Egui(command) => {
let Some((texture_bind_group, bindless_offset)) =
bind_groups.get(&command.egui_texture)
else {
index_offset += command.vertices_count as u32;
continue;
};
render_pass.set_bind_group(1, texture_bind_group, &[]);
render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
render_pass.set_index_buffer(index_buffer.slice(..), 0, IndexFormat::Uint32);
if let Some(bindless_offset) = bindless_offset
&& last_bindless_offset != Some(bindless_offset)
{
last_bindless_offset = Some(bindless_offset);
// Use push constant to cheaply provide which texture to use inside
// binding array. This is used to avoid costly set_bind_group operations
// when frequent switching between textures is being done
render_pass.set_push_constants(
ShaderStages::FRAGMENT,
0,
bytemuck::bytes_of(bindless_offset),
);
}
// NOTE: vertices_count is actually the indices count (poorly named in EguiDraw struct)
render_pass.draw_indexed(
index_offset..(index_offset + command.vertices_count as u32),
0,
0..1,
);
index_offset += command.vertices_count as u32;
}
DrawPrimitive::PaintCallback(command) => {
let info = egui::PaintCallbackInfo {
viewport: command.rect,
clip_rect: draw_command.clip_rect,
pixels_per_point: data.pixels_per_point,
screen_size_px: [viewport.physical_size.x, viewport.physical_size.y],
};
let viewport = info.viewport_in_pixels();
if viewport.width_px > 0 && viewport.height_px > 0 {
requires_reset = true;
render_pass.set_viewport(
viewport.left_px as f32,
viewport.top_px as f32,
viewport.width_px as f32,
viewport.height_px as f32,
0.,
1.,
);
command.callback.cb().render(
info,
&mut render_pass,
RenderEntity::from(input_view_entity),
pipeline_key,
world,
);
}
}
}
}
Ok(())
}
}