/* * Copyright (C) 2021 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "Test.h" #if PLATFORM(COCOA) && ENABLE(WEBGL) #import "TestUtilities.h" #import "WebCoreUtilities.h" #import #import #import #import #import #import #import namespace TestWebKitAPI { namespace { class TestedGraphicsContextGLCocoa : public WebCore::GraphicsContextGLCocoa { public: static RefPtr create(WebCore::GraphicsContextGLAttributes&& attributes) { auto context = adoptRef(*new TestedGraphicsContextGLCocoa(WTFMove(attributes))); if (!context->initialize()) return nullptr; return context; } RefPtr layerContentsDisplayDelegate() final { return nullptr; } private: TestedGraphicsContextGLCocoa(WebCore::GraphicsContextGLAttributes attributes) : WebCore::GraphicsContextGLCocoa(WTFMove(attributes), { }) { } }; class GraphicsContextGLCocoaTest : public ::testing::Test { public: void SetUp() override // NOLINT { m_scopedProcessType = ScopedSetAuxiliaryProcessTypeForTesting { WebCore::AuxiliaryProcessType::GPU }; } void TearDown() override // NOLINT { m_scopedProcessType = std::nullopt; } private: std::optional m_scopedProcessType; }; } static const int expectedDisplayBufferPoolSize = 3; static RefPtr createDefaultTestContext(WebCore::IntSize contextSize) { WebCore::GraphicsContextGLAttributes attributes; attributes.useMetal = true; attributes.antialias = false; attributes.depth = false; attributes.stencil = false; attributes.alpha = true; attributes.preserveDrawingBuffer = false; auto context = TestedGraphicsContextGLCocoa::create(WTFMove(attributes)); if (!context) return nullptr; context->reshape(contextSize.width(), contextSize.height()); return context; } static ::testing::AssertionResult changeContextContents(TestedGraphicsContextGLCocoa& context, int iteration) { context.markContextChanged(); WebCore::Color expected { iteration % 2 ? WebCore::Color::green : WebCore::Color::yellow }; auto [r, g, b, a] = expected.toColorTypeLossy>().resolved(); context.clearColor(r, g, b, a); context.clear(WebCore::GraphicsContextGL::COLOR_BUFFER_BIT); uint8_t gotValues[4] = { }; auto sampleAt = context.getInternalFramebufferSize(); sampleAt.contract(2, 3); sampleAt.clampNegativeToZero(); context.readnPixels(sampleAt.width(), sampleAt.height(), 1, 1, WebCore::GraphicsContextGL::RGBA, WebCore::GraphicsContextGL::UNSIGNED_BYTE, gotValues); WebCore::Color got { WebCore::SRGBA { gotValues[0], gotValues[1], gotValues[2], gotValues[3] } }; if (got != expected) return ::testing::AssertionFailure() << "Failed to verify draw to context. Got: " << got << ", expected: " << expected << "."; return ::testing::AssertionSuccess(); } #if PLATFORM(MAC) || PLATFORM(MACCATALYST) static RetainPtr>> allDevices() { return adoptNS(MTLCopyAllDevices()); } static RetainPtr> lowPowerDevice() { auto devices = allDevices(); for (id device in devices.get()) { if (device.lowPower) return device; } return nullptr; } static RetainPtr> highPerformanceDevice() { auto devices = allDevices(); for (id device in devices.get()) { if (!device.lowPower && !device.removable) return device; } return nullptr; } static bool hasMultipleGPUs() { return highPerformanceDevice() && lowPowerDevice(); } // Sanity check for the MultipleGPUs* tests. TEST_F(GraphicsContextGLCocoaTest, MultipleGPUsHaveDifferentGPUs) { if (!hasMultipleGPUs()) return; auto a = highPerformanceDevice(); auto b = lowPowerDevice(); EXPECT_NE(a.get(), nullptr); EXPECT_NE(b.get(), nullptr); EXPECT_NE(a.get(), b.get()); } // Tests for a bug where high-performance context would use low-power GPU if low-power or default // context was created first. Test is applicable only for Metal, since GPU selection for OpenGL is // very different. TEST_F(GraphicsContextGLCocoaTest, MultipleGPUsDifferentPowerPreferenceMetal) { if (!hasMultipleGPUs()) return; WebCore::GraphicsContextGLAttributes attributes; attributes.useMetal = true; EXPECT_EQ(attributes.powerPreference, WebCore::GraphicsContextGLPowerPreference::Default); auto defaultContext = TestedGraphicsContextGLCocoa::create(WebCore::GraphicsContextGLAttributes { attributes }); ASSERT_NE(defaultContext, nullptr); attributes.powerPreference = WebCore::GraphicsContextGLPowerPreference::LowPower; auto lowPowerContext = TestedGraphicsContextGLCocoa::create(WebCore::GraphicsContextGLAttributes { attributes }); ASSERT_NE(lowPowerContext, nullptr); attributes.powerPreference = WebCore::GraphicsContextGLPowerPreference::HighPerformance; auto highPerformanceContext = TestedGraphicsContextGLCocoa::create(WebCore::GraphicsContextGLAttributes { attributes }); ASSERT_NE(highPerformanceContext, nullptr); EXPECT_NE(lowPowerContext->getString(WebCore::GraphicsContextGL::RENDERER), highPerformanceContext->getString(WebCore::GraphicsContextGL::RENDERER)); EXPECT_EQ(defaultContext->getString(WebCore::GraphicsContextGL::RENDERER), lowPowerContext->getString(WebCore::GraphicsContextGL::RENDERER)); } // Tests that requesting context with windowGPUID from low power device results // to same thing as requesting default low power context. // Tests that windowGPUID from low power device still respects high performance request. TEST_F(GraphicsContextGLCocoaTest, MultipleGPUsExplicitLowPowerDeviceMetal) { if (!hasMultipleGPUs()) return; WebCore::GraphicsContextGLAttributes attributes1; attributes1.useMetal = true; attributes1.powerPreference = WebCore::GraphicsContextGLPowerPreference::LowPower; auto lowPowerContext = TestedGraphicsContextGLCocoa::create(WebCore::GraphicsContextGLAttributes { attributes1 }); ASSERT_NE(lowPowerContext, nullptr); WebCore::GraphicsContextGLAttributes attributes2; attributes2.useMetal = true; attributes2.windowGPUID = [lowPowerDevice() registryID]; auto explicitDeviceContext = TestedGraphicsContextGLCocoa::create(WebCore::GraphicsContextGLAttributes { attributes2 }); ASSERT_NE(explicitDeviceContext.get(), nullptr); // Context with windowGPUID from low power device results to same thing as requesting default low power context. EXPECT_EQ(lowPowerContext->getString(WebCore::GraphicsContextGL::RENDERER), explicitDeviceContext->getString(WebCore::GraphicsContextGL::RENDERER)); // High performance request on a low power explicit device as windowGPUID respects the high performance request. attributes2.powerPreference = WebCore::GraphicsContextGLPowerPreference::HighPerformance; auto highPerformanceExplicitDeviceContext = TestedGraphicsContextGLCocoa::create(WebCore::GraphicsContextGLAttributes { attributes2 }); ASSERT_NE(highPerformanceExplicitDeviceContext.get(), nullptr); EXPECT_NE(highPerformanceExplicitDeviceContext->getString(WebCore::GraphicsContextGL::RENDERER), explicitDeviceContext->getString(WebCore::GraphicsContextGL::RENDERER)); } // Tests that requesting context with windowGPUID from high performance device results to same thing // as requesting default high performance context. // Tests that windowGPUID from high performance device still uses that device even when low-power context is requested. // It is likely that context on a specific window + gpu is the lowest power if the window is on that gpu. TEST_F(GraphicsContextGLCocoaTest, MultipleGPUsExplicitHighPerformanceDeviceMetal) { if (!hasMultipleGPUs()) return; WebCore::GraphicsContextGLAttributes attributes1; attributes1.useMetal = true; attributes1.powerPreference = WebCore::GraphicsContextGLPowerPreference::HighPerformance; auto highPerformanceContext = TestedGraphicsContextGLCocoa::create(WebCore::GraphicsContextGLAttributes { attributes1 }); ASSERT_NE(highPerformanceContext, nullptr); WebCore::GraphicsContextGLAttributes attributes2; attributes2.useMetal = true; attributes2.windowGPUID = [highPerformanceDevice() registryID]; auto explicitDeviceContext = TestedGraphicsContextGLCocoa::create(WebCore::GraphicsContextGLAttributes { attributes2 }); ASSERT_NE(explicitDeviceContext.get(), nullptr); // Context with windowGPUID from high performance device results to same thing as requesting default high performance context. EXPECT_EQ(highPerformanceContext->getString(WebCore::GraphicsContextGL::RENDERER), explicitDeviceContext->getString(WebCore::GraphicsContextGL::RENDERER)); // Low power request on a high performance explicit device as windowGPUID ignores the low power request. attributes2.powerPreference = WebCore::GraphicsContextGLPowerPreference::LowPower; auto lowPowerExplicitDeviceContext = TestedGraphicsContextGLCocoa::create(WebCore::GraphicsContextGLAttributes { attributes2 }); ASSERT_NE(lowPowerExplicitDeviceContext.get(), nullptr); EXPECT_EQ(lowPowerExplicitDeviceContext->getString(WebCore::GraphicsContextGL::RENDERER), explicitDeviceContext->getString(WebCore::GraphicsContextGL::RENDERER)); } // Tests that requesting GraphicsContextGL instances with different devices results in different underlying // EGLDisplays. TEST_F(GraphicsContextGLCocoaTest, MultipleGPUsDifferentGPUIDsMetal) { if (!hasMultipleGPUs()) return; Vector> contexts; auto devices = allDevices(); for (id device in devices.get()) { WebCore::GraphicsContextGLAttributes attributes; attributes.useMetal = true; attributes.windowGPUID = [device registryID]; auto context = TestedGraphicsContextGLCocoa::create(WebCore::GraphicsContextGLAttributes { attributes }); EXPECT_NE(context.get(), nullptr); if (!context) continue; contexts.append(context.releaseNonNull()); } EXPECT_GT(contexts.size(), 1u); // The requested EGLDisplays must differ if the devices differ. for (auto itA = contexts.begin(); itA != contexts.end(); ++itA) { for (auto itB = itA + 1; itB != contexts.end(); ++itB) EXPECT_NE((*itA)->platformDisplay(), (*itB)->platformDisplay()); } } #endif TEST_F(GraphicsContextGLCocoaTest, DisplayBuffersAreRecycled) { auto context = createDefaultTestContext({ 20, 20 }); ASSERT_NE(context, nullptr); RetainPtr expectedDisplayBuffers[expectedDisplayBufferPoolSize]; for (int i = 0; i < 50; ++i) { EXPECT_TRUE(changeContextContents(*context, i)); context->prepareForDisplay(); auto* surface = context->displayBuffer(); ASSERT_NE(surface, nullptr); int slot = i % expectedDisplayBufferPoolSize; if (!expectedDisplayBuffers[slot]) expectedDisplayBuffers[slot] = surface->surface(); EXPECT_EQ(expectedDisplayBuffers[slot].get(), surface->surface()) << "for i:" << i << " slot: " << slot; } for (int i = 0; i < expectedDisplayBufferPoolSize - 1; ++i) { for (int j = i + 1; j < expectedDisplayBufferPoolSize; ++j) EXPECT_NE(expectedDisplayBuffers[i].get(), expectedDisplayBuffers[j].get()) << "for i: " << i << " j:" << j; } } // Test that drawing buffers are not recycled if `GraphicsContextGLOpenGL::markDisplayBufferInUse()` // is called. TEST_F(GraphicsContextGLCocoaTest, DisplayBuffersAreNotRecycledWhenMarkedInUse) { auto context = createDefaultTestContext({ 20, 20 }); ASSERT_NE(context, nullptr); HashSet> seenSurfaceRefs; for (int i = 0; i < 50; ++i) { EXPECT_TRUE(changeContextContents(*context, i)); context->prepareForDisplay(); WebCore::IOSurface* surface = context->displayBuffer(); ASSERT_NE(surface, nullptr); IOSurfaceRef surfaceRef = surface->surface(); EXPECT_NE(surfaceRef, nullptr); EXPECT_FALSE(seenSurfaceRefs.contains(surfaceRef)); seenSurfaceRefs.add(surfaceRef); context->markDisplayBufferInUse(); } ASSERT_EQ(seenSurfaceRefs.size(), 50u); } // Test that drawing buffers are not recycled if the use count of the underlying IOSurface // changes. Use count is modified for example by CoreAnimation when the IOSurface is attached // to the contents. TEST_F(GraphicsContextGLCocoaTest, DisplayBuffersAreNotRecycledWhedInUse) { auto context = createDefaultTestContext({ 20, 20 }); ASSERT_NE(context, nullptr); HashSet> seenSurfaceRefs; for (int i = 0; i < 50; ++i) { EXPECT_TRUE(changeContextContents(*context, i)); context->prepareForDisplay(); WebCore::IOSurface* surface = context->displayBuffer(); ASSERT_NE(surface, nullptr); IOSurfaceRef surfaceRef = surface->surface(); EXPECT_NE(surfaceRef, nullptr); EXPECT_FALSE(seenSurfaceRefs.contains(surfaceRef)); seenSurfaceRefs.add(surfaceRef); IOSurfaceIncrementUseCount(surfaceRef); } ASSERT_EQ(seenSurfaceRefs.size(), 50u); } // Test that drawing to GraphicsContextGL and marking the display buffer in use does not leak big // amounts of memory for each displayed buffer. TEST_F(GraphicsContextGLCocoaTest, UnrecycledDisplayBuffersNoLeaks) { // The test detects the leak by observing memory footprint. However, some of the freed IOSurface // memory (130mb) stays resident, presumably by intention of IOKit. The test would originally leak // 2.7gb so the intended bug would be detected with 150mb error range. size_t footprintError = 150 * 1024 * 1024; size_t footprintChange = 0; auto context = createDefaultTestContext({ 2048, 2048 }); ASSERT_NE(context, nullptr); WTF::releaseFastMallocFreeMemory(); auto lastFootprint = memoryFootprint(); for (int i = 0; i < 50; ++i) { EXPECT_TRUE(changeContextContents(*context, i)); context->prepareForDisplay(); EXPECT_NE(context->displayBuffer(), nullptr); context->markDisplayBufferInUse(); } EXPECT_TRUE(memoryFootprintChangedBy(lastFootprint, footprintChange, footprintError)); } } #endif