[Impeller] Exploit dynamic state in OpenGL for fewer program links. (#53764)

Fixes https://github.com/flutter/flutter/issues/145125
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 312b14e..bd21559 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -42231,6 +42231,8 @@
 ORIGIN: ../../../flutter/impeller/renderer/backend/gles/surface_gles.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/gles/texture_gles.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/gles/texture_gles.h + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/impeller/renderer/backend/gles/unique_handle_gles.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/impeller/renderer/backend/gles/unique_handle_gles.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/metal/allocator_mtl.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/metal/allocator_mtl.mm + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/impeller/renderer/backend/metal/blit_pass_mtl.h + ../../../flutter/LICENSE
@@ -45097,6 +45099,8 @@
 FILE: ../../../flutter/impeller/renderer/backend/gles/surface_gles.h
 FILE: ../../../flutter/impeller/renderer/backend/gles/texture_gles.cc
 FILE: ../../../flutter/impeller/renderer/backend/gles/texture_gles.h
+FILE: ../../../flutter/impeller/renderer/backend/gles/unique_handle_gles.cc
+FILE: ../../../flutter/impeller/renderer/backend/gles/unique_handle_gles.h
 FILE: ../../../flutter/impeller/renderer/backend/metal/allocator_mtl.h
 FILE: ../../../flutter/impeller/renderer/backend/metal/allocator_mtl.mm
 FILE: ../../../flutter/impeller/renderer/backend/metal/blit_pass_mtl.h
diff --git a/impeller/fixtures/BUILD.gn b/impeller/fixtures/BUILD.gn
index b121d17..ce20901 100644
--- a/impeller/fixtures/BUILD.gn
+++ b/impeller/fixtures/BUILD.gn
@@ -38,6 +38,8 @@
     "sepia.frag",
     "sepia.vert",
     "simple.vert",
+    "spec_constant.frag",
+    "spec_constant.vert",
     "stage1.comp",
     "stage2.comp",
     "swizzle.frag",
diff --git a/impeller/fixtures/spec_constant.frag b/impeller/fixtures/spec_constant.frag
new file mode 100644
index 0000000..844d5c4
--- /dev/null
+++ b/impeller/fixtures/spec_constant.frag
@@ -0,0 +1,19 @@
+// 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.
+
+in vec4 v_color;
+in vec4 v_color2;
+
+layout(constant_id = 0) const float some_fraction = 1.0;
+
+out vec4 frag_color;
+
+uniform FragInfo {
+  float time;
+}
+frag_info;
+
+void main() {
+  frag_color = mix(v_color, v_color2, some_fraction);
+}
diff --git a/impeller/fixtures/spec_constant.vert b/impeller/fixtures/spec_constant.vert
new file mode 100644
index 0000000..d9ff2b6
--- /dev/null
+++ b/impeller/fixtures/spec_constant.vert
@@ -0,0 +1,16 @@
+// 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.
+
+in vec2 position;
+in vec4 color;
+in vec4 color2;
+
+out vec4 v_color;
+out vec4 v_color2;
+
+void main() {
+  gl_Position = vec4(position, 0.0, 1.0);
+  v_color = color;
+  v_color2 = color2;
+}
diff --git a/impeller/renderer/backend/gles/BUILD.gn b/impeller/renderer/backend/gles/BUILD.gn
index 5b97e1e..3a99fa9 100644
--- a/impeller/renderer/backend/gles/BUILD.gn
+++ b/impeller/renderer/backend/gles/BUILD.gn
@@ -20,11 +20,13 @@
     "test/mock_gles.cc",
     "test/mock_gles.h",
     "test/mock_gles_unittests.cc",
+    "test/pipeline_library_gles_unittests.cc",
     "test/proc_table_gles_unittests.cc",
     "test/specialization_constants_unittests.cc",
   ]
   deps = [
     ":gles",
+    "//flutter/impeller/playground:playground_test",
     "//flutter/testing:testing_lib",
   ]
 }
@@ -80,6 +82,8 @@
     "surface_gles.h",
     "texture_gles.cc",
     "texture_gles.h",
