aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
diff options
context:
space:
mode:
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi/smoke.rs')
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs1028
1 files changed, 703 insertions, 325 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
index 522f95f..ef19ee8 100644
--- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs
@@ -210,83 +210,46 @@ impl VulkanSmokeRenderer {
rgba8: vec![255, 255, 255, 0],
mip_levels: Vec::new(),
};
- // Fallback stages are immutable compatibility images. Keep one
- // allocation for each fallback kind and map every logical material
- // slot to it. Authored images remain one-per-slot because dynamic
- // phase uploads must never couple two materials.
+ // Equal source pixels and authored mip chains can share one image.
+ // Dynamic updates detach their slot before changing a shared image.
let texture_sources = if create_info.materials.is_empty() {
vec![
- (&fallback_texture, Some(0_usize)),
- (&fallback_detail_texture, Some(1_usize)),
- (&fallback_overlay_texture, Some(2_usize)),
- (&fallback_detail_texture, Some(1_usize)),
+ &fallback_texture,
+ &fallback_detail_texture,
+ &fallback_overlay_texture,
+ &fallback_detail_texture,
]
} else {
let mut sources = Vec::with_capacity(create_info.materials.len() * 4);
for material in &create_info.materials {
- sources.push((&material.texture, None));
- sources.push((
+ sources.push(&material.texture);
+ sources.push(
material
.detail_texture
.as_ref()
- .map_or(&fallback_detail_texture, |texture| texture),
- material.detail_texture.is_none().then_some(1_usize),
- ));
- sources.push((
+ .unwrap_or(&fallback_detail_texture),
+ );
+ sources.push(
material
.overlay_texture
.as_ref()
- .map_or(&fallback_overlay_texture, |texture| texture),
- material.overlay_texture.is_none().then_some(2_usize),
- ));
- sources.push((
+ .unwrap_or(&fallback_overlay_texture),
+ );
+ sources.push(
material
.overlay_detail_texture
.as_ref()
- .map_or(&fallback_detail_texture, |texture| texture),
- material.overlay_detail_texture.is_none().then_some(1_usize),
- ));
+ .unwrap_or(&fallback_detail_texture),
+ );
}
sources
};
- let mut textures = Vec::with_capacity(texture_sources.len());
- let mut texture_slot_indices = Vec::with_capacity(texture_sources.len());
- let mut fallback_image_indices = [None; 3];
- for (texture_source, fallback_kind) in texture_sources {
- let texture_index = if let Some(fallback_kind) = fallback_kind {
- if let Some(texture_index) = fallback_image_indices[fallback_kind] {
- texture_index
- } else {
- let image = match create_static_texture_image(
- &instance,
- &device,
- command_pool,
- texture_source,
- ) {
- Ok(image) => image,
- Err(error) => {
- for texture in &textures {
- destroy_allocated_image(&device, texture);
- }
- // SAFETY: These resources belong to this live device and are rolled back before it drops.
- unsafe { device.device().destroy_command_pool(command_pool, None) };
- destroy_allocated_buffer(&device, &index_buffer);
- destroy_allocated_buffer(&device, &vertex_buffer);
- return Err(error);
- }
- };
- let texture_index = textures.len();
- textures.push(image);
- fallback_image_indices[fallback_kind] = Some(texture_index);
- texture_index
- }
- } else {
- let image = match create_static_texture_image(
- &instance,
- &device,
- command_pool,
- texture_source,
- ) {
+ let (unique_sources, texture_slot_indices) = deduplicate_texture_sources(&texture_sources);
+ let mut textures = Vec::with_capacity(unique_sources.len());
+ for texture_source in unique_sources {
+ let image =
+ match create_static_texture_image(&instance, &device, command_pool, texture_source)
+ {
Ok(image) => image,
Err(error) => {
for texture in &textures {
@@ -299,12 +262,9 @@ impl VulkanSmokeRenderer {
return Err(error);
}
};
- let texture_index = textures.len();
- textures.push(image);
- texture_index
- };
- texture_slot_indices.push(texture_index);
+ textures.push(image);
}
+ let initial_texture_image_count = textures.len() as u64;
let material_alphas = if create_info.materials.is_empty() {
vec![[1.0, 0.0]]
} else {
@@ -397,6 +357,33 @@ impl VulkanSmokeRenderer {
.map(|material| material.material_index)
.collect()
};
+ let reflection_planes = super::reflection::mesh_reflection_planes(&create_info.mesh);
+ let unsupported_nonplanar = create_info
+ .mesh
+ .draw_ranges
+ .iter()
+ .zip(&reflection_planes)
+ .filter(|(range, plane)| range.terrain_reflection && plane.is_none())
+ .count();
+ let mut plane_heights = reflection_planes
+ .iter()
+ .flatten()
+ .map(|z| z.to_bits())
+ .collect::<Vec<_>>();
+ plane_heights.sort_unstable();
+ plane_heights.dedup();
+ if !plane_heights.is_empty() {
+ eprintln!("render_coverage planar_reflection_render_context_approx={} (native shifted window and bump UV; main-view depth range, sky/fog/FX and swapchain-sized target remain approximations; non-perspective cameras use mirror fallback)", plane_heights.len());
+ eprintln!("render_coverage planar_reflection_other_planes_unsupported={} (nearest active plane is reflected)", plane_heights.len().saturating_sub(1));
+ }
+ if unsupported_nonplanar != 0 {
+ eprintln!("render_coverage planar_reflection_nonplanar_unsupported={unsupported_nonplanar} (ordinary terrain fallback)");
+ }
+ let mut frame_uniforms = super::VulkanFrameUniforms::from_camera(create_info.camera);
+ if let Some(bounds) = super::reflection::mesh_reflection_bounds(&create_info.mesh) {
+ frame_uniforms.embm_bounds = bounds;
+ eprintln!("render_reflection embm_bounds={bounds:?}");
+ }
let mut renderer = Self {
instance: Some(instance),
validation,
@@ -404,6 +391,7 @@ impl VulkanSmokeRenderer {
device: Some(device),
swapchain: Some(swapchain),
command_pool,
+ pipeline_cache: vk::PipelineCache::null(),
swapchain_resources: None,
vertex_buffer: Some(vertex_buffer),
index_buffer: Some(index_buffer),
@@ -418,6 +406,7 @@ impl VulkanSmokeRenderer {
.map(|range| range.index_count)
.collect(),
draw_ranges: create_info.mesh.draw_ranges.clone(),
+ reflection_planes,
draw_texture_indices,
draw_order: (0..create_info.mesh.draw_ranges.len()).collect(),
draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()],
@@ -440,7 +429,7 @@ impl VulkanSmokeRenderer {
material_specular_dirty: true,
readback_enabled: false,
camera: create_info.camera,
- frame_uniforms: super::VulkanFrameUniforms::from_camera(create_info.camera),
+ frame_uniforms,
point_lights: Vec::new(),
point_lights_explicit: false,
frame_sync: Vec::new(),
@@ -464,8 +453,26 @@ impl VulkanSmokeRenderer {
readback_copy_count: 0,
readback_byte_count: 0,
readback_fnv1a64: 0,
+ mesh_vertex_upload_bytes: 0,
+ mesh_index_upload_bytes: 0,
+ mesh_full_upload_bytes: 0,
+ initial_texture_image_count,
+ texture_detach_count: 0,
+ pipeline_creation_count: 0,
},
};
+ // SAFETY: This cache is owned by the renderer and only used from its
+ // exclusive methods; it outlives every pipeline creation and swapchain.
+ renderer.pipeline_cache = unsafe {
+ renderer
+ .device_ref()?
+ .device()
+ .create_pipeline_cache(&vk::PipelineCacheCreateInfo::default(), None)
+ }
+ .map_err(|result| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkCreatePipelineCache",
+ result,
+ })?;
renderer.rebuild_swapchain_resources(false)?;
let device_ref = renderer.device_ref()?;
let swapchain_ref = renderer.swapchain_ref()?;
@@ -495,6 +502,12 @@ impl VulkanSmokeRenderer {
readback_copy_count: 0,
readback_byte_count: 0,
readback_fnv1a64: 0,
+ mesh_vertex_upload_bytes: 0,
+ mesh_index_upload_bytes: 0,
+ mesh_full_upload_bytes: 0,
+ initial_texture_image_count,
+ texture_detach_count: 0,
+ pipeline_creation_count: renderer.report.pipeline_creation_count,
};
Ok(renderer)
}
@@ -536,6 +549,22 @@ impl VulkanSmokeRenderer {
self.frame_uniforms
}
+ fn active_reflection_plane(&self) -> Option<f32> {
+ // shortcut: one target serves the nearest active water height; add per-plane targets
+ // when accepting maps whose visible water surfaces have different heights.
+ self.draw_ranges
+ .iter()
+ .zip(&self.reflection_planes)
+ .filter(|(range, _)| range.index_count != 0)
+ .filter_map(|(_, plane)| *plane)
+ .filter(|&z| z < self.frame_uniforms.camera_position[2])
+ .min_by(|a, b| {
+ (self.frame_uniforms.camera_position[2] - a)
+ .abs()
+ .total_cmp(&(self.frame_uniforms.camera_position[2] - b).abs())
+ })
+ }
+
/// Replaces the camera for subsequent frames without recreating GPU resources.
///
/// # Errors
@@ -628,7 +657,7 @@ impl VulkanSmokeRenderer {
///
/// The initial mesh owns enough vertices for the scene's dynamic slots;
/// callers update those slots in place as camera-relative environment
- /// geometry moves. The device is idled before the host-visible write so
+ /// geometry moves. Submitted readers finish before the host-visible write so
/// no submitted command can observe a partial update.
pub fn update_vertices(
&mut self,
@@ -639,15 +668,8 @@ impl VulkanSmokeRenderer {
context: "updated static mesh vertex count differs from allocation",
});
}
- // SAFETY: The renderer owns the queue and all submitted work; waiting
- // for idle makes the shared vertex allocation quiescent.
- unsafe { self.device_ref()?.device().device_wait_idle() }.map_err(|result| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkDeviceWaitIdle(static mesh vertex update)",
- result,
- }
- })?;
- write_static_mesh_vertex_buffer(self.device_ref()?, self.vertex_buffer_ref()?, vertices)
+ self.wait_for_submitted_frames()?;
+ write_static_mesh_vertex_buffer(self.device_ref()?, self.vertex_buffer_ref()?, 0, vertices)
}
/// Uploads the current shared mesh while retaining dynamic draw state.
@@ -658,6 +680,9 @@ impl VulkanSmokeRenderer {
/// transparency classification, and active counts owned by other setters
/// remain untouched. The allocations grow to the largest submitted frame
/// and never grow for a frame that fits the retained capacity.
+ /// `unchanged_vertices` and `unchanged_indices` retain prefixes already
+ /// uploaded by the caller; only the remaining suffixes are written. A
+ /// growing allocation receives the full slice, including its prefix.
///
/// A range with `index_count == 0` is valid and disables that range for the
/// frame. The mesh itself still needs at least one vertex and one index so
@@ -667,6 +692,8 @@ impl VulkanSmokeRenderer {
vertices: &[super::VulkanStaticVertex],
indices: &[u32],
geometry_ranges: &[super::VulkanDynamicDrawRange],
+ unchanged_vertices: usize,
+ unchanged_indices: usize,
) -> Result<(), VulkanSmokeRendererError> {
validate_geometry_upload(
vertices,
@@ -676,19 +703,17 @@ impl VulkanSmokeRenderer {
self.vertex_count,
self.index_capacity,
)?;
+ let vertex_upload =
+ geometry_upload_range(vertices.len(), unchanged_vertices, self.vertex_count)?;
+ let index_upload =
+ geometry_upload_range(indices.len(), unchanged_indices, self.index_capacity)?;
let vertex_grew = vertices.len() > self.vertex_count;
let index_grew = indices.len() > self.index_capacity;
- // SAFETY: The renderer owns the queue and all submitted work. Waiting
- // before either writing or replacing a shared allocation keeps old
- // command buffers from observing a partial upload.
- unsafe { self.device_ref()?.device().device_wait_idle() }.map_err(|result| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkDeviceWaitIdle(dynamic mesh update)",
- result,
- }
- })?;
+ if !vertex_upload.is_empty() || !index_upload.is_empty() {
+ self.wait_for_submitted_frames()?;
+ }
let (new_vertex_buffer, new_index_buffer) = if vertex_grew || index_grew {
let instance = self.instance_ref()?;
@@ -712,9 +737,12 @@ impl VulkanSmokeRenderer {
None
};
if !vertex_grew {
- if let Err(error) =
- write_static_mesh_vertex_buffer(device, self.vertex_buffer_ref()?, vertices)
- {
+ if let Err(error) = write_static_mesh_vertex_buffer(
+ device,
+ self.vertex_buffer_ref()?,
+ vertex_upload.start,
+ &vertices[vertex_upload.clone()],
+ ) {
if let Some(buffer) = new_vertex_buffer.as_ref() {
destroy_allocated_buffer(device, buffer);
}
@@ -725,9 +753,12 @@ impl VulkanSmokeRenderer {
}
}
if !index_grew {
- if let Err(error) =
- write_static_mesh_index_buffer(device, self.index_buffer_ref()?, indices)
- {
+ if let Err(error) = write_static_mesh_index_buffer(
+ device,
+ self.index_buffer_ref()?,
+ index_upload.start,
+ &indices[index_upload.clone()],
+ ) {
if let Some(buffer) = new_vertex_buffer.as_ref() {
destroy_allocated_buffer(device, buffer);
}
@@ -740,8 +771,18 @@ impl VulkanSmokeRenderer {
(new_vertex_buffer, new_index_buffer)
} else {
let device = self.device_ref()?;
- write_static_mesh_vertex_buffer(device, self.vertex_buffer_ref()?, vertices)?;
- write_static_mesh_index_buffer(device, self.index_buffer_ref()?, indices)?;
+ write_static_mesh_vertex_buffer(
+ device,
+ self.vertex_buffer_ref()?,
+ vertex_upload.start,
+ &vertices[vertex_upload.clone()],
+ )?;
+ write_static_mesh_index_buffer(
+ device,
+ self.index_buffer_ref()?,
+ index_upload.start,
+ &indices[index_upload.clone()],
+ )?;
(None, None)
};
@@ -767,6 +808,19 @@ impl VulkanSmokeRenderer {
geometry_ranges,
&mut self.draw_order_dirty,
);
+ self.report.mesh_vertex_upload_bytes = self
+ .report
+ .mesh_vertex_upload_bytes
+ .saturating_add((vertex_upload.len() as u64).saturating_mul(56));
+ self.report.mesh_index_upload_bytes = self
+ .report
+ .mesh_index_upload_bytes
+ .saturating_add((index_upload.len() as u64).saturating_mul(4));
+ self.report.mesh_full_upload_bytes = self
+ .report
+ .mesh_full_upload_bytes
+ .saturating_add((vertices.len() as u64).saturating_mul(56))
+ .saturating_add((indices.len() as u64).saturating_mul(4));
Ok(())
}
@@ -776,7 +830,7 @@ impl VulkanSmokeRenderer {
///
/// The updated image must have the same dimensions and generated/authored
/// mip count as the texture allocated during renderer creation. The
- /// renderer idles its device before the transfer, so callers may invoke
+ /// renderer waits for submitted readers before the transfer, so callers may invoke
/// this between frame updates without taking ownership of Vulkan handles.
pub fn update_material_texture(
&mut self,
@@ -807,17 +861,7 @@ impl VulkanSmokeRenderer {
context: "dynamic material texture index is out of bounds",
});
}
- {
- let device = self.device_ref()?;
- // SAFETY: every submission made by this renderer uses this device; the
- // idle wait makes all descriptor image reads quiescent before upload.
- unsafe { device.device().device_wait_idle() }.map_err(|result| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkDeviceWaitIdle(dynamic texture update)",
- result,
- }
- })?;
- }
+ self.wait_for_submitted_frames()?;
let shared_texture = self
.texture_slot_indices
.iter()
@@ -825,7 +869,7 @@ impl VulkanSmokeRenderer {
.count()
> 1;
if shared_texture {
- // A fallback image can serve many logical slots. Detach the
+ // One image can serve many logical slots. Detach the
// requested slot before uploading so this public update method
// keeps its one-material/one-slot semantics.
let image = {
@@ -836,6 +880,7 @@ impl VulkanSmokeRenderer {
let new_texture_index = self.textures.len();
self.textures.push(image);
self.texture_slot_indices[logical_texture_index] = new_texture_index;
+ self.report.texture_detach_count = self.report.texture_detach_count.saturating_add(1);
let device = self.device_ref()?;
let resources = self.resources_ref()?;
update_material_texture_descriptor(
@@ -976,11 +1021,7 @@ impl VulkanSmokeRenderer {
},
)?;
self.draw_range_base_pipeline_states[range_index] = next_base;
- let pipeline_changed = range.pipeline_state != next_state;
range.pipeline_state = next_state;
- if pipeline_changed {
- self.rebuild_pipeline_resources()?;
- }
Ok(())
}
@@ -1049,7 +1090,6 @@ impl VulkanSmokeRenderer {
},
)?;
let next_state = apply_world_transparency(base_state, transparent);
- let pipeline_changed = range.pipeline_state != next_state;
range.pipeline_state = next_state;
// Calling this setter opts the range into the world queue. Fixed
// screen/sky/weather ranges remain `None` because their pass order is
@@ -1059,9 +1099,6 @@ impl VulkanSmokeRenderer {
Some(transparent),
&mut self.draw_order_dirty,
);
- if pipeline_changed {
- self.rebuild_pipeline_resources()?;
- }
Ok(())
}
@@ -1388,6 +1425,24 @@ impl VulkanSmokeRenderer {
})
}
+ fn wait_for_submitted_frames(&self) -> Result<(), VulkanSmokeRendererError> {
+ let fences: Vec<_> = self.frame_sync.iter().map(|sync| sync.fence).collect();
+ if fences.is_empty() {
+ return Ok(());
+ }
+ // SAFETY: Every graphics submission signals one of these owned fences.
+ // Presentation does not read mesh buffers, textures or descriptor sets.
+ unsafe {
+ self.device_ref()?
+ .device()
+ .wait_for_fences(&fences, true, u64::MAX)
+ }
+ .map_err(|result| VulkanSmokeRendererError::VulkanOperation {
+ context: "vkWaitForFences(shared resource update)",
+ result,
+ })
+ }
+
/// Draws and presents one indexed-triangle frame.
///
/// # Errors
@@ -1433,14 +1488,7 @@ impl VulkanSmokeRenderer {
if needs_point_light_growth {
// Updating every material's binding 6 and replacing the shared
// SSBO requires all prior command buffers to finish.
- // SAFETY: The renderer owns the logical device and no submission
- // can observe the replacement after this idle point.
- unsafe { self.device_ref()?.device().device_wait_idle() }.map_err(|result| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkDeviceWaitIdle(point-light buffer growth)",
- result,
- }
- })?;
+ self.wait_for_submitted_frames()?;
let instance =
self.instance
.as_ref()
@@ -1464,14 +1512,7 @@ impl VulkanSmokeRenderer {
// The storage buffer is shared by all in-flight frames. Wait for
// every prior use before replacing the records so a setter cannot
// race a vertex shader from the previous frame.
- // SAFETY: The renderer owns the logical device and no submission
- // can observe the host write after this idle point.
- unsafe { self.device_ref()?.device().device_wait_idle() }.map_err(|result| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkDeviceWaitIdle(material specular update)",
- result,
- }
- })?;
+ self.wait_for_submitted_frames()?;
let resources = self.resources_ref()?;
write_material_specular_buffer(
self.device_ref()?,
@@ -1519,6 +1560,34 @@ impl VulkanSmokeRenderer {
}
self.images_in_flight[image_index_usize] = in_flight_fence;
let frame_bytes = self.frame_uniforms.to_ne_bytes();
+ let reflection_bytes = self
+ .active_reflection_plane()
+ .map(|height| {
+ let mut frame = self.frame_uniforms;
+ frame.clip_from_world = fparkan_render::water::legacy_embm_reflection_clip(
+ frame.clip_from_world,
+ frame.camera_position,
+ height,
+ frame.embm_bounds,
+ )
+ .or_else(|| {
+ fparkan_render::reflect_clip_from_world_about_z(frame.clip_from_world, height)
+ })
+ .ok_or(VulkanSmokeRendererError::InvalidStaticCamera {
+ context: "non-finite reflected clip transform",
+ })?;
+ frame.camera_position[2] = 2.0 * height - frame.camera_position[2];
+ if !frame.is_finite() {
+ return Err(VulkanSmokeRendererError::InvalidStaticCamera {
+ context: "non-finite reflected camera position",
+ });
+ }
+ let mut bytes = frame.to_ne_bytes();
+ let offset = super::smoke_types::REFLECTION_CLIP_HEIGHT_OFFSET;
+ bytes[offset..offset + 4].copy_from_slice(&height.to_ne_bytes());
+ Ok(bytes)
+ })
+ .transpose()?;
{
let explicit_point_lights = self.point_lights_explicit;
let legacy_point_light = self.frame_uniforms.point_light;
@@ -1544,9 +1613,18 @@ impl VulkanSmokeRenderer {
device,
&resources.frame_uniform_buffer,
resources.frame_uniform_stride,
- self.current_frame,
+ self.current_frame * 2,
&frame_bytes,
)?;
+ if let Some(bytes) = &reflection_bytes {
+ write_frame_uniform(
+ device,
+ &resources.frame_uniform_buffer,
+ resources.frame_uniform_stride,
+ self.current_frame * 2 + 1,
+ bytes,
+ )?;
+ }
write_point_light_buffer(
device,
&resources.point_light_buffer,
@@ -1683,11 +1761,15 @@ impl VulkanSmokeRenderer {
device,
swapchain,
self.command_pool,
+ self.pipeline_cache,
self.vertex_buffer_ref()?,
self.index_buffer_ref()?,
self.textures_ref()?,
&self.texture_slot_indices,
- &self.draw_ranges,
+ &self.draw_range_base_pipeline_states,
+ self.draw_ranges
+ .iter()
+ .any(|range| range.terrain_reflection),
self.depth_request,
reuse_command_pool,
)?,
@@ -1706,32 +1788,14 @@ impl VulkanSmokeRenderer {
self.report.swapchain_image_count = swapchain_image_count;
self.report.swapchain_image_format = self.swapchain_ref()?.report.plan.format.format;
self.report.swapchain_image_usage = swapchain_image_usage;
+ self.report.pipeline_creation_count = self
+ .report
+ .pipeline_creation_count
+ .saturating_add(resources.pipelines.len() as u64);
self.swapchain_resources = Some(resources);
Ok(())
}
- /// Rebuilds only the swapchain-owned pipeline bundle after a structural
- /// range-state change. Draw-range setters can be called while a renderer
- /// is live, so all prior submissions are completed before handles are
- /// destroyed and recreated.
- fn rebuild_pipeline_resources(&mut self) -> Result<(), VulkanSmokeRendererError> {
- if self.swapchain_resources.is_none() {
- return Ok(());
- }
- let device = self.device_ref()?;
- // SAFETY: The renderer owns every submission on this device. Waiting
- // here makes the old pipelines, framebuffers, and command buffers
- // quiescent before their destruction.
- unsafe { device.device().device_wait_idle() }.map_err(|result| {
- VulkanSmokeRendererError::VulkanOperation {
- context: "vkDeviceWaitIdle(draw range pipeline update)",
- result,
- }
- })?;
- self.destroy_swapchain_resources();
- self.rebuild_swapchain_resources(true)
- }
-
/// Recomputes the stable command-recording order. Without explicit
/// backgrounds, opaque world ranges followed by the sorted transparent
/// world ranges replace the existing world slots. With explicit
@@ -1761,6 +1825,16 @@ impl VulkanSmokeRenderer {
self.rebuild_draw_order();
self.draw_order_dirty = false;
}
+ let reflection_plane = self.active_reflection_plane();
+ let shifted_reflection = reflection_plane.is_some_and(|height| {
+ fparkan_render::water::legacy_embm_reflection_clip(
+ self.frame_uniforms.clip_from_world,
+ self.frame_uniforms.camera_position,
+ height,
+ self.frame_uniforms.embm_bounds,
+ )
+ .is_some()
+ });
let device = self.device_ref()?;
let swapchain = self.swapchain_ref()?;
let resources = self.resources_ref()?;
@@ -1807,185 +1881,269 @@ impl VulkanSmokeRenderer {
height: swapchain.report.plan.extent.1,
},
};
- let render_pass_info = vk::RenderPassBeginInfo::default()
- .render_pass(resources.render_pass)
- .framebuffer(resources.framebuffers[image_index])
- .render_area(render_area)
- .clear_values(&clear_values);
- // SAFETY: All commands target live frame resources owned by this renderer.
- unsafe {
- device.device().cmd_begin_render_pass(
- command_buffer,
- &render_pass_info,
- vk::SubpassContents::INLINE,
- );
- let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
- let offsets = [0_u64];
- device
- .device()
- .cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets);
- device.device().cmd_bind_index_buffer(
- command_buffer,
- self.index_buffer_ref()?.buffer,
- 0,
- vk::IndexType::UINT32,
- );
- let mut viewport_depth = None;
- for &range_index in &self.draw_order {
- let range = self.draw_ranges.get(range_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "stable draw order range handle",
- },
- )?;
- let texture_index = *self.draw_texture_indices.get(range_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "stable draw texture range handle",
- },
- )?;
- let pipeline = resources.pipelines.get(&range.pipeline_key()).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static draw pipeline variant",
- },
- )?;
- device.device().cmd_bind_pipeline(
+ for reflected in [true, false] {
+ if reflected && reflection_plane.is_none() {
+ continue;
+ }
+ let framebuffer = if reflected {
+ resources.reflection.framebuffer
+ } else {
+ resources.framebuffers[image_index]
+ };
+ let render_pass_info = vk::RenderPassBeginInfo::default()
+ .render_pass(resources.render_pass)
+ .framebuffer(framebuffer)
+ .render_area(render_area)
+ .clear_values(&clear_values);
+ // SAFETY: All commands target live frame resources owned by this renderer.
+ unsafe {
+ device.device().cmd_begin_render_pass(
command_buffer,
- vk::PipelineBindPoint::GRAPHICS,
- *pipeline,
+ &render_pass_info,
+ vk::SubpassContents::INLINE,
);
- let frame_offset = u32::try_from(
- resources
- .frame_uniform_stride
- .saturating_mul(u64::try_from(self.current_frame).unwrap_or(u64::MAX)),
- )
- .unwrap_or(u32::MAX);
- let material_offset = u32::try_from(
- resources
- .material_specular_stride
- .saturating_mul(u64::try_from(texture_index).unwrap_or(u64::MAX)),
- )
- .unwrap_or(u32::MAX);
- let point_light_offset = u32::try_from(
- resources
- .point_light_stride
- .saturating_mul(u64::try_from(self.current_frame).unwrap_or(u64::MAX)),
- )
- .unwrap_or(u32::MAX);
- let descriptor_set = resources.descriptor_sets.get(texture_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static material descriptor set",
- },
- )?;
- device.device().cmd_bind_descriptor_sets(
+ let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
+ let offsets = [0_u64];
+ device.device().cmd_bind_vertex_buffers(
command_buffer,
- vk::PipelineBindPoint::GRAPHICS,
- resources.pipeline_layout,
0,
- std::slice::from_ref(descriptor_set),
- &[frame_offset, material_offset, point_light_offset],
+ &vertex_buffers,
+ &offsets,
);
- let alpha_cutoff = range.alpha_test_cutoff().to_ne_bytes();
- device.device().cmd_push_constants(
+ device.device().cmd_bind_index_buffer(
command_buffer,
- resources.pipeline_layout,
- vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ self.index_buffer_ref()?.buffer,
0,
- &alpha_cutoff,
+ vk::IndexType::UINT32,
);
- let material_alpha = self.material_alphas.get(texture_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static material alpha",
- },
- )?;
- let diffuse_alpha_override = self
- .draw_range_diffuse_alpha_overrides
- .get(range_index)
- .ok_or(VulkanSmokeRendererError::InvariantViolation {
- context: "draw-range diffuse-alpha override",
- })?;
- let material_alphas =
- draw_material_alphas(*material_alpha, *diffuse_alpha_override)
- .map(f32::to_ne_bytes)
- .concat();
- device.device().cmd_push_constants(
- command_buffer,
- resources.pipeline_layout,
- vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
- 4,
- &material_alphas,
- );
- let material_lighting = self.material_lighting.get(texture_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static material lighting",
- },
- )?;
- let material_lighting = material_lighting
- .iter()
- .flatten()
- .flat_map(|value| value.to_ne_bytes())
- .collect::<Vec<_>>();
- device.device().cmd_push_constants(
- command_buffer,
- resources.pipeline_layout,
- vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
- 12,
- &material_lighting,
- );
- let material_uv_transforms = self.material_uv_transforms.get(texture_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static material UV transforms",
- },
- )?;
- let material_uv_transforms = material_uv_transforms
- .iter()
- .flatten()
- .flat_map(|value| value.to_ne_bytes())
- .collect::<Vec<_>>();
- device.device().cmd_push_constants(
- command_buffer,
- resources.pipeline_layout,
- vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
- 60,
- &material_uv_transforms,
- );
- let material_mode = self.material_unlit.get(texture_index).ok_or(
- VulkanSmokeRendererError::InvariantViolation {
- context: "static material unlit flag",
- },
- )?;
- let material_mode = if range.terrain_base_only {
- ((*material_mode).round() as i32 | 16) as f32
- } else {
- *material_mode
- };
- let viewport = super::swapchain_resources::draw_viewport(
- swapchain.report.plan.extent,
- material_mode,
- );
- let next_viewport_depth = [viewport.min_depth, viewport.max_depth];
- if viewport_depth != Some(next_viewport_depth) {
- device
- .device()
- .cmd_set_viewport(command_buffer, 0, &[viewport]);
- viewport_depth = Some(next_viewport_depth);
- }
- let material_mode = [material_mode.to_ne_bytes()];
- device.device().cmd_push_constants(
+ device.device().cmd_bind_descriptor_sets(
command_buffer,
+ vk::PipelineBindPoint::GRAPHICS,
resources.pipeline_layout,
- vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
- 124,
- &material_mode.concat(),
- );
- device.device().cmd_draw_indexed(
- command_buffer,
- range.index_count,
1,
- range.first_index,
- 0,
- 0,
+ &[resources.reflection.sets
+ [usize::from(!reflected && reflection_plane.is_some())]],
+ &[],
);
+ let mut viewport_depth = None;
+ for &range_index in &self.draw_order {
+ let range = self.draw_ranges.get(range_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "stable draw order range handle",
+ },
+ )?;
+ let texture_index = *self.draw_texture_indices.get(range_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "stable draw texture range handle",
+ },
+ )?;
+ let mode = self.material_unlit[texture_index].round() as i32;
+ if reflected && (range.terrain_reflection || mode & 8 != 0) {
+ continue;
+ }
+ let state = if reflected {
+ super::reflection::reflected_pipeline_state(range.pipeline_state)
+ } else {
+ range.pipeline_state
+ };
+ let pipeline = resources.pipelines.get(&state.into()).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "static draw pipeline variant",
+ },
+ )?;
+ device.device().cmd_bind_pipeline(
+ command_buffer,
+ vk::PipelineBindPoint::GRAPHICS,
+ *pipeline,
+ );
+ let frame_offset = u32::try_from(
+ resources.frame_uniform_stride.saturating_mul(
+ u64::try_from(self.current_frame * 2 + usize::from(reflected))
+ .unwrap_or(u64::MAX),
+ ),
+ )
+ .unwrap_or(u32::MAX);
+ let material_offset = u32::try_from(
+ resources
+ .material_specular_stride
+ .saturating_mul(u64::try_from(texture_index).unwrap_or(u64::MAX)),
+ )
+ .unwrap_or(u32::MAX);
+ let point_light_offset = u32::try_from(
+ resources
+ .point_light_stride
+ .saturating_mul(u64::try_from(self.current_frame).unwrap_or(u64::MAX)),
+ )
+ .unwrap_or(u32::MAX);
+ let descriptor_set = resources.descriptor_sets.get(texture_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "static material descriptor set",
+ },
+ )?;
+ device.device().cmd_bind_descriptor_sets(
+ command_buffer,
+ vk::PipelineBindPoint::GRAPHICS,
+ resources.pipeline_layout,
+ 0,
+ std::slice::from_ref(descriptor_set),
+ &[frame_offset, material_offset, point_light_offset],
+ );
+ let alpha_cutoff = range.alpha_test_cutoff().to_ne_bytes();
+ device.device().cmd_push_constants(
+ command_buffer,
+ resources.pipeline_layout,
+ vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ 0,
+ &alpha_cutoff,
+ );
+ let material_alpha = self.material_alphas.get(texture_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "static material alpha",
+ },
+ )?;
+ let diffuse_alpha_override = self
+ .draw_range_diffuse_alpha_overrides
+ .get(range_index)
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "draw-range diffuse-alpha override",
+ })?;
+ let material_alphas =
+ draw_material_alphas(*material_alpha, *diffuse_alpha_override)
+ .map(f32::to_ne_bytes)
+ .concat();
+ device.device().cmd_push_constants(
+ command_buffer,
+ resources.pipeline_layout,
+ vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ 4,
+ &material_alphas,
+ );
+ let material_lighting = self.material_lighting.get(texture_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "static material lighting",
+ },
+ )?;
+ let material_lighting = material_lighting
+ .iter()
+ .flatten()
+ .flat_map(|value| value.to_ne_bytes())
+ .collect::<Vec<_>>();
+ device.device().cmd_push_constants(
+ command_buffer,
+ resources.pipeline_layout,
+ vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ 12,
+ &material_lighting,
+ );
+ let material_uv_transforms = self
+ .material_uv_transforms
+ .get(texture_index)
+ .ok_or(VulkanSmokeRendererError::InvariantViolation {
+ context: "static material UV transforms",
+ })?;
+ let material_uv_transforms = material_uv_transforms
+ .iter()
+ .flatten()
+ .flat_map(|value| value.to_ne_bytes())
+ .collect::<Vec<_>>();
+ device.device().cmd_push_constants(
+ command_buffer,
+ resources.pipeline_layout,
+ vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ 60,
+ &material_uv_transforms,
+ );
+ let material_mode = self.material_unlit.get(texture_index).ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "static material unlit flag",
+ },
+ )?;
+ let material_mode = if range.terrain_base_only {
+ ((*material_mode).round() as i32 | 16) as f32
+ } else {
+ *material_mode
+ };
+ let material_mode = (material_mode.round() as i32
+ | if reflected { 32 } else { 0 }
+ | if !reflected
+ && reflection_plane.is_some()
+ && self.reflection_planes[range_index] == reflection_plane
+ {
+ 64 | if shifted_reflection { 128 } else { 0 }
+ } else {
+ 0
+ }) as f32;
+ let viewport = super::swapchain_resources::draw_viewport(
+ swapchain.report.plan.extent,
+ material_mode,
+ );
+ let next_viewport_depth = [viewport.min_depth, viewport.max_depth];
+ if viewport_depth != Some(next_viewport_depth) {
+ device
+ .device()
+ .cmd_set_viewport(command_buffer, 0, &[viewport]);
+ viewport_depth = Some(next_viewport_depth);
+ }
+ let material_mode = [material_mode.to_ne_bytes()];
+ device.device().cmd_push_constants(
+ command_buffer,
+ resources.pipeline_layout,
+ vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
+ 124,
+ &material_mode.concat(),
+ );
+ device.device().cmd_draw_indexed(
+ command_buffer,
+ range.index_count,
+ 1,
+ range.first_index,
+ 0,
+ 0,
+ );
+ }
+ device.device().cmd_end_render_pass(command_buffer);
+ if reflected {
+ let image = resources.reflection.color.as_ref().ok_or(
+ VulkanSmokeRendererError::InvariantViolation {
+ context: "reflection target",
+ },
+ )?;
+ let barrier = vk::ImageMemoryBarrier::default()
+ .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
+ .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
+ .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
+ .dst_access_mask(vk::AccessFlags::SHADER_READ)
+ .image(image.image)
+ .subresource_range(super::color_subresource_range(1));
+ device.device().cmd_pipeline_barrier(
+ command_buffer,
+ vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
+ vk::PipelineStageFlags::FRAGMENT_SHADER,
+ vk::DependencyFlags::empty(),
+ &[],
+ &[],
+ &[barrier],
+ );
+ }
}
- device.device().cmd_end_render_pass(command_buffer);
+ }
+ // SAFETY: The main pass has completed its writes to this acquired swapchain image.
+ unsafe {
+ let barrier = vk::ImageMemoryBarrier::default()
+ .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
+ .new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
+ .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
+ .image(resources.images[image_index])
+ .subresource_range(super::color_subresource_range(1));
+ device.device().cmd_pipeline_barrier(
+ command_buffer,
+ vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
+ vk::PipelineStageFlags::BOTTOM_OF_PIPE,
+ vk::DependencyFlags::empty(),
+ &[],
+ &[],
+ &[barrier],
+ );
if self.readback_enabled {
if let (Some(image), Some(readback)) = (
resources.images.get(image_index),
@@ -2048,7 +2206,7 @@ impl VulkanSmokeRenderer {
}
}
- // SAFETY: The render pass owns the attachment layout transitions for this clear-and-present path.
+ // SAFETY: Every render pass and attachment transition has been recorded in this buffer.
unsafe { device.device().end_command_buffer(command_buffer) }.map_err(|error| {
VulkanSmokeRendererError::VulkanOperation {
context: "vkEndCommandBuffer",
@@ -2081,6 +2239,15 @@ impl VulkanSmokeRenderer {
fn destroy_device_owned_resources(&mut self) {
self.destroy_swapchain_resources();
if let Some(device) = self.device.as_ref() {
+ if self.pipeline_cache != vk::PipelineCache::null() {
+ // SAFETY: All pipeline creation has finished on this device.
+ unsafe {
+ device
+ .device()
+ .destroy_pipeline_cache(self.pipeline_cache, None)
+ };
+ self.pipeline_cache = vk::PipelineCache::null();
+ }
for texture in self.textures.drain(..) {
destroy_allocated_image(device, &texture);
}
@@ -2231,6 +2398,40 @@ impl Drop for VulkanSmokeRenderer {
}
}
+fn deduplicate_texture_sources<'a>(
+ sources: &[&'a super::VulkanStaticTexture],
+) -> (Vec<&'a super::VulkanStaticTexture>, Vec<usize>) {
+ let mut indices = std::collections::HashMap::new();
+ let mut unique = Vec::new();
+ let mut slots = Vec::with_capacity(sources.len());
+ for &source in sources {
+ let index = *indices.entry(source).or_insert_with(|| {
+ let index = unique.len();
+ unique.push(source);
+ index
+ });
+ slots.push(index);
+ }
+ (unique, slots)
+}
+
+fn geometry_upload_range(
+ count: usize,
+ unchanged: usize,
+ capacity: usize,
+) -> Result<std::ops::Range<usize>, VulkanSmokeRendererError> {
+ if unchanged > count || unchanged > capacity {
+ return Err(VulkanSmokeRendererError::InvalidStaticMesh {
+ context: "unchanged mesh prefix exceeds uploaded or retained geometry",
+ });
+ }
+ Ok(if count > capacity {
+ 0..count
+ } else {
+ unchanged..count
+ })
+}
+
fn validate_geometry_upload(
vertices: &[super::VulkanStaticVertex],
indices: &[u32],
@@ -2405,6 +2606,18 @@ fn draw_material_alphas(
]
}
+pub(super) fn pipeline_variants(original: LegacyPipelineState) -> [LegacyPipelineState; 12] {
+ std::array::from_fn(|index| match index {
+ 0 => original,
+ 1 => apply_world_transparency(original, true),
+ _ => {
+ world_material_pipeline_states(original, ((index - 2) / 2) as u8, Some(index % 2 != 0))
+ .expect("categories 0..=4 are valid")
+ .1
+ }
+ })
+}
+
fn world_material_pipeline_states(
original: LegacyPipelineState,
category: u8,
@@ -2572,12 +2785,15 @@ mod tests {
use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh};
use super::{
apply_geometry_range_updates, apply_world_transparency, completed_readback_buffer_index,
- draw_material_alphas, mark_draw_order_dirty_on_activity_change,
- set_and_mark_dirty_if_changed, sorted_world_draw_order,
- take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order,
- validate_geometry_upload, world_material_pipeline_states, RollbackOnDrop,
+ deduplicate_texture_sources, draw_material_alphas, geometry_upload_range,
+ mark_draw_order_dirty_on_activity_change, pipeline_variants, set_and_mark_dirty_if_changed,
+ sorted_world_draw_order, take_runtime_children_with_validation_snapshot,
+ take_runtime_owners_in_dependency_order, validate_geometry_upload,
+ world_material_pipeline_states, RollbackOnDrop,
+ };
+ use fparkan_render::{
+ LegacyBlendMode, LegacyCullMode, LegacyDepthBias, LegacyDepthMode, LegacyPipelineState,
};
- use fparkan_render::{LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState};
use std::cell::RefCell;
use std::rc::Rc;
@@ -2682,6 +2898,7 @@ mod tests {
batch_flags: 0x108,
pipeline_state: base_state,
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 17,
},
VulkanStaticDrawRange {
@@ -2692,6 +2909,7 @@ mod tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
},
];
@@ -2731,6 +2949,93 @@ mod tests {
}
#[test]
+ fn pipeline_variants_keep_authored_alpha_and_prepare_both_depth_policies() {
+ let original = LegacyPipelineState {
+ alpha_test: true,
+ depth: LegacyDepthMode::TestWrite,
+ cull: LegacyCullMode::FrontFace,
+ depth_bias: LegacyDepthBias::ProjectedDecal,
+ ..LegacyPipelineState::default()
+ };
+ let states = pipeline_variants(original);
+ let keys: std::collections::BTreeSet<_> = states
+ .iter()
+ .map(|state| fparkan_render::PipelineKey::from(*state))
+ .collect();
+ assert_eq!(keys.len(), 12);
+ assert_eq!(states[0], original);
+ assert_eq!(states[1], apply_world_transparency(original, true));
+ for state in states {
+ assert_eq!(state.cull, original.cull);
+ assert_eq!(state.depth_bias, original.depth_bias);
+ }
+ let disabled = pipeline_variants(LegacyPipelineState {
+ depth: LegacyDepthMode::Disabled,
+ ..original
+ });
+ assert!(disabled
+ .iter()
+ .all(|state| state.depth == LegacyDepthMode::Disabled));
+ }
+
+ #[test]
+ fn texture_deduplication_compares_pixels_extents_and_authored_mips() {
+ use super::super::{VulkanStaticTexture, VulkanStaticTextureMip};
+ let base = VulkanStaticTexture {
+ width: 2,
+ height: 2,
+ rgba8: vec![255; 16],
+ mip_levels: Vec::new(),
+ };
+ let copy = base.clone();
+ let authored = VulkanStaticTexture {
+ mip_levels: vec![
+ VulkanStaticTextureMip {
+ width: 2,
+ height: 2,
+ rgba8: base.rgba8.clone(),
+ },
+ VulkanStaticTextureMip {
+ width: 1,
+ height: 1,
+ rgba8: vec![255; 4],
+ },
+ ],
+ ..base.clone()
+ };
+ let mut different_mip = authored.clone();
+ different_mip.mip_levels[1].rgba8[0] = 0;
+ let different_extent = VulkanStaticTexture {
+ width: 1,
+ height: 4,
+ ..base.clone()
+ };
+ let sources = [
+ &base,
+ &copy,
+ &authored,
+ &different_mip,
+ &different_extent,
+ &copy,
+ ];
+ for source in sources {
+ source.validate().expect("valid texture");
+ }
+ let (unique, slots) = deduplicate_texture_sources(&sources);
+ assert_eq!(unique.len(), 4);
+ assert_eq!(slots, [0, 0, 1, 2, 3, 0]);
+ }
+
+ #[test]
+ fn geometry_upload_preserves_prefix_until_allocation_grows() {
+ assert_eq!(geometry_upload_range(12, 8, 16), Ok(8..12));
+ assert_eq!(geometry_upload_range(8, 8, 16), Ok(8..8));
+ assert_eq!(geometry_upload_range(20, 8, 16), Ok(0..20));
+ assert!(geometry_upload_range(8, 9, 16).is_err());
+ assert!(geometry_upload_range(20, 17, 16).is_err());
+ }
+
+ #[test]
fn geometry_update_can_disable_empty_shadow_range_without_zero_sized_upload() {
let mesh = VulkanStaticMesh::smoke_triangle();
let geometry = [VulkanDynamicDrawRange {
@@ -2761,6 +3066,7 @@ mod tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
}];
let mut capacities = [6];
@@ -2947,6 +3253,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
@@ -2971,6 +3278,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
},
super::super::VulkanStaticDrawRange {
@@ -2981,6 +3289,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
},
super::super::VulkanStaticDrawRange {
@@ -2991,6 +3300,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
},
];
@@ -3015,6 +3325,7 @@ mod tests {
batch_flags: 0,
pipeline_state: fparkan_render::LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
@@ -3108,6 +3419,7 @@ mod tests {
batch_flags: 0,
pipeline_state: LegacyPipelineState::default(),
terrain_base_only: false,
+ terrain_reflection: false,
alpha_test_reference: 0,
})
.collect::<Vec<_>>();
@@ -3145,12 +3457,75 @@ mod tests {
}
#[test]
+ fn control_fx_queue_follows_opaque_and_sky_without_reactivating_empty_ranges() {
+ const SCREEN_GRADIENT: usize = 0;
+ const CONTROL_FX: usize = 1;
+ const TERRAIN: usize = 2;
+ const BUILDING: usize = 3;
+ const SHADOW: usize = 4;
+ const NEBULA: usize = 5;
+ const DOME: usize = 6;
+ const CLOUDS: usize = 7;
+ const EMPTY_CONTROL_FX: usize = 8;
+ const SUN: usize = 9;
+ const WEATHER: usize = 10;
+ let mut ranges = (0..11)
+ .map(|index| VulkanStaticDrawRange {
+ first_index: index * 3,
+ index_count: 3,
+ material_index: 0,
+ lightmap_index: u8::MAX,
+ batch_flags: 0,
+ pipeline_state: LegacyPipelineState::default(),
+ terrain_base_only: false,
+ terrain_reflection: false,
+ alpha_test_reference: 0,
+ })
+ .collect::<Vec<_>>();
+ ranges[EMPTY_CONTROL_FX].index_count = 0;
+ let mut world_queue = vec![None; ranges.len()];
+ world_queue[TERRAIN] = Some(false);
+ world_queue[BUILDING] = Some(false);
+ world_queue[CONTROL_FX] = Some(true);
+ world_queue[EMPTY_CONTROL_FX] = Some(true);
+ let mut sort_keys = vec![None; ranges.len()];
+ sort_keys[CONTROL_FX] = Some(20.0);
+ sort_keys[EMPTY_CONTROL_FX] = Some(30.0);
+
+ let order = sorted_world_draw_order(
+ ranges.len(),
+ &ranges,
+ &world_queue,
+ &sort_keys,
+ &[NEBULA, DOME, CLOUDS],
+ );
+ assert_eq!(
+ order,
+ vec![
+ SCREEN_GRADIENT,
+ TERRAIN,
+ BUILDING,
+ SHADOW,
+ NEBULA,
+ DOME,
+ CLOUDS,
+ CONTROL_FX,
+ EMPTY_CONTROL_FX,
+ SUN,
+ WEATHER,
+ ]
+ );
+ assert_eq!(ranges[EMPTY_CONTROL_FX].index_count, 0);
+ }
+
+ #[test]
fn material_category_and_transparency_preserve_world_state_in_either_order() {
let original = LegacyPipelineState {
blend: LegacyBlendMode::Opaque,
depth: LegacyDepthMode::TestWrite,
cull: LegacyCullMode::FrontFace,
alpha_test: false,
+ depth_bias: LegacyDepthBias::ProjectedDecal,
};
let (base, category_first) =
world_material_pipeline_states(original, 1, Some(true)).expect("category 1");
@@ -3162,14 +3537,17 @@ mod tests {
assert!(base.alpha_test);
assert_eq!(base.depth, LegacyDepthMode::TestWrite);
assert_eq!(base.cull, LegacyCullMode::FrontFace);
+ assert_eq!(base.depth_bias, LegacyDepthBias::ProjectedDecal);
assert_eq!(category_first.depth, LegacyDepthMode::TestReadOnly);
assert_eq!(category_first.cull, LegacyCullMode::FrontFace);
+ assert_eq!(category_first.depth_bias, LegacyDepthBias::ProjectedDecal);
let fixed_state = LegacyPipelineState {
blend: LegacyBlendMode::Additive,
depth: LegacyDepthMode::Disabled,
cull: LegacyCullMode::Disabled,
alpha_test: false,
+ depth_bias: LegacyDepthBias::Disabled,
};
assert_eq!(
world_material_pipeline_states(fixed_state, 1, None),