blob: 850dfb57aa5a6b8068ea766957af3c70f08e23d4 [file]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter GPU API tests.
// The flutter_gpu package is located at //flutter/impeller/lib/gpu.
// ignore_for_file: avoid_relative_lib_imports
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:test/test.dart';
import 'package:vector_math/vector_math.dart';
import '../../lib/gpu/lib/gpu.dart' as gpu;
import 'goldens.dart';
import 'impeller_enabled.dart';
ByteData float32(List<double> values) {
return Float32List.fromList(values).buffer.asByteData();
}
ByteData unlitUBO(Matrix4 mvp, Vector4 color) {
return float32(<double>[
mvp[0], mvp[1], mvp[2], mvp[3], //
mvp[4], mvp[5], mvp[6], mvp[7], //
mvp[8], mvp[9], mvp[10], mvp[11], //
mvp[12], mvp[13], mvp[14], mvp[15], //
color.r, color.g, color.b, color.a,
]);
}
gpu.RenderPipeline createUnlitRenderPipeline() {
final gpu.ShaderLibrary? library = gpu.ShaderLibrary.fromAsset('test.shaderbundle');
assert(library != null);
final gpu.Shader? vertex = library!['UnlitVertex'];
assert(vertex != null);
final gpu.Shader? fragment = library['UnlitFragment'];
assert(fragment != null);
return gpu.gpuContext.createRenderPipeline(vertex!, fragment!);
}
class RenderPassState {
RenderPassState(this.renderTexture, this.commandBuffer, this.renderPass);
final gpu.Texture renderTexture;
final gpu.CommandBuffer commandBuffer;
final gpu.RenderPass renderPass;
}
/// Create a simple RenderPass with simple color and depth-stencil attachments.
RenderPassState createSimpleRenderPass({Vector4? clearColor}) {
final gpu.Texture renderTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.devicePrivate,
100,
100,
);
final gpu.Texture depthStencilTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient,
100,
100,
format: gpu.gpuContext.defaultDepthStencilFormat,
);
final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
final renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: renderTexture, clearValue: clearColor),
depthStencilAttachment: gpu.DepthStencilAttachment(texture: depthStencilTexture),
);
final gpu.RenderPass renderPass = commandBuffer.createRenderPass(renderTarget);
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,
);
final gpu.Texture depthStencilTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient,
100,
100,
format: gpu.gpuContext.defaultDepthStencilFormat,
sampleCount: 4,
);
// 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,
);
final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
final 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);
final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo');
state.renderPass.bindUniform(vertInfo, vertInfoData);
state.renderPass.draw(3);
}
void main() async {
final ImageComparer comparer = await ImageComparer.create();
test('gpu.context throws exception when Impeller is not enabled', () async {
try {
// ignore: unnecessary_statements
gpu.gpuContext; // Force the context to instantiate.
if (!impellerEnabled) {
fail('Exception not thrown, but Impeller is not enabled.');
}
} catch (e) {
if (impellerEnabled && flutterGpuEnabled) {
fail('Exception thrown even though Impeller and Flutter GPU are both enabled: $e');
}
if (!impellerEnabled) {
expect(e.toString(), contains('Flutter GPU requires the Impeller rendering backend'));
}
}
});
test('gpu.context throws exception when Flutter GPU is not enabled', () async {
try {
// ignore: unnecessary_statements
gpu.gpuContext; // Force the context to instantiate.
if (!flutterGpuEnabled) {
fail('Exception not thrown, but Flutter GPU is not enabled.');
}
} catch (e) {
if (flutterGpuEnabled && impellerEnabled) {
fail('Exception thrown even though Flutter GPU and Impeller are both enabled: $e');
}
if (impellerEnabled) {
expect(
e.toString(),
contains('Flutter GPU must be enabled via the Flutter GPU manifest setting'),
);
}
}
});
test('gpu.context is available when Impeller and Flutter GPU are enabled', () async {
try {
// ignore: unnecessary_statements
gpu.gpuContext; // Force the context to instantiate.
} catch (e) {
if (impellerEnabled && flutterGpuEnabled) {
fail('Exception thrown even though Impeller and Flutter GPU are enabled: $e');
}
}
});
test('GpuContext.minimumUniformByteAlignment', () async {
final int alignment = gpu.gpuContext.minimumUniformByteAlignment;
expect(alignment, greaterThanOrEqualTo(16));
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('HostBuffer.emplace', () async {
final gpu.HostBuffer hostBuffer = gpu.gpuContext.createHostBuffer();
final gpu.BufferView view0 = hostBuffer.emplace(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
);
expect(view0.offsetInBytes, 0);
expect(view0.lengthInBytes, 4);
final gpu.BufferView view1 = hostBuffer.emplace(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
);
expect(view1.offsetInBytes, equals(gpu.gpuContext.minimumUniformByteAlignment));
expect(view1.lengthInBytes, 4);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('HostBuffer.reset', () async {
final gpu.HostBuffer hostBuffer = gpu.gpuContext.createHostBuffer();
final gpu.BufferView view0 = hostBuffer.emplace(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
);
expect(view0.offsetInBytes, 0);
expect(view0.lengthInBytes, 4);
hostBuffer.reset();
final gpu.BufferView view1 = hostBuffer.emplace(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
);
expect(view1.offsetInBytes, 0);
expect(view1.lengthInBytes, 4);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('HostBuffer reuses DeviceBuffers after N frames', () async {
final gpu.HostBuffer hostBuffer = gpu.gpuContext.createHostBuffer();
final gpu.BufferView view0 = hostBuffer.emplace(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
);
for (var i = 0; i < hostBuffer.frameCount; i++) {
hostBuffer.reset();
}
final gpu.BufferView view1 = hostBuffer.emplace(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
);
expect(view0.buffer, equals(view1.buffer));
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('GpuContext.createDeviceBuffer', () async {
final gpu.DeviceBuffer deviceBuffer = gpu.gpuContext.createDeviceBuffer(
gpu.StorageMode.hostVisible,
4,
);
expect(deviceBuffer.sizeInBytes, 4);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('DeviceBuffer.overwrite', () async {
final gpu.DeviceBuffer deviceBuffer = gpu.gpuContext.createDeviceBuffer(
gpu.StorageMode.hostVisible,
4,
);
final bool success = deviceBuffer.overwrite(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
);
deviceBuffer.flush();
expect(success, true);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('DeviceBuffer.overwrite fails when out of bounds', () async {
final gpu.DeviceBuffer deviceBuffer = gpu.gpuContext.createDeviceBuffer(
gpu.StorageMode.hostVisible,
4,
);
final bool success = deviceBuffer.overwrite(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
destinationOffsetInBytes: 1,
);
deviceBuffer.flush();
expect(success, false);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('DeviceBuffer.overwrite throws for negative destination offset', () async {
final gpu.DeviceBuffer deviceBuffer = gpu.gpuContext.createDeviceBuffer(
gpu.StorageMode.hostVisible,
4,
);
try {
deviceBuffer.overwrite(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
destinationOffsetInBytes: -1,
);
deviceBuffer.flush();
fail('Exception not thrown for negative destination offset.');
} catch (e) {
expect(e.toString(), contains('destinationOffsetInBytes must be positive'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('GpuContext.createTexture', () async {
final gpu.Texture texture = gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 100, 100);
// Check the defaults.
expect(texture.coordinateSystem, gpu.TextureCoordinateSystem.renderToTexture);
expect(texture.width, 100);
expect(texture.height, 100);
expect(texture.storageMode, gpu.StorageMode.hostVisible);
expect(texture.sampleCount, 1);
expect(texture.textureType, gpu.TextureType.texture2D);
expect(texture.format, gpu.PixelFormat.r8g8b8a8UNormInt);
expect(texture.enableRenderTargetUsage, true);
expect(texture.enableShaderReadUsage, true);
expect(!texture.enableShaderWriteUsage, true);
expect(texture.format.bytesPerBlock, 4);
expect(texture.getBaseMipLevelSizeInBytes(), 40000);
expect(texture.mipLevelCount, 1);
expect(texture.sliceCount, 1);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('GpuContext.createTexture allocates an r32Float texture', () async {
final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
4,
4,
format: gpu.PixelFormat.r32Float,
);
expect(texture.format, gpu.PixelFormat.r32Float);
expect(texture.format.bytesPerBlock, 4);
expect(texture.getBaseMipLevelSizeInBytes(), 4 * 4 * 4);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('PixelFormat block introspection matches the format table', () async {
// Uncompressed formats report block dims of 1.
expect(gpu.PixelFormat.r8g8b8a8UNormInt.isCompressed, false);
expect(gpu.PixelFormat.r8g8b8a8UNormInt.blockWidth, 1);
expect(gpu.PixelFormat.r8g8b8a8UNormInt.blockHeight, 1);
expect(gpu.PixelFormat.r8g8b8a8UNormInt.bytesPerBlock, 4);
expect(gpu.PixelFormat.r8g8b8a8UNormInt.compressionFamily, isNull);
expect(gpu.PixelFormat.r32Float.bytesPerBlock, 4);
// BC1 / ETC2 RGB8: 4x4 blocks, 8 bytes per block.
expect(gpu.PixelFormat.bc1RGBAUNormInt.isCompressed, true);
expect(gpu.PixelFormat.bc1RGBAUNormInt.blockWidth, 4);
expect(gpu.PixelFormat.bc1RGBAUNormInt.blockHeight, 4);
expect(gpu.PixelFormat.bc1RGBAUNormInt.bytesPerBlock, 8);
expect(gpu.PixelFormat.bc1RGBAUNormInt.compressionFamily, gpu.TextureCompressionFamily.bc);
expect(gpu.PixelFormat.etc2RGB8UNormInt.bytesPerBlock, 8);
expect(gpu.PixelFormat.etc2RGB8UNormInt.compressionFamily, gpu.TextureCompressionFamily.etc2);
// BC7 / ETC2 RGBA / ASTC 4x4: 4x4 blocks, 16 bytes per block.
expect(gpu.PixelFormat.bc7RGBAUNormInt.bytesPerBlock, 16);
expect(gpu.PixelFormat.etc2RGBA8UNormInt.bytesPerBlock, 16);
expect(gpu.PixelFormat.astc4x4LDR.bytesPerBlock, 16);
expect(gpu.PixelFormat.astc4x4LDR.compressionFamily, gpu.TextureCompressionFamily.astc);
// ASTC 8x8: 8x8 blocks, 16 bytes per block.
expect(gpu.PixelFormat.astc8x8LDR.blockWidth, 8);
expect(gpu.PixelFormat.astc8x8LDR.blockHeight, 8);
expect(gpu.PixelFormat.astc8x8LDR.bytesPerBlock, 16);
});
test('GpuContext.supportsTextureCompression returns a bool per family', () async {
// The exact answer depends on the device, but each query must return.
expect(gpu.gpuContext.supportsTextureCompression(gpu.TextureCompressionFamily.bc), isA<bool>());
expect(
gpu.gpuContext.supportsTextureCompression(gpu.TextureCompressionFamily.etc2),
isA<bool>(),
);
expect(
gpu.gpuContext.supportsTextureCompression(gpu.TextureCompressionFamily.astc),
isA<bool>(),
);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('GpuContext.supportsTextureFormat rejects render-target on compressed', () async {
// Sample-only is always permitted to be queried (subject to the family
// capability); render-target and shader-write are always rejected for
// compressed formats.
expect(
gpu.gpuContext.supportsTextureFormat(gpu.PixelFormat.bc7RGBAUNormInt, renderTarget: true),
false,
);
expect(
gpu.gpuContext.supportsTextureFormat(gpu.PixelFormat.bc7RGBAUNormInt, shaderWrite: true),
false,
);
// Uncompressed formats are not gated by per-format usage today.
expect(
gpu.gpuContext.supportsTextureFormat(gpu.PixelFormat.r8g8b8a8UNormInt, renderTarget: true),
true,
);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Texture.getMipLevelSizeInBytes is block-aware for compressed formats', () async {
if (!gpu.gpuContext.supportsTextureCompression(gpu.TextureCompressionFamily.bc)) {
markTestSkipped('BC texture compression is not supported on this device.');
return;
}
final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
16,
16,
format: gpu.PixelFormat.bc1RGBAUNormInt,
enableRenderTargetUsage: false,
mipLevelCount: 3,
);
// 16x16 -> 4x4 blocks * 8 bytes = 128.
expect(texture.getMipLevelSizeInBytes(0), 128);
// 8x8 -> 2x2 blocks * 8 bytes = 32.
expect(texture.getMipLevelSizeInBytes(1), 32);
// 4x4 -> 1x1 block * 8 bytes = 8.
expect(texture.getMipLevelSizeInBytes(2), 8);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('GpuContext.createTexture allocates a compressed texture when supported', () async {
if (!gpu.gpuContext.supportsTextureCompression(gpu.TextureCompressionFamily.bc)) {
markTestSkipped('BC texture compression is not supported on this device.');
return;
}
final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
8,
8,
format: gpu.PixelFormat.bc1RGBAUNormInt,
enableRenderTargetUsage: false,
);
expect(texture.format, gpu.PixelFormat.bc1RGBAUNormInt);
expect(texture.format.isCompressed, true);
// 8x8 -> 2x2 blocks * 8 bytes = 32.
expect(texture.getBaseMipLevelSizeInBytes(), 32);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Texture.overwrite uploads block-aligned compressed data', () async {
if (!gpu.gpuContext.supportsTextureCompression(gpu.TextureCompressionFamily.bc)) {
markTestSkipped('BC texture compression is not supported on this device.');
return;
}
final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
8,
8,
format: gpu.PixelFormat.bc1RGBAUNormInt,
enableRenderTargetUsage: false,
);
final ByteData bytes = Uint8List(32).buffer.asByteData();
texture.overwrite(bytes);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Texture.overwrite rejects wrong size for compressed format', () async {
if (!gpu.gpuContext.supportsTextureCompression(gpu.TextureCompressionFamily.bc)) {
markTestSkipped('BC texture compression is not supported on this device.');
return;
}
final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
8,
8,
format: gpu.PixelFormat.bc1RGBAUNormInt,
enableRenderTargetUsage: false,
);
try {
// 8x8 BC1 needs 32 bytes; 16 is wrong.
texture.overwrite(Uint8List(16).buffer.asByteData());
fail('Exception not thrown for wrong compressed buffer size.');
} catch (e) {
expect(e.toString(), contains('must exactly match the size of mip level 0'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('GpuContext.createTexture rejects compressed format as render target', () async {
try {
gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
8,
8,
format: gpu.PixelFormat.bc1RGBAUNormInt,
);
fail('Exception not thrown for compressed render target.');
} catch (e) {
expect(e.toString(), contains('sample-only'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('GpuContext.createTexture rejects compressed format with shader write', () async {
try {
gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
8,
8,
format: gpu.PixelFormat.bc1RGBAUNormInt,
enableRenderTargetUsage: false,
enableShaderWriteUsage: true,
);
fail('Exception not thrown for compressed shader write.');
} catch (e) {
expect(e.toString(), contains('sample-only'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('GpuContext.createTexture rejects compressed format with MSAA', () async {
try {
gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
8,
8,
format: gpu.PixelFormat.bc1RGBAUNormInt,
enableRenderTargetUsage: false,
sampleCount: 4,
);
fail('Exception not thrown for compressed MSAA.');
} catch (e) {
expect(e.toString(), contains('sample-only'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('GpuContext.createTexture rejects non-block-aligned compressed dimensions', () async {
try {
// 5 is not a multiple of the 4x4 BC1 block.
gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
5,
8,
format: gpu.PixelFormat.bc1RGBAUNormInt,
enableRenderTargetUsage: false,
);
fail('Exception not thrown for non-block-aligned compressed dimensions.');
} catch (e) {
expect(e.toString(), contains('multiple of the block size'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Texture.fullMipCount', () async {
// Matches Impeller's `ISize::MipCount`: `floor(log2(min(w, h)))`,
// clamped to a minimum of 1.
expect(gpu.Texture.fullMipCount(1, 1), 1);
expect(gpu.Texture.fullMipCount(2, 1), 1);
expect(gpu.Texture.fullMipCount(2, 2), 1);
expect(gpu.Texture.fullMipCount(4, 4), 2);
expect(gpu.Texture.fullMipCount(8, 8), 3);
expect(gpu.Texture.fullMipCount(100, 100), 6);
expect(gpu.Texture.fullMipCount(1024, 1024), 10);
// Non-square: count uses the smaller dimension.
expect(gpu.Texture.fullMipCount(1024, 1), 1);
expect(gpu.Texture.fullMipCount(1, 256), 1);
});
test('GpuContext.createTexture with mipLevelCount allocates a mip chain', () async {
final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
8,
8,
mipLevelCount: 3,
);
expect(texture.mipLevelCount, 3);
// Per-level sizes: 8*8*4=256, 4*4*4=64, 2*2*4=16.
expect(texture.getMipLevelSizeInBytes(0), 256);
expect(texture.getMipLevelSizeInBytes(1), 64);
expect(texture.getMipLevelSizeInBytes(2), 16);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('GpuContext.createTexture rejects out-of-range mipLevelCount', () async {
try {
gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 8, 8, mipLevelCount: 0);
fail('Exception not thrown for mipLevelCount=0.');
} catch (e) {
expect(e.toString(), contains('mipLevelCount'));
}
try {
// Max for 8x8 is 3.
gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 8, 8, mipLevelCount: 4);
fail('Exception not thrown for mipLevelCount above the maximum.');
} catch (e) {
expect(e.toString(), contains('mipLevelCount'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test(
'GpuContext.createTexture fails if invalid sampleCount and texture type is passed.',
() async {
try {
gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
100,
100,
sampleCount: 4,
textureType: gpu.TextureType.texture2D,
);
fail('Exception not thrown when creating an invalid texture.');
} catch (e) {
expect(e.toString(), contains('Texture creation failed'));
}
},
skip: !(impellerEnabled && flutterGpuEnabled),
);
test('Texture.overwrite', () async {
final gpu.Texture texture = gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 2, 2);
const red = ui.Color.fromARGB(0xFF, 0xFF, 0, 0);
const green = ui.Color.fromARGB(0xFF, 0, 0xFF, 0);
texture.overwrite(
Int32List.fromList(<int>[red.value, green.value, green.value, red.value]).buffer.asByteData(),
);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Texture.overwrite throws for wrong buffer size', () async {
final gpu.Texture texture = gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 100, 100);
const red = ui.Color.fromARGB(0xFF, 0xFF, 0, 0);
try {
texture.overwrite(
Int32List.fromList(<int>[red.value, red.value, red.value, red.value]).buffer.asByteData(),
);
fail('Exception not thrown for wrong buffer size.');
} catch (e) {
expect(
e.toString(),
contains(
'The length of sourceBytes (bytes: 16) must exactly match the size of mip level 0 (bytes: 40000)',
),
);
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Texture.overwrite writes to a non-zero mip level', () async {
final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
8,
8,
mipLevelCount: 3,
);
const red = ui.Color.fromARGB(0xFF, 0xFF, 0, 0);
const blue = ui.Color.fromARGB(0xFF, 0, 0, 0xFF);
// Mip 0: 8x8 = 64 texels.
texture.overwrite(Int32List.fromList(List<int>.filled(64, red.value)).buffer.asByteData());
// Mip 1: 4x4 = 16 texels.
texture.overwrite(
Int32List.fromList(List<int>.filled(16, blue.value)).buffer.asByteData(),
mipLevel: 1,
);
// Mip 2: 2x2 = 4 texels.
texture.overwrite(
Int32List.fromList(List<int>.filled(4, red.value)).buffer.asByteData(),
mipLevel: 2,
);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Texture.overwrite throws for an out-of-range mipLevel', () async {
final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
4,
4,
mipLevelCount: 2,
);
const red = ui.Color.fromARGB(0xFF, 0xFF, 0, 0);
try {
texture.overwrite(Int32List.fromList(<int>[red.value]).buffer.asByteData(), mipLevel: 2);
fail('Exception not thrown for out-of-range mipLevel.');
} catch (e) {
expect(e.toString(), contains('mipLevel (2) must be in the range [0, 2)'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Texture.overwrite throws for an out-of-range slice', () async {
final gpu.Texture texture = gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 2, 2);
const red = ui.Color.fromARGB(0xFF, 0xFF, 0, 0);
try {
texture.overwrite(
Int32List.fromList(List<int>.filled(4, red.value)).buffer.asByteData(),
slice: 1,
);
fail('Exception not thrown for out-of-range slice.');
} catch (e) {
expect(e.toString(), contains('slice (1) must be in the range [0, 1)'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Texture.overwrite writes each face of a cubemap', () async {
final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
2,
2,
textureType: gpu.TextureType.textureCube,
);
expect(texture.sliceCount, 6);
const colors = <ui.Color>[
ui.Color.fromARGB(0xFF, 0xFF, 0, 0),
ui.Color.fromARGB(0xFF, 0, 0xFF, 0),
ui.Color.fromARGB(0xFF, 0, 0, 0xFF),
ui.Color.fromARGB(0xFF, 0xFF, 0xFF, 0),
ui.Color.fromARGB(0xFF, 0xFF, 0, 0xFF),
ui.Color.fromARGB(0xFF, 0, 0xFF, 0xFF),
];
for (var slice = 0; slice < 6; slice++) {
final int v = colors[slice].value;
texture.overwrite(Int32List.fromList(<int>[v, v, v, v]).buffer.asByteData(), slice: slice);
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Texture.asImage returns a valid ui.Image handle', () async {
final gpu.Texture texture = gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 100, 100);
final ui.Image image = texture.asImage();
expect(image.width, 100);
expect(image.height, 100);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Texture.asImage throws when not shader readable', () async {
final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible,
100,
100,
enableShaderReadUsage: false,
);
try {
texture.asImage();
fail('Exception not thrown when not shader readable.');
} catch (e) {
expect(
e.toString(),
contains('Only shader readable Flutter GPU textures can be used as UI Images'),
);
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('RenderPass.setStencilReference doesnt throw for valid values', () async {
final RenderPassState state = createSimpleRenderPass();
state.renderPass.setStencilReference(0);
state.renderPass.setStencilReference(2 << 30);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('RenderPass.setStencilReference throws for invalid values', () async {
final RenderPassState state = createSimpleRenderPass();
try {
state.renderPass.setStencilReference(-1);
fail('Exception not thrown for out of bounds stencil reference.');
} catch (e) {
expect(e.toString(), contains('The stencil reference value must be in the range'));
}
try {
state.renderPass.setStencilReference(2 << 31);
fail('Exception not thrown for out of bounds stencil reference.');
} catch (e) {
expect(e.toString(), contains('The stencil reference value must be in the range'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('RenderPass.setStencilConfig doesnt throw for valid values', () async {
final RenderPassState state = createSimpleRenderPass();
state.renderPass.setStencilConfig(gpu.StencilConfig());
state.renderPass.setStencilConfig(
gpu.StencilConfig(
compareFunction: gpu.CompareFunction.notEqual,
depthFailureOperation: gpu.StencilOperation.decrementWrap,
depthStencilPassOperation: gpu.StencilOperation.incrementWrap,
stencilFailureOperation: gpu.StencilOperation.invert,
readMask: 0,
writeMask: 0,
),
targetFace: gpu.StencilFace.back,
);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('RenderPass.setStencilConfig throws for invalid masks', () async {
final RenderPassState state = createSimpleRenderPass();
try {
state.renderPass.setStencilConfig(gpu.StencilConfig(readMask: -1));
fail('Exception not thrown for invalid stencil read mask.');
} catch (e) {
expect(e.toString(), contains('The stencil read mask must be in the range'));
}
try {
state.renderPass.setStencilConfig(gpu.StencilConfig(readMask: 0xFFFFFFFF + 1));
fail('Exception not thrown for invalid stencil read mask.');
} catch (e) {
expect(e.toString(), contains('The stencil read mask must be in the range'));
}
try {
state.renderPass.setStencilConfig(gpu.StencilConfig(writeMask: -1));
fail('Exception not thrown for invalid stencil write mask.');
} catch (e) {
expect(e.toString(), contains('The stencil write mask must be in the range'));
}
try {
state.renderPass.setStencilConfig(gpu.StencilConfig(writeMask: 0xFFFFFFFF + 1));
fail('Exception not thrown for invalid stencil write mask.');
} catch (e) {
expect(e.toString(), contains('The stencil write mask must be in the range'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('RenderPass.bindTexture throws for deviceTransient Textures', () async {
final RenderPassState state = createSimpleRenderPass();
final gpu.RenderPipeline pipeline = createUnlitRenderPipeline();
// Although this is a non-texture uniform slot, it'll work fine for the
// purposes of testing this error.
final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo');
final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient,
100,
100,
);
try {
state.renderPass.bindTexture(vertInfo, texture);
fail('Exception not thrown when binding a transient texture.');
} catch (e) {
expect(
e.toString(),
contains('Textures with StorageMode.deviceTransient cannot be bound to a RenderPass'),
);
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
// Performs no draw calls. Just clears the render target to a solid green color.
test('Can render clear color', () async {
final RenderPassState state = createSimpleRenderPass(clearColor: Colors.lime);
state.commandBuffer.submit();
final ui.Image image = state.renderTexture.asImage();
await comparer.addGoldenImage(image, 'flutter_gpu_test_clear_color.png');
}, skip: !(impellerEnabled && flutterGpuEnabled));
// Regression test for https://github.com/flutter/flutter/issues/157324
test('Can bind uniforms in range', () async {
final RenderPassState state = createSimpleRenderPass();
final gpu.RenderPipeline pipeline = createUnlitRenderPipeline();
final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo');
final ByteData vertInfoData = 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
]);
final gpu.DeviceBuffer uniformBuffer = gpu.gpuContext.createDeviceBufferWithCopy(vertInfoData);
final gooduniformBufferView = gpu.BufferView(
uniformBuffer,
offsetInBytes: 0,
lengthInBytes: uniformBuffer.sizeInBytes,
);
state.renderPass.bindUniform(vertInfo, gooduniformBufferView);
final badUniformBufferView = gpu.BufferView(
uniformBuffer,
offsetInBytes: 0,
lengthInBytes: uniformBuffer.sizeInBytes + 1,
);
try {
state.renderPass.bindUniform(vertInfo, badUniformBufferView);
fail('Exception not thrown for bad buffer view range.');
} catch (e) {
expect(e.toString(), contains('Failed to bind uniform'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
// Renders a green triangle pointing downwards.
test('Can render triangle', () async {
final RenderPassState state = createSimpleRenderPass();
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 && flutterGpuEnabled));
// Renders the same green triangle as the non-indexed test, this time by
// walking a 3-entry index buffer with drawIndexed.
test('Can render triangle with drawIndexed', () async {
final RenderPassState state = createSimpleRenderPass();
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 indices = transients.emplace(
Uint16List.fromList(<int>[0, 1, 2]).buffer.asByteData(),
);
state.renderPass.bindVertexBuffer(vertices);
state.renderPass.bindIndexBuffer(indices, gpu.IndexType.int16);
state.renderPass.bindUniform(
pipeline.vertexShader.getUniformSlot('VertInfo'),
transients.emplace(unlitUBO(Matrix4.identity(), Colors.lime)),
);
state.renderPass.drawIndexed(3);
state.commandBuffer.submit();
final ui.Image image = state.renderTexture.asImage();
await comparer.addGoldenImage(image, 'flutter_gpu_test_triangle.png');
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('drawIndexed throws when no index buffer is bound', () async {
final RenderPassState state = createSimpleRenderPass();
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]),
);
state.renderPass.bindVertexBuffer(vertices);
state.renderPass.bindUniform(
pipeline.vertexShader.getUniformSlot('VertInfo'),
transients.emplace(unlitUBO(Matrix4.identity(), Colors.lime)),
);
expect(
() => state.renderPass.drawIndexed(3),
throwsA(
isA<Exception>().having(
(Exception e) => e.toString(),
'message',
contains('Failed to append drawIndexed'),
),
),
);
}, skip: !(impellerEnabled && flutterGpuEnabled));
// Regression test for flutter/flutter#186393: a Flutter GPU shader whose
// uniform block uses an instance variable name that does not normalize
// to the block name (e.g. `uniform ColorParams { ... } params;`) used
// to silently bind every member to GL location -1 on the OpenGL ES
// backend, producing a black render. `impellerc` now canonicalizes the
// instance name to `_<BlockName>` for GL targets so the block resolves
// correctly on all backends.
test('Uniform block with non-conforming instance name binds on all backends', () async {
final RenderPassState state = createSimpleRenderPass();
final gpu.ShaderLibrary library = gpu.ShaderLibrary.fromAsset('test.shaderbundle')!;
final gpu.RenderPipeline pipeline = gpu.gpuContext.createRenderPipeline(
library['UnlitVertex']!,
library['UnlitFragmentAltInstance']!,
);
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]),
);
state.renderPass.bindVertexBuffer(vertices);
// Vertex shader writes v_color from VertInfo.color; pick white so the
// final pixel color is dictated entirely by ColorParams.base_color.
state.renderPass.bindUniform(
pipeline.vertexShader.getUniformSlot('VertInfo'),
transients.emplace(unlitUBO(Matrix4.identity(), Vector4(1, 1, 1, 1))),
);
// Bind the fragment shader's uniform block by its *block* name. Without
// canonicalization, GLES would bind the members to location -1 and
// sample zeros here.
state.renderPass.bindUniform(
pipeline.fragmentShader.getUniformSlot('ColorParams'),
transients.emplace(float32(<double>[1.0, 0.0, 0.0, 1.0])),
);
state.renderPass.draw(3);
state.commandBuffer.submit();
final ui.Image image = state.renderTexture.asImage();
await comparer.addGoldenImage(image, 'flutter_gpu_uniform_block_alt_instance.png');
// Belt-and-suspenders: also assert programmatically that the center of
// the rendered triangle is red. If the bug regresses, the uniform reads
// zero and the triangle renders as fully transparent black. Sampling a
// single pixel inside the triangle catches that without relying on
// golden-file plumbing.
final ByteData? bytes = await image.toByteData();
expect(bytes, isNotNull);
final int centerOffset = (image.width ~/ 2 + image.height ~/ 2 * image.width) * 4;
final int b0 = bytes!.getUint8(centerOffset);
final int b1 = bytes.getUint8(centerOffset + 1);
final int b2 = bytes.getUint8(centerOffset + 2);
final int b3 = bytes.getUint8(centerOffset + 3);
// Format may be RGBA or BGRA depending on backend; check that exactly
// one of the first three channels is red-saturated and the others are
// dark, with full alpha.
expect(
b0 + b1 + b2,
greaterThan(200),
reason:
'Center pixel was black (channels=$b0,$b1,$b2,$b3); '
'uniform block likely failed to bind.',
);
expect(b3, greaterThan(200), reason: 'Center pixel alpha was low (channels=$b0,$b1,$b2,$b3).');
}, skip: !(impellerEnabled && flutterGpuEnabled));
// A custom VertexLayout that matches the shader bundle's default for the
// UnlitVertex shader (one buffer at slot 0, vec2 position at offset 0)
// should produce identical pipeline behavior. This pins the shape of the
// VertexLayout/VertexBuffer/VertexAttribute/VertexFormat API and the FFI
// plumbing through createRenderPipeline.
test('Can render triangle with explicit VertexLayout', () async {
final RenderPassState state = createSimpleRenderPass();
final gpu.ShaderLibrary library = gpu.ShaderLibrary.fromAsset('test.shaderbundle')!;
final gpu.RenderPipeline pipeline = gpu.gpuContext.createRenderPipeline(
library['UnlitVertex']!,
library['UnlitFragment']!,
vertexLayout: const gpu.VertexLayout(
buffers: <gpu.VertexBuffer>[
gpu.VertexBuffer(
strideInBytes: 8,
attributes: <gpu.VertexAttribute>[
gpu.VertexAttribute(name: 'position', format: gpu.VertexFormat.float32x2),
],
),
],
),
);
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 vertInfo = transients.emplace(unlitUBO(Matrix4.identity(), Colors.lime));
state.renderPass.bindVertexBuffer(vertices);
state.renderPass.bindUniform(pipeline.vertexShader.getUniformSlot('VertInfo'), vertInfo);
state.renderPass.draw(3);
state.commandBuffer.submit();
final ui.Image image = state.renderTexture.asImage();
await comparer.addGoldenImage(image, 'flutter_gpu_test_triangle.png');
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('createRenderPipeline rejects VertexLayout with wrong attribute format', () async {
final gpu.ShaderLibrary library = gpu.ShaderLibrary.fromAsset('test.shaderbundle')!;
try {
gpu.gpuContext.createRenderPipeline(
library['UnlitVertex']!,
library['UnlitFragment']!,
vertexLayout: const gpu.VertexLayout(
buffers: <gpu.VertexBuffer>[
gpu.VertexBuffer(
strideInBytes: 8,
attributes: <gpu.VertexAttribute>[
// UnlitVertex declares a float `vec2 position`, so binding
// a uint32x2 (different scalar type class) here must throw.
// Component-count mismatches are NOT errors: a buffer can
// supply more or fewer components than the shader reads,
// matching the default-substitution rules every modern HAL
// uses.
gpu.VertexAttribute(name: 'position', format: gpu.VertexFormat.uint32x2),
],
),
],
),
);
fail('Expected exception for mismatched VertexFormat scalar type.');
} catch (e) {
expect(
e.toString(),
contains("format does not match the vertex shader's declared input type"),
);
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('createRenderPipeline rejects VertexAttribute that overruns stride', () async {
final gpu.ShaderLibrary library = gpu.ShaderLibrary.fromAsset('test.shaderbundle')!;
try {
gpu.gpuContext.createRenderPipeline(
library['UnlitVertex']!,
library['UnlitFragment']!,
vertexLayout: const gpu.VertexLayout(
buffers: <gpu.VertexBuffer>[
gpu.VertexBuffer(
strideInBytes: 8,
attributes: <gpu.VertexAttribute>[
// float32x2 (8 bytes) at offset 4 with stride 8 overruns by 4.
gpu.VertexAttribute(
name: 'position',
offsetInBytes: 4,
format: gpu.VertexFormat.float32x2,
),
],
),
],
),
);
fail('Expected exception for offset+format overruning stride.');
} catch (e) {
expect(e.toString(), contains('overruns stride'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('createRenderPipeline rejects VertexLayout with overlapping attributes', () async {
final gpu.ShaderLibrary library = gpu.ShaderLibrary.fromAsset('test.shaderbundle')!;
try {
gpu.gpuContext.createRenderPipeline(
library['UnlitVertex']!,
library['UnlitFragment']!,
vertexLayout: const gpu.VertexLayout(
buffers: <gpu.VertexBuffer>[
gpu.VertexBuffer(
strideInBytes: 8,
attributes: <gpu.VertexAttribute>[
// Two attributes both occupying bytes [0, 8) in the same
// buffer. The second one isn't a real shader input, but
// the overlap check fires before the name check.
gpu.VertexAttribute(name: 'position', format: gpu.VertexFormat.float32x2),
gpu.VertexAttribute(name: 'aliased', format: gpu.VertexFormat.float32x2),
],
),
],
),
);
fail('Expected exception for overlapping VertexAttributes.');
} catch (e) {
final msg = e.toString();
expect(msg, contains('overlaps'));
expect(msg, contains("'position'"));
expect(msg, contains("'aliased'"));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('bindVertexBuffer throws RangeError for out-of-range slot', () async {
final RenderPassState state = createSimpleRenderPass();
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]),
);
expect(() => state.renderPass.bindVertexBuffer(vertices, slot: -1), throwsRangeError);
expect(() => state.renderPass.bindVertexBuffer(vertices, slot: 16), throwsRangeError);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('draw throws StateError on sparse vertex buffer bindings', () async {
final RenderPassState state = createSimpleRenderPass();
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]),
);
// Bind only slot 1 (skipping slot 0). draw() must surface a clear
// error rather than letting the underlying HAL validation fail
// silently or render with an empty slot.
state.renderPass.bindVertexBuffer(vertices, slot: 1);
expect(
() => state.renderPass.draw(3),
throwsA(
isA<StateError>().having(
(StateError e) => e.message,
'message',
allOf(contains('sparse'), contains('slot(s) 0')),
),
),
);
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('createRenderPipeline rejects VertexAttribute with unknown name', () async {
final gpu.ShaderLibrary library = gpu.ShaderLibrary.fromAsset('test.shaderbundle')!;
try {
gpu.gpuContext.createRenderPipeline(
library['UnlitVertex']!,
library['UnlitFragment']!,
vertexLayout: const gpu.VertexLayout(
buffers: <gpu.VertexBuffer>[
gpu.VertexBuffer(
strideInBytes: 8,
attributes: <gpu.VertexAttribute>[
gpu.VertexAttribute(
name: 'nonexistent_attribute',
format: gpu.VertexFormat.float32x2,
),
],
),
],
),
);
fail('Expected exception for unknown attribute name.');
} catch (e) {
expect(
e.toString(),
contains('does not match any input declared by the bound vertex shader'),
);
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
// Two distinct shader bundles can ship the same entrypoint names without
// colliding in the shared shader registry. `test.shaderbundle` and
// `test_alt.shaderbundle` both export `UnlitFragment` and `UnlitVertex`
// entrypoints; without the per-bundle namespacing they would register at
// the same registry key and the second load would evict the first. With
// namespacing the asset paths (the bundle library_ids) make the keys
// distinct, so both bundles can be loaded and used in the same process.
test('Two shader bundles with the same entrypoint names do not collide', () async {
final gpu.ShaderLibrary? libraryA = gpu.ShaderLibrary.fromAsset('test.shaderbundle');
final gpu.ShaderLibrary? libraryB = gpu.ShaderLibrary.fromAsset('test_alt.shaderbundle');
expect(libraryA, isNotNull);
expect(libraryB, isNotNull);
final gpu.Shader? unlitVertexA = libraryA!['UnlitVertex'];
final gpu.Shader? unlitFragmentA = libraryA['UnlitFragment'];
final gpu.Shader? unlitVertexB = libraryB!['UnlitVertex'];
final gpu.Shader? unlitFragmentB = libraryB['UnlitFragment'];
expect(unlitVertexA, isNotNull);
expect(unlitFragmentA, isNotNull);
expect(unlitVertexB, isNotNull);
expect(unlitFragmentB, isNotNull);
// Both pipelines must register and remain usable. Without namespacing,
// `RuntimeEffectContents::RegisterShader`-style eviction would tear one
// of these pipelines down at registration time and the second draw
// would render against invalid state. With namespacing they coexist.
final gpu.RenderPipeline pipelineA = gpu.gpuContext.createRenderPipeline(
unlitVertexA!,
unlitFragmentA!,
);
final gpu.RenderPipeline pipelineB = gpu.gpuContext.createRenderPipeline(
unlitVertexB!,
unlitFragmentB!,
);
void drawWithPipeline(RenderPassState state, gpu.RenderPipeline pipeline, Vector4 color) {
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);
final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo');
state.renderPass.bindUniform(vertInfo, vertInfoData);
state.renderPass.draw(3);
}
final RenderPassState stateA = createSimpleRenderPass();
drawWithPipeline(stateA, pipelineA, Colors.lime);
stateA.commandBuffer.submit();
final RenderPassState stateB = createSimpleRenderPass();
drawWithPipeline(stateB, pipelineB, Colors.lime);
stateB.commandBuffer.submit();
}, skip: !(impellerEnabled && flutterGpuEnabled));
// Renders a green triangle pointing downwards using polygon mode line.
test('Can render triangle with polygon mode line.', () async {
final RenderPassState 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());
// Set polygon mode.
state.renderPass.setPolygonMode(gpu.PolygonMode.line);
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);
final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo');
state.renderPass.bindUniform(vertInfo, vertInfoData);
state.renderPass.draw(3);
state.commandBuffer.submit();
final ui.Image image = state.renderTexture.asImage();
await comparer.addGoldenImage(image, 'flutter_gpu_test_triangle_polygon_mode.png');
}, skip: !(impellerEnabled && flutterGpuEnabled));
// Renders a green triangle pointing downwards, with 4xMSAA.
test('Can render triangle with MSAA', () async {
final RenderPassState 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 && flutterGpuEnabled && gpu.gpuContext.doesSupportOffscreenMSAA));
test('Rendering with MSAA throws exception when offscreen MSAA is not supported', () async {
try {
final RenderPassState 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: !(impellerEnabled && flutterGpuEnabled && !gpu.gpuContext.doesSupportOffscreenMSAA));
// Renders a hollow green triangle pointing downwards.
test('Can render hollowed out triangle using stencil ops', () async {
final RenderPassState 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 innerClipVertInfo = transients.emplace(
float32(<double>[
0.5, 0, 0, 0, // mvp
0, 0.5, 0, 0, // mvp
0, 0, 0.5, 0, // mvp
0, 0, 0, 1, // mvp
0, 1, 0, 1, // color
]),
);
final gpu.BufferView outerGreenVertInfo = 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);
final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo');
// First, punch out a scaled down triangle in the stencil buffer.
// Since the stencil buffer is initialized to 0, we set the stencil ref to 1
// and the compare to `equial`, which will result in the stencil test
// failing. But on failure, we increment the stencil in order to punch out
// the triangle.
state.renderPass.bindUniform(vertInfo, innerClipVertInfo);
state.renderPass.setStencilReference(1);
state.renderPass.setStencilConfig(
gpu.StencilConfig(
compareFunction: gpu.CompareFunction.equal,
stencilFailureOperation: gpu.StencilOperation.incrementClamp,
),
);
state.renderPass.draw(3);
// Next, render the outer triangle with the stencil ref set to zero, so that
// the stencil test passes everywhere except where the inner triangle was
// punched out.
state.renderPass.setStencilReference(0);
// Set the stencil config to turn off the increment. For this golden test
// we technically don't need to do this, but we do it here just to exercise
// the API.
state.renderPass.setStencilConfig(
gpu.StencilConfig(compareFunction: gpu.CompareFunction.equal),
);
state.renderPass.bindUniform(vertInfo, outerGreenVertInfo);
state.renderPass.draw(3);
state.commandBuffer.submit();
final ui.Image image = state.renderTexture.asImage();
await comparer.addGoldenImage(image, 'flutter_gpu_test_triangle_stencil.png');
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('Drawing respects cull mode', () async {
final RenderPassState state = createSimpleRenderPass();
final gpu.RenderPipeline pipeline = createUnlitRenderPipeline();
state.renderPass.bindPipeline(pipeline);
state.renderPass.setColorBlendEnable(true);
state.renderPass.setColorBlendEquation(gpu.ColorBlendEquation());
final gpu.HostBuffer transients = gpu.gpuContext.createHostBuffer();
// Counter-clockwise triangle.
final triangle = <double>[
-0.5, 0.5, //
0.0, -0.5, //
0.5, 0.5, //
];
final gpu.BufferView vertices = transients.emplace(float32(triangle));
void drawTriangle(Vector4 color) {
final gpu.BufferView vertInfoUboFront = transients.emplace(
unlitUBO(Matrix4.identity(), color),
);
final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo');
state.renderPass.bindVertexBuffer(vertices);
state.renderPass.bindUniform(vertInfo, vertInfoUboFront);
state.renderPass.draw(3);
}
// Draw the green rectangle.
// Defaults to clockwise winding order. So frontface culling should not
// impact the green triangle.
state.renderPass.setCullMode(gpu.CullMode.frontFace);
drawTriangle(Colors.lime);
// Backface cull a red triangle.
state.renderPass.setCullMode(gpu.CullMode.backFace);
drawTriangle(Colors.red);
// Invert the winding mode and frontface cull a red rectangle.
state.renderPass.setWindingOrder(gpu.WindingOrder.counterClockwise);
state.renderPass.setCullMode(gpu.CullMode.frontFace);
drawTriangle(Colors.red);
state.commandBuffer.submit();
final ui.Image image = state.renderTexture.asImage();
await comparer.addGoldenImage(image, 'flutter_gpu_test_cull_mode.png');
}, skip: !(impellerEnabled && flutterGpuEnabled));
// Renders a hexagon using line strip primitive type.
test('Can render hollow hexagon using line strip primitive type', () async {
final RenderPassState 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());
// Set primitive type
state.renderPass.setPrimitiveType(gpu.PrimitiveType.lineStrip);
final gpu.HostBuffer transients = gpu.gpuContext.createHostBuffer();
final gpu.BufferView vertices = transients.emplace(
float32(<double>[1.0, 0.0, 0.5, 0.8, -0.5, 0.8, -1.0, 0.0, -0.5, -0.8, 0.5, -0.8, 1.0, 0.0]),
);
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);
final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo');
state.renderPass.bindUniform(vertInfo, vertInfoData);
state.renderPass.draw(7);
state.commandBuffer.submit();
final ui.Image image = state.renderTexture.asImage();
await comparer.addGoldenImage(image, 'flutter_gpu_test_hexgon_line_strip.png');
}, skip: !(impellerEnabled && flutterGpuEnabled));
// Renders the middle part triangle using scissor.
test('Can render portion of the triangle using scissor', () async {
final RenderPassState 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());
// Set primitive type.
state.renderPass.setPrimitiveType(gpu.PrimitiveType.triangle);
// Set scissor.
state.renderPass.setScissor(gpu.Scissor(x: 25, width: 50, height: 100));
final gpu.HostBuffer transients = gpu.gpuContext.createHostBuffer();
final gpu.BufferView vertices = transients.emplace(
float32(<double>[-1.0, -1.0, 0.0, 1.0, 1.0, -1.0]),
);
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);
final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo');
state.renderPass.bindUniform(vertInfo, vertInfoData);
state.renderPass.draw(3);
state.commandBuffer.submit();
final ui.Image image = state.renderTexture.asImage();
await comparer.addGoldenImage(image, 'flutter_gpu_test_scissor.png');
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('RenderPass.setScissor doesnt throw for valid values', () async {
final RenderPassState state = createSimpleRenderPass();
state.renderPass.setScissor(gpu.Scissor(x: 25, width: 50, height: 100));
state.renderPass.setScissor(gpu.Scissor(width: 50, height: 100));
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('RenderPass.setScissor throws for invalid values', () async {
final RenderPassState state = createSimpleRenderPass();
try {
state.renderPass.setScissor(gpu.Scissor(x: -1, width: 50, height: 100));
fail('Exception not thrown for invalid scissor.');
} catch (e) {
expect(e.toString(), contains('Invalid values for scissor. All values should be positive.'));
}
try {
state.renderPass.setScissor(gpu.Scissor(width: 50, height: -100));
fail('Exception not thrown for invalid scissor.');
} catch (e) {
expect(e.toString(), contains('Invalid values for scissor. All values should be positive.'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('RenderPass.setViewport doesnt throw for valid values', () async {
final RenderPassState state = createSimpleRenderPass();
state.renderPass.setViewport(gpu.Viewport(x: 25, width: 50, height: 100));
state.renderPass.setViewport(gpu.Viewport(width: 50, height: 100));
}, skip: !(impellerEnabled && flutterGpuEnabled));
test('RenderPass.setViewport throws for invalid values', () async {
final RenderPassState state = createSimpleRenderPass();
try {
state.renderPass.setViewport(gpu.Viewport(x: -1, width: 50, height: 100));
fail('Exception not thrown for invalid viewport.');
} catch (e) {
expect(e.toString(), contains('Invalid values for viewport. All values should be positive.'));
}
try {
state.renderPass.setViewport(gpu.Viewport(width: 50, height: -100));
fail('Exception not thrown for invalid viewport.');
} catch (e) {
expect(e.toString(), contains('Invalid values for viewport. All values should be positive.'));
}
}, skip: !(impellerEnabled && flutterGpuEnabled));
// Renders the middle part triangle using viewport.
test('Can render portion of the triangle using viewport', () async {
final RenderPassState 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());
// Set primitive type.
state.renderPass.setPrimitiveType(gpu.PrimitiveType.triangle);
// Set viewport.
state.renderPass.setViewport(gpu.Viewport(x: 25, width: 50, height: 100));
final gpu.HostBuffer transients = gpu.gpuContext.createHostBuffer();
final gpu.BufferView vertices = transients.emplace(
float32(<double>[-1.0, -1.0, 0.0, 1.0, 1.0, -1.0]),
);
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);
final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo');
state.renderPass.bindUniform(vertInfo, vertInfoData);
state.renderPass.draw(3);
state.commandBuffer.submit();
final ui.Image image = state.renderTexture.asImage();
await comparer.addGoldenImage(image, 'flutter_gpu_test_viewport.png');
}, skip: !(impellerEnabled && flutterGpuEnabled));
}