Centralize world and viewport game loops
Some checks failed
CI / rust-skia (Rust only) (push) Has been cancelled
CI / required (push) Has been cancelled

This commit is contained in:
2026-08-23 13:42:01 +02:00
parent cb9233c59c
commit 33d1cf6827
18 changed files with 2541 additions and 287 deletions

View File

@@ -45,6 +45,19 @@ enum RenderCommand {
limits: RenderLimits,
reply: mpsc::SyncSender<Result<RenderedFrame, RenderError>>,
},
LoadScene {
camera: Camera,
renderables: std::sync::Arc<[Renderable]>,
textures: std::sync::Arc<[Texture]>,
limits: RenderLimits,
reply: mpsc::SyncSender<Result<RenderTimings, RenderError>>,
},
View {
camera: Camera,
background_srgb: [u8; 4],
limits: RenderLimits,
reply: mpsc::SyncSender<Result<RenderedFrame, RenderError>>,
},
Stop,
}
@@ -102,6 +115,32 @@ impl Renderer {
limits,
));
}
RenderCommand::LoadScene {
camera,
renderables,
textures,
limits,
reply,
} => {
let _ = reply.send(backend.load_scene(
camera,
&renderables,
&textures,
limits,
));
}
RenderCommand::View {
camera,
background_srgb,
limits,
reply,
} => {
let _ = reply.send(backend.render_view(
camera,
background_srgb,
limits,
));
}
RenderCommand::Stop => break,
}
}
@@ -170,6 +209,83 @@ impl Renderer {
frame.timings.command_copy = command_copy;
Ok(frame)
}
pub fn load_scene(
&self,
camera: Camera,
renderables: &[Renderable],
textures: &[Texture],
limits: RenderLimits,
) -> Result<RenderTimings, RenderError> {
self.load_scene_shared(
camera,
renderables.to_vec().into(),
textures.to_vec().into(),
limits,
)
}
pub fn load_scene_shared(
&self,
camera: Camera,
renderables: std::sync::Arc<[Renderable]>,
textures: std::sync::Arc<[Texture]>,
limits: RenderLimits,
) -> Result<RenderTimings, RenderError> {
let started = Instant::now();
validate_scene(camera, &renderables, &textures, limits)?;
let validation = started.elapsed();
let started = Instant::now();
let (sender, receiver) = mpsc::sync_channel(1);
let command = RenderCommand::LoadScene {
camera,
renderables,
textures,
limits,
reply: sender,
};
let command_copy = started.elapsed();
self.send(command)?;
let mut timings = receiver
.recv_timeout(Duration::from_mins(2))
.map_err(|_| RenderError::TimedOut)??;
timings.validation = validation;
timings.command_copy = command_copy;
Ok(timings)
}
pub fn render_view(
&self,
camera: Camera,
background_srgb: [u8; 4],
limits: RenderLimits,
) -> Result<RenderedFrame, RenderError> {
camera.basis()?;
if limits.width == 0 || limits.height == 0 || limits.far_distance == 0 {
return Err(RenderError::InvalidScene);
}
let (sender, receiver) = mpsc::sync_channel(1);
self.send(RenderCommand::View {
camera,
background_srgb,
limits,
reply: sender,
})?;
receiver
.recv_timeout(Duration::from_mins(2))
.map_err(|_| RenderError::TimedOut)?
}
fn send(&self, command: RenderCommand) -> Result<(), RenderError> {
self.commands
.try_send(command)
.map_err(|error| match error {
mpsc::TrySendError::Full(_) => RenderError::TimedOut,
mpsc::TrySendError::Disconnected(_) => {
RenderError::Device("renderer thread stopped".into())
}
})
}
}
impl Drop for Renderer {
@@ -189,6 +305,9 @@ impl Drop for Renderer {
#[derive(Component)]
struct FrameEntity;
#[derive(Component)]
struct PersistentSceneEntity;
struct LegacyMaterialPlugin;
struct PbrMaterialPlugin;
@@ -328,13 +447,14 @@ struct CachedGpuMesh {
struct BevyBackend {
apps: SubApps,
frame_entities: Vec<Entity>,
scene_entities: Vec<Entity>,
texture_handles: Vec<GpuTexture>,
texture_cache: HashMap<u64, Vec<CachedGpuTexture>>,
mesh_cache: HashMap<u64, Vec<CachedGpuMesh>>,
pbr_handles: Vec<Handle<PbrBevyMaterial>>,
legacy_handles: Vec<Handle<LegacyBevyMaterial>>,
environment_map: EnvironmentMapLight,
target: Option<Handle<Image>>,
target: Option<(u32, u32, Handle<Image>)>,
}
impl BevyBackend {
@@ -381,6 +501,7 @@ impl BevyBackend {
Ok(Self {
apps: std::mem::take(app.sub_apps_mut()),
frame_entities: Vec::new(),
scene_entities: Vec::new(),
texture_handles: Vec::new(),
texture_cache: HashMap::new(),
mesh_cache: HashMap::new(),
@@ -400,18 +521,28 @@ impl BevyBackend {
background_srgb: [u8; 4],
limits: RenderLimits,
) -> Result<RenderedFrame, RenderError> {
let mut timings = self.load_scene(camera, renderables, textures, limits)?;
let frame = self.render_view(camera, background_srgb, limits)?;
timings.clear_previous_frame += frame.timings.clear_previous_frame;
timings.scene_setup += frame.timings.scene_setup;
timings.capture += frame.timings.capture;
Ok(RenderedFrame {
rgba: frame.rgba,
timings,
})
}
#[allow(clippy::too_many_lines)]
fn load_scene(
&mut self,
camera: Camera,
renderables: &[Renderable],
textures: &[Texture],
_limits: RenderLimits,
) -> Result<RenderTimings, RenderError> {
let started = Instant::now();
self.clear_frame();
self.clear_scene();
let clear_previous_frame = started.elapsed();
self.apps
.main
.world_mut()
.insert_resource(ClearColor(Color::srgba_u8(
background_srgb[0],
background_srgb[1],
background_srgb[2],
background_srgb[3],
)));
let started = Instant::now();
self.texture_handles = add_textures(
self.apps.main.world_mut(),
@@ -443,7 +574,7 @@ impl BevyBackend {
.spawn((
Mesh3d(mesh.clone()),
MeshMaterial3d(material.clone()),
FrameEntity,
PersistentSceneEntity,
))
.id();
self.legacy_handles.push(material);
@@ -463,15 +594,70 @@ impl BevyBackend {
.spawn((
Mesh3d(mesh.clone()),
MeshMaterial3d(material.clone()),
FrameEntity,
PersistentSceneEntity,
))
.id();
self.pbr_handles.push(material);
entity
}
};
self.frame_entities.push(entity);
self.scene_entities.push(entity);
}
let light = self
.apps
.main
.world_mut()
.spawn((
DirectionalLight {
illuminance: 12_000.0,
shadow_maps_enabled: false,
..Default::default()
},
Transform::from_xyz(40.0, 80.0, 30.0).looking_at(Vec3::ZERO, Vec3::Y),
PersistentSceneEntity,
))
.id();
self.scene_entities.push(light);
Ok(RenderTimings {
clear_previous_frame,
texture_cache_sync,
mesh_cache_sync,
scene_setup: started.elapsed(),
..Default::default()
})
}
fn render_view(
&mut self,
camera: Camera,
background_srgb: [u8; 4],
limits: RenderLimits,
) -> Result<RenderedFrame, RenderError> {
let started = Instant::now();
self.clear_frame();
let clear_previous_frame = started.elapsed();
self.apps
.main
.world_mut()
.insert_resource(ClearColor(Color::srgba_u8(
background_srgb[0],
background_srgb[1],
background_srgb[2],
background_srgb[3],
)));
let camera_position = world_vector(camera.position).extend(1.0);
for handle in &self.legacy_handles {
if let Some(mut material) = self
.apps
.main
.world_mut()
.resource_mut::<Assets<LegacyBevyMaterial>>()
.get_mut(handle)
{
material.uniform.camera_position = camera_position;
}
}
let started = Instant::now();
let target = self.new_target(limits.width, limits.height);
let transform = camera_transform(camera)?;
let camera_entity = self
@@ -494,21 +680,6 @@ impl BevyBackend {
))
.id();
self.frame_entities.push(camera_entity);
let light = self
.apps
.main
.world_mut()
.spawn((
DirectionalLight {
illuminance: 12_000.0,
shadow_maps_enabled: false,
..Default::default()
},
Transform::from_xyz(40.0, 80.0, 30.0).looking_at(Vec3::ZERO, Vec3::Y),
FrameEntity,
))
.id();
self.frame_entities.push(light);
let scene_setup = started.elapsed();
let started = Instant::now();
let rgba = self.capture(&target)?;
@@ -516,8 +687,6 @@ impl BevyBackend {
rgba,
timings: RenderTimings {
clear_previous_frame,
texture_cache_sync,
mesh_cache_sync,
scene_setup,
capture: started.elapsed(),
..Default::default()
@@ -526,7 +695,12 @@ impl BevyBackend {
}
fn new_target(&mut self, width: u32, height: u32) -> RenderTarget {
if let Some(old) = self.target.take() {
if let Some((old_width, old_height, handle)) = &self.target
&& (*old_width, *old_height) == (width, height)
{
return handle.clone().into();
}
if let Some((_, _, old)) = self.target.take() {
self.apps
.main
.world_mut()
@@ -550,7 +724,7 @@ impl BevyBackend {
.world_mut()
.resource_mut::<Assets<Image>>()
.add(image);
self.target = Some(handle.clone());
self.target = Some((width, height, handle.clone()));
handle.into()
}
@@ -600,6 +774,13 @@ impl BevyBackend {
for entity in self.frame_entities.drain(..) {
let _ = self.apps.main.world_mut().despawn(entity);
}
}
fn clear_scene(&mut self) {
self.clear_frame();
for entity in self.scene_entities.drain(..) {
let _ = self.apps.main.world_mut().despawn(entity);
}
let world = self.apps.main.world_mut();
self.texture_handles.clear();
let mut materials = world.resource_mut::<Assets<PbrBevyMaterial>>();
@@ -1231,6 +1412,36 @@ mod tests {
.collect::<Vec<_>>()
);
}
let limits = RenderLimits {
width: 96,
height: 64,
max_triangles: 2,
max_texture_bytes: 64,
max_texture_pixels: 16,
far_distance: 64,
};
renderer
.load_scene(
camera,
&[renderable(Material::BlinnPhong(BlinnPhongMaterial {
diffuse_color_srgb: [30, 220, 60, 255],
fullbright: true,
..Default::default()
}))],
&textures,
limits,
)
.unwrap();
for _ in 0..2 {
let frame = renderer
.render_view(camera, [0, 0, 0, 255], limits)
.unwrap();
assert_eq!(frame.rgba.len(), 96 * 64 * 4);
assert_eq!(frame.timings.command_copy, Duration::ZERO);
assert_eq!(frame.timings.texture_cache_sync, Duration::ZERO);
assert_eq!(frame.timings.mesh_cache_sync, Duration::ZERO);
}
}
fn texture_slot(texture: usize, rotation_radians: f32) -> TextureSlot {