idk? like.. updating my understanding of the code base after half a year of not touching it

This commit is contained in:
janis 2026-03-26 12:08:48 +01:00
parent fd1855dc73
commit c1c69d5e54
13 changed files with 1711 additions and 159 deletions

View file

@ -1,3 +1,8 @@
[package]
name = "vidya"
version = "0.1.0"
edition = "2024"
[workspace] [workspace]
resolver = "2" resolver = "2"
@ -5,9 +10,16 @@ members = [
"crates/renderer", "crates/renderer",
"crates/window", "crates/window",
"crates/game", "crates/game",
"crates/text", "crates/bevy_vulkan_render", "crates/text",
"crates/bevy_vulkan_render",
] ]
[dependencies]
bevy_vulkan_render = { path = "crates/bevy_vulkan_render" }
[dev-dependencies]
bevy = { workspace = true }
[profile.debug-release] [profile.debug-release]
inherits = "release" inherits = "release"
opt-level = 2 opt-level = 2
@ -16,7 +28,7 @@ debug = true
[workspace.dependencies] [workspace.dependencies]
anyhow = "1.0.89" anyhow = "1.0.89"
thiserror = "2.0" thiserror = "2.0"
derive_more = { version = "1.0.0", features = ["deref", "deref_mut"] } derive_more = { version = "1.0.0", features = ["deref", "debug", "deref_mut"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = {version ="0.3", features = ["env-filter"]} tracing-subscriber = {version ="0.3", features = ["env-filter"]}
@ -28,7 +40,7 @@ thread_local = "1.1.8"
ash = "0.38.0" ash = "0.38.0"
ash-window = "0.13.0" ash-window = "0.13.0"
vk-mem = "0.4.0" vk-mem = "0.5.0"
vk-sync = "0.1.6" vk-sync = "0.1.6"
arrayvec = "0.7.6" arrayvec = "0.7.6"
@ -51,14 +63,23 @@ raw-window-handle = "0.6"
egui = "0.32" egui = "0.32"
egui_winit_platform = "0.27" egui_winit_platform = "0.27"
bevy_ecs = { version = "0.17.0-dev", git = "https://github.com/bevyengine/bevy", features = ["multi_threaded", "trace"]} bevy = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_window = { version = "0.17.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_app = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_asset = { version = "0.17.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_asset = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_reflect = { version = "0.17.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_derive = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_utils = { version = "0.17.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_ecs = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy", features = ["multi_threaded", "trace"]}
bevy_derive = { version = "0.17.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_hierarchy = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_math = { version = "0.17.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_log = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_transform = { version = "0.17.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_math = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_hierarchy = { version = "0.17.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_platform = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_tasks = { version = "0.17.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_reflect = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_app = { version = "0.17.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_tasks = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_time = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_transform = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_utils = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_window = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
bevy_winit = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
[[example]]
name = "bevy_integration"
path = "examples/bevy_integration.rs"

View file

@ -3,9 +3,22 @@ name = "bevy_vulkan_render"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[features]
trace = ["tracing"]
default = ["trace"]
[dependencies] [dependencies]
bevy_ecs = { workspace = true } tracing = { workspace = true, optional = true }
bevy_app = { workspace = true } bevy_app = { workspace = true }
bevy_asset = { workspace = true }
bevy_derive = { workspace = true }
bevy_ecs = { workspace = true }
bevy_log = { workspace = true }
bevy_platform = { workspace = true }
bevy_reflect = { workspace = true }
bevy_tasks = { workspace = true } bevy_tasks = { workspace = true }
bevy_time = { workspace = true }
bevy_utils = { workspace = true }
bevy_window = { workspace = true } bevy_window = { workspace = true }
renderer = { path = "../renderer" } renderer = { path = "../renderer" }
async-channel = "2"

View file

@ -0,0 +1,153 @@
use crate::extract_plugin::MainWorld;
use bevy_ecs::{
change_detection::Tick,
prelude::*,
query::FilteredAccessSet,
system::{
ReadOnlySystemParam, SystemMeta, SystemParam, SystemParamItem, SystemParamValidationError,
SystemState,
},
world::unsafe_world_cell::UnsafeWorldCell,
};
use core::ops::{Deref, DerefMut};
/// A helper for accessing [`MainWorld`] content using a system parameter.
///
/// A [`SystemParam`] adapter which applies the contained `SystemParam` to the [`World`]
/// contained in [`MainWorld`]. This parameter only works for systems run
/// during the [`ExtractSchedule`](crate::ExtractSchedule).
///
/// This requires that the contained [`SystemParam`] does not mutate the world, as it
/// uses a read-only reference to [`MainWorld`] internally.
///
/// ## Context
///
/// [`ExtractSchedule`] is used to extract (move) data from the simulation world ([`MainWorld`]) to the
/// render world. The render world drives rendering each frame (generally to a `Window`).
/// This design is used to allow performing calculations related to rendering a prior frame at the same
/// time as the next frame is simulated, which increases throughput (FPS).
///
/// [`Extract`] is used to get data from the main world during [`ExtractSchedule`].
///
/// ## Examples
///
/// ```
/// use bevy_ecs::prelude::*;
/// use bevy_render::Extract;
/// use bevy_render::sync_world::RenderEntity;
/// # #[derive(Component)]
/// // Do make sure to sync the cloud entities before extracting them.
/// # struct Cloud;
/// fn extract_clouds(mut commands: Commands, clouds: Extract<Query<RenderEntity, With<Cloud>>>) {
/// for cloud in &clouds {
/// commands.entity(cloud).insert(Cloud);
/// }
/// }
/// ```
///
/// [`ExtractSchedule`]: crate::ExtractSchedule
/// [Window]: bevy_window::Window
pub struct Extract<'w, 's, P>
where
P: ReadOnlySystemParam + 'static,
{
item: SystemParamItem<'w, 's, P>,
}
#[doc(hidden)]
pub struct ExtractState<P: SystemParam + 'static> {
state: SystemState<P>,
main_world_state: <Res<'static, MainWorld> as SystemParam>::State,
}
// SAFETY: The only `World` access (`Res<MainWorld>`) is read-only.
unsafe impl<P> ReadOnlySystemParam for Extract<'_, '_, P> where P: ReadOnlySystemParam {}
// SAFETY: The only `World` access is properly registered by `Res<MainWorld>::init_state`.
// This call will also ensure that there are no conflicts with prior params.
unsafe impl<P> SystemParam for Extract<'_, '_, P>
where
P: ReadOnlySystemParam,
{
type State = ExtractState<P>;
type Item<'w, 's> = Extract<'w, 's, P>;
fn init_state(world: &mut World) -> Self::State {
let mut main_world = world.resource_mut::<MainWorld>();
ExtractState {
state: SystemState::new(&mut main_world),
main_world_state: Res::<MainWorld>::init_state(world),
}
}
fn init_access(
state: &Self::State,
system_meta: &mut SystemMeta,
component_access_set: &mut FilteredAccessSet,
world: &mut World,
) {
Res::<MainWorld>::init_access(
&state.main_world_state,
system_meta,
component_access_set,
world,
);
}
#[inline]
unsafe fn get_param<'w, 's>(
state: &'s mut Self::State,
system_meta: &SystemMeta,
world: UnsafeWorldCell<'w>,
change_tick: Tick,
) -> Result<Self::Item<'w, 's>, SystemParamValidationError> {
// SAFETY:
// - The caller ensures that `world` is the same one that `init_state` was called with.
// - The caller ensures that no other `SystemParam`s will conflict with the accesses we have registered.
let main_world = unsafe {
Res::<MainWorld>::get_param(
&mut state.main_world_state,
system_meta,
world,
change_tick,
)?
};
let item = state.state.get(main_world.into_inner())?;
Ok(Extract { item })
}
}
impl<'w, 's, P> Deref for Extract<'w, 's, P>
where
P: ReadOnlySystemParam,
{
type Target = SystemParamItem<'w, 's, P>;
#[inline]
fn deref(&self) -> &Self::Target {
&self.item
}
}
impl<'w, 's, P> DerefMut for Extract<'w, 's, P>
where
P: ReadOnlySystemParam,
{
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.item
}
}
impl<'a, 'w, 's, P> IntoIterator for &'a Extract<'w, 's, P>
where
P: ReadOnlySystemParam,
&'a SystemParamItem<'w, 's, P>: IntoIterator,
{
type Item = <&'a SystemParamItem<'w, 's, P> as IntoIterator>::Item;
type IntoIter = <&'a SystemParamItem<'w, 's, P> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
(&self.item).into_iter()
}
}

View file

@ -0,0 +1,139 @@
use std::ops::{Deref, DerefMut};
use bevy_app::{App, Plugin, SubApp};
use bevy_ecs::{
resource::Resource,
schedule::{IntoScheduleConfigs, Schedule, ScheduleBuildSettings, ScheduleLabel, Schedules},
world::{Mut, World},
};
use bevy_utils::default;
use crate::{
Render, RenderApp, RenderSystems, ScratchMainWorld, SyncWorldPlugin,
sync_world::{despawn_temporary_render_entities, entity_sync_system},
};
/// The simulation [`World`] of the application, stored as a resource.
///
/// This resource is only available during [`ExtractSchedule`] and not
/// during command application of that schedule.
/// See [`Extract`] for more details.
#[derive(Resource, Default)]
pub struct MainWorld(World);
impl Deref for MainWorld {
type Target = World;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for MainWorld {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// Schedule which extract data from the main world and inserts it into the render world.
///
/// This step should be kept as short as possible to increase the "pipelining potential" for
/// running the next frame while rendering the current frame.
///
/// This schedule is run on the main world, but its buffers are not applied
/// until it is returned to the render world.
#[derive(ScheduleLabel, PartialEq, Eq, Debug, Clone, Hash, Default)]
pub struct ExtractSchedule;
/// Executes the [`ExtractSchedule`] step of the renderer.
/// This updates the render world with the extracted ECS data of the current frame.
fn extract(main_world: &mut World, render_world: &mut World) {
// temporarily add the app world to the render world as a resource
let scratch_world = main_world.remove_resource::<ScratchMainWorld>().unwrap();
let inserted_world = core::mem::replace(main_world, scratch_world.0);
render_world.insert_resource(MainWorld(inserted_world));
render_world.run_schedule(ExtractSchedule);
// move the app world back, as if nothing happened.
let inserted_world = render_world.remove_resource::<MainWorld>().unwrap();
let scratch_world = core::mem::replace(main_world, inserted_world.0);
main_world.insert_resource(ScratchMainWorld(scratch_world));
}
/// Applies the commands from the extract schedule. This happens during
/// the render schedule rather than during extraction to allow the commands to run in parallel with the
/// main app when pipelined rendering is enabled.
fn apply_extract_commands(render_world: &mut World) {
render_world.resource_scope(|render_world, mut schedules: Mut<Schedules>| {
schedules
.get_mut(ExtractSchedule)
.unwrap()
.apply_deferred(render_world);
});
}
/// Plugin that sets up the [`RenderApp`] and handles extracting data from the
/// main world to the render world.
pub struct ExtractPlugin {
/// Function that gets run at the beginning of each extraction.
///
/// Gets the main world and render world as arguments (in that order).
pub pre_extract: fn(&mut World, &mut World),
}
impl Default for ExtractPlugin {
fn default() -> Self {
Self {
pre_extract: |_, _| {},
}
}
}
impl Plugin for ExtractPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(SyncWorldPlugin);
app.init_resource::<ScratchMainWorld>();
let mut render_app = SubApp::new();
let mut extract_schedule = Schedule::new(ExtractSchedule);
// We skip applying any commands during the ExtractSchedule
// so commands can be applied on the render thread.
extract_schedule.set_build_settings(ScheduleBuildSettings {
auto_insert_apply_deferred: false,
..default()
});
extract_schedule.set_apply_final_deferred(false);
render_app.add_schedule(Render::base_schedule());
render_app.add_schedule(extract_schedule);
render_app.add_systems(
Render,
(
// This set applies the commands from the extract schedule while the render schedule
// is running in parallel with the main app.
apply_extract_commands.in_set(RenderSystems::ExtractCommands),
despawn_temporary_render_entities.in_set(RenderSystems::PostCleanup),
),
);
let pre_extract = self.pre_extract;
render_app.set_extract(move |main_world, render_world| {
pre_extract(main_world, render_world);
{
#[cfg(feature = "trace")]
let _stage_span = bevy_log::info_span!("entity_sync").entered();
entity_sync_system(main_world, render_world);
}
// run extract schedule
extract(main_world, render_world);
});
let (sender, receiver) = bevy_time::create_time_channels();
render_app.insert_resource(sender);
app.insert_resource(receiver);
app.insert_sub_app(RenderApp, render_app);
}
}

View file

@ -1,54 +1,149 @@
use std::ops::{Deref, DerefMut};
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_ecs::prelude::*; use bevy_asset::AssetServer;
use bevy_ecs::{prelude::*, schedule::ScheduleBuildSettings};
use bevy_app::AppLabel; use bevy_app::AppLabel;
use bevy_ecs::schedule::ScheduleLabel; use bevy_ecs::schedule::ScheduleLabel;
use bevy_window::{PrimaryWindow, RawHandleWrapperHolder}; use bevy_utils::default;
use crate::{
extract_plugin::{ExtractPlugin, ExtractSchedule},
sync_world::despawn_temporary_render_entities,
window::{
ExtractedWindows, Surfaces, configure_surfaces, extract_windows, need_surface_configuration,
},
};
pub mod prelude {
pub use crate::window::{SurfaceData, Surfaces};
}
// The main plugin for the Vulkan renderer.
// This plugin is responsible for setting up the render app, and spawning the render thread.
// The render thread waits for the main app to send it render app, and then updates the render up before sending it back to the main app.
// Before the main app sends the render app, it runs the sync and extract schedules to update the entity mappings as well as extract any required entities and components from the main world to the render world.
// Then, the main world sends the render app to the render thread and starts to execute its schedules until the next frame, at which point it will wait for the render app to be sent back before running the sync and extract schedules again.
// The render thread will apply the extract commands in order to allow the main thread to get back to work sooner.
/// The main render schedule. /// The main render schedule.
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] #[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone, Default)]
pub struct Render; pub struct Render;
impl Render {
/// Sets up the base structure of the rendering [`Schedule`].
///
/// The sets defined in this enum are configured to run in order.
pub fn base_schedule() -> Schedule {
use RenderSystems::*;
let mut schedule = Schedule::new(Self);
schedule.configure_sets((ExtractCommands, Render, Cleanup, PostCleanup).chain());
schedule.configure_sets((ExtractCommands, Prepare).chain());
schedule
}
}
/// The startup schedule of the [`RenderApp`] /// The startup schedule of the [`RenderApp`]
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] #[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone, Default)]
pub struct RenderStartup; pub struct RenderStartup;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct VulkanRenderPlugin; pub struct RenderPlugin;
/// A label for the rendering sub-app. /// A label for the rendering sub-app.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)] #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)]
pub struct RenderApp; pub struct RenderApp;
impl Plugin for VulkanRenderPlugin { /// The systems sets of the default [`App`] rendering schedule.
///
/// These can be useful for ordering, but you almost never want to add your systems to these sets.
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum RenderSystems {
/// This is used for applying the commands from the [`ExtractSchedule`]
ExtractCommands,
/// Prepare render resources from extracted data for the GPU based on their sorted order.
/// Create [`BindGroups`](render_resource::BindGroup) that depend on those data.
Prepare,
/// Actual rendering happens here.
/// In most cases, only the render backend should insert resources here.
Render,
/// Cleanup render resources here.
Cleanup,
/// Final cleanup occurs: all entities will be despawned.
///
/// Runs after [`Cleanup`](RenderSystems::Cleanup).
PostCleanup,
}
mod extract_plugin;
fn mock_render_system() {
eprintln!("This is a mock render system. It doesn't do anything.");
}
impl Plugin for RenderPlugin {
fn build(&self, app: &mut bevy_app::App) { fn build(&self, app: &mut bevy_app::App) {
app.add_systems(Startup, startup); app.add_plugins(ExtractPlugin::default());
let asset_server = app.world().resource::<AssetServer>().clone();
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.insert_resource(asset_server);
render_app.init_schedule(RenderStartup);
render_app.update_schedule = Some(Render.intern());
render_app.add_systems(Render, mock_render_system.in_set(RenderSystems::Render));
}
} }
} }
fn init_vulkan(app: &mut App) { fn init_vulkan(app: &mut App) -> Option<()> {
let mut window_query = app let device = renderer::device::Device::new_from_default_desc(None, &[]).ok()?;
.world_mut()
.query_filtered::<&RawHandleWrapperHolder, With<PrimaryWindow>>();
let primary_window = window_query.single(app.world()).ok().cloned(); let device = RenderDevice { device };
app.insert_resource(device.clone());
let render_app = app.sub_app_mut(RenderApp);
render_app.insert_resource(device.clone());
Some(())
} }
fn initialize_render_app(app: &mut App) { /// A "scratch" world used to avoid allocating new worlds every frame when
let mut render_app = SubApp::new(); /// swapping out the [`MainWorld`] for [`ExtractSchedule`].
render_app.update_schedule = Some(Render.intern()); #[derive(Resource, Default)]
struct ScratchMainWorld(World);
let mut should_run_startup = true; fn render_start() {
render_app.set_extract(move |main, render| { #[cfg(feature = "trace")]
if should_run_startup { tracing::event!(tracing::Level::DEBUG, "render_start");
render.run_schedule(RenderStartup);
should_run_startup = false;
}
// extract logic here
});
app.insert_sub_app(RenderApp, render_app);
} }
fn startup() {} fn render_startup() {
#[cfg(feature = "trace")]
tracing::event!(tracing::Level::DEBUG, "render_startup");
}
mod extract_param;
mod rendering;
mod sync_world;
mod window;
pub use rendering::RenderingPlugin;
pub use sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet, SyncWorldPlugin};
#[derive(Resource, Clone)]
pub struct RenderDevice {
device: renderer::device::Device,
}
impl Deref for RenderDevice {
type Target = renderer::device::Device;
fn deref(&self) -> &Self::Target {
&self.device
}
}

