[Flutter GPU] Breaking: Use exceptions for resource creation errors. (#162104)

Resolves https://github.com/flutter/flutter/issues/143891.

This patch includes breaking changes, but this API is still in preview.
* Breaking: Rename `Texture.GetBaseMipLevelSizeInBytes` to
`Texture.getBaseMipLevelSizeInBytes`.
* Breaking: Make `Texture.overwrite` throw exception instead of
returning false.
* Non-breaking: Make `DeviceBuffer`/`Texture` creation throw exceptions
instead of returning nullables.

We can incrementally add more specific exceptions for resource creation
failure.
This commit is contained in:
Brandon DeRosier 2025-01-31 17:46:34 -08:00 committed by GitHub
parent 806772b528
commit 039d0db698
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 63 additions and 81 deletions

View File

@ -66,17 +66,16 @@ ByteData float32(List<double> values) {
@pragma('vm:entry-point') @pragma('vm:entry-point')
void canCreateRenderPassAndSubmit(int width, int height) { void canCreateRenderPassAndSubmit(int width, int height) {
final gpu.Texture? renderTexture = gpu.gpuContext.createTexture( final gpu.Texture renderTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.devicePrivate, gpu.StorageMode.devicePrivate,
width, width,
height, height,
); );
assert(renderTexture != null);
final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer(); final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
final gpu.RenderTarget renderTarget = gpu.RenderTarget.singleColor( final gpu.RenderTarget renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: renderTexture!), gpu.ColorAttachment(texture: renderTexture),
); );
final gpu.RenderPass encoder = commandBuffer.createRenderPass(renderTarget); final gpu.RenderPass encoder = commandBuffer.createRenderPass(renderTarget);

View File

@ -244,9 +244,6 @@ base class HostBuffer {
StorageMode.hostVisible, StorageMode.hostVisible,
length, length,
); );
if (buffer == null) {
throw Exception('Failed to allocate DeviceBuffer of length $length');
}
return buffer; return buffer;
} }

View File

