Start wiring up fragment program for OpenGLES (#49347)

Makes fragment programs work for OpenGLES backend.

Fixes https://github.com/flutter/flutter/issues/113715
Fixes https://github.com/flutter/flutter/issues/105538
I cannot find a dedicated issue for this, but there probably is someone somewhere and I don't want to file a new one for it.
diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc
index b7a7e0d..61ddc20 100644
--- a/impeller/aiks/aiks_unittests.cc
+++ b/impeller/aiks/aiks_unittests.cc
@@ -3033,13 +3033,15 @@
 
 // Regression test for https://github.com/flutter/flutter/issues/126701 .
 TEST_P(AiksTest, CanRenderClippedRuntimeEffects) {
-  if (GetParam() != PlaygroundBackend::kMetal) {
+  if (!BackendSupportsFragmentProgram()) {
     GTEST_SKIP_("This backend doesn't support runtime effects.");
   }
 
   auto runtime_stages =
       OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
-  auto runtime_stage = runtime_stages[RuntimeStageBackend::kMetal];
+
+  auto runtime_stage =
+      runtime_stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())];
   ASSERT_TRUE(runtime_stage);
   ASSERT_TRUE(runtime_stage->IsDirty());
 
@@ -3068,7 +3070,7 @@
 }
 
 TEST_P(AiksTest, DrawPaintTransformsBounds) {
-  if (GetParam() != PlaygroundBackend::kMetal) {
+  if (!BackendSupportsFragmentProgram()) {
     GTEST_SKIP_("This backend doesn't support runtime effects.");
   }
 
diff --git a/impeller/core/shader_types.h b/impeller/core/shader_types.h
index 8b267db..25c405b 100644
--- a/impeller/core/shader_types.h
+++ b/impeller/core/shader_types.h
@@ -70,6 +70,7 @@
 };
 
 struct ShaderMetadata {
+  // This must match the uniform name in the shader program.
   std::string name;
   std::vector<ShaderStructMemberMetadata> members;
 };
diff --git a/impeller/entity/contents/runtime_effect_contents.cc b/impeller/entity/contents/runtime_effect_contents.cc
index 82ad350..a2a5244 100644
--- a/impeller/entity/contents/runtime_effect_contents.cc
+++ b/impeller/entity/contents/runtime_effect_contents.cc
@@ -11,6 +11,7 @@
 #include "flutter/fml/make_copyable.h"
 #include "impeller/base/validation.h"
 #include "impeller/core/formats.h"
+#include "impeller/core/runtime_types.h"
 #include "impeller/core/shader_types.h"
 #include "impeller/entity/contents/clip_contents.h"
 #include "impeller/entity/contents/content_context.h"
@@ -41,16 +42,53 @@
   return false;
 }
 
+static ShaderType GetShaderType(RuntimeUniformType type) {
+  switch (type) {
+    case kSampledImage:
+      return ShaderType::kSampledImage;
+    case kFloat:
+      return ShaderType::kFloat;
+    case kBoolean:
+    case kSignedByte:
+    case kUnsignedByte:
+    case kSignedShort:
+    case kUnsignedShort:
+    case kSignedInt:
+    case kUnsignedInt:
+    case kSignedInt64:
+    case kUnsignedInt64:
+    case kHalfFloat:
+    case kDouble:
+      VALIDATION_LOG << "Unsupported uniform type.";
+      return ShaderType::kVoid;
+  }
+}
+
+static std::shared_ptr<ShaderMetadata> MakeShaderMetadata(
+    const RuntimeUniformDescription& uniform) {
+  auto metadata = std::make_shared<ShaderMetadata>();
+  metadata->name = uniform.name;
+  metadata->members.emplace_back(ShaderStructMemberMetadata{
+      .type = GetShaderType(uniform.type),
+      .size = uniform.GetSize(),
+      .byte_length = uniform.bit_width / 8,
+  });
+
+  return metadata;
+}
+
 bool RuntimeEffectContents::Render(const ContentContext& renderer,
                                    const Entity& entity,
                                    RenderPass& pass) const {
-// TODO(jonahwilliams): FragmentProgram API is not fully wired up on Android.
-// Disable until this is complete so that integration tests and benchmarks can
-// run m3 applications.
-#ifdef FML_OS_ANDROID
-  return true;
-#else
-
+  // TODO(jonahwilliams): FragmentProgram API is not fully wired up on Android.
+  // Disable until this is complete so that integration tests and benchmarks can
+  // run m3 applications.
+  if (renderer.GetContext()->GetBackendType() ==
+      Context::BackendType::kVulkan) {
+    FML_DLOG(WARNING)
+        << "Fragment programs not supported on Vulkan. Content dropped.";
+    return true;
+  }
   auto context = renderer.GetContext();
   auto library = context->GetShaderLibrary();
 
@@ -172,10 +210,7 @@
   size_t buffer_index = 0;
   size_t buffer_offset = 0;
   for (const auto& uniform : runtime_stage_->GetUniforms()) {
-    // TODO(113715): Populate this metadata once GLES is able to handle
-    //               non-struct uniform names.
-    std::shared_ptr<ShaderMetadata> metadata =
-        std::make_shared<ShaderMetadata>();
+    std::shared_ptr<ShaderMetadata> metadata = MakeShaderMetadata(uniform);
 
     switch (uniform.type) {
       case kSampledImage: {
@@ -226,9 +261,7 @@
 
   size_t sampler_index = 0;
   for (const auto& uniform : runtime_stage_->GetUniforms()) {
-    // TODO(113715): Populate this metadata once GLES is able to handle
-    //               non-struct uniform names.
-    ShaderMetadata metadata;
+    std::shared_ptr<ShaderMetadata> metadata = MakeShaderMetadata(uniform);
 
     switch (uniform.type) {
       case kSampledImage: {
@@ -241,7 +274,7 @@
         SampledImageSlot image_slot;
         image_slot.name = uniform.name.c_str();
         image_slot.texture_index = uniform.location - minimum_sampler_index;
-        cmd.BindResource(ShaderStage::kFragment, image_slot, metadata,
+        cmd.BindResource(ShaderStage::kFragment, image_slot, *metadata,
                          input.texture, sampler);
 
         sampler_index++;
@@ -260,7 +293,6 @@
     return restore.Render(renderer, entity, pass);
   }
   return true;
-#endif  // FML_OS_ANDROID
 }
 
 }  // namespace impeller
diff --git a/impeller/entity/entity_unittests.cc b/impeller/entity/entity_unittests.cc
index 4cb0c41..91d6b6a 100644
--- a/impeller/entity/entity_unittests.cc
+++ b/impeller/entity/entity_unittests.cc
@@ -2132,13 +2132,14 @@
 }
 
 TEST_P(EntityTest, RuntimeEffect) {
-  if (GetParam() != PlaygroundBackend::kMetal) {
+  if (!BackendSupportsFragmentProgram()) {
     GTEST_SKIP_("This backend doesn't support runtime effects.");
   }
 
   auto runtime_stages =
       OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
-  auto runtime_stage = runtime_stages[RuntimeStageBackend::kMetal];
+  auto runtime_stage =
+      runtime_stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())];
   ASSERT_TRUE(runtime_stage);
   ASSERT_TRUE(runtime_stage->IsDirty());
 
diff --git a/impeller/fixtures/BUILD.gn b/impeller/fixtures/BUILD.gn
index 1867b65..bce07ac 100644
--- a/impeller/fixtures/BUILD.gn
+++ b/impeller/fixtures/BUILD.gn
@@ -60,7 +60,14 @@
     "gradient.frag",
   ]
   sl_file_extension = "iplr"
-  shader_target_flag = "--runtime-stage-metal"
+
+  # TODO(dnfield): Make impellerc able to bundle multiple non-SkSL shaders.
+  # https://github.com/flutter/flutter/issues/140817
+  if (is_ios || is_mac) {
+    shader_target_flag = "--runtime-stage-metal"
+  } else {
+    shader_target_flag = "--runtime-stage-gles"
+  }
   iplr = true
 }
 
diff --git a/impeller/golden_tests/golden_playground_test.h b/impeller/golden_tests/golden_playground_test.h
index f411615..e4fdde8 100644
--- a/impeller/golden_tests/golden_playground_test.h
+++ b/impeller/golden_tests/golden_playground_test.h
@@ -36,6 +36,12 @@
 
   PlaygroundBackend GetBackend() const;
 
+  // TODO(dnfield): Delete this once
+  // https://github.com/flutter/flutter/issues/122823 is fixed.
+  bool BackendSupportsFragmentProgram() const {
+    return GetBackend() != PlaygroundBackend::kVulkan;
+  }
+
   void SetTypographerContext(
       std::shared_ptr<TypographerContext> typographer_context);
 
diff --git a/impeller/playground/playground.h b/impeller/playground/playground.h
index 57e6b88..6f8f37f 100644
--- a/impeller/playground/playground.h
+++ b/impeller/playground/playground.h
@@ -12,6 +12,7 @@
 #include "flutter/fml/macros.h"
 #include "flutter/fml/status.h"
 #include "flutter/fml/time/time_delta.h"
+#include "impeller/core/runtime_types.h"
 #include "impeller/core/texture.h"
 #include "impeller/geometry/point.h"
 #include "impeller/image/compressed_image.h"
@@ -30,6 +31,19 @@
   kVulkan,
 };
 
+constexpr inline RuntimeStageBackend PlaygroundBackendToRuntimeStageBackend(
+    PlaygroundBackend backend) {
+  switch (backend) {
+    case PlaygroundBackend::kMetal:
+      return RuntimeStageBackend::kMetal;
+    case PlaygroundBackend::kOpenGLES:
+      return RuntimeStageBackend::kOpenGLES;
+    case PlaygroundBackend::kVulkan:
+      return RuntimeStageBackend::kVulkan;
+  }
+  FML_UNREACHABLE();
+}
+
 std::string PlaygroundBackendToString(PlaygroundBackend backend);
 
 class Playground {
diff --git a/impeller/playground/playground_test.h b/impeller/playground/playground_test.h
index e2051a2..65ac08e 100644
--- a/impeller/playground/playground_test.h
+++ b/impeller/playground/playground_test.h
@@ -42,6 +42,12 @@
   // |Playground|
   std::string GetWindowTitle() const override;
 
+  // TODO(dnfield): Delete this once
+  // https://github.com/flutter/flutter/issues/122823 is fixed.
+  bool BackendSupportsFragmentProgram() const {
+    return GetBackend() != PlaygroundBackend::kVulkan;
+  }
+
  private:
   // |Playground|
   bool ShouldKeepRendering() const;
diff --git a/impeller/renderer/backend/gles/buffer_bindings_gles.cc b/impeller/renderer/backend/gles/buffer_bindings_gles.cc
index 696ab62..899cbbb 100644
--- a/impeller/renderer/backend/gles/buffer_bindings_gles.cc
+++ b/impeller/renderer/backend/gles/buffer_bindings_gles.cc
@@ -65,8 +65,10 @@
   std::string result;
   result.reserve(struct_name.length() + member.length() + (is_array ? 4 : 1));
   result += struct_name;
-  result += '.';
-  result += member;
+  if (!member.empty()) {
+    result += '.';
+    result += member;
+  }
   if (is_array) {
     result += "[0]";
   }
@@ -312,6 +314,9 @@
             );
             continue;
         }
+        VALIDATION_LOG << "Size " << member.size
+                       << " could not be mapped ShaderType::kFloat for key: "
+                       << member.name;
       case ShaderType::kBoolean:
       case ShaderType::kSignedByte:
       case ShaderType::kUnsignedByte:
diff --git a/impeller/runtime_stage/runtime_stage_unittests.cc b/impeller/runtime_stage/runtime_stage_unittests.cc
index f5b95aa..082b4a4 100644
--- a/impeller/runtime_stage/runtime_stage_unittests.cc
+++ b/impeller/runtime_stage/runtime_stage_unittests.cc
@@ -23,18 +23,26 @@
 using RuntimeStageTest = RuntimeStagePlayground;
 INSTANTIATE_PLAYGROUND_SUITE(RuntimeStageTest);
 
-TEST(RuntimeStageTest, CanReadValidBlob) {
+TEST_P(RuntimeStageTest, CanReadValidBlob) {
+  if (!BackendSupportsFragmentProgram()) {
+    GTEST_SKIP_("This backend doesn't support runtime effects.");
+  }
+
   const std::shared_ptr<fml::Mapping> fixture =
       flutter::testing::OpenFixtureAsMapping("ink_sparkle.frag.iplr");
   ASSERT_TRUE(fixture);
   ASSERT_GT(fixture->GetSize(), 0u);
   auto stages = RuntimeStage::DecodeRuntimeStages(fixture);
-  auto stage = stages[RuntimeStageBackend::kMetal];
+  auto stage = stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())];
   ASSERT_TRUE(stage->IsValid());
   ASSERT_EQ(stage->GetShaderStage(), RuntimeShaderStage::kFragment);
 }
 
-TEST(RuntimeStageTest, CanRejectInvalidBlob) {
+TEST_P(RuntimeStageTest, CanRejectInvalidBlob) {
+  if (!BackendSupportsFragmentProgram()) {
+    GTEST_SKIP_("This backend doesn't support runtime effects.");
+  }
+
   ScopedValidationDisable disable_validation;
   const std::shared_ptr<fml::Mapping> fixture =
       flutter::testing::OpenFixtureAsMapping("ink_sparkle.frag.iplr");
@@ -46,16 +54,20 @@
   ::memset(junk_allocation->GetBuffer(), 127, junk_allocation->GetLength());
   auto stages = RuntimeStage::DecodeRuntimeStages(
       CreateMappingFromAllocation(junk_allocation));
-  ASSERT_FALSE(stages[RuntimeStageBackend::kMetal]);
+  ASSERT_FALSE(stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())]);
 }
 
-TEST(RuntimeStageTest, CanReadUniforms) {
+TEST_P(RuntimeStageTest, CanReadUniforms) {
+  if (!BackendSupportsFragmentProgram()) {
+    GTEST_SKIP_("This backend doesn't support runtime effects.");
+  }
+
   const std::shared_ptr<fml::Mapping> fixture =
       flutter::testing::OpenFixtureAsMapping("ink_sparkle.frag.iplr");
   ASSERT_TRUE(fixture);
   ASSERT_GT(fixture->GetSize(), 0u);
   auto stages = RuntimeStage::DecodeRuntimeStages(fixture);
-  auto stage = stages[RuntimeStageBackend::kMetal];
+  auto stage = stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())];
 
   ASSERT_TRUE(stage->IsValid());
   ASSERT_EQ(stage->GetUniforms().size(), 17u);
@@ -191,15 +203,16 @@
 }
 
 TEST_P(RuntimeStageTest, CanRegisterStage) {
-  if (GetParam() != PlaygroundBackend::kMetal) {
-    GTEST_SKIP_("Skipped: https://github.com/flutter/flutter/issues/105538");
+  if (!BackendSupportsFragmentProgram()) {
+    GTEST_SKIP_("This backend doesn't support runtime effects.");
   }
+
   const std::shared_ptr<fml::Mapping> fixture =
       flutter::testing::OpenFixtureAsMapping("ink_sparkle.frag.iplr");
   ASSERT_TRUE(fixture);
   ASSERT_GT(fixture->GetSize(), 0u);
   auto stages = RuntimeStage::DecodeRuntimeStages(fixture);
-  auto stage = stages[RuntimeStageBackend::kMetal];
+  auto stage = stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())];
   ASSERT_TRUE(stage->IsValid());
   std::promise<bool> registration;
   auto future = registration.get_future();
@@ -229,11 +242,11 @@
 }
 
 TEST_P(RuntimeStageTest, CanCreatePipelineFromRuntimeStage) {
-  if (GetParam() != PlaygroundBackend::kMetal) {
-    GTEST_SKIP_("Skipped: https://github.com/flutter/flutter/issues/105538");
+  if (!BackendSupportsFragmentProgram()) {
+    GTEST_SKIP_("This backend doesn't support runtime effects.");
   }
   auto stages = OpenAssetAsRuntimeStage("ink_sparkle.frag.iplr");
-  auto stage = stages[RuntimeStageBackend::kMetal];
+  auto stage = stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())];
 
   ASSERT_TRUE(stage);
   ASSERT_NE(stage, nullptr);