Skip to content

Commit 64a83f4

Browse files
rluedersjtmcdole
authored andcommitted
[Impeller] Retry uncompressed when fixed-rate compression is exhausted (flutter#187586)
Fixes flutter#187564 ## Issue On the Pixel 10 (Tensor G5 / **Imagination PowerVR** GPU), Impeller's Vulkan backend crashes the raster thread under GPU pressure: ``` [ERROR:impeller/renderer/backend/vulkan/allocator_vk.cc] Unable to allocate Vulkan Image: ErrorCompressionExhaustedEXT Type: Texture2D ... Usage: { ShaderRead, RenderTarget } [ERROR:impeller/renderer/render_target.cc] Could not create color texture. [ERROR:impeller/renderer/render_target.cc] Render target does not have color attachment at index 0. F/libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid NNNN (1.raster) ``` (In release builds the same failure surfaces as `[FATAL:flutter/impeller/display_list/canvas.cc(1471)] Check failed: back_texture. Context is valid:0` SIGABRT, or a bare SIGSEGV @ 0x20 on the `*.raster` thread.) ### Root cause Impeller requests **fixed-rate image compression** (`VkImageCompressionControlEXT`) for lossy render targets (`CreateOffscreenMSAA`'s color-resolve texture). When the PowerVR driver's fixed-rate-compression resources are exhausted, `vmaCreateImage` returns `VK_ERROR_COMPRESSION_EXHAUSTED_EXT`. `AllocatedTextureSourceVK` did not handle this — it logged and returned an invalid texture, which became a render target with no color attachment, and the raster thread then dereferenced the null texture. This is a regression: it does **not** reproduce on Flutter 3.41.9, but does on 3.44.x. It is PowerVR-specific (the error is tied to the driver's compression resource pool) and GPU-state-dependent (it fires once cumulative GPU compression pressure is high enough). ## Fix `VK_ERROR_COMPRESSION_EXHAUSTED_EXT` is, per the Vulkan spec, only returned for fixed-rate compression requests — so retrying the same allocation **without** compression is always a valid recovery. On that error, `AllocatedTextureSourceVK` now unlinks `VkImageCompressionControlEXT` from the create-info chain and retries `vmaCreateImage` once uncompressed (logging a one-time warning). The texture is identical, just stored uncompressed; the only cost is on the failure path (a small amount of extra memory/bandwidth for the affected texture, instead of a crash). ## Tests - **Unit test** (`allocator_vk_unittests.cc` → `RetriesUncompressedOnCompressionExhausted`): extends the Vulkan mock to advertise fixed-rate compression and to fail the first compressed `vkCreateImage` with `VK_ERROR_COMPRESSION_EXHAUSTED_EXT`, then asserts the allocator returns a **valid** texture and called `vkCreateImage` **twice** (compressed attempt + uncompressed retry). Passes; the other allocator tests are unaffected. - **On-device A/B** (Pixel 10, locally-built `android_profile_arm64`, same repro, same GPU-pressured state): | Engine | Result | |---|---| | Unpatched | `ErrorCompressionExhausted` → **SIGSEGV** on `1.raster` (crash) | | Patched | exhaustion → retry → **survives**; logs `Fixed-rate image compression exhausted; falling back to uncompressed image allocation` | ## Repro A pure-Flutter app that stacks enough full-screen `BackdropFilter` / `saveLayer` offscreen render targets to exhaust the PowerVR compression pool (see `lib/main.dart` in the linked repro). Crashes on the unpatched engine; runs on the patched one. Stable on either with `--no-enable-impeller`. ## Environment - Flutter 3.44.x (regressed from 3.41.9) - Device: Pixel 10, Android 17 (API 37) - GPU: Imagination PowerVR (Tensor G5); renderer Impeller / Vulkan ## Pre-launch checklist - [x] Added a unit test (or explained why not possible). - [x] Listed any breaking changes (none). - [x] Signed the [CLA](https://cla.developers.google.com/). --------- Co-authored-by: John "codefu" McDole <codefu@google.com>
1 parent 69c8c61 commit 64a83f4

4 files changed

Lines changed: 179 additions & 25 deletions

File tree

engine/src/flutter/impeller/renderer/backend/vulkan/allocator_vk.cc

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
#include "impeller/renderer/backend/vulkan/allocator_vk.h"
66

77
#include <memory>
8+
#include <mutex>
89
#include <utility>
910

11+
#include "flutter/fml/logging.h"
1012
#include "flutter/fml/memory/ref_ptr.h"
1113
#include "flutter/fml/trace_event.h"
1214
#include "impeller/base/allocation_size.h"
@@ -343,34 +345,63 @@ class AllocatedTextureSourceVK final : public TextureSourceVK {
343345
desc.storage_mode, supports_memoryless_textures));
344346
alloc_nfo.flags = ToVmaAllocationCreateFlags(desc.storage_mode);
345347

346-
auto create_info_native =
347-
static_cast<vk::ImageCreateInfo::NativeType>(image_info);
348-
349348
VkImage vk_image = VK_NULL_HANDLE;
350349
VmaAllocation allocation = {};
351350
VmaAllocationInfo allocation_info = {};
352-
{
353-
auto result = vk::Result{::vmaCreateImage(allocator, //
354-
&create_info_native, //
355-
&alloc_nfo, //
356-
&vk_image, //
357-
&allocation, //
358-
&allocation_info //
359-
)};
360-
if (result != vk::Result::eSuccess) {
361-
VALIDATION_LOG << "Unable to allocate Vulkan Image: "
362-
<< vk::to_string(result)
363-
<< " Type: " << TextureTypeToString(desc.type)
364-
<< " Mode: " << StorageModeToString(desc.storage_mode)
365-
<< " Usage: " << TextureUsageMaskToString(desc.usage)
366-
<< " [VK]Flags: " << vk::to_string(image_info.flags)
367-
<< " [VK]Format: " << vk::to_string(image_info.format)
368-
<< " [VK]Usage: " << vk::to_string(image_info.usage)
369-
<< " [VK]Mem. Flags: "
370-
<< vk::to_string(vk::MemoryPropertyFlags(
371-
alloc_nfo.preferredFlags));
372-
return;
373-
}
351+
352+
// Performs the VMA image allocation using the current create-info chain.
353+
// The native create-info is re-derived from the chain on each call, so an
354+
// unlink of the compression-control struct between calls is reflected.
355+
const auto try_create_image = [&]() -> vk::Result {
356+
vk::ImageCreateInfo::NativeType create_info_native =
357+
static_cast<vk::ImageCreateInfo::NativeType>(
358+
image_info_chain.get<vk::ImageCreateInfo>());
359+
return vk::Result{::vmaCreateImage(allocator, //
360+
&create_info_native, //
361+
&alloc_nfo, //
362+
&vk_image, //
363+
&allocation, //
364+
&allocation_info //
365+
)};
366+
};
367+
368+
// Fixed-rate compression was requested iff a rate was selected above.
369+
const bool requested_compression = frc_rate.has_value();
370+
vk::Result alloc_result = try_create_image();
371+
372+
// Some drivers (e.g. PowerVR) can return VK_ERROR_COMPRESSION_EXHAUSTED_EXT
373+
// when fixed-rate compression resources are depleted. Per the Vulkan spec
374+
// this error is only returned for fixed-rate compression requests, so
375+
// retrying without compression is a valid recovery. Without it the
376+
// allocation fails, the texture is invalid, and the resulting null render
377+
// target crashes the raster thread.
378+
if (alloc_result == vk::Result::eErrorCompressionExhaustedEXT &&
379+
requested_compression) {
380+
static std::once_flag warn_once;
381+
std::call_once(warn_once, [] {
382+
FML_LOG(WARNING)
383+
<< "Fixed-rate image compression exhausted; falling back to "
384+
"uncompressed image allocation. (This message is logged once.)";
385+
});
386+
// The compression-control struct is only present here because compression
387+
// was requested above, so unlinking it once is safe (no double-unlink).
388+
image_info_chain.unlink<vk::ImageCompressionControlEXT>();
389+
alloc_result = try_create_image();
390+
}
391+
392+
if (alloc_result != vk::Result::eSuccess) {
393+
VALIDATION_LOG << "Unable to allocate Vulkan Image: "
394+
<< vk::to_string(alloc_result)
395+
<< " Type: " << TextureTypeToString(desc.type)
396+
<< " Mode: " << StorageModeToString(desc.storage_mode)
397+
<< " Usage: " << TextureUsageMaskToString(desc.usage)
398+
<< " [VK]Flags: " << vk::to_string(image_info.flags)
399+
<< " [VK]Format: " << vk::to_string(image_info.format)
400+
<< " [VK]Usage: " << vk::to_string(image_info.usage)
401+
<< " [VK]Mem. Flags: "
402+
<< vk::to_string(
403+
vk::MemoryPropertyFlags(alloc_nfo.preferredFlags));
404+
return;
374405
}
375406

376407
auto image = vk::Image{vk_image};

engine/src/flutter/impeller/renderer/backend/vulkan/allocator_vk_unittests.cc

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
#include <algorithm>
6+
57
#include "flutter/testing/testing.h" // IWYU pragma: keep
68
#include "gtest/gtest.h"
79
#include "impeller/base/allocation_size.h"
@@ -93,6 +95,41 @@ TEST(AllocatorVKTest, ImageResourceKeepsVulkanDeviceAlive) {
9395
ASSERT_TRUE(weak_allocator.lock());
9496
}
9597

98+
TEST(AllocatorVKTest, RetriesUncompressedOnCompressionExhausted) {
99+
// Advertise fixed-rate compression support, then force the first (compressed)
100+
// vkCreateImage to fail with VK_ERROR_COMPRESSION_EXHAUSTED_EXT, as the
101+
// PowerVR driver does when its fixed-rate-compression resources are depleted.
102+
auto const context =
103+
MockVulkanContextBuilder()
104+
.SetDeviceExtensions(
105+
{"VK_KHR_swapchain", "VK_EXT_image_compression_control"})
106+
.SetCompressionExhaustedCreateImageFailures(1)
107+
.Build();
108+
ASSERT_TRUE(context);
109+
auto allocator = context->GetResourceAllocator();
110+
ASSERT_TRUE(allocator);
111+
112+
// A lossy (fixed-rate-compressed) render target. The first compressed
113+
// allocation fails with COMPRESSION_EXHAUSTED; the allocator must retry
114+
// without compression and still produce a valid texture instead of returning
115+
// null (a null render target previously crashed the raster thread).
116+
auto texture = allocator->CreateTexture(TextureDescriptor{
117+
.storage_mode = StorageMode::kDevicePrivate,
118+
.format = PixelFormat::kR8G8B8A8UNormInt,
119+
.size = {64, 64},
120+
.usage = TextureUsage::kRenderTarget | TextureUsage::kShaderRead,
121+
.compression_type = CompressionType::kLossy,
122+
});
123+
124+
ASSERT_TRUE(texture);
125+
EXPECT_TRUE(texture->IsValid());
126+
127+
// vkCreateImage was called twice: the compressed attempt (which failed with
128+
// COMPRESSION_EXHAUSTED) and the uncompressed retry (which succeeded).
129+
auto const called = GetMockVulkanFunctions(context->GetDevice());
130+
EXPECT_EQ(std::count(called->begin(), called->end(), "vkCreateImage"), 2);
131+
}
132+
96133
#ifdef IMPELLER_DEBUG
97134

98135
TEST(AllocatorVKTest, RecreateSwapchainWhenSizeChanges) {

engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.cc

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ struct MockVulkanState {
122122
wait_for_fences_callback;
123123
std::function<std::remove_pointer_t<PFN_vkAcquireNextImageKHR>>
124124
acquire_next_image_callback;
125+
// When > 0, the next vkCreateImage for a fixed-rate-compressed image returns
126+
// VK_ERROR_COMPRESSION_EXHAUSTED_EXT and decrements (models PowerVR).
127+
int compression_exhausted_create_image_failures = 0;
125128
};
126129

127130
class MockVulkanStatePtr {
@@ -242,6 +245,52 @@ void vkGetPhysicalDeviceProperties(VkPhysicalDevice physicalDevice,
242245
}
243246
}
244247

248+
void vkGetPhysicalDeviceFeatures2(VkPhysicalDevice physicalDevice,
249+
VkPhysicalDeviceFeatures2* pFeatures) {
250+
// Advertise the features the mock supports by walking the pNext chain.
251+
auto* next = reinterpret_cast<VkBaseOutStructure*>(pFeatures->pNext);
252+
while (next != nullptr) {
253+
if (next->sType ==
254+
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_COMPRESSION_CONTROL_FEATURES_EXT) {
255+
reinterpret_cast<VkPhysicalDeviceImageCompressionControlFeaturesEXT*>(
256+
next)
257+
->imageCompressionControl = VK_TRUE;
258+
}
259+
next = next->pNext;
260+
}
261+
}
262+
263+
VkResult vkGetPhysicalDeviceImageFormatProperties2(
264+
VkPhysicalDevice physicalDevice,
265+
const VkPhysicalDeviceImageFormatInfo2* pImageFormatInfo,
266+
VkImageFormatProperties2* pImageFormatProperties) {
267+
// Report fixed-rate compression support when it is queried (i.e. the input
268+
// carries a VkImageCompressionControlEXT and the output a
269+
// VkImageCompressionPropertiesEXT).
270+
bool compression_requested = false;
271+
const auto* in =
272+
reinterpret_cast<const VkBaseInStructure*>(pImageFormatInfo->pNext);
273+
while (in != nullptr) {
274+
if (in->sType == VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_CONTROL_EXT) {
275+
compression_requested = true;
276+
}
277+
in = in->pNext;
278+
}
279+
auto* out =
280+
reinterpret_cast<VkBaseOutStructure*>(pImageFormatProperties->pNext);
281+
while (compression_requested && out != nullptr) {
282+
if (out->sType == VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_PROPERTIES_EXT) {
283+
auto* props = reinterpret_cast<VkImageCompressionPropertiesEXT*>(out);
284+
props->imageCompressionFlags =
285+
VK_IMAGE_COMPRESSION_FIXED_RATE_EXPLICIT_EXT;
286+
props->imageCompressionFixedRateFlags =
287+
VK_IMAGE_COMPRESSION_FIXED_RATE_4BPC_BIT_EXT;
288+
}
289+
out = out->pNext;
290+
}
291+
return VK_SUCCESS;
292+
}
293+
245294
void vkGetPhysicalDeviceQueueFamilyProperties(
246295
VkPhysicalDevice physicalDevice,
247296
uint32_t* pQueueFamilyPropertyCount,
@@ -368,6 +417,21 @@ VkResult vkCreateImage(VkDevice device,
368417
const VkImageCreateInfo* pCreateInfo,
369418
const VkAllocationCallbacks* pAllocator,
370419
VkImage* pImage) {
420+
reinterpret_cast<MockDevice*>(device)->AddCalledFunction("vkCreateImage");
421+
// Simulate VK_ERROR_COMPRESSION_EXHAUSTED_EXT for fixed-rate-compressed image
422+
// creates (the spec only returns this error for compression requests).
423+
if (g_mock_vulkan_state &&
424+
g_mock_vulkan_state->compression_exhausted_create_image_failures > 0) {
425+
const auto* next =
426+
reinterpret_cast<const VkBaseInStructure*>(pCreateInfo->pNext);
427+
while (next != nullptr) {
428+
if (next->sType == VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_CONTROL_EXT) {
429+
g_mock_vulkan_state->compression_exhausted_create_image_failures--;
430+
return VK_ERROR_COMPRESSION_EXHAUSTED_EXT;
431+
}
432+
next = next->pNext;
433+
}
434+
}
371435
*pImage = reinterpret_cast<VkImage>(0xD0D0CACA);
372436
return VK_SUCCESS;
373437
}
@@ -907,6 +971,14 @@ PFN_vkVoidFunction GetMockVulkanProcAddress(VkInstance instance,
907971
vkGetPhysicalDeviceFormatProperties);
908972
} else if (strcmp("vkGetPhysicalDeviceProperties", pName) == 0) {
909973
return reinterpret_cast<PFN_vkVoidFunction>(vkGetPhysicalDeviceProperties);
974+
} else if (strcmp("vkGetPhysicalDeviceFeatures2", pName) == 0 ||
975+
strcmp("vkGetPhysicalDeviceFeatures2KHR", pName) == 0) {
976+
return reinterpret_cast<PFN_vkVoidFunction>(vkGetPhysicalDeviceFeatures2);
977+
} else if (strcmp("vkGetPhysicalDeviceImageFormatProperties2", pName) == 0 ||
978+
strcmp("vkGetPhysicalDeviceImageFormatProperties2KHR", pName) ==
979+
0) {
980+
return reinterpret_cast<PFN_vkVoidFunction>(
981+
vkGetPhysicalDeviceImageFormatProperties2);
910982
} else if (strcmp("vkGetPhysicalDeviceQueueFamilyProperties", pName) == 0) {
911983
return reinterpret_cast<PFN_vkVoidFunction>(
912984
vkGetPhysicalDeviceQueueFamilyProperties);
@@ -1097,6 +1169,8 @@ std::shared_ptr<ContextVK> MockVulkanContextBuilder::Build() {
10971169
g_mock_vulkan_state->acquire_next_image_callback =
10981170
acquire_next_image_callback_;
10991171
g_mock_vulkan_state->wait_for_fences_callback = wait_for_fences_callback_;
1172+
g_mock_vulkan_state->compression_exhausted_create_image_failures =
1173+
compression_exhausted_create_image_failures_;
11001174
settings.embedder_data = embedder_data_;
11011175
std::shared_ptr<ContextVK> result = ContextVK::Create(std::move(settings));
11021176
return result;

engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,17 @@ class MockVulkanContextBuilder {
134134
return *this;
135135
}
136136

137+
/// Make the next `count` calls to vkCreateImage for a fixed-rate-compressed
138+
/// image (one carrying a VkImageCompressionControlEXT in its pNext chain)
139+
/// fail with VK_ERROR_COMPRESSION_EXHAUSTED_EXT. Also advertises the
140+
/// VK_EXT_image_compression_control feature/format support so the
141+
/// fixed-rate-compression path is exercised.
142+
MockVulkanContextBuilder& SetCompressionExhaustedCreateImageFailures(
143+
int count) {
144+
compression_exhausted_create_image_failures_ = count;
145+
return *this;
146+
}
147+
137148
private:
138149
std::function<void(ContextVK::Settings&)> settings_callback_;
139150
std::vector<std::string> instance_extensions_;
@@ -151,6 +162,7 @@ class MockVulkanContextBuilder {
151162
acquire_next_image_callback_;
152163
std::function<std::remove_pointer_t<PFN_vkWaitForFences>>
153164
wait_for_fences_callback_;
165+
int compression_exhausted_create_image_failures_ = 0;
154166
};
155167

156168
/// @brief Override the image size returned by all swapchain images.

0 commit comments

Comments
 (0)