Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 6b262a0

Browse files
authored
[Impeller] implements a retry mechanism for dart:ui/Image.toByteData. (#46840)
Design doc: [link](https://docs.google.com/document/d/1Uuiw3pdQxNFTA8OQuZ-kuvYg1NB42XgccQCZeqr4oII/edit#heading=h.hn6wreyrz6fm) fixes: flutter/flutter#135245 One slight deviation from the design doc is that I decided to make ContextMTL respond to changes to the SyncSwitch instead of having it observe the app state directly. The benefits are: 1) This keeps that functionality in one location 1) It makes writing tests much easier 1) There's no need of conditional compilation between macos and ios 1) There is no need to add an objc class [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 172ad1f commit 6b262a0

File tree

11 files changed

+279
-26
lines changed

11 files changed

+279
-26
lines changed

fml/synchronization/sync_switch.cc

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,26 @@ void SyncSwitch::Execute(const SyncSwitch::Handlers& handlers) const {
3232
}
3333

3434
void SyncSwitch::SetSwitch(bool value) {
35+
{
36+
fml::UniqueLock lock(*mutex_);
37+
value_ = value;
38+
}
39+
for (Observer* observer : observers_) {
40+
observer->OnSyncSwitchUpdate(value);
41+
}
42+
}
43+
44+
void SyncSwitch::AddObserver(Observer* observer) const {
3545
fml::UniqueLock lock(*mutex_);
36-
value_ = value;
46+
if (std::find(observers_.begin(), observers_.end(), observer) ==
47+
observers_.end()) {
48+
observers_.push_back(observer);
49+
}
3750
}
3851

52+
void SyncSwitch::RemoveObserver(Observer* observer) const {
53+
fml::UniqueLock lock(*mutex_);
54+
observers_.erase(std::remove(observers_.begin(), observers_.end(), observer),
55+
observers_.end());
56+
}
3957
} // namespace fml

fml/synchronization/sync_switch.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ namespace fml {
2121
/// at a time.
2222
class SyncSwitch {
2323
public:
24+
/// Observes changes to the SyncSwitch.
25+
class Observer {
26+
public:
27+
virtual ~Observer() = default;
28+
/// `new_is_disabled` not guaranteed to be the value of the SyncSwitch
29+
/// during execution, it should be checked with calls to
30+
/// SyncSwitch::Execute.
31+
virtual void OnSyncSwitchUpdate(bool new_is_disabled) = 0;
32+
};
33+
2434
/// Represents the 2 code paths available when calling |SyncSwitch::Execute|.
2535
struct Handlers {
2636
/// Sets the handler that will be executed if the |SyncSwitch| is true.
@@ -53,8 +63,15 @@ class SyncSwitch {
5363
/// @param[in] value New value for the |SyncSwitch|.
5464
void SetSwitch(bool value);
5565

66+
/// Threadsafe.
67+
void AddObserver(Observer* observer) const;
68+
69+
/// Threadsafe.
70+
void RemoveObserver(Observer* observer) const;
71+
5672
private:
5773
mutable std::unique_ptr<fml::SharedMutex> mutex_;
74+
mutable std::vector<Observer*> observers_;
5875
bool value_;
5976

6077
FML_DISALLOW_COPY_AND_ASSIGN(SyncSwitch);

impeller/renderer/backend/metal/context_mtl.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
#include <Metal/Metal.h>
88

9+
#include <deque>
910
#include <string>
10-
#include <vector>
1111

1212
#include "flutter/fml/concurrent_message_loop.h"
1313
#include "flutter/fml/macros.h"
@@ -93,7 +93,20 @@ class ContextMTL final : public Context,
9393

9494
std::shared_ptr<const fml::SyncSwitch> GetIsGpuDisabledSyncSwitch() const;
9595

96+
// |Context|
97+
void StoreTaskForGPU(std::function<void()> task) override;
98+
9699
private:
100+
class SyncSwitchObserver : public fml::SyncSwitch::Observer {
101+
public:
102+
SyncSwitchObserver(ContextMTL& parent);
103+
virtual ~SyncSwitchObserver() = default;
104+
void OnSyncSwitchUpdate(bool new_value) override;
105+
106+
private:
107+
ContextMTL& parent_;
108+
};
109+
97110
id<MTLDevice> device_ = nullptr;
98111
id<MTLCommandQueue> command_queue_ = nullptr;
99112
std::shared_ptr<ShaderLibraryMTL> shader_library_;
@@ -103,6 +116,8 @@ class ContextMTL final : public Context,
103116
std::shared_ptr<const Capabilities> device_capabilities_;
104117
std::shared_ptr<fml::ConcurrentMessageLoop> raster_message_loop_;
105118
std::shared_ptr<const fml::SyncSwitch> is_gpu_disabled_sync_switch_;
119+
std::deque<std::function<void()>> tasks_awaiting_gpu_;
120+
std::unique_ptr<SyncSwitchObserver> sync_switch_observer_;
106121
bool is_valid_ = false;
107122

108123
ContextMTL(
@@ -114,6 +129,8 @@ class ContextMTL final : public Context,
114129
std::shared_ptr<CommandBuffer> CreateCommandBufferInQueue(
115130
id<MTLCommandQueue> queue) const;
116131

132+
void FlushTasksAwaitingGPU();
133+
117134
FML_DISALLOW_COPY_AND_ASSIGN(ContextMTL);
118135
};
119136

impeller/renderer/backend/metal/context_mtl.mm

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ static bool DeviceSupportsComputeSubgroups(id<MTLDevice> device) {
8383
return;
8484
}
8585

86+
sync_switch_observer_.reset(new SyncSwitchObserver(*this));
87+
is_gpu_disabled_sync_switch_->AddObserver(sync_switch_observer_.get());
88+
8689
// Worker task runner.
8790
{
8891
raster_message_loop_ = fml::ConcurrentMessageLoop::Create(
@@ -284,7 +287,9 @@ new ContextMTL(device, command_queue,
284287
return context;
285288
}
286289

287-
ContextMTL::~ContextMTL() = default;
290+
ContextMTL::~ContextMTL() {
291+
is_gpu_disabled_sync_switch_->RemoveObserver(sync_switch_observer_.get());
292+
}
288293

289294
Context::BackendType ContextMTL::GetBackendType() const {
290295
return Context::BackendType::kMetal;
@@ -376,4 +381,28 @@ new ContextMTL(device, command_queue,
376381
return buffer;
377382
}
378383

384+
void ContextMTL::StoreTaskForGPU(std::function<void()> task) {
385+
tasks_awaiting_gpu_.emplace_back(std::move(task));
386+
while (tasks_awaiting_gpu_.size() > kMaxTasksAwaitingGPU) {
387+
tasks_awaiting_gpu_.front()();
388+
tasks_awaiting_gpu_.pop_front();
389+
}
390+
}
391+
392+
void ContextMTL::FlushTasksAwaitingGPU() {
393+
for (const auto& task : tasks_awaiting_gpu_) {
394+
task();
395+
}
396+
tasks_awaiting_gpu_.clear();
397+
}
398+
399+
ContextMTL::SyncSwitchObserver::SyncSwitchObserver(ContextMTL& parent)
400+
: parent_(parent) {}
401+
402+
void ContextMTL::SyncSwitchObserver::OnSyncSwitchUpdate(bool new_is_disabled) {
403+
if (!new_is_disabled) {
404+
parent_.FlushTasksAwaitingGPU();
405+
}
406+
}
407+
379408
} // namespace impeller

impeller/renderer/context.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ class Context {
5252
kVulkan,
5353
};
5454

55+
/// The maximum number of tasks that should ever be stored for
56+
/// `StoreTaskForGPU`.
57+
///
58+
/// This number was arbitrarily chosen. The idea is that this is a somewhat
59+
/// rare situation where tasks happen to get executed in that tiny amount of
60+
/// time while an app is being backgrounded but still executing.
61+
static constexpr int32_t kMaxTasksAwaitingGPU = 10;
62+
5563
//----------------------------------------------------------------------------
5664
/// @brief Destroys an Impeller context.
5765
///
@@ -176,6 +184,19 @@ class Context {
176184

177185
CaptureContext capture;
178186

187+
/// Stores a task on the `ContextMTL` that is awaiting access for the GPU.
188+
///
189+
/// The task will be executed in the event that the GPU access has changed to
190+
/// being available or that the task has been canceled. The task should
191+
/// operate with the `SyncSwitch` to make sure the GPU is accessible.
192+
///
193+
/// Threadsafe.
194+
///
195+
/// `task` will be executed on the platform thread.
196+
virtual void StoreTaskForGPU(std::function<void()> task) {
197+
FML_CHECK(false && "not supported in this context");
198+
}
199+
179200
protected:
180201
Context();
181202

lib/ui/fixtures/ui_test.dart

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,11 @@ external void _validateExternal(Uint8List result);
325325
@pragma('vm:external-name', 'ValidateError')
326326
external void _validateError(String? error);
327327
@pragma('vm:external-name', 'TurnOffGPU')
328-
external void _turnOffGPU();
328+
external void _turnOffGPU(bool value);
329+
@pragma('vm:external-name', 'FlushGpuAwaitingTasks')
330+
external void _flushGpuAwaitingTasks();
331+
@pragma('vm:external-name', 'ValidateNotNull')
332+
external void _validateNotNull(Object? object);
329333

330334
@pragma('vm:entry-point')
331335
Future<void> toByteDataWithoutGPU() async {
@@ -338,12 +342,40 @@ Future<void> toByteDataWithoutGPU() async {
338342
canvas.drawCircle(c, 25.0, paint);
339343
final Picture picture = pictureRecorder.endRecording();
340344
final Image image = await picture.toImage(100, 100);
341-
_turnOffGPU();
345+
_turnOffGPU(true);
346+
Timer flusher = Timer.periodic(Duration(milliseconds: 1), (timer) {
347+
_flushGpuAwaitingTasks();
348+
});
342349
try {
343350
ByteData? byteData = await image.toByteData();
344351
_validateError(null);
345-
} catch (ex) {
346-
_validateError(ex.toString());
352+
} catch (error) {
353+
_validateError(error.toString());
354+
} finally {
355+
flusher.cancel();
356+
}
357+
}
358+
359+
@pragma('vm:entry-point')
360+
Future<void> toByteDataRetries() async {
361+
final PictureRecorder pictureRecorder = PictureRecorder();
362+
final Canvas canvas = Canvas(pictureRecorder);
363+
final Paint paint = Paint()
364+
..color = Color.fromRGBO(255, 255, 255, 1.0)
365+
..style = PaintingStyle.fill;
366+
final Offset c = Offset(50.0, 50.0);
367+
canvas.drawCircle(c, 25.0, paint);
368+
final Picture picture = pictureRecorder.endRecording();
369+
final Image image = await picture.toImage(100, 100);
370+
_turnOffGPU(true);
371+
Future<void>.delayed(Duration(milliseconds: 10), () {
372+
_turnOffGPU(false);
373+
});
374+
try {
375+
ByteData? byteData = await image.toByteData();
376+
_validateNotNull(byteData);
377+
} catch (error) {
378+
_validateNotNull(null);
347379
}
348380
}
349381

lib/ui/painting/image_encoding_impeller.cc

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,68 @@ sk_sp<SkImage> ConvertBufferToSkImage(
5656
return raster_image;
5757
}
5858

59-
void DoConvertImageToRasterImpeller(
59+
[[nodiscard]] fml::Status DoConvertImageToRasterImpeller(
6060
const sk_sp<DlImage>& dl_image,
61-
std::function<void(fml::StatusOr<sk_sp<SkImage>>)> encode_task,
61+
const std::function<void(fml::StatusOr<sk_sp<SkImage>>)>& encode_task,
6262
const std::shared_ptr<const fml::SyncSwitch>& is_gpu_disabled_sync_switch,
6363
const std::shared_ptr<impeller::Context>& impeller_context) {
64+
fml::Status result;
6465
is_gpu_disabled_sync_switch->Execute(
6566
fml::SyncSwitch::Handlers()
66-
.SetIfTrue([&encode_task] {
67-
encode_task(
68-
fml::Status(fml::StatusCode::kUnavailable, "GPU unavailable."));
67+
.SetIfTrue([&result] {
68+
result =
69+
fml::Status(fml::StatusCode::kUnavailable, "GPU unavailable.");
6970
})
7071
.SetIfFalse([&dl_image, &encode_task, &impeller_context] {
7172
ImageEncodingImpeller::ConvertDlImageToSkImage(
72-
dl_image, std::move(encode_task), impeller_context);
73+
dl_image, encode_task, impeller_context);
7374
}));
75+
return result;
76+
}
77+
78+
/// Same as `DoConvertImageToRasterImpeller` but it will attempt to retry the
79+
/// operation if `DoConvertImageToRasterImpeller` returns kUnavailable when the
80+
/// GPU becomes available again.
81+
void DoConvertImageToRasterImpellerWithRetry(
82+
const sk_sp<DlImage>& dl_image,
83+
std::function<void(fml::StatusOr<sk_sp<SkImage>>)>&& encode_task,
84+
const std::shared_ptr<const fml::SyncSwitch>& is_gpu_disabled_sync_switch,
85+
const std::shared_ptr<impeller::Context>& impeller_context,
86+
const fml::RefPtr<fml::TaskRunner>& retry_runner) {
87+
fml::Status status = DoConvertImageToRasterImpeller(
88+
dl_image, encode_task, is_gpu_disabled_sync_switch, impeller_context);
89+
if (!status.ok()) {
90+
// If the conversion failed because of the GPU is unavailable, store the
91+
// task on the Context so it can be executed when the GPU becomes available.
92+
if (status.code() == fml::StatusCode::kUnavailable) {
93+
impeller_context->StoreTaskForGPU(
94+
[dl_image, encode_task = std::move(encode_task),
95+
is_gpu_disabled_sync_switch, impeller_context,
96+
retry_runner]() mutable {
97+
auto retry_task = [dl_image, encode_task = std::move(encode_task),
98+
is_gpu_disabled_sync_switch, impeller_context] {
99+
fml::Status retry_status = DoConvertImageToRasterImpeller(
100+
dl_image, encode_task, is_gpu_disabled_sync_switch,
101+
impeller_context);
102+
if (!retry_status.ok()) {
103+
// The retry failed for some reason, maybe the GPU became
104+
// unavailable again. Don't retry again, just fail in this case.
105+
encode_task(retry_status);
106+
}
107+
};
108+
// If a `retry_runner` is specified, post the retry to it, otherwise
109+
// execute it directly.
110+
if (retry_runner) {
111+
retry_runner->PostTask(retry_task);
112+
} else {
113+
retry_task();
114+
}
115+
});
116+
} else {
117+
// Pass on errors that are not `kUnavailable`.
118+
encode_task(status);
119+
}
120+
}
74121
}
75122

76123
} // namespace
@@ -153,18 +200,20 @@ void ImageEncodingImpeller::ConvertImageToRaster(
153200
};
154201

155202
if (dl_image->owning_context() != DlImage::OwningContext::kRaster) {
156-
DoConvertImageToRasterImpeller(dl_image, std::move(encode_task),
157-
is_gpu_disabled_sync_switch,
158-
impeller_context);
203+
DoConvertImageToRasterImpellerWithRetry(dl_image, std::move(encode_task),
204+
is_gpu_disabled_sync_switch,
205+
impeller_context,
206+
/*retry_runner=*/nullptr);
159207
return;
160208
}
161209

162210
raster_task_runner->PostTask([dl_image, encode_task = std::move(encode_task),
163211
io_task_runner, is_gpu_disabled_sync_switch,
164-
impeller_context]() mutable {
165-
DoConvertImageToRasterImpeller(dl_image, std::move(encode_task),
166-
is_gpu_disabled_sync_switch,
167-
impeller_context);
212+
impeller_context,
213+
raster_task_runner]() mutable {
214+
DoConvertImageToRasterImpellerWithRetry(
215+
dl_image, std::move(encode_task), is_gpu_disabled_sync_switch,
216+
impeller_context, raster_task_runner);
168217
});
169218
}
170219

0 commit comments

Comments
 (0)