| // |
| // Copyright 2020 The ANGLE Project Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| // CaptureReplayTest.cpp: |
| // Application that runs replay for testing of capture replay |
| // |
| |
| #include "common/debug.h" |
| #include "common/system_utils.h" |
| #include "platform/PlatformMethods.h" |
| #include "traces_export.h" |
| #include "util/EGLPlatformParameters.h" |
| #include "util/EGLWindow.h" |
| #include "util/OSWindow.h" |
| #include "util/shader_utils.h" |
| |
| #include <stdint.h> |
| #include <string.h> |
| #include <fstream> |
| #include <functional> |
| #include <iostream> |
| #include <list> |
| #include <memory> |
| #include <ostream> |
| #include <string> |
| #include <utility> |
| |
| #include "frame_capture_test_utils.h" |
| |
| namespace |
| { |
| EGLWindow *gEGLWindow = nullptr; |
| constexpr char kResultTag[] = "*RESULT"; |
| constexpr char kTracePath[] = ANGLE_CAPTURE_REPLAY_TEST_NAMES_PATH; |
| constexpr int kInitializationFailure = -1; |
| constexpr int kSerializationFailure = -2; |
| constexpr int kExitSuccess = 0; |
| |
| // This arbitrary value rejects placeholder serialized states. In practice they are many thousands |
| // of characters long. See frame_capture_utils_mock.cpp for the current placeholder string. |
| constexpr size_t kTooShortStateLength = 40; |
| |
| [[maybe_unused]] bool IsGLExtensionEnabled(const std::string &extName) |
| { |
| return angle::CheckExtensionExists(reinterpret_cast<const char *>(glGetString(GL_EXTENSIONS)), |
| extName); |
| } |
| |
| [[maybe_unused]] void KHRONOS_APIENTRY DebugCallback(GLenum source, |
| GLenum type, |
| GLuint id, |
| GLenum severity, |
| GLsizei length, |
| const GLchar *message, |
| const void *userParam) |
| { |
| printf("%s\n", message); |
| } |
| |
| [[maybe_unused]] void LogError(angle::PlatformMethods *platform, const char *errorMessage) |
| { |
| printf("ERR: %s\n", errorMessage); |
| } |
| |
| [[maybe_unused]] void LogWarning(angle::PlatformMethods *platform, const char *warningMessage) |
| { |
| printf("WARN: %s\n", warningMessage); |
| } |
| |
| [[maybe_unused]] void LogInfo(angle::PlatformMethods *platform, const char *infoMessage) |
| { |
| printf("INFO: %s\n", infoMessage); |
| } |
| |
| bool CompareSerializedContexts(const char *capturedSerializedContextState, |
| const char *replaySerializedContextState) |
| { |
| |
| return strcmp(replaySerializedContextState, capturedSerializedContextState) == 0; |
| } |
| |
| EGLImage KHRONOS_APIENTRY EGLCreateImage(EGLDisplay display, |
| EGLContext context, |
| EGLenum target, |
| EGLClientBuffer buffer, |
| const EGLAttrib *attrib_list) |
| { |
| |
| GLWindowContext ctx = reinterpret_cast<GLWindowContext>(context); |
| return gEGLWindow->createImage(ctx, target, buffer, attrib_list); |
| } |
| |
| EGLImage KHRONOS_APIENTRY EGLCreateImageKHR(EGLDisplay display, |
| EGLContext context, |
| EGLenum target, |
| EGLClientBuffer buffer, |
| const EGLint *attrib_list) |
| { |
| |
| GLWindowContext ctx = reinterpret_cast<GLWindowContext>(context); |
| return gEGLWindow->createImageKHR(ctx, target, buffer, attrib_list); |
| } |
| |
| EGLBoolean KHRONOS_APIENTRY EGLDestroyImage(EGLDisplay display, EGLImage image) |
| { |
| return gEGLWindow->destroyImage(image); |
| } |
| |
| EGLBoolean KHRONOS_APIENTRY EGLDestroyImageKHR(EGLDisplay display, EGLImage image) |
| { |
| return gEGLWindow->destroyImageKHR(image); |
| } |
| |
| EGLSurface KHRONOS_APIENTRY EGLCreatePbufferSurface(EGLDisplay display, |
| EGLConfig *config, |
| const EGLint *attrib_list) |
| { |
| return gEGLWindow->createPbufferSurface(attrib_list); |
| } |
| |
| EGLBoolean KHRONOS_APIENTRY EGLDestroySurface(EGLDisplay display, EGLSurface surface) |
| { |
| return gEGLWindow->destroySurface(surface); |
| } |
| |
| EGLBoolean KHRONOS_APIENTRY EGLBindTexImage(EGLDisplay display, EGLSurface surface, EGLint buffer) |
| { |
| return gEGLWindow->bindTexImage(surface, buffer); |
| } |
| |
| EGLBoolean KHRONOS_APIENTRY EGLReleaseTexImage(EGLDisplay display, |
| EGLSurface surface, |
| EGLint buffer) |
| { |
| return gEGLWindow->releaseTexImage(surface, buffer); |
| } |
| |
| EGLBoolean KHRONOS_APIENTRY EGLMakeCurrent(EGLDisplay display, |
| EGLSurface draw, |
| EGLSurface read, |
| EGLContext context) |
| { |
| return gEGLWindow->makeCurrent(draw, read, context); |
| } |
| } // namespace |
| |
| GenericProc KHRONOS_APIENTRY TraceLoadProc(const char *procName) |
| { |
| if (!gEGLWindow) |
| { |
| std::cout << "No Window pointer in TraceLoadProc.\n"; |
| return nullptr; |
| } |
| else |
| { |
| if (strcmp(procName, "eglCreateImage") == 0) |
| { |
| return reinterpret_cast<GenericProc>(EGLCreateImage); |
| } |
| if (strcmp(procName, "eglCreateImageKHR") == 0) |
| { |
| return reinterpret_cast<GenericProc>(EGLCreateImageKHR); |
| } |
| if (strcmp(procName, "eglDestroyImage") == 0) |
| { |
| return reinterpret_cast<GenericProc>(EGLDestroyImage); |
| } |
| if (strcmp(procName, "eglDestroyImageKHR") == 0) |
| { |
| return reinterpret_cast<GenericProc>(EGLDestroyImageKHR); |
| } |
| if (strcmp(procName, "eglCreatePbufferSurface") == 0) |
| { |
| return reinterpret_cast<GenericProc>(EGLCreatePbufferSurface); |
| } |
| if (strcmp(procName, "eglDestroySurface") == 0) |
| { |
| return reinterpret_cast<GenericProc>(EGLDestroySurface); |
| } |
| if (strcmp(procName, "eglBindTexImage") == 0) |
| { |
| return reinterpret_cast<GenericProc>(EGLBindTexImage); |
| } |
| if (strcmp(procName, "eglReleaseTexImage") == 0) |
| { |
| return reinterpret_cast<GenericProc>(EGLReleaseTexImage); |
| } |
| if (strcmp(procName, "eglMakeCurrent") == 0) |
| { |
| return reinterpret_cast<GenericProc>(EGLMakeCurrent); |
| } |
| return gEGLWindow->getProcAddress(procName); |
| } |
| } |
| |
| class CaptureReplayTests |
| { |
| public: |
| CaptureReplayTests() |
| { |
| // Load EGL library so we can initialize the display. |
| mEntryPointsLib.reset( |
| angle::OpenSharedLibrary(ANGLE_EGL_LIBRARY_NAME, angle::SearchType::ModuleDir)); |
| |
| mOSWindow = OSWindow::New(); |
| mOSWindow->disableErrorMessageDialog(); |
| } |
| |
| ~CaptureReplayTests() |
| { |
| EGLWindow::Delete(&mEGLWindow); |
| OSWindow::Delete(&mOSWindow); |
| } |
| |
| bool initializeTest(const std::string &execDir, const angle::TraceInfo &traceInfo) |
| { |
| if (!mOSWindow->initialize(traceInfo.name, traceInfo.drawSurfaceWidth, |
| traceInfo.drawSurfaceHeight)) |
| { |
| return false; |
| } |
| |
| mOSWindow->disableErrorMessageDialog(); |
| mOSWindow->setVisible(true); |
| |
| if (mEGLWindow && !mEGLWindow->isContextVersion(traceInfo.contextClientMajorVersion, |
| traceInfo.contextClientMinorVersion)) |
| { |
| EGLWindow::Delete(&mEGLWindow); |
| mEGLWindow = nullptr; |
| } |
| |
| if (!mEGLWindow) |
| { |
| // TODO: to support desktop OpenGL traces, capture the client api and profile mask in |
| // TraceInfo |
| const EGLenum testClientAPI = EGL_OPENGL_ES_API; |
| const EGLint testProfileMask = 0; |
| |
| mEGLWindow = EGLWindow::New(testClientAPI, traceInfo.contextClientMajorVersion, |
| traceInfo.contextClientMinorVersion, testProfileMask); |
| } |
| |
| ConfigParameters configParams; |
| configParams.redBits = traceInfo.configRedBits; |
| configParams.greenBits = traceInfo.configGreenBits; |
| configParams.blueBits = traceInfo.configBlueBits; |
| configParams.alphaBits = traceInfo.configAlphaBits; |
| configParams.depthBits = traceInfo.configDepthBits; |
| configParams.stencilBits = traceInfo.configStencilBits; |
| |
| configParams.clientArraysEnabled = traceInfo.areClientArraysEnabled; |
| configParams.bindGeneratesResource = traceInfo.isBindGeneratesResourcesEnabled; |
| configParams.webGLCompatibility = traceInfo.isWebGLCompatibilityEnabled; |
| configParams.robustResourceInit = traceInfo.isRobustResourceInitEnabled; |
| |
| mPlatformParams.renderer = traceInfo.displayPlatformType; |
| mPlatformParams.deviceType = traceInfo.displayDeviceType; |
| mPlatformParams.enable(angle::Feature::ForceInitShaderVariables); |
| mPlatformParams.enable(angle::Feature::EnableCaptureLimits); |
| |
| #if defined(ANGLE_ENABLE_ASSERTS) |
| mPlatformMethods.logError = LogError; |
| mPlatformMethods.logWarning = LogWarning; |
| mPlatformMethods.logInfo = LogInfo; |
| mPlatformParams.platformMethods = &mPlatformMethods; |
| #endif // defined(ANGLE_ENABLE_ASSERTS) |
| |
| if (!mEGLWindow->initializeGL(mOSWindow, mEntryPointsLib.get(), |
| angle::GLESDriverType::AngleEGL, mPlatformParams, |
| configParams)) |
| { |
| mOSWindow->destroy(); |
| return false; |
| } |
| |
| gEGLWindow = mEGLWindow; |
| LoadTraceEGL(TraceLoadProc); |
| LoadTraceGLES(TraceLoadProc); |
| |
| // Disable vsync |
| if (!mEGLWindow->setSwapInterval(0)) |
| { |
| cleanupTest(); |
| return false; |
| } |
| |
| #if defined(ANGLE_ENABLE_ASSERTS) |
| if (IsGLExtensionEnabled("GL_KHR_debug")) |
| { |
| EnableDebugCallback(DebugCallback, nullptr); |
| } |
| #endif |
| |
| // Load trace |
| mTraceLibrary.reset(new angle::TraceLibrary(traceInfo.name, traceInfo)); |
| if (!mTraceLibrary->valid()) |
| { |
| std::cout << "Failed to load trace library: " << traceInfo.name << "\n"; |
| return false; |
| } |
| |
| if (traceInfo.isBinaryDataCompressed) |
| { |
| mTraceLibrary->setBinaryDataDecompressCallback(angle::DecompressBinaryData, |
| angle::DeleteBinaryData); |
| } |
| |
| std::stringstream binaryPathStream; |
| binaryPathStream << execDir << angle::GetPathSeparator() |
| << ANGLE_CAPTURE_REPLAY_TEST_DATA_DIR; |
| |
| mTraceLibrary->setBinaryDataDir(binaryPathStream.str().c_str()); |
| |
| mTraceLibrary->setupReplay(); |
| return true; |
| } |
| |
| void cleanupTest() |
| { |
| mTraceLibrary->finishReplay(); |
| mTraceLibrary.reset(nullptr); |
| mEGLWindow->destroyGL(); |
| mOSWindow->destroy(); |
| } |
| |
| void swap() { mEGLWindow->swap(); } |
| |
| int runTest(const std::string &exeDir, const angle::TraceInfo &traceInfo) |
| { |
| if (!initializeTest(exeDir, traceInfo)) |
| { |
| return kInitializationFailure; |
| } |
| |
| for (uint32_t frame = traceInfo.frameStart; frame <= traceInfo.frameEnd; frame++) |
| { |
| mTraceLibrary->replayFrame(frame); |
| |
| const char *replayedSerializedState = |
| reinterpret_cast<const char *>(glGetString(GL_SERIALIZED_CONTEXT_STRING_ANGLE)); |
| const char *capturedSerializedState = mTraceLibrary->getSerializedContextState(frame); |
| |
| if (replayedSerializedState == nullptr || |
| strlen(replayedSerializedState) <= kTooShortStateLength) |
| { |
| printf("Could not retrieve replay serialized state string.\n"); |
| return kSerializationFailure; |
| } |
| |
| if (capturedSerializedState == nullptr || |
| strlen(capturedSerializedState) <= kTooShortStateLength) |
| { |
| printf("Could not retrieve captured serialized state string.\n"); |
| return kSerializationFailure; |
| } |
| |
| // Swap always to allow RenderDoc/other tools to capture frames. |
| swap(); |
| if (!CompareSerializedContexts(replayedSerializedState, capturedSerializedState)) |
| { |
| printf("Serialized contexts differ, saving files.\n"); |
| |
| std::ostringstream replayName; |
| replayName << exeDir << angle::GetPathSeparator() << traceInfo.name |
| << "_ContextReplayed" << frame << ".json"; |
| |
| std::ofstream debugReplay(replayName.str()); |
| if (!debugReplay) |
| { |
| printf("Error opening debug replay stream.\n"); |
| } |
| else |
| { |
| debugReplay << (replayedSerializedState ? replayedSerializedState : "") << "\n"; |
| printf("Wrote %s.\n", replayName.str().c_str()); |
| } |
| |
| std::ostringstream captureName; |
| captureName << exeDir << angle::GetPathSeparator() << traceInfo.name |
| << "_ContextCaptured" << frame << ".json"; |
| |
| std::ofstream debugCapture(captureName.str()); |
| if (!debugCapture) |
| { |
| printf("Error opening debug capture stream.\n"); |
| } |
| else |
| { |
| debugCapture << (capturedSerializedState ? capturedSerializedState : "") |
| << "\n"; |
| printf("Wrote %s.\n", captureName.str().c_str()); |
| } |
| |
| cleanupTest(); |
| return kSerializationFailure; |
| } |
| } |
| cleanupTest(); |
| return kExitSuccess; |
| } |
| |
| int run() |
| { |
| std::string startingDirectory = angle::GetCWD().value(); |
| |
| // Set CWD to executable directory. |
| std::string exeDir = angle::GetExecutableDirectory(); |
| |
| std::vector<std::string> traces; |
| |
| std::stringstream tracePathStream; |
| tracePathStream << exeDir << angle::GetPathSeparator() << kTracePath; |
| |
| if (!angle::LoadTraceNamesFromJSON(tracePathStream.str(), &traces)) |
| { |
| std::cout << "Unable to load trace names from " << kTracePath << "\n"; |
| return 1; |
| } |
| |
| for (const std::string &trace : traces) |
| { |
| std::stringstream traceJsonPathStream; |
| traceJsonPathStream << exeDir << angle::GetPathSeparator() |
| << ANGLE_CAPTURE_REPLAY_TEST_DATA_DIR << angle::GetPathSeparator() |
| << trace << ".json"; |
| std::string traceJsonPath = traceJsonPathStream.str(); |
| |
| int result = kInitializationFailure; |
| angle::TraceInfo traceInfo = {}; |
| if (!angle::LoadTraceInfoFromJSON(trace, traceJsonPath, &traceInfo)) |
| { |
| std::cout << "Unable to load trace data: " << traceJsonPath << "\n"; |
| } |
| else |
| { |
| result = runTest(exeDir, traceInfo); |
| } |
| std::cout << kResultTag << " " << trace << " " << result << "\n"; |
| } |
| |
| angle::SetCWD(startingDirectory.c_str()); |
| return kExitSuccess; |
| } |
| |
| private: |
| OSWindow *mOSWindow = nullptr; |
| EGLWindow *mEGLWindow = nullptr; |
| EGLPlatformParameters mPlatformParams; |
| angle::PlatformMethods mPlatformMethods; |
| // Handle to the entry point binding library. |
| std::unique_ptr<angle::Library> mEntryPointsLib; |
| std::unique_ptr<angle::TraceLibrary> mTraceLibrary; |
| }; |
| |
| int main(int argc, char **argv) |
| { |
| CaptureReplayTests app; |
| return app.run(); |
| } |