diff options
| author | Valentin Popov <valentin@popov.link> | 2026-11-01 00:31:26 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-11-01 00:31:26 +0300 |
| commit | 15aadf7f47b608d4990d2c981e451a7af3ae2cee (patch) | |
| tree | 499a0954af51201fe3ad966668fee540d9e8e091 /adapters/fparkan-render-vulkan | |
| parent | 3aa9493a02abd27f4f0c803f594047d37ff9058b (diff) | |
| download | fparkan-15aadf7f47b608d4990d2c981e451a7af3ae2cee.tar.xz fparkan-15aadf7f47b608d4990d2c981e451a7af3ae2cee.zip | |
perf(render): prewarm pipeline variants with a Vulkan cache
Diffstat (limited to 'adapters/fparkan-render-vulkan')
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 105 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs | 3 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs | 70 |
3 files changed, 119 insertions, 59 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index 2140cab..1b50914 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -364,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), @@ -429,8 +430,21 @@ impl VulkanSmokeRenderer { 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()?; @@ -465,6 +479,7 @@ impl VulkanSmokeRenderer { mesh_full_upload_bytes: 0, initial_texture_image_count, texture_detach_count: 0, + pipeline_creation_count: renderer.report.pipeline_creation_count, }; Ok(renderer) } @@ -962,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(()) } @@ -1035,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 @@ -1045,9 +1055,6 @@ impl VulkanSmokeRenderer { Some(transparent), &mut self.draw_order_dirty, ); - if pipeline_changed { - self.rebuild_pipeline_resources()?; - } Ok(()) } @@ -1673,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, )?, @@ -1696,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 @@ -2071,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); } @@ -2429,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, @@ -2597,7 +2608,7 @@ mod tests { use super::{ apply_geometry_range_updates, apply_world_transparency, completed_readback_buffer_index, deduplicate_texture_sources, draw_material_alphas, geometry_upload_range, - mark_draw_order_dirty_on_activity_change, set_and_mark_dirty_if_changed, + 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, @@ -2758,6 +2769,36 @@ 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 { diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 5bc5d8c..5d6f12f 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -1343,6 +1343,8 @@ pub struct VulkanSmokeRendererReport { 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. @@ -1485,6 +1487,7 @@ 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>, diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs index 973e8b1..a44f79d 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs @@ -174,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> { @@ -281,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 @@ -449,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, @@ -510,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, @@ -946,23 +955,29 @@ fn depth_bias_parameters(depth_bias: LegacyDepthBias) -> (bool, f32, f32) { 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) => { @@ -982,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), @@ -1142,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 { |
