use crate::ecs::world::CORE;
use crate::ecs::world::World;
use crate::render_driver::render_entity;
pub type SystemFn = fn(&mut World);
#[derive(Clone)]
pub struct FrameScheduleEntry {
pub name: &'static str,
pub system: SystemFn,
}
#[derive(Clone, Default)]
pub struct FrameSchedule {
pub entries: Vec<FrameScheduleEntry>,
}
#[derive(Clone, Default)]
pub struct Schedules {
pub frame: FrameSchedule,
pub retained_ui: FrameSchedule,
}
pub fn schedule_push(schedule: &mut FrameSchedule, name: &'static str, system: SystemFn) {
schedule.entries.push(FrameScheduleEntry { name, system });
}
pub fn schedule_insert_before(
schedule: &mut FrameSchedule,
target: &str,
name: &'static str,
system: SystemFn,
) {
let position = schedule
.entries
.iter()
.position(|entry| entry.name == target)
.unwrap_or_else(|| panic!("schedule_insert_before: no system named \"{target}\""));
schedule
.entries
.insert(position, FrameScheduleEntry { name, system });
}
pub fn schedule_insert_after(
schedule: &mut FrameSchedule,
target: &str,
name: &'static str,
system: SystemFn,
) {
let position = schedule
.entries
.iter()
.position(|entry| entry.name == target)
.unwrap_or_else(|| panic!("schedule_insert_after: no system named \"{target}\""))
+ 1;
schedule
.entries
.insert(position, FrameScheduleEntry { name, system });
}
pub fn schedule_remove(schedule: &mut FrameSchedule, name: &str) {
schedule.entries.retain(|entry| entry.name != name);
}
pub fn schedule_contains(schedule: &FrameSchedule, name: &str) -> bool {
schedule.entries.iter().any(|entry| entry.name == name)
}
pub fn schedule_run(schedule: &FrameSchedule, world: &mut World) {
let _span = tracing::info_span!("systems").entered();
for entry in &schedule.entries {
(entry.system)(world);
}
}
fn material_table_entry(
registry: &crate::ecs::material::resources::MaterialRegistry,
index: usize,
) -> crate::render::config::RenderMaterialEntry {
match registry.registry.entries[index].as_ref() {
Some(material) => crate::render::config::RenderMaterialEntry {
material: material.clone(),
texture_ids: registry.texture_ids.get(index).copied().unwrap_or_default(),
},
None => crate::render::config::RenderMaterialEntry::default(),
}
}
pub fn render_sync_materials_system(world: &mut World) {
let registry_version = world.resources.assets.material_registry.version;
let registry_names_version = world.resources.assets.material_registry.names_version;
let (rebuild_entity_maps, first_run) = {
let sync = &mut world.resources.render_scene_sync;
let entities_dirty = std::mem::take(&mut sync.materials_dirty);
let version_dirty = sync.materials_version_seen != registry_version;
let names_dirty = sync.materials_names_version_seen != registry_names_version;
let first_run = !sync.materials_initialized;
if !entities_dirty && !version_dirty && !first_run {
return;
}
sync.materials_initialized = true;
sync.materials_version_seen = registry_version;
sync.materials_names_version_seen = registry_names_version;
(entities_dirty || names_dirty || first_run, first_run)
};
let _span = tracing::info_span!("render_sync_materials").entered();
use crate::render::material::AlphaMode;
let mut dirty_indices =
std::mem::take(&mut world.resources.assets.material_registry.dirty_indices);
dirty_indices.sort_unstable();
dirty_indices.dedup();
{
let resources = &mut world.resources;
let registry = &resources.assets.material_registry;
let table = &mut resources.renderer_state.render_materials;
let entry_count = registry.registry.entries.len();
if first_run || table.entries.len() > entry_count {
table.entries.clear();
table.entries.reserve(entry_count);
for index in 0..entry_count {
table.entries.push(material_table_entry(registry, index));
}
} else {
if table.entries.len() < entry_count {
table.entries.resize_with(
entry_count,
crate::render::config::RenderMaterialEntry::default,
);
}
for &index in &dirty_indices {
let index = index as usize;
if index < entry_count {
table.entries[index] = material_table_entry(registry, index);
}
}
}
table.name_to_id.clear();
for (index, name_slot) in registry.registry.index_to_name.iter().enumerate() {
if let Some(name) = name_slot
&& registry.registry.entries[index].is_some()
{
table.name_to_id.insert(name.clone(), index as u32 + 1);
}
}
table.transparent_ids.clear();
table.mask_ids.clear();
table.double_sided_ids.clear();
for (index, entry) in table.entries.iter().enumerate() {
let material_id = index as u32 + 1;
let material = &entry.material;
if material.alpha_mode == AlphaMode::Blend || material.transmission_factor > 0.0 {
table.transparent_ids.insert(material_id);
}
if matches!(material.alpha_mode, AlphaMode::Mask | AlphaMode::Dither) {
table.mask_ids.insert(material_id);
}
if material.double_sided {
table.double_sided_ids.insert(material_id);
}
}
}
{
let table = &mut world.resources.renderer_state.render_materials;
table.generation = table.generation.wrapping_add(1);
}
if !rebuild_entity_maps {
return;
}
let mesh_entities: Vec<crate::ecs::world::Entity> = world.ecs.worlds[CORE]
.query_entities(crate::ecs::world::RENDER_MESH)
.filter(|entity| {
!world.ecs.worlds[CORE].entity_has_components(*entity, crate::ecs::world::SKIN)
})
.collect();
let skinned_entities: Vec<crate::ecs::world::Entity> = world.ecs.worlds[CORE]
.query_entities(crate::ecs::world::SKIN | crate::ecs::world::RENDER_MESH)
.collect();
let instanced_entities: Vec<crate::ecs::world::Entity> = world.ecs.worlds[CORE]
.query_entities(crate::ecs::world::INSTANCED_MESH)
.collect();
let registry = &world.resources.assets.material_registry;
let mut entity_to_id: std::collections::HashMap<crate::ecs::world::Entity, u32> =
std::collections::HashMap::new();
let mut entity_to_name: std::collections::HashMap<crate::ecs::world::Entity, String> =
std::collections::HashMap::new();
for &entity in mesh_entities
.iter()
.chain(skinned_entities.iter())
.chain(instanced_entities.iter())
{
use crate::render::generational_registry::{registry_entry, registry_lookup_index};
let Some(material_ref) = world.get::<crate::ecs::material::components::MaterialRef>(entity)
else {
continue;
};
entity_to_name.insert(entity, material_ref.name.clone());
let id = material_ref
.id
.and_then(|id| {
registry_entry(®istry.registry, id.index, id.generation).map(|_| id.index + 1)
})
.or_else(|| {
registry_lookup_index(®istry.registry, &material_ref.name).and_then(
|(index, _generation)| {
registry.registry.entries[index as usize]
.as_ref()
.map(|_| index + 1)
},
)
})
.unwrap_or(0);
entity_to_id.insert(entity, id);
}
let table = &mut world.resources.renderer_state.render_materials;
table.entity_to_id = entity_to_id
.into_iter()
.map(|(entity, id)| (render_entity(entity), id))
.collect();
table.entity_to_name = entity_to_name
.into_iter()
.map(|(entity, name)| (render_entity(entity), name))
.collect();
}
pub mod system_names {
pub const INITIALIZE_AUDIO: &str = "initialize_audio";
pub const BUILD_AUDIO_BUSES: &str = "build_audio_buses";
pub const UPDATE_AUDIO: &str = "update_audio";
#[cfg(target_arch = "wasm32")]
pub const SYNC_WASM_CANVAS_SIZE: &str = "sync_wasm_canvas_size";
pub const ENSURE_BOUNDING_VOLUMES: &str = "ensure_bounding_volumes";
pub const SWAP_EVENTS: &str = "swap_events";
pub const RUN_PHYSICS: &str = "run_physics";
pub const EMIT_COLLISION_EVENTS: &str = "emit_collision_events";
pub const UPDATE_ANIMATION_PLAYERS: &str = "update_animation_players";
pub const APPLY_ANIMATIONS: &str = "apply_animations";
pub const UPDATE_TWEENS: &str = "update_tweens";
pub const TRANSFORM_SYSTEMS: &str = "transform_systems";
pub const UPDATE_INSTANCED_MESH_CACHES: &str = "update_instanced_mesh_caches";
pub const RENDER_SYNC: &str = "render_sync";
pub const RENDER_SYNC_MATERIALS: &str = "render_sync_materials";
pub const RENDER_SYNC_ANIMATION: &str = "render_sync_animation";
pub const RENDER_SYNC_SKINNING: &str = "render_sync_skinning";
pub const RETAINED_UI: &str = "retained_ui";
#[cfg(feature = "gizmos")]
pub const GIZMO_OVERLAY: &str = "gizmo_overlay";
#[cfg(feature = "gizmos")]
pub const NAV_GIZMO_OVERLAY: &str = "nav_gizmo_overlay";
pub const APPLY_IME_ALLOWED: &str = "apply_ime_allowed";
pub const RESET_MOUSE: &str = "reset_mouse";
pub const RESET_KEYBOARD: &str = "reset_keyboard";
pub const RESET_TOUCH: &str = "reset_touch";
pub const PROCESS_COMMANDS: &str = "process_commands";
pub const CLEANUP_UNUSED_RESOURCES: &str = "cleanup_unused_resources";
pub const RUN_NAVMESH: &str = "run_navmesh";
pub const POLL_FILE_WATCHER: &str = "poll_file_watcher";
pub const POLL_ASSET_WATCHER: &str = "poll_asset_watcher";
pub const UPDATE_PARTICLE_EMITTERS: &str = "update_particle_emitters";
pub const SYNC_CLOTH_MESHES: &str = "sync_cloth_meshes";
pub const UPDATE_VFX: &str = "update_vfx";
}
pub mod retained_ui_system_names {
pub const INPUT_SYNC: &str = "ui_retained_input_sync";
pub const GAMEPAD_NAVIGATION: &str = "ui_gamepad_navigation";
pub const LAYOUT_PICKING: &str = "ui_layout_picking";
pub const WIDGET_INTERACTION: &str = "ui_widget_interaction";
pub const EVENT_BUBBLE: &str = "ui_event_bubble";
pub const EMIT_CLICK_EVENTS: &str = "ui_emit_click_events";
pub const LAYOUT_STATE_UPDATE: &str = "ui_layout_state_update";
pub const DOCKED_PANEL_LAYOUT: &str = "ui_docked_panel_layout";
pub const RESPONSIVE_APPLY: &str = "ui_responsive_apply";
pub const LAYOUT_COMPUTE: &str = "ui_layout_compute";
pub const THEME_TRANSITION_TICK: &str = "ui_theme_transition_tick";
pub const THEME_APPLY: &str = "ui_theme_apply";
pub const LAYOUT_COLOR_BLEND: &str = "ui_layout_color_blend";
pub const LAYOUT_TRANSFORM_BLEND: &str = "ui_layout_transform_blend";
pub const LAYOUT_RENDER_SYNC: &str = "ui_layout_render_sync";
}
pub fn build_default_frame_schedule() -> FrameSchedule {
let entries: &[(&str, SystemFn)] = &[
(
system_names::SWAP_EVENTS,
crate::ecs::event::swap_events_system,
),
#[cfg(feature = "audio")]
(
system_names::INITIALIZE_AUDIO,
crate::ecs::audio::systems::initialize_audio_system,
),
#[cfg(feature = "audio")]
(
system_names::BUILD_AUDIO_BUSES,
crate::ecs::audio::systems::build_audio_buses_system,
),
#[cfg(feature = "audio")]
(
system_names::UPDATE_AUDIO,
crate::ecs::audio::systems::update_audio_system,
),
#[cfg(target_arch = "wasm32")]
(
system_names::SYNC_WASM_CANVAS_SIZE,
crate::ecs::camera::systems::sync_wasm_canvas_size,
),
(
system_names::ENSURE_BOUNDING_VOLUMES,
crate::ecs::bounding_volume::systems::ensure_bounding_volumes,
),
#[cfg(feature = "physics")]
(
system_names::RUN_PHYSICS,
crate::ecs::physics::systems::run_physics_systems,
),
#[cfg(feature = "physics")]
(
system_names::EMIT_COLLISION_EVENTS,
crate::ecs::event::emit_collision_events_system,
),
(
system_names::UPDATE_ANIMATION_PLAYERS,
crate::ecs::animation::systems::update_animation_players,
),
(
system_names::APPLY_ANIMATIONS,
crate::ecs::animation::systems::apply_animations,
),
(
system_names::UPDATE_TWEENS,
crate::ecs::tween::update_tweens_system,
),
#[cfg(feature = "navmesh")]
(
system_names::RUN_NAVMESH,
crate::ecs::navmesh::systems::run_navmesh_systems,
),
(
system_names::UPDATE_PARTICLE_EMITTERS,
crate::ecs::particles::systems::update_particle_emitters,
),
(
system_names::SYNC_CLOTH_MESHES,
crate::ecs::cloth::systems::sync_cloth_meshes_system,
),
(
system_names::UPDATE_VFX,
crate::ecs::vfx::systems::update_vfx_system,
),
(
system_names::TRANSFORM_SYSTEMS,
crate::ecs::transform::systems::run_systems,
),
(
system_names::UPDATE_INSTANCED_MESH_CACHES,
crate::ecs::mesh::systems::update_instanced_mesh_caches_system,
),
(
system_names::RENDER_SYNC,
crate::ecs::graphics::scene_sync::render_sync_system,
),
(
system_names::RENDER_SYNC_MATERIALS,
render_sync_materials_system,
),
(
system_names::RENDER_SYNC_ANIMATION,
crate::ecs::graphics::animation_sync::render_sync_animation_system,
),
(
system_names::RENDER_SYNC_SKINNING,
crate::ecs::skin::systems::render_sync_skinning_system,
),
(system_names::RETAINED_UI, run_retained_ui_schedule),
#[cfg(feature = "gizmos")]
(
system_names::GIZMO_OVERLAY,
crate::ecs::gizmos::gizmo_overlay_system,
),
#[cfg(feature = "gizmos")]
(
system_names::NAV_GIZMO_OVERLAY,
crate::ecs::gizmos::nav_gizmo_overlay_system,
),
(
system_names::APPLY_IME_ALLOWED,
crate::ecs::input::systems::apply_ime_allowed_system,
),
(
system_names::RESET_MOUSE,
crate::ecs::input::systems::reset_mouse_system,
),
(
system_names::RESET_KEYBOARD,
crate::ecs::input::systems::reset_keyboard_system,
),
(
system_names::RESET_TOUCH,
crate::ecs::input::systems::reset_touch_system,
),
#[cfg(all(feature = "file_watcher", not(target_arch = "wasm32")))]
(
system_names::POLL_FILE_WATCHER,
crate::ecs::file_watcher::poll_file_watcher_system,
),
#[cfg(all(
feature = "assets",
feature = "file_watcher",
not(target_arch = "wasm32")
))]
(
system_names::POLL_ASSET_WATCHER,
crate::ecs::asset_watcher::poll_asset_watcher_system,
),
(
system_names::PROCESS_COMMANDS,
crate::ecs::world::process_commands_system,
),
(
system_names::CLEANUP_UNUSED_RESOURCES,
crate::ecs::world::cleanup_unused_resources_system,
),
];
let mut schedule = FrameSchedule::default();
for &(name, system) in entries {
schedule_push(&mut schedule, name, system);
}
schedule
}
pub fn build_default_retained_ui_schedule() -> FrameSchedule {
let entries: &[(&str, SystemFn)] = &[
(
retained_ui_system_names::INPUT_SYNC,
crate::ecs::ui::widget_systems::ui_retained_input_sync_system,
),
(
retained_ui_system_names::LAYOUT_PICKING,
crate::ecs::ui::picking::ui_layout_picking_system,
),
#[cfg(feature = "gamepad")]
(
retained_ui_system_names::GAMEPAD_NAVIGATION,
crate::ecs::ui::gamepad_navigation::ui_gamepad_navigation_system,
),
(
retained_ui_system_names::WIDGET_INTERACTION,
crate::ecs::ui::widget_systems::ui_widget_interaction_system,
),
(
retained_ui_system_names::EVENT_BUBBLE,
crate::ecs::ui::widget_systems::ui_event_bubble_system,
),
(
retained_ui_system_names::EMIT_CLICK_EVENTS,
crate::ecs::ui::widget_systems::ui_emit_click_events_system,
),
(
retained_ui_system_names::LAYOUT_STATE_UPDATE,
crate::ecs::ui::systems::ui_layout_state_update_system,
),
(
retained_ui_system_names::DOCKED_PANEL_LAYOUT,
crate::ecs::ui::systems::ui_docked_panel_layout_system,
),
(
retained_ui_system_names::RESPONSIVE_APPLY,
crate::ecs::ui::systems::ui_responsive_apply_system,
),
(
retained_ui_system_names::LAYOUT_COMPUTE,
crate::ecs::ui::systems::ui_layout_compute_system,
),
(
retained_ui_system_names::THEME_TRANSITION_TICK,
crate::ecs::ui::systems::ui_theme_transition_tick_system,
),
(
retained_ui_system_names::THEME_APPLY,
crate::ecs::ui::systems::ui_theme_apply_system,
),
(
retained_ui_system_names::LAYOUT_COLOR_BLEND,
crate::ecs::ui::systems::ui_layout_color_blend_system,
),
(
retained_ui_system_names::LAYOUT_TRANSFORM_BLEND,
crate::ecs::ui::systems::ui_layout_transform_blend_system,
),
(
retained_ui_system_names::LAYOUT_RENDER_SYNC,
crate::ecs::ui::render_sync::ui_layout_render_sync_system,
),
];
let mut schedule = FrameSchedule::default();
for &(name, system) in entries {
schedule_push(&mut schedule, name, system);
}
schedule
}
pub fn run_retained_ui_schedule(world: &mut World) {
let schedule = std::mem::take(&mut world.resources.schedules.retained_ui);
schedule_run(&schedule, world);
world.resources.schedules.retained_ui = schedule;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_schedule_contains_one_retained_ui_entry() {
let schedule = build_default_frame_schedule();
let count = schedule
.entries
.iter()
.filter(|entry| entry.name == system_names::RETAINED_UI)
.count();
assert_eq!(
count, 1,
"frame schedule must collapse retained UI into a single entry"
);
}
#[test]
fn retained_ui_schedule_orders_pipeline_correctly() {
let schedule = build_default_retained_ui_schedule();
let names: Vec<&str> = schedule.entries.iter().map(|entry| entry.name).collect();
let expected_prefix = [
retained_ui_system_names::INPUT_SYNC,
retained_ui_system_names::LAYOUT_PICKING,
#[cfg(feature = "gamepad")]
retained_ui_system_names::GAMEPAD_NAVIGATION,
retained_ui_system_names::WIDGET_INTERACTION,
retained_ui_system_names::EVENT_BUBBLE,
retained_ui_system_names::EMIT_CLICK_EVENTS,
retained_ui_system_names::LAYOUT_STATE_UPDATE,
retained_ui_system_names::DOCKED_PANEL_LAYOUT,
retained_ui_system_names::RESPONSIVE_APPLY,
retained_ui_system_names::LAYOUT_COMPUTE,
retained_ui_system_names::THEME_TRANSITION_TICK,
retained_ui_system_names::THEME_APPLY,
retained_ui_system_names::LAYOUT_COLOR_BLEND,
retained_ui_system_names::LAYOUT_TRANSFORM_BLEND,
retained_ui_system_names::LAYOUT_RENDER_SYNC,
];
assert_eq!(names, expected_prefix);
}
}