cyb/shell/src/app.rs

//! 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
}

Homonyms

cyberia/cyberia-my/src/app.rs
cyberia/research/cyberia-my/src/app.rs
cyb/evy/forks/bevy_render/src/render_graph/app.rs

Graph