aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src/ffi
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 07:07:45 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 07:07:45 +0300
commitefaba9001fb64eca14db3668c1c5295f078691b5 (patch)
tree867a465547d992bbb4b048cc438f6e97d9398b2d /adapters/fparkan-render-vulkan/src/ffi
parentb915554f3c5695bd8b9707359ffaa51ea8742341 (diff)
downloadfparkan-efaba9001fb64eca14db3668c1c5295f078691b5.tar.xz
fparkan-efaba9001fb64eca14db3668c1c5295f078691b5.zip
feat(render): bind materials per mesh batch
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi')
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke.rs72
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs93
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs81
3 files changed, 187 insertions, 59 deletions
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)]