blob: 00b9c42986fb52426099bfe253c111b04716faae [file] [log] [blame]
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "GfxstreamEnd2EndTests.h"
#include <dlfcn.h>
#include <log/log.h>
#include <filesystem>
#include "ProcessPipe.h"
#include "RutabagaLayer.h"
#include "aemu/base/Path.h"
#include "gfxstream/ImageUtils.h"
#include "gfxstream/RutabagaLayerTestUtils.h"
#include "gfxstream/Strings.h"
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
namespace gfxstream {
namespace tests {
namespace {
using testing::AnyOf;
using testing::Eq;
using testing::Gt;
using testing::IsFalse;
using testing::IsTrue;
using testing::Not;
using testing::NotNull;
std::string GetTestDataPath(const std::string& basename) {
const std::filesystem::path testBinaryDirectory = gfxstream::guest::getProgramDirectory();
return (testBinaryDirectory / "testdata" / basename).string();
}
} // namespace
std::string GfxstreamTransportToEnvVar(GfxstreamTransport transport) {
switch (transport) {
case GfxstreamTransport::kVirtioGpuAsg: {
return "virtio-gpu-asg";
}
case GfxstreamTransport::kVirtioGpuPipe: {
return "virtio-gpu-pipe";
}
}
}
std::string GfxstreamTransportToString(GfxstreamTransport transport) {
switch (transport) {
case GfxstreamTransport::kVirtioGpuAsg: {
return "VirtioGpuAsg";
}
case GfxstreamTransport::kVirtioGpuPipe: {
return "VirtioGpuPipe";
}
}
}
std::string TestParams::ToString() const {
std::string ret;
ret += (with_gl ? "With" : "Without");
ret += "Gl";
ret += (with_vk ? "With" : "Without");
ret += "Vk";
ret += "SampleCount" + std::to_string(samples);
if (!with_features.empty()) {
ret += "WithFeatures_";
ret += Join(with_features, "_");
ret += "_";
}
ret += "Over";
ret += GfxstreamTransportToString(with_transport);
return ret;
}
std::ostream& operator<<(std::ostream& os, const TestParams& params) {
return os << params.ToString();
}
std::string GetTestName(const ::testing::TestParamInfo<TestParams>& info) {
return info.param.ToString();
}
std::vector<TestParams> WithAndWithoutFeatures(const std::vector<TestParams>& params,
const std::vector<std::string>& features) {
std::vector<TestParams> output;
output.reserve(params.size() * 2);
// Copy of all of the existing test params:
output.insert(output.end(), params.begin(), params.end());
// Copy of all of the existing test params with the new features:
for (TestParams copy : params) {
copy.with_features.insert(features.begin(), features.end());
output.push_back(copy);
}
return output;
}
std::unique_ptr<GuestGlDispatchTable> GfxstreamEnd2EndTest::SetupGuestGl() {
const std::filesystem::path testDirectory = gfxstream::guest::getProgramDirectory();
const std::string eglLibPath = (testDirectory / "libEGL_emulation_with_host.so").string();
const std::string gles2LibPath = (testDirectory / "libGLESv2_emulation_with_host.so").string();
void* eglLib = dlopen(eglLibPath.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!eglLib) {
ALOGE("Failed to load Gfxstream EGL library from %s.", eglLibPath.c_str());
return nullptr;
}
void* gles2Lib = dlopen(gles2LibPath.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!gles2Lib) {
ALOGE("Failed to load Gfxstream GLES2 library from %s.", gles2LibPath.c_str());
return nullptr;
}
using GenericFnType = void*(void);
using GetProcAddrType = GenericFnType*(const char*);
auto eglGetAddr = reinterpret_cast<GetProcAddrType*>(dlsym(eglLib, "eglGetProcAddress"));
if (!eglGetAddr) {
ALOGE("Failed to load Gfxstream EGL library from %s.", eglLibPath.c_str());
return nullptr;
}
auto gl = std::make_unique<GuestGlDispatchTable>();
#define LOAD_EGL_FUNCTION(return_type, function_name, signature) \
gl-> function_name = reinterpret_cast< return_type (*) signature >(eglGetAddr( #function_name ));
LIST_RENDER_EGL_FUNCTIONS(LOAD_EGL_FUNCTION)
LIST_RENDER_EGL_EXTENSIONS_FUNCTIONS(LOAD_EGL_FUNCTION)
#define LOAD_GLES2_FUNCTION(return_type, function_name, signature, callargs) \
gl->function_name = \
reinterpret_cast<return_type(*) signature>(dlsym(gles2Lib, #function_name)); \
if (!gl->function_name) { \
gl->function_name = \
reinterpret_cast<return_type(*) signature>(eglGetAddr(#function_name)); \
}
LIST_GLES_FUNCTIONS(LOAD_GLES2_FUNCTION, LOAD_GLES2_FUNCTION)
return gl;
}
std::unique_ptr<GuestRenderControlDispatchTable> GfxstreamEnd2EndTest::SetupGuestRc() {
const std::filesystem::path testDirectory = gfxstream::guest::getProgramDirectory();
const std::string rcLibPath =
(testDirectory / "libgfxstream_guest_rendercontrol_with_host.so").string();
void* rcLib = dlopen(rcLibPath.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!rcLib) {
ALOGE("Failed to load Gfxstream RenderControl library from %s.", rcLibPath.c_str());
return nullptr;
}
auto rc = std::make_unique<GuestRenderControlDispatchTable>();
#define LOAD_RENDERCONTROL_FUNCTION(name) \
rc->name = reinterpret_cast<PFN_##name>(dlsym(rcLib, #name)); \
if (rc->name == nullptr) { \
ALOGE("Failed to load RenderControl function %s", #name); \
return nullptr; \
}
LOAD_RENDERCONTROL_FUNCTION(rcCreateDevice);
LOAD_RENDERCONTROL_FUNCTION(rcDestroyDevice);
LOAD_RENDERCONTROL_FUNCTION(rcCompose);
return rc;
}
std::unique_ptr<vkhpp::DynamicLoader> GfxstreamEnd2EndTest::SetupGuestVk() {
const std::filesystem::path testDirectory = gfxstream::guest::getProgramDirectory();
const std::string vkLibPath = (testDirectory / "libgfxstream_guest_vulkan_with_host.so").string();
auto dl = std::make_unique<vkhpp::DynamicLoader>(vkLibPath);
if (!dl->success()) {
ALOGE("Failed to load Vulkan from: %s", vkLibPath.c_str());
return nullptr;
}
auto getInstanceProcAddr = dl->getProcAddress<PFN_vkGetInstanceProcAddr>("vk_icdGetInstanceProcAddr");
if (!getInstanceProcAddr) {
ALOGE("Failed to load Vulkan vkGetInstanceProcAddr. %s", dlerror());
return nullptr;
}
VULKAN_HPP_DEFAULT_DISPATCHER.init(getInstanceProcAddr);
return dl;
}
void GfxstreamEnd2EndTest::SetUp() {
const TestParams params = GetParam();
const std::string transportValue = GfxstreamTransportToEnvVar(params.with_transport);
ASSERT_THAT(setenv("GFXSTREAM_TRANSPORT", transportValue.c_str(), /*overwrite=*/1), Eq(0));
ASSERT_THAT(setenv("GFXSTREAM_EMULATED_VIRTIO_GPU_WITH_GL",
params.with_gl ? "Y" : "N", /*overwrite=*/1), Eq(0));
ASSERT_THAT(setenv("GFXSTREAM_EMULATED_VIRTIO_GPU_WITH_VK", params.with_vk ? "Y" : "N",
/*overwrite=*/1),
Eq(0));
std::vector<std::string> featureEnables;
for (const std::string& feature : params.with_features) {
featureEnables.push_back(feature + ":enabled");
}
const std::string features = Join(featureEnables, ",");
ASSERT_THAT(setenv("GFXSTREAM_EMULATED_VIRTIO_GPU_RENDERER_FEATURES", features.c_str(),
/*overwrite=*/1),
Eq(0));
if (params.with_gl) {
mGl = SetupGuestGl();
ASSERT_THAT(mGl, NotNull());
}
if (params.with_vk) {
mVk = SetupGuestVk();
ASSERT_THAT(mVk, NotNull());
}
mRc = SetupGuestRc();
ASSERT_THAT(mRc, NotNull());
mAnwHelper.reset(createPlatformANativeWindowHelper());
mGralloc.reset(createPlatformGralloc());
mSync.reset(createPlatformSyncHelper());
}
void GfxstreamEnd2EndTest::TearDownGuest() {
if (mGl) {
EGLDisplay display = mGl->eglGetCurrentDisplay();
if (display != EGL_NO_DISPLAY) {
mGl->eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
mGl->eglTerminate(display);
}
mGl->eglReleaseThread();
mGl.reset();
}
mVk.reset();
mRc.reset();
mAnwHelper.reset();
mGralloc.reset();
mSync.reset();
processPipeRestart();
}
void GfxstreamEnd2EndTest::TearDownHost() {
const uint32_t users = GetNumActiveEmulatedVirtioGpuUsers();
if (users != 0) {
ALOGE("The EmulationVirtioGpu was found to still be active by %" PRIu32
" after the "
"end of the test. Please ensure you have fully destroyed all objects created "
"during the test (Gralloc allocations, ANW allocations, etc).",
users);
abort();
}
}
void GfxstreamEnd2EndTest::TearDown() {
TearDownGuest();
TearDownHost();
}
void GfxstreamEnd2EndTest::SetUpEglContextAndSurface(
uint32_t contextVersion,
uint32_t width,
uint32_t height,
EGLDisplay* outDisplay,
EGLContext* outContext,
EGLSurface* outSurface) {
ASSERT_THAT(contextVersion, AnyOf(Eq(2), Eq(3)))
<< "Invalid context version requested.";
EGLDisplay display = mGl->eglGetDisplay(EGL_DEFAULT_DISPLAY);
ASSERT_THAT(display, Not(Eq(EGL_NO_DISPLAY)));
int versionMajor = 0;
int versionMinor = 0;
ASSERT_THAT(mGl->eglInitialize(display, &versionMajor, &versionMinor), IsTrue());
ASSERT_THAT(mGl->eglBindAPI(EGL_OPENGL_ES_API), IsTrue());
// clang-format off
static const EGLint configAttributes[] = {
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_NONE,
};
// clang-format on
int numConfigs = 0;
ASSERT_THAT(mGl->eglChooseConfig(display, configAttributes, nullptr, 1, &numConfigs), IsTrue());
ASSERT_THAT(numConfigs, Gt(0));
EGLConfig config = nullptr;
ASSERT_THAT(mGl->eglChooseConfig(display, configAttributes, &config, 1, &numConfigs), IsTrue());
ASSERT_THAT(config, Not(Eq(nullptr)));
// clang-format off
static const EGLint surfaceAttributes[] = {
EGL_WIDTH, static_cast<EGLint>(width),
EGL_HEIGHT, static_cast<EGLint>(height),
EGL_NONE,
};
// clang-format on
EGLSurface surface = mGl->eglCreatePbufferSurface(display, config, surfaceAttributes);
ASSERT_THAT(surface, Not(Eq(EGL_NO_SURFACE)));
// clang-format off
static const EGLint contextAttribs[] = {
EGL_CONTEXT_CLIENT_VERSION, static_cast<EGLint>(contextVersion),
EGL_NONE,
};
// clang-format on
EGLContext context = mGl->eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
ASSERT_THAT(context, Not(Eq(EGL_NO_CONTEXT)));
ASSERT_THAT(mGl->eglMakeCurrent(display, surface, surface, context), IsTrue());
*outDisplay = display;
*outContext = context;
*outSurface = surface;
}
void GfxstreamEnd2EndTest::TearDownEglContextAndSurface(
EGLDisplay display,
EGLContext context,
EGLSurface surface) {
ASSERT_THAT(mGl->eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT), IsTrue());
ASSERT_THAT(mGl->eglDestroyContext(display, context), IsTrue());
ASSERT_THAT(mGl->eglDestroySurface(display, surface), IsTrue());
}
GlExpected<ScopedGlShader> ScopedGlShader::MakeShader(GlDispatch& dispatch, GLenum type,
const std::string& source) {
GLuint shader = dispatch.glCreateShader(type);
if (!shader) {
return android::base::unexpected("Failed to create shader.");
}
const GLchar* sourceTyped = (const GLchar*)source.c_str();
const GLint sourceLength = source.size();
dispatch.glShaderSource(shader, 1, &sourceTyped, &sourceLength);
dispatch.glCompileShader(shader);
GLint compileStatus;
dispatch.glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);
if (compileStatus != GL_TRUE) {
GLint errorLogLength = 0;
dispatch.glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &errorLogLength);
if (!errorLogLength) {
errorLogLength = 512;
}
std::vector<GLchar> errorLog(errorLogLength);
dispatch.glGetShaderInfoLog(shader, errorLogLength, &errorLogLength, errorLog.data());
const std::string errorString = errorLogLength == 0 ? "" : errorLog.data();
ALOGE("Shader compilation failed with: \"%s\"", errorString.c_str());
dispatch.glDeleteShader(shader);
return android::base::unexpected(errorString);
}
return ScopedGlShader(dispatch, shader);
}
GlExpected<ScopedGlProgram> ScopedGlProgram::MakeProgram(GlDispatch& dispatch,
const std::string& vertSource,
const std::string& fragSource) {
auto vertShader = GL_EXPECT(ScopedGlShader::MakeShader(dispatch, GL_VERTEX_SHADER, vertSource));
auto fragShader =
GL_EXPECT(ScopedGlShader::MakeShader(dispatch, GL_FRAGMENT_SHADER, fragSource));
GLuint program = dispatch.glCreateProgram();
dispatch.glAttachShader(program, vertShader);
dispatch.glAttachShader(program, fragShader);
dispatch.glLinkProgram(program);
GLint linkStatus;
dispatch.glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
if (linkStatus != GL_TRUE) {
GLint errorLogLength = 0;
dispatch.glGetProgramiv(program, GL_INFO_LOG_LENGTH, &errorLogLength);
if (!errorLogLength) {
errorLogLength = 512;
}
std::vector<char> errorLog(errorLogLength, 0);
dispatch.glGetProgramInfoLog(program, errorLogLength, nullptr, errorLog.data());
const std::string errorString = errorLogLength == 0 ? "" : errorLog.data();
ALOGE("Program link failed with: \"%s\"", errorString.c_str());
dispatch.glDeleteProgram(program);
return android::base::unexpected(errorString);
}
return ScopedGlProgram(dispatch, program);
}
GlExpected<ScopedGlProgram> ScopedGlProgram::MakeProgram(
GlDispatch& dispatch, GLenum programBinaryFormat,
const std::vector<uint8_t>& programBinaryData) {
GLuint program = dispatch.glCreateProgram();
dispatch.glProgramBinary(program, programBinaryFormat, programBinaryData.data(),
programBinaryData.size());
GLint linkStatus;
dispatch.glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
if (linkStatus != GL_TRUE) {
GLint errorLogLength = 0;
dispatch.glGetProgramiv(program, GL_INFO_LOG_LENGTH, &errorLogLength);
if (!errorLogLength) {
errorLogLength = 512;
}
std::vector<char> errorLog(errorLogLength, 0);
dispatch.glGetProgramInfoLog(program, errorLogLength, nullptr, errorLog.data());
const std::string errorString = errorLogLength == 0 ? "" : errorLog.data();
ALOGE("Program link failed with: \"%s\"", errorString.c_str());
dispatch.glDeleteProgram(program);
return android::base::unexpected(errorString);
}
return ScopedGlProgram(dispatch, program);
}
GlExpected<ScopedAHardwareBuffer> ScopedAHardwareBuffer::Allocate(Gralloc& gralloc, uint32_t width,
uint32_t height,
uint32_t format) {
AHardwareBuffer* ahb = nullptr;
int status = gralloc.allocate(width, height, format, -1, &ahb);
if (status != 0) {
return android::base::unexpected(std::string("Failed to allocate AHB with width:") +
std::to_string(width) + std::string(" height:") +
std::to_string(height) + std::string(" format:") +
std::to_string(format));
}
return ScopedAHardwareBuffer(gralloc, ahb);
}
GlExpected<ScopedGlShader> GfxstreamEnd2EndTest::SetUpShader(GLenum type,
const std::string& source) {
if (!mGl) {
return android::base::unexpected("Gl not enabled for this test.");
}
return ScopedGlShader::MakeShader(*mGl, type, source);
}
GlExpected<ScopedGlProgram> GfxstreamEnd2EndTest::SetUpProgram(const std::string& vertSource,
const std::string& fragSource) {
if (!mGl) {
return android::base::unexpected("Gl not enabled for this test.");
}
return ScopedGlProgram::MakeProgram(*mGl, vertSource, fragSource);
}
GlExpected<ScopedGlProgram> GfxstreamEnd2EndTest::SetUpProgram(
GLenum programBinaryFormat, const std::vector<uint8_t>& programBinaryData) {
if (!mGl) {
return android::base::unexpected("Gl not enabled for this test.");
}
return ScopedGlProgram::MakeProgram(*mGl, programBinaryFormat, programBinaryData);
}
VkExpected<GfxstreamEnd2EndTest::TypicalVkTestEnvironment>
GfxstreamEnd2EndTest::SetUpTypicalVkTestEnvironment(const TypicalVkTestEnvironmentOptions& opts) {
const auto availableInstanceLayers = vkhpp::enumerateInstanceLayerProperties().value;
ALOGV("Available instance layers:");
for (const vkhpp::LayerProperties& layer : availableInstanceLayers) {
ALOGV(" - %s", layer.layerName.data());
}
constexpr const bool kEnableValidationLayers = true;
std::vector<const char*> requestedInstanceExtensions;
std::vector<const char*> requestedInstanceLayers;
if (kEnableValidationLayers) {
requestedInstanceExtensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
const vkhpp::ApplicationInfo applicationInfo{
.pApplicationName = ::testing::UnitTest::GetInstance()->current_test_info()->name(),
.applicationVersion = 1,
.pEngineName = "Gfxstream Testing Engine",
.engineVersion = 1,
.apiVersion = opts.apiVersion,
};
const vkhpp::InstanceCreateInfo instanceCreateInfo{
.pNext = opts.instanceCreateInfoPNext ? *opts.instanceCreateInfoPNext : nullptr,
.pApplicationInfo = &applicationInfo,
.enabledLayerCount = static_cast<uint32_t>(requestedInstanceLayers.size()),
.ppEnabledLayerNames = requestedInstanceLayers.data(),
.enabledExtensionCount = static_cast<uint32_t>(requestedInstanceExtensions.size()),
.ppEnabledExtensionNames = requestedInstanceExtensions.data(),
};
auto instance = VK_EXPECT_RV(vkhpp::createInstanceUnique(instanceCreateInfo));
VULKAN_HPP_DEFAULT_DISPATCHER.init(*instance);
auto physicalDevices = VK_EXPECT_RV(instance->enumeratePhysicalDevices());
ALOGV("Available physical devices:");
for (const auto& physicalDevice : physicalDevices) {
const auto physicalDeviceProps = physicalDevice.getProperties();
ALOGV(" - %s", physicalDeviceProps.deviceName.data());
}
if (physicalDevices.empty()) {
ALOGE("No physical devices available?");
return android::base::unexpected(vkhpp::Result::eErrorUnknown);
}
auto physicalDevice = std::move(physicalDevices[0]);
{
const auto physicalDeviceProps = physicalDevice.getProperties();
ALOGV("Selected physical device: %s", physicalDeviceProps.deviceName.data());
}
{
const auto exts = VK_EXPECT_RV(physicalDevice.enumerateDeviceExtensionProperties());
ALOGV("Available physical device extensions:");
for (const auto& ext : exts) {
ALOGV(" - %s", ext.extensionName.data());
}
}
uint32_t graphicsQueueFamilyIndex = -1;
{
const auto props = physicalDevice.getQueueFamilyProperties();
for (uint32_t i = 0; i < props.size(); i++) {
const auto& prop = props[i];
if (prop.queueFlags & vkhpp::QueueFlagBits::eGraphics) {
graphicsQueueFamilyIndex = i;
break;
}
}
}
if (graphicsQueueFamilyIndex == -1) {
ALOGE("Failed to find graphics queue.");
return android::base::unexpected(vkhpp::Result::eErrorUnknown);
}
const float queuePriority = 1.0f;
const vkhpp::DeviceQueueCreateInfo deviceQueueCreateInfo = {
.queueFamilyIndex = graphicsQueueFamilyIndex,
.queueCount = 1,
.pQueuePriorities = &queuePriority,
};
std::vector<const char*> deviceExtensions = {
VK_ANDROID_NATIVE_BUFFER_EXTENSION_NAME,
VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME,
};
if (opts.deviceExtensions) {
for (const std::string& ext : *opts.deviceExtensions) {
deviceExtensions.push_back(ext.c_str());
}
}
const vkhpp::DeviceCreateInfo deviceCreateInfo = {
.pNext = opts.deviceCreateInfoPNext ? *opts.deviceCreateInfoPNext : nullptr,
.pQueueCreateInfos = &deviceQueueCreateInfo,
.queueCreateInfoCount = 1,
.enabledLayerCount = 0,
.ppEnabledLayerNames = nullptr,
.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size()),
.ppEnabledExtensionNames = deviceExtensions.data(),
};
auto device = VK_EXPECT_RV(physicalDevice.createDeviceUnique(deviceCreateInfo));
auto queue = device->getQueue(graphicsQueueFamilyIndex, 0);
return TypicalVkTestEnvironment{
.instance = std::move(instance),
.physicalDevice = std::move(physicalDevice),
.device = std::move(device),
.queue = std::move(queue),
.queueFamilyIndex = graphicsQueueFamilyIndex,
};
}
void GfxstreamEnd2EndTest::SnapshotSaveAndLoad() {
auto directory = testing::TempDir();
std::shared_ptr<gfxstream::EmulatedVirtioGpu> emulation = gfxstream::EmulatedVirtioGpu::Get();
emulation->SnapshotSave(directory);
emulation->SnapshotRestore(directory);
}
GlExpected<Image> GfxstreamEnd2EndTest::LoadImage(const std::string& basename) {
const std::string filepath = GetTestDataPath(basename);
if (!std::filesystem::exists(filepath)) {
return android::base::unexpected("File " + filepath + " does not exist.");
}
if (!std::filesystem::is_regular_file(filepath)) {
return android::base::unexpected("File " + filepath + " is not a regular file.");
}
Image image;
uint32_t sourceWidth = 0;
uint32_t sourceHeight = 0;
std::vector<uint32_t> sourcePixels;
if (!LoadRGBAFromPng(filepath, &image.width, &image.height, &image.pixels)) {
return android::base::unexpected("Failed to load " + filepath + " as RGBA PNG.");
}
return image;
}
GlExpected<Image> GfxstreamEnd2EndTest::AsImage(ScopedAHardwareBuffer& ahb) {
Image actual;
actual.width = ahb.GetWidth();
if (actual.width == 0) {
return android::base::unexpected("Failed to query AHB width.");
}
actual.height = ahb.GetHeight();
if (actual.height == 0) {
return android::base::unexpected("Failed to query AHB height.");
}
actual.pixels.resize(actual.width * actual.height);
{
uint8_t* ahbPixels = GL_EXPECT(ahb.Lock());
std::memcpy(actual.pixels.data(), ahbPixels, actual.pixels.size() * sizeof(uint32_t));
ahb.Unlock();
}
return actual;
}
GlExpected<ScopedAHardwareBuffer> GfxstreamEnd2EndTest::CreateAHBFromImage(
const std::string& basename) {
auto image = GL_EXPECT(LoadImage(basename));
auto ahb = GL_EXPECT(
ScopedAHardwareBuffer::Allocate(*mGralloc, image.width, image.height, GFXSTREAM_AHB_FORMAT_R8G8B8A8_UNORM));
{
uint8_t* ahbPixels = GL_EXPECT(ahb.Lock());
std::memcpy(ahbPixels, image.pixels.data(), image.pixels.size() * sizeof(uint32_t));
ahb.Unlock();
}
return std::move(ahb);
}
bool GfxstreamEnd2EndTest::ArePixelsSimilar(uint32_t expectedPixel, uint32_t actualPixel) {
const uint8_t* actualRGBA = reinterpret_cast<const uint8_t*>(&actualPixel);
const uint8_t* expectedRGBA = reinterpret_cast<const uint8_t*>(&expectedPixel);
constexpr const uint32_t kRGBA8888Tolerance = 2;
for (uint32_t channel = 0; channel < 4; channel++) {
const uint8_t actualChannel = actualRGBA[channel];
const uint8_t expectedChannel = expectedRGBA[channel];
if ((std::max(actualChannel, expectedChannel) - std::min(actualChannel, expectedChannel)) >
kRGBA8888Tolerance) {
return false;
}
}
return true;
}
bool GfxstreamEnd2EndTest::AreImagesSimilar(const Image& expected, const Image& actual) {
if (actual.width != expected.width) {
ADD_FAILURE() << "Image comparison failed: " << "expected.width " << expected.width << "vs"
<< "actual.width " << actual.width;
return false;
}
if (actual.height != expected.height) {
ADD_FAILURE() << "Image comparison failed: " << "expected.height " << expected.height
<< "vs" << "actual.height " << actual.height;
return false;
}
const uint32_t width = actual.width;
const uint32_t height = actual.height;
const uint32_t* actualPixels = actual.pixels.data();
const uint32_t* expectedPixels = expected.pixels.data();
bool imagesSimilar = true;
uint32_t reportedIncorrectPixels = 0;
constexpr const uint32_t kMaxReportedIncorrectPixels = 5;
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
const uint32_t actualPixel = actualPixels[y * height + x];
const uint32_t expectedPixel = expectedPixels[y * width + x];
if (!ArePixelsSimilar(expectedPixel, actualPixel)) {
imagesSimilar = false;
if (reportedIncorrectPixels < kMaxReportedIncorrectPixels) {
reportedIncorrectPixels++;
const uint8_t* actualRGBA = reinterpret_cast<const uint8_t*>(&actualPixel);
const uint8_t* expectedRGBA = reinterpret_cast<const uint8_t*>(&expectedPixel);
// clang-format off
ADD_FAILURE()
<< "Pixel comparison failed at (" << x << ", " << y << ") "
<< " with actual "
<< " r:" << static_cast<int>(actualRGBA[0])
<< " g:" << static_cast<int>(actualRGBA[1])
<< " b:" << static_cast<int>(actualRGBA[2])
<< " a:" << static_cast<int>(actualRGBA[3])
<< " but expected "
<< " r:" << static_cast<int>(expectedRGBA[0])
<< " g:" << static_cast<int>(expectedRGBA[1])
<< " b:" << static_cast<int>(expectedRGBA[2])
<< " a:" << static_cast<int>(expectedRGBA[3]);
// clang-format on
}
}
}
}
return imagesSimilar;
}
GlExpected<Ok> GfxstreamEnd2EndTest::CompareAHBWithGolden(ScopedAHardwareBuffer& ahb,
const std::string& goldenBasename) {
Image actual = GL_EXPECT(AsImage(ahb));
GlExpected<Image> expected = LoadImage(goldenBasename);
bool imagesAreSimilar = false;
if (expected.ok()) {
imagesAreSimilar = AreImagesSimilar(*expected, actual);
} else {
imagesAreSimilar = false;
}
if (!imagesAreSimilar && kSaveImagesIfComparisonFailed) {
static uint32_t sImageNumber{1};
const std::string outputBasename = std::to_string(sImageNumber++) + "_" + goldenBasename;
const std::string output =
(std::filesystem::temp_directory_path() / outputBasename).string();
SaveRGBAToPng(actual.width, actual.height, actual.pixels.data(), output);
ADD_FAILURE() << "Saved image comparison actual image to " << output;
}
if (!imagesAreSimilar) {
return android::base::unexpected(
"Image comparison failed (consider setting kSaveImagesIfComparisonFailed to true to "
"see the actual image generated).");
}
return {};
}
} // namespace tests
} // namespace gfxstream