Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/licenses_golden/excluded_files
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
../../../flutter/impeller/display_list/skia_conversions_unittests.cc
../../../flutter/impeller/docs
../../../flutter/impeller/entity/contents/checkerboard_contents_unittests.cc
../../../flutter/impeller/entity/contents/content_context_unittests.cc
../../../flutter/impeller/entity/contents/filters/directional_gaussian_blur_filter_contents_unittests.cc
../../../flutter/impeller/entity/contents/filters/gaussian_blur_filter_contents_unittests.cc
../../../flutter/impeller/entity/contents/filters/inputs/filter_input_unittests.cc
Expand Down
1 change: 1 addition & 0 deletions impeller/entity/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ impeller_component("entity_unittests") {

sources = [
"contents/checkerboard_contents_unittests.cc",
"contents/content_context_unittests.cc",
"contents/filters/directional_gaussian_blur_filter_contents_unittests.cc",
"contents/filters/gaussian_blur_filter_contents_unittests.cc",
"contents/filters/inputs/filter_input_unittests.cc",
Expand Down
12 changes: 12 additions & 0 deletions impeller/entity/contents/content_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,16 @@ ContentContext::GetCachedRuntimeEffectPipeline(
return it->second;
}

void ContentContext::ClearCachedRuntimeEffectPipeline(
const std::string& unique_entrypoint_name) const {
for (auto it = runtime_effect_pipelines_.begin();
it != runtime_effect_pipelines_.end();) {
if (it->first.unique_entrypoint_name == unique_entrypoint_name) {
it = runtime_effect_pipelines_.erase(it);
} else {
it++;
}
}
}

} // namespace impeller
5 changes: 5 additions & 0 deletions impeller/entity/contents/content_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,11 @@ class ContentContext {
const std::function<std::shared_ptr<Pipeline<PipelineDescriptor>>()>&
create_callback) const;

/// Used by hot reload/hot restart to clear a cached pipeline from
/// GetCachedRuntimeEffectPipeline.
void ClearCachedRuntimeEffectPipeline(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its possible that from frame to frame you will get different values for options, which means you may not invalidate all of the pipeline descriptors associated with this shader consider this scenario:

Frame 1: Use and cache shader with Rect geometry, triangle geometry is used.
Frame 2: Hot reload, Use shader with tessellated geometry, triangle strip is used. Pipeline descripor matching shader name + triangle strip is removed.
Frame 3: Hot reload, use shader with rect geometry, old pipeline descriptor from frame 1 is recycled.

You'd need to clear out all possible pipelines for a given shader variant. Since this is a debug only feature, you could iterate the cache and evict everything that has a name match. Or switch to a two level cache.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shader comes in as dirty initially. That would mean evicting everything with the same name every time we got a new geometry, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its a one to many mapping of shaders to pipelines. So if the shader is dirty, then all pipelines (regardless of the ContentContextOptions must be marked dirty). You'd probably need to special case the first time you see a shader?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we really are worried about this case I think we need to just plain have a service extension that kills the cache. It's just hard to get that right with rendering so that the cache gets killed and some frame isn't in flight that puts the shader back into the cache before the new shader version comes through.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace frame numbers with two kinds of draws in the same frame. If you don't invalidate all pipeline descriptors marching the name, then only the first will get updated when you got reload

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you're right, I have a sample program now that uses different blend mode.

Invalidating all the ones with the same name ... seems to work. Let's just do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've updated the evict method to just evict by name rather than including the options, and written some tests for it. This works locally for hot reload with a simple app where the blend modes are different on the same shader.

const std::string& unique_entrypoint_name) const;

/// @brief Retrieve the currnent host buffer for transient storage.
///
/// This is only safe to use from the raster threads. Other threads should
Expand Down
139 changes: 139 additions & 0 deletions impeller/entity/contents/content_context_unittests.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// 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 <cstdint>

#include "gtest/gtest.h"

#include "impeller/core/allocator.h"
#include "impeller/core/device_buffer_descriptor.h"
#include "impeller/entity/contents/content_context.h"
#include "impeller/geometry/color.h"
#include "impeller/renderer/capabilities.h"
#include "impeller/renderer/pipeline.h"
#include "impeller/renderer/pipeline_descriptor.h"

namespace impeller {
namespace testing {

class FakeAllocator : public Allocator {
public:
FakeAllocator() : Allocator() {}

uint16_t MinimumBytesPerRow(PixelFormat format) const override { return 0; }
ISize GetMaxTextureSizeSupported() const override { return ISize(); }

std::shared_ptr<DeviceBuffer> OnCreateBuffer(
const DeviceBufferDescriptor& desc) override {
return nullptr;
}
std::shared_ptr<Texture> OnCreateTexture(
const TextureDescriptor& desc) override {
return nullptr;
}
};

class FakeContext : public Context {
public:
FakeContext() : Context(), allocator_(std::make_shared<FakeAllocator>()) {}

BackendType GetBackendType() const override { return BackendType::kVulkan; }
std::string DescribeGpuModel() const override { return ""; }
bool IsValid() const override { return false; }
const std::shared_ptr<const Capabilities>& GetCapabilities() const override {
return capabilities_;
}
std::shared_ptr<Allocator> GetResourceAllocator() const override {
return allocator_;
}
std::shared_ptr<ShaderLibrary> GetShaderLibrary() const { return nullptr; }
std::shared_ptr<SamplerLibrary> GetSamplerLibrary() const { return nullptr; }
std::shared_ptr<PipelineLibrary> GetPipelineLibrary() const {
return nullptr;
}
std::shared_ptr<CommandBuffer> CreateCommandBuffer() const { return nullptr; }
void Shutdown() {}

private:
std::shared_ptr<Allocator> allocator_;
std::shared_ptr<const Capabilities> capabilities_;
};

class FakePipeline : public Pipeline<PipelineDescriptor> {
public:
FakePipeline() : Pipeline({}, PipelineDescriptor{}) {}

bool IsValid() const override { return false; }
};

static std::shared_ptr<FakePipeline> CreateFakePipelineCallback() {
return std::make_shared<FakePipeline>();
}

TEST(ContentContext, CachesPipelines) {
auto context = std::make_shared<FakeContext>();
ContentContext content_context(context, nullptr);
ContentContextOptions optionsA{.blend_mode = BlendMode::kSourceOver};
ContentContextOptions optionsB{.blend_mode = BlendMode::kSource};

auto pipelineA = content_context.GetCachedRuntimeEffectPipeline(
"A", optionsA, CreateFakePipelineCallback);

auto pipelineA2 = content_context.GetCachedRuntimeEffectPipeline(
"A", optionsA, CreateFakePipelineCallback);

auto pipelineA3 = content_context.GetCachedRuntimeEffectPipeline(
"A", optionsB, CreateFakePipelineCallback);

auto pipelineB = content_context.GetCachedRuntimeEffectPipeline(
"B", optionsB, CreateFakePipelineCallback);

ASSERT_EQ(pipelineA.get(), pipelineA2.get());
ASSERT_NE(pipelineA.get(), pipelineA3.get());
ASSERT_NE(pipelineB.get(), pipelineA.get());
}

TEST(ContentContext, InvalidatesAllPipelinesWithSameUniqueNameOnClear) {
auto context = std::make_shared<FakeContext>();
ContentContext content_context(context, nullptr);
ContentContextOptions optionsA{.blend_mode = BlendMode::kSourceOver};
ContentContextOptions optionsB{.blend_mode = BlendMode::kSource};

auto pipelineA = content_context.GetCachedRuntimeEffectPipeline(
"A", optionsA, CreateFakePipelineCallback);

auto pipelineA2 = content_context.GetCachedRuntimeEffectPipeline(
"A", optionsB, CreateFakePipelineCallback);

auto pipelineB = content_context.GetCachedRuntimeEffectPipeline(
"B", optionsB, CreateFakePipelineCallback);

ASSERT_TRUE(pipelineA);
ASSERT_TRUE(pipelineA2);
ASSERT_TRUE(pipelineB);

ASSERT_EQ(pipelineA, content_context.GetCachedRuntimeEffectPipeline(
"A", optionsA, CreateFakePipelineCallback));
ASSERT_EQ(pipelineA2, content_context.GetCachedRuntimeEffectPipeline(
"A", optionsB, CreateFakePipelineCallback));
ASSERT_EQ(pipelineB, content_context.GetCachedRuntimeEffectPipeline(
"B", optionsB, CreateFakePipelineCallback));

content_context.ClearCachedRuntimeEffectPipeline("A");

ASSERT_NE(pipelineA, content_context.GetCachedRuntimeEffectPipeline(
"A", optionsA, CreateFakePipelineCallback));
ASSERT_NE(pipelineA2, content_context.GetCachedRuntimeEffectPipeline(
"A", optionsB, CreateFakePipelineCallback));
ASSERT_EQ(pipelineB, content_context.GetCachedRuntimeEffectPipeline(
"B", optionsB, CreateFakePipelineCallback));

content_context.ClearCachedRuntimeEffectPipeline("B");

ASSERT_NE(pipelineB, content_context.GetCachedRuntimeEffectPipeline(
"B", optionsB, CreateFakePipelineCallback));
}

} // namespace testing
} // namespace impeller
29 changes: 14 additions & 15 deletions impeller/entity/contents/runtime_effect_contents.cc
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,21 @@ bool RuntimeEffectContents::Render(const ContentContext& renderer,
std::shared_ptr<const ShaderFunction> function = library->GetFunction(
runtime_stage_->GetEntrypoint(), ShaderStage::kFragment);

//--------------------------------------------------------------------------
/// Resolve geometry and content context options.
///

auto geometry_result =
GetGeometry()->GetPositionBuffer(renderer, entity, pass);
auto options = OptionsFromPassAndEntity(pass, entity);
if (geometry_result.prevent_overdraw) {
options.stencil_compare = CompareFunction::kEqual;
options.stencil_operation = StencilOperation::kIncrementClamp;
}
options.primitive_type = geometry_result.type;

if (function && runtime_stage_->IsDirty()) {
renderer.ClearCachedRuntimeEffectPipeline(runtime_stage_->GetEntrypoint());
context->GetPipelineLibrary()->RemovePipelinesWithEntryPoint(function);
library->UnregisterFunction(runtime_stage_->GetEntrypoint(),
ShaderStage::kFragment);
Expand Down Expand Up @@ -119,13 +133,6 @@ bool RuntimeEffectContents::Render(const ContentContext& renderer,
runtime_stage_->SetClean();
}

//--------------------------------------------------------------------------
/// Resolve geometry.
///

auto geometry_result =
GetGeometry()->GetPositionBuffer(renderer, entity, pass);

//--------------------------------------------------------------------------
/// Set up the command. Defer setting up the pipeline until the descriptor set
/// layouts are known from the uniforms.
Expand Down Expand Up @@ -275,14 +282,6 @@ bool RuntimeEffectContents::Render(const ContentContext& renderer,
}

/// Now that the descriptor set layouts are known, get the pipeline.

auto options = OptionsFromPassAndEntity(pass, entity);
if (geometry_result.prevent_overdraw) {
options.stencil_compare = CompareFunction::kEqual;
options.stencil_operation = StencilOperation::kIncrementClamp;
}
options.primitive_type = geometry_result.type;

auto create_callback =
[&]() -> std::shared_ptr<Pipeline<PipelineDescriptor>> {
PipelineDescriptor desc;
Expand Down
35 changes: 28 additions & 7 deletions impeller/entity/entity_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2139,14 +2139,10 @@ TEST_P(EntityTest, RuntimeEffect) {
ASSERT_TRUE(runtime_stage);
ASSERT_TRUE(runtime_stage->IsDirty());

bool first_frame = true;
bool expect_dirty = true;
Pipeline<PipelineDescriptor>* first_pipeline = nullptr;
auto callback = [&](ContentContext& context, RenderPass& pass) -> bool {
if (first_frame) {
first_frame = false;
} else {
assert(runtime_stage->IsDirty() == false);
}
EXPECT_EQ(runtime_stage->IsDirty(), expect_dirty);

auto contents = std::make_shared<RuntimeEffectContents>();
contents->SetGeometry(Geometry::MakeCover());
Expand All @@ -2168,13 +2164,38 @@ TEST_P(EntityTest, RuntimeEffect) {
Entity entity;
entity.SetContents(contents);
bool result = contents->Render(context, entity, pass);
if (!first_pipeline) {

if (expect_dirty) {
EXPECT_NE(first_pipeline, pass.GetCommands().back().pipeline.get());
first_pipeline = pass.GetCommands().back().pipeline.get();
} else {
EXPECT_EQ(pass.GetCommands().back().pipeline.get(), first_pipeline);
}

expect_dirty = false;
return result;
};

// Simulate some renders and hot reloading of the shader.
auto content_context = GetContentContext();
{
RenderTarget target;
testing::MockRenderPass mock_pass(GetContext(), target);
callback(*content_context, mock_pass);
callback(*content_context, mock_pass);

// Dirty the runtime stage.
runtime_stages = OpenAssetAsRuntimeStage("runtime_stage_example.frag.iplr");
runtime_stage =
runtime_stages[PlaygroundBackendToRuntimeStageBackend(GetBackend())];

ASSERT_TRUE(runtime_stage->IsDirty());
expect_dirty = true;

callback(*content_context, mock_pass);
callback(*content_context, mock_pass);
}

ASSERT_TRUE(OpenPlaygroundHere(callback));
}

Expand Down