| // |
| // Copyright 2025 The ANGLE Project Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| // frame_capture_binary_data.cpp: |
| // Common code for the ANGLE trace replay large trace binary data definition. |
| // |
| |
| #ifdef UNSAFE_BUFFERS_BUILD |
| # pragma allow_unsafe_buffers |
| #endif |
| |
| #define USE_SYSTEM_ZLIB |
| #include "compression_utils_portable.h" |
| |
| #include "common/mathutil.h" |
| #include "frame_capture_binary_data.h" |
| |
| #include <array> |
| #include <string> |
| |
| namespace angle |
| { |
| |
| // Return current size of all binary data |
| size_t FrameCaptureBinaryData::totalSize() const |
| { |
| return ((mBlockCount - 1) * mDataBlockSize) + mCurrentBlockOffset; |
| } |
| |
| // Determine if any blocks have been saved to disk, i.e., if we have run out of resident |
| // blocks |
| bool FrameCaptureBinaryData::isSwapMode() const |
| { |
| return (mStoredBlocks > 0); |
| } |
| |
| void FrameCaptureBinaryData::storeResidentBlocks() |
| { |
| // Write out all resident binary data blocks by calling storeBlock on each, deleting |
| // front() from vector |
| if (!isSwapMode()) |
| { |
| while (mData.size() > 1) |
| { |
| storeBlock(); |
| mData.erase(mData.begin()); |
| } |
| } |
| |
| storeBlock(); |
| } |
| |
| void FrameCaptureBinaryData::updateGetDataCache(size_t blockId) |
| { |
| const ReplayBlockDescription &desc = mReplayBlockDescriptions[blockId]; |
| |
| mCacheBlockId = blockId; |
| mCacheBlockBeginOffset = desc.beginDataOffset; |
| mCacheBlockEndOffset = desc.endDataOffset; |
| mCacheBlockBaseAddress = desc.residentAddress; |
| |
| // The location for the swap block differs for load and store. For store it will ultimately be |
| // zero as it's unnecessary to utilize the full BinaryDataSize. For load, it will end up |
| // as the last of the resident blocks. |
| if (blockId >= mMaxResidentBlockIndex) |
| { |
| mCurrentTransientLoadedBlockId = blockId; |
| } |
| } |
| |
| // Resident blocks will have a valid memory address at residentAddress |
| bool FrameCaptureBinaryData::isBlockResident(size_t blockId) const |
| { |
| return (mReplayBlockDescriptions[blockId].residentAddress != nullptr); |
| } |
| |
| void FrameCaptureBinaryData::setBlockResident(size_t blockId, uint8_t *address) |
| { |
| mReplayBlockDescriptions[blockId].residentAddress = address; |
| } |
| |
| void FrameCaptureBinaryData::setBlockNonResident(size_t blockId) |
| { |
| mReplayBlockDescriptions[blockId].residentAddress = nullptr; |
| } |
| |
| void FrameCaptureBinaryData::setBlockSize(size_t blockSize) |
| { |
| if (!gl::isPow2(blockSize)) |
| { |
| FATAL() << "Binary Data File Blocksize specified is not a power of 2: " << blockSize; |
| } |
| mDataBlockSize = blockSize; |
| } |
| |
| void FrameCaptureBinaryData::setBinaryDataSize(size_t binaryDataSize) |
| { |
| if (!gl::isPow2(binaryDataSize)) |
| { |
| FATAL() << "Binary Data File Binary Data Size specified is not a power of 2: " |
| << binaryDataSize; |
| } |
| mMaxResidentBinarySize = binaryDataSize; |
| } |
| |
| std::vector<uint8_t> &FrameCaptureBinaryData::prepareStoreBlock(size_t blockId) |
| { |
| // Ensure mData has enough vectors up to and including the target index |
| if (!isSwapMode()) |
| { |
| mData.resize(mData.size() + 1); |
| } |
| |
| mBlockCount = blockId + 1; |
| |
| mData.back().resize(mDataBlockSize); |
| mCurrentBlockOffset = 0; |
| |
| return mData.back(); |
| } |
| |
| std::vector<uint8_t> &FrameCaptureBinaryData::prepareLoadBlock(size_t blockId) |
| { |
| size_t destBlockIndex = std::min(blockId, mMaxResidentBlockIndex); |
| |
| // Ensure mData has enough vectors up to the target index |
| if (destBlockIndex >= mData.size()) |
| { |
| mData.resize(destBlockIndex + 1); |
| } |
| |
| if (isSwapBlock(destBlockIndex)) |
| { |
| // If not the same block, mark previous block occupying swap slot as non-resident |
| if (blockId != mCurrentTransientLoadedBlockId) |
| { |
| // Since this is the swap block, we aren't actually freeing any memory. But we need |
| // a way to indicate whether a transient block is loaded. This way each logical |
| // block knows whether it is resident, and where. |
| setBlockNonResident(mCurrentTransientLoadedBlockId); |
| } |
| // Track which logical block is now in the swap slot |
| mCurrentTransientLoadedBlockId = blockId; |
| } |
| |
| mData.back().resize(mDataBlockSize); |
| mCurrentBlockOffset = 0; |
| |
| return mData.back(); |
| } |
| |
| // Write file index entries to the end of compressed binary data files |
| BinaryFileIndexInfo FrameCaptureBinaryData::appendFileIndex() |
| { |
| BinaryFileIndexInfo indexInfo; |
| indexInfo.version = kLongTraceVersionId; |
| indexInfo.blockSize = mDataBlockSize; |
| indexInfo.blockCount = mBlockCount; |
| indexInfo.residentSize = mMaxResidentBinarySize; |
| indexInfo.indexOffset = 0; |
| |
| if (mIsBinaryDataCompressed) |
| { |
| size_t indexDataOffset = mFileStream->getPosition(); |
| // Copy index entries (index data trailer) to end of compressed data file |
| for (auto &entry : mFileIndex) |
| { |
| mFileStream->write(reinterpret_cast<const uint8_t *>(&entry), sizeof(FileBlockInfo)); |
| } |
| indexInfo.indexOffset = indexDataOffset; |
| } |
| |
| // Return index information for saving in JSON file |
| return indexInfo; |
| } |
| |
| // Read in file index data from a compressed file and construct an access index |
| void FrameCaptureBinaryData::constructBlockDescIndex(size_t indexOffset) |
| { |
| if (mIsBinaryDataCompressed) |
| { |
| // Move to the beginning of the index data in the compressed file |
| mFileStream->seek(indexOffset, kSeekBegin); |
| |
| // Populate the replay block description array |
| for (size_t i = 0; i < mBlockCount; i++) |
| { |
| // Read in block's information data |
| FileBlockInfo blockInfo; |
| mFileStream->read(reinterpret_cast<uint8_t *>(&blockInfo), sizeof(FileBlockInfo)); |
| |
| // Create and save a block description from the block information |
| ReplayBlockDescription blockDesc = {}; |
| blockDesc.fileOffset = blockInfo.fileOffset; |
| blockDesc.beginDataOffset = blockInfo.dataOffset; |
| blockDesc.endDataOffset = blockInfo.dataOffset + blockInfo.dataSize - 1; |
| blockDesc.dataSize = blockInfo.dataSize; |
| mReplayBlockDescriptions.push_back(blockDesc); |
| } |
| } |
| else |
| { |
| // Create block descriptions from fixed size calculations |
| mFileStream->seek(0, kSeekEnd); |
| size_t size = mFileStream->getPosition(); |
| mFileStream->seek(0, kSeekBegin); |
| |
| size_t remaining = size; |
| while (remaining > 0) |
| { |
| // The final block is typically smaller than mDataBlockSize |
| size_t dataSize = std::min(remaining, mDataBlockSize); |
| size_t offset = size - remaining; |
| |
| // Create and save a block description |
| ReplayBlockDescription blockDesc = {}; |
| blockDesc.fileOffset = offset; |
| blockDesc.beginDataOffset = offset; |
| blockDesc.endDataOffset = offset + dataSize - 1; |
| blockDesc.dataSize = dataSize; |
| mReplayBlockDescriptions.push_back(blockDesc); |
| remaining -= dataSize; |
| } |
| } |
| } |
| |
| size_t FrameCaptureBinaryData::append(const void *data, size_t size) |
| { |
| if (mData.empty()) |
| { |
| prepareStoreBlock(0); |
| mBlockCount = 1; |
| } |
| |
| ASSERT(totalSize() % kBinaryAlignment == 0); |
| size_t startingOffset = totalSize(); |
| const size_t sizeToIncrease = rx::roundUpPow2(size, kBinaryAlignment); |
| |
| // If the requested data size will not fit into the current block, allocate |
| // a new block |
| if (mCurrentBlockOffset + sizeToIncrease > mDataBlockSize) |
| { |
| size_t newBlockId = (startingOffset + sizeToIncrease) / mDataBlockSize; |
| |
| if (!isSwapMode()) |
| { |
| if (newBlockId > mMaxResidentBlockIndex) |
| { |
| // All resident blocks are full, store them to disk |
| storeResidentBlocks(); |
| } |
| else |
| { |
| // Resident blocks available, no need to store to disk |
| } |
| } |
| else |
| { |
| // Resident blocks have been saved, write this block to disk |
| storeBlock(); |
| } |
| prepareStoreBlock(newBlockId); |
| startingOffset = totalSize(); |
| } |
| |
| memcpy(mData.back().data() + mCurrentBlockOffset, data, size); |
| mCurrentBlockOffset += sizeToIncrease; |
| return startingOffset; |
| } |
| |
| const uint8_t *FrameCaptureBinaryData::getData(size_t offset) |
| { |
| // This is the fastpath for this function, misses should be negligible |
| if (offset >= mCacheBlockBeginOffset && offset < mCacheBlockEndOffset) |
| { |
| return (mCacheBlockBaseAddress + (offset - mCacheBlockBeginOffset)); |
| } |
| |
| // Calculate new block id for binary data to be loaded |
| size_t newBlockId = offset / mDataBlockSize; |
| // Swap block into memory if it is nonresident |
| if (!isBlockResident(newBlockId)) |
| { |
| loadBlock(newBlockId); |
| } |
| // Update the fastpath cache variables |
| updateGetDataCache(newBlockId); |
| |
| return (mCacheBlockBaseAddress + (offset - mCacheBlockBeginOffset)); |
| } |
| |
| void FrameCaptureBinaryData::clear() |
| { |
| mCurrentBlockOffset = 0; |
| mFileIndex.clear(); |
| mReplayBlockDescriptions.clear(); |
| mData.clear(); |
| } |
| |
| // Helper class for compression/decompression operations |
| class ZLibHelper |
| { |
| // See the following file for details on these variables and helpers: |
| // https://chromium.googlesource.com/chromium/src/+/master/third_party/zlib/google/compression_utils_portable.cc |
| static constexpr int kZlibMemoryLevel = 8; |
| static constexpr int kWindowBitsToGetGzipHeader = 16; |
| |
| public: |
| ZLibHelper(FrameCaptureBinaryData::Mode mode) : mMode(mode), mStream(), mInitialized(false) |
| { |
| int ret = 0; |
| mStream.zalloc = Z_NULL; |
| mStream.zfree = Z_NULL; |
| mStream.opaque = Z_NULL; |
| mStream.avail_in = 0; |
| mStream.next_in = Z_NULL; |
| |
| if (mMode == FrameCaptureBinaryData::Mode::Load) |
| { |
| ret = inflateInit2(&mStream, MAX_WBITS + kWindowBitsToGetGzipHeader); |
| } |
| else if (mMode == FrameCaptureBinaryData::Mode::Store) |
| { |
| ret = deflateInit2(&mStream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, |
| MAX_WBITS + kWindowBitsToGetGzipHeader, kZlibMemoryLevel, |
| Z_DEFAULT_STRATEGY); |
| } |
| else |
| { |
| FATAL() << "Invalid Mode Enum in ZLibHelper"; |
| } |
| |
| if (ret != Z_OK) |
| { |
| FATAL() << "Zlib helper initialization failed: " << ret; |
| } |
| mInitialized = true; |
| } |
| |
| ~ZLibHelper() |
| { |
| if (mInitialized) |
| { |
| if (mMode == FrameCaptureBinaryData::Mode::Load) |
| { |
| inflateEnd(&mStream); |
| } |
| else if (mMode == FrameCaptureBinaryData::Mode::Store) |
| { |
| deflateEnd(&mStream); |
| } |
| else |
| { |
| FATAL() << "Invalid Mode Enum in ZLibHelper"; |
| } |
| } |
| } |
| |
| z_stream *getStream() { return &mStream; } |
| |
| ZLibHelper(const ZLibHelper &) = delete; |
| ZLibHelper &operator=(const ZLibHelper &) = delete; |
| ZLibHelper(ZLibHelper &&) = delete; |
| ZLibHelper &operator=(ZLibHelper &&) = delete; |
| |
| private: |
| FrameCaptureBinaryData::Mode mMode; |
| z_stream mStream; |
| bool mInitialized; |
| }; |
| |
| // Configure binary data output parameters and prepare file for writing |
| void FrameCaptureBinaryData::initializeBinaryDataStore(bool compression, |
| const std::string &outDir, |
| const std::string &fileName) |
| { |
| std::string binaryDataFileName = outDir + fileName; |
| mStoredBlocks = 0; |
| mIsBinaryDataCompressed = compression; |
| |
| if ((mMaxResidentBinarySize / mDataBlockSize) <= 1) |
| { |
| FATAL() << "Error,insufficient resident memory specified or available"; |
| } |
| mMaxResidentBlockIndex = (mMaxResidentBinarySize / mDataBlockSize) - 1; |
| |
| mFileStream = new FileStream(binaryDataFileName, Mode::Store); |
| } |
| |
| // Optionally compress and then write a single data block to disk |
| void FrameCaptureBinaryData::storeBlock() |
| { |
| std::vector<uint8_t> &storeBlock = mData.front(); |
| |
| // The last block to be saved will be resized to fit used data |
| if (mCaptureComplete && mData.size() == 1) |
| { |
| storeBlock.resize(mCurrentBlockOffset); |
| } |
| |
| if (mIsBinaryDataCompressed) |
| { |
| // Use zlib library, based on example/doc here: https://zlib.net/zlib_how.html |
| ZLibHelper compressor(Mode::Store); |
| z_stream *zStream = compressor.getStream(); |
| |
| int deflateStatus = 0; |
| using ZlibBuffer = std::array<unsigned char, kZlibBufferSize>; |
| std::unique_ptr<ZlibBuffer> compressBuffer(new ZlibBuffer()); |
| |
| FileBlockInfo fileIndexEntry; |
| fileIndexEntry.fileOffset = mFileStream->getPosition(); // CompressedFileOffset |
| fileIndexEntry.dataOffset = mStoredBlocks * mDataBlockSize; // UncompressedOffset |
| fileIndexEntry.dataSize = storeBlock.size(); // Size of block |
| // Save file index data |
| mFileIndex.push_back(fileIndexEntry); |
| |
| const unsigned char *uncompressedDataPtr = storeBlock.data(); |
| size_t remainingBytesToCompress = storeBlock.size(); |
| |
| while (remainingBytesToCompress > 0) |
| { |
| size_t bytesToCompress = |
| std::min(remainingBytesToCompress, static_cast<size_t>(kZlibBufferSize)); |
| zStream->avail_in = static_cast<uInt>(bytesToCompress); |
| zStream->next_in = const_cast<unsigned char *>(uncompressedDataPtr); |
| |
| do |
| { |
| zStream->avail_out = kZlibBufferSize; |
| zStream->next_out = compressBuffer->data(); |
| |
| int flushMode = Z_NO_FLUSH; |
| if (remainingBytesToCompress <= kZlibBufferSize) |
| { |
| flushMode = Z_FINISH; |
| } |
| deflateStatus = deflate(zStream, flushMode); |
| if (deflateStatus == Z_STREAM_ERROR) |
| { |
| FATAL() << "Error during deflate: Z_STREAM_ERROR"; |
| } |
| // This is the compressed data size about to be written |
| unsigned bytesCompressed = kZlibBufferSize - zStream->avail_out; |
| mFileStream->write(compressBuffer->data(), bytesCompressed); |
| } while (zStream->avail_out == 0); |
| |
| uncompressedDataPtr += bytesToCompress; |
| remainingBytesToCompress -= bytesToCompress; |
| } |
| } |
| else |
| { |
| mFileStream->write(storeBlock.data(), storeBlock.size()); |
| } |
| |
| mStoredBlocks++; |
| } |
| |
| BinaryFileIndexInfo FrameCaptureBinaryData::closeBinaryDataStore() |
| { |
| mCaptureComplete = true; |
| storeResidentBlocks(); |
| |
| BinaryFileIndexInfo indexInfo; |
| indexInfo = appendFileIndex(); |
| clear(); |
| return indexInfo; |
| } |
| |
| // Sets up binary data loader with config data from the trace fixture |
| void FrameCaptureBinaryData::configureBinaryDataLoader(bool compression, |
| size_t blockCount, |
| size_t blockSize, |
| size_t residentSize, |
| size_t indexOffset, |
| const std::string &fileName) |
| { |
| mIsBinaryDataCompressed = compression; |
| mFileName = fileName; |
| mMaxResidentBinarySize = residentSize; |
| mDataBlockSize = blockSize; |
| mBlockCount = blockCount; |
| mMaxResidentBlockIndex = (mMaxResidentBinarySize / mDataBlockSize) - 1; |
| mCurrentTransientLoadedBlockId = mMaxResidentBlockIndex; |
| mIndexOffset = indexOffset; |
| } |
| |
| // Setup binary data file access, init index and preload data blocks up to limit |
| void FrameCaptureBinaryData::initializeBinaryDataLoader() |
| { |
| // Create file stream manager |
| mFileStream = new FileStream(mFileName.c_str(), Mode::Load); |
| |
| // Assemble binary data file/cache index |
| constructBlockDescIndex(mIndexOffset); |
| |
| // Preload binary data blocks up to limit |
| size_t blocksToPreload = |
| std::min(mReplayBlockDescriptions.size(), (mMaxResidentBlockIndex + 1)); |
| for (size_t i = 0; i < blocksToPreload; i++) |
| { |
| loadBlock(i); |
| } |
| |
| // Initialize getData cache |
| updateGetDataCache(0); |
| } |
| |
| // Load a single data block into memory |
| void FrameCaptureBinaryData::loadBlock(size_t blockId) |
| { |
| std::vector<uint8_t> &uncompressedDataBlock = prepareLoadBlock(blockId); |
| |
| // Move to start of this data block in the data file |
| mFileStream->seek(mReplayBlockDescriptions[blockId].fileOffset, kSeekBegin); |
| |
| if (mIsBinaryDataCompressed) |
| { |
| // Use zlib library, based on example/doc here: https://zlib.net/zlib_how.html |
| ZLibHelper decompressor(Mode::Load); |
| z_stream *zStream = decompressor.getStream(); |
| int inflateStatus = 0; |
| size_t bytesDecompressed = 0; |
| |
| using ZlibBuffer = std::array<unsigned char, kZlibBufferSize>; |
| std::unique_ptr<ZlibBuffer> compressedDataBuffer(new ZlibBuffer()); |
| zStream->avail_out = static_cast<uInt>(mDataBlockSize); |
| zStream->next_out = uncompressedDataBlock.data(); |
| |
| do |
| { |
| if (zStream->avail_in == 0) |
| { |
| zStream->avail_in = static_cast<uInt>( |
| mFileStream->read(compressedDataBuffer->data(), kZlibBufferSize)); |
| zStream->next_in = compressedDataBuffer->data(); |
| } |
| |
| do |
| { |
| int availableOutputSpace = static_cast<int>(mDataBlockSize - mCurrentBlockOffset); |
| zStream->avail_out = availableOutputSpace; |
| zStream->next_out = uncompressedDataBlock.data() + mCurrentBlockOffset; |
| inflateStatus = inflate(zStream, Z_NO_FLUSH); |
| ASSERT(inflateStatus != Z_STREAM_ERROR); |
| if (inflateStatus == Z_NEED_DICT || inflateStatus == Z_DATA_ERROR || |
| inflateStatus == Z_MEM_ERROR) |
| { |
| FATAL() << "Zlib inflate failed: " << inflateStatus; |
| } |
| bytesDecompressed = availableOutputSpace - zStream->avail_out; |
| mCurrentBlockOffset += bytesDecompressed; |
| } while (zStream->avail_out == 0 && mCurrentBlockOffset < mDataBlockSize); |
| } while (inflateStatus != Z_STREAM_END && mCurrentBlockOffset != mDataBlockSize); |
| } |
| else |
| { |
| mCurrentBlockOffset = mFileStream->read(uncompressedDataBlock.data(), mDataBlockSize); |
| } |
| |
| // Except for the last block this resize will be a no-op |
| uncompressedDataBlock.resize(mCurrentBlockOffset); |
| // Indicate that this block is now loaded |
| setBlockResident(blockId, uncompressedDataBlock.data()); |
| } |
| |
| void FrameCaptureBinaryData::closeBinaryDataLoader() |
| { |
| clear(); |
| } |
| |
| int FileStreamSeek(FILE *stream, long long offset, int whence) |
| { |
| #if defined(ANGLE_PLATFORM_WINDOWS) |
| return _fseeki64(stream, offset, whence); |
| #else |
| return fseeko(stream, static_cast<off_t>(offset), whence); |
| #endif |
| } |
| |
| long long FileStreamTell(FILE *stream) |
| { |
| #if defined(ANGLE_PLATFORM_WINDOWS) |
| return _ftelli64(stream); |
| #else |
| return ftello(stream); |
| #endif |
| } |
| |
| void FileStream::write(const uint8_t *data, size_t size) |
| { |
| if (fwrite(data, 1, size, mFile) != size) |
| { |
| if (ferror(mFile)) |
| { |
| FATAL() << "Error writing " << size << " bytes to binary data file."; |
| } |
| } |
| if (fflush(mFile) != 0) |
| { |
| FATAL() << "Error flushing data to binary data file."; |
| } |
| } |
| |
| size_t FileStream::read(uint8_t *buffer, size_t size) |
| { |
| size_t readBytes = fread(buffer, 1, size, mFile); |
| if (readBytes < size && ferror(mFile)) |
| { |
| FATAL() << "Error reading from binary data file."; |
| } |
| return readBytes; |
| } |
| |
| void FileStream::seek(long long offset, int whence) |
| { |
| if (FileStreamSeek(mFile, offset, whence) != 0) |
| { |
| FATAL() << "Error seeking in binary data file with offset " << offset << " and whence " |
| << whence; |
| } |
| } |
| |
| size_t FileStream::getPosition() |
| { |
| long long offset = FileStreamTell(mFile); |
| if (offset == -1) |
| { |
| FATAL() << "Error getting position in binary data file " << mFilePath; |
| } |
| angle::CheckedNumeric<size_t> checkedOffset(offset); |
| size_t safeOffset = 0; |
| if (!checkedOffset.AssignIfValid(&safeOffset)) |
| { |
| FATAL() << "ANGLE file seek position offset out of range"; |
| } |
| |
| return safeOffset; |
| } |
| } // namespace angle |