@ -63,8 +63,8 @@ base class GpuContext extends NativeFieldWrapperClass1 {
/// The [storageMode] must be either [StorageMode.hostVisible] or /// The [storageMode] must be either [StorageMode.hostVisible] or
/// [StorageMode.devicePrivate], otherwise an exception will be thrown. /// [StorageMode.devicePrivate], otherwise an exception will be thrown.
/// ///
/// Returns [null] if the [DeviceBuffer] creation failed. /// Throws an exception if the [DeviceBuffer] creation failed.
DeviceBuffer? createDeviceBuffer(StorageMode storageMode, int sizeInBytes) { DeviceBuffer createDeviceBuffer(StorageMode storageMode, int sizeInBytes) {
if (storageMode == StorageMode.deviceTransient) { if (storageMode == StorageMode.deviceTransient) {
throw Exception( throw Exception(
'DeviceBuffers cannot be set to StorageMode.deviceTransient', 'DeviceBuffers cannot be set to StorageMode.deviceTransient',
@ -75,7 +75,10 @@ base class GpuContext extends NativeFieldWrapperClass1 {
storageMode, storageMode,
sizeInBytes, sizeInBytes,
); );
return result.isValid ? result : null; if (!result.isValid) {
throw Exception('DeviceBuffer creation failed');
}
return result;
} }
/// Allocates a new region of host-visible GPU-resident memory, initialized /// Allocates a new region of host-visible GPU-resident memory, initialized
@ -85,10 +88,13 @@ base class GpuContext extends NativeFieldWrapperClass1 {
/// from the host, the [StorageMode] of the new [DeviceBuffer] is /// from the host, the [StorageMode] of the new [DeviceBuffer] is
/// automatically set to [StorageMode.hostVisible]. /// automatically set to [StorageMode.hostVisible].
/// ///
/// Returns [null] if the [DeviceBuffer] creation failed. /// Throws an exception if the [DeviceBuffer] creation failed.
DeviceBuffer? createDeviceBufferWithCopy(ByteData data) { DeviceBuffer createDeviceBufferWithCopy(ByteData data) {
DeviceBuffer result = DeviceBuffer._initializeWithHostData(this, data); DeviceBuffer result = DeviceBuffer._initializeWithHostData(this, data);
return result.isValid ? result : null; if (!result.isValid) {
throw Exception('DeviceBuffer creation failed');
}
return result;
} }
/// Creates a bump allocator that managed a [DeviceBuffer] block list. /// Creates a bump allocator that managed a [DeviceBuffer] block list.
@ -102,8 +108,8 @@ base class GpuContext extends NativeFieldWrapperClass1 {
/// Allocates a new texture in GPU-resident memory. /// Allocates a new texture in GPU-resident memory.
/// ///
/// Returns [null] if the [Texture] creation failed. /// Throws an exception if the [Texture] creation failed.
Texture? createTexture( Texture createTexture(
StorageMode storageMode, StorageMode storageMode,
int width, int width,
int height, { int height, {
@ -127,7 +133,10 @@ base class GpuContext extends NativeFieldWrapperClass1 {
enableShaderReadUsage, enableShaderReadUsage,
enableShaderWriteUsage, enableShaderWriteUsage,
); );
return result.isValid ? result : null; if (!result.isValid) {
throw Exception('Texture creation failed');
}
return result;
} }
/// Create a new command buffer that can be used to submit GPU commands. /// Create a new command buffer that can be used to submit GPU commands.

View File

@ -73,7 +73,7 @@ base class Texture extends NativeFieldWrapperClass1 {
return _bytesPerTexel(); return _bytesPerTexel();
} }
int GetBaseMipLevelSizeInBytes() { int getBaseMipLevelSizeInBytes() {
return bytesPerTexel * width * height; return bytesPerTexel * width * height;
} }
@ -86,21 +86,23 @@ base class Texture extends NativeFieldWrapperClass1 {
/// level, otherwise an exception will be thrown. The size of the base mip /// level, otherwise an exception will be thrown. The size of the base mip
/// level is always `width * height * bytesPerPixel`. /// level is always `width * height * bytesPerPixel`.
/// ///
/// Returns [true] if the write was successful, or [false] if the write /// Throws an exception if the write failed due to an internal error.
/// failed due to an internal error. void overwrite(ByteData sourceBytes) {
bool overwrite(ByteData sourceBytes) {
if (storageMode != StorageMode.hostVisible) { if (storageMode != StorageMode.hostVisible) {
throw Exception( throw Exception(
'Texture.overwrite can only be used with Textures that are host visible', 'Texture.overwrite can only be used with Textures that are host visible',
); );
} }
int baseMipSize = GetBaseMipLevelSizeInBytes(); int baseMipSize = getBaseMipLevelSizeInBytes();
if (sourceBytes.lengthInBytes != baseMipSize) { if (sourceBytes.lengthInBytes != baseMipSize) {
throw Exception( throw Exception(
'The length of sourceBytes (bytes: ${sourceBytes.lengthInBytes}) must exactly match the size of the base mip level (bytes: ${baseMipSize})', 'The length of sourceBytes (bytes: ${sourceBytes.lengthInBytes}) must exactly match the size of the base mip level (bytes: ${baseMipSize})',
); );
} }
return _overwrite(sourceBytes); bool success = _overwrite(sourceBytes);
if (!success) {
throw Exception("Texture overwrite failed");
}
} }
ui.Image asImage() { ui.Image asImage() {

View File

@ -52,26 +52,24 @@ class RenderPassState {
/// Create a simple RenderPass with simple color and depth-stencil attachments. /// Create a simple RenderPass with simple color and depth-stencil attachments.
RenderPassState createSimpleRenderPass({Vector4? clearColor}) { RenderPassState createSimpleRenderPass({Vector4? clearColor}) {
final gpu.Texture? renderTexture = gpu.gpuContext.createTexture( final gpu.Texture renderTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.devicePrivate, gpu.StorageMode.devicePrivate,
100, 100,
100, 100,
); );
assert(renderTexture != null);
final gpu.Texture? depthStencilTexture = gpu.gpuContext.createTexture( final gpu.Texture depthStencilTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient, gpu.StorageMode.deviceTransient,
100, 100,
100, 100,
format: gpu.gpuContext.defaultDepthStencilFormat, format: gpu.gpuContext.defaultDepthStencilFormat,
); );
assert(depthStencilTexture != null);
final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer(); final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
final gpu.RenderTarget renderTarget = gpu.RenderTarget.singleColor( final gpu.RenderTarget renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: renderTexture!, clearValue: clearColor), gpu.ColorAttachment(texture: renderTexture, clearValue: clearColor),
depthStencilAttachment: gpu.DepthStencilAttachment(texture: depthStencilTexture!), depthStencilAttachment: gpu.DepthStencilAttachment(texture: depthStencilTexture),
); );
final gpu.RenderPass renderPass = commandBuffer.createRenderPass(renderTarget); final gpu.RenderPass renderPass = commandBuffer.createRenderPass(renderTarget);
@ -83,49 +81,46 @@ RenderPassState createSimpleRenderPassWithMSAA() {
// Create transient MSAA attachments, which will live entirely in tile memory // Create transient MSAA attachments, which will live entirely in tile memory
// for most GPUs. // for most GPUs.
final gpu.Texture? renderTexture = gpu.gpuContext.createTexture( final gpu.Texture renderTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient, gpu.StorageMode.deviceTransient,
100, 100,
100, 100,
format: gpu.gpuContext.defaultColorFormat, format: gpu.gpuContext.defaultColorFormat,
sampleCount: 4, sampleCount: 4,
); );
assert(renderTexture != null);
final gpu.Texture? depthStencilTexture = gpu.gpuContext.createTexture( final gpu.Texture depthStencilTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient, gpu.StorageMode.deviceTransient,
100, 100,
100, 100,
format: gpu.gpuContext.defaultDepthStencilFormat, format: gpu.gpuContext.defaultDepthStencilFormat,
sampleCount: 4, sampleCount: 4,
); );
assert(depthStencilTexture != null);
// Create the single-sample resolve texture that live in DRAM and will be // Create the single-sample resolve texture that live in DRAM and will be
// drawn to the screen. // drawn to the screen.
final gpu.Texture? resolveTexture = gpu.gpuContext.createTexture( final gpu.Texture resolveTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.devicePrivate, gpu.StorageMode.devicePrivate,
100, 100,
100, 100,
format: gpu.gpuContext.defaultColorFormat, format: gpu.gpuContext.defaultColorFormat,
); );
assert(resolveTexture != null);
final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer(); final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
final gpu.RenderTarget renderTarget = gpu.RenderTarget.singleColor( final gpu.RenderTarget renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment( gpu.ColorAttachment(
texture: renderTexture!, texture: renderTexture,
resolveTexture: resolveTexture, resolveTexture: resolveTexture,
storeAction: gpu.StoreAction.multisampleResolve, storeAction: gpu.StoreAction.multisampleResolve,
), ),
depthStencilAttachment: gpu.DepthStencilAttachment(texture: depthStencilTexture!), depthStencilAttachment: gpu.DepthStencilAttachment(texture: depthStencilTexture),
); );
final gpu.RenderPass renderPass = commandBuffer.createRenderPass(renderTarget); final gpu.RenderPass renderPass = commandBuffer.createRenderPass(renderTarget);
return RenderPassState(resolveTexture!, commandBuffer, renderPass); return RenderPassState(resolveTexture, commandBuffer, renderPass);
} }
void drawTriangle(RenderPassState state, Vector4 color) { void drawTriangle(RenderPassState state, Vector4 color) {
@ -228,23 +223,21 @@ void main() async {
}, skip: !impellerEnabled); }, skip: !impellerEnabled);
test('GpuContext.createDeviceBuffer', () async { test('GpuContext.createDeviceBuffer', () async {
final gpu.DeviceBuffer? deviceBuffer = gpu.gpuContext.createDeviceBuffer( final gpu.DeviceBuffer deviceBuffer = gpu.gpuContext.createDeviceBuffer(
gpu.StorageMode.hostVisible, gpu.StorageMode.hostVisible,
4, 4,
); );
assert(deviceBuffer != null);
expect(deviceBuffer!.sizeInBytes, 4); expect(deviceBuffer.sizeInBytes, 4);
}, skip: !impellerEnabled); }, skip: !impellerEnabled);
test('DeviceBuffer.overwrite', () async { test('DeviceBuffer.overwrite', () async {
final gpu.DeviceBuffer? deviceBuffer = gpu.gpuContext.createDeviceBuffer( final gpu.DeviceBuffer deviceBuffer = gpu.gpuContext.createDeviceBuffer(
gpu.StorageMode.hostVisible, gpu.StorageMode.hostVisible,
4, 4,
); );
assert(deviceBuffer != null);
final bool success = deviceBuffer!.overwrite( final bool success = deviceBuffer.overwrite(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(), Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
); );
deviceBuffer.flush(); deviceBuffer.flush();
@ -252,13 +245,12 @@ void main() async {
}, skip: !impellerEnabled); }, skip: !impellerEnabled);
test('DeviceBuffer.overwrite fails when out of bounds', () async { test('DeviceBuffer.overwrite fails when out of bounds', () async {
final gpu.DeviceBuffer? deviceBuffer = gpu.gpuContext.createDeviceBuffer( final gpu.DeviceBuffer deviceBuffer = gpu.gpuContext.createDeviceBuffer(
gpu.StorageMode.hostVisible, gpu.StorageMode.hostVisible,
4, 4,
); );
assert(deviceBuffer != null);
final bool success = deviceBuffer!.overwrite( final bool success = deviceBuffer.overwrite(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(), Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
destinationOffsetInBytes: 1, destinationOffsetInBytes: 1,
); );
@ -267,14 +259,13 @@ void main() async {
}, skip: !impellerEnabled); }, skip: !impellerEnabled);
test('DeviceBuffer.overwrite throws for negative destination offset', () async { test('DeviceBuffer.overwrite throws for negative destination offset', () async {
final gpu.DeviceBuffer? deviceBuffer = gpu.gpuContext.createDeviceBuffer( final gpu.DeviceBuffer deviceBuffer = gpu.gpuContext.createDeviceBuffer(
gpu.StorageMode.hostVisible, gpu.StorageMode.hostVisible,
4, 4,
); );
assert(deviceBuffer != null);
try { try {
deviceBuffer!.overwrite( deviceBuffer.overwrite(
Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(), Int8List.fromList(<int>[0, 1, 2, 3]).buffer.asByteData(),
destinationOffsetInBytes: -1, destinationOffsetInBytes: -1,
); );
@ -286,15 +277,10 @@ void main() async {
}, skip: !impellerEnabled); }, skip: !impellerEnabled);
test('GpuContext.createTexture', () async { test('GpuContext.createTexture', () async {
final gpu.Texture? texture = gpu.gpuContext.createTexture( final gpu.Texture texture = gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 100, 100);
gpu.StorageMode.hostVisible,
100,
100,
);
assert(texture != null);
// Check the defaults. // Check the defaults.
expect(texture!.coordinateSystem, gpu.TextureCoordinateSystem.renderToTexture); expect(texture.coordinateSystem, gpu.TextureCoordinateSystem.renderToTexture);
expect(texture.width, 100); expect(texture.width, 100);
expect(texture.height, 100); expect(texture.height, 100);
expect(texture.storageMode, gpu.StorageMode.hostVisible); expect(texture.storageMode, gpu.StorageMode.hostVisible);
@ -304,33 +290,25 @@ void main() async {
expect(texture.enableShaderReadUsage, true); expect(texture.enableShaderReadUsage, true);
expect(!texture.enableShaderWriteUsage, true); expect(!texture.enableShaderWriteUsage, true);
expect(texture.bytesPerTexel, 4); expect(texture.bytesPerTexel, 4);
expect(texture.GetBaseMipLevelSizeInBytes(), 40000); expect(texture.getBaseMipLevelSizeInBytes(), 40000);
}, skip: !impellerEnabled); }, skip: !impellerEnabled);
test('Texture.overwrite', () async { test('Texture.overwrite', () async {
final gpu.Texture? texture = gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 2, 2); final gpu.Texture texture = gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 2, 2);
assert(texture != null);
const ui.Color red = ui.Color.fromARGB(0xFF, 0xFF, 0, 0); const ui.Color red = ui.Color.fromARGB(0xFF, 0xFF, 0, 0);
const ui.Color green = ui.Color.fromARGB(0xFF, 0, 0xFF, 0); const ui.Color green = ui.Color.fromARGB(0xFF, 0, 0xFF, 0);
final bool success = texture!.overwrite( texture.overwrite(
Int32List.fromList(<int>[red.value, green.value, green.value, red.value]).buffer.asByteData(), Int32List.fromList(<int>[red.value, green.value, green.value, red.value]).buffer.asByteData(),
); );
expect(success, true);
}, skip: !impellerEnabled); }, skip: !impellerEnabled);
test('Texture.overwrite throws for wrong buffer size', () async { test('Texture.overwrite throws for wrong buffer size', () async {
final gpu.Texture? texture = gpu.gpuContext.createTexture( final gpu.Texture texture = gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 100, 100);
gpu.StorageMode.hostVisible,
100,
100,
);
assert(texture != null);
const ui.Color red = ui.Color.fromARGB(0xFF, 0xFF, 0, 0); const ui.Color red = ui.Color.fromARGB(0xFF, 0xFF, 0, 0);
try { try {
texture!.overwrite( texture.overwrite(
Int32List.fromList(<int>[red.value, red.value, red.value, red.value]).buffer.asByteData(), Int32List.fromList(<int>[red.value, red.value, red.value, red.value]).buffer.asByteData(),
); );
fail('Exception not thrown for wrong buffer size.'); fail('Exception not thrown for wrong buffer size.');
@ -345,29 +323,23 @@ void main() async {
}, skip: !impellerEnabled); }, skip: !impellerEnabled);
test('Texture.asImage returns a valid ui.Image handle', () async { test('Texture.asImage returns a valid ui.Image handle', () async {
final gpu.Texture? texture = gpu.gpuContext.createTexture( final gpu.Texture texture = gpu.gpuContext.createTexture(gpu.StorageMode.hostVisible, 100, 100);
gpu.StorageMode.hostVisible,
100,
100,
);
assert(texture != null);
final ui.Image image = texture!.asImage(); final ui.Image image = texture.asImage();
expect(image.width, 100); expect(image.width, 100);
expect(image.height, 100); expect(image.height, 100);
}, skip: !impellerEnabled); }, skip: !impellerEnabled);
test('Texture.asImage throws when not shader readable', () async { test('Texture.asImage throws when not shader readable', () async {
final gpu.Texture? texture = gpu.gpuContext.createTexture( final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.StorageMode.hostVisible, gpu.StorageMode.hostVisible,
100, 100,
100, 100,
enableShaderReadUsage: false, enableShaderReadUsage: false,
); );
assert(texture != null);
try { try {
texture!.asImage(); texture.asImage();
fail('Exception not thrown when not shader readable.'); fail('Exception not thrown when not shader readable.');
} catch (e) { } catch (e) {
expect( expect(
@ -457,8 +429,11 @@ void main() async {
// purposes of testing this error. // purposes of testing this error.
final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo'); final gpu.UniformSlot vertInfo = pipeline.vertexShader.getUniformSlot('VertInfo');
final gpu.Texture texture = final gpu.Texture texture = gpu.gpuContext.createTexture(
gpu.gpuContext.createTexture(gpu.StorageMode.deviceTransient, 100, 100)!; gpu.StorageMode.deviceTransient,
100,
100,
);
try { try {
state.renderPass.bindTexture(vertInfo, texture); state.renderPass.bindTexture(vertInfo, texture);
@ -495,7 +470,7 @@ void main() async {
0, 0, 0, 1, // mvp 0, 0, 0, 1, // mvp
0, 1, 0, 1, // color 0, 1, 0, 1, // color
]); ]);
final uniformBuffer = gpu.gpuContext.createDeviceBufferWithCopy(vertInfoData)!; final uniformBuffer = gpu.gpuContext.createDeviceBufferWithCopy(vertInfoData);
final gooduniformBufferView = gpu.BufferView( final gooduniformBufferView = gpu.BufferView(
uniformBuffer, uniformBuffer,
offsetInBytes: 0, offsetInBytes: 0,