feat(runtime): retain all object visuals in snapshots
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Test / Lint (push) Failing after 1m59s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 09:19:56 +04:00
parent 8296400cad
commit 2b735c693b
2 changed files with 103 additions and 35 deletions
+96 -35
View File
@@ -131,42 +131,48 @@ fn render_snapshot_with_assets(
) -> RenderSnapshot { ) -> RenderSnapshot {
let mut draws = Vec::with_capacity(snapshot.objects.len()); let mut draws = Vec::with_capacity(snapshot.objects.len());
for (index, handle) in snapshot.objects.iter().enumerate() { for (index, handle) in snapshot.objects.iter().enumerate() {
let stable_order = u64::from(handle.slot); let visuals = mission_assets.map_or(&[][..], |assets| assets.visuals_for_object(index));
let prepared = mission_assets.and_then(|assets| { let visual_count = visuals.len().max(1);
assets for visual_index in 0..visual_count {
.visual_for_object(index) let prepared = mission_assets.and_then(|assets| {
.and_then(|visual_id| assets.visual_by_id(visual_id)) visuals
}); .get(visual_index)
let mesh = if let Some(visual) = prepared { .and_then(|visual_id| assets.visual_by_id(*visual_id))
visual.mesh.as_ref().map_or_else(
|| GpuMeshId(u64::from(handle.slot) + 1),
|_| GpuMeshId(visual.id.raw()),
)
} else {
GpuMeshId(u64::from(handle.slot) + 1)
};
let material = prepared
.and_then(PreparedVisual::primary_material_id)
.map_or(GpuMaterialId(1), |material_id| {
GpuMaterialId(material_id.raw())
}); });
let draw_id = snapshot let mesh = prepared.map_or_else(
.tick || GpuMeshId(u64::from(handle.slot) + 1),
.0 |visual| GpuMeshId(visual.id.raw()),
.wrapping_mul(1_000_003) );
.wrapping_add(stable_order); let material = prepared
draws.push(RenderSnapshotDraw { .and_then(PreparedVisual::primary_material_id)
id: DrawId(draw_id), .map_or(GpuMaterialId(1), |material_id| {
phase: RenderPhase::Opaque, GpuMaterialId(material_id.raw())
object_id: None, });
mesh, let stable_order = if visual_count == 1 {
material_slots: vec![material], u64::from(handle.slot)
material_index: 0, } else {
pipeline_state: fparkan_render::LegacyPipelineState::default(), u64::from(handle.slot)
transform: identity_transform(index_to_f32(index)), .wrapping_mul(1_000_003)
range: IndexRange { start: 0, count: 3 }, .wrapping_add(u64::try_from(visual_index).unwrap_or(u64::MAX))
stable_order, };
}); let draw_id = snapshot
.tick
.0
.wrapping_mul(1_000_003)
.wrapping_add(stable_order);
draws.push(RenderSnapshotDraw {
id: DrawId(draw_id),
phase: RenderPhase::Opaque,
object_id: None,
mesh,
material_slots: vec![material],
material_index: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
transform: identity_transform(index_to_f32(index)),
range: IndexRange { start: 0, count: 3 },
stable_order,
});
}
} }
RenderSnapshot { RenderSnapshot {
camera: CameraSnapshot::default(), camera: CameraSnapshot::default(),
@@ -274,6 +280,7 @@ fn usage() -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use fparkan_assets::AssetId;
use fparkan_world::{ObjectHandle, StateHash, Tick}; use fparkan_world::{ObjectHandle, StateHash, Tick};
use std::path::Path; use std::path::Path;
@@ -337,6 +344,60 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn render_snapshot_retains_every_prepared_visual_for_an_object() -> Result<(), String> {
let snapshot = WorldSnapshot {
tick: Tick(3),
objects: vec![ObjectHandle {
generation: 1,
slot: 7,
}],
events: Vec::new(),
hash: StateHash([0; 32]),
};
let first = prepared_visual(101, 201);
let second = prepared_visual(102, 202);
let assets = MissionAssets {
visuals: vec![first, second],
object_visuals: vec![vec![AssetId::new(101), AssetId::new(102)]],
..MissionAssets::default()
};
let commands = render_snapshot_commands_with_assets(&snapshot, Some(&assets))?;
let draws: Vec<_> = commands
.commands
.iter()
.filter_map(|command| match command {
RenderCommand::Draw(draw) => Some(draw),
_ => None,
})
.collect();
assert_eq!(draws.len(), 2);
assert_eq!(draws[0].mesh, GpuMeshId(101));
assert_eq!(draws[1].mesh, GpuMeshId(102));
assert_eq!(draws[0].material, GpuMaterialId(201));
assert_eq!(draws[1].material, GpuMaterialId(202));
assert_ne!(draws[0].stable_order, draws[1].stable_order);
Ok(())
}
fn prepared_visual(id: u64, material: u64) -> PreparedVisual {
PreparedVisual {
id: AssetId::new(id),
mesh: None,
model_id: None,
wear_id: None,
model_nodes: 0,
model_slots: 0,
model_batches: 0,
material_count: 1,
material_ids: vec![AssetId::new(material)],
texture_ids: Vec::new(),
lightmap_ids: Vec::new(),
texture_count: 0,
lightmap_count: 0,
}
}
#[test] #[test]
#[ignore = "requires licensed corpus"] #[ignore = "requires licensed corpus"]
fn selected_is_and_is2_missions_produce_approved_render_captures() { fn selected_is_and_is2_missions_produce_approved_render_captures() {
+7
View File
@@ -909,6 +909,13 @@ Comparator прежде сравнивает `vkformat` identity из имен
отклоняет mismatch до byte scan; raw blobs разных Vulkan formats нельзя отклоняет mismatch до byte scan; raw blobs разных Vulkan formats нельзя
считать сопоставимыми без явной conversion contract. считать сопоставимыми без явной conversion contract.
`fparkan-game` render-snapshot bridge больше не берёт только первый prepared
visual mission object. Каждый `MissionAssets::visuals_for_object` entry
создаёт отдельный backend-neutral draw с own mesh/material IDs; object без
prepared visual сохраняет один compatibility fallback draw. Это сохраняет
multi-component prototype graph для будущего real Vulkan renderer, однако
текущий game app всё ещё использует planning backend и triangle ranges.
`Land.msh` использует отдельный geometry-only bridge: validated `TerrainFace28` `Land.msh` использует отдельный geometry-only bridge: validated `TerrainFace28`
сохраняет source triangle order, а его positions и packed UV0 попадают в тот же сохраняет source triangle order, а его positions и packed UV0 попадают в тот же
static vertex/index upload path. Для текущего диагностического viewer XZ bounds static vertex/index upload path. Для текущего диагностического viewer XZ bounds