[Flutter GPU] Allow attaching specific texture mip levels and slices for rendering (#187685)
Fixes flutter/flutter#150455
Surfaces the Impeller HAL support for rendering into a specific texture
mip level and slice (landed in #187470) to Flutter GPU.
- Adds `mipLevel` and `slice` (both default 0) to `ColorAttachment` and
`DepthStencilAttachment`. They select the subresource of the attachment
texture to render into: a non-zero mip level, a cube map face, or both.
- Validates, in Dart and unconditionally, that each attachment's
`mipLevel` and `slice` are in range (including the MSAA resolve texture)
and that all attachments resolve to the same size. Out-of-range
subresources and size mismatches are undefined behavior in release,
where the engine-side checks are compiled out.
- Adds `GpuContext.doesSupportFramebufferRenderMipmap`, lifted onto the
base `Capabilities` interface so it can be queried. Rendering into a
cube face is always available. Rendering into a non-zero mip level
returns true on Metal and Vulkan and false on GLES: the GLES texture
storage path yields an incomplete framebuffer for a non-base mip
attachment, so the capability is conservative until that is reworked
(tracked separately). Cube face rendering on GLES is unaffected.
Tests cover rendering into a cube slice, rendering into a non-zero mip
level (gated on the capability), and the range and size validation.
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.cc b/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.cc
index 5c0f467..200f6d9 100644
--- a/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.cc
+++ b/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.cc
@@ -51,9 +51,6 @@
static const constexpr char* kTextureCompressionAstcHdrExt =
"GL_KHR_texture_compression_astc_hdr";
-// https://registry.khronos.org/OpenGL/extensions/OES/OES_fbo_render_mipmap.txt
-static const constexpr char* kFboRenderMipmapExt = "GL_OES_fbo_render_mipmap";
-
CapabilitiesGLES::CapabilitiesGLES(const ProcTableGLES& gl) {
{
GLint value = 0;
@@ -193,12 +190,6 @@
desc->HasExtension(kTextureCompressionAstcOesExt);
supports_texture_compression_etc2_ =
desc->IsES() && desc->GetGlVersion().major_version >= 3;
-
- // Non-zero mip levels are renderable on desktop GL, ES 3.0+, or ES 2.0 with
- // GL_OES_fbo_render_mipmap.
- supports_fbo_render_mipmap_ = !desc->IsES() ||
- desc->GetGlVersion().major_version >= 3 ||
- desc->HasExtension(kFboRenderMipmapExt);
}
bool CapabilitiesGLES::IsES() const {
@@ -206,7 +197,13 @@
}
bool CapabilitiesGLES::SupportsFramebufferRenderMipmap() const {
- return supports_fbo_render_mipmap_;
+ // Rendering into a non-zero mip level is not yet supported on the GLES
+ // backend. The texture storage path allocates levels with mutable, lazily
+ // allocated glTexImage2D storage, which yields an incomplete framebuffer
+ // when a non-base mip level is attached. Until that is reworked, do not
+ // advertise the capability so callers fall back instead of failing to
+ // create the framebuffer. Rendering into a cube map face is unaffected.
+ return false;
}
size_t CapabilitiesGLES::GetMaxTextureUnits(ShaderStage stage) const {
diff --git a/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.h b/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.h
index 21c0036..28d5fab 100644
--- a/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.h
+++ b/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.h
@@ -80,10 +80,10 @@
/// @brief Whether this is an ES GL variant or (if false) desktop GL.
bool IsES() const;
- /// @brief Whether a non-zero mip level of a texture can be attached to a
- /// framebuffer. Core ES 2.0 only allows mip level 0; ES 3.0+ and the
- /// GL_OES_fbo_render_mipmap extension lift that restriction.
- bool SupportsFramebufferRenderMipmap() const;
+ // |Capabilities|
+ /// Always false. Rendering into a non-zero mip level is not yet implemented
+ /// on the GLES backend; see SupportsFramebufferRenderMipmap in the .cc file.
+ bool SupportsFramebufferRenderMipmap() const override;
// |Capabilities|
bool SupportsOffscreenMSAA() const override;
@@ -159,7 +159,6 @@
bool supports_offscreen_msaa_ = false;
bool supports_implicit_msaa_ = false;
bool supports_32bit_primitive_indices_ = false;
- bool supports_fbo_render_mipmap_ = false;
bool is_angle_ = false;
bool is_es_ = false;
bool supports_texture_compression_bc_ = false;
diff --git a/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.cc b/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.cc
index e1cb178..4c00888 100644
--- a/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.cc
+++ b/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.cc
@@ -696,8 +696,8 @@
const auto& gl = reactor_->GetProcTable();
if (mip_level > 0 &&
!gl.GetCapabilities()->SupportsFramebufferRenderMipmap()) {
- VALIDATION_LOG << "Attaching a non-zero mip level requires OpenGL ES 3.0 "
- "or the GL_OES_fbo_render_mipmap extension.";
+ VALIDATION_LOG << "Rendering into a non-zero mip level is not supported on "
+ "the GLES backend.";
return false;
}
if (!EnsureSliceMipLevelStorage(slice, mip_level)) {
diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc
index 17716a3..191fc8c 100644
--- a/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc
+++ b/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc
@@ -881,6 +881,10 @@
return false;
}
+bool CapabilitiesVK::SupportsFramebufferRenderMipmap() const {
+ return true;
+}
+
bool CapabilitiesVK::SupportsTextureCompression(
CompressedTextureFamily family) const {
switch (family) {
diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.h b/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.h
index ece2f51..223fe2b 100644
--- a/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.h
+++ b/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.h
@@ -280,6 +280,9 @@
CompressedTextureFamily family) const override;
// |Capabilities|
+ bool SupportsFramebufferRenderMipmap() const override;
+
+ // |Capabilities|
PixelFormat GetDefaultColorFormat() const override;
// |Capabilities|
diff --git a/engine/src/flutter/impeller/renderer/capabilities.cc b/engine/src/flutter/impeller/renderer/capabilities.cc
index 7442349..1851d80 100644
--- a/engine/src/flutter/impeller/renderer/capabilities.cc
+++ b/engine/src/flutter/impeller/renderer/capabilities.cc
@@ -104,6 +104,9 @@
}
// |Capabilities|
+ bool SupportsFramebufferRenderMipmap() const override { return true; }
+
+ // |Capabilities|
bool SupportsTextureCompression(
CompressedTextureFamily family) const override {
switch (family) {
diff --git a/engine/src/flutter/impeller/renderer/capabilities.h b/engine/src/flutter/impeller/renderer/capabilities.h
index f7d1ecb..3a89fe4 100644
--- a/engine/src/flutter/impeller/renderer/capabilities.h
+++ b/engine/src/flutter/impeller/renderer/capabilities.h
@@ -134,6 +134,12 @@
virtual bool SupportsTextureCompression(
CompressedTextureFamily family) const = 0;
+ /// @brief Whether a non-zero mip level of a texture can be attached as a
+ /// render target. Rendering into a cube map face or array layer is
+ /// always supported. Metal and Vulkan support this; the GLES backend
+ /// does not yet, so it returns false there.
+ virtual bool SupportsFramebufferRenderMipmap() const = 0;
+
/// @brief The minimum alignment of uniform value offsets in bytes.
virtual size_t GetMinimumUniformAlignment() const = 0;
diff --git a/engine/src/flutter/impeller/renderer/testing/mocks.h b/engine/src/flutter/impeller/renderer/testing/mocks.h
index c753bf5..32ad994 100644
--- a/engine/src/flutter/impeller/renderer/testing/mocks.h
+++ b/engine/src/flutter/impeller/renderer/testing/mocks.h
@@ -252,6 +252,7 @@
MOCK_METHOD(bool, SupportsPrimitiveRestart, (), (const override));
MOCK_METHOD(bool, Supports32BitPrimitiveIndices, (), (const override));
MOCK_METHOD(bool, SupportsExtendedRangeFormats, (), (const override));
+ MOCK_METHOD(bool, SupportsFramebufferRenderMipmap, (), (const override));
MOCK_METHOD(bool,
SupportsTextureCompression,
(CompressedTextureFamily),
diff --git a/engine/src/flutter/lib/gpu/context.cc b/engine/src/flutter/lib/gpu/context.cc
index 7a6e81f..5945187 100644
--- a/engine/src/flutter/lib/gpu/context.cc
+++ b/engine/src/flutter/lib/gpu/context.cc
@@ -140,6 +140,13 @@
return flutter::gpu::SupportsNormalOffscreenMSAA(wrapper->GetContext());
}
+extern bool InternalFlutterGpu_Context_GetSupportsFramebufferRenderMipmap(
+ flutter::gpu::Context* wrapper) {
+ return wrapper->GetContext()
+ .GetCapabilities()
+ ->SupportsFramebufferRenderMipmap();
+}
+
extern bool InternalFlutterGpu_Context_SupportsTextureCompression(
flutter::gpu::Context* wrapper,
int family) {
diff --git a/engine/src/flutter/lib/gpu/context.h b/engine/src/flutter/lib/gpu/context.h
index 56672a00..b6693dc 100644
--- a/engine/src/flutter/lib/gpu/context.h
+++ b/engine/src/flutter/lib/gpu/context.h
@@ -83,6 +83,10 @@
flutter::gpu::Context* wrapper);
FLUTTER_GPU_EXPORT
+extern bool InternalFlutterGpu_Context_GetSupportsFramebufferRenderMipmap(
+ flutter::gpu::Context* wrapper);
+
+FLUTTER_GPU_EXPORT
extern bool InternalFlutterGpu_Context_SupportsTextureCompression(
flutter::gpu::Context* wrapper,
int family);
diff --git a/engine/src/flutter/lib/gpu/lib/src/context.dart b/engine/src/flutter/lib/gpu/lib/src/context.dart
index dd67e48..5273e13 100644
--- a/engine/src/flutter/lib/gpu/lib/src/context.dart
+++ b/engine/src/flutter/lib/gpu/lib/src/context.dart
@@ -58,6 +58,15 @@
return _getSupportsOffscreenMSAA();
}
+ /// Whether the backend can attach a non-zero mip level of a texture as a
+ /// render target (see [ColorAttachment.mipLevel]). Rendering into a cube map
+ /// face or array layer is always supported; only non-zero mip levels are
+ /// gated. True on Metal and Vulkan; currently false on the GLES backend,
+ /// where rendering into non-zero mip levels is not yet implemented.
+ bool get doesSupportFramebufferRenderMipmap {
+ return _getSupportsFramebufferRenderMipmap();
+ }
+
/// Whether this device supports the given family of block-compressed
/// texture formats. Hardware support is granted on a per-family basis.
///
@@ -263,6 +272,11 @@
)
external bool _getSupportsOffscreenMSAA();
+ @Native<Bool Function(Pointer<Void>)>(
+ symbol: 'InternalFlutterGpu_Context_GetSupportsFramebufferRenderMipmap',
+ )
+ external bool _getSupportsFramebufferRenderMipmap();
+
@Native<Bool Function(Pointer<Void>, Int)>(
symbol: 'InternalFlutterGpu_Context_SupportsTextureCompression',
)
diff --git a/engine/src/flutter/lib/gpu/lib/src/render_pass.dart b/engine/src/flutter/lib/gpu/lib/src/render_pass.dart
index b9d6db4..4ade61c 100644
--- a/engine/src/flutter/lib/gpu/lib/src/render_pass.dart
+++ b/engine/src/flutter/lib/gpu/lib/src/render_pass.dart
@@ -13,6 +13,8 @@
vm.Vector4? clearValue = null,
required this.texture,
this.resolveTexture = null,
+ this.mipLevel = 0,
+ this.slice = 0,
}) : clearValue = clearValue ?? vm.Vector4.zero();
LoadAction loadAction;
@@ -22,6 +24,19 @@
Texture texture;
Texture? resolveTexture;
+ /// The mip level of [texture] to render into. Must be in the range
+ /// `[0, texture.mipLevelCount)`.
+ ///
+ /// Rendering into a non-zero mip level is supported on Metal and Vulkan but
+ /// not currently on the GLES backend. See
+ /// [GpuContext.doesSupportFramebufferRenderMipmap].
+ int mipLevel;
+
+ /// The slice of [texture] to render into. For cubemap textures this selects
+ /// the face in the order `+X, -X, +Y, -Y, +Z, -Z`. Must be in the range
+ /// `[0, texture.sliceCount)` (always 0 for non-cubemap textures).
+ int slice;
+
void _validate() {
if (resolveTexture != null) {
if (resolveTexture!.format != texture.format) {
@@ -69,6 +84,8 @@
this.stencilStoreAction = StoreAction.dontCare,
this.stencilClearValue = 0,
required this.texture,
+ this.mipLevel = 0,
+ this.slice = 0,
});
LoadAction depthLoadAction;
@@ -81,6 +98,14 @@
Texture texture;
+ /// The mip level of [texture] to render into. Must match the mip level of
+ /// the color attachments so all attachments share the same size. See
+ /// [ColorAttachment.mipLevel].
+ int mipLevel;
+
+ /// The slice of [texture] to render into. See [ColorAttachment.slice].
+ int slice;
+
void _validate() {
if (texture.storageMode == StorageMode.deviceTransient) {
if (depthLoadAction == LoadAction.load) {
@@ -219,10 +244,84 @@
}
}
+ /// Validates attachment mip levels, slices, and sizes. Out-of-range
+ /// subresources and mismatched sizes are undefined behavior in the engine,
+ /// and the engine-side checks are compiled out in release builds, so this
+ /// runs unconditionally.
+ void _validateAttachments() {
+ // The size of the first attachment, against which the rest are checked.
+ // Each attachment renders into its mip level, clamped to a minimum of 1x1.
+ int? width;
+ int? height;
+ void accumulate(Texture texture, int mipLevel) {
+ final int w = texture.width >> mipLevel;
+ final int h = texture.height >> mipLevel;
+ final int mipWidth = w < 1 ? 1 : w;
+ final int mipHeight = h < 1 ? 1 : h;
+ if (width == null) {
+ width = mipWidth;
+ height = mipHeight;
+ } else if (width != mipWidth || height != mipHeight) {
+ throw Exception(
+ "All render target attachments must render into the same size. "
+ "Check that all color and depth-stencil attachments use matching "
+ "texture sizes and mip levels.",
+ );
+ }
+ }
+
+ for (final color in colorAttachments) {
+ _validateAttachmentSubresource(
+ color.texture,
+ color.mipLevel,
+ color.slice,
+ "ColorAttachment",
+ );
+ if (color.resolveTexture != null) {
+ _validateAttachmentSubresource(
+ color.resolveTexture!,
+ color.mipLevel,
+ color.slice,
+ "ColorAttachment resolve texture",
+ );
+ }
+ accumulate(color.texture, color.mipLevel);
+ }
+
+ final ds = depthStencilAttachment;
+ if (ds != null) {
+ _validateAttachmentSubresource(
+ ds.texture,
+ ds.mipLevel,
+ ds.slice,
+ "DepthStencilAttachment",
+ );
+ accumulate(ds.texture, ds.mipLevel);
+ }
+ }
+
final List<ColorAttachment> colorAttachments;
final DepthStencilAttachment? depthStencilAttachment;
}
+void _validateAttachmentSubresource(
+ Texture texture,
+ int mipLevel,
+ int slice,
+ String label,
+) {
+ if (mipLevel < 0 || mipLevel >= texture.mipLevelCount) {
+ throw Exception(
+ "$label mipLevel ($mipLevel) must be in the range [0, ${texture.mipLevelCount - 1}] for this texture",
+ );
+ }
+ if (slice < 0 || slice >= texture.sliceCount) {
+ throw Exception(
+ "$label slice ($slice) must be in the range [0, ${texture.sliceCount - 1}] for textures of type ${texture.textureType}",
+ );
+ }
+}
+
base class RenderPass extends NativeFieldWrapperClass1 {
/// The maximum number of vertex buffer slots that can be bound to a single
/// draw. Matches `flutter::gpu::RenderPass::kMaxVertexBufferSlots` on the
@@ -246,6 +345,7 @@
CommandBuffer commandBuffer,
RenderTarget renderTarget,
) {
+ renderTarget._validateAttachments();
assert(() {
renderTarget._validate();
return true;
@@ -265,6 +365,8 @@
color.clearValue.a,
color.texture,
color.resolveTexture,
+ color.mipLevel,
+ color.slice,
);
if (error != null) {
throw Exception(error);
@@ -280,6 +382,8 @@
ds.stencilStoreAction.index,
ds.stencilClearValue,
ds.texture,
+ ds.mipLevel,
+ ds.slice,
);
if (error != null) {
throw Exception(error);
@@ -593,6 +697,8 @@
Float,
Pointer<Void>,
Handle,
+ Int,
+ Int,
)
>(symbol: 'InternalFlutterGpu_RenderPass_SetColorAttachment')
external String? _setColorAttachment(
@@ -606,6 +712,8 @@
double clearColorA,
Texture texture,
Texture? resolveTexture,
+ int mipLevel,
+ int slice,
);
@Native<
@@ -618,6 +726,8 @@
Int,
Int,
Pointer<Void>,
+ Int,
+ Int,
)
>(symbol: 'InternalFlutterGpu_RenderPass_SetDepthStencilAttachment')
external String? _setDepthStencilAttachment(
@@ -628,6 +738,8 @@
int stencilStoreAction,
int stencilClearValue,
Texture texture,
+ int mipLevel,
+ int slice,
);
@Native<Handle Function(Pointer<Void>, Pointer<Void>)>(
diff --git a/engine/src/flutter/lib/gpu/lib/src/texture.dart b/engine/src/flutter/lib/gpu/lib/src/texture.dart
index 5903621..06ff2f4 100644
--- a/engine/src/flutter/lib/gpu/lib/src/texture.dart
+++ b/engine/src/flutter/lib/gpu/lib/src/texture.dart
@@ -154,13 +154,13 @@
void overwrite(ByteData sourceBytes, {int mipLevel = 0, int slice = 0}) {
if (mipLevel < 0 || mipLevel >= mipLevelCount) {
throw Exception(
- 'mipLevel ($mipLevel) must be in the range [0, $mipLevelCount) for this texture',
+ 'mipLevel ($mipLevel) must be in the range [0, ${mipLevelCount - 1}] for this texture',
);
}
final int slices = sliceCount;
if (slice < 0 || slice >= slices) {
throw Exception(
- 'slice ($slice) must be in the range [0, $slices) for textures of type $textureType',
+ 'slice ($slice) must be in the range [0, ${slices - 1}] for textures of type $textureType',
);
}
final int expectedSize = getMipLevelSizeInBytes(mipLevel);
diff --git a/engine/src/flutter/lib/gpu/render_pass.cc b/engine/src/flutter/lib/gpu/render_pass.cc
index 643e219..c355a58 100644
--- a/engine/src/flutter/lib/gpu/render_pass.cc
+++ b/engine/src/flutter/lib/gpu/render_pass.cc
@@ -275,13 +275,17 @@
float clear_color_b,
float clear_color_a,
flutter::gpu::Texture* texture,
- Dart_Handle resolve_texture_wrapper) {
+ Dart_Handle resolve_texture_wrapper,
+ int mip_level,
+ int slice) {
impeller::ColorAttachment desc;
desc.load_action = flutter::gpu::ToImpellerLoadAction(load_action);
desc.store_action = flutter::gpu::ToImpellerStoreAction(store_action);
desc.clear_color = impeller::Color(clear_color_r, clear_color_g,
clear_color_b, clear_color_a);
desc.texture = texture->GetTexture();
+ desc.mip_level = mip_level;
+ desc.slice = slice;
if (!Dart_IsNull(resolve_texture_wrapper)) {
flutter::gpu::Texture* resolve_texture =
tonic::DartConverter<flutter::gpu::Texture*>::FromDart(
@@ -308,13 +312,17 @@
int stencil_load_action,
int stencil_store_action,
int stencil_clear_value,
- flutter::gpu::Texture* texture) {
+ flutter::gpu::Texture* texture,
+ int mip_level,
+ int slice) {
{
impeller::DepthAttachment desc;
desc.load_action = flutter::gpu::ToImpellerLoadAction(depth_load_action);
desc.store_action = flutter::gpu::ToImpellerStoreAction(depth_store_action);
desc.clear_depth = depth_clear_value;
desc.texture = texture->GetTexture();
+ desc.mip_level = mip_level;
+ desc.slice = slice;
wrapper->GetRenderTarget().SetDepthAttachment(desc);
}
{
@@ -324,6 +332,8 @@
flutter::gpu::ToImpellerStoreAction(stencil_store_action);
desc.clear_stencil = stencil_clear_value;
desc.texture = texture->GetTexture();
+ desc.mip_level = mip_level;
+ desc.slice = slice;
wrapper->GetRenderTarget().SetStencilAttachment(desc);
}
diff --git a/engine/src/flutter/lib/gpu/render_pass.h b/engine/src/flutter/lib/gpu/render_pass.h
index a0b4c55..4a80aa6 100644
--- a/engine/src/flutter/lib/gpu/render_pass.h
+++ b/engine/src/flutter/lib/gpu/render_pass.h
@@ -142,7 +142,9 @@
float clear_color_b,
float clear_color_a,
flutter::gpu::Texture* texture,
- Dart_Handle resolve_texture_wrapper);
+ Dart_Handle resolve_texture_wrapper,
+ int mip_level,
+ int slice);
FLUTTER_GPU_EXPORT
extern Dart_Handle InternalFlutterGpu_RenderPass_SetDepthStencilAttachment(
@@ -153,7 +155,9 @@
int stencil_load_action,
int stencil_store_action,
int stencil_clear_value,
- flutter::gpu::Texture* texture);
+ flutter::gpu::Texture* texture,
+ int mip_level,
+ int slice);
FLUTTER_GPU_EXPORT
extern Dart_Handle InternalFlutterGpu_RenderPass_Begin(
diff --git a/engine/src/flutter/testing/dart/gpu_test.dart b/engine/src/flutter/testing/dart/gpu_test.dart
index 94053c9..8a57c95 100644
--- a/engine/src/flutter/testing/dart/gpu_test.dart
+++ b/engine/src/flutter/testing/dart/gpu_test.dart
@@ -676,7 +676,7 @@
texture.overwrite(Int32List.fromList(<int>[red.value]).buffer.asByteData(), mipLevel: 2);
fail('Exception not thrown for out-of-range mipLevel.');
} catch (e) {
- expect(e.toString(), contains('mipLevel (2) must be in the range [0, 2)'));
+ expect(e.toString(), contains('mipLevel (2) must be in the range [0, 1]'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
@@ -690,7 +690,7 @@
);
fail('Exception not thrown for out-of-range slice.');
} catch (e) {
- expect(e.toString(), contains('slice (1) must be in the range [0, 1)'));
+ expect(e.toString(), contains('slice (1) must be in the range [0, 0]'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
@@ -850,6 +850,110 @@
await comparer.addGoldenImage(image, 'flutter_gpu_test_clear_color.png');
}, skip: !(impellerEnabled && flutterGpuEnabled));
+ test('GpuContext.doesSupportFramebufferRenderMipmap returns a bool', () async {
+ expect(gpu.gpuContext.doesSupportFramebufferRenderMipmap, isA<bool>());
+ }, skip: !(impellerEnabled && flutterGpuEnabled));
+
+ test('Can render into a cube map slice', () async {
+ final gpu.Texture texture = gpu.gpuContext.createTexture(
+ gpu.StorageMode.devicePrivate,
+ 4,
+ 4,
+ textureType: gpu.TextureType.textureCube,
+ );
+ expect(texture.sliceCount, 6);
+
+ final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
+ final renderTarget = gpu.RenderTarget.singleColor(
+ gpu.ColorAttachment(texture: texture, slice: 2, clearValue: Colors.lime),
+ );
+ commandBuffer.createRenderPass(renderTarget);
+ commandBuffer.submit();
+ }, skip: !(impellerEnabled && flutterGpuEnabled));
+
+ test('Can render into a non-zero mip level', () async {
+ // Rendering into a non-zero mip level needs ES 3.0 or
+ // GL_OES_fbo_render_mipmap on the GLES backend.
+ if (!gpu.gpuContext.doesSupportFramebufferRenderMipmap) {
+ markTestSkipped('Backend does not support rendering into non-zero mip levels.');
+ return;
+ }
+ final gpu.Texture texture = gpu.gpuContext.createTexture(
+ gpu.StorageMode.devicePrivate,
+ 8,
+ 8,
+ mipLevelCount: 3,
+ );
+
+ final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
+ final renderTarget = gpu.RenderTarget.singleColor(
+ gpu.ColorAttachment(texture: texture, mipLevel: 1, clearValue: Colors.lime),
+ );
+ commandBuffer.createRenderPass(renderTarget);
+ commandBuffer.submit();
+ }, skip: !(impellerEnabled && flutterGpuEnabled));
+
+ test('ColorAttachment throws for an out-of-range mipLevel', () async {
+ final gpu.Texture texture = gpu.gpuContext.createTexture(
+ gpu.StorageMode.devicePrivate,
+ 4,
+ 4,
+ mipLevelCount: 2,
+ );
+ final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
+ final renderTarget = gpu.RenderTarget.singleColor(
+ gpu.ColorAttachment(texture: texture, mipLevel: 2),
+ );
+ try {
+ commandBuffer.createRenderPass(renderTarget);
+ fail('Exception not thrown for out-of-range mipLevel.');
+ } catch (e) {
+ expect(e.toString(), contains('mipLevel (2) must be in the range [0, 1]'));
+ }
+ }, skip: !(impellerEnabled && flutterGpuEnabled));
+
+ test('ColorAttachment throws for an out-of-range slice', () async {
+ final gpu.Texture texture = gpu.gpuContext.createTexture(gpu.StorageMode.devicePrivate, 4, 4);
+ final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
+ final renderTarget = gpu.RenderTarget.singleColor(
+ gpu.ColorAttachment(texture: texture, slice: 1),
+ );
+ try {
+ commandBuffer.createRenderPass(renderTarget);
+ fail('Exception not thrown for out-of-range slice.');
+ } catch (e) {
+ expect(e.toString(), contains('slice (1) must be in the range [0, 0]'));
+ }
+ }, skip: !(impellerEnabled && flutterGpuEnabled));
+
+ test('RenderTarget throws when attachment sizes do not match', () async {
+ // The color attachment renders into mip 1 (4x4) while the depth-stencil
+ // attachment renders into mip 0 (8x8).
+ final gpu.Texture color = gpu.gpuContext.createTexture(
+ gpu.StorageMode.devicePrivate,
+ 8,
+ 8,
+ mipLevelCount: 2,
+ );
+ final gpu.Texture depthStencil = gpu.gpuContext.createTexture(
+ gpu.StorageMode.deviceTransient,
+ 8,
+ 8,
+ format: gpu.gpuContext.defaultDepthStencilFormat,
+ );
+ final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
+ final renderTarget = gpu.RenderTarget.singleColor(
+ gpu.ColorAttachment(texture: color, mipLevel: 1),
+ depthStencilAttachment: gpu.DepthStencilAttachment(texture: depthStencil),
+ );
+ try {
+ commandBuffer.createRenderPass(renderTarget);
+ fail('Exception not thrown for mismatched attachment sizes.');
+ } catch (e) {
+ expect(e.toString(), contains('must render into the same size'));
+ }
+ }, skip: !(impellerEnabled && flutterGpuEnabled));
+
// Regression test for https://github.com/flutter/flutter/issues/157324
test('Can bind uniforms in range', () async {
final RenderPassState state = createSimpleRenderPass();