Skip to content

Commit

Permalink
[Flutter GPU] Add support for drawing Flutter GPU textures in the pla…
Browse files Browse the repository at this point in the history
…yground. (#52352)
  • Loading branch information
bdero committed Apr 26, 2024
1 parent 11a857e commit d794580
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 44 deletions.
16 changes: 11 additions & 5 deletions impeller/fixtures/dart_tests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ void sayHi() {
print('Hi');
}

/// Pass a texture back to the playground for rendering to the surface.
@pragma('vm:external-name', 'SetDisplayTexture')
external void setDisplayTexture(gpu.Texture texture);

@pragma('vm:entry-point')
void instantiateDefaultContext() {
// ignore: unused_local_variable
Expand Down Expand Up @@ -102,9 +106,9 @@ ByteData float32(List<double> values) {
}

@pragma('vm:entry-point')
void canCreateRenderPassAndSubmit() {
final gpu.Texture? renderTexture =
gpu.gpuContext.createTexture(gpu.StorageMode.devicePrivate, 100, 100);
void canCreateRenderPassAndSubmit(int width, int height) {
final gpu.Texture? renderTexture = gpu.gpuContext
.createTexture(gpu.StorageMode.devicePrivate, width, height);
assert(renderTexture != null);

final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
Expand All @@ -123,9 +127,9 @@ void canCreateRenderPassAndSubmit() {

final gpu.HostBuffer transients = gpu.gpuContext.createHostBuffer();
final gpu.BufferView vertices = transients.emplace(float32(<double>[
-0.5, -0.5, //
-0.5, 0.5, //
0.0, -0.5, //
0.5, 0.5, //
0.5, -0.5, //
]));
final gpu.BufferView vertInfoData = transients.emplace(float32(<double>[
1, 0, 0, 0, // mvp
Expand All @@ -142,4 +146,6 @@ void canCreateRenderPassAndSubmit() {
encoder.draw();

commandBuffer.submit();

setDisplayTexture(renderTexture);
}
4 changes: 4 additions & 0 deletions impeller/playground/playground.cc
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ void Playground::SetupWindow() {
start_time_ = fml::TimePoint::Now().ToEpochDelta();
}

bool Playground::IsPlaygroundEnabled() const {
return switches_.enable_playground;
}

void Playground::TeardownWindow() {
if (context_) {
context_->Shutdown();
Expand Down
2 changes: 2 additions & 0 deletions impeller/playground/playground.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ class Playground {

void TeardownWindow();

bool IsPlaygroundEnabled() const;

Point GetCursorPosition() const;

ISize GetWindowSize() const;
Expand Down
245 changes: 206 additions & 39 deletions impeller/renderer/renderer_dart_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@
#include "flutter/common/task_runners.h"
#include "flutter/lib/gpu/context.h"
#include "flutter/lib/gpu/shader_library.h"
#include "flutter/lib/gpu/texture.h"
#include "flutter/runtime/dart_isolate.h"
#include "flutter/runtime/dart_vm_lifecycle.h"
#include "flutter/testing/dart_fixture.h"
#include "flutter/testing/dart_isolate_runner.h"
#include "flutter/testing/test_dart_native_resolver.h"
#include "flutter/testing/testing.h"
#include "fml/memory/ref_ptr.h"
#include "impeller/fixtures/texture.frag.h"
#include "impeller/fixtures/texture.vert.h"
#include "impeller/playground/playground_test.h"
#include "impeller/renderer/pipeline_library.h"
#include "impeller/renderer/render_pass.h"
#include "impeller/renderer/vertex_buffer_builder.h"

#include "gtest/gtest.h"
#include "third_party/imgui/imgui.h"
Expand Down Expand Up @@ -46,6 +52,23 @@ class RendererDartTest : public PlaygroundTest,
isolate_ = CreateDartIsolate();
assert(isolate_);
assert(isolate_->get()->GetPhase() == flutter::DartIsolate::Phase::Running);

// Set up native callbacks.
//
// Note: The Dart isolate is configured (by
// `RendererDartTest::CreateDartIsolate`) to use the main thread, so
// there's no need for additional synchronization.
{
auto set_display_texture = [this](Dart_NativeArguments args) {
flutter::gpu::Texture* texture =
tonic::DartConverter<flutter::gpu::Texture*>::FromDart(
Dart_GetNativeArgument(args, 0));
assert(texture != nullptr); // Should always be a valid pointer.
received_texture_ = texture->GetTexture();
};
AddNativeCallback("SetDisplayTexture",
CREATE_NATIVE_ENTRY(set_display_texture));
}
}

flutter::testing::AutoIsolateShutdown* GetIsolate() {
Expand All @@ -58,6 +81,169 @@ class RendererDartTest : public PlaygroundTest,
return isolate_.get();
}

/// @brief Run a Dart function that's expected to create a texture and pass
/// it back for rendering via `drawToPlayground`.
std::shared_ptr<Texture> GetRenderedTextureFromDart(
const char* dart_function_name) {
bool success =
GetIsolate()->RunInIsolateScope([this, &dart_function_name]() -> bool {
Dart_Handle args[] = {tonic::ToDart(GetWindowSize().width),
tonic::ToDart(GetWindowSize().height)};
if (tonic::CheckAndHandleError(
::Dart_Invoke(Dart_RootLibrary(),
tonic::ToDart(dart_function_name), 2, args))) {
return false;
}
return true;
});
if (!success) {
FML_LOG(ERROR) << "Failed to invoke dart test function:"
<< dart_function_name;
return nullptr;
}
if (!received_texture_) {
FML_LOG(ERROR) << "Dart test function `" << dart_function_name
<< "` did not invoke `drawToPlaygroundSurface`.";
return nullptr;
}
return received_texture_;
}

/// @brief Invokes a Dart function.
///
/// Returns false if invoking the function failed or if any unhandled
/// exceptions were thrown.
bool RunDartFunction(const char* dart_function_name) {
return GetIsolate()->RunInIsolateScope([&dart_function_name]() -> bool {
if (tonic::CheckAndHandleError(
::Dart_Invoke(Dart_RootLibrary(),
tonic::ToDart(dart_function_name), 0, nullptr))) {
return false;
}
return true;
});
}

/// @brief Invokes a Dart function with the window's width and height as
/// arguments.
///
/// Returns false if invoking the function failed or if any unhandled
/// exceptions were thrown.
bool RunDartFunctionWithWindowSize(const char* dart_function_name) {
return GetIsolate()->RunInIsolateScope(
[this, &dart_function_name]() -> bool {
Dart_Handle args[] = {tonic::ToDart(GetWindowSize().width),
tonic::ToDart(GetWindowSize().height)};
if (tonic::CheckAndHandleError(
::Dart_Invoke(Dart_RootLibrary(),
tonic::ToDart(dart_function_name), 2, args))) {
return false;
}
return true;
});
}

/// @brief Call a dart function that produces a texture and render the result
/// in the playground.
///
/// If the playground isn't enabled, the function is simply run once
/// in order to verify that it doesn't throw any unhandled exceptions.
bool RenderDartToPlayground(const char* dart_function_name) {
if (!IsPlaygroundEnabled()) {
// If the playground is not enabled, run the function instead to at least
// verify that it doesn't crash.
return RunDartFunctionWithWindowSize(dart_function_name);
}

auto context = GetContext();
assert(context != nullptr);

//------------------------------------------------------------------------------
/// Prepare pipeline.
///

using TextureVS = TextureVertexShader;
using TextureFS = TextureFragmentShader;
using TexturePipelineBuilder = PipelineBuilder<TextureVS, TextureFS>;

auto pipeline_desc =
TexturePipelineBuilder::MakeDefaultPipelineDescriptor(*context);
if (!pipeline_desc.has_value()) {
FML_LOG(ERROR) << "Failed to create default pipeline descriptor.";
return false;
}
pipeline_desc->SetSampleCount(SampleCount::kCount4);
pipeline_desc->SetStencilAttachmentDescriptors(std::nullopt);
pipeline_desc->SetDepthStencilAttachmentDescriptor(std::nullopt);
pipeline_desc->SetStencilPixelFormat(PixelFormat::kUnknown);
pipeline_desc->SetDepthPixelFormat(PixelFormat::kUnknown);

auto pipeline =
context->GetPipelineLibrary()->GetPipeline(pipeline_desc).Get();
if (!pipeline || !pipeline->IsValid()) {
FML_LOG(ERROR) << "Failed to create default pipeline.";
return false;
}

//------------------------------------------------------------------------------
/// Prepare vertex data.
///

VertexBufferBuilder<TextureVS::PerVertexData> texture_vtx_builder;

// Always stretch out the texture to fill the screen.

// clang-format off
texture_vtx_builder.AddVertices({
{{-0.5, -0.5, 0.0}, {0.0, 0.0}}, // 1
{{ 0.5, -0.5, 0.0}, {1.0, 0.0}}, // 2
{{ 0.5, 0.5, 0.0}, {1.0, 1.0}}, // 3
{{-0.5, -0.5, 0.0}, {0.0, 0.0}}, // 1
{{ 0.5, 0.5, 0.0}, {1.0, 1.0}}, // 3
{{-0.5, 0.5, 0.0}, {0.0, 1.0}}, // 4
});
// clang-format on

//------------------------------------------------------------------------------
/// Prepare sampler.
///

const auto& sampler = context->GetSamplerLibrary()->GetSampler({});
if (!sampler) {
FML_LOG(ERROR) << "Failed to create default sampler.";
return false;
}

//------------------------------------------------------------------------------
/// Render to playground.
///

SinglePassCallback callback = [&](RenderPass& pass) {
auto texture = GetRenderedTextureFromDart(dart_function_name);
if (!texture) {
return false;
}

auto buffer = HostBuffer::Create(context->GetResourceAllocator());

pass.SetVertexBuffer(texture_vtx_builder.CreateVertexBuffer(
*context->GetResourceAllocator()));

TextureVS::UniformBuffer uniforms;
uniforms.mvp = Matrix();
TextureVS::BindUniformBuffer(pass, buffer->EmplaceUniform(uniforms));
TextureFS::BindTextureContents(pass, texture, sampler);

pass.SetPipeline(pipeline);

if (!pass.Draw().ok()) {
return false;
}
return true;
};
return OpenPlaygroundHere(callback);
}

private:
std::unique_ptr<flutter::testing::AutoIsolateShutdown> CreateDartIsolate() {
const auto settings = CreateSettingsForFixture();
Expand All @@ -76,65 +262,46 @@ class RendererDartTest : public PlaygroundTest,
flutter::DartVMRef vm_ref_;
fml::RefPtr<fml::TaskRunner> current_task_runner_;
std::unique_ptr<flutter::testing::AutoIsolateShutdown> isolate_;

std::shared_ptr<Texture> received_texture_;
};

INSTANTIATE_PLAYGROUND_SUITE(RendererDartTest);

TEST_P(RendererDartTest, CanRunDartInPlaygroundFrame) {
auto isolate = GetIsolate();

SinglePassCallback callback = [&](RenderPass& pass) {
ImGui::Begin("Dart test", nullptr);
ImGui::Text(
"This test executes Dart code during the playground frame callback.");
ImGui::End();

return isolate->RunInIsolateScope([]() -> bool {
if (tonic::CheckAndHandleError(::Dart_Invoke(
Dart_RootLibrary(), tonic::ToDart("sayHi"), 0, nullptr))) {
return false;
}
return true;
});
return RunDartFunction("sayHi");
};
OpenPlaygroundHere(callback);
ASSERT_TRUE(OpenPlaygroundHere(callback));
}

TEST_P(RendererDartTest, CanInstantiateFlutterGPUContext) {
auto isolate = GetIsolate();
bool result = isolate->RunInIsolateScope([]() -> bool {
if (tonic::CheckAndHandleError(::Dart_Invoke(
Dart_RootLibrary(), tonic::ToDart("instantiateDefaultContext"), 0,
nullptr))) {
return false;
}
return true;
});
/// These test entries correspond to Dart functions located in
/// `flutter/impeller/fixtures/dart_tests.dart`

ASSERT_TRUE(result);
TEST_P(RendererDartTest, CanInstantiateFlutterGPUContext) {
ASSERT_TRUE(RunDartFunction("instantiateDefaultContext"));
}

#define DART_TEST_CASE(name) \
TEST_P(RendererDartTest, name) { \
auto isolate = GetIsolate(); \
bool result = isolate->RunInIsolateScope([]() -> bool { \
if (tonic::CheckAndHandleError(::Dart_Invoke( \
Dart_RootLibrary(), tonic::ToDart(#name), 0, nullptr))) { \
return false; \
} \
return true; \
}); \
ASSERT_TRUE(result); \
}
TEST_P(RendererDartTest, CanCreateShaderLibrary) {
ASSERT_TRUE(RunDartFunction("canCreateShaderLibrary"));
}

/// These test entries correspond to Dart functions located in
/// `flutter/impeller/fixtures/dart_tests.dart`
TEST_P(RendererDartTest, CanReflectUniformStructs) {
ASSERT_TRUE(RunDartFunction("canReflectUniformStructs"));
}

DART_TEST_CASE(canCreateShaderLibrary);
DART_TEST_CASE(canReflectUniformStructs);
DART_TEST_CASE(uniformBindFailsForInvalidHostBufferOffset);
TEST_P(RendererDartTest, UniformBindFailsForInvalidHostBufferOffset) {
ASSERT_TRUE(RunDartFunction("uniformBindFailsForInvalidHostBufferOffset"));
}

DART_TEST_CASE(canCreateRenderPassAndSubmit);
TEST_P(RendererDartTest, CanCreateRenderPassAndSubmit) {
ASSERT_TRUE(RenderDartToPlayground("canCreateRenderPassAndSubmit"));
}

} // namespace testing
} // namespace impeller

0 comments on commit d794580

Please sign in to comment.