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

Composite multiple layers in Windows software rendering #51759

Merged
merged 20 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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
111 changes: 99 additions & 12 deletions shell/platform/windows/compositor_software.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,105 @@ bool CompositorSoftware::Present(FlutterViewId view_id,
return view->ClearSoftwareBitmap();
}

// TODO: Support compositing layers and platform views.
// See: https://github.com/flutter/flutter/issues/31713
FML_DCHECK(layers_count == 1);
FML_DCHECK(layers[0]->offset.x == 0 && layers[0]->offset.y == 0);
FML_DCHECK(layers[0]->type == kFlutterLayerContentTypeBackingStore);
FML_DCHECK(layers[0]->backing_store->type ==
kFlutterBackingStoreTypeSoftware);

const auto& backing_store = layers[0]->backing_store->software;

return view->PresentSoftwareBitmap(
backing_store.allocation, backing_store.row_bytes, backing_store.height);
// Bypass composition logic if there is only one layer.
if (layers_count == 1) {
auto& layer = *layers[0];
if (layer.type == kFlutterLayerContentTypeBackingStore &&
layer.offset.x == 0 && layer.offset.y == 0) {
auto& backing_store = *layer.backing_store;
FML_DCHECK(backing_store.type == kFlutterBackingStoreTypeSoftware);
auto& software = backing_store.software;
return view->PresentSoftwareBitmap(software.allocation,
software.row_bytes, software.height);
}
}

// Composite many layers.
int x_min = INT_MAX;
Copy link
Member

Choose a reason for hiding this comment

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

Could we split this up and have a helper for the compositing step?

int x_max = INT_MIN;
int y_min = INT_MAX;
int y_max = INT_MIN;
for (const FlutterLayer** layer = layers; layer < layers + layers_count;
layer++) {
auto& offset = (*layer)->offset;
auto& size = (*layer)->size;
x_min = std::min(x_min, static_cast<int>(offset.x));
y_min = std::min(y_min, static_cast<int>(offset.y));
x_max = std::max(x_max, static_cast<int>(offset.x + size.width));
y_max = std::max(y_max, static_cast<int>(offset.y + size.height));
}

int width = x_max - x_min;
int height = y_max - y_min;
std::vector<uint32_t> allocation(width * height, 0xff000000);

for (const FlutterLayer** layer = layers; layer < layers + layers_count;
layer++) {
// TODO(schectman): handle platform view type layers.
// https://github.com/flutter/flutter/issues/143375
if ((*layer)->type == kFlutterLayerContentTypeBackingStore) {
BlendLayer(allocation, **layer, x_min, y_min, width, height);
} else {
FML_UNREACHABLE();
return false;
}
}

return view->PresentSoftwareBitmap(static_cast<void*>(allocation.data()),
width * 4, height);
Copy link
Member

Choose a reason for hiding this comment

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

For someone not familiar with the bitmap allocation, this 4 might look like a magic value. Could we add a constant for the 4 with a quick comment for clarity?

}

