Skip to content

Commit

Permalink
Setup a Metal test surface and add a new unit-test target that tests …
Browse files Browse the repository at this point in the history
…the testing utilities.

`//flutter/testing` now contains a lot of utilities used by other test targets.
This includes stuff like working with render targets that use either OpenGL or
Metal, fixtures for interacting with the Dart VM, test assertion predicates,
etc.. However, these utilities themselves are not tested as part of a standalone
test suite. Instead, only the test targets that include it exercise these
utilities. Since these are no longer trivial, a new test target has been added
that tests the testing utilities directly.
  • Loading branch information
chinmaygarde committed Nov 23, 2019
1 parent 591144d commit ed30d77
Show file tree
Hide file tree
Showing 9 changed files with 343 additions and 4 deletions.
1 change: 1 addition & 0 deletions BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ group("flutter") {
"$flutter_root/shell/platform/common/cpp/client_wrapper:client_wrapper_unittests",
"$flutter_root/shell/platform/embedder:embedder_unittests",
"$flutter_root/shell/platform/glfw/client_wrapper:client_wrapper_glfw_unittests",
"$flutter_root/testing:testing_unittests",
"$flutter_root/third_party/txt:txt_unittests",
]

Expand Down
54 changes: 54 additions & 0 deletions testing/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import("//flutter/shell/config.gni")
import("//flutter/testing/testing.gni")

source_set("testing_lib") {
testonly = true

Expand Down Expand Up @@ -77,4 +80,55 @@ if (current_toolchain == host_toolchain) {
"//third_party/swiftshader_flutter:swiftshader",
]
}

# All targets on all platforms should be able to the Metal utilities. On
# platforms where Metal in not available, the tests must be skipped or
# implemented to use another available client rendering API. This is usually
# either OpenGL which is portably implemented via SwiftShader or the software
# backend. This way, all tests compile on all platforms but the Metal backend
# exercised on platforms where Metal itself is available.
source_set("metal") {
testonly = true

sources = [
"$flutter_root/testing/test_metal_surface.cc",
"$flutter_root/testing/test_metal_surface.h",
]

defines = []

if (shell_enable_metal) {
sources += [ "$flutter_root/testing/test_metal_surface_impl.mm" ]
defines += [ "TESTING_ENABLE_METAL" ]
}

deps = [
":skia",
"$flutter_root/fml",
]
}
}

test_fixtures("testing_fixtures") {
fixtures = []
}

# The //flutter/testing library provides utility methods to other test targets.
# This test target tests the testing utilities.
executable("testing_unittests") {
testonly = true

sources = [
"$flutter_root/testing/test_metal_surface_unittests.cc",
]

deps = [
":dart",
":metal",
":opengl",
":skia",
":testing",
":testing_fixtures",
":testing_lib",
]
}
2 changes: 2 additions & 0 deletions testing/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ def RunCCTests(build_dir, filter):

RunEngineExecutable(build_dir, 'ui_unittests', filter, shuffle_flags)

RunEngineExecutable(build_dir, 'testing_unittests', filter, shuffle_flags)

# These unit-tests are Objective-C and can only run on Darwin.
if IsMac():
RunEngineExecutable(build_dir, 'flutter_channels_unittests', filter, shuffle_flags)
Expand Down
46 changes: 46 additions & 0 deletions testing/test_metal_surface.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "flutter/testing/test_metal_surface.h"

#if TESTING_ENABLE_METAL
#include "flutter/testing/test_metal_surface_impl.h"
#endif // TESTING_ENABLE_METAL

namespace flutter {

bool TestMetalSurface::PlatformSupportsMetal() {
#if TESTING_ENABLE_METAL
return true;
#else // TESTING_ENABLE_METAL
return false;
#endif // TESTING_ENABLE_METAL
}

std::unique_ptr<TestMetalSurface> TestMetalSurface::Create(
SkISize surface_size) {
#if TESTING_ENABLE_METAL
return std::make_unique<TestMetalSurfaceImpl>(surface_size);
#else // TESTING_ENABLE_METAL
return nullptr;
#endif // TESTING_ENABLE_METAL
}

TestMetalSurface::TestMetalSurface() = default;

TestMetalSurface::~TestMetalSurface() = default;

bool TestMetalSurface::IsValid() const {
return impl_ ? impl_->IsValid() : false;
}

sk_sp<GrContext> TestMetalSurface::GetGrContext() const {
return impl_ ? impl_->GetGrContext() : nullptr;
}

sk_sp<SkSurface> TestMetalSurface::GetSurface() const {
return impl_ ? impl_->GetSurface() : nullptr;
}

} // namespace flutter
47 changes: 47 additions & 0 deletions testing/test_metal_surface.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef FLUTTER_TESTING_TEST_METAL_SURFACE_H_
#define FLUTTER_TESTING_TEST_METAL_SURFACE_H_

#include "flutter/fml/macros.h"
#include "third_party/skia/include/core/SkSize.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "third_party/skia/include/gpu/GrContext.h"

namespace flutter {

//------------------------------------------------------------------------------
/// @brief Creates a MTLTexture backed SkSurface and context that can be
/// used to render to in unit-tests.
///
class TestMetalSurface {
public:
static bool PlatformSupportsMetal();

static std::unique_ptr<TestMetalSurface> Create(
SkISize surface_size = SkISize::MakeEmpty());

virtual ~TestMetalSurface();

virtual bool IsValid() const;

virtual sk_sp<GrContext> GetGrContext() const;

virtual sk_sp<SkSurface> GetSurface() const;

protected:
TestMetalSurface();

private:
std::unique_ptr<TestMetalSurface> impl_;

TestMetalSurface(std::unique_ptr<TestMetalSurface> impl);

FML_DISALLOW_COPY_AND_ASSIGN(TestMetalSurface);
};

} // namespace flutter

#endif // FLUTTER_TESTING_TEST_METAL_SURFACE_H_
39 changes: 39 additions & 0 deletions testing/test_metal_surface_impl.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef FLUTTER_TESTING_TEST_METAL_SURFACE_IMPL_H_
#define FLUTTER_TESTING_TEST_METAL_SURFACE_IMPL_H_

#include "flutter/fml/macros.h"
#include "flutter/testing/test_metal_surface.h"

namespace flutter {

class TestMetalSurfaceImpl : public TestMetalSurface {
public:
TestMetalSurfaceImpl(SkISize surface_size);

// |TestMetalSurface|
~TestMetalSurfaceImpl() override;

private:
bool is_valid_ = false;
sk_sp<GrContext> context_;
sk_sp<SkSurface> surface_;

// |TestMetalSurface|
bool IsValid() const override;

// |TestMetalSurface|
sk_sp<GrContext> GetGrContext() const override;

// |TestMetalSurface|
sk_sp<SkSurface> GetSurface() const override;

FML_DISALLOW_COPY_AND_ASSIGN(TestMetalSurfaceImpl);
};

} // namespace flutter

#endif // FLUTTER_TESTING_TEST_METAL_SURFACE_IMPL_H_
119 changes: 119 additions & 0 deletions testing/test_metal_surface_impl.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "flutter/testing/test_metal_surface_impl.h"

#include <Metal/Metal.h>

#include "flutter/fml/logging.h"
#include "flutter/fml/platform/darwin/scoped_nsobject.h"
#include "third_party/skia/include/core/SkSurface.h"

namespace flutter {

TestMetalSurfaceImpl::TestMetalSurfaceImpl(SkISize surface_size) {
if (surface_size.isEmpty()) {
FML_LOG(ERROR) << "Size of test Metal surface was empty.";
return;
}

auto device = fml::scoped_nsobject{[MTLCreateSystemDefaultDevice() retain]};
if (!device) {
FML_LOG(ERROR) << "Could not acquire Metal device.";
return;
}

auto command_queue = fml::scoped_nsobject{[device.get() newCommandQueue]};
if (!command_queue) {
FML_LOG(ERROR) << "Could not create the default command queue.";
return;
}

auto texture_descriptor = fml::scoped_nsobject{[[MTLTextureDescriptor
texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
width:surface_size.width()
height:surface_size.height()
mipmapped:NO] retain]};

// The most pessimistic option and disables all optimizations but allows tests
// the most flexible access to the surface. They may read and wrote to the
// surface from shaders or use as a pixel view.
texture_descriptor.get().usage = MTLTextureUsageUnknown;

if (!texture_descriptor) {
FML_LOG(ERROR) << "Invalid texture descriptor.";
return;
}

auto texture = fml::scoped_nsobject{
[device.get() newTextureWithDescriptor:texture_descriptor.get()]};

if (!texture) {
FML_LOG(ERROR) << "Could not create texture from texture descriptor.";
return;
}

auto skia_context = GrContext::MakeMetal(device.get(), command_queue.get());

if (skia_context) {
// Skia wants ownership of the device and queue. If a context was created,
// we now no longer own the argument. Release the arguments only on
// successful creation of the context.
FML_ALLOW_UNUSED_LOCAL(device.release());
FML_ALLOW_UNUSED_LOCAL(command_queue.release());
} else {
FML_LOG(ERROR) << "Could not create the GrContext from the Metal Device "
"and command queue.";
return;
}

GrMtlTextureInfo skia_texture_info;
skia_texture_info.fTexture = sk_cf_obj<const void*>{[texture.get() retain]};

auto backend_render_target = GrBackendRenderTarget{
surface_size.width(), // width
surface_size.height(), // height
1, // sample count
skia_texture_info // texture info
};

auto surface = SkSurface::MakeFromBackendRenderTarget(
skia_context.get(), // context
backend_render_target, // backend render target
kTopLeft_GrSurfaceOrigin, // surface origin
kBGRA_8888_SkColorType, // color type
nullptr, // color space
nullptr, // surface properties
nullptr, // release proc (texture is already ref counted in sk_cf_obj)
nullptr // release context
);

if (!surface) {
FML_LOG(ERROR) << "Could not create Skia surface from a Metal texture.";
return;
}

surface_ = std::move(surface);
context_ = std::move(skia_context);

is_valid_ = true;
}

// |TestMetalSurface|
TestMetalSurfaceImpl::~TestMetalSurfaceImpl() = default;

// |TestMetalSurface|
bool TestMetalSurfaceImpl::IsValid() const {
return is_valid_;
}
// |TestMetalSurface|
sk_sp<GrContext> TestMetalSurfaceImpl::GetGrContext() const {
return IsValid() ? context_ : nullptr;
}
// |TestMetalSurface|
sk_sp<SkSurface> TestMetalSurfaceImpl::GetSurface() const {
return IsValid() ? surface_ : nullptr;
}

} // namespace flutter
34 changes: 34 additions & 0 deletions testing/test_metal_surface_unittests.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "flutter/testing/test_metal_surface.h"
#include "flutter/testing/testing.h"

namespace flutter {
namespace testing {

TEST(TestMetalSurface, EmptySurfaceIsInvalid) {
if (!TestMetalSurface::PlatformSupportsMetal()) {
GTEST_SKIP();
}

auto surface = TestMetalSurface::Create();
ASSERT_NE(surface, nullptr);
ASSERT_FALSE(surface->IsValid());
}

TEST(TestMetalSurface, CanCreateValidTestMetalSurface) {
if (!TestMetalSurface::PlatformSupportsMetal()) {
GTEST_SKIP();
}

auto surface = TestMetalSurface::Create(SkISize::Make(100, 100));
ASSERT_NE(surface, nullptr);
ASSERT_TRUE(surface->IsValid());
ASSERT_NE(surface->GetSurface(), nullptr);
ASSERT_NE(surface->GetGrContext(), nullptr);
}

} // namespace testing
} // namespace flutter
5 changes: 1 addition & 4 deletions tools/gn
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def get_out_dir(args):
if args.enable_vulkan:
target_dir.append('vulkan')

if args.enable_metal:
if args.enable_metal and args.target_os == 'ios':
target_dir.append('metal')

return os.path.join(args.out_dir, 'out', '_'.join(target_dir))
Expand Down Expand Up @@ -75,9 +75,6 @@ def to_gn_args(args):
if args.target_os != 'android' and args.enable_vulkan:
raise Exception('--enable-vulkan is only supported on Android')

if args.target_os != 'ios' and args.enable_metal:
raise Exception('--enable-metal is only supported on iOS')

runtime_mode = args.runtime_mode

gn_args = {}
Expand Down

0 comments on commit ed30d77

Please sign in to comment.