feat(runtime): retain all object visuals in snapshots
This commit is contained in:
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user