Skip to content

[Impeller] Harden the Vulkan backend against driver failures and resource exhaustion#189580

Open
sero583 wants to merge 4 commits into
flutter:masterfrom
sero583:impeller-vulkan-hardening
Open

[Impeller] Harden the Vulkan backend against driver failures and resource exhaustion#189580
sero583 wants to merge 4 commits into
flutter:masterfrom
sero583:impeller-vulkan-hardening

Conversation

@sero583

@sero583 sero583 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

This PR hardens Impeller's Vulkan backend against driver failures and resource exhaustion. Every fix traces to a specific crash, validation error, or unbounded-growth pattern observed during sustained stress testing on AMD RDNA 2 hardware (Radeon RX 6750 XT, Windows 10) and Mesa dzn (Vulkan on D3D12 under WSL2), using a 9-scene GPU benchmark suite (https://github.com/sero583/flutter-benchmark).

This is the first PR of the series splitting #183382 (Impeller Vulkan for Linux and Windows desktops), per review guidance there; design doc: https://flutter.dev/go/impeller-backend-desktop. The changes here are platform-agnostic and benefit the existing Android Vulkan path directly. Several issues addressed by the original branch were independently reported and fixed after that work was first published in March (#185122, #186749, #187761); this PR rebases onto those and ports the remaining fixes.

Device loss handling

On some drivers (observed on AMD), a vkQueueSubmit that fails with VK_ERROR_OUT_OF_HOST_MEMORY leaves the driver in a corrupted state: the command pool and command buffer handles passed to the failed submission are freed internally, and any further Vulkan call (including vkDeviceWaitIdle) can access-fault inside the ICD.

  • ContextVK gains an atomic device_lost_ flag with IsDeviceLost() / MarkDeviceLost(). CreateCommandBuffer() and CommandQueueVK::Submit() short-circuit once lost, and KHRSwapchainImplVK::AcquireNextDrawable() skips the frame.
  • On submission failure, tracked objects are abandoned without Vulkan calls (AbandonForDriverCrash() on command pools and descriptor pools), the device is marked lost, and per-heap usage/budget is logged (VK_EXT_memory_budget when the driver fills the chained struct) to make field reports diagnosable.
  • FenceWaiterVK releases rather than destroys the fence when the submit callback fails. Some drivers partially track the fence even though the submission failed; destroying it triggers VUID-vkDestroyFence-fence-01120 and crashes. This extends the reasoning of [Impeller] Move queue submission into a callback that is invoked by FenceWaiterVK::AddFence only if it can accept the fence #187761 to the submit-failure path.

Submission backpressure

Without backpressure, the CPU queues submissions faster than the GPU drains them; fences, tracked resources, and driver objects accumulate until VK_ERROR_OUT_OF_HOST_MEMORY (reproducible within minutes at uncapped frame rates in debug builds).

  • CommandQueueVK caps concurrent in-flight submissions (3 = kMaxFramesInFlight + 1) with a condition variable; the slot is released from the fence completion callback. A generous 5 s timeout cancels the submission rather than queueing unbounded work. The GpuSubmissionTracker bookkeeping introduced by [Impeller] Recycle HostBuffer arena entries only after GPU completion #188965 is preserved on every path: completions are recorded exactly once whether the submission succeeds, fails in vkQueueSubmit, or is rejected by the fence waiter, so HostBuffer arena recycling never stalls.
  • ContextVK::FlushCommandBuffers() submits in chunks of 64 so per-submit driver allocations stay bounded on frames that accumulate very large command buffer counts (heavy blur/filter workloads).
  • The embedder-managed image path in GPUSurfaceVulkanImpeller gains a two-entry frame fence ring mirroring the KHR swapchain's WaitForFence backpressure, with a bounded wait that drops the frame under extreme pressure. Image views for embedder-provided images are now cached instead of allocated (and leaked) every frame, and in-flight frames are drained before views are destroyed on resize or teardown (VUID-vkDestroyImageView-imageView-01026).

Command pool lifecycle