void CompositorSoftware::BlendLayer(std::vector<uint32_t>& allocation,
Copy link
Member

Choose a reason for hiding this comment

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

Instead of an instance method that's declared in the header, should we make this be a top-level function in an anonymous namespace in this file?

In my mind the header file is best for public APIs or methods that need to interact with instance fields. However, this helper is self-contained logic that doesn't require any instance members.

const FlutterLayer& layer,
int x_min,
int y_min,
int width,
int height) const {
FML_DCHECK(layer.type == kFlutterLayerContentTypeBackingStore);
auto& backing_store = *layer.backing_store;
FML_DCHECK(backing_store.type == kFlutterBackingStoreTypeSoftware);
auto src_data =
static_cast<const uint32_t*>(backing_store.software.allocation);
auto& offset = layer.offset;
auto& size = layer.size;

for (int y_src = 0; y_src < size.height; y_src++) {
Copy link
Member

Choose a reason for hiding this comment

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

Will take a closer look, but first thoughts:

  1. but it feels like it should be possible to update the loop conditions to avoid the two < 0, >= height checks below.
  2. this feels like there should be a way to parallelise this a bit

Copy link
Contributor Author

@yaakovschectman yaakovschectman Apr 4, 2024

Choose a reason for hiding this comment

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

I suppose the obvious way to parallelize this would be to use the GPU, but this is the software compositor, so that seems self defeating.
Also, I will test adjusting the loop statements once it rebuilds.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Feel free to check now.

Copy link
Member

@loic-sharma loic-sharma Apr 5, 2024

Choose a reason for hiding this comment

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

I suspect the right way to parallelize would be to vectorize this code if possible... I wonder if Windows has any helpers that can do efficient bitmap blending? If not, that seems like too much work for an MVP.

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 only ones I'm familiar with work with HDCs instead of pixel data in memory, and so we would need to create those new DCs per layer to apply them as far as I understand

int y_dst = y_src + offset.y - y_min;
if (y_dst < 0) {
continue;
}
if (y_dst >= height) {
break;
}
for (int x_src = 0; x_src < size.width; x_src++) {
int x_dst = x_src + offset.x + x_min;
if (x_dst < 0) {
continue;
}
if (x_dst >= width) {
break;
}
size_t i_src = y_src * size.width + x_src;
size_t i_dst = y_dst * width + x_dst;
uint32_t src = src_data[i_src];
uint32_t dst = allocation[i_dst];

int r_src = (src >> 0) & 0xff;
int g_src = (src >> 8) & 0xff;
int b_src = (src >> 16) & 0xff;
int a_src = (src >> 24) & 0xff;

int r_dst = (dst >> 0) & 0xff;
int g_dst = (dst >> 8) & 0xff;
int b_dst = (dst >> 16) & 0xff;

int r = (r_dst * 255 + (r_src - r_dst) * a_src) / 255;
int g = (g_dst * 255 + (g_src - g_dst) * a_src) / 255;
int b = (b_dst * 255 + (b_src - b_dst) * a_src) / 255;

allocation[i_dst] = (r << 0) | (g << 8) | (b << 16) | (0xff << 24);
}
}
}

} // namespace flutter
7 changes: 7 additions & 0 deletions shell/platform/windows/compositor_software.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ class CompositorSoftware : public Compositor {
size_t layers_count) override;

private:
void BlendLayer(std::vector<uint32_t>& allocation,
const FlutterLayer& layer,
int x_min,
int y_min,
int width,
int height) const;

FlutterWindowsEngine* engine_;
};

Expand Down
109 changes: 109 additions & 0 deletions shell/platform/windows/compositor_software_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,114 @@ TEST_F(CompositorSoftwareTest, HeadlessPresentIgnored) {
ASSERT_TRUE(compositor.CollectBackingStore(&backing_store));
}

