use glamx::Vec4;
use khal::Shader;
use khal::backend::{
Backend, GpuBackend as KhalGpuBackend, GpuBackendError, GpuTimestamps, WebGpu,
};
use khal::re_exports::wgpu::{Features, Limits};
use std::time::Duration;
use kiss3d::prelude::Color;
use kiss3d::scene::{SceneNode2d, SceneNode3d};
use kiss3d::window::{NumSamples, Window};
#[cfg(feature = "dim3")]
use kiss3d::camera::{FixedView2d, OrbitCamera3d};
#[cfg(feature = "dim2")]
use kiss3d::camera::{FixedView3d, PanZoomCamera2d};
use nexus::rbd::dynamics::WgRbdPrepRender;
use nexus::rbd::math::Pose;
use nexus::rbd::pipeline::RunStats;
use nexus::state::{NexusCounts, NexusState};
use rapier::prelude::{RigidBodyHandle, SharedShape};
use crate::backend::BackendType;
use crate::graphics::RenderContext;
use crate::{DemoKind, RunState, Transition, UiSections};
pub struct UiState {
pub run_state: RunState,
pub run_stats: RunStats,
pub sync_time: Duration,
pub ui_sections: UiSections,
pub backend_type: BackendType,
pub gpu_init_error: Option<String>,
pub demos: Vec<(String, DemoKind)>,
pub selected_demo: usize,
pub(crate) transition: Option<Transition>,
pub sim_settings: SimSettings,
pub(crate) settings_demo: Option<usize>,
pub(crate) has_rbd: bool,
pub(crate) counts: NexusCounts,
}
#[derive(Clone)]
pub struct SimSettings {
pub rbd_steps_per_frame: u32,
}
impl Default for SimSettings {
fn default() -> Self {
Self {
rbd_steps_per_frame: 1,
}
}
}
#[cfg(feature = "dim2")]
pub type SceneNode = SceneNode2d;
#[cfg(feature = "dim3")]
pub type SceneNode = SceneNode3d;
const COMPILE_BANNER_PRESENT_FRAMES: u32 = 10;
pub struct NexusViewer {
window: Window,
scene2d: SceneNode2d,
scene3d: SceneNode3d,
#[cfg(feature = "dim3")]
camera3d: OrbitCamera3d,
#[cfg(feature = "dim3")]
up_axis: glamx::Vec3,
#[cfg(feature = "dim3")]
camera2d: FixedView2d,
#[cfg(feature = "dim2")]
camera3d: FixedView3d,
#[cfg(feature = "dim2")]
camera2d: PanZoomCamera2d,
webgpu: Option<KhalGpuBackend>,
webgpu_shared: bool,
#[cfg(feature = "cuda")]
cuda: Option<KhalGpuBackend>,
#[cfg(feature = "metal")]
metal: Option<KhalGpuBackend>,
cpu: Option<KhalGpuBackend>,
nexus_render: RenderContext,
rbd_prep_render: Option<WgRbdPrepRender>,
render_resources_backend: Option<BackendType>,
last_gpu_pass_times: Vec<(String, f64)>,
last_gpu_total_time_ms: f64,
pub ui: UiState,
}
impl NexusViewer {
pub async fn new(demos: Vec<(String, DemoKind)>) -> Self {
let setup = kiss3d::window::CanvasSetup {
required_features: Features::PASSTHROUGH_SHADERS,
..Default::default()
};
let mut window = Window::new_with_setup("nexus demos", 1200, 900, setup).await;
window.set_background_color(Color::new(245.0 / 255.0, 245.0 / 255.0, 236.0 / 255.0, 1.0));
window.set_samples(NumSamples::One);
#[cfg(feature = "dim2")]
let (camera2d, camera3d) = {
let mut sidescroll = PanZoomCamera2d::default();
sidescroll.look_at(glamx::Vec2::new(0.0, 100.0), 7.5);
(sidescroll, FixedView3d::default())
};
#[cfg(feature = "dim3")]
let (camera2d, camera3d) = {
let arc_ball = OrbitCamera3d::new(
glamx::Vec3::new(-100.0, 100.0, -100.0),
glamx::Vec3::new(0.0, 40.0, 0.0),
);
(FixedView2d::default(), arc_ball)
};
let scene3d = SceneNode3d::empty();
let scene2d = SceneNode2d::empty();
let mut viewer = Self {
window,
scene2d,
scene3d,
camera3d,
#[cfg(feature = "dim3")]
up_axis: glamx::Vec3::Y,
camera2d,
webgpu: None,
webgpu_shared: false,
#[cfg(feature = "cuda")]
cuda: None,
#[cfg(feature = "metal")]
metal: None,
cpu: {
#[cfg(feature = "cpu")]
{
Some(KhalGpuBackend::Cpu)
}
#[cfg(not(feature = "cpu"))]
{
None
}
},
nexus_render: RenderContext::new(),
rbd_prep_render: None,
render_resources_backend: None,
last_gpu_pass_times: Vec::new(),
last_gpu_total_time_ms: 0.0,
ui: UiState {
run_state: RunState::Paused,
run_stats: RunStats::default(),
sync_time: Duration::default(),
ui_sections: UiSections {
show_examples: true,
show_settings: false,
show_performance: true,
},
backend_type: BackendType::Gpu,
gpu_init_error: None,
demos,
selected_demo: 0,
transition: None,
sim_settings: SimSettings::default(),
settings_demo: None,
has_rbd: false,
counts: NexusCounts::default(),
},
};
viewer.webgpu = viewer.init_webgpu().await;
viewer
}
pub fn with_backend(mut self, backend_type: BackendType) -> Self {
self.ui.backend_type = backend_type;
self
}
pub fn with_cpu(mut self) -> Self {
self.ui.backend_type = BackendType::Cpu;
self
}
pub fn with_running(mut self) -> Self {
self.ui.run_state = RunState::Running;
self
}
pub fn with_selected_demo(mut self, idx: usize) -> Self {
self.ui.selected_demo = idx;
self
}
pub fn selected_demo(&self) -> usize {
self.ui.selected_demo
}
pub fn quitting(&self) -> bool {
matches!(self.ui.transition, Some(Transition::Quit))
}
pub fn clear_transition(&mut self) {
self.ui.transition = None;
}
async fn init_webgpu(&mut self) -> Option<KhalGpuBackend> {
if kiss3d::context::Context::is_initialized() {
let ctxt = kiss3d::context::Context::get();
let wgpu = WebGpu::from_device(
(*ctxt.instance).clone(),
(*ctxt.adapter).clone(),
(*ctxt.device).clone(),
(*ctxt.queue).clone(),
);
self.webgpu_shared = true;
return Some(KhalGpuBackend::WebGpu(wgpu));
}
self.webgpu_shared = false;
let limits = Limits {
max_buffer_size: 1 << 30, max_storage_buffer_binding_size: 1 << 30,
max_compute_workgroup_storage_size: 19904,
..Default::default()
};
match WebGpu::new(Default::default(), limits).await.map(|wgpu| {
KhalGpuBackend::WebGpu(wgpu)
}) {
Ok(gpu) => Some(gpu),
Err(e) => {
self.ui.gpu_init_error = Some(format!(
"GPU backend not available, initialization failed:\n\"{}\"\n",
e
));
None
}
}
}
fn direct_render_path(&self) -> bool {
self.webgpu_shared
&& self.ui.backend_type == BackendType::Gpu
&& matches!(self.webgpu, Some(KhalGpuBackend::WebGpu(_)))
}
#[cfg(feature = "cuda")]
fn init_cuda(&mut self) -> Option<KhalGpuBackend> {
match khal::backend::cuda::Cuda::new(0) {
Ok(cuda) => Some(KhalGpuBackend::Cuda(cuda)),
Err(e) => {
self.ui.gpu_init_error = Some(format!(
"CUDA backend not available, initialization failed:\n\"{:?}\"\n",
e
));
None
}
}
}
#[cfg(feature = "metal")]
fn init_metal(&mut self) -> Option<KhalGpuBackend> {
match khal::backend::metal::Metal::new() {
Ok(metal) => Some(KhalGpuBackend::Metal(metal)),
Err(e) => {
self.ui.gpu_init_error = Some(format!(
"Metal backend not available, initialization failed:\n\"{:?}\"\n",
e
));
None
}
}
}
fn ensure_backend_initialized(&mut self) {
match self.ui.backend_type {
#[cfg(feature = "cuda")]
BackendType::Cuda if self.cuda.is_none() => {
self.cuda = self.init_cuda();
}
#[cfg(feature = "metal")]
BackendType::Metal if self.metal.is_none() => {
self.metal = self.init_metal();
}
_ => {}
}
}
pub fn gpu(&self) -> Option<&KhalGpuBackend> {
match self.ui.backend_type {
BackendType::Gpu => self.webgpu.as_ref(),
#[cfg(feature = "cuda")]
BackendType::Cuda => self.cuda.as_ref(),
#[cfg(feature = "metal")]
BackendType::Metal => self.metal.as_ref(),
_ => None,
}
}
pub fn scene2d_mut(&mut self) -> &mut SceneNode2d {
&mut self.scene2d
}
pub fn scene3d_mut(&mut self) -> &mut SceneNode3d {
&mut self.scene3d
}
pub fn backend(&self) -> &KhalGpuBackend {
match self.ui.backend_type {
BackendType::Gpu => self.webgpu.as_ref().unwrap(),
#[cfg(feature = "cuda")]
BackendType::Cuda => self.cuda.as_ref().unwrap(),
#[cfg(feature = "metal")]
BackendType::Metal => self.metal.as_ref().unwrap(),
BackendType::Cpu => self
.cpu
.as_ref()
.expect("CPU backend unavailable: compile with the 'cpu' feature"),
#[allow(unreachable_patterns)]
_ => panic!("selected backend is not available in this build"),
}
}
pub fn gpu_available(&self) -> bool {
self.webgpu.is_some()
}
#[cfg(feature = "dim3")]
pub fn set_camera(&mut self, eye: glamx::Vec3, target: glamx::Vec3) {
let mut camera = OrbitCamera3d::new(eye, target);
camera.set_up_axis(self.up_axis);
self.camera3d = camera;
}
#[cfg(feature = "dim3")]
pub fn set_up_axis(&mut self, up: glamx::Vec3) {
self.up_axis = up;
self.camera3d.set_up_axis(up);
}
#[cfg(feature = "dim2")]
pub fn set_camera_2d(&mut self, center: glamx::Vec2, zoom: f32) {
self.camera2d.look_at(center, zoom);
}
pub fn init_backend(&mut self) {
self.ensure_backend_initialized();
}
pub fn insert_shape(&mut self, handle: RigidBodyHandle, shape: &SharedShape, local_pose: Pose) {
self.insert_shape_in(0, handle, shape, local_pose, None)
}
pub fn insert_shape_with_color(
&mut self,
handle: RigidBodyHandle,
shape: &SharedShape,
local_pose: Pose,
color: Vec4,
) {
self.insert_shape_in(0, handle, shape, local_pose, Some(color))
}
pub fn insert_shape_in(
&mut self,
env: u32,
handle: RigidBodyHandle,
shape: &SharedShape,
local_pose: Pose,
color: Option<Vec4>,
) {
self.nexus_render.insert_shape(
#[cfg(feature = "dim2")]
&mut self.scene2d,
#[cfg(feature = "dim3")]
&mut self.scene3d,
env,
handle,
shape,
local_pose,
color,
)
}
pub fn insert_visual_shape(
&mut self,
env: u32,
handle: RigidBodyHandle,
shape: &SharedShape,
local_pose: Pose,
) {
self.nexus_render.insert_shape(
#[cfg(feature = "dim2")]
&mut self.scene2d,
#[cfg(feature = "dim3")]
&mut self.scene3d,
env,
handle,
shape,
local_pose,
None,
)
}
#[cfg(feature = "dim3")]
pub fn insert_visual_mesh(
&mut self,
env: u32,
handle: RigidBodyHandle,
shape: &SharedShape,
local_pose: Pose,
color: [f32; 4],
uvs: Option<&[[f32; 2]]>,
normals: Option<&[[f32; 3]]>,
texture: Option<&std::path::Path>,
material: Option<crate::graphics::RenderMaterial>,
) {
self.nexus_render.insert_visual_mesh(
&mut self.scene3d,
env,
handle,
shape,
local_pose,
color,
uvs,
normals,
texture,
material,
);
}
async fn sync_timestamps(&mut self, timestamps: Option<&mut GpuTimestamps>) {
if let Some(timestamps) = timestamps
&& let Some(results) = timestamps.try_take(self.backend())
{
if !results.is_empty() {
let mut aggregated: Vec<(String, f64)> = Vec::new();
for r in &results {
if let Some(existing) =
aggregated.iter_mut().find(|(label, _)| label == &r.label)
{
existing.1 += r.duration_ms;
} else {
aggregated.push((r.label.clone(), r.duration_ms));
}
}
self.last_gpu_total_time_ms = aggregated.iter().map(|e| e.1).sum();
self.last_gpu_pass_times = aggregated;
}
timestamps.reset();
}
}
async fn sync_with_readback(&mut self, state: &mut NexusState) -> Result<(), GpuBackendError> {
if let Some(rbd) = state.rbd.as_ref() {
let poses = rbd.body_poses();
let mut cache = vec![Pose::default(); poses.len() as usize];
let _ = self
.backend()
.slow_read_buffer(poses.buffer(), &mut cache)
.await;
self.nexus_render.update_instances_from_poses(state, &cache);
#[cfg(feature = "dim3")]
if self.nexus_render.has_visual_nodes() {
let body_poses = rbd.body_poses();
let mut body_cache = vec![Pose::default(); body_poses.len() as usize];
let _ = self
.backend()
.slow_read_buffer(body_poses.buffer(), &mut body_cache)
.await;
self.nexus_render.update_visual_nodes(state, &body_cache);
}
}
Ok(())
}
async fn sync_without_readback(
&mut self,
state: &mut NexusState,
) -> Result<(), GpuBackendError> {
if let Some(rbd) = state.rbd.as_ref() {
let backend = self.backend().clone();
if self.rbd_prep_render.is_none() {
self.rbd_prep_render = WgRbdPrepRender::from_backend(&backend).ok();
}
if let Some(shader) = self.rbd_prep_render.as_ref() {
let body_poses = rbd.body_poses();
let mut enc = backend.begin_encoding();
let _ = self
.nexus_render
.update_instances_direct(&backend, state, body_poses, shader, &mut enc);
#[cfg(feature = "dim3")]
if self.nexus_render.has_visual_nodes() {
let _ = self
.nexus_render
.update_visual_nodes_direct(&backend, state, body_poses, shader, &mut enc);
}
let _ = backend.submit(enc);
}
}
Ok(())
}
pub async fn sync(
&mut self,
state: &mut NexusState,
timestamps: Option<&mut GpuTimestamps>,
) -> Result<(), GpuBackendError> {
if self.render_resources_backend != Some(self.ui.backend_type) {
self.invalidate_render_resources();
self.render_resources_backend = Some(self.ui.backend_type);
}
if !self.direct_render_path() {
self.backend().synchronize()?;
}
let t0 = web_time::Instant::now();
if self.ui.settings_demo != Some(self.ui.selected_demo) {
self.ui.has_rbd = state.rbd.is_some();
self.ui.sim_settings.rbd_steps_per_frame = state.rbd_steps_per_frame();
self.ui.settings_demo = Some(self.ui.selected_demo);
} else {
let s = self.ui.sim_settings.clone();
state.set_rbd_steps_per_frame(s.rbd_steps_per_frame);
}
if self.direct_render_path() {
self.sync_without_readback(state).await?;
} else {
self.sync_with_readback(state).await?;
}
self.sync_timestamps(timestamps).await;
if !self.last_gpu_pass_times.is_empty() {
state.run_stats.gpu_pass_times = self.last_gpu_pass_times.clone();
state.run_stats.gpu_total_time_ms = self.last_gpu_total_time_ms;
}
self.ui.run_stats = state.run_stats.clone();
self.ui.sync_time = t0.elapsed();
self.ui.counts = state.counts();
Ok(())
}
pub fn backend_type(&self) -> BackendType {
self.ui.backend_type
}
pub async fn show_compile_banner(&mut self) {
for _ in 0..COMPILE_BANNER_PRESENT_FRAMES {
let _ = self
.window
.render(
Some(&mut self.scene3d),
Some(&mut self.scene2d),
Some(&mut self.camera3d),
Some(&mut self.camera2d),
None,
None,
)
.await;
let gpu_available = self.webgpu.is_some();
self.window.draw_ui(|ctx| {
crate::ui::setup_custom_theme(ctx);
crate::ui::main_panel(ctx, &mut self.ui, gpu_available);
crate::ui::compiling_overlay(ctx);
});
}
}
fn invalidate_render_resources(&mut self) {
self.rbd_prep_render = None;
}
pub fn clear_scene(&mut self) {
self.scene3d = SceneNode3d::empty();
self.scene2d = SceneNode2d::empty();
self.nexus_render.clear();
}
pub fn simulating(&mut self) -> bool {
match self.ui.run_state {
RunState::Paused => false,
RunState::Running => true,
RunState::Step => {
self.ui.run_state = RunState::Paused;
true
}
}
}
pub async fn render_frame(&mut self) -> bool {
let cont = self
.window
.render(
Some(&mut self.scene3d),
Some(&mut self.scene2d),
Some(&mut self.camera3d),
Some(&mut self.camera2d),
None,
None,
)
.await;
if !cont {
self.ui.transition = Some(Transition::Quit);
return false;
}
let gpu_available = self.webgpu.is_some();
self.window.draw_ui(|ctx| {
crate::ui::setup_custom_theme(ctx);
crate::ui::main_panel(ctx, &mut self.ui, gpu_available);
});
self.ui.transition.is_none()
}
pub fn draw_custom_ui(&mut self, ui_fn: impl FnOnce(&kiss3d::egui::Context)) {
self.window.draw_ui(ui_fn);
}
}