| // 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/lib/escher/vk/shader_program.h" |
| |
| #include "src/ui/lib/escher/defaults/default_shader_program_factory.h" |
| #include "src/ui/lib/escher/flatland/flatland_static_config.h" |
| #include "src/ui/lib/escher/impl/vulkan_utils.h" |
| #include "src/ui/lib/escher/mesh/tessellation.h" |
| // TODO(fxbug.dev/7272): remove PaperRenderer shader dependency. |
| #include "src/ui/lib/escher/paper/paper_render_funcs.h" |
| #include "src/ui/lib/escher/paper/paper_renderer_static_config.h" |
| #include "src/ui/lib/escher/paper/paper_shape_cache.h" |
| #include "src/ui/lib/escher/renderer/batch_gpu_uploader.h" |
| #include "src/ui/lib/escher/shaders/util/spirv_file_util.h" |
| #include "src/ui/lib/escher/shape/mesh.h" |
| #include "src/ui/lib/escher/test/common/gtest_escher.h" |
| #include "src/ui/lib/escher/test/vk/vulkan_tester.h" |
| #include "src/ui/lib/escher/third_party/granite/vk/render_pass.h" |
| #include "src/ui/lib/escher/util/image_utils.h" |
| #include "src/ui/lib/escher/util/string_utils.h" |
| #include "src/ui/lib/escher/vk/command_buffer.h" |
| #include "src/ui/lib/escher/vk/impl/pipeline_layout_cache.h" |
| #include "src/ui/lib/escher/vk/pipeline_builder.h" |
| #include "src/ui/lib/escher/vk/shader_module_template.h" |
| #include "src/ui/lib/escher/vk/shader_variant_args.h" |
| #include "src/ui/lib/escher/vk/texture.h" |
| |
| #if ESCHER_USE_RUNTIME_GLSL |
| // This include requires a nogncheck to workaround a known GN issue that doesn't |
| // take ifdefs into account when checking includes, meaning that even when |
| // ESCHER_USE_RUNTIME_GLSL is false, GN will check the validity of this include |
| // and find that it shouldn't be allowed, since there is no shaderc when that |
| // macro is false. "nogncheck" prevents this. |
| #include "third_party/shaderc/libshaderc/include/shaderc/shaderc.hpp" // nogncheck |
| #endif |
| |
| #include <chrono> |
| |
| namespace { |
| using namespace escher; |
| |
| // TODO(fxbug.dev/24580): This number needs to be queried via sysmem or vulkan. |
| const uint32_t kYuvSize = 64; |
| |
| class ShaderProgramTest : public ::testing::Test, public VulkanTester { |
| protected: |
| ShaderProgramTest() |
| : vk_debug_report_callback_registry_( |
| VK_TESTS_SUPPRESSED() |
| ? nullptr |
| : test::EscherEnvironment::GetGlobalTestEnvironment()->GetVulkanInstance(), |
| std::make_optional<VulkanInstance::DebugReportCallback>( |
| test::impl::VkDebugReportCollector::HandleDebugReport, &vk_debug_report_collector_), |
| {}), |
| vk_debug_report_collector_() {} |
| const MeshPtr& ring_mesh1() const { return ring_mesh1_; } |
| const MeshPtr& ring_mesh2() const { return ring_mesh2_; } |
| const MeshPtr& sphere_mesh() const { return sphere_mesh_; } |
| |
| test::impl::VkDebugReportCallbackRegistry& vk_debug_report_callback_registry() { |
| return vk_debug_report_callback_registry_; |
| } |
| test::impl::VkDebugReportCollector& vk_debug_report_collector() { |
| return vk_debug_report_collector_; |
| } |
| |
| private: |
| void SetUp() override { |
| vk_debug_report_callback_registry().RegisterDebugReportCallbacks(); |
| auto escher = test::GetEscher(); |
| EXPECT_TRUE(escher->Cleanup()); |
| |
| // TODO(fxbug.dev/7272): remove PaperRenderer shader dependency. |
| auto factory = escher->shader_program_factory(); |
| bool success = factory->filesystem()->InitializeWithRealFiles({ |
| "shaders/model_renderer/main.vert", |
| "shaders/paper/common/use.glsl", |
| "shaders/test/main.frag", |
| "shaders/test/shadow_map_generation.frag", |
| "shaders/test/shadow_map_lighting.frag", |
| }); |
| EXPECT_TRUE(success); |
| |
| BatchGpuUploader gpu_uploader(escher->GetWeakPtr()); |
| ring_mesh1_ = NewRingMesh(escher, &gpu_uploader, |
| MeshSpec{MeshAttribute::kPosition2D | MeshAttribute::kUV}, 8, |
| vec2(0.f, 0.f), 300.f, 200.f); |
| ring_mesh2_ = NewRingMesh(escher, &gpu_uploader, |
| MeshSpec{MeshAttribute::kPosition2D | MeshAttribute::kUV}, 8, |
| vec2(0.f, 0.f), 400.f, 300.f); |
| sphere_mesh_ = NewSphereMesh(escher, &gpu_uploader, |
| MeshSpec{MeshAttribute::kPosition3D | MeshAttribute::kUV}, 8, |
| vec3(0.f, 0.f, 0.f), 300.f); |
| gpu_uploader.Submit(); |
| escher->vk_device().waitIdle(); |
| } |
| |
| void TearDown() override { |
| ring_mesh1_ = ring_mesh2_ = sphere_mesh_ = nullptr; |
| |
| auto escher = test::GetEscher(); |
| escher->vk_device().waitIdle(); |
| EXPECT_TRUE(escher->Cleanup()); |
| |
| escher->shader_program_factory()->Clear(); |
| |
| EXPECT_VULKAN_VALIDATION_OK(); |
| vk_debug_report_callback_registry().DeregisterDebugReportCallbacks(); |
| } |
| |
| MeshPtr ring_mesh1_; |
| MeshPtr ring_mesh2_; |
| MeshPtr sphere_mesh_; |
| |
| test::impl::VkDebugReportCallbackRegistry vk_debug_report_callback_registry_; |
| test::impl::VkDebugReportCollector vk_debug_report_collector_; |
| }; |
| |
| // Test to make sure that the shader data constants located in |
| // paper_renderer_static_config.h can be used to properly load |
| // vulkan shader programs. |
| VK_TEST_F(ShaderProgramTest, ShaderConstantsTest) { |
| auto escher = test::GetEscher(); |
| |
| auto program1 = escher->GetProgram(kAmbientLightProgramData); |
| auto program2 = escher->GetProgram(kAmbientLightProgramData); |
| auto program3 = escher->GetProgram(kShadowVolumeGeometryDebugProgramData); |
| auto program4 = escher->GetProgram(kShadowVolumeGeometryDebugProgramData); |
| |
| // The first two programs use the same variant args, so should be identical, |
| // and similarly with the last two. |
| EXPECT_EQ(program1, program2); |
| EXPECT_EQ(program3, program4); |
| EXPECT_NE(program1, program3); |
| } |
| |
| // This test simply records the length of time in microseconds that it takes for |
| // Escher to load up all of the shaders it uses for PaperRenderer and Flatland. |
| // This is useful for getting a quick idea for how long this process takes on |
| // different platforms as well as depending on whether or not we're recompiling |
| // shader source code or loading in precompiled binaries. |
| VK_TEST_F(ShaderProgramTest, TimingTest) { |
| auto escher = test::GetEscher(); |
| |
| // Clear out the shader program factory's cache so that the test is hermetic and |
| // does not change depending on whether or not previous tests have loaded these |
| // shaders into the cache already. |
| escher->shader_program_factory()->Clear(); |
| |
| auto start = std::chrono::high_resolution_clock::now(); |
| auto program1 = escher->GetProgram(kAmbientLightProgramData); |
| auto program2 = escher->GetProgram(kNoLightingProgramData); |
| auto program3 = escher->GetProgram(kPointLightProgramData); |
| auto program4 = escher->GetProgram(kShadowVolumeGeometryProgramData); |
| auto program5 = escher->GetProgram(kShadowVolumeGeometryDebugProgramData); |
| auto program6 = escher->GetProgram(kFlatlandStandardProgram); |
| auto program7 = escher->GetProgram(kFlatlandColorConversionProgram); |
| auto stop = std::chrono::high_resolution_clock::now(); |
| auto duration = std::chrono::duration_cast<std::chrono::microseconds>(stop - start); |
| FX_LOGS(INFO) << "Time taken to load shaders: " << duration.count() << " microseconds."; |
| } |
| |
| // Go through all of the shader programs in |paper_renderer_static_config.h| and make |
| // sure that all their spirv can be properly found on disk. |
| VK_TEST_F(ShaderProgramTest, SpirVReadFileTest) { |
| auto escher = test::GetEscher(); |
| auto base_path = *escher->shader_program_factory()->filesystem()->base_path() + "/shaders/"; |
| auto load_and_check_program = [&](const ShaderProgramData& program) { |
| for (const auto& iter : program.source_files) { |
| std::vector<uint32_t> spirv; |
| if (iter.second.size() == 0) { |
| continue; |
| } |
| EXPECT_TRUE(shader_util::ReadSpirvFromDisk(program.args, base_path, iter.second, &spirv)) |
| << iter.second; |
| EXPECT_TRUE(spirv.size() > 0); |
| } |
| }; |
| |
| load_and_check_program(kAmbientLightProgramData); |
| load_and_check_program(kNoLightingProgramData); |
| load_and_check_program(kPointLightProgramData); |
| load_and_check_program(kShadowVolumeGeometryProgramData); |
| load_and_check_program(kShadowVolumeGeometryDebugProgramData); |
| load_and_check_program(kFlatlandStandardProgram); |
| load_and_check_program(kFlatlandColorConversionProgram); |
| } |
| |
| // Test to check to the "SpirvExistsOnDisk" function, which determines |
| // if the spirv contents of a file on disk have changed relative to a |
| // different spirv vector. |
| // |
| // This test checks against real Escher shader files, which means that |
| // it will fail if someone modifies a shader source file for Escher but |
| // forgets to run the precompile script to generate the spirv. This will |
| // help in keeping the precompiled shaders up to date. |
| // |
| // This test is only meant to be run locally by the Escher development team, |
| // as such it is not included on CQ at all, which has ESCHER_USE_RUNTIME_GLSL |
| // set to false by default. This is because it is possible for other teams |
| // (e.g. Spinel) to update the SpirV compiler, which would cause the new |
| // shader spirv to differ from that on disk, causing this test to fail. Since |
| // we do not want other teams to be burned with having to run the shader |
| // precompile script, it is the job of the Escher team to run this test locally |
| // when making shader changes to make sure all precompiled shaders are checked |
| // in successfully. |
| #if ESCHER_TEST_FOR_GLSL_SPIRV_MISMATCH |
| VK_TEST_F(ShaderProgramTest, SpirvNotChangedTest) { |
| auto escher = test::GetEscher(); |
| auto filesystem = escher->shader_program_factory()->filesystem(); |
| |
| auto check_spirv_change = [&](const ShaderProgramData& program_data) { |
| // Loop over all the shader stages for the provided program. |
| for (const auto& iter : program_data.source_files) { |
| // Skip if path is empty. |
| if (iter.second.length() == 0) { |
| continue; |
| } |
| |
| ShaderStage stage = iter.first; |
| EXPECT_TRUE(program_data.source_files.count(stage)); |
| auto compiler = std::make_unique<shaderc::Compiler>(); |
| std::string shader_name = iter.second; |
| auto shader = fxl::MakeRefCounted<ShaderModuleTemplate>(vk::Device(), compiler.get(), stage, |
| shader_name, filesystem); |
| |
| // The shader source code should still compile properly. |
| std::vector<uint32_t> spirv; |
| EXPECT_TRUE(shader->CompileVariantToSpirv(program_data.args, &spirv)); |
| |
| // The new spirv should not be any different than the spirv that is already on disk. |
| EXPECT_FALSE(shader_util::SpirvExistsOnDisk( |
| program_data.args, *filesystem->base_path() + "/shaders/", shader_name, spirv)); |
| }; |
| }; |
| |
| check_spirv_change(kAmbientLightProgramData); |
| check_spirv_change(kNoLightingProgramData); |
| check_spirv_change(kPointLightProgramData); |
| check_spirv_change(kShadowVolumeGeometryProgramData); |
| check_spirv_change(kShadowVolumeGeometryDebugProgramData); |
| check_spirv_change(kFlatlandStandardProgram); |
| check_spirv_change(kFlatlandColorConversionProgram); |
| } |
| #endif // ESCHER_TEST_FOR_GLSL_SPIRV_MISMATCH |
| |
| VK_TEST_F(ShaderProgramTest, CachedVariants) { |
| auto escher = test::GetEscher(); |
| |
| // TODO(fxbug.dev/7272): remove PaperRenderer shader dependency. |
| ShaderVariantArgs variant1({{"USE_ATTRIBUTE_UV", "1"}, |
| {"USE_PAPER_SHADER_PUSH_CONSTANTS", "1"}, |
| {"NO_SHADOW_LIGHTING_PASS", "1"}}); |
| ShaderVariantArgs variant2({{"USE_ATTRIBUTE_UV", "0"}, |
| {"USE_PAPER_SHADER_PUSH_CONSTANTS", "1"}, |
| {"NO_SHADOW_LIGHTING_PASS", "1"}}); |
| const char* kMainVert = "shaders/model_renderer/main.vert"; |
| const char* kMainFrag = "shaders/test/main.frag"; |
| |
| auto program1 = escher->GetGraphicsProgram(kMainVert, kMainFrag, variant1); |
| auto program2 = escher->GetGraphicsProgram(kMainVert, kMainFrag, variant1); |
| auto program3 = escher->GetGraphicsProgram(kMainVert, kMainFrag, variant2); |
| auto program4 = escher->GetGraphicsProgram(kMainVert, kMainFrag, variant2); |
| |
| // The first two programs use the same variant args, so should be identical, |
| // and similarly with the last two. |
| EXPECT_EQ(program1, program2); |
| EXPECT_EQ(program3, program4); |
| EXPECT_NE(program1, program3); |
| } |
| |
| VK_TEST_F(ShaderProgramTest, NonExistentShaderDeathTest) { |
| auto escher = test::GetEscher(); |
| |
| ShaderVariantArgs variant1({{"USE_ATTRIBUTE_UV", "1"}, |
| {"USE_PAPER_SHADER_PUSH_CONSTANTS", "1"}, |
| {"NO_SHADOW_LIGHTING_PASS", "1"}}); |
| const char* kNonExistentVert = "shaders/NON_EXISTENT_SHADER.vert"; |
| const char* kNonExistentFrag = "shaders/NON_EXISTENT_SHADER.frag"; |
| |
| EXPECT_DEATH( |
| { auto program = escher->GetGraphicsProgram(kNonExistentVert, kNonExistentFrag, variant1); }, |
| ""); |
| } |
| |
| // Helper function for tests below. Typically clients only populate a RenderPassInfo; RenderPasses |
| // are lazily generated/cached internally by CommandBufferPipelineState::FlushGraphicsPipeline(). |
| // This creates/returns an actual Vulkan render pass. |
| impl::RenderPassPtr CreateRenderPassForTest() { |
| auto escher = test::GetEscher(); |
| |
| // Use the same output format as Scenic screenshots. |
| constexpr vk::Format kScenicScreenshotFormat = vk::Format::eB8G8R8A8Unorm; |
| const vk::Format kDepthStencilFormat = |
| ESCHER_CHECKED_VK_RESULT(escher->device()->caps().GetMatchingDepthStencilFormat( |
| {vk::Format::eD24UnormS8Uint, vk::Format::eD32SfloatS8Uint})); |
| |
| RenderPassInfo::AttachmentInfo color_info; |
| color_info.format = kScenicScreenshotFormat; |
| color_info.swapchain_layout = vk::ImageLayout::eColorAttachmentOptimal; |
| color_info.sample_count = 1; |
| |
| RenderPassInfo info; |
| RenderPassInfo::InitRenderPassInfo(&info, color_info, |
| /*depth_format=*/kDepthStencilFormat, |
| /*msaa_format=*/vk::Format::eUndefined, /*sample_count=*/1, |
| /*use_transient_depth_and_msaa*/ false); |
| |
| return fxl::MakeRefCounted<impl::RenderPass>(escher->resource_recycler(), info); |
| } |
| |
| // Helper function which sets up vertex attribute bindings that will be used for pipeline creation |
| // in tests. It doesn't really matter what vertex format is used, so we just use the standard one |
| // used by PaperShapeCache. |
| void SetupVertexAttributeBindingsForTest(CommandBufferPipelineState* cbps) { |
| MeshSpec mesh_spec = PaperShapeCache::kStandardMeshSpec(); |
| const uint32_t total_attribute_count = mesh_spec.total_attribute_count(); |
| BlockAllocator allocator(512); |
| RenderFuncs::VertexAttributeBinding* attribute_bindings = |
| RenderFuncs::NewVertexAttributeBindings(PaperRenderFuncs::kMeshAttributeBindingLocations, |
| &allocator, mesh_spec, total_attribute_count); |
| |
| for (uint32_t i = 0; i < total_attribute_count; ++i) { |
| attribute_bindings[i].Bind(cbps); |
| } |
| } |
| |
| // This tests the most direct form of pipeline generation, without all of the laziness and caching |
| // done by CommandBuffer. Fundamentally this requires 4 things: |
| // 1) a set of vk::ShaderModules |
| // 2) a vk::PipelineLayout |
| // 3) a vk::RenderPass |
| // 4) a description of the static Vulkan state that the pipeline will be used with |
| // |
| // ShaderProgram provides both 1) and 2), the latter via introspecting each modules' SPIR-V code. |
| // The test constructs 3) and 4). The latter is achieved |
| // is provided directly by the ShaderProgram, and 2) is |
| // |
| // - a ShaderProgram, in particular: |
| // - the set of vk::ShaderModules that it encapsulates |
| // - the PipelineLayout* obtained by inspection of the shader modules' SPIR-V code |
| // - a MeshSpec, which defines the pipeline's vertex attribute bindings |
| // - a CommandBufferPipelineState, responsible for: |
| // - representing all static state required to build a pipeline, i.e. everything except |
| // - building the pipeline based on that state. |
| VK_TEST_F(ShaderProgramTest, GeneratePipelineDirectly) { |
| auto escher = test::GetEscher(); |
| |
| // 1), 2): obtain the ShaderProgram and the corresponding PipelineLayout. |
| // TODO(fxbug.dev/7272): remove PaperRenderer shader dependency. |
| auto program = ClearPipelineStash(escher->GetProgram(escher::kNoLightingProgramData)); |
| EXPECT_TRUE(program); |
| PipelineLayoutPtr pipeline_layout = |
| program->ObtainPipelineLayout(escher->pipeline_layout_cache(), nullptr); |
| |
| // 3): create a RenderPass. |
| // NOTE: typically, RenderPasses are lazily generated/cached by |
| // CommandBufferPipelineState::FlushGraphicsPipeline(). |
| impl::RenderPassPtr render_pass = CreateRenderPassForTest(); |
| |
| // 4) Specify the static Vulkan state. |
| CommandBufferPipelineState cbps(escher->pipeline_builder()->GetWeakPtr()); |
| SetupVertexAttributeBindingsForTest(&cbps); |
| cbps.set_render_pass(render_pass.get()); |
| cbps.SetToDefaultState(CommandBuffer::DefaultState::kOpaque); |
| |
| // 5) Build a pipeline (smoke-test). |
| EXPECT_EQ(0U, program->stashed_graphics_pipeline_count()); |
| vk::Pipeline pipeline_orig = cbps.FlushGraphicsPipeline(pipeline_layout.get(), program.get()); |
| EXPECT_EQ(1U, program->stashed_graphics_pipeline_count()); |
| |
| // 6) Verify that, when blending is disabled, we get the same cached pipeline with different |
| // blend-ops and blend-factors. |
| const vk::BlendOp alpha_op = vk::BlendOp::eMin; |
| const vk::BlendOp alpha_op_orig = cbps.static_state()->get_alpha_blend_op(); |
| EXPECT_NE(alpha_op, alpha_op_orig); // otherwise the test is bogus |
| const vk::BlendOp color_op = vk::BlendOp::eMin; |
| const vk::BlendOp color_op_orig = cbps.static_state()->get_color_blend_op(); |
| EXPECT_NE(color_op, color_op_orig); // otherwise the test is bogus |
| const vk::BlendFactor dst_alpha_blend = vk::BlendFactor::eOne; |
| const vk::BlendFactor src_alpha_blend = vk::BlendFactor::eOne; |
| const vk::BlendFactor dst_color_blend = vk::BlendFactor::eOne; |
| const vk::BlendFactor src_color_blend = vk::BlendFactor::eOne; |
| const vk::BlendFactor dst_alpha_blend_orig = cbps.static_state()->get_dst_alpha_blend(); |
| const vk::BlendFactor src_alpha_blend_orig = cbps.static_state()->get_src_alpha_blend(); |
| const vk::BlendFactor dst_color_blend_orig = cbps.static_state()->get_dst_color_blend(); |
| const vk::BlendFactor src_color_blend_orig = cbps.static_state()->get_src_color_blend(); |
| EXPECT_NE(dst_alpha_blend, dst_alpha_blend_orig); // otherwise the test is bogus |
| EXPECT_NE(src_alpha_blend, src_alpha_blend_orig); // otherwise the test is bogus |
| EXPECT_NE(dst_color_blend, dst_color_blend_orig); // otherwise the test is bogus |
| EXPECT_NE(src_color_blend, src_color_blend_orig); // otherwise the test is bogus |
| |
| cbps.SetBlendFactors(src_color_blend, src_alpha_blend, dst_color_blend, dst_alpha_blend); |
| cbps.SetBlendOp(color_op, alpha_op); |
| vk::Pipeline pipeline2 = cbps.FlushGraphicsPipeline(pipeline_layout.get(), program.get()); |
| EXPECT_EQ(pipeline_orig, pipeline2); |
| |
| // 7) Verify that, when blending is enabled, different blend-ops and blend-factors result in |
| // different pipelines. |
| cbps.SetBlendEnable(true); |
| vk::Pipeline pipeline3 = cbps.FlushGraphicsPipeline(pipeline_layout.get(), program.get()); |
| cbps.SetBlendFactors(src_color_blend_orig, src_alpha_blend_orig, dst_color_blend_orig, |
| dst_alpha_blend_orig); |
| vk::Pipeline pipeline4 = cbps.FlushGraphicsPipeline(pipeline_layout.get(), program.get()); |
| cbps.SetBlendOp(color_op_orig, alpha_op_orig); |
| vk::Pipeline pipeline5 = cbps.FlushGraphicsPipeline(pipeline_layout.get(), program.get()); |
| EXPECT_NE(pipeline_orig, pipeline3); |
| EXPECT_NE(pipeline_orig, pipeline4); |
| EXPECT_NE(pipeline_orig, pipeline5); |
| EXPECT_NE(pipeline3, pipeline4); |
| EXPECT_NE(pipeline3, pipeline5); |
| EXPECT_NE(pipeline4, pipeline5); |
| |
| // 8) Verify that, when blending is enabled, changing blend constants only makes a difference when |
| // the blend-factor is eConstantColor. |
| cbps.potential_static_state()->blend_constants[0] = 0.77f; |
| cbps.potential_static_state()->blend_constants[3] = 0.66f; |
| vk::Pipeline pipeline6 = cbps.FlushGraphicsPipeline(pipeline_layout.get(), program.get()); |
| EXPECT_EQ(pipeline5, pipeline6); |
| cbps.potential_static_state()->blend_constants[0] = 0.55f; |
| cbps.potential_static_state()->blend_constants[3] = 0.44f; |
| vk::Pipeline pipeline7 = cbps.FlushGraphicsPipeline(pipeline_layout.get(), program.get()); |
| EXPECT_EQ(pipeline5, pipeline7); |
| cbps.SetBlendFactors(vk::BlendFactor::eConstantColor, vk::BlendFactor::eConstantColor, |
| vk::BlendFactor::eConstantColor, vk::BlendFactor::eConstantColor); |
| vk::Pipeline pipeline8 = cbps.FlushGraphicsPipeline(pipeline_layout.get(), program.get()); |
| EXPECT_NE(pipeline7, pipeline8); |
| cbps.potential_static_state()->blend_constants[0] = 0.77f; |
| cbps.potential_static_state()->blend_constants[3] = 0.66f; |
| vk::Pipeline pipeline9 = cbps.FlushGraphicsPipeline(pipeline_layout.get(), program.get()); |
| EXPECT_NE(pipeline6, pipeline9); |
| // This is similar to comparing 5 vs. 6, except this time the blend-factor is eConstantCOlor. |
| EXPECT_NE(pipeline8, pipeline9); |
| } |
| |
| VK_TEST_F(ShaderProgramTest, PipelineBuilder) { |
| auto escher = test::GetEscher(); |
| |
| // 1), 2): obtain the ShaderPrograms and the corresponding PipelineLayouts. |
| // TODO(fxbug.dev/7272): remove PaperRenderer shader dependency. |
| auto program1 = ClearPipelineStash(escher->GetProgram(escher::kNoLightingProgramData)); |
| auto program2 = ClearPipelineStash(escher->GetProgram(escher::kPointLightProgramData)); |
| EXPECT_TRUE(program1); |
| EXPECT_TRUE(program2); |
| PipelineLayoutPtr pipeline_layout1 = |
| program1->ObtainPipelineLayout(escher->pipeline_layout_cache(), nullptr); |
| PipelineLayoutPtr pipeline_layout2 = |
| program2->ObtainPipelineLayout(escher->pipeline_layout_cache(), nullptr); |
| |
| // 3): create a RenderPass. |
| // NOTE: typically, RenderPasses are lazily generated/cached by |
| // CommandBufferPipelineState::FlushGraphicsPipeline(). |
| impl::RenderPassPtr render_pass = CreateRenderPassForTest(); |
| |
| // 4) Specify the static Vulkan state. |
| CommandBufferPipelineState cbps(escher->pipeline_builder()->GetWeakPtr()); |
| SetupVertexAttributeBindingsForTest(&cbps); |
| cbps.set_render_pass(render_pass.get()); |
| cbps.SetToDefaultState(CommandBuffer::DefaultState::kOpaque); |
| |
| // 5) Set up two similar vk::GraphicsPipelineCreateInfo structs, one with stencil buffer enabled |
| // and the other without. These will be passed to PipelineBuilder instances. |
| BlockAllocator allocator(128); |
| auto create_info1 = |
| cbps.InitGraphicsPipelineCreateInfo(&allocator, pipeline_layout1.get(), program1.get()); |
| cbps.SetStencilTest(true); |
| auto create_info2 = |
| cbps.InitGraphicsPipelineCreateInfo(&allocator, pipeline_layout2.get(), program2.get()); |
| |
| // 6) This callback will be invoked after set_log_pipeline_creation_callback() has injected |
| // it into a PipelineBuilder. |
| size_t log_graphics_callback_count = 0; |
| size_t log_compute_callback_count = 0; |
| auto log_callback = [&log_graphics_callback_count, &log_compute_callback_count]( |
| const vk::GraphicsPipelineCreateInfo* graphics_info, |
| const vk::ComputePipelineCreateInfo* compute_info) { |
| EXPECT_TRUE(graphics_info || compute_info); |
| EXPECT_TRUE(!graphics_info || !compute_info); |
| if (graphics_info) { |
| ++log_graphics_callback_count; |
| } else { |
| ++log_compute_callback_count; |
| } |
| }; |
| |
| // Now we start testing the pipeline builder! |
| |
| // Test that we can create pipelines using a pipeline builder which doesn't us a VkPipeline cache. |
| { |
| PipelineBuilder builder(escher->vk_device()); |
| |
| auto pipeline1 = builder.BuildGraphicsPipeline(*create_info1, /*do_logging=*/false); |
| |
| auto pipeline2 = builder.BuildGraphicsPipeline(*create_info2, /*do_logging=*/true); |
| |
| // Neither of the above pipelines resulted in logging, because no callback had been set. |
| // After this, newly-built pipelines will trigger invocation of this callback, but only if the |
| // |do_logging| arg is true. |
| builder.set_log_pipeline_creation_callback(log_callback); |
| |
| auto pipeline3 = builder.BuildGraphicsPipeline(*create_info1, /*do_logging=*/false); |
| EXPECT_EQ(0U, log_graphics_callback_count); |
| auto pipeline4 = builder.BuildGraphicsPipeline(*create_info2, /*do_logging=*/true); |
| EXPECT_EQ(1U, log_graphics_callback_count); |
| auto pipeline5 = builder.BuildGraphicsPipeline(*create_info1, /*do_logging=*/true); |
| EXPECT_EQ(2U, log_graphics_callback_count); |
| auto pipeline6 = builder.BuildGraphicsPipeline(*create_info2, /*do_logging=*/true); |
| EXPECT_EQ(3U, log_graphics_callback_count); |
| |
| ASSERT_TRUE(pipeline1); |
| ASSERT_TRUE(pipeline2); |
| ASSERT_TRUE(pipeline3); |
| ASSERT_TRUE(pipeline4); |
| ASSERT_TRUE(pipeline5); |
| ASSERT_TRUE(pipeline6); |
| escher->vk_device().destroyPipeline(pipeline1); |
| escher->vk_device().destroyPipeline(pipeline2); |
| escher->vk_device().destroyPipeline(pipeline3); |
| escher->vk_device().destroyPipeline(pipeline4); |
| escher->vk_device().destroyPipeline(pipeline5); |
| escher->vk_device().destroyPipeline(pipeline6); |
| } |
| |
| // Test that we can create pipelines using a VkPipelineCache, and that creating the "same" |
| // pipeline twice does not result in a second invocation of the StorePipelineCacheDataCallback. |
| |
| // TODO(fxbug.dev/49692): SwiftShader ICD doesn't store cached pipeline to disk correctly. |
| // So we disabled all the EXPECT checks on SwiftShader. We need to remove this |
| // after the bug is solved. |
| { |
| // Keeps track of the number of times that a newly-built pipeline results in updated |
| // cache data, which the application should persist to disk. |
| size_t updated_vk_cache_data_count = 0; |
| |
| // Store the latest cache data obtained via the |
| std::vector<uint8_t> latest_vk_cache_data; |
| |
| auto updated_vk_cache_data_callback = [&updated_vk_cache_data_count, |
| &latest_vk_cache_data](std::vector<uint8_t> data) { |
| ++updated_vk_cache_data_count; |
| latest_vk_cache_data = std::move(data); |
| }; |
| |
| PipelineBuilder builder(escher->vk_device(), nullptr, 0, updated_vk_cache_data_callback); |
| // This time we enable the logging callback from the beginning. |
| builder.set_log_pipeline_creation_callback(log_callback); |
| log_graphics_callback_count = log_compute_callback_count = 0; |
| |
| // The callback is not invoked eagerly when the pipeline is built, rather it is invoked when |
| // MaybeStorePipelineCacheData() is polled. |
| auto pipeline1a = builder.BuildGraphicsPipeline(*create_info1, /*do_logging=*/true); |
| EXPECT_EQ(1U, log_graphics_callback_count); |
| EXEC_IF_NOT_SWIFTSHADER(EXPECT_EQ(0U, updated_vk_cache_data_count)); |
| builder.MaybeStorePipelineCacheData(); |
| EXEC_IF_NOT_SWIFTSHADER(EXPECT_EQ(1U, updated_vk_cache_data_count)); |
| |
| // Same thing, with different pipeline create info. |
| auto pipeline2a = builder.BuildGraphicsPipeline(*create_info2, /*do_logging=*/true); |
| EXPECT_EQ(2U, log_graphics_callback_count); |
| EXEC_IF_NOT_SWIFTSHADER(EXPECT_EQ(1U, updated_vk_cache_data_count)); |
| builder.MaybeStorePipelineCacheData(); |
| EXEC_IF_NOT_SWIFTSHADER(EXPECT_EQ(2U, updated_vk_cache_data_count)); |
| |
| // Creating additional pipelines with previously-seen create_info does not result in a change to |
| // the persisted Vk cache data. |
| auto pipeline1b = builder.BuildGraphicsPipeline(*create_info1, /*do_logging=*/true); |
| auto pipeline2b = builder.BuildGraphicsPipeline(*create_info2, /*do_logging=*/true); |
| builder.MaybeStorePipelineCacheData(); |
| EXPECT_EQ(4U, log_graphics_callback_count); |
| EXEC_IF_NOT_SWIFTSHADER(EXPECT_EQ(2U, updated_vk_cache_data_count)); |
| |
| // Create a new builder, primed with the data needed to build pipeline1 and pipeline2. Building |
| // these will not result in any new data to persist. |
| PipelineBuilder builder2(escher->vk_device(), latest_vk_cache_data.data(), |
| latest_vk_cache_data.size(), updated_vk_cache_data_callback); |
| // Build pipeline1/pipeline2 in the opposite order, just in case it makes a difference to the |
| // particular Vulkan implementation. |
| auto pipeline2c = builder.BuildGraphicsPipeline(*create_info2, /*do_logging=*/true); |
| auto pipeline1c = builder.BuildGraphicsPipeline(*create_info1, /*do_logging=*/true); |
| builder.MaybeStorePipelineCacheData(); |
| EXPECT_EQ(6U, log_graphics_callback_count); |
| EXEC_IF_NOT_SWIFTSHADER(EXPECT_EQ(2U, updated_vk_cache_data_count)); |
| |
| ASSERT_TRUE(pipeline1a); |
| ASSERT_TRUE(pipeline1b); |
| ASSERT_TRUE(pipeline1c); |
| ASSERT_TRUE(pipeline2a); |
| ASSERT_TRUE(pipeline2b); |
| ASSERT_TRUE(pipeline2c); |
| escher->vk_device().destroyPipeline(pipeline1a); |
| escher->vk_device().destroyPipeline(pipeline1b); |
| escher->vk_device().destroyPipeline(pipeline1c); |
| escher->vk_device().destroyPipeline(pipeline2a); |
| escher->vk_device().destroyPipeline(pipeline2b); |
| escher->vk_device().destroyPipeline(pipeline2c); |
| } |
| } |
| |
| // Used by InitRenderPassInfo() and InitRenderPassInfoHelper(). |
| static constexpr uint32_t kRenderTargetAttachmentIndex = 0; |
| static constexpr uint32_t kResolveTargetAttachmentIndex = 1; |
| |
| // Helper function which factors out common code from the two InitRenderPassInfo() variants. |
| static void TestMultipleSubpassHelper(RenderPassInfo* rp, |
| const RenderPassInfo::AttachmentInfo& color_info, |
| const RenderPassInfo::AttachmentInfo& depth_stencil_info, |
| const RenderPassInfo::AttachmentInfo* msaa_info) { |
| FX_DCHECK(color_info.sample_count == 1); |
| FX_DCHECK((!msaa_info && depth_stencil_info.sample_count == 1) || |
| (msaa_info && msaa_info->sample_count > 1 && |
| msaa_info->sample_count == depth_stencil_info.sample_count)); |
| |
| rp->color_attachment_infos[0] = color_info; |
| rp->depth_stencil_attachment_info = depth_stencil_info; |
| |
| { |
| rp->num_color_attachments = 1; |
| // Clear and store color attachment 0, the sole color attachment. |
| rp->clear_attachments = 1u << kRenderTargetAttachmentIndex; |
| rp->store_attachments = 1u << kRenderTargetAttachmentIndex; |
| |
| // Standard flags for a depth-testing render-pass that needs to first clear |
| // the depth image. |
| rp->op_flags = RenderPassInfo::kClearDepthStencilOp | RenderPassInfo::kOptimalColorLayoutOp | |
| RenderPassInfo::kOptimalDepthStencilLayoutOp; |
| // NOTE: the flags above assume that there is a depth/stencil attachment. If not, the flags |
| // will need to be modified (shouldn't be hard, but out of scope of current CL). |
| FX_DCHECK(depth_stencil_info.format != vk::Format::eUndefined); |
| |
| rp->clear_color[0].setFloat32({0.f, 0.f, 0.f, 0.f}); |
| |
| for (uint32_t i = 0; i < 2; i++) { |
| rp->subpasses.push_back(RenderPassInfo::Subpass{ |
| .color_attachments = {kRenderTargetAttachmentIndex}, |
| .input_attachments = {}, |
| .resolve_attachments = {kResolveTargetAttachmentIndex}, |
| .num_color_attachments = 1, |
| .num_input_attachments = 0, |
| .num_resolve_attachments = 0, |
| }); |
| } |
| } |
| |
| for (size_t i = rp->num_color_attachments; i < VulkanLimits::kNumColorAttachments; ++i) { |
| rp->color_attachment_infos[i] = {}; |
| rp->color_attachments[i] = nullptr; |
| } |
| } |
| |
| bool TestRenderPassSubpasses(RenderPassInfo* rp, vk::Rect2D render_area, |
| const ImagePtr& output_image, const TexturePtr& depth_texture) { |
| FX_DCHECK(output_image->info().sample_count == 1); |
| rp->render_area = render_area; |
| |
| AttachmentInfo color_info; |
| AttachmentInfo depth_stencil_info; |
| AttachmentInfo msaa_info; |
| if (output_image) { |
| if (!output_image->is_swapchain_image()) { |
| FX_LOGS(ERROR) << "RenderPassInfo::InitRenderPassInfo(): Output image doesn't have valid " |
| "swapchain layout."; |
| return false; |
| } |
| if (output_image->swapchain_layout() != output_image->layout()) { |
| FX_LOGS(ERROR) << "RenderPassInfo::InitRenderPassInfo(): Current layout of output image " |
| "does not match its swapchain layout."; |
| return false; |
| } |
| color_info.InitFromImage(output_image); |
| } |
| if (depth_texture) { |
| depth_stencil_info.InitFromImage(depth_texture->image()); |
| } |
| |
| TestMultipleSubpassHelper(rp, color_info, depth_stencil_info, |
| msaa_texture ? &msaa_info : nullptr); |
| |
| // TODO(fxbug.dev/43279): Can we get away sharing image views across multiple RenderPassInfo |
| // structs? |
| ImageViewPtr output_image_view = |
| allocator ? allocator->ObtainImageView(output_image) : ImageView::New(output_image); |
| |
| rp->color_attachments[kRenderTargetAttachmentIndex] = std::move(output_image_view); |
| rp->depth_stencil_attachment = depth_texture; |
| return true; |
| } |
| |
| // Command buffer should be able to use a render pass that contains |
| // 2 or more subpasses. |
| VK_TEST_F(ShaderProgramTest, MultipleSubpasses) { |
| auto escher = test::GetEscher(); |
| |
| auto program = escher->GetProgram(kFlatlandStandardProgram); |
| EXPECT_TRUE(program); |
| |
| auto cb = CommandBuffer::NewForGraphics(escher, /*use_protected_memory=*/false); |
| |
| auto depth_format_result = escher->device()->caps().GetMatchingDepthFormat(); |
| bool has_depth_attachment = depth_format_result.result != vk::Result::eSuccess; |
| |
| const uint32_t kWidth = 512, kHeight = 512; |
| |
| auto color_attachment = escher->NewAttachmentTexture(vk::Format::eB8G8R8A8Unorm, kWidth, kHeight, |
| 1, vk::Filter::eNearest); |
| auto depth_attachment = has_depth_attachment |
| ? escher->NewAttachmentTexture(depth_format_result.value, kWidth, |
| kHeight, 1, vk::Filter::eNearest) |
| : TexturePtr(); |
| |
| // Initialize the render pass. |
| RenderPassInfo render_pass; |
| vk::Rect2D render_area = {{0, 0}, {color_attachment->width(), color_attachment->height()}}; |
| |
| if (!TestRenderpassSubpasses(&render_pass, render_area, color_attachment, depth_attachment) { |
| FX_LOGS(ERROR) << "RectangleCompositor::DrawBatch(): RenderPassInfo initialization failed. " |
| "Exiting."; |
| return; |
| } |
| |
| |
| cb->BeginRenderPass(render_pass_info); |
| |
| // Setting the program doesn't immediately result in a pipeline being set. |
| cb->SetShaderProgram(program); |
| EXPECT_EQ(GetCurrentVkPipeline(cb), vk::Pipeline()); |
| |
| cb->Draw(/*vertex_count*/6); |
| |
| cb->NextSubpass(); |
| cb->SetShaderProgram(program); |
| |
| cb->Draw(/*vertex_count*/6); |
| |
| cb->EndRenderPass(); |
| |
| // TODO(fxbug.dev/7174): ideally only submitted CommandBuffers would need to be |
| // cleaned up: if a never-submitted CB is destroyed, then it shouldn't |
| // keep anything alive, and it shouldn't cause problems in e.g. |
| // CommandBufferPool due to a forever-straggling buffer. |
| EXPECT_TRUE(cb->Submit(nullptr)); |
| } |
| |
| // TODO(fxbug.dev/7174): we need to set up so many meshes, materials, framebuffers, etc. |
| // before we can obtain pipelines, we might as well just make this an end-to-end |
| // test and actually render. Or, go the other direction and manually set up |
| // state in a standalone CommandBufferPipelineState object. |
| // NOTE: see some of the other tests above, such as GeneratePipelineWithoutCommandBuffer... |
| // it is now possible to generate pipelines more directly. |
| VK_TEST_F(ShaderProgramTest, GeneratePipelines) { |
| auto escher = test::GetEscher(); |
| |
| // TODO(fxbug.dev/7272): remove PaperRenderer shader dependency. |
| auto program = escher->GetProgram(escher::kNoLightingProgramData); |
| EXPECT_TRUE(program); |
| |
| auto cb = CommandBuffer::NewForGraphics(escher, /*use_protected_memory=*/false); |
| |
| auto depth_format_result = escher->device()->caps().GetMatchingDepthFormat(); |
| bool has_depth_attachment = depth_format_result.result != vk::Result::eSuccess; |
| |
| auto color_attachment = |
| escher->NewAttachmentTexture(vk::Format::eB8G8R8A8Unorm, 512, 512, 1, vk::Filter::eNearest); |
| auto depth_attachment = has_depth_attachment |
| ? escher->NewAttachmentTexture(depth_format_result.value, 512, 512, 1, |
| vk::Filter::eNearest) |
| : TexturePtr(); |
| |
| // TODO(fxbug.dev/7174): add support for setting an initial image layout (is there |
| // already a bug for this? If not, add one). Then, use this so we don't need |
| // to immediately set a barrier on the new color attachment. |
| // Alternately/additionally, note that we don't need to do this for the depth |
| // attachment (because we aren't loading it we can treat it as initially |
| // eUndefined)... there's no reason that we shouldn't be able to do this for |
| // the color attachment too. |
| cb->ImageBarrier( |
| color_attachment->image(), vk::ImageLayout::eUndefined, |
| vk::ImageLayout::eColorAttachmentOptimal, vk::PipelineStageFlagBits::eTopOfPipe, |
| vk::AccessFlags(), vk::PipelineStageFlagBits::eColorAttachmentOutput, |
| vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite); |
| |
| RenderPassInfo render_pass_info; |
| render_pass_info.color_attachments[0] = color_attachment; |
| render_pass_info.num_color_attachments = 1; |
| // Clear and store color attachment 0, the sole color attachment. |
| render_pass_info.clear_attachments = 1u; |
| render_pass_info.store_attachments = 1u; |
| render_pass_info.depth_stencil_attachment = depth_attachment; |
| render_pass_info.op_flags = RenderPassInfo::kOptimalColorLayoutOp; |
| if (depth_attachment) { |
| render_pass_info.op_flags |= |
| RenderPassInfo::kClearDepthStencilOp | RenderPassInfo::kOptimalDepthStencilLayoutOp; |
| } |
| |
| // TODO(fxbug.dev/44566): simplify this test to not need images/command-buffers. |
| RenderPassInfo::InitRenderPassAttachmentInfosFromImages(&render_pass_info); |
| EXPECT_TRUE(render_pass_info.Validate()); |
| |
| // TODO(fxbug.dev/7174): move into ShaderProgramTest. |
| BatchGpuUploader gpu_uploader(escher->GetWeakPtr(), 0); |
| auto noise_image = image_utils::NewNoiseImage(escher->image_cache(), &gpu_uploader, 512, 512); |
| auto upload_semaphore = escher::Semaphore::New(escher->vk_device()); |
| gpu_uploader.AddSignalSemaphore(upload_semaphore); |
| gpu_uploader.Submit(); |
| cb->AddWaitSemaphore(std::move(upload_semaphore), vk::PipelineStageFlagBits::eFragmentShader); |
| auto noise_texture = escher->NewTexture(noise_image, vk::Filter::eLinear); |
| |
| cb->BeginRenderPass(render_pass_info); |
| |
| // Setting the program doesn't immediately result in a pipeline being set. |
| cb->SetShaderProgram(program); |
| EXPECT_EQ(GetCurrentVkPipeline(cb), vk::Pipeline()); |
| |
| // We'll use the same texture for both meshes. |
| cb->BindTexture(1, 1, noise_texture); |
| |
| auto mesh = ring_mesh1(); |
| auto ab = &mesh->attribute_buffer(0); |
| |
| cb->BindIndices(mesh->index_buffer(), mesh->index_buffer_offset(), vk::IndexType::eUint32); |
| |
| cb->BindVertices(0, ab->buffer, ab->offset, ab->stride); |
| cb->SetVertexAttributes(0, 0, vk::Format::eR32G32Sfloat, |
| mesh->spec().attribute_offset(0, MeshAttribute::kPosition2D)); |
| cb->SetVertexAttributes(0, 2, vk::Format::eR32G32Sfloat, |
| mesh->spec().attribute_offset(0, MeshAttribute::kUV)); |
| |
| // Set the command buffer to a known default state, and obtain a pipeline. |
| cb->SetToDefaultState(CommandBuffer::DefaultState::kOpaque); |
| |
| auto depth_read_write_pipeline = ObtainGraphicsPipeline(cb); |
| EXPECT_NE(depth_read_write_pipeline, vk::Pipeline()); |
| |
| // Requesting another pipeline with the same state returns the same cached |
| // pipeline. |
| EXPECT_EQ(depth_read_write_pipeline, ObtainGraphicsPipeline(cb)); |
| |
| // Changing the state results in a different pipeline being returned. |
| cb->SetDepthTestAndWrite(true, false); |
| auto depth_readonly_pipeline = ObtainGraphicsPipeline(cb); |
| EXPECT_NE(depth_readonly_pipeline, vk::Pipeline()); |
| EXPECT_NE(depth_readonly_pipeline, depth_read_write_pipeline); |
| |
| // Requesting another pipeline with the same state returns the same cached |
| // pipeline. |
| EXPECT_EQ(depth_readonly_pipeline, ObtainGraphicsPipeline(cb)); |
| |
| // Changing to a different mesh with the same layout doesn't change the |
| // obtained pipeline. |
| mesh = ring_mesh2(); |
| ab = &mesh->attribute_buffer(0); |
| |
| cb->BindIndices(mesh->index_buffer(), mesh->index_buffer_offset(), vk::IndexType::eUint32); |
| |
| cb->BindVertices(0, ab->buffer, ab->offset, ab->stride); |
| |
| cb->SetVertexAttributes(0, 0, vk::Format::eR32G32Sfloat, |
| mesh->spec().attribute_offset(0, MeshAttribute::kPosition2D)); |
| cb->SetVertexAttributes(0, 2, vk::Format::eR32G32Sfloat, |
| mesh->spec().attribute_offset(0, MeshAttribute::kUV)); |
| |
| EXPECT_EQ(depth_readonly_pipeline, ObtainGraphicsPipeline(cb)); |
| |
| // Changing to a mesh with a different layout results in a different pipeline. |
| mesh = sphere_mesh(); |
| ab = &mesh->attribute_buffer(0); |
| |
| cb->BindIndices(mesh->index_buffer(), mesh->index_buffer_offset(), vk::IndexType::eUint32); |
| |
| cb->BindVertices(0, ab->buffer, ab->offset, ab->stride); |
| |
| cb->SetVertexAttributes(0, 0, vk::Format::eR32G32B32Sfloat, |
| mesh->spec().attribute_offset(0, MeshAttribute::kPosition3D)); |
| cb->SetVertexAttributes(2, 0, vk::Format::eR32G32Sfloat, |
| mesh->spec().attribute_offset(0, MeshAttribute::kUV)); |
| |
| EXPECT_NE(depth_readonly_pipeline, ObtainGraphicsPipeline(cb)); |
| EXPECT_NE(vk::Pipeline(), ObtainGraphicsPipeline(cb)); |
| |
| vk::Pipeline last_pipeline = ObtainGraphicsPipeline(cb); |
| |
| // Switching to an immutable sampler changes the pipeline. |
| ImageInfo info; |
| info.width = kYuvSize; |
| info.height = kYuvSize; |
| info.format = vk::Format::eG8B8R82Plane420Unorm; |
| info.usage = vk::ImageUsageFlagBits::eSampled; |
| info.is_mutable = false; |
| |
| auto yuv_image = escher->image_cache()->NewImage(info); |
| auto yuv_texture = escher->NewTexture(yuv_image, vk::Filter::eLinear); |
| |
| EXPECT_TRUE(yuv_texture->sampler()->is_immutable()); |
| |
| cb->SetShaderProgram(program, yuv_texture->sampler()); |
| EXPECT_NE(last_pipeline, ObtainGraphicsPipeline(cb)); |
| EXPECT_NE(vk::Pipeline(), ObtainGraphicsPipeline(cb)); |
| |
| auto yuv_pipeline = ObtainGraphicsPipeline(cb); |
| last_pipeline = ObtainGraphicsPipeline(cb); |
| |
| // Using the same sampler does not. |
| cb->SetShaderProgram(program, yuv_texture->sampler()); |
| EXPECT_EQ(last_pipeline, ObtainGraphicsPipeline(cb)); |
| EXPECT_NE(vk::Pipeline(), ObtainGraphicsPipeline(cb)); |
| |
| last_pipeline = ObtainGraphicsPipeline(cb); |
| |
| // Using a different sampler does cause the pipeline to change, because |
| // immutable samplers require custom descriptor sets, and pipelines are bound |
| // to specific descriptor sets at construction time. |
| cb->SetShaderProgram(program, noise_texture->sampler()); |
| EXPECT_NE(last_pipeline, ObtainGraphicsPipeline(cb)); |
| EXPECT_NE(vk::Pipeline(), ObtainGraphicsPipeline(cb)); |
| |
| last_pipeline = ObtainGraphicsPipeline(cb); |
| |
| // Using the previous YUV sampler reuses the old pipeline. |
| cb->SetShaderProgram(program, yuv_texture->sampler()); |
| EXPECT_NE(last_pipeline, ObtainGraphicsPipeline(cb)); |
| EXPECT_NE(vk::Pipeline(), ObtainGraphicsPipeline(cb)); |
| EXPECT_EQ(yuv_pipeline, ObtainGraphicsPipeline(cb)); |
| |
| cb->EndRenderPass(); |
| |
| // TODO(fxbug.dev/7174): ideally only submitted CommandBuffers would need to be |
| // cleaned up: if a never-submitted CB is destroyed, then it shouldn't |
| // keep anything alive, and it shouldn't cause problems in e.g. |
| // CommandBufferPool due to a forever-straggling buffer. |
| EXPECT_TRUE(cb->Submit(nullptr)); |
| } |
| |
| // This tests if PipelineLayoutCache is keeping elements alive when ObtainPipelineLayout() is used. |
| VK_TEST_F(ShaderProgramTest, ObtainPipelineLayoutHitsPipelineLayoutCache) { |
| auto escher = test::GetEscher(); |
| auto program = ClearPipelineStash(escher->GetProgram(escher::kNoLightingProgramData)); |
| EXPECT_TRUE(program); |
| |
| // We should use cache to generate pipeline layouts. |
| auto* cache = escher->pipeline_layout_cache(); |
| cache->Clear(); |
| PipelineLayoutPtr pipeline_layout1 = |
| program->ObtainPipelineLayout(escher->pipeline_layout_cache(), nullptr); |
| EXPECT_EQ(1u, cache->size()); |
| PipelineLayoutPtr pipeline_layout2 = |
| program->ObtainPipelineLayout(escher->pipeline_layout_cache(), nullptr); |
| EXPECT_EQ(1u, cache->size()); |
| EXPECT_EQ(pipeline_layout1.get(), pipeline_layout2.get()); |
| |
| // After a number of frames pipeline layout falls out of |cache|. |
| const int kNumFrames = 5; |
| uint64_t frame_number_ = 0; |
| for (int i = 0; i < kNumFrames; ++i) { |
| auto frame = escher->NewFrame("ShaderProgramTest", ++frame_number_); |
| frame->EndFrame(SemaphorePtr(), [] {}); |
| } |
| EXPECT_EQ(0u, cache->size()); |
| |
| // ObtainPipelineLayout keeps pipeline layout alive. |
| PipelineLayoutPtr first_pipeline_layout = |
| program->ObtainPipelineLayout(escher->pipeline_layout_cache(), nullptr); |
| for (int i = 0; i < kNumFrames; ++i) { |
| auto frame = escher->NewFrame("ShaderProgramTest", ++frame_number_); |
| PipelineLayoutPtr cur_pipeline_layout = |
| program->ObtainPipelineLayout(escher->pipeline_layout_cache(), nullptr); |
| frame->EndFrame(SemaphorePtr(), [] {}); |
| EXPECT_EQ(first_pipeline_layout.get(), cur_pipeline_layout.get()); |
| } |
| EXPECT_EQ(1u, cache->size()); |
| } |
| |
| } // anonymous namespace |