| // 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 <future> |
| #include <numeric> |
| |
| #include "compute_tessellator.h" |
| #include "flutter/fml/synchronization/waitable_event.h" |
| #include "flutter/fml/time/time_point.h" |
| #include "flutter/testing/testing.h" |
| #include "gmock/gmock.h" |
| #include "impeller/base/strings.h" |
| #include "impeller/core/formats.h" |
| #include "impeller/core/host_buffer.h" |
| #include "impeller/display_list/skia_conversions.h" |
| #include "impeller/entity/contents/content_context.h" |
| #include "impeller/fixtures/golden_paths.h" |
| #include "impeller/fixtures/sample.comp.h" |
| #include "impeller/fixtures/stage1.comp.h" |
| #include "impeller/fixtures/stage2.comp.h" |
| #include "impeller/geometry/path.h" |
| #include "impeller/geometry/path_builder.h" |
| #include "impeller/geometry/path_component.h" |
| #include "impeller/playground/compute_playground_test.h" |
| #include "impeller/renderer/command_buffer.h" |
| #include "impeller/renderer/compute_pipeline_builder.h" |
| #include "impeller/renderer/compute_tessellator.h" |
| #include "impeller/renderer/path_polyline.comp.h" |
| #include "impeller/renderer/pipeline_library.h" |
| #include "impeller/renderer/render_pass.h" |
| #include "impeller/renderer/stroke.comp.h" |
| #include "third_party/imgui/imgui.h" |
| #include "third_party/skia/include/utils/SkParsePath.h" |
| |
| namespace impeller { |
| namespace testing { |
| |
| using ComputeSubgroupTest = ComputePlaygroundTest; |
| INSTANTIATE_COMPUTE_SUITE(ComputeSubgroupTest); |
| |
| TEST_P(ComputeSubgroupTest, CapabilitiesSuportSubgroups) { |
| auto context = GetContext(); |
| ASSERT_TRUE(context); |
| ASSERT_TRUE(context->GetCapabilities()->SupportsComputeSubgroups()); |
| } |
| |
| TEST_P(ComputeSubgroupTest, PathPlayground) { |
| // Renders stroked SVG paths in an interactive playground. |
| using SS = StrokeComputeShader; |
| |
| auto context = GetContext(); |
| ASSERT_TRUE(context); |
| ASSERT_TRUE(context->GetCapabilities()->SupportsComputeSubgroups()); |
| char svg_path_data[16384] = |
| "M140 20 " |
| "C73 20 20 74 20 140 " |
| "c0 135 136 170 228 303 " |
| "88-132 229-173 229-303 " |
| "0-66-54-120-120-120 " |
| "-48 0-90 28-109 69 " |
| "-19-41-60-69-108-69z"; |
| size_t vertex_count = 0; |
| Scalar stroke_width = 1.0; |
| |
| auto vertex_buffer = CreateHostVisibleDeviceBuffer<SS::VertexBuffer<2048>>( |
| context, "VertexBuffer"); |
| auto vertex_buffer_count = |
| CreateHostVisibleDeviceBuffer<SS::VertexBufferCount>(context, |
| "VertexCount"); |
| |
| auto callback = [&](RenderPass& pass) -> bool { |
| ::memset(vertex_buffer_count->OnGetContents(), 0, |
| sizeof(SS::VertexBufferCount)); |
| ::memset(vertex_buffer->OnGetContents(), 0, sizeof(SS::VertexBuffer<2048>)); |
| const auto* main_viewport = ImGui::GetMainViewport(); |
| ImGui::SetNextWindowPos( |
| ImVec2(main_viewport->WorkPos.x + 650, main_viewport->WorkPos.y + 20)); |
| ImGui::Begin("Path data", nullptr, ImGuiWindowFlags_AlwaysAutoResize); |
| ImGui::InputTextMultiline("Path", svg_path_data, |
| IM_ARRAYSIZE(svg_path_data)); |
| ImGui::DragFloat("Stroke width", &stroke_width, .1, 0.0, 25.0); |
| |
| SkPath sk_path; |
| if (SkParsePath::FromSVGString(svg_path_data, &sk_path)) { |
| std::promise<bool> promise; |
| auto future = promise.get_future(); |
| |
| auto path = skia_conversions::ToPath(sk_path); |
| auto host_buffer = HostBuffer::Create(context->GetResourceAllocator()); |
| auto status = |
| ComputeTessellator{} |
| .SetStrokeWidth(stroke_width) |
| .Tessellate(path, *host_buffer, context, |
| DeviceBuffer::AsBufferView(vertex_buffer), |
| DeviceBuffer::AsBufferView(vertex_buffer_count), |
| [vertex_buffer_count, &vertex_count, |
| &promise](CommandBuffer::Status status) { |
| vertex_count = |
| reinterpret_cast<SS::VertexBufferCount*>( |
| vertex_buffer_count->OnGetContents()) |
| ->count; |
| promise.set_value( |
| status == CommandBuffer::Status::kCompleted); |
| }); |
| switch (status) { |
| case ComputeTessellator::Status::kCommandInvalid: |
| ImGui::Text("Failed to submit compute job (invalid command)"); |
| break; |
| case ComputeTessellator::Status::kTooManyComponents: |
| ImGui::Text("Failed to submit compute job (too many components) "); |
| break; |
| case ComputeTessellator::Status::kOk: |
| break; |
| } |
| if (!future.get()) { |
| ImGui::Text("Failed to submit compute job."); |
| return false; |
| } |
| if (vertex_count > 0) { |
| ImGui::Text("Vertex count: %zu", vertex_count); |
| } |
| } else { |
| ImGui::Text("Failed to parse path data"); |
| } |
| ImGui::End(); |
| |
| ContentContext renderer(context, nullptr); |
| if (!renderer.IsValid()) { |
| return false; |
| } |
| |
| using VS = SolidFillPipeline::VertexShader; |
| |
| pass.SetCommandLabel("Draw Stroke"); |
| pass.SetStencilReference(0); |
| |
| ContentContextOptions options; |
| options.sample_count = pass.GetRenderTarget().GetSampleCount(); |
| options.color_attachment_pixel_format = |
| pass.GetRenderTarget().GetRenderTargetPixelFormat(); |
| options.has_depth_stencil_attachments = |
| pass.GetRenderTarget().GetStencilAttachment().has_value(); |
| options.blend_mode = BlendMode::kSourceIn; |
| options.primitive_type = PrimitiveType::kTriangleStrip; |
| |
| options.stencil_mode = |
| ContentContextOptions::StencilMode::kLegacyClipIncrement; |
| |
| pass.SetPipeline(renderer.GetSolidFillPipeline(options)); |
| |
| auto count = reinterpret_cast<SS::VertexBufferCount*>( |
| vertex_buffer_count->OnGetContents()) |
| ->count; |
| |
| pass.SetVertexBuffer( |
| VertexBuffer{.vertex_buffer = DeviceBuffer::AsBufferView(vertex_buffer), |
| .vertex_count = count, |
| .index_type = IndexType::kNone}); |
| |
| VS::FrameInfo frame_info; |
| auto world_matrix = Matrix::MakeScale(GetContentScale()); |
| frame_info.mvp = pass.GetOrthographicTransform() * world_matrix; |
| frame_info.color = Color::Red().Premultiply(); |
| VS::BindFrameInfo( |
| pass, renderer.GetTransientsBuffer().EmplaceUniform(frame_info)); |
| |
| return pass.Draw().ok(); |
| }; |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); |
| } |
| |
| TEST_P(ComputeSubgroupTest, LargePath) { |
| // The path in here is large enough to highlight issues around exceeding |
| // subgroup size. |
| using SS = StrokeComputeShader; |
| |
| auto context = GetContext(); |
| ASSERT_TRUE(context); |
| ASSERT_TRUE(context->GetCapabilities()->SupportsComputeSubgroups()); |
| size_t vertex_count = 0; |
| Scalar stroke_width = 1.0; |
| |
| auto vertex_buffer = CreateHostVisibleDeviceBuffer<SS::VertexBuffer<2048>>( |
| context, "VertexBuffer"); |
| auto vertex_buffer_count = |
| CreateHostVisibleDeviceBuffer<SS::VertexBufferCount>(context, |
| "VertexCount"); |
| |
| auto complex_path = |
| PathBuilder{} |
| .MoveTo({359.934, 96.6335}) |
| .CubicCurveTo({358.189, 96.7055}, {356.436, 96.7908}, |
| {354.673, 96.8895}) |
| .CubicCurveTo({354.571, 96.8953}, {354.469, 96.9016}, |
| {354.367, 96.9075}) |
| .CubicCurveTo({352.672, 97.0038}, {350.969, 97.113}, |
| {349.259, 97.2355}) |
| .CubicCurveTo({349.048, 97.2506}, {348.836, 97.2678}, |
| {348.625, 97.2834}) |
| .CubicCurveTo({347.019, 97.4014}, {345.407, 97.5299}, |
| {343.789, 97.6722}) |
| .CubicCurveTo({343.428, 97.704}, {343.065, 97.7402}, |
| {342.703, 97.7734}) |
| .CubicCurveTo({341.221, 97.9086}, {339.736, 98.0505}, |
| {338.246, 98.207}) |
| .CubicCurveTo({337.702, 98.2642}, {337.156, 98.3292}, |
| {336.612, 98.3894}) |
| .CubicCurveTo({335.284, 98.5356}, {333.956, 98.6837}, |
| {332.623, 98.8476}) |
| .CubicCurveTo({332.495, 98.8635}, {332.366, 98.8818}, |
| {332.237, 98.8982}) |
| .LineTo({332.237, 102.601}) |
| .LineTo({321.778, 102.601}) |
| .LineTo({321.778, 100.382}) |
| .CubicCurveTo({321.572, 100.413}, {321.367, 100.442}, |
| {321.161, 100.476}) |
| .CubicCurveTo({319.22, 100.79}, {317.277, 101.123}, |
| {315.332, 101.479}) |
| .CubicCurveTo({315.322, 101.481}, {315.311, 101.482}, |
| {315.301, 101.484}) |
| .LineTo({310.017, 105.94}) |
| .LineTo({309.779, 105.427}) |
| .LineTo({314.403, 101.651}) |
| .CubicCurveTo({314.391, 101.653}, {314.379, 101.656}, |
| {314.368, 101.658}) |
| .CubicCurveTo({312.528, 102.001}, {310.687, 102.366}, |
| {308.846, 102.748}) |
| .CubicCurveTo({307.85, 102.955}, {306.855, 103.182}, {305.859, 103.4}) |
| .CubicCurveTo({305.048, 103.579}, {304.236, 103.75}, |
| {303.425, 103.936}) |
| .LineTo({299.105, 107.578}) |
| .LineTo({298.867, 107.065}) |
| .LineTo({302.394, 104.185}) |
| .LineTo({302.412, 104.171}) |
| .CubicCurveTo({301.388, 104.409}, {300.366, 104.67}, |
| {299.344, 104.921}) |
| .CubicCurveTo({298.618, 105.1}, {297.89, 105.269}, {297.165, 105.455}) |
| .CubicCurveTo({295.262, 105.94}, {293.36, 106.445}, |
| {291.462, 106.979}) |
| .CubicCurveTo({291.132, 107.072}, {290.802, 107.163}, |
| {290.471, 107.257}) |
| .CubicCurveTo({289.463, 107.544}, {288.455, 107.839}, |
| {287.449, 108.139}) |
| .CubicCurveTo({286.476, 108.431}, {285.506, 108.73}, |
| {284.536, 109.035}) |
| .CubicCurveTo({283.674, 109.304}, {282.812, 109.579}, |
| {281.952, 109.859}) |
| .CubicCurveTo({281.177, 110.112}, {280.406, 110.377}, |
| {279.633, 110.638}) |
| .CubicCurveTo({278.458, 111.037}, {277.256, 111.449}, |
| {276.803, 111.607}) |
| .CubicCurveTo({276.76, 111.622}, {276.716, 111.637}, |
| {276.672, 111.653}) |
| .CubicCurveTo({275.017, 112.239}, {273.365, 112.836}, |
| {271.721, 113.463}) |
| .LineTo({271.717, 113.449}) |
| .CubicCurveTo({271.496, 113.496}, {271.238, 113.559}, |
| {270.963, 113.628}) |
| .CubicCurveTo({270.893, 113.645}, {270.822, 113.663}, |
| {270.748, 113.682}) |
| .CubicCurveTo({270.468, 113.755}, {270.169, 113.834}, |
| {269.839, 113.926}) |
| .CubicCurveTo({269.789, 113.94}, {269.732, 113.957}, |
| {269.681, 113.972}) |
| .CubicCurveTo({269.391, 114.053}, {269.081, 114.143}, |
| {268.756, 114.239}) |
| .CubicCurveTo({268.628, 114.276}, {268.5, 114.314}, |
| {268.367, 114.354}) |
| .CubicCurveTo({268.172, 114.412}, {267.959, 114.478}, |
| {267.752, 114.54}) |
| .CubicCurveTo({263.349, 115.964}, {258.058, 117.695}, |
| {253.564, 119.252}) |
| .CubicCurveTo({253.556, 119.255}, {253.547, 119.258}, |
| {253.538, 119.261}) |
| .CubicCurveTo({251.844, 119.849}, {250.056, 120.474}, |
| {248.189, 121.131}) |
| .CubicCurveTo({248, 121.197}, {247.812, 121.264}, {247.621, 121.331}) |
| .CubicCurveTo({247.079, 121.522}, {246.531, 121.715}, |
| {245.975, 121.912}) |
| .CubicCurveTo({245.554, 122.06}, {245.126, 122.212}, |
| {244.698, 122.364}) |
| .CubicCurveTo({244.071, 122.586}, {243.437, 122.811}, |
| {242.794, 123.04}) |
| .CubicCurveTo({242.189, 123.255}, {241.58, 123.472}, |
| {240.961, 123.693}) |
| .CubicCurveTo({240.659, 123.801}, {240.357, 123.909}, |
| {240.052, 124.018}) |
| .CubicCurveTo({239.12, 124.351}, {238.18, 124.687}, {237.22, 125.032}) |
| .LineTo({237.164, 125.003}) |
| .CubicCurveTo({236.709, 125.184}, {236.262, 125.358}, |
| {235.81, 125.538}) |
| .CubicCurveTo({235.413, 125.68}, {234.994, 125.832}, |
| {234.592, 125.977}) |
| .CubicCurveTo({234.592, 125.977}, {234.591, 125.977}, |
| {234.59, 125.977}) |
| .CubicCurveTo({222.206, 130.435}, {207.708, 135.753}, |
| {192.381, 141.429}) |
| .CubicCurveTo({162.77, 151.336}, {122.17, 156.894}, {84.1123, 160}) |
| .LineTo({360, 160}) |
| .LineTo({360, 119.256}) |
| .LineTo({360, 106.332}) |
| .LineTo({360, 96.6307}) |
| .CubicCurveTo({359.978, 96.6317}, {359.956, 96.6326}, |
| {359.934, 96.6335}) |
| .Close() |
| .TakePath(); |
| |
| auto callback = [&](RenderPass& pass) -> bool { |
| ::memset(vertex_buffer_count->OnGetContents(), 0, |
| sizeof(SS::VertexBufferCount)); |
| ::memset(vertex_buffer->OnGetContents(), 0, sizeof(SS::VertexBuffer<2048>)); |
| |
| ContentContext renderer(context, nullptr); |
| if (!renderer.IsValid()) { |
| return false; |
| } |
| |
| ComputeTessellator{} |
| .SetStrokeWidth(stroke_width) |
| .Tessellate( |
| complex_path, renderer.GetTransientsBuffer(), context, |
| DeviceBuffer::AsBufferView(vertex_buffer), |
| DeviceBuffer::AsBufferView(vertex_buffer_count), |
| [vertex_buffer_count, &vertex_count](CommandBuffer::Status status) { |
| vertex_count = reinterpret_cast<SS::VertexBufferCount*>( |
| vertex_buffer_count->OnGetContents()) |
| ->count; |
| }); |
| |
| using VS = SolidFillPipeline::VertexShader; |
| |
| pass.SetCommandLabel("Draw Stroke"); |
| pass.SetStencilReference(0); |
| |
| ContentContextOptions options; |
| options.sample_count = pass.GetRenderTarget().GetSampleCount(); |
| options.color_attachment_pixel_format = |
| pass.GetRenderTarget().GetRenderTargetPixelFormat(); |
| options.has_depth_stencil_attachments = |
| pass.GetRenderTarget().GetStencilAttachment().has_value(); |
| options.blend_mode = BlendMode::kSourceIn; |
| options.primitive_type = PrimitiveType::kTriangleStrip; |
| |
| options.stencil_mode = |
| ContentContextOptions::StencilMode::kLegacyClipIncrement; |
| |
| pass.SetPipeline(renderer.GetSolidFillPipeline(options)); |
| |
| auto count = reinterpret_cast<SS::VertexBufferCount*>( |
| vertex_buffer_count->OnGetContents()) |
| ->count; |
| |
| pass.SetVertexBuffer( |
| VertexBuffer{.vertex_buffer = DeviceBuffer::AsBufferView(vertex_buffer), |
| .vertex_count = count, |
| .index_type = IndexType::kNone}); |
| |
| VS::FrameInfo frame_info; |
| auto world_matrix = Matrix::MakeScale(GetContentScale()); |
| frame_info.mvp = pass.GetOrthographicTransform() * world_matrix; |
| frame_info.color = Color::Red().Premultiply(); |
| VS::BindFrameInfo( |
| pass, renderer.GetTransientsBuffer().EmplaceUniform(frame_info)); |
| |
| return pass.Draw().ok(); |
| }; |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); |
| } |
| |
| TEST_P(ComputeSubgroupTest, QuadAndCubicInOnePath) { |
| using SS = StrokeComputeShader; |
| |
| auto context = GetContext(); |
| ASSERT_TRUE(context); |
| ASSERT_TRUE(context->GetCapabilities()->SupportsComputeSubgroups()); |
| |
| auto vertex_buffer = CreateHostVisibleDeviceBuffer<SS::VertexBuffer<2048>>( |
| context, "VertexBuffer"); |
| auto vertex_buffer_count = |
| CreateHostVisibleDeviceBuffer<SS::VertexBufferCount>(context, |
| "VertexBufferCount"); |
| |
| auto path = PathBuilder{} |
| .AddCubicCurve({140, 20}, {73, 20}, {20, 74}, {20, 140}) |
| .AddQuadraticCurve({20, 140}, {93, 90}, {100, 42}) |
| .TakePath(); |
| |
| auto tessellator = ComputeTessellator{}; |
| |
| fml::AutoResetWaitableEvent latch; |
| |
| auto host_buffer = HostBuffer::Create(context->GetResourceAllocator()); |
| auto status = tessellator.Tessellate( |
| path, *host_buffer, context, DeviceBuffer::AsBufferView(vertex_buffer), |
| DeviceBuffer::AsBufferView(vertex_buffer_count), |
| [&latch](CommandBuffer::Status status) { |
| EXPECT_EQ(status, CommandBuffer::Status::kCompleted); |
| latch.Signal(); |
| }); |
| |
| ASSERT_EQ(status, ComputeTessellator::Status::kOk); |
| |
| auto callback = [&](RenderPass& pass) -> bool { |
| ContentContext renderer(context, nullptr); |
| if (!renderer.IsValid()) { |
| return false; |
| } |
| |
| using VS = SolidFillPipeline::VertexShader; |
| |
| pass.SetCommandLabel("Draw Stroke"); |
| pass.SetStencilReference(0); |
| |
| ContentContextOptions options; |
| options.sample_count = pass.GetRenderTarget().GetSampleCount(); |
| options.color_attachment_pixel_format = |
| pass.GetRenderTarget().GetRenderTargetPixelFormat(); |
| options.has_depth_stencil_attachments = |
| pass.GetRenderTarget().GetStencilAttachment().has_value(); |
| options.blend_mode = BlendMode::kSourceIn; |
| options.primitive_type = PrimitiveType::kTriangleStrip; |
| |
| options.stencil_mode = |
| ContentContextOptions::StencilMode::kLegacyClipIncrement; |
| |
| pass.SetPipeline(renderer.GetSolidFillPipeline(options)); |
| |
| auto count = reinterpret_cast<SS::VertexBufferCount*>( |
| vertex_buffer_count->OnGetContents()) |
| ->count; |
| |
| pass.SetVertexBuffer( |
| VertexBuffer{.vertex_buffer = DeviceBuffer::AsBufferView(vertex_buffer), |
| .vertex_count = count, |
| .index_type = IndexType::kNone}); |
| |
| VS::FrameInfo frame_info; |
| auto world_matrix = Matrix::MakeScale(GetContentScale()); |
| frame_info.mvp = pass.GetOrthographicTransform() * world_matrix; |
| frame_info.color = Color::Red().Premultiply(); |
| VS::BindFrameInfo( |
| pass, renderer.GetTransientsBuffer().EmplaceUniform(frame_info)); |
| |
| return pass.Draw().ok(); |
| }; |
| ASSERT_TRUE(OpenPlaygroundHere(callback)); |
| |
| // The latch is down here because it's expected that on Metal the backend |
| // will take care of synchronizing the buffer between the compute and render |
| // pass usages, since it's not MTLHeap allocated. However, if playgrounds |
| // are disabled, no render pass actually gets submitted and we need to do a |
| // CPU latch here. |
| latch.Wait(); |
| |
| auto vertex_count = reinterpret_cast<SS::VertexBufferCount*>( |
| vertex_buffer_count->OnGetContents()) |
| ->count; |
| EXPECT_EQ(vertex_count, golden_cubic_and_quad_points.size()); |
| auto vertex_buffer_data = |
| reinterpret_cast<SS::VertexBuffer<2048>*>(vertex_buffer->OnGetContents()); |
| for (size_t i = 0; i < vertex_count; i++) { |
| EXPECT_LT(std::abs(golden_cubic_and_quad_points[i].x - |
| vertex_buffer_data->position[i].x), |
| 1e-3); |
| EXPECT_LT(std::abs(golden_cubic_and_quad_points[i].y - |
| vertex_buffer_data->position[i].y), |
| 1e-3); |
| } |
| } |
| |
| } // namespace testing |
| } // namespace impeller |