View file

@ -0,0 +1,205 @@
use async_channel::{Receiver, Sender};
use bevy_app::{App, AppExit, AppLabel, Plugin, SubApp};
use bevy_ecs::{
resource::Resource,
schedule::MainThreadExecutor,
world::{Mut, World},
};
use bevy_tasks::ComputeTaskPool;
use crate::RenderApp;
/// A Label for the sub app that runs the parts of pipelined rendering that need to run on the main thread.
///
/// The Main schedule of this app can be used to run logic after the render schedule starts, but
/// before I/O processing. This can be useful for something like frame pacing.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)]
pub struct RenderExtractApp;
/// Channels used by the main app to send and receive the render app.
#[derive(Resource)]
pub struct RenderAppChannels {
app_to_render_sender: Sender<SubApp>,
render_to_app_receiver: Receiver<SubApp>,
render_app_in_render_thread: bool,
}
impl RenderAppChannels {
/// Create a `RenderAppChannels` from a [`async_channel::Receiver`] and [`async_channel::Sender`]
pub fn new(
app_to_render_sender: Sender<SubApp>,
render_to_app_receiver: Receiver<SubApp>,
) -> Self {
Self {
app_to_render_sender,
render_to_app_receiver,
render_app_in_render_thread: false,
}
}
/// Send the `render_app` to the rendering thread.
pub fn send_blocking(&mut self, render_app: SubApp) {
self.app_to_render_sender.send_blocking(render_app).unwrap();
self.render_app_in_render_thread = true;
}
/// Receive the `render_app` from the rendering thread.
/// Return `None` if the render thread has panicked.
pub async fn recv(&mut self) -> Option<SubApp> {
let render_app = self.render_to_app_receiver.recv().await.ok()?;
self.render_app_in_render_thread = false;
Some(render_app)
}
}
impl Drop for RenderAppChannels {
fn drop(&mut self) {
if self.render_app_in_render_thread {
// Any non-send data in the render world was initialized on the main thread.
// So on dropping the main world and ending the app, we block and wait for
// the render world to return to drop it. Which allows the non-send data
// drop methods to run on the correct thread.
self.render_to_app_receiver.recv_blocking().ok();
}
}
}
/// The [`PipelinedRenderingPlugin`] can be added to your application to enable pipelined rendering.
///
/// This moves rendering into a different thread, so that the Nth frame's rendering can
/// be run at the same time as the N + 1 frame's simulation.
///
/// ```text
/// |--------------------|--------------------|--------------------|--------------------|
/// | simulation thread | frame 1 simulation | frame 2 simulation | frame 3 simulation |
/// |--------------------|--------------------|--------------------|--------------------|
/// | rendering thread | | frame 1 rendering | frame 2 rendering |
/// |--------------------|--------------------|--------------------|--------------------|
/// ```
///
/// The plugin is dependent on the [`RenderApp`] added by [`crate::RenderPlugin`] and so must
/// be added after that plugin. If it is not added after, the plugin will do nothing.
///
/// A single frame of execution looks something like below
///
/// ```text
/// |---------------------------------------------------------------------------|
/// | | | RenderExtractApp schedule | winit events | main schedule |
/// | sync | extract |----------------------------------------------------------|
/// | | | extract commands | rendering schedule |
/// |---------------------------------------------------------------------------|
/// ```
///
/// - `sync` is the step where the entity-entity mapping between the main and render world is updated.
/// This is run on the main app's thread. For more information checkout [`SyncWorldPlugin`].
/// - `extract` is the step where data is copied from the main world to the render world.
/// This is run on the main app's thread.
/// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the
/// main schedule can start sooner.
/// - Then the `rendering schedule` is run. See [`RenderSystems`](crate::RenderSystems) for the standard steps in this process.
/// - In parallel to the rendering thread the [`RenderExtractApp`] schedule runs. By
/// default, this schedule is empty. But it is useful if you need something to run before I/O processing.
/// - Next all the `winit events` are processed.
/// - And finally the `main app schedule` is run.
/// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again.
///
/// [`SyncWorldPlugin`]: crate::sync_world::SyncWorldPlugin
#[derive(Default)]
pub struct RenderingPlugin;
impl Plugin for RenderingPlugin {
fn build(&self, app: &mut App) {
// Don't add RenderExtractApp if RenderApp isn't initialized.
if app.get_sub_app(RenderApp).is_none() {
return;
}
app.insert_resource(MainThreadExecutor::new());
let mut sub_app = SubApp::new();
sub_app.set_extract(renderer_extract);
app.insert_sub_app(RenderExtractApp, sub_app);
}
// Sets up the render thread and inserts resources into the main app used for controlling the render thread.
fn cleanup(&self, app: &mut App) {
// skip setting up when headless
if app.get_sub_app(RenderExtractApp).is_none() {
return;
}
let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::<SubApp>(1);
let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::<SubApp>(1);
let mut render_app = app
.remove_sub_app(RenderApp)
.expect("Unable to get RenderApp. Another plugin may have removed the RenderApp before RenderingPlugin");
// clone main thread executor to render world
let executor = app.world().get_resource::<MainThreadExecutor>().unwrap();
render_app.world_mut().insert_resource(executor.clone());
render_to_app_sender.send_blocking(render_app).unwrap();
app.insert_resource(RenderAppChannels::new(
app_to_render_sender,
render_to_app_receiver,
));
std::thread::spawn(move || {
#[cfg(feature = "trace")]
let _span = tracing::info_span!("render thread").entered();
let compute_task_pool = ComputeTaskPool::get();
loop {
// run a scope here to allow main world to use this thread while it's waiting for the render app
let sent_app = compute_task_pool
.scope(|s| {
s.spawn(async { app_to_render_receiver.recv().await });
})
.pop();
let Some(Ok(mut render_app)) = sent_app else {
break;
};
{
#[cfg(feature = "trace")]
let _sub_app_span = tracing::info_span!("sub app", name = ?RenderApp).entered();
render_app.update();
}
if render_to_app_sender.send_blocking(render_app).is_err() {
break;
}
}
tracing::debug!("exiting pipelined rendering thread");
});
}
}
// This function waits for the rendering world to be received,
// runs extract, and then sends the rendering world back to the render thread.
fn renderer_extract(app_world: &mut World, _world: &mut World) {
app_world.resource_scope(|world, main_thread_executor: Mut<MainThreadExecutor>| {
world.resource_scope(|world, mut render_channels: Mut<RenderAppChannels>| {
// we use a scope here to run any main thread tasks that the render world still needs to run
// while we wait for the render world to be received.
if let Some(mut render_app) = ComputeTaskPool::get()
.scope_with_executor(true, Some(&*main_thread_executor.0), |s| {
s.spawn(async { render_channels.recv().await });
})
.pop()
.unwrap()
{
render_app.extract(world);
render_channels.send_blocking(render_app);
} else {
// Renderer thread panicked
world.write_message(AppExit::error());
}
});
});
}

