/****************************************************************************** * The MIT License (MIT) * * Copyright (c) 2019-2024 Baldur Karlsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. ******************************************************************************/ #include "streamio.h" #include #include "api/replay/stringise.h" #include "common/timing.h" Compressor::~Compressor() { if(m_Ownership == Ownership::Stream && m_Write) delete m_Write; } Decompressor::~Decompressor() { if(m_Ownership == Ownership::Stream && m_Read) delete m_Read; } static const uint64_t initialBufferSize = 64 * 1024; const byte StreamWriter::empty[128] = {}; StreamReader::StreamReader(const byte *buffer, uint64_t bufferSize) { m_InputSize = m_BufferSize = bufferSize; m_BufferHead = m_BufferBase = AllocAlignedBuffer(m_BufferSize); memcpy(m_BufferHead, buffer, (size_t)m_BufferSize); m_Ownership = Ownership::Nothing; } StreamReader::StreamReader(const bytebuf &buffer) { m_InputSize = m_BufferSize = buffer.size(); m_BufferHead = m_BufferBase = AllocAlignedBuffer(m_BufferSize); memcpy(m_BufferHead, buffer.data(), (size_t)m_BufferSize); m_Ownership = Ownership::Nothing; } StreamReader::StreamReader(StreamInvalidType, RDResult res) { m_InputSize = 0; m_BufferSize = 0; m_BufferHead = m_BufferBase = NULL; m_Ownership = Ownership::Nothing; m_Error = res; if(m_Error == ResultCode::Succeeded) SET_ERROR_RESULT(m_Error, ResultCode::InvalidParameter, "Invalid stream created with no error code"); } StreamReader::StreamReader(StreamDummyType) { m_InputSize = 0; m_BufferSize = 0; m_BufferHead = m_BufferBase = NULL; m_Ownership = Ownership::Nothing; m_Dummy = true; } StreamReader::StreamReader(Network::Socket *sock, Ownership own) { if(sock == NULL) { SET_ERROR_RESULT(m_Error, ResultCode::InvalidParameter, "Stream created with invalid socket"); m_InputSize = 0; m_BufferSize = 0; m_BufferHead = m_BufferBase = NULL; m_Ownership = Ownership::Nothing; return; } m_Sock = sock; m_BufferSize = initialBufferSize; m_BufferBase = AllocAlignedBuffer(m_BufferSize); m_BufferHead = m_BufferBase; // for sockets we use m_InputSize to indicate how much data has been read into the buffer. m_InputSize = 0; m_Ownership = own; } StreamReader::StreamReader(FILE *file, uint64_t fileSize, Ownership own) { if(file == NULL) { SET_ERROR_RESULT(m_Error, ResultCode::InvalidParameter, "Stream created with invalid file handle"); m_InputSize = 0; m_BufferSize = 0; m_BufferHead = m_BufferBase = NULL; m_Ownership = Ownership::Nothing; return; } m_File = file; m_InputSize = fileSize; m_BufferSize = initialBufferSize; m_BufferHead = m_BufferBase = AllocAlignedBuffer(m_BufferSize); ReadFromExternal(m_BufferBase, RDCMIN(m_InputSize, m_BufferSize)); m_Ownership = own; } StreamReader::StreamReader(FILE *file) { if(file == NULL) { SET_ERROR_RESULT(m_Error, ResultCode::InvalidParameter, "Stream created with invalid file handle"); m_InputSize = 0; m_BufferSize = 0; m_BufferHead = m_BufferBase = NULL; m_Ownership = Ownership::Nothing; return; } FileIO::fseek64(file, 0, SEEK_END); m_InputSize = FileIO::ftell64(file); FileIO::fseek64(file, 0, SEEK_SET); m_File = file; m_BufferSize = initialBufferSize; m_BufferHead = m_BufferBase = AllocAlignedBuffer(m_BufferSize); ReadFromExternal(m_BufferBase, RDCMIN(m_InputSize, m_BufferSize)); m_Ownership = Ownership::Stream; } StreamReader::StreamReader(StreamReader *reader, uint64_t bufferSize) { m_InputSize = m_BufferSize = bufferSize; m_BufferHead = m_BufferBase = AllocAlignedBuffer(m_BufferSize); reader->Read(m_BufferBase, bufferSize); m_Ownership = Ownership::Nothing; } StreamReader::StreamReader(Decompressor *decompressor, uint64_t uncompressedSize, Ownership own) { m_Decompressor = decompressor; m_InputSize = uncompressedSize; m_BufferSize = initialBufferSize; m_BufferHead = m_BufferBase = AllocAlignedBuffer(m_BufferSize); m_Ownership = own; ReadFromExternal(m_BufferBase, RDCMIN(uncompressedSize, m_BufferSize)); } StreamReader::~StreamReader() { for(StreamCloseCallback cb : m_Callbacks) cb(); FreeAlignedBuffer(m_BufferBase); if(m_Ownership == Ownership::Stream) { if(m_File) FileIO::fclose(m_File); if(m_Decompressor) delete m_Decompressor; } } void StreamReader::SetOffset(uint64_t offs) { if(m_File || m_Decompressor) { RDCERR("File and decompress stream readers do not support seeking"); return; } m_BufferHead = m_BufferBase + offs; } bool StreamReader::Reserve(uint64_t numBytes) { RDCASSERT(m_Sock || m_File || m_Decompressor); // store old buffer and the read data, so we can move it into the new buffer byte *oldBuffer = m_BufferBase; // always keep at least a certain window behind what we read. uint64_t backwardsWindow = RDCMIN(64ULL, m_BufferHead - m_BufferBase); byte *currentData = m_BufferHead - backwardsWindow; uint64_t currentDataSize = m_BufferSize - (m_BufferHead - m_BufferBase) + backwardsWindow; if(m_Sock) currentDataSize = m_InputSize - (m_BufferHead - m_BufferBase) + backwardsWindow; uint64_t BufferOffset = m_BufferHead - m_BufferBase; // if we are reading more than our current buffer size, expand the buffer size if(numBytes + backwardsWindow > m_BufferSize) { // very conservative resizing - don't do "double and add" - to avoid // a 1GB buffer being read and needing to allocate 2GB. The cost is we // will reallocate a bit more often m_BufferSize = numBytes + backwardsWindow; m_BufferBase = AllocAlignedBuffer(m_BufferSize); } // move the unread data into the buffer memmove(m_BufferBase, currentData, (size_t)currentDataSize); if(BufferOffset > backwardsWindow) { m_ReadOffset += BufferOffset - backwardsWindow; m_BufferHead = m_BufferBase + backwardsWindow; } else { m_BufferHead = m_BufferBase + BufferOffset; } if(m_Sock) m_InputSize = currentDataSize; // if there's anything left of the file to read in, do so now bool ret = false; uint64_t readSize = RDCMIN(m_BufferSize - currentDataSize, m_InputSize - m_ReadOffset - currentDataSize); // we'll read as much as possible anyway using m_BufferSize, but we need to know how much we // *must* read. if(m_Sock) readSize = numBytes - Available(); ret = ReadFromExternal(m_BufferBase + currentDataSize, readSize); if(oldBuffer != m_BufferBase && m_BufferBase) FreeAlignedBuffer(oldBuffer); return ret; } bool StreamReader::ReadLargeBuffer(void *buffer, uint64_t length) { RDCASSERT(m_File || m_Decompressor); byte *dest = (byte *)buffer; // first exhaust whatever we have in the current buffer. { const uint64_t avail = Available(); // if we don't have 128 bytes left over we shouldn't be in here RDCASSERT(avail + 128 <= length, avail, length); // don't actually read if the destination buffer is NULL if(dest) { memcpy(dest, m_BufferHead, (size_t)avail); dest += avail; } length -= avail; m_ReadOffset += m_BufferSize; } // now read everything but the last 128 bytes directly from the external source if(length > 128) { uint64_t directReadLength = length - 128; length -= directReadLength; m_ReadOffset += directReadLength; if(buffer) { bool ret = ReadFromExternal(dest, directReadLength); dest += directReadLength; // if we failed, return now if(!ret) return ret; } else { // if we have no buffer to read into, just seek the stream in buffer-sized chunks using the // existing buffer. Ensure the buffer is big enough to do this at a reasonable rate. if(m_BufferSize < 1024 * 1024) { m_BufferSize = 1024 * 1024; FreeAlignedBuffer(m_BufferBase); m_BufferBase = AllocAlignedBuffer(m_BufferSize); } while(directReadLength > 0) { uint64_t chunkRead = RDCMIN(m_BufferSize, directReadLength); bool ret = ReadFromExternal(m_BufferBase, chunkRead); // if we failed, return now if(!ret) return ret; directReadLength -= chunkRead; } } } // we now have exactly 128 bytes to read, guaranteed by how the function is called. // we read that into the end of our buffer deliberately so that we can leave the buffer in the // right state to have a backwards window (though it shouldn't be needed for large serialises like // this). if(m_BufferSize < 128) { m_BufferSize = 128; FreeAlignedBuffer(m_BufferBase); m_BufferBase = AllocAlignedBuffer(m_BufferSize); } // set the head to *after* where we're reading, this is where it'll end up after the read. m_BufferHead = m_BufferBase + m_BufferSize; // read the 128 bytes m_ReadOffset += 128; bool ret = ReadFromExternal(m_BufferHead - 128, 128); // memcpy it where it's needed if(dest && ret) memcpy(dest, m_BufferHead - 128, 128); // adjust read offset back for the 'fake' buffer we leave behind m_ReadOffset -= m_BufferSize; return ret; } bool StreamReader::ReadFromExternal(void *buffer, uint64_t length) { bool success = true; if(m_Decompressor) { success = m_Decompressor->Read(buffer, length); if(!success) m_Error = m_Decompressor->GetError(); } else if(m_File) { uint64_t numRead = FileIO::fread(buffer, 1, (size_t)length, m_File); success = (numRead == length); if(!success) { if(FileIO::feof(m_File)) SET_ERROR_RESULT(m_Error, ResultCode::FileIOFailed, "Error reading from file: hit end of file unexpectedly. Out of disk space " "or truncated file?"); else SET_ERROR_RESULT(m_Error, ResultCode::FileIOFailed, "Error reading from file: %s", FileIO::ErrorString().c_str()); } } else if(m_Sock) { if(!m_Sock->Connected()) { success = false; m_Error = m_Sock->GetError(); if(m_Error == ResultCode::Succeeded) SET_ERROR_RESULT(m_Error, ResultCode::NetworkIOFailed, "Socket unexpectedly disconnected during reading"); } else { // first get the required data blocking (this will sleep the thread until it comes in). byte *readDest = (byte *)buffer; // we expect to be reading into our window buffer RDCASSERT(readDest >= m_BufferBase && readDest <= m_BufferBase + m_BufferSize); success = m_Sock->RecvDataBlocking(readDest, (uint32_t)length); if(success) { m_InputSize += length; readDest += length; uint32_t bufSize = uint32_t(m_BufferSize - m_InputSize); if(m_InputSize > m_BufferSize) { bufSize = 0; RDCERR("Invalid read in ReadFromExternal!"); } // now read more, as much as possible, to try and batch future reads success = m_Sock->RecvDataNonBlocking(readDest, bufSize); if(success) m_InputSize += bufSize; } if(!success) { m_Error = m_Sock->GetError(); if(m_Error == ResultCode::Succeeded) SET_WARNING_RESULT(m_Error, ResultCode::NetworkIOFailed, "Socket unexpectedly disconnected during reading"); } } } else { // we're in an error-state, nothing to read from return false; } if(!success) { // move to error state FreeAlignedBuffer(m_BufferBase); if(m_Ownership == Ownership::Stream) { if(m_File) FileIO::fclose(m_File); if(m_Sock) delete m_Sock; if(m_Decompressor) delete m_Decompressor; } m_File = NULL; m_Sock = NULL; m_Decompressor = NULL; m_ReadOffset = 0; m_InputSize = 0; m_BufferSize = 0; m_BufferHead = m_BufferBase = NULL; m_Ownership = Ownership::Nothing; } return success; } FileWriter *FileWriter::MakeDefault(FILE *file, Ownership own) { if(file == NULL) return NULL; FileWriter *ret = new FileWriter(file, own); ret->m_ThreadRunning = 0; return ret; } FileWriter *FileWriter::MakeThreaded(FILE *file, Ownership own) { if(file == NULL) return NULL; FileWriter *ret = new FileWriter(file, own); for(size_t i = 0; i < NumBlocks; i++) ret->m_AllocBlocks[i] = {AllocAlignedBuffer(BlockSize), 0}; ret->m_ProducerOwned.append(ret->m_AllocBlocks, NumBlocks); ret->m_ThreadRunning = 1; ret->m_Thread = Threading::CreateThread([ret]() { ret->ThreadEntry(); }); return ret; } RDResult FileWriter::Write(const void *data, uint64_t length) { if(m_ThreadRunning == 0) return WriteUnthreaded(data, length); return WriteThreaded(data, length); } RDResult FileWriter::WriteUnthreaded(const void *data, uint64_t length) { // this may be called directly in Write, or deferred on the thread. It is unsynchronised and // internal RDResult result; uint64_t written = (uint64_t)FileIO::fwrite(data, 1, (size_t)length, m_File); if(written != length) { SET_ERROR_RESULT(result, ResultCode::FileIOFailed, "Writing to file failed: %s", FileIO::ErrorString().c_str()); } return result; } RDResult FileWriter::WriteThreaded(const void *data, uint64_t length) { // if write fits in this block, memcpy and return. We allow this to completely fill a block, it // will get flushed on the next write or Flush() call if(!m_ProducerOwned.empty() && length <= BlockSize - m_ProducerOwned.back().second) { memcpy(m_ProducerOwned.back().first + m_ProducerOwned.back().second, data, (size_t)length); m_ProducerOwned.back().second += length; return ResultCode::Succeeded; } RDResult ret; // write doesn't fit in the block (or we don't have one free) // loop until all bytes are written const byte *dataPtr = (byte *)data; while(length > 0) { // blocks to submit, we'll have at least one rdcarray pending; // while we have free blocks that we own, and still bytes to write while(length > 0 && !m_ProducerOwned.empty()) { Block &curBlock = m_ProducerOwned.back(); // write either the rest of what will fit in the block, or the rest of the data, whatever is // smaller uint64_t writeSize = RDCMIN(length, BlockSize - curBlock.second); memcpy(curBlock.first + curBlock.second, dataPtr, (size_t)writeSize); curBlock.second += writeSize; dataPtr += writeSize; length -= writeSize; // should not be possible with writeSize above being clamped if(curBlock.second > BlockSize) { RDCERR("Block has been overrun"); // truncate writes to be safe curBlock.second = BlockSize; return ResultCode::InternalError; } // if the block is completely full, push it to the consumer if(curBlock.second == BlockSize) { m_ProducerOwned.pop_back(); pending.push_back(curBlock); } } // we got here, either we ran out of blocks to write to (or more likely) we finished writing. // now we push the pending list to the consumer and at the same time grab any blocks that have // freed up. Hold the lock while modifying those block-passing lists m_Lock.Lock(); if(!pending.empty()) { m_PendingForConsumer.append(pending); pending.clear(); } if(!m_CompletedFromConsumer.empty()) { m_ProducerOwned.insert(0, m_CompletedFromConsumer); m_CompletedFromConsumer.clear(); } if(ret == ResultCode::Succeeded) ret = m_Error; m_Lock.Unlock(); // if we still have bytes to write and are waiting for blocks to free up, sleep here so we // don't busy loop trying to get more blocks if(length > 0) Threading::Sleep(5); } return ret; } void FileWriter::ThreadEntry() { rdcarray completed; RDResult error; int busyLoopCounter = 0; // loop as long as the thread is not being killed while(Atomic::CmpExch32(&m_ThreadKill, 0, 0) == 0) { Block work = {}; // hold the lock, take any new work and return any completed work m_Lock.Lock(); m_ConsumerOwned.append(m_PendingForConsumer); m_CompletedFromConsumer.append(completed); m_PendingForConsumer.clear(); completed.clear(); // don't overwrite an old error, but update if there's a new error if(m_Error == ResultCode::Succeeded && error != ResultCode::Succeeded) m_Error = error; m_Lock.Unlock(); // grab work to do if we can if(!m_ConsumerOwned.empty()) { work = m_ConsumerOwned[0]; m_ConsumerOwned.erase(0); busyLoopCounter = 0; } if(work.second) { RDResult res = WriteUnthreaded(work.first, work.second); if(error == ResultCode::Succeeded) error = res; // reset the offset/size to 0 when returning it completed.push_back({work.first, 0}); } // after a certain number of loops without any work start to do small sleeps to break up the // busy loop if(busyLoopCounter++ > 500) Threading::Sleep(1); } Atomic::CmpExch32(&m_ThreadRunning, 1, 0); } RDResult FileWriter::Flush() { // if we have some writes, push these now even with a partial block if(!m_ProducerOwned.empty() && m_ProducerOwned.back().second > 0) { Block b = m_ProducerOwned.back(); m_ProducerOwned.pop_back(); // hold the lock so we can push this incomplete block through m_Lock.Lock(); m_PendingForConsumer.push_back(b); m_Lock.Unlock(); // all other blocks should be empty for(Block &owned : m_ProducerOwned) RDCASSERTEQUAL(owned.second, 0); } // loop as long as the thread is alive. Flushing is rare so we don't mind sleeping here // if we're unthreaded this loop just won't execute while(Atomic::CmpExch32(&m_ThreadRunning, 1, 1) > 0) { m_Lock.Lock(); // take ownership of any blocks due to us m_ProducerOwned.insert(0, m_CompletedFromConsumer); m_CompletedFromConsumer.clear(); m_Lock.Unlock(); // if we own all the blocks again, we're done if(m_ProducerOwned.size() == NumBlocks) break; Threading::Sleep(1); } RDResult ret; // flush the underlying file bool success = FileIO::fflush(m_File); m_Lock.Lock(); if(!success && m_Error == ResultCode::Succeeded) { SET_ERROR_RESULT(m_Error, ResultCode::FileIOFailed, "File flushing failed: %s", FileIO::ErrorString().c_str()); } ret = m_Error; m_Lock.Unlock(); return ret; } FileWriter::~FileWriter() { if(m_Thread) { // ensure we've written everything Flush(); // ask the thread to stop Atomic::Inc32(&m_ThreadKill); Threading::JoinThread(m_Thread); Threading::CloseThread(m_Thread); m_Thread = 0; for(size_t i = 0; i < NumBlocks; i++) FreeAlignedBuffer(m_AllocBlocks[i].first); } if(m_Ownership == Ownership::Stream) FileIO::fclose(m_File); } StreamWriter::StreamWriter(uint64_t initialBufSize) { m_BufferBase = m_BufferHead = AllocAlignedBuffer(initialBufSize); m_BufferEnd = m_BufferBase + initialBufSize; m_Ownership = Ownership::Nothing; } StreamWriter::StreamWriter(StreamInvalidType, RDResult res) { m_BufferBase = m_BufferHead = m_BufferEnd = NULL; m_Ownership = Ownership::Nothing; m_InMemory = false; m_Error = res; if(m_Error == ResultCode::Succeeded) SET_ERROR_RESULT(m_Error, ResultCode::InvalidParameter, "Invalid stream created with no error code"); } StreamWriter::StreamWriter(Network::Socket *sock, Ownership own) { if(sock == NULL) { SET_ERROR_RESULT(m_Error, ResultCode::InvalidParameter, "Stream created with invalid socket"); m_BufferHead = m_BufferBase = m_BufferEnd = NULL; m_Ownership = Ownership::Nothing; m_InMemory = false; return; } m_BufferBase = m_BufferHead = AllocAlignedBuffer(initialBufferSize); m_BufferEnd = m_BufferBase + initialBufferSize; m_Sock = sock; m_Ownership = own; m_InMemory = false; } StreamWriter::StreamWriter(FileWriter *file, Ownership own) { if(file == NULL) { SET_ERROR_RESULT(m_Error, ResultCode::InvalidParameter, "Stream created with invalid file handle"); m_BufferHead = m_BufferBase = m_BufferEnd = NULL; m_Ownership = Ownership::Nothing; m_InMemory = false; return; } m_BufferBase = m_BufferHead = m_BufferEnd = NULL; m_File = file; m_Ownership = own; m_InMemory = false; } StreamWriter::StreamWriter(Compressor *compressor, Ownership own) { m_BufferBase = m_BufferHead = m_BufferEnd = NULL; m_Compressor = compressor; m_Ownership = own; m_InMemory = false; } StreamWriter::~StreamWriter() { if(m_Ownership == Ownership::Stream) { if(m_File) delete m_File; if(m_Compressor) delete m_Compressor; } else { if(m_File) m_File->Flush(); } for(StreamCloseCallback cb : m_Callbacks) cb(); FreeAlignedBuffer(m_BufferBase); } bool StreamWriter::SendSocketData(const void *data, uint64_t numBytes) { // try to coalesce small writes without doing blocking sends, at least until we're flushed. // if the buffer is already full, flush it. if(m_BufferHead + numBytes >= m_BufferEnd) { bool success = FlushSocketData(); if(!success) return false; } // if it's larger than our buffer (even after flushing) just write directly if(m_BufferHead + numBytes >= m_BufferEnd) { bool success = m_Sock->SendDataBlocking(data, (uint32_t)numBytes); if(!success) { RDResult res = m_Sock->GetError(); if(res == ResultCode::Succeeded) SET_ERROR_RESULT(res, ResultCode::NetworkIOFailed, "Socket unexpectedly disconnected during sending"); HandleError(res); return false; } } else { // otherwise, write it into the in-memory buffer memcpy(m_BufferHead, data, (size_t)numBytes); m_BufferHead += numBytes; } return true; } bool StreamWriter::FlushSocketData() { // send out what we have buffered up bool success = m_Sock->SendDataBlocking(m_BufferBase, uint32_t(m_BufferHead - m_BufferBase)); if(!success) { RDResult res = m_Sock->GetError(); if(res == ResultCode::Succeeded) SET_ERROR_RESULT(res, ResultCode::NetworkIOFailed, "Socket unexpectedly disconnected during sending"); HandleError(res); return false; } // reset buffer to the start m_BufferHead = m_BufferBase; return true; } void StreamWriter::HandleError(RDResult result) { if(m_Error == ResultCode::Succeeded) m_Error = result; FreeAlignedBuffer(m_BufferBase); if(m_Ownership == Ownership::Stream) { if(m_File) delete m_File; if(m_Sock) delete m_Sock; if(m_Compressor) delete m_Compressor; } m_BufferBase = m_BufferHead = m_BufferEnd = NULL; m_WriteSize = 0; m_File = NULL; m_Sock = NULL; m_Compressor = NULL; m_Ownership = Ownership::Nothing; m_InMemory = false; } void StreamTransfer(StreamWriter *writer, StreamReader *reader, RENDERDOC_ProgressCallback progress) { uint64_t totalSize = reader->GetSize(); if(totalSize == 0) { if(progress) progress(1.0f); return; } // copy 1MB at a time const uint64_t StreamIOChunkSize = 1024 * 1024; const uint64_t bufSize = RDCMIN(StreamIOChunkSize, totalSize); uint64_t numBufs = totalSize / bufSize; // last remaining partial buffer if(totalSize % (uint64_t)bufSize > 0) numBufs++; byte *buf = new byte[(size_t)bufSize]; if(progress) progress(0.0001f); for(uint64_t i = 0; i < numBufs; i++) { uint64_t payloadLength = RDCMIN(bufSize, totalSize); reader->Read(buf, payloadLength); writer->Write(buf, payloadLength); totalSize -= payloadLength; if(progress) progress(float(i + 1) / float(numBufs)); } if(progress) progress(1.0f); delete[] buf; }