Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flutter GPU] Add missing MSAA stuff. #55424

Merged
merged 8 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
12 changes: 12 additions & 0 deletions lib/gpu/context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@
#include "flutter/lib/ui/ui_dart_state.h"
#include "fml/make_copyable.h"
#include "impeller/core/platform.h"
#include "impeller/renderer/context.h"
#include "tonic/converter/dart_converter.h"

namespace flutter {
namespace gpu {

bool SupportsNormalOffscreenMSAA(const impeller::Context& context) {
auto& capabilities = context.GetCapabilities();
return capabilities->SupportsOffscreenMSAA() &&
!capabilities->SupportsImplicitResolvingMSAA();
}

IMPLEMENT_WRAPPERTYPEINFO(flutter_gpu, Context);

std::shared_ptr<impeller::Context> Context::default_context_;
Expand Down Expand Up @@ -111,3 +118,8 @@ extern int InternalFlutterGpu_Context_GetMinimumUniformByteAlignment(
flutter::gpu::Context* wrapper) {
return impeller::DefaultUniformAlignment();
}

extern bool InternalFlutterGpu_Context_GetSupportsOffscreenMSAA(
flutter::gpu::Context* wrapper) {
return flutter::gpu::SupportsNormalOffscreenMSAA(*wrapper->GetContext());
}
6 changes: 6 additions & 0 deletions lib/gpu/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
namespace flutter {
namespace gpu {

bool SupportsNormalOffscreenMSAA(const impeller::Context& context);

class Context : public RefCountedDartWrappable<Context> {
DEFINE_WRAPPERTYPEINFO();
FML_FRIEND_MAKE_REF_COUNTED(Context);
Expand Down Expand Up @@ -74,6 +76,10 @@ FLUTTER_GPU_EXPORT
extern int InternalFlutterGpu_Context_GetMinimumUniformByteAlignment(
flutter::gpu::Context* wrapper);

FLUTTER_GPU_EXPORT
extern bool InternalFlutterGpu_Context_GetSupportsOffscreenMSAA(
flutter::gpu::Context* wrapper);

} // extern "C"

#endif // FLUTTER_LIB_GPU_CONTEXT_H_
8 changes: 5 additions & 3 deletions lib/gpu/lib/src/command_buffer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ part of flutter_gpu;
typedef CompletionCallback<T> = void Function(bool success);

base class CommandBuffer extends NativeFieldWrapperClass1 {
final GpuContext _gpuContext;

/// Creates a new CommandBuffer.
CommandBuffer._(GpuContext gpuContext) {
_initialize(gpuContext);
CommandBuffer._(this._gpuContext) {
_initialize(_gpuContext);
}

RenderPass createRenderPass(RenderTarget renderTarget) {
return RenderPass._(this, renderTarget);
return RenderPass._(_gpuContext, this, renderTarget);
}

void submit({CompletionCallback? completionCallback}) {
Expand Down
16 changes: 16 additions & 0 deletions lib/gpu/lib/src/context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ base class GpuContext extends NativeFieldWrapperClass1 {
return _getMinimumUniformByteAlignment();
}

/// Whether the backend supports multisample anti-aliasing for offscreen
/// color and stencil attachments. A subset of OpenGLES-only devices do not
/// support this functionality.
///
/// Any texture created via [createTexture] is an offscreen texture.
/// There is currently no way to render directly against the "onscreen"
/// texture that the framework renders to, so all Flutter GPU textures are
/// "offscreen".
bool get doesSupportOffscreenMSAA {
Copy link
Member

Choose a reason for hiding this comment

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

Just a thought. Maybe this doesn't actually need to be exposed at all.Just let folks construct the same MSAA or non-MSAA topolgy, and if its not supported then ignore the transient attachments and use the resolve attachment.

Practically speaking, the number of devices flutter targets that does not support MSAA is sooo small. I don't know if its worth carrying around in our API.

Additionaly, I don't know if this handles it but we need to do MSAA a bit differently for GLES. We don't ever actually allocate the transient attachment.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oooh, I think for the common case of kMultisampleResolve, coercing to kStore and ignoring the multisample texture just about works. But unfortunately, things get funky with kStoreAndMultisampleResolve and I don't think we can reasonably assume the user's intent.

Additionaly, I don't know if this handles it but we need to do MSAA a bit differently for GLES. We don't ever actually allocate the transient attachment.

IIRC we did this in a way where it automagically does the right thing when you try to allocate a transient multisample texture on GLES (unless I'm thinking of the wrong thing). :)

If there's special HAL usage surrounding this, could you link to where we're doing it in the 2D renderer to help jog my memory?

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if it would work automatically or not, see

if (context.GetCapabilities()->SupportsImplicitResolvingMSAA()) {

Copy link
Member Author

Choose a reason for hiding this comment

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

I think SupportsImplicitResolvingMSAA is just for MSAA against the onscreen texture

Copy link
Member

Choose a reason for hiding this comment

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

No, this is used for all offscreen MSAA on the GLES backend.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh I'm remembering this now. I don't know if I can easily hide this complexity under the API. With the way the HAL currently works, I would have to surface and document SupportsImplicitResolvingMSAA. The user is in an alternate reality where they have to creates, attach, and bind fake multisample textures.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure the right way to fix this in the API, but maybe consider only supporting MSAA on metal/vulkan for now and coming back to GLES?

Copy link
Member Author

@bdero bdero Oct 19, 2024

Choose a reason for hiding this comment

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

Yeah. I changed this to add a graceful fallback to not use MSAA when "normal" MSAA support is not possible. I kept the doesSupportOffscreenMSAA check, but now it also returns false if "implicit" MSAA is available.

A renderer could easily use this check to fallback to other AA techniques (such as FXAA) when MSAA is unavailable.

return _getSupportsOffscreenMSAA();
}

/// Allocates a new region of GPU-resident memory.
///
/// The [storageMode] must be either [StorageMode.hostVisible] or
Expand Down Expand Up @@ -134,6 +146,10 @@ base class GpuContext extends NativeFieldWrapperClass1 {
@Native<Int Function(Pointer<Void>)>(
symbol: 'InternalFlutterGpu_Context_GetMinimumUniformByteAlignment')
external int _getMinimumUniformByteAlignment();

@Native<Bool Function(Pointer<Void>)>(
symbol: 'InternalFlutterGpu_Context_GetSupportsOffscreenMSAA')
external bool _getSupportsOffscreenMSAA();
}

/// The default graphics context.
Expand Down
61 changes: 60 additions & 1 deletion lib/gpu/lib/src/render_pass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,37 @@ base class ColorAttachment {

Texture texture;
Texture? resolveTexture;

void _validate() {
if (resolveTexture != null) {
if (resolveTexture!.format != texture.format) {
throw Exception(
"ColorAttachment MSAA resolve texture must have the same format as the texture");
}
if (resolveTexture!.width != texture.width ||
resolveTexture!.height != texture.height) {
throw Exception(
"ColorAttachment MSAA resolve texture must have the same dimensions as the texture");
}
if (resolveTexture!.sampleCount != 1) {
throw Exception(
"ColorAttachment MSAA resolve texture must have a sample count of 1");
}
if (texture.sampleCount <= 1) {
throw Exception(
"ColorAttachment must have a sample count greater than 1 when a MSAA resolve texture is set");
}
if (storeAction != StoreAction.multisampleResolve &&
storeAction != StoreAction.storeAndMultisampleResolve) {
throw Exception(
"ColorAttachment StoreAction must be multisampleResolve or storeAndMultisampleResolve when a resolve texture is set");
}
if (resolveTexture!.storageMode == StorageMode.deviceTransient) {
throw Exception(
"ColorAttachment MSAA resolve texture must not have a storage mode of deviceTransient");
}
}
}
}

base class DepthStencilAttachment {
Expand All @@ -43,6 +74,19 @@ base class DepthStencilAttachment {
int stencilClearValue;

Texture texture;

void _validate() {
if (texture.storageMode == StorageMode.deviceTransient) {
if (depthLoadAction == LoadAction.load) {
throw Exception(
"DepthStencilAttachment depthLoadAction must not be load when the texture has a storage mode of deviceTransient");
}
if (stencilLoadAction == LoadAction.load) {
throw Exception(
"DepthStencilAttachment stencilLoadAction must not be load when the texture has a storage mode of deviceTransient");
}
}
}
}

base class StencilConfig {
Expand Down Expand Up @@ -117,13 +161,28 @@ base class RenderTarget {
colorAttachments: [colorAttachment],
depthStencilAttachment: depthStencilAttachment);

_validate() {
for (final color in colorAttachments) {
color._validate();
}
if (depthStencilAttachment != null) {
depthStencilAttachment!._validate();
}
}

final List<ColorAttachment> colorAttachments;
final DepthStencilAttachment? depthStencilAttachment;
}

base class RenderPass extends NativeFieldWrapperClass1 {
/// Creates a new RenderPass.
RenderPass._(CommandBuffer commandBuffer, RenderTarget renderTarget) {
RenderPass._(GpuContext gpuContext, CommandBuffer commandBuffer,
RenderTarget renderTarget) {
assert(() {
renderTarget._validate();
return true;
}());

_initialize();
String? error;
for (final (index, color) in renderTarget.colorAttachments.indexed) {
Expand Down
9 changes: 9 additions & 0 deletions lib/gpu/render_pass.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "impeller/renderer/pipeline.h"
#include "impeller/renderer/pipeline_descriptor.h"
#include "impeller/renderer/pipeline_library.h"
#include "lib/gpu/context.h"
#include "lib/ui/ui_dart_state.h"
#include "tonic/converter/dart_converter.h"

Expand Down Expand Up @@ -242,6 +243,14 @@ Dart_Handle InternalFlutterGpu_RenderPass_SetColorAttachment(
tonic::DartConverter<flutter::gpu::Texture*>::FromDart(
resolve_texture_wrapper);
desc.resolve_texture = resolve_texture->GetTexture();

// If the backend doesn't support normal MSAA, gracefully fallback to
// rendering without MSAA.
if (!flutter::gpu::SupportsNormalOffscreenMSAA(*wrapper->GetContext())) {
desc.texture = desc.resolve_texture;
desc.resolve_texture = nullptr;
desc.store_action = impeller::StoreAction::kStore;
}
}
wrapper->GetRenderTarget().SetColorAttachment(desc, color_attachment_index);
return Dart_Null();
Expand Down
116 changes: 88 additions & 28 deletions testing/dart/gpu_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ RenderPassState createSimpleRenderPass({Vector4? clearColor}) {
final gpu.Texture? depthStencilTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient, 100, 100,
format: gpu.gpuContext.defaultDepthStencilFormat);
assert(depthStencilTexture != null);

final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();

Expand All @@ -74,6 +75,66 @@ RenderPassState createSimpleRenderPass({Vector4? clearColor}) {
return RenderPassState(renderTexture, commandBuffer, renderPass);
}

RenderPassState createSimpleRenderPassWithMSAA() {
// Create transient MSAA attachments, which will live entirely in tile memory
// for most GPUs.

final gpu.Texture? renderTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient, 100, 100,
format: gpu.gpuContext.defaultColorFormat, sampleCount: 4);
assert(renderTexture != null);

final gpu.Texture? depthStencilTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient, 100, 100,
format: gpu.gpuContext.defaultDepthStencilFormat, sampleCount: 4);
assert(depthStencilTexture != null);

// Create the single-sample resolve texture that live in DRAM and will be
// drawn to the screen.

final gpu.Texture? resolveTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.devicePrivate, 100, 100,
format: gpu.gpuContext.defaultColorFormat);
assert(resolveTexture != null);

final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();

final gpu.RenderTarget renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(
texture: renderTexture!,
resolveTexture: resolveTexture,
storeAction: gpu.StoreAction.multisampleResolve),
depthStencilAttachment:
gpu.DepthStencilAttachment(texture: depthStencilTexture!));

final gpu.RenderPass renderPass =
commandBuffer.createRenderPass(renderTarget);

return RenderPassState(resolveTexture!, commandBuffer, renderPass);
}

void drawTriangle(RenderPassState state, Vector4 color) {
final gpu.RenderPipeline pipeline = createUnlitRenderPipeline();

state.renderPass.bindPipeline(pipeline);

final gpu.HostBuffer transients = gpu.gpuContext.createHostBuffer();
final gpu.BufferView vertices = transients.emplace(float32(<double>[
-0.5, 0.5, //
0.0, -0.5, //
0.5, 0.5, //
]));
final gpu.BufferView vertInfoData =
transients.emplace(unlitUBO(Matrix4.identity(), color));
state.renderPass.bindVertexBuffer(vertices, 3);

final gpu.UniformSlot vertInfo =
pipeline.vertexShader.getUniformSlot('VertInfo');
state.renderPass.bindUniform(vertInfo, vertInfoData);

state.renderPass.draw();
}

void main() async {
final ImageComparer comparer = await ImageComparer.create();

Expand Down Expand Up @@ -337,34 +398,7 @@ void main() async {
// Renders a green triangle pointing downwards.
test('Can render triangle', () async {
final state = createSimpleRenderPass();

final gpu.RenderPipeline pipeline = createUnlitRenderPipeline();
state.renderPass.bindPipeline(pipeline);

// Configure blending with defaults (just to test the bindings).
state.renderPass.setColorBlendEnable(true);
state.renderPass.setColorBlendEquation(gpu.ColorBlendEquation());

final gpu.HostBuffer transients = gpu.gpuContext.createHostBuffer();
final gpu.BufferView vertices = transients.emplace(float32(<double>[
-0.5, 0.5, //
0.0, -0.5, //
0.5, 0.5, //
]));
final gpu.BufferView vertInfoData = transients.emplace(float32(<double>[
1, 0, 0, 0, // mvp
0, 1, 0, 0, // mvp
0, 0, 1, 0, // mvp
0, 0, 0, 1, // mvp
0, 1, 0, 1, // color
]));
state.renderPass.bindVertexBuffer(vertices, 3);

final gpu.UniformSlot vertInfo =
pipeline.vertexShader.getUniformSlot('VertInfo');
state.renderPass.bindUniform(vertInfo, vertInfoData);
state.renderPass.draw();

drawTriangle(state, Colors.lime);
state.commandBuffer.submit();

final ui.Image image = state.renderTexture.asImage();
Expand Down Expand Up @@ -411,6 +445,32 @@ void main() async {
await comparer.addGoldenImage(image, 'flutter_gpu_test_triangle_polygon_mode.png');
}, skip: !impellerEnabled);

// Renders a green triangle pointing downwards, with 4xMSAA.
test('Can render triangle with MSAA', () async {
final state = createSimpleRenderPassWithMSAA();
drawTriangle(state, Colors.lime);
state.commandBuffer.submit();

final ui.Image image = state.renderTexture.asImage();
await comparer.addGoldenImage(image, 'flutter_gpu_test_triangle_msaa.png');
}, skip: !(impellerEnabled && gpu.gpuContext.doesSupportOffscreenMSAA));

test(
'Rendering with MSAA throws exception when offscreen MSAA is not supported',
() async {
try {
final state = createSimpleRenderPassWithMSAA();
drawTriangle(state, Colors.lime);
state.commandBuffer.submit();
fail('Exception not thrown when offscreen MSAA is not supported.');
} catch (e) {
expect(
e.toString(),
contains(
'The backend does not support multisample anti-aliasing for offscreen color and stencil attachments'));
}
}, skip: gpu.gpuContext.doesSupportOffscreenMSAA);

// Renders a hollow green triangle pointing downwards.
test('Can render hollowed out triangle using stencil ops', () async {
final state = createSimpleRenderPass();
Expand Down