+    "unique_handle_gles.cc",
+    "unique_handle_gles.h",
   ]
 
   if (!is_android && !is_fuchsia) {
diff --git a/impeller/renderer/backend/gles/handle_gles.h b/impeller/renderer/backend/gles/handle_gles.h
index 3f6d7bb..cc52c8c 100644
--- a/impeller/renderer/backend/gles/handle_gles.h
+++ b/impeller/renderer/backend/gles/handle_gles.h
@@ -28,16 +28,35 @@
 
 class ReactorGLES;
 
+//------------------------------------------------------------------------------
+/// @brief      Represents a handle to an underlying OpenGL object. Unlike
+///             OpenGL object handles, these handles can be collected on any
+///             thread as long as their destruction is scheduled in a reactor.
+///
 struct HandleGLES {
   HandleType type = HandleType::kUnknown;
   std::optional<UniqueID> name;
 
+  //----------------------------------------------------------------------------
+  /// @brief      Creates a dead handle.
+  ///
+  /// @return     The handle.
+  ///
   static HandleGLES DeadHandle() {
     return HandleGLES{HandleType::kUnknown, std::nullopt};
   }
 
+  //----------------------------------------------------------------------------
+  /// @brief      Determines if the handle is dead.
+  ///
+  /// @return     True if dead, False otherwise.
+  ///
   constexpr bool IsDead() const { return !name.has_value(); }
 
+  //----------------------------------------------------------------------------
+  /// @brief      Get the hash value of this handle. Handles can be used as map
+  ///             keys.
+  ///
   struct Hash {
     std::size_t operator()(const HandleGLES& handle) const {
       return fml::HashCombine(
@@ -46,6 +65,9 @@
     }
   };
 
+  //----------------------------------------------------------------------------
+  /// @brief      A comparer used to test the equality of two handles.
+  ///
   struct Equal {
     bool operator()(const HandleGLES& lhs, const HandleGLES& rhs) const {
       return lhs.type == rhs.type && lhs.name == rhs.name;
diff --git a/impeller/renderer/backend/gles/pipeline_gles.cc b/impeller/renderer/backend/gles/pipeline_gles.cc
index eb99661..557eca2 100644
--- a/impeller/renderer/backend/gles/pipeline_gles.cc
+++ b/impeller/renderer/backend/gles/pipeline_gles.cc
@@ -8,23 +8,19 @@
 
 PipelineGLES::PipelineGLES(ReactorGLES::Ref reactor,
                            std::weak_ptr<PipelineLibrary> library,
-                           const PipelineDescriptor& desc)
+                           const PipelineDescriptor& desc,
+                           std::shared_ptr<UniqueHandleGLES> handle)
     : Pipeline(std::move(library), desc),
       reactor_(std::move(reactor)),
-      handle_(reactor_ ? reactor_->CreateHandle(HandleType::kProgram)
-                       : HandleGLES::DeadHandle()),
-      is_valid_(!handle_.IsDead()) {
+      handle_(std::move(handle)),
+      is_valid_(handle_->IsValid()) {
   if (is_valid_) {
-    reactor_->SetDebugLabel(handle_, GetDescriptor().GetLabel());
+    reactor_->SetDebugLabel(handle_->Get(), GetDescriptor().GetLabel());
   }
 }
 
 // |Pipeline|
-PipelineGLES::~PipelineGLES() {
-  if (!handle_.IsDead()) {
-    reactor_->CollectHandle(handle_);
-  }
-}
+PipelineGLES::~PipelineGLES() = default;
 
 // |Pipeline|
 bool PipelineGLES::IsValid() const {
@@ -32,6 +28,10 @@
 }
 
 const HandleGLES& PipelineGLES::GetProgramHandle() const {
+  return handle_->Get();
+}
+
+const std::shared_ptr<UniqueHandleGLES> PipelineGLES::GetSharedHandle() const {
   return handle_;
 }
 
@@ -58,10 +58,10 @@
 }
 
 [[nodiscard]] bool PipelineGLES::BindProgram() const {
-  if (handle_.IsDead()) {
+  if (!handle_->IsValid()) {
     return false;
   }
-  auto handle = reactor_->GetGLHandle(handle_);
+  auto handle = reactor_->GetGLHandle(handle_->Get());
   if (!handle.has_value()) {
     return false;
   }
diff --git a/impeller/renderer/backend/gles/pipeline_gles.h b/impeller/renderer/backend/gles/pipeline_gles.h
index c4f0aeb..e8a3ae8 100644
--- a/impeller/renderer/backend/gles/pipeline_gles.h
+++ b/impeller/renderer/backend/gles/pipeline_gles.h
@@ -7,8 +7,8 @@
 
 #include "impeller/base/backend_cast.h"
 #include "impeller/renderer/backend/gles/buffer_bindings_gles.h"
-#include "impeller/renderer/backend/gles/handle_gles.h"
 #include "impeller/renderer/backend/gles/reactor_gles.h"
+#include "impeller/renderer/backend/gles/unique_handle_gles.h"
 #include "impeller/renderer/pipeline.h"
 
 namespace impeller {
@@ -24,6 +24,8 @@
 
   const HandleGLES& GetProgramHandle() const;
 
+  const std::shared_ptr<UniqueHandleGLES> GetSharedHandle() const;
+
   [[nodiscard]] bool BindProgram() const;
 
   [[nodiscard]] bool UnbindProgram() const;
@@ -37,7 +39,7 @@
   friend PipelineLibraryGLES;
 
   ReactorGLES::Ref reactor_;
-  HandleGLES handle_;
+  std::shared_ptr<UniqueHandleGLES> handle_;
   std::unique_ptr<BufferBindingsGLES> buffer_bindings_;
   bool is_valid_ = false;
 
@@ -46,7 +48,8 @@
 
   PipelineGLES(ReactorGLES::Ref reactor,
                std::weak_ptr<PipelineLibrary> library,
-               const PipelineDescriptor& desc);
+               const PipelineDescriptor& desc,
+               std::shared_ptr<UniqueHandleGLES> handle);
 
   PipelineGLES(const PipelineGLES&) = delete;
 
diff --git a/impeller/renderer/backend/gles/pipeline_library_gles.cc b/impeller/renderer/backend/gles/pipeline_library_gles.cc
index b730f16..5ab1ee5 100644
--- a/impeller/renderer/backend/gles/pipeline_library_gles.cc
+++ b/impeller/renderer/backend/gles/pipeline_library_gles.cc
@@ -179,6 +179,79 @@
   return reactor_ != nullptr;
 }
 
+std::shared_ptr<PipelineGLES> PipelineLibraryGLES::CreatePipeline(
+    const std::weak_ptr<PipelineLibrary>& weak_library,
+    const PipelineDescriptor& desc,
+    const std::shared_ptr<const ShaderFunction>& vert_function,
+    const std::shared_ptr<const ShaderFunction>& frag_function) {
+  auto strong_library = weak_library.lock();
+
+  if (!strong_library) {
+    VALIDATION_LOG << "Library was collected before a pending pipeline "
+                      "creation could finish.";
+    return nullptr;
+  }
+
+  auto& library = PipelineLibraryGLES::Cast(*strong_library);
+
+  const auto& reactor = library.GetReactor();
+
+  if (!reactor) {
+    return nullptr;
+  }
+
+  auto program_key = ProgramKey{vert_function, frag_function,
+                                desc.GetSpecializationConstants()};
+
+  auto cached_program = library.GetProgramForKey(program_key);
+
+  const auto has_cached_program = !!cached_program;
+
+  auto pipeline = std::shared_ptr<PipelineGLES>(new PipelineGLES(
+      reactor,       //
+      weak_library,  //
+      desc,          //
+      has_cached_program
+          ? std::move(cached_program)
+          : std::make_shared<UniqueHandleGLES>(reactor, HandleType::kProgram)));
+
+  auto program = reactor->GetGLHandle(pipeline->GetProgramHandle());
+
+  if (!program.has_value()) {
+    VALIDATION_LOG << "Could not obtain program handle.";
+    return nullptr;
+  }
+
+  const auto link_result = !has_cached_program ? LinkProgram(*reactor,       //
+                                                             pipeline,       //
+                                                             vert_function,  //
+                                                             frag_function   //
+                                                             )
+                                               : true;
+
+  if (!link_result) {
+    VALIDATION_LOG << "Could not link pipeline program.";
+    return nullptr;
+  }
+
+  if (!pipeline->BuildVertexDescriptor(reactor->GetProcTable(),
+                                       program.value())) {
+    VALIDATION_LOG << "Could not build pipeline vertex descriptors.";
+    return nullptr;
+  }
+
+  if (!pipeline->IsValid()) {
+    VALIDATION_LOG << "Pipeline validation checks failed.";
+    return nullptr;
+  }
+
+  if (!has_cached_program) {
+    library.SetProgramForKey(program_key, pipeline->GetSharedHandle());
+  }
+
+  return pipeline;
+}
+
 // |PipelineLibrary|
 PipelineFuture<PipelineDescriptor> PipelineLibraryGLES::GetPipeline(
     PipelineDescriptor descriptor,
@@ -209,49 +282,16 @@
   auto pipeline_future =
       PipelineFuture<PipelineDescriptor>{descriptor, promise->get_future()};
   pipelines_[descriptor] = pipeline_future;
-  auto weak_this = weak_from_this();
 
-  auto result = reactor_->AddOperation(
-      [promise, weak_this, reactor_ptr = reactor_, descriptor, vert_function,
-       frag_function](const ReactorGLES& reactor) {
-        auto strong_this = weak_this.lock();
-        if (!strong_this) {
-          promise->set_value(nullptr);
-          VALIDATION_LOG << "Library was collected before a pending pipeline "
-                            "creation could finish.";
-          return;
-        }
-        auto pipeline = std::shared_ptr<PipelineGLES>(
-            new PipelineGLES(reactor_ptr, strong_this, descriptor));
-        auto program = reactor.GetGLHandle(pipeline->GetProgramHandle());
-        if (!program.has_value()) {
-          promise->set_value(nullptr);
-          VALIDATION_LOG << "Could not obtain program handle.";
-          return;
-        }
-        const auto link_result = LinkProgram(reactor,        //
-                                             pipeline,       //
-                                             vert_function,  //
-                                             frag_function   //
-        );
-        if (!link_result) {
-          promise->set_value(nullptr);
-          VALIDATION_LOG << "Could not link pipeline program.";
-          return;
-        }
-        if (!pipeline->BuildVertexDescriptor(reactor.GetProcTable(),
-                                             program.value())) {
-          promise->set_value(nullptr);
-          VALIDATION_LOG << "Could not build pipeline vertex descriptors.";
-          return;
-        }
-        if (!pipeline->IsValid()) {
-          promise->set_value(nullptr);
-          VALIDATION_LOG << "Pipeline validation checks failed.";
-          return;
-        }
-        promise->set_value(std::move(pipeline));
-      });
+  const auto result = reactor_->AddOperation([promise,                       //
+                                              weak_this = weak_from_this(),  //
+                                              descriptor,                    //
+                                              vert_function,                 //
+                                              frag_function                  //
+  ](const ReactorGLES& reactor) {
+    promise->set_value(
+        CreatePipeline(weak_this, descriptor, vert_function, frag_function));
+  });
   FML_CHECK(result);
 
   return pipeline_future;
@@ -280,4 +320,25 @@
 // |PipelineLibrary|
 PipelineLibraryGLES::~PipelineLibraryGLES() = default;
 
+const ReactorGLES::Ref& PipelineLibraryGLES::GetReactor() const {
+  return reactor_;
+}
+
+std::shared_ptr<UniqueHandleGLES> PipelineLibraryGLES::GetProgramForKey(
+    const ProgramKey& key) {
+  Lock lock(programs_mutex_);
+  auto found = programs_.find(key);
+  if (found != programs_.end()) {
+    return found->second;
+  }
+  return nullptr;
+}
+
+void PipelineLibraryGLES::SetProgramForKey(
+    const ProgramKey& key,
+    std::shared_ptr<UniqueHandleGLES> program) {
+  Lock lock(programs_mutex_);
+  programs_[key] = std::move(program);
+}
+
 }  // namespace impeller
diff --git a/impeller/renderer/backend/gles/pipeline_library_gles.h b/impeller/renderer/backend/gles/pipeline_library_gles.h
index 1ee7c22..99d5da6 100644
--- a/impeller/renderer/backend/gles/pipeline_library_gles.h
+++ b/impeller/renderer/backend/gles/pipeline_library_gles.h
@@ -5,23 +5,92 @@
 #ifndef FLUTTER_IMPELLER_RENDERER_BACKEND_GLES_PIPELINE_LIBRARY_GLES_H_
 #define FLUTTER_IMPELLER_RENDERER_BACKEND_GLES_PIPELINE_LIBRARY_GLES_H_
 
+#include <unordered_map>
+#include <vector>
+
+#include "flutter/fml/hash_combine.h"
+#include "impeller/base/thread.h"
 #include "impeller/renderer/backend/gles/reactor_gles.h"
+#include "impeller/renderer/backend/gles/unique_handle_gles.h"
 #include "impeller/renderer/pipeline_library.h"
+#include "impeller/renderer/shader_function.h"
 
 namespace impeller {
 
 class ContextGLES;
+class PipelineGLES;
 
-class PipelineLibraryGLES final : public PipelineLibrary {
+class PipelineLibraryGLES final
+    : public PipelineLibrary,
+      public BackendCast<PipelineLibraryGLES, PipelineLibrary> {
  public:
   // |PipelineLibrary|
   ~PipelineLibraryGLES() override;
 
+  PipelineLibraryGLES(const PipelineLibraryGLES&) = delete;
+
+  PipelineLibraryGLES& operator=(const PipelineLibraryGLES&) = delete;
+
  private:
   friend ContextGLES;
 
+  //----------------------------------------------------------------------------
+  /// @brief      A subset of the items in a pipeline descriptor (and the items
+  ///             they reference in shader libraries) whose dynamism requires a
+  ///             program object re-compilation and link. In all other cases,
+  ///             creating a pipeline variant reuses an existing (compatible)
+  ///             program object.
+  ///
+  struct ProgramKey {
+    std::shared_ptr<const ShaderFunction> vertex_shader;
+    std::shared_ptr<const ShaderFunction> fragment_shader;
+    //--------------------------------------------------------------------------
+    /// Specialization constants used in the shaders affect defines used when
+    /// compiling and linking the program.
+    ///
+    std::vector<Scalar> specialization_constants;
+
+    ProgramKey(std::shared_ptr<const ShaderFunction> p_vertex_shader,
+               std::shared_ptr<const ShaderFunction> p_fragment_shader,
+               std::vector<Scalar> p_specialization_constants)
+        : vertex_shader(std::move(p_vertex_shader)),
+          fragment_shader(std::move(p_fragment_shader)),
+          specialization_constants(std::move(p_specialization_constants)) {}
+
+    struct Hash {
+      std::size_t operator()(const ProgramKey& key) const {
+        auto seed = fml::HashCombine();
+        if (key.vertex_shader) {
+          fml::HashCombineSeed(seed, key.vertex_shader->GetHash());
+        }
+        if (key.fragment_shader) {
+          fml::HashCombineSeed(seed, key.fragment_shader->GetHash());
+        }
+        for (const auto& constant : key.specialization_constants) {
+          fml::HashCombineSeed(seed, constant);
+        }
+        return seed;
+      }
+    };
+
+    struct Equal {
+      bool operator()(const ProgramKey& lhs, const ProgramKey& rhs) const {
+        return DeepComparePointer(lhs.vertex_shader, rhs.vertex_shader) &&
+               DeepComparePointer(lhs.fragment_shader, rhs.fragment_shader) &&
+               lhs.specialization_constants == rhs.specialization_constants;
+      }
+    };
+  };
+
+  using ProgramMap = std::unordered_map<ProgramKey,
+                                        std::shared_ptr<UniqueHandleGLES>,
+                                        ProgramKey::Hash,
+                                        ProgramKey::Equal>;
+
   ReactorGLES::Ref reactor_;
   PipelineMap pipelines_;
+  Mutex programs_mutex_;
+  ProgramMap programs_ IPLR_GUARDED_BY(programs_mutex_);
 
   explicit PipelineLibraryGLES(ReactorGLES::Ref reactor);
 
@@ -41,9 +110,18 @@
   void RemovePipelinesWithEntryPoint(
       std::shared_ptr<const ShaderFunction> function) override;
 
-  PipelineLibraryGLES(const PipelineLibraryGLES&) = delete;
+  const ReactorGLES::Ref& GetReactor() const;
 
-  PipelineLibraryGLES& operator=(const PipelineLibraryGLES&) = delete;
+  static std::shared_ptr<PipelineGLES> CreatePipeline(
+      const std::weak_ptr<PipelineLibrary>& weak_library,
+      const PipelineDescriptor& desc,
+      const std::shared_ptr<const ShaderFunction>& vert_shader,
+      const std::shared_ptr<const ShaderFunction>& frag_shader);
+
+  std::shared_ptr<UniqueHandleGLES> GetProgramForKey(const ProgramKey& key);
+
+  void SetProgramForKey(const ProgramKey& key,
+                        std::shared_ptr<UniqueHandleGLES> program);
 };
 
 }  // namespace impeller
diff --git a/impeller/renderer/backend/gles/test/pipeline_library_gles_unittests.cc b/impeller/renderer/backend/gles/test/pipeline_library_gles_unittests.cc
new file mode 100644
index 0000000..23d54c4
--- /dev/null
+++ b/impeller/renderer/backend/gles/test/pipeline_library_gles_unittests.cc
@@ -0,0 +1,71 @@
+// 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.
+
+#include "impeller/fixtures/spec_constant.frag.h"
+#include "impeller/fixtures/spec_constant.vert.h"
+#include "impeller/playground/playground_test.h"
+#include "impeller/renderer/backend/gles/pipeline_gles.h"
+#include "impeller/renderer/backend/gles/pipeline_library_gles.h"
+#include "impeller/renderer/pipeline_library.h"
+
+namespace impeller::testing {
+
+using PipelineLibraryGLESTest = PlaygroundTest;
+INSTANTIATE_OPENGLES_PLAYGROUND_SUITE(PipelineLibraryGLESTest);
+
+// NOLINTBEGIN(bugprone-unchecked-optional-access)
+TEST_P(PipelineLibraryGLESTest, ProgramHandlesAreReused) {
+  using VS = SpecConstantVertexShader;
+  using FS = SpecConstantFragmentShader;
+  auto context = GetContext();
+  ASSERT_TRUE(context);
+  auto desc = PipelineBuilder<VS, FS>::MakeDefaultPipelineDescriptor(*context);
+  ASSERT_TRUE(desc.has_value());
+  auto pipeline = context->GetPipelineLibrary()->GetPipeline(desc).Get();
+  ASSERT_TRUE(pipeline && pipeline->IsValid());
+  auto new_desc = desc;
+  // Changing the sample counts should not result in a new program object.
+  new_desc->SetSampleCount(SampleCount::kCount4);
+  // Make sure we don't hit the top-level descriptor cache. This will cause
+  // caching irrespective of backends.
+  ASSERT_FALSE(desc->IsEqual(new_desc.value()));
+  auto new_pipeline =
+      context->GetPipelineLibrary()->GetPipeline(new_desc).Get();
+  ASSERT_TRUE(new_pipeline && new_pipeline->IsValid());
+  const auto& pipeline_gles = PipelineGLES::Cast(*pipeline);
+  const auto& new_pipeline_gles = PipelineGLES::Cast(*new_pipeline);
+  // The program handles should be live and equal.
+  ASSERT_FALSE(pipeline_gles.GetProgramHandle().IsDead());
+  ASSERT_FALSE(new_pipeline_gles.GetProgramHandle().IsDead());
+  ASSERT_EQ(pipeline_gles.GetProgramHandle().name.value(),
+            new_pipeline_gles.GetProgramHandle().name.value());
+}
+
+TEST_P(PipelineLibraryGLESTest, ChangingSpecConstantsCausesNewProgramObject) {
+  using VS = SpecConstantVertexShader;
+  using FS = SpecConstantFragmentShader;
+  auto context = GetContext();
+  ASSERT_TRUE(context);
+  auto desc = PipelineBuilder<VS, FS>::MakeDefaultPipelineDescriptor(*context);
+  ASSERT_TRUE(desc.has_value());
+  desc->SetSpecializationConstants({2.0f});
+  auto pipeline = context->GetPipelineLibrary()->GetPipeline(desc).Get();
+  ASSERT_TRUE(pipeline && pipeline->IsValid());
+  auto new_desc = desc;
+  // Changing the spec. constants should result in a new program object.
+  new_desc->SetSpecializationConstants({4.0f});
+  auto new_pipeline =
+      context->GetPipelineLibrary()->GetPipeline(new_desc).Get();
+  ASSERT_TRUE(new_pipeline && new_pipeline->IsValid());
+  const auto& pipeline_gles = PipelineGLES::Cast(*pipeline);
+  const auto& new_pipeline_gles = PipelineGLES::Cast(*new_pipeline);
+  // The program handles should be live and equal.
+  ASSERT_FALSE(pipeline_gles.GetProgramHandle().IsDead());
+  ASSERT_FALSE(new_pipeline_gles.GetProgramHandle().IsDead());
+  ASSERT_FALSE(pipeline_gles.GetProgramHandle().name.value() ==
+               new_pipeline_gles.GetProgramHandle().name.value());
+}
+// NOLINTEND(bugprone-unchecked-optional-access)
+
+}  // namespace impeller::testing
diff --git a/impeller/renderer/backend/gles/unique_handle_gles.cc b/impeller/renderer/backend/gles/unique_handle_gles.cc
new file mode 100644
index 0000000..ddf4081
--- /dev/null
+++ b/impeller/renderer/backend/gles/unique_handle_gles.cc
@@ -0,0 +1,40 @@
+// 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.
+
+#include "impeller/renderer/backend/gles/unique_handle_gles.h"
+
+#include <utility>
+
+namespace impeller {
+
+UniqueHandleGLES::UniqueHandleGLES(ReactorGLES::Ref reactor, HandleType type)
+    : reactor_(std::move(reactor)) {
+  if (reactor_) {
+    handle_ = reactor_->CreateHandle(type);
+  }
+}
+
+UniqueHandleGLES::UniqueHandleGLES(ReactorGLES::Ref reactor, HandleGLES handle)
+    : reactor_(std::move(reactor)), handle_(handle) {}
+
+UniqueHandleGLES::~UniqueHandleGLES() {
+  if (!handle_.IsDead() && reactor_) {
+    reactor_->CollectHandle(handle_);
+  }
+}
+
+const HandleGLES& UniqueHandleGLES::Get() const {
+  return handle_;
+}
+
+bool UniqueHandleGLES::IsValid() const {
+  return !handle_.IsDead();
+}
+
+UniqueHandleGLES::UniqueHandleGLES(UniqueHandleGLES&& other) {
+  std::swap(reactor_, other.reactor_);
+  std::swap(handle_, other.handle_);
+}
+
+}  // namespace impeller
diff --git a/impeller/renderer/backend/gles/unique_handle_gles.h b/impeller/renderer/backend/gles/unique_handle_gles.h
new file mode 100644
index 0000000..91c3bc2
--- /dev/null
+++ b/impeller/renderer/backend/gles/unique_handle_gles.h
@@ -0,0 +1,43 @@
+// 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.
+
+#ifndef FLUTTER_IMPELLER_RENDERER_BACKEND_GLES_UNIQUE_HANDLE_GLES_H_
+#define FLUTTER_IMPELLER_RENDERER_BACKEND_GLES_UNIQUE_HANDLE_GLES_H_
+
+#include "impeller/renderer/backend/gles/handle_gles.h"
+#include "impeller/renderer/backend/gles/reactor_gles.h"
+
+namespace impeller {
+
+//------------------------------------------------------------------------------
+/// @brief      A unique handle to an OpenGL object. The collection of this
+///             handle scheduled the destruction of the associated OpenGL object
+///             in the reactor.
+///
+class UniqueHandleGLES {
+ public:
+  UniqueHandleGLES(ReactorGLES::Ref reactor, HandleType type);
+
+  UniqueHandleGLES(ReactorGLES::Ref reactor, HandleGLES handle);
+
+  ~UniqueHandleGLES();
+
+  UniqueHandleGLES(UniqueHandleGLES&&);
+
+  UniqueHandleGLES(const UniqueHandleGLES&) = delete;
+
+  UniqueHandleGLES& operator=(const UniqueHandleGLES&) = delete;
+
+  const HandleGLES& Get() const;
+
+  bool IsValid() const;
+
+ private:
+  ReactorGLES::Ref reactor_ = nullptr;
+  HandleGLES handle_ = HandleGLES::DeadHandle();
+};
+
+}  // namespace impeller
+
+#endif  // FLUTTER_IMPELLER_RENDERER_BACKEND_GLES_UNIQUE_HANDLE_GLES_H_