fix(peon_worker): Handle vulkan initialization errors in gpu recorder (#13035) 37997d7366

Co-authored-by: Arthur Vivian <arthur@rive.app>
diff --git a/.rive_head b/.rive_head
index 609cb37..50fe441 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-ac4657c21c27ec4b99b415d54ca45a8b135fea63
+37997d7366fa70e190725e00f399a9dacd4c8a38
diff --git a/renderer/rive_vk_bootstrap/src/vulkan_frame_synchronizer.cpp b/renderer/rive_vk_bootstrap/src/vulkan_frame_synchronizer.cpp
index c7cac00..d4a8ca2 100644
--- a/renderer/rive_vk_bootstrap/src/vulkan_frame_synchronizer.cpp
+++ b/renderer/rive_vk_bootstrap/src/vulkan_frame_synchronizer.cpp
@@ -63,7 +63,7 @@
 
         VK_CONFIRM_OR_RETURN_MSG(
             m_vkCreateFence(m_device, &fenceCreateInfo, nullptr, &sync.fence),
-            "Failed to create Vulkan fance");
+            "Failed to create Vulkan fence");
 
         VkCommandBufferAllocateInfo cbufferAllocateInfo = {
             .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
@@ -104,19 +104,34 @@
 
 VulkanFrameSynchronizer::~VulkanFrameSynchronizer()
 {
+    // The instance command function pointers are loaded first thing in the
+    // constructor, but if construction failed partway (e.g. an OOM creating a
+    // fence) this destructor still runs while some pointers are null. Guard
+    // every call so teardown of a partially-constructed object cannot segfault.
+    //
     // Note that the derived class will have already waited for the device to be
     // idle so we can safely destroy things here.
     for (auto& frame : m_inFlightFrames)
     {
-        destroySemaphore(frame.semaphore);
-        m_vkFreeCommandBuffers(m_device,
-                               m_commandPool,
-                               1,
-                               &frame.commandBuffer);
-        m_vkDestroyFence(m_device, frame.fence, nullptr);
+        if (m_vkDestroySemaphore != nullptr)
+        {
+            destroySemaphore(frame.semaphore);
+        }
+        if (m_vkFreeCommandBuffers != nullptr &&
+            m_commandPool != VK_NULL_HANDLE)
+        {
+            m_vkFreeCommandBuffers(m_device,
+                                   m_commandPool,
+                                   1,
+                                   &frame.commandBuffer);
+        }
+        if (m_vkDestroyFence != nullptr)
+        {
+            m_vkDestroyFence(m_device, frame.fence, nullptr);
+        }
     }
 
-    if (m_commandPool != VK_NULL_HANDLE)
+    if (m_commandPool != VK_NULL_HANDLE && m_vkDestroyCommandPool != nullptr)
     {
         m_vkDestroyCommandPool(m_device, m_commandPool, nullptr);
     }
diff --git a/renderer/rive_vk_bootstrap/src/vulkan_headless_frame_synchronizer.cpp b/renderer/rive_vk_bootstrap/src/vulkan_headless_frame_synchronizer.cpp
index 503cd0e..31e2dd4 100644
--- a/renderer/rive_vk_bootstrap/src/vulkan_headless_frame_synchronizer.cpp
+++ b/renderer/rive_vk_bootstrap/src/vulkan_headless_frame_synchronizer.cpp
@@ -83,8 +83,15 @@
 
 VulkanHeadlessFrameSynchronizer::~VulkanHeadlessFrameSynchronizer()
 {
-    // Don't do anything until everything is flushed through.
-    m_vkDeviceWaitIdle(vkDevice());
+    // Guard against partial construction: if the base constructor failed (e.g.
+    // an OOM creating a fence), this destructor still runs even though the
+    // derived device function pointers were never loaded. Calling the null
+    // m_vkDeviceWaitIdle here would segfault and crash the whole process.
+    if (m_vkDeviceWaitIdle != nullptr)
+    {
+        // Don't do anything until everything is flushed through.
+        m_vkDeviceWaitIdle(vkDevice());
+    }
 }
 
 bool VulkanHeadlessFrameSynchronizer::isFrameStarted() const