// Test compositing an upper layer on a base layer, each 2x2 pixels.
// Base layer has opaque pixel values:
// BLACK RED
// GREEN WHITE
// Overlay layer has pixel values:
// RED: 127 WHITE: 0
// BLUE: 127 BLACK: 255
TEST_F(CompositorSoftwareTest, PresentMultiLayers) {
Copy link
Member

Choose a reason for hiding this comment

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

Could we also have a test for layers that don't overlap fully?

UseEngineWithView();

auto compositor = CompositorSoftware{engine()};

FlutterBackingStoreConfig config = {sizeof(config), {2, 2}};
FlutterBackingStore backing_store0 = {sizeof(FlutterBackingStore), nullptr};
FlutterBackingStore backing_store1 = {sizeof(FlutterBackingStore), nullptr};

ASSERT_TRUE(compositor.CreateBackingStore(config, &backing_store0));
ASSERT_TRUE(compositor.CreateBackingStore(config, &backing_store1));

uint32_t pixels0[4] = {0xff000000, 0xff0000ff, 0xff00ff00, 0xffffffff};
uint32_t pixels1[4] = {0x7f0000ff, 0x00ffffff, 0x7fff0000, 0xff000000};

std::memcpy(const_cast<void*>(backing_store0.software.allocation), pixels0,
sizeof(uint32_t) * 4);
std::memcpy(const_cast<void*>(backing_store1.software.allocation), pixels1,
sizeof(uint32_t) * 4);

FlutterLayer layer0 = {};
layer0.type = kFlutterLayerContentTypeBackingStore;
layer0.backing_store = &backing_store0;
layer0.offset = {0, 0};
layer0.size = {2, 2};

FlutterLayer layer1 = layer0;
layer1.backing_store = &backing_store1;
const FlutterLayer* layer_ptr[2] = {&layer0, &layer1};

EXPECT_CALL(*view(), PresentSoftwareBitmap)
.WillOnce([&](const void* allocation, size_t row_bytes, size_t height) {
auto pixel_data = static_cast<const uint32_t*>(allocation);
EXPECT_EQ(row_bytes, 2 * sizeof(uint32_t));
EXPECT_EQ(height, 2);
EXPECT_EQ(pixel_data[0], 0xff00007f);
EXPECT_EQ(pixel_data[1], 0xff0000ff);
EXPECT_EQ(pixel_data[2], 0xff7f8000);
EXPECT_EQ(pixel_data[3], 0xff000000);
return true;
});
EXPECT_TRUE(compositor.Present(view()->view_id(), layer_ptr, 2));

ASSERT_TRUE(compositor.CollectBackingStore(&backing_store0));
ASSERT_TRUE(compositor.CollectBackingStore(&backing_store1));
}

// Test compositing layers with offsets.
// 0th layer is a single red pixel in the top-left.
// 1st layer is a row of two blue pixels on the second row.
TEST_F(CompositorSoftwareTest, PresentOffsetLayers) {
UseEngineWithView();

auto compositor = CompositorSoftware{engine()};

FlutterBackingStoreConfig config0 = {sizeof(FlutterBackingStoreConfig),
{1, 1}};
FlutterBackingStore backing_store0 = {sizeof(FlutterBackingStore), nullptr};
FlutterBackingStoreConfig config1 = {sizeof(FlutterBackingStoreConfig),
{2, 1}};
FlutterBackingStore backing_store1 = {sizeof(FlutterBackingStore), nullptr};

ASSERT_TRUE(compositor.CreateBackingStore(config0, &backing_store0));
ASSERT_TRUE(compositor.CreateBackingStore(config1, &backing_store1));

uint32_t pixels0 = 0xff0000ff;
uint32_t pixels1[2] = {0xffff0000, 0xffff0000};

std::memcpy(const_cast<void*>(backing_store0.software.allocation), &pixels0,
sizeof(uint32_t) * 1);
std::memcpy(const_cast<void*>(backing_store1.software.allocation), pixels1,
sizeof(uint32_t) * 2);

FlutterLayer layer0 = {};
layer0.type = kFlutterLayerContentTypeBackingStore;
layer0.backing_store = &backing_store0;
layer0.offset = {0, 0};
layer0.size = {1, 1};

FlutterLayer layer1 = layer0;
layer1.backing_store = &backing_store1;
layer1.offset = {0, 1};
layer1.size = {2, 1};
const FlutterLayer* layer_ptr[2] = {&layer0, &layer1};

EXPECT_CALL(*view(), PresentSoftwareBitmap)
.WillOnce([&](const void* allocation, size_t row_bytes, size_t height) {
auto pixel_data = static_cast<const uint32_t*>(allocation);
EXPECT_EQ(row_bytes, 2 * sizeof(uint32_t));
EXPECT_EQ(height, 2);
EXPECT_EQ(pixel_data[0], 0xff0000ff);
EXPECT_EQ(pixel_data[1], 0xff000000);
EXPECT_EQ(pixel_data[2], 0xffff0000);
EXPECT_EQ(pixel_data[3], 0xffff0000);
return true;
});
EXPECT_TRUE(compositor.Present(view()->view_id(), layer_ptr, 2));

ASSERT_TRUE(compositor.CollectBackingStore(&backing_store0));
ASSERT_TRUE(compositor.CollectBackingStore(&backing_store1));
}

} // namespace testing
} // namespace flutter