mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-07-23 16:01:38 +00:00
964 lines
25 KiB
C++
964 lines
25 KiB
C++
/******************************************************************************
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2019-2025 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 <errno.h>
|
|
#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<uint64_t>(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<Block> 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<Block> 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;
|
|
}
|