View file

@ -0,0 +1,613 @@
use bevy_app::Plugin;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::{ContainsEntity, Entity, EntityEquivalent, EntityHash},
lifecycle::{Add, Remove},
observer::On,
query::With,
reflect::ReflectComponent,
resource::Resource,
system::{Local, Query, ResMut, SystemState},
world::{Mut, World},
};
use bevy_platform::collections::{HashMap, HashSet};
use bevy_reflect::{Reflect, std_traits::ReflectDefault};
/// A plugin that synchronizes entities with [`SyncToRenderWorld`] between the main world and the render world.
///
/// All entities with the [`SyncToRenderWorld`] component are kept in sync. It
/// is automatically added as a required component by [`ExtractComponentPlugin`]
/// and [`SyncComponentPlugin`], so it doesn't need to be added manually when
/// spawning or as a required component when either of these plugins are used.
///
/// # Implementation
///
/// Bevy's renderer is architected independently from the main app.
/// It operates in its own separate ECS [`World`], so the renderer logic can run in parallel with the main world logic.
/// This is called "Pipelined Rendering", see [`PipelinedRenderingPlugin`] for more information.
///
/// [`SyncWorldPlugin`] is the first thing that runs every frame and it maintains an entity-to-entity mapping
/// between the main world and the render world.
/// It does so by spawning and despawning entities in the render world, to match spawned and despawned entities in the main world.
/// The link between synced entities is maintained by the [`RenderEntity`] and [`MainEntity`] components.
///
/// The [`RenderEntity`] contains the corresponding render world entity of a main world entity, while [`MainEntity`] contains
/// the corresponding main world entity of a render world entity.
/// For convenience, [`QueryData`](bevy_ecs::query::QueryData) implementations are provided for both components:
/// adding [`MainEntity`] to a query (without a `&`) will return the corresponding main world [`Entity`],
/// and adding [`RenderEntity`] will return the corresponding render world [`Entity`].
/// If you have access to the component itself, the underlying entities can be accessed by calling `.id()`.
///
/// Synchronization is necessary preparation for extraction ([`ExtractSchedule`](crate::ExtractSchedule)), which copies over component data from the main
/// to the render world for these entities.
///
/// ```text
/// |--------------------------------------------------------------------|
/// | | | Main world update |
/// | sync | extract |---------------------------------------------------|
/// | | | Render world update |
/// |--------------------------------------------------------------------|
/// ```
///
/// An example for synchronized main entities 1v1 and 18v1
///
/// ```text
/// |---------------------------Main World------------------------------|
/// | Entity | Component |
/// |-------------------------------------------------------------------|
/// | ID: 1v1 | PointLight | RenderEntity(ID: 3V1) | SyncToRenderWorld |
/// | ID: 18v1 | PointLight | RenderEntity(ID: 5V1) | SyncToRenderWorld |
/// |-------------------------------------------------------------------|
///
/// |----------Render World-----------|
/// | Entity | Component |
/// |---------------------------------|
/// | ID: 3v1 | MainEntity(ID: 1V1) |
/// | ID: 5v1 | MainEntity(ID: 18V1) |
/// |---------------------------------|
///
/// ```
///
/// Note that this effectively establishes a link between the main world entity and the render world entity.
/// Not every entity needs to be synchronized, however; only entities with the [`SyncToRenderWorld`] component are synced.
/// Adding [`SyncToRenderWorld`] to a main world component will establish such a link.
/// Once a synchronized main entity is despawned, its corresponding render entity will be automatically
/// despawned in the next `sync`.
///
/// The sync step does not copy any of component data between worlds, since its often not necessary to transfer over all
/// the components of a main world entity.
/// The render world probably cares about a `Position` component, but not a `Velocity` component.
/// The extraction happens in its own step, independently from, and after synchronization.
///
/// Moreover, [`SyncWorldPlugin`] only synchronizes *entities*. [`RenderAsset`](crate::render_asset::RenderAsset)s like meshes and textures are handled
/// differently.
///
/// [`PipelinedRenderingPlugin`]: crate::pipelined_rendering::PipelinedRenderingPlugin
/// [`ExtractComponentPlugin`]: crate::extract_component::ExtractComponentPlugin
/// [`SyncComponentPlugin`]: crate::sync_component::SyncComponentPlugin
#[derive(Default)]
pub struct SyncWorldPlugin;
impl Plugin for SyncWorldPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.init_resource::<PendingSyncEntity>();
app.add_observer(
|add: On<Add, SyncToRenderWorld>, mut pending: ResMut<PendingSyncEntity>| {
pending.push(EntityRecord::Added(add.entity));
},
);
app.add_observer(
|remove: On<Remove, SyncToRenderWorld>,
mut pending: ResMut<PendingSyncEntity>,
query: Query<&RenderEntity>| {
if let Ok(e) = query.get(remove.entity) {
pending.push(EntityRecord::Removed(*e));
};
},
);
}
}
/// Marker component that indicates that its entity needs to be synchronized to the render world.
///
/// This component is automatically added as a required component by [`ExtractComponentPlugin`] and [`SyncComponentPlugin`].
/// For more information see [`SyncWorldPlugin`].
///
/// NOTE: This component should persist throughout the entity's entire lifecycle.
/// If this component is removed from its entity, the entity will be despawned.
///
/// [`ExtractComponentPlugin`]: crate::extract_component::ExtractComponentPlugin
/// [`SyncComponentPlugin`]: crate::sync_component::SyncComponentPlugin
#[derive(Component, Copy, Clone, Debug, Default, Reflect)]
#[reflect(Component, Default, Clone)]
#[component(storage = "SparseSet")]
pub struct SyncToRenderWorld;
/// Component added on the main world entities that are synced to the Render World in order to keep track of the corresponding render world entity.
///
/// Can also be used as a newtype wrapper for render world entities.
#[derive(Component, Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, Reflect)]
#[component(clone_behavior = Ignore)]
#[reflect(Component, Clone)]
pub struct RenderEntity(Entity);
impl RenderEntity {
#[inline]
pub fn id(&self) -> Entity {
self.0
}
}
impl From<Entity> for RenderEntity {
fn from(entity: Entity) -> Self {
RenderEntity(entity)
}
}
impl ContainsEntity for RenderEntity {
fn entity(&self) -> Entity {
self.id()
}
}
// SAFETY: RenderEntity is a newtype around Entity that derives its comparison traits.
unsafe impl EntityEquivalent for RenderEntity {}
/// Component added on the render world entities to keep track of the corresponding main world entity.
///
/// Can also be used as a newtype wrapper for main world entities.
#[derive(Component, Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Reflect)]
#[reflect(Component, Clone)]
pub struct MainEntity(Entity);
impl MainEntity {
#[inline]
pub fn id(&self) -> Entity {
self.0
}
}
impl From<Entity> for MainEntity {
fn from(entity: Entity) -> Self {
MainEntity(entity)
}
}
impl ContainsEntity for MainEntity {
fn entity(&self) -> Entity {
self.id()
}
}
// SAFETY: RenderEntity is a newtype around Entity that derives its comparison traits.
unsafe impl EntityEquivalent for MainEntity {}
/// A [`HashMap`] pre-configured to use [`EntityHash`] hashing with a [`MainEntity`].
pub type MainEntityHashMap<V> = HashMap<MainEntity, V, EntityHash>;
/// A [`HashSet`] pre-configured to use [`EntityHash`] hashing with a [`MainEntity`]..
pub type MainEntityHashSet = HashSet<MainEntity, EntityHash>;
/// Marker component that indicates that its entity needs to be despawned at the end of the frame.
#[derive(Component, Copy, Clone, Debug, Default, Reflect)]
#[reflect(Component, Default, Clone)]
pub struct TemporaryRenderEntity;
/// A record enum to what entities with [`SyncToRenderWorld`] have been added or removed.
#[derive(Debug)]
pub(crate) enum EntityRecord {
/// When an entity is spawned on the main world, notify the render world so that it can spawn a corresponding
/// entity. This contains the main world entity.
Added(Entity),
/// When an entity is despawned on the main world, notify the render world so that the corresponding entity can be
/// despawned. This contains the render world entity.
Removed(RenderEntity),
/// When a component is removed from an entity, notify the render world so that the corresponding component can be
/// removed. This contains the main world entity.
ComponentRemoved(Entity),
}
// Entity Record in MainWorld pending to Sync
#[derive(Resource, Default, Deref, DerefMut)]
pub(crate) struct PendingSyncEntity {
records: Vec<EntityRecord>,
}
pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut World) {
main_world.resource_scope(|world, mut pending: Mut<PendingSyncEntity>| {
// TODO : batching record
for record in pending.drain(..) {
match record {
EntityRecord::Added(e) => {
if let Ok(mut main_entity) = world.get_entity_mut(e) {
match main_entity.entry::<RenderEntity>() {
bevy_ecs::world::ComponentEntry::Occupied(_) => {
panic!("Attempting to synchronize an entity that has already been synchronized!");
}
bevy_ecs::world::ComponentEntry::Vacant(entry) => {
let id = render_world.spawn(MainEntity(e)).id();
entry.insert(RenderEntity(id));
}
};
}
}
EntityRecord::Removed(render_entity) => {
if let Ok(ec) = render_world.get_entity_mut(render_entity.id()) {
ec.despawn();
};
}
EntityRecord::ComponentRemoved(main_entity) => {
let Some(mut render_entity) = world.get_mut::<RenderEntity>(main_entity) else {
continue;
};
if let Ok(render_world_entity) = render_world.get_entity_mut(render_entity.id()) {
// In order to handle components that extract to derived components, we clear the entity
// and let the extraction system re-add the components.
render_world_entity.despawn();
let id = render_world.spawn(MainEntity(main_entity)).id();
render_entity.0 = id;
}
},
}
}
});
}
pub(crate) fn despawn_temporary_render_entities(
world: &mut World,
state: &mut SystemState<Query<Entity, With<TemporaryRenderEntity>>>,
mut local: Local<Vec<Entity>>,
) {
let query = state.get(world).unwrap();
local.extend(query.iter());
// Ensure next frame allocation keeps order
local.sort_unstable_by_key(|e| e.index());
for e in local.drain(..).rev() {
world.despawn(e);
}
}
/// This module exists to keep the complex unsafe code out of the main module.
///
/// The implementations for both [`MainEntity`] and [`RenderEntity`] should stay in sync,
/// and are based off of the `&T` implementation in `bevy_ecs`.
mod render_entities_world_query_impls {
use super::{MainEntity, RenderEntity};
use bevy_ecs::{
archetype::Archetype,
change_detection::Tick,
component::{ComponentId, Components},
entity::Entity,
query::{
ArchetypeQueryData, FilteredAccess, IterQueryData, QueryData, ReadOnlyQueryData,
ReleaseStateQueryData, SingleEntityQueryData, WorldQuery,
},
storage::{Table, TableRow},
world::{World, unsafe_world_cell::UnsafeWorldCell},
};
// SAFETY: defers completely to `&RenderEntity` implementation,
// and then only modifies the output safely.
unsafe impl WorldQuery for RenderEntity {
type Fetch<'w> = <&'static RenderEntity as WorldQuery>::Fetch<'w>;
type State = <&'static RenderEntity as WorldQuery>::State;
fn shrink_fetch<'wlong: 'wshort, 'wshort>(
fetch: Self::Fetch<'wlong>,
) -> Self::Fetch<'wshort> {
fetch
}
#[inline]
unsafe fn init_fetch<'w, 's>(
world: UnsafeWorldCell<'w>,
component_id: &'s ComponentId,
last_run: Tick,
this_run: Tick,
) -> Self::Fetch<'w> {
// SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`.
unsafe {
<&RenderEntity as WorldQuery>::init_fetch(world, component_id, last_run, this_run)
}
}
const IS_DENSE: bool = <&'static RenderEntity as WorldQuery>::IS_DENSE;
#[inline]
unsafe fn set_archetype<'w, 's>(
fetch: &mut Self::Fetch<'w>,
component_id: &'s ComponentId,
archetype: &'w Archetype,
table: &'w Table,
) {
// SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`.
unsafe {
<&RenderEntity as WorldQuery>::set_archetype(fetch, component_id, archetype, table);
}
}
#[inline]
unsafe fn set_table<'w, 's>(
fetch: &mut Self::Fetch<'w>,
&component_id: &'s ComponentId,
table: &'w Table,
) {
// SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`.
unsafe { <&RenderEntity as WorldQuery>::set_table(fetch, &component_id, table) }
}
fn update_component_access(&component_id: &ComponentId, access: &mut FilteredAccess) {
<&RenderEntity as WorldQuery>::update_component_access(&component_id, access);
}
fn init_state(world: &mut World) -> ComponentId {
<&RenderEntity as WorldQuery>::init_state(world)
}
fn get_state(components: &Components) -> Option<Self::State> {
<&RenderEntity as WorldQuery>::get_state(components)
}
fn matches_component_set(
&state: &ComponentId,
set_contains_id: &impl Fn(ComponentId) -> bool,
) -> bool {
<&RenderEntity as WorldQuery>::matches_component_set(&state, set_contains_id)
}
}
// SAFETY: Component access of Self::ReadOnly is a subset of Self.
// Self::ReadOnly matches exactly the same archetypes/tables as Self.
unsafe impl QueryData for RenderEntity {
const IS_READ_ONLY: bool = true;
const IS_ARCHETYPAL: bool = <&MainEntity as QueryData>::IS_ARCHETYPAL;
type ReadOnly = RenderEntity;
type Item<'w, 's> = Entity;
fn shrink<'wlong: 'wshort, 'wshort, 's>(
item: Self::Item<'wlong, 's>,
) -> Self::Item<'wshort, 's> {
item
}
#[inline(always)]
unsafe fn fetch<'w, 's>(
state: &'s Self::State,
fetch: &mut Self::Fetch<'w>,
entity: Entity,
table_row: TableRow,
) -> Option<Self::Item<'w, 's>> {
// SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`.
let component =
unsafe { <&RenderEntity as QueryData>::fetch(state, fetch, entity, table_row) };
component.map(RenderEntity::id)
}
fn iter_access(
state: &Self::State,
) -> impl Iterator<Item = bevy_ecs::query::EcsAccessType<'_>> {
<&RenderEntity as QueryData>::iter_access(state)
}
}
// SAFETY: access is read only and only on the current entity
unsafe impl IterQueryData for RenderEntity {}
// SAFETY: the underlying `Entity` is copied, and no mutable access is provided.
unsafe impl ReadOnlyQueryData for RenderEntity {}
unsafe impl SingleEntityQueryData for RenderEntity {}
impl ArchetypeQueryData for RenderEntity {}
impl ReleaseStateQueryData for RenderEntity {
fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> {
item
}
}
// SAFETY: defers completely to `&RenderEntity` implementation,
// and then only modifies the output safely.
unsafe impl WorldQuery for MainEntity {
type Fetch<'w> = <&'static MainEntity as WorldQuery>::Fetch<'w>;
type State = <&'static MainEntity as WorldQuery>::State;
fn shrink_fetch<'wlong: 'wshort, 'wshort>(
fetch: Self::Fetch<'wlong>,
) -> Self::Fetch<'wshort> {
fetch
}
#[inline]
unsafe fn init_fetch<'w, 's>(
world: UnsafeWorldCell<'w>,
component_id: &'s ComponentId,
last_run: Tick,
this_run: Tick,
) -> Self::Fetch<'w> {
// SAFETY: defers to the `&T` implementation, with T set to `MainEntity`.
unsafe {
<&MainEntity as WorldQuery>::init_fetch(world, component_id, last_run, this_run)
}
}
const IS_DENSE: bool = <&'static MainEntity as WorldQuery>::IS_DENSE;
#[inline]
unsafe fn set_archetype<'w, 's>(
fetch: &mut Self::Fetch<'w>,
component_id: &ComponentId,
archetype: &'w Archetype,
table: &'w Table,
) {
// SAFETY: defers to the `&T` implementation, with T set to `MainEntity`.
unsafe {
<&MainEntity as WorldQuery>::set_archetype(fetch, component_id, archetype, table);
}
}
#[inline]
unsafe fn set_table<'w, 's>(
fetch: &mut Self::Fetch<'w>,
&component_id: &'s ComponentId,
table: &'w Table,
) {
// SAFETY: defers to the `&T` implementation, with T set to `MainEntity`.
unsafe { <&MainEntity as WorldQuery>::set_table(fetch, &component_id, table) }
}
fn update_component_access(&component_id: &ComponentId, access: &mut FilteredAccess) {
<&MainEntity as WorldQuery>::update_component_access(&component_id, access);
}
fn init_state(world: &mut World) -> ComponentId {
<&MainEntity as WorldQuery>::init_state(world)
}
fn get_state(components: &Components) -> Option<Self::State> {
<&MainEntity as WorldQuery>::get_state(components)
}
fn matches_component_set(
&state: &ComponentId,
set_contains_id: &impl Fn(ComponentId) -> bool,
) -> bool {
<&MainEntity as WorldQuery>::matches_component_set(&state, set_contains_id)
}
}
// SAFETY: Component access of Self::ReadOnly is a subset of Self.
// Self::ReadOnly matches exactly the same archetypes/tables as Self.
unsafe impl QueryData for MainEntity {
const IS_READ_ONLY: bool = true;
const IS_ARCHETYPAL: bool = <&MainEntity as QueryData>::IS_ARCHETYPAL;
type ReadOnly = MainEntity;
type Item<'w, 's> = Entity;
fn shrink<'wlong: 'wshort, 'wshort, 's>(
item: Self::Item<'wlong, 's>,
) -> Self::Item<'wshort, 's> {
item
}
#[inline(always)]
unsafe fn fetch<'w, 's>(
state: &'s Self::State,
fetch: &mut Self::Fetch<'w>,
entity: Entity,
table_row: TableRow,
) -> Option<Self::Item<'w, 's>> {
// SAFETY: defers to the `&T` implementation, with T set to `MainEntity`.
let component =
unsafe { <&MainEntity as QueryData>::fetch(state, fetch, entity, table_row) };
component.map(MainEntity::id)
}
fn iter_access(
state: &Self::State,
) -> impl Iterator<Item = bevy_ecs::query::EcsAccessType<'_>> {
<&MainEntity as QueryData>::iter_access(state)
}
}
// SAFETY: access is read only and only on the current entity
unsafe impl IterQueryData for MainEntity {}
// SAFETY: the underlying `Entity` is copied, and no mutable access is provided.
unsafe impl ReadOnlyQueryData for MainEntity {}
unsafe impl SingleEntityQueryData for MainEntity {}
impl ArchetypeQueryData for MainEntity {}
impl ReleaseStateQueryData for MainEntity {
fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> {
item
}
}
}
#[cfg(test)]
mod tests {
use bevy_ecs::{
component::Component,
entity::Entity,
lifecycle::{Add, Remove},
observer::On,
query::With,
system::{Query, ResMut},
world::World,
};
use super::{
EntityRecord, MainEntity, PendingSyncEntity, RenderEntity, SyncToRenderWorld,
entity_sync_system,
};
#[derive(Component)]
struct RenderDataComponent;
#[test]
fn sync_world() {
let mut main_world = World::new();
let mut render_world = World::new();
main_world.init_resource::<PendingSyncEntity>();
main_world.add_observer(
|add: On<Add, SyncToRenderWorld>, mut pending: ResMut<PendingSyncEntity>| {
pending.push(EntityRecord::Added(add.entity));
},
);
main_world.add_observer(
|remove: On<Remove, SyncToRenderWorld>,
mut pending: ResMut<PendingSyncEntity>,
query: Query<&RenderEntity>| {
if let Ok(e) = query.get(remove.entity) {
pending.push(EntityRecord::Removed(*e));
};
},
);
// spawn some empty entities for test
for _ in 0..99 {
main_world.spawn_empty();
}
// spawn
let main_entity = main_world
.spawn(RenderDataComponent)
// indicates that its entity needs to be synchronized to the render world
.insert(SyncToRenderWorld)
.id();
entity_sync_system(&mut main_world, &mut render_world);
let mut q = render_world.query_filtered::<Entity, With<MainEntity>>();
// Only one synchronized entity
assert!(q.iter(&render_world).count() == 1);
let render_entity = q.single(&render_world).unwrap();
let render_entity_component = main_world.get::<RenderEntity>(main_entity).unwrap();
assert!(render_entity_component.id() == render_entity);
let main_entity_component = render_world
.get::<MainEntity>(render_entity_component.id())
.unwrap();
assert!(main_entity_component.id() == main_entity);
// despawn
main_world.despawn(main_entity);
entity_sync_system(&mut main_world, &mut render_world);
// Only one synchronized entity
assert!(q.iter(&render_world).count() == 0);
}
}

