// 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 #include #include "GfxstreamEnd2EndTests.h" #include #include #include #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& info) { return info.param.ToString(); } std::vector WithAndWithoutFeatures(const std::vector& params, const std::vector& features) { std::vector 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 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(dlsym(eglLib, "eglGetProcAddress")); if (!eglGetAddr) { ALOGE("Failed to load Gfxstream EGL library from %s.", eglLibPath.c_str()); return nullptr; } auto gl = std::make_unique(); #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(dlsym(gles2Lib, #function_name)); \ if (!gl->function_name) { \ gl->function_name = \ reinterpret_cast(eglGetAddr(#function_name)); \ } LIST_GLES_FUNCTIONS(LOAD_GLES2_FUNCTION, LOAD_GLES2_FUNCTION) return gl; } std::unique_ptr 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(); #define LOAD_RENDERCONTROL_FUNCTION(name) \ rc->name = reinterpret_cast(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 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(vkLibPath); if (!dl->success()) { ALOGE("Failed to load Vulkan from: %s", vkLibPath.c_str()); return nullptr; } auto getInstanceProcAddr = dl->getProcAddress("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 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(width), EGL_HEIGHT, static_cast(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(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::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 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::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 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::MakeProgram( GlDispatch& dispatch, GLenum programBinaryFormat, const std::vector& 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 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::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 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 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 GfxstreamEnd2EndTest::SetUpProgram( GLenum programBinaryFormat, const std::vector& programBinaryData) { if (!mGl) { return android::base::unexpected("Gl not enabled for this test."); } return ScopedGlProgram::MakeProgram(*mGl, programBinaryFormat, programBinaryData); } VkExpected 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 requestedInstanceExtensions; std::vector 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(requestedInstanceLayers.size()), .ppEnabledLayerNames = requestedInstanceLayers.data(), .enabledExtensionCount = static_cast(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 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(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 emulation = gfxstream::EmulatedVirtioGpu::Get(); emulation->SnapshotSave(directory); emulation->SnapshotRestore(directory); } GlExpected 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 sourcePixels; if (!LoadRGBAFromPng(filepath, &image.width, &image.height, &image.pixels)) { return android::base::unexpected("Failed to load " + filepath + " as RGBA PNG."); } return image; } GlExpected 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); const uint32_t ahbFormat = ahb.GetAHBFormat(); if (ahbFormat != GFXSTREAM_AHB_FORMAT_R8G8B8A8_UNORM && ahbFormat != GFXSTREAM_AHB_FORMAT_B8G8R8A8_UNORM) { return android::base::unexpected("Unhandled AHB format " + std::to_string(ahbFormat)); } { uint8_t* ahbPixels = GL_EXPECT(ahb.Lock()); std::memcpy(actual.pixels.data(), ahbPixels, actual.pixels.size() * sizeof(uint32_t)); ahb.Unlock(); } if (ahbFormat == GFXSTREAM_AHB_FORMAT_B8G8R8A8_UNORM) { for (uint32_t& pixel : actual.pixels) { uint8_t* pixelComponents = reinterpret_cast(&pixel); std::swap(pixelComponents[0], pixelComponents[2]); } } return actual; } GlExpected 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(&actualPixel); const uint8_t* expectedRGBA = reinterpret_cast(&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(&actualPixel); const uint8_t* expectedRGBA = reinterpret_cast(&expectedPixel); // clang-format off ADD_FAILURE() << "Pixel comparison failed at (" << x << ", " << y << ") " << " with actual " << " r:" << static_cast(actualRGBA[0]) << " g:" << static_cast(actualRGBA[1]) << " b:" << static_cast(actualRGBA[2]) << " a:" << static_cast(actualRGBA[3]) << " but expected " << " r:" << static_cast(expectedRGBA[0]) << " g:" << static_cast(expectedRGBA[1]) << " b:" << static_cast(expectedRGBA[2]) << " a:" << static_cast(expectedRGBA[3]); // clang-format on } } } } return imagesSimilar; } GlExpected GfxstreamEnd2EndTest::CompareAHBWithGolden(ScopedAHardwareBuffer& ahb, const std::string& goldenBasename) { Image actual = GL_EXPECT(AsImage(ahb)); GlExpected 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