Descriptor pools

  • AbandonForDriverCrash() releases pool handles without Vulkan calls after a fatal submission failure, and the destructor releases handles when the context or recycler is already gone during teardown.
  • A newly allocated descriptor set is registered in the per-pipeline cache only after the allocation succeeded; previously a garbage handle was cached when vkAllocateDescriptorSets failed.
  • CreateNewPool failures during the out-of-pool-memory fallback are propagated instead of retrying with an empty pool list.
  • Set-recycling semantics are unchanged from the current implementation.

Barrier and render pass correctness

  • Six barrier fixes in BlitPassVK: pre-copy barriers use transfer stage/access, post-copy barriers flush transfer writes, and mipmap barriers match the actual current layout. Fixes corrupted glyph atlases and SYNC-HAZARD-WRITE-AFTER-WRITE reported by the synchronization validation layer on AMD RDNA 2.
  • RenderPassBuilderVK moves subpass dependencies to a counted builder, removes eByRegion from VK_SUBPASS_EXTERNAL dependencies (meaningless per Vulkan spec section 7.1 outside self-dependencies), and gains SetFramebufferFetchEnabled() so input attachment references and self-dependencies are omitted on drivers without framebuffer fetch support (Mesa dzn rejects them). The flag is wired through pipeline creation, render pass creation, and the compat render pass used for PSO construction.

Driver workarounds

  • DriverInfoVK exposes GetDriverID() (Vulkan 1.2) and detects Mesa dzn.
  • New skip_sub_region_buffer_to_image_copy workaround: Mesa dzn reports minImageTransferGranularity of (0,0,0) and corrupts textures on sub-region buffer-to-image copies; BlitPassVK stages such copies through a full-size temporary texture.

Memory management

  • The VMA buffer pool is capped at 256 blocks (about 1 GB). The pool otherwise grows without bound when high-water-mark demand spikes and pool-internal fragmentation prevents block reuse.
  • Image allocation failure paths no longer leak the VMA allocation when image view creation fails.
  • On Windows, the working set is periodically trimmed (EmptyWorkingSet, gated by cooldown, RSS threshold, and RSS-to-VMA ratio) when Resizable BAR VRAM mappings inflate RSS to several GB while actual VMA usage is small. This is cosmetic (the pages are GPU-mapped VRAM, not leaks) but prevents alarming numbers in monitoring tools. Happy to split this into a follow-up if reviewers prefer platform-specific code out of allocator_vk.cc.

KHR swapchain robustness

  • The synchronizer fence is re-signaled with an empty submit on every early return after a failed or unusable acquireNextImageKHR, including the eErrorSurfaceLostKHR path added by [Impeller] Do not log VK_ERROR_SURFACE_LOST_KHR errors returned by vkAcquireNextImageKHR #183338. WaitForFence resets the fence before the acquire; without a new signal, the next wrap-around to the same synchronizer deadlocks in waitForFences with an infinite timeout.
  • Zero-extent surfaces (minimized windows) skip the frame instead of attempting swapchain recreation, preserving recovery on restore.

Embedder-controlled presentation

  • CapabilitiesVK allows an embedder that presents rendered images itself (for example through a platform compositor) to create a Vulkan context without the surface, WSI, or swapchain extensions. Previously a device without VK_KHR_surface was rejected outright. The relaxation only activates in embedder mode when the surface extension is absent; embedders that do provide it are unaffected. This is the enabling change for the surfaceless Windows DirectComposition backend, and is a no-op for existing (Android, KHR-swapchain) users.

Testing

New unit tests:

  • CommandQueueVKTest.SubmitAfterDeviceLostIsCancelled: no submission reaches the queue once the device is marked lost.
  • CommandQueueVKTest.ThrottleAllowsSustainedSubmissions: in-flight slots are released as submissions complete (more submissions than the cap).
  • ContextVKTest.CreateCommandBufferShortCircuitsAfterDeviceLost.
  • ContextVKTest.EmbedderWithoutSurfaceExtensionsIsSurfaceless: an embedder device with no surface/swapchain extensions creates a valid context.
  • ContextVKTest.EmbedderWithSurfaceExtensionsStillEnablesThem: when the embedder provides the surface extension it is still honored.

