Every point/spot shadow view executes a full Core3d schedule run, including the per-view GPU-preprocessing chain (13 runs/frame with 12 spot shadow casters)
Bevy version and features
0.19.1 (crates.io). Not default features:
default-features = false, features = std, async_executor, multi_threaded,
bevy_asset, bevy_image, ktx2, zstd_rust, bevy_window, bevy_winit,
bevy_render, bevy_core_pipeline, bevy_pbr, bevy_post_process,
bevy_picking, mesh_picking, pbr_light_textures, tonemapping_luts
[Optional] Relevant system information
Rust: cargo 1.97.1 (c980f4866 2026-06-30)
OS: Windows 10
Adapter:
AdapterInfo { name: "NVIDIA GeForce RTX 3060 Ti", vendor: 4318, device: 9353, device_type: DiscreteGpu, device_pci_bus_id: "0000:01:00.0", driver: "NVIDIA", driver_info: "595.95", backend: Vulkan, subgroup_min_size: 32, subgroup_max_size: 32, transient_saves_memory: false }
What you did
Rendered a large streamed scene (a Second Life-style world viewer): ~50k standing
mesh instances, one 3D camera, and up to 12 shadow-casting spot lights, with GPU
preprocessing active ("GPU preprocessing is fully supported on this device") so
draws are indirect. Frame times were far worse than GPU load explained (200–450 ms
frames at ~15% GPU utilization), so we instrumented the render schedule directly:
timestamp systems at every RenderSystems set boundary, inside the RenderGraph
schedule around the Submit set, and inside Core3d around its four
Core3dSystems sets — plus a counter system registered in Core3d to count
schedule executions per frame, classified camera-vs-auxiliary via CurrentView +
ExtractedCamera.
What went wrong
What we expected: shadow views add shadow-pass encode cost, roughly proportional to caster count and shadow-map work.
What actually happened: every point/spot shadow view executes the full
Core3dschedule.bevy_pbrspawns each spot shadow view (and each point-light cube face) withRootNonCameraView(Core3d.intern())(bevy_pbr/src/render/light.rs, two spawn sites), andcamera_driver(bevy_core_pipeline/src/schedule.rs) callsworld.run_schedule(Core3d)once per root view. With 12 spot shadow casters our in-schedule counter measured 13–14 fullCore3dexecutions per frame.The pass systems themselves are nearly free — all four
Core3dSystemssets total 8–10 ms/frame summed across all 13 runs (draws are indirect). The cost is the un-set systems that run on every schedule execution:unpack_bins,clear_indirect_parameters_metadata,early/late_gpu_preprocess, the threebuild_indirect_parametersstages (bevy_pbr/src/render/gpu_preprocess.rs, registered.before(early_prepass)etc.), and the shadow-pass systems. This out-of-set residue measured 110–257 ms/frame, scaling with BOTH schedule-run count (58 ms @ 3 runs → 137 ms @ 12 runs, same scene) AND instance count at a fixed run count (115 → 257 ms as the world streamed in) — i.e., per-view × per-instance. Net effect: the render thread spends the frame re-running instance-scaled indirect-draw bookkeeping once per shadow view, while the GPU idles at 15%.
Additional information
Ruled out empirically:
- Archetype/entity accumulation: render-world census flat (~319 archetypes, ~1k entities) while the cost doubled.
- Executor setup as the main cost: swapping
Core3dtoSingleThreadedExecutorrecovered only ~15–20% (~93–104 ms vs ~110–137 ms at matched windows). Worth having — it cost nothing elsewhere — but not the story. - Shadow encode itself: with all shadows disabled the camera's single run still shows ~100 ms of the same out-of-set chain at 47k instances.
Workaround in use: schedule.set_executor(SingleThreadedExecutor::new()) on
Core3d for the ~15–20%, and capping shadow-casting spot lights, each of which
costs a full schedule run (~9–12 ms at this scene size). Point lights would be 6×
that (one run per cube face).
Theories/suggestions: share or batch the GPU-preprocessing/indirect-parameters
chain across root views per frame instead of per schedule run, or give shadow
views a minimal schedule rather than full Core3d.
Related: #23215 (backtrace documenting the camera_driver → Core3d nesting),
#24448 (0.19 CPU regression, different mechanism), #11378 and discussion #8304
(executor per-invocation overhead — the ~15–20% component), #4724 / #17564
(archetype floor — ruled out here). Suggested labels: A-Rendering, C-Performance,
P-Regression.
Source: bevyengine/bevy