Skip to main content

solari/
solari.rs

1//! Demonstrates realtime dynamic raytraced lighting using Bevy Solari.
2
3use argh::FromArgs;
4use bevy::{
5    camera::CameraMainTextureUsages,
6    camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},
7    diagnostic::{Diagnostic, DiagnosticPath, DiagnosticsStore},
8    gltf::GltfMaterialName,
9    image::{ImageAddressMode, ImageLoaderSettings},
10    mesh::{Indices, VertexAttributeValues},
11    post_process::bloom::Bloom,
12    prelude::*,
13    render::{diagnostic::RenderDiagnosticsPlugin, render_resource::TextureUsages},
14    solari::{
15        pathtracer::{Pathtracer, PathtracingPlugin},
16        prelude::{RaytracingMesh3d, SolariLighting, SolariPlugins},
17    },
18    world_serialization::WorldInstanceReady,
19};
20use chacha20::ChaCha8Rng;
21use rand::{RngExt, SeedableRng};
22use std::f32::consts::PI;
23
24#[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
25use bevy::{
26    anti_alias::dlss::{
27        Dlss, DlssProjectId, DlssRayReconstructionFeature, DlssRayReconstructionSupported,
28    },
29    render::camera::{MipBias, TemporalJitter},
30};
31
32/// `bevy_solari` demo.
33#[derive(FromArgs, Resource, Clone, Copy)]
34struct Args {
35    /// use the reference pathtracer instead of the realtime lighting system.
36    #[argh(switch)]
37    pathtracer: Option<bool>,
38    /// stress test a scene with many lights.
39    #[argh(switch)]
40    many_lights: Option<bool>,
41}
42
43fn main() {
44    let args: Args = argh::from_env();
45
46    let mut app = App::new();
47
48    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
49    app.insert_resource(DlssProjectId(bevy_asset::uuid::uuid!(
50        "5417916c-0291-4e3f-8f65-326c1858ab96" // Don't copy paste this - generate your own UUID!
51    )));
52
53    app.add_plugins((
54        DefaultPlugins,
55        SolariPlugins,
56        FreeCameraPlugin,
57        RenderDiagnosticsPlugin,
58    ))
59    .insert_resource(args);
60
61    if args.many_lights == Some(true) {
62        app.add_systems(Startup, setup_many_lights);
63    } else {
64        app.add_systems(Startup, setup_pica_pica);
65    }
66
67    if args.pathtracer == Some(true) {
68        app.add_plugins(PathtracingPlugin);
69    } else {
70        #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
71        app.add_systems(Update, toggle_dlss_rr);
72
73        app.add_systems(Update, toggle_restir);
74
75        if args.many_lights != Some(true) {
76            app.add_systems(Update, (pause_scene, toggle_lights, patrol_path));
77        }
78        app.add_systems(PostUpdate, (update_control_text, update_performance_text));
79    }
80
81    app.run();
82}
83
84fn setup_pica_pica(
85    mut commands: Commands,
86    asset_server: Res<AssetServer>,
87    args: Res<Args>,
88    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_rr_supported: Option<
89        Res<DlssRayReconstructionSupported>,
90    >,
91) {
92    commands
93        .spawn((
94            WorldAssetRoot(
95                asset_server.load(
96                    GltfAssetLabel::Scene(0)
97                        .from_asset("https://github.com/bevyengine/bevy_asset_files/raw/2a5950295a8b6d9d051d59c0df69e87abcda58c3/pica_pica/mini_diorama_01.glb")
98                ),
99            ),
100            Transform::from_scale(Vec3::splat(10.0)),
101        ))
102        .observe(add_raytracing_meshes_on_scene_load);
103
104    commands
105        .spawn((
106            WorldAssetRoot(asset_server.load(
107                GltfAssetLabel::Scene(0).from_asset("https://github.com/bevyengine/bevy_asset_files/raw/2a5950295a8b6d9d051d59c0df69e87abcda58c3/pica_pica/robot_01.glb")
108            )),
109            Transform::from_scale(Vec3::splat(2.0))
110                .with_translation(Vec3::new(-2.0, 0.05, -2.1))
111                .with_rotation(Quat::from_rotation_y(PI / 2.0)),
112            PatrolPath {
113                path: vec![
114                    (Vec3::new(-2.0, 0.05, -2.1), Quat::from_rotation_y(PI / 2.0)),
115                    (Vec3::new(2.2, 0.05, -2.1), Quat::from_rotation_y(0.0)),
116                    (
117                        Vec3::new(2.2, 0.05, 2.1),
118                        Quat::from_rotation_y(3.0 * PI / 2.0),
119                    ),
120                    (Vec3::new(-2.0, 0.05, 2.1), Quat::from_rotation_y(PI)),
121                ],
122                i: 0,
123            },
124        ))
125        .observe(add_raytracing_meshes_on_scene_load);
126
127    commands.spawn((
128        DirectionalLight {
129            illuminance: light_consts::lux::FULL_DAYLIGHT,
130            shadow_maps_enabled: false, // Solari replaces shadow mapping
131            ..default()
132        },
133        Transform::from_rotation(Quat::from_xyzw(
134            -0.13334629,
135            -0.86597735,
136            -0.3586996,
137            0.3219264,
138        )),
139    ));
140
141    let mut camera = commands.spawn((
142        Camera3d::default(),
143        Camera {
144            clear_color: ClearColorConfig::Custom(Color::BLACK),
145            ..default()
146        },
147        FreeCamera {
148            walk_speed: 3.0,
149            run_speed: 10.0,
150            ..Default::default()
151        },
152        Transform::from_translation(Vec3::new(0.219417, 2.5764852, 6.9718704)).with_rotation(
153            Quat::from_xyzw(-0.1466768, 0.013738206, 0.002037309, 0.989087),
154        ),
155        // Msaa::Off and CameraMainTextureUsages with STORAGE_BINDING are required for Solari
156        CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING),
157        Msaa::Off,
158    ));
159
160    if args.pathtracer == Some(true) {
161        camera.insert(Pathtracer::default());
162    } else {
163        camera.insert(SolariLighting::default());
164    }
165
166    // Using DLSS Ray Reconstruction for denoising (and cheaper rendering via upscaling) is _highly_ recommended when using Solari
167    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
168    if dlss_rr_supported.is_some() {
169        camera.insert(Dlss::<DlssRayReconstructionFeature>::default());
170    }
171
172    commands.spawn((
173        ControlText,
174        Text::default(),
175        Node {
176            position_type: PositionType::Absolute,
177            bottom: px(12.0),
178            left: px(12.0),
179            ..default()
180        },
181    ));
182
183    commands.spawn((
184        Node {
185            position_type: PositionType::Absolute,
186            right: px(0.0),
187            padding: px(4.0).all(),
188            border_radius: BorderRadius::bottom_left(px(4.0)),
189            ..default()
190        },
191        BackgroundColor(Color::srgba(0.10, 0.10, 0.10, 0.8)),
192        children![(
193            PerformanceText,
194            Text::default(),
195            TextFont {
196                font_size: FontSize::Px(8.0),
197                ..default()
198            },
199        )],
200    ));
201}
202
203fn setup_many_lights(
204    mut commands: Commands,
205    asset_server: Res<AssetServer>,
206    mut meshes: ResMut<Assets<Mesh>>,
207    mut materials: ResMut<Assets<StandardMaterial>>,
208    args: Res<Args>,
209    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_rr_supported: Option<
210        Res<DlssRayReconstructionSupported>,
211    >,
212) {
213    let mut rng = ChaCha8Rng::seed_from_u64(42);
214
215    let mut plane_mesh = Plane3d::default()
216        .mesh()
217        .size(400.0, 400.0)
218        .build()
219        .with_generated_tangents()
220        .unwrap();
221    match plane_mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0).unwrap() {
222        VertexAttributeValues::Float32x2(items) => {
223            items.iter_mut().flatten().for_each(|x| *x *= 3.0);
224        }
225        _ => unreachable!(),
226    }
227    let plane_mesh = meshes.add(plane_mesh);
228    let cube_mesh = meshes.add(
229        Cuboid::default()
230            .mesh()
231            .build()
232            .with_generated_tangents()
233            .unwrap(),
234    );
235    let sphere_mesh = meshes.add(
236        Sphere::new(1.0)
237            .mesh()
238            .build()
239            .with_generated_tangents()
240            .unwrap(),
241    );
242
243    commands
244        .spawn((
245            RaytracingMesh3d(plane_mesh.clone()),
246            MeshMaterial3d(
247                materials.add(StandardMaterial {
248                    base_color_texture: Some(
249                        asset_server
250                            .load_builder()
251                            .with_settings::<ImageLoaderSettings>(|settings| {
252                                settings
253                                    .sampler
254                                    .get_or_init_descriptor()
255                                    .set_address_mode(ImageAddressMode::Repeat);
256                            })
257                            .load("textures/uv_checker_bw.png"),
258                    ),
259                    perceptual_roughness: 0.0,
260                    ..default()
261                }),
262            ),
263        ))
264        .insert_if(Mesh3d(plane_mesh), || args.pathtracer != Some(true));
265
266    for _ in 0..8000 {
267        commands
268            .spawn((
269                RaytracingMesh3d(cube_mesh.clone()),
270                MeshMaterial3d(materials.add(StandardMaterial {
271                    base_color: Color::srgb(rng.random(), rng.random(), rng.random()),
272                    perceptual_roughness: rng.random(),
273                    ..default()
274                })),
275                Transform::default()
276                    .with_scale(Vec3 {
277                        x: rng.random_range(0.2..=2.0),
278                        y: rng.random_range(0.2..=2.0),
279                        z: rng.random_range(0.2..=2.0),
280                    })
281                    .with_translation(Vec3::new(
282                        rng.random_range(-180.0..=180.0),
283                        0.2,
284                        rng.random_range(-180.0..=180.0),
285                    )),
286            ))
287            .insert_if(Mesh3d(cube_mesh.clone()), || args.pathtracer != Some(true));
288    }
289
290    for x in -10..=10 {
291        for y in -10..=10 {
292            commands
293                .spawn((
294                    RaytracingMesh3d(sphere_mesh.clone()),
295                    MeshMaterial3d(
296                        materials.add(StandardMaterial {
297                            emissive: Color::linear_rgb(
298                                rng.random::<f32>() * 60000.0,
299                                rng.random::<f32>() * 60000.0,
300                                rng.random::<f32>() * 60000.0,
301                            )
302                            .into(),
303                            ..default()
304                        }),
305                    ),
306                    Transform::default().with_translation(Vec3::new(
307                        (x * 20) as f32,
308                        7.0,
309                        (y * 20) as f32,
310                    )),
311                ))
312                .insert_if(Mesh3d(sphere_mesh.clone()), || {
313                    args.pathtracer != Some(true)
314                });
315        }
316    }
317
318    let mut camera = commands.spawn((
319        Camera3d::default(),
320        Camera {
321            clear_color: ClearColorConfig::Custom(Color::BLACK),
322            ..default()
323        },
324        FreeCamera {
325            walk_speed: 3.0,
326            run_speed: 10.0,
327            ..Default::default()
328        },
329        Transform::from_translation(Vec3::new(6.11329, 166.74896, 451.8226)).with_rotation(
330            Quat::from_xyzw(-0.183938, 0.009093744, 0.0017017953, 0.9828943),
331        ),
332        // Msaa::Off and CameraMainTextureUsages with STORAGE_BINDING are required for Solari
333        CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING),
334        Msaa::Off,
335        Bloom {
336            intensity: 0.1,
337            ..Bloom::NATURAL
338        },
339    ));
340
341    if args.pathtracer == Some(true) {
342        camera.insert(Pathtracer::default());
343    } else {
344        camera.insert(SolariLighting::default());
345    }
346
347    // Using DLSS Ray Reconstruction for denoising (and cheaper rendering via upscaling) is _highly_ recommended when using Solari
348    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
349    if dlss_rr_supported.is_some() {
350        camera.insert(Dlss::<DlssRayReconstructionFeature>::default());
351    }
352
353    commands.spawn((
354        ControlText,
355        Text::default(),
356        Node {
357            position_type: PositionType::Absolute,
358            bottom: px(12.0),
359            left: px(12.0),
360            ..default()
361        },
362    ));
363
364    commands.spawn((
365        Node {
366            position_type: PositionType::Absolute,
367            right: px(0.0),
368            padding: px(4.0).all(),
369            border_radius: BorderRadius::bottom_left(px(4.0)),
370            ..default()
371        },
372        BackgroundColor(Color::srgba(0.10, 0.10, 0.10, 0.8)),
373        children![(
374            PerformanceText,
375            Text::default(),
376            TextFont {
377                font_size: FontSize::Px(8.0),
378                ..default()
379            },
380        )],
381    ));
382}
383
384fn add_raytracing_meshes_on_scene_load(
385    scene_ready: On<WorldInstanceReady>,
386    children: Query<&Children>,
387    mesh_query: Query<(
388        &Mesh3d,
389        &MeshMaterial3d<StandardMaterial>,
390        Option<&GltfMaterialName>,
391    )>,
392    mut meshes: ResMut<Assets<Mesh>>,
393    mut materials: ResMut<Assets<StandardMaterial>>,
394    mut commands: Commands,
395    args: Res<Args>,
396) {
397    for descendant in children.iter_descendants(scene_ready.entity) {
398        if let Ok((Mesh3d(mesh_handle), MeshMaterial3d(material_handle), material_name)) =
399            mesh_query.get(descendant)
400        {
401            // Add raytracing mesh component
402            commands
403                .entity(descendant)
404                .insert(RaytracingMesh3d(mesh_handle.clone()));
405
406            // Ensure meshes are Solari compatible
407            let mut mesh = meshes.get_mut(mesh_handle).unwrap();
408            if !mesh.contains_attribute(Mesh::ATTRIBUTE_UV_0) {
409                let vertex_count = mesh.count_vertices();
410                mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0]; vertex_count]);
411                mesh.insert_attribute(
412                    Mesh::ATTRIBUTE_TANGENT,
413                    vec![[0.0, 0.0, 0.0, 0.0]; vertex_count],
414                );
415            }
416            if !mesh.contains_attribute(Mesh::ATTRIBUTE_TANGENT) {
417                mesh.generate_tangents().unwrap();
418            }
419            if mesh.contains_attribute(Mesh::ATTRIBUTE_UV_1) {
420                mesh.remove_attribute(Mesh::ATTRIBUTE_UV_1);
421            }
422            if let Some(indices) = mesh.indices_mut()
423                && let Indices::U16(_) = indices
424            {
425                *indices = Indices::U32(indices.iter().map(|i| i as u32).collect());
426            }
427
428            // Prevent rasterization if using pathtracer
429            if args.pathtracer == Some(true) {
430                commands.entity(descendant).remove::<Mesh3d>();
431            }
432
433            // Adjust scene materials to better demo Solari features
434            if material_name.map(|s| s.0.as_str()) == Some("material") {
435                let mut material = materials.get_mut(material_handle).unwrap();
436                material.emissive = LinearRgba::BLACK;
437            }
438            if material_name.map(|s| s.0.as_str()) == Some("Lights") {
439                let mut material = materials.get_mut(material_handle).unwrap();
440                material.emissive =
441                    LinearRgba::from(Color::srgb(0.941, 0.714, 0.043)) * 1_000_000.0;
442                material.alpha_mode = AlphaMode::Opaque;
443                material.specular_transmission = 0.0;
444
445                commands.insert_resource(RobotLightMaterial(material_handle.clone()));
446            }
447            if material_name.map(|s| s.0.as_str()) == Some("Glass_Dark_01") {
448                let mut material = materials.get_mut(material_handle).unwrap();
449                material.alpha_mode = AlphaMode::Opaque;
450                material.specular_transmission = 0.0;
451            }
452        }
453    }
454}
455
456#[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
457fn toggle_dlss_rr(
458    key_input: Res<ButtonInput<KeyCode>>,
459    camera: Single<(Entity, Has<Dlss<DlssRayReconstructionFeature>>), With<SolariLighting>>,
460    dlss_rr_supported: Option<Res<DlssRayReconstructionSupported>>,
461    mut commands: Commands,
462) {
463    if key_input.just_pressed(KeyCode::Digit3) && dlss_rr_supported.is_some() {
464        let (entity, dlss) = *camera;
465        if dlss {
466            commands
467                .entity(entity)
468                .remove::<(Dlss<DlssRayReconstructionFeature>, TemporalJitter, MipBias)>();
469        } else {
470            commands
471                .entity(entity)
472                .insert(Dlss::<DlssRayReconstructionFeature>::default());
473        }
474    }
475}
476
477fn toggle_restir(
478    key_input: Res<ButtonInput<KeyCode>>,
479    mut solari_lighting: Single<&mut SolariLighting>,
480) {
481    if key_input.just_pressed(KeyCode::KeyR) {
482        solari_lighting.restir = !solari_lighting.restir;
483    }
484}
485
486fn pause_scene(mut time: ResMut<Time<Virtual>>, key_input: Res<ButtonInput<KeyCode>>) {
487    if key_input.just_pressed(KeyCode::Space) {
488        time.toggle();
489    }
490}
491
492#[derive(Resource)]
493struct RobotLightMaterial(Handle<StandardMaterial>);
494
495fn toggle_lights(
496    key_input: Res<ButtonInput<KeyCode>>,
497    robot_light_material: Option<Res<RobotLightMaterial>>,
498    mut materials: ResMut<Assets<StandardMaterial>>,
499    directional_light: Query<Entity, With<DirectionalLight>>,
500    mut commands: Commands,
501) {
502    if key_input.just_pressed(KeyCode::Digit1) {
503        if let Ok(directional_light) = directional_light.single() {
504            commands.entity(directional_light).despawn();
505        } else {
506            commands.spawn((
507                DirectionalLight {
508                    illuminance: light_consts::lux::FULL_DAYLIGHT,
509                    shadow_maps_enabled: false, // Solari replaces shadow mapping
510                    ..default()
511                },
512                Transform::from_rotation(Quat::from_xyzw(
513                    -0.13334629,
514                    -0.86597735,
515                    -0.3586996,
516                    0.3219264,
517                )),
518            ));
519        }
520    }
521
522    if key_input.just_pressed(KeyCode::Digit2)
523        && let Some(robot_light_material) = robot_light_material
524    {
525        let mut material = materials.get_mut(&robot_light_material.0).unwrap();
526        if material.emissive == LinearRgba::BLACK {
527            material.emissive = LinearRgba::from(Color::srgb(0.941, 0.714, 0.043)) * 1_000_000.0;
528        } else {
529            material.emissive = LinearRgba::BLACK;
530        }
531    }
532}
533
534#[derive(Component)]
535struct PatrolPath {
536    path: Vec<(Vec3, Quat)>,
537    i: usize,
538}
539
540fn patrol_path(mut query: Query<(&mut PatrolPath, &mut Transform)>, time: Res<Time<Virtual>>) {
541    for (mut path, mut transform) in query.iter_mut() {
542        let (mut target_position, mut target_rotation) = path.path[path.i];
543        let mut distance_to_target = transform.translation.distance(target_position);
544        if distance_to_target < 0.01 {
545            transform.translation = target_position;
546            transform.rotation = target_rotation;
547
548            path.i = (path.i + 1) % path.path.len();
549            (target_position, target_rotation) = path.path[path.i];
550            distance_to_target = transform.translation.distance(target_position);
551        }
552
553        let direction = (target_position - transform.translation).normalize();
554        let movement = direction * time.delta_secs();
555
556        if movement.length() > distance_to_target {
557            transform.translation = target_position;
558            transform.rotation = target_rotation;
559        } else {
560            transform.translation += movement;
561        }
562    }
563}
564
565#[derive(Component)]
566struct ControlText;
567
568fn update_control_text(
569    mut text: Single<&mut Text, With<ControlText>>,
570    solari_lighting: Single<&SolariLighting>,
571    robot_light_material: Option<Res<RobotLightMaterial>>,
572    materials: Res<Assets<StandardMaterial>>,
573    directional_light: Query<Entity, With<DirectionalLight>>,
574    time: Res<Time<Virtual>>,
575    args: Res<Args>,
576    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_rr_supported: Option<
577        Res<DlssRayReconstructionSupported>,
578    >,
579    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_camera: Query<
580        Has<Dlss<DlssRayReconstructionFeature>>,
581        With<SolariLighting>,
582    >,
583) {
584    text.0.clear();
585
586    if args.many_lights != Some(true) {
587        if time.is_paused() {
588            text.0.push_str("(Space): Resume");
589        } else {
590            text.0.push_str("(Space): Pause");
591        }
592
593        if directional_light.single().is_ok() {
594            text.0.push_str("\n(1): Disable directional light");
595        } else {
596            text.0.push_str("\n(1): Enable directional light");
597        }
598
599        match robot_light_material.and_then(|m| materials.get(&m.0)) {
600            Some(robot_light_material) if robot_light_material.emissive != LinearRgba::BLACK => {
601                text.0.push_str("\n(2): Disable robot emissive light");
602            }
603            _ => {
604                text.0.push_str("\n(2): Enable robot emissive light");
605            }
606        }
607    }
608
609    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
610    if dlss_rr_supported.is_some() {
611        if matches!(dlss_camera.single(), Ok(true)) {
612            text.0.push_str("\n(3): Disable DLSS Ray Reconstruction");
613        } else {
614            text.0.push_str("\n(3): Enable DLSS Ray Reconstruction");
615        }
616    } else {
617        text.0
618            .push_str("\nDenoising: DLSS Ray Reconstruction not supported");
619    }
620
621    #[cfg(any(not(feature = "dlss"), feature = "force_disable_dlss"))]
622    text.0
623        .push_str("\nDenoising: App not compiled with DLSS support");
624
625    if solari_lighting.restir {
626        text.0.push_str("\n(R): Disable ReSTIR");
627    } else {
628        text.0.push_str("\n(R): Enable ReSTIR");
629    }
630}
631
632#[derive(Component)]
633struct PerformanceText;
634
635fn update_performance_text(
636    mut text: Single<&mut Text, With<PerformanceText>>,
637    diagnostics: Res<DiagnosticsStore>,
638    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_camera: Query<
639        Has<Dlss<DlssRayReconstructionFeature>>,
640        With<SolariLighting>,
641    >,
642) {
643    text.0.clear();
644
645    let mut total = 0.0;
646    let mut add_diagnostic = |name: &str, path: &'static str| {
647        let path = DiagnosticPath::new(path);
648        if let Some(value) = diagnostics.get(&path).and_then(Diagnostic::smoothed) {
649            text.push_str(&format!("{name:17}  {value:.2} ms\n"));
650            total += value;
651        }
652    };
653
654    (add_diagnostic)(
655        "Light tiles",
656        "render/solari_lighting/presample_light_tiles/elapsed_gpu",
657    );
658    (add_diagnostic)(
659        "World cache",
660        "render/solari_lighting/world_cache/elapsed_gpu",
661    );
662    (add_diagnostic)("Lighting", "render/solari_lighting/lighting/elapsed_gpu");
663    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
664    if matches!(dlss_camera.single(), Ok(true)) {
665        (add_diagnostic)("DLSS-RR", "render/dlss_ray_reconstruction/elapsed_gpu");
666    }
667    text.push_str(&format!("{:17}  {total:.2} ms\n", "Total"));
668
669    if let Some(world_cache_active_cells_count) = diagnostics
670        .get(&DiagnosticPath::new(
671            "render/solari_lighting/world_cache_active_cells_count",
672        ))
673        .and_then(Diagnostic::smoothed)
674    {
675        text.push_str(&format!(
676            "\nWorld cache cells {} ({:.0}%)",
677            world_cache_active_cells_count as u32,
678            (world_cache_active_cells_count * 100.0) / (2u64.pow(20) as f64)
679        ));
680    }
681}