Test infrastructure fix: the DeathRattle helper in command_pool_vk_unittests.cc invoked a moved-from std::function from its destructor (WaitForReclaim moves the local into a UniqueResourceVKT), which aborts with bad_function_call when running impeller_unittests on a Windows host and took the entire CommandPoolRecyclerVKTest suite down with it. With the guard in place, that suite and CommandPoolVKTest.DestroysCleanlyIfDeviceIsDestroyed (previously also crashing on Windows hosts) run and pass on this branch.

Suite results on a Windows host with this branch rebased onto current master: the full impeller_unittests suite runs 4550 tests, 2960 passed, 0 failed (the remainder are playground variants skipped as unsupported on the host). Two pre-existing issues are excluded as unrelated: the color emoji playground crashes (#189565, fix in #189572) and a debug-STL assert in a display_list gradient-stops test that reproduces identically on unmodified master.

Not unit-testable with the current mock (noted for reviewers): the fence release on failed submission (the mock's vkQueueSubmit cannot be made to fail) and Mesa dzn detection via driverID (the mock exposes no VkPhysicalDeviceVulkan12Properties). Extending the mock for failure injection could be follow-up work.

Manual: 9-scene benchmark suite on AMD RDNA 2 (Windows 10) and Mesa dzn (WSL2), no crashes, deadlocks, or validation errors under VK_LAYER_KHRONOS_validation with sync validation enabled.

Part of #181711

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

sero583 added 3 commits July 16, 2026 14:37
…urce exhaustion

Stability fixes for the Vulkan backend discovered through sustained
stress testing on AMD RDNA 2 hardware and Mesa dzn (Vulkan on D3D12 in
WSL2), first published as part of flutter#183382 and rebased onto the current
FenceWaiterVK submission architecture.

Device loss handling:
- ContextVK gains an atomic device_lost_ flag. CreateCommandBuffer()
  and CommandQueueVK::Submit() short-circuit once the device is lost,
  since any Vulkan call on a driver that entered a corrupted OOM state
  can access-fault inside the ICD.
- On vkQueueSubmit failure, tracked objects are abandoned without
  making Vulkan calls (AbandonForDriverCrash on command pools and
  descriptor pools), the device is marked lost, and per-heap memory
  budgets are logged for diagnosis.
- FenceWaiterVK releases rather than destroys the fence when the
  submit callback fails; some drivers partially track the fence even
  though the submission failed, and vkDestroyFence then crashes
  (VUID-vkDestroyFence-fence-01120 observed on AMD).

Submission backpressure:
- CommandQueueVK caps concurrent in-flight submissions (3) with a
  condition variable, releasing the slot from the fence completion
  callback. Prevents unbounded host memory growth when the CPU
  outpaces the GPU.
- ContextVK::FlushCommandBuffers() submits in chunks of 64 to keep
  per-submit driver allocations bounded on frames that accumulate very
  large command buffer counts.
- GPUSurfaceVulkanImpeller (embedder-managed image path) adds a
  two-entry frame fence ring mirroring the KHR swapchain's
  backpressure, with a bounded wait that skips the frame under extreme
  GPU pressure. Image views for embedder images are now cached instead
  of being allocated (and leaked) per frame, and in-flight frames are
  drained before views are destroyed on resize or teardown.

Command pool lifecycle:
- BackgroundCommandPoolVK releases its handles without Vulkan calls
  when the recycler is gone; the device may already be destroyed at
  that point. This is the same class of teardown race as the
  CommandPoolVK destructor fix that landed in flutter#186749.
- The recycled pool list is capped at 8 entries, evicting oldest
  first.
- unused_command_buffers_ is annotated IPLR_GUARDED_BY(pool_mutex_).

Descriptor pools:
- Reset via vkResetDescriptorPool on reclaim. Reclaimed pools
  previously stayed at capacity and produced
  VK_ERROR_OUT_OF_POOL_MEMORY on every reuse.

Barrier and render pass correctness:
- Six barrier fixes in the blit pass: pre-copy barriers use transfer
  stage and access flags, post-copy barriers flush transfer writes,
  and mipmap generation barriers match the actual current layout.
  Fixes corrupted glyph atlases and SYNC-HAZARD-WRITE-AFTER-WRITE
  reported by the synchronization validation layer on AMD RDNA 2.
- RenderPassBuilderVK moves subpass dependencies to a counted builder,
  drops eByRegion from external (VK_SUBPASS_EXTERNAL) dependencies
  where it has no meaning per Vulkan spec section 7.1, and gains
  SetFramebufferFetchEnabled() so input attachment references and
  self-dependencies are omitted on drivers without framebuffer fetch
  support. The flag is wired through pipeline and render pass creation
  and the compat render pass used for PSO construction.

Driver workarounds:
- DriverInfoVK exposes GetDriverID() (Vulkan 1.2) and detects Mesa
  dzn.
- New skip_sub_region_buffer_to_image_copy workaround: Mesa dzn
  reports minImageTransferGranularity of (0,0,0) and corrupts textures
  on sub-region buffer-to-image copies; the blit pass stages such
  copies through a full-size temporary texture.

Memory management:
- The VMA buffer pool is capped at 256 blocks (about 1 GB); the pool
  otherwise grows without bound when high-water-mark demand spikes and
  fragmentation prevents block reuse.
- Image allocation failure paths no longer leak the VMA allocation
  when image view creation fails.
- On Windows, the working set is periodically trimmed when Resizable
  BAR mappings inflate RSS far beyond actual VMA usage (gated by
  cooldown, RSS threshold, and RSS-to-VMA ratio).

KHR swapchain robustness:
- The synchronizer fence is re-signaled with an empty submit on every
  early return after a failed or unusable acquireNextImageKHR
  (including the surface-lost path added in flutter#183338). The fence was
  reset by WaitForFence, and without a new signal the next wrap-around
  to the same synchronizer deadlocks in waitForFences with an infinite
  timeout.
- AcquireNextDrawable short-circuits when the device is marked lost.
- Zero-extent surfaces (minimized windows) skip the frame instead of
  attempting swapchain recreation, preserving recovery on restore.

Embedder mode:
- CapabilitiesVK enumerates instance layers in embedder mode so that
  validation layers can be detected and enabled when requested.
…ottle tests

Adjust the descriptor pool changes to keep the current set-recycling
semantics (used sets return to the unused cache on reclaim) instead of
resetting pools on reclaim, which would have invalidated the cached set
handles that DescriptorsAreRecycled encodes as intended behavior. The
safety fixes remain: AbandonForDriverCrash, handle release on teardown
when the context or recycler is gone, propagating CreateNewPool
failures, and registering a new descriptor set in the cache only after
the allocation succeeded (a garbage handle was previously cached when
vkAllocateDescriptorSets failed).

Guard the DeathRattle test helper against invoking a moved-from
callback. WaitForReclaim moves the local instance into a
UniqueResourceVKT, and the moved-from local still runs the destructor;
calling the empty std::function aborts with bad_function_call. This
reproduces on unmodified master when running impeller_unittests on a
Windows host and made every CommandPoolRecyclerVKTest case crash there.

New tests:
- CommandQueueVKTest.SubmitAfterDeviceLostIsCancelled: no submission
  reaches the queue once the device is marked lost.
- CommandQueueVKTest.ThrottleAllowsSustainedSubmissions: in-flight
  slots are released as submissions complete.
- ContextVKTest.CreateCommandBufferShortCircuitsAfterDeviceLost.
An embedder that presents rendered images itself, for example through a
platform compositor, supplies a Vulkan device without the surface, WSI, or
swapchain extensions. CapabilitiesVK previously rejected such a device
because it required VK_KHR_surface and a windowing-system integration
extension. Relax those requirements in embedder mode when the surface
extension is absent, so presentation becomes the embedder's
responsibility. Embedders that do provide the surface extension are
unaffected.

Adds regression tests covering the surfaceless case and the case where
the embedder still provides the surface extension.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces robust device-lost tracking, frame throttling for embedder-managed images, and memory capping to prevent host memory exhaustion and driver crashes in Impeller's Vulkan backend. It also adds workarounds for Mesa dzn drivers and refines Vulkan barriers and subpass dependencies. Feedback on these changes highlights several issues: a Vulkan destruction order violation when handling allocation failures in allocator_vk.cc, a potential barrier validation warning in blit_pass_vk.cc due to layout assumptions, an ineffective thread-local rate limit for process-wide working set trimming, and potential crashes during teardown or resize on lost devices if invalid image views and fences are destroyed instead of released.

Comment on lines 470 to 477
if (rt_result != vk::Result::eSuccess) {
VALIDATION_LOG << "Unable to create a render target image view: "
<< vk::to_string(rt_result);
// Nothing owns the image yet; destroy it to avoid leaking the
// allocation.
vmaDestroyImage(allocator, vk_image, allocation);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If rt_result fails, image_view (the primary one created on line 443) and any previously successful rt_image_views are already created. Destroying the image via vmaDestroyImage before these image views are destroyed violates Vulkan's destruction order rules (all image views must be destroyed before their underlying image is destroyed). This can trigger validation layer errors or driver crashes. We should clear rt_image_views and reset image_view before destroying the image.

Suggested change
if (rt_result != vk::Result::eSuccess) {
VALIDATION_LOG << "Unable to create a render target image view: "
<< vk::to_string(rt_result);
// Nothing owns the image yet; destroy it to avoid leaking the
// allocation.
vmaDestroyImage(allocator, vk_image, allocation);
return;
}
if (rt_result != vk::Result::eSuccess) {
VALIDATION_LOG << "Unable to create a render target image view: "
<< vk::to_string(rt_result);
rt_image_views.clear();
image_view.reset();
vmaDestroyImage(allocator, vk_image, allocation);
return;
}

Comment on lines +108 to +114
dst_barrier.src_access = vk::AccessFlagBits::eTransferWrite;
dst_barrier.src_stage = vk::PipelineStageFlagBits::eTransfer;
// dstAccessMask must only list accesses valid for TRANSFER_DST_OPTIMAL.
// eShaderRead is not permitted in that layout; including it triggers AMD
// best practices validation layer message ID -212008545 (0xF35D019F).
dst_barrier.dst_access = vk::AccessFlagBits::eTransferWrite;
dst_barrier.dst_stage = vk::PipelineStageFlagBits::eTransfer;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In OnCopyTextureToTextureCommand, the destination texture dst is unconditionally assumed to have been previously written by a transfer operation, setting src_access to eTransferWrite and src_stage to eTransfer. However, if dst is not in eTransferDstOptimal (e.g., it is a newly created texture in eUndefined or was last used in a shader in eShaderReadOnlyOptimal), this will violate Vulkan's rules for image memory barriers and trigger validation warnings. We should conditionally set src_access and src_stage based on dst.GetLayout(), matching the correct pattern implemented in OnCopyBufferToTextureCommand (line 282).

Suggested change
dst_barrier.src_access = vk::AccessFlagBits::eTransferWrite;
dst_barrier.src_stage = vk::PipelineStageFlagBits::eTransfer;
// dstAccessMask must only list accesses valid for TRANSFER_DST_OPTIMAL.
// eShaderRead is not permitted in that layout; including it triggers AMD
// best practices validation layer message ID -212008545 (0xF35D019F).
dst_barrier.dst_access = vk::AccessFlagBits::eTransferWrite;
dst_barrier.dst_stage = vk::PipelineStageFlagBits::eTransfer;
if (dst.GetLayout() == vk::ImageLayout::eTransferDstOptimal) {
dst_barrier.src_access = vk::AccessFlagBits::eTransferWrite;
dst_barrier.src_stage = vk::PipelineStageFlagBits::eTransfer;
} else {
dst_barrier.src_access = vk::AccessFlagBits::eShaderRead;
dst_barrier.src_stage = vk::PipelineStageFlagBits::eFragmentShader;
}
// dstAccessMask must only list accesses valid for TRANSFER_DST_OPTIMAL.
// eShaderRead is not permitted in that layout; including it triggers AMD
// best practices validation layer message ID -212008545 (0xF35D019F).
dst_barrier.dst_access = vk::AccessFlagBits::eTransferWrite;
dst_barrier.dst_stage = vk::PipelineStageFlagBits::eTransfer;

Comment on lines +677 to +696
static thread_local int trim_cooldown = 0;
if (trim_cooldown > 0) {
--trim_cooldown;
}

if (trim_cooldown == 0) {
size_t vma_mb = DebugGetHeapUsage().ConvertTo<MebiBytes>().GetSize();
if (vma_mb < kMaxVmaMB) {
PROCESS_MEMORY_COUNTERS pmc = {};
pmc.cb = sizeof(pmc);
if (::GetProcessMemoryInfo(::GetCurrentProcess(), &pmc, sizeof(pmc))) {
size_t rss_mb = pmc.WorkingSetSize / kBytesPerMB;
if (rss_mb > kMinRssMB && rss_mb > vma_mb * kRssToVmaRatio) {
trim_cooldown = kTrimCooldownFrames;
::EmptyWorkingSet(::GetCurrentProcess());
FML_DLOG(INFO) << "Working set trimmed: " << rss_mb << " MB";
}
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The trim_cooldown is declared as static thread_local int. Since EmptyWorkingSet is a process-wide operation, having a thread-local cooldown means that if multiple threads (e.g., multiple raster threads for different engine instances) call this function, they will not share the cooldown. This can lead to the working set being trimmed much more frequently than the intended 300-frame interval, potentially causing performance degradation as pages are repeatedly forced out and faulted back in. Using a process-wide static atomic variable would ensure the rate limit is correctly enforced across all threads.

Suggested change
static thread_local int trim_cooldown = 0;
if (trim_cooldown > 0) {
--trim_cooldown;
}
if (trim_cooldown == 0) {
size_t vma_mb = DebugGetHeapUsage().ConvertTo<MebiBytes>().GetSize();
if (vma_mb < kMaxVmaMB) {
PROCESS_MEMORY_COUNTERS pmc = {};
pmc.cb = sizeof(pmc);
if (::GetProcessMemoryInfo(::GetCurrentProcess(), &pmc, sizeof(pmc))) {
size_t rss_mb = pmc.WorkingSetSize / kBytesPerMB;
if (rss_mb > kMinRssMB && rss_mb > vma_mb * kRssToVmaRatio) {
trim_cooldown = kTrimCooldownFrames;
::EmptyWorkingSet(::GetCurrentProcess());
FML_DLOG(INFO) << "Working set trimmed: " << rss_mb << " MB";
}
}
}
}
static std::atomic<int> trim_cooldown{0};
if (trim_cooldown.load() > 0) {
trim_cooldown.fetch_sub(1);
}
if (trim_cooldown.load() == 0) {
size_t vma_mb = DebugGetHeapUsage().ConvertTo<MebiBytes>().GetSize();
if (vma_mb < kMaxVmaMB) {
PROCESS_MEMORY_COUNTERS pmc = {};
pmc.cb = sizeof(pmc);
if (::GetProcessMemoryInfo(::GetCurrentProcess(), &pmc, sizeof(pmc))) {
size_t rss_mb = pmc.WorkingSetSize / kBytesPerMB;
if (rss_mb > kMinRssMB && rss_mb > vma_mb * kRssToVmaRatio) {
trim_cooldown.store(kTrimCooldownFrames);
::EmptyWorkingSet(::GetCurrentProcess());
FML_DLOG(INFO) << "Working set trimmed: " << rss_mb << " MB";
}
}
}
}

Comment on lines +104 to +119
if (impeller_context_) {
auto& context_vk = impeller::ContextVK::Cast(*impeller_context_);
if (!context_vk.IsDeviceLost()) {
for (auto& fence : frame_fences_) {
if (fence) {
auto wait_result = context_vk.GetDevice().waitForFences(
{*fence}, VK_TRUE, kFenceDrainTimeoutNs);
if (wait_result != impeller::vk::Result::eSuccess) {
FML_LOG(ERROR) << "Frame fence wait failed during teardown: "
<< impeller::vk::to_string(wait_result);
}
}
}
}
}
cached_image_views_.clear();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the device is lost, calling cached_image_views_.clear() will invoke vkDestroyImageView on each cached image view, and the destructor of frame_fences_ will invoke vkDestroyFence on each fence. On a corrupted driver (such as the AMD driver failure described in the PR), calling these Vulkan APIs on already-invalid handles can cause validation errors or access violations/crashes inside the ICD. To harden the teardown path, we should release the handles of both cached_image_views_ and frame_fences_ without making Vulkan API calls when the device is lost.

  if (impeller_context_) {
    auto& context_vk = impeller::ContextVK::Cast(*impeller_context_);
    if (!context_vk.IsDeviceLost()) {
      for (auto& fence : frame_fences_) {
        if (fence) {
          auto wait_result = context_vk.GetDevice().waitForFences(
              {*fence}, VK_TRUE, kFenceDrainTimeoutNs);
          if (wait_result != impeller::vk::Result::eSuccess) {
            FML_LOG(ERROR) << "Frame fence wait failed during teardown: "
                           << impeller::vk::to_string(wait_result);
          }
        }
      }
    } else {
      for (auto& fence : frame_fences_) {
        fence.release();
      }
      for (auto& [key, view] : cached_image_views_) {
        view.release();
      }
    }
  }
  cached_image_views_.clear();

Comment on lines +287 to +297
for (size_t i = 0; i < kMaxFramesInFlight; i++) {
if (frame_fences_[i]) {
auto wait = context_vk.GetDevice().waitForFences(
{*frame_fences_[i]}, VK_TRUE, kFenceDrainTimeoutNs);
if (wait != impeller::vk::Result::eSuccess) {
FML_LOG(ERROR) << "Failed to wait for in-flight fence on resize: "
<< impeller::vk::to_string(wait);
}
}
}
cached_image_views_.clear();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

On resize, if the device is lost, calling waitForFences and cached_image_views_.clear() (which destroys the image views) can crash or trigger validation errors on a corrupted driver. We should check !context_vk.IsDeviceLost() before waiting for fences, and if the device is lost, we should release the cached image views instead of destroying them.

      if (!context_vk.IsDeviceLost()) {
        for (size_t i = 0; i < kMaxFramesInFlight; i++) {
          if (frame_fences_[i]) {
            auto wait = context_vk.GetDevice().waitForFences(
                {*frame_fences_[i]}, VK_TRUE, kFenceDrainTimeoutNs);
            if (wait != impeller::vk::Result::eSuccess) {
              FML_LOG(ERROR) << "Failed to wait for in-flight fence on resize: "
                             << impeller::vk::to_string(wait);
            }
          }
        }
      } else {
        for (auto& [key, view] : cached_image_views_) {
          view.release();
        }
      }
      cached_image_views_.clear();

Follow-ups to the first review pass on the Vulkan hardening change:

- AllocatedTextureSourceVK: destroy the already-created image views
  before the image when a render target view fails to create partway
  through the per-subresource view loop.
- BlitPassVK: pick the destination barrier's source scope from the
  texture's tracked layout in OnCopyTextureToTextureCommand, mirroring
  OnCopyBufferToTextureCommand. The unconditional eTransferWrite scope
  can trip best-practices validation when the destination was last
  sampled; the WAW protection for back-to-back transfer writes in the
  atlas growth path is preserved in the eTransferDstOptimal branch.
- AllocatorVK: make the Windows working-set trim cooldown a
  process-wide atomic instead of thread_local, since EmptyWorkingSet
  affects the whole process.
- GPUSurfaceVulkanImpeller: on a lost device, abandon the cached image
  views and frame fences instead of waiting on or destroying them, in
  both the destructor and the resize drain, matching the
  AbandonForDriverCrash policy. Adds a teardown-after-device-loss test.
@sero583

sero583 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Acknowledging the bot's review. I'm currently short on time, but I will go through this in detail later.

At a quick glance: the bot caught some valid edge cases (like the teardown paths on a lost device and the thread_local scope), which I will address. However, its analysis on the Vulkan barrier rules (blit_pass_vk.cc) is incorrect and the suggested change would actually re-introduce the WAW hazard the code aims to fix.

I will sort through the valid points, implement the fixes locally, and push an update as soon as I have time to get back to this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

e: impeller Impeller rendering backend issues and features requests engine flutter/engine related. See also e: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant