[Impeller] Implement a MockGLES, that provides trampolines for ProcGLESTable (flutter/engine#46433)

Unblocks testing https://github.com/flutter/flutter/issues/135715. See
`README.md` for details.

I made one non-testing change to the proc_table itself, which is if a
function call will fail, we will print out what is about to fail in the
validation log. It was useful when debugging the test creation itself,
and it's only enabled when GL call checks are enabled anyway.

@gaaclarke I originally implemented it with `FML_THREAD_LOCAL`, but
figured doing a global lock essentially was the same thing, and would
prevent parallel test runs from stepping on each-other in weird ways?

/cc @chinmaygarde for visibility.
This commit is contained in:
Matan Lurey 2023-10-03 16:23:25 -06:00 committed by GitHub
parent 217689a9c0
commit b6dfcba94b
8 changed files with 331 additions and 1 deletions

View File

@ -154,6 +154,7 @@
../../../flutter/impeller/golden_tests_harvester/test
../../../flutter/impeller/image/README.md
../../../flutter/impeller/playground
../../../flutter/impeller/renderer/backend/gles/test
../../../flutter/impeller/renderer/backend/vulkan/blit_command_vk_unittests.cc
../../../flutter/impeller/renderer/backend/vulkan/command_encoder_vk_unittests.cc
../../../flutter/impeller/renderer/backend/vulkan/command_pool_vk_unittests.cc

View File

@ -109,6 +109,10 @@ impeller_component("impeller_unittests") {
deps += [ "//flutter/impeller/renderer/backend/vulkan:vulkan_unittests" ]
}
if (impeller_enable_opengles) {
deps += [ "//flutter/impeller/renderer/backend/gles:gles_unittests" ]
}
if (glfw_vulkan_library != "") {
deps += [
"//third_party/swiftshader",

View File

@ -2,6 +2,7 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//flutter/vulkan/config.gni")
import("../../../tools/impeller.gni")
config("gles_config") {
@ -10,6 +11,19 @@ config("gles_config") {
include_dirs = [ "//third_party/angle/include" ]
}
impeller_component("gles_unittests") {
testonly = true
sources = [
"test/mock_gles.cc",
"test/mock_gles.h",
"test/mock_gles_unittests.cc",
]
deps = [
":gles",
"//flutter/testing:testing_lib",
]
}
impeller_component("gles") {
public_configs = []

View File

@ -21,6 +21,9 @@ bool GLErrorIsFatal(GLenum value);
struct AutoErrorCheck {
const PFNGLGETERRORPROC error_fn;
// TODO(matanlurey) Change to string_view.
// https://github.com/flutter/flutter/issues/135922
const char* name;
AutoErrorCheck(PFNGLGETERRORPROC error, const char* name)
@ -49,6 +52,9 @@ template <class T>
struct GLProc {
using GLFunctionType = T;
// TODO(matanlurey) Change to string_view.
// https://github.com/flutter/flutter/issues/135922
//----------------------------------------------------------------------------
/// The name of the GL function.
///
@ -75,6 +81,11 @@ struct GLProc {
auto operator()(Args&&... args) const {
#ifdef IMPELLER_DEBUG
AutoErrorCheck error(error_fn, name);
// We check for the existence of extensions, and reset the function pointer
// but it's still called unconditionally below, and will segfault. This
// validation log will at least give us a hint as to what's going on.
FML_CHECK(IsAvailable()) << "GL function " << name << " is not available. "
<< "This is likely due to a missing extension.";
#endif // IMPELLER_DEBUG
#ifdef IMPELLER_TRACE_ALL_GL_CALLS
TRACE_EVENT0("impeller", name);
@ -85,7 +96,6 @@ struct GLProc {
constexpr bool IsAvailable() const { return function != nullptr; }
void Reset() {
name = nullptr;
function = nullptr;
error_fn = nullptr;
}
@ -200,6 +210,7 @@ class ProcTableGLES {
public:
using Resolver = std::function<void*(const char* function_name)>;
explicit ProcTableGLES(Resolver resolver);
ProcTableGLES(ProcTableGLES&& other) = default;
~ProcTableGLES();

View File

@ -0,0 +1,48 @@
# `MockGLES`
This directory contains a mock implementation of the GLES backend.
Most functions are implemented as no-ops, have a default implementation that is not configurable, or just record the call. The latter is useful for testing:
```cc
TEST(MockGLES, Example) {
// Creates a mock GLES implementation and sets it as the current one.
auto mock_gles = MockGLES::Init();
auto& gl = mock_gles->GetProcTable();
// Call the proc table methods as usual, or pass the proc table to a class
// that needs it.
gl.PushDebugGroupKHR(GL_DEBUG_SOURCE_APPLICATION_KHR, 0, -1, "test");
gl.PopDebugGroupKHR();
// Method names are recorded and can be inspected.
//
// Note that many built-ins, like glGetString, are not recorded (otherwise the // logs would be much bigger and less useful).
auto calls = mock_gles->GetCapturedCalls();
EXPECT_EQ(calls, std::vector<std::string>(
{"PushDebugGroupKHR", "PopDebugGroupKHR"}));
}
```
To add a new function, do the following:
1. Add a new top-level method to [`mock_gles.cc`](mock_gles.cc):
```cc
void glFooBar() {
recordCall("glFooBar");
}
```
2. Edit the `kMockResolver`, and add a new `else if` clause:
```diff
+ else if (strcmp(name, "glFooBar") == 0) {
+ return reinterpret_cast<void*>(&glFooBar);
} else {
return reinterpret_cast<void*>(&glDoNothing);
}
```
It's possible we'll want to add a more sophisticated mechanism for mocking
besides capturing calls, but this is a good start. PRs welcome!

View File

@ -0,0 +1,147 @@
// 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 <memory>
#include "GLES3/gl3.h"
#include "fml/logging.h"
#include "impeller/renderer/backend/gles/proc_table_gles.h"
#include "impeller/renderer/backend/gles/test/mock_gles.h"
namespace impeller {
namespace testing {
// OpenGLES is not thread safe.
//
// This mutex is used to ensure that only one test is using the mock at a time.
static std::mutex g_test_lock;
static std::weak_ptr<MockGLES> g_mock_gles;
// Has friend visibility into MockGLES to record calls.
void RecordGLCall(const char* name) {
if (auto mock_gles = g_mock_gles.lock()) {
mock_gles->RecordCall(name);
}
}
template <typename T, typename U>
struct CheckSameSignature : std::false_type {};
template <typename Ret, typename... Args>
struct CheckSameSignature<Ret(Args...), Ret(Args...)> : std::true_type {};
// This is a stub function that does nothing/records nothing.
void doNothing() {}
auto const kMockVendor = (unsigned char*)"MockGLES";
auto const kMockVersion = (unsigned char*)"3.0";
auto const kExtensions = std::vector<unsigned char*>{
(unsigned char*)"GL_KHR_debug" //
};
const unsigned char* mockGetString(GLenum name) {
switch (name) {
case GL_VENDOR:
return kMockVendor;
case GL_VERSION:
return kMockVersion;
case GL_SHADING_LANGUAGE_VERSION:
return kMockVersion;
default:
return (unsigned char*)"";
}
}
static_assert(CheckSameSignature<decltype(mockGetString), //
decltype(glGetString)>::value);
const unsigned char* mockGetStringi(GLenum name, GLuint index) {
switch (name) {
case GL_EXTENSIONS:
return kExtensions[index];
default:
return (unsigned char*)"";
}
}
static_assert(CheckSameSignature<decltype(mockGetStringi), //
decltype(glGetStringi)>::value);
void mockGetIntegerv(GLenum name, int* value) {
switch (name) {
case GL_NUM_EXTENSIONS: {
*value = kExtensions.size();
} break;
case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS:
*value = 8;
break;
default:
*value = 0;
break;
}
}
static_assert(CheckSameSignature<decltype(mockGetIntegerv), //
decltype(glGetIntegerv)>::value);
GLenum mockGetError() {
return GL_NO_ERROR;
}
static_assert(CheckSameSignature<decltype(mockGetError), //
decltype(glGetError)>::value);
void mockPopDebugGroupKHR() {
RecordGLCall("PopDebugGroupKHR");
}
static_assert(CheckSameSignature<decltype(mockPopDebugGroupKHR), //
decltype(glPopDebugGroupKHR)>::value);
void mockPushDebugGroupKHR(GLenum source,
GLuint id,
GLsizei length,
const GLchar* message) {
RecordGLCall("PushDebugGroupKHR");
}
static_assert(CheckSameSignature<decltype(mockPushDebugGroupKHR), //
decltype(glPushDebugGroupKHR)>::value);
std::shared_ptr<MockGLES> MockGLES::Init() {
// If we cannot obtain a lock, MockGLES is already being used elsewhere.
FML_CHECK(g_test_lock.try_lock())
<< "MockGLES is already being used by another test.";
auto mock_gles = std::shared_ptr<MockGLES>(new MockGLES());
g_mock_gles = mock_gles;
return mock_gles;
}
const ProcTableGLES::Resolver kMockResolver = [](const char* name) {
if (strcmp(name, "glPopDebugGroupKHR") == 0) {
return reinterpret_cast<void*>(&mockPopDebugGroupKHR);
} else if (strcmp(name, "glPushDebugGroupKHR") == 0) {
return reinterpret_cast<void*>(&mockPushDebugGroupKHR);
} else if (strcmp(name, "glGetString") == 0) {
return reinterpret_cast<void*>(&mockGetString);
} else if (strcmp(name, "glGetStringi") == 0) {
return reinterpret_cast<void*>(&mockGetStringi);
} else if (strcmp(name, "glGetIntegerv") == 0) {
return reinterpret_cast<void*>(&mockGetIntegerv);
} else if (strcmp(name, "glGetError") == 0) {
return reinterpret_cast<void*>(&mockGetError);
} else {
return reinterpret_cast<void*>(&doNothing);
}
};
MockGLES::MockGLES() : proc_table_(kMockResolver) {}
MockGLES::~MockGLES() {
g_test_lock.unlock();
}
} // namespace testing
} // namespace impeller

View File

@ -0,0 +1,57 @@
// 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 <memory>
#include "fml/macros.h"
#include "impeller/renderer/backend/gles/proc_table_gles.h"
namespace impeller {
namespace testing {
/// @brief Provides a mocked version of the |ProcTableGLES| class.
///
/// Typically, Open GLES at runtime will be provided the host's GLES bindings
/// (as function pointers). This class maintains a set of function pointers that
/// appear to be GLES functions, but are actually just stubs that record
/// invocations.
///
/// See `README.md` for more information.
class MockGLES final {
public:
/// @brief Returns an initialized |MockGLES| instance.
///
/// This method overwrites mocked global GLES function pointers to record
/// invocations on this instance of |MockGLES|. As such, it should only be
/// called once per test.
static std::shared_ptr<MockGLES> Init();
/// @brief Returns a configured |ProcTableGLES| instance.
const ProcTableGLES& GetProcTable() const { return proc_table_; }
/// @brief Returns a vector of the names of all recorded calls.
///
/// Calls are cleared after this method is called.
std::vector<std::string> GetCapturedCalls() {
std::vector<std::string> calls = captured_calls_;
captured_calls_.clear();
return calls;
}
~MockGLES();
private:
friend void RecordGLCall(const char* name);
MockGLES();
void RecordCall(const char* name) { captured_calls_.emplace_back(name); }
const ProcTableGLES proc_table_;
std::vector<std::string> captured_calls_;
FML_DISALLOW_COPY_AND_ASSIGN(MockGLES);
};
} // namespace testing
} // namespace impeller

View File

@ -0,0 +1,48 @@
// 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 "flutter/testing/testing.h" // IWYU pragma: keep
#include "gtest/gtest.h"
#include "impeller/renderer/backend/gles/proc_table_gles.h"
#include "impeller/renderer/backend/gles/test/mock_gles.h"
namespace impeller {
namespace testing {
// This test just checks that the proc table is initialized correctly.
//
// If this test doesn't pass, no test that uses the proc table will pass.
TEST(MockGLES, CanInitialize) {
auto mock_gles = MockGLES::Init();
EXPECT_EQ(mock_gles->GetProcTable().GetString(GL_VENDOR),
(unsigned char*)"MockGLES");
}
// Tests we can call two functions and capture the calls.
TEST(MockGLES, CapturesPushAndPopDebugGroup) {
auto mock_gles = MockGLES::Init();
auto& gl = mock_gles->GetProcTable();
gl.PushDebugGroupKHR(GL_DEBUG_SOURCE_APPLICATION_KHR, 0, -1, "test");
gl.PopDebugGroupKHR();
auto calls = mock_gles->GetCapturedCalls();
EXPECT_EQ(calls, std::vector<std::string>(
{"PushDebugGroupKHR", "PopDebugGroupKHR"}));
}
// Tests that if we call a function we have not mocked, it's OK.
TEST(MockGLES, CanCallUnmockedFunction) {
auto mock_gles = MockGLES::Init();
auto& gl = mock_gles->GetProcTable();
gl.DeleteFramebuffers(1, nullptr);
// Test should still complete.
// If we end up mocking DeleteFramebuffers, delete this test.
}
} // namespace testing
} // namespace impeller