diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index b1735e606a812..05c6078103ac5 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -611,6 +611,8 @@ FILE: ../../../flutter/shell/common/rasterizer.cc FILE: ../../../flutter/shell/common/rasterizer.h FILE: ../../../flutter/shell/common/run_configuration.cc FILE: ../../../flutter/shell/common/run_configuration.h +FILE: ../../../flutter/shell/common/serialization_callbacks.cc +FILE: ../../../flutter/shell/common/serialization_callbacks.h FILE: ../../../flutter/shell/common/shell.cc FILE: ../../../flutter/shell/common/shell.h FILE: ../../../flutter/shell/common/shell_benchmarks.cc @@ -629,6 +631,7 @@ FILE: ../../../flutter/shell/common/shell_test_platform_view_vulkan.h FILE: ../../../flutter/shell/common/shell_unittests.cc FILE: ../../../flutter/shell/common/skia_event_tracer_impl.cc FILE: ../../../flutter/shell/common/skia_event_tracer_impl.h +FILE: ../../../flutter/shell/common/skp_shader_warmup_unittests.cc FILE: ../../../flutter/shell/common/switches.cc FILE: ../../../flutter/shell/common/switches.h FILE: ../../../flutter/shell/common/thread_host.cc diff --git a/shell/common/BUILD.gn b/shell/common/BUILD.gn index e4d1c956658b9..9e5fa56148f07 100644 --- a/shell/common/BUILD.gn +++ b/shell/common/BUILD.gn @@ -82,6 +82,8 @@ source_set_maybe_fuchsia_legacy("common") { "rasterizer.h", "run_configuration.cc", "run_configuration.h", + "serialization_callbacks.cc", + "serialization_callbacks.h", "shell.cc", "shell.h", "shell_io_manager.cc", @@ -264,6 +266,7 @@ if (enable_unittests) { "persistent_cache_unittests.cc", "pipeline_unittests.cc", "shell_unittests.cc", + "skp_shader_warmup_unittests.cc", ] deps = [ diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc index 9735854f41842..85e1ef1fbae3f 100644 --- a/shell/common/rasterizer.cc +++ b/shell/common/rasterizer.cc @@ -9,6 +9,7 @@ #include "flutter/fml/time/time_delta.h" #include "flutter/fml/time/time_point.h" #include "flutter/shell/common/persistent_cache.h" +#include "flutter/shell/common/serialization_callbacks.h" #include "third_party/skia/include/core/SkEncodedImageFormat.h" #include "third_party/skia/include/core/SkImageEncoder.h" #include "third_party/skia/include/core/SkPictureRecorder.h" @@ -447,10 +448,6 @@ RasterStatus Rasterizer::DrawToSurface(flutter::LayerTree& layer_tree) { return RasterStatus::kFailed; } -static sk_sp SerializeTypeface(SkTypeface* typeface, void* ctx) { - return typeface->serialize(SkTypeface::SerializeBehavior::kDoIncludeData); -} - static sk_sp ScreenshotLayerTreeAsPicture( flutter::LayerTree* tree, flutter::CompositorContext& compositor_context) { @@ -462,16 +459,30 @@ static sk_sp ScreenshotLayerTreeAsPicture( SkMatrix root_surface_transformation; root_surface_transformation.reset(); +#if defined(LEGACY_FUCHSIA_EMBEDDER) + // TODO(arbreng: fxb/55805) Our ScopedFrame implementation doesnt do the + // right thing here so initialize the base class directly. This wont be + // needed after we move to using the embedder API on Fuchsia. + auto frame = std::make_unique( + compositor_context, nullptr, recorder.getRecordingCanvas(), nullptr, + root_surface_transformation, false, true, nullptr); +#else // TODO(amirh): figure out how to take a screenshot with embedded UIView. // https://github.com/flutter/flutter/issues/23435 auto frame = compositor_context.AcquireFrame( nullptr, recorder.getRecordingCanvas(), nullptr, root_surface_transformation, false, true, nullptr); +#endif // defined(LEGACY_FUCHSIA_EMBEDDER) frame->Raster(*tree, true); +#if defined(OS_FUCHSIA) SkSerialProcs procs = {0}; - procs.fTypefaceProc = SerializeTypeface; + procs.fImageProc = SerializeImageWithoutData; +#else + SkSerialProcs procs = {0}; + procs.fTypefaceProc = SerializeTypefaceWithData; +#endif return recorder.finishRecordingAsPicture()->serialize(&procs); } diff --git a/shell/common/serialization_callbacks.cc b/shell/common/serialization_callbacks.cc new file mode 100644 index 0000000000000..b482f5fab9f42 --- /dev/null +++ b/shell/common/serialization_callbacks.cc @@ -0,0 +1,74 @@ +// 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/fml/logging.h" +#include "include/core/SkImage.h" +#include "include/core/SkPicture.h" +#include "include/core/SkStream.h" +#include "include/core/SkTypeface.h" + +namespace flutter { + +sk_sp SerializeTypefaceWithoutData(SkTypeface* typeface, void* ctx) { + return typeface->serialize(SkTypeface::SerializeBehavior::kDoIncludeData); +} + +sk_sp SerializeTypefaceWithData(SkTypeface* typeface, void* ctx) { + return typeface->serialize(SkTypeface::SerializeBehavior::kDontIncludeData); +} + +struct ImageMetaData { + int32_t width; + int32_t height; + uint32_t color_type; + uint32_t alpha_type; + bool has_color_space; +} __attribute__((packed)); + +sk_sp SerializeImageWithoutData(SkImage* image, void* ctx) { + auto info = image->imageInfo(); + SkDynamicMemoryWStream stream; + + ImageMetaData metadata = {info.width(), info.height(), + static_cast(info.colorType()), + static_cast(info.alphaType()), + static_cast(info.colorSpace())}; + stream.write(&metadata, sizeof(ImageMetaData)); + + if (info.colorSpace()) { + auto color_space_data = info.colorSpace()->serialize(); + FML_CHECK(color_space_data); + SkMemoryStream color_space_stream(color_space_data); + stream.writeStream(&color_space_stream, color_space_data->size()); + } + + return stream.detachAsData(); +}; + +sk_sp DeserializeImageWithoutData(const void* data, + size_t length, + void* ctx) { + FML_CHECK(length >= sizeof(ImageMetaData)); + auto metadata = static_cast(data); + sk_sp color_space = nullptr; + if (metadata->has_color_space) { + color_space = SkColorSpace::Deserialize( + static_cast(data) + sizeof(ImageMetaData), + length - sizeof(ImageMetaData)); + } + + auto image_size = SkISize::Make(metadata->width, metadata->height); + auto info = SkImageInfo::Make( + image_size, static_cast(metadata->color_type), + static_cast(metadata->alpha_type), color_space); + sk_sp image_data = + SkData::MakeUninitialized(image_size.width() * image_size.height() * 4); + memset(image_data->writable_data(), 0x0f, image_data->size()); + sk_sp image = + SkImage::MakeRasterData(info, image_data, image_size.width() * 4); + + return image; +}; + +} // namespace flutter diff --git a/shell/common/serialization_callbacks.h b/shell/common/serialization_callbacks.h new file mode 100644 index 0000000000000..6e339fd3c48c2 --- /dev/null +++ b/shell/common/serialization_callbacks.h @@ -0,0 +1,27 @@ +// 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_SHELL_COMMON_SERIALIZATION_CALLBACKS_H_ +#define FLUTTER_SHELL_COMMON_SERIALIZATION_CALLBACKS_H_ + +#include "flutter/fml/logging.h" +#include "include/core/SkImage.h" +#include "include/core/SkPicture.h" +#include "include/core/SkStream.h" +#include "include/core/SkTypeface.h" + +namespace flutter { + +sk_sp SerializeTypefaceWithoutData(SkTypeface* typeface, void* ctx); +sk_sp SerializeTypefaceWithData(SkTypeface* typeface, void* ctx); + +// Serializes only the metadata of the image and not the underlying pixel data. +sk_sp SerializeImageWithoutData(SkImage* image, void* ctx); +sk_sp DeserializeImageWithoutData(const void* data, + size_t length, + void* ctx); + +} // namespace flutter + +#endif // FLUTTER_SHELL_COMMON_SERIALIZATION_CALLBACKS_H_ diff --git a/shell/common/skp_shader_warmup_unittests.cc b/shell/common/skp_shader_warmup_unittests.cc new file mode 100644 index 0000000000000..689f2d9322fcc --- /dev/null +++ b/shell/common/skp_shader_warmup_unittests.cc @@ -0,0 +1,248 @@ +// 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 + +#include "flutter/assets/directory_asset_bundle.h" +#include "flutter/flow/layers/container_layer.h" +#include "flutter/flow/layers/layer.h" +#include "flutter/flow/layers/physical_shape_layer.h" +#include "flutter/flow/layers/picture_layer.h" +#include "flutter/fml/command_line.h" +#include "flutter/fml/file.h" +#include "flutter/fml/log_settings.h" +#include "flutter/fml/unique_fd.h" +#include "flutter/shell/common/persistent_cache.h" +#include "flutter/shell/common/serialization_callbacks.h" +#include "flutter/shell/common/shell_test.h" +#include "flutter/shell/common/switches.h" +#include "flutter/shell/version/version.h" +#include "flutter/testing/testing.h" +#include "include/core/SkPicture.h" +#include "include/core/SkPictureRecorder.h" +#include "include/core/SkSerialProcs.h" + +namespace flutter { +namespace testing { + +#if defined(OS_FUCHSIA) + +static void WaitForIO(Shell* shell) { + std::promise io_task_finished; + shell->GetTaskRunners().GetIOTaskRunner()->PostTask( + [&io_task_finished]() { io_task_finished.set_value(true); }); + io_task_finished.get_future().wait(); +} + +class SkpWarmupTest : public ShellTest { + public: + SkpWarmupTest() {} + + void TestWarmup(const SkISize& draw_size, const LayerTreeBuilder& builder) { + // Create a temp dir to store the persistent cache + fml::ScopedTemporaryDirectory dir; + PersistentCache::SetCacheDirectoryPath(dir.path()); + PersistentCache::ResetCacheForProcess(); + + auto settings = CreateSettingsForFixture(); + settings.cache_sksl = true; + settings.dump_skp_on_shader_compilation = true; + + fml::AutoResetWaitableEvent firstFrameLatch; + settings.frame_rasterized_callback = + [&firstFrameLatch](const FrameTiming& t) { firstFrameLatch.Signal(); }; + + auto config = RunConfiguration::InferFromSettings(settings); + config.SetEntrypoint("emptyMain"); + std::unique_ptr shell = CreateShell(settings); + PlatformViewNotifyCreated(shell.get()); + RunEngine(shell.get(), std::move(config)); + + // Initially, we should have no SkSL cache + auto cache = PersistentCache::GetCacheForProcess()->LoadSkSLs(); + ASSERT_EQ(cache.size(), 0u); + + PumpOneFrame(shell.get(), draw_size.width(), draw_size.height(), builder); + firstFrameLatch.Wait(); + WaitForIO(shell.get()); + + // Count the number of shaders this builder generated. We use this as a + // proxy for whether new shaders were generated, since skia will dump an skp + // any time a new shader is compiled. + int skp_count = 0; + fml::FileVisitor skp_count_visitor = [&skp_count]( + const fml::UniqueFD& directory, + const std::string& filename) { + if (filename.size() >= 4 && + filename.substr(filename.size() - 4, 4) == ".skp") { + skp_count += 1; + } + return true; + }; + fml::VisitFilesRecursively(dir.fd(), skp_count_visitor); + int first_skp_count = skp_count; + skp_count = 0; + ASSERT_GT(first_skp_count, 0); + + // Deserialize all skps into memory + std::vector> pictures; + fml::FileVisitor skp_deserialize_visitor = + [&pictures](const fml::UniqueFD& directory, + const std::string& filename) { + if (filename.size() >= 4 && + filename.substr(filename.size() - 4, 4) == ".skp") { + auto fd = fml::OpenFileReadOnly(directory, filename.c_str()); + if (fd.get() < 0) { + FML_LOG(ERROR) << "Failed to open " << filename; + return true; + } + // Deserialize + sk_sp data = SkData::MakeFromFD(fd.get()); + std::unique_ptr stream = SkMemoryStream::Make(data); + + SkDeserialProcs procs = {0}; + procs.fImageProc = DeserializeImageWithoutData; + sk_sp picture = + SkPicture::MakeFromStream(stream.get(), &procs); + pictures.push_back(std::move(picture)); + fd.reset(); + } + return true; + }; + fml::VisitFilesRecursively(dir.fd(), skp_deserialize_visitor); + ASSERT_GT(pictures.size(), 0ul); + + // Reinitialize shell with clean cache and verify that drawing again dumps + // the same number of shaders + fml::RemoveFilesInDirectory(dir.fd()); + PersistentCache::ResetCacheForProcess(); + DestroyShell(std::move(shell)); + auto config2 = RunConfiguration::InferFromSettings(settings); + config2.SetEntrypoint("emptyMain"); + shell = CreateShell(settings); + PlatformViewNotifyCreated(shell.get()); + RunEngine(shell.get(), std::move(config2)); + firstFrameLatch.Reset(); + PumpOneFrame(shell.get(), draw_size.width(), draw_size.height(), builder); + firstFrameLatch.Wait(); + WaitForIO(shell.get()); + + // Verify same number of shaders dumped + fml::VisitFilesRecursively(dir.fd(), skp_count_visitor); + int second_skp_count = skp_count; + skp_count = 0; + ASSERT_EQ(second_skp_count, first_skp_count); + + // Reinitialize shell and draw deserialized skps to warm up shaders + fml::RemoveFilesInDirectory(dir.fd()); + PersistentCache::ResetCacheForProcess(); + DestroyShell(std::move(shell)); + auto config3 = RunConfiguration::InferFromSettings(settings); + config3.SetEntrypoint("emptyMain"); + shell = CreateShell(settings); + PlatformViewNotifyCreated(shell.get()); + RunEngine(shell.get(), std::move(config3)); + firstFrameLatch.Reset(); + + for (auto& picture : pictures) { + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + LayerTreeBuilder picture_builder = + [picture, queue](std::shared_ptr root) { + auto picture_layer = std::make_shared( + SkPoint::Make(0, 0), SkiaGPUObject(picture, queue), + /* is_complex */ false, + /* will_change */ false); + root->Add(picture_layer); + }; + PumpOneFrame(shell.get(), picture->cullRect().width(), + picture->cullRect().height(), picture_builder); + } + firstFrameLatch.Wait(); + WaitForIO(shell.get()); + + // Verify same number of shaders dumped + fml::VisitFilesRecursively(dir.fd(), skp_count_visitor); + int third_skp_count = skp_count; + skp_count = 0; + ASSERT_EQ(third_skp_count, first_skp_count); + + // Remove files generated + fml::RemoveFilesInDirectory(dir.fd()); + + // Draw orignal material again + firstFrameLatch.Reset(); + PumpOneFrame(shell.get(), draw_size.width(), draw_size.height(), builder); + + firstFrameLatch.Wait(); + WaitForIO(shell.get()); + + // Verify no new shaders dumped + fml::VisitFilesRecursively(dir.fd(), skp_count_visitor); + int fourth_skp_count = skp_count; + skp_count = 0; + ASSERT_EQ(fourth_skp_count, 0); + + // Clean Up + fml::RemoveFilesInDirectory(dir.fd()); + } +}; + +TEST_F(SkpWarmupTest, Basic) { + SkISize draw_size = SkISize::Make(100, 100); + // Draw something to trigger shader compilations. + LayerTreeBuilder builder = + [&draw_size](std::shared_ptr root) { + SkPath path; + path.addCircle(draw_size.width() / 2, draw_size.height() / 2, 20); + auto physical_shape_layer = std::make_shared( + SK_ColorRED, SK_ColorBLUE, 1.0f, path, Clip::antiAlias); + root->Add(physical_shape_layer); + }; + TestWarmup(draw_size, builder); +} + +TEST_F(SkpWarmupTest, Image) { + SkISize draw_size = SkISize::Make(100, 100); + // We reuse this builder to draw the same content sever times in this test + LayerTreeBuilder builder = [&draw_size, + this](std::shared_ptr root) { + SkPictureRecorder recorder; + auto canvas = + recorder.beginRecording(draw_size.width(), draw_size.height()); + + // include an image so we can test that the warmup works even with image + // data excluded from the skp + auto image_size = + SkISize::Make(draw_size.width() / 2, draw_size.height() / 2); + auto color_space = SkColorSpace::MakeSRGB(); + auto info = + SkImageInfo::Make(image_size, SkColorType::kRGBA_8888_SkColorType, + SkAlphaType::kPremul_SkAlphaType, color_space); + sk_sp image_data = + SkData::MakeUninitialized(image_size.width() * image_size.height() * 4); + memset(image_data->writable_data(), 0x0f, image_data->size()); + sk_sp image = + SkImage::MakeRasterData(info, image_data, image_size.width() * 4); + + canvas->drawImage(image, image_size.width(), image_size.height()); + + auto picture = recorder.finishRecordingAsPicture(); + + fml::RefPtr queue = fml::MakeRefCounted( + this->GetCurrentTaskRunner(), fml::TimeDelta::FromSeconds(0)); + auto picture_layer = std::make_shared( + SkPoint::Make(0, 0), SkiaGPUObject(picture, queue), + /* is_complex */ false, + /* will_change */ false); + root->Add(picture_layer); + }; + + TestWarmup(draw_size, builder); +} + +#endif + +} // namespace testing +} // namespace flutter