Skip to content

Commit

Permalink
[Flutter GPU] Add missing MSAA stuff.
Browse files Browse the repository at this point in the history
  • Loading branch information
bdero committed Sep 25, 2024
1 parent 3e15127 commit e1b0e6a
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 38 deletions.
5 changes: 5 additions & 0 deletions lib/gpu/context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,8 @@ extern int InternalFlutterGpu_Context_GetMinimumUniformByteAlignment(
flutter::gpu::Context* wrapper) {
return impeller::DefaultUniformAlignment();
}

extern bool InternalFlutterGpu_Context_GetSupportsOffscreenMSAA(
flutter::gpu::Context* wrapper) {
return wrapper->GetContext()->GetCapabilities()->SupportsOffscreenMSAA();
}
4 changes: 4 additions & 0 deletions lib/gpu/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,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 {
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
87 changes: 82 additions & 5 deletions lib/gpu/lib/src/render_pass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,32 @@ base class ColorAttachment {

Texture texture;
Texture? resolveTexture;

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

assert(
texture.storageMode != StorageMode.deviceTransient ||
loadAction != LoadAction.load,
"ColorAttachment loadAction must not be load when the texture has a storage mode of deviceTransient");
}
}

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

Texture texture;
Texture? resolveTexture;

void _assertValid() {
if (resolveTexture != null) {
assert(resolveTexture!.format == texture.format,
"DepthStencilAttachment MSAA resolve texture must have the same format as the texture");
assert(
resolveTexture!.width == texture.width &&
resolveTexture!.height == texture.height,
"DepthStencilAttachment MSAA resolve texture must have the same dimensions as the texture");
assert(resolveTexture!.sampleCount == 1,
"DepthStencilAttachment MSAA resolve texture must have a sample count of 1");
assert(texture.sampleCount > 1,
"DepthStencilAttachment must have a sample count greater than 1 when a MSAA resolve texture is set");
assert(
depthStoreAction == StoreAction.multisampleResolve ||
depthStoreAction == StoreAction.storeAndMultisampleResolve,
"DepthStencilAttachment depthStoreAction must be multisampleResolve or storeAndMultisampleResolve when a resolve texture is set");
assert(
stencilStoreAction == StoreAction.multisampleResolve ||
stencilStoreAction == StoreAction.storeAndMultisampleResolve,
"DepthStencilAttachment stencilStoreAction must be multisampleResolve or storeAndMultisampleResolve when a resolve texture is set");
assert(resolveTexture!.storageMode != StorageMode.deviceTransient,
"DepthStencilAttachment MSAA resolve texture must not have a storage mode of deviceTransient");
}

if (texture.storageMode == StorageMode.deviceTransient) {
assert(
texture.storageMode != StorageMode.deviceTransient ||
depthLoadAction != LoadAction.load,
"DepthStencilAttachment depthLoadAction must not be load when the texture has a storage mode of deviceTransient");
assert(
texture.storageMode != StorageMode.deviceTransient ||
stencilLoadAction != LoadAction.load,
"DepthStencilAttachment stencilLoadAction must not be load when the texture has a storage mode of deviceTransient");
}
}
}

