use bevy_app::App;
use bevy_asset::{Assets, Handle, uuid_handle};
use bevy_camera::{
Camera, CameraOutputMode, ClearColor, ClearColorConfig, CompositingSpace,
NormalizedRenderTarget, RenderTarget, Viewport,
};
use bevy_core_pipeline::{
Core2d, Core2dSystems, Core3d, Core3dSystems, tonemapping::tonemapping, upscaling::upscaling,
};
use bevy_ecs::entity::ContainsEntity;
use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemParam;
use bevy_image::Image;
use bevy_mesh::VertexBufferLayout;
use bevy_render::{
Extract, ExtractSchedule, GpuResourceAppExt, Render, RenderApp, RenderSystems,
camera::ExtractedCamera,
render_asset::RenderAssets,
render_resource::{
BindGroup, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, BindingResource,
BindingType, BlendState, Buffer, BufferAddress, BufferBindingType, BufferDescriptor,
BufferSize, BufferUsages, COPY_BUFFER_ALIGNMENT, CachedRenderPipelineId, ColorTargetState,
ColorWrites, CommandEncoderDescriptor, Extent3d, FilterMode, FragmentState, IndexFormat,
LoadOp, MipmapFilterMode, MultisampleState, Operations, Origin3d, PipelineCache,
PrimitiveState, PrimitiveTopology, RawBufferVec, RenderPassColorAttachment,
RenderPassDescriptor, RenderPipelineDescriptor, Sampler, SamplerBindingType,
SamplerDescriptor, ShaderStages, SpecializedRenderPipeline, SpecializedRenderPipelines,
StoreOp, TexelCopyBufferLayout, TexelCopyTextureInfo, Texture, TextureAspect,
TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
TextureView, TextureViewDescriptor, TextureViewDimension, VertexAttribute, VertexFormat,
VertexState, VertexStepMode, WgpuFeatures,
},
renderer::{
RenderContext, RenderDevice, RenderGraph, RenderGraphSystems, RenderQueue, ViewQuery,
},
texture::GpuImage,
view::{ExtractedView, ExtractedWindows, Msaa, ViewTarget},
};
use bevy_shader::Shader;
use bevy_window::PrimaryWindow;
use bytemuck::{Pod, Zeroable};
use dear_imgui_rs as imgui;
use imgui::render::{DrawCmdSnapshot, DrawIdx, TextureBinding};
use std::collections::{HashMap, HashSet};
use std::mem::size_of;
pub use crate::texture::ImguiBevyTextures;
use crate::{ImguiBackendStatus, ImguiViewportWindow};
pub const IMGUI_SHADER_HANDLE: Handle<Shader> =
uuid_handle!("2c893cad-60d2-4e92-8544-4ab807ed9c5a");
type OverlayCameraQuery<'w> = Query<
'w,
'w,
(
Entity,
&'w Camera,
&'w RenderTarget,
Option<&'w ImguiOverlayCamera>,
Option<&'w ImguiOverlayDisabled>,
),
>;
const COPY_BYTES_PER_ROW_ALIGNMENT: u32 = 256;
const MANAGED_TEXTURE_NAMESPACE: u64 = 0x4000_0000_0000_0000;
pub const IMGUI_VERTEX_ENTRY_POINT: &str = "vs_main";
pub const IMGUI_FRAGMENT_ENTRY_POINT: &str = "fs_main";
pub const IMGUI_SHADER_SOURCE: &str = r#"
struct VertexInput {
@location(0) position: vec2<f32>,
@location(1) uv: vec2<f32>,
@location(2) color: vec4<f32>,
};
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) uv: vec2<f32>,
};
struct ImguiUniforms {
mvp: mat4x4<f32>,
gamma: f32,
_padding: vec3<f32>,
};
@group(0) @binding(0)
var<uniform> uniforms: ImguiUniforms;
@group(1) @binding(0)
var imgui_texture: texture_2d<f32>;
@group(1) @binding(1)
var imgui_sampler: sampler;
@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.position = uniforms.mvp * vec4<f32>(in.position, 0.0, 1.0);
out.color = in.color;
out.uv = in.uv;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let color = in.color * textureSample(imgui_texture, imgui_sampler, in.uv);
let corrected = pow(color.rgb, vec3<f32>(uniforms.gamma));
return vec4<f32>(corrected, color.a);
}
"#;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct ImguiUniforms {
pub mvp: [[f32; 4]; 4],
pub gamma: f32,
pub _padding: [f32; 7],
}
impl ImguiUniforms {
#[must_use]
pub fn from_display_rect(display_pos: [f32; 2], display_size: [f32; 2]) -> Self {
let left = display_pos[0];
let right = display_pos[0] + display_size[0];
let top = display_pos[1];
let bottom = display_pos[1] + display_size[1];
Self {
mvp: [
[2.0 / (right - left), 0.0, 0.0, 0.0],
[0.0, 2.0 / (top - bottom), 0.0, 0.0],
[0.0, 0.0, 0.5, 0.0],
[
(right + left) / (left - right),
(top + bottom) / (bottom - top),
0.5,
1.0,
],
],
gamma: 1.0,
_padding: [0.0; 7],
}
}
#[must_use]
pub fn with_gamma(mut self, gamma: f32) -> Self {
self.gamma = gamma;
self
}
#[must_use]
pub fn gamma_for_target(
format: TextureFormat,
compositing_space: Option<CompositingSpace>,
) -> f32 {
if format.is_srgb() || compositing_space == Some(CompositingSpace::Srgb) {
2.2
} else {
1.0
}
}
}
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
pub struct RenderFeature;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImguiCameraTarget {
pub camera: Entity,
pub order: isize,
pub target: NormalizedRenderTarget,
pub viewport_id: Option<imgui::Id>,
pub camera_viewport: Option<ImguiCameraViewport>,
pub explicit: bool,
}
#[derive(Component, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub struct ImguiOverlayCamera;
#[derive(Component, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub struct ImguiOverlayDisabled;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ImguiCameraViewport {
pub physical_position: [u32; 2],
pub physical_size: [u32; 2],
}
impl From<&Viewport> for ImguiCameraViewport {
fn from(viewport: &Viewport) -> Self {
Self {
physical_position: [viewport.physical_position.x, viewport.physical_position.y],
physical_size: [viewport.physical_size.x, viewport.physical_size.y],
}
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct ImguiGpuVertex {
pub position: [f32; 2],
pub uv: [f32; 2],
pub color: u32,
}
impl From<imgui::render::DrawVert> for ImguiGpuVertex {
fn from(value: imgui::render::DrawVert) -> Self {
Self {
position: value.pos,
uv: value.uv,
color: value.col,
}
}
}
#[must_use]
pub fn imgui_vertex_buffer_layout() -> VertexBufferLayout {
VertexBufferLayout {
array_stride: size_of::<ImguiGpuVertex>() as BufferAddress,
step_mode: VertexStepMode::Vertex,
attributes: vec![
VertexAttribute {
format: VertexFormat::Float32x2,
offset: 0,
shader_location: 0,
},
VertexAttribute {
format: VertexFormat::Float32x2,
offset: 8,
shader_location: 1,
},
VertexAttribute {
format: VertexFormat::Unorm8x4,
offset: 16,
shader_location: 2,
},
],
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ImguiScissorRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ImguiSampler {
#[default]
Linear,
Nearest,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImguiPreparedDraw {
pub camera: Entity,
pub order: isize,
pub target: NormalizedRenderTarget,
pub viewport_id: Option<imgui::Id>,
pub texture: TextureBinding,
pub sampler: ImguiSampler,
pub scissor: ImguiScissorRect,
pub framebuffer_size: [u32; 2],
pub camera_viewport: Option<ImguiCameraViewport>,
pub index_range: std::ops::Range<u32>,
pub vertex_offset: i32,
}
#[derive(Resource, Clone, Debug, Default)]
pub struct ImguiPreparedRenderFrame {
frame_index: Option<u64>,
uniforms: Option<ImguiUniforms>,
uniforms_by_camera: HashMap<Entity, ImguiUniforms>,
vertices: Vec<ImguiGpuVertex>,
indices: Vec<DrawIdx>,
draws: Vec<ImguiPreparedDraw>,
texture_request_count: usize,
}
impl ImguiPreparedRenderFrame {
#[must_use]
pub fn frame_index(&self) -> Option<u64> {
self.frame_index
}
#[must_use]
pub fn uniforms(&self) -> Option<ImguiUniforms> {
self.uniforms
}
#[must_use]
pub fn uniforms_for_camera(&self, camera: Entity) -> Option<ImguiUniforms> {
self.uniforms_by_camera
.get(&camera)
.copied()
.or(self.uniforms)
}
#[must_use]
pub fn vertices(&self) -> &[ImguiGpuVertex] {
&self.vertices
}
#[must_use]
pub fn indices(&self) -> &[DrawIdx] {
&self.indices
}
#[must_use]
pub fn draws(&self) -> &[ImguiPreparedDraw] {
&self.draws
}
#[must_use]
pub fn texture_request_count(&self) -> usize {
self.texture_request_count
}
fn replace(&mut self, frame: PreparedFrameData) {
self.frame_index = Some(frame.frame_index);
self.uniforms = frame.uniforms;
self.uniforms_by_camera = frame.uniforms_by_camera;
self.vertices = frame.vertices;
self.indices = frame.indices;
self.draws = frame.draws;
self.texture_request_count = frame.texture_request_count;
}
fn clear(&mut self, frame_index: Option<u64>) {
self.frame_index = frame_index;
self.uniforms = None;
self.uniforms_by_camera.clear();
self.vertices.clear();
self.indices.clear();
self.draws.clear();
self.texture_request_count = 0;
}
}
struct PreparedFrameData {
frame_index: u64,
uniforms: Option<ImguiUniforms>,
uniforms_by_camera: HashMap<Entity, ImguiUniforms>,
vertices: Vec<ImguiGpuVertex>,
indices: Vec<DrawIdx>,
draws: Vec<ImguiPreparedDraw>,
texture_request_count: usize,
}
#[derive(Resource)]
pub struct ImguiGpuBuffers {
vertices: RawBufferVec<ImguiGpuVertex>,
indices: RawBufferVec<DrawIdx>,
}
impl Default for ImguiGpuBuffers {
fn default() -> Self {
let mut vertices = RawBufferVec::new(BufferUsages::VERTEX);
vertices.set_label(Some("dear_imgui_bevy_vertices"));
let mut indices = RawBufferVec::new(BufferUsages::INDEX);
indices.set_label(Some("dear_imgui_bevy_indices"));
Self { vertices, indices }
}
}
impl ImguiGpuBuffers {
#[must_use]
pub fn vertex_len(&self) -> usize {
self.vertices.len()
}
#[must_use]
pub fn index_len(&self) -> usize {
self.indices.len()
}
#[must_use]
pub fn has_uploaded_buffers(&self) -> bool {
self.vertices.buffer().is_some() && self.indices.buffer().is_some()
}
fn vertex_buffer(&self) -> Option<&Buffer> {
self.vertices.buffer()
}
fn index_buffer(&self) -> Option<&Buffer> {
self.indices.buffer()
}
fn upload(
&mut self,
prepared: &ImguiPreparedRenderFrame,
render_device: &RenderDevice,
render_queue: &RenderQueue,
) {
self.vertices.clear();
self.indices.clear();
for vertex in prepared.vertices() {
self.vertices.push(*vertex);
}
for index in prepared.indices() {
self.indices.push(*index);
}
pad_index_buffer_for_copy_alignment(&mut self.indices);
self.vertices.write_buffer(render_device, render_queue);
self.indices.write_buffer(render_device, render_queue);
}
}
fn pad_index_buffer_for_copy_alignment(indices: &mut RawBufferVec<DrawIdx>) {
let byte_len = indices.len() * size_of::<DrawIdx>();
if byte_len.is_multiple_of(COPY_BUFFER_ALIGNMENT as usize) {
return;
}
debug_assert_eq!(size_of::<DrawIdx>(), 2);
indices.push(DrawIdx::default());
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct ImguiPipelineKey {
pub target_format: TextureFormat,
pub sample_count: u32,
}
#[derive(Resource, Clone)]
pub struct ImguiRenderPipeline {
shader: Handle<Shader>,
common_layout: BindGroupLayoutDescriptor,
texture_layout: BindGroupLayoutDescriptor,
}
impl ImguiRenderPipeline {
#[must_use]
pub fn shader(&self) -> &Handle<Shader> {
&self.shader
}
#[must_use]
pub fn common_layout(&self) -> &BindGroupLayoutDescriptor {
&self.common_layout
}
#[must_use]
pub fn texture_layout(&self) -> &BindGroupLayoutDescriptor {
&self.texture_layout
}
}
impl Default for ImguiRenderPipeline {
fn default() -> Self {
let common_layout = BindGroupLayoutDescriptor::new(
"dear_imgui_bevy_common_layout",
&[bevy_render::render_resource::BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: BufferSize::new(size_of::<ImguiUniforms>() as u64),
},
count: None,
}],
);
let texture_layout = BindGroupLayoutDescriptor::new(
"dear_imgui_bevy_texture_layout",
&[
bevy_render::render_resource::BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
bevy_render::render_resource::BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: None,
},
],
);
Self {
shader: IMGUI_SHADER_HANDLE,
common_layout,
texture_layout,
}
}
}
impl SpecializedRenderPipeline for ImguiRenderPipeline {
type Key = ImguiPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("dear_imgui_bevy_pipeline".into()),
layout: vec![self.common_layout.clone(), self.texture_layout.clone()],
vertex: VertexState {
shader: self.shader.clone(),
entry_point: Some(IMGUI_VERTEX_ENTRY_POINT.into()),
buffers: vec![imgui_vertex_buffer_layout()],
..Default::default()
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
entry_point: Some(IMGUI_FRAGMENT_ENTRY_POINT.into()),
targets: vec![Some(ColorTargetState {
format: key.target_format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
..Default::default()
}),
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleList,
..Default::default()
},
multisample: MultisampleState {
count: key.sample_count,
..Default::default()
},
zero_initialize_workgroup_memory: true,
..Default::default()
}
}
}
#[derive(Resource)]
pub struct ImguiPipelineGpuResources {
uniforms_by_camera: HashMap<Entity, ImguiCameraUniformResources>,
_fallback_texture: Texture,
_fallback_view: TextureView,
fallback_bind_group: BindGroup,
}
struct ImguiCameraUniformResources {
buffer: Buffer,
bind_group: BindGroup,
}
impl FromWorld for ImguiPipelineGpuResources {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let render_queue = world.resource::<RenderQueue>();
let pipeline_cache = world.resource::<PipelineCache>();
let pipeline = world.resource::<ImguiRenderPipeline>();
let texture_layout = pipeline_cache.get_bind_group_layout(pipeline.texture_layout());
let sampler = create_standard_imgui_sampler(render_device, ImguiSampler::Linear);
let fallback_texture = render_device.create_texture(&TextureDescriptor {
label: Some("dear_imgui_bevy_fallback_texture"),
size: Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
});
write_texture_rows(
render_queue,
&fallback_texture,
Origin3d::ZERO,
1,
1,
4,
&[255, 255, 255, 255],
);
let fallback_view = fallback_texture.create_view(&TextureViewDescriptor::default());
let fallback_bind_group = create_texture_sampler_bind_group(
render_device,
&texture_layout,
Some("dear_imgui_bevy_fallback_texture_bind_group"),
&fallback_view,
&sampler,
);
Self {
uniforms_by_camera: HashMap::new(),
_fallback_texture: fallback_texture,
_fallback_view: fallback_view,
fallback_bind_group,
}
}
}
impl ImguiPipelineGpuResources {
fn prepare_camera_uniforms(
&mut self,
prepared: &ImguiPreparedRenderFrame,
render_device: &RenderDevice,
render_queue: &RenderQueue,
pipeline_cache: &PipelineCache,
pipeline: &ImguiRenderPipeline,
) {
let active_cameras = prepared
.draws()
.iter()
.map(|draw| draw.camera)
.collect::<std::collections::HashSet<_>>();
self.uniforms_by_camera
.retain(|camera, _| active_cameras.contains(camera));
for camera in active_cameras {
let Some(uniforms) = prepared.uniforms_for_camera(camera) else {
continue;
};
let resources = self.uniforms_by_camera.entry(camera).or_insert_with(|| {
create_camera_uniform_resources(camera, render_device, pipeline_cache, pipeline)
});
render_queue.write_buffer(&resources.buffer, 0, bytemuck::bytes_of(&uniforms));
}
}
fn update_camera_uniforms(
&self,
camera: Entity,
render_queue: &RenderQueue,
uniforms: ImguiUniforms,
) -> Option<&BindGroup> {
let resources = self.uniforms_by_camera.get(&camera)?;
render_queue.write_buffer(&resources.buffer, 0, bytemuck::bytes_of(&uniforms));
Some(&resources.bind_group)
}
#[must_use]
pub fn uniform_bind_group_count(&self) -> usize {
self.uniforms_by_camera.len()
}
fn fallback_bind_group(&self) -> &BindGroup {
&self.fallback_bind_group
}
}
fn create_camera_uniform_resources(
camera: Entity,
render_device: &RenderDevice,
pipeline_cache: &PipelineCache,
pipeline: &ImguiRenderPipeline,
) -> ImguiCameraUniformResources {
let common_layout = pipeline_cache.get_bind_group_layout(pipeline.common_layout());
let _ = camera;
let uniform_buffer = render_device.create_buffer(&BufferDescriptor {
label: Some("dear_imgui_bevy_uniforms_camera"),
size: size_of::<ImguiUniforms>() as BufferAddress,
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = render_device.create_bind_group(
Some("dear_imgui_bevy_common_bind_group"),
&common_layout,
&[BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
);
ImguiCameraUniformResources {
buffer: uniform_buffer,
bind_group,
}
}
struct ImguiRenderTexture {
texture: Option<Texture>,
_view: Option<TextureView>,
extent: Option<[u32; 2]>,
linear_bind_group: BindGroup,
nearest_bind_group: BindGroup,
}
impl ImguiRenderTexture {
fn clone_for_legacy_id(&self) -> Self {
Self {
texture: None,
_view: None,
extent: None,
linear_bind_group: self.linear_bind_group.clone(),
nearest_bind_group: self.nearest_bind_group.clone(),
}
}
}
struct ImguiTextureUpload<'a> {
format: imgui::texture::TextureFormat,
width: u32,
height: u32,
row_pitch: usize,
pixels: &'a [u8],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ImguiTextureViewCompatibility {
texture_usage: TextureUsages,
view_usage: Option<TextureUsages>,
sample_count: u32,
texture_dimension: TextureDimension,
depth_or_array_layers: u32,
view_dimension: Option<TextureViewDimension>,
format: TextureFormat,
aspect: TextureAspect,
}
impl ImguiTextureViewCompatibility {
fn from_gpu_image(gpu_image: &GpuImage) -> Self {
let texture_descriptor = &gpu_image.texture_descriptor;
let view_descriptor = gpu_image.texture_view_descriptor.as_ref();
Self {
texture_usage: texture_descriptor.usage,
view_usage: view_descriptor.and_then(|descriptor| descriptor.usage),
sample_count: texture_descriptor.sample_count,
texture_dimension: texture_descriptor.dimension,
depth_or_array_layers: texture_descriptor.size.depth_or_array_layers,
view_dimension: view_descriptor.and_then(|descriptor| descriptor.dimension),
format: view_descriptor
.and_then(|descriptor| descriptor.format)
.unwrap_or(texture_descriptor.format),
aspect: view_descriptor.map_or(TextureAspect::All, |descriptor| descriptor.aspect),
}
}
fn supports_imgui_sampling(self, device_features: WgpuFeatures) -> bool {
if !self
.resolved_view_usage()
.contains(TextureUsages::TEXTURE_BINDING)
{
return false;
}
if self.sample_count != 1 || self.resolved_view_dimension() != TextureViewDimension::D2 {
return false;
}
matches!(
self.format
.sample_type(Some(self.aspect), Some(device_features)),
Some(TextureSampleType::Float { filterable: true })
)
}
fn resolved_view_usage(self) -> TextureUsages {
let usage = self.view_usage.unwrap_or_else(TextureUsages::empty);
if usage.is_empty() {
self.texture_usage
} else {
usage
}
}
fn resolved_view_dimension(self) -> TextureViewDimension {
let default_dimension = match self.texture_dimension {
TextureDimension::D1 => TextureViewDimension::D1,
TextureDimension::D2 => {
if self.depth_or_array_layers == 1 {
TextureViewDimension::D2
} else {
TextureViewDimension::D2Array
}
}
TextureDimension::D3 => TextureViewDimension::D3,
};
self.view_dimension.unwrap_or(default_dimension)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ImguiViewportTarget {
viewport_id: imgui::Id,
window: Entity,
}
#[derive(Resource, Default)]
pub struct ImguiTextureBindGroups {
textures: HashMap<TextureBinding, ImguiRenderTexture>,
bevy_image_bindings: HashSet<TextureBinding>,
}
impl ImguiTextureBindGroups {
pub fn insert(&mut self, texture: TextureBinding, bind_group: BindGroup) {
self.bevy_image_bindings.remove(&texture);
self.textures.insert(
texture,
ImguiRenderTexture {
texture: None,
_view: None,
extent: None,
linear_bind_group: bind_group.clone(),
nearest_bind_group: bind_group,
},
);
}
pub fn remove(&mut self, texture: &TextureBinding) {
self.textures.remove(texture);
self.bevy_image_bindings.remove(texture);
}
#[must_use]
pub fn len(&self) -> usize {
self.textures.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.textures.is_empty()
}
fn get(&self, texture: &TextureBinding, sampler: ImguiSampler) -> Option<&BindGroup> {
self.textures.get(texture).map(|texture| match sampler {
ImguiSampler::Linear => &texture.linear_bind_group,
ImguiSampler::Nearest => &texture.nearest_bind_group,
})
}
fn insert_render_texture(
&mut self,
texture: TextureBinding,
render_texture: ImguiRenderTexture,
) {
self.bevy_image_bindings.remove(&texture);
self.textures.insert(texture, render_texture);
}
fn insert_bevy_image(&mut self, texture: TextureBinding, bind_group: BindGroup) {
self.textures.insert(
texture,
ImguiRenderTexture {
texture: None,
_view: None,
extent: None,
linear_bind_group: bind_group.clone(),
nearest_bind_group: bind_group,
},
);
self.bevy_image_bindings.insert(texture);
}
fn retain_bevy_image_bindings(&mut self, active_bindings: &HashSet<TextureBinding>) {
let stale_bindings = self
.bevy_image_bindings
.difference(active_bindings)
.copied()
.collect::<Vec<_>>();
for binding in stale_bindings {
self.remove(&binding);
}
}
}
#[derive(Resource, Clone, Debug, Default)]
pub struct ImguiExtractedBevyTextures {
textures: Vec<(imgui::TextureId, bevy_asset::AssetId<Image>)>,
}
impl ImguiExtractedBevyTextures {
#[must_use]
pub fn textures(&self) -> &[(imgui::TextureId, bevy_asset::AssetId<Image>)] {
&self.textures
}
#[must_use]
pub fn len(&self) -> usize {
self.textures.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.textures.is_empty()
}
fn replace(&mut self, textures: Vec<(imgui::TextureId, bevy_asset::AssetId<Image>)>) {
self.textures = textures;
}
}
#[derive(Resource, Default)]
pub struct ImguiQueuedPipelines {
by_camera: HashMap<Entity, CachedRenderPipelineId>,
}
impl ImguiQueuedPipelines {
#[must_use]
pub fn get(&self, camera: Entity) -> Option<CachedRenderPipelineId> {
self.by_camera.get(&camera).copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.by_camera.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_camera.is_empty()
}
}
#[derive(Resource, Clone, Debug, Default)]
pub struct ImguiExtractedRenderFrame {
frame_index: Option<u64>,
snapshot: Option<imgui::render::snapshot::FrameSnapshot>,
camera_targets: Vec<ImguiCameraTarget>,
}
impl ImguiExtractedRenderFrame {
#[must_use]
pub fn frame_index(&self) -> Option<u64> {
self.frame_index
}
#[must_use]
pub fn snapshot(&self) -> Option<&imgui::render::snapshot::FrameSnapshot> {
self.snapshot.as_ref()
}
#[must_use]
pub fn camera_targets(&self) -> &[ImguiCameraTarget] {
&self.camera_targets
}
fn replace(
&mut self,
frame_index: u64,
snapshot: imgui::render::snapshot::FrameSnapshot,
camera_targets: Vec<ImguiCameraTarget>,
) {
self.frame_index = Some(frame_index);
self.snapshot = Some(snapshot);
self.camera_targets = camera_targets;
}
fn clear(&mut self, frame_index: u64) {
self.frame_index = (frame_index > 0).then_some(frame_index);
self.snapshot = None;
self.camera_targets.clear();
}
}
#[derive(Resource, Default)]
struct ImguiRenderExtractionInstalled;
pub(crate) fn install_render_extraction(app: &mut App) -> bool {
install_imgui_shader_asset(app);
app.init_resource::<crate::ImguiTextureFeedbackQueue>();
let texture_feedback = app
.world()
.resource::<crate::ImguiTextureFeedbackQueue>()
.clone();
if app.get_sub_app_mut(RenderApp).is_none() {
return false;
}
install_standard_draw_callbacks(app);
let render_app = app
.get_sub_app_mut(RenderApp)
.expect("RenderApp availability was checked before installing callbacks");
if render_app
.world()
.contains_resource::<ImguiRenderExtractionInstalled>()
{
return true;
}
render_app
.init_resource::<ImguiExtractedRenderFrame>()
.init_resource::<ImguiExtractedBevyTextures>()
.init_resource::<ImguiPreparedRenderFrame>()
.init_resource::<ImguiGpuBuffers>()
.init_resource::<ImguiRenderPipeline>()
.init_resource::<SpecializedRenderPipelines<ImguiRenderPipeline>>()
.init_resource::<ImguiTextureBindGroups>()
.init_resource::<ImguiQueuedPipelines>()
.init_gpu_resource::<ImguiPipelineGpuResources>()
.insert_resource(texture_feedback)
.insert_resource(ImguiRenderExtractionInstalled)
.add_systems(
ExtractSchedule,
(extract_imgui_bevy_textures, extract_imgui_render_frame).chain(),
)
.add_systems(
Render,
(prepare_imgui_render_frame, queue_imgui_pipelines)
.chain()
.in_set(RenderSystems::Queue),
)
.add_systems(
Render,
upload_imgui_buffers.in_set(RenderSystems::PrepareResources),
)
.add_systems(
Render,
prepare_imgui_texture_bind_groups.in_set(RenderSystems::PrepareBindGroups),
)
.add_systems(
Render,
prepare_imgui_uniform_bind_groups.in_set(RenderSystems::PrepareBindGroups),
)
.add_systems(
RenderGraph,
ensure_presentable_window_outputs.in_set(RenderGraphSystems::Finish),
)
.add_systems(
Core2d,
render_imgui_overlay
.after(tonemapping)
.before(upscaling)
.in_set(Core2dSystems::PostProcess),
)
.add_systems(
Core3d,
render_imgui_overlay
.after(tonemapping)
.before(upscaling)
.in_set(Core3dSystems::PostProcess),
);
true
}
fn install_standard_draw_callbacks(app: &mut App) {
let Some(mut imgui_context) = app.world_mut().get_non_send_mut::<crate::ImguiContext>() else {
return;
};
install_standard_draw_callbacks_for_context(imgui_context.context_mut());
}
pub(crate) fn install_standard_draw_callbacks_for_context(context: &mut imgui::Context) {
let platform_io = context.platform_io_mut();
platform_io.set_draw_callback_reset_render_state_raw(Some(imgui_bevy_draw_callback_reset));
platform_io.set_draw_callback_set_sampler_linear_raw(Some(imgui_bevy_draw_callback_linear));
platform_io.set_draw_callback_set_sampler_nearest_raw(Some(imgui_bevy_draw_callback_nearest));
}
unsafe extern "C" fn imgui_bevy_draw_callback_reset(
_parent_list: *const imgui::sys::ImDrawList,
_cmd: *const imgui::sys::ImDrawCmd,
) {
}
unsafe extern "C" fn imgui_bevy_draw_callback_linear(
_parent_list: *const imgui::sys::ImDrawList,
_cmd: *const imgui::sys::ImDrawCmd,
) {
}
unsafe extern "C" fn imgui_bevy_draw_callback_nearest(
_parent_list: *const imgui::sys::ImDrawList,
_cmd: *const imgui::sys::ImDrawCmd,
) {
}
fn install_imgui_shader_asset(app: &mut App) {
app.init_resource::<Assets<Shader>>();
app.world_mut()
.resource_mut::<Assets<Shader>>()
.insert(
IMGUI_SHADER_HANDLE.id(),
Shader::from_wgsl(IMGUI_SHADER_SOURCE, "dear_imgui_bevy/imgui.wgsl"),
)
.expect("UUID shader handles are always valid asset ids");
}
fn extract_imgui_bevy_textures(
registry: Extract<Option<Res<crate::ImguiBevyTextures>>>,
mut extracted: ResMut<ImguiExtractedBevyTextures>,
) {
let textures = registry
.as_ref()
.map(|registry| registry.iter().collect::<Vec<_>>())
.unwrap_or_default();
extracted.replace(textures);
}
fn extract_imgui_render_frame(
mut extracted: ResMut<ImguiExtractedRenderFrame>,
output: Extract<Res<crate::ImguiFrameOutput>>,
backend_status: Extract<Res<ImguiBackendStatus>>,
primary_window: Extract<Query<Entity, With<PrimaryWindow>>>,
viewport_windows: Extract<Query<(Entity, &ImguiViewportWindow)>>,
cameras: Extract<OverlayCameraQuery<'_>>,
) {
let Some(snapshot) = output.snapshot().cloned() else {
extracted.clear(output.frame_index());
return;
};
let primary_window = primary_window.single().ok();
let viewport_targets = if backend_status.multi_viewport_supported {
collect_viewport_targets(viewport_windows.iter())
} else {
Vec::new()
};
let camera_targets = collect_camera_targets(primary_window, &viewport_targets, cameras.iter());
extracted.replace(output.frame_index(), snapshot, camera_targets);
}
fn collect_camera_targets<'w>(
primary_window: Option<Entity>,
viewport_targets: &[ImguiViewportTarget],
cameras: impl Iterator<
Item = (
Entity,
&'w Camera,
&'w RenderTarget,
Option<&'w ImguiOverlayCamera>,
Option<&'w ImguiOverlayDisabled>,
),
>,
) -> Vec<ImguiCameraTarget> {
let targets = cameras
.filter(|(_, camera, _, _, overlay_disabled)| {
camera.is_active && overlay_disabled.is_none()
})
.filter_map(|(entity, camera, target, overlay_camera, _)| {
target
.normalize(primary_window)
.map(|target| ImguiCameraTarget {
camera: entity,
order: camera.order,
viewport_id: viewport_id_for_target(&target, viewport_targets),
camera_viewport: camera.viewport.as_ref().map(ImguiCameraViewport::from),
target,
explicit: overlay_camera.is_some(),
})
})
.collect::<Vec<_>>();
let mut targets = select_overlay_camera_per_target(targets);
targets.sort_by_key(|target| (target.order, target.camera));
targets
}
fn select_overlay_camera_per_target(targets: Vec<ImguiCameraTarget>) -> Vec<ImguiCameraTarget> {
let mut by_render_target = HashMap::<NormalizedRenderTarget, ImguiCameraTarget>::new();
for target in targets {
match by_render_target.entry(target.target.clone()) {
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(target);
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
let current = entry.get();
if overlay_target_precedence(&target) >= overlay_target_precedence(current) {
entry.insert(target);
}
}
}
}
by_render_target.into_values().collect()
}
fn overlay_target_precedence(target: &ImguiCameraTarget) -> (bool, isize, Entity) {
(target.explicit, target.order, target.camera)
}
fn collect_viewport_targets<'w>(
viewport_windows: impl Iterator<Item = (Entity, &'w ImguiViewportWindow)>,
) -> Vec<ImguiViewportTarget> {
viewport_windows
.map(|(window, viewport_window)| ImguiViewportTarget {
viewport_id: viewport_window.viewport_id,
window,
})
.collect()
}
fn viewport_id_for_target(
target: &NormalizedRenderTarget,
viewport_targets: &[ImguiViewportTarget],
) -> Option<imgui::Id> {
let NormalizedRenderTarget::Window(window) = target else {
return None;
};
let entity = window.entity();
viewport_targets
.iter()
.find(|target| target.window == entity)
.map(|target| target.viewport_id)
}
fn prepare_imgui_render_frame(
extracted: Res<ImguiExtractedRenderFrame>,
mut prepared: ResMut<ImguiPreparedRenderFrame>,
) {
let Some(snapshot) = extracted.snapshot() else {
prepared.clear(extracted.frame_index());
return;
};
let Some(frame_index) = extracted.frame_index() else {
prepared.clear(None);
return;
};
let primary_uniforms = valid_display_rect(&snapshot.draw).map(|_| {
ImguiUniforms::from_display_rect(snapshot.draw.display_pos, snapshot.draw.display_size)
});
let (vertices, indices, draws, uniforms_by_camera) =
prepare_snapshot_draw_data(snapshot, extracted.camera_targets());
prepared.replace(PreparedFrameData {
frame_index,
uniforms: primary_uniforms,
uniforms_by_camera,
vertices,
indices,
draws,
texture_request_count: snapshot.texture_requests.len(),
});
}
fn upload_imgui_buffers(
prepared: Res<ImguiPreparedRenderFrame>,
mut gpu_buffers: ResMut<ImguiGpuBuffers>,
render_device: Option<Res<RenderDevice>>,
render_queue: Option<Res<RenderQueue>>,
) {
if let (Some(render_device), Some(render_queue)) = (render_device, render_queue) {
gpu_buffers.upload(&prepared, &render_device, &render_queue);
}
}
fn prepare_imgui_uniform_bind_groups(
prepared: Res<ImguiPreparedRenderFrame>,
render_device: Option<Res<RenderDevice>>,
render_queue: Option<Res<RenderQueue>>,
pipeline_cache: Option<Res<PipelineCache>>,
pipeline: Res<ImguiRenderPipeline>,
mut gpu_resources: Option<ResMut<ImguiPipelineGpuResources>>,
) {
let (Some(render_device), Some(render_queue), Some(pipeline_cache), Some(gpu_resources)) = (
render_device,
render_queue,
pipeline_cache,
gpu_resources.as_deref_mut(),
) else {
return;
};
gpu_resources.prepare_camera_uniforms(
&prepared,
&render_device,
&render_queue,
&pipeline_cache,
&pipeline,
);
}
#[derive(SystemParam)]
struct ImguiTextureBindGroupParams<'w> {
extracted: Res<'w, ImguiExtractedRenderFrame>,
extracted_bevy_textures: Res<'w, ImguiExtractedBevyTextures>,
gpu_images: Option<Res<'w, RenderAssets<GpuImage>>>,
render_device: Option<Res<'w, RenderDevice>>,
render_queue: Option<Res<'w, RenderQueue>>,
pipeline_cache: Option<Res<'w, PipelineCache>>,
pipeline: Res<'w, ImguiRenderPipeline>,
texture_feedback: Res<'w, crate::ImguiTextureFeedbackQueue>,
}
fn prepare_imgui_texture_bind_groups(
params: ImguiTextureBindGroupParams,
mut texture_bind_groups: ResMut<ImguiTextureBindGroups>,
) {
let (Some(render_device), Some(render_queue), Some(pipeline_cache)) = (
params.render_device,
params.render_queue,
params.pipeline_cache,
) else {
return;
};
let Some(snapshot) = params.extracted.snapshot() else {
prepare_bevy_image_texture_bind_groups(
params.gpu_images.as_deref(),
¶ms.extracted_bevy_textures,
&render_device,
&pipeline_cache,
¶ms.pipeline,
&mut texture_bind_groups,
);
return;
};
for request in &snapshot.texture_requests {
match &request.op {
imgui::render::TextureOp::Create {
format,
width,
height,
row_pitch,
pixels,
} => {
if !validate_managed_texture_extent(&render_device, *width, *height) {
continue;
}
if let Some(render_texture) = create_imgui_render_texture(
&render_device,
&render_queue,
&pipeline_cache,
¶ms.pipeline,
ImguiTextureUpload {
format: *format,
width: *width,
height: *height,
row_pitch: *row_pitch,
pixels,
},
) {
let tex_id = managed_texture_id(request.id);
texture_bind_groups.insert_render_texture(
TextureBinding::Legacy(tex_id),
render_texture.clone_for_legacy_id(),
);
texture_bind_groups
.insert_render_texture(TextureBinding::Managed(request.id), render_texture);
params.texture_feedback.push(
imgui::render::snapshot::TextureFeedback::with_tex_id(
request.id,
imgui::texture::TextureStatus::OK,
tex_id,
),
);
}
}
imgui::render::TextureOp::Update {
format,
width,
height,
rects,
} => {
if !validate_managed_texture_extent(&render_device, *width, *height) {
continue;
}
if let Some(render_texture) = texture_bind_groups
.textures
.get(&TextureBinding::Managed(request.id))
{
let Some(texture_extent) = render_texture.extent else {
continue;
};
if texture_extent != [*width, *height] {
continue;
}
let Some(texture) = render_texture.texture.as_ref() else {
continue;
};
let Some(updates) =
convert_imgui_texture_update_rects(*format, *width, *height, rects)
else {
continue;
};
for update in updates {
write_texture_rows(
&render_queue,
texture,
update.origin,
update.width,
update.height,
update.row_pitch,
&update.pixels,
);
}
params
.texture_feedback
.push(imgui::render::snapshot::TextureFeedback::status(
request.id,
imgui::texture::TextureStatus::OK,
));
}
}
imgui::render::TextureOp::Destroy => {
texture_bind_groups.remove(&TextureBinding::Managed(request.id));
texture_bind_groups.remove(&TextureBinding::Legacy(managed_texture_id(request.id)));
params
.texture_feedback
.push(imgui::render::snapshot::TextureFeedback::status(
request.id,
imgui::texture::TextureStatus::Destroyed,
));
}
}
}
prepare_bevy_image_texture_bind_groups(
params.gpu_images.as_deref(),
¶ms.extracted_bevy_textures,
&render_device,
&pipeline_cache,
¶ms.pipeline,
&mut texture_bind_groups,
);
}
fn managed_texture_id(id: imgui::render::snapshot::ManagedTextureId) -> imgui::TextureId {
imgui::TextureId::new(MANAGED_TEXTURE_NAMESPACE | (u64::from(id.raw() as u32) + 1))
}
fn validate_managed_texture_extent(render_device: &RenderDevice, width: u32, height: u32) -> bool {
managed_texture_extent_supported(
width,
height,
render_device.limits().max_texture_dimension_2d,
)
}
fn managed_texture_extent_supported(width: u32, height: u32, max_dimension_2d: u32) -> bool {
width > 0 && height > 0 && width <= max_dimension_2d && height <= max_dimension_2d
}
fn validate_texture_update_rect(
texture_width: u32,
texture_height: u32,
rect: imgui::TextureRect,
) -> bool {
let x = u32::from(rect.x);
let y = u32::from(rect.y);
let w = u32::from(rect.w);
let h = u32::from(rect.h);
w > 0
&& h > 0
&& x.checked_add(w).is_some_and(|right| right <= texture_width)
&& y.checked_add(h)
.is_some_and(|bottom| bottom <= texture_height)
}
fn create_imgui_render_texture(
render_device: &RenderDevice,
render_queue: &RenderQueue,
pipeline_cache: &PipelineCache,
pipeline: &ImguiRenderPipeline,
upload: ImguiTextureUpload<'_>,
) -> Option<ImguiRenderTexture> {
let (pixels, row_pitch) = convert_imgui_texture_pixels(
upload.format,
upload.width,
upload.height,
upload.row_pitch,
upload.pixels,
)?;
let texture = render_device.create_texture(&TextureDescriptor {
label: Some("dear_imgui_bevy_texture"),
size: Extent3d {
width: upload.width,
height: upload.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
});
write_texture_rows(
render_queue,
&texture,
Origin3d::ZERO,
upload.width,
upload.height,
row_pitch,
&pixels,
);
let view = texture.create_view(&TextureViewDescriptor::default());
let layout = pipeline_cache.get_bind_group_layout(pipeline.texture_layout());
let linear_sampler = create_standard_imgui_sampler(render_device, ImguiSampler::Linear);
let nearest_sampler = create_standard_imgui_sampler(render_device, ImguiSampler::Nearest);
let linear_bind_group = create_texture_sampler_bind_group(
render_device,
&layout,
Some("dear_imgui_bevy_texture_bind_group"),
&view,
&linear_sampler,
);
let nearest_bind_group = create_texture_sampler_bind_group(
render_device,
&layout,
Some("dear_imgui_bevy_texture_bind_group_nearest"),
&view,
&nearest_sampler,
);
Some(ImguiRenderTexture {
texture: Some(texture),
_view: Some(view),
extent: Some([upload.width, upload.height]),
linear_bind_group,
nearest_bind_group,
})
}
fn create_standard_imgui_sampler(render_device: &RenderDevice, sampler: ImguiSampler) -> Sampler {
match sampler {
ImguiSampler::Linear => render_device.create_sampler(&SamplerDescriptor {
label: Some("dear_imgui_bevy_sampler"),
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: MipmapFilterMode::Linear,
..Default::default()
}),
ImguiSampler::Nearest => render_device.create_sampler(&SamplerDescriptor {
label: Some("dear_imgui_bevy_nearest_sampler"),
mag_filter: FilterMode::Nearest,
min_filter: FilterMode::Nearest,
mipmap_filter: MipmapFilterMode::Nearest,
..Default::default()
}),
}
}
fn create_texture_sampler_bind_group(
render_device: &RenderDevice,
layout: &BindGroupLayout,
label: Option<&'static str>,
view: &TextureView,
sampler: &Sampler,
) -> BindGroup {
render_device.create_bind_group(
label,
layout,
&[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(sampler),
},
],
)
}
struct ConvertedTextureUpdateRect {
origin: Origin3d,
width: u32,
height: u32,
row_pitch: u32,
pixels: Vec<u8>,
}
fn convert_imgui_texture_update_rects(
format: imgui::texture::TextureFormat,
texture_width: u32,
texture_height: u32,
rects: &[imgui::render::snapshot::TextureUploadRect],
) -> Option<Vec<ConvertedTextureUpdateRect>> {
if rects.is_empty() {
return None;
}
let mut updates = Vec::with_capacity(rects.len());
for rect in rects {
if !validate_texture_update_rect(texture_width, texture_height, rect.rect) {
return None;
}
let width = u32::from(rect.rect.w);
let height = u32::from(rect.rect.h);
let (pixels, row_pitch) =
convert_imgui_texture_pixels(format, width, height, rect.row_pitch, &rect.data)?;
updates.push(ConvertedTextureUpdateRect {
origin: Origin3d {
x: u32::from(rect.rect.x),
y: u32::from(rect.rect.y),
z: 0,
},
width,
height,
row_pitch,
pixels,
});
}
Some(updates)
}
fn prepare_bevy_image_texture_bind_groups(
gpu_images: Option<&RenderAssets<GpuImage>>,
extracted_bevy_textures: &ImguiExtractedBevyTextures,
render_device: &RenderDevice,
pipeline_cache: &PipelineCache,
pipeline: &ImguiRenderPipeline,
texture_bind_groups: &mut ImguiTextureBindGroups,
) {
retain_extracted_bevy_image_bindings(extracted_bevy_textures, texture_bind_groups);
let Some(gpu_images) = gpu_images else {
return;
};
for (texture_id, asset_id) in extracted_bevy_textures.textures() {
let binding = TextureBinding::Legacy(*texture_id);
let Some(gpu_image) = gpu_images.get(*asset_id) else {
texture_bind_groups.remove(&binding);
continue;
};
let Some(bind_group) = create_bevy_image_texture_bind_group(
render_device,
pipeline_cache,
pipeline,
gpu_image,
) else {
texture_bind_groups.remove(&binding);
continue;
};
texture_bind_groups.insert_bevy_image(binding, bind_group);
}
}
fn retain_extracted_bevy_image_bindings(
extracted_bevy_textures: &ImguiExtractedBevyTextures,
texture_bind_groups: &mut ImguiTextureBindGroups,
) {
let active_bindings = extracted_bevy_textures
.textures()
.iter()
.map(|(texture_id, _)| TextureBinding::Legacy(*texture_id))
.collect::<HashSet<_>>();
texture_bind_groups.retain_bevy_image_bindings(&active_bindings);
}
fn create_bevy_image_texture_bind_group(
render_device: &RenderDevice,
pipeline_cache: &PipelineCache,
pipeline: &ImguiRenderPipeline,
gpu_image: &GpuImage,
) -> Option<BindGroup> {
if !ImguiTextureViewCompatibility::from_gpu_image(gpu_image)
.supports_imgui_sampling(render_device.features())
{
return None;
}
let layout = pipeline_cache.get_bind_group_layout(pipeline.texture_layout());
Some(create_texture_sampler_bind_group(
render_device,
&layout,
Some("dear_imgui_bevy_image_texture_bind_group"),
&gpu_image.texture_view,
&gpu_image.sampler,
))
}
fn convert_imgui_texture_pixels(
format: imgui::texture::TextureFormat,
width: u32,
height: u32,
row_pitch: usize,
pixels: &[u8],
) -> Option<(Vec<u8>, u32)> {
let width = usize::try_from(width).ok()?;
let height = usize::try_from(height).ok()?;
if width == 0 || height == 0 {
return None;
}
match format {
imgui::texture::TextureFormat::RGBA32 => {
let dst_row_pitch = width.checked_mul(4)?;
copy_or_repack_rows(pixels, row_pitch, dst_row_pitch, height)
.map(|pixels| (pixels, dst_row_pitch as u32))
}
imgui::texture::TextureFormat::Alpha8 => {
let mut rgba = vec![0; width.checked_mul(height)?.checked_mul(4)?];
for row in 0..height {
let src_start = row.checked_mul(row_pitch)?;
let src_end = src_start.checked_add(width)?;
if src_end > pixels.len() {
return None;
}
for (col, alpha) in pixels[src_start..src_end].iter().copied().enumerate() {
let dst = row.checked_mul(width)?.checked_add(col)?.checked_mul(4)?;
rgba[dst..dst + 4].copy_from_slice(&[255, 255, 255, alpha]);
}
}
Some((rgba, width.checked_mul(4)? as u32))
}
}
}
fn copy_or_repack_rows(
pixels: &[u8],
src_row_pitch: usize,
dst_row_pitch: usize,
rows: usize,
) -> Option<Vec<u8>> {
if src_row_pitch < dst_row_pitch {
return None;
}
let required_src = src_row_pitch.checked_mul(rows)?;
if pixels.len() < required_src {
return None;
}
if src_row_pitch == dst_row_pitch {
return Some(pixels[..required_src].to_vec());
}
let mut out = vec![0; dst_row_pitch.checked_mul(rows)?];
for row in 0..rows {
let src = row.checked_mul(src_row_pitch)?;
let dst = row.checked_mul(dst_row_pitch)?;
out[dst..dst + dst_row_pitch].copy_from_slice(&pixels[src..src + dst_row_pitch]);
}
Some(out)
}
fn write_texture_rows(
render_queue: &RenderQueue,
texture: &Texture,
origin: Origin3d,
width: u32,
height: u32,
row_pitch: u32,
pixels: &[u8],
) {
if width == 0 || height == 0 || row_pitch == 0 {
return;
}
let alignment = COPY_BYTES_PER_ROW_ALIGNMENT;
let padded_row_pitch = row_pitch.div_ceil(alignment) * alignment;
if padded_row_pitch == row_pitch {
render_queue.write_texture(
TexelCopyTextureInfo {
texture: &**texture,
mip_level: 0,
origin,
aspect: TextureAspect::All,
},
pixels,
TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(row_pitch),
rows_per_image: Some(height),
},
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
return;
}
let row_pitch = usize::try_from(row_pitch).ok();
let padded_row_pitch = usize::try_from(padded_row_pitch).ok();
let height_usize = usize::try_from(height).ok();
let (Some(row_pitch), Some(padded_row_pitch), Some(height_usize)) =
(row_pitch, padded_row_pitch, height_usize)
else {
return;
};
let Some(required) = row_pitch.checked_mul(height_usize) else {
return;
};
if pixels.len() < required {
return;
}
let Some(padded_len) = padded_row_pitch.checked_mul(height_usize) else {
return;
};
let mut padded = vec![0; padded_len];
for row in 0..height_usize {
let src = row * row_pitch;
let dst = row * padded_row_pitch;
padded[dst..dst + row_pitch].copy_from_slice(&pixels[src..src + row_pitch]);
}
render_queue.write_texture(
TexelCopyTextureInfo {
texture: &**texture,
mip_level: 0,
origin,
aspect: TextureAspect::All,
},
&padded,
TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_row_pitch as u32),
rows_per_image: Some(height),
},
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
}
fn queue_imgui_pipelines(
prepared: Res<ImguiPreparedRenderFrame>,
views: Query<(&ExtractedView, Option<&Msaa>)>,
pipeline_cache: Option<Res<PipelineCache>>,
pipeline: Res<ImguiRenderPipeline>,
mut pipelines: ResMut<SpecializedRenderPipelines<ImguiRenderPipeline>>,
mut queued: ResMut<ImguiQueuedPipelines>,
) {
queued.by_camera.clear();
let Some(pipeline_cache) = pipeline_cache else {
return;
};
for (view, msaa) in &views {
let camera = view.retained_view_entity.main_entity.id();
if !prepared.draws().iter().any(|draw| draw.camera == camera) {
continue;
}
let sample_count = msaa.map_or(1, Msaa::samples);
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
ImguiPipelineKey {
target_format: view.target_format,
sample_count,
},
);
queued.by_camera.insert(camera, pipeline_id);
}
}
#[derive(SystemParam)]
struct ImguiRenderPassParams<'w> {
pipeline_cache: Option<Res<'w, PipelineCache>>,
render_queue: Option<Res<'w, RenderQueue>>,
queued: Res<'w, ImguiQueuedPipelines>,
prepared: Res<'w, ImguiPreparedRenderFrame>,
gpu_buffers: Res<'w, ImguiGpuBuffers>,
gpu_resources: Option<Res<'w, ImguiPipelineGpuResources>>,
texture_bind_groups: Res<'w, ImguiTextureBindGroups>,
}
fn render_imgui_overlay(
view: ViewQuery<(&ViewTarget, &ExtractedView, Option<&ExtractedCamera>)>,
params: ImguiRenderPassParams,
mut render_context: RenderContext,
) {
let Some(pipeline_cache) = params.pipeline_cache else {
return;
};
let Some(gpu_resources) = params.gpu_resources else {
return;
};
let Some(render_queue) = params.render_queue else {
return;
};
if !params.gpu_buffers.has_uploaded_buffers() {
return;
}
let (view_target, view, camera_metadata) = view.into_inner();
let camera = view.retained_view_entity.main_entity.id();
let Some(pipeline_id) = params.queued.get(camera) else {
return;
};
let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id) else {
return;
};
let drawable = params
.prepared
.draws()
.iter()
.filter(|draw| draw.camera == camera)
.collect::<Vec<_>>();
if drawable.is_empty() {
return;
}
let Some(uniforms) = params.prepared.uniforms_for_camera(camera) else {
return;
};
let uniforms = uniforms.with_gamma(ImguiUniforms::gamma_for_target(
view.target_format,
camera_metadata.and_then(|camera| camera.compositing_space),
));
let render_target_size = camera_metadata
.and_then(|camera| camera.physical_target_size)
.map(|size| [size.x, size.y]);
let Some(common_bind_group) =
gpu_resources.update_camera_uniforms(camera, &render_queue, uniforms)
else {
return;
};
let color_attachment = view_target.get_color_attachment();
let mut render_pass =
render_context
.command_encoder()
.begin_render_pass(&RenderPassDescriptor {
label: Some("dear_imgui_bevy_overlay_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: color_attachment.view,
depth_slice: color_attachment.depth_slice,
resolve_target: color_attachment.resolve_target,
ops: Operations {
load: LoadOp::Load,
store: StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, common_bind_group, &[]);
if let Some(viewport) = render_viewport_for_pass(&drawable, render_target_size) {
render_pass.set_viewport(
viewport.physical_position[0] as f32,
viewport.physical_position[1] as f32,
viewport.physical_size[0] as f32,
viewport.physical_size[1] as f32,
0.0,
1.0,
);
}
if let Some(vertex_buffer) = params.gpu_buffers.vertex_buffer() {
render_pass.set_vertex_buffer(0, *vertex_buffer.slice(..));
} else {
return;
}
if let Some(index_buffer) = params.gpu_buffers.index_buffer() {
render_pass.set_index_buffer(*index_buffer.slice(..), IndexFormat::Uint16);
} else {
return;
}
for draw in drawable {
let texture_bind_group = params
.texture_bind_groups
.get(&draw.texture, draw.sampler)
.unwrap_or_else(|| gpu_resources.fallback_bind_group());
let Some(scissor) = scissor_for_render_pass(draw, render_target_size) else {
continue;
};
render_pass.set_bind_group(1, texture_bind_group, &[]);
render_pass.set_scissor_rect(scissor.x, scissor.y, scissor.width, scissor.height);
render_pass.draw_indexed(draw.index_range.clone(), draw.vertex_offset, 0..1);
}
}
fn ensure_presentable_window_outputs(
windows: Res<ExtractedWindows>,
views: Query<(&ViewTarget, &ExtractedCamera)>,
clear_color: Res<ClearColor>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
) {
let mut encoder = None;
for window in windows.values() {
let mut view_needs_present = false;
let mut output_color = None;
for (view_target, camera) in &views {
if !camera_targets_window(camera, window.entity) {
continue;
}
view_needs_present |= view_target.needs_present();
output_color.get_or_insert_with(|| output_clear_color(Some(camera), &clear_color));
}
if !window.needs_initial_present && !view_needs_present {
continue;
}
let Some(swapchain_view) = window.swap_chain_texture_view.as_ref() else {
continue;
};
let encoder = encoder.get_or_insert_with(|| {
render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("dear_imgui_bevy_presentable_output_encoder"),
})
});
let ops = if view_needs_present {
Operations {
load: LoadOp::Load,
store: StoreOp::Store,
}
} else {
let clear_color = output_color.flatten().unwrap_or(clear_color.0);
let clear_color: bevy_color::LinearRgba = clear_color.into();
Operations {
load: LoadOp::Clear(clear_color.into()),
store: StoreOp::Store,
}
};
let render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("dear_imgui_bevy_clear_unwritten_output"),
color_attachments: &[Some(RenderPassColorAttachment {
view: swapchain_view,
depth_slice: None,
resolve_target: None,
ops,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
drop(render_pass);
}
if let Some(encoder) = encoder {
render_queue.submit([encoder.finish()]);
}
}
fn camera_targets_window(camera: &ExtractedCamera, window: Entity) -> bool {
matches!(
camera.target,
Some(NormalizedRenderTarget::Window(target)) if target.entity() == window
)
}
fn output_clear_color(
camera: Option<&ExtractedCamera>,
clear_color: &ClearColor,
) -> Option<bevy_color::Color> {
match camera.map(|camera| camera.output_mode) {
Some(CameraOutputMode::Skip) => None,
Some(CameraOutputMode::Write {
clear_color: ClearColorConfig::Custom(color),
..
}) => Some(color),
Some(CameraOutputMode::Write {
clear_color: ClearColorConfig::Default | ClearColorConfig::None,
..
})
| None => Some(clear_color.0),
}
}
fn prepare_snapshot_draw_data(
snapshot: &imgui::render::FrameSnapshot,
camera_targets: &[ImguiCameraTarget],
) -> (
Vec<ImguiGpuVertex>,
Vec<DrawIdx>,
Vec<ImguiPreparedDraw>,
HashMap<Entity, ImguiUniforms>,
) {
let viewport_draws = snapshot_viewport_draws(snapshot);
let vertex_count = viewport_draws
.iter()
.flat_map(|(_, draw)| &draw.draw_lists)
.map(|list| list.vtx.len())
.sum();
let index_count = viewport_draws
.iter()
.flat_map(|(_, draw)| &draw.draw_lists)
.map(|list| list.idx.len())
.sum();
let mut vertices = Vec::with_capacity(vertex_count);
let mut indices = Vec::with_capacity(index_count);
let mut draws = Vec::new();
let mut uniforms_by_camera = HashMap::new();
let mut list_vertex_base = 0usize;
let mut list_index_base = 0usize;
for (viewport_id, draw) in viewport_draws {
let target_cameras = camera_targets
.iter()
.filter(|target| target.viewport_id == viewport_id)
.collect::<Vec<_>>();
if target_cameras.is_empty() {
continue;
}
if valid_display_rect(draw).is_none() {
continue;
}
let target_cameras = target_cameras
.into_iter()
.filter_map(|target| {
let uniforms = uniforms_for_target_draw(draw, target)?;
Some((target, uniforms))
})
.collect::<Vec<_>>();
if target_cameras.is_empty() {
continue;
}
for (target, uniforms) in &target_cameras {
uniforms_by_camera.insert(target.camera, *uniforms);
}
let mut active_sampler = ImguiSampler::Linear;
for list in &draw.draw_lists {
vertices.extend(list.vtx.iter().copied().map(ImguiGpuVertex::from));
indices.extend(list.idx.iter().copied());
for command in &list.commands {
let (count, clip_rect, texture, vtx_offset, idx_offset) = match command {
DrawCmdSnapshot::Elements {
count,
clip_rect,
texture,
vtx_offset,
idx_offset,
} => (*count, *clip_rect, *texture, *vtx_offset, *idx_offset),
DrawCmdSnapshot::ResetRenderState | DrawCmdSnapshot::SetSamplerLinear => {
active_sampler = ImguiSampler::Linear;
continue;
}
DrawCmdSnapshot::SetSamplerNearest => {
active_sampler = ImguiSampler::Nearest;
continue;
}
};
let Some(scissor) = scissor_from_clip_rect(draw, clip_rect) else {
continue;
};
let Some(framebuffer_size) = framebuffer_size_for_draw(draw) else {
continue;
};
let Some(index_start) = list_index_base.checked_add(idx_offset) else {
continue;
};
let Some(index_end) = index_start.checked_add(count) else {
continue;
};
let Some(vertex_offset) = list_vertex_base.checked_add(vtx_offset) else {
continue;
};
if index_end > list_index_base + list.idx.len()
|| vertex_offset > list_vertex_base + list.vtx.len()
{
continue;
}
let local_index_end = index_end - list_index_base;
if draw_indices_reference_out_of_bounds(
&list.idx[idx_offset..local_index_end],
vertex_offset,
vertices.len(),
) {
continue;
}
let Ok(index_start) = u32::try_from(index_start) else {
continue;
};
let Ok(index_end) = u32::try_from(index_end) else {
continue;
};
let Ok(vertex_offset) = i32::try_from(vertex_offset) else {
continue;
};
for (target, _) in &target_cameras {
draws.push(ImguiPreparedDraw {
camera: target.camera,
order: target.order,
target: target.target.clone(),
viewport_id,
texture,
sampler: active_sampler,
scissor,
framebuffer_size,
camera_viewport: target.camera_viewport,
index_range: index_start..index_end,
vertex_offset,
});
}
}
list_vertex_base += list.vtx.len();
list_index_base += list.idx.len();
}
}
(vertices, indices, draws, uniforms_by_camera)
}
fn snapshot_viewport_draws(
snapshot: &imgui::render::FrameSnapshot,
) -> Vec<(Option<imgui::Id>, &imgui::render::DrawDataSnapshot)> {
if snapshot.viewports.is_empty() {
return vec![(None, &snapshot.draw)];
}
let mut draws = snapshot
.viewports
.iter()
.map(|viewport| (Some(viewport.viewport_id), &viewport.draw))
.collect::<Vec<_>>();
if !draws.iter().any(|(viewport_id, _)| viewport_id.is_none()) {
draws.insert(0, (None, &snapshot.draw));
}
draws
}
fn scissor_from_clip_rect(
draw: &imgui::render::DrawDataSnapshot,
clip_rect: [f32; 4],
) -> Option<ImguiScissorRect> {
let valid_rect = valid_display_rect(draw)?;
if !clip_rect.iter().all(|value| value.is_finite()) {
return None;
}
if clip_rect[2] <= clip_rect[0] || clip_rect[3] <= clip_rect[1] {
return None;
}
let scale = draw.framebuffer_scale;
let min_x = ((clip_rect[0] - draw.display_pos[0]) * scale[0]).floor();
let min_y = ((clip_rect[1] - draw.display_pos[1]) * scale[1]).floor();
let max_x = ((clip_rect[2] - draw.display_pos[0]) * scale[0]).ceil();
let max_y = ((clip_rect[3] - draw.display_pos[1]) * scale[1]).ceil();
let framebuffer_width = valid_rect.framebuffer_width;
let framebuffer_height = valid_rect.framebuffer_height;
let min_x = min_x.clamp(0.0, framebuffer_width);
let min_y = min_y.clamp(0.0, framebuffer_height);
let max_x = max_x.clamp(min_x, framebuffer_width);
let max_y = max_y.clamp(min_y, framebuffer_height);
let width = max_x - min_x;
let height = max_y - min_y;
if width <= 0.0 || height <= 0.0 {
return None;
}
Some(ImguiScissorRect {
x: min_x as u32,
y: min_y as u32,
width: width as u32,
height: height as u32,
})
}
fn framebuffer_size_for_draw(draw: &imgui::render::DrawDataSnapshot) -> Option<[u32; 2]> {
let valid_rect = valid_display_rect(draw)?;
Some([
valid_rect.framebuffer_width as u32,
valid_rect.framebuffer_height as u32,
])
}
fn uniforms_for_target_draw(
draw: &imgui::render::DrawDataSnapshot,
target: &ImguiCameraTarget,
) -> Option<ImguiUniforms> {
if let Some(viewport) = target.camera_viewport {
let [viewport_width, viewport_height] = viewport.physical_size;
if viewport_width == 0 || viewport_height == 0 {
return None;
}
let [scale_x, scale_y] = draw.framebuffer_scale;
if scale_x <= 0.0 || scale_y <= 0.0 || !scale_x.is_finite() || !scale_y.is_finite() {
return None;
}
let display_pos = [
draw.display_pos[0] + viewport.physical_position[0] as f32 / scale_x,
draw.display_pos[1] + viewport.physical_position[1] as f32 / scale_y,
];
let display_size = [
viewport.physical_size[0] as f32 / scale_x,
viewport.physical_size[1] as f32 / scale_y,
];
if ![
display_pos[0],
display_pos[1],
display_size[0],
display_size[1],
]
.iter()
.all(|value| value.is_finite())
{
return None;
}
return Some(ImguiUniforms::from_display_rect(display_pos, display_size));
}
Some(ImguiUniforms::from_display_rect(
draw.display_pos,
draw.display_size,
))
}
fn render_viewport_for_draw(draw: &ImguiPreparedDraw) -> Option<ImguiCameraViewport> {
let camera_viewport = draw.camera_viewport?;
let [width, height] = camera_viewport.physical_size;
if width == 0 || height == 0 {
return None;
}
Some(camera_viewport)
}
fn render_viewport_for_pass(
drawable: &[&ImguiPreparedDraw],
render_target_size: Option<[u32; 2]>,
) -> Option<ImguiCameraViewport> {
let viewport = drawable
.iter()
.find_map(|draw| render_viewport_for_draw(draw))?;
let Some(render_target_size) = render_target_size else {
return Some(viewport);
};
let viewport_rect = intersect_scissor_with_rect(
ImguiScissorRect {
x: viewport.physical_position[0],
y: viewport.physical_position[1],
width: viewport.physical_size[0],
height: viewport.physical_size[1],
},
[0, 0],
render_target_size,
)?;
Some(ImguiCameraViewport {
physical_position: [viewport_rect.x, viewport_rect.y],
physical_size: [viewport_rect.width, viewport_rect.height],
})
}
fn scissor_for_render_pass(
draw: &ImguiPreparedDraw,
render_target_size: Option<[u32; 2]>,
) -> Option<ImguiScissorRect> {
let viewport = render_viewport_for_draw(draw);
let scissor = match viewport {
Some(viewport) => intersect_scissor_with_camera_viewport(draw.scissor, viewport)?,
None => draw.scissor,
};
match render_target_size {
Some(size) => intersect_scissor_with_rect(scissor, [0, 0], size),
None => Some(scissor),
}
}
fn intersect_scissor_with_camera_viewport(
scissor: ImguiScissorRect,
viewport: ImguiCameraViewport,
) -> Option<ImguiScissorRect> {
intersect_scissor_with_rect(scissor, viewport.physical_position, viewport.physical_size)
}
fn intersect_scissor_with_rect(
scissor: ImguiScissorRect,
rect_position: [u32; 2],
rect_size: [u32; 2],
) -> Option<ImguiScissorRect> {
let [rect_width, rect_height] = rect_size;
if rect_width == 0 || rect_height == 0 {
return None;
}
let scissor_min_x = u64::from(scissor.x);
let scissor_min_y = u64::from(scissor.y);
let scissor_max_x = scissor_min_x.checked_add(u64::from(scissor.width))?;
let scissor_max_y = scissor_min_y.checked_add(u64::from(scissor.height))?;
let viewport_min_x = u64::from(rect_position[0]);
let viewport_min_y = u64::from(rect_position[1]);
let viewport_max_x = viewport_min_x.checked_add(u64::from(rect_width))?;
let viewport_max_y = viewport_min_y.checked_add(u64::from(rect_height))?;
let min_x = scissor_min_x.max(viewport_min_x);
let min_y = scissor_min_y.max(viewport_min_y);
let max_x = scissor_max_x.min(viewport_max_x);
let max_y = scissor_max_y.min(viewport_max_y);
if max_x <= min_x || max_y <= min_y {
return None;
}
Some(ImguiScissorRect {
x: u32::try_from(min_x).ok()?,
y: u32::try_from(min_y).ok()?,
width: u32::try_from(max_x - min_x).ok()?,
height: u32::try_from(max_y - min_y).ok()?,
})
}
fn draw_indices_reference_out_of_bounds(
indices: &[DrawIdx],
vertex_offset: usize,
vertex_count: usize,
) -> bool {
indices.iter().copied().max().is_some_and(|max_index| {
usize::from(max_index)
.checked_add(vertex_offset)
.is_none_or(|absolute_index| absolute_index >= vertex_count)
})
}
#[derive(Clone, Copy)]
struct ValidDisplayRect {
framebuffer_width: f32,
framebuffer_height: f32,
}
fn valid_display_rect(draw: &imgui::render::DrawDataSnapshot) -> Option<ValidDisplayRect> {
let [display_x, display_y] = draw.display_pos;
let [display_width, display_height] = draw.display_size;
let [scale_x, scale_y] = draw.framebuffer_scale;
if ![
display_x,
display_y,
display_width,
display_height,
scale_x,
scale_y,
]
.iter()
.all(|value| value.is_finite())
{
return None;
}
if display_width <= 0.0 || display_height <= 0.0 || scale_x <= 0.0 || scale_y <= 0.0 {
return None;
}
let framebuffer_width = (display_width * scale_x).ceil();
let framebuffer_height = (display_height * scale_y).ceil();
if !framebuffer_width.is_finite()
|| !framebuffer_height.is_finite()
|| framebuffer_width <= 0.0
|| framebuffer_height <= 0.0
|| framebuffer_width > u32::MAX as f32
|| framebuffer_height > u32::MAX as f32
{
return None;
}
Some(ValidDisplayRect {
framebuffer_width,
framebuffer_height,
})
}
#[cfg(test)]
mod tests {
use super::*;
use bevy_asset::AssetId;
use bevy_ecs::schedule::ScheduleLabel;
use bevy_render::{renderer::initialize_renderer, settings::WgpuSettings};
type RawDrawCallback =
unsafe extern "C" fn(*const imgui::sys::ImDrawList, *const imgui::sys::ImDrawCmd);
fn assert_fn_ptr_eq(actual: imgui::sys::ImDrawCallback, expected: RawDrawCallback) {
assert_eq!(
actual.map(|callback| std::ptr::fn_addr_eq(callback, expected) as u8),
Some(1)
);
}
#[test]
fn texture_conversion_repackages_padded_rgba_rows() {
let pixels = [
1, 2, 3, 4, 9, 9, 9, 9, 5, 6, 7, 8, 8, 8, 8, 8,
];
let (converted, row_pitch) =
convert_imgui_texture_pixels(imgui::texture::TextureFormat::RGBA32, 1, 2, 8, &pixels)
.expect("valid padded RGBA32 upload should convert");
assert_eq!(row_pitch, 4);
assert_eq!(converted, [1, 2, 3, 4, 5, 6, 7, 8]);
}
#[test]
fn texture_conversion_expands_alpha8_to_white_rgba() {
let pixels = [0, 128, 255, 64];
let (converted, row_pitch) =
convert_imgui_texture_pixels(imgui::texture::TextureFormat::Alpha8, 2, 2, 2, &pixels)
.expect("valid Alpha8 upload should convert");
assert_eq!(row_pitch, 8);
assert_eq!(
converted,
[
255, 255, 255, 0, 255, 255, 255, 128, 255, 255, 255, 255, 255, 255, 255, 64,
]
);
}
#[test]
fn index_buffer_upload_pads_to_copy_alignment() {
let mut indices = RawBufferVec::new(BufferUsages::INDEX);
indices.push(1);
indices.push(2);
indices.push(3);
pad_index_buffer_for_copy_alignment(&mut indices);
assert_eq!(indices.len(), 4);
assert_eq!(indices.values(), &vec![1, 2, 3, 0]);
}
#[test]
fn gamma_helper_uses_srgb_for_srgb_targets_and_compositing_space() {
assert_eq!(
ImguiUniforms::gamma_for_target(TextureFormat::Rgba8UnormSrgb, None),
2.2
);
assert_eq!(
ImguiUniforms::gamma_for_target(
TextureFormat::Rgba8Unorm,
Some(CompositingSpace::Srgb)
),
2.2
);
assert_eq!(
ImguiUniforms::gamma_for_target(TextureFormat::Rgba8Unorm, None),
1.0
);
assert_eq!(
ImguiUniforms::gamma_for_target(
TextureFormat::Rgba8Unorm,
Some(CompositingSpace::Linear)
),
1.0
);
}
#[test]
fn render_installation_exposes_standard_sampler_callbacks() {
let mut app = App::new();
app.add_plugins(bevy_render::extract_plugin::ExtractPlugin::default());
app.sub_app_mut(RenderApp).update_schedule = Some(Render.intern());
app.add_plugins(crate::ImguiPlugin::default());
let context = app
.world()
.get_non_send::<crate::ImguiContext>()
.expect("ImguiPlugin should install the context");
let platform_io = context.context().platform_io();
assert_fn_ptr_eq(
platform_io.draw_callback_reset_render_state_raw(),
imgui_bevy_draw_callback_reset,
);
assert_fn_ptr_eq(
platform_io.draw_callback_set_sampler_linear_raw(),
imgui_bevy_draw_callback_linear,
);
assert_fn_ptr_eq(
platform_io.draw_callback_set_sampler_nearest_raw(),
imgui_bevy_draw_callback_nearest,
);
}
#[test]
fn prepared_draws_preserve_standard_sampler_callback_state() {
let camera = Entity::from_raw_u32(7).expect("test entity index should be valid");
let snapshot = imgui::render::FrameSnapshot {
draw: imgui::render::DrawDataSnapshot {
display_pos: [0.0, 0.0],
display_size: [32.0, 32.0],
framebuffer_scale: [1.0, 1.0],
draw_lists: vec![imgui::render::DrawListSnapshot {
vtx: vec![
imgui::render::DrawVert::new([0.0, 0.0], [0.0, 0.0], 0xFFFF_FFFF),
imgui::render::DrawVert::new([1.0, 0.0], [1.0, 0.0], 0xFFFF_FFFF),
imgui::render::DrawVert::new([0.0, 1.0], [0.0, 1.0], 0xFFFF_FFFF),
],
idx: vec![0, 1, 2],
commands: vec![
DrawCmdSnapshot::SetSamplerNearest,
DrawCmdSnapshot::Elements {
count: 3,
clip_rect: [0.0, 0.0, 16.0, 16.0],
texture: TextureBinding::Legacy(imgui::TextureId::new(1)),
vtx_offset: 0,
idx_offset: 0,
},
DrawCmdSnapshot::ResetRenderState,
DrawCmdSnapshot::Elements {
count: 3,
clip_rect: [0.0, 0.0, 16.0, 16.0],
texture: TextureBinding::Legacy(imgui::TextureId::new(1)),
vtx_offset: 0,
idx_offset: 0,
},
],
}],
},
viewports: Vec::new(),
texture_requests: Vec::new(),
};
let targets = [ImguiCameraTarget {
camera,
order: 0,
target: NormalizedRenderTarget::Window(
bevy_window::WindowRef::Entity(camera)
.normalize(None)
.expect("entity window target should normalize"),
),
viewport_id: None,
camera_viewport: None,
explicit: false,
}];
let (_, _, draws, _) = prepare_snapshot_draw_data(&snapshot, &targets);
assert_eq!(draws.len(), 2);
assert_eq!(draws[0].sampler, ImguiSampler::Nearest);
assert_eq!(draws[1].sampler, ImguiSampler::Linear);
}
#[test]
fn prepared_draws_preserve_sampler_state_across_draw_lists() {
let camera = Entity::from_raw_u32(8).expect("test entity index should be valid");
let snapshot = imgui::render::FrameSnapshot {
draw: imgui::render::DrawDataSnapshot {
display_pos: [0.0, 0.0],
display_size: [32.0, 32.0],
framebuffer_scale: [1.0, 1.0],
draw_lists: vec![
imgui::render::DrawListSnapshot {
vtx: vec![
imgui::render::DrawVert::new([0.0, 0.0], [0.0, 0.0], 0xFFFF_FFFF),
imgui::render::DrawVert::new([1.0, 0.0], [1.0, 0.0], 0xFFFF_FFFF),
imgui::render::DrawVert::new([0.0, 1.0], [0.0, 1.0], 0xFFFF_FFFF),
],
idx: vec![0, 1, 2],
commands: vec![
DrawCmdSnapshot::SetSamplerNearest,
DrawCmdSnapshot::Elements {
count: 3,
clip_rect: [0.0, 0.0, 16.0, 16.0],
texture: TextureBinding::Legacy(imgui::TextureId::new(1)),
vtx_offset: 0,
idx_offset: 0,
},
],
},
imgui::render::DrawListSnapshot {
vtx: vec![
imgui::render::DrawVert::new([2.0, 0.0], [0.0, 0.0], 0xFFFF_FFFF),
imgui::render::DrawVert::new([3.0, 0.0], [1.0, 0.0], 0xFFFF_FFFF),
imgui::render::DrawVert::new([2.0, 1.0], [0.0, 1.0], 0xFFFF_FFFF),
],
idx: vec![0, 1, 2],
commands: vec![DrawCmdSnapshot::Elements {
count: 3,
clip_rect: [0.0, 0.0, 16.0, 16.0],
texture: TextureBinding::Legacy(imgui::TextureId::new(1)),
vtx_offset: 0,
idx_offset: 0,
}],
},
],
},
viewports: Vec::new(),
texture_requests: Vec::new(),
};
let targets = [ImguiCameraTarget {
camera,
order: 0,
target: NormalizedRenderTarget::Window(
bevy_window::WindowRef::Entity(camera)
.normalize(None)
.expect("entity window target should normalize"),
),
viewport_id: None,
camera_viewport: None,
explicit: false,
}];
let (_, _, draws, _) = prepare_snapshot_draw_data(&snapshot, &targets);
assert_eq!(draws.len(), 2);
assert_eq!(draws[0].sampler, ImguiSampler::Nearest);
assert_eq!(draws[1].sampler, ImguiSampler::Nearest);
}
#[test]
fn prepared_draws_skip_commands_with_out_of_range_index_or_vertex_offsets() {
let camera = Entity::from_raw_u32(9).expect("test entity index should be valid");
let snapshot = imgui::render::FrameSnapshot {
draw: imgui::render::DrawDataSnapshot {
display_pos: [0.0, 0.0],
display_size: [32.0, 32.0],
framebuffer_scale: [1.0, 1.0],
draw_lists: vec![imgui::render::DrawListSnapshot {
vtx: vec![
imgui::render::DrawVert::new([0.0, 0.0], [0.0, 0.0], 0xFFFF_FFFF),
imgui::render::DrawVert::new([1.0, 0.0], [1.0, 0.0], 0xFFFF_FFFF),
imgui::render::DrawVert::new([0.0, 1.0], [0.0, 1.0], 0xFFFF_FFFF),
],
idx: vec![0, 1, 2, 3, 1, 2],
commands: vec![
DrawCmdSnapshot::Elements {
count: 1,
clip_rect: [0.0, 0.0, 16.0, 16.0],
texture: TextureBinding::Legacy(imgui::TextureId::new(1)),
vtx_offset: 0,
idx_offset: 6,
},
DrawCmdSnapshot::Elements {
count: 1,
clip_rect: [0.0, 0.0, 16.0, 16.0],
texture: TextureBinding::Legacy(imgui::TextureId::new(1)),
vtx_offset: 0,
idx_offset: 3,
},
DrawCmdSnapshot::Elements {
count: 3,
clip_rect: [0.0, 0.0, 16.0, 16.0],
texture: TextureBinding::Legacy(imgui::TextureId::new(1)),
vtx_offset: 4,
idx_offset: 3,
},
DrawCmdSnapshot::Elements {
count: 3,
clip_rect: [0.0, 0.0, 16.0, 16.0],
texture: TextureBinding::Legacy(imgui::TextureId::new(1)),
vtx_offset: 0,
idx_offset: 0,
},
],
}],
},
viewports: Vec::new(),
texture_requests: Vec::new(),
};
let targets = [ImguiCameraTarget {
camera,
order: 0,
target: NormalizedRenderTarget::Window(
bevy_window::WindowRef::Entity(camera)
.normalize(None)
.expect("entity window target should normalize"),
),
viewport_id: None,
camera_viewport: None,
explicit: false,
}];
let (_, _, draws, _) = prepare_snapshot_draw_data(&snapshot, &targets);
assert_eq!(draws.len(), 1);
assert_eq!(draws[0].index_range, 0..3);
assert_eq!(draws[0].vertex_offset, 0);
}
#[test]
fn draw_index_validation_rejects_absolute_indices_outside_uploaded_vertices() {
assert!(!draw_indices_reference_out_of_bounds(&[0, 1, 2], 0, 3));
assert!(!draw_indices_reference_out_of_bounds(&[0, 1, 2], 3, 6));
assert!(draw_indices_reference_out_of_bounds(&[3], 0, 3));
assert!(draw_indices_reference_out_of_bounds(&[0], 3, 3));
}
#[test]
fn scissor_rejects_non_finite_or_invalid_display_rects() {
let mut draw = imgui::render::DrawDataSnapshot {
display_pos: [0.0, 0.0],
display_size: [32.0, 32.0],
framebuffer_scale: [1.0, 1.0],
draw_lists: Vec::new(),
};
assert!(scissor_from_clip_rect(&draw, [0.0, 0.0, 16.0, 16.0]).is_some());
assert!(scissor_from_clip_rect(&draw, [f32::NAN, 0.0, 16.0, 16.0]).is_none());
assert!(scissor_from_clip_rect(&draw, [8.0, 0.0, 8.0, 16.0]).is_none());
draw.display_size = [0.0, 32.0];
assert!(scissor_from_clip_rect(&draw, [0.0, 0.0, 16.0, 16.0]).is_none());
draw.display_size = [32.0, 32.0];
draw.framebuffer_scale = [f32::INFINITY, 1.0];
assert!(scissor_from_clip_rect(&draw, [0.0, 0.0, 16.0, 16.0]).is_none());
draw.framebuffer_scale = [-1.0, 1.0];
assert!(scissor_from_clip_rect(&draw, [0.0, 0.0, 16.0, 16.0]).is_none());
}
#[test]
fn render_pass_scissor_intersects_draws_with_camera_viewport_without_scaling() {
let scissor = intersect_scissor_with_camera_viewport(
ImguiScissorRect {
x: 320,
y: 180,
width: 640,
height: 360,
},
ImguiCameraViewport {
physical_position: [640, 0],
physical_size: [640, 360],
},
)
.expect("valid scissor should map into a valid camera viewport");
assert_eq!(
scissor,
ImguiScissorRect {
x: 640,
y: 180,
width: 320,
height: 180,
}
);
}
#[test]
fn render_pass_scissor_is_clamped_to_real_render_target_size() {
let scissor = scissor_for_render_pass(
&ImguiPreparedDraw {
camera: Entity::from_raw_u32(13).expect("test entity index should be valid"),
order: 0,
target: NormalizedRenderTarget::Window(
bevy_window::WindowRef::Entity(
Entity::from_raw_u32(14).expect("test entity index should be valid"),
)
.normalize(None)
.expect("entity window target should normalize"),
),
viewport_id: Some(imgui::Id::from(0x570)),
texture: TextureBinding::Legacy(imgui::TextureId::new(1)),
sampler: ImguiSampler::Linear,
scissor: ImguiScissorRect {
x: 0,
y: 0,
width: 570,
height: 392,
},
framebuffer_size: [570, 392],
camera_viewport: None,
index_range: 0..3,
vertex_offset: 0,
},
Some([570, 390]),
)
.expect("overlapping scissor should be clipped instead of rejected");
assert_eq!(
scissor,
ImguiScissorRect {
x: 0,
y: 0,
width: 570,
height: 390,
},
"render pass scissors must never exceed the real WGPU target extent"
);
}
#[test]
fn camera_viewport_uniforms_use_logical_viewport_rect_without_scaling_imgui_coordinates() {
let camera = Entity::from_raw_u32(12).expect("test entity index should be valid");
let snapshot = imgui::render::FrameSnapshot {
draw: imgui::render::DrawDataSnapshot {
display_pos: [0.0, 0.0],
display_size: [640.0, 360.0],
framebuffer_scale: [2.0, 2.0],
draw_lists: vec![draw_list_for_test()],
},
viewports: Vec::new(),
texture_requests: Vec::new(),
};
let target = ImguiCameraTarget {
camera,
order: 0,
target: NormalizedRenderTarget::Window(
bevy_window::WindowRef::Entity(camera)
.normalize(None)
.expect("entity window target should normalize"),
),
viewport_id: None,
camera_viewport: Some(ImguiCameraViewport {
physical_position: [640, 0],
physical_size: [640, 720],
}),
explicit: true,
};
let (_, _, draws, uniforms_by_camera) = prepare_snapshot_draw_data(&snapshot, &[target]);
assert_eq!(draws.len(), 1);
assert_eq!(
draws[0].scissor,
ImguiScissorRect {
x: 0,
y: 0,
width: 32,
height: 32,
},
"prepared draw scissors stay in source framebuffer coordinates"
);
assert_eq!(
scissor_for_render_pass(&draws[0], None),
None,
"commands outside the camera viewport are clipped instead of scaled into it"
);
assert_eq!(
uniforms_by_camera.get(&camera).copied(),
Some(ImguiUniforms::from_display_rect(
[320.0, 0.0],
[320.0, 360.0]
))
);
}
#[test]
fn prepared_draws_skip_only_viewports_with_invalid_display_rects() {
let primary_camera = Entity::from_raw_u32(10).expect("test entity index should be valid");
let secondary_camera = Entity::from_raw_u32(11).expect("test entity index should be valid");
let secondary_viewport = imgui::Id::from(0xBEEF);
let snapshot = imgui::render::FrameSnapshot {
draw: imgui::render::DrawDataSnapshot {
display_pos: [0.0, 0.0],
display_size: [f32::NAN, 32.0],
framebuffer_scale: [1.0, 1.0],
draw_lists: vec![draw_list_for_test()],
},
viewports: vec![imgui::render::ViewportDrawDataSnapshot {
viewport_id: secondary_viewport,
draw: imgui::render::DrawDataSnapshot {
display_pos: [0.0, 0.0],
display_size: [32.0, 32.0],
framebuffer_scale: [1.0, 1.0],
draw_lists: vec![draw_list_for_test()],
},
}],
texture_requests: Vec::new(),
};
let targets = [
camera_target_for_test(primary_camera, None),
camera_target_for_test(secondary_camera, Some(secondary_viewport)),
];
let (_, _, draws, uniforms_by_camera) = prepare_snapshot_draw_data(&snapshot, &targets);
assert_eq!(draws.len(), 1);
assert_eq!(draws[0].camera, secondary_camera);
assert!(!uniforms_by_camera.contains_key(&primary_camera));
assert!(uniforms_by_camera.contains_key(&secondary_camera));
}
#[test]
fn bevy_image_binding_tracking_prunes_unregistered_legacy_ids() {
let mut texture_bind_groups = ImguiTextureBindGroups::default();
let registered = TextureBinding::Legacy(imgui::TextureId::new(42));
let still_active = TextureBinding::Legacy(imgui::TextureId::new(43));
texture_bind_groups.bevy_image_bindings.insert(registered);
texture_bind_groups.bevy_image_bindings.insert(still_active);
texture_bind_groups.retain_bevy_image_bindings(&HashSet::from([still_active]));
assert!(
!texture_bind_groups
.bevy_image_bindings
.contains(®istered)
);
assert!(
texture_bind_groups
.bevy_image_bindings
.contains(&still_active)
);
}
#[test]
fn extracted_bevy_image_binding_retention_does_not_require_gpu_images() {
let mut extracted = ImguiExtractedBevyTextures::default();
let mut texture_bind_groups = ImguiTextureBindGroups::default();
let stale = TextureBinding::Legacy(imgui::TextureId::new(42));
let still_active_id = imgui::TextureId::new(43);
let still_active = TextureBinding::Legacy(still_active_id);
texture_bind_groups.bevy_image_bindings.insert(stale);
texture_bind_groups.bevy_image_bindings.insert(still_active);
extracted.replace(vec![(still_active_id, AssetId::<Image>::default())]);
retain_extracted_bevy_image_bindings(&extracted, &mut texture_bind_groups);
assert!(!texture_bind_groups.bevy_image_bindings.contains(&stale));
assert!(
texture_bind_groups
.bevy_image_bindings
.contains(&still_active)
);
}
#[test]
fn bevy_image_sampling_compatibility_accepts_filterable_float_2d_views() {
assert!(
imgui_texture_view_compatibility(TextureFormat::Rgba8Unorm)
.supports_imgui_sampling(WgpuFeatures::empty())
);
assert!(
imgui_texture_view_compatibility(TextureFormat::Rgba8UnormSrgb)
.supports_imgui_sampling(WgpuFeatures::empty())
);
assert!(
imgui_texture_view_compatibility(TextureFormat::Rgba32Float)
.supports_imgui_sampling(WgpuFeatures::FLOAT32_FILTERABLE)
);
}
#[test]
fn bevy_image_sampling_compatibility_rejects_views_that_cannot_match_imgui_layout() {
let unsupported_cases = [
imgui_texture_view_compatibility(TextureFormat::Rgba8Uint),
imgui_texture_view_compatibility(TextureFormat::Rgba8Sint),
imgui_texture_view_compatibility(TextureFormat::Depth32Float),
imgui_texture_view_compatibility(TextureFormat::Rgba32Float),
ImguiTextureViewCompatibility {
texture_usage: TextureUsages::COPY_DST,
..imgui_texture_view_compatibility(TextureFormat::Rgba8Unorm)
},
ImguiTextureViewCompatibility {
view_usage: Some(TextureUsages::COPY_SRC),
..imgui_texture_view_compatibility(TextureFormat::Rgba8Unorm)
},
ImguiTextureViewCompatibility {
sample_count: 4,
..imgui_texture_view_compatibility(TextureFormat::Rgba8Unorm)
},
ImguiTextureViewCompatibility {
texture_dimension: TextureDimension::D3,
..imgui_texture_view_compatibility(TextureFormat::Rgba8Unorm)
},
ImguiTextureViewCompatibility {
depth_or_array_layers: 2,
view_dimension: None,
..imgui_texture_view_compatibility(TextureFormat::Rgba8Unorm)
},
ImguiTextureViewCompatibility {
depth_or_array_layers: 2,
view_dimension: Some(TextureViewDimension::D2Array),
..imgui_texture_view_compatibility(TextureFormat::Rgba8Unorm)
},
];
for compatibility in unsupported_cases {
assert!(
!compatibility.supports_imgui_sampling(WgpuFeatures::empty()),
"{compatibility:?} should not be bound to the fixed ImGui texture layout"
);
}
}
#[test]
fn managed_texture_extent_validation_rejects_zero_or_device_oversized_textures() {
assert!(managed_texture_extent_supported(1, 1, 2048));
assert!(managed_texture_extent_supported(2048, 2048, 2048));
assert!(!managed_texture_extent_supported(0, 1, 2048));
assert!(!managed_texture_extent_supported(1, 0, 2048));
assert!(!managed_texture_extent_supported(2049, 1, 2048));
assert!(!managed_texture_extent_supported(1, 2049, 2048));
}
#[test]
fn texture_update_rect_validation_rejects_empty_or_out_of_bounds_rects() {
assert!(validate_texture_update_rect(
64,
32,
imgui::TextureRect {
x: 8,
y: 4,
w: 16,
h: 8,
},
));
assert!(validate_texture_update_rect(
64,
32,
imgui::TextureRect {
x: 63,
y: 31,
w: 1,
h: 1,
},
));
assert!(!validate_texture_update_rect(
64,
32,
imgui::TextureRect {
x: 8,
y: 4,
w: 0,
h: 8,
},
));
assert!(!validate_texture_update_rect(
64,
32,
imgui::TextureRect {
x: 8,
y: 4,
w: 16,
h: 0,
},
));
assert!(!validate_texture_update_rect(
64,
32,
imgui::TextureRect {
x: 63,
y: 0,
w: 2,
h: 1,
},
));
assert!(!validate_texture_update_rect(
64,
32,
imgui::TextureRect {
x: 0,
y: 31,
w: 1,
h: 2,
},
));
}
#[test]
fn texture_update_rect_conversion_requires_every_requested_rect_to_convert() {
let valid = imgui::render::snapshot::TextureUploadRect {
rect: imgui::TextureRect {
x: 0,
y: 0,
w: 2,
h: 1,
},
row_pitch: 8,
data: vec![1, 2, 3, 4, 5, 6, 7, 8],
};
let converted = convert_imgui_texture_update_rects(
imgui::texture::TextureFormat::RGBA32,
4,
4,
std::slice::from_ref(&valid),
)
.expect("valid update rect should convert");
assert_eq!(converted.len(), 1);
assert_eq!(converted[0].origin, Origin3d { x: 0, y: 0, z: 0 });
assert_eq!(converted[0].width, 2);
assert_eq!(converted[0].height, 1);
assert_eq!(converted[0].row_pitch, 8);
assert_eq!(converted[0].pixels, valid.data);
assert!(
convert_imgui_texture_update_rects(imgui::texture::TextureFormat::RGBA32, 4, 4, &[],)
.is_none(),
"empty update lists must not acknowledge a texture update as complete"
);
let out_of_bounds = imgui::render::snapshot::TextureUploadRect {
rect: imgui::TextureRect {
x: 3,
y: 0,
w: 2,
h: 1,
},
row_pitch: 8,
data: vec![1, 2, 3, 4, 5, 6, 7, 8],
};
assert!(
convert_imgui_texture_update_rects(
imgui::texture::TextureFormat::RGBA32,
4,
4,
&[valid.clone(), out_of_bounds],
)
.is_none(),
"one invalid rect should keep the whole texture update pending"
);
let short_row = imgui::render::snapshot::TextureUploadRect {
rect: imgui::TextureRect {
x: 0,
y: 0,
w: 2,
h: 1,
},
row_pitch: 4,
data: vec![1, 2, 3, 4],
};
assert!(
convert_imgui_texture_update_rects(
imgui::texture::TextureFormat::RGBA32,
4,
4,
&[valid, short_row],
)
.is_none(),
"one unconvertible rect should keep the whole texture update pending"
);
}
#[test]
#[ignore = "requires DEAR_IMGUI_BEVY_GPU_HARNESS=1 and a working native wgpu adapter"]
fn bevy_image_texture_bind_groups_use_real_render_assets_when_gpu_harness_is_enabled() {
if std::env::var_os("DEAR_IMGUI_BEVY_GPU_HARNESS").is_none() {
return;
}
let RenderHarnessResources {
render_device,
pipeline_cache,
} = initialize_render_harness_resources();
let pipeline = ImguiRenderPipeline::default();
let mut extracted = ImguiExtractedBevyTextures::default();
let mut gpu_images = RenderAssets::<GpuImage>::default();
let mut texture_bind_groups = ImguiTextureBindGroups::default();
let texture_id = imgui::TextureId::new(42);
let image_id = AssetId::<Image>::default();
let binding = TextureBinding::Legacy(texture_id);
extracted.replace(vec![(texture_id, image_id)]);
gpu_images.insert(
image_id,
gpu_image(&render_device, TextureUsages::TEXTURE_BINDING),
);
prepare_bevy_image_texture_bind_groups(
Some(&gpu_images),
&extracted,
&render_device,
&pipeline_cache,
&pipeline,
&mut texture_bind_groups,
);
assert_eq!(texture_bind_groups.len(), 1);
assert!(
texture_bind_groups
.get(&binding, ImguiSampler::Linear)
.is_some(),
"registered Bevy image handles should resolve to a real bind group"
);
gpu_images.remove(image_id);
prepare_bevy_image_texture_bind_groups(
Some(&gpu_images),
&extracted,
&render_device,
&pipeline_cache,
&pipeline,
&mut texture_bind_groups,
);
assert!(
texture_bind_groups.is_empty(),
"missing RenderAssets<GpuImage> entries should remove stale bind groups"
);
gpu_images.insert(
image_id,
gpu_image(&render_device, TextureUsages::TEXTURE_BINDING),
);
extracted.replace(vec![(texture_id, image_id)]);
prepare_bevy_image_texture_bind_groups(
Some(&gpu_images),
&extracted,
&render_device,
&pipeline_cache,
&pipeline,
&mut texture_bind_groups,
);
assert_eq!(texture_bind_groups.len(), 1);
extracted.replace(Vec::new());
prepare_bevy_image_texture_bind_groups(
Some(&gpu_images),
&extracted,
&render_device,
&pipeline_cache,
&pipeline,
&mut texture_bind_groups,
);
assert!(
texture_bind_groups.is_empty(),
"unregistered Bevy image handles should remove stale bind groups"
);
}
#[test]
#[ignore = "requires DEAR_IMGUI_BEVY_GPU_HARNESS=1 and a working native wgpu adapter"]
fn bevy_image_texture_bind_groups_ignore_non_sampled_gpu_images_when_gpu_harness_is_enabled() {
if std::env::var_os("DEAR_IMGUI_BEVY_GPU_HARNESS").is_none() {
return;
}
let RenderHarnessResources {
render_device,
pipeline_cache,
} = initialize_render_harness_resources();
let pipeline = ImguiRenderPipeline::default();
let mut extracted = ImguiExtractedBevyTextures::default();
let mut gpu_images = RenderAssets::<GpuImage>::default();
let mut texture_bind_groups = ImguiTextureBindGroups::default();
let texture_id = imgui::TextureId::new(99);
let image_id = AssetId::<Image>::default();
let binding = TextureBinding::Legacy(texture_id);
extracted.replace(vec![(texture_id, image_id)]);
gpu_images.insert(image_id, gpu_image(&render_device, TextureUsages::COPY_DST));
prepare_bevy_image_texture_bind_groups(
Some(&gpu_images),
&extracted,
&render_device,
&pipeline_cache,
&pipeline,
&mut texture_bind_groups,
);
assert_eq!(texture_bind_groups.len(), 0);
assert!(
texture_bind_groups
.get(&binding, ImguiSampler::Linear)
.is_none()
);
}
struct RenderHarnessResources {
render_device: RenderDevice,
pipeline_cache: PipelineCache,
}
fn initialize_render_harness_resources() -> RenderHarnessResources {
let settings = WgpuSettings::default();
let resources = bevy_platform::future::block_on(initialize_renderer(
settings
.backends
.expect("render harness should configure an explicit backend"),
None,
&settings,
));
let render_device = resources.0.clone();
let render_adapter = resources.3.clone();
RenderHarnessResources {
render_device: render_device.clone(),
pipeline_cache: PipelineCache::new(render_device, render_adapter, true),
}
}
fn imgui_texture_view_compatibility(format: TextureFormat) -> ImguiTextureViewCompatibility {
ImguiTextureViewCompatibility {
texture_usage: TextureUsages::TEXTURE_BINDING,
view_usage: None,
sample_count: 1,
texture_dimension: TextureDimension::D2,
depth_or_array_layers: 1,
view_dimension: None,
format,
aspect: TextureAspect::All,
}
}
fn gpu_image(render_device: &RenderDevice, usage: TextureUsages) -> GpuImage {
let texture_descriptor = TextureDescriptor {
label: Some("dear_imgui_bevy_harness_image"),
size: Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage,
view_formats: &[],
};
let texture = render_device.create_texture(&texture_descriptor);
let texture_view = texture.create_view(&TextureViewDescriptor::default());
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
GpuImage {
texture,
texture_view,
sampler,
texture_descriptor,
texture_view_descriptor: None,
had_data: true,
}
}
fn draw_list_for_test() -> imgui::render::DrawListSnapshot {
imgui::render::DrawListSnapshot {
vtx: vec![
imgui::render::DrawVert::new([0.0, 0.0], [0.0, 0.0], 0xFFFF_FFFF),
imgui::render::DrawVert::new([1.0, 0.0], [1.0, 0.0], 0xFFFF_FFFF),
imgui::render::DrawVert::new([0.0, 1.0], [0.0, 1.0], 0xFFFF_FFFF),
],
idx: vec![0, 1, 2],
commands: vec![DrawCmdSnapshot::Elements {
count: 3,
clip_rect: [0.0, 0.0, 16.0, 16.0],
texture: TextureBinding::Legacy(imgui::TextureId::new(1)),
vtx_offset: 0,
idx_offset: 0,
}],
}
}
fn camera_target_for_test(camera: Entity, viewport_id: Option<imgui::Id>) -> ImguiCameraTarget {
ImguiCameraTarget {
camera,
order: 0,
target: NormalizedRenderTarget::Window(
bevy_window::WindowRef::Entity(camera)
.normalize(None)
.expect("entity window target should normalize"),
),
viewport_id,
camera_viewport: None,
explicit: false,
}
}
}