// Copyright © SixtyFPS GmbH // 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, smol::channel::Sender), 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::(2); let (bevy_front_buffer_sender, bevy_front_buffer_receiver) = smol::channel::bounded::(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::().unwrap(); back_buffer.0 = Some(next_back_buffer.clone()); let mut manual_texture_views = world .get_resource_mut::() .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::(), ); app.add_plugins(SlintRenderToTexturePlugin(bevy_front_buffer_sender)); app.add_plugins(ExtractResourcePlugin::::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); /// Plugin for Render world part of work struct SlintRenderToTexturePlugin(smol::channel::Sender); 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::(); 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); #[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::().unwrap(); let back_buffer = world.get_resource::().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(()) } }