mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-21 07:41:51 +00:00
251 lines
9.8 KiB
Rust
251 lines
9.8 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 slint::wgpu_26::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,
|
||
> {
|
||
#[allow(unused_mut)]
|
||
let mut backends = wgpu::Backends::from_env().unwrap_or_default();
|
||
|
||
// Skia’s Vulkan backend is currently broken on Windows.
|
||
// See: https://github.com/slint-ui/slint/issues/9320
|
||
#[cfg(target_family = "windows")]
|
||
backends.remove(wgpu::Backends::VULKAN);
|
||
|
||
let bevy::render::settings::RenderResources(
|
||
render_device,
|
||
render_queue,
|
||
adapter_info,
|
||
adapter,
|
||
instance,
|
||
) = bevy::render::renderer::initialize_renderer(
|
||
backends,
|
||
None,
|
||
&bevy::render::settings::WgpuSettings::default(),
|
||
)
|
||
.await;
|
||
|
||
let selector =
|
||
slint::BackendSelector::new().require_wgpu_26(slint::wgpu_26::WGPUConfiguration::Manual {
|
||
instance: (**instance.0).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::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::texture::ManualTextureViews>()
|
||
.unwrap();
|
||
manual_texture_views.clear();
|
||
manual_texture_views.insert(
|
||
texture_view_handle,
|
||
bevy::render::texture::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::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,
|
||
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(())
|
||
}
|
||
}
|