//! The one cyb App โ every platform builds it here.
//!
//! Desktop (`main.rs`) and Android (`lib.rs`'s `android_main`) call
//! [`build_app`] and get the same Bevy app: same worlds, same chrome, same
//! nav. Only the surfaces that name a platform differ โ global hotkeys, the
//! tray, and the hide-on-close window contract exist where a desktop does.
use bevy::prelude::*;
use bevy::render::RenderApp;
use bevy::render::renderer::{RenderDevice, RenderQueue};
use crate::{agent, shell, worlds};
/// Exposes the render device/queue on the main world so worlds can allocate
/// GPU resources without reaching into the render sub-app.
struct GpuBridgePlugin;
impl Plugin for GpuBridgePlugin {
fn build(&self, _app: &mut App) {}
fn finish(&self, app: &mut App) {
let (device, queue) = {
let render_app = app.sub_app(RenderApp);
let device = render_app.world().resource::<RenderDevice>().clone();
let queue = render_app.world().resource::<RenderQueue>().clone();
(device, queue)
};
// Off Apple, mir's device facade shares Bevy's wgpu device instead of
// opening a second one: two VkDevices in one process hang the PowerVR
// driver on the Pixel 10 (intermittently, mid-dispatch) where MoltenVK
// tolerated it. One device, one queue, no cross-device races.
#[cfg(not(target_vendor = "apple"))]
mir::gpu::install_shared(device.wgpu_device().clone(), (***queue).clone());
app.insert_resource(device);
app.insert_resource(queue);
// Android: switch bevy's GPU mesh preprocessing off. cyb's scene is
// UI + mir's own compute โ there are no 3D meshes to preprocess โ and
// the occlusion-culling variant of mesh_preprocess.wgsl samples a
// depth pyramid from a compute shader, which the PowerVR driver on
// the Pixel 10 (Tensor G5) aborts on: `spvcompiler: Unhandled
// sampler flag combo` โ SIGABRT at pipeline compile. This plugin's
// finish runs after bevy_render's, so the override wins.
#[cfg(target_os = "android")]
{
use bevy::render::batching::gpu_preprocessing::{
GpuPreprocessingMode, GpuPreprocessingSupport,
};
app.sub_app_mut(RenderApp).insert_resource(GpuPreprocessingSupport {
max_supported_mode: GpuPreprocessingMode::None,
});
}
}
}
pub fn build_app() -> App {
let mut app = App::new();
// Vulkan platforms: ask the device for VK_EXT_external_memory_host when
// the adapter has it. mir's unimem wrap imports pinned blocks through it
// โ the GPU reads the block's own pages, nothing is copied. The callback
// runs at device creation; mir checks enabled_device_extensions() at
// wrap time, so a driver without the extension just means the copy path.
#[cfg(any(target_os = "android", target_os = "linux"))]
{
use bevy::render::renderer::raw_vulkan_init::RawVulkanInitSettings;
let mut raw = RawVulkanInitSettings::default();
// SAFETY: adds an extension only when the physical device lists it;
// removes nothing, changes nothing else.
unsafe {
raw.add_create_device_callback(|args, adapter, _features| {
// Every handle type mir can import from, in the order it
// tries them. AHardwareBuffer is Android's IOSurface and
// needs queue_family_foreign alongside; host-pointer import
// is the desktop-Vulkan route. wgpu-hal already enables the
// fd/dma-buf pair, so those are not listed here.
const WANTED: &[&std::ffi::CStr] = &[
ash::android::external_memory_android_hardware_buffer::NAME,
ash::ext::queue_family_foreign::NAME,
ash::ext::external_memory_host::NAME,
];
let Ok(props) = adapter
.shared_instance()
.raw_instance()
.enumerate_device_extension_properties(adapter.raw_physical_device())
else {
return;
};
for ext in WANTED {
let supported = props.iter().any(|p| {
p.extension_name_as_c_str().is_ok_and(|n| n == *ext)
});
if supported && !args.extensions.contains(ext) {
args.extensions.push(ext);
}
}
});
}
app.insert_resource(raw);
}
#[allow(unused_mut)]
let mut window_plugin = WindowPlugin {
primary_window: Some(Window {
title: "cyb".into(),
resolution: (1280u32, 800u32).into(),
// Uncapped: mailbox where the platform has it, immediate else.
// The graph is the frame cost and it pays per pixel, not per hz.
present_mode: bevy::window::PresentMode::AutoNoVsync,
..default()
}),
..default()
};
// The robot outlives its window: closing hides, the tray brings it back,
// only tray โ Quit ends the process. See `shell/window.rs`. On Android
// the OS owns the app lifecycle, so the defaults stand.
#[cfg(not(target_os = "android"))]
{
window_plugin.close_when_requested = false;
window_plugin.exit_condition = bevy::window::ExitCondition::DontExit;
}
app.add_plugins(
DefaultPlugins.set(window_plugin).set(bevy::render::RenderPlugin {
render_creation: bevy::render::settings::RenderCreation::Automatic(
bevy::render::settings::WgpuSettings { ..default() },
),
..default()
}),
)
.insert_resource(ClearColor(bevy::color::Color::BLACK))
.add_plugins(GpuBridgePlugin)
.add_plugins(prysm::PrysmPlugin)
.add_plugins(worlds::WorldsPlugin)
.add_plugins(shell::chrome::ChromePlugin)
.add_plugins(shell::platform::PlatformPlugin)
.add_plugins(shell::nav::NavPlugin)
.add_plugins(mir::bevy::GraphWorldPlugin)
.add_plugins(worlds::graph::GraphBridgePlugin)
.add_plugins(worlds::viewer::ViewerPlugin)
.add_plugins(worlds::com::ComWorldPlugin)
.add_plugins(worlds::soma_bridge::SomaBridgePlugin)
.add_plugins(worlds::attention::AttentionPlugin)
.add_plugins(worlds::body::BodyWorldPlugin)
.add_plugins(worlds::robot::RobotWorldPlugin)
.add_plugins(worlds::sigma::SigmaWorldPlugin)
.add_plugins(worlds::models::ModelsWorldPlugin)
.add_plugins(worlds::vault::VaultWorldPlugin)
.add_plugins(agent::AgentPlugin);
// `CYB_SHOT=/path.png` (with optional `CYB_SHOT_AT=secs`, default 8)
// saves one frame of the app's own framebuffer โ UI included โ and is how
// a build proves what it looks like without anyone photographing a
// screen. The app's framebuffer shows the app alone, which is the point.
#[cfg(not(target_os = "android"))]
if let Ok(path) = std::env::var("CYB_SHOT") {
let at: f32 = std::env::var("CYB_SHOT_AT").ok().and_then(|s| s.parse().ok()).unwrap_or(8.0);
app.add_systems(
bevy::prelude::Update,
move |mut commands: bevy::prelude::Commands,
time: bevy::prelude::Res<bevy::prelude::Time>,
mut done: bevy::prelude::Local<bool>| {
if *done || time.elapsed_secs() < at {
return;
}
*done = true;
use bevy::render::view::screenshot::{save_to_disk, Screenshot};
commands.spawn(Screenshot::primary_window()).observe(save_to_disk(path.clone()));
bevy::log::info!("shot: saving to {path}");
},
);
}
#[cfg(not(target_os = "android"))]
app.add_plugins(shell::hotkeys::HotkeysPlugin)
.add_plugins(shell::window::WindowLifecyclePlugin)
.add_plugins(shell::tray::TrayPlugin);
app
}
//! The one cyb App โ every platform builds it here.
//!
//! Desktop (`main.rs`) and Android (`lib.rs`'s `android_main`) call
//! [`build_app`] and get the same Bevy app: same worlds, same chrome, same
//! nav. Only the surfaces that name a platform differ โ global hotkeys, the
//! tray, and the hide-on-close window contract exist where a desktop does.
use *;
use RenderApp;
use ;
use crate::;
/// Exposes the render device/queue on the main world so worlds can allocate
/// GPU resources without reaching into the render sub-app.
;