From 792bfc713d8113dba584afcd62c6d1178f6a2238 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Wed, 6 Mar 2024 10:57:20 -0800 Subject: [PATCH 1/3] [Impeller] Create a new render target with the specified attachment configs when reusing cached render target textures (#51208) If the RenderTargetCache::Create methods reuse a cached RenderTarget as is, then the returned RenderTarget may contain AttachmentConfig parameters that do not match the ones passed to the create method. This PR instead creates a new RenderTarget based on the existing textures from the cached RenderTarget. --- impeller/entity/render_target_cache.cc | 33 +++- impeller/entity/render_target_cache.h | 11 +- .../entity/render_target_cache_unittests.cc | 25 +++ impeller/renderer/render_target.cc | 166 ++++++++++-------- impeller/renderer/render_target.h | 21 +-- 5 files changed, 166 insertions(+), 90 deletions(-) diff --git a/impeller/entity/render_target_cache.cc b/impeller/entity/render_target_cache.cc index 3a9710a93e5e9..4a574758e2422 100644 --- a/impeller/entity/render_target_cache.cc +++ b/impeller/entity/render_target_cache.cc @@ -33,7 +33,11 @@ RenderTarget RenderTargetCache::CreateOffscreen( int mip_count, const std::string& label, RenderTarget::AttachmentConfig color_attachment_config, - std::optional stencil_attachment_config) { + std::optional stencil_attachment_config, + const std::shared_ptr& existing_color_texture, + const std::shared_ptr& existing_depth_stencil_texture) { + FML_DCHECK(existing_color_texture == nullptr && + existing_depth_stencil_texture == nullptr); auto config = RenderTargetConfig{ .size = size, .mip_count = static_cast(mip_count), @@ -44,7 +48,14 @@ RenderTarget RenderTargetCache::CreateOffscreen( const auto other_config = render_target_data.config; if (!render_target_data.used_this_frame && other_config == config) { render_target_data.used_this_frame = true; - return render_target_data.render_target; + auto color0 = render_target_data.render_target.GetColorAttachments() + .find(0u) + ->second; + auto depth = render_target_data.render_target.GetDepthAttachment(); + std::shared_ptr depth_tex = depth ? depth->texture : nullptr; + return RenderTargetAllocator::CreateOffscreen( + context, size, mip_count, label, color_attachment_config, + stencil_attachment_config, color0.texture, depth_tex); } } RenderTarget created_target = RenderTargetAllocator::CreateOffscreen( @@ -66,7 +77,13 @@ RenderTarget RenderTargetCache::CreateOffscreenMSAA( int mip_count, const std::string& label, RenderTarget::AttachmentConfigMSAA color_attachment_config, - std::optional stencil_attachment_config) { + std::optional stencil_attachment_config, + const std::shared_ptr& existing_color_msaa_texture, + const std::shared_ptr& existing_color_resolve_texture, + const std::shared_ptr& existing_depth_stencil_texture) { + FML_DCHECK(existing_color_msaa_texture == nullptr && + existing_color_resolve_texture == nullptr && + existing_depth_stencil_texture == nullptr); auto config = RenderTargetConfig{ .size = size, .mip_count = static_cast(mip_count), @@ -77,7 +94,15 @@ RenderTarget RenderTargetCache::CreateOffscreenMSAA( const auto other_config = render_target_data.config; if (!render_target_data.used_this_frame && other_config == config) { render_target_data.used_this_frame = true; - return render_target_data.render_target; + auto color0 = render_target_data.render_target.GetColorAttachments() + .find(0u) + ->second; + auto depth = render_target_data.render_target.GetDepthAttachment(); + std::shared_ptr depth_tex = depth ? depth->texture : nullptr; + return RenderTargetAllocator::CreateOffscreenMSAA( + context, size, mip_count, label, color_attachment_config, + stencil_attachment_config, color0.texture, color0.resolve_texture, + depth_tex); } } RenderTarget created_target = RenderTargetAllocator::CreateOffscreenMSAA( diff --git a/impeller/entity/render_target_cache.h b/impeller/entity/render_target_cache.h index bc60951ca9a4f..838814e45f0d6 100644 --- a/impeller/entity/render_target_cache.h +++ b/impeller/entity/render_target_cache.h @@ -33,7 +33,10 @@ class RenderTargetCache : public RenderTargetAllocator { RenderTarget::AttachmentConfig color_attachment_config = RenderTarget::kDefaultColorAttachmentConfig, std::optional stencil_attachment_config = - RenderTarget::kDefaultStencilAttachmentConfig) override; + RenderTarget::kDefaultStencilAttachmentConfig, + const std::shared_ptr& existing_color_texture = nullptr, + const std::shared_ptr& existing_depth_stencil_texture = + nullptr) override; RenderTarget CreateOffscreenMSAA( const Context& context, @@ -43,7 +46,11 @@ class RenderTargetCache : public RenderTargetAllocator { RenderTarget::AttachmentConfigMSAA color_attachment_config = RenderTarget::kDefaultColorAttachmentConfigMSAA, std::optional stencil_attachment_config = - RenderTarget::kDefaultStencilAttachmentConfig) override; + RenderTarget::kDefaultStencilAttachmentConfig, + const std::shared_ptr& existing_color_msaa_texture = nullptr, + const std::shared_ptr& existing_color_resolve_texture = nullptr, + const std::shared_ptr& existing_depth_stencil_texture = + nullptr) override; // visible for testing. size_t CachedTextureCount() const; diff --git a/impeller/entity/render_target_cache_unittests.cc b/impeller/entity/render_target_cache_unittests.cc index a3a47dde96ea1..c39d10ae1185a 100644 --- a/impeller/entity/render_target_cache_unittests.cc +++ b/impeller/entity/render_target_cache_unittests.cc @@ -87,5 +87,30 @@ TEST_P(RenderTargetCacheTest, DoesNotPersistFailedAllocations) { EXPECT_EQ(render_target_cache.CachedTextureCount(), 0u); } +TEST_P(RenderTargetCacheTest, CachedTextureGetsNewAttachmentConfig) { + auto render_target_cache = + RenderTargetCache(GetContext()->GetResourceAllocator()); + + render_target_cache.Start(); + RenderTarget::AttachmentConfig color_attachment_config = + RenderTarget::kDefaultColorAttachmentConfig; + RenderTarget target1 = render_target_cache.CreateOffscreen( + *GetContext(), {100, 100}, 1, "Offscreen1", color_attachment_config); + render_target_cache.End(); + + render_target_cache.Start(); + color_attachment_config.clear_color = Color::Red(); + RenderTarget target2 = render_target_cache.CreateOffscreen( + *GetContext(), {100, 100}, 1, "Offscreen2", color_attachment_config); + render_target_cache.End(); + + auto color1 = target1.GetColorAttachments().find(0)->second; + auto color2 = target2.GetColorAttachments().find(0)->second; + // The second color attachment should reuse the first attachment's texture + // but with attributes from the second AttachmentConfig. + EXPECT_EQ(color2.texture, color1.texture); + EXPECT_EQ(color2.clear_color, Color::Red()); +} + } // namespace testing } // namespace impeller diff --git a/impeller/renderer/render_target.cc b/impeller/renderer/render_target.cc index 9cab7db94db69..cb45342d1fadc 100644 --- a/impeller/renderer/render_target.cc +++ b/impeller/renderer/render_target.cc @@ -261,37 +261,46 @@ RenderTarget RenderTargetAllocator::CreateOffscreen( int mip_count, const std::string& label, RenderTarget::AttachmentConfig color_attachment_config, - std::optional stencil_attachment_config) { + std::optional stencil_attachment_config, + const std::shared_ptr& existing_color_texture, + const std::shared_ptr& existing_depth_stencil_texture) { if (size.IsEmpty()) { return {}; } RenderTarget target; - PixelFormat pixel_format = context.GetCapabilities()->GetDefaultColorFormat(); - TextureDescriptor color_tex0; - color_tex0.storage_mode = color_attachment_config.storage_mode; - color_tex0.format = pixel_format; - color_tex0.size = size; - color_tex0.mip_count = mip_count; - color_tex0.usage = static_cast(TextureUsage::kRenderTarget) | - static_cast(TextureUsage::kShaderRead); + + std::shared_ptr color0_tex; + if (existing_color_texture) { + color0_tex = existing_color_texture; + } else { + PixelFormat pixel_format = + context.GetCapabilities()->GetDefaultColorFormat(); + TextureDescriptor color0_tex_desc; + color0_tex_desc.storage_mode = color_attachment_config.storage_mode; + color0_tex_desc.format = pixel_format; + color0_tex_desc.size = size; + color0_tex_desc.mip_count = mip_count; + color0_tex_desc.usage = static_cast(TextureUsage::kRenderTarget) | + static_cast(TextureUsage::kShaderRead); + color0_tex = allocator_->CreateTexture(color0_tex_desc); + if (!color0_tex) { + return {}; + } + } + color0_tex->SetLabel(SPrintF("%s Color Texture", label.c_str())); ColorAttachment color0; color0.clear_color = color_attachment_config.clear_color; color0.load_action = color_attachment_config.load_action; color0.store_action = color_attachment_config.store_action; - color0.texture = allocator_->CreateTexture(color_tex0); - - if (!color0.texture) { - return {}; - } - color0.texture->SetLabel(SPrintF("%s Color Texture", label.c_str())); + color0.texture = color0_tex; target.SetColorAttachment(color0, 0u); if (stencil_attachment_config.has_value()) { - target.SetupDepthStencilAttachments(context, *allocator_, size, false, - label, - stencil_attachment_config.value()); + target.SetupDepthStencilAttachments( + context, *allocator_, size, false, label, + stencil_attachment_config.value(), existing_depth_stencil_texture); } else { target.SetStencilAttachment(std::nullopt); target.SetDepthAttachment(std::nullopt); @@ -306,7 +315,10 @@ RenderTarget RenderTargetAllocator::CreateOffscreenMSAA( int mip_count, const std::string& label, RenderTarget::AttachmentConfigMSAA color_attachment_config, - std::optional stencil_attachment_config) { + std::optional stencil_attachment_config, + const std::shared_ptr& existing_color_msaa_texture, + const std::shared_ptr& existing_color_resolve_texture, + const std::shared_ptr& existing_depth_stencil_texture) { if (size.IsEmpty()) { return {}; } @@ -315,45 +327,50 @@ RenderTarget RenderTargetAllocator::CreateOffscreenMSAA( PixelFormat pixel_format = context.GetCapabilities()->GetDefaultColorFormat(); // Create MSAA color texture. - - TextureDescriptor color0_tex_desc; - color0_tex_desc.storage_mode = color_attachment_config.storage_mode; - color0_tex_desc.type = TextureType::kTexture2DMultisample; - color0_tex_desc.sample_count = SampleCount::kCount4; - color0_tex_desc.format = pixel_format; - color0_tex_desc.size = size; - color0_tex_desc.usage = static_cast(TextureUsage::kRenderTarget); - - if (context.GetCapabilities()->SupportsImplicitResolvingMSAA()) { - // See below ("SupportsImplicitResolvingMSAA") for more details. - color0_tex_desc.storage_mode = StorageMode::kDevicePrivate; - } - - auto color0_msaa_tex = allocator_->CreateTexture(color0_tex_desc); - if (!color0_msaa_tex) { - VALIDATION_LOG << "Could not create multisample color texture."; - return {}; + std::shared_ptr color0_msaa_tex; + if (existing_color_msaa_texture) { + color0_msaa_tex = existing_color_msaa_texture; + } else { + TextureDescriptor color0_tex_desc; + color0_tex_desc.storage_mode = color_attachment_config.storage_mode; + color0_tex_desc.type = TextureType::kTexture2DMultisample; + color0_tex_desc.sample_count = SampleCount::kCount4; + color0_tex_desc.format = pixel_format; + color0_tex_desc.size = size; + color0_tex_desc.usage = static_cast(TextureUsage::kRenderTarget); + if (context.GetCapabilities()->SupportsImplicitResolvingMSAA()) { + // See below ("SupportsImplicitResolvingMSAA") for more details. + color0_tex_desc.storage_mode = StorageMode::kDevicePrivate; + } + color0_msaa_tex = allocator_->CreateTexture(color0_tex_desc); + if (!color0_msaa_tex) { + VALIDATION_LOG << "Could not create multisample color texture."; + return {}; + } } color0_msaa_tex->SetLabel( SPrintF("%s Color Texture (Multisample)", label.c_str())); // Create color resolve texture. - - TextureDescriptor color0_resolve_tex_desc; - color0_resolve_tex_desc.storage_mode = - color_attachment_config.resolve_storage_mode; - color0_resolve_tex_desc.format = pixel_format; - color0_resolve_tex_desc.size = size; - color0_resolve_tex_desc.compression_type = CompressionType::kLossy; - color0_resolve_tex_desc.usage = - static_cast(TextureUsage::kRenderTarget) | - static_cast(TextureUsage::kShaderRead); - color0_resolve_tex_desc.mip_count = mip_count; - - auto color0_resolve_tex = allocator_->CreateTexture(color0_resolve_tex_desc); - if (!color0_resolve_tex) { - VALIDATION_LOG << "Could not create color texture."; - return {}; + std::shared_ptr color0_resolve_tex; + if (existing_color_resolve_texture) { + color0_resolve_tex = existing_color_resolve_texture; + } else { + TextureDescriptor color0_resolve_tex_desc; + color0_resolve_tex_desc.storage_mode = + color_attachment_config.resolve_storage_mode; + color0_resolve_tex_desc.format = pixel_format; + color0_resolve_tex_desc.size = size; + color0_resolve_tex_desc.compression_type = CompressionType::kLossy; + color0_resolve_tex_desc.usage = + static_cast(TextureUsage::kRenderTarget) | + static_cast(TextureUsage::kShaderRead); + color0_resolve_tex_desc.mip_count = mip_count; + color0_resolve_tex = allocator_->CreateTexture(color0_resolve_tex_desc); + if (!color0_resolve_tex) { + VALIDATION_LOG << "Could not create color texture."; + return {}; + } } color0_resolve_tex->SetLabel(SPrintF("%s Color Texture", label.c_str())); @@ -383,7 +400,8 @@ RenderTarget RenderTargetAllocator::CreateOffscreenMSAA( if (stencil_attachment_config.has_value()) { target.SetupDepthStencilAttachments(context, *allocator_, size, true, label, - stencil_attachment_config.value()); + stencil_attachment_config.value(), + existing_depth_stencil_texture); } else { target.SetDepthAttachment(std::nullopt); target.SetStencilAttachment(std::nullopt); @@ -398,24 +416,28 @@ void RenderTarget::SetupDepthStencilAttachments( ISize size, bool msaa, const std::string& label, - RenderTarget::AttachmentConfig stencil_attachment_config) { - TextureDescriptor depth_stencil_texture_desc; - depth_stencil_texture_desc.storage_mode = - stencil_attachment_config.storage_mode; - if (msaa) { - depth_stencil_texture_desc.type = TextureType::kTexture2DMultisample; - depth_stencil_texture_desc.sample_count = SampleCount::kCount4; - } - depth_stencil_texture_desc.format = - context.GetCapabilities()->GetDefaultDepthStencilFormat(); - depth_stencil_texture_desc.size = size; - depth_stencil_texture_desc.usage = - static_cast(TextureUsage::kRenderTarget); - - auto depth_stencil_texture = - allocator.CreateTexture(depth_stencil_texture_desc); - if (!depth_stencil_texture) { - return; // Error messages are handled by `Allocator::CreateTexture`. + RenderTarget::AttachmentConfig stencil_attachment_config, + const std::shared_ptr& existing_depth_stencil_texture) { + std::shared_ptr depth_stencil_texture; + if (existing_depth_stencil_texture) { + depth_stencil_texture = existing_depth_stencil_texture; + } else { + TextureDescriptor depth_stencil_texture_desc; + depth_stencil_texture_desc.storage_mode = + stencil_attachment_config.storage_mode; + if (msaa) { + depth_stencil_texture_desc.type = TextureType::kTexture2DMultisample; + depth_stencil_texture_desc.sample_count = SampleCount::kCount4; + } + depth_stencil_texture_desc.format = + context.GetCapabilities()->GetDefaultDepthStencilFormat(); + depth_stencil_texture_desc.size = size; + depth_stencil_texture_desc.usage = + static_cast(TextureUsage::kRenderTarget); + depth_stencil_texture = allocator.CreateTexture(depth_stencil_texture_desc); + if (!depth_stencil_texture) { + return; // Error messages are handled by `Allocator::CreateTexture`. + } } DepthAttachment depth0; diff --git a/impeller/renderer/render_target.h b/impeller/renderer/render_target.h index 0bfce5137c8fa..6d635276ec1fa 100644 --- a/impeller/renderer/render_target.h +++ b/impeller/renderer/render_target.h @@ -84,7 +84,8 @@ class RenderTarget final { bool msaa, const std::string& label = "Offscreen", RenderTarget::AttachmentConfig stencil_attachment_config = - RenderTarget::kDefaultStencilAttachmentConfig); + RenderTarget::kDefaultStencilAttachmentConfig, + const std::shared_ptr& depth_stencil_texture = nullptr); SampleCount GetSampleCount() const; @@ -152,7 +153,9 @@ class RenderTargetAllocator { RenderTarget::AttachmentConfig color_attachment_config = RenderTarget::kDefaultColorAttachmentConfig, std::optional stencil_attachment_config = - RenderTarget::kDefaultStencilAttachmentConfig); + RenderTarget::kDefaultStencilAttachmentConfig, + const std::shared_ptr& existing_color_texture = nullptr, + const std::shared_ptr& existing_depth_stencil_texture = nullptr); virtual RenderTarget CreateOffscreenMSAA( const Context& context, @@ -162,7 +165,10 @@ class RenderTargetAllocator { RenderTarget::AttachmentConfigMSAA color_attachment_config = RenderTarget::kDefaultColorAttachmentConfigMSAA, std::optional stencil_attachment_config = - RenderTarget::kDefaultStencilAttachmentConfig); + RenderTarget::kDefaultStencilAttachmentConfig, + const std::shared_ptr& existing_color_msaa_texture = nullptr, + const std::shared_ptr& existing_color_resolve_texture = nullptr, + const std::shared_ptr& existing_depth_stencil_texture = nullptr); /// @brief Mark the beginning of a frame workload. /// @@ -176,15 +182,6 @@ class RenderTargetAllocator { virtual void End(); private: - void SetupDepthStencilAttachments( - Allocator& allocator, - const Context& context, - ISize size, - bool msaa, - const std::string& label = "Offscreen", - RenderTarget::AttachmentConfig stencil_attachment_config = - RenderTarget::kDefaultStencilAttachmentConfig); - std::shared_ptr allocator_; }; From 5945dad2b5529df00869b39d7907f367937beb6d Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Wed, 6 Mar 2024 10:58:46 -0800 Subject: [PATCH 2/3] [Impeller] Apply padding for alignment when doing HostBuffer::Emplace with a callback (#51221) --- impeller/core/host_buffer.cc | 17 +++++++++++------ .../entity/contents/host_buffer_unittests.cc | 10 ++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/impeller/core/host_buffer.cc b/impeller/core/host_buffer.cc index 0752818256076..67d3ca260af80 100644 --- a/impeller/core/host_buffer.cc +++ b/impeller/core/host_buffer.cc @@ -110,19 +110,24 @@ std::tuple> HostBuffer::EmplaceInternal( return std::make_tuple(Range{0, length}, device_buffer); } - auto old_length = GetLength(); - if (old_length + length > kAllocatorBlockSize) { + size_t padding = 0; + if (align > 0 && offset_ % align) { + padding = align - (offset_ % align); + } + if (offset_ + padding + length > kAllocatorBlockSize) { MaybeCreateNewBuffer(); + } else { + offset_ += padding; } - old_length = GetLength(); auto current_buffer = GetCurrentBuffer(); auto contents = current_buffer->OnGetContents(); - cb(contents + old_length); - current_buffer->Flush(Range{old_length, length}); + cb(contents + offset_); + Range output_range(offset_, length); + current_buffer->Flush(output_range); offset_ += length; - return std::make_tuple(Range{old_length, length}, std::move(current_buffer)); + return std::make_tuple(output_range, std::move(current_buffer)); } std::tuple> HostBuffer::EmplaceInternal( diff --git a/impeller/entity/contents/host_buffer_unittests.cc b/impeller/entity/contents/host_buffer_unittests.cc index 3ae3c285e07c0..28ac62dccea72 100644 --- a/impeller/entity/contents/host_buffer_unittests.cc +++ b/impeller/entity/contents/host_buffer_unittests.cc @@ -146,5 +146,15 @@ TEST_P(HostBufferTest, UnusedBuffersAreDiscardedWhenResetting) { EXPECT_EQ(buffer->GetStateForTest().current_frame, 0u); } +TEST_P(HostBufferTest, EmplaceWithProcIsAligned) { + auto buffer = HostBuffer::Create(GetContext()->GetResourceAllocator()); + + BufferView view = buffer->Emplace(std::array()); + EXPECT_EQ(view.range, Range(0, 21)); + + view = buffer->Emplace(64, 16, [](uint8_t*) {}); + EXPECT_EQ(view.range, Range(32, 64)); +} + } // namespace testing } // namespace impeller From 44405aedba13da57396f75261885a689eece3a33 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Wed, 6 Mar 2024 11:17:57 -0800 Subject: [PATCH 3/3] [Impeller] Append fewer redundant points when bridging triangle strips between contours. (#51232) Optimizes out 1-2 points per contour. Improvement to the TessellateConvex changes I made to get StC over the line. --- impeller/tessellator/tessellator.cc | 20 ++++++++++++++----- impeller/tessellator/tessellator_unittests.cc | 10 ++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/impeller/tessellator/tessellator.cc b/impeller/tessellator/tessellator.cc index 40a1a542ab761..8f5edc61374d5 100644 --- a/impeller/tessellator/tessellator.cc +++ b/impeller/tessellator/tessellator.cc @@ -189,6 +189,7 @@ std::vector Tessellator::TessellateConvex(const Path& path, output.reserve(polyline.points->size() + (4 * (polyline.contours.size() - 1))); + bool previous_contour_odd_points = false; for (auto j = 0u; j < polyline.contours.size(); j++) { auto [start, end] = polyline.GetContourPointBounds(j); auto first_point = polyline.GetPoint(start); @@ -205,22 +206,31 @@ std::vector Tessellator::TessellateConvex(const Path& path, output.emplace_back(output.back()); output.emplace_back(first_point); output.emplace_back(first_point); - output.emplace_back(first_point); + + // If the contour has an odd number of points, insert an extra point when + // bridging to the next contour to preserve the correct triangle winding + // order. + if (previous_contour_odd_points) { + output.emplace_back(first_point); + } } else { output.emplace_back(first_point); } size_t a = start + 1; size_t b = end - 1; - while (a <= b) { - // If the contour has an odd number of points, two identical points will - // be appended when a == b. This ensures the triangle winding order will - // remain the same after bridging to the next contour. + while (a < b) { output.emplace_back(polyline.GetPoint(a)); output.emplace_back(polyline.GetPoint(b)); a++; b--; } + if (a == b) { + previous_contour_odd_points = false; + output.emplace_back(polyline.GetPoint(a)); + } else { + previous_contour_odd_points = true; + } } return output; } diff --git a/impeller/tessellator/tessellator_unittests.cc b/impeller/tessellator/tessellator_unittests.cc index d01a3ba33b2c3..772a794b85f87 100644 --- a/impeller/tessellator/tessellator_unittests.cc +++ b/impeller/tessellator/tessellator_unittests.cc @@ -111,9 +111,7 @@ TEST(TessellatorTest, TessellateConvex) { auto pts = t.TessellateConvex( PathBuilder{}.AddRect(Rect::MakeLTRB(0, 0, 10, 10)).TakePath(), 1.0); - std::vector expected = { - {0, 0}, {10, 0}, {0, 10}, {10, 10}, {10, 10}, // - }; + std::vector expected = {{0, 0}, {10, 0}, {0, 10}, {10, 10}}; EXPECT_EQ(pts, expected); } @@ -125,9 +123,9 @@ TEST(TessellatorTest, TessellateConvex) { .TakePath(), 1.0); - std::vector expected = { - {0, 0}, {10, 0}, {0, 10}, {10, 10}, {10, 10}, {10, 10}, {20, 20}, - {20, 20}, {20, 20}, {30, 20}, {20, 30}, {30, 30}, {30, 30}}; + std::vector expected = {{0, 0}, {10, 0}, {0, 10}, {10, 10}, + {10, 10}, {20, 20}, {20, 20}, {30, 20}, + {20, 30}, {30, 30}}; EXPECT_EQ(pts, expected); } }