View file

@ -0,0 +1,164 @@
use std::{
collections::HashSet,
ops::{Deref, DerefMut},
};
use bevy_ecs::{entity::EntityHashMap, prelude::*};
use bevy_window::{PrimaryWindow, RawHandleWrapper, Window, WindowClosing};
use renderer::{ash::vk, swapchain::WindowSurface};
use crate::{RenderDevice, extract_param::Extract};
pub struct ExtractedWindow {
pub entity: Entity,
pub handle: RawHandleWrapper,
pub physical_width: u32,
pub physical_height: u32,
pub size_changed: bool,
/// On wayland, windows will not show unless they are presented at least once.
pub needs_initial_present: bool,
}
#[derive(Default, Resource)]
pub struct ExtractedWindows {
pub primary: Option<Entity>,
pub windows: EntityHashMap<ExtractedWindow>,
}
impl Deref for ExtractedWindows {
type Target = EntityHashMap<ExtractedWindow>;
fn deref(&self) -> &Self::Target {
&self.windows
}
}
impl DerefMut for ExtractedWindows {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.windows
}
}
#[derive(Default, Resource)]
pub struct Surfaces {
surfaces: EntityHashMap<SurfaceData>,
configured: HashSet<Entity>,
}
impl Surfaces {
fn remove(&mut self, entity: &Entity) {
self.surfaces.remove(entity);
self.configured.remove(entity);
}
pub fn iter(&self) -> impl Iterator<Item = (&Entity, &SurfaceData)> {
self.surfaces.iter()
}
}
pub struct SurfaceData {
pub surface: WindowSurface,
}
impl Deref for SurfaceData {
type Target = WindowSurface;
fn deref(&self) -> &Self::Target {
&self.surface
}
}
#[allow(clippy::type_complexity)]
pub(crate) fn extract_windows(
mut extracted_windows: ResMut<ExtractedWindows>,
mut closing: Extract<MessageReader<WindowClosing>>,
windows: Extract<Query<(Entity, &RawHandleWrapper, &Window, Option<&PrimaryWindow>)>>,
mut removed: Extract<RemovedComponents<RawHandleWrapper>>,
mut surfaces: ResMut<Surfaces>,
) {
for (entity, handle, window, primary) in windows.iter() {
if primary.is_some() {
extracted_windows.primary = Some(entity);
}
let (new_width, new_height) = (
window.resolution.physical_width().max(1),
window.resolution.physical_height().max(1),
);
let extracted_window = extracted_windows.entry(entity).or_insert(ExtractedWindow {
entity,
handle: handle.clone(),
physical_width: new_width,
physical_height: new_height,
size_changed: false,
needs_initial_present: true,
});
if extracted_window.physical_width != new_width
|| extracted_window.physical_height != new_height
{
extracted_window.physical_width = new_width;
extracted_window.physical_height = new_height;
extracted_window.size_changed = true;
}
}
for closing in closing.read() {
extracted_windows.remove(&closing.window);
surfaces.remove(&closing.window);
}
for removed in removed.read() {
extracted_windows.remove(&removed);
surfaces.remove(&removed);
}
}
pub(crate) fn need_surface_configuration(
windows: Res<ExtractedWindows>,
surfaces: Res<Surfaces>,
) -> bool {
windows
.windows
.values()
.any(|window| !surfaces.configured.contains(&window.entity) || window.size_changed)
}
pub(crate) fn configure_surfaces(
windows: Res<ExtractedWindows>,
mut surfaces: ResMut<Surfaces>,
device: Res<RenderDevice>,
) {
for window in windows.windows.values() {
let surface = surfaces.surfaces.entry(window.entity).or_insert_with(|| {
let (display_handle, window_handle) = (
window.handle.get_display_handle(),
window.handle.get_window_handle(),
);
let surface = WindowSurface::new(
(**device).clone(),
vk::Extent2D {
width: window.physical_width,
height: window.physical_height,
},
window_handle,
display_handle,
)
.expect("Failed to create window surface");
SurfaceData { surface }
});
if window.size_changed {
surface
.surface
.recreate_with(Some(vk::Extent2D {
width: window.physical_width,
height: window.physical_height,
}))
.expect("Failed to recreate surface");
}
surfaces.configured.insert(window.entity);
}
}

