fix: preserve material samples across frame retries
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s

This commit is contained in:
Valentin Popov committed 2026-10-20 00:18:04 +04:00
1 parent 02f461cdfb
commit 1217d70d52
4 files changed
+486 -167

No files matched your search

Generated
+1
View File
@@ -507,6 +507,7 @@ dependencies = [
"fparkan-animation", "fparkan-animation",
"fparkan-assets", "fparkan-assets",
"fparkan-fx", "fparkan-fx",
"fparkan-material",
"fparkan-msh", "fparkan-msh",
"fparkan-path", "fparkan-path",
"fparkan-platform-winit", "fparkan-platform-winit",
+3
View File
@@ -24,3 +24,6 @@ rodio = { version = "0.22.2", default-features = false, features = ["playback",
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
winit = { version = "0.30", default-features = false, features = ["rwh_06"] } winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
[dev-dependencies]
fparkan-material = { path = "../../crates/fparkan-material" }
+474 -167
View File
@@ -739,7 +739,6 @@ impl MaterialPhaseBinding {
struct MaterialVariantSet { struct MaterialVariantSet {
phase_material_indices: Vec<usize>, phase_material_indices: Vec<usize>,
range_indices: Vec<usize>, range_indices: Vec<usize>,
pipeline_category: u8,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -753,7 +752,7 @@ struct MaterialAnimationBinding {
stage_textures: Option<MaterialStageTextures>, stage_textures: Option<MaterialStageTextures>,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, PartialEq)]
struct ActiveMaterialState { struct ActiveMaterialState {
material_index: usize, material_index: usize,
diffuse_alpha: f32, diffuse_alpha: f32,
@@ -772,19 +771,51 @@ fn selected_material_phase(
Ok((material_index, pipeline_category)) Ok((material_index, pipeline_category))
} }
fn update_material_animations( #[derive(Clone, Copy, Debug, PartialEq)]
struct MaterialRangeSelection {
range_index: usize,
material_index: usize,
pipeline_category: u8,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct MaterialPropertiesUpdate {
material_index: usize,
diffuse_alpha: f32,
overlay_alpha: f32,
base_directional: [f32; 3],
base_additive: [f32; 3],
overlay_directional: Option<[f32; 3]>,
overlay_additive: Option<[f32; 3]>,
specular_rgb: [f32; 3],
specular_power: u8,
overlay_specular_rgb: Option<[f32; 3]>,
overlay_power: Option<u8>,
uv_transforms: [[f32; 4]; 4],
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct MaterialStagePhaseUpdate {
binding_index: usize,
material_index: usize,
phases: [usize; 3],
}
#[derive(Clone, Debug, Default, PartialEq)]
struct MaterialFrameUpdate {
active_materials: HashMap<usize, ActiveMaterialState>,
range_selections: Vec<MaterialRangeSelection>,
properties: Vec<MaterialPropertiesUpdate>,
stage_phases: Vec<MaterialStagePhaseUpdate>,
}
fn sample_material_animations(
bindings: &mut [MaterialAnimationBinding], bindings: &mut [MaterialAnimationBinding],
materials: &[VulkanStaticMaterial], materials: &[VulkanStaticMaterial],
renderer: &mut VulkanSmokeRenderer,
clock_ms: u32, clock_ms: u32,
) -> Result<HashMap<usize, ActiveMaterialState>, String> { ) -> Result<MaterialFrameUpdate, String> {
// Keep the resolved selector beside the draw range for this frame. The let mut update = MaterialFrameUpdate::default();
// renderer's material arrays are keyed by selector, so any later for (binding_index, binding) in bindings.iter_mut().enumerate() {
// environment pass that writes alpha or lighting must target the phase
// variant selected above rather than the material that happened to be
// active when the range was allocated.
let mut active_materials = HashMap::new();
for binding in bindings {
let base_sample = binding.base.sample(binding.initial_phase_index, clock_ms)?; let base_sample = binding.base.sample(binding.initial_phase_index, clock_ms)?;
let detail_sample = binding let detail_sample = binding
.detail .detail
@@ -802,7 +833,7 @@ fn update_material_animations(
.map(|overlay_detail| overlay_detail.sample(binding.initial_phase_index, clock_ms)) .map(|overlay_detail| overlay_detail.sample(binding.initial_phase_index, clock_ms))
.transpose()?; .transpose()?;
let mut updated_materials = HashSet::new(); let mut updated_materials = HashSet::new();
for variant in &mut binding.variants { for variant in &binding.variants {
let (material_index, pipeline_category) = let (material_index, pipeline_category) =
selected_material_phase(variant, base_sample.phase_index, |index| { selected_material_phase(variant, base_sample.phase_index, |index| {
materials materials
@@ -810,25 +841,19 @@ fn update_material_animations(
.map(|material| material.pipeline_category) .map(|material| material.pipeline_category)
})?; })?;
for &range_index in &variant.range_indices { for &range_index in &variant.range_indices {
active_materials.insert( update.active_materials.insert(
range_index, range_index,
ActiveMaterialState { ActiveMaterialState {
material_index, material_index,
diffuse_alpha: base_sample.coefficients.opacity, diffuse_alpha: base_sample.coefficients.opacity,
}, },
); );
renderer update.range_selections.push(MaterialRangeSelection {
.set_draw_range_material(range_index, material_index) range_index,
.map_err(|error| format!("update MAT0 material draw selector: {error}"))?; material_index,
if variant.pipeline_category != pipeline_category { pipeline_category,
renderer });
.set_draw_range_material_category(range_index, pipeline_category)
.map_err(|error| {
format!("update MAT0 material pipeline category: {error}")
})?;
} }
}
variant.pipeline_category = pipeline_category;
if !updated_materials.insert(material_index) { if !updated_materials.insert(material_index) {
continue; continue;
} }
@@ -852,87 +877,16 @@ fn update_material_animations(
overlay_coefficients.map(|coefficients| coefficients.additive_rgb), overlay_coefficients.map(|coefficients| coefficients.additive_rgb),
) )
}; };
renderer if !material.lightmap_mode && binding.stage_textures.is_some() {
.set_material_alphas( update.stage_phases.push(MaterialStagePhaseUpdate {
binding_index,
material_index, material_index,
base_coefficients.opacity.clamp(0.0, 1.0), phases: [
overlay_coefficients
.map_or(0.0, |coefficients| coefficients.opacity.clamp(0.0, 1.0)),
)
.map_err(|error| format!("update MAT0 material alpha: {error}"))?;
renderer
.set_material_lighting_with_overlay(
material_index,
base_directional,
base_additive,
overlay_directional,
overlay_additive,
)
.map_err(|error| format!("update MAT0 material lighting: {error}"))?;
renderer
.set_material_specular(
material_index,
base_coefficients.specular_rgb,
base_coefficients.power,
overlay_coefficients.map(|coefficients| coefficients.specular_rgb),
overlay_coefficients.map(|coefficients| coefficients.power),
)
.map_err(|error| format!("update MAT0 material specular: {error}"))?;
// Detail and overlay textures have independent MAT0 phase
// selectors. Their descriptor images are updated only when a
// selected base material sees a new stage tuple; this keeps the
// steady-state frame path from idling Vulkan for every draw.
if !material.lightmap_mode {
if let Some(stage_textures) = binding.stage_textures.as_mut() {
let stage_phases = [
detail_sample.map_or(0, |sample| sample.phase_index), detail_sample.map_or(0, |sample| sample.phase_index),
overlay_sample.map_or(0, |sample| sample.phase_index), overlay_sample.map_or(0, |sample| sample.phase_index),
overlay_detail_sample.map_or(0, |sample| sample.phase_index), overlay_detail_sample.map_or(0, |sample| sample.phase_index),
]; ],
if stage_textures });
.applied_material_phases
.get(&material_index)
.copied()
!= Some(stage_phases)
{
let detail_texture = stage_textures
.detail
.as_ref()
.and_then(|textures| textures.get(stage_phases[0]))
.cloned();
let overlay_texture = stage_textures
.overlay
.as_ref()
.and_then(|textures| textures.get(stage_phases[1]))
.cloned();
let overlay_detail_texture = stage_textures
.overlay_detail
.as_ref()
.and_then(|textures| textures.get(stage_phases[2]))
.cloned();
if let Some(texture) = detail_texture.as_ref() {
renderer
.update_material_texture(material_index, 1, texture)
.map_err(|error| format!("update MAT0 detail texture: {error}"))?;
}
if let Some(texture) = overlay_texture.as_ref() {
renderer
.update_material_texture(material_index, 2, texture)
.map_err(|error| format!("update MAT0 overlay texture: {error}"))?;
}
if let Some(texture) = overlay_detail_texture.as_ref() {
renderer
.update_material_texture(material_index, 3, texture)
.map_err(|error| {
format!("update MAT0 overlay detail texture: {error}")
})?;
}
stage_textures
.applied_material_phases
.insert(material_index, stage_phases);
}
}
} }
let overlay_uv = overlay_sample let overlay_uv = overlay_sample
.map(|sample| { .map(|sample| {
@@ -980,15 +934,113 @@ fn update_material_animations(
let base_uv = binding let base_uv = binding
.base .base
.checked_page_uv_transform(base_sample.phase_index)?; .checked_page_uv_transform(base_sample.phase_index)?;
renderer update.properties.push(MaterialPropertiesUpdate {
.set_material_uv_transforms(
material_index, material_index,
[base_uv, detail_uv, overlay_uv, overlay_detail_uv], diffuse_alpha: base_coefficients.opacity.clamp(0.0, 1.0),
overlay_alpha: overlay_coefficients
.map_or(0.0, |coefficients| coefficients.opacity.clamp(0.0, 1.0)),
base_directional,
base_additive,
overlay_directional,
overlay_additive,
specular_rgb: base_coefficients.specular_rgb,
specular_power: base_coefficients.power,
overlay_specular_rgb: overlay_coefficients
.map(|coefficients| coefficients.specular_rgb),
overlay_power: overlay_coefficients.map(|coefficients| coefficients.power),
uv_transforms: [base_uv, detail_uv, overlay_uv, overlay_detail_uv],
});
}
}
Ok(update)
}
fn apply_material_frame_update(
update: &MaterialFrameUpdate,
bindings: &mut [MaterialAnimationBinding],
renderer: &mut VulkanSmokeRenderer,
) -> Result<(), String> {
for selection in &update.range_selections {
renderer
.set_draw_range_material(selection.range_index, selection.material_index)
.map_err(|error| format!("update MAT0 material draw selector: {error}"))?;
renderer
.set_draw_range_material_category(selection.range_index, selection.pipeline_category)
.map_err(|error| format!("update MAT0 material pipeline category: {error}"))?;
}
for properties in &update.properties {
renderer
.set_material_alphas(
properties.material_index,
properties.diffuse_alpha,
properties.overlay_alpha,
) )
.map_err(|error| format!("update MAT0 material alpha: {error}"))?;
renderer
.set_material_lighting_with_overlay(
properties.material_index,
properties.base_directional,
properties.base_additive,
properties.overlay_directional,
properties.overlay_additive,
)
.map_err(|error| format!("update MAT0 material lighting: {error}"))?;
renderer
.set_material_specular(
properties.material_index,
properties.specular_rgb,
properties.specular_power,
properties.overlay_specular_rgb,
properties.overlay_power,
)
.map_err(|error| format!("update MAT0 material specular: {error}"))?;
renderer
.set_material_uv_transforms(properties.material_index, properties.uv_transforms)
.map_err(|error| format!("update MAT0 material Page transform: {error}"))?; .map_err(|error| format!("update MAT0 material Page transform: {error}"))?;
} }
for stage_update in &update.stage_phases {
let stage_textures = bindings
.get_mut(stage_update.binding_index)
.and_then(|binding| binding.stage_textures.as_mut())
.ok_or_else(|| "MAT0 stage texture binding disappeared".to_string())?;
if stage_textures
.applied_material_phases
.get(&stage_update.material_index)
.copied()
== Some(stage_update.phases)
{
continue;
} }
Ok(active_materials) let textures = [
stage_textures
.detail
.as_ref()
.and_then(|textures| textures.get(stage_update.phases[0])),
stage_textures
.overlay
.as_ref()
.and_then(|textures| textures.get(stage_update.phases[1])),
stage_textures
.overlay_detail
.as_ref()
.and_then(|textures| textures.get(stage_update.phases[2])),
];
for (stage, texture) in textures.into_iter().enumerate() {
if let Some(texture) = texture {
renderer
.update_material_texture(stage_update.material_index, stage + 1, texture)
.map_err(|error| match stage {
0 => format!("update MAT0 detail texture: {error}"),
1 => format!("update MAT0 overlay texture: {error}"),
_ => format!("update MAT0 overlay detail texture: {error}"),
})?;
}
}
stage_textures
.applied_material_phases
.insert(stage_update.material_index, stage_update.phases);
}
Ok(())
} }
fn native_world_transparent(range: &VulkanStaticDrawRange, diffuse_alpha: f32) -> bool { fn native_world_transparent(range: &VulkanStaticDrawRange, diffuse_alpha: f32) -> bool {
@@ -1449,7 +1501,6 @@ fn append_environment_material(
variants: vec![MaterialVariantSet { variants: vec![MaterialVariantSet {
phase_material_indices, phase_material_indices,
range_indices: Vec::new(), range_indices: Vec::new(),
pipeline_category: phases[0].pipeline_category,
}], }],
base: MaterialPhaseBinding { base: MaterialPhaseBinding {
document, document,
@@ -2678,23 +2729,17 @@ impl EnvironmentGpuScene {
fn update( fn update(
&mut self, &mut self,
mesh: &mut VulkanStaticMesh, mesh: &mut VulkanStaticMesh,
materials: &[VulkanStaticMaterial],
renderer: &mut VulkanSmokeRenderer, renderer: &mut VulkanSmokeRenderer,
material_update: &MaterialFrameUpdate,
sky_frame: &SkyFrame, sky_frame: &SkyFrame,
sky_mesh: &SkyMesh, sky_mesh: &SkyMesh,
environment_frame: &EnvironmentFrame, environment_frame: &EnvironmentFrame,
camera: &PreviewCameraFrame, camera: &PreviewCameraFrame,
viewport: [f32; 2], viewport: [f32; 2],
material_clock_ms: u32,
terrain: &TerrainWorld, terrain: &TerrainWorld,
sun_occlusion_range_indices: &[usize], sun_occlusion_range_indices: &[usize],
) -> Result<(), String> { ) -> Result<(), String> {
let active_materials = update_material_animations( let active_materials = &material_update.active_materials;
&mut self.material_animations,
materials,
renderer,
material_clock_ms,
)?;
// Resolve the native glare once for the frame. CSky feeds the // Resolve the native glare once for the frame. CSky feeds the
// boosted primary color back into the light manager and uses the // boosted primary color back into the light manager and uses the
// positive boost delta for both the screen-gradient horizon floor and // positive boost delta for both the screen-gradient horizon floor and
@@ -5033,7 +5078,6 @@ fn static_preview_terrain_base_materials(
variants: vec![MaterialVariantSet { variants: vec![MaterialVariantSet {
phase_material_indices, phase_material_indices,
range_indices: Vec::new(), range_indices: Vec::new(),
pipeline_category: base_phases[initial_phase_index].1.pipeline_category,
}], }],
base: MaterialPhaseBinding { base: MaterialPhaseBinding {
document: base_document, document: base_document,
@@ -5184,7 +5228,6 @@ fn static_preview_component_materials(
variants: vec![MaterialVariantSet { variants: vec![MaterialVariantSet {
phase_material_indices, phase_material_indices,
range_indices: Vec::new(), range_indices: Vec::new(),
pipeline_category: material.pipeline_category,
}], }],
base: MaterialPhaseBinding { base: MaterialPhaseBinding {
document: material.mat0.clone(), document: material.mat0.clone(),
@@ -5299,7 +5342,7 @@ fn append_static_preview_component_with_lightmaps(
// advances to another phase. The range's original selector identifies // advances to another phase. The range's original selector identifies
// the base binding, while the lightmap selector identifies the secondary // the base binding, while the lightmap selector identifies the secondary
// texture. Keep the generated phase family and the binding/variant it // texture. Keep the generated phase family and the binding/variant it
// belongs to so update_material_animations can rebind every frame. // belongs to so the pending MAT0 packet can rebind every frame.
let mut lightmap_materials: HashMap<(usize, u8), (Vec<usize>, usize, usize)> = HashMap::new(); let mut lightmap_materials: HashMap<(usize, u8), (Vec<usize>, usize, usize)> = HashMap::new();
for range in component.draw_ranges { for range in component.draw_ranges {
let source_material_index = selector_remap let source_material_index = selector_remap
@@ -5331,7 +5374,6 @@ fn append_static_preview_component_with_lightmaps(
"static preview material selector {source_material_index} has no MAT0 animation binding" "static preview material selector {source_material_index} has no MAT0 animation binding"
) )
})?; })?;
let initial_phase_index = bindings[binding_index].initial_phase_index;
let lightmap = lightmap_textures let lightmap = lightmap_textures
.get(usize::from(range.lightmap_index)) .get(usize::from(range.lightmap_index))
.expect("lightmap presence checked above"); .expect("lightmap presence checked above");
@@ -5371,16 +5413,6 @@ fn append_static_preview_component_with_lightmaps(
materials.push(lightmap_material); materials.push(lightmap_material);
phase_material_indices.push(usize::from(index)); phase_material_indices.push(usize::from(index));
} }
let initial_selector = phase_material_indices
.get(initial_phase_index)
.copied()
.ok_or_else(|| "static preview lightmap phase table is empty".to_string())?;
let pipeline_category = materials
.get(initial_selector)
.map(|material| material.pipeline_category)
.ok_or_else(|| {
"static preview lightmap material selector is unavailable".to_string()
})?;
let bindings = material_animations.as_deref_mut().ok_or_else(|| { let bindings = material_animations.as_deref_mut().ok_or_else(|| {
"static preview lightmap animation storage is unavailable".to_string() "static preview lightmap animation storage is unavailable".to_string()
})?; })?;
@@ -5388,7 +5420,6 @@ fn append_static_preview_component_with_lightmaps(
bindings[binding_index].variants.push(MaterialVariantSet { bindings[binding_index].variants.push(MaterialVariantSet {
phase_material_indices: phase_material_indices.clone(), phase_material_indices: phase_material_indices.clone(),
range_indices: Vec::new(), range_indices: Vec::new(),
pipeline_category,
}); });
let value = (phase_material_indices, binding_index, variant_index); let value = (phase_material_indices, binding_index, variant_index);
lightmap_materials.insert(key, value.clone()); lightmap_materials.insert(key, value.clone());
@@ -5580,6 +5611,8 @@ struct SceneFrameUpdate {
camera: PreviewCameraFrame, camera: PreviewCameraFrame,
viewport: [f32; 2], viewport: [f32; 2],
aspect: f32, aspect: f32,
material_update: MaterialFrameUpdate,
environment_material_update: Option<MaterialFrameUpdate>,
environment: Option<DynamicEnvironmentFrame>, environment: Option<DynamicEnvironmentFrame>,
} }
@@ -6123,34 +6156,6 @@ impl StaticVulkanApp {
if self.renderer.is_none() { if self.renderer.is_none() {
return Ok(None); return Ok(None);
} }
let active_materials = {
let renderer = self
.renderer
.as_mut()
.ok_or_else(|| "native Vulkan renderer was not initialized".to_string())?;
update_material_animations(
&mut self.material_animations,
&self.materials,
renderer,
self.material_time_ms,
)?
};
{
let renderer = self
.renderer
.as_mut()
.ok_or_else(|| "native Vulkan renderer was not initialized".to_string())?;
Self::update_world_transparency(
&self.mesh,
&self.materials,
&self.world_range_indices,
&self.world_range_index_counts,
&self.native_handedness_by_range,
self.view_camera.position,
&active_materials,
renderer,
)?;
}
let camera = self.view_camera; let camera = self.view_camera;
if let Some(audio) = self.audio.as_mut() { if let Some(audio) = self.audio.as_mut() {
audio audio
@@ -6180,13 +6185,13 @@ impl StaticVulkanApp {
if let Some(frame) = environment_frame.as_ref() { if let Some(frame) = environment_frame.as_ref() {
self.environment_primitive_count = frame.environment_frame.primitives.len(); self.environment_primitive_count = frame.environment_frame.primitives.len();
} }
self.apply_environment_view(environment_frame.as_ref(), camera, viewport)?;
Ok(environment_frame) Ok(environment_frame)
} }
fn apply_environment_view( fn apply_environment_view(
&mut self, &mut self,
frame: Option<&DynamicEnvironmentFrame>, frame: Option<&DynamicEnvironmentFrame>,
environment_material_update: Option<&MaterialFrameUpdate>,
camera: PreviewCameraFrame, camera: PreviewCameraFrame,
viewport: [f32; 2], viewport: [f32; 2],
) -> Result<(), String> { ) -> Result<(), String> {
@@ -6205,16 +6210,17 @@ impl StaticVulkanApp {
frame.absolute_time_seconds, frame.absolute_time_seconds,
); );
if let Some(environment_gpu) = self.environment_gpu.as_mut() { if let Some(environment_gpu) = self.environment_gpu.as_mut() {
let material_update = environment_material_update
.ok_or_else(|| "sampled environment material state is missing".to_string())?;
environment_gpu.update( environment_gpu.update(
&mut self.mesh, &mut self.mesh,
&self.materials,
renderer, renderer,
material_update,
&sky_frame, &sky_frame,
&frame.sky_mesh, &frame.sky_mesh,
&frame.environment_frame, &frame.environment_frame,
&camera, &camera,
viewport, viewport,
self.material_time_ms,
&self.terrain, &self.terrain,
&self.sun_occlusion_range_indices, &self.sun_occlusion_range_indices,
)?; )?;
@@ -6285,7 +6291,6 @@ impl StaticVulkanApp {
.environment_frame .environment_frame
.reproject_for_camera(environment_camera); .reproject_for_camera(environment_camera);
} }
self.apply_environment_view(update.environment.as_ref(), camera, viewport)?;
update.camera = camera; update.camera = camera;
update.viewport = viewport; update.viewport = viewport;
update.aspect = aspect; update.aspect = aspect;
@@ -6293,6 +6298,52 @@ impl StaticVulkanApp {
Ok(()) Ok(())
} }
fn apply_pending_scene_update(&mut self) -> Result<(), String> {
let Some(frame) = self.lifecycle.pending.take() else {
return Ok(());
};
let result = (|| {
{
let Some(renderer) = self.renderer.as_mut() else {
return Err("native Vulkan renderer was not initialized".to_string());
};
apply_material_frame_update(
&frame.material_update,
&mut self.material_animations,
renderer,
)?;
Self::update_world_transparency(
&self.mesh,
&self.materials,
&self.world_range_indices,
&self.world_range_index_counts,
&self.native_handedness_by_range,
frame.camera.position,
&frame.material_update.active_materials,
renderer,
)?;
if let Some(material_update) = frame.environment_material_update.as_ref() {
let environment_gpu = self.environment_gpu.as_mut().ok_or_else(|| {
"sampled environment material state lost its owner".to_string()
})?;
apply_material_frame_update(
material_update,
&mut environment_gpu.material_animations,
renderer,
)?;
}
}
self.apply_environment_view(
frame.environment.as_ref(),
frame.environment_material_update.as_ref(),
frame.camera,
frame.viewport,
)
})();
self.lifecycle.pending = Some(frame);
result
}
fn sample_frame_clock(&mut self) -> FrameClockSample { fn sample_frame_clock(&mut self) -> FrameClockSample {
if let Some(target_ms) = self.control_clock_target_ms { if let Some(target_ms) = self.control_clock_target_ms {
return self.lifecycle.sample_replay(target_ms, self.target_frames); return self.lifecycle.sample_replay(target_ms, self.target_frames);
@@ -6324,12 +6375,30 @@ impl StaticVulkanApp {
self.update_free_camera(sample.delta_seconds, viewport)?; self.update_free_camera(sample.delta_seconds, viewport)?;
self.update_material_clock(sample); self.update_material_clock(sample);
self.update_controlled_component_geometry(sample)?; self.update_controlled_component_geometry(sample)?;
let material_update = sample_material_animations(
&mut self.material_animations,
&self.materials,
self.material_time_ms,
)?;
let environment_material_update = self
.environment_gpu
.as_mut()
.map(|environment_gpu| {
sample_material_animations(
&mut environment_gpu.material_animations,
&self.materials,
self.material_time_ms,
)
})
.transpose()?;
let environment = self.update_environment(sample, viewport)?; let environment = self.update_environment(sample, viewport)?;
Ok(SceneFrameUpdate { Ok(SceneFrameUpdate {
sample, sample,
camera: self.view_camera, camera: self.view_camera,
viewport, viewport,
aspect, aspect,
material_update,
environment_material_update,
environment, environment,
}) })
} }
@@ -6678,6 +6747,13 @@ impl ApplicationHandler for StaticVulkanApp {
let size = window.inner_size(); let size = window.inner_size();
size.width != 0 && size.height != 0 size.width != 0 && size.height != 0
}); });
if drawable_available {
if let Err(error) = self.apply_pending_scene_update() {
self.error = Some(error);
event_loop.exit();
return;
}
}
let outcome = { let outcome = {
let Some(renderer) = self.renderer.as_mut() else { let Some(renderer) = self.renderer.as_mut() else {
self.error = Some("native Vulkan renderer was not initialized".to_string()); self.error = Some("native Vulkan renderer was not initialized".to_string());
@@ -7080,6 +7156,168 @@ fn usage() -> String {
mod tests { mod tests {
use super::*; use super::*;
use fparkan_fx::environment::{WeatherInput, WeatherState}; use fparkan_fx::environment::{WeatherInput, WeatherState};
use fparkan_material::{MaterialAnimationBlock, MaterialKey};
fn material_test_phase(parameters: [u8; 18]) -> MaterialPhase {
MaterialPhase {
parameters,
texture_raw: [0; 16],
}
}
fn random_material_binding(range_index: usize) -> MaterialAnimationBinding {
let mut overlay_phase = [0; 18];
overlay_phase[..3].copy_from_slice(&[100, 50, 25]);
overlay_phase[3] = 25;
overlay_phase[4..7].copy_from_slice(&[30, 60, 90]);
overlay_phase[8..11].copy_from_slice(&[40, 80, 120]);
overlay_phase[16] = 5;
let mut active_phase = [0; 18];
active_phase[..3].copy_from_slice(&[25, 50, 75]);
active_phase[3] = 75;
active_phase[4..7].copy_from_slice(&[64, 128, 192]);
active_phase[8..11].copy_from_slice(&[100, 150, 200]);
active_phase[16] = 9;
active_phase[17] = 1;
let document = Mat0Document {
version: 0,
animation_block_count: 1,
phases: vec![
material_test_phase(overlay_phase),
material_test_phase(active_phase),
],
prefix: Vec::new(),
header_opaque: [0; 2],
animation_blocks: vec![MaterialAnimationBlock {
header_raw: 3,
keys: vec![
MaterialKey {
k0: 0,
k1: 20,
k2: 0,
},
MaterialKey {
k0: 1,
k1: 40,
k2: 0,
},
],
bytes: Vec::new(),
}],
};
let frozen_binding = |phase_index: usize, uv_transform: [f32; 4]| MaterialPhaseBinding {
document: document.clone(),
animation_block_index: 0,
wear_row_start_ms: 0,
random_state: 0,
frozen_phase_index: Some(phase_index),
phase_uv_transforms: vec![uv_transform; 2],
};
let texture = solid_environment_texture([255; 4]);
MaterialAnimationBinding {
initial_phase_index: 0,
variants: vec![MaterialVariantSet {
phase_material_indices: vec![4, 5],
range_indices: vec![range_index],
}],
base: MaterialPhaseBinding {
document: document.clone(),
animation_block_index: 0,
wear_row_start_ms: 0,
random_state: 0,
frozen_phase_index: None,
phase_uv_transforms: vec![[0.0, 0.0, 0.5, 0.5], [0.5, 0.25, 0.25, 0.5]],
},
detail: Some(frozen_binding(1, [0.1, 0.2, 0.3, 0.4])),
overlay: Some(frozen_binding(0, [0.3, 0.4, 0.5, 0.6])),
overlay_detail: Some(frozen_binding(1, [0.6, 0.7, 0.8, 0.9])),
stage_textures: Some(MaterialStageTextures {
detail: Some(vec![texture.clone(), texture.clone()]),
overlay: Some(vec![texture.clone()]),
overlay_detail: Some(vec![texture.clone(), texture]),
applied_material_phases: HashMap::from([(5, [1, 0, 1])]),
}),
}
}
fn packet_test_materials() -> Vec<VulkanStaticMaterial> {
let mut materials = Vec::new();
for _ in 0..6 {
append_environment_gradient_material(&mut materials).expect("test material");
}
materials[4].pipeline_category = 1;
materials[5].pipeline_category = 3;
materials
}
#[test]
fn sampled_material_packet_keeps_native_selector_properties_and_stage_phases() {
let materials = packet_test_materials();
let mut bindings = vec![random_material_binding(7)];
let packet = sample_material_animations(&mut bindings, &materials, 500)
.expect("sample random MAT0 state");
assert_eq!(
packet.range_selections,
vec![MaterialRangeSelection {
range_index: 7,
material_index: 5,
pipeline_category: 3,
}]
);
assert_eq!(
packet.active_materials.get(&7),
Some(&ActiveMaterialState {
material_index: 5,
diffuse_alpha: 0.75,
})
);
let properties = packet.properties[0];
assert_eq!(
(properties.diffuse_alpha, properties.overlay_alpha),
(0.75, 0.25)
);
assert_eq!(
(properties.base_directional, properties.base_additive),
(
[64.0 / 255.0, 128.0 / 255.0, 192.0 / 255.0],
[25.0 / 255.0, 50.0 / 255.0, 75.0 / 255.0],
)
);
assert_eq!(
(properties.overlay_directional, properties.overlay_additive),
(
Some([30.0 / 255.0, 60.0 / 255.0, 90.0 / 255.0]),
Some([100.0 / 255.0, 50.0 / 255.0, 25.0 / 255.0]),
)
);
assert_eq!(
(properties.specular_rgb, properties.specular_power),
([100.0 / 255.0, 150.0 / 255.0, 200.0 / 255.0], 9)
);
assert_eq!(
(properties.overlay_specular_rgb, properties.overlay_power),
(Some([40.0 / 255.0, 80.0 / 255.0, 120.0 / 255.0]), Some(5))
);
assert_eq!(
properties.uv_transforms,
[
[0.5, 0.25, 0.25, 0.5],
[0.1, 0.2, 0.3, 0.4],
[0.3, 0.4, 0.5, 0.6],
[0.6, 0.7, 0.8, 0.9],
]
);
assert_eq!(
packet.stage_phases,
vec![MaterialStagePhaseUpdate {
binding_index: 0,
material_index: 5,
phases: [1, 0, 1],
}]
);
assert_eq!(bindings[0].base.random_state, 2_531_011);
}
fn control_pose_test_model() -> ModelAsset { fn control_pose_test_model() -> ModelAsset {
let mut nodes = vec![u8::MAX; 38]; let mut nodes = vec![u8::MAX; 38];
@@ -7482,6 +7720,18 @@ mod tests {
); );
app.free_camera = Some(free_camera); app.free_camera = Some(free_camera);
app.pressed_keys.insert(KeyCode::KeyW); app.pressed_keys.insert(KeyCode::KeyW);
app.materials = packet_test_materials();
app.material_animations = vec![random_material_binding(0)];
let empty_range = app.shadow_range;
app.environment_gpu = Some(EnvironmentGpuScene {
material_animations: vec![random_material_binding(1)],
sky_layers: Vec::new(),
sprites: Vec::new(),
rain: empty_range,
snow: empty_range,
lightning: empty_range,
lightning_material_index: 0,
});
let start = app.last_tick; let start = app.last_tick;
let sample = app.sample_live_frame_clock_at(start + Duration::from_millis(225)); let sample = app.sample_live_frame_clock_at(start + Duration::from_millis(225));
assert_eq!(sample.time_ms, 225); assert_eq!(sample.time_ms, 225);
@@ -7489,6 +7739,17 @@ mod tests {
let mut update = app let mut update = app
.prepare_scene_update(sample, [1280.0, 720.0]) .prepare_scene_update(sample, [1280.0, 720.0])
.expect("apply one full live tick to the scene"); .expect("apply one full live tick to the scene");
assert!(update.environment.is_none());
assert!(update.environment_material_update.is_some());
let world_material_packet = update.material_update.clone();
let sky_material_packet = update.environment_material_update.clone();
assert_eq!(app.material_animations[0].base.random_state, 2_531_011);
assert_eq!(
app.environment_gpu.as_ref().unwrap().material_animations[0]
.base
.random_state,
2_531_011
);
let expected_resized_camera = app let expected_resized_camera = app
.free_camera .free_camera
.expect("free-flight input controller") .expect("free-flight input controller")
@@ -7560,8 +7821,37 @@ mod tests {
let controller_after_input = app.free_camera.unwrap(); let controller_after_input = app.free_camera.unwrap();
app.refresh_pending_viewport([800.0, 600.0]) app.refresh_pending_viewport([800.0, 600.0])
.expect("refresh only viewport-dependent state after recreation"); .expect("refresh only viewport-dependent state after recreation");
let refreshed = app.lifecycle.pending.as_ref().expect("pending sample"); let refreshed = app
.lifecycle
.pending
.as_ref()
.expect("pending sample")
.clone();
assert_eq!(refreshed.sample, sample); assert_eq!(refreshed.sample, sample);
assert_eq!(refreshed.material_update, world_material_packet);
assert_eq!(refreshed.environment_material_update, sky_material_packet);
assert_eq!(app.material_animations[0].base.random_state, 2_531_011);
assert_eq!(
app.environment_gpu.as_ref().unwrap().material_animations[0]
.base
.random_state,
2_531_011
);
app.refresh_pending_viewport([1024.0, 768.0])
.expect("refresh the same pending sample a second time");
let repeated = app.lifecycle.pending.as_ref().expect("pending sample");
assert_eq!(repeated.sample, sample);
assert_eq!(repeated.material_update, world_material_packet);
assert_eq!(repeated.environment_material_update, sky_material_packet);
assert_eq!(repeated.viewport, [1024.0, 768.0]);
assert_eq!(repeated.aspect, 4.0 / 3.0);
assert_eq!(app.material_animations[0].base.random_state, 2_531_011);
assert_eq!(
app.environment_gpu.as_ref().unwrap().material_animations[0]
.base
.random_state,
2_531_011
);
assert_eq!(refreshed.viewport, [800.0, 600.0]); assert_eq!(refreshed.viewport, [800.0, 600.0]);
assert_eq!(refreshed.aspect, 4.0 / 3.0); assert_eq!(refreshed.aspect, 4.0 / 3.0);
assert_eq!(refreshed.camera.position, update.camera.position); assert_eq!(refreshed.camera.position, update.camera.position);
@@ -7635,6 +7925,25 @@ mod tests {
reprojected_counts[slot] += 1; reprojected_counts[slot] += 1;
} }
assert!(reprojected_counts.iter().all(|count| *count > 0)); assert!(reprojected_counts.iter().all(|count| *count > 0));
assert_eq!(
app.apply_pending_scene_update().unwrap_err(),
"native Vulkan renderer was not initialized"
);
let restored = app
.lifecycle
.pending
.as_ref()
.expect("restored pending sample");
assert_eq!(restored.sample, sample);
assert_eq!(restored.material_update, world_material_packet);
assert_eq!(restored.environment_material_update, sky_material_packet);
assert_eq!(app.material_animations[0].base.random_state, 2_531_011);
assert_eq!(
app.environment_gpu.as_ref().unwrap().material_animations[0]
.base
.random_state,
2_531_011
);
assert_eq!(app.mesh.vertices, b_vertices); assert_eq!(app.mesh.vertices, b_vertices);
assert_eq!(app.shadow_casters[0].triangles, b_triangles); assert_eq!(app.shadow_casters[0].triangles, b_triangles);
assert_eq!( assert_eq!(
@@ -7710,6 +8019,8 @@ mod tests {
camera, camera,
viewport, viewport,
aspect, aspect,
material_update: MaterialFrameUpdate::default(),
environment_material_update: None,
environment: None, environment: None,
} }
} }
@@ -7866,7 +8177,6 @@ mod tests {
variants: vec![MaterialVariantSet { variants: vec![MaterialVariantSet {
phase_material_indices: vec![10], phase_material_indices: vec![10],
range_indices: vec![0, 2], range_indices: vec![0, 2],
pipeline_category: 0,
}], }],
base: empty_binding(), base: empty_binding(),
detail: None, detail: None,
@@ -7927,7 +8237,6 @@ mod tests {
let variant = MaterialVariantSet { let variant = MaterialVariantSet {
phase_material_indices: vec![10, 11], phase_material_indices: vec![10, 11],
range_indices: vec![3], range_indices: vec![3],
pipeline_category: 1,
}; };
let category_for = |material_index| match material_index { let category_for = |material_index| match material_index {
10 => Some(1), 10 => Some(1),
@@ -8195,7 +8504,6 @@ mod tests {
variants: vec![MaterialVariantSet { variants: vec![MaterialVariantSet {
phase_material_indices: vec![10, 11], phase_material_indices: vec![10, 11],
range_indices: Vec::new(), range_indices: Vec::new(),
pipeline_category: 0,
}], }],
base: MaterialPhaseBinding { base: MaterialPhaseBinding {
document: empty_mat0, document: empty_mat0,
@@ -8216,7 +8524,6 @@ mod tests {
bindings[binding_index].variants.push(MaterialVariantSet { bindings[binding_index].variants.push(MaterialVariantSet {
phase_material_indices: vec![20, 21], phase_material_indices: vec![20, 21],
range_indices: vec![7], range_indices: vec![7],
pipeline_category: 0,
}); });
let lightmap_variant = &bindings[binding_index].variants[1]; let lightmap_variant = &bindings[binding_index].variants[1];
+8
View File
@@ -33,6 +33,14 @@ input controller и применяются следующим sample. Экран
шага симуляции окружения и повторной отправки звуковых событий. Пауза нулевого шага симуляции окружения и повторной отправки звуковых событий. Пауза нулевого
drawable не добавляет время. drawable не добавляет время.
Pending frame хранит пакет MAT0 для мира и, при наличии GPU-окружения, отдельный
пакет неба. Пакеты содержат выбранные селектор и категорию конвейера, фазовые
коэффициенты материала, четыре UV-преобразования и кортеж фаз дополнительных
текстур. Повторное применение пакета при retry или нескольких resize не выбирает
фазу заново и не продвигает RNG. Перед отрисовкой сначала применяются материалы
мира и их порядок прозрачности, затем MAT0 неба, после чего viewport-зависимый
проход неба записывает свои переопределения.
## Элемент отрисовки ## Элемент отрисовки
Подготовленный элемент отрисовки содержит: Подготовленный элемент отрисовки содержит: