// 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. // ignore_for_file: public_member_api_docs part of flutter_gpu; /// A reference to a byte range within a GPU-resident [Buffer]. class BufferView { /// The device buffer of this view. final DeviceBuffer buffer; /// The start of the view, in bytes starting from the beginning of the /// [buffer]. final int offsetInBytes; /// The length of the view. final int lengthInBytes; /// Create a new view into a buffer on the GPU. const BufferView( this.buffer, { required this.offsetInBytes, required this.lengthInBytes, }); } /// [DeviceBuffer] is a region of memory allocated on the device heap /// (GPU-resident memory). base class DeviceBuffer extends NativeFieldWrapperClass1 { bool _valid = false; get isValid { return _valid; } /// Creates a new DeviceBuffer. DeviceBuffer._initialize( GpuContext gpuContext, StorageMode storageMode, int sizeInBytes, ) : storageMode = storageMode, sizeInBytes = sizeInBytes { _valid = _initialize(gpuContext, storageMode.index, sizeInBytes); } /// Creates a new host visible DeviceBuffer with data copied from the host. DeviceBuffer._initializeWithHostData(GpuContext gpuContext, ByteData data) : storageMode = StorageMode.hostVisible, sizeInBytes = data.lengthInBytes { _valid = _initializeWithHostData(gpuContext, data); } final StorageMode storageMode; final int sizeInBytes; void _bindAsVertexBuffer( RenderPass renderPass, int offsetInBytes, int lengthInBytes, int vertexCount, ) { renderPass._bindVertexBufferDevice( this, offsetInBytes, lengthInBytes, vertexCount, ); } void _bindAsIndexBuffer( RenderPass renderPass, int offsetInBytes, int lengthInBytes, IndexType indexType, int indexCount, ) { renderPass._bindIndexBufferDevice( this, offsetInBytes, lengthInBytes, indexType.index, indexCount, ); } bool _bindAsUniform( RenderPass renderPass, UniformSlot slot, int offsetInBytes, int lengthInBytes, ) { return renderPass._bindUniformDevice( slot.shader, slot.uniformName, this, offsetInBytes, lengthInBytes, ); } /// Wrap with native counterpart. @Native, Int, Int)>( symbol: 'InternalFlutterGpu_DeviceBuffer_Initialize', ) external bool _initialize( GpuContext gpuContext, int storageMode, int sizeInBytes, ); /// Wrap with native counterpart. @Native, Handle)>( symbol: 'InternalFlutterGpu_DeviceBuffer_InitializeWithHostData', ) external bool _initializeWithHostData(GpuContext gpuContext, ByteData data); /// Overwrite a range of bytes within an existing [DeviceBuffer]. /// /// This method can only be used if the [DeviceBuffer] was created with /// [StorageMode.hostVisible]. An exception will be thrown otherwise. /// /// After new writes have been staged, the [DeviceBuffer.flush] should be /// called prior to accessing the data. Otherwise, the updated data will not /// be copied to the GPU on devices that don't have host coherent memory. /// /// The entire length of [sourceBytes] will be copied into the [DeviceBuffer], /// starting at byte index [destinationOffsetInBytes] in the [DeviceBuffer]. /// If performing this copy would result in an out of bounds write to the /// buffer, then the write will not be attempted and will fail. /// /// Returns [true] if the write was successful, or [false] if the write /// failed due to an internal error. bool overwrite(ByteData sourceBytes, {int destinationOffsetInBytes = 0}) { if (storageMode != StorageMode.hostVisible) { throw Exception( 'DeviceBuffer.overwrite can only be used with DeviceBuffers that are host visible', ); } if (destinationOffsetInBytes < 0) { throw Exception('destinationOffsetInBytes must be positive'); } return _overwrite(sourceBytes, destinationOffsetInBytes); } @Native, Handle, Int)>( symbol: 'InternalFlutterGpu_DeviceBuffer_Overwrite', ) external bool _overwrite(ByteData bytes, int destinationOffsetInBytes); /// Flush the contents of the [DeviceBuffer] to the GPU. /// /// This method can only be used if the [DeviceBuffer] was created with /// [StorageMode.hostVisible]. An exception will be thrown otherwise. /// /// If [lengthInBytes] is set to -1, the entire buffer will be flushed. /// /// On devices with coherent host memory (memory shared between the CPU and /// GPU), this method is a no-op. void flush({int offsetInBytes = 0, int lengthInBytes = -1}) { if (storageMode != StorageMode.hostVisible) { throw Exception( 'DeviceBuffer.flush can only be used with DeviceBuffers that are host visible', ); } if (offsetInBytes < 0 || offsetInBytes >= sizeInBytes) { throw Exception('offsetInBytes must be within the bounds of the buffer'); } if (lengthInBytes < -1) { throw Exception('lengthInBytes must be either positive or -1'); } if (lengthInBytes != -1 && offsetInBytes + lengthInBytes > sizeInBytes) { throw Exception( 'The provided range must not be too large to fit within the buffer', ); } _flush(offsetInBytes, lengthInBytes); } @Native, Int, Int)>( symbol: 'InternalFlutterGpu_DeviceBuffer_Flush', ) external void _flush(int offsetInBytes, int lengthInBytes); } /// [HostBuffer] is a bump allocator that managed a [DeviceBuffer] block list. /// /// This is useful for chunking sparse data uploads, especially ephemeral /// uniform or vertex data that needs to change from frame to frame. /// /// Different platforms have different data alignment requirements when reading /// [DeviceBuffer] data for shader uniforms. [HostBuffer] uses /// [GpuContext.minimumUniformByteAlignment] to align each emplacement /// automatically, so that they may be used in uniform bindings. /// /// The length of each [DeviceBuffer] block is determined by /// [blockLengthInBytes] and cannot be changed after creation of the /// [HostBuffer]. If [HostBuffer.emplace] is given a [ByteData] that is larger /// than [blockLengthInBytes], a new [DeviceBuffer] block is created that /// matches the size of the oversized [ByteData]. base class HostBuffer { /// The default length to use for each [DeviceBuffer] block. static const int kDefaultBlockLengthInBytes = 1024000; // 1024 Kb /// The length to use for each [DeviceBuffer] block. final int blockLengthInBytes; static const int _kFrameCount = 4; /// The number of frames to cycle through before reusing device buffers. /// Cycling to the next frame happens when [reset] is called. int get frameCount { return _kFrameCount; } final GpuContext _gpuContext; /// The current frame. Rotates through [frameCount] frames when [reset] is /// called. int _frameCursor = 0; /// The buffer within the current frame to be used for the next emplacement. int _bufferCursor = 0; /// The offset within the current block to be used for the next emplacement. int _offsetCursor = 0; final List> _buffers = []; /// Creates a new HostBuffer. HostBuffer._initialize( this._gpuContext, { this.blockLengthInBytes = HostBuffer.kDefaultBlockLengthInBytes, }) { for (int i = 0; i < frameCount; i++) { List frame = []; _buffers.add(frame); _buffers[i].add(_allocateNewBlock(blockLengthInBytes)); } } DeviceBuffer _allocateNewBlock(length) { final buffer = _gpuContext.createDeviceBuffer( StorageMode.hostVisible, length, ); return buffer; } /// Prepare a new buffer range to be used for storing the given bytes. /// Allocates a new block if necessary. BufferView _allocateEmplacement(ByteData bytes) { if (bytes.lengthInBytes > blockLengthInBytes) { return BufferView( _allocateNewBlock(bytes.lengthInBytes), offsetInBytes: 0, lengthInBytes: bytes.lengthInBytes, ); } int padding = _gpuContext.minimumUniformByteAlignment - (_offsetCursor % _gpuContext.minimumUniformByteAlignment); // If the padding is the full alignment size, then we're already aligned. // So reset the padding to zero. padding %= _gpuContext.minimumUniformByteAlignment; if (_offsetCursor + padding >= blockLengthInBytes) { DeviceBuffer buffer = _allocateNewBlock(blockLengthInBytes); _buffers[_frameCursor].add(buffer); _bufferCursor++; _offsetCursor = bytes.lengthInBytes; return BufferView( buffer, offsetInBytes: 0, lengthInBytes: blockLengthInBytes, ); } _offsetCursor += padding; final view = BufferView( _buffers[_frameCursor][_bufferCursor], offsetInBytes: _offsetCursor, lengthInBytes: bytes.lengthInBytes, ); _offsetCursor += bytes.lengthInBytes; return view; } /// Append byte data to the end of the [HostBuffer] and produce a [BufferView] /// that references the new data in the buffer. /// /// This method automatically inserts padding in-between emplace calls in the /// buffer if necessary to abide by platform-specific uniform alignment /// requirements. /// /// The [DeviceBuffer] referenced in the [BufferView] has already been /// flushed, so there is no need to call [DeviceBuffer.flush] before /// referencing it in a command. BufferView emplace(ByteData bytes) { BufferView view = _allocateEmplacement(bytes); if (!view.buffer.overwrite( bytes, destinationOffsetInBytes: view.offsetInBytes, )) { throw Exception( 'Failed to write range (offset=${view.offsetInBytes}, length=${view.lengthInBytes}) ' 'to HostBuffer-managed DeviceBuffer (frame=$_frameCursor, buffer=$_bufferCursor, offset=$_offsetCursor).', ); } return view; } /// Resets the bump allocator to the beginning of the first [DeviceBuffer] /// block. void reset() { _frameCursor = (_frameCursor + 1) % frameCount; _bufferCursor = 0; _offsetCursor = 0; } }