diff --git a/engine/src/flutter/impeller/fixtures/flutter_gpu_array_texture.frag b/engine/src/flutter/impeller/fixtures/flutter_gpu_array_texture.frag new file mode 100644 index 0000000000000..7144025aefbb9 --- /dev/null +++ b/engine/src/flutter/impeller/fixtures/flutter_gpu_array_texture.frag @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +uniform sampler2DArray tex; + +uniform FragInfo { + float layer; +} +frag_info; + +in vec2 v_texture_coords; +in vec4 v_color; +out vec4 frag_color; + +void main() { + frag_color = v_color * texture(tex, vec3(v_texture_coords, frag_info.layer)); +} diff --git a/engine/src/flutter/impeller/renderer/backend/gles/blit_command_gles.cc b/engine/src/flutter/impeller/renderer/backend/gles/blit_command_gles.cc index 92e20bfb49294..236da01244062 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/blit_command_gles.cc +++ b/engine/src/flutter/impeller/renderer/backend/gles/blit_command_gles.cc @@ -182,11 +182,17 @@ bool BlitCopyBufferToTextureCommandGLES::Encode( texture_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + slice; break; case TextureType::kTexture2DArray: - // TODO(bdero): Upload into 2D array layers via glTexSubImage3D from the - // blit path. Direct uploads via Texture::SetContents are supported. - VALIDATION_LOG << "Blitting into a 2D array texture is not yet supported " - "on the OpenGLES backend."; - return false; + // The Flutter GPU Dart API gates array-texture creation on + // GpuContext.doesSupportTextureArrays, so this is a backstop for + // contexts without array support (e.g. OpenGL ES 2.0). + if (!reactor.GetProcTable().GetCapabilities()->SupportsTextureArrays()) { + VALIDATION_LOG + << "2D array textures are not supported on this context."; + return false; + } + texture_type = GL_TEXTURE_2D_ARRAY; + texture_target = GL_TEXTURE_2D_ARRAY; + break; case TextureType::kTextureExternalOES: texture_type = GL_TEXTURE_EXTERNAL_OES; texture_target = GL_TEXTURE_EXTERNAL_OES; @@ -224,6 +230,14 @@ bool BlitCopyBufferToTextureCommandGLES::Encode( // sub-image; glCompressedTexImage2D redefines the entire mip level. Require // the upload to cover the full mip level starting at the origin. if (gles_format->is_compressed) { + if (tex_descriptor.type == TextureType::kTexture2DArray) { + // TODO(bdero): Support compressed 2D array uploads. The level must be + // allocated with glCompressedTexImage3D covering every layer and filled + // per layer with glCompressedTexSubImage3D. + VALIDATION_LOG << "Compressed 2D array textures are not yet supported " + "on the OpenGLES backend."; + return false; + } const auto mip_width = std::max(1, tex_descriptor.size.width >> mip_level); const auto mip_height = @@ -248,6 +262,45 @@ bool BlitCopyBufferToTextureCommandGLES::Encode( return true; } + if (tex_descriptor.type == TextureType::kTexture2DArray) { + // glTexImage3D allocates this mip level for every layer at once, so the + // level's storage is tracked with a single entry (slice 0). + if (!texture_gles.IsSliceMipLevelInitialized(0, mip_level)) { + const auto level_width = + std::max(1, tex_descriptor.size.width >> mip_level); + const auto level_height = + std::max(1, tex_descriptor.size.height >> mip_level); + gl.TexImage3D( + /*target=*/texture_target, // + /*level=*/static_cast(mip_level), // + /*internal_format=*/gles_format->internal_format, // + /*width=*/level_width, // + /*height=*/level_height, // + /*depth=*/ + static_cast(tex_descriptor.array_layer_count), // + /*border=*/0u, // + /*format=*/gles_format->external_format, // + /*type=*/gles_format->type, // + /*data=*/nullptr // + ); + texture_gles.MarkSliceMipLevelInitialized(0, mip_level); + } + gl.PixelStorei(GL_UNPACK_ALIGNMENT, 1); + gl.TexSubImage3D( + /*target=*/texture_target, // + /*level=*/static_cast(mip_level), // + /*xoffset=*/destination_region.GetX(), // + /*yoffset=*/destination_region.GetY(), // + /*zoffset=*/static_cast(slice), // + /*width=*/destination_region.GetWidth(), // + /*height=*/destination_region.GetHeight(), // + /*depth=*/1, // + /*format=*/gles_format->external_format, // + /*type=*/gles_format->type, // + /*data=*/tex_data); // + return true; + } + // GL_INVALID_OPERATION if the requested mip level has not been defined by // a previous glTexImage2D operation. Allocate the requested mip lazily on // first write, only for the level the upload is actually targeting. The 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 db4df2a33e915..3aadc50460bab 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.cc +++ b/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.cc @@ -288,7 +288,7 @@ bool CapabilitiesGLES::SupportsTextureMaxLevel() const { return supports_texture_max_level_; } -bool CapabilitiesGLES::SupportsTextureArray() const { +bool CapabilitiesGLES::SupportsTextureArrays() const { return supports_texture_array_; } 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 fa7b22af6380c..5cfb7fc82fb5a 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.h +++ b/engine/src/flutter/impeller/renderer/backend/gles/capabilities_gles.h @@ -100,7 +100,8 @@ class CapabilitiesGLES final /// available below them through GL_EXT_texture_array (desktop GL 2.x) /// or GL_NV_texture_array (OpenGL ES 2.0). When absent, callers must /// fall back to a texture atlas. - bool SupportsTextureArray() const; + // |Capabilities| + bool SupportsTextureArrays() const override; // |Capabilities| bool SupportsOffscreenMSAA() const override; diff --git a/engine/src/flutter/impeller/renderer/backend/gles/test/capabilities_unittests.cc b/engine/src/flutter/impeller/renderer/backend/gles/test/capabilities_unittests.cc index edb515240abd0..9c94103c935d5 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/test/capabilities_unittests.cc +++ b/engine/src/flutter/impeller/renderer/backend/gles/test/capabilities_unittests.cc @@ -93,14 +93,14 @@ TEST(CapabilitiesGLES, SupportsTextureArrayOnES3) { // 2D array textures are core on OpenGL ES 3.0, no extension needed. auto mock_gles = MockGLES::Init(std::nullopt, "OpenGL ES 3.0"); auto capabilities = mock_gles->GetProcTable().GetCapabilities(); - EXPECT_TRUE(capabilities->SupportsTextureArray()); + EXPECT_TRUE(capabilities->SupportsTextureArrays()); } TEST(CapabilitiesGLES, DoesNotSupportTextureArrayOnES2WithoutExtension) { auto const extensions = std::vector{"GL_KHR_debug"}; auto mock_gles = MockGLES::Init(extensions, "OpenGL ES 2.0"); auto capabilities = mock_gles->GetProcTable().GetCapabilities(); - EXPECT_FALSE(capabilities->SupportsTextureArray()); + EXPECT_FALSE(capabilities->SupportsTextureArrays()); } TEST(CapabilitiesGLES, SupportsTextureArrayViaNVExtensionOnES2) { @@ -112,7 +112,7 @@ TEST(CapabilitiesGLES, SupportsTextureArrayViaNVExtensionOnES2) { }; auto mock_gles = MockGLES::Init(extensions, "OpenGL ES 2.0"); auto capabilities = mock_gles->GetProcTable().GetCapabilities(); - EXPECT_TRUE(capabilities->SupportsTextureArray()); + EXPECT_TRUE(capabilities->SupportsTextureArrays()); } TEST(CapabilitiesGLES, SupportsTextureArrayViaEXTExtension) { @@ -124,7 +124,7 @@ TEST(CapabilitiesGLES, SupportsTextureArrayViaEXTExtension) { }; auto mock_gles = MockGLES::Init(extensions, "OpenGL ES 2.0"); auto capabilities = mock_gles->GetProcTable().GetCapabilities(); - EXPECT_TRUE(capabilities->SupportsTextureArray()); + EXPECT_TRUE(capabilities->SupportsTextureArrays()); } } // namespace testing diff --git a/engine/src/flutter/impeller/renderer/backend/gles/test/texture_gles_unittests.cc b/engine/src/flutter/impeller/renderer/backend/gles/test/texture_gles_unittests.cc index 06adbe9f032c4..745ccd1cd23a6 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/test/texture_gles_unittests.cc +++ b/engine/src/flutter/impeller/renderer/backend/gles/test/texture_gles_unittests.cc @@ -10,11 +10,16 @@ #include "flutter/testing/testing.h" #include "gtest/gtest.h" #include "impeller/base/validation.h" +#include "impeller/core/buffer_view.h" #include "impeller/core/formats.h" +#include "impeller/core/range.h" #include "impeller/core/texture_descriptor.h" #include "impeller/renderer/backend/gles/handle_gles.h" #include "impeller/renderer/backend/gles/proc_table_gles.h" #include "impeller/renderer/backend/gles/test/mock_gles.h" +#include "impeller/renderer/blit_pass.h" +#include "impeller/renderer/command_buffer.h" +#include "impeller/renderer/command_queue.h" namespace impeller::testing { @@ -155,24 +160,25 @@ TEST_P(TextureGLESTest, CanCreateAndUpload2DArrayTexture) { if (!context_gles.GetReactor() ->GetProcTable() .GetCapabilities() - ->SupportsTextureArray()) { + ->SupportsTextureArrays()) { GTEST_SKIP() << "2D array textures are not supported on this context."; } + // More than 6 layers, to cover slices beyond the cubemap face range. TextureDescriptor desc; desc.storage_mode = StorageMode::kHostVisible; desc.size = {2, 2}; desc.format = PixelFormat::kR8G8B8A8UNormInt; desc.type = TextureType::kTexture2DArray; - desc.array_layer_count = 3; + desc.array_layer_count = 8; desc.mip_count = 1; auto texture = GetContext()->GetResourceAllocator()->CreateTexture(desc); ASSERT_TRUE(texture); EXPECT_EQ(static_cast(texture->GetTextureDescriptor().array_layer_count), - 3); - EXPECT_TRUE(texture->IsSliceValid(2)); - EXPECT_FALSE(texture->IsSliceValid(3)); + 8); + EXPECT_TRUE(texture->IsSliceValid(7)); + EXPECT_FALSE(texture->IsSliceValid(8)); // Every layer can be uploaded. std::vector layer(2u * 2u * 4u, 0xFF); @@ -181,6 +187,28 @@ TEST_P(TextureGLESTest, CanCreateAndUpload2DArrayTexture) { EXPECT_TRUE(texture->SetContents(layer.data(), layer.size(), slice)); } EXPECT_TRUE(context_gles.GetReactor()->React()); + + // Every layer can also be uploaded through the blit path. + auto staging = GetContext()->GetResourceAllocator()->CreateBufferWithCopy( + layer.data(), layer.size()); + ASSERT_TRUE(staging); + auto command_buffer = GetContext()->CreateCommandBuffer(); + ASSERT_TRUE(command_buffer); + auto blit_pass = command_buffer->CreateBlitPass(); + ASSERT_TRUE(blit_pass); + for (size_t slice = 0; slice < static_cast(desc.array_layer_count); + ++slice) { + EXPECT_TRUE(blit_pass->AddCopy(BufferView(staging, Range(0, layer.size())), + texture, std::nullopt, + /*label=*/"ArrayLayerUpload", + /*mip_level=*/0, slice)); + } + EXPECT_TRUE(blit_pass->EncodeCommands()); + EXPECT_TRUE(GetContext() + ->GetCommandQueue() + ->Submit({std::move(command_buffer)}) + .ok()); + EXPECT_TRUE(context_gles.GetReactor()->React()); } TEST_P(TextureGLESTest, CreatingAndBindingEmptyTexturesDoesNotCrash) { 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 a19756936f464..d06b3d457a345 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.cc +++ b/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.cc @@ -280,7 +280,7 @@ bool TextureGLES::OnSetContents(std::shared_ptr mapping, // Bail out synchronously on contexts without array support (e.g. ES 2.0). // The glTexImage3D/glTexSubImage3D procs are null there, so queuing the // upload would dereference a null proc on the reactor thread. - if (!reactor_->GetProcTable().GetCapabilities()->SupportsTextureArray()) { + if (!reactor_->GetProcTable().GetCapabilities()->SupportsTextureArrays()) { VALIDATION_LOG << "2D array textures are not supported on this context."; return false; } @@ -506,7 +506,7 @@ void TextureGLES::InitializeContentsIfNecessary() { MarkSliceMipLevelInitialized(face, 0); } } else if (desc.type == TextureType::kTexture2DArray) { - if (!gl.GetCapabilities()->SupportsTextureArray()) { + if (!gl.GetCapabilities()->SupportsTextureArrays()) { VALIDATION_LOG << "2D array textures are not supported on this " "context."; return; 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 b2a72a37bcb3a..bdf224eb40984 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.cc @@ -558,6 +558,10 @@ bool CapabilitiesVK::SupportsManuallyMippedTextures() const { return true; } +bool CapabilitiesVK::SupportsTextureArrays() const { + return true; +} + void CapabilitiesVK::SetOffscreenFormat(PixelFormat pixel_format) const { default_color_format_ = pixel_format; } 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 96b57c076e9c1..71cf85bbf34d4 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.h +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/capabilities_vk.h @@ -275,6 +275,9 @@ class CapabilitiesVK final : public Capabilities, // |Capabilities| bool SupportsManuallyMippedTextures() const override; + // |Capabilities| + bool SupportsTextureArrays() const override; + // |Capabilities| bool SupportsExtendedRangeFormats() const override; diff --git a/engine/src/flutter/impeller/renderer/blit_pass.cc b/engine/src/flutter/impeller/renderer/blit_pass.cc index 5fcdc47c94220..a3ac61390e6cf 100644 --- a/engine/src/flutter/impeller/renderer/blit_pass.cc +++ b/engine/src/flutter/impeller/renderer/blit_pass.cc @@ -152,7 +152,7 @@ bool BlitPass::AddCopy(BufferView source, << destination->GetMipCount() << " mip levels."; return false; } - if (slice > 5) { + if (!destination->IsSliceValid(slice)) { VALIDATION_LOG << "Invalid value for slice: " << slice; return false; } diff --git a/engine/src/flutter/impeller/renderer/capabilities.cc b/engine/src/flutter/impeller/renderer/capabilities.cc index 99f96852ec4e3..df1764ed3d71f 100644 --- a/engine/src/flutter/impeller/renderer/capabilities.cc +++ b/engine/src/flutter/impeller/renderer/capabilities.cc @@ -101,6 +101,9 @@ class StandardCapabilities final : public Capabilities { // |Capabilities| bool SupportsManuallyMippedTextures() const override { return true; } + // |Capabilities| + bool SupportsTextureArrays() const override { return true; } + // |Capabilities| bool SupportsExtendedRangeFormats() const override { return supports_extended_range_formats_; diff --git a/engine/src/flutter/impeller/renderer/capabilities.h b/engine/src/flutter/impeller/renderer/capabilities.h index 2c290911bcc4f..30f95eefb8945 100644 --- a/engine/src/flutter/impeller/renderer/capabilities.h +++ b/engine/src/flutter/impeller/renderer/capabilities.h @@ -100,6 +100,12 @@ class Capabilities { /// bounded to the levels the texture declares. virtual bool SupportsManuallyMippedTextures() const = 0; + /// @brief Whether 2D array textures (`TextureType::kTexture2DArray`) can be + /// allocated and sampled. True everywhere except OpenGL ES 2.0 + /// contexts without an array-texture extension, where callers must + /// fall back to a texture atlas. + virtual bool SupportsTextureArrays() const = 0; + /// @brief Returns a supported `PixelFormat` for textures that store /// 4-channel colors (red/green/blue/alpha). virtual PixelFormat GetDefaultColorFormat() const = 0; diff --git a/engine/src/flutter/impeller/renderer/testing/mocks.h b/engine/src/flutter/impeller/renderer/testing/mocks.h index 71850b83ffe17..7998b05574613 100644 --- a/engine/src/flutter/impeller/renderer/testing/mocks.h +++ b/engine/src/flutter/impeller/renderer/testing/mocks.h @@ -257,6 +257,8 @@ class MockCapabilities : public Capabilities { MOCK_METHOD(bool, SupportsPrimitiveRestart, (), (const override)); MOCK_METHOD(bool, Supports32BitPrimitiveIndices, (), (const override)); MOCK_METHOD(bool, SupportsManuallyMippedTextures, (), (const override)); + + MOCK_METHOD(bool, SupportsTextureArrays, (), (const override)); MOCK_METHOD(bool, SupportsExtendedRangeFormats, (), (const override)); MOCK_METHOD(bool, SupportsFramebufferRenderMipmap, (), (const override)); MOCK_METHOD(bool, diff --git a/engine/src/flutter/lib/gpu/context.cc b/engine/src/flutter/lib/gpu/context.cc index 30aee9b3b567e..fdb183bf4f74b 100644 --- a/engine/src/flutter/lib/gpu/context.cc +++ b/engine/src/flutter/lib/gpu/context.cc @@ -154,6 +154,19 @@ extern bool InternalFlutterGpu_Context_GetSupportsManuallyMippedTextures( ->SupportsManuallyMippedTextures(); } +extern bool InternalFlutterGpu_Context_GetSupportsTextureArrays( + flutter::gpu::Context* wrapper) { + // TODO(bdero): Shader bundles do not carry an OpenGL ES 3.0 shader variant + // yet, so user shaders cannot sample 2D array textures on the GLES backend + // even when the context itself supports them. Once the bundle format gains + // an ES 3.0 variant, gate on the context capability alone. + if (wrapper->GetContext().GetBackendType() == + impeller::Context::BackendType::kOpenGLES) { + return false; + } + return wrapper->GetContext().GetCapabilities()->SupportsTextureArrays(); +} + extern int InternalFlutterGpu_Context_GetMaxSamplerAnisotropy( flutter::gpu::Context* wrapper) { return wrapper->GetContext().GetCapabilities()->GetMaxSamplerAnisotropy(); diff --git a/engine/src/flutter/lib/gpu/context.h b/engine/src/flutter/lib/gpu/context.h index a9e98eec6729b..c0ce6ab62c92c 100644 --- a/engine/src/flutter/lib/gpu/context.h +++ b/engine/src/flutter/lib/gpu/context.h @@ -90,6 +90,10 @@ FLUTTER_GPU_EXPORT extern bool InternalFlutterGpu_Context_GetSupportsManuallyMippedTextures( flutter::gpu::Context* wrapper); +FLUTTER_GPU_EXPORT +extern bool InternalFlutterGpu_Context_GetSupportsTextureArrays( + flutter::gpu::Context* wrapper); + FLUTTER_GPU_EXPORT extern int InternalFlutterGpu_Context_GetMaxSamplerAnisotropy( flutter::gpu::Context* wrapper); diff --git a/engine/src/flutter/lib/gpu/formats.h b/engine/src/flutter/lib/gpu/formats.h index 43a66e7aaa2a5..3eefaae6f53e3 100644 --- a/engine/src/flutter/lib/gpu/formats.h +++ b/engine/src/flutter/lib/gpu/formats.h @@ -50,6 +50,50 @@ constexpr FlutterGPUStorageMode FromImpellerStorageMode( } } +enum class FlutterGPUTextureType { + kTexture2D, + kTexture2DMultisample, + kTextureCube, + kTextureExternalOES, + kTexture2DArray, +}; + +constexpr impeller::TextureType ToImpellerTextureType( + FlutterGPUTextureType value) { + switch (value) { + case FlutterGPUTextureType::kTexture2D: + return impeller::TextureType::kTexture2D; + case FlutterGPUTextureType::kTexture2DMultisample: + return impeller::TextureType::kTexture2DMultisample; + case FlutterGPUTextureType::kTextureCube: + return impeller::TextureType::kTextureCube; + case FlutterGPUTextureType::kTextureExternalOES: + return impeller::TextureType::kTextureExternalOES; + case FlutterGPUTextureType::kTexture2DArray: + return impeller::TextureType::kTexture2DArray; + } +} + +constexpr impeller::TextureType ToImpellerTextureType(int value) { + return ToImpellerTextureType(static_cast(value)); +} + +constexpr FlutterGPUTextureType FromImpellerTextureType( + impeller::TextureType value) { + switch (value) { + case impeller::TextureType::kTexture2D: + return FlutterGPUTextureType::kTexture2D; + case impeller::TextureType::kTexture2DMultisample: + return FlutterGPUTextureType::kTexture2DMultisample; + case impeller::TextureType::kTextureCube: + return FlutterGPUTextureType::kTextureCube; + case impeller::TextureType::kTextureExternalOES: + return FlutterGPUTextureType::kTextureExternalOES; + case impeller::TextureType::kTexture2DArray: + return FlutterGPUTextureType::kTexture2DArray; + } +} + enum class FlutterGPUPixelFormat { kUnknown, kA8UNormInt, diff --git a/engine/src/flutter/lib/gpu/lib/src/context.dart b/engine/src/flutter/lib/gpu/lib/src/context.dart index 98df73cc9655f..d61dc05057964 100644 --- a/engine/src/flutter/lib/gpu/lib/src/context.dart +++ b/engine/src/flutter/lib/gpu/lib/src/context.dart @@ -78,6 +78,14 @@ base class GpuContext extends NativeFieldWrapperClass1 { return _getSupportsManuallyMippedTextures(); } + /// Whether 2D array textures ([TextureType.texture2DArray]) can be created, + /// uploaded, and sampled by shaders. True on Metal and Vulkan. Currently + /// false on the GLES backend, where shader bundles do not yet carry a + /// shader variant that can sample array textures. + bool get doesSupportTextureArrays { + return _getSupportsTextureArrays(); + } + /// The maximum anisotropy clamp supported by device samplers (see /// [SamplerOptions.maxAnisotropy]). /// @@ -189,6 +197,10 @@ base class GpuContext extends NativeFieldWrapperClass1 { bool enableShaderReadUsage = true, bool enableShaderWriteUsage = false, int mipLevelCount = 1, + + /// The number of layers to allocate for a [TextureType.texture2DArray] + /// texture. Must be 1 (the default) for all other texture types. + int layerCount = 1, }) { final resolvedTextureType = textureType ?? @@ -202,6 +214,25 @@ base class GpuContext extends NativeFieldWrapperClass1 { 'for a ${width}x$height texture', ); } + if (layerCount < 1) { + throw ArgumentError('layerCount ($layerCount) must be at least 1'); + } + if (layerCount > 1 && resolvedTextureType != TextureType.texture2DArray) { + throw ArgumentError( + 'layerCount ($layerCount) must be 1 for textures of type ' + '$resolvedTextureType', + ); + } + if (resolvedTextureType == TextureType.texture2DArray && sampleCount != 1) { + throw ArgumentError('2D array textures do not support multisampling'); + } + if (resolvedTextureType == TextureType.texture2DArray && + !doesSupportTextureArrays) { + throw ArgumentError( + '2D array textures are not supported by this GpuContext. Check ' + 'GpuContext.doesSupportTextureArrays before creating one.', + ); + } if (format.isCompressed) { if (enableRenderTargetUsage || enableShaderWriteUsage || @@ -235,6 +266,7 @@ base class GpuContext extends NativeFieldWrapperClass1 { enableShaderReadUsage, enableShaderWriteUsage, mipLevelCount, + layerCount, ); // `Texture._initialize` throws on failure, so `result` is always valid here. return result; @@ -319,6 +351,11 @@ base class GpuContext extends NativeFieldWrapperClass1 { ) external bool _getSupportsManuallyMippedTextures(); + @Native)>( + symbol: 'InternalFlutterGpu_Context_GetSupportsTextureArrays', + ) + external bool _getSupportsTextureArrays(); + @Native)>( symbol: 'InternalFlutterGpu_Context_GetMaxSamplerAnisotropy', ) diff --git a/engine/src/flutter/lib/gpu/lib/src/formats.dart b/engine/src/flutter/lib/gpu/lib/src/formats.dart index 4f0e3a9934ba7..063c7d796993b 100644 --- a/engine/src/flutter/lib/gpu/lib/src/formats.dart +++ b/engine/src/flutter/lib/gpu/lib/src/formats.dart @@ -642,4 +642,9 @@ enum TextureType { /// A texture sourced from an external source. textureExternalOES, + + /// A 2-dimensional texture with multiple same-size layers, sampled in + /// shaders as `sampler2DArray`. The layer count is set with the + /// `layerCount` argument to `GpuContext.createTexture`. + texture2DArray, } 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 040231ae6add8..444efb08a8266 100644 --- a/engine/src/flutter/lib/gpu/lib/src/render_pass.dart +++ b/engine/src/flutter/lib/gpu/lib/src/render_pass.dart @@ -33,8 +33,9 @@ base class ColorAttachment { 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). + /// the face in the order `+X, -X, +Y, -Y, +Z, -Z`; for 2D array textures it + /// selects the layer. Must be in the range `[0, texture.sliceCount)` + /// (always 0 for other texture types). int slice; void _validate() { diff --git a/engine/src/flutter/lib/gpu/lib/src/texture.dart b/engine/src/flutter/lib/gpu/lib/src/texture.dart index 632cdba474190..8b5d0bd102efc 100644 --- a/engine/src/flutter/lib/gpu/lib/src/texture.dart +++ b/engine/src/flutter/lib/gpu/lib/src/texture.dart @@ -45,6 +45,7 @@ base class Texture extends NativeFieldWrapperClass1 { this.enableShaderReadUsage, this.enableShaderWriteUsage, this.mipLevelCount, + this.layerCount, ) : _gpuContext = gpuContext { if (sampleCount != 1 && sampleCount != 4) { throw Exception("Only a sample count of 1 or 4 is currently supported"); @@ -61,6 +62,7 @@ base class Texture extends NativeFieldWrapperClass1 { enableShaderReadUsage, enableShaderWriteUsage, mipLevelCount, + layerCount, ); if (!_valid) { // The engine logs the specific reason (for example, a compressed format @@ -84,7 +86,8 @@ base class Texture extends NativeFieldWrapperClass1 { enableRenderTargetUsage = true, enableShaderReadUsage = true, enableShaderWriteUsage = false, - mipLevelCount = 1; + mipLevelCount = 1, + layerCount = 1; /// Wraps the GPU texture that backs [image] as a Flutter GPU [Texture], /// without copying any pixel data. @@ -101,7 +104,7 @@ base class Texture extends NativeFieldWrapperClass1 { /// yet when this is called. Throws if [image] has no compatible texture. factory Texture.fromImage(GpuContext gpuContext, ui.Image image) { final Int32List info = _imageTextureInfo(gpuContext, image); - if (info.length != 10) { + if (info.length != 11) { throw Exception( 'Texture.fromImage could not wrap the image because it is not backed ' 'by a compatible GPU texture. Use an image from the asynchronous ' @@ -125,7 +128,8 @@ base class Texture extends NativeFieldWrapperClass1 { enableRenderTargetUsage = info[6] != 0, enableShaderReadUsage = info[7] != 0, enableShaderWriteUsage = info[8] != 0, - mipLevelCount = info[9] { + mipLevelCount = info[9], + layerCount = info[10] { _valid = _initializeFromImage(gpuContext, image); if (!_valid) { throw Exception("Texture.fromImage failed to wrap the image texture"); @@ -159,9 +163,23 @@ base class Texture extends NativeFieldWrapperClass1 { /// [Texture.fullMipCount] to compute the maximum for a given size. final int mipLevelCount; + /// The number of layers allocated for this texture. Always 1 for texture + /// types other than [TextureType.texture2DArray]. + final int layerCount; + /// The number of slices in this texture. Determined by [textureType]: - /// 1 for 2D and external textures, 6 for cubemap textures. - int get sliceCount => textureType == TextureType.textureCube ? 6 : 1; + /// 1 for 2D and external textures, 6 for cubemap textures, and + /// [layerCount] for 2D array textures. + int get sliceCount { + switch (textureType) { + case TextureType.textureCube: + return 6; + case TextureType.texture2DArray: + return layerCount; + default: + return 1; + } + } /// Returns the width of the texture at [mipLevel], clamped at 1. int getMipLevelWidth(int mipLevel) { @@ -202,9 +220,10 @@ base class Texture extends NativeFieldWrapperClass1 { /// [mipLevel] selects which mip level to write to. Defaults to 0 (base /// level). Must be in the range `[0, mipLevelCount)`. /// - /// [slice] selects which slice to write to for cubemap textures, where - /// each face is a separate slice in the order - /// `+X, -X, +Y, -Y, +Z, -Z`. Must be 0 for non-cubemap textures. + /// [slice] selects which slice to write to. For cubemap textures each face + /// is a separate slice in the order `+X, -X, +Y, -Y, +Z, -Z`; for 2D array + /// textures each layer is a slice, in the range `[0, layerCount)`. Must be + /// 0 for other texture types. /// /// The length of [sourceBytes] must exactly match the size returned by /// [getMipLevelSizeInBytes] for the requested [mipLevel]. For @@ -269,6 +288,7 @@ base class Texture extends NativeFieldWrapperClass1 { Bool, Bool, Int, + Int, ) >(symbol: 'InternalFlutterGpu_Texture_Initialize') external bool _initialize( @@ -283,6 +303,7 @@ base class Texture extends NativeFieldWrapperClass1 { bool enableShaderReadUsage, bool enableShaderWriteUsage, int mipLevelCount, + int layerCount, ); @Native, Pointer, Handle, Int, Int)>( diff --git a/engine/src/flutter/lib/gpu/texture.cc b/engine/src/flutter/lib/gpu/texture.cc index 39a83e99f6699..e5ba0615e0591 100644 --- a/engine/src/flutter/lib/gpu/texture.cc +++ b/engine/src/flutter/lib/gpu/texture.cc @@ -5,6 +5,7 @@ #include "flutter/lib/gpu/texture.h" #include +#include #include "flutter/lib/gpu/formats.h" #include "flutter/lib/ui/painting/image.h" @@ -213,15 +214,21 @@ bool InternalFlutterGpu_Texture_Initialize(Dart_Handle wrapper, bool enable_render_target_usage, bool enable_shader_read_usage, bool enable_shader_write_usage, - int mip_level_count) { + int mip_level_count, + int layer_count) { if (mip_level_count < 1) { return false; } + if (layer_count < 1) { + return false; + } + FML_DCHECK(layer_count <= std::numeric_limits::max()); impeller::TextureDescriptor desc; desc.storage_mode = flutter::gpu::ToImpellerStorageMode(storage_mode); desc.size = {width, height}; desc.format = flutter::gpu::ToImpellerPixelFormat(format); desc.mip_count = static_cast(mip_level_count); + desc.array_layer_count = static_cast(layer_count); desc.usage = {}; if (enable_render_target_usage) { desc.usage |= impeller::TextureUsage::kRenderTarget; @@ -242,7 +249,7 @@ bool InternalFlutterGpu_Texture_Initialize(Dart_Handle wrapper, default: return false; } - desc.type = static_cast(texture_type); + desc.type = flutter::gpu::ToImpellerTextureType(texture_type); if (!impeller::IsMultisampleCapable(desc.type) && desc.sample_count != impeller::SampleCount::kCount1) { return false; @@ -291,7 +298,7 @@ Dart_Handle InternalFlutterGpu_Texture_ImageTextureInfo( const impeller::TextureUsageMask usage = desc.usage; // Layout must match the parsing in the Dart `Texture._fromImage`. - int32_t values[10]; + int32_t values[11]; values[0] = static_cast( flutter::gpu::FromImpellerStorageMode(desc.storage_mode)); values[1] = @@ -299,12 +306,13 @@ Dart_Handle InternalFlutterGpu_Texture_ImageTextureInfo( values[2] = static_cast(desc.size.width); values[3] = static_cast(desc.size.height); values[4] = static_cast(desc.sample_count); - // The Flutter GPU `TextureType` enum mirrors `impeller::TextureType`. - values[5] = static_cast(desc.type); + values[5] = + static_cast(flutter::gpu::FromImpellerTextureType(desc.type)); values[6] = (usage & impeller::TextureUsage::kRenderTarget) ? 1 : 0; values[7] = (usage & impeller::TextureUsage::kShaderRead) ? 1 : 0; values[8] = (usage & impeller::TextureUsage::kShaderWrite) ? 1 : 0; values[9] = static_cast(desc.mip_count); + values[10] = static_cast(desc.array_layer_count); const intptr_t length = sizeof(values) / sizeof(values[0]); Dart_Handle list = Dart_NewTypedData(Dart_TypedData_kInt32, length); diff --git a/engine/src/flutter/lib/gpu/texture.h b/engine/src/flutter/lib/gpu/texture.h index 50ad8a426958b..ed019804a2af9 100644 --- a/engine/src/flutter/lib/gpu/texture.h +++ b/engine/src/flutter/lib/gpu/texture.h @@ -60,7 +60,8 @@ extern bool InternalFlutterGpu_Texture_Initialize( bool enable_render_target_usage, bool enable_shader_read_usage, bool enable_shader_write_usage, - int mip_level_count); + int mip_level_count, + int layer_count); FLUTTER_GPU_EXPORT extern bool InternalFlutterGpu_Texture_Overwrite( diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn b/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn index 137ef1b01e009..45cdd7656f745 100644 --- a/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn +++ b/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn @@ -65,10 +65,11 @@ if (enable_unittests) { "//flutter/impeller/fixtures/flutter_gpu_optimized_out_sampler.frag", "//flutter/impeller/fixtures/flutter_gpu_texture.frag", "//flutter/impeller/fixtures/flutter_gpu_texture.vert", + "//flutter/impeller/fixtures/flutter_gpu_array_texture.frag", ] fixtures = rebase_path("//flutter/impeller/fixtures") - shader_bundle = "{\"InstancedFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_instanced.frag\"}, \"InstancedVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_instanced.vert\"}, \"UnlitFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit.frag\"}, \"UnlitVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_unlit.vert\"}, \"UnlitFragmentAltInstance\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit_alt_instance.frag\"}, \"OptimizedOutSamplerFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_optimized_out_sampler.frag\"}, \"TextureFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_texture.frag\"}, \"TextureVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_texture.vert\"}}" + shader_bundle = "{\"InstancedFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_instanced.frag\"}, \"InstancedVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_instanced.vert\"}, \"UnlitFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit.frag\"}, \"UnlitVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_unlit.vert\"}, \"UnlitFragmentAltInstance\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit_alt_instance.frag\"}, \"OptimizedOutSamplerFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_optimized_out_sampler.frag\"}, \"TextureFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_texture.frag\"}, \"TextureVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_texture.vert\"}, \"ArrayTextureFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_array_texture.frag\"}}" shader_bundle_output = "test.shaderbundle" } diff --git a/engine/src/flutter/testing/dart/gpu_test.dart b/engine/src/flutter/testing/dart/gpu_test.dart index ae4f0cdf14fa7..a66b67ffc2295 100644 --- a/engine/src/flutter/testing/dart/gpu_test.dart +++ b/engine/src/flutter/testing/dart/gpu_test.dart @@ -93,6 +93,16 @@ Future createTextureRenderPipeline() async { return gpu.gpuContext.createRenderPipeline(vertex!, fragment!); } +Future createArrayTextureRenderPipeline() async { + final gpu.ShaderLibrary? library = await gpu.ShaderLibrary.fromAsset('test.shaderbundle'); + assert(library != null); + final gpu.Shader? vertex = library!['TextureVertex']; + assert(vertex != null); + final gpu.Shader? fragment = library['ArrayTextureFragment']; + assert(fragment != null); + return gpu.gpuContext.createRenderPipeline(vertex!, fragment!); +} + class RenderPassState { RenderPassState(this.renderTexture, this.commandBuffer, this.renderPass); @@ -1597,6 +1607,121 @@ void main() async { await comparer.addGoldenImage(image, 'flutter_gpu_test_manually_mipped_texture.png'); }, skip: !(impellerEnabled && flutterGpuEnabled)); + test('2D array textures can be created and uploaded per layer', () async { + if (!gpu.gpuContext.doesSupportTextureArrays) { + // Creating an array texture on a backend that does not support them + // fails fast at creation instead of later at upload. + expect( + () => gpu.gpuContext.createTexture( + gpu.StorageMode.hostVisible, + 4, + 4, + textureType: gpu.TextureType.texture2DArray, + layerCount: 2, + ), + throwsArgumentError, + ); + return; + } + + // More than 6 layers, to cover slices beyond the cubemap face range. + final gpu.Texture texture = gpu.gpuContext.createTexture( + gpu.StorageMode.hostVisible, + 4, + 4, + textureType: gpu.TextureType.texture2DArray, + layerCount: 8, + ); + expect(texture.textureType, gpu.TextureType.texture2DArray); + expect(texture.layerCount, 8); + expect(texture.sliceCount, 8); + + final layer = Uint8List(4 * 4 * 4); + for (var slice = 0; slice < texture.sliceCount; slice++) { + layer.fillRange(0, layer.length, 0x10 * (slice + 1)); + texture.overwrite(layer.buffer.asByteData(), slice: slice); + } + + // Out-of-range slices and layer counts are rejected. + expect(() => texture.overwrite(layer.buffer.asByteData(), slice: 8), throwsException); + expect( + () => gpu.gpuContext.createTexture( + gpu.StorageMode.hostVisible, + 4, + 4, + textureType: gpu.TextureType.texture2DArray, + layerCount: 0, + ), + throwsArgumentError, + ); + expect( + () => gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 4, 4, layerCount: 2), + throwsArgumentError, + ); + }, skip: !(impellerEnabled && flutterGpuEnabled)); + + test('sampling a 2D array texture reads the selected layer', () async { + if (!gpu.gpuContext.doesSupportTextureArrays) { + return; + } + + // Each layer is a solid gray with a distinct, ascending intensity so the + // check below is independent of RGBA/BGRA channel order. + const layerValues = [0x28, 0x78, 0xDC]; + final gpu.Texture texture = gpu.gpuContext.createTexture( + gpu.StorageMode.hostVisible, + 4, + 4, + textureType: gpu.TextureType.texture2DArray, + layerCount: layerValues.length, + ); + final layer = Uint8List(4 * 4 * 4); + for (var slice = 0; slice < layerValues.length; slice++) { + layer.fillRange(0, layer.length, layerValues[slice]); + texture.overwrite(layer.buffer.asByteData(), slice: slice); + } + + final gpu.RenderPipeline pipeline = await createArrayTextureRenderPipeline(); + for (var slice = 0; slice < layerValues.length; slice++) { + final RenderPassState state = createSimpleRenderPass(); + state.renderPass.bindPipeline(pipeline); + + // A fullscreen quad with white vertex colors, so the sampled layer value + // passes through unmodified. + final gpu.HostBuffer transients = gpu.gpuContext.createHostBuffer(); + final gpu.BufferView vertices = transients.emplace( + float32([ + -1, -1, 0, 0, 0, 1, 1, 1, 1, // + 1, -1, 0, 1, 0, 1, 1, 1, 1, // + 1, 1, 0, 1, 1, 1, 1, 1, 1, // + -1, -1, 0, 0, 0, 1, 1, 1, 1, // + 1, 1, 0, 1, 1, 1, 1, 1, 1, // + -1, 1, 0, 0, 1, 1, 1, 1, 1, // + ]), + ); + state.renderPass.bindVertexBuffer(vertices); + state.renderPass.bindUniform( + pipeline.vertexShader.getUniformSlot('VertInfo'), + transients.emplace(mvpUBO(Matrix4.identity())), + ); + state.renderPass.bindUniform( + pipeline.fragmentShader.getUniformSlot('FragInfo'), + transients.emplace(float32([slice.toDouble()])), + ); + state.renderPass.bindTexture(pipeline.fragmentShader.getUniformSlot('tex'), texture); + state.renderPass.draw(6); + state.commandBuffer.submit(); + + final ByteData pixels = await readTextureBytes(state.renderTexture); + final int value = pixels.getUint8(0); + expect( + (value - layerValues[slice]).abs(), + lessThanOrEqualTo(1), + reason: 'Expected layer $slice value ${layerValues[slice]}, got $value', + ); + } + }, skip: !(impellerEnabled && flutterGpuEnabled)); + test('drawIndexed throws when no index buffer is bound', () async { final RenderPassState state = createSimpleRenderPass(); final gpu.RenderPipeline pipeline = await createUnlitRenderPipeline();