Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
41527d3
Add Flutter GPU surface API
bdero May 31, 2026
7ec7a99
Address Flutter GPU surface review comments
bdero Jun 2, 2026
8e36788
Compile GPU surface only with Impeller rendering
bdero Jun 2, 2026
93e2999
Revert "Compile GPU surface only with Impeller rendering"
bdero Jun 2, 2026
76757fd
Fix Flutter GPU unit test build deps
bdero Jun 2, 2026
e65e2a4
Use [[maybe_unused]] instead of (void) casts in surface.cc
bdero Jun 4, 2026
8a6a8d1
Retrigger CI
bdero Jun 4, 2026
7592dbc
Throw a clear error when a presented surface frame is never submitted
bdero Jun 5, 2026
7e2f158
Refactor surface API into a sealed GpuSurface family
bdero Jun 9, 2026
331f030
Return Dart_Null from PresentFrame on success
bdero Jun 9, 2026
d54fc5e
Drop deprecated gmock Invoke wrapper in command buffer test
bdero Jun 9, 2026
c092c5a
Add test fixtures dependency to gpu_unittests
bdero Jun 9, 2026
98a7800
Document the present or discard contract on GpuSurfaceFrame
bdero Jun 9, 2026
6d7012b
Run clang-format on command_buffer_unittests.cc
bdero Jun 9, 2026
9c25938
Merge branch 'master' into bdero/flutter-gpu-surface
bdero Jun 10, 2026
4680042
Merge branch 'master' into bdero/flutter-gpu-surface
bdero Jun 11, 2026
840d040
Drop removed TextureCoordinateSystem references
bdero Jun 11, 2026
feca459
Merge branch 'master' into bdero/flutter-gpu-surface
bdero Jun 12, 2026
e7a20b8
Merge branch 'master' into bdero/flutter-gpu-surface
bdero Jun 12, 2026
4caeea8
Merge branch 'master' into bdero/flutter-gpu-surface
bdero Jun 12, 2026
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
4 changes: 4 additions & 0 deletions engine/src/flutter/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ group("unittests") {
"//flutter/txt:txt_unittests",
]

if (!is_fuchsia) {
public_deps += [ "//flutter/lib/gpu:gpu_unittests" ]
}

# The accessibility library only supports Mac and Windows at the moment.
if (is_mac || is_win) {
public_deps +=
Expand Down
23 changes: 22 additions & 1 deletion engine/src/flutter/lib/gpu/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ source_set("gpu") {
"shader.h",
"shader_library.cc",
"shader_library.h",
"surface.cc",
"surface.h",
"texture.cc",
"texture.h",
]
Expand All @@ -76,5 +78,24 @@ source_set("gpu") {
}
}

if (enable_unittests) {
if (enable_unittests && !is_fuchsia) {
test_fixtures("gpu_unittests_fixtures") {
fixtures = []
}

executable("gpu_unittests") {
testonly = true

sources = [ "command_buffer_unittests.cc" ]

deps = [
":gpu",
":gpu_unittests_fixtures",
"//flutter/impeller/renderer/testing:mocks",
"//flutter/testing",
"//flutter/testing:dart",
"//third_party/googletest:gmock",
"//third_party/googletest:gtest",
]
}
}
48 changes: 43 additions & 5 deletions engine/src/flutter/lib/gpu/command_buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,44 @@ bool CommandBuffer::CopyTextureToTexture(Texture& source,
"CommandBuffer.copyTextureToTexture");
}

bool CommandBuffer::AddCompletionCallback(
impeller::CommandBuffer::CompletionCallback completion_callback) {
if (submitted_) {
return false;
}
if (completion_callback) {
completion_callbacks_.push_back(std::move(completion_callback));
}
return true;
}

bool CommandBuffer::Submit() {
return CommandBuffer::Submit({});
}

bool CommandBuffer::Submit(
const impeller::CommandBuffer::CompletionCallback& completion_callback) {
if (submitted_) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd suggest adding a unit test for these c++ level changes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 7ec7a99.

return false;
}
submitted_ = true;

std::vector<impeller::CommandBuffer::CompletionCallback> callbacks =
std::move(completion_callbacks_);
if (completion_callback) {
callbacks.push_back(completion_callback);
}
impeller::CommandBuffer::CompletionCallback combined_completion_callback;
if (!callbacks.empty()) {
combined_completion_callback =
[callbacks = std::move(callbacks)](
impeller::CommandBuffer::Status status) mutable {
for (auto& callback : callbacks) {
callback(status);
}
};
}

