| /* |
| * Copyright 2025 Rive |
| */ |
| |
| #include "common/testing_window.hpp" |
| #include "rive/renderer.hpp" |
| #include "rive/factory.hpp" |
| |
| #include <catch.hpp> |
| |
| using namespace rive; |
| using namespace rive::gpu; |
| |
| // Factories to manually instantiate real rendering contexts, for unit testing |
| // the full pipeline. |
| struct FactoryWrapper |
| { |
| const char* displayName; |
| std::function<std::unique_ptr<TestingWindow>()> function; |
| }; |
| static FactoryWrapper testingWindowFactories[] = { |
| {"Vulkan", |
| []() { |
| return std::unique_ptr<TestingWindow>( |
| TestingWindow::MakeVulkanTexture({ |
| #ifdef RIVE_ANDROID |
| // Android doesn't support validation layers for command line |
| // apps like the unit_tests. |
| .disableValidationLayers = true, |
| // The OnePlus7 doesn't support debug callbacks either for |
| // command line apps. |
| .disableDebugCallbacks = true, |
| #endif |
| })); |
| }}, |
| #if defined(__APPLE__) |
| {"Metal", |
| []() { |
| return std::unique_ptr<TestingWindow>( |
| TestingWindow::MakeMetalTexture({})); |
| }}, |
| #endif |
| #ifdef _WIN32 |
| {"D3D12", |
| []() { |
| return std::unique_ptr<TestingWindow>(TestingWindow::MakeFiddleContext( |
| TestingWindow::Backend::d3d12, |
| {}, |
| TestingWindow::Visibility::headless, |
| nullptr)); |
| }}, |
| {"D3D12 atomic", |
| []() { |
| return std::unique_ptr<TestingWindow>(TestingWindow::MakeFiddleContext( |
| TestingWindow::Backend::d3d12, |
| {.atomic = true}, |
| TestingWindow::Visibility::headless, |
| nullptr)); |
| }}, |
| {"D3D11", |
| []() { |
| return std::unique_ptr<TestingWindow>(TestingWindow::MakeFiddleContext( |
| TestingWindow::Backend::d3d, |
| {}, |
| TestingWindow::Visibility::headless, |
| nullptr)); |
| }}, |
| {"D3D11 atomic", |
| []() { |
| return std::unique_ptr<TestingWindow>(TestingWindow::MakeFiddleContext( |
| TestingWindow::Backend::d3d, |
| {.atomic = true}, |
| TestingWindow::Visibility::headless, |
| nullptr)); |
| }}, |
| {"OpenGL", |
| []() { |
| return std::unique_ptr<TestingWindow>(TestingWindow::MakeFiddleContext( |
| TestingWindow::Backend::gl, |
| {}, |
| TestingWindow::Visibility::headless, |
| nullptr)); |
| }}, |
| {"OpenGL atomic", |
| []() { |
| return std::unique_ptr<TestingWindow>(TestingWindow::MakeFiddleContext( |
| TestingWindow::Backend::gl, |
| {.atomic = true}, |
| TestingWindow::Visibility::headless, |
| nullptr)); |
| }}, |
| #endif |
| #ifdef RIVE_ANDROID |
| {"EGL (GL backend)", |
| []() { |
| return std::unique_ptr<TestingWindow>( |
| TestingWindow::MakeEGL(TestingWindow::Backend::gl, {}, nullptr)); |
| }}, |
| #endif |
| }; |
| |
| // Ensure that rendering still succeeds when compilations fail (e.g., by falling |
| // back on an uber shader or at least not crashing). Valid compilations may fail |
| // in the real world if the device is pressed for resources or in a bad state. |
| TEST_CASE("synthesizedFailureType", "[rendering]") |
| { |
| // TODO: There are potentially stronger ways to build some of these |
| // synthesized failures if we were to pass SynthesizedFailureType as a |
| // creation option instead of on beginFrame |
| for (auto failureType : {SynthesizedFailureType::shaderCompilation, |
| SynthesizedFailureType::ubershaderLoad, |
| SynthesizedFailureType::pipelineCreation}) |
| { |
| switch (failureType) |
| { |
| case SynthesizedFailureType::shaderCompilation: |
| printf("testing synthesied shader compilation failure\n"); |
| break; |
| case SynthesizedFailureType::ubershaderLoad: |
| printf("testing synthesized ubershader load failure\n"); |
| break; |
| case SynthesizedFailureType::pipelineCreation: |
| printf("testing synthesized pipeline creation failure\n"); |
| break; |
| case SynthesizedFailureType::none: |
| // android compiler complains (rightly) if this case isn't here. |
| RIVE_UNREACHABLE(); |
| } |
| for (auto& testingWindowFactory : testingWindowFactories) |
| { |
| printf(" testing with '%s' factory\n", |
| testingWindowFactory.displayName); |
| std::unique_ptr<TestingWindow> window = |
| testingWindowFactory.function(); |
| if (window == nullptr) |
| { |
| continue; |
| } |
| Factory* factory = window->factory(); |
| |
| window->resize(32, 32); |
| |
| // Expected colors after we draw a cyan rectangle. |
| std::vector<uint8_t> drawColors; |
| drawColors.reserve(32 * 32 * 4); |
| for (size_t i = 0; i < 32 * 32; ++i) |
| drawColors.insert(drawColors.end(), {0x00, 0xff, 0xff, 0xff}); |
| |
| // Expected colors when only the clear happens (because even the |
| // uber shader failed to compile). |
| std::vector<uint8_t> clearColors; |
| clearColors.reserve(32 * 32 * 4); |
| for (size_t i = 0; i < 32 * 32; ++i) |
| clearColors.insert(clearColors.end(), {0xff, 0x00, 0x00, 0xff}); |
| |
| for (bool disableRasterOrdering : {false, true}) |
| { |
| auto renderer = window->beginFrame({ |
| .clearColor = 0xffff0000, |
| .doClear = true, |
| .disableRasterOrdering = disableRasterOrdering, |
| .synthesizedFailureType = failureType, |
| }); |
| |
| rcp<RenderPath> path = |
| factory->makeRenderPath(AABB{0, 0, 32, 32}); |
| rcp<RenderPaint> paint = factory->makeRenderPaint(); |
| paint->color(0xff00ffff); |
| renderer->drawPath(path.get(), paint.get()); |
| |
| std::vector<uint8_t> pixels; |
| window->endFrame(&pixels); |
| |
| // There are two acceptable results to this test: |
| // |
| // 1) The draw happens anyway because we fell back on a |
| // precompiled uber shader. |
| // |
| // 2) The uber shader also synthesizes a compilation faiulre, so |
| // only the clear color makes it through. |
| if (pixels != drawColors && pixels != clearColors) |
| { |
| printf("Expected {%02x, %02x, %02x, %02x} or {%02x, %02x, " |
| "%02x, %02x}, got {%02x, %02x, %02x, %02x}", |
| drawColors[0], |
| drawColors[1], |
| drawColors[2], |
| drawColors[3], |
| clearColors[0], |
| clearColors[1], |
| clearColors[2], |
| clearColors[3], |
| pixels[0], |
| pixels[1], |
| pixels[2], |
| pixels[3]); |
| } |
| CHECK((pixels == drawColors || pixels == clearColors)); |
| } |
| } |
| } |
| } |
| |
| #if !defined(RIVE_TOOLS_NO_GL) && (defined(_WIN32) || defined(RIVE_ANDROID)) |
| #include "rive/renderer/render_context.hpp" |
| #include "rive/renderer/rive_renderer.hpp" |
| #include "rive/renderer/gl/render_target_gl.hpp" |
| |
| // A headless GL window, or null if this device has no GL driver. |
| static std::unique_ptr<TestingWindow> makeGLTestingWindow() |
| { |
| #ifdef RIVE_ANDROID |
| // Android has no GLFW, so it goes through EGL instead. |
| return std::unique_ptr<TestingWindow>( |
| TestingWindow::MakeEGL(TestingWindow::Backend::gl, {}, nullptr)); |
| #else |
| return std::unique_ptr<TestingWindow>( |
| TestingWindow::MakeFiddleContext(TestingWindow::Backend::gl, |
| {}, |
| TestingWindow::Visibility::headless, |
| nullptr)); |
| #endif |
| } |
| |
| // TextureRenderTargetGL caches the framebuffer attachment for its target |
| // texture. setTargetTexture() has to invalidate that cache for the depthStencil |
| // framebuffer as well as the plain one -- otherwise a render target that is |
| // reused with a new texture, at an unchanged sample count, keeps drawing into |
| // the texture it was last attached to. |
| TEST_CASE("TextureRenderTargetGL_retargetsDepthStencilFramebuffer", |
| "[rendering]") |
| { |
| constexpr static uint32_t Width = 32, Height = 32; |
| // 0xAARRGGBB. |
| constexpr static ColorInt Red = 0xffff0000; |
| constexpr static ColorInt Cyan = 0xff00ffff; |
| |
| std::unique_ptr<TestingWindow> window = makeGLTestingWindow(); |
| if (window == nullptr) |
| { |
| return; |
| } |
| RenderContext* renderContext = window->renderContext(); |
| REQUIRE(renderContext != nullptr); |
| Factory* factory = window->factory(); |
| |
| // Two interchangeable target textures, each seeded with a sentinel so an |
| // untouched texture is distinguishable from a correctly drawn one. |
| GLuint textures[2]; |
| glGenTextures(2, textures); |
| for (GLuint tex : textures) |
| { |
| glBindTexture(GL_TEXTURE_2D, tex); |
| glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, Width, Height); |
| } |
| |
| GLuint readbackFBO; |
| glGenFramebuffers(1, &readbackFBO); |
| auto readback = [&](GLuint tex) { |
| std::vector<uint8_t> pixels(Width * Height * 4); |
| glBindFramebuffer(GL_FRAMEBUFFER, readbackFBO); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, |
| GL_COLOR_ATTACHMENT0, |
| GL_TEXTURE_2D, |
| tex, |
| 0); |
| glReadPixels(0, |
| 0, |
| Width, |
| Height, |
| GL_RGBA, |
| GL_UNSIGNED_BYTE, |
| pixels.data()); |
| return pixels; |
| }; |
| auto solidColor = [](ColorInt color) { |
| std::vector<uint8_t> pixels; |
| pixels.reserve(Width * Height * 4); |
| for (size_t i = 0; i < Width * Height; ++i) |
| { |
| pixels.insert(pixels.end(), |
| {static_cast<uint8_t>(colorRed(color)), |
| static_cast<uint8_t>(colorGreen(color)), |
| static_cast<uint8_t>(colorBlue(color)), |
| static_cast<uint8_t>(colorAlpha(color))}); |
| } |
| return pixels; |
| }; |
| |
| // One render target, reused across every sample count and both textures. |
| auto renderTarget = make_rcp<TextureRenderTargetGL>(Width, Height); |
| |
| // 4 -> 1 exercises the sample count changing, and drawing twice at the same |
| // count exercises the case the cache invalidation exists for. |
| for (uint32_t msaaSampleCount : {4u, 1u, 4u}) |
| { |
| for (int i = 0; i < 2; ++i) |
| { |
| const ColorInt color = (i == 0) ? Red : Cyan; |
| renderTarget->setTargetTexture(textures[i]); |
| renderContext->beginFrame({ |
| .renderTargetWidth = Width, |
| .renderTargetHeight = Height, |
| .loadAction = gpu::LoadAction::clear, |
| .clearColor = color, |
| .msaaSampleCount = msaaSampleCount, |
| .clockwiseFillOverride = true, |
| }); |
| RiveRenderer renderer(renderContext); |
| rcp<RenderPath> path = |
| factory->makeRenderPath(AABB{0, 0, Width, Height}); |
| rcp<RenderPaint> paint = factory->makeRenderPaint(); |
| paint->color(color); |
| renderer.drawPath(path.get(), paint.get()); |
| renderContext->flush({.renderTarget = renderTarget.get()}); |
| } |
| |
| // The second draw must have landed in textures[1], and must not have |
| // clobbered textures[0]. |
| INFO("msaaSampleCount " << msaaSampleCount); |
| CHECK(readback(textures[1]) == solidColor(Cyan)); |
| CHECK(readback(textures[0]) == solidColor(Red)); |
| } |
| |
| glDeleteFramebuffers(1, &readbackFBO); |
| glDeleteTextures(2, textures); |
| } |
| #endif |
| |
| // namespace rive::gpu |