base class StencilConfig {
Expand Down Expand Up @@ -117,6 +180,15 @@ base class RenderTarget {
colorAttachments: [colorAttachment],
depthStencilAttachment: depthStencilAttachment);

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

final List<ColorAttachment> colorAttachments;
final DepthStencilAttachment? depthStencilAttachment;
}
Expand All @@ -132,7 +204,10 @@ int _colorToInt(ui.Color color) {

base class RenderPass extends NativeFieldWrapperClass1 {
/// Creates a new RenderPass.
RenderPass._(CommandBuffer commandBuffer, RenderTarget renderTarget) {
RenderPass._(GpuContext gpuContext, CommandBuffer commandBuffer,
RenderTarget renderTarget) {
renderTarget._assertValid();

_initialize();
String? error;
for (final (index, color) in renderTarget.colorAttachments.indexed) {
Expand All @@ -156,7 +231,8 @@ base class RenderPass extends NativeFieldWrapperClass1 {
ds.stencilLoadAction.index,
ds.stencilStoreAction.index,
ds.stencilClearValue,
ds.texture);
ds.texture,
ds.resolveTexture);
if (error != null) {
throw Exception(error);
}
Expand Down Expand Up @@ -291,8 +367,8 @@ base class RenderPass extends NativeFieldWrapperClass1 {
Texture? resolveTexture);

@Native<
Handle Function(
Pointer<Void>, Int, Int, Float, Int, Int, Int, Pointer<Void>)>(
Handle Function(Pointer<Void>, Int, Int, Float, Int, Int, Int,
Pointer<Void>, Handle)>(
symbol: 'InternalFlutterGpu_RenderPass_SetDepthStencilAttachment')
external String? _setDepthStencilAttachment(
int depthLoadAction,
Expand All @@ -301,7 +377,8 @@ base class RenderPass extends NativeFieldWrapperClass1 {
int stencilLoadAction,
int stencilStoreAction,
int stencilClearValue,
Texture texture);
Texture texture,
Texture? resolveTexture);

@Native<Handle Function(Pointer<Void>, Pointer<Void>)>(
symbol: 'InternalFlutterGpu_RenderPass_Begin')
Expand Down
16 changes: 15 additions & 1 deletion lib/gpu/render_pass.cc
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,24 @@ Dart_Handle InternalFlutterGpu_RenderPass_SetDepthStencilAttachment(
int stencil_load_action,
int stencil_store_action,
int stencil_clear_value,
flutter::gpu::Texture* texture) {
flutter::gpu::Texture* texture,
Dart_Handle resolve_texture_wrapper) {
flutter::gpu::Texture* resolve_texture = nullptr;

if (!Dart_IsNull(resolve_texture_wrapper)) {
resolve_texture = tonic::DartConverter<flutter::gpu::Texture*>::FromDart(
resolve_texture_wrapper);
}

{
impeller::DepthAttachment desc;
desc.load_action = flutter::gpu::ToImpellerLoadAction(depth_load_action);
desc.store_action = flutter::gpu::ToImpellerStoreAction(depth_store_action);
desc.clear_depth = depth_clear_value;
desc.texture = texture->GetTexture();
if (resolve_texture) {
desc.resolve_texture = resolve_texture->GetTexture();
}
wrapper->GetRenderTarget().SetDepthAttachment(desc);
}
{
Expand All @@ -257,6 +268,9 @@ Dart_Handle InternalFlutterGpu_RenderPass_SetDepthStencilAttachment(
flutter::gpu::ToImpellerStoreAction(stencil_store_action);
desc.clear_stencil = stencil_clear_value;
desc.texture = texture->GetTexture();
if (resolve_texture) {
desc.resolve_texture = resolve_texture->GetTexture();
}
wrapper->GetRenderTarget().SetStencilAttachment(desc);
}

Expand Down
3 changes: 2 additions & 1 deletion lib/gpu/render_pass.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ extern Dart_Handle InternalFlutterGpu_RenderPass_SetDepthStencilAttachment(
int stencil_load_action,
int stencil_store_action,
int stencil_clear_value,
flutter::gpu::Texture* texture);
flutter::gpu::Texture* texture,
Dart_Handle resolve_texture_wrapper);

FLUTTER_GPU_EXPORT
extern Dart_Handle InternalFlutterGpu_RenderPass_Begin(
Expand Down
120 changes: 92 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() {
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,70 @@ RenderPassState createSimpleRenderPass() {
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);

// TODO(bdero): Overwrite bindings with the same slot so we don't need to clear.
// https://github.com/flutter/flutter/issues/155335
state.renderPass.clearBindings();

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 @@ -327,40 +392,39 @@ 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();
await comparer.addGoldenImage(image, 'flutter_gpu_test_triangle.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

0 comments on commit e1b0e6a

Please sign in to comment.