impeller::ISize::MipCount (in impeller/geometry/size.h) computes the maximum mip chain length as floor(log2(min(width, height))) clamped at 1. The canonical formula used by Vulkan, Metal, OpenGL, D3D, and WebGPU is floor(log2(max(width, height))) + 1.
The current Impeller formula differs in two ways:
- It uses the smaller dimension (
min) instead of the larger (max).
- It returns one less level than the canonical count (no
+ 1).
Why this matters
For a 1024×1 texture, the canonical mip chain length is 11. Impeller's formula returns 1, meaning Allocator::CreateTexture silently allocates fewer mip levels than the underlying API would otherwise support. Subsequent writes to higher mip levels then fail at the Impeller boundary.
This came up while landing the Flutter GPU mipLevelCount work in #185890. The Dart-side Texture.fullMipCount helper was originally written to match the canonical formula, which caused tests to fail because the Impeller allocator silently capped at the smaller value. The Dart helper was then updated to mirror Impeller's actual behavior to avoid the silent-undercount footgun, and a doc comment now calls out the discrepancy.
Proposed fix
- Update
ISize::MipCount to floor(log2(max(width, height))) + 1, clamped at 1 for empty/zero inputs.
- Audit callers for any places that depended on the smaller-dimension or off-by-one behavior, particularly the texture-allocation paths in each backend.
- Once Impeller is updated, restore the canonical formula in
Texture.fullMipCount on the Dart side and update the doc comment.
Repro
Impeller:
ISize{1024, 1}.MipCount(); // returns 1
// Canonical answer: 11
Flutter GPU:
gpu.gpuContext.createTexture(StorageMode.hostVisible, 1024, 1, mipLevelCount: 11);
// Today: throws because the Dart-side `fullMipCount` returns 1, matching
// Impeller's allocator cap. With a corrected `ISize::MipCount`, this would
// allocate the expected chain.
Low priority IMO. Flutter GPU's Dart API works correctly given Impeller's current behavior. This is about lifting an artificial restriction on what mip chains the engine can allocate.
impeller::ISize::MipCount(inimpeller/geometry/size.h) computes the maximum mip chain length asfloor(log2(min(width, height)))clamped at 1. The canonical formula used by Vulkan, Metal, OpenGL, D3D, and WebGPU isfloor(log2(max(width, height))) + 1.The current Impeller formula differs in two ways:
min) instead of the larger (max).+ 1).Why this matters
For a 1024×1 texture, the canonical mip chain length is 11. Impeller's formula returns 1, meaning
Allocator::CreateTexturesilently allocates fewer mip levels than the underlying API would otherwise support. Subsequent writes to higher mip levels then fail at the Impeller boundary.This came up while landing the Flutter GPU
mipLevelCountwork in #185890. The Dart-sideTexture.fullMipCounthelper was originally written to match the canonical formula, which caused tests to fail because the Impeller allocator silently capped at the smaller value. The Dart helper was then updated to mirror Impeller's actual behavior to avoid the silent-undercount footgun, and a doc comment now calls out the discrepancy.Proposed fix
ISize::MipCounttofloor(log2(max(width, height))) + 1, clamped at 1 for empty/zero inputs.Texture.fullMipCounton the Dart side and update the doc comment.Repro
Impeller:
ISize{1024, 1}.MipCount(); // returns 1 // Canonical answer: 11Flutter GPU:
Low priority IMO. Flutter GPU's Dart API works correctly given Impeller's current behavior. This is about lifting an artificial restriction on what mip chains the engine can allocate.