use dear_imgui_rs::render::ReconciledFrame;
use dear_imgui_rs::{DockNodeFlags, FrameToken, WindowFlags};
#[cfg(feature = "test-engine")]
use dear_imgui_test_engine::{FrameDriveOutcome, MainRenderOutcome, TestFrameDriver};
use super::admission::{
SurfaceAcquisition, SurfaceAdmissionBackend, SurfaceDispatch, admit_surface_frame,
dispatch_surface_frame, settle_surface_presentation,
};
use super::lifecycle::{LifecycleAction, SurfaceEvent};
use super::ownership::RuntimeOwnership;
use super::recovery::RuntimeGenerations;
use super::state::{RuntimeGeneration, UiState, WindowState};
use crate::{
AddOns, AppConfig, Application, ApplicationStage, DockingApi, FrameContext,
PrepareFrameContext, RunError,
};
struct RuntimeSurfaceAdmission<'a> {
window: &'a mut WindowState,
generations: &'a mut RuntimeGenerations<RuntimeGeneration>,
config: &'a AppConfig,
}
impl SurfaceAdmissionBackend for RuntimeSurfaceAdmission<'_> {
type Frame = wgpu::SurfaceTexture;
fn acquire(&mut self) -> SurfaceAcquisition<Self::Frame> {
match self.window.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(frame) => SurfaceAcquisition::Success(frame),
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => SurfaceAcquisition::Suboptimal(frame),
wgpu::CurrentSurfaceTexture::Lost => SurfaceAcquisition::Lost,
wgpu::CurrentSurfaceTexture::Outdated => SurfaceAcquisition::Outdated,
wgpu::CurrentSurfaceTexture::Timeout => SurfaceAcquisition::Timeout,
wgpu::CurrentSurfaceTexture::Occluded => SurfaceAcquisition::Occluded,
wgpu::CurrentSurfaceTexture::Validation => SurfaceAcquisition::Validation,
}
}
fn record_event(&mut self, event: SurfaceEvent) -> LifecycleAction {
self.generations.surface_event(event)
}
fn recover(&mut self, action: LifecycleAction) -> Result<(), RunError> {
let generation = self
.generations
.current()
.ok_or_else(|| RunError::Recovery {
message: "surface recovery requested without an active GPU generation".to_owned(),
})?;
match action {
LifecycleAction::RecreateSurface => self.window.recreate_surface(
&generation.gpu.adapter,
&generation.gpu.device,
self.config,
),
LifecycleAction::ReconfigureSurface => {
self.window.reconfigure(&generation.gpu.device);
Ok(())
}
_ => Err(RunError::Recovery {
message: format!("surface admission requested invalid recovery action {action:?}"),
}),
}
}
}
struct AdmittedWgpuFrameDriver<'a> {
renderer: &'a mut dear_imgui_wgpu::WgpuRenderer,
device: &'a wgpu::Device,
queue: &'a wgpu::Queue,
surface_frame: Option<wgpu::SurfaceTexture>,
clear_color: wgpu::Color,
rendered: bool,
presented: bool,
}
impl<'a> AdmittedWgpuFrameDriver<'a> {
fn new(
renderer: &'a mut dear_imgui_wgpu::WgpuRenderer,
device: &'a wgpu::Device,
queue: &'a wgpu::Queue,
surface_frame: wgpu::SurfaceTexture,
clear_color: wgpu::Color,
) -> Self {
Self {
renderer,
device,
queue,
surface_frame: Some(surface_frame),
clear_color,
rendered: false,
presented: false,
}
}
fn prepare_frame<'frame>(
&mut self,
frame: FrameToken<'frame>,
) -> Result<ReconciledFrame<'frame>, RunError> {
let pending = frame.render(
self.renderer
.renderer_consumer()
.map_err(RunError::Render)?,
);
self.renderer
.reconcile_frame(pending)
.map_err(RunError::Render)
}
fn render_main_frame(&mut self, frame: ReconciledFrame<'_>) -> Result<(), RunError> {
if self.rendered {
return Err(RunError::Recovery {
message: "admitted surface frame was rendered more than once".to_owned(),
});
}
let surface_frame = self
.surface_frame
.as_ref()
.ok_or_else(|| RunError::Recovery {
message: "admitted surface frame was consumed before rendering".to_owned(),
})?;
let framebuffer_extent =
dear_imgui_wgpu::FramebufferExtent::from_texture(&surface_frame.texture);
let view = surface_frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Dear App render encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Dear App render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(self.clear_color),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
self.renderer
.render_reconciled(frame, &mut render_pass, framebuffer_extent)
.map_err(RunError::Render)?;
}
self.queue.submit(Some(encoder.finish()));
self.rendered = true;
Ok(())
}
fn present_frame(&mut self) -> Result<(), RunError> {
if !self.rendered {
return Err(RunError::Recovery {
message: "admitted surface frame was presented before rendering".to_owned(),
});
}
let surface_frame = self
.surface_frame
.take()
.ok_or_else(|| RunError::Recovery {
message: "admitted surface frame was presented more than once".to_owned(),
})?;
self.queue.present(surface_frame);
self.presented = true;
Ok(())
}
const fn was_presented(&self) -> bool {
self.presented
}
}
#[cfg(feature = "test-engine")]
impl TestFrameDriver for AdmittedWgpuFrameDriver<'_> {
type PreparedFrame<'frame> = ReconciledFrame<'frame>;
type PrepareError = RunError;
type RenderError = RunError;
type PresentError = RunError;
fn prepare<'frame>(
&mut self,
frame: FrameToken<'frame>,
_frame_index: u64,
) -> Result<Self::PreparedFrame<'frame>, Self::PrepareError> {
self.prepare_frame(frame)
}
fn prepared_context_id(frame: &Self::PreparedFrame<'_>) -> dear_imgui_rs::ContextId {
frame.context_id()
}
fn render_main(
&mut self,
frame: Self::PreparedFrame<'_>,
_frame_index: u64,
) -> Result<MainRenderOutcome, Self::RenderError> {
self.render_main_frame(frame)?;
Ok(MainRenderOutcome::ReadyToPresent)
}
fn present(&mut self, _frame_index: u64) -> Result<(), Self::PresentError> {
self.present_frame()
}
}
fn drive_admitted_frame<A: Application>(
application: &mut A,
frame: FrameToken<'_>,
frame_index: u64,
driver: &mut AdmittedWgpuFrameDriver<'_>,
) -> Result<(), RunError> {
#[cfg(feature = "test-engine")]
if let Some(engine) = application.test_engine() {
return match engine
.drive_frame(frame, frame_index, driver)
.map_err(|source| RunError::TestEngineFrame {
frame: frame_index,
source: Box::new(source),
})? {
FrameDriveOutcome::Presented => Ok(()),
FrameDriveOutcome::Skipped => Err(RunError::Recovery {
message: "an admitted surface frame was unexpectedly skipped".to_owned(),
}),
_ => Err(RunError::Recovery {
message: "Test Engine returned an unsupported frame-drive outcome".to_owned(),
}),
};
}
#[cfg(not(feature = "test-engine"))]
let _ = (application, frame_index);
let reconciled = driver.prepare_frame(frame)?;
driver.render_main_frame(reconciled)?;
driver.present_frame()
}
pub(super) fn render_surface_frame<A: Application>(
ownership: &mut RuntimeOwnership,
clear_color: wgpu::Color,
admitted_frame_count: &mut u64,
application: &mut A,
config: &AppConfig,
) -> Result<SurfaceDispatch<bool>, RunError> {
let admitted = {
let RuntimeOwnership {
window,
generations,
..
} = ownership;
let mut backend = RuntimeSurfaceAdmission {
window,
generations,
config,
};
admit_surface_frame(&mut backend)?
};
let dispatch =
dispatch_surface_frame(admitted, admitted_frame_count, |admitted, frame_index| {
let RuntimeOwnership {
window,
ui,
generations,
} = ownership;
let generation = generations
.current_mut()
.ok_or_else(|| RunError::Recovery {
message: "render requested without an active GPU generation".to_owned(),
})?;
let UiState {
context,
platform,
#[cfg(feature = "implot")]
implot,
#[cfg(feature = "imnodes")]
imnodes,
#[cfg(feature = "implot3d")]
implot3d,
docking,
} = ui;
super::state::run_application_frame_boundary(
context,
ApplicationStage::PrepareFrame,
|context| {
let mut prepare_frame = PrepareFrameContext {
imgui: context,
window: &window.window,
};
application.prepare_frame(&mut prepare_frame)
},
)?;
super::state::validate_supported_imgui_config(context)?;
platform
.prepare_frame(context, &window.window)
.map_err(|error| super::state::platform_error("Winit frame preparation", error))?;
let mut exit_requested = false;
let frame = build_frame(context, |ui| {
draw_dockspace(ui, docking.flags, config)?;
let addons = AddOns {
#[cfg(feature = "implot")]
implot: implot.as_ref(),
#[cfg(feature = "imnodes")]
imnodes: imnodes.as_ref(),
#[cfg(feature = "implot3d")]
implot3d: implot3d.as_ref(),
docking: DockingApi {
controller: docking,
},
};
let mut frame = FrameContext {
ui,
addons,
gpu: generation.api(),
exit_requested: &mut exit_requested,
};
application
.frame(&mut frame)
.map_err(|error| error.during_application_stage(ApplicationStage::Frame))?;
platform
.prepare_render(ui, &window.window)
.map_err(|error| {
super::state::platform_error("Winit render preparation", error)
})?;
Ok(())
})?;
let generation = generations
.current_mut()
.ok_or_else(|| RunError::Recovery {
message: "render submission requested without an active GPU generation"
.to_owned(),
})?;
let reconfigure_after_present = admitted.reconfigure_after_present;
let gpu = &mut generation.gpu;
let mut driver = AdmittedWgpuFrameDriver::new(
&mut gpu.renderer,
&gpu.device,
&gpu.queue,
admitted.frame,
clear_color,
);
let result = drive_admitted_frame(application, frame, frame_index, &mut driver);
let was_presented = driver.was_presented();
drop(driver);
settle_surface_presentation(result, was_presented, reconfigure_after_present, || {
window.reconfigure(&generation.gpu.device)
})?;
Ok(exit_requested)
})?;
Ok(dispatch)
}
pub(super) fn build_frame<'ctx>(
context: &'ctx mut dear_imgui_rs::Context,
build: impl FnOnce(&dear_imgui_rs::Ui) -> Result<(), RunError>,
) -> Result<FrameToken<'ctx>, RunError> {
let frame = context.try_begin_frame().map_err(RunError::ImGuiFrame)?;
build(frame.ui())?;
Ok(frame)
}
fn draw_dockspace(
ui: &dear_imgui_rs::Ui,
flags: DockNodeFlags,
config: &AppConfig,
) -> Result<(), RunError> {
let Some((host_window_name, mut window_flags)) = config.docking.full_viewport_host() else {
return Ok(());
};
let viewport = ui.main_viewport();
ui.set_next_window_viewport(viewport.id());
if flags.contains(DockNodeFlags::PASSTHRU_CENTRAL_NODE) {
window_flags |= WindowFlags::NO_BACKGROUND;
}
let mut dockspace_result = Ok(());
ui.window(host_window_name)
.flags(window_flags)
.position(viewport.pos(), dear_imgui_rs::Condition::Always)
.size(viewport.size(), dear_imgui_rs::Condition::Always)
.build(|| {
dockspace_result = ui
.dockspace()
.flags(flags)
.build()
.map(|_| ())
.map_err(RunError::Dockspace);
});
dockspace_result
}