mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Cache rotating mesh binding sets to remove measured render preparation overhead
This commit is contained in:
1 parent
8d35f5267b
commit
ea7c527073
3 files changed
+125
-2
No files matched your search
@@ -109,3 +109,23 @@ Ten map tests passed. The supplied-map benchmark is opt-in via
|
||||
`SKATE_TEST_CUSTOM_MAP`. Frame timing can be captured with the existing
|
||||
`SKATE_PERF_REPORT` environment setting (10-second warmup, report/exit after
|
||||
25 seconds); no automated gameplay was run.
|
||||
|
||||
## Frame capture: mesh binding preparation
|
||||
|
||||
The user-run capture after collision clustering measured 83.5 FPS / 11.98 ms
|
||||
mean frame time. Main schedule time was 2.80 ms, physics 1.55 ms/frame, and the
|
||||
render mesh-bind-group preparation section 9.75 ms. CPU query improvements
|
||||
therefore did not address the dominant measured render preparation cost.
|
||||
|
||||
The vendored renderer now caches complete per-phase model/skin/morph/lightmap
|
||||
binding sets, retaining at most four rotating allocation combinations per
|
||||
phase. Buffer contents can update without recreating descriptors. Keys include
|
||||
model allocation/offset/size, current/previous skin and morph buffer IDs, morph
|
||||
mesh/texture/skin-layout identities, lightmap revision and layout identity.
|
||||
Pipeline changes clear the cache; removed phases and replaced map resources
|
||||
retire their entries. The existing opt-in GPU cache test covers rotating
|
||||
allocations, reuse, size changes, skin/morph replacement and bounded retention.
|
||||
A new gameplay capture is required to quantify the resulting FPS change.
|
||||
|
||||
Validation: release build check passed, and the explicit Vulkan GPU cache
|
||||
regression test passed. No gameplay was launched for this change.
|
||||
+1
@@ -99,6 +99,7 @@ fn cache_tracks_binding_identity_and_texture_replacement() {
|
||||
mip_level_count: 1,
|
||||
had_data: false,
|
||||
};
|
||||
crate::render::mesh::check_phase_mesh_cache(&device, &a, &b, &image.texture_view);
|
||||
let fallback = FallbackImage {
|
||||
d1: image.clone(),
|
||||
d2: image.clone(),
|
||||
|
||||
Vendored
+104
-2
@@ -2745,7 +2745,7 @@ impl SpecializedMeshPipeline for MeshPipeline {
|
||||
///
|
||||
/// If GPU mesh preprocessing isn't in use, these are global to the scene. If
|
||||
/// GPU mesh preprocessing is in use, these are specific to a single phase.
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct MeshPhaseBindGroups {
|
||||
model_only: Option<BindGroup>,
|
||||
skinned: Option<MeshBindGroupPair>,
|
||||
@@ -2843,11 +2843,96 @@ impl PhaseLightmapCache {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MeshBindGroupPair {
|
||||
motion_vectors: BindGroup,
|
||||
no_motion_vectors: BindGroup,
|
||||
}
|
||||
|
||||
/// Bindings refer to allocations, not their current animated contents. Keep a
|
||||
/// bounded set for the rotating current/previous skin and morph buffers.
|
||||
#[derive(Default)]
|
||||
pub struct PhaseMeshCache {
|
||||
entries: std::collections::VecDeque<(LightmapTableKey, MeshBindingSignature, MeshPhaseBindGroups)>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct MeshBindingSignature {
|
||||
skins: [BufferId; 2],
|
||||
weights: [Option<BufferId>; 2],
|
||||
morphs: Vec<(AssetId<Mesh>, TextureViewId, bool)>,
|
||||
lightmaps: u64,
|
||||
layout: BindGroupLayoutId,
|
||||
}
|
||||
|
||||
impl PhaseMeshCache {
|
||||
fn get(&mut self, model: &BindingResource, signature: &MeshBindingSignature) -> Option<MeshPhaseBindGroups> {
|
||||
let BindingResource::Buffer(model) = model else { return None; };
|
||||
let index = self.entries.iter().position(|(key, old, _)| {
|
||||
&*key.buffer == model.buffer && key.offset == model.offset && key.size == model.size && old == signature
|
||||
})?;
|
||||
let entry = self.entries.remove(index).unwrap();
|
||||
let groups = entry.2.clone();
|
||||
self.entries.push_front(entry);
|
||||
Some(groups)
|
||||
}
|
||||
|
||||
fn insert(&mut self, model: &BindingResource, signature: MeshBindingSignature, groups: MeshPhaseBindGroups) {
|
||||
let BindingResource::Buffer(model) = model else { return; };
|
||||
// Old map resources must not be held by the rotating-buffer cache.
|
||||
self.entries.retain(|(_, old, _)| old.morphs == signature.morphs && old.lightmaps == signature.lightmaps && old.layout == signature.layout);
|
||||
self.entries.push_front((LightmapTableKey {
|
||||
buffer: Buffer::from(model.buffer.clone()), offset: model.offset, size: model.size,
|
||||
layout: signature.layout, revision: signature.lightmaps,
|
||||
}, signature, groups));
|
||||
self.entries.truncate(4);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn check_phase_mesh_cache(device: &RenderDevice, a: &Buffer, b: &Buffer, target: &TextureView) {
|
||||
let layout = device.create_bind_group_layout(None, &[]);
|
||||
let group = device.create_bind_group(None, &layout, &[]);
|
||||
let model = |size| BindingResource::Buffer(BufferBinding { buffer: a, offset: 0, size: BufferSize::new(size) });
|
||||
let signature = |reverse| MeshBindingSignature {
|
||||
skins: if reverse { [b.id(), a.id()] } else { [a.id(), b.id()] },
|
||||
weights: [Some(a.id()), Some(b.id())],
|
||||
morphs: vec![(AssetId::default(), target.id(), true)],
|
||||
lightmaps: 1, layout: layout.id(),
|
||||
};
|
||||
let groups = || MeshPhaseBindGroups { model_only: Some(group.clone()), ..default() };
|
||||
let mut cache = PhaseMeshCache::default();
|
||||
for reverse in [false, true] { cache.insert(&model(256), signature(reverse), groups()); }
|
||||
for _ in 0..16 {
|
||||
for reverse in [true, false] {
|
||||
assert_eq!(cache.get(&model(256), &signature(reverse)).unwrap().model_only.unwrap().id(), group.id());
|
||||
}
|
||||
}
|
||||
assert!(cache.get(&model(512), &signature(false)).is_none());
|
||||
let mut changed = signature(false);
|
||||
changed.weights.swap(0, 1);
|
||||
assert!(cache.get(&model(256), &changed).is_none());
|
||||
changed = signature(false);
|
||||
changed.skins = [a.id(), a.id()];
|
||||
assert!(cache.get(&model(256), &changed).is_none());
|
||||
changed = signature(false);
|
||||
changed.morphs[0].2 = false;
|
||||
assert!(cache.get(&model(256), &changed).is_none());
|
||||
changed = signature(false);
|
||||
changed.morphs.clear();
|
||||
assert!(cache.get(&model(256), &changed).is_none());
|
||||
cache.insert(&model(256), changed, groups());
|
||||
assert_eq!(cache.entries.len(), 1);
|
||||
assert!(cache.get(&model(256), &signature(false)).is_none());
|
||||
for size in 1..16 { cache.insert(&model(size * 16), signature(false), groups()); }
|
||||
assert_eq!(cache.entries.len(), 4);
|
||||
changed = signature(false);
|
||||
changed.lightmaps += 1;
|
||||
assert!(cache.get(&model(256), &changed).is_none());
|
||||
cache.insert(&model(256), changed, groups());
|
||||
assert_eq!(cache.entries.len(), 1);
|
||||
}
|
||||
|
||||
/// All bind groups for meshes currently loaded.
|
||||
#[derive(Resource)]
|
||||
pub enum MeshBindGroups {
|
||||
@@ -2919,7 +3004,9 @@ pub fn prepare_mesh_bind_groups(
|
||||
mut render_lightmaps: ResMut<RenderLightmaps>,
|
||||
mut lightmap_tables: Local<TypeIdMap<PhaseLightmapCache>>,
|
||||
mut morph_meshes: Local<MorphMeshCache>,
|
||||
mut mesh_tables: Local<TypeIdMap<PhaseMeshCache>>,
|
||||
) {
|
||||
if mesh_pipeline.is_changed() { mesh_tables.clear(); }
|
||||
if meshes.is_changed() {
|
||||
morph_meshes.refresh(&meshes);
|
||||
}
|
||||
@@ -2929,6 +3016,7 @@ pub fn prepare_mesh_bind_groups(
|
||||
.into_inner()
|
||||
.instance_data_binding()
|
||||
{
|
||||
mesh_tables.retain(|phase, _| *phase == TypeId::of::<()>());
|
||||
// In this path, we only have a single set of bind groups for all phases.
|
||||
let cpu_preprocessing_mesh_bind_groups = prepare_mesh_bind_groups_for_phase(
|
||||
instance_data_binding,
|
||||
@@ -2941,6 +3029,7 @@ pub fn prepare_mesh_bind_groups(
|
||||
&mut render_lightmaps,
|
||||
lightmap_tables.entry(TypeId::of::<()>()).or_default(),
|
||||
&morph_meshes.0,
|
||||
mesh_tables.entry(TypeId::of::<()>()).or_default(),
|
||||
);
|
||||
|
||||
commands.insert_resource(MeshBindGroups::CpuPreprocessing(
|
||||
@@ -2974,11 +3063,13 @@ pub fn prepare_mesh_bind_groups(
|
||||
&mut render_lightmaps,
|
||||
lightmap_tables.entry(*phase_type_id).or_default(),
|
||||
&morph_meshes.0,
|
||||
mesh_tables.entry(*phase_type_id).or_default(),
|
||||
);
|
||||
|
||||
gpu_preprocessing_mesh_bind_groups.insert(*phase_type_id, mesh_phase_bind_groups);
|
||||
}
|
||||
|
||||
mesh_tables.retain(|phase, _| gpu_batched_instance_buffers.phase_instance_buffers.contains_key(phase));
|
||||
lightmap_tables.retain(|phase, _| {
|
||||
gpu_batched_instance_buffers
|
||||
.phase_instance_buffers
|
||||
@@ -3002,10 +3093,20 @@ fn prepare_mesh_bind_groups_for_phase(
|
||||
render_lightmaps: &mut RenderLightmaps,
|
||||
lightmap_table: &mut PhaseLightmapCache,
|
||||
morph_meshes: &[AssetId<Mesh>],
|
||||
cache: &mut PhaseMeshCache,
|
||||
) -> MeshPhaseBindGroups {
|
||||
let layouts = &mesh_pipeline.mesh_layouts;
|
||||
let signature = MeshBindingSignature {
|
||||
skins: [skins_uniform.current_buffer.id(), skins_uniform.prev_buffer.id()],
|
||||
weights: [weights_uniform.current_buffer.buffer().map(Buffer::id), weights_uniform.prev_buffer.buffer().map(Buffer::id)],
|
||||
morphs: morph_meshes.iter().filter_map(|&id| meshes.get(id).and_then(|mesh|
|
||||
mesh.morph_targets.as_ref().map(|target| (id, target.id(), is_skinned(&mesh.layout))))).collect(),
|
||||
lightmaps: render_lightmaps.binding_revision,
|
||||
layout: pipeline_cache.get_bind_group_layout(&layouts.lightmapped).id(),
|
||||
};
|
||||
if let Some(groups) = cache.get(&model, &signature) { return groups; }
|
||||
|
||||
// TODO: Reuse allocations.
|
||||
// Buffer contents change each frame; bindings only change with resources.
|
||||
let mut groups = MeshPhaseBindGroups {
|
||||
model_only: Some(layouts.model_only(render_device, pipeline_cache, &model)),
|
||||
..default()
|
||||
@@ -3103,6 +3204,7 @@ fn prepare_mesh_bind_groups_for_phase(
|
||||
lightmap_table.groups = std::sync::Arc::new(bindings);
|
||||
}
|
||||
groups.lightmaps = lightmap_table.groups.clone();
|
||||
cache.insert(&model, signature, groups.clone());
|
||||
|
||||
groups
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user