// For the GLES backend, command queue submission just flushes the reactor,
// which needs to happen on the raster thread.
if (context_->GetBackendType() == impeller::Context::BackendType::kOpenGLES) {
Expand All @@ -118,7 +150,7 @@ bool CommandBuffer::Submit(

task_runners.GetRasterTaskRunner()->PostTask(
fml::MakeCopyable([context = context_, command_buffer = command_buffer_,
completion_callback = completion_callback,
completion_callback = combined_completion_callback,
encodables = encodables_]() mutable {
for (auto& encodable : encodables) {
if (!encodable.EncodeCommands()) {
Expand All @@ -130,8 +162,11 @@ bool CommandBuffer::Submit(
}
}

context->GetCommandQueue()->Submit({command_buffer},
completion_callback);
auto status = context->GetCommandQueue()->Submit({command_buffer},
completion_callback);
if (!status.ok() && completion_callback) {
completion_callback(impeller::CommandBuffer::Status::kError);
}
context->DisposeThreadLocalCachedResources();
}));
return true;
Expand All @@ -143,9 +178,12 @@ bool CommandBuffer::Submit(
}
}

auto status = context_->GetCommandQueue()->Submit({command_buffer_},
completion_callback);
auto status = context_->GetCommandQueue()->Submit(
{command_buffer_}, combined_completion_callback);
context_->DisposeThreadLocalCachedResources();
if (!status.ok() && combined_completion_callback) {
combined_completion_callback(impeller::CommandBuffer::Status::kError);
}
return status.ok();
}

Expand Down
6 changes: 6 additions & 0 deletions engine/src/flutter/lib/gpu/command_buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class CommandBuffer : public RefCountedDartWrappable<CommandBuffer> {

void AddRenderPass(std::shared_ptr<impeller::RenderPass> render_pass);

bool AddCompletionCallback(
impeller::CommandBuffer::CompletionCallback completion_callback);

bool CopyBufferToTexture(DeviceBuffer& source,
size_t source_offset,
size_t source_length,
Expand Down Expand Up @@ -73,6 +76,9 @@ class CommandBuffer : public RefCountedDartWrappable<CommandBuffer> {
std::shared_ptr<impeller::BlitPass> GetOrCreateBlitPass();

std::vector<Encodable> encodables_;
std::vector<impeller::CommandBuffer::CompletionCallback>
completion_callbacks_;
bool submitted_ = false;

FML_DISALLOW_COPY_AND_ASSIGN(CommandBuffer);
};
Expand Down
112 changes: 112 additions & 0 deletions engine/src/flutter/lib/gpu/command_buffer_unittests.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// 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 "flutter/lib/gpu/command_buffer.h"

#include <memory>
#include <vector>

#include "fml/status.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "impeller/renderer/testing/mocks.h"

namespace flutter::gpu {
namespace {

using ::impeller::testing::MockCommandBuffer;
using ::impeller::testing::MockCommandQueue;
using ::impeller::testing::MockImpellerContext;
using ::testing::_;
using ::testing::DoAll;
using ::testing::Return;

TEST(FlutterGpuCommandBufferTest,
InvokesRegisteredCompletionCallbacksOnSubmit) {
auto context = std::make_shared<MockImpellerContext>();
auto impeller_command_buffer = std::make_shared<MockCommandBuffer>(context);
auto command_queue = std::make_shared<MockCommandQueue>();
CommandBuffer command_buffer(context, impeller_command_buffer);

std::vector<impeller::CommandBuffer::Status> statuses;

EXPECT_TRUE(command_buffer.AddCompletionCallback(
[&statuses](impeller::CommandBuffer::Status status) {
statuses.push_back(status);
}));
EXPECT_TRUE(command_buffer.AddCompletionCallback(
[&statuses](impeller::CommandBuffer::Status status) {
statuses.push_back(status);
}));

EXPECT_CALL(*context, GetBackendType)
.WillOnce(Return(impeller::Context::BackendType::kMetal));
EXPECT_CALL(*context, GetCommandQueue).WillOnce(Return(command_queue));
EXPECT_CALL(*command_queue, Submit(_, _, _))
.WillOnce(DoAll(
[](const std::vector<std::shared_ptr<impeller::CommandBuffer>>&
buffers,
const impeller::CommandQueue::CompletionCallback& callback,
bool block_on_schedule) {
EXPECT_EQ(buffers.size(), 1u);
EXPECT_FALSE(block_on_schedule);
callback(impeller::CommandBuffer::Status::kCompleted);
},
Return(fml::Status())));

EXPECT_TRUE(command_buffer.Submit(
[&statuses](impeller::CommandBuffer::Status status) {
statuses.push_back(status);
}));
EXPECT_EQ(statuses.size(), 3u);
EXPECT_EQ(statuses[0], impeller::CommandBuffer::Status::kCompleted);
EXPECT_EQ(statuses[1], impeller::CommandBuffer::Status::kCompleted);
EXPECT_EQ(statuses[2], impeller::CommandBuffer::Status::kCompleted);
}

TEST(FlutterGpuCommandBufferTest, RejectsCompletionCallbacksAfterSubmit) {
auto context = std::make_shared<MockImpellerContext>();
auto impeller_command_buffer = std::make_shared<MockCommandBuffer>(context);
auto command_queue = std::make_shared<MockCommandQueue>();
CommandBuffer command_buffer(context, impeller_command_buffer);

EXPECT_CALL(*context, GetBackendType)
.WillOnce(Return(impeller::Context::BackendType::kMetal));
EXPECT_CALL(*context, GetCommandQueue).WillOnce(Return(command_queue));
EXPECT_CALL(*command_queue, Submit(_, _, _)).WillOnce(Return(fml::Status()));

EXPECT_TRUE(command_buffer.Submit());
EXPECT_FALSE(command_buffer.AddCompletionCallback(
[](impeller::CommandBuffer::Status status) { (void)status; }));
EXPECT_FALSE(command_buffer.Submit());
}

TEST(FlutterGpuCommandBufferTest,
InvokesRegisteredCompletionCallbacksWhenSubmitFails) {
auto context = std::make_shared<MockImpellerContext>();
auto impeller_command_buffer = std::make_shared<MockCommandBuffer>(context);
auto command_queue = std::make_shared<MockCommandQueue>();
CommandBuffer command_buffer(context, impeller_command_buffer);

std::vector<impeller::CommandBuffer::Status> statuses;

EXPECT_TRUE(command_buffer.AddCompletionCallback(
[&statuses](impeller::CommandBuffer::Status status) {
statuses.push_back(status);
}));

EXPECT_CALL(*context, GetBackendType)
.WillOnce(Return(impeller::Context::BackendType::kMetal));
EXPECT_CALL(*context, GetCommandQueue).WillOnce(Return(command_queue));
EXPECT_CALL(*command_queue, Submit(_, _, _))
.WillOnce(Return(fml::Status(fml::StatusCode::kInternal,
"Command queue submit failed.")));

EXPECT_FALSE(command_buffer.Submit());
EXPECT_EQ(statuses.size(), 1u);
EXPECT_EQ(statuses[0], impeller::CommandBuffer::Status::kError);
}

} // namespace
} // namespace flutter::gpu
1 change: 1 addition & 0 deletions engine/src/flutter/lib/gpu/lib/gpu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ part 'src/context.dart';
part 'src/formats.dart';
part 'src/natives.dart';
part 'src/texture.dart';
part 'src/surface.dart';
part 'src/render_pass.dart';
part 'src/render_pipeline.dart';
part 'src/shader.dart';
Expand Down
9 changes: 9 additions & 0 deletions engine/src/flutter/lib/gpu/lib/src/command_buffer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ base class CommandBuffer extends NativeFieldWrapperClass1 {
_initialize(_gpuContext);
}

bool _submitted = false;

/// Whether [submit] has been called on this command buffer.
bool get submitted => _submitted;

RenderPass createRenderPass(RenderTarget renderTarget) {
return RenderPass._(_gpuContext, this, renderTarget);
}
Expand Down Expand Up @@ -233,10 +238,14 @@ base class CommandBuffer extends NativeFieldWrapperClass1 {
}

void submit({CompletionCallback? completionCallback}) {
if (_submitted) {
throw StateError('CommandBuffer has already been submitted.');
}
String? error = _submit(completionCallback);
if (error != null) {
throw Exception(error);
}
_submitted = true;
}

/// Wrap with native counterpart.
Expand Down
20 changes: 20 additions & 0 deletions engine/src/flutter/lib/gpu/lib/src/context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,26 @@ base class GpuContext extends NativeFieldWrapperClass1 {
return result;
}

/// Creates an image surface for rendering content that Flutter draws as a
/// [ui.Image].
///
/// A [GpuImageSurface] owns the final color textures that are converted into
/// [ui.Image] handles and drawn by Flutter. Use it for animated or frequently
/// updated render targets where the app should not have to guess how many
/// textures are needed to avoid overwriting a frame Flutter may still be
/// sampling.
///
/// The surface manages only the final presentable color texture. Renderers
/// should continue to create ordinary [Texture] objects for depth, stencil,
/// multisample, and intermediate color attachments.
GpuImageSurface createImageSurface(
int width,
int height, {
PixelFormat? format,
}) {
return GpuImageSurface._(this, width, height, format ?? defaultColorFormat);
}

/// Create a new command buffer that can be used to submit GPU commands.
CommandBuffer createCommandBuffer() {
return CommandBuffer._(this);
Expand Down
Loading
Loading