From fd9ce461c2668dcbf7ac6a5ecd543cd12952d698 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 07:39:38 +0400 Subject: [PATCH] feat(render): sample original TEXM in Vulkan --- .../shaders/manifest.json | 2 +- .../shaders/triangle.frag | 5 +- .../shaders/triangle.frag.spv | Bin 500 -> 788 bytes .../shaders/triangle.vert | 3 + .../shaders/triangle.vert.spv | Bin 1012 -> 1160 bytes .../fparkan-render-vulkan/src/asset_mesh.rs | 7 + adapters/fparkan-render-vulkan/src/ffi.rs | 29 ++- .../src/ffi/resources.rs | 9 +- .../fparkan-render-vulkan/src/ffi/smoke.rs | 49 ++-- .../src/ffi/smoke_types.rs | 5 + .../src/ffi/swapchain_resources.rs | 219 +++++++++++++++++- .../fparkan-render-vulkan/src/ffi/tests.rs | 8 +- .../src/shader_manifest.rs | 6 +- docs/rendering/renderer_truth_table.md | 4 +- fixtures/acceptance/coverage.tsv | 2 +- 15 files changed, 307 insertions(+), 41 deletions(-) diff --git a/adapters/fparkan-render-vulkan/shaders/manifest.json b/adapters/fparkan-render-vulkan/shaders/manifest.json index ee61039..40ec615 100644 --- a/adapters/fparkan-render-vulkan/shaders/manifest.json +++ b/adapters/fparkan-render-vulkan/shaders/manifest.json @@ -1 +1 @@ -{"schema":2,"target_env":"vulkan1.1","compiler":{"name":"glslangValidator","version":"11:16.3.0","binary_sha256":"9bcd69d830b350aaa6e2254915ff74e46070e217b67f38daad27c1fc1f22910f"},"validator":{"name":"spirv-val","version":"SPIRV-Tools v2026.2 unknown hash, 2026-04-29T17:02:58+00:00","binary_sha256":"f6d5b96ff19f073f3af0c0bcfa0c18702d288d3ec598efc242d01cd104d8354f"},"modules":[{"name":"triangle.vert","stage":"vertex","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert","source_sha256":"1e57f14d193fc61457c0749081c452ad25669998913107df12f3ccc3c33e0341","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","word_count":253,"sha256":"4d3ceca7b42ebc971d831b0a0d816457397bd9aeda47fb8d44c4b1aeaa5e7ba0","descriptor_sets":0,"push_constant_bytes":0,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S vert -e main adapters/fparkan-render-vulkan/shaders/triangle.vert -o adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","interface_hash":"23e1d3d9d32e7f7ec0b9ca87f8b86be8f8363c7eb5d745fc5a157cb8433eb138"},{"name":"triangle.frag","stage":"fragment","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag","source_sha256":"f19e74d001d07fb537d4b0f9e621f9b8bc40eeb68816130220853abea6bd4445","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","word_count":125,"sha256":"5a7441be03cd3c25d557268b2e58d5aa50504c87bffcb4c3fd7cbcf007db0b96","descriptor_sets":0,"push_constant_bytes":0,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S frag -e main adapters/fparkan-render-vulkan/shaders/triangle.frag -o adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","interface_hash":"f09342c22d58c8768151ab8579e54e49af586434a4005d16a24e816d881a64f0"}],"manifest_hash":"11e3feb65200ebd2ac87b7e776e9c6433a5da9d71a651bfadea89a51be17ff05"} \ No newline at end of file +{"schema":2,"target_env":"vulkan1.1","compiler":{"name":"glslangValidator","version":"11:16.3.0","binary_sha256":"9bcd69d830b350aaa6e2254915ff74e46070e217b67f38daad27c1fc1f22910f"},"validator":{"name":"spirv-val","version":"SPIRV-Tools v2026.2 unknown hash, 2026-04-29T17:02:58+00:00","binary_sha256":"f6d5b96ff19f073f3af0c0bcfa0c18702d288d3ec598efc242d01cd104d8354f"},"modules":[{"name":"triangle.vert","stage":"vertex","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert","source_sha256":"82f9ee39a30b094203041205954576ca820d75771421bb27ddd300359e4e9043","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","word_count":290,"sha256":"1984662f78873b70135ec444ccd86d86fa66dfca3f615d19ddaf0cda1587d4cf","descriptor_sets":0,"push_constant_bytes":0,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S vert -e main adapters/fparkan-render-vulkan/shaders/triangle.vert -o adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","interface_hash":"23e1d3d9d32e7f7ec0b9ca87f8b86be8f8363c7eb5d745fc5a157cb8433eb138"},{"name":"triangle.frag","stage":"fragment","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag","source_sha256":"70b600a0c79b6cdee1747a47caf7854bbe0bb9a693b3fe29345e659df549d96f","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","word_count":197,"sha256":"49dae3e1c46d5d23cccf3b161c36ea0b3a606e89c2289dbfed3e4fe991eb8556","descriptor_sets":1,"push_constant_bytes":0,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S frag -e main adapters/fparkan-render-vulkan/shaders/triangle.frag -o adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","interface_hash":"446615153811b02f05622dc3cdeae998461a6a225a3d2098bba0e0d158a21027"}],"manifest_hash":"038ecdb57832ac2d45a1ca6da5ec058b34f4f31b7170aa68fa612ac9d0ae7565"} diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.frag b/adapters/fparkan-render-vulkan/shaders/triangle.frag index 35bd374..3200d46 100644 --- a/adapters/fparkan-render-vulkan/shaders/triangle.frag +++ b/adapters/fparkan-render-vulkan/shaders/triangle.frag @@ -1,8 +1,11 @@ #version 450 layout(location = 0) in vec3 in_color; +layout(location = 1) in vec2 in_uv; layout(location = 0) out vec4 out_color; +layout(set = 0, binding = 0) uniform sampler2D base_color; + void main() { - out_color = vec4(in_color, 1.0); + out_color = texture(base_color, in_uv) * vec4(in_color, 1.0); } diff --git a/adapters/fparkan-render-vulkan/shaders/triangle.frag.spv b/adapters/fparkan-render-vulkan/shaders/triangle.frag.spv index 95f7c3750ea6f172a52fbc5f7281ddc59158987a..3f268690272a37042bd3fda336ae5bc0a8b0e49f 100644 GIT binary patch literal 788 zcmYk3xlTe+6h$u}gQ5(gAfg7eG$9shOpIF6P-rYH2@VB`LLy*o^+w0%=yVKco_w?*UMJZ$=p_(#zD_HS1 z9E}aEf-0zk3R+`%kl~lcev_DmUCznEm^Ycv22YdM$vbzs4eaA^`jU|5S$i}d%s*m{ zeejdC&_iRbWFLL&bbu%E4;OCaTT{I|8MkJVUt?z4cfmXFYv)^4PzOwVrdTuVAeuuU_9B<Yn+%dk|9T39j%{kx2KJMzV#t%#udj;!^CE$Ge&aesW|A5LjsR1?e zYV6wp_Pj%5zh=x~{<+waybxky)EG_b1s50+ZwbK-*<{@%?UoR){B8Uy zZcKQdZNI`a)0y|3`R25(=1w=X8le{!!rL&FmCy_g++OI^c6fMvxHqnT@4erDXU0-! z)rsaTlchXB|D0xp{vs;7jIN;1&`q?(uZg|pVVf8C-?U+TS5B&vvobG7_;S|CiaAxk zv#xoQ%V`~ZkRv4C2?OkTesYwKex;)-y#m5cd3|H$IIFUJ)(s|5F7@OqzJl1AUcUIiN-I?zq_8hXgyKb3)h}~1{9AbC8kKIx1 zEJNzMqy7)->nqbQamQIpP8;)lBj?STun9UKzl#+c zPrwT^U;g6^yKjs?#CN6`7solR_ZVzFds%M@>sxv2iM>y(_XOX1F)sFAa@JeJTF?D9 z_+Q5+sIoi58~EmLg8NwW<+kv=Z>(WVb*`saXE5$uV!ykbeV$_z)VlT&zd#l9ozq*# zxIN?D%@@ByL-_Z_emi*9c9sLayZ3P?Mg9h+jr|1cZ6BfD^)pfLFE-|#R#9(c%v*|M y>?OW){iK>RB&eJcxovzoXFsBwGrdL=)R=oX`&Vi>_Z!sPxW7Hc=KrU{2lN(f=t<=O literal 1012 zcmYL`%}WAd5XN6!zskzY)IQ8sItJ3AB8ZA8u!9Ff=qh4MENr`?D@3RMw*IOvLCnUJYsHa0B`^ zrO5@SA3nFU_2eE%c=W?ROuVl<>W^hJ8?Kl7j`U?`9{7)ty!4GwZnYudPPdA3Pkds` w-H#-b!hN^_Gx5p))rMD+Khm&43jhEB diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index 8e45f08..2cf340f 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -100,6 +100,13 @@ pub fn project_msh_to_static_mesh( (position[2] - center_z) * scale, ], color: [0.82, 0.72, 0.31], + // The exact fixed-point normalization of Res5 is still under + // disassembly. Until then the asset viewer uses its documented XZ + // planar projection, rather than inventing a scale for raw i16 UV0. + uv: [ + (position[0] - center_x) / extent + 0.5, + (position[2] - center_z) / extent + 0.5, + ], }) .collect(); diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs index 2ee3168..315d4ff 100644 --- a/adapters/fparkan-render-vulkan/src/ffi.rs +++ b/adapters/fparkan-render-vulkan/src/ffi.rs @@ -90,7 +90,8 @@ const EXT_DEBUG_UTILS_EXTENSION: &str = "VK_EXT_debug_utils"; const VALIDATION_LAYER_NAME: &str = "VK_LAYER_KHRONOS_validation"; pub(crate) const SPIRV_MAGIC: u32 = 0x0723_0203; pub(crate) const SPIRV_VERSION_1_0: u32 = 0x0001_0000; -pub(crate) const TRIANGLE_VERTEX_SHADER_WORDS: &[u32] = &[ +#[allow(dead_code)] +const LEGACY_TRIANGLE_VERTEX_SHADER_WORDS: &[u32] = &[ 0x0723_0203, 0x0001_0300, 0x0008_000b, @@ -345,7 +346,8 @@ pub(crate) const TRIANGLE_VERTEX_SHADER_WORDS: &[u32] = &[ 0x0001_00fd, 0x0001_0038, ]; -pub(crate) const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[ +#[allow(dead_code)] +const LEGACY_TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[ 0x0723_0203, 0x0001_0300, 0x0008_000b, @@ -473,5 +475,28 @@ pub(crate) const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[ 0x0001_0038, ]; +const fn spirv_words(bytes: &[u8]) -> [u32; WORD_COUNT] { + let mut words = [0_u32; WORD_COUNT]; + let mut index = 0; + while index < WORD_COUNT { + let offset = index * 4; + words[index] = u32::from_le_bytes([ + bytes[offset], + bytes[offset + 1], + bytes[offset + 2], + bytes[offset + 3], + ]); + index += 1; + } + words +} + +static TRIANGLE_VERTEX_SHADER_DATA: [u32; 290] = + spirv_words(include_bytes!("../shaders/triangle.vert.spv")); +static TRIANGLE_FRAGMENT_SHADER_DATA: [u32; 197] = + spirv_words(include_bytes!("../shaders/triangle.frag.spv")); +pub(crate) const TRIANGLE_VERTEX_SHADER_WORDS: &[u32] = &TRIANGLE_VERTEX_SHADER_DATA; +pub(crate) const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &TRIANGLE_FRAGMENT_SHADER_DATA; + #[cfg(test)] mod tests; diff --git a/adapters/fparkan-render-vulkan/src/ffi/resources.rs b/adapters/fparkan-render-vulkan/src/ffi/resources.rs index f3726f5..089e759 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/resources.rs @@ -44,9 +44,14 @@ pub(super) fn create_static_mesh_vertex_buffer( device: &VulkanLogicalDeviceProbe, mesh: &VulkanStaticMesh, ) -> Result { - let mut bytes = Vec::with_capacity(mesh.vertices.len() * 5 * std::mem::size_of::()); + let mut bytes = Vec::with_capacity(mesh.vertices.len() * 7 * std::mem::size_of::()); for vertex in &mesh.vertices { - for value in vertex.position.into_iter().chain(vertex.color) { + for value in vertex + .position + .into_iter() + .chain(vertex.color) + .chain(vertex.uv) + { bytes.extend_from_slice(&value.to_ne_bytes()); } } diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index a57394e..d8cc0d5 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -181,21 +181,25 @@ impl VulkanSmokeRenderer { return Err(error); } }; - let texture = match create_info.texture.as_ref() { - None => None, - Some(texture) => { - match create_static_texture_image(&instance, &device, command_pool, texture) { - Ok(image) => Some(image), - Err(error) => { - // 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); - } - } - } + // Keep the compatibility triangle path valid: it samples a white texel + // when no original TEXM was supplied. + let fallback_texture = super::VulkanStaticTexture { + width: 1, + height: 1, + rgba8: vec![255, 255, 255, 255], }; + let texture_source = create_info.texture.as_ref().unwrap_or(&fallback_texture); + let texture = + match create_static_texture_image(&instance, &device, command_pool, texture_source) { + Ok(image) => Some(image), + Err(error) => { + // 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, @@ -318,6 +322,14 @@ impl VulkanSmokeRenderer { }) } + fn texture_ref(&self) -> Result<&super::VulkanAllocatedImage, VulkanSmokeRendererError> { + self.texture + .as_ref() + .ok_or(VulkanSmokeRendererError::InvariantViolation { + context: "static material texture", + }) + } + fn surface_ref(&self) -> Result<&VulkanSurfaceProbe, VulkanSmokeRendererError> { self.surface .as_ref() @@ -532,6 +544,7 @@ impl VulkanSmokeRenderer { self.command_pool, self.vertex_buffer_ref()?, self.index_buffer_ref()?, + self.texture_ref()?, reuse_command_pool, )?, |resources| destroy_swapchain_resources(device, self.command_pool, resources), @@ -608,6 +621,14 @@ 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 diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 165633d..af84fed 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -42,6 +42,8 @@ pub struct VulkanStaticVertex { pub position: [f32; 2], /// Linear RGB vertex color. pub color: [f32; 3], + /// Texture coordinate consumed by the static material bridge. + pub uv: [f32; 2], } /// Static indexed geometry uploaded to live Vulkan buffers. @@ -94,14 +96,17 @@ impl VulkanStaticMesh { VulkanStaticVertex { position: [0.0, -0.55], color: [1.0, 0.2, 0.2], + uv: [0.5, 0.0], }, VulkanStaticVertex { position: [0.55, 0.55], color: [0.2, 1.0, 0.2], + uv: [1.0, 1.0], }, VulkanStaticVertex { position: [-0.55, 0.55], color: [0.2, 0.4, 1.0], + uv: [0.0, 1.0], }, ], indices: vec![0, 1, 2], diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs index dd68ef4..50bf0a1 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain_resources.rs @@ -3,7 +3,7 @@ use ash::vk; use super::{ - color_subresource_range, VulkanAllocatedBuffer, VulkanLogicalDeviceProbe, + color_subresource_range, VulkanAllocatedBuffer, VulkanAllocatedImage, VulkanLogicalDeviceProbe, VulkanSmokeRendererError, VulkanSwapchainProbe, TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS, }; @@ -12,6 +12,10 @@ pub(super) struct VulkanSwapchainResources { pub(super) image_views: Vec, pub(super) render_pass: vk::RenderPass, 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) sampler: vk::Sampler, pub(super) pipeline: vk::Pipeline, pub(super) framebuffers: Vec, pub(super) command_buffers: Vec, @@ -21,6 +25,9 @@ struct PartialSwapchainResources { image_views: Vec, render_pass: Option, pipeline_layout: Option, + descriptor_set_layout: Option, + descriptor_pool: Option, + sampler: Option, pipeline: Option, framebuffers: Vec, command_buffers: Vec, @@ -32,6 +39,7 @@ pub(super) fn create_swapchain_resources( command_pool: vk::CommandPool, vertex_buffer: &VulkanAllocatedBuffer, index_buffer: &VulkanAllocatedBuffer, + texture: &VulkanAllocatedImage, reuse_command_pool: bool, ) -> Result { // SAFETY: The swapchain is live and owned by this renderer for the duration of the query. @@ -52,14 +60,26 @@ pub(super) fn create_swapchain_resources( )?, render_pass: None, pipeline_layout: None, + descriptor_set_layout: None, + descriptor_pool: None, + sampler: None, pipeline: None, framebuffers: Vec::new(), command_buffers: Vec::new(), }; - let (render_pass, pipeline_layout, pipeline) = match create_swapchain_pipeline_bundle( + let ( + render_pass, + pipeline_layout, + pipeline, + descriptor_set_layout, + descriptor_pool, + descriptor_set, + sampler, + ) = match create_swapchain_pipeline_bundle( device, swapchain.report.plan.format.format, swapchain.report.plan.extent, + texture, ) { Ok(bundle) => bundle, Err(error) => { @@ -69,6 +89,9 @@ pub(super) fn create_swapchain_resources( }; partial.render_pass = Some(render_pass); partial.pipeline_layout = Some(pipeline_layout); + partial.descriptor_set_layout = Some(descriptor_set_layout); + partial.descriptor_pool = Some(descriptor_pool); + partial.sampler = Some(sampler); partial.pipeline = Some(pipeline); let framebuffers = match create_swapchain_framebuffers( device, @@ -101,6 +124,10 @@ pub(super) fn create_swapchain_resources( image_views: partial.image_views, render_pass, pipeline_layout, + descriptor_set_layout, + descriptor_pool, + descriptor_set, + sampler, pipeline, framebuffers: partial.framebuffers, command_buffers: partial.command_buffers, @@ -119,16 +146,44 @@ fn create_swapchain_image_views( Ok(image_views) } +#[allow(clippy::type_complexity)] fn create_swapchain_pipeline_bundle( device: &VulkanLogicalDeviceProbe, format: i32, extent: (u32, u32), -) -> Result<(vk::RenderPass, vk::PipelineLayout, vk::Pipeline), VulkanSmokeRendererError> { + texture: &VulkanAllocatedImage, +) -> Result< + ( + vk::RenderPass, + vk::PipelineLayout, + vk::Pipeline, + vk::DescriptorSetLayout, + vk::DescriptorPool, + vk::DescriptorSet, + vk::Sampler, + ), + VulkanSmokeRendererError, +> { let render_pass = create_render_pass(device, format)?; - let pipeline_layout = create_pipeline_layout(device).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) }; - })?; + let (descriptor_set_layout, descriptor_pool, descriptor_set, sampler) = + create_texture_descriptor_bundle(device, texture).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) }; + })?; + let pipeline_layout = + create_pipeline_layout(device, descriptor_set_layout).inspect_err(|_| { + // SAFETY: The descriptor resources and render pass were created above and are rolled back on this device. + unsafe { + 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); + } + })?; let pipeline = create_graphics_pipeline(device, render_pass, pipeline_layout, extent) .inspect_err(|_| { // SAFETY: These objects were created above on this live logical device and are destroyed on setup failure. @@ -136,10 +191,25 @@ fn create_swapchain_pipeline_bundle( 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); } })?; - Ok((render_pass, pipeline_layout, pipeline)) + Ok(( + render_pass, + pipeline_layout, + pipeline, + descriptor_set_layout, + descriptor_pool, + descriptor_set, + sampler, + )) } fn create_swapchain_framebuffers( @@ -248,9 +318,11 @@ fn create_render_pass( fn create_pipeline_layout( device: &VulkanLogicalDeviceProbe, + descriptor_set_layout: vk::DescriptorSetLayout, ) -> Result { - let create_info = vk::PipelineLayoutCreateInfo::default(); - // SAFETY: The pipeline layout has no descriptor sets or push constants in Stage 0 smoke. + let set_layouts = [descriptor_set_layout]; + let create_info = vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts); + // SAFETY: The descriptor-set layout belongs to this live logical device. unsafe { device.device().create_pipeline_layout(&create_info, None) }.map_err(|error| { VulkanSmokeRendererError::VulkanOperation { context: "vkCreatePipelineLayout", @@ -259,6 +331,106 @@ fn create_pipeline_layout( }) } +#[allow(clippy::too_many_lines)] +fn create_texture_descriptor_bundle( + device: &VulkanLogicalDeviceProbe, + texture: &VulkanAllocatedImage, +) -> Result< + ( + vk::DescriptorSetLayout, + vk::DescriptorPool, + vk::DescriptorSet, + vk::Sampler, + ), + VulkanSmokeRendererError, +> { + let binding = vk::DescriptorSetLayoutBinding::default() + .binding(0) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::FRAGMENT); + let bindings = [binding]; + let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings); + // SAFETY: The layout description is stack-owned and references no external memory. + let layout = unsafe { + device + .device() + .create_descriptor_set_layout(&layout_info, None) + } + .map_err(|result| VulkanSmokeRendererError::VulkanOperation { + context: "vkCreateDescriptorSetLayout", + result, + })?; + let pool_size = vk::DescriptorPoolSize::default() + .ty(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .descriptor_count(1); + let pool_sizes = [pool_size]; + let pool_info = vk::DescriptorPoolCreateInfo::default() + .pool_sizes(&pool_sizes) + .max_sets(1); + 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| { + // SAFETY: The layout was created above on this device and is rolled back once. + unsafe { device.device().destroy_descriptor_set_layout(layout, None) }; + VulkanSmokeRendererError::VulkanOperation { + context: "vkCreateDescriptorPool", + result, + } + })?; + let layouts = [layout]; + 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) } + .map_err(|result| { + // SAFETY: Resources were created above on this device and are rolled back once. + unsafe { + device.device().destroy_descriptor_pool(pool, None); + device.device().destroy_descriptor_set_layout(layout, None); + }; + VulkanSmokeRendererError::VulkanOperation { + context: "vkAllocateDescriptorSets", + result, + } + })?[0]; + let sampler_info = vk::SamplerCreateInfo::default() + .mag_filter(vk::Filter::LINEAR) + .min_filter(vk::Filter::LINEAR) + .mipmap_mode(vk::SamplerMipmapMode::LINEAR) + .address_mode_u(vk::SamplerAddressMode::REPEAT) + .address_mode_v(vk::SamplerAddressMode::REPEAT) + .address_mode_w(vk::SamplerAddressMode::REPEAT) + .max_lod(0.0); + let sampler = + // SAFETY: The sampler create info is stack-owned and has no unsupported optional features. + unsafe { device.device().create_sampler(&sampler_info, None) }.map_err(|result| { + // SAFETY: Resources were created above on this device and are rolled back once. + unsafe { + device.device().destroy_descriptor_pool(pool, None); + device.device().destroy_descriptor_set_layout(layout, None); + }; + VulkanSmokeRendererError::VulkanOperation { + context: "vkCreateSampler", + 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)) +} + #[allow(clippy::cast_precision_loss)] fn extent_component_to_f32(value: u32) -> f32 { value as f32 @@ -293,7 +465,7 @@ fn create_graphics_pipeline( ]; let vertex_binding = vk::VertexInputBindingDescription::default() .binding(0) - .stride(u32::try_from(5 * std::mem::size_of::()).unwrap_or(u32::MAX)) + .stride(u32::try_from(7 * std::mem::size_of::()).unwrap_or(u32::MAX)) .input_rate(vk::VertexInputRate::VERTEX); let vertex_attributes = [ vk::VertexInputAttributeDescription::default() @@ -306,6 +478,11 @@ fn create_graphics_pipeline( .location(1) .format(vk::Format::R32G32B32_SFLOAT) .offset(u32::try_from(2 * std::mem::size_of::()).unwrap_or(u32::MAX)), + vk::VertexInputAttributeDescription::default() + .binding(0) + .location(2) + .format(vk::Format::R32G32_SFLOAT) + .offset(u32::try_from(5 * std::mem::size_of::()).unwrap_or(u32::MAX)), ]; let vertex_bindings = [vertex_binding]; let vertex_input_state = vk::PipelineVertexInputStateCreateInfo::default() @@ -461,6 +638,13 @@ pub(super) fn destroy_swapchain_resources( device .device() .destroy_pipeline_layout(resources.pipeline_layout, None); + device.device().destroy_sampler(resources.sampler, None); + device + .device() + .destroy_descriptor_pool(resources.descriptor_pool, None); + device + .device() + .destroy_descriptor_set_layout(resources.descriptor_set_layout, None); device .device() .destroy_render_pass(resources.render_pass, None); @@ -493,6 +677,19 @@ fn destroy_partial_swapchain_resources( .device() .destroy_pipeline_layout(pipeline_layout, None); } + if let Some(sampler) = partial.sampler { + device.device().destroy_sampler(sampler, None); + } + if let Some(descriptor_pool) = partial.descriptor_pool { + device + .device() + .destroy_descriptor_pool(descriptor_pool, None); + } + if let Some(descriptor_set_layout) = partial.descriptor_set_layout { + device + .device() + .destroy_descriptor_set_layout(descriptor_set_layout, None); + } if let Some(render_pass) = partial.render_pass { device.device().destroy_render_pass(render_pass, None); } diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs index 9c927a8..731fb91 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs @@ -609,10 +609,10 @@ fn triangle_shader_manifest_hashes_are_stable() { TRIANGLE_VERTEX_SOURCE_SHA256 ); assert_eq!(report.modules[0].spirv_path, TRIANGLE_VERTEX_SPIRV_PATH); - assert_eq!(report.modules[0].word_count, 253); + assert_eq!(report.modules[0].word_count, 290); assert_eq!( report.modules[0].sha256, - "4d3ceca7b42ebc971d831b0a0d816457397bd9aeda47fb8d44c4b1aeaa5e7ba0" + "1984662f78873b70135ec444ccd86d86fa66dfca3f615d19ddaf0cda1587d4cf" ); assert_eq!(report.modules[0].descriptor_sets, 0); assert_eq!(report.modules[0].push_constant_bytes, 0); @@ -627,11 +627,11 @@ fn triangle_shader_manifest_hashes_are_stable() { assert!(!report.modules[0].interface_hash.is_empty()); assert_eq!( report.modules[1].sha256, - "5a7441be03cd3c25d557268b2e58d5aa50504c87bffcb4c3fd7cbcf007db0b96" + "49dae3e1c46d5d23cccf3b161c36ea0b3a606e89c2289dbfed3e4fe991eb8556" ); assert_eq!( report.manifest_hash, - "11e3feb65200ebd2ac87b7e776e9c6433a5da9d71a651bfadea89a51be17ff05" + "038ecdb57832ac2d45a1ca6da5ec058b34f4f31b7170aa68fa612ac9d0ae7565" ); } diff --git a/adapters/fparkan-render-vulkan/src/shader_manifest.rs b/adapters/fparkan-render-vulkan/src/shader_manifest.rs index b513aa6..259a222 100644 --- a/adapters/fparkan-render-vulkan/src/shader_manifest.rs +++ b/adapters/fparkan-render-vulkan/src/shader_manifest.rs @@ -20,7 +20,7 @@ pub(crate) const SPIRV_VALIDATOR_BINARY_SHA256: &str = pub(crate) const TRIANGLE_VERTEX_SOURCE_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.vert"; pub(crate) const TRIANGLE_VERTEX_SOURCE_SHA256: &str = - "1e57f14d193fc61457c0749081c452ad25669998913107df12f3ccc3c33e0341"; + "82f9ee39a30b094203041205954576ca820d75771421bb27ddd300359e4e9043"; pub(crate) const TRIANGLE_VERTEX_SPIRV_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.vert.spv"; pub(crate) const TRIANGLE_VERTEX_COMPILE_COMMAND: &str = @@ -29,7 +29,7 @@ pub(crate) const TRIANGLE_VERTEX_VALIDATE_COMMAND: &str = "spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv"; const TRIANGLE_FRAGMENT_SOURCE_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.frag"; const TRIANGLE_FRAGMENT_SOURCE_SHA256: &str = - "f19e74d001d07fb537d4b0f9e621f9b8bc40eeb68816130220853abea6bd4445"; + "70b600a0c79b6cdee1747a47caf7854bbe0bb9a693b3fe29345e659df549d96f"; const TRIANGLE_FRAGMENT_SPIRV_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.frag.spv"; const TRIANGLE_FRAGMENT_COMPILE_COMMAND: &str = @@ -225,7 +225,7 @@ pub fn triangle_shader_manifest() -> Vec { name: "triangle.frag", stage: VulkanShaderStage::Fragment, entry_point: "main", - descriptor_sets: 0, + descriptor_sets: 1, push_constant_bytes: 0, source_path: TRIANGLE_FRAGMENT_SOURCE_PATH, source_sha256: TRIANGLE_FRAGMENT_SOURCE_SHA256, diff --git a/docs/rendering/renderer_truth_table.md b/docs/rendering/renderer_truth_table.md index 2d0130d..61396ac 100644 --- a/docs/rendering/renderer_truth_table.md +++ b/docs/rendering/renderer_truth_table.md @@ -8,7 +8,7 @@ | Path | Native window / swapchain | Draws pixels | Uses original assets | Acceptance class | Что доказывает | Чего не доказывает | | --- | --- | --- | --- | --- | --- | --- | -| `fparkan-vulkan-smoke` / `VulkanSmokeRenderer` | Yes | Yes | Static MSH plus TEXM upload | `covered-gpu` for Stage 0 smoke and explicit MSH/TEXM bridge IDs | Loader, instance, surface, swapchain, submit/present, validation-clean triangle path; original MSH indexed draw; TEXM RGBA8 staging upload and `SHADER_READ_ONLY_OPTIMAL` transition | Texture sampling, descriptors, terrain, gameplay rendering | +| `fparkan-vulkan-smoke` / `VulkanSmokeRenderer` | Yes | Yes | Static MSH plus sampled TEXM | `covered-gpu` for Stage 0 smoke and explicit MSH/TEXM/descriptor bridge IDs | Loader, instance, surface, swapchain, submit/present, validation-clean triangle path; original MSH indexed draw; TEXM RGBA8 staging upload, `SHADER_READ_ONLY_OPTIMAL`, sampler/descriptor binding and fragment sampling | Original material mapping, exact MSH UV scale, terrain, gameplay rendering | | `VulkanPlanningBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Deterministic command validation, canonical capture, frame submission planning | Любой live GPU draw, pixel parity, validation-clean asset frame | | `RecordingBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Stable command capture for backend-neutral tests | Native window, Vulkan, GPU resource lifetime, pixels | | `NullBackend` | No | No | Optional CPU-side IDs only | Usually `covered` for validation-only rows | Command stream framing and bounds validation | Capture stability, GPU execution, pixels | @@ -29,7 +29,7 @@ ## Current repository status -- Реальный Vulkan в репозитории имеет smoke triangle path и узкий static asset bridge: `fparkan-vulkan-smoke --model-root --model-archive system.rlb --model-name MTCHECK.MSH --texture-root --texture-archive Textures.lib --texture-name DEFAULT.0` загружает validated original MSH, разворачивает batch `base_vertex`, нормализует XZ viewer plane, выполняет real indexed draw и загружает decoded TEXM mip-0 в device-local image. Image переходится в sampling layout, но ещё не связан descriptor set-ом или fragment shader; это не texture/material/terrain renderer. +- Реальный Vulkan в репозитории имеет smoke triangle path и узкий static asset bridge: `fparkan-vulkan-smoke --model-root --model-archive system.rlb --model-name MTCHECK.MSH --texture-root --texture-archive Textures.lib --texture-name DEFAULT.0` загружает validated original MSH, разворачивает batch `base_vertex`, нормализует XZ viewer plane, выполняет real indexed draw и загружает decoded TEXM mip-0 в device-local image. Image переходит в sampling layout, записывается в `set=0,binding=0` combined image sampler и sampled fragment shader-ом. Для static viewer UV сейчас XZ-planar: parser сохраняет raw packed `Res5` UV0, но его fixed-point scale ещё не доказан. Это не WEAR/MAT0, exact material, terrain или gameplay renderer. - `apps/fparkan-game` сейчас выдает `render-planning` JSON report поверх synthetic window descriptor и `VulkanPlanningBackend`. - `apps/fparkan-viewer` сейчас inspection-only CLI и не открывает live Vulkan diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv index 892fd94..1d6e955 100644 --- a/fixtures/acceptance/coverage.tsv +++ b/fixtures/acceptance/coverage.tsv @@ -296,7 +296,7 @@ S2-PLANNING-RENDER-008 covered-planning cargo test -p fparkan-render --offline r S2-PLANNING-RENDER-009 covered cargo xtask policy S3-VK-MESH-UPLOAD-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-mtcheck-msh.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root --model-archive system.rlb --model-name MTCHECK.MSH; original MSH reports 128 vertices and 252 indices S3-VK-TEXM-UPLOAD-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-mtcheck-texm-upload.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root --model-archive system.rlb --model-name MTCHECK.MSH --texture-root --texture-archive Textures.lib --texture-name DEFAULT.0; original TEXM 16x16 decoded RGBA8, staged into device-local image and transitioned to SHADER_READ_ONLY_OPTIMAL -S3-VK-DESCRIPTOR-001 blocked awaits Stage 3 descriptor set contract implementation +S3-VK-DESCRIPTOR-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-texm-sampled.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root --model-archive system.rlb --model-name MTCHECK.MSH --texture-root --texture-archive Textures.lib --texture-name DEFAULT.0; original TEXM is bound as set=0,binding=0 combined image sampler and sampled in fragment shader; validation_warning_count=0 and validation_error_count=0 S3-VK-PIPELINE-001 blocked awaits Stage 3 graphics pipeline key and cache implementation S3-VK-DRAW-MODEL-001 covered-gpu cargo run -p fparkan-vulkan-smoke --release --locked -- --out target/fparkan/stage3-mtcheck-msh.json --frames 300 --resize-frame 120 --timeout-seconds 90 --model-root --model-archive system.rlb --model-name MTCHECK.MSH; live Win32 Vulkan indexed MSH draw/present S3-VK-DRAW-TERRAIN-001 blocked awaits Stage 3 terrain Vulkan draw path