From b897fb76a0bf7be578d236af3c5764ccdebaf2a3 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:04:17 -0700 Subject: [PATCH] Refactored Canvas to disallow null inline contexts. (#174530) One of the possible explanations for the crash in https://github.com/flutter/flutter/issues/168210#issuecomment-3130088036 is a null inline pass context. This makes that statically impossible (barring an allocation problem). tests: exempt, just a refactor ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter/impeller/display_list/canvas.cc | 93 +++++++++++-------- .../flutter/impeller/display_list/canvas.h | 28 +++--- 2 files changed, 68 insertions(+), 53 deletions(-) diff --git a/engine/src/flutter/impeller/display_list/canvas.cc b/engine/src/flutter/impeller/display_list/canvas.cc index 2fb492d6cac..43cd1ec9796 100644 --- a/engine/src/flutter/impeller/display_list/canvas.cc +++ b/engine/src/flutter/impeller/display_list/canvas.cc @@ -871,9 +871,10 @@ void Canvas::ClipGeometry(const Geometry& geometry, if (clip_state_result.clip_did_change) { // We only need to update the pass scissor if the clip state has changed. - SetClipScissor(clip_coverage_stack_.CurrentClipCoverage(), - *render_passes_.back().inline_pass_context->GetRenderPass(), - GetGlobalPassPosition()); + SetClipScissor( + clip_coverage_stack_.CurrentClipCoverage(), + *render_passes_.back().GetInlinePassContext()->GetRenderPass(), + GetGlobalPassPosition()); } ++transform_stack_.back().clip_height; @@ -892,16 +893,16 @@ void Canvas::ClipGeometry(const Geometry& geometry, entity.SetClipDepth(clip_depth); GeometryResult geometry_result = geometry.GetPositionBuffer( - renderer_, // - entity, // - *render_passes_.back().inline_pass_context->GetRenderPass() // + renderer_, // + entity, // + *render_passes_.back().GetInlinePassContext()->GetRenderPass() // ); clip_contents.SetGeometry(geometry_result); clip_coverage_stack_.GetLastReplayResult().clip_contents.SetGeometry( geometry_result); clip_contents.Render( - renderer_, *render_passes_.back().inline_pass_context->GetRenderPass(), + renderer_, *render_passes_.back().GetInlinePassContext()->GetRenderPass(), clip_depth); } @@ -1217,9 +1218,9 @@ std::optional Canvas::GetLocalCoverageLimit() const { FML_CHECK(!render_passes_.empty()); const LazyRenderingConfig& back_render_pass = render_passes_.back(); std::shared_ptr back_texture = - back_render_pass.inline_pass_context->GetTexture(); + back_render_pass.GetInlinePassContext()->GetTexture(); FML_CHECK(back_texture) << "Context is valid:" - << back_render_pass.inline_pass_context->IsValid(); + << back_render_pass.GetInlinePassContext()->IsValid(); // The maximum coverage of the subpass. Subpasses textures should never // extend outside the parent pass texture or the current clip coverage. @@ -1509,7 +1510,7 @@ bool Canvas::Restore() { auto lazy_render_pass = std::move(render_passes_.back()); render_passes_.pop_back(); // Force the render pass to be constructed if it never was. - lazy_render_pass.inline_pass_context->GetRenderPass(); + lazy_render_pass.GetInlinePassContext()->GetRenderPass(); SaveLayerState save_layer_state = save_layer_state_.back(); save_layer_state_.pop_back(); @@ -1517,12 +1518,12 @@ bool Canvas::Restore() { std::shared_ptr contents = CreateContentsForSubpassTarget( save_layer_state.paint, // - lazy_render_pass.inline_pass_context->GetTexture(), // + lazy_render_pass.GetInlinePassContext()->GetTexture(), // Matrix::MakeTranslation(Vector3{-global_pass_position}) * // transform_stack_.back().transform // ); - lazy_render_pass.inline_pass_context->EndPass(); + lazy_render_pass.GetInlinePassContext()->EndPass(); // Round the subpass texture position for pixel alignment with the parent // pass render target. By default, we draw subpass textures with nearest @@ -1581,8 +1582,8 @@ bool Canvas::Restore() { } element_entity.Render( - renderer_, // - *render_passes_.back().inline_pass_context->GetRenderPass() // + renderer_, // + *render_passes_.back().GetInlinePassContext()->GetRenderPass() // ); clip_coverage_stack_.PopSubpass(); transform_stack_.pop_back(); @@ -1606,9 +1607,9 @@ bool Canvas::Restore() { if (clip_state_result.clip_did_change) { // We only need to update the pass scissor if the clip state has changed. SetClipScissor( - clip_coverage_stack_.CurrentClipCoverage(), // - *render_passes_.back().inline_pass_context->GetRenderPass(), // - GetGlobalPassPosition() // + clip_coverage_stack_.CurrentClipCoverage(), // + *render_passes_.back().GetInlinePassContext()->GetRenderPass(), // + GetGlobalPassPosition() // ); } } @@ -1805,11 +1806,12 @@ void Canvas::AddRenderEntityToCurrentPass(Entity& entity, bool reuse_depth) { // with the current backdrop. if (render_passes_.back().IsApplyingClearColor()) { std::optional maybe_color = entity.AsBackgroundColor( - render_passes_.back().inline_pass_context->GetTexture()->GetSize()); + render_passes_.back().GetInlinePassContext()->GetTexture()->GetSize()); if (maybe_color.has_value()) { Color color = maybe_color.value(); RenderTarget& render_target = render_passes_.back() - .inline_pass_context->GetPassTarget() + .GetInlinePassContext() + ->GetPassTarget() .GetRenderTarget(); ColorAttachment attachment = render_target.GetColorAttachment(0); // Attachment.clear color needs to be premultiplied at all times, but the @@ -1872,7 +1874,7 @@ void Canvas::AddRenderEntityToCurrentPass(Entity& entity, bool reuse_depth) { } const std::shared_ptr& result = - render_passes_.back().inline_pass_context->GetRenderPass(); + render_passes_.back().GetInlinePassContext()->GetRenderPass(); if (!result) { // Failure to produce a render pass should be explained by specific errors // in `InlinePassContext::GetRenderPass()`, so avoid log spam and don't @@ -1884,7 +1886,7 @@ void Canvas::AddRenderEntityToCurrentPass(Entity& entity, bool reuse_depth) { } RenderPass& Canvas::GetCurrentRenderPass() const { - return *render_passes_.back().inline_pass_context->GetRenderPass(); + return *render_passes_.back().GetInlinePassContext()->GetRenderPass(); } void Canvas::SetBackdropData( @@ -1912,8 +1914,8 @@ std::shared_ptr Canvas::FlipBackdrop(Point global_pass_position, // In cases where there are no contents, we // could instead check the clear color and initialize a 1x2 CPU texture // instead of ending the pass. - rendering_config.inline_pass_context->GetRenderPass(); - if (!rendering_config.inline_pass_context->EndPass()) { + rendering_config.GetInlinePassContext()->GetRenderPass(); + if (!rendering_config.GetInlinePassContext()->EndPass()) { VALIDATION_LOG << "Failed to end the current render pass in order to read from " "the backdrop texture and apply an advanced blend or backdrop " @@ -1921,23 +1923,19 @@ std::shared_ptr Canvas::FlipBackdrop(Point global_pass_position, // Note: adding this render pass ensures there are no later crashes from // unbalanced save layers. Ideally, this method would return false and the // renderer could handle that by terminating dispatch. - render_passes_.push_back(LazyRenderingConfig( - renderer_, std::move(rendering_config.entity_pass_target), - std::move(rendering_config.inline_pass_context))); + render_passes_.emplace_back(std::move(rendering_config)); return nullptr; } const std::shared_ptr& input_texture = - rendering_config.inline_pass_context->GetTexture(); + rendering_config.GetInlinePassContext()->GetTexture(); if (!input_texture) { VALIDATION_LOG << "Failed to fetch the color texture in order to " "apply an advanced blend or backdrop filter."; // Note: see above. - render_passes_.push_back(LazyRenderingConfig( - renderer_, std::move(rendering_config.entity_pass_target), - std::move(rendering_config.inline_pass_context))); + render_passes_.emplace_back(std::move(rendering_config)); return nullptr; } @@ -1960,17 +1958,15 @@ std::shared_ptr Canvas::FlipBackdrop(Point global_pass_position, LazyRenderingConfig(renderer_, std::move(entity_pass_target))); requires_readback_ = false; } else { - render_passes_.push_back(LazyRenderingConfig( - renderer_, std::move(rendering_config.entity_pass_target), - std::move(rendering_config.inline_pass_context))); + render_passes_.emplace_back(std::move(rendering_config)); // If the current texture is being cached for a BDF we need to ensure we // don't recycle it during recording; remove it from the entity pass target. if (should_remove_texture) { - render_passes_.back().entity_pass_target->RemoveSecondary(); + render_passes_.back().GetEntityPassTarget()->RemoveSecondary(); } } RenderPass& current_render_pass = - *render_passes_.back().inline_pass_context->GetRenderPass(); + *render_passes_.back().GetInlinePassContext()->GetRenderPass(); // Eagerly restore the BDF contents. @@ -2028,7 +2024,8 @@ bool Canvas::BlitToOnscreen(bool is_onscreen) { auto command_buffer = renderer_.GetContext()->CreateCommandBuffer(); command_buffer->SetLabel("EntityPass Root Command Buffer"); auto offscreen_target = render_passes_.back() - .inline_pass_context->GetPassTarget() + .GetInlinePassContext() + ->GetPassTarget() .GetRenderTarget(); if (SupportsBlitToOnscreen()) { auto blit_pass = command_buffer->CreateBlitPass(); @@ -2093,8 +2090,8 @@ bool Canvas::EnsureFinalMipmapGeneration() const { void Canvas::EndReplay() { FML_DCHECK(render_passes_.size() == 1u); - render_passes_.back().inline_pass_context->GetRenderPass(); - render_passes_.back().inline_pass_context->EndPass( + render_passes_.back().GetInlinePassContext()->GetRenderPass(); + render_passes_.back().GetInlinePassContext()->EndPass( /*is_onscreen=*/!requires_readback_ && is_onscreen_); backdrop_data_.clear(); @@ -2119,4 +2116,24 @@ void Canvas::EndReplay() { Initialize(initial_cull_rect_); } +LazyRenderingConfig::LazyRenderingConfig( + ContentContext& renderer, + std::unique_ptr p_entity_pass_target) + : entity_pass_target_(std::move(p_entity_pass_target)) { + inline_pass_context_ = + std::make_unique(renderer, *entity_pass_target_); +} + +bool LazyRenderingConfig::IsApplyingClearColor() const { + return !inline_pass_context_->IsActive(); +} + +EntityPassTarget* LazyRenderingConfig::GetEntityPassTarget() const { + return entity_pass_target_.get(); +} + +InlinePassContext* LazyRenderingConfig::GetInlinePassContext() const { + return inline_pass_context_.get(); +} + } // namespace impeller diff --git a/engine/src/flutter/impeller/display_list/canvas.h b/engine/src/flutter/impeller/display_list/canvas.h index 8fd8b51b28c..c7e2fb6f95f 100644 --- a/engine/src/flutter/impeller/display_list/canvas.h +++ b/engine/src/flutter/impeller/display_list/canvas.h @@ -96,25 +96,23 @@ enum class ContentBoundsPromise { kMayClipContents, }; -struct LazyRenderingConfig { - std::unique_ptr entity_pass_target; - std::unique_ptr inline_pass_context; +class LazyRenderingConfig { + public: + LazyRenderingConfig(ContentContext& renderer, + std::unique_ptr p_entity_pass_target); + + LazyRenderingConfig(LazyRenderingConfig&&) = default; /// Whether or not the clear color texture can still be updated. - bool IsApplyingClearColor() const { return !inline_pass_context->IsActive(); } + bool IsApplyingClearColor() const; - LazyRenderingConfig(ContentContext& renderer, - std::unique_ptr p_entity_pass_target) - : entity_pass_target(std::move(p_entity_pass_target)) { - inline_pass_context = - std::make_unique(renderer, *entity_pass_target); - } + EntityPassTarget* GetEntityPassTarget() const; - LazyRenderingConfig(ContentContext& renderer, - std::unique_ptr entity_pass_target, - std::unique_ptr inline_pass_context) - : entity_pass_target(std::move(entity_pass_target)), - inline_pass_context(std::move(inline_pass_context)) {} + InlinePassContext* GetInlinePassContext() const; + + private: + std::unique_ptr entity_pass_target_; + std::unique_ptr inline_pass_context_; }; class Canvas {