[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')
void canCreateRenderPassAndSubmit(int width, int height) {
final gpu.Texture? renderTexture = gpu.gpuContext.createTexture(
final gpu.Texture renderTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.devicePrivate,
width,
height,
);
assert(renderTexture != null);
final gpu.CommandBuffer commandBuffer = gpu.gpuContext.createCommandBuffer();
final gpu.RenderTarget renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: renderTexture!),
gpu.ColorAttachment(texture: renderTexture),
);
final gpu.RenderPass encoder = commandBuffer.createRenderPass(renderTarget);

View File

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

View File

@ -63,8 +63,8 @@ base class GpuContext extends NativeFieldWrapperClass1 {
/// The [storageMode] must be either [StorageMode.hostVisible] or
/// [StorageMode.devicePrivate], otherwise an exception will be thrown.
///
/// Returns [null] if the [DeviceBuffer] creation failed.
DeviceBuffer? createDeviceBuffer(StorageMode storageMode, int sizeInBytes) {
/// Throws an exception if the [DeviceBuffer] creation failed.
DeviceBuffer createDeviceBuffer(StorageMode storageMode, int sizeInBytes) {
if (storageMode == StorageMode.deviceTransient) {
throw Exception(
'DeviceBuffers cannot be set to StorageMode.deviceTransient',
@ -75,7 +75,10 @@ base class GpuContext extends NativeFieldWrapperClass1 {
storageMode,
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
@ -85,10 +88,13 @@ base class GpuContext extends NativeFieldWrapperClass1 {
/// from the host, the [StorageMode] of the new [DeviceBuffer] is
/// automatically set to [StorageMode.hostVisible].
///
/// Returns [null] if the [DeviceBuffer] creation failed.
DeviceBuffer? createDeviceBufferWithCopy(ByteData data) {
/// Throws an exception if the [DeviceBuffer] creation failed.
DeviceBuffer createDeviceBufferWithCopy(ByteData 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.
@ -102,8 +108,8 @@ base class GpuContext extends NativeFieldWrapperClass1 {
/// Allocates a new texture in GPU-resident memory.
///
/// Returns [null] if the [Texture] creation failed.
Texture? createTexture(
/// Throws an exception if the [Texture] creation failed.
Texture createTexture(
StorageMode storageMode,
int width,
int height, {
@ -127,7 +133,10 @@ base class GpuContext extends NativeFieldWrapperClass1 {
enableShaderReadUsage,
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.

View File

@ -73,7 +73,7 @@ base class Texture extends NativeFieldWrapperClass1 {
return _bytesPerTexel();
}
int GetBaseMipLevelSizeInBytes() {
int getBaseMipLevelSizeInBytes() {
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 is always `width * height * bytesPerPixel`.
///
/// Returns [true] if the write was successful, or [false] if the write
/// failed due to an internal error.
bool overwrite(ByteData sourceBytes) {
/// Throws an exception if the write failed due to an internal error.
void overwrite(ByteData sourceBytes) {
if (storageMode != StorageMode.hostVisible) {
throw Exception(
'Texture.overwrite can only be used with Textures that are host visible',
);
}
int baseMipSize = GetBaseMipLevelSizeInBytes();
int baseMipSize = getBaseMipLevelSizeInBytes();
if (sourceBytes.lengthInBytes != baseMipSize) {
throw Exception(
'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() {

View File

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