[Impeller] Validate GLES texture units against the combined limit (#189332)
Fixes #189331.
The GLES backend allocates fragment stage texture units after the vertex
stage's, but validated the running unit index against the per-stage
maximum. On a driver reporting the minimum 16 fragment units (ANGLE on
D3D11), a draw using all 16 fragment samplers plus a vertex stage
texture failed validation and the render pass aborted, crashing
skinned-mesh rendering in Flutter GPU apps on Windows.
Texture units are a combined resource in GL; the per-stage limits bound
only how many samplers one stage references. This validates the unit
index against the combined limit and the per-stage sampler count against
the per-stage limit. Adds unit tests for the previously rejected case
and both overflow cases.
## 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.
- [x] All existing and new tests are passing.
diff --git a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.cc b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.cc
index 2055e51..b5a6fad 100644
--- a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.cc
+++ b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.cc
@@ -493,6 +493,7 @@
ShaderStage stage,
size_t unit_start_index) {
size_t active_index = unit_start_index;
+ size_t stage_texture_count = 0;
for (auto i = 0u; i < texture_range.length; i++) {
const TextureAndSampler& data = bound_textures[texture_range.offset + i];
if (data.stage != stage) {
@@ -513,11 +514,21 @@
//--------------------------------------------------------------------------
/// Set the active texture unit.
///
- if (active_index >= gl.GetCapabilities()->GetMaxTextureUnits(stage)) {
+ /// Units are a combined resource; the per-stage limits cap only how many
+ /// samplers one stage references, not the unit indices they bind to.
+ ///
+ stage_texture_count++;
+ if (stage_texture_count > gl.GetCapabilities()->GetMaxTextureUnits(stage)) {
VALIDATION_LOG << "Texture units specified exceed the capabilities for "
"this shader stage.";
return std::nullopt;
}
+ if (active_index >=
+ gl.GetCapabilities()->max_combined_texture_image_units) {
+ VALIDATION_LOG << "Texture units specified exceed the combined texture "
+ "unit limit.";
+ return std::nullopt;
+ }
gl.ActiveTexture(GL_TEXTURE0 + active_index);
//--------------------------------------------------------------------------
diff --git a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.h b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.h
index 12cef03..c00b55e 100644
--- a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.h
+++ b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles.h
@@ -21,6 +21,10 @@
FML_TEST_CLASS(BufferBindingsGLESTest, BindArrayData);
FML_TEST_CLASS(BufferBindingsGLESTest, BindUniformDataVerticesAndMatrices);
FML_TEST_CLASS(BufferBindingsGLESTest, BindUniformFailsWithoutFloatType);
+FML_TEST_CLASS(BufferBindingsGLESTest,
+ BindsTexturesAcrossThePerStageUnitBoundary);
+FML_TEST_CLASS(BufferBindingsGLESTest, RejectsTexturesBeyondThePerStageLimit);
+FML_TEST_CLASS(BufferBindingsGLESTest, RejectsTexturesBeyondTheCombinedLimit);
} // namespace testing
//------------------------------------------------------------------------------
@@ -66,6 +70,12 @@
BindUniformDataVerticesAndMatrices);
FML_FRIEND_TEST(testing::BufferBindingsGLESTest,
BindUniformFailsWithoutFloatType);
+ FML_FRIEND_TEST(testing::BufferBindingsGLESTest,
+ BindsTexturesAcrossThePerStageUnitBoundary);
+ FML_FRIEND_TEST(testing::BufferBindingsGLESTest,
+ RejectsTexturesBeyondThePerStageLimit);
+ FML_FRIEND_TEST(testing::BufferBindingsGLESTest,
+ RejectsTexturesBeyondTheCombinedLimit);
//----------------------------------------------------------------------------
/// @brief The arguments to glVertexAttribPointer.
///
diff --git a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles_unittests.cc b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles_unittests.cc
index 9bae5b1..494fc8d 100644
--- a/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles_unittests.cc
+++ b/engine/src/flutter/impeller/renderer/backend/gles/buffer_bindings_gles_unittests.cc
@@ -5,11 +5,14 @@
#include "flutter/testing/testing.h" // IWYU pragma: keep
#include "gtest/gtest.h"
#include "impeller/core/shader_types.h"
+#include "impeller/core/texture_descriptor.h"
#include "impeller/renderer/backend/gles/buffer_bindings_gles.h"
#include "impeller/renderer/backend/gles/device_buffer_gles.h"
#include "impeller/renderer/backend/gles/formats_gles.h"
#include "impeller/renderer/backend/gles/reactor_gles.h"
+#include "impeller/renderer/backend/gles/sampler_library_gles.h"
#include "impeller/renderer/backend/gles/test/mock_gles.h"
+#include "impeller/renderer/backend/gles/texture_gles.h"
#include "impeller/renderer/command.h"
namespace impeller {
@@ -19,6 +22,7 @@
}
using ::testing::_;
+using ::testing::NiceMock;
TEST(BufferBindingsGLESTest, ToVertexAttribTypeSupportedFormats) {
EXPECT_EQ(ToVertexAttribType(VertexAttributeFormat::kFloat32x3),
@@ -361,5 +365,138 @@
/*expected_bound_size=*/32);
}
+namespace {
+
+// Owns the reactor, sampler, and metadata behind a set of texture bindings.
+struct BoundTexturesFixture {
+ std::shared_ptr<ReactorGLES> reactor;
+ std::shared_ptr<TestWorker> worker;
+ std::unique_ptr<SamplerLibrary> sampler_library;
+ raw_ptr<const Sampler> sampler;
+ std::vector<std::unique_ptr<ShaderMetadata>> metadata;
+ std::vector<TextureAndSampler> bound_textures;
+ absl::flat_hash_map<std::string, GLint> uniform_bindings;
+
+ explicit BoundTexturesFixture(std::unique_ptr<ProcTableGLES> proc_table) {
+ reactor = std::make_shared<ReactorGLES>(std::move(proc_table));
+ worker = std::make_shared<TestWorker>();
+ reactor->AddWorker(worker);
+ sampler_library = std::make_unique<SamplerLibraryGLES>(
+ /*supports_decal_sampler_address_mode=*/false);
+ sampler = sampler_library->GetSampler({});
+ }
+
+ void AddTextures(ShaderStage stage, size_t count) {
+ for (size_t i = 0; i < count; i++) {
+ TextureDescriptor desc;
+ desc.storage_mode = StorageMode::kDevicePrivate;
+ desc.type = TextureType::kTexture2D;
+ desc.format = PixelFormat::kR8G8B8A8UNormInt;
+ desc.size = {1, 1};
+ desc.mip_count = 1u;
+ desc.usage = TextureUsage::kShaderRead;
+ auto texture = std::make_shared<TextureGLES>(reactor, desc);
+ const std::string name = "tex" + std::to_string(metadata.size());
+ const std::string key = "TEX" + std::to_string(metadata.size());
+ uniform_bindings[key] = static_cast<GLint>(100 + metadata.size());
+ auto meta = std::make_unique<ShaderMetadata>();
+ meta->name = name;
+ TextureAndSampler data = {};
+ data.stage = stage;
+ data.texture = TextureResource(meta.get(), std::move(texture));
+ data.sampler = sampler;
+ metadata.push_back(std::move(meta));
+ bound_textures.push_back(std::move(data));
+ }
+ }
+};
+
+// Capabilities of a minimum-spec ES3 driver (16 per stage, 32 combined).
+std::unique_ptr<NiceMock<MockGLESImpl>> MakeSixteenUnitMockImpl() {
+ auto impl = std::make_unique<NiceMock<MockGLESImpl>>();
+ EXPECT_CALL(*impl, GetIntegerv(_, _))
+ .WillRepeatedly([](GLenum name, GLint* value) {
+ switch (name) {
+ case GL_MAX_TEXTURE_IMAGE_UNITS:
+ case GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS:
+ *value = 16;
+ break;
+ case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS:
+ *value = 32;
+ break;
+ default:
+ break;
+ }
+ });
+ return impl;
+}
+
+} // namespace
+
+// One vertex texture pushes the last of 16 fragment samplers onto unit 16,
+// which must bind on a 16-per-stage driver since units are combined in GL.
+TEST(BufferBindingsGLESTest, BindsTexturesAcrossThePerStageUnitBoundary) {
+ std::shared_ptr<MockGLES> mock_gl = MockGLES::Init(MakeSixteenUnitMockImpl());
+ BoundTexturesFixture fixture(
+ std::make_unique<ProcTableGLES>(kMockResolverGLES));
+ fixture.AddTextures(ShaderStage::kVertex, 1);
+ fixture.AddTextures(ShaderStage::kFragment, 16);
+ ASSERT_TRUE(fixture.reactor->React());
+
+ BufferBindingsGLES bindings;
+ bindings.SetUniformBindings(std::move(fixture.uniform_bindings));
+ std::vector<BufferResource> bound_buffers;
+ EXPECT_TRUE(bindings.BindUniformData(
+ fixture.reactor->GetProcTable(), fixture.bound_textures, bound_buffers,
+ Range{0, fixture.bound_textures.size()}, Range{0, 0}));
+}
+
+// More samplers in one stage than its limit is still rejected.
+TEST(BufferBindingsGLESTest, RejectsTexturesBeyondThePerStageLimit) {
+ std::shared_ptr<MockGLES> mock_gl = MockGLES::Init(MakeSixteenUnitMockImpl());
+ BoundTexturesFixture fixture(
+ std::make_unique<ProcTableGLES>(kMockResolverGLES));
+ fixture.AddTextures(ShaderStage::kFragment, 17);
+ ASSERT_TRUE(fixture.reactor->React());
+
+ BufferBindingsGLES bindings;
+ bindings.SetUniformBindings(std::move(fixture.uniform_bindings));
+ std::vector<BufferResource> bound_buffers;
+ EXPECT_FALSE(bindings.BindUniformData(
+ fixture.reactor->GetProcTable(), fixture.bound_textures, bound_buffers,
+ Range{0, fixture.bound_textures.size()}, Range{0, 0}));
+}
+
+// Units past the combined limit are rejected even when each stage is within
+// its per-stage limit.
+TEST(BufferBindingsGLESTest, RejectsTexturesBeyondTheCombinedLimit) {
+ auto impl = std::make_unique<NiceMock<MockGLESImpl>>();
+ EXPECT_CALL(*impl, GetIntegerv(_, _))
+ .WillRepeatedly([](GLenum name, GLint* value) {
+ switch (name) {
+ case GL_MAX_TEXTURE_IMAGE_UNITS:
+ case GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS:
+ case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS:
+ *value = 8;
+ break;
+ default:
+ break;
+ }
+ });
+ std::shared_ptr<MockGLES> mock_gl = MockGLES::Init(std::move(impl));
+ BoundTexturesFixture fixture(
+ std::make_unique<ProcTableGLES>(kMockResolverGLES));
+ fixture.AddTextures(ShaderStage::kVertex, 8);
+ fixture.AddTextures(ShaderStage::kFragment, 8);
+ ASSERT_TRUE(fixture.reactor->React());
+
+ BufferBindingsGLES bindings;
+ bindings.SetUniformBindings(std::move(fixture.uniform_bindings));
+ std::vector<BufferResource> bound_buffers;
+ EXPECT_FALSE(bindings.BindUniformData(
+ fixture.reactor->GetProcTable(), fixture.bound_textures, bound_buffers,
+ Range{0, fixture.bound_textures.size()}, Range{0, 0}));
+}
+
} // namespace testing
} // namespace impeller
diff --git a/engine/src/flutter/impeller/renderer/backend/gles/test/mock_gles.cc b/engine/src/flutter/impeller/renderer/backend/gles/test/mock_gles.cc
index f4d4815..dbd7024 100644
--- a/engine/src/flutter/impeller/renderer/backend/gles/test/mock_gles.cc
+++ b/engine/src/flutter/impeller/renderer/backend/gles/test/mock_gles.cc
@@ -98,7 +98,9 @@
*value = g_extensions.size();
} break;
case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS:
+ // Minimum default; a registered mock may overwrite it.
*value = 8;
+ CallMockMethod(&IMockGLESImpl::GetIntegerv, name, value);
break;
case GL_MAX_LABEL_LENGTH_KHR:
*value = 64;