[iOS] reduce wide gamut memory by 50% (for onscreen surfaces). (#165601)

If the onscreen surface is opaque, resolve to an alpha-less wide gamut
format. This reduces the bits per color from 40 to 32, which allows us
to fit in a 32 bit texture size, down from 64. This should reduce memory
usage by nearly 50 MB for typical applications.
This commit is contained in:
Jonah Williams
2025-03-25 08:05:06 -07:00
committed by GitHub
parent 33cf3b65bc
commit e60822e1a6
8 changed files with 64 additions and 24 deletions

View File

@@ -1723,12 +1723,20 @@ bool Canvas::SupportsBlitToOnscreen() const {
}
bool Canvas::BlitToOnscreen(bool is_onscreen) {
auto command_buffer = renderer_.GetContext()->CreateCommandBuffer();
std::shared_ptr<CommandBuffer> command_buffer =
renderer_.GetContext()->CreateCommandBuffer();
command_buffer->SetLabel("EntityPass Root Command Buffer");
auto offscreen_target = render_passes_.back()
.inline_pass_context->GetPassTarget()
.GetRenderTarget();
if (SupportsBlitToOnscreen()) {
RenderTarget offscreen_target = render_passes_.back()
.inline_pass_context->GetPassTarget()
.GetRenderTarget();
// If the src and destination format differ (due to wide gamut, alpha-less
// format, et cetera), then a draw must always be performed instead of a blit.
if (SupportsBlitToOnscreen() &&
offscreen_target.GetRenderTargetTexture()
->GetTextureDescriptor()
.format == render_target_.GetRenderTargetTexture()
->GetTextureDescriptor()
.format) {
auto blit_pass = command_buffer->CreateBlitPass();
blit_pass->AddCopy(offscreen_target.GetRenderTargetTexture(),
render_target_.GetRenderTargetTexture());

View File

@@ -24,11 +24,12 @@ namespace testing {
std::unique_ptr<Canvas> CreateTestCanvas(
ContentContext& context,
std::optional<Rect> cull_rect = std::nullopt,
bool requires_readback = false) {
bool requires_readback = false,
std::optional<PixelFormat> format = std::nullopt) {
TextureDescriptor onscreen_desc;
onscreen_desc.size = {100, 100};
onscreen_desc.format =
context.GetDeviceCapabilities().GetDefaultColorFormat();
format.value_or(context.GetDeviceCapabilities().GetDefaultColorFormat());
onscreen_desc.usage = TextureUsage::kRenderTarget;
onscreen_desc.storage_mode = StorageMode::kDevicePrivate;
onscreen_desc.sample_count = SampleCount::kCount1;
@@ -385,5 +386,18 @@ TEST_P(AiksTest, SupportsBlitToOnscreen) {
}
}
TEST_P(AiksTest, SupportsBlitToOnscreenWithDifferentFormat) {
if (GetBackend() == PlaygroundBackend::kOpenGLES) {
GTEST_SKIP() << "Not valid on GLES";
}
// Create an onscreen format which is different than the offscreen format,
// then make the canvas perform a restore to verify a blit is not used.
ContentContext context(GetContext(), nullptr);
auto canvas = CreateTestCanvas(context, Rect::MakeLTRB(0, 0, 100, 100),
/*requires_readback=*/true,
/*format=*/PixelFormat::kB8G8R8A8UNormIntSRGB);
canvas->EndReplay();
}
} // namespace testing
} // namespace impeller

View File

@@ -52,7 +52,11 @@ std::shared_ptr<Texture> SwapchainTransientsMTL::GetMSAATexture() {
TextureDescriptor desc;
desc.size = size_;
desc.sample_count = SampleCount::kCount4;
desc.format = format_;
if (format_ == PixelFormat::kB10G10R10XR) {
desc.format = PixelFormat::kB10G10R10A10XR;
} else {
desc.format = format_;
}
desc.storage_mode = StorageMode::kDeviceTransient;
desc.usage = TextureUsage::kRenderTarget;
desc.type = TextureType::kTexture2DMultisample;

View File

@@ -73,6 +73,14 @@ TEST_P(SwapchainTransientsMTLTest, CanAllocateSwapchainTextures) {
// Texture cache is invalidated when pixel format changes.
transients->SetSizeAndFormat({2, 2}, PixelFormat::kB10G10R10A10XR);
EXPECT_NE(resolve, transients->GetResolveTexture());
// Transients converts alpha-less format to have alpha for MSAA.
transients->SetSizeAndFormat({2, 2}, PixelFormat::kB10G10R10XR);
EXPECT_EQ(transients->GetMSAATexture()->GetTextureDescriptor().format,
PixelFormat::kB10G10R10A10XR);
EXPECT_EQ(transients->GetResolveTexture()->GetTextureDescriptor().format,
PixelFormat::kB10G10R10XR);
}
} // namespace testing

View File

@@ -275,6 +275,9 @@ extern CFTimeInterval display_link_target;
} else if (self.pixelFormat == MTLPixelFormatBGRA10_XR) {
pixelFormat = kCVPixelFormatType_40ARGBLEWideGamut;
bytesPerElement = 8;
} else if (self.pixelFormat == MTLPixelFormatBGR10_XR) {
pixelFormat = kCVPixelFormatType_30RGBLEPackedWideGamut;
bytesPerElement = 4;
} else {
FML_LOG(ERROR) << "Unsupported pixel format: " << self.pixelFormat;
return nil;

View File

@@ -51,9 +51,12 @@ FLUTTER_ASSERT_ARC
CAMetalLayer* layer = (CAMetalLayer*)self.layer;
#pragma clang diagnostic pop
layer.pixelFormat = pixelFormat;
if (pixelFormat == MTLPixelFormatRGBA16Float || pixelFormat == MTLPixelFormatBGRA10_XR) {
if (pixelFormat == MTLPixelFormatRGBA16Float || pixelFormat == MTLPixelFormatBGRA10_XR ||
pixelFormat == MTLPixelFormatBGR10_XR) {
self->_colorSpaceRef = fml::CFRef(CGColorSpaceCreateWithName(kCGColorSpaceExtendedSRGB));
layer.colorspace = self->_colorSpaceRef;
// Overlay layers always need an alpha channel.
layer.pixelFormat = MTLPixelFormatBGRA10_XR;
}
}
return self;

View File

@@ -78,16 +78,6 @@ FLUTTER_ASSERT_ARC
return self;
}
static void PrintWideGamutWarningOnce() {
static BOOL did_print = NO;
if (did_print) {
return;
}
FML_DLOG(WARNING) << "Rendering wide gamut colors is turned on but isn't "
"supported, downgrading the color gamut to sRGB.";
did_print = YES;
}
- (void)layoutSubviews {
if ([self.layer isKindOfClass:[CAMetalLayer class]]) {
// It is a known Apple bug that CAMetalLayer incorrectly reports its supported
@@ -96,6 +86,7 @@ static void PrintWideGamutWarningOnce() {
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
CAMetalLayer* layer = (CAMetalLayer*)self.layer;
#pragma clang diagnostic pop
CGFloat screenScale = self.screen.scale;
layer.allowsGroupOpacity = YES;
layer.contentsScale = screenScale;
@@ -104,9 +95,13 @@ static void PrintWideGamutWarningOnce() {
if (_isWideGamutEnabled && self.isWideGamutSupported) {
fml::CFRef<CGColorSpaceRef> srgb(CGColorSpaceCreateWithName(kCGColorSpaceExtendedSRGB));
layer.colorspace = srgb;
layer.pixelFormat = MTLPixelFormatBGRA10_XR;
} else if (_isWideGamutEnabled && !self.isWideGamutSupported) {
PrintWideGamutWarningOnce();
// If the flutter layer is opaque, then use an alpha-less format for the onscreen
// texture. This will reduce wide gamut memory usage by 50%, and Impeller will
// still correctly use alpha for MSAA textures and any offscreen save layer usage.
// For non-wide gamut formats there is no point in removing the alpha channel as
// the textures must align to 32 bits (32 -> 24 = 32) whereas wide gamut is (40 -> 32 = 32)
// instead of 64.
layer.pixelFormat = layer.opaque ? MTLPixelFormatBGR10_XR : MTLPixelFormatBGRA10_XR;
}
}

View File

@@ -4,6 +4,7 @@
#import "flutter/shell/platform/darwin/ios/ios_surface_metal_impeller.h"
#include "flutter/impeller/core/formats.h"
#include "flutter/impeller/renderer/backend/metal/formats_mtl.h"
#include "flutter/impeller/renderer/context.h"
#include "flutter/shell/gpu/gpu_surface_metal_impeller.h"
@@ -43,8 +44,12 @@ void IOSSurfaceMetalImpeller::UpdateStorageSizeIfNecessary() {
// |IOSSurface|
std::unique_ptr<Surface> IOSSurfaceMetalImpeller::CreateGPUSurface() {
impeller_context_->UpdateOffscreenLayerPixelFormat(
impeller::FromMTLPixelFormat(layer_.pixelFormat));
// Convert alpha-less onscreen format to alpha including format.
impeller::PixelFormat pixel_format = impeller::FromMTLPixelFormat(layer_.pixelFormat);
if (pixel_format == impeller::PixelFormat::kB10G10R10XR) {
pixel_format = impeller::PixelFormat::kB10G10R10A10XR;
}
impeller_context_->UpdateOffscreenLayerPixelFormat(pixel_format);
return std::make_unique<GPUSurfaceMetalImpeller>(this, //
aiks_context_ //
);