slint/examples/bevy/slint_bevy_adapter.rs
Simon Hausmann 35c972c227
Some checks are pending
autofix.ci / format_fix (push) Waiting to run
autofix.ci / lint_typecheck (push) Waiting to run
CI / node_test (ubuntu-22.04) (push) Blocked by required conditions
CI / files-changed (push) Waiting to run
CI / build_and_test (--exclude bevy-example, ubuntu-22.04, 1.82) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, --exclude bevy-example, windows-2022, 1.82) (push) Blocked by required conditions
CI / node_test (macos-14) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, macos-14, stable) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, beta) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, stable) (push) Blocked by required conditions
CI / build_and_test (ubuntu-22.04, nightly) (push) Blocked by required conditions
CI / cpp_test_driver (ubuntu-22.04) (push) Blocked by required conditions
CI / cpp_test_driver (windows-2022) (push) Blocked by required conditions
CI / cpp_cmake (macos-14, 1.82) (push) Blocked by required conditions
CI / cpp_cmake (ubuntu-22.04, stable) (push) Blocked by required conditions
CI / cpp_cmake (windows-2022, nightly) (push) Blocked by required conditions
CI / cpp_package_test (push) Blocked by required conditions
CI / node_test (windows-2022) (push) Blocked by required conditions
CI / python_test (macos-14) (push) Blocked by required conditions
CI / python_test (ubuntu-22.04) (push) Blocked by required conditions
CI / python_test (windows-2022) (push) Blocked by required conditions
CI / cpp_test_driver (macos-13) (push) Blocked by required conditions
CI / mcu (stm32h735g, thumbv7em-none-eabihf) (push) Blocked by required conditions
CI / vsce_build_test (push) Blocked by required conditions
CI / mcu (pico-st7789, thumbv6m-none-eabi) (push) Blocked by required conditions
CI / mcu (pico2-st7789, thumbv8m.main-none-eabihf) (push) Blocked by required conditions
CI / mcu-embassy (push) Blocked by required conditions
CI / ffi_32bit_build (push) Blocked by required conditions
CI / docs (push) Blocked by required conditions
CI / wasm (push) Blocked by required conditions
CI / wasm_demo (push) Blocked by required conditions
CI / tree-sitter (push) Blocked by required conditions
CI / updater_test (0.3.0) (push) Blocked by required conditions
CI / fmt_test (push) Blocked by required conditions
CI / esp-idf-quick (push) Blocked by required conditions
CI / android (push) Blocked by required conditions
CI / miri (push) Blocked by required conditions
CI / test-figma-inspector (push) Blocked by required conditions
Add example of rendering a Bevy scene into Slint (#8380)
2025-05-19 15:25:40 +02:00

257 lines
10 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
//! This module provides function(s) to integrate a bevy App into a Slint application.
//!
//! The integration's entry point is [`run_bevy_app_with_slint()`], which will launch the
//! bevy [`App`] in a thread separate from the main thread and supply textures of the rendered
//! scenes via channels.
use std::sync::Arc;
use slint::wgpu_24::wgpu;
use bevy::{
prelude::*,
render::{
extract_resource::{ExtractResource, ExtractResourcePlugin},
render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext, RenderLabel},
renderer::RenderContext,
settings::RenderCreation,
RenderApp, RenderPlugin,
},
};
/// This enum describes the two kinds of message the Slint application send to the bevy integration thread.
pub enum ControlMessage {
/// Send this message when you don't need a previously received texture anymore.
ReleaseFrontBufferTexture { texture: wgpu::Texture },
/// Send this message to adjust the size of the scene textures.
ResizeBuffers { width: u32, height: u32 },
}
/// Initializes Bevy and Slint, spawns a bevy [`App`], and supplies textures of the rendered scenes via channels.
///
/// Use the `bevy_app_pre_default_plugins_callback` callback to add any plugins to the app before the default plugins.
/// Use the `bevy_main` callback to add systems, plugins, etc. to your app and call [`App::run()`].
///
/// If successful, this function returns two channels:
/// - Use the receiver channel to obtain textures for use in the Slint UI. These textures have the scene of your default
/// camera rendered into.
/// - Use the [`ControlMessage`] sender channel to return textures that you don't need anymore, as well as to inform the
/// renderer to resize the texture if needed.
///
/// *Note*: At the moment only one single camera is supported.
pub async fn run_bevy_app_with_slint(
bevy_app_pre_default_plugins_callback: impl FnOnce(&mut App) + Send + 'static,
bevy_main: impl FnOnce(App) + Send + 'static,
) -> Result<
(smol::channel::Receiver<wgpu::Texture>, smol::channel::Sender<ControlMessage>),
slint::PlatformError,
> {
let backends = wgpu::Backends::from_env().unwrap_or_default();
let dx12_shader_compiler = wgpu::Dx12Compiler::from_env().unwrap_or_default();
let gles_minor_version = wgpu::Gles3MinorVersion::from_env().unwrap_or_default();
let instance = wgpu::util::new_instance_with_webgpu_detection(&wgpu::InstanceDescriptor {
backends,
flags: wgpu::InstanceFlags::from_build_config().with_env(),
backend_options: wgpu::BackendOptions {
dx12: wgpu::Dx12BackendOptions { shader_compiler: dx12_shader_compiler },
gl: wgpu::GlBackendOptions { gles_minor_version },
},
})
.await;
let (render_device, render_queue, adapter_info, adapter) =
bevy::render::renderer::initialize_renderer(
&instance,
&bevy::render::settings::WgpuSettings::default(),
&wgpu::RequestAdapterOptions::default(),
)
.await;
let selector =
slint::BackendSelector::new().require_wgpu_24(slint::wgpu_24::WGPUConfiguration::Manual {
instance: instance.clone(),
adapter: (**adapter.0).clone(),
device: render_device.wgpu_device().clone(),
queue: (**render_queue.0).clone(),
});
selector.select()?;
let (control_message_sender, control_message_receiver) =
smol::channel::bounded::<ControlMessage>(2);
let (bevy_front_buffer_sender, bevy_front_buffer_receiver) =
smol::channel::bounded::<wgpu::Texture>(2);
let wgpu_device = render_device.wgpu_device().clone();
let create_texture = move |label, width, height| {
wgpu_device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Can only render to SRGB texture - https://github.com/bevyengine/bevy/issues/15201
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
};
let front_buffer = create_texture("Front Buffer", 640, 480);
let back_buffer = create_texture("Back Buffer", 640, 480);
let inflight_buffer = create_texture("Back Buffer", 640, 480);
let mut buffer_width = 640;
let mut buffer_height = 480;
let _bevy_thread = std::thread::spawn(move || {
let runner = move |mut app: bevy::app::App| {
app.finish();
app.cleanup();
let mut next_texture_view_id: u32 = 0;
loop {
let mut next_back_buffer = match control_message_receiver.recv_blocking() {
Ok(ControlMessage::ReleaseFrontBufferTexture { texture }) => texture,
Ok(ControlMessage::ResizeBuffers { width, height }) => {
buffer_width = width;
buffer_height = height;
continue;
}
Err(_) => break,
};
if next_back_buffer.width() != buffer_width
|| next_back_buffer.height() != buffer_height
{
next_back_buffer = create_texture("back buffer", buffer_width, buffer_height);
}
let texture_view = next_back_buffer.create_view(&wgpu::TextureViewDescriptor {
label: Some("bevy back buffer texture view"),
format: None,
dimension: None,
..Default::default()
});
let texture_view_handle =
bevy::render::camera::ManualTextureViewHandle(next_texture_view_id);
next_texture_view_id += 1;
{
let world = app.world_mut();
let mut back_buffer = world.get_resource_mut::<BackBuffer>().unwrap();
back_buffer.0 = Some(next_back_buffer.clone());
let mut manual_texture_views = world
.get_resource_mut::<bevy::render::camera::ManualTextureViews>()
.unwrap();
manual_texture_views.clear();
manual_texture_views.insert(
texture_view_handle,
bevy::render::camera::ManualTextureView {
texture_view: texture_view.into(),
size: (next_back_buffer.width(), next_back_buffer.height()).into(),
format: bevy::render::render_resource::TextureFormat::Rgba8UnormSrgb,
},
);
let mut cameras = world.query::<&mut Camera>();
if let Some(mut c) = cameras.iter_mut(world).next() {
c.target =
bevy::render::camera::RenderTarget::TextureView(texture_view_handle);
}
}
app.update();
}
bevy::app::AppExit::Success
};
let mut app = App::new();
app.set_runner(runner);
app.insert_resource(BackBuffer(None));
bevy_app_pre_default_plugins_callback(&mut app);
app.add_plugins(
DefaultPlugins.set(ImagePlugin::default_nearest()).set(RenderPlugin {
render_creation: RenderCreation::manual(
render_device,
render_queue,
adapter_info,
adapter,
bevy::render::renderer::RenderInstance(Arc::new(
bevy::render::renderer::WgpuWrapper::new(instance),
)),
),
..default()
}), //.disable::<bevy::winit::WinitPlugin>(),
);
app.add_plugins(SlintRenderToTexturePlugin(bevy_front_buffer_sender));
app.add_plugins(ExtractResourcePlugin::<BackBuffer>::default());
bevy_main(app)
});
control_message_sender
.send_blocking(ControlMessage::ReleaseFrontBufferTexture { texture: back_buffer })
.unwrap();
control_message_sender
.send_blocking(ControlMessage::ReleaseFrontBufferTexture { texture: inflight_buffer })
.unwrap();
control_message_sender
.send_blocking(ControlMessage::ReleaseFrontBufferTexture { texture: front_buffer })
.unwrap();
Ok((bevy_front_buffer_receiver, control_message_sender))
}
#[derive(Resource, Deref)]
struct FrontBufferReturnSender(smol::channel::Sender<wgpu::Texture>);
/// Plugin for Render world part of work
struct SlintRenderToTexturePlugin(smol::channel::Sender<wgpu::Texture>);
impl Plugin for SlintRenderToTexturePlugin {
fn build(&self, app: &mut App) {
let render_app = app.sub_app_mut(RenderApp);
let mut graph = render_app.world_mut().resource_mut::<RenderGraph>();
graph.add_node(SlintSwapChain, SlintSwapChainDriver);
graph.add_node_edge(bevy::render::graph::CameraDriverLabel, SlintSwapChain);
render_app.insert_resource(FrontBufferReturnSender(self.0.clone()));
}
}
#[derive(Clone, Resource, ExtractResource, Deref, DerefMut)]
struct BackBuffer(pub Option<wgpu::Texture>);
#[derive(Debug, PartialEq, Eq, Clone, Hash, RenderLabel)]
struct SlintSwapChain;
#[derive(Default)]
struct SlintSwapChainDriver;
impl render_graph::Node for SlintSwapChainDriver {
fn run(
&self,
_graph: &mut RenderGraphContext,
_render_context: &mut RenderContext,
world: &World,
) -> Result<(), NodeRunError> {
let front_buffer_sender = world.get_resource::<FrontBufferReturnSender>().unwrap();
let back_buffer = world.get_resource::<BackBuffer>().unwrap();
if let Some(bb) = &back_buffer.0 {
// silently ignore errors when the sender is closed. Reporting an error would just result in bevy panicing,
// while a closed channel is indicating a shutdown condition.
front_buffer_sender.0.send_blocking(bb.clone()).ok();
}
Ok(())
}
}