blob: 54f66d82fb85175cd16695d0f267048b12c68a26 [file] [log] [blame]
// Copyright 2018 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/waterfall/waterfall_demo.h"
#include "src/lib/files/file.h"
#include "src/ui/examples/escher/waterfall/scenes/paper_demo_scene1.h"
#include "src/ui/lib/escher/defaults/default_shader_program_factory.h"
#include "src/ui/lib/escher/impl/vulkan_utils.h"
#include "src/ui/lib/escher/mesh/tessellation.h"
#include "src/ui/lib/escher/paper/paper_renderer_static_config.h"
#include "src/ui/lib/escher/paper/paper_scene.h"
#include "src/ui/lib/escher/paper/paper_shader_structs.h"
#include "src/ui/lib/escher/renderer/batch_gpu_uploader.h"
#include "src/ui/lib/escher/renderer/frame.h"
#include "src/ui/lib/escher/scene/camera.h"
#include "src/ui/lib/escher/scene/viewing_volume.h"
#include "src/ui/lib/escher/shape/mesh.h"
#include "src/ui/lib/escher/util/enum_utils.h"
#include "src/ui/lib/escher/util/image_utils.h"
#include "src/ui/lib/escher/util/trace_macros.h"
#include "src/ui/lib/escher/vk/shader_module_template.h"
#include "src/ui/lib/escher/vk/shader_program.h"
#include "src/ui/lib/escher/vk/texture.h"
using namespace escher;
static constexpr float kNear = 1.f;
static constexpr float kFar = -200.f;
static constexpr size_t kMaxNumPointLights = 2;
static constexpr vk::Format kYuvTextureFormat = vk::Format::eG8B8G8R8422Unorm;
static constexpr vk::ImageTiling kYuvTextureTiling = vk::ImageTiling::eOptimal;
namespace {
bool IsImageFormatSupported(vk::PhysicalDevice device, vk::Format format, vk::ImageTiling tiling) {
vk::FormatProperties properties = device.getFormatProperties(format);
if (tiling == vk::ImageTiling::eLinear) {
return properties.linearTilingFeatures != vk::FormatFeatureFlags();
} else {
return properties.optimalTilingFeatures != vk::FormatFeatureFlags();
}
}
} // namespace
WaterfallDemo::WaterfallDemo(escher::EscherWeakPtr escher_in, vk::Format swapchain_format, int argc,
char** argv)
: Demo(std::move(escher_in), "Waterfall Demo") {
ProcessCommandLineArgs(argc, argv);
// Initialize filesystem with files before creating renderer; it will use them
// to generate the necessary ShaderPrograms.
escher()->shader_program_factory()->filesystem()->InitializeWithRealFiles(
kPaperRendererShaderPaths);
auto& device_caps = escher()->device()->caps();
renderer_ = escher::PaperRenderer::New(GetEscherWeakPtr());
// Determine the allowable MSAA sample counts to cycle through with "M" key.
{
auto filtered =
// TODO(fxbug.dev/44326): 8x MSAA causes a segfault on NVIDIA/Linux.
// device_caps.GetAllMatchingSampleCounts({1U, 2U, 4U, 8});
device_caps.GetAllMatchingSampleCounts({1U, 2U, 4U});
FX_CHECK(!filtered.empty());
allowed_sample_counts_.reserve(filtered.size());
for (auto sample_count : filtered) {
// PaperRendererConfig expects uint8_t, not size_t.
allowed_sample_counts_.push_back(static_cast<uint8_t>(sample_count));
}
if (allowed_sample_counts_.size() >= 2) {
// Start with the cheapest available MSAA.
current_sample_count_index_ = 1;
}
}
if (device_caps.allow_ycbcr && IsImageFormatSupported(escher()->vk_physical_device(),
kYuvTextureFormat, kYuvTextureTiling)) {
InitializeYcbcrTexture();
}
renderer_config_.debug_frame_number = true;
renderer_config_.shadow_type = PaperRendererShadowType::kShadowVolume;
renderer_config_.msaa_sample_count = allowed_sample_counts_[current_sample_count_index_];
renderer_config_.num_depth_buffers = 2;
renderer_config_.depth_stencil_format =
ESCHER_CHECKED_VK_RESULT(device_caps.GetMatchingDepthStencilFormat(
{vk::Format::eD24UnormS8Uint, vk::Format::eD32SfloatS8Uint}));
WarmPipelineCache(swapchain_format);
renderer_->SetConfig(renderer_config_);
// Start with 1 light. Number of lights can be cycled via CycleNumLights(). Light positions
// and colors are animated by UpdateLighting().
paper_scene_ = fxl::MakeRefCounted<PaperScene>();
paper_scene_->point_lights.resize(1);
}
WaterfallDemo::~WaterfallDemo() {}
void WaterfallDemo::WarmPipelineCache(vk::Format swapchain_format) const {
TRACE_DURATION("gfx", "WaterfallDemo::WarmPipelineCache");
// Make a copy of the config, we'll be modifying it.
PaperRendererConfig config = renderer_config_;
// Build pipelines for rendering to the display as well as offscreen.
std::vector<std::pair<vk::Format, vk::ImageLayout>> formats_and_layouts = {
{swapchain_format, vk::ImageLayout::eColorAttachmentOptimal},
{swapchain_format, vk::ImageLayout::ePresentSrcKHR},
};
std::vector<escher::SamplerPtr> immutable_samplers;
if (ycbcr_tex_) {
immutable_samplers.push_back(ycbcr_tex_->sampler());
}
for (size_t count : allowed_sample_counts_) {
config.msaa_sample_count = static_cast<uint8_t>(count);
for (auto shadow_type : EnumArray<PaperRendererShadowType>()) {
if (!renderer_->SupportsShadowType(shadow_type)) {
continue;
}
config.shadow_type = shadow_type;
for (auto& p : formats_and_layouts) {
auto& output_format = p.first;
auto& output_swapchain_layout = p.second;
PaperRenderer::WarmPipelineAndRenderPassCaches(escher(), config, output_format,
output_swapchain_layout, immutable_samplers,
/*use_protected_memory*/ false);
}
}
}
}
void WaterfallDemo::SetWindowSize(vk::Extent2D window_size) {
FX_CHECK(paper_scene_);
if (window_size_ == window_size)
return;
window_size_ = window_size;
paper_scene_->bounding_box =
escher::BoundingBox(vec3(0.f, 0.f, kFar), vec3(window_size.width, window_size.height, kNear));
InitializeDemoScenes();
}
void WaterfallDemo::InitializeYcbcrTexture() {
FX_CHECK(escher()->device()->caps().allow_ycbcr);
const char* kYuvFramePath = "/assets/bbb_frame.yuv";
constexpr uint32_t kYuvFrameWidth = 320;
constexpr uint32_t kYuvFrameHeight = 180;
constexpr vk::Format kYuvFrameFormat = kYuvTextureFormat;
auto base = escher()->shader_program_factory()->filesystem()->base_path();
FX_CHECK(base);
std::string path = *base + kYuvFramePath;
std::vector<uint8_t> data;
FX_CHECK(files::ReadFileToVector(path, &data)) << "failed to read: " << path;
auto image = escher()->gpu_allocator()->AllocateImage(
escher()->resource_recycler(),
{.format = kYuvFrameFormat,
.width = kYuvFrameWidth,
.height = kYuvFrameHeight,
.usage = vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst,
.tiling = kYuvTextureTiling});
BatchGpuUploader gpu_uploader(escher()->GetWeakPtr(), 0);
image_utils::WritePixelsToImage(&gpu_uploader, data.data(), image,
vk::ImageLayout::eShaderReadOnlyOptimal);
// NOTE: we *could* use a semaphore to guarantee that the upload finishes before we use the
// texture. However, we still haven't warmed the pipeline cache; this will take millisecond, by
// which time the texture should be finished uploading.
gpu_uploader.Submit();
ycbcr_tex_ = escher::Texture::New(escher()->resource_recycler(), image, vk::Filter::eNearest);
}
void WaterfallDemo::InitializeDemoScenes() {
demo_scenes_.clear();
demo_scenes_.emplace_back(new PaperDemoScene1(this, ycbcr_tex_));
demo_scenes_.emplace_back(new PaperDemoScene1(this));
demo_scenes_.back()->ToggleGraph();
for (auto& scene : demo_scenes_) {
scene->Init(paper_scene_.get());
}
}
void WaterfallDemo::ProcessCommandLineArgs(int argc, char** argv) {
for (int i = 1; i < argc; ++i) {
if (!strcmp("--debug", argv[i])) {
show_debug_info_ = true;
} else if (!strcmp("--no-debug", argv[i])) {
show_debug_info_ = false;
}
}
}
void WaterfallDemo::CycleNumLights() {
uint32_t num_point_lights = (paper_scene_->num_point_lights() + 1) % (kMaxNumPointLights + 1);
paper_scene_->point_lights.resize(num_point_lights);
FX_LOGS(INFO) << "WaterfallDemo number of point lights: " << num_point_lights;
}
void WaterfallDemo::CycleAnimationState() {
animation_state_ = (animation_state_ + 1) % 3;
switch (animation_state_) {
case 0:
object_stopwatch_.Start();
lighting_stopwatch_.Start();
break;
case 1:
object_stopwatch_.Stop();
lighting_stopwatch_.Start();
break;
case 2:
object_stopwatch_.Stop();
lighting_stopwatch_.Stop();
break;
default:
FX_CHECK(false) << "animation_state_ must be 0-2, is: " << animation_state_;
}
}
bool WaterfallDemo::HandleKeyPress(std::string key) {
if (key.size() > 1) {
if (key == "SPACE") {
CycleAnimationState();
return true;
}
return Demo::HandleKeyPress(key);
} else {
char key_char = key[0];
switch (key_char) {
// Cycle through camera projection modes.
case 'C': {
camera_projection_mode_ = (camera_projection_mode_ + 1) % 5;
const char* kCameraModeStrings[5] = {
"orthographic", "perspective", "tilted perspective", "tilted perspective from corner",
"stereo",
};
FX_LOGS(INFO) << "Camera projection mode: " << kCameraModeStrings[camera_projection_mode_];
return true;
}
// Toggle display of debug information.
case 'D': {
show_debug_info_ = !show_debug_info_;
renderer_config_.debug = show_debug_info_;
FX_LOGS(INFO) << "WaterfallDemo " << (show_debug_info_ ? "enabled" : "disabled")
<< " debugging.";
renderer_->SetConfig(renderer_config_);
return true;
}
case 'L': {
CycleNumLights();
return true;
}
// Cycle through MSAA sample counts.
case 'M': {
current_sample_count_index_ =
(current_sample_count_index_ + 1) % allowed_sample_counts_.size();
renderer_config_.msaa_sample_count = allowed_sample_counts_[current_sample_count_index_];
FX_LOGS(INFO) << "MSAA sample count: " << unsigned{renderer_config_.msaa_sample_count};
renderer_->SetConfig(renderer_config_);
return true;
}
// Cycle through shadow algorithms..
case 'S': {
auto& shadow_type = renderer_config_.shadow_type;
shadow_type = EnumCycle(shadow_type);
while (!renderer_->SupportsShadowType(shadow_type)) {
FX_LOGS(INFO) << "WaterfallDemo skipping unsupported shadow type: " << shadow_type;
shadow_type = EnumCycle(shadow_type);
}
renderer_->SetConfig(renderer_config_);
FX_LOGS(INFO) << "WaterfallDemo changed shadow type: " << renderer_config_;
return true;
}
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '0':
current_scene_ = static_cast<int>(demo_scenes_.size() + (key_char - '0') - 1) %
static_cast<int>(demo_scenes_.size());
FX_LOGS(INFO) << "Current scene index: " << current_scene_;
return true;
default:
return Demo::HandleKeyPress(key);
}
}
}
// Helper function for DrawFrame().
static std::vector<escher::Camera> GenerateCameras(int camera_projection_mode,
const escher::ViewingVolume& volume,
const escher::FramePtr& frame) {
switch (camera_projection_mode) {
// Orthographic full-screen.
case 0: {
return {escher::Camera::NewOrtho(volume)};
}
// Perspective where floor plane is full-screen, and parallel to screen.
case 1: {
vec3 eye(volume.width() / 2, volume.height() / 2, -10000);
vec3 target(volume.width() / 2, volume.height() / 2, 0);
vec3 up(0, -1, 0);
return {
escher::Camera::NewPerspective(volume, glm::lookAt(eye, target, up), glm::radians(8.f))};
}
// Perspective from tilted viewpoint (from x-center of stage).
case 2: {
vec3 eye(volume.width() / 2, 6000, -2000);
vec3 target(volume.width() / 2, volume.height() / 2, 0);
vec3 up(0, -1, 0);
return {
escher::Camera::NewPerspective(volume, glm::lookAt(eye, target, up), glm::radians(15.f))};
} break;
// Perspective from tilted viewpoint (from corner).
case 3: {
vec3 eye(volume.width() / 3, 6000, -3000);
vec3 target(volume.width() / 2, volume.height() / 3, 0);
vec3 up(0, -1, 0);
return {
escher::Camera::NewPerspective(volume, glm::lookAt(eye, target, up), glm::radians(15.f))};
} break;
// Stereo/Perspective from tilted viewpoint (from corner). This also
// demonstrates the ability to provide the view-projection matrix in a
// buffer instead of having the PaperRenderer upload the vp-matrix itself.
// This is typically used with a "pose buffer" in HMD applications.
// NOTE: the camera's transform must be fairly close to what will be read
// from the pose buffer, because the camera's position is used for z-sorting
// etc.
case 4: {
vec3 eye(volume.width() / 2, 6000, -3500);
vec3 eye_offset(40.f, 0.f, 0.f);
vec3 target(volume.width() / 2, volume.height() / 2, 0);
vec3 up(0, -1, 0);
float fov = glm::radians(15.f);
auto left_camera =
escher::Camera::NewPerspective(volume, glm::lookAt(eye - eye_offset, target, up), fov);
auto right_camera =
escher::Camera::NewPerspective(volume, glm::lookAt(eye + eye_offset, target, up), fov);
// Obtain a buffer and populate it as though it were obtained by invoking
// PoseBufferLatchingShader.
auto binding =
escher::NewPaperShaderUniformBinding<escher::PaperShaderLatchedPoseBuffer>(frame);
binding.first->vp_matrix[0] = left_camera.projection() * left_camera.transform();
binding.first->vp_matrix[1] = right_camera.projection() * right_camera.transform();
escher::BufferPtr latched_pose_buffer(binding.second.buffer);
// Both cameras use the same buffer, but index into it using a different
// eye index. NOTE: if you comment these lines out, there will be no
// visible difference, because PaperRenderer will compute/upload the same
// project * transform matrix. What would happen if you swap the kLeft
// and kRight?
left_camera.SetLatchedPoseBuffer(latched_pose_buffer, escher::CameraEye::kLeft);
right_camera.SetLatchedPoseBuffer(latched_pose_buffer, escher::CameraEye::kRight);
left_camera.SetViewport({0.f, 0.25f, 0.5f, 0.5f});
right_camera.SetViewport({0.5f, 0.25f, 0.5f, 0.5f});
return {left_camera, right_camera};
} break;
default:
// Should not happen.
FX_DCHECK(false);
return {escher::Camera::NewOrtho(volume)};
}
}
static void UpdateLighting(PaperScene* paper_scene, const escher::Stopwatch& stopwatch,
PaperRendererShadowType shadow_type) {
const size_t num_point_lights = paper_scene->num_point_lights();
if (num_point_lights == 0 || shadow_type == PaperRendererShadowType::kNone) {
paper_scene->ambient_light.color = vec3(1, 1, 1);
return;
}
// Set the ambient light to an arbitrary value that looks OK. The intensities
// of the point lights will be chosen so that the total light intensity on an
// unshadowed fragment is vec3(1,1,1).
const vec3 kAmbientLightColor(0.4f, 0.5f, 0.5f);
paper_scene->ambient_light.color = kAmbientLightColor;
for (auto& pl : paper_scene->point_lights) {
pl.color = (vec3(1.f, 1.f, 1.f) - kAmbientLightColor) / float(num_point_lights);
// Choose a light intensity that looks good with the falloff. If an object
// is too close to the light it will appear washed out.
// TODO(fxbug.dev/7260): add HDR support to address this.
pl.color *= 2.5f;
pl.falloff = 0.001f;
}
const double current_time = stopwatch.GetElapsedSeconds();
// Simple animation of point light.
const float width = paper_scene->width();
const float height = paper_scene->height();
if (num_point_lights == 1) {
paper_scene->point_lights[0].position =
vec3(width * .3f, height * .3f, -(800.f + 200.f * sin(current_time * 1.2f)));
} else {
FX_DCHECK(num_point_lights == 2);
paper_scene->point_lights[0].position =
vec3(width * .3f, height * .3f, -(800.f + 300.f * sin(current_time * 1.2f)));
paper_scene->point_lights[1].position =
vec3(width * (0.6f + 0.3f * sin(current_time * 0.7f)),
height * (0.4f + 0.2f * sin(current_time * 0.6f)), -900.f);
// Make the light colors subtly different.
vec3 color_diff = vec3(.02f, -.01f, .04f) * paper_scene->point_lights[0].color;
paper_scene->point_lights[0].color += color_diff;
paper_scene->point_lights[1].color -= color_diff;
}
}
void WaterfallDemo::DrawFrame(const FramePtr& frame, const ImagePtr& output_image,
const escher::SemaphorePtr& framebuffer_acquired) {
TRACE_DURATION("gfx", "WaterfallDemo::DrawFrame");
// WarmPipelineCache() generated all of the necessary pipelines.
frame->DisableLazyPipelineCreation();
SetWindowSize({output_image->width(), output_image->height()});
frame->cmds()->AddWaitSemaphore(framebuffer_acquired,
vk::PipelineStageFlagBits::eColorAttachmentOutput);
std::vector<Camera> cameras =
GenerateCameras(camera_projection_mode_, ViewingVolume(paper_scene_->bounding_box), frame);
// Animate light positions and intensities.
UpdateLighting(paper_scene_.get(), lighting_stopwatch_, renderer_config_.shadow_type);
auto gpu_uploader =
std::make_shared<BatchGpuUploader>(escher()->GetWeakPtr(), frame->frame_number());
renderer_->BeginFrame(frame, gpu_uploader, paper_scene_, std::move(cameras), output_image);
{
TRACE_DURATION("gfx", "WaterfallDemo::DrawFrame[scene]");
demo_scenes_[current_scene_]->Update(object_stopwatch_, paper_scene_.get(), renderer_.get());
}
renderer_->FinalizeFrame();
escher::SemaphorePtr upload_semaphore;
if (gpu_uploader->HasContentToUpload()) {
upload_semaphore = escher::Semaphore::New(escher()->vk_device());
gpu_uploader->AddSignalSemaphore(upload_semaphore);
}
gpu_uploader->Submit();
renderer_->EndFrame(std::move(upload_semaphore));
}