use super::*; use crate::ffi::{SPIRV_MAGIC, SPIRV_VERSION_1_0}; use crate::policy::{KHR_PORTABILITY_SUBSET_EXTENSION, KHR_SWAPCHAIN_EXTENSION}; use crate::shader::{validate_spirv_container, validate_triangle_shaders, VulkanShaderError}; use crate::*; use fparkan_platform::{DepthStencilSupport, RenderRequest}; #[test] fn device_scoring_is_deterministic_and_prefers_discrete_unified_queue() { let devices = vec![ device("SwiftShader", VulkanDeviceType::Cpu, 0, true, false), device("Discrete", VulkanDeviceType::DiscreteGpu, 1, true, false), device( "Integrated", VulkanDeviceType::IntegratedGpu, 2, true, false, ), ]; let report = select_physical_device(&devices).expect("selected device"); assert_eq!(report.device_name, "Discrete"); assert_eq!(report.graphics_queue_family, 1); assert_eq!(report.present_queue_family, 1); assert!(!report.portability_subset); assert_eq!(report.enabled_extensions, vec![KHR_SWAPCHAIN_EXTENSION]); } #[test] fn device_selection_skips_rejected_candidates_before_accepting_valid_gpu() { let mut rejected = device("Rejected", VulkanDeviceType::DiscreteGpu, 0, true, false); rejected.queue_families[0].present = false; let accepted = device("Accepted", VulkanDeviceType::IntegratedGpu, 2, true, false); let report = select_physical_device(&[rejected, accepted]).expect("selected fallback device"); assert_eq!(report.device_name, "Accepted"); assert_eq!(report.graphics_queue_family, 2); assert_eq!(report.present_queue_family, 2); assert_eq!( report.rejected_devices, vec![VulkanRejectedDeviceReport { device_name: "Rejected".to_string(), reason_code: "no_present_queue", reason: "Vulkan device Rejected has no present queue".to_string(), }] ); } #[test] fn queue_family_selection_prefers_lowest_index_unified_family() { let mut candidate = device( "Unified later in list", VulkanDeviceType::DiscreteGpu, 7, true, false, ); candidate.queue_families = vec![ VulkanQueueFamily { index: 9, graphics: true, present: true, }, VulkanQueueFamily { index: 3, graphics: true, present: true, }, VulkanQueueFamily { index: 1, graphics: true, present: false, }, ]; let report = select_physical_device(&[candidate]).expect("selected unified queue"); assert_eq!(report.graphics_queue_family, 3); assert_eq!(report.present_queue_family, 3); } #[test] fn portability_subset_is_reported_and_enabled_when_exposed() { let report = select_physical_device(&[device( "MoltenVK", VulkanDeviceType::IntegratedGpu, 0, true, true, )]) .expect("selected device"); assert!(report.portability_subset); assert_eq!( report.enabled_extensions, vec![ KHR_SWAPCHAIN_EXTENSION.to_string(), KHR_PORTABILITY_SUBSET_EXTENSION.to_string() ] ); } #[test] fn missing_loader_candidates_are_reported() { assert_eq!( select_physical_device(&[]), Err(VulkanCapabilityError::NoPhysicalDevice) ); } #[test] fn rejects_low_api_version() { let mut candidate = device("Old GPU", VulkanDeviceType::DiscreteGpu, 0, true, false); candidate.api_version = vk::API_VERSION_1_0; assert!(matches!( select_physical_device(&[candidate]), Err(VulkanCapabilityError::ApiVersionTooLow { .. }) )); } #[test] fn rejects_missing_graphics_present_swapchain_and_format() { let mut no_graphics = device("No graphics", VulkanDeviceType::DiscreteGpu, 0, true, false); no_graphics.queue_families[0].graphics = false; assert!(matches!( select_physical_device(&[no_graphics]), Err(VulkanCapabilityError::NoGraphicsQueue { .. }) )); let mut no_present = device("No present", VulkanDeviceType::DiscreteGpu, 0, true, false); no_present.queue_families[0].present = false; assert!(matches!( select_physical_device(&[no_present]), Err(VulkanCapabilityError::NoPresentQueue { .. }) )); let no_swapchain = device( "No swapchain", VulkanDeviceType::DiscreteGpu, 0, false, false, ); assert!(matches!( select_physical_device(&[no_swapchain]), Err(VulkanCapabilityError::MissingSwapchainExtension { .. }) )); let mut no_format = device("No format", VulkanDeviceType::DiscreteGpu, 0, true, false); no_format.surface_formats.clear(); assert!(matches!( select_physical_device(&[no_format]), Err(VulkanCapabilityError::MissingSurfaceFormat { .. }) )); let mut no_present_mode = device( "No present mode", VulkanDeviceType::DiscreteGpu, 0, true, false, ); no_present_mode.present_modes.clear(); assert!(matches!( select_physical_device(&[no_present_mode]), Err(VulkanCapabilityError::MissingPresentMode { .. }) )); let mut no_color_attachment = device( "No color attachment", VulkanDeviceType::DiscreteGpu, 0, true, false, ); no_color_attachment .surface_capabilities .supported_usage_flags = vk::ImageUsageFlags::TRANSFER_DST.as_raw(); assert!(matches!( select_physical_device(&[no_color_attachment]), Err(VulkanCapabilityError::MissingColorAttachmentUsage { .. }) )); } #[test] fn capability_gate_rejects_devices_without_requested_depth_stencil_support() { let mut no_depth = device("No depth", VulkanDeviceType::DiscreteGpu, 0, true, false); no_depth.supported_depth_stencil_formats = vec![vk::Format::D32_SFLOAT.as_raw()]; assert!(matches!( select_physical_device(&[no_depth]), Err(VulkanCapabilityError::MissingDepthStencilFormat { .. }) )); } #[test] fn capability_gate_respects_request_specific_depth_profiles() { let mut no_stencil = device("No stencil", VulkanDeviceType::DiscreteGpu, 0, true, false); no_stencil.supported_depth_stencil_formats = vec![vk::Format::D32_SFLOAT.as_raw()]; let relaxed_request = RenderRequest { depth: DepthStencilSupport { depth_bits: 32, stencil_bits: 0, }, ..RenderRequest::conservative() }; let report = select_physical_device_for_request(&[no_stencil], relaxed_request) .expect("selected device for depth-only request"); assert_eq!(report.device_name, "No stencil"); assert!(report.rejected_devices.is_empty()); } #[test] fn depth_attachment_selection_is_canonical_and_request_scoped() { let supported = [ vk::Format::D32_SFLOAT_S8_UINT.as_raw(), vk::Format::D24_UNORM_S8_UINT.as_raw(), vk::Format::D32_SFLOAT.as_raw(), ]; assert_eq!( select_depth_stencil_attachment_format( &supported, DepthStencilSupport { depth_bits: 24, stencil_bits: 8, }, ), Some(vk::Format::D24_UNORM_S8_UINT.as_raw()) ); assert_eq!( select_depth_stencil_attachment_format( &supported, DepthStencilSupport { depth_bits: 32, stencil_bits: 0, }, ), Some(vk::Format::D32_SFLOAT.as_raw()) ); assert_eq!( select_depth_stencil_attachment_format( &supported, DepthStencilSupport { depth_bits: 0, stencil_bits: 0, }, ), None ); } #[test] fn capability_report_preserves_informational_sampled_formats_and_limits() { let report = select_physical_device(&[device( "Telemetry GPU", VulkanDeviceType::DiscreteGpu, 0, true, false, )]) .expect("selected device"); assert_eq!( report.informational_capabilities.sampled_color_formats, vec![vk::Format::B8G8R8A8_SRGB.as_raw()] ); assert_eq!( report.informational_capabilities.sampled_depth_formats, vec![vk::Format::D32_SFLOAT.as_raw()] ); assert_eq!( report.informational_capabilities.limits, VulkanDeviceLimits { max_image_dimension_2d: 4096, max_sampler_allocation_count: 4096, max_per_stage_descriptor_samplers: 16, max_bound_descriptor_sets: 4, } ); } #[test] fn loader_error_display_is_actionable() { assert_eq!( VulkanLoaderError::Unavailable { message: "dlopen failed".to_string(), } .to_string(), "Vulkan loader is unavailable: dlopen failed" ); } #[test] fn instance_plan_is_sorted_deduplicated_and_portability_aware() { let plan = plan_vulkan_instance(&VulkanInstanceConfig { application_name: "FParkan".to_string(), required_extensions: vec![ "VK_KHR_surface".to_string(), KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string(), "VK_KHR_surface".to_string(), ], enable_portability_enumeration: true, enable_validation: true, }); assert_eq!(plan.create_flags, 1); assert!(plan.validation_requested); assert_eq!( plan.enabled_extensions, vec![ "VK_EXT_debug_utils".to_string(), KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string(), "VK_KHR_surface".to_string(), ] ); } #[test] fn instance_plan_adds_portability_extension_when_requested() { let plan = plan_vulkan_instance(&VulkanInstanceConfig { application_name: "FParkan".to_string(), required_extensions: vec!["VK_KHR_surface".to_string()], enable_portability_enumeration: true, enable_validation: false, }); assert_eq!( plan.enabled_extensions, vec![ KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string(), "VK_KHR_surface".to_string() ] ); assert_eq!(plan.create_flags, 1); } #[test] fn invalid_instance_extension_name_is_reported_before_loader_use() { assert_eq!( cstring_vec(&["bad\0extension".to_string()]), Err(VulkanInstanceError::InvalidExtensionName { extension: "bad\0extension".to_string() }) ); } #[test] fn missing_instance_extension_is_reported_before_create_instance() { assert_eq!( ensure_instance_extensions_available( &[ "VK_EXT_debug_utils".to_string(), "VK_KHR_surface".to_string(), ], &["VK_KHR_surface".to_string()], ), Err(VulkanInstanceError::MissingInstanceExtension { extension: "VK_EXT_debug_utils".to_string(), }) ); } #[test] fn surface_plan_requires_native_handles() { assert_eq!( plan_vulkan_surface(None), Err(VulkanSurfaceError::MissingNativeHandles) ); assert_eq!( VulkanSurfaceError::MissingNativeHandles.to_string(), "native window/display handles are required for Vulkan surface creation" ); } #[test] fn static_surface_extension_name_is_decoded() { let name = extension_name(ash::khr::surface::NAME.as_ptr()).expect("extension name"); assert_eq!(name, "VK_KHR_surface"); } #[test] fn swapchain_plan_prefers_srgb_mailbox_and_clamps_extent() { let plan = plan_vulkan_swapchain(&swapchain_request()).expect("swapchain plan"); assert_eq!( plan.format, VulkanSurfaceFormat { format: vk::Format::B8G8R8A8_SRGB.as_raw(), color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(), } ); 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] fn swapchain_plan_uses_fifo_and_current_extent_fallbacks() { let mut request = swapchain_request(); request.preferred_present_mode = vk::PresentModeKHR::IMMEDIATE.as_raw(); request.present_modes = vec![vk::PresentModeKHR::FIFO.as_raw()]; request.capabilities.current_extent = Some((800, 600)); let plan = plan_vulkan_swapchain(&request).expect("swapchain plan"); assert_eq!(plan.present_mode, vk::PresentModeKHR::FIFO.as_raw()); assert_eq!(plan.extent, (800, 600)); } #[test] fn swapchain_plan_accepts_undefined_surface_format_by_picking_stage0_default() { let mut request = swapchain_request(); request.formats = vec![VulkanSurfaceFormat { format: vk::Format::UNDEFINED.as_raw(), color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(), }]; let plan = plan_vulkan_swapchain(&request).expect("swapchain plan"); assert_eq!( plan.format, VulkanSurfaceFormat { format: vk::Format::B8G8R8A8_SRGB.as_raw(), color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(), } ); } #[test] fn swapchain_plan_rejects_missing_surface_data_and_empty_extent() { let mut request = swapchain_request(); request.formats.clear(); assert_eq!( plan_vulkan_swapchain(&request), Err(VulkanSwapchainError::MissingSurfaceFormat) ); let mut request = swapchain_request(); request.present_modes.clear(); assert_eq!( plan_vulkan_swapchain(&request), Err(VulkanSwapchainError::MissingPresentMode) ); let mut request = swapchain_request(); request.capabilities.current_extent = Some((0, 600)); assert_eq!( plan_vulkan_swapchain(&request), Err(VulkanSwapchainError::EmptyExtent) ); } #[test] fn checked_in_shaders_have_valid_spirv_containers() { assert_eq!(validate_triangle_shaders(), Ok(())); } #[test] fn shader_container_rejects_invalid_spirv() { assert_eq!( validate_spirv_container("triangle.vert", &[0xFFFF_FFFF, SPIRV_VERSION_1_0, 0, 1, 0]), Err(VulkanShaderError::InvalidMagic { name: "triangle.vert", found: 0xFFFF_FFFF, }) ); assert_eq!( validate_spirv_container("triangle.vert", &[SPIRV_MAGIC, 0, 0, 1, 0]), Err(VulkanShaderError::UnsupportedVersion { name: "triangle.vert", found: 0, }) ); assert_eq!( validate_spirv_container("triangle.vert", &[SPIRV_MAGIC, SPIRV_VERSION_1_0, 0, 0, 0]), Err(VulkanShaderError::InvalidBound { name: "triangle.vert", }) ); } fn device( name: &str, device_type: VulkanDeviceType, queue_index: u32, swapchain: bool, portability_subset: bool, ) -> VulkanPhysicalDeviceRecord { let mut extensions = Vec::new(); if swapchain { extensions.push(KHR_SWAPCHAIN_EXTENSION.to_string()); } if portability_subset { extensions.push(KHR_PORTABILITY_SUBSET_EXTENSION.to_string()); } VulkanPhysicalDeviceRecord { name: name.to_string(), api_version: MIN_VULKAN_API_VERSION, device_type, extensions, queue_families: vec![VulkanQueueFamily { index: queue_index, graphics: true, present: true, }], surface_formats: vec![VulkanSurfaceFormat { format: vk::Format::B8G8R8A8_SRGB.as_raw(), color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(), }], present_modes: vec![ vk::PresentModeKHR::FIFO.as_raw(), vk::PresentModeKHR::MAILBOX.as_raw(), ], surface_capabilities: default_surface_capabilities(), supported_depth_stencil_formats: vec![ vk::Format::D24_UNORM_S8_UINT.as_raw(), vk::Format::D32_SFLOAT_S8_UINT.as_raw(), vk::Format::D32_SFLOAT.as_raw(), ], sampled_image_formats: vec![ vk::Format::B8G8R8A8_SRGB.as_raw(), vk::Format::D32_SFLOAT.as_raw(), ], limits: VulkanDeviceLimits { max_image_dimension_2d: 4096, max_sampler_allocation_count: 4096, max_per_stage_descriptor_samplers: 16, max_bound_descriptor_sets: 4, }, } } fn swapchain_request() -> VulkanSwapchainRequest { VulkanSwapchainRequest { drawable_extent: (1280, 720), formats: vec![ VulkanSurfaceFormat { format: vk::Format::R8G8B8A8_UNORM.as_raw(), color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(), }, VulkanSurfaceFormat { format: vk::Format::B8G8R8A8_SRGB.as_raw(), color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(), }, ], present_modes: vec![ vk::PresentModeKHR::FIFO.as_raw(), vk::PresentModeKHR::MAILBOX.as_raw(), ], capabilities: default_surface_capabilities(), preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(), } } fn default_surface_capabilities() -> VulkanSwapchainSurfaceCapabilities { VulkanSwapchainSurfaceCapabilities { current_extent: None, min_extent: (320, 240), max_extent: (1024, 768), min_image_count: 2, max_image_count: 3, supported_usage_flags: vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(), } }