diff --git a/framegen/CMakeLists.txt b/framegen/CMakeLists.txt index c2e9ac9b..785017cb 100644 --- a/framegen/CMakeLists.txt +++ b/framegen/CMakeLists.txt @@ -39,6 +39,10 @@ target_include_directories(lsfg-vk-framegen target_link_libraries(lsfg-vk-framegen PUBLIC volk) +if(ANDROID) + target_link_libraries(lsfg-vk-framegen PUBLIC log) +endif() + # diagnostics if(CMAKE_BUILD_TYPE STREQUAL "Debug") set_target_properties(lsfg-vk-framegen PROPERTIES diff --git a/framegen/include/common/utils.hpp b/framegen/include/common/utils.hpp index 82e70390..1d336b91 100644 --- a/framegen/include/common/utils.hpp +++ b/framegen/include/common/utils.hpp @@ -19,6 +19,22 @@ namespace LSFG::Utils { + /// + /// Compat shim for vkCmdPipelineBarrier2. + /// + /// On Vulkan 1.3 devices and on 1.1/1.2 with VK_KHR_synchronization2 the + /// real vkCmdPipelineBarrier2 (or its KHR alias) is invoked. On older + /// drivers that lack sync2 entirely (e.g. Mali-G57 MC2 with API 1.1.177), + /// we translate the dependency info into the legacy sync1 barrier flags + /// and call vkCmdPipelineBarrier instead. The framegen pipeline only uses + /// barrier flags whose low 32 bits are identical between sync1 and sync2 + /// (COMPUTE_SHADER, TRANSFER, TOP_OF_PIPE, BOTTOM_OF_PIPE, SHADER_READ, + /// SHADER_WRITE, TRANSFER_READ/WRITE), so a direct truncation is safe. + /// + /// Picks the implementation lazily based on which entry points volk + /// resolved against the framegen device. + void cmdPipelineBarrier2(VkCommandBuffer cb, const VkDependencyInfo* dep); + /// /// Insert memory barriers for images in a command buffer. /// diff --git a/framegen/src/common/utils.cpp b/framegen/src/common/utils.cpp index 20346c55..6436ae40 100644 --- a/framegen/src/common/utils.cpp +++ b/framegen/src/common/utils.cpp @@ -21,6 +21,144 @@ using namespace LSFG; using namespace LSFG::Utils; +namespace { + +// Convert a sync2 stage mask to a sync1 stage mask. +// +// Every sync2 stage flag we use in the framegen barrier path lives in the low +// 32 bits and matches its sync1 counterpart bit-for-bit (COMPUTE_SHADER_BIT, +// TRANSFER_BIT, TOP_OF_PIPE, BOTTOM_OF_PIPE, ALL_COMMANDS, etc.). The high +// 32 bits encode bits new in sync2 (COPY_BIT, RESOLVE_BIT, BLIT_BIT, etc.) +// that don't exist in sync1; we coalesce those onto TRANSFER_BIT (their +// sync1 equivalent) since the framegen pipeline doesn't care about the +// finer-grained distinction. Anything truly unmappable is folded into +// ALL_COMMANDS_BIT — over-conservative but always correct. +VkPipelineStageFlags sync2_to_sync1_stages(VkPipelineStageFlags2 mask) { + VkPipelineStageFlags out = static_cast( + mask & 0xFFFFFFFFULL); + // Bits introduced by sync2 (high 32-bit half of VkPipelineStageFlags2). + // We map them to the closest sync1 equivalent. NONE_BIT is silently 0. + constexpr VkPipelineStageFlags2 SYNC2_NEW_BITS = + VK_PIPELINE_STAGE_2_COPY_BIT | + VK_PIPELINE_STAGE_2_RESOLVE_BIT | + VK_PIPELINE_STAGE_2_BLIT_BIT | + VK_PIPELINE_STAGE_2_CLEAR_BIT | + VK_PIPELINE_STAGE_2_INDEX_INPUT_BIT | + VK_PIPELINE_STAGE_2_VERTEX_ATTRIBUTE_INPUT_BIT | + VK_PIPELINE_STAGE_2_PRE_RASTERIZATION_SHADERS_BIT; + if (mask & SYNC2_NEW_BITS) { + out |= VK_PIPELINE_STAGE_TRANSFER_BIT; + } + if (out == 0 && mask != 0) { + // Unknown high-bit-only mask. Be safe. + out = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; + } + return out; +} + +// Convert a sync2 access mask to a sync1 access mask. +// The same low-32-bit-aliased rule applies; new sync2 access bits get folded +// onto TRANSFER_READ/WRITE for the same reason as the stage mask above. +VkAccessFlags sync2_to_sync1_access(VkAccessFlags2 mask) { + VkAccessFlags out = static_cast(mask & 0xFFFFFFFFULL); + constexpr VkAccessFlags2 SYNC2_NEW_BITS = + VK_ACCESS_2_SHADER_SAMPLED_READ_BIT | + VK_ACCESS_2_SHADER_STORAGE_READ_BIT; + if (mask & SYNC2_NEW_BITS) { + out |= VK_ACCESS_SHADER_READ_BIT; + } + if (mask & VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT) { + out |= VK_ACCESS_SHADER_WRITE_BIT; + } + return out; +} + +} // namespace + +void Utils::cmdPipelineBarrier2(VkCommandBuffer cb, const VkDependencyInfo* dep) { + if (dep == nullptr) return; + + // Fast path: the device exposed sync2 (either core 1.3 or KHR extension, + // and Device::Device aliased the global if it was the KHR variant). + if (vkCmdPipelineBarrier2 != nullptr) { + vkCmdPipelineBarrier2(cb, dep); + return; + } + + // Slow path: translate to vkCmdPipelineBarrier (Vulkan 1.0 core). + // Aggregate the per-barrier stage masks into the single src/dst pair the + // sync1 API takes. Over-aggregating is safe (introduces only extra + // synchronization), under-aggregating would race. + VkPipelineStageFlags srcStage = 0; + VkPipelineStageFlags dstStage = 0; + + std::vector memBarriers; + memBarriers.reserve(dep->memoryBarrierCount); + for (uint32_t i = 0; i < dep->memoryBarrierCount; ++i) { + const auto& m2 = dep->pMemoryBarriers[i]; + srcStage |= sync2_to_sync1_stages(m2.srcStageMask); + dstStage |= sync2_to_sync1_stages(m2.dstStageMask); + memBarriers.push_back(VkMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = sync2_to_sync1_access(m2.srcAccessMask), + .dstAccessMask = sync2_to_sync1_access(m2.dstAccessMask), + }); + } + + std::vector bufBarriers; + bufBarriers.reserve(dep->bufferMemoryBarrierCount); + for (uint32_t i = 0; i < dep->bufferMemoryBarrierCount; ++i) { + const auto& b2 = dep->pBufferMemoryBarriers[i]; + srcStage |= sync2_to_sync1_stages(b2.srcStageMask); + dstStage |= sync2_to_sync1_stages(b2.dstStageMask); + bufBarriers.push_back(VkBufferMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = sync2_to_sync1_access(b2.srcAccessMask), + .dstAccessMask = sync2_to_sync1_access(b2.dstAccessMask), + .srcQueueFamilyIndex = b2.srcQueueFamilyIndex, + .dstQueueFamilyIndex = b2.dstQueueFamilyIndex, + .buffer = b2.buffer, + .offset = b2.offset, + .size = b2.size, + }); + } + + std::vector imgBarriers; + imgBarriers.reserve(dep->imageMemoryBarrierCount); + for (uint32_t i = 0; i < dep->imageMemoryBarrierCount; ++i) { + const auto& i2 = dep->pImageMemoryBarriers[i]; + srcStage |= sync2_to_sync1_stages(i2.srcStageMask); + dstStage |= sync2_to_sync1_stages(i2.dstStageMask); + imgBarriers.push_back(VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = sync2_to_sync1_access(i2.srcAccessMask), + .dstAccessMask = sync2_to_sync1_access(i2.dstAccessMask), + .oldLayout = i2.oldLayout, + .newLayout = i2.newLayout, + .srcQueueFamilyIndex = i2.srcQueueFamilyIndex, + .dstQueueFamilyIndex = i2.dstQueueFamilyIndex, + .image = i2.image, + .subresourceRange = i2.subresourceRange, + }); + } + + // Empty stage masks would be a spec violation (must be at least + // TOP_OF_PIPE / BOTTOM_OF_PIPE). Guarantee a valid call by defaulting to + // ALL_COMMANDS — over-conservative but always correct. + if (srcStage == 0) srcStage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; + if (dstStage == 0) dstStage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; + + vkCmdPipelineBarrier(cb, + srcStage, dstStage, + 0, + static_cast(memBarriers.size()), memBarriers.data(), + static_cast(bufBarriers.size()), bufBarriers.data(), + static_cast(imgBarriers.size()), imgBarriers.data()); +} + BarrierBuilder& BarrierBuilder::addR2W(Core::Image& image) { this->barriers.emplace_back(VkImageMemoryBarrier2 { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, @@ -69,7 +207,7 @@ void BarrierBuilder::build() const { .imageMemoryBarrierCount = static_cast(this->barriers.size()), .pImageMemoryBarriers = this->barriers.data() }; - vkCmdPipelineBarrier2(this->commandBuffer->handle(), &dependencyInfo); + Utils::cmdPipelineBarrier2(this->commandBuffer->handle(), &dependencyInfo); } void Utils::uploadImage(const Core::Device& device, const Core::CommandPool& commandPool, @@ -169,7 +307,7 @@ void Utils::clearImage(const Core::Device& device, Core::Image& image, bool whit .pImageMemoryBarriers = &barrier }; image.setLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - vkCmdPipelineBarrier2(cmdBuf.handle(), &dependencyInfo); + Utils::cmdPipelineBarrier2(cmdBuf.handle(), &dependencyInfo); const float clearValue = white ? 1.0F : 0.0F; const VkClearColorValue clearColor = {{ clearValue, clearValue, clearValue, clearValue }}; diff --git a/framegen/src/core/device.cpp b/framegen/src/core/device.cpp index cc14ea2f..bf3d6b08 100644 --- a/framegen/src/core/device.cpp +++ b/framegen/src/core/device.cpp @@ -13,6 +13,29 @@ #include #include +#ifdef __ANDROID__ +#include +#define LSFG_FRAMEGEN_LOGI(...) \ + __android_log_print(ANDROID_LOG_INFO, "lsfg-vk-framegen", __VA_ARGS__) +#define LSFG_FRAMEGEN_LOGW(...) \ + __android_log_print(ANDROID_LOG_WARN, "lsfg-vk-framegen", __VA_ARGS__) +#else +#define LSFG_FRAMEGEN_LOGI(...) do {} while (0) +#define LSFG_FRAMEGEN_LOGW(...) do {} while (0) +#endif + +// Build-fingerprint stamp. The string changes whenever this file is edited +// (via __DATE__/__TIME__), which forces NDK/CMake to detect a real source-level +// change rather than reusing the cached `lsfg-vk-framegen.a`. The variable is +// referenced from Device::Device so the linker can't dead-strip it; the value +// also lands in the runtime log so users/devs can confirm a fresh build is on +// the device by grepping for "framegen build stamp". +namespace LSFG::Core { +extern const char* kFramegenBuildStamp; +const char* kFramegenBuildStamp = + "framegen build stamp: " __DATE__ " " __TIME__; +} + using namespace LSFG::Core; const std::vector requiredExtensions = { @@ -47,6 +70,14 @@ const Image& Device::getFallbackDescriptorImage() const { } Device::Device(const Instance& instance, uint64_t deviceUUID) { + // First line of any framegen device init: prints the build stamp at INFO. + // Functionality-irrelevant by design — its only purpose is to give field + // testers a single grep-able marker that proves the loaded .so contains + // the storage-image / DEVICE_LOST fix series. If the user hits DEVICE_LOST + // and this line is absent from logcat, their APK was linked against a + // stale cached `lsfg-vk-framegen.a` and a clean rebuild is required. + LSFG_FRAMEGEN_LOGI("Entering Device::Device — %s", kFramegenBuildStamp); + // get all physical devices uint32_t deviceCount{}; auto res = vkEnumeratePhysicalDevices(instance.handle(), &deviceCount, nullptr); @@ -60,6 +91,7 @@ Device::Device(const Instance& instance, uint64_t deviceUUID) { // get device by uuid std::optional physicalDevice; + uint32_t apiVersion = VK_API_VERSION_1_0; for (const auto& device : devices) { VkPhysicalDeviceProperties properties; vkGetPhysicalDeviceProperties(device, &properties); @@ -68,6 +100,7 @@ Device::Device(const Instance& instance, uint64_t deviceUUID) { static_cast(properties.vendorID) << 32 | properties.deviceID; if (deviceUUID == uuid || deviceUUID == 0x1463ABAC) { physicalDevice = device; + apiVersion = properties.apiVersion; break; } } @@ -75,6 +108,9 @@ Device::Device(const Instance& instance, uint64_t deviceUUID) { throw LSFG::vulkan_error(VK_ERROR_INITIALIZATION_FAILED, "Could not find physical device with UUID"); + const bool api12 = apiVersion >= VK_API_VERSION_1_2; + const bool api13 = apiVersion >= VK_API_VERSION_1_3; + // find queue family indices uint32_t familyCount{}; vkGetPhysicalDeviceQueueFamilyProperties(*physicalDevice, &familyCount, nullptr); @@ -116,6 +152,54 @@ Device::Device(const Instance& instance, uint64_t deviceUUID) { enabledExtensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); } + // The compute chain (BarrierBuilder, external acquire/release) calls + // vkCmdPipelineBarrier2. That entry point is core in Vulkan 1.3 and comes + // from VK_KHR_synchronization2 on 1.1/1.2. + // + // On devices that have NEITHER 1.3 nor the sync2 extension (e.g. Mali-G57 + // MC2 with API 1.1.177), we route every barrier through Utils::cmdPipelineBarrier2, + // which translates VkDependencyInfo into the legacy sync1 + // vkCmdPipelineBarrier signature at runtime. Framegen's barriers only use + // stage/access bits whose low 32 bits match between sync1 and sync2, so + // the translation is functionally equivalent. We log which path is taken + // so it's obvious in field reports. + const bool hasSync2Ext = hasExtension(availableExtensions, + VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME); + if (!api13 && hasSync2Ext) { + enabledExtensions.push_back(VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME); + } + const bool sync2Available = api13 || hasSync2Ext; + // Same story for timeline semaphores: core in 1.2, extension on 1.1. + // Framegen's CommandBuffer::submit takes optional timeline values and the + // structure type is queried from runtime. If the device doesn't support + // timeline semaphores at all, framegen still can't run. + const bool hasTimelineSemExt = hasExtension(availableExtensions, + VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME); + if (!api12) { + if (!hasTimelineSemExt) { + throw LSFG::vulkan_error(VK_ERROR_EXTENSION_NOT_PRESENT, + "VK_KHR_timeline_semaphore not available on this device " + "(apiVersion < 1.2 and the extension is absent)"); + } + enabledExtensions.push_back(VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME); + } + // vulkanMemoryModel: core-promoted in 1.2 but optional. On 1.1 it lives in + // VK_KHR_vulkan_memory_model and is required by the framegen shaders that + // use coherent memory accesses across workgroups. + const bool hasVulkanMemoryModelExt = hasExtension(availableExtensions, + VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME); + if (!api12 && hasVulkanMemoryModelExt) { + enabledExtensions.push_back(VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME); + } + // shaderFloat16 is gated by VK_KHR_shader_float16_int8 on 1.1; promoted to + // core in 1.2. Only request it if the extension/feature is actually present + // (we'll probe the feature itself below). + const bool hasFloat16ExtName = hasExtension(availableExtensions, + VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME); + if (!api12 && hasFloat16ExtName) { + enabledExtensions.push_back(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME); + } + // Probe FP16 support on this physical device. The LSFG-Android port can // load precompiled SPIR-V FP16 shader variants from Lossless.dll (resource // IDs 304..351) which carry `OpCapability Float16`. Vulkan rejects those at @@ -123,37 +207,261 @@ Device::Device(const Instance& instance, uint64_t deviceUUID) { // shaderFloat16 feature explicitly enabled. We probe and unconditionally // enable it when supported — there's no downside on FP32-only sessions and // it lets the FP16 path "just work" when the user toggles it on. + // + // Also probe the core 1.0 storage-image features. The LSFG compute shader + // chain reads from and writes to R16G16B16A16_SFLOAT storage images + // (gamma/delta/generate); on Mali (Bifrost/Valhall) this is rejected at + // dispatch time and surfaces as VK_ERROR_DEVICE_LOST on the first present + // unless `shaderStorageImageExtendedFormats` is explicitly enabled at + // device-create time. The same applies to image read/write without an + // explicit `format` qualifier in SPIR-V — without + // `shaderStorageImageReadWithoutFormat` / `WriteWithoutFormat` the driver + // is allowed to UB the dispatch. Enable each only when the physical device + // advertises it (the validation layers reject create_device with features + // the device doesn't support, and several PowerVR/Adreno revisions only + // expose a subset). VkPhysicalDeviceShaderFloat16Int8Features fp16Probe{ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT16_INT8_FEATURES, }; + // Probe vulkanMemoryModel / timelineSemaphore through the core 1.2 feature + // struct on api>=1.2 devices. vulkanMemoryModel is *optional* in core 1.2/1.3 + // and notoriously absent on Mali Bifrost/Valhall (G57/G68/G77). Some Mali + // drivers silently accept a request for the unsupported feature at + // vkCreateDevice time, then DEVICE_LOST on the first compute dispatch — which + // is exactly what we see on the Mali-G57 (API 1.3.225) field log: device + // creation succeeds, the first presentContext submits a command buffer, and + // the driver returns VK_ERROR_DEVICE_LOST (-4) because the shader's + // OpCapability VulkanMemoryModel was never legally enabled. + VkPhysicalDeviceVulkan12Features vk12Probe{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, + }; + VkPhysicalDeviceVulkan13Features vk13Probe{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, + }; + void* probeNext = &fp16Probe; + if (api12) { + vk12Probe.pNext = probeNext; + probeNext = &vk12Probe; + } + if (api13) { + vk13Probe.pNext = probeNext; + probeNext = &vk13Probe; + } VkPhysicalDeviceFeatures2 featsProbe{ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, - .pNext = &fp16Probe, + .pNext = probeNext, }; vkGetPhysicalDeviceFeatures2(*physicalDevice, &featsProbe); const bool hasFloat16 = fp16Probe.shaderFloat16 == VK_TRUE; + const VkPhysicalDeviceFeatures& probedCore = featsProbe.features; + // Promoted-core probe results. On api<1.2 these stay false and we fall + // through to the extension-based path below (which has its own probes). + const bool hasVulkanMemoryModelCore = api12 && vk12Probe.vulkanMemoryModel == VK_TRUE; + const bool hasTimelineSemCore = api12 && vk12Probe.timelineSemaphore == VK_TRUE; + const bool hasSync2Core = api13 && vk13Probe.synchronization2 == VK_TRUE; + + // Build the core feature struct we'll request. We must only request what + // the device supports — anything else fails vkCreateDevice. All four + // fields below are required by the LSFG shader chain on Mali; absence + // would manifest as DEVICE_LOST on first dispatch. + VkPhysicalDeviceFeatures enabledCoreFeatures{}; + enabledCoreFeatures.shaderStorageImageExtendedFormats = + probedCore.shaderStorageImageExtendedFormats; + enabledCoreFeatures.shaderStorageImageReadWithoutFormat = + probedCore.shaderStorageImageReadWithoutFormat; + enabledCoreFeatures.shaderStorageImageWriteWithoutFormat = + probedCore.shaderStorageImageWriteWithoutFormat; + // shaderInt16 commonly accompanies FP16 paths; harmless on FP32-only sessions + // and required by some Lossless.dll FP16 shader variants. + enabledCoreFeatures.shaderInt16 = probedCore.shaderInt16; + + // Diagnostic: log which storage-image features were probed vs. enabled. + // Tagged "lsfg-vk-framegen" so it's easy to correlate with session logs. + // On Mali-G57 this is the load-bearing line: if any of the *Format* fields + // shows probed=0, the device cannot legally execute LSFG's compute chain + // and presentContext will hit DEVICE_LOST regardless of this fix. + // + // The build stamp below is referenced (and printed) here so the linker + // can't dead-strip it and so users running an older cached .so can verify + // by log-grep whether the build they have actually contains this fix. + LSFG_FRAMEGEN_LOGI("%s", kFramegenBuildStamp); + LSFG_FRAMEGEN_LOGI( + "Device features probe: storageImageExtendedFormats=%d, " + "storageImageReadWithoutFormat=%d, storageImageWriteWithoutFormat=%d, " + "shaderInt16=%d, shaderFloat16=%d, robustness2=%d, " + "vulkanMemoryModel(core)=%d, timelineSemaphore(core)=%d, sync2(core)=%d", + (int)probedCore.shaderStorageImageExtendedFormats, + (int)probedCore.shaderStorageImageReadWithoutFormat, + (int)probedCore.shaderStorageImageWriteWithoutFormat, + (int)probedCore.shaderInt16, + (int)hasFloat16, + (int)hasRobustness2, + (int)hasVulkanMemoryModelCore, + (int)hasTimelineSemCore, + (int)hasSync2Core); + if (probedCore.shaderStorageImageExtendedFormats != VK_TRUE) { + // Hard warning: this is the feature LSFG's R16G16B16A16_SFLOAT storage + // image accesses depend on. Without it, dispatch is undefined behavior + // on Mali/Adreno and surfaces as DEVICE_LOST on first present. There's + // no shader-side workaround we can apply at runtime; logging it loudly + // gives upstream / users a clear pointer to the actual root cause. + LSFG_FRAMEGEN_LOGW( + "shaderStorageImageExtendedFormats=FALSE — LSFG compute chain " + "expects R16G16B16A16_SFLOAT storage; first dispatch will likely " + "DEVICE_LOST on this device"); + } + + // vulkanMemoryModel is only required by the DXBC→SPIR-V translator path + // (thirdparty/dxbc emits OpCapability VulkanMemoryModel unconditionally). + // The Lossless precompiled FP16 SPIR-V blobs (resource IDs 304..351) use + // OpMemoryModel Logical GLSL450 with NO VulkanMemoryModel capability — + // verified across all 49 blobs by disassembling them (see _analysis/*.dis). + // + // So a Mali Bifrost/Valhall device (G57/G68/G77 — no vulkanMemoryModel + // support) CAN run framegen IF the user enables the FP16 toggle: the + // shader loader then bypasses dxvk and feeds the device the precompiled + // GLSL450 SPIR-V directly. We therefore do NOT fail-fast here; the device + // is created without vulkanMemoryModel enabled (the gating above turned + // the request into VK_FALSE), shader load via the FP16 path will succeed, + // and dispatch will work. If the user has FP16 disabled on such a device, + // the DXBC path's first dispatch will DEVICE_LOST — that's caught later + // by lsfg_render_loop's auto-disable, and the diagnostic line above + // ("vulkanMemoryModel(core)=0") points the field reporter at the cause. // create logical device const float queuePriority{1.0F}; // highest priority + + // Feature chain. The shape depends on the device's apiVersion: + // 1.3+: chain VkPhysicalDeviceVulkan1{2,3}Features (these structs are only + // valid on devices that advertise the corresponding core version). + // 1.1/1.2: chain extension-specific feature structs + // (VkPhysicalDeviceSynchronization2FeaturesKHR, etc.). The promoted + // core structs would have unknown sType on a 1.1 driver — Mali + // drivers in particular have been observed to silently corrupt + // state when handed an unrecognized core feature struct, leading + // to SIGSEGV on the first dispatch even though vkCreateDevice + // appeared to succeed. + // + // Every feature is gated on actually being supported (probedCore / + // hasFloat16 / hasRobustness2 / hasVulkanMemoryModelExt) so that we don't + // request something the driver rejects. VkPhysicalDeviceRobustness2FeaturesEXT robustness2{ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT, .nullDescriptor = VK_TRUE, }; - VkPhysicalDeviceVulkan13Features features13{ - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, - .pNext = hasRobustness2 ? &robustness2 : nullptr, - .synchronization2 = VK_TRUE - }; - // shaderFloat16 is exposed in core Vulkan 1.2 — same struct we already - // chain. Setting it conditionally avoids regressing devices that don't - // advertise the feature (the validation layers reject create_device when - // requested features are unsupported). - VkPhysicalDeviceVulkan12Features features12{ - .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, - .pNext = &features13, - .shaderFloat16 = hasFloat16 ? VK_TRUE : VK_FALSE, - .timelineSemaphore = VK_TRUE, - .vulkanMemoryModel = VK_TRUE + void* featureChainHead = nullptr; + + // Branch A: full Vulkan 1.3 device — use the consolidated promoted structs. + VkPhysicalDeviceVulkan13Features features13{}; + VkPhysicalDeviceVulkan12Features features12{}; + + // Branch B: pre-1.3 device — use extension-specific feature structs that + // are valid back to Vulkan 1.1 and gated by VK_KHR_synchronization2 / + // VK_KHR_timeline_semaphore / VK_KHR_vulkan_memory_model / VK_KHR_shader_float16_int8. + VkPhysicalDeviceSynchronization2FeaturesKHR sync2Feat{}; + VkPhysicalDeviceTimelineSemaphoreFeaturesKHR timelineSemFeat{}; + VkPhysicalDeviceVulkanMemoryModelFeaturesKHR memModelFeat{}; + VkPhysicalDeviceShaderFloat16Int8Features fp16Feat{}; + + if (api13) { + // Every field below MUST be probed before being requested. Mali drivers + // (G57/G68/G77 etc.) routinely return success from vkCreateDevice when + // an unsupported optional feature is requested, then fail with + // VK_ERROR_DEVICE_LOST on the first compute dispatch — there is no + // earlier signal that the request was bogus. The Mali-G57 field log + // for this fix shows exactly that pattern: vkCreateDevice OK, init OK, + // first presentContext → DEVICE_LOST. The culprit was the unconditional + // vulkanMemoryModel = VK_TRUE; the device does not advertise it but + // the driver took the request anyway. + features13 = VkPhysicalDeviceVulkan13Features{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, + .pNext = hasRobustness2 ? &robustness2 : nullptr, + .synchronization2 = hasSync2Core ? VK_TRUE : VK_FALSE, + }; + features12 = VkPhysicalDeviceVulkan12Features{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, + .pNext = &features13, + .shaderFloat16 = hasFloat16 ? VK_TRUE : VK_FALSE, + .timelineSemaphore = hasTimelineSemCore ? VK_TRUE : VK_FALSE, + .vulkanMemoryModel = hasVulkanMemoryModelCore ? VK_TRUE : VK_FALSE, + }; + featureChainHead = &features12; + } else { + // Walk the chain bottom-up so we can stitch pNext links cleanly. + // The KHR feature struct sTypes are aliased to the corresponding + // promoted core sTypes on 1.2/1.3 drivers, so it's safe to chain + // them whether the feature comes from an extension or core. + void* next = hasRobustness2 ? static_cast(&robustness2) : nullptr; + + // sync2 feature is only chained when the extension is enabled. On + // legacy devices without sync2 we route every barrier through the + // Utils::cmdPipelineBarrier2 compat shim and don't need this struct. + if (hasSync2Ext) { + sync2Feat = VkPhysicalDeviceSynchronization2FeaturesKHR{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES_KHR, + .pNext = next, + .synchronization2 = VK_TRUE, + }; + next = &sync2Feat; + } + + // Timeline semaphores: core in 1.2. On 1.1 it's an extension we've + // already added to enabledExtensions. Either way it's safe to chain + // this struct — the sType is the same as the core 1.2 alias. + timelineSemFeat = VkPhysicalDeviceTimelineSemaphoreFeaturesKHR{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES_KHR, + .pNext = next, + .timelineSemaphore = VK_TRUE, + }; + next = &timelineSemFeat; + + // vulkanMemoryModel: core in 1.2 (optional even there). On 1.1 it + // requires VK_KHR_vulkan_memory_model. Required only by the DXBC path + // (the FP16 SPIR-V blobs from Lossless.dll use Logical GLSL450 with no + // VMM capability — verified in _analysis/*.dis). So on devices that + // don't support it we still create the device (just without the + // feature enabled) and let the user pick the FP16 path; if they pick + // DXBC the dispatch will DEVICE_LOST and the auto-disable in the + // render loop kicks in. + const bool memModelOk = api12 + ? hasVulkanMemoryModelCore + : hasVulkanMemoryModelExt; + if (memModelOk) { + memModelFeat = VkPhysicalDeviceVulkanMemoryModelFeaturesKHR{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES_KHR, + .pNext = next, + .vulkanMemoryModel = VK_TRUE, + }; + next = &memModelFeat; + } + if (hasFloat16) { + // The structure type alias is the same as the core 1.2 struct. + fp16Feat = VkPhysicalDeviceShaderFloat16Int8Features{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT16_INT8_FEATURES, + .pNext = next, + .shaderFloat16 = VK_TRUE, + }; + next = &fp16Feat; + } + featureChainHead = next; + } + LSFG_FRAMEGEN_LOGI( + "Device path: apiVersion=%u.%u sync2=%s timeline_via_ext=%d " + "memModel_via_ext=%d fp16_via_ext=%d robustness2=%d", + VK_VERSION_MAJOR(apiVersion), VK_VERSION_MINOR(apiVersion), + api13 ? "core1.3" : (hasSync2Ext ? "ext" : "compat-shim"), + (int)(!api12 && hasTimelineSemExt), + (int)(!api12 && hasVulkanMemoryModelExt), + (int)(!api12 && hasFloat16ExtName), + (int)hasRobustness2); + + // Use VkPhysicalDeviceFeatures2 in pNext (mutually exclusive with + // pEnabledFeatures per spec) so we can chain the core features alongside + // the 1.2/1.3/robustness2 structs above. + VkPhysicalDeviceFeatures2 enabledFeatures2{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, + .pNext = featureChainHead, + .features = enabledCoreFeatures, }; const VkDeviceQueueCreateInfo computeQueueDesc{ .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, @@ -163,11 +471,13 @@ Device::Device(const Instance& instance, uint64_t deviceUUID) { }; const VkDeviceCreateInfo deviceCreateInfo{ .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, - .pNext = &features12, + .pNext = &enabledFeatures2, .queueCreateInfoCount = 1, .pQueueCreateInfos = &computeQueueDesc, .enabledExtensionCount = static_cast(enabledExtensions.size()), .ppEnabledExtensionNames = enabledExtensions.data() + // pEnabledFeatures intentionally NULL: enabledFeatures2 carries them + // via pNext (the spec forbids both being non-null). }; VkDevice deviceHandle{}; res = vkCreateDevice(*physicalDevice, &deviceCreateInfo, nullptr, &deviceHandle); @@ -176,6 +486,36 @@ Device::Device(const Instance& instance, uint64_t deviceUUID) { volkLoadDevice(deviceHandle); + // Wire up the global vkCmdPipelineBarrier2 symbol so the BarrierBuilder + // / external-barrier paths can call through it. Three cases: + // + // 1. Vulkan 1.3 device: volk already loaded the core entry point. + // Nothing to do. + // + // 2. Pre-1.3 device with VK_KHR_synchronization2 enabled: volk loaded + // vkCmdPipelineBarrier2KHR but NOT vkCmdPipelineBarrier2. They map + // to the same dispatch slot in the ICD, so we alias the global so + // callers can use the unsuffixed symbol uniformly. + // + // 3. Pre-1.3 device WITHOUT sync2 in any form (e.g. Mali-G57 MC2 with + // API 1.1.177): leave the global as NULL. Every framegen call site + // now goes through Utils::cmdPipelineBarrier2, which detects this + // case and translates VkDependencyInfo into the legacy sync1 + // vkCmdPipelineBarrier signature on the fly. Framegen still runs + // end-to-end on these devices, just without the sync2 fast-path. + if (!api13 && vkCmdPipelineBarrier2 == nullptr && + vkCmdPipelineBarrier2KHR != nullptr) { + vkCmdPipelineBarrier2 = reinterpret_cast( + vkCmdPipelineBarrier2KHR); + LSFG_FRAMEGEN_LOGI( + "Aliased vkCmdPipelineBarrier2 -> vkCmdPipelineBarrier2KHR " + "(pre-1.3 device, sync2 via extension)"); + } else if (vkCmdPipelineBarrier2 == nullptr) { + LSFG_FRAMEGEN_LOGI( + "vkCmdPipelineBarrier2 unavailable (no sync2 core or extension); " + "BarrierBuilder will use the sync1 compat path"); + } + // get compute queue VkQueue queueHandle{}; vkGetDeviceQueue(deviceHandle, *computeFamilyIdx, 0, &queueHandle); diff --git a/framegen/v3.1_src/context.cpp b/framegen/v3.1_src/context.cpp index 6b7d51dc..4283d599 100644 --- a/framegen/v3.1_src/context.cpp +++ b/framegen/v3.1_src/context.cpp @@ -74,7 +74,7 @@ void emit_external_barriers(const Core::CommandBuffer& buf, .imageMemoryBarrierCount = static_cast(barriers.size()), .pImageMemoryBarriers = barriers.data(), }; - vkCmdPipelineBarrier2(buf.handle(), &dependencyInfo); + LSFG::Utils::cmdPipelineBarrier2(buf.handle(), &dependencyInfo); } } // namespace diff --git a/framegen/v3.1p_src/context.cpp b/framegen/v3.1p_src/context.cpp index 4b01791d..48fc82f0 100644 --- a/framegen/v3.1p_src/context.cpp +++ b/framegen/v3.1p_src/context.cpp @@ -75,7 +75,7 @@ void emit_external_barriers(const Core::CommandBuffer& buf, .imageMemoryBarrierCount = static_cast(barriers.size()), .pImageMemoryBarriers = barriers.data(), }; - vkCmdPipelineBarrier2(buf.handle(), &dependencyInfo); + LSFG::Utils::cmdPipelineBarrier2(buf.handle(), &dependencyInfo); } } // namespace