diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index 4862ca0..0d5e6b4 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -241,6 +241,7 @@ impl VulkanSmokeRenderer { enabled_extension_count: 0, swapchain_extent: (0, 0), swapchain_image_count: 0, + swapchain_image_usage: 0, }, }; renderer.rebuild_swapchain_resources(false)?; @@ -268,6 +269,7 @@ impl VulkanSmokeRenderer { .unwrap_or(u32::MAX), swapchain_extent: swapchain_ref.report.plan.extent, swapchain_image_count: swapchain_ref.report.image_count, + swapchain_image_usage: swapchain_ref.report.plan.image_usage, }; Ok(renderer) } @@ -571,11 +573,13 @@ impl VulkanSmokeRenderer { let frame_sync = create_frame_sync(device)?; let swapchain_extent = self.swapchain_ref()?.report.plan.extent; let swapchain_image_count = self.swapchain_ref()?.report.image_count; + let swapchain_image_usage = self.swapchain_ref()?.report.plan.image_usage; let resources = resources.commit(); self.images_in_flight = vec![vk::Fence::null(); resources.image_views.len()]; self.frame_sync = frame_sync; self.report.swapchain_extent = swapchain_extent; self.report.swapchain_image_count = swapchain_image_count; + self.report.swapchain_image_usage = swapchain_image_usage; self.swapchain_resources = Some(resources); Ok(()) } diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 70881d3..cbf978a 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -457,6 +457,8 @@ pub struct VulkanSmokeRendererReport { pub swapchain_extent: (u32, u32), /// Current swapchain image count. pub swapchain_image_count: u32, + /// Current swapchain image-usage flags as raw Vulkan bits. + pub swapchain_image_usage: u32, } /// Measured validation counters from the live smoke loop. diff --git a/adapters/fparkan-render-vulkan/src/ffi/swapchain.rs b/adapters/fparkan-render-vulkan/src/ffi/swapchain.rs index ca754d4..96ddedb 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/swapchain.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/swapchain.rs @@ -185,7 +185,7 @@ pub fn create_vulkan_swapchain_probe_for_extent( height: plan.extent.1, }) .image_array_layers(1) - .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) + .image_usage(vk::ImageUsageFlags::from_raw(plan.image_usage)) .image_sharing_mode(sharing_mode) .queue_family_indices(&queue_family_indices) .pre_transform(raw_capabilities.current_transform) diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs index 44e064b..0f6f5a8 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs @@ -94,6 +94,7 @@ fn frame_submission_plan_json_is_stable() -> Result<(), RenderError> { }, present_mode: vk::PresentModeKHR::FIFO.as_raw(), image_count: 3, + image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(), }; let plan = plan_vulkan_frame_submission(&swapchain, &commands)?; @@ -543,6 +544,23 @@ fn swapchain_plan_prefers_srgb_mailbox_and_clamps_extent() { assert_eq!(plan.present_mode, vk::PresentModeKHR::MAILBOX.as_raw()); assert_eq!(plan.extent, (1024, 720)); assert_eq!(plan.image_count, 3); + assert_eq!( + plan.image_usage, + vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw() + ); +} + +#[test] +fn swapchain_plan_enables_transfer_source_when_surface_supports_it() { + let mut request = swapchain_request(); + request.capabilities.supported_usage_flags |= vk::ImageUsageFlags::TRANSFER_SRC.as_raw(); + + let plan = plan_vulkan_swapchain(&request).expect("swapchain plan"); + + assert_eq!( + plan.image_usage, + (vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_SRC).as_raw() + ); } #[test] @@ -606,7 +624,7 @@ fn swapchain_plan_json_and_recreation_reports_are_stable() { let plan = plan_vulkan_swapchain(&swapchain_request()).expect("swapchain plan"); assert_eq!( render_swapchain_plan_json(&plan), - "{\"schema\":1,\"extent\":[1024,720],\"format\":50,\"color_space\":0,\"present_mode\":1,\"image_count\":3}" + "{\"schema\":1,\"extent\":[1024,720],\"format\":50,\"color_space\":0,\"present_mode\":1,\"image_count\":3,\"image_usage\":16}" ); let report = swapchain_recreation_report( diff --git a/adapters/fparkan-render-vulkan/src/planning_backend.rs b/adapters/fparkan-render-vulkan/src/planning_backend.rs index b68b054..9a91712 100644 --- a/adapters/fparkan-render-vulkan/src/planning_backend.rs +++ b/adapters/fparkan-render-vulkan/src/planning_backend.rs @@ -163,5 +163,6 @@ fn default_stage0_swapchain_plan() -> VulkanSwapchainPlan { }, present_mode: vk::PresentModeKHR::FIFO.as_raw(), image_count: 2, + image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(), } } diff --git a/adapters/fparkan-render-vulkan/src/policy.rs b/adapters/fparkan-render-vulkan/src/policy.rs index bb26aee..c79b8dc 100644 --- a/adapters/fparkan-render-vulkan/src/policy.rs +++ b/adapters/fparkan-render-vulkan/src/policy.rs @@ -96,6 +96,8 @@ pub struct VulkanSwapchainPlan { pub present_mode: i32, /// Selected image count. pub image_count: u32, + /// Selected image-usage flags as raw Vulkan bits. + pub image_usage: u32, } /// Swapchain planning error. @@ -425,6 +427,7 @@ pub fn plan_vulkan_swapchain( format, present_mode, image_count: select_image_count(request.capabilities), + image_usage: select_swapchain_image_usage(request.capabilities), }) } @@ -521,6 +524,7 @@ pub fn render_swapchain_plan_json(plan: &VulkanSwapchainPlan) -> String { color_space: i32, present_mode: i32, image_count: u32, + image_usage: u32, } serialize_json_or_fallback( @@ -531,8 +535,9 @@ pub fn render_swapchain_plan_json(plan: &VulkanSwapchainPlan) -> String { color_space: plan.format.color_space, present_mode: plan.present_mode, image_count: plan.image_count, + image_usage: plan.image_usage, }, - "{\"schema\":0,\"extent\":[0,0],\"format\":0,\"color_space\":0,\"present_mode\":0,\"image_count\":0}", + "{\"schema\":0,\"extent\":[0,0],\"format\":0,\"color_space\":0,\"present_mode\":0,\"image_count\":0,\"image_usage\":0}", ) } @@ -809,6 +814,15 @@ fn supports_depth_stencil_request( select_depth_stencil_attachment_format(&device.supported_depth_stencil_formats, depth).is_some() } +fn select_swapchain_image_usage(capabilities: VulkanSwapchainSurfaceCapabilities) -> u32 { + let mut usage = vk::ImageUsageFlags::COLOR_ATTACHMENT; + let supported = vk::ImageUsageFlags::from_raw(capabilities.supported_usage_flags); + if supported.contains(vk::ImageUsageFlags::TRANSFER_SRC) { + usage |= vk::ImageUsageFlags::TRANSFER_SRC; + } + usage.as_raw() +} + /// Selects the first canonical depth/stencil attachment format supported by a device. /// /// The order is part of the Windows Vulkan compatibility contract and is shared diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index 2b7e00b..24e4dd0 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -778,6 +778,9 @@ impl SmokeApp { vulkan_swapchain_image_count: renderer .as_ref() .map_or(0, |snapshot| snapshot.report.swapchain_image_count), + vulkan_swapchain_image_usage: renderer + .as_ref() + .map_or(0, |snapshot| snapshot.report.swapchain_image_usage), vulkan_portability_enumeration: renderer .as_ref() .is_some_and(|snapshot| snapshot.report.portability_enumeration), @@ -980,6 +983,7 @@ fn render_timeout_failure_report( vulkan_swapchain_width: 0, vulkan_swapchain_height: 0, vulkan_swapchain_image_count: 0, + vulkan_swapchain_image_usage: 0, vulkan_portability_enumeration: false, vulkan_portability_subset_enabled: false, }; @@ -1200,6 +1204,7 @@ struct SmokeReport<'a> { vulkan_swapchain_width: u32, vulkan_swapchain_height: u32, vulkan_swapchain_image_count: u32, + vulkan_swapchain_image_usage: u32, vulkan_portability_enumeration: bool, vulkan_portability_subset_enabled: bool, } @@ -1703,6 +1708,7 @@ mod tests { vulkan_swapchain_width: 960, vulkan_swapchain_height: 540, vulkan_swapchain_image_count: 3, + vulkan_swapchain_image_usage: 17, vulkan_portability_enumeration: false, vulkan_portability_subset_enabled: false, }) diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index fbbc928..e2f3b88 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -859,6 +859,22 @@ dynamic material data и не входит в `PipelineKey`. Также не у дополнительного дизассемблирования. Это частично реализованная compatibility boundary, а не заявление о готовой parity fixed-function state. +### Capability-gated swapchain source for future pixel capture + +Pixel parity требует чтения результата GPU, но swapchain image разрешает это +только если surface сообщает `VK_IMAGE_USAGE_TRANSFER_SRC_BIT`. Поэтому +swapchain policy всегда запрашивает обязательный `COLOR_ATTACHMENT`, а +`TRANSFER_SRC` добавляет лишь при фактической поддержке surface; выбранные raw +usage bits попадают в deterministic plan и native smoke report. Это не создаёт +readback buffer, не копирует pixels и не закрывает pixel-capture acceptance. + +На Windows GOG native smoke 2026-07-18 (`MTCHECK.MSH` из `system.rlb` и +`DEFAULT.0` из `Textures.lib`) AMD Radeon Pro WX 3200 Series подтвердил +`image_usage=17` (`COLOR_ATTACHMENT | TRANSFER_SRC`): 300 frames, 3 resize +events, 2 swapchain recreations, Vulkan validation warnings/errors `0/0`. +Следующий шаг должен выполнить явный image-to-buffer copy и сравнить полученные +bytes с зафиксированным reference capture. + `Land.msh` использует отдельный geometry-only bridge: validated `TerrainFace28` сохраняет source triangle order, а его positions и packed UV0 попадают в тот же static vertex/index upload path. Для текущего диагностического viewer XZ bounds