diff options
Diffstat (limited to 'adapters')
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/asset_mesh.rs | 35 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi.rs | 5 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 72 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs | 93 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs | 81 |
5 files changed, 225 insertions, 61 deletions
diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index b90499a..fdc378f 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -68,6 +68,7 @@ pub fn project_msh_to_static_mesh( draw_ranges.push(VulkanStaticDrawRange { first_index, index_count: u32::from(batch.index_count), + material_index: batch.material_index, }); } if indices.is_empty() { @@ -181,6 +182,7 @@ mod tests { vec![VulkanStaticDrawRange { first_index: 0, index_count: 3, + material_index: 0, }] ); assert_eq!(mesh.vertices[1].position, [-0.8, -0.8]); @@ -203,4 +205,37 @@ mod tests { assert_eq!(mesh.vertices[1].uv, [0.0, 2.0]); assert_eq!(mesh.vertices[2].uv, [-1.0, 0.5]); } + + #[test] + fn retains_each_source_batch_material_selector() { + let mut second = batch(3, 3, 0); + second.material_index = 7; + let mesh = project_msh_to_static_mesh(&model( + vec![ + [-2.0, 0.0, -1.0], + [2.0, 0.0, -1.0], + [-2.0, 0.0, 3.0], + [2.0, 0.0, 3.0], + ], + vec![0, 1, 2, 1, 3, 2], + vec![batch(0, 3, 0), second], + )) + .expect("representable MSH"); + + assert_eq!( + mesh.draw_ranges, + vec![ + VulkanStaticDrawRange { + first_index: 0, + index_count: 3, + material_index: 0, + }, + VulkanStaticDrawRange { + first_index: 3, + index_count: 3, + material_index: 7, + }, + ] + ); + } } diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs index 3beedbd..91c8254 100644 --- a/adapters/fparkan-render-vulkan/src/ffi.rs +++ b/adapters/fparkan-render-vulkan/src/ffi.rs @@ -65,8 +65,9 @@ pub use self::runtime::{ pub use self::smoke_types::{ VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, - VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticDrawRange, VulkanStaticMesh, - VulkanStaticTexture, VulkanStaticVertex, VulkanValidationReport, + VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanStaticDrawRange, + VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture, VulkanStaticVertex, + VulkanValidationReport, }; #[cfg(test)] use self::surface::extension_name; diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index a071e74..533ab1c 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -2,6 +2,7 @@ use ash::vk; +use super::smoke_types::resolve_draw_texture_indices; use super::{ create_command_pool, create_frame_sync, create_static_mesh_index_buffer, create_static_mesh_vertex_buffer, create_static_texture_image, create_swapchain_resources, @@ -109,11 +110,9 @@ impl VulkanSmokeRenderer { .mesh .validate() .map_err(|context| VulkanSmokeRendererError::InvalidStaticMesh { context })?; - if let Some(texture) = &create_info.texture { - texture - .validate() - .map_err(|context| VulkanSmokeRendererError::InvalidStaticTexture { context })?; - } + let draw_texture_indices = + resolve_draw_texture_indices(&create_info.mesh.draw_ranges, &create_info.materials) + .map_err(|context| VulkanSmokeRendererError::InvalidStaticMesh { context })?; let bootstrap_progress = create_info.bootstrap_progress.as_ref(); let shader_manifest = validate_shader_manifest(&triangle_shader_manifest()) .map_err(VulkanSmokeRendererError::ShaderManifest)?; @@ -188,18 +187,31 @@ impl VulkanSmokeRenderer { height: 1, rgba8: vec![255, 255, 255, 255], }; - let texture_source = create_info.texture.as_ref().unwrap_or(&fallback_texture); - let texture = + let texture_sources = if create_info.materials.is_empty() { + vec![&fallback_texture] + } else { + create_info + .materials + .iter() + .map(|material| &material.texture) + .collect() + }; + let mut textures = Vec::with_capacity(texture_sources.len()); + for texture_source in texture_sources { match create_static_texture_image(&instance, &device, command_pool, texture_source) { - Ok(image) => Some(image), + Ok(image) => textures.push(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 mut renderer = Self { instance: Some(instance), validation, @@ -210,8 +222,9 @@ impl VulkanSmokeRenderer { swapchain_resources: None, vertex_buffer: Some(vertex_buffer), index_buffer: Some(index_buffer), - texture, + textures, draw_ranges: create_info.mesh.draw_ranges.clone(), + draw_texture_indices, frame_sync: Vec::new(), images_in_flight: Vec::new(), current_frame: 0, @@ -322,12 +335,14 @@ impl VulkanSmokeRenderer { }) } - fn texture_ref(&self) -> Result<&super::VulkanAllocatedImage, VulkanSmokeRendererError> { - self.texture - .as_ref() - .ok_or(VulkanSmokeRendererError::InvariantViolation { - context: "static material texture", + fn textures_ref(&self) -> Result<&[super::VulkanAllocatedImage], VulkanSmokeRendererError> { + if self.textures.is_empty() { + Err(VulkanSmokeRendererError::InvariantViolation { + context: "static material textures", }) + } else { + Ok(&self.textures) + } } fn surface_ref(&self) -> Result<&VulkanSurfaceProbe, VulkanSmokeRendererError> { @@ -544,7 +559,7 @@ impl VulkanSmokeRenderer { self.command_pool, self.vertex_buffer_ref()?, self.index_buffer_ref()?, - self.texture_ref()?, + self.textures_ref()?, reuse_command_pool, )?, |resources| destroy_swapchain_resources(device, self.command_pool, resources), @@ -621,14 +636,6 @@ impl VulkanSmokeRenderer { vk::PipelineBindPoint::GRAPHICS, resources.pipeline, ); - device.device().cmd_bind_descriptor_sets( - command_buffer, - vk::PipelineBindPoint::GRAPHICS, - resources.pipeline_layout, - 0, - &[resources.descriptor_set], - &[], - ); let vertex_buffers = [self.vertex_buffer_ref()?.buffer]; let offsets = [0_u64]; device @@ -640,7 +647,20 @@ impl VulkanSmokeRenderer { 0, vk::IndexType::UINT16, ); - for range in &self.draw_ranges { + for (range, &texture_index) in self.draw_ranges.iter().zip(&self.draw_texture_indices) { + 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), + &[], + ); device.device().cmd_draw_indexed( command_buffer, range.index_count, @@ -689,7 +709,7 @@ impl VulkanSmokeRenderer { fn destroy_device_owned_resources(&mut self) { self.destroy_swapchain_resources(); if let Some(device) = self.device.as_ref() { - if let Some(texture) = self.texture.take() { + for texture in self.textures.drain(..) { destroy_allocated_image(device, &texture); } if let Some(buffer) = self.index_buffer.take() { diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 0821aba..da2694e 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -29,8 +29,12 @@ pub struct VulkanSmokeRendererCreateInfo { /// This initial bridge keeps positions in clip-space. MSH transforms, /// materials, and textures are deliberately higher-level Stage 3 work. pub mesh: VulkanStaticMesh, - /// Optional RGBA8 texture uploaded before the first live frame. - pub texture: Option<VulkanStaticTexture>, + /// Material textures uploaded before the first live frame. + /// + /// An empty list retains the compatibility white fallback. A singleton list + /// is a deliberately explicit one-material compatibility mode; otherwise + /// every source batch selector must resolve to one entry in this list. + pub materials: Vec<VulkanStaticMaterial>, /// Optional shared bootstrap progress tracker for failure evidence. pub bootstrap_progress: Option<Arc<VulkanSmokeBootstrapProgress>>, } @@ -64,6 +68,17 @@ pub struct VulkanStaticDrawRange { pub first_index: u32, /// Number of indices in this draw. pub index_count: u32, + /// Original positional `Batch20.material_index` selector. + pub material_index: u16, +} + +/// One diffuse material texture keyed by an original MSH batch selector. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VulkanStaticMaterial { + /// Positional material selector used by one or more source batches. + pub material_index: u16, + /// Decoded RGBA8 diffuse texture for this selector. + pub texture: VulkanStaticTexture, } /// Decoded RGBA8 image accepted by the initial Vulkan texture upload path. @@ -98,6 +113,39 @@ impl VulkanStaticTexture { } } +/// Resolves each source range to its descriptor-set material index. +/// +/// Empty material input selects the renderer's white fallback. A singleton is +/// the explicit direct-TEXM compatibility mode. Multiple materials must map +/// each `Batch20.material_index` exactly and never duplicate a selector. +pub(super) fn resolve_draw_texture_indices( + draw_ranges: &[VulkanStaticDrawRange], + materials: &[VulkanStaticMaterial], +) -> Result<Vec<usize>, &'static str> { + for (index, material) in materials.iter().enumerate() { + material.texture.validate()?; + if materials[..index] + .iter() + .any(|previous| previous.material_index == material.material_index) + { + return Err("static material selectors must be unique"); + } + } + draw_ranges + .iter() + .map(|range| { + if materials.len() <= 1 { + Ok(0) + } else { + materials + .iter() + .position(|material| material.material_index == range.material_index) + .ok_or("static mesh draw range has no material texture") + } + }) + .collect() +} + impl VulkanStaticMesh { /// Returns the compatibility triangle used by the native Stage 0 smoke app. #[must_use] @@ -124,6 +172,7 @@ impl VulkanStaticMesh { draw_ranges: vec![VulkanStaticDrawRange { first_index: 0, index_count: 3, + material_index: 0, }], } } @@ -189,6 +238,7 @@ mod static_mesh_tests { draw_ranges: vec![VulkanStaticDrawRange { first_index: 0, index_count: 2, + material_index: 0, }], }; let out_of_range_index = VulkanStaticMesh { @@ -207,6 +257,42 @@ mod static_mesh_tests { Err("static mesh index exceeds vertex count") ); } + + #[test] + fn material_descriptors_follow_source_batch_selectors() { + let ranges = [ + VulkanStaticDrawRange { + first_index: 0, + index_count: 3, + material_index: 7, + }, + VulkanStaticDrawRange { + first_index: 3, + index_count: 3, + material_index: 2, + }, + ]; + let texture = || VulkanStaticTexture { + width: 1, + height: 1, + rgba8: vec![255; 4], + }; + let materials = [ + VulkanStaticMaterial { + material_index: 2, + texture: texture(), + }, + VulkanStaticMaterial { + material_index: 7, + texture: texture(), + }, + ]; + + assert_eq!( + resolve_draw_texture_indices(&ranges, &materials), + Ok(vec![1, 0]) + ); + } } /// Shared bootstrap progress used to report partial renderer startup evidence. @@ -421,8 +507,9 @@ pub struct VulkanSmokeRenderer { pub(super) swapchain_resources: Option<VulkanSwapchainResources>, pub(super) vertex_buffer: Option<VulkanAllocatedBuffer>, pub(super) index_buffer: Option<VulkanAllocatedBuffer>, - pub(super) texture: Option<VulkanAllocatedImage>, + pub(super) textures: Vec<VulkanAllocatedImage>, pub(super) draw_ranges: Vec<VulkanStaticDrawRange>, + pub(super) draw_texture_indices: Vec<usize>, pub(super) frame_sync: Vec<VulkanFrameSync>, pub(super) images_in_flight: Vec<vk::Fence>, pub(super) current_frame: usize, diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs index 50bf0a1..8dbbc27 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs @@ -14,7 +14,7 @@ pub(super) struct VulkanSwapchainResources { pub(super) pipeline_layout: vk::PipelineLayout, pub(super) descriptor_set_layout: vk::DescriptorSetLayout, pub(super) descriptor_pool: vk::DescriptorPool, - pub(super) descriptor_set: vk::DescriptorSet, + pub(super) descriptor_sets: Vec<vk::DescriptorSet>, pub(super) sampler: vk::Sampler, pub(super) pipeline: vk::Pipeline, pub(super) framebuffers: Vec<vk::Framebuffer>, @@ -39,7 +39,7 @@ pub(super) fn create_swapchain_resources( command_pool: vk::CommandPool, vertex_buffer: &VulkanAllocatedBuffer, index_buffer: &VulkanAllocatedBuffer, - texture: &VulkanAllocatedImage, + textures: &[VulkanAllocatedImage], reuse_command_pool: bool, ) -> Result<VulkanSwapchainResources, VulkanSmokeRendererError> { // SAFETY: The swapchain is live and owned by this renderer for the duration of the query. @@ -73,13 +73,13 @@ pub(super) fn create_swapchain_resources( pipeline, descriptor_set_layout, descriptor_pool, - descriptor_set, + descriptor_sets, sampler, ) = match create_swapchain_pipeline_bundle( device, swapchain.report.plan.format.format, swapchain.report.plan.extent, - texture, + textures, ) { Ok(bundle) => bundle, Err(error) => { @@ -126,7 +126,7 @@ pub(super) fn create_swapchain_resources( pipeline_layout, descriptor_set_layout, descriptor_pool, - descriptor_set, + descriptor_sets, sampler, pipeline, framebuffers: partial.framebuffers, @@ -151,7 +151,7 @@ fn create_swapchain_pipeline_bundle( device: &VulkanLogicalDeviceProbe, format: i32, extent: (u32, u32), - texture: &VulkanAllocatedImage, + textures: &[VulkanAllocatedImage], ) -> Result< ( vk::RenderPass, @@ -159,14 +159,14 @@ fn create_swapchain_pipeline_bundle( vk::Pipeline, vk::DescriptorSetLayout, vk::DescriptorPool, - vk::DescriptorSet, + Vec<vk::DescriptorSet>, vk::Sampler, ), VulkanSmokeRendererError, > { let render_pass = create_render_pass(device, format)?; - let (descriptor_set_layout, descriptor_pool, descriptor_set, sampler) = - create_texture_descriptor_bundle(device, texture).inspect_err(|_| { + let (descriptor_set_layout, descriptor_pool, descriptor_sets, sampler) = + create_texture_descriptor_bundle(device, textures).inspect_err(|_| { // SAFETY: The render pass was created above on this live logical device and is destroyed on setup failure. unsafe { device.device().destroy_render_pass(render_pass, None) }; })?; @@ -207,7 +207,7 @@ fn create_swapchain_pipeline_bundle( pipeline, descriptor_set_layout, descriptor_pool, - descriptor_set, + descriptor_sets, sampler, )) } @@ -334,12 +334,12 @@ fn create_pipeline_layout( #[allow(clippy::too_many_lines)] fn create_texture_descriptor_bundle( device: &VulkanLogicalDeviceProbe, - texture: &VulkanAllocatedImage, + textures: &[VulkanAllocatedImage], ) -> Result< ( vk::DescriptorSetLayout, vk::DescriptorPool, - vk::DescriptorSet, + Vec<vk::DescriptorSet>, vk::Sampler, ), VulkanSmokeRendererError, @@ -361,13 +361,23 @@ fn create_texture_descriptor_bundle( context: "vkCreateDescriptorSetLayout", result, })?; + let texture_count = u32::try_from(textures.len()).map_err(|_| { + VulkanSmokeRendererError::InvariantViolation { + context: "static material texture count exceeds Vulkan descriptor limit", + } + })?; + if texture_count == 0 { + return Err(VulkanSmokeRendererError::InvariantViolation { + context: "static material texture list is empty", + }); + } let pool_size = vk::DescriptorPoolSize::default() .ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) - .descriptor_count(1); + .descriptor_count(texture_count); let pool_sizes = [pool_size]; let pool_info = vk::DescriptorPoolCreateInfo::default() .pool_sizes(&pool_sizes) - .max_sets(1); + .max_sets(texture_count); let pool = // SAFETY: The pool description is stack-owned and reserves exactly one descriptor. unsafe { device.device().create_descriptor_pool(&pool_info, None) }.map_err(|result| { @@ -378,12 +388,12 @@ fn create_texture_descriptor_bundle( result, } })?; - let layouts = [layout]; + let layouts = vec![layout; textures.len()]; let allocate_info = vk::DescriptorSetAllocateInfo::default() .descriptor_pool(pool) .set_layouts(&layouts); // SAFETY: The pool and layout are live on this logical device. - let descriptor_set = unsafe { device.device().allocate_descriptor_sets(&allocate_info) } + let descriptor_sets = unsafe { device.device().allocate_descriptor_sets(&allocate_info) } .map_err(|result| { // SAFETY: Resources were created above on this device and are rolled back once. unsafe { @@ -394,7 +404,7 @@ fn create_texture_descriptor_bundle( context: "vkAllocateDescriptorSets", result, } - })?[0]; + })?; let sampler_info = vk::SamplerCreateInfo::default() .mag_filter(vk::Filter::LINEAR) .min_filter(vk::Filter::LINEAR) @@ -416,19 +426,30 @@ fn create_texture_descriptor_bundle( result, } })?; - let image_info = vk::DescriptorImageInfo::default() - .sampler(sampler) - .image_view(texture.view) - .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL); - let image_infos = [image_info]; - let write = vk::WriteDescriptorSet::default() - .dst_set(descriptor_set) - .dst_binding(0) - .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) - .image_info(&image_infos); - // SAFETY: The descriptor set, sampler and image view are live and the texture upload completed its shader-read transition. - unsafe { device.device().update_descriptor_sets(&[write], &[]) }; - Ok((layout, pool, descriptor_set, sampler)) + let image_infos = textures + .iter() + .map(|texture| { + vk::DescriptorImageInfo::default() + .sampler(sampler) + .image_view(texture.view) + .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) + }) + .collect::<Vec<_>>(); + let writes = descriptor_sets + .iter() + .copied() + .zip(image_infos.iter()) + .map(|(descriptor_set, image_info)| { + vk::WriteDescriptorSet::default() + .dst_set(descriptor_set) + .dst_binding(0) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .image_info(std::slice::from_ref(image_info)) + }) + .collect::<Vec<_>>(); + // SAFETY: Descriptor sets, sampler and image views are live; every texture upload completed its shader-read transition. + unsafe { device.device().update_descriptor_sets(&writes, &[]) }; + Ok((layout, pool, descriptor_sets, sampler)) } #[allow(clippy::cast_precision_loss)] |
