diff options
Diffstat (limited to 'adapters/fparkan-render-vulkan/src')
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/asset_mesh.rs | 23 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi.rs | 3 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/resources.rs | 41 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 502 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs | 91 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs | 103 |
6 files changed, 544 insertions, 219 deletions
diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index f691b34..826e89a 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -1105,7 +1105,21 @@ fn project_land_msh_to_static_mesh_with_lod0( pub fn project_land_msh_to_static_mesh_in_world_space( terrain: &LandMeshDocument, ) -> Result<VulkanStaticMesh, VulkanAssetMeshError> { - let lod0 = static_terrain_lod0_assignments(terrain)?; + project_land_msh_lod_to_static_mesh_in_world_space(terrain, TerrainLod::Lod0) +} + +/// Projects one authored terrain LOD without changing source positions or attributes. +/// +/// # Errors +/// Returns an error for invalid slot assignments or an empty selected LOD. +pub fn project_land_msh_lod_to_static_mesh_in_world_space( + terrain: &LandMeshDocument, + lod: TerrainLod, +) -> Result<VulkanStaticMesh, VulkanAssetMeshError> { + let mut lod0 = terrain + .face_lod_assignments() + .map_err(VulkanAssetMeshError::InvalidTerrainLodMapping)?; + lod0.retain(|assignment| assignment.lod == lod); let indices = static_terrain_indices(terrain, &lod0)?; let vertices = terrain .positions @@ -2118,6 +2132,13 @@ mod tests { assert_eq!(mesh.draw_ranges[0].index_count, 6); assert_eq!(mesh.draw_ranges[0].material_index, 0x0101); + let far = project_land_msh_lod_to_static_mesh_in_world_space(&terrain, TerrainLod::Lod1) + .expect("LOD1 terrain projection"); + assert_eq!(far.indices, [3, 4, 5, 3, 4, 5]); + assert_eq!(far.draw_ranges.len(), 1); + assert_eq!(far.draw_ranges[0].material_index, 0x0202); + assert_eq!(far.vertices[3].position, [10.0, 10.0, 10.0]); + terrain.nodes_raw.clear(); assert!(matches!( project_land_msh_to_static_mesh_in_world_space(&terrain), diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs index 9584405..b98fa0d 100644 --- a/adapters/fparkan-render-vulkan/src/ffi.rs +++ b/adapters/fparkan-render-vulkan/src/ffi.rs @@ -22,7 +22,8 @@ mod validation; pub use self::asset_mesh::{ node38_pose, node38_pose_from_hierarchy, node38_pose_relative_to_root, - node38_pose_relative_to_root_from_hierarchy, project_land_msh_to_static_mesh, + node38_pose_relative_to_root_from_hierarchy, + project_land_msh_lod_to_static_mesh_in_world_space, project_land_msh_to_static_mesh, project_land_msh_to_static_mesh_in_world_space, project_land_msh_to_static_mesh_in_xy_frame, project_msh_to_static_mesh, project_msh_to_static_mesh_in_world_space, project_msh_to_static_mesh_in_world_space_with_node_fallback_poses, diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs index 0b43565..fe71d12 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs @@ -243,18 +243,25 @@ pub(super) fn create_vertex_buffer( /// Replaces the bytes of a host-visible static vertex allocation. /// -/// The live renderer waits for the device before calling this helper, so a +/// The live renderer waits for submitted readers before calling this helper, so a /// single shared allocation can safely carry camera-relative sky and weather /// geometry between frames. pub(super) fn write_static_mesh_vertex_buffer( device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer, + first_vertex: usize, vertices: &[super::VulkanStaticVertex], ) -> Result<(), VulkanSmokeRendererError> { let bytes = encode_static_vertices(vertices)?; + let offset = first_vertex + .checked_mul(14 * std::mem::size_of::<f32>()) + .ok_or(VulkanSmokeRendererError::InvalidStaticMesh { + context: "static mesh vertex upload offset overflows address space", + })?; write_mapped_buffer( device, buffer, + offset, &bytes, "vkMapMemory(static mesh vertex update)", ) @@ -295,29 +302,28 @@ fn encode_static_vertices( fn write_mapped_buffer( device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer, + offset: usize, bytes: &[u8], context: &'static str, ) -> Result<(), VulkanSmokeRendererError> { - let byte_len = - u64::try_from(bytes.len()).map_err(|_| VulkanSmokeRendererError::InvalidStaticMesh { - context: "static mesh upload exceeds address space", - })?; - if byte_len == 0 { - return Err(VulkanSmokeRendererError::InvalidStaticMesh { - context: "static mesh upload cannot be empty", - }); + if bytes.is_empty() { + return Ok(()); } // SAFETY: The caller validates that the allocation is large enough and // waits for all queue work that could read it before this write. let mapped = unsafe { - device - .device() - .map_memory(buffer.memory, 0, byte_len, vk::MemoryMapFlags::empty()) + device.device().map_memory( + buffer.memory, + 0, + vk::WHOLE_SIZE, + vk::MemoryMapFlags::empty(), + ) } .map_err(|result| VulkanSmokeRendererError::VulkanOperation { context, result })?; - // SAFETY: `mapped` covers exactly the byte slice copied below. + // SAFETY: The caller bounds the upload against the allocation. Mapping + // from zero also meets minMemoryMapAlignment for arbitrary vertex offsets. unsafe { - std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::<u8>(), bytes.len()); + std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::<u8>().add(offset), bytes.len()); device.device().unmap_memory(buffer.memory); } Ok(()) @@ -351,12 +357,19 @@ pub(super) fn create_index_buffer( pub(super) fn write_static_mesh_index_buffer( device: &VulkanLogicalDeviceProbe, buffer: &VulkanAllocatedBuffer, + first_index: usize, indices: &[u32], ) -> Result<(), VulkanSmokeRendererError> { let bytes = encode_static_indices(indices)?; + let offset = first_index.checked_mul(std::mem::size_of::<u32>()).ok_or( + VulkanSmokeRendererError::InvalidStaticMesh { + context: "static mesh index upload offset overflows address space", + }, + )?; write_mapped_buffer( device, buffer, + offset, &bytes, "vkMapMemory(static mesh index update)", ) diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index 522f95f..1b50914 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 { @@ -404,6 +364,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), @@ -464,8 +425,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 +474,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) } @@ -628,7 +613,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 +624,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 +636,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 +648,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 +659,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 +693,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 +709,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 +727,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 +764,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 +786,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 +817,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 +825,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 +836,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 +977,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 +1046,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 +1055,6 @@ impl VulkanSmokeRenderer { Some(transparent), &mut self.draw_order_dirty, ); - if pipeline_changed { - self.rebuild_pipeline_resources()?; - } Ok(()) } @@ -1388,6 +1381,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 +1444,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 +1468,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()?, @@ -1683,11 +1680,12 @@ 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.depth_request, reuse_command_pool, )?, @@ -1706,32 +1704,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 @@ -2081,6 +2061,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 +2220,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 +2428,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 +2607,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; @@ -2731,6 +2769,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, + ©, + &authored, + &different_mip, + &different_extent, + ©, + ]; + 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 { @@ -3145,12 +3270,74 @@ 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, + 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 +3349,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), diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index f004cb2..5d6f12f 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -284,6 +284,9 @@ impl VulkanFrameUniforms { .chain([self.fog_start, self.fog_end]) .chain(self.camera_position) .all(f32::is_finite) + && (self.fog_end <= self.fog_start + || ((self.fog_start * self.fog_start).is_finite() + && (self.fog_end * self.fog_end).is_finite())) } /// Serializes one frame slot in the exact `std140` member order. @@ -344,7 +347,17 @@ impl VulkanFrameUniforms { self.fog_color[2], 0.0, ]); - values[84..88].copy_from_slice(&[self.fog_start, self.fog_end, 0.0, 0.0]); + let squared_fog_range = if self.fog_end > self.fog_start { + [self.fog_start * self.fog_start, self.fog_end * self.fog_end] + } else { + [0.0; 2] + }; + values[84..88].copy_from_slice(&[ + self.fog_start, + self.fog_end, + squared_fog_range[0], + squared_fog_range[1], + ]); values[88..92].copy_from_slice(&[ self.camera_position[0], self.camera_position[1], @@ -546,7 +559,7 @@ impl VulkanStaticMaterial { } /// One authored RGBA8 mip level accepted by the Vulkan texture upload path. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct VulkanStaticTextureMip { /// Mip width in texels. pub width: u32, @@ -557,7 +570,7 @@ pub struct VulkanStaticTextureMip { } /// Decoded RGBA8 image accepted by the initial Vulkan texture upload path. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct VulkanStaticTexture { /// Image width in texels. pub width: u32, @@ -756,8 +769,8 @@ impl VulkanStaticMesh { material_index: 0, lightmap_index: u8::MAX, batch_flags: 0, - terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }], } @@ -875,6 +888,31 @@ mod static_mesh_tests { } #[test] + fn fog_uniforms_match_live_native_squared_distance_range() { + // Terrain ShadeIndexedStrided: far=0x442389e6, far²=0x48d0f1ca; + // distance²=0x47707b04 gives visibility=0x3f5b2b94. + let frame = VulkanFrameUniforms { + fog_start: 0.0, + fog_end: f32::from_bits(0x4423_89e6), + ..Default::default() + }; + assert!(frame.is_finite()); + let bytes = frame.to_ne_bytes(); + let read = |offset| f32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); + assert_eq!(read(344), 0.0); + assert_eq!(read(348).to_bits(), 0x48d0_f1ca); + let visibility = 1.0 - (f32::from_bits(0x4770_7b04) - read(344)) / (read(348) - read(344)); + assert!((visibility - f32::from_bits(0x3f5b_2b94)).abs() < 1.0e-6); + assert!(VulkanFrameUniforms::default().is_finite()); + assert!(!VulkanFrameUniforms { + fog_start: 0.0, + fog_end: f32::MAX, + ..frame + } + .is_finite()); + } + + #[test] fn static_camera_composes_recovered_d3d7_view_before_projection() { let transform = RawCameraTransform { words: [ @@ -992,8 +1030,8 @@ mod static_mesh_tests { material_index: 0, lightmap_index: u8::MAX, batch_flags: 0, - terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }], }; @@ -1023,8 +1061,8 @@ mod static_mesh_tests { material_index: 7, lightmap_index: u8::MAX, batch_flags: 0, - terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }, VulkanStaticDrawRange { @@ -1033,8 +1071,8 @@ mod static_mesh_tests { material_index: 2, lightmap_index: u8::MAX, batch_flags: 0, - terrain_base_only: false, pipeline_state: LegacyPipelineState::default(), + terrain_base_only: false, alpha_test_reference: 0, }, ]; @@ -1101,6 +1139,11 @@ mod static_mesh_tests { }, ]; + assert_eq!( + resolve_draw_texture_indices(&ranges, &materials), + Ok(vec![1, 0]) + ); + let mut screen_tl = materials[0].clone(); screen_tl.screen_tl = true; screen_tl.sky_far_depth = true; @@ -1109,11 +1152,9 @@ mod static_mesh_tests { far_sky.unlit = true; far_sky.sky_far_depth = true; assert_eq!(far_sky.encoded_material_mode(), 5.0); - - assert_eq!( - resolve_draw_texture_indices(&ranges, &materials), - Ok(vec![1, 0]) - ); + let mut nebula_with_stars = far_sky.clone(); + nebula_with_stars.sky_nebula_stars = true; + assert_eq!(nebula_with_stars.encoded_material_mode(), 6.0); let mut non_finite_base = materials.clone(); non_finite_base[0].specular_rgb[0] = f32::NAN; @@ -1149,6 +1190,7 @@ mod static_mesh_tests { fn alpha_cutoff_is_dynamic_and_disabled_outside_alpha_test_pipeline() { let base = VulkanStaticMesh::smoke_triangle().draw_ranges[0]; let disabled = VulkanStaticDrawRange { + terrain_base_only: false, alpha_test_reference: 200, ..base }; @@ -1157,6 +1199,7 @@ mod static_mesh_tests { alpha_test: true, ..LegacyPipelineState::default() }, + terrain_base_only: false, alpha_test_reference: 128, ..base }; @@ -1167,6 +1210,7 @@ mod static_mesh_tests { alpha_test: true, ..LegacyPipelineState::default() }, + terrain_base_only: false, alpha_test_reference: 0, ..base }; @@ -1176,6 +1220,7 @@ mod static_mesh_tests { alpha_test: true, ..LegacyPipelineState::default() }, + terrain_base_only: false, alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, ..base }; @@ -1288,6 +1333,18 @@ pub struct VulkanSmokeRendererReport { pub readback_byte_count: u64, /// Stable FNV-1a hash of the final completed readback artifact. pub readback_fnv1a64: u64, + /// Vertex bytes written by dynamic geometry uploads, excluding initialization. + pub mesh_vertex_upload_bytes: u64, + /// Index bytes written by dynamic geometry uploads, excluding initialization. + pub mesh_index_upload_bytes: u64, + /// Bytes the same updates would write without retaining unchanged prefixes. + pub mesh_full_upload_bytes: u64, + /// Distinct GPU images allocated for initial material texture slots. + pub initial_texture_image_count: u64, + /// Shared images detached for independent material-slot updates. + pub texture_detach_count: u64, + /// Graphics pipelines created across swapchain lifetimes. + pub pipeline_creation_count: u64, } /// Measured validation counters from the live smoke loop. @@ -1430,12 +1487,12 @@ pub struct VulkanSmokeRenderer { pub(super) device: Option<VulkanLogicalDeviceProbe>, pub(super) swapchain: Option<VulkanSwapchainProbe>, pub(super) command_pool: vk::CommandPool, + pub(super) pipeline_cache: vk::PipelineCache, pub(super) swapchain_resources: Option<VulkanSwapchainResources>, pub(super) vertex_buffer: Option<VulkanAllocatedBuffer>, pub(super) index_buffer: Option<VulkanAllocatedBuffer>, - /// Positional material slots mapped to the owned image list. Fallback - /// stages may share one image without coupling independently animated - /// material textures. + /// Positional material slots mapped to deduplicated images. Updating a + /// shared image first detaches that slot, preserving independent animation. pub(super) texture_slot_indices: Vec<usize>, pub(super) textures: Vec<VulkanAllocatedImage>, pub(super) vertex_count: usize, @@ -1472,8 +1529,8 @@ pub struct VulkanSmokeRenderer { pub(super) material_selectors: Vec<u16>, /// Whether the host-side specular records need a fence-safe GPU upload. pub(super) material_specular_dirty: bool, - /// Encoded material mode pushed at offset 124: 0 lit, 1 unlit, 2 native - /// sky nebula/stars texture composite, 3 native lightmap composite. + /// Encoded material mode pushed at offset 124: low bits select the + /// combiner, bit 4 selects sky far-depth, and bit 8 selects screen/TL. pub(super) material_unlit: Vec<f32>, pub(super) readback_enabled: bool, pub(super) camera: VulkanStaticCamera, diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs index 05ce927..a44f79d 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs @@ -3,7 +3,8 @@ use ash::vk; use fparkan_platform::DepthStencilSupport; use fparkan_render::{ - LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState, PipelineKey, + LegacyBlendMode, LegacyCullMode, LegacyDepthBias, LegacyDepthMode, LegacyPipelineState, + PipelineKey, }; use std::collections::BTreeMap; @@ -173,11 +174,12 @@ pub(super) fn create_swapchain_resources( device: &VulkanLogicalDeviceProbe, swapchain: &VulkanSwapchainProbe, command_pool: vk::CommandPool, + pipeline_cache: vk::PipelineCache, vertex_buffer: &VulkanAllocatedBuffer, index_buffer: &VulkanAllocatedBuffer, textures: &[VulkanAllocatedImage], texture_slot_indices: &[usize], - draw_ranges: &[super::VulkanStaticDrawRange], + base_pipeline_states: &[LegacyPipelineState], depth_request: DepthStencilSupport, reuse_command_pool: bool, ) -> Result<VulkanSwapchainResources, VulkanSmokeRendererError> { @@ -280,11 +282,12 @@ pub(super) fn create_swapchain_resources( ) = match create_swapchain_pipeline_bundle( instance, device, + pipeline_cache, swapchain.report.plan.format.format, swapchain.report.plan.extent, textures, texture_slot_indices, - draw_ranges, + base_pipeline_states, depth_request, partial .frame_uniform_buffer @@ -448,11 +451,12 @@ fn create_swapchain_image_views( fn create_swapchain_pipeline_bundle( instance: &VulkanInstanceProbe, device: &VulkanLogicalDeviceProbe, + pipeline_cache: vk::PipelineCache, format: i32, extent: (u32, u32), textures: &[VulkanAllocatedImage], texture_slot_indices: &[usize], - draw_ranges: &[super::VulkanStaticDrawRange], + base_pipeline_states: &[LegacyPipelineState], depth_request: DepthStencilSupport, frame_uniform_buffer: &VulkanAllocatedBuffer, material_specular_buffer: &VulkanAllocatedBuffer, @@ -509,25 +513,31 @@ fn create_swapchain_pipeline_bundle( } destroy_depth_attachment(device, &depth_attachment); })?; - let pipelines = - create_graphics_pipeline_cache(device, render_pass, pipeline_layout, extent, draw_ranges) - .inspect_err(|_| { - // SAFETY: These objects were created above on this live logical device and are destroyed on setup failure. - unsafe { - device - .device() - .destroy_pipeline_layout(pipeline_layout, None); - device.device().destroy_sampler(sampler, None); - device - .device() - .destroy_descriptor_pool(descriptor_pool, None); - device - .device() - .destroy_descriptor_set_layout(descriptor_set_layout, None); - device.device().destroy_render_pass(render_pass, None); - } - destroy_depth_attachment(device, &depth_attachment); - })?; + let pipelines = create_graphics_pipeline_cache( + device, + pipeline_cache, + render_pass, + pipeline_layout, + extent, + base_pipeline_states, + ) + .inspect_err(|_| { + // SAFETY: These objects were created above on this live logical device and are destroyed on setup failure. + unsafe { + device + .device() + .destroy_pipeline_layout(pipeline_layout, None); + device.device().destroy_sampler(sampler, None); + device + .device() + .destroy_descriptor_pool(descriptor_pool, None); + device + .device() + .destroy_descriptor_set_layout(descriptor_set_layout, None); + device.device().destroy_render_pass(render_pass, None); + } + destroy_depth_attachment(device, &depth_attachment); + })?; Ok(( depth_attachment, render_pass, @@ -935,25 +945,39 @@ pub(super) fn draw_viewport(extent: (u32, u32), material_mode: f32) -> vk::Viewp } } +fn depth_bias_parameters(depth_bias: LegacyDepthBias) -> (bool, f32, f32) { + match depth_bias { + LegacyDepthBias::Disabled => (false, 0.0, 0.0), + // shortcut: Vulkan constantFactor=-1.0/slopeFactor=0.0 is empirical; verify native raster state before claiming parity. + LegacyDepthBias::ProjectedDecal => (true, -1.0, 0.0), + } +} + fn create_graphics_pipeline_cache( device: &VulkanLogicalDeviceProbe, + pipeline_cache: vk::PipelineCache, render_pass: vk::RenderPass, pipeline_layout: vk::PipelineLayout, extent: (u32, u32), - draw_ranges: &[super::VulkanStaticDrawRange], + base_pipeline_states: &[LegacyPipelineState], ) -> Result<BTreeMap<PipelineKey, vk::Pipeline>, VulkanSmokeRendererError> { let mut pipelines = BTreeMap::new(); - for range in draw_ranges { - let key = range.pipeline_key(); + for state in base_pipeline_states + .iter() + .copied() + .flat_map(super::smoke::pipeline_variants) + { + let key = PipelineKey::from(state); if pipelines.contains_key(&key) { continue; } let pipeline = match create_graphics_pipeline( device, + pipeline_cache, render_pass, pipeline_layout, extent, - range.pipeline_state, + state, ) { Ok(pipeline) => pipeline, Err(error) => { @@ -973,6 +997,7 @@ fn create_graphics_pipeline_cache( fn create_graphics_pipeline( device: &VulkanLogicalDeviceProbe, + pipeline_cache: vk::PipelineCache, render_pass: vk::RenderPass, pipeline_layout: vk::PipelineLayout, extent: (u32, u32), @@ -1053,6 +1078,8 @@ fn create_graphics_pipeline( let viewport_state = vk::PipelineViewportStateCreateInfo::default() .viewports(&viewports) .scissors(&scissors); + let (depth_bias_enable, depth_bias_constant_factor, depth_bias_slope_factor) = + depth_bias_parameters(state.depth_bias); let rasterization_state = vk::PipelineRasterizationStateCreateInfo::default() .depth_clamp_enable(false) .rasterizer_discard_enable(false) @@ -1064,7 +1091,10 @@ fn create_graphics_pipeline( LegacyCullMode::FrontFace => vk::CullModeFlags::FRONT, }) .front_face(vk::FrontFace::CLOCKWISE) - .depth_bias_enable(false); + .depth_bias_enable(depth_bias_enable) + .depth_bias_constant_factor(depth_bias_constant_factor) + .depth_bias_slope_factor(depth_bias_slope_factor) + .depth_bias_clamp(0.0); let multisample_state = vk::PipelineMultisampleStateCreateInfo::default() .sample_shading_enable(false) .rasterization_samples(vk::SampleCountFlags::TYPE_1); @@ -1128,7 +1158,7 @@ fn create_graphics_pipeline( let pipeline_result = unsafe { device .device() - .create_graphics_pipelines(vk::PipelineCache::null(), &create_infos, None) + .create_graphics_pipelines(pipeline_cache, &create_infos, None) }; // SAFETY: The shader modules were created above on this live logical device and are no longer needed after pipeline creation. unsafe { @@ -1337,7 +1367,20 @@ fn destroy_partial_swapchain_resources( #[cfg(test)] mod tests { - use super::draw_viewport; + use super::{depth_bias_parameters, draw_viewport}; + use fparkan_render::LegacyDepthBias; + + #[test] + fn projected_decal_bias_parameters_use_empirical_vulkan_factors() { + assert_eq!( + depth_bias_parameters(LegacyDepthBias::Disabled), + (false, 0.0, 0.0) + ); + assert_eq!( + depth_bias_parameters(LegacyDepthBias::ProjectedDecal), + (true, -1.0, 0.0) + ); + } #[test] fn world_viewport_depth_keeps_distant_geometry_in_front_of_screen_sun() { |