View file

@ -32,6 +32,10 @@ futures = { workspace = true }
smol = { workspace = true } smol = { workspace = true }
# tokio = {workspace = true, features = ["rt", "sync"]} # tokio = {workspace = true, features = ["rt", "sync"]}
bevy_tasks = { workspace = true }
derive_more = { workspace = true , features = ["debug"] }
cfg-if = "1.0.0" cfg-if = "1.0.0"
dyn-clone = "1" dyn-clone = "1"
crossbeam = "0.8.4" crossbeam = "0.8.4"

View file

@ -1,13 +1,13 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::{BTreeMap, BTreeSet, HashMap}, collections::{BTreeMap, BTreeSet, HashMap, HashSet},
ffi::{CStr, CString}, ffi::{CStr, CString},
ops::Deref, ops::Deref,
sync::Arc, sync::Arc,
}; };
use ash::{ use ash::{
khr, ext, khr,
prelude::VkResult, prelude::VkResult,
vk::{self, Handle}, vk::{self, Handle},
}; };
@ -136,6 +136,24 @@ impl core::fmt::Debug for DeviceInner {
} }
} }
#[macro_export]
macro_rules! make_extention {
($module:path) => {{
use $module::{NAME as EXTENSION_NAME, SPEC_VERSION as EXTENSION_VERSION};
Extension {
name: EXTENSION_NAME.to_str().unwrap(),
version: EXTENSION_VERSION,
}
}};
($module:path as $version:expr) => {{
use $module::*;
Extension {
name: NAME.to_str().unwrap(),
version: $version,
}
}};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Extension<'a> { pub struct Extension<'a> {
pub name: &'a str, pub name: &'a str,
@ -159,8 +177,8 @@ pub struct DeviceDesc<'a> {
pub features: crate::PhysicalDeviceFeatures, pub features: crate::PhysicalDeviceFeatures,
} }
const VALIDATION_LAYER_NAME: &'static core::ffi::CStr = c"VK_LAYER_KHRONOS_validation"; const VALIDATION_LAYER_NAME: &core::ffi::CStr = c"VK_LAYER_KHRONOS_validation";
const DEBUG_LAYERS: [&'static core::ffi::CStr; 1] = [VALIDATION_LAYER_NAME]; const DEBUG_LAYERS: [&core::ffi::CStr; 1] = [VALIDATION_LAYER_NAME];
impl DeviceDesc<'_> { impl DeviceDesc<'_> {
fn debug_layer_settings() -> &'static [vk::LayerSettingEXT<'static>; 3] { fn debug_layer_settings() -> &'static [vk::LayerSettingEXT<'static>; 3] {
static SETTINGS: std::sync::LazyLock<[vk::LayerSettingEXT; 3]> = static SETTINGS: std::sync::LazyLock<[vk::LayerSettingEXT; 3]> =
@ -362,20 +380,21 @@ impl DeviceBuilder {
.unwrap(); .unwrap();
// find present queue first because it is rather more important than a secondary compute queue // find present queue first because it is rather more important than a secondary compute queue
let present = let present = if !queue_families.0.get(graphics as usize).unwrap().is_present {
if !queue_families.0.get(graphics as usize).unwrap().is_present { queue_families.find_first(|family| family.is_present)
queue_families.find_first(|family| family.is_present) } else {
None
}
.or({
if display_handle.is_none() {
// in this case the graphics queue will be used by default
tracing::info!("no display handle, using graphics queue family as fallback");
Some(graphics)
} else { } else {
tracing::warn!("no present queue available, this is unexpected!");
None None
}.or({ }
if display_handle.is_none() { });
// in this case the graphics queue will be used by default
tracing::info!("no present queue available, using graphics queue as fallback for headless_surface");
Some(graphics)
} else {
tracing::warn!("no present queue available, this is unexpected!");
None}
});
let async_compute = queue_families.find_first(|family| family.is_compute); let async_compute = queue_families.find_first(|family| family.is_compute);
let transfer = queue_families.find_first(|family| family.is_transfer); let transfer = queue_families.find_first(|family| family.is_transfer);
@ -386,13 +405,19 @@ impl DeviceBuilder {
use std::collections::btree_map::Entry; use std::collections::btree_map::Entry;
let index = match unique_families.entry(family) { let index = match unique_families.entry(family) {
Entry::Vacant(vacant_entry) => { Entry::Vacant(vacant_entry) => {
vacant_entry.insert(1); vacant_entry.insert(0);
0 0
} }
Entry::Occupied(mut occupied_entry) => { Entry::Occupied(mut occupied_entry) => {
let max = queue_families.0[family as usize].num_queues;
let idx = occupied_entry.get_mut(); let idx = occupied_entry.get_mut();
*idx += 1; if *idx + 1 >= max {
*idx - 1 tracing::warn!("ran out of queues in family {family}, reusing queue {idx}");
*idx
} else {
*idx += 1;
*idx
}
} }
}; };
@ -400,28 +425,34 @@ impl DeviceBuilder {
}; };
let graphics = helper(graphics); let graphics = helper(graphics);
let async_compute = async_compute.map(|f| helper(f)).unwrap_or(graphics); let async_compute = async_compute.map(&mut helper).unwrap_or(graphics);
let transfer = transfer.map(|f| helper(f)).unwrap_or(async_compute); let transfer = transfer.map(&mut helper).unwrap_or(async_compute);
let present = present.map(|f| helper(f)).unwrap_or(graphics); let present = present.map(&mut helper).unwrap_or(graphics);
tracing::debug!(
"selected queue families: graphics={:?}, async_compute={:?}, transfer={:?}, present={:?}",
graphics,
async_compute,
transfer,
present
);
let families = unique_families let families = unique_families
.into_iter() .into_iter()
.filter(|&(_family, count)| count > 0) .map(|(family, count)| (family, count + 1))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// family of each queue, of which one is allocated for each queue, with // family of each queue, of which one is allocated for each queue, with
// graphics being the fallback queue for compute and transfer, and // graphics being the fallback queue for compute and transfer, and
// present possibly being `None`, in which case it is Graphics // present possibly being `None`, in which case it is Graphics
let queues = DeviceQueueFamilies { DeviceQueueFamilies {
families, families,
graphics, graphics,
async_compute, async_compute,
transfer, transfer,
present, present,
properties: queue_familiy_properties.into_boxed_slice(), properties: queue_familiy_properties.into_boxed_slice(),
}; }
queues
} }
fn choose_physical_device( fn choose_physical_device(
@ -458,18 +489,16 @@ impl DeviceBuilder {
requirements.compatible_with(&query_features) requirements.compatible_with(&query_features)
}) })
.max_by_key(|(_, props)| { .max_by_key(|(_, props)| {
let score = match props.base.device_type { match props.base.device_type {
vk::PhysicalDeviceType::DISCRETE_GPU => 5, vk::PhysicalDeviceType::DISCRETE_GPU => 5,
vk::PhysicalDeviceType::INTEGRATED_GPU => 4, vk::PhysicalDeviceType::INTEGRATED_GPU => 4,
vk::PhysicalDeviceType::VIRTUAL_GPU => 3, vk::PhysicalDeviceType::VIRTUAL_GPU => 3,
vk::PhysicalDeviceType::CPU => 2, vk::PhysicalDeviceType::CPU => 2,
vk::PhysicalDeviceType::OTHER => 1, vk::PhysicalDeviceType::OTHER => 1,
_ => unreachable!(), _ => unreachable!(),
}; }
// score based on limits or other properties // TODO: score based on limits or other properties
score
}) })
.ok_or(Error::NoPhysicalDevice)?; .ok_or(Error::NoPhysicalDevice)?;
@ -499,13 +528,13 @@ impl DeviceBuilder {
} }
} }
/// returns a tuple of supported/enabled extensions and unsupported/requested extensions /// returns a tuple of supported-or-enabled extensions and unsupported-and-requested extensions
fn get_extensions<'a>( fn get_extensions<'a>(
entry: &ash::Entry, entry: &ash::Entry,
layers: &[&'a CStr], layers: &[&'a CStr],
extensions: impl Iterator<Item = Extension<'a>> + 'a, extensions: impl Iterator<Item = Extension<'a>> + 'a,
display_handle: Option<RawDisplayHandle>, display_handle: Option<RawDisplayHandle>,
) -> Result<(Vec<Extension<'a>>, Vec<Extension<'a>>)> { ) -> Result<(HashSet<Extension<'a>>, HashSet<Extension<'a>>)> {
unsafe { unsafe {
let available_extensions = Self::get_available_extensions(entry, layers)?; let available_extensions = Self::get_available_extensions(entry, layers)?;
@ -519,21 +548,70 @@ impl DeviceBuilder {
}) })
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
let mut out_extensions = Vec::new(); tracing::debug!(
let mut unsupported_extensions = Vec::new(); "Available extensions: {:?}",
available_extension_names.iter().collect::<Vec<_>>()
);
let mut out_extensions = HashSet::new();
let mut unsupported_extensions = HashSet::new();
let mut wsi_extensions = Vec::new();
wsi_extensions.push(make_extention!(khr::surface));
// taken from wgpu-hal/src/vulkan/instance.rs:
if cfg!(all(
unix,
not(target_os = "android"),
not(target_os = "macos")
)) {
wsi_extensions.push(make_extention!(khr::xlib_surface));
wsi_extensions.push(make_extention!(khr::xcb_surface));
wsi_extensions.push(make_extention!(khr::wayland_surface));
}
if cfg!(target_os = "windows") {
wsi_extensions.push(make_extention!(khr::win32_surface));
}
if cfg!(target_os = "android") {
wsi_extensions.push(make_extention!(khr::android_surface));
}
if cfg!(target_os = "macos") {
wsi_extensions.push(make_extention!(ext::metal_surface));
wsi_extensions.push(make_extention!(khr::portability_enumeration));
}
if cfg!(all(
unix,
not(target_vendor = "apple"),
not(target_family = "wasm")
)) {
wsi_extensions.push(make_extention!(ext::acquire_drm_display));
wsi_extensions.push(make_extention!(ext::direct_mode_display));
wsi_extensions.push(make_extention!(khr::display));
}
for extension in extensions { for extension in extensions {
if let Some(available_version) = available_extension_names.get(&extension.name) if let Some(available_version) = available_extension_names.get(&extension.name)
&& *available_version >= extension.version && *available_version >= extension.version
{ {
out_extensions.push(extension); out_extensions.insert(extension);
} else { } else {
unsupported_extensions.push(extension); unsupported_extensions.insert(extension);
} }
} }
for extension in wsi_extensions {
if let Some(available_version) = available_extension_names.get(&extension.name)
&& *available_version >= extension.version
{
out_extensions.insert(extension);
}
// don't warn about missing WSI extensions, these might not all
// be needed and weren't requested by the user.
}
// if a display handle is provided, ensure the required WSI extensions are present
let required_extension_names = display_handle let required_extension_names = display_handle
.map(|display_handle| ash_window::enumerate_required_extensions(display_handle)) .map(ash_window::enumerate_required_extensions)
.unwrap_or(Ok(&[]))?; .unwrap_or(Ok(&[]))?;
for &extension in required_extension_names { for &extension in required_extension_names {
@ -546,9 +624,9 @@ impl DeviceBuilder {
if let Some(available_version) = available_extension_names.get(&extension.name) if let Some(available_version) = available_extension_names.get(&extension.name)
&& *available_version >= extension.version && *available_version >= extension.version
{ {
out_extensions.push(extension); out_extensions.insert(extension);
} else { } else {
unsupported_extensions.push(extension); unsupported_extensions.insert(extension);
} }
} }
@ -572,6 +650,14 @@ impl DeviceBuilder {
.collect::<core::result::Result<BTreeSet<_>, _>>() .collect::<core::result::Result<BTreeSet<_>, _>>()
.map_err(|_| (Vec::<&'a CStr>::new(), wants_layers.clone()))?; .map_err(|_| (Vec::<&'a CStr>::new(), wants_layers.clone()))?;
tracing::debug!(
"Available layers: {:?}",
available_layer_names
.iter()
.map(|s| s.to_str().unwrap_or("<invalid utf8>"))
.collect::<Vec<_>>()
);
let mut out_layers = Vec::new(); let mut out_layers = Vec::new();
let mut unsupported_layers = Vec::new(); let mut unsupported_layers = Vec::new();
@ -666,33 +752,21 @@ impl Device {
.engine_version(vk::make_api_version(0, 0, 1, 0)); .engine_version(vk::make_api_version(0, 0, 1, 0));
let mut validation_info = let mut validation_info =
vk::LayerSettingsCreateInfoEXT::default().settings(&desc.layer_settings); vk::LayerSettingsCreateInfoEXT::default().settings(desc.layer_settings);
cfg_if::cfg_if! { let extra_instance_extensions = [
if #[cfg(debug_assertions)] { make_extention!(ext::debug_utils as 1),
let debug_layers = [Extension { #[cfg(debug_assertions)]
name: "VK_EXT_layer_settings", make_extention!(ext::layer_settings),
version: 2, ];
}];
} else {
let debug_layers = [];
}
};
let extra_instance_extensions = [Extension { let layers = DeviceBuilder::get_layers(&entry, desc.layers.iter().cloned()).unwrap();
name: "VK_EXT_debug_utils",
version: 1,
}]
.into_iter()
.chain(debug_layers.into_iter());
let layers = DeviceBuilder::get_layers(&entry, desc.layers.into_iter().cloned()).unwrap();
let (extensions, unsupported_extensions) = DeviceBuilder::get_extensions( let (extensions, unsupported_extensions) = DeviceBuilder::get_extensions(
&entry, &entry,
&layers, &layers,
desc.instance_extensions desc.instance_extensions
.into_iter() .iter()
.cloned() .cloned()
.chain(extra_instance_extensions), .chain(extra_instance_extensions),
desc.display_handle, desc.display_handle,
@ -808,11 +882,13 @@ impl Device {
.enabled_extension_names(&extensions) .enabled_extension_names(&extensions)
.push_next(&mut features2); .push_next(&mut features2);
tracing::debug!("creating device: {:#?}", device_info);
let device = unsafe { let device = unsafe {
let device = instance let device = instance
.instance .instance
.create_device(physical.pdev, &device_info, None)?; .create_device(physical.pdev, &device_info, None)?;
tracing::debug!("allocating queues: {queue_infos:#?}");
let allocated_queues = queue_infos let allocated_queues = queue_infos
.iter() .iter()
.flat_map(|info| { .flat_map(|info| {
@ -825,8 +901,17 @@ impl Device {
}) })
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
let get_queue = let get_queue = |(family, index)| {
|(family, index)| allocated_queues.get(&(family, index)).cloned().unwrap(); allocated_queues
.get(&(family, index))
.cloned()
.unwrap_or_else(|| {
panic!(
"
queue family {family} index {index} failed to allocate"
)
})
};
let main_queue = get_queue(physical.queue_families.graphics); let main_queue = get_queue(physical.queue_families.graphics);
let present_queue = get_queue(physical.queue_families.present); let present_queue = get_queue(physical.queue_families.present);
@ -1050,6 +1135,7 @@ pub trait DeviceOwned<T> {
fn handle(&self) -> T; fn handle(&self) -> T;
} }
/// Macro for helping create and destroy Vulkan objects which are owned by a device.
#[macro_export] #[macro_export]
macro_rules! define_device_owned_handle { macro_rules! define_device_owned_handle {
($(#[$attr:meta])* ($(#[$attr:meta])*
@ -1059,15 +1145,15 @@ macro_rules! define_device_owned_handle {
} $(=> |$this:ident| $dtor:stmt)?) => { } $(=> |$this:ident| $dtor:stmt)?) => {
$(#[$attr])* $(#[$attr])*
$ty_vis struct $ty { $ty_vis struct $ty {
inner: crate::device::DeviceOwnedDebugObject<$handle>, inner: $crate::device::DeviceOwnedDebugObject<$handle>,
$( $(
$(#[$field_attr])* $(#[$field_attr])*
$field_vis $field_name: $field_ty, $field_vis $field_name: $field_ty,
)* )*
} }
impl crate::device::DeviceOwned<$handle> for $ty { impl $crate::device::DeviceOwned<$handle> for $ty {
fn device(&self) -> &crate::device::Device { fn device(&self) -> &$crate::device::Device {
self.inner.dev() self.inner.dev()
} }
fn handle(&self) -> $handle { fn handle(&self) -> $handle {
@ -1076,14 +1162,15 @@ macro_rules! define_device_owned_handle {
} }
impl $ty { impl $ty {
#[allow(clippy::too_many_arguments, reason = "This function is generated by a macro")]
fn construct( fn construct(
device: crate::device::Device, device: $crate::device::Device,
handle: $handle, handle: $handle,
name: Option<::std::borrow::Cow<'static, str>>, name: Option<::std::borrow::Cow<'static, str>>,
$($field_name: $field_ty,)* $($field_name: $field_ty,)*
) -> ::ash::prelude::VkResult<Self> { ) -> ::ash::prelude::VkResult<Self> {
Ok(Self { Ok(Self {
inner: crate::device::DeviceOwnedDebugObject::new( inner: $crate::device::DeviceOwnedDebugObject::new(
device, device,
handle, handle,
name, name,

View file

@ -1,12 +1,13 @@
use std::{ use std::{
marker::PhantomData, marker::PhantomData,
sync::{ sync::{
atomic::{AtomicU32, AtomicU64},
Arc, Arc,
atomic::{AtomicU32, AtomicU64, Ordering},
}, },
}; };
use ash::{ use ash::{
khr,
prelude::VkResult, prelude::VkResult,
vk::{self, Handle}, vk::{self, Handle},
}; };
@ -14,46 +15,63 @@ use parking_lot::{RawMutex, RwLock};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
use crate::{ use crate::{
define_device_owned_handle, Instance, Result, define_device_owned_handle,
device::{Device, DeviceOwned}, device::{Device, DeviceOwned},
images, sync, images, sync,
util::RawMutexGuard, util::RawMutexGuard,
Instance, Result,
}; };
define_device_owned_handle! { use derive_more::Debug;
#[derive(Debug)] #[derive(Debug)]
pub Surface(vk::SurfaceKHR) { pub struct Surface {
} => |this| unsafe { raw: vk::SurfaceKHR,
this.device().instance().surface.destroy_surface(this.handle(), None); #[debug(skip)]
functor: khr::surface::Instance,
}
impl Drop for Surface {
fn drop(&mut self) {
unsafe {
self.functor.destroy_surface(self.raw, None);
}
} }
} }
impl Surface { impl Surface {
#[allow(dead_code)] #[allow(dead_code)]
pub fn headless(device: Device) -> Result<Self> { pub fn headless(instance: &Arc<Instance>) -> Result<Self> {
unsafe { let headless_instance =
let instance = device.instance(); ash::ext::headless_surface::Instance::new(&instance.entry, &instance.instance);
let headless_instance = let functor = khr::surface::Instance::new(&instance.entry, &instance.instance);
ash::ext::headless_surface::Instance::new(&instance.entry, &instance.instance);
let surface = headless_instance // SAFETY: the headless surface does not have any platform-specific requirements, and does not depend on any external handles, so it is safe to create without additional guarantees.
// (note): ash marks this function as unsafe, likely because of
// auto-generated bindings from vk.xml, but doesn't provide any safety
// bounds.
unsafe {
let raw = headless_instance
.create_headless_surface(&vk::HeadlessSurfaceCreateInfoEXT::default(), None)?; .create_headless_surface(&vk::HeadlessSurfaceCreateInfoEXT::default(), None)?;
Ok(Self::construct( Ok(Self { raw, functor })
device,
surface,
Some("headless-surface".into()),
)?)
} }
} }
pub fn create( /// # Safety
device: Device, ///
/// The caller must ensure that the provided display and window handles are
/// valid and remain valid for the lifetime of the surface. Namely, the
/// window handle must refer to a valid window that is associated with the
/// display handle, and both must not be destroyed while the surface is
/// still in use. Additionally, the caller must ensure that the instance
/// was created with the appropriate platform-specific surface extensions
/// enabled.
pub unsafe fn new_from_raw_window_handle(
instance: &Arc<Instance>,
display_handle: RawDisplayHandle, display_handle: RawDisplayHandle,
window_handle: raw_window_handle::RawWindowHandle, window_handle: RawWindowHandle,
) -> Result<Self> { ) -> Result<Self> {
let instance = device.instance(); let functor = khr::surface::Instance::new(&instance.entry, &instance.instance);
// SAFETY: the caller guarantees the validity of the display and window handles, and that they remain valid for the lifetime of the surface.
let surface = unsafe { let surface = unsafe {
ash_window::create_surface( ash_window::create_surface(
&instance.entry, &instance.entry,
@ -64,11 +82,10 @@ impl Surface {
)? )?
}; };
Ok(Self::construct( Ok(Self {
device, raw: surface,
surface, functor,
Some(format!("{:?}_surface", window_handle).into()), })
)?)
} }
} }
@ -126,7 +143,6 @@ impl core::fmt::Debug for Swapchain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Swapchain") f.debug_struct("Swapchain")
.field("inner", &self.inner) .field("inner", &self.inner)
.field("surface", &self.surface)
.field("present_mode", &self.present_mode) .field("present_mode", &self.present_mode)
.field("color_space", &self.color_space) .field("color_space", &self.color_space)
.field("format", &self.format) .field("format", &self.format)
@ -233,19 +249,14 @@ impl Swapchain {
image_count, image_count,
min_image_count, min_image_count,
extent, extent,
} = Self::get_swapchain_params_from_surface( } = Self::get_swapchain_params_from_surface(device.instance(), surface.raw, pdev, extent)?;
device.instance(),
surface.handle(),
pdev,
extent,
)?;
let (swapchain, images) = { let (swapchain, images) = {
let lock = old_swapchain.as_ref().map(|handle| handle.lock()); let lock = old_swapchain.as_ref().map(|handle| handle.lock());
Self::create_vkswapchainkhr( Self::create_vkswapchainkhr(
&device, &device,
surface.handle(), surface.raw,
&device.queue_families().swapchain_family_indices(), &device.queue_families().swapchain_family_indices(),
extent, extent,
lock.as_ref().map(|lock| **lock), lock.as_ref().map(|lock| **lock),
@ -283,7 +294,7 @@ impl Swapchain {
..Default::default() ..Default::default()
}); });
}) })
.map(|img| Arc::new(img)) .map(Arc::new)
}) })
.collect::<VkResult<Vec<_>>>()?; .collect::<VkResult<Vec<_>>>()?;
@ -374,7 +385,7 @@ impl Swapchain {
Some( Some(
format!( format!(
"swapchain-{}_{}", "swapchain-{}_{}",
surface.handle().as_raw(), surface.raw.as_raw(),
SWAPCHAIN_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) SWAPCHAIN_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
) )
.into(), .into(),
@ -410,7 +421,7 @@ impl Swapchain {
self.surface.clone(), self.surface.clone(),
self.device().phy(), self.device().phy(),
extent, extent,
Some(&self), Some(self),
) )
} }
@ -421,11 +432,9 @@ impl Swapchain {
) -> impl std::future::Future<Output = VkResult<(SwapchainFrame, bool)>> { ) -> impl std::future::Future<Output = VkResult<(SwapchainFrame, bool)>> {
let frame = self let frame = self
.current_frame .current_frame
.fetch_update( .try_update(Ordering::Release, Ordering::Relaxed, |i| {
std::sync::atomic::Ordering::Release, Some((i + 1) % self.max_in_flight_images())
std::sync::atomic::Ordering::Relaxed, })
|i| Some((i + 1) % self.max_in_flight_images()),
)
.unwrap() as usize; .unwrap() as usize;
tracing::trace!(frame, "acquiring image for frame {frame}"); tracing::trace!(frame, "acquiring image for frame {frame}");
@ -478,10 +487,7 @@ impl Swapchain {
let swpchain = self.lock(); let swpchain = self.lock();
let queue = self.device().present_queue().lock(); let queue = self.device().present_queue().lock();
let wait_semaphores = wait let wait_semaphores = wait.as_slice();
.as_ref()
.map(|sema| core::slice::from_ref(sema))
.unwrap_or_default();
// TODO: make this optional for devices with no support for present_wait/present_id // TODO: make this optional for devices with no support for present_wait/present_id
// let present_id = self // let present_id = self
@ -506,6 +512,7 @@ impl Swapchain {
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)]
fn create_vkswapchainkhr( fn create_vkswapchainkhr(
device: &Device, device: &Device,
surface: vk::SurfaceKHR, surface: vk::SurfaceKHR,
@ -638,7 +645,9 @@ impl WindowSurface {
window: RawWindowHandle, window: RawWindowHandle,
display: RawDisplayHandle, display: RawDisplayHandle,
) -> Result<Self> { ) -> Result<Self> {
let surface = Arc::new(Surface::create(device.clone(), display, window)?); let surface = Arc::new(unsafe {
Surface::new_from_raw_window_handle(device.instance(), display, window)?
});
let swapchain = RwLock::new(Arc::new(Swapchain::new( let swapchain = RwLock::new(Arc::new(Swapchain::new(
device.clone(), device.clone(),
surface.clone(), surface.clone(),
@ -690,6 +699,10 @@ impl WindowSurface {
Ok(frame) Ok(frame)
} }
pub fn acquire_image_blocking(&self) -> Result<SwapchainFrame> {
smol::block_on(self.acquire_image())
}
pub fn recreate_with(&self, extent: Option<vk::Extent2D>) -> Result<()> { pub fn recreate_with(&self, extent: Option<vk::Extent2D>) -> Result<()> {
let mut swapchain = self.current_swapchain.write(); let mut swapchain = self.current_swapchain.write();
*swapchain = Arc::new(swapchain.recreate(extent)?); *swapchain = Arc::new(swapchain.recreate(extent)?);
@ -719,7 +732,7 @@ mod tests {
], ],
)?; )?;
let surface = Arc::new(Surface::headless(device.clone())?); let surface = Arc::new(Surface::headless(device.instance())?);
let swapchain = Arc::new(Swapchain::new( let swapchain = Arc::new(Swapchain::new(
device.clone(), device.clone(),

View file

@ -0,0 +1,42 @@
use bevy::a11y::AccessibilityPlugin;
use bevy::app::TerminalCtrlCHandlerPlugin;
use bevy::camera::CameraPlugin;
use bevy::diagnostic::DiagnosticsPlugin;
use bevy::input::InputPlugin;
use bevy::log::LogPlugin;
use bevy::mesh::MeshPlugin;
use bevy::prelude::*;
use bevy::state::app::StatesPlugin;
use bevy::text::TextPlugin;
use bevy::window::WindowPlugin;
use bevy::winit::WinitPlugin;
use bevy_vulkan_render::{RenderApp, RenderPlugin as VulkanRenderPlugin};
fn main() {
eprintln!("Hello, world!");
let mut app = App::new();
app.add_plugins((
MinimalPlugins,
LogPlugin::default(),
TransformPlugin,
DiagnosticsPlugin,
AssetPlugin::default(),
ImagePlugin::default_nearest(),
WindowPlugin::default(),
AccessibilityPlugin,
TerminalCtrlCHandlerPlugin,
WinitPlugin::default(),
InputPlugin,
(
MeshPlugin,
CameraPlugin,
TextPlugin,
StatesPlugin,
VulkanRenderPlugin,
),
));
app.run();
}

3
src/lib.rs Normal file
View file

@ -0,0 +1,3 @@
//! Empty crate
pub use bevy_vulkan_render as bevy_render;