blob: 369d7b69dbda5d29b7b9373657f8abb14bd94014 [file] [log] [blame]
/*
* 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 <Metal/Metal.h>
#import <WebCore/Color.h>
#import <WebCore/GraphicsContextGLCocoa.h>
#import <WebCore/ProcessIdentity.h>
#import <optional>
#import <wtf/HashSet.h>
#import <wtf/MemoryFootprint.h>
namespace TestWebKitAPI {
namespace {
class TestedGraphicsContextGLCocoa : public WebCore::GraphicsContextGLCocoa {
public:
static RefPtr<TestedGraphicsContextGLCocoa> create(WebCore::GraphicsContextGLAttributes&& attributes)
{
auto context = adoptRef(*new TestedGraphicsContextGLCocoa(WTFMove(attributes)));
if (!context->initialize())
return nullptr;
return context;
}
RefPtr<WebCore::GraphicsLayerContentsDisplayDelegate> 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<ScopedSetAuxiliaryProcessTypeForTesting> m_scopedProcessType;
};
}
static const int expectedDisplayBufferPoolSize = 3;
static RefPtr<TestedGraphicsContextGLCocoa> 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<WebCore::SRGBA<float>>().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<uint8_t> { 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<NSArray<id<MTLDevice>>> allDevices()
{
return adoptNS(MTLCopyAllDevices());
}
static RetainPtr<id<MTLDevice>> lowPowerDevice()
{
auto devices = allDevices();
for (id<MTLDevice> device in devices.get()) {
if (device.lowPower)
return device;
}
return nullptr;
}
static RetainPtr<id<MTLDevice>> highPerformanceDevice()
{
auto devices = allDevices();
for (id<MTLDevice> 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<Ref<TestedGraphicsContextGLCocoa>> contexts;
auto devices = allDevices();
for (id<MTLDevice> 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<IOSurfaceRef> 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<RetainPtr<IOSurfaceRef>> 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<RetainPtr<IOSurfaceRef>> 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