| /* |
| * Copyright 2022 Rive |
| */ |
| |
| #include "testing_window.hpp" |
| |
| #if defined(RIVE_TOOLS_NO_GL) || defined(__EMSCRIPTEN__) |
| |
| TestingWindow* TestingWindow::MakeEGL(Backend backend, |
| const BackendParams& backendParams, |
| void* platformWindow) |
| { |
| return nullptr; |
| } |
| |
| #else |
| |
| #include <cassert> |
| #include <stdio.h> |
| |
| #define EGL_EGL_PROTOTYPES 0 |
| #include <EGL/egl.h> |
| #include <EGL/eglext.h> |
| |
| // Include after Windows headers due to conflicts with #defines. |
| #include "common/offscreen_render_target.hpp" |
| #include "rive/renderer/gl/render_context_gl_impl.hpp" |
| #include "rive/renderer/gl/render_target_gl.hpp" |
| #include "rive/renderer.hpp" |
| #include "rive/renderer/gl/render_target_gl.hpp" |
| #include "testing_gl_renderer.hpp" |
| |
| #if defined(_WIN32) |
| |
| #include <libloaderapi.h> |
| |
| using PlatformLibraryHandle = HMODULE; |
| using PlatformProcAddress = FARPROC; |
| |
| static PlatformLibraryHandle platform_dlopen(const char* libname) |
| { |
| PlatformLibraryHandle lib = LoadLibraryA(libname); |
| if (!lib) |
| { |
| WCHAR err[512]; |
| FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | |
| FORMAT_MESSAGE_IGNORE_INSERTS, |
| 0, |
| GetLastError(), |
| 0, |
| err, |
| 512, |
| 0); |
| fprintf(stderr, "Failed load %s. %ws\n", libname, err); |
| } |
| return lib; |
| } |
| |
| static PlatformProcAddress platform_dlsym(PlatformLibraryHandle lib, |
| const char* symbol) |
| { |
| |
| return GetProcAddress(lib, symbol); |
| } |
| |
| #else |
| |
| #include <dlfcn.h> |
| |
| using PlatformLibraryHandle = void*; |
| using PlatformProcAddress = void*; |
| |
| static PlatformLibraryHandle platform_dlopen(const char* libname) |
| { |
| std::string filename = libname; |
| #ifdef __APPLE__ |
| filename.append(".dylib"); |
| #else |
| filename.append(".so"); |
| #endif |
| PlatformLibraryHandle lib = dlopen(filename.c_str(), RTLD_LAZY); |
| if (!lib) |
| { |
| fprintf(stderr, "Failed load %s. %s\n", filename.c_str(), dlerror()); |
| } |
| return lib; |
| } |
| |
| static PlatformProcAddress platform_dlsym(PlatformLibraryHandle lib, |
| const char* symbol) |
| { |
| return dlsym(lib, symbol); |
| } |
| |
| #endif |
| |
| static PlatformLibraryHandle s_LibEGL = nullptr; |
| |
| #define GET_EGL_PROC(eglProc) \ |
| eglProc = (decltype(eglProc))platform_dlsym(s_LibEGL, #eglProc); \ |
| if (!eglProc) \ |
| { \ |
| fprintf(stderr, "Failed locate " #eglProc " in libEGL.\n"); \ |
| abort(); \ |
| } |
| |
| static PFNEGLGETDISPLAYPROC eglGetDisplay; |
| static PFNEGLQUERYSTRINGPROC eglQueryString; |
| static PFNEGLINITIALIZEPROC eglInitialize; |
| static PFNEGLCHOOSECONFIGPROC eglChooseConfig; |
| static PFNEGLCREATECONTEXTPROC eglCreateContext; |
| static PFNEGLDESTROYCONTEXTPROC eglDestroyContext; |
| static PFNEGLCREATEPBUFFERSURFACEPROC eglCreatePbufferSurface; |
| static PFNEGLCREATEWINDOWSURFACEPROC eglCreateWindowSurface; |
| static PFNEGLQUERYSURFACEPROC eglQuerySurface; |
| static PFNEGLDESTROYSURFACEPROC eglDestroySurface; |
| static PFNEGLMAKECURRENTPROC eglMakeCurrent; |
| static PFNEGLSWAPBUFFERSPROC eglSwapBuffers; |
| static PFNEGLGETPROCADDRESSPROC eglGetProcAddress; |
| |
| static void init_egl() |
| { |
| if (s_LibEGL) |
| { |
| return; |
| } |
| |
| s_LibEGL = platform_dlopen("libEGL"); |
| if (!s_LibEGL) |
| { |
| fprintf(stderr, |
| "Failed load libEGL. Did you build ANGLE and copy over libEGL " |
| "and libGLESv2?\n"); |
| abort(); |
| } |
| |
| GET_EGL_PROC(eglGetDisplay); |
| GET_EGL_PROC(eglQueryString); |
| GET_EGL_PROC(eglInitialize); |
| GET_EGL_PROC(eglChooseConfig); |
| GET_EGL_PROC(eglCreateContext); |
| GET_EGL_PROC(eglDestroyContext); |
| GET_EGL_PROC(eglCreatePbufferSurface); |
| GET_EGL_PROC(eglCreateWindowSurface); |
| GET_EGL_PROC(eglQuerySurface); |
| GET_EGL_PROC(eglDestroySurface); |
| GET_EGL_PROC(eglMakeCurrent); |
| GET_EGL_PROC(eglSwapBuffers); |
| GET_EGL_PROC(eglGetProcAddress); |
| } |
| |
| #ifdef DEBUG |
| static void GL_APIENTRY err_msg_callback(GLenum source, |
| GLenum type, |
| GLuint id, |
| GLenum severity, |
| GLsizei length, |
| const GLchar* message, |
| const void* userParam) |
| { |
| if (type == GL_DEBUG_TYPE_ERROR_KHR) |
| { |
| printf("GL ERROR: %s\n", message); |
| fflush(stdout); |
| abort(); |
| } |
| else if (type == GL_DEBUG_TYPE_PERFORMANCE_KHR) |
| { |
| // Don't display GL perf warnings during normal testing. |
| #if 0 |
| printf("GL PERF: %s\n", message); |
| #endif |
| } |
| else |
| { |
| printf("GL MISC: %s\n", message); |
| } |
| } |
| #endif |
| |
| // Use an offscreen, platform independent window. |
| class EGLWindow |
| { |
| public: |
| virtual ~EGLWindow() { deleteSurface(); } |
| |
| operator EGLSurface() const { return m_surface; } |
| int width() const { return m_width; } |
| int height() const { return m_height; } |
| bool isMSAA() const { return m_isMSAA; } |
| |
| virtual bool isOffscreen() const = 0; |
| |
| virtual bool resize(int width, int height) = 0; |
| |
| protected: |
| EGLWindow(EGLDisplay display, EGLConfig config, int samples) : |
| m_display(display), |
| m_config(config), |
| // "samples == 1" means "msaa mode with 1 sample". |
| // "samples == 0" means "non-msaa mode". |
| m_isMSAA(samples > 0) |
| {} |
| |
| void deleteSurface() |
| { |
| if (m_surface && !eglDestroySurface(m_display, m_surface)) |
| { |
| fprintf(stderr, "eglDestroySurface failed.\n"); |
| abort(); |
| } |
| } |
| |
| const EGLDisplay m_display; |
| const EGLConfig m_config; |
| const bool m_isMSAA; |
| void* m_surface = nullptr; |
| int m_width = 0; |
| int m_height = 0; |
| }; |
| |
| class PbufferWindow : public EGLWindow |
| { |
| public: |
| PbufferWindow(EGLDisplay display, EGLConfig config, int samples) : |
| EGLWindow(display, config, samples) |
| { |
| resize(1, 1); |
| } |
| |
| bool isOffscreen() const final { return true; } |
| |
| bool resize(int width, int height) final |
| { |
| deleteSurface(); |
| const EGLint surfaceAttribs[] = {EGL_WIDTH, |
| width, |
| EGL_HEIGHT, |
| height, |
| EGL_NONE}; |
| m_surface = |
| eglCreatePbufferSurface(m_display, m_config, surfaceAttribs); |
| if (!m_surface) |
| { |
| fprintf(stderr, "eglCreatePbufferSurface failed.\n"); |
| abort(); |
| } |
| m_width = width; |
| m_height = height; |
| return true; |
| } |
| }; |
| |
| class NativeWindow : public EGLWindow |
| { |
| public: |
| NativeWindow(EGLDisplay display, |
| EGLConfig config, |
| int samples, |
| void* platformWindow) : |
| EGLWindow(display, config, samples) |
| { |
| m_surface = eglCreateWindowSurface( |
| m_display, |
| m_config, |
| reinterpret_cast<EGLNativeWindowType>(platformWindow), |
| nullptr); |
| if (!m_surface) |
| { |
| fprintf(stderr, "eglCreateWindowSurface failed.\n"); |
| abort(); |
| } |
| EGLint eglInt; |
| eglQuerySurface(m_display, m_surface, EGL_WIDTH, &eglInt); |
| m_width = eglInt; |
| eglQuerySurface(m_display, m_surface, EGL_HEIGHT, &eglInt); |
| m_height = eglInt; |
| } |
| |
| bool isOffscreen() const final { return false; } |
| |
| bool resize(int width, int height) final |
| { |
| return width <= m_width && height <= m_height; |
| } |
| }; |
| |
| class TestingWindowEGL : public TestingWindow |
| { |
| public: |
| TestingWindowEGL(const BackendParams& backendParams, |
| EGLint angleBackend, |
| void* platformWindow) : |
| m_renderer(TestingGLRenderer::Make(backendParams)) |
| { |
| init_egl(); |
| |
| int samples = backendParams.msaa ? 4 : 0; |
| |
| #ifdef RIVE_DESKTOP_GL |
| if (angleBackend != EGL_NONE) |
| { |
| PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT; |
| GET_EGL_PROC(eglGetPlatformDisplayEXT); |
| |
| const EGLint displayAttribs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, |
| angleBackend, |
| EGL_NONE}; |
| m_Display = eglGetPlatformDisplayEXT( |
| EGL_PLATFORM_ANGLE_ANGLE, |
| reinterpret_cast<void*>(EGL_DEFAULT_DISPLAY), |
| displayAttribs); |
| if (!m_Display) |
| { |
| fprintf(stderr, "eglGetPlatformDisplayEXT failed.\n"); |
| abort(); |
| } |
| samples = 0; // Test our offscreen MSAA path on ANGLE. |
| } |
| else |
| #endif |
| { |
| assert(angleBackend == EGL_NONE); |
| m_Display = eglGetDisplay(EGL_DEFAULT_DISPLAY); |
| if (!m_Display) |
| { |
| fprintf(stderr, "eglGetDisplay failed.\n"); |
| abort(); |
| } |
| } |
| |
| EGLint majorVersion; |
| EGLint minorVersion; |
| if (!eglInitialize(m_Display, &majorVersion, &minorVersion)) |
| { |
| fprintf(stderr, "eglInitialize failed.\n"); |
| abort(); |
| } |
| // printf("Initialized EGL version %i.%i\n", majorVersion, |
| // minorVersion); printf("EGL_VENDOR: %s\n", eglQueryString(m_Display, |
| // EGL_VENDOR)); printf("EGL_VERSION: %s\n", eglQueryString(m_Display, |
| // EGL_VERSION)); |
| if (angleBackend != EGL_NONE && |
| !strstr(eglQueryString(m_Display, EGL_VERSION), "ANGLE")) |
| { |
| fprintf(stderr, |
| "libEGL does not appear to be ANGLE. Did you build ANGLE " |
| "and copy over libEGL " |
| "and libGLESv2?\n"); |
| abort(); |
| } |
| |
| EGLint numConfigs; |
| const EGLint configAttribs[] = {EGL_SURFACE_TYPE, |
| EGL_PBUFFER_BIT, |
| EGL_RENDERABLE_TYPE, |
| EGL_OPENGL_ES3_BIT, |
| EGL_COLOR_BUFFER_TYPE, |
| EGL_RGB_BUFFER, |
| EGL_RED_SIZE, |
| 8, |
| EGL_GREEN_SIZE, |
| 8, |
| EGL_BLUE_SIZE, |
| 8, |
| EGL_ALPHA_SIZE, |
| 8, |
| EGL_DEPTH_SIZE, |
| samples == 0 ? 0 : 24, |
| EGL_STENCIL_SIZE, |
| samples == 0 ? 0 : 8, |
| EGL_SAMPLE_BUFFERS, |
| samples == 0 ? 0 : 1, |
| EGL_SAMPLES, |
| samples, |
| EGL_NONE}; |
| |
| EGLConfig config; |
| if (!eglChooseConfig(m_Display, configAttribs, &config, 1, &numConfigs)) |
| { |
| fprintf(stderr, "eglChooseConfig failed.\n"); |
| abort(); |
| } |
| |
| // Create an ES 3.0 context. This very closely mirrors webgl2. |
| const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, |
| 3, |
| EGL_CONTEXT_MINOR_VERSION, |
| 0, |
| EGL_NONE}; |
| m_Context = |
| eglCreateContext(m_Display, config, nullptr, contextAttribs); |
| if (m_Context == EGL_NO_CONTEXT) |
| { |
| fprintf(stderr, "eglCreateContext failed.\n"); |
| abort(); |
| } |
| |
| if (platformWindow != nullptr) |
| { |
| m_window = std::make_unique<NativeWindow>(m_Display, |
| config, |
| samples, |
| platformWindow); |
| } |
| else |
| { |
| m_window = |
| std::make_unique<PbufferWindow>(m_Display, config, samples); |
| } |
| m_width = m_window->width(); |
| m_height = m_window->height(); |
| if (!eglMakeCurrent(m_Display, *m_window, *m_window, m_Context)) |
| { |
| fprintf(stderr, "eglMakeCurrent failed.\n"); |
| abort(); |
| } |
| |
| #ifdef RIVE_DESKTOP_GL |
| // Load the OpenGL API using glad. |
| if (!gladLoadCustomLoader( |
| reinterpret_cast<GLADloadfunc>(eglGetProcAddress))) |
| { |
| fprintf(stderr, "Failed to initialize glad.\n"); |
| abort(); |
| } |
| #endif |
| |
| auto rendererStr = |
| reinterpret_cast<const char*>(glGetString(GL_RENDERER)); |
| printf("==== EGL GPU: OpenGL %s; %s; %s ====\n", |
| glGetString(GL_VENDOR), |
| rendererStr, |
| glGetString(GL_VERSION)); |
| |
| int extensionCount; |
| glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount); |
| #ifdef DEBUG |
| for (int i = 0; i < extensionCount; ++i) |
| { |
| auto* ext = |
| reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, i)); |
| if (strcmp(ext, "GL_KHR_debug") == 0 && |
| // Our shader compiles can take over 30 SECONDS on PowerVR when |
| // debug output is enabled. Just don't use it. |
| !strstr(rendererStr, "PowerVR")) |
| { |
| glEnable(GL_DEBUG_OUTPUT_KHR); |
| glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_KHR); |
| |
| auto glDebugMessageControlKHR = |
| reinterpret_cast<PFNGLDEBUGMESSAGECONTROLKHRPROC>( |
| eglGetProcAddress("glDebugMessageControlKHR")); |
| assert(glDebugMessageControlKHR); |
| glDebugMessageControlKHR(GL_DONT_CARE, |
| GL_DONT_CARE, |
| GL_DONT_CARE, |
| 0, |
| NULL, |
| GL_TRUE); |
| |
| auto glDebugMessageCallbackKHR = |
| reinterpret_cast<PFNGLDEBUGMESSAGECALLBACKKHRPROC>( |
| eglGetProcAddress("glDebugMessageCallbackKHR")); |
| assert(glDebugMessageCallbackKHR); |
| glDebugMessageCallbackKHR(&err_msg_callback, nullptr); |
| |
| // printf("%s enabled.\n", ext); |
| continue; |
| } |
| } |
| #endif |
| |
| m_renderer->init(reinterpret_cast<void*>(eglGetProcAddress)); |
| } |
| |
| ~TestingWindowEGL() |
| { |
| eglMakeCurrent(m_Display, |
| EGL_NO_SURFACE, |
| EGL_NO_SURFACE, |
| EGL_NO_CONTEXT); |
| |
| if (m_Context) |
| { |
| eglDestroyContext(m_Display, m_Context); |
| } |
| } |
| |
| rive::Factory* factory() override { return m_renderer->factory(); } |
| |
| void resize(int width, int height) override |
| { |
| if (width == m_width && height == m_height) |
| { |
| return; |
| } |
| |
| // EXT_shader_pixel_local_storage has issues rendering to an offscreen |
| // Pbuffer. |
| bool needsOffscreenWorkaround = !m_window->isMSAA() && |
| renderContextGLImpl() |
| ->capabilities() |
| .EXT_shader_pixel_local_storage && |
| m_window->isOffscreen(); |
| if (needsOffscreenWorkaround || !m_window->resize(width, height)) |
| { |
| // ARM Mali GPUs seem to hang while rendering goldens to a Pbuffer |
| // with EXT_shader_pixel_local_storage. Rendering to a texture |
| // instead seems to work. |
| m_headlessRenderTexture = glutils::Texture(); |
| glActiveTexture(GL_TEXTURE0); |
| glBindTexture(GL_TEXTURE_2D, m_headlessRenderTexture); |
| glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height); |
| } |
| else |
| { |
| m_headlessRenderTexture = glutils::Texture::Zero(); |
| if (!eglMakeCurrent(m_Display, *m_window, *m_window, m_Context)) |
| { |
| fprintf(stderr, "eglMakeCurrent failed.\n"); |
| abort(); |
| } |
| } |
| |
| glViewport(0, 0, width, height); |
| m_width = width; |
| m_height = height; |
| } |
| |
| std::unique_ptr<rive::Renderer> beginFrame( |
| const FrameOptions& options) override |
| { |
| auto renderer = |
| m_renderer->reset(m_width, m_height, m_headlessRenderTexture); |
| m_renderer->beginFrame(options); |
| return renderer; |
| } |
| |
| void endFrame(std::vector<uint8_t>* pixelData) override |
| { |
| m_renderer->flush(); |
| if (m_headlessRenderTexture != 0 && !m_window->isOffscreen()) |
| { |
| // Copy the offscreen texture back to the main window for |
| // visualization purposes. |
| glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); |
| glBlitFramebuffer(0, |
| 0, |
| m_width, |
| m_height, |
| 0, |
| 0, |
| m_width, |
| m_height, |
| GL_COLOR_BUFFER_BIT, |
| GL_NEAREST); |
| } |
| if (pixelData) |
| { |
| pixelData->resize(m_height * m_width * 4); |
| static_cast<rive::gpu::RenderTargetGL*>(m_renderer->renderTarget()) |
| ->bindDestinationFramebuffer(GL_READ_FRAMEBUFFER); |
| glReadPixels(0, |
| 0, |
| m_width, |
| m_height, |
| GL_RGBA, |
| GL_UNSIGNED_BYTE, |
| pixelData->data()); |
| } |
| eglSwapBuffers(m_Display, *m_window); |
| } |
| |
| rive::gpu::RenderContext* renderContext() const override |
| { |
| return m_renderer->renderContext(); |
| } |
| |
| rive::gpu::RenderContextGLImpl* renderContextGLImpl() const override |
| { |
| if (auto ctx = renderContext()) |
| { |
| return ctx->static_impl_cast<rive::gpu::RenderContextGLImpl>(); |
| } |
| return nullptr; |
| } |
| |
| rive::gpu::RenderTarget* renderTarget() const override |
| { |
| return m_renderer->renderTarget(); |
| } |
| |
| rive::rcp<rive_tests::OffscreenRenderTarget> makeOffscreenRenderTarget( |
| uint32_t width, |
| uint32_t height, |
| bool riveRenderable) const override |
| { |
| return rive_tests::OffscreenRenderTarget::MakeGL(renderContextGLImpl(), |
| width, |
| height); |
| } |
| |
| void flushPLSContext( |
| rive::gpu::RenderTarget* offscreenRenderTarget) override |
| { |
| m_renderer->flushPLSContext(offscreenRenderTarget); |
| } |
| |
| private: |
| const std::unique_ptr<TestingGLRenderer> m_renderer; |
| void* m_Display; |
| void* m_Context; |
| std::unique_ptr<EGLWindow> m_window; |
| glutils::Texture m_headlessRenderTexture = glutils::Texture::Zero(); |
| }; |
| |
| TestingWindow* TestingWindow::MakeEGL(Backend backend, |
| const BackendParams& backendParams, |
| void* platformWindow) |
| { |
| EGLint angleBackend = EGL_NONE; |
| if (backend == Backend::angle) |
| { |
| #ifdef __APPLE__ |
| angleBackend = EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE; |
| #elif defined(_WIN32) |
| angleBackend = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; |
| #else |
| angleBackend = EGL_PLATFORM_ANGLE_TYPE_VULKAN_ANGLE; |
| #endif |
| } |
| return new TestingWindowEGL(backendParams, angleBackend, platformWindow); |
| } |
| |
| #endif |