Skip to content

Commit

Permalink
[macOS] Implement unobstructed platform views (flutter#42960)
Browse files Browse the repository at this point in the history
Fixes flutter/flutter#129073

## Changes to Embedder API

Introduced `FlutterRegion` to represent arbitrary region:
```cpp
typedef struct {
  /// The size of this struct. Must be sizeof(FlutterRegion).
  size_t struct_size;
  /// Number of rectangles in the region.
  size_t rects_count;
  /// The rectangles that make up the region.
  FlutterRect* rects;
} FlutterRegion;
```

Note that this is identical to struct `FlutterDamage` with more generic
naming. Maybe down the line we could deprecate `FlutterDamage` and use
`FlutterRegion` instead.

Introduced `FlutterBackingStorePresentInfo`:
```cpp

typedef struct {
  size_t struct_size;

  /// The area of the backing store that contains Flutter contents. Pixels
  /// outside of this area are transparent and the embedder may choose not
  /// to render them. Coordinates are in physical pixels.
  FlutterRegion* paint_region;
} FlutterBackingStorePresentInfo;
```
In future this struct may also contain more precise hit test region
(when framework supports it) and/or information relevant to partial
repaint (buffer damage, frame damage).

Added a `backing_store_present_info` field to `FlutterLayer`:

```cpp
typedef struct {
  ...
  /// Extra information for the backing store that the embedder may
  /// use during presentation.
  FlutterBackingStorePresentInfo* backing_store_present_info;
} FlutterLayer;
```

## Changes to the macOS embedder

This PR adds support for `FLTEnableSurfaceDebugInfo` flag in main bundle
`Info.plist` that enables visual indicators of overlay layers.

## Example of unobstructed platform views


https://github.com/flutter/flutter/assets/96958/09a75eee-316b-4d53-a8b4-d6bb4e1e52f7

## 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] and the [C++,
Objective-C, Java style guides].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I added new tests to check the change I am making or feature I am
adding, or Hixie said the PR is test-exempt. See [testing the engine]
for instructions on writing and running engine tests.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I signed the [CLA].
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
  • Loading branch information
knopp authored Aug 31, 2023
1 parent 1f1071d commit 30f69f3
Show file tree
Hide file tree
Showing 14 changed files with 872 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@
info.surface = surface;
info.offset = CGPointMake(layer->offset.x, layer->offset.y);
info.zIndex = i;
FlutterBackingStorePresentInfo* present_info = layer->backing_store_present_info;
if (present_info != nullptr && present_info->paint_region != nullptr) {
auto paint_region = present_info->paint_region;
// Safe because the size of FlutterRect is not expected to change.
info.paintRegion = std::vector<FlutterRect>(
paint_region->rects, paint_region->rects + paint_region->rects_count);
}
[surfaces addObject:info];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>

#include <vector>

#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterSurface.h"

/**
Expand All @@ -15,6 +17,7 @@
@property(readwrite, strong, nonatomic, nonnull) FlutterSurface* surface;
@property(readwrite, nonatomic) CGPoint offset;
@property(readwrite, nonatomic) size_t zIndex;
@property(readwrite, nonatomic) std::vector<FlutterRect> paintRegion;

@end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ @interface FlutterSurfaceManager () {

// Currently visible layers.
NSMutableArray<CALayer*>* _layers;

// Whether to highlight borders of overlay surfaces. Determined by
// FLTEnableSurfaceDebugInfo value in main bundle Info.plist.
NSNumber* _enableSurfaceDebugInfo;
CATextLayer* _infoLayer;
}

/**
Expand All @@ -38,6 +43,60 @@ - (void)commit:(NSArray<FlutterSurfacePresentInfo*>*)surfaces;

@end

static NSColor* GetBorderColorForLayer(int layer) {
NSArray* colors = @[
[NSColor yellowColor],
[NSColor cyanColor],
[NSColor magentaColor],
[NSColor greenColor],
[NSColor purpleColor],
[NSColor orangeColor],
[NSColor blueColor],
];
return colors[layer % colors.count];
}

/// Creates sublayers for given layer, each one displaying a portion of the
/// of the surface determined by a rectangle in the provided paint region.
static void UpdateContentSubLayers(CALayer* layer,
IOSurfaceRef surface,
CGFloat scale,
CGSize surfaceSize,
NSColor* borderColor,
const std::vector<FlutterRect>& paintRegion) {
// Adjust sublayer count to paintRegion count.
while (layer.sublayers.count > paintRegion.size()) {
[layer.sublayers.lastObject removeFromSuperlayer];
}

while (layer.sublayers.count < paintRegion.size()) {
CALayer* newLayer = [CALayer layer];
[layer addSublayer:newLayer];
}

for (size_t i = 0; i < paintRegion.size(); i++) {
CALayer* subLayer = [layer.sublayers objectAtIndex:i];
const auto& rect = paintRegion[i];
subLayer.frame = CGRectMake(rect.left / scale, rect.top / scale,
(rect.right - rect.left) / scale, (rect.bottom - rect.top) / scale);

double width = surfaceSize.width;
double height = surfaceSize.height;

subLayer.contentsRect =
CGRectMake(rect.left / width, rect.top / height, (rect.right - rect.left) / width,
(rect.bottom - rect.top) / height);

if (borderColor != nil) {
// Visualize sublayer
subLayer.borderColor = borderColor.CGColor;
subLayer.borderWidth = 1.0;
}

subLayer.contents = (__bridge id)surface;
}
}

@implementation FlutterSurfaceManager

- (instancetype)initWithDevice:(id<MTLDevice>)device
Expand Down Expand Up @@ -77,6 +136,17 @@ - (FlutterSurface*)surfaceForSize:(CGSize)size {
return surface;
}

- (BOOL)enableSurfaceDebugInfo {
if (_enableSurfaceDebugInfo == nil) {
_enableSurfaceDebugInfo =
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"FLTEnableSurfaceDebugInfo"];
if (_enableSurfaceDebugInfo == nil) {
_enableSurfaceDebugInfo = @NO;
}
}
return [_enableSurfaceDebugInfo boolValue];
}

- (void)commit:(NSArray<FlutterSurfacePresentInfo*>*)surfaces {
FML_DCHECK([NSThread isMainThread]);

Expand All @@ -100,16 +170,38 @@ - (void)commit:(NSArray<FlutterSurfacePresentInfo*>*)surfaces {
[_layers addObject:layer];
}

bool enableSurfaceDebugInfo = self.enableSurfaceDebugInfo;

// Update contents of surfaces.
for (size_t i = 0; i < surfaces.count; ++i) {
FlutterSurfacePresentInfo* info = surfaces[i];
CALayer* layer = _layers[i];
CGFloat scale = _containingLayer.contentsScale;
layer.frame = CGRectMake(info.offset.x / scale, info.offset.y / scale,
info.surface.size.width / scale, info.surface.size.height / scale);
layer.contents = (__bridge id)info.surface.ioSurface;
if (i == 0) {
layer.frame = CGRectMake(info.offset.x / scale, info.offset.y / scale,
info.surface.size.width / scale, info.surface.size.height / scale);
layer.contents = (__bridge id)info.surface.ioSurface;
} else {
layer.frame = CGRectZero;
NSColor* borderColor = enableSurfaceDebugInfo ? GetBorderColorForLayer(i - 1) : nil;
UpdateContentSubLayers(layer, info.surface.ioSurface, scale, info.surface.size, borderColor,
info.paintRegion);
}
layer.zPosition = info.zIndex;
}

if (enableSurfaceDebugInfo) {
if (_infoLayer == nil) {
_infoLayer = [[CATextLayer alloc] init];
[_containingLayer addSublayer:_infoLayer];
_infoLayer.fontSize = 15;
_infoLayer.foregroundColor = [NSColor yellowColor].CGColor;
_infoLayer.frame = CGRectMake(15, 15, 300, 100);
_infoLayer.contentsScale = _containingLayer.contentsScale;
_infoLayer.zPosition = 100000;
}
_infoLayer.string = [NSString stringWithFormat:@"Surface count: %li", _layers.count];
}
}

static CGSize GetRequiredFrameSize(NSArray<FlutterSurfacePresentInfo*>* surfaces) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ - (instancetype)init {
self = [super initWithFrame:NSZeroRect];
if (self) {
[self setWantsLayer:YES];
self.layer.contentsScale = 2.0;
}
return self;
}
Expand All @@ -48,13 +49,16 @@ - (void)onPresent:(CGSize)frameSize withBlock:(nonnull dispatch_block_t)block {
delegate:testView];
}

static FlutterSurfacePresentInfo* CreatePresentInfo(FlutterSurface* surface,
CGPoint offset = CGPointZero,
size_t index = 0) {
static FlutterSurfacePresentInfo* CreatePresentInfo(
FlutterSurface* surface,
CGPoint offset = CGPointZero,
size_t index = 0,
const std::vector<FlutterRect>& paintRegion = {}) {
FlutterSurfacePresentInfo* res = [[FlutterSurfacePresentInfo alloc] init];
res.surface = surface;
res.offset = offset;
res.zIndex = index;
res.paintRegion = paintRegion;
return res;
}

Expand Down Expand Up @@ -160,6 +164,10 @@ - (void)onPresent:(CGSize)frameSize withBlock:(nonnull dispatch_block_t)block {
EXPECT_EQ(surface3, surface1);
}

inline bool operator==(const CGRect& lhs, const CGRect& rhs) {
return CGRectEqualToRect(lhs, rhs);
}

TEST(FlutterSurfaceManager, LayerManagement) {
TestView* testView = [[TestView alloc] init];
FlutterSurfaceManager* surfaceManager = CreateSurfaceManager(testView);
Expand All @@ -176,15 +184,68 @@ - (void)onPresent:(CGSize)frameSize withBlock:(nonnull dispatch_block_t)block {
auto surface2_2 = [surfaceManager surfaceForSize:CGSizeMake(20, 20)];
[surfaceManager present:@[
CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2)
CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
{
FlutterRect{0, 0, 20, 20},
FlutterRect{40, 0, 60, 20},
})
]
notify:nil];

EXPECT_EQ(testView.layer.sublayers.count, 2ul);
EXPECT_EQ([testView.layer.sublayers objectAtIndex:0].zPosition, 1.0);
EXPECT_EQ([testView.layer.sublayers objectAtIndex:1].zPosition, 2.0);
EXPECT_EQ(testView.layer.sublayers[0].zPosition, 1.0);
EXPECT_EQ(testView.layer.sublayers[1].zPosition, 2.0);
CALayer* firstOverlaySublayer;
{
NSArray<CALayer*>* sublayers = testView.layer.sublayers[1].sublayers;
EXPECT_EQ(sublayers.count, 2ul);
EXPECT_TRUE(CGRectEqualToRect(sublayers[0].frame, CGRectMake(0, 0, 10, 10)));
EXPECT_TRUE(CGRectEqualToRect(sublayers[1].frame, CGRectMake(20, 0, 10, 10)));
EXPECT_TRUE(CGRectEqualToRect(sublayers[0].contentsRect, CGRectMake(0, 0, 1, 1)));
EXPECT_TRUE(CGRectEqualToRect(sublayers[1].contentsRect, CGRectMake(2, 0, 1, 1)));
EXPECT_EQ(sublayers[0].contents, sublayers[1].contents);
firstOverlaySublayer = sublayers[0];
}
EXPECT_TRUE(CGSizeEqualToSize(testView.presentedFrameSize, CGSizeMake(70, 70)));

// Check second overlay sublayer is removed while first is reused and updated
[surfaceManager present:@[
CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
{
FlutterRect{0, 10, 20, 20},
})
]
notify:nil];
EXPECT_EQ(testView.layer.sublayers.count, 2ul);
{
NSArray<CALayer*>* sublayers = testView.layer.sublayers[1].sublayers;
EXPECT_EQ(sublayers.count, 1ul);
EXPECT_EQ(sublayers[0], firstOverlaySublayer);
EXPECT_TRUE(CGRectEqualToRect(sublayers[0].frame, CGRectMake(0, 5, 10, 5)));
}

// Check that second overlay sublayer is added back while first is reused and updated
[surfaceManager present:@[
CreatePresentInfo(surface2_1, CGPointMake(20, 10), 1),
CreatePresentInfo(surface2_2, CGPointMake(40, 50), 2,
{
FlutterRect{0, 0, 20, 20},
FlutterRect{40, 0, 60, 20},
})
]
notify:nil];

EXPECT_EQ(testView.layer.sublayers.count, 2ul);
{
NSArray<CALayer*>* sublayers = testView.layer.sublayers[1].sublayers;
EXPECT_EQ(sublayers.count, 2ul);
EXPECT_EQ(sublayers[0], firstOverlaySublayer);
EXPECT_TRUE(CGRectEqualToRect(sublayers[0].frame, CGRectMake(0, 0, 10, 10)));
EXPECT_TRUE(CGRectEqualToRect(sublayers[1].frame, CGRectMake(20, 0, 10, 10)));
EXPECT_EQ(sublayers[0].contents, sublayers[1].contents);
}

auto surface3_1 = [surfaceManager surfaceForSize:CGSizeMake(50, 30)];
[surfaceManager present:@[ CreatePresentInfo(surface3_1, CGPointMake(20, 10)) ] notify:nil];

Expand Down
25 changes: 25 additions & 0 deletions shell/platform/embedder/embedder.h
Original file line number Diff line number Diff line change
Expand Up @@ -1663,6 +1663,27 @@ typedef enum {
kFlutterLayerContentTypePlatformView,
} FlutterLayerContentType;

/// A region represented by a collection of non-overlapping rectangles.
typedef struct {
/// The size of this struct. Must be sizeof(FlutterRegion).
size_t struct_size;
/// Number of rectangles in the region.
size_t rects_count;
/// The rectangles that make up the region.
FlutterRect* rects;
} FlutterRegion;

/// Contains additional information about the backing store provided
/// during presentation to the embedder.
typedef struct {
size_t struct_size;

/// The area of the backing store that contains Flutter contents. Pixels
/// outside of this area are transparent and the embedder may choose not
/// to render them. Coordinates are in physical pixels.
FlutterRegion* paint_region;
} FlutterBackingStorePresentInfo;

typedef struct {
/// This size of this struct. Must be sizeof(FlutterLayer).
size_t struct_size;
Expand All @@ -1682,6 +1703,10 @@ typedef struct {
FlutterPoint offset;
/// The size of the layer (in physical pixels).
FlutterSize size;

/// Extra information for the backing store that the embedder may
/// use during presentation.
FlutterBackingStorePresentInfo* backing_store_present_info;
} FlutterLayer;

typedef bool (*FlutterBackingStoreCreateCallback)(
Expand Down
5 changes: 5 additions & 0 deletions shell/platform/embedder/embedder_external_view.cc
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ bool EmbedderExternalView::HasPlatformView() const {
return view_identifier_.platform_view_id.has_value();
}

std::list<SkRect> EmbedderExternalView::GetEngineRenderedContentsRegion(
const SkRect& query) const {
return slice_->searchNonOverlappingDrawnRects(query);
}

bool EmbedderExternalView::HasEngineRenderedContents() {
if (has_engine_rendered_contents_.has_value()) {
return has_engine_rendered_contents_.value();
Expand Down
2 changes: 2 additions & 0 deletions shell/platform/embedder/embedder_external_view.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ class EmbedderExternalView {

bool Render(const EmbedderRenderTarget& render_target);

std::list<SkRect> GetEngineRenderedContentsRegion(const SkRect& query) const;

private:
// End the recording of the slice.
// Noop if the slice's recording has already ended.
Expand Down
11 changes: 10 additions & 1 deletion shell/platform/embedder/embedder_external_view_embedder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,17 @@ void EmbedderExternalViewEmbedder::SubmitFrame(
// platform view.
if (external_view->HasEngineRenderedContents()) {
const auto& exteral_render_target = matched_render_targets.at(view_id);
const auto& external_view = pending_views_.at(view_id);
auto rect_list =
external_view->GetEngineRenderedContentsRegion(SkRect::MakeIWH(
pending_frame_size_.width(), pending_frame_size_.height()));
std::vector<SkIRect> rects;
rects.reserve(rect_list.size());
for (const auto& rect : rect_list) {
rects.push_back(rect.roundOut());
}
presented_layers.PushBackingStoreLayer(
exteral_render_target->GetBackingStore());
exteral_render_target->GetBackingStore(), rects);
}
}

Expand Down
31 changes: 30 additions & 1 deletion shell/platform/embedder/embedder_layers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ EmbedderLayers::EmbedderLayers(SkISize frame_size,

EmbedderLayers::~EmbedderLayers() = default;

void EmbedderLayers::PushBackingStoreLayer(const FlutterBackingStore* store) {
void EmbedderLayers::PushBackingStoreLayer(
const FlutterBackingStore* store,
const std::vector<SkIRect>& paint_region_vec) {
FlutterLayer layer = {};

layer.struct_size = sizeof(FlutterLayer);
Expand All @@ -35,6 +37,33 @@ void EmbedderLayers::PushBackingStoreLayer(const FlutterBackingStore* store) {
layer.size.width = transformed_layer_bounds.width();
layer.size.height = transformed_layer_bounds.height();

auto paint_region_rects = std::make_unique<std::vector<FlutterRect>>();
paint_region_rects->reserve(paint_region_vec.size());

for (const auto& rect : paint_region_vec) {
auto transformed_rect =
root_surface_transformation_.mapRect(SkRect::Make(rect));
paint_region_rects->push_back(FlutterRect{
transformed_rect.x(),
transformed_rect.y(),
transformed_rect.right(),
transformed_rect.bottom(),
});
}

auto paint_region = std::make_unique<FlutterRegion>();
paint_region->struct_size = sizeof(FlutterRegion);
paint_region->rects = paint_region_rects->data();
paint_region->rects_count = paint_region_rects->size();
rects_referenced_.push_back(std::move(paint_region_rects));

auto present_info = std::make_unique<FlutterBackingStorePresentInfo>();
present_info->struct_size = sizeof(FlutterBackingStorePresentInfo);
present_info->paint_region = paint_region.get();
regions_referenced_.push_back(std::move(paint_region));
layer.backing_store_present_info = present_info.get();

present_info_referenced_.push_back(std::move(present_info));
presented_layers_.push_back(layer);
}

Expand Down
Loading

0 comments on commit 30f69f3

Please sign in to comment.