| // Copyright 2016 The Fuchsia 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 "src/ui/examples/escher/common/demo_harness.h" |
| |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <iostream> |
| #include <set> |
| |
| #include "src/lib/files/directory.h" |
| #include "src/lib/files/file.h" |
| #include "src/lib/fxl/memory/ref_ptr.h" |
| #include "src/ui/examples/escher/common/demo.h" |
| #include "src/ui/lib/escher/escher_process_init.h" |
| #include "src/ui/lib/escher/renderer/frame.h" |
| #include "src/ui/lib/escher/resources/resource_recycler.h" |
| #include "src/ui/lib/escher/util/trace_macros.h" |
| #include "src/ui/lib/escher/vk/gpu_mem.h" |
| #include "src/ui/lib/escher/vk/image.h" |
| #include "src/ui/lib/escher/vk/pipeline_builder.h" |
| #include "src/ui/lib/escher/vk/vulkan_instance.h" |
| |
| #define VK_CHECK_RESULT(XXX) FX_CHECK(XXX.result == vk::Result::eSuccess) |
| |
| static constexpr uint64_t kLogGpuTimestampsEveryNFrames = 200; |
| |
| DemoHarness::DemoHarness(WindowParams window_params) : window_params_(window_params) { |
| // Init() is called by DemoHarness::New(). |
| } |
| |
| DemoHarness::~DemoHarness() { FX_DCHECK(shutdown_complete_); } |
| |
| void DemoHarness::Init(InstanceParams instance_params) { |
| FX_LOGS(INFO) << "Initializing " << window_params_.window_name |
| << (window_params_.use_fullscreen ? " (fullscreen " : " (windowed ") |
| << window_params_.width << "x" << window_params_.height << ")"; |
| InitWindowSystem(); |
| CreateInstance(std::move(instance_params)); |
| vk::SurfaceKHR surface = CreateWindowAndSurface(window_params_); |
| |
| std::set<std::string> device_extension_names; |
| AppendPlatformSpecificDeviceExtensionNames(&device_extension_names); |
| CreateDeviceAndQueue({std::move(device_extension_names), |
| {VK_KHR_MAINTENANCE1_EXTENSION_NAME, VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, |
| VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME}, |
| surface}); |
| |
| escher::GlslangInitializeProcess(); |
| CreateEscher(); |
| |
| CreateSwapchain(); |
| } |
| |
| void DemoHarness::Shutdown() { |
| FX_DCHECK(!shutdown_complete_); |
| shutdown_complete_ = true; |
| |
| escher()->vk_device().waitIdle(); |
| escher()->Cleanup(); |
| |
| DestroySwapchain(); |
| |
| escher::GlslangFinalizeProcess(); |
| DestroyEscher(); |
| |
| DestroyDevice(); |
| DestroyInstance(); |
| ShutdownWindowSystem(); |
| } |
| |
| void DemoHarness::CreateInstance(InstanceParams params) { |
| TRACE_DURATION("gfx", "DemoHarness::CreateInstance"); |
| |
| // Add our own required layers and extensions in addition to those provided |
| // by the caller. Verify that they are all available, and obtain info about |
| // them that is used: |
| // - to create the instance. |
| // - for future reference. |
| AppendPlatformSpecificInstanceExtensionNames(¶ms); |
| |
| // We need this extension for getting debug callbacks. |
| params.extension_names.insert("VK_EXT_debug_report"); |
| |
| instance_ = escher::VulkanInstance::New(std::move(params)); |
| FX_CHECK(instance_); |
| |
| instance_->RegisterDebugReportCallback(RedirectDebugReport, this); |
| } |
| |
| void DemoHarness::CreateDeviceAndQueue(escher::VulkanDeviceQueues::Params params) { |
| TRACE_DURATION("gfx", "DemoHarness::CreateDeviceAndQueue"); |
| device_queues_ = escher::VulkanDeviceQueues::New(instance_, std::move(params)); |
| } |
| |
| void DemoHarness::CreateSwapchain() { |
| TRACE_DURATION("gfx", "DemoHarness::CreateSwapchain"); |
| |
| FX_CHECK(!swapchain_.swapchain); |
| FX_CHECK(swapchain_.images.empty()); |
| |
| vk::SurfaceCapabilitiesKHR surface_caps; |
| { |
| auto result = physical_device().getSurfaceCapabilitiesKHR(surface()); |
| VK_CHECK_RESULT(result); |
| surface_caps = std::move(result.value); |
| } |
| |
| std::vector<vk::PresentModeKHR> present_modes; |
| { |
| auto result = physical_device().getSurfacePresentModesKHR(surface()); |
| VK_CHECK_RESULT(result); |
| present_modes = std::move(result.value); |
| } |
| |
| // TODO: handle undefined width/height. |
| vk::Extent2D swapchain_extent = surface_caps.currentExtent; |
| constexpr uint32_t VK_UNDEFINED_WIDTH_OR_HEIGHT = 0xFFFFFFFF; |
| if (swapchain_extent.width == VK_UNDEFINED_WIDTH_OR_HEIGHT) { |
| swapchain_extent.width = window_params_.width; |
| } |
| if (swapchain_extent.height == VK_UNDEFINED_WIDTH_OR_HEIGHT) { |
| swapchain_extent.height = window_params_.height; |
| } |
| |
| if (swapchain_extent.width != window_params_.width || |
| swapchain_extent.height != window_params_.height) { |
| window_params_.width = swapchain_extent.width; |
| window_params_.height = swapchain_extent.height; |
| } |
| FX_CHECK(swapchain_extent.width == window_params_.width); |
| FX_CHECK(swapchain_extent.height == window_params_.height); |
| |
| // FIFO mode is always available, but we will try to find a more efficient |
| // mode. |
| vk::PresentModeKHR swapchain_present_mode = vk::PresentModeKHR::eFifo; |
| // TODO: Find out why these modes are causing lower performance on Skylake |
| #if 0 |
| for (auto& mode : present_modes) { |
| if (mode == vk::PresentModeKHR::eMailbox) { |
| // Best choice: lowest-latency non-tearing mode. |
| swapchain_present_mode = vk::PresentModeKHR::eMailbox; |
| break; |
| } |
| if (mode == vk::PresentModeKHR::eImmediate) { |
| // Satisfactory choice: fastest, but tears. |
| swapchain_present_mode = vk::PresentModeKHR::eImmediate; |
| } |
| } |
| #endif |
| |
| // Determine number of images in the swapchain. |
| swapchain_image_count_ = window_params_.desired_swapchain_image_count; |
| if (surface_caps.minImageCount > swapchain_image_count_) { |
| swapchain_image_count_ = surface_caps.minImageCount; |
| } else if (surface_caps.maxImageCount < swapchain_image_count_ && |
| surface_caps.maxImageCount != 0) { // 0 means "no limit" |
| swapchain_image_count_ = surface_caps.maxImageCount; |
| } |
| |
| // TODO: choosing an appropriate pre-transform will probably be important on |
| // mobile devices. |
| auto pre_transform = vk::SurfaceTransformFlagBitsKHR::eIdentity; |
| |
| // Pick a format and color-space for the swap-chain. |
| vk::Format format = vk::Format::eUndefined; |
| vk::ColorSpaceKHR color_space = vk::ColorSpaceKHR::eSrgbNonlinear; |
| { |
| auto result = physical_device().getSurfaceFormatsKHR(surface()); |
| VK_CHECK_RESULT(result); |
| for (auto& sf : result.value) { |
| if (sf.colorSpace != color_space) |
| continue; |
| |
| // TODO: remove this once Magma supports SRGB swapchains. |
| if (sf.format == vk::Format::eB8G8R8A8Unorm) { |
| format = sf.format; |
| break; |
| } |
| |
| if (sf.format == vk::Format::eB8G8R8A8Srgb) { |
| // eB8G8R8A8Srgb is our favorite! |
| format = sf.format; |
| break; |
| } else if (format == vk::Format::eUndefined) { |
| // Anything is better than eUndefined. |
| format = sf.format; |
| } |
| } |
| } |
| FX_CHECK(format != vk::Format::eUndefined); |
| |
| // TODO: old_swapchain will come into play (I think) when we support |
| // resizing the window. |
| vk::SwapchainKHR old_swapchain = nullptr; |
| |
| // Using eTransferDst allows us to blit debug info onto the surface. |
| // Using eSampled allows us to save memory by using the color attachment |
| // for intermediate computation. |
| const vk::ImageUsageFlags kImageUsageFlags = vk::ImageUsageFlagBits::eColorAttachment | |
| vk::ImageUsageFlagBits::eTransferDst | |
| vk::ImageUsageFlagBits::eSampled; |
| |
| // Create the swapchain. |
| vk::SwapchainKHR swapchain; |
| { |
| vk::SwapchainCreateInfoKHR info; |
| info.surface = surface(); |
| info.minImageCount = swapchain_image_count_; |
| info.imageFormat = format; |
| info.imageColorSpace = color_space; |
| info.imageExtent = swapchain_extent; |
| info.imageArrayLayers = 1; // TODO: what is this? |
| info.imageUsage = kImageUsageFlags; |
| info.queueFamilyIndexCount = 1; |
| uint32_t queue_family_index = device_queues_->vk_main_queue_family(); |
| info.pQueueFamilyIndices = &queue_family_index; |
| info.preTransform = pre_transform; |
| info.presentMode = swapchain_present_mode; |
| info.oldSwapchain = old_swapchain; |
| info.clipped = true; |
| |
| auto result = device().createSwapchainKHR(info); |
| VK_CHECK_RESULT(result); |
| swapchain = result.value; |
| } |
| |
| if (old_swapchain) { |
| // Note: destroying the swapchain also cleans up all its associated |
| // presentable images once the platform is done with them. |
| device().destroySwapchainKHR(old_swapchain); |
| } |
| |
| // Obtain swapchain images and buffers. |
| { |
| auto result = device().getSwapchainImagesKHR(swapchain); |
| VK_CHECK_RESULT(result); |
| |
| std::vector<vk::Image> images(std::move(result.value)); |
| std::vector<escher::ImagePtr> escher_images; |
| escher_images.reserve(images.size()); |
| for (auto& im : images) { |
| escher::ImageInfo image_info; |
| image_info.format = format; |
| image_info.width = swapchain_extent.width; |
| image_info.height = swapchain_extent.height; |
| image_info.usage = kImageUsageFlags; |
| |
| auto escher_image = escher::Image::WrapVkImage(escher()->resource_recycler(), image_info, im, |
| vk::ImageLayout::eUndefined); |
| FX_CHECK(escher_image); |
| escher_images.push_back(escher_image); |
| } |
| swapchain_ = escher::VulkanSwapchain(swapchain, escher_images, swapchain_extent.width, |
| swapchain_extent.height, format, color_space); |
| } |
| |
| // Create swapchain helper. |
| swapchain_helper_ = std::make_unique<escher::VulkanSwapchainHelper>(swapchain_, device(), |
| GetVulkanContext().queue); |
| } |
| |
| void DemoHarness::CreateEscher() { |
| TRACE_DURATION("gfx", "DemoHarness::CreateEscher"); |
| |
| FX_CHECK(!escher_); |
| escher_ = std::make_unique<escher::Escher>(device_queues_, filesystem_); |
| |
| // Replace Escher's default pipeline builder. |
| { |
| std::string vk_pipeline_cache_path = GetCacheDirectoryPath() + "/vk_pipeline_cache"; |
| std::vector<uint8_t> initial_bytes; |
| files::ReadFileToVector(vk_pipeline_cache_path, &initial_bytes); |
| |
| auto pipeline_builder = std::make_unique<escher::PipelineBuilder>( |
| device(), initial_bytes.empty() ? nullptr : initial_bytes.data(), initial_bytes.size(), |
| [path = std::move(vk_pipeline_cache_path)](std::vector<uint8_t> bytes) { |
| if (!files::WriteFile(path, reinterpret_cast<char*>(bytes.data()), bytes.size())) { |
| FX_LOGS(WARNING) << "Failed to write " << bytes.size() |
| << " bytes to VkPipelineCache data file: " << path; |
| } |
| }); |
| |
| // DemoHarness expects pipeline creation to always be done at well-defined times (typically at |
| // startup). The program will crash if there is an attempt to unexpectedly lazily generate a |
| // pipeline. A less draconian approach will be desirable for some other applications, such as |
| // Scenic. |
| pipeline_builder->set_log_pipeline_creation_callback( |
| [](const vk::GraphicsPipelineCreateInfo* graphics_info, |
| const vk::ComputePipelineCreateInfo* compute_info) { |
| if (graphics_info) { |
| FX_CHECK(false) << "attempted to lazily generate a Vulkan graphics pipeline."; |
| } else { |
| FX_CHECK(false) << "attempted to lazily generate a Vulkan compute pipeline."; |
| } |
| }); |
| |
| escher_->set_pipeline_builder(std::move(pipeline_builder)); |
| |
| // Ensure that the cache directory exists. |
| if (!files::IsDirectory(GetCacheDirectoryPath())) { |
| if (!files::CreateDirectory(GetCacheDirectoryPath())) { |
| FX_LOGS(WARNING) << "Failed to create cache directory: " << GetCacheDirectoryPath(); |
| } |
| } |
| } |
| } |
| |
| void DemoHarness::DestroyEscher() { escher_.reset(); } |
| |
| void DemoHarness::DestroySwapchain() { |
| swapchain_helper_.reset(); |
| |
| swapchain_.images.clear(); |
| |
| FX_CHECK(swapchain_.swapchain); |
| device().destroySwapchainKHR(swapchain_.swapchain); |
| swapchain_.swapchain = nullptr; |
| } |
| |
| void DemoHarness::DestroyDevice() { |
| if (auto surf = surface()) { |
| instance().destroySurfaceKHR(surf); |
| } |
| device_queues_ = nullptr; |
| } |
| |
| void DemoHarness::DestroyInstance() { instance_ = nullptr; } |
| |
| VkBool32 DemoHarness::HandleDebugReport(VkDebugReportFlagsEXT flags_in, |
| VkDebugReportObjectTypeEXT object_type_in, uint64_t object, |
| size_t location, int32_t message_code, |
| const char* pLayerPrefix, const char* pMessage) { |
| vk::DebugReportFlagsEXT flags(static_cast<vk::DebugReportFlagBitsEXT>(flags_in)); |
| vk::DebugReportObjectTypeEXT object_type( |
| static_cast<vk::DebugReportObjectTypeEXT>(object_type_in)); |
| |
| bool fatal = false; |
| |
| // Macro to facilitate matching messages. Example usage: |
| // if (MATCH_REPORT(DescriptorSet, 0, "VUID-VkWriteDescriptorSet-descriptorType-01403")) { |
| // FX_LOGS(INFO) << "ignoring descriptor set problem: " << pMessage << "\n\n"; |
| // return false; |
| // } |
| #define MATCH_REPORT(OTYPE, CODE, X) \ |
| ((object_type == vk::DebugReportObjectTypeEXT::e##OTYPE) && (message_code == CODE) && \ |
| (0 == strncmp(pMessage + 3, X, strlen(X) - 1))) |
| |
| if (flags == vk::DebugReportFlagBitsEXT::eInformation) { |
| // Paranoid check that there aren't multiple flags. |
| FX_DCHECK(flags == vk::DebugReportFlagBitsEXT::eInformation); |
| |
| std::cerr << "## Vulkan Information: "; |
| } else if (flags == vk::DebugReportFlagBitsEXT::eWarning) { |
| std::cerr << "## Vulkan Warning: "; |
| } else if (flags == vk::DebugReportFlagBitsEXT::ePerformanceWarning) { |
| std::cerr << "## Vulkan Performance Warning: "; |
| } else if (flags == vk::DebugReportFlagBitsEXT::eError) { |
| // Treat all errors as fatal. |
| fatal = true; |
| std::cerr << "## Vulkan Error: "; |
| } else if (flags == vk::DebugReportFlagBitsEXT::eDebug) { |
| std::cerr << "## Vulkan Debug: "; |
| } else { |
| // This should never happen, unless a new value has been added to |
| // vk::DebugReportFlagBitsEXT. In that case, add a new if-clause above. |
| fatal = true; |
| std::cerr << "## Vulkan Unknown Message Type (flags: " << vk::to_string(flags) << "): "; |
| } |
| |
| std::cerr << pMessage << " (layer: " << pLayerPrefix << " code: " << message_code |
| << " object-type: " << vk::to_string(object_type) << " object: " << object |
| << " location: " << location << ")" << std::endl; |
| |
| // Crash immediately on fatal errors. |
| FX_CHECK(!fatal); |
| |
| return false; |
| } |
| |
| escher::VulkanContext DemoHarness::GetVulkanContext() { return device_queues_->GetVulkanContext(); } |
| |
| bool DemoHarness::MaybeDrawFrame() { |
| static constexpr size_t kOffscreenBenchmarkFrameCount = 1000; |
| |
| if (run_offscreen_benchmark_) { |
| TRACE_DURATION("gfx", "escher::DemoHarness::MaybeDrawFrame (benchmarking)"); |
| |
| run_offscreen_benchmark_ = false; |
| |
| Demo::RunOffscreenBenchmark(demo_, swapchain_.width, swapchain_.height, swapchain_.format, |
| kOffscreenBenchmarkFrameCount); |
| |
| // Guarantee that there are no frames in flight. |
| escher()->vk_device().waitIdle(); |
| FX_CHECK(escher()->Cleanup()); |
| outstanding_frames_ = 0; |
| } |
| |
| if (IsAtMaxOutstandingFrames()) { |
| // Try clean up; maybe a frame is actually already finished. |
| escher()->Cleanup(); |
| if (IsAtMaxOutstandingFrames()) { |
| // Still too many frames in flight. Try again later. |
| return false; |
| } |
| } |
| |
| { |
| TRACE_DURATION("gfx", "escher::DemoHarness::MaybeDrawFrame (drawing)", "frame", frame_count_); |
| |
| auto frame = escher()->NewFrame(demo_->name(), frame_count_, enable_gpu_logging_); |
| OnFrameCreated(); |
| |
| swapchain_helper_->DrawFrame([&, this](const escher::ImagePtr& output_image, |
| const escher::SemaphorePtr& framebuffer_acquired, |
| const escher::SemaphorePtr& render_finished) { |
| if (output_image->layout() != output_image->swapchain_layout()) { |
| // No need to synchronize, because the entire command buffer is synchronized via |
| // |framebuffer_acquired|. Would be nice to roll this barrier into |swapchain_helper_|, |
| // but then it would need to know about the command buffer, which may not be desirable. |
| frame->cmds()->ImageBarrier(output_image, output_image->layout(), |
| output_image->swapchain_layout(), |
| vk::PipelineStageFlagBits::eBottomOfPipe, vk::AccessFlags(), |
| vk::PipelineStageFlagBits::eTopOfPipe, vk::AccessFlags()); |
| } |
| |
| demo_->DrawFrame(frame, output_image, framebuffer_acquired); |
| frame->EndFrame(render_finished, [this]() { OnFrameDestroyed(); }); |
| }); |
| } |
| |
| if (++frame_count_ == 1) { |
| first_frame_microseconds_ = stopwatch_.GetElapsedMicroseconds(); |
| stopwatch_.Reset(); |
| } else if (frame_count_ % kLogGpuTimestampsEveryNFrames == 0) { |
| enable_gpu_logging_ = true; |
| |
| // Print out FPS and memory stats. |
| FX_LOGS(INFO) << "---- Average frame rate: " << ComputeFps(); |
| FX_LOGS(INFO) << "---- Total GPU memory: " << (escher()->GetNumGpuBytesAllocated() / 1024) |
| << "kB"; |
| } else { |
| enable_gpu_logging_ = false; |
| } |
| |
| escher()->Cleanup(); |
| return true; |
| } |
| |
| bool DemoHarness::HandleKeyPress(std::string key) { |
| if (key == "ESCAPE") { |
| SetShouldQuit(); |
| return true; |
| } |
| if (demo_) { |
| return demo_->HandleKeyPress(std::move(key)); |
| } |
| return false; |
| } |
| |
| bool DemoHarness::IsAtMaxOutstandingFrames() { |
| FX_DCHECK(outstanding_frames_ <= Demo::kMaxOutstandingFrames); |
| return outstanding_frames_ >= Demo::kMaxOutstandingFrames; |
| } |
| |
| void DemoHarness::OnFrameCreated() { |
| FX_DCHECK(!IsAtMaxOutstandingFrames()); |
| ++outstanding_frames_; |
| } |
| |
| void DemoHarness::OnFrameDestroyed() { |
| FX_DCHECK(outstanding_frames_ > 0); |
| --outstanding_frames_; |
| FX_DCHECK(!IsAtMaxOutstandingFrames()); |
| } |
| |
| void DemoHarness::Run(Demo* demo) { |
| BeginRun(demo); |
| RunForPlatform(demo); |
| EndRun(); |
| } |
| |
| void DemoHarness::BeginRun(Demo* demo) { |
| FX_CHECK(demo); |
| FX_CHECK(!demo_); |
| demo_ = demo; |
| frame_count_ = 0; |
| first_frame_microseconds_ = 0; |
| stopwatch_.Reset(); |
| } |
| |
| void DemoHarness::EndRun() { |
| FX_CHECK(demo_); |
| demo_ = nullptr; |
| |
| FX_LOGS(INFO) << "Average frame rate: " << ComputeFps(); |
| FX_LOGS(INFO) << "First frame took: " << first_frame_microseconds_ / 1000.0 << " milliseconds"; |
| escher()->Cleanup(); |
| } |
| |
| double DemoHarness::ComputeFps() { |
| // Omit the first frame when computing the average, because it is generating |
| // pipelines. We subtract 2 instead of 1 because we just incremented it in |
| // DrawFrame(). |
| // |
| // TODO(fxbug.dev/7248): This could be improved. For example, when called from the |
| // destructor we don't know how much time has elapsed since the last |
| // DrawFrame(); it might be more accurate to subtract 1 instead of 2. Also, |
| // on Linux the swapchain allows us to queue up many DrawFrame() calls so if |
| // we quit after a short time then the FPS will be artificially high. |
| auto microseconds = stopwatch_.GetElapsedMicroseconds(); |
| return (frame_count_ - 2) * 1000000.0 / (microseconds - first_frame_microseconds_); |
| } |