doom3-bfg/neo/framework/File_SaveGame.cpp

1151 lines
30 KiB
C++
Raw Permalink Normal View History

2012-11-26 18:58:24 +00:00
/*
===========================================================================
Doom 3 BFG Edition GPL Source Code
Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company.
This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code").
Doom 3 BFG Edition Source Code is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Doom 3 BFG Edition Source Code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Doom 3 BFG Edition Source Code. If not, see <http://www.gnu.org/licenses/>.
In addition, the Doom 3 BFG Edition Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 BFG Edition Source Code. If not, please request a copy in writing from id Software at the address below.
If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
===========================================================================
*/
#pragma hdrstop
#include "../idlib/precompiled.h"
#include "File_SaveGame.h"
/*
TODO: CRC on each block
*/
/*
========================
ZlibAlloc
========================
*/
void * ZlibAlloc( void *opaque, uInt items, uInt size ) {
return Mem_Alloc( items * size, TAG_SAVEGAMES );
}
/*
========================
ZlibFree
========================
*/
void ZlibFree( void *opaque, void * address ) {
Mem_Free( address );
}
idCVar sgf_threads( "sgf_threads", "2", CVAR_INTEGER, "0 = all foreground, 1 = background write, 2 = background write + compress" );
idCVar sgf_checksums( "sgf_checksums", "1", CVAR_BOOL, "enable save game file checksums" );
idCVar sgf_testCorruption( "sgf_testCorruption", "-1", CVAR_INTEGER, "test corruption at the 128 kB compressed block" );
// this is supposed to get faster going from -15 to -9, but it gets slower as well as worse compression
idCVar sgf_windowBits( "sgf_windowBits", "-15", CVAR_INTEGER, "zlib window bits" );
bool idFile_SaveGamePipelined::cancelToTerminate = false;
class idSGFcompressThread : public idSysThread {
public:
virtual int Run() { sgf->CompressBlock(); return 0; }
idFile_SaveGamePipelined * sgf;
};
class idSGFdecompressThread : public idSysThread {
public:
virtual int Run() { sgf->DecompressBlock(); return 0; }
idFile_SaveGamePipelined * sgf;
};
class idSGFwriteThread : public idSysThread {
public:
virtual int Run() { sgf->WriteBlock(); return 0; }
idFile_SaveGamePipelined * sgf;
};
class idSGFreadThread : public idSysThread {
public:
virtual int Run() { sgf->ReadBlock(); return 0; }
idFile_SaveGamePipelined * sgf;
};
/*
============================
idFile_SaveGamePipelined::idFile_SaveGamePipelined
============================
*/
idFile_SaveGamePipelined::idFile_SaveGamePipelined() :
mode( CLOSED ),
compressedLength( 0 ),
uncompressedProducedBytes( 0 ),
uncompressedConsumedBytes( 0 ),
compressedProducedBytes( 0 ),
compressedConsumedBytes( 0 ),
dataZlib( NULL ),
bytesZlib( 0 ),
dataIO( NULL ),
bytesIO( 0 ),
zLibFlushType( Z_NO_FLUSH ),
zStreamEndHit( false ),
numChecksums( 0 ),
nativeFile( NULL ),
nativeFileEndHit( false ),
finished( false ),
readThread( NULL ),
writeThread( NULL ),
decompressThread( NULL ),
compressThread( NULL ),
blockFinished( true ),
buildVersion( "" ),
saveFormatVersion( 0 ) {
memset( &zStream, 0, sizeof( zStream ) );
memset( compressed, 0, sizeof( compressed ) );
memset( uncompressed, 0, sizeof( uncompressed ) );
zStream.zalloc = ZlibAlloc;
zStream.zfree = ZlibFree;
}
/*
============================
idFile_SaveGamePipelined::~idFile_SaveGamePipelined
============================
*/
idFile_SaveGamePipelined::~idFile_SaveGamePipelined() {
Finish();
// free the threads
if ( compressThread != NULL ) {
delete compressThread;
compressThread = NULL;
}
if ( decompressThread != NULL ) {
delete decompressThread;
decompressThread = NULL;
}
if ( readThread != NULL ) {
delete readThread;
readThread = NULL;
}
if ( writeThread != NULL ) {
delete writeThread;
writeThread = NULL;
}
// close the native file
/* if ( nativeFile != NULL ) {
delete nativeFile;
nativeFile = NULL;
} */
dataZlib = NULL;
dataIO = NULL;
}
/*
========================
idFile_SaveGamePipelined::ReadBuildVersion
========================
*/
bool idFile_SaveGamePipelined::ReadBuildVersion() {
return ReadString( buildVersion ) != 0;
}
/*
========================
idFile_SaveGamePipelined::ReadSaveFormatVersion
========================
*/
bool idFile_SaveGamePipelined::ReadSaveFormatVersion() {
if ( ReadBig( pointerSize ) <= 0 ) {
return false;
}
return ReadBig( saveFormatVersion ) != 0;
}
/*
========================
idFile_SaveGamePipelined::GetPointerSize
========================
*/
int idFile_SaveGamePipelined::GetPointerSize() const {
if ( pointerSize == 0 ) {
// in original savegames we weren't saving the pointer size, so the 2 high bytes of the save version will be 0
return 4;
} else {
return pointerSize;
}
}
/*
============================
idFile_SaveGamePipelined::Finish
============================
*/
void idFile_SaveGamePipelined::Finish() {
if ( mode == WRITE ) {
// wait for the compression thread to complete, which may kick off a write
if ( compressThread != NULL ) {
compressThread->WaitForThread();
}
// force the next compression to emit everything
zLibFlushType = Z_FINISH;
FlushUncompressedBlock();
if ( compressThread != NULL ) {
compressThread->WaitForThread();
}
if ( writeThread != NULL ) {
// wait for the IO thread to exit
writeThread->WaitForThread();
} else if ( nativeFile == NULL && !nativeFileEndHit ) {
// wait for the last block to be consumed
blockRequested.Wait();
finished = true;
blockAvailable.Raise();
blockFinished.Wait();
}
// free zlib tables
deflateEnd( &zStream );
} else if ( mode == READ ) {
// wait for the decompression thread to complete, which may kick off a read
if ( decompressThread != NULL ) {
decompressThread->WaitForThread();
}
if ( readThread != NULL ) {
// wait for the IO thread to exit
readThread->WaitForThread();
} else if ( nativeFile == NULL && !nativeFileEndHit ) {
// wait for the last block to be consumed
blockAvailable.Wait();
finished = true;
blockRequested.Raise();
blockFinished.Wait();
}
// free zlib tables
inflateEnd( &zStream );
}
mode = CLOSED;
}
/*
============================
idFile_SaveGamePipelined::Abort
============================
*/
void idFile_SaveGamePipelined::Abort() {
if ( mode == WRITE ) {
if ( compressThread != NULL ) {
compressThread->WaitForThread();
}
if ( writeThread != NULL ) {
writeThread->WaitForThread();
} else if ( nativeFile == NULL && !nativeFileEndHit ) {
blockRequested.Wait();
finished = true;
dataIO = NULL;
bytesIO = 0;
blockAvailable.Raise();
blockFinished.Wait();
}
} else if ( mode == READ ) {
if ( decompressThread != NULL ) {
decompressThread->WaitForThread();
}
if ( readThread != NULL ) {
readThread->WaitForThread();
} else if ( nativeFile == NULL && !nativeFileEndHit ) {
blockAvailable.Wait();
finished = true;
dataIO = NULL;
bytesIO = 0;
blockRequested.Raise();
blockFinished.Wait();
}
}
mode = CLOSED;
}
/*
===================================================================================
WRITE PATH
===================================================================================
*/
/*
============================
idFile_SaveGamePipelined::OpenForWriting
============================
*/
bool idFile_SaveGamePipelined::OpenForWriting( const char * const filename, bool useNativeFile ) {
assert( mode == CLOSED );
name = filename;
osPath = filename;
mode = WRITE;
nativeFile = NULL;
numChecksums = 0;
if ( useNativeFile ) {
nativeFile = fileSystem->OpenFileWrite( filename );
if ( nativeFile == NULL ) {
return false;
}
}
// raw deflate with no header / checksum
// use max memory for fastest compression
// optimize for higher speed
//mem.PushHeap();
int status = deflateInit2( &zStream, Z_BEST_SPEED, Z_DEFLATED, sgf_windowBits.GetInteger(), 9, Z_DEFAULT_STRATEGY );
//mem.PopHeap();
if ( status != Z_OK ) {
idLib::FatalError( "idFile_SaveGamePipelined::OpenForWriting: deflateInit2() error %i", status );
}
// initial buffer setup
zStream.avail_out = COMPRESSED_BLOCK_SIZE;
zStream.next_out = (Bytef * )compressed;
if ( sgf_checksums.GetBool() ) {
zStream.avail_out -= sizeof( uint32 );
}
if ( sgf_threads.GetInteger() >= 1 ) {
compressThread = new (TAG_IDFILE) idSGFcompressThread();
compressThread->sgf = this;
compressThread->StartWorkerThread( "SGF_CompressThread", CORE_2B, THREAD_NORMAL );
}
if ( nativeFile != NULL && sgf_threads.GetInteger() >= 2 ) {
writeThread = new (TAG_IDFILE) idSGFwriteThread();
writeThread->sgf = this;
writeThread->StartWorkerThread( "SGF_WriteThread", CORE_2A, THREAD_NORMAL );
}
return true;
}
/*
============================
idFile_SaveGamePipelined::OpenForWriting
============================
*/
bool idFile_SaveGamePipelined::OpenForWriting( idFile * file ) {
assert( mode == CLOSED );
if ( file == NULL ) {
return false;
}
name = file->GetName();
osPath = file->GetFullPath();
mode = WRITE;
nativeFile = file;
numChecksums = 0;
// raw deflate with no header / checksum
// use max memory for fastest compression
// optimize for higher speed
//mem.PushHeap();
int status = deflateInit2( &zStream, Z_BEST_SPEED, Z_DEFLATED, sgf_windowBits.GetInteger(), 9, Z_DEFAULT_STRATEGY );
//mem.PopHeap();
if ( status != Z_OK ) {
idLib::FatalError( "idFile_SaveGamePipelined::OpenForWriting: deflateInit2() error %i", status );
}
// initial buffer setup
zStream.avail_out = COMPRESSED_BLOCK_SIZE;
zStream.next_out = (Bytef * )compressed;
if ( sgf_checksums.GetBool() ) {
zStream.avail_out -= sizeof( uint32 );
}
if ( sgf_threads.GetInteger() >= 1 ) {
compressThread = new (TAG_IDFILE) idSGFcompressThread();
compressThread->sgf = this;
compressThread->StartWorkerThread( "SGF_CompressThread", CORE_2B, THREAD_NORMAL );
}
if ( nativeFile != NULL && sgf_threads.GetInteger() >= 2 ) {
writeThread = new (TAG_IDFILE) idSGFwriteThread();
writeThread->sgf = this;
writeThread->StartWorkerThread( "SGF_WriteThread", CORE_2A, THREAD_NORMAL );
}
return true;
}
/*
============================
idFile_SaveGamePipelined::NextWriteBlock
Modifies:
dataIO
bytesIO
============================
*/
bool idFile_SaveGamePipelined::NextWriteBlock( blockForIO_t * block ) {
assert( mode == WRITE );
blockRequested.Raise(); // the background thread is done with the last block
if ( nativeFileEndHit ) {
return false;
}
blockAvailable.Wait(); // wait for a new block to come through the pipeline
if ( finished || block == NULL ) {
nativeFileEndHit = true;
blockRequested.Raise();
blockFinished.Raise();
return false;
}
compressedLength += bytesIO;
block->data = dataIO;
block->bytes = bytesIO;
dataIO = NULL;
bytesIO = 0;
return true;
}
/*
============================
idFile_SaveGamePipelined::WriteBlock
Modifies:
dataIO
bytesIO
nativeFile
============================
*/
void idFile_SaveGamePipelined::WriteBlock() {
assert( nativeFile != NULL );
compressedLength += bytesIO;
nativeFile->Write( dataIO, bytesIO );
dataIO = NULL;
bytesIO = 0;
}
/*
============================
idFile_SaveGamePipelined::FlushCompressedBlock
Called when a compressed block fills up, and also to flush the final partial block.
Flushes everything from [compressedConsumedBytes -> compressedProducedBytes)
Reads:
compressed
compressedProducedBytes
Modifies:
dataZlib
bytesZlib
compressedConsumedBytes
============================
*/
void idFile_SaveGamePipelined::FlushCompressedBlock() {
// block until the background thread is done with the last block
if ( writeThread != NULL ) {
writeThread->WaitForThread();
} if ( nativeFile == NULL ) {
if ( !nativeFileEndHit ) {
blockRequested.Wait();
}
}
// prepare the next block to be written out
dataIO = &compressed[ compressedConsumedBytes & ( COMPRESSED_BUFFER_SIZE - 1 ) ];
bytesIO = compressedProducedBytes - compressedConsumedBytes;
compressedConsumedBytes = compressedProducedBytes;
if ( writeThread != NULL ) {
// signal a new block is available to be written out
writeThread->SignalWork();
} else if ( nativeFile != NULL ) {
// write syncronously
WriteBlock();
} else {
// signal a new block is available to be written out
blockAvailable.Raise();
}
}
/*
============================
idFile_SaveGamePipelined::CompressBlock
Called when an uncompressed block fills up, and also to flush the final partial block.
Flushes everything from [uncompressedConsumedBytes -> uncompressedProducedBytes)
Modifies:
dataZlib
bytesZlib
compressed
compressedProducedBytes
zStream
zStreamEndHit
============================
*/
void idFile_SaveGamePipelined::CompressBlock() {
zStream.next_in = (Bytef * )dataZlib;
zStream.avail_in = (uInt) bytesZlib;
dataZlib = NULL;
bytesZlib = 0;
// if this is the finish block, we may need to write
// multiple buffers even after all input has been consumed
while( zStream.avail_in > 0 || zLibFlushType == Z_FINISH ) {
const int zstat = deflate( &zStream, zLibFlushType );
if ( zstat != Z_OK && zstat != Z_STREAM_END ) {
idLib::FatalError( "idFile_SaveGamePipelined::CompressBlock: deflate() returned %i", zstat );
}
if ( zStream.avail_out == 0 || zLibFlushType == Z_FINISH ) {
if ( sgf_checksums.GetBool() ) {
size_t blockSize = zStream.total_out + numChecksums * sizeof( uint32 ) - compressedProducedBytes;
uint32 checksum = MD5_BlockChecksum( zStream.next_out - blockSize, blockSize );
zStream.next_out[0] = ( ( checksum >> 0 ) & 0xFF );
zStream.next_out[1] = ( ( checksum >> 8 ) & 0xFF );
zStream.next_out[2] = ( ( checksum >> 16 ) & 0xFF );
zStream.next_out[3] = ( ( checksum >> 24 ) & 0xFF );
numChecksums++;
}
// flush the output buffer IO
compressedProducedBytes = zStream.total_out + numChecksums * sizeof( uint32 );
FlushCompressedBlock();
if ( zstat == Z_STREAM_END ) {
assert( zLibFlushType == Z_FINISH );
zStreamEndHit = true;
return;
}
assert( 0 == ( compressedProducedBytes & ( COMPRESSED_BLOCK_SIZE - 1 ) ) );
zStream.avail_out = COMPRESSED_BLOCK_SIZE;
zStream.next_out = (Bytef * )&compressed[ compressedProducedBytes & ( COMPRESSED_BUFFER_SIZE - 1 ) ];
if ( sgf_checksums.GetBool() ) {
zStream.avail_out -= sizeof( uint32 );
}
}
}
}
/*
============================
idFile_SaveGamePipelined::FlushUncompressedBlock
Called when an uncompressed block fills up, and also to flush the final partial block.
Flushes everything from [uncompressedConsumedBytes -> uncompressedProducedBytes)
Reads:
uncompressed
uncompressedProducedBytes
Modifies:
dataZlib
bytesZlib
uncompressedConsumedBytes
============================
*/
void idFile_SaveGamePipelined::FlushUncompressedBlock() {
// block until the background thread has completed
if ( compressThread != NULL ) {
// make sure thread has completed the last work
compressThread->WaitForThread();
}
// prepare the next block to be consumed by Zlib
dataZlib = &uncompressed[ uncompressedConsumedBytes & ( UNCOMPRESSED_BUFFER_SIZE - 1 ) ];
bytesZlib = uncompressedProducedBytes - uncompressedConsumedBytes;
uncompressedConsumedBytes = uncompressedProducedBytes;
if ( compressThread != NULL ) {
// signal thread for more work
compressThread->SignalWork();
} else {
// run syncronously
CompressBlock();
}
}
/*
============================
idFile_SaveGamePipelined::Write
Modifies:
uncompressed
uncompressedProducedBytes
============================
*/
int idFile_SaveGamePipelined::Write( const void * buffer, int length ) {
if ( buffer == NULL || length <= 0 ) {
return 0;
}
#if 1 // quick and dirty fix for user-initiated forced shutdown during a savegame
if ( cancelToTerminate ) {
if ( mode != CLOSED ) {
Abort();
}
return 0;
}
#endif
assert( mode == WRITE );
size_t lengthRemaining = length;
const byte * buffer_p = (const byte *)buffer;
while ( lengthRemaining > 0 ) {
const size_t ofsInBuffer = uncompressedProducedBytes & ( UNCOMPRESSED_BUFFER_SIZE - 1 );
const size_t ofsInBlock = uncompressedProducedBytes & ( UNCOMPRESSED_BLOCK_SIZE - 1 );
const size_t remainingInBlock = UNCOMPRESSED_BLOCK_SIZE - ofsInBlock;
const size_t copyToBlock = ( lengthRemaining < remainingInBlock ) ? lengthRemaining : remainingInBlock;
memcpy( uncompressed + ofsInBuffer, buffer_p, copyToBlock );
uncompressedProducedBytes += copyToBlock;
buffer_p += copyToBlock;
lengthRemaining -= copyToBlock;
if ( copyToBlock == remainingInBlock ) {
FlushUncompressedBlock();
}
}
return length;
}
/*
===================================================================================
READ PATH
===================================================================================
*/
/*
============================
idFile_SaveGamePipelined::OpenForReading
============================
*/
bool idFile_SaveGamePipelined::OpenForReading( const char * const filename, bool useNativeFile ) {
assert( mode == CLOSED );
name = filename;
osPath = filename;
mode = READ;
nativeFile = NULL;
numChecksums = 0;
if ( useNativeFile ) {
nativeFile = fileSystem->OpenFileRead( filename );
if ( nativeFile == NULL ) {
return false;
}
}
// init zlib for raw inflate with a 32k dictionary
//mem.PushHeap();
int status = inflateInit2( &zStream, sgf_windowBits.GetInteger() );
//mem.PopHeap();
if ( status != Z_OK ) {
idLib::FatalError( "idFile_SaveGamePipelined::OpenForReading: inflateInit2() error %i", status );
}
// spawn threads
if ( sgf_threads.GetInteger() >= 1 ) {
decompressThread = new (TAG_IDFILE) idSGFdecompressThread();
decompressThread->sgf = this;
decompressThread->StartWorkerThread( "SGF_DecompressThread", CORE_2B, THREAD_NORMAL );
}
if ( nativeFile != NULL && sgf_threads.GetInteger() >= 2 ) {
readThread = new (TAG_IDFILE) idSGFreadThread();
readThread->sgf = this;
readThread->StartWorkerThread( "SGF_ReadThread", CORE_2A, THREAD_NORMAL );
}
return true;
}
/*
============================
idFile_SaveGamePipelined::OpenForReading
============================
*/
bool idFile_SaveGamePipelined::OpenForReading( idFile * file ) {
assert( mode == CLOSED );
if ( file == NULL ) {
return false;
}
name = file->GetName();
osPath = file->GetFullPath();
mode = READ;
nativeFile = file;
numChecksums = 0;
// init zlib for raw inflate with a 32k dictionary
//mem.PushHeap();
int status = inflateInit2( &zStream, sgf_windowBits.GetInteger() );
//mem.PopHeap();
if ( status != Z_OK ) {
idLib::FatalError( "idFile_SaveGamePipelined::OpenForReading: inflateInit2() error %i", status );
}
// spawn threads
if ( sgf_threads.GetInteger() >= 1 ) {
decompressThread = new (TAG_IDFILE) idSGFdecompressThread();
decompressThread->sgf = this;
decompressThread->StartWorkerThread( "SGF_DecompressThread", CORE_1B, THREAD_NORMAL );
}
if ( nativeFile != NULL && sgf_threads.GetInteger() >= 2 ) {
readThread = new (TAG_IDFILE) idSGFreadThread();
readThread->sgf = this;
readThread->StartWorkerThread( "SGF_ReadThread", CORE_1A, THREAD_NORMAL );
}
return true;
}
/*
============================
idFile_SaveGamePipelined::NextReadBlock
Reads the next data block from the filesystem into the memory buffer.
Modifies:
compressed
compressedProducedBytes
nativeFileEndHit
============================
*/
bool idFile_SaveGamePipelined::NextReadBlock( blockForIO_t * block, size_t lastReadBytes ) {
assert( mode == READ );
assert( ( lastReadBytes & ( COMPRESSED_BLOCK_SIZE - 1 ) ) == 0 || block == NULL );
compressedProducedBytes += lastReadBytes;
blockAvailable.Raise(); // a new block is available for the pipeline to consume
if ( nativeFileEndHit ) {
return false;
}
blockRequested.Wait(); // wait for the last block to be consumed by the pipeline
if ( finished || block == NULL ) {
nativeFileEndHit = true;
blockAvailable.Raise();
blockFinished.Raise();
return false;
}
assert( 0 == ( compressedProducedBytes & ( COMPRESSED_BLOCK_SIZE - 1 ) ) );
block->data = & compressed[compressedProducedBytes & ( COMPRESSED_BUFFER_SIZE - 1 )];
block->bytes = COMPRESSED_BLOCK_SIZE;
return true;
}
/*
============================
idFile_SaveGamePipelined::ReadBlock
Reads the next data block from the filesystem into the memory buffer.
Modifies:
compressed
compressedProducedBytes
nativeFile
nativeFileEndHit
============================
*/
void idFile_SaveGamePipelined::ReadBlock() {
assert( nativeFile != NULL );
// normally run in a separate thread
if ( nativeFileEndHit ) {
return;
}
// when we are reading the last block of the file, we may not fill the entire block
assert( 0 == ( compressedProducedBytes & ( COMPRESSED_BLOCK_SIZE - 1 ) ) );
byte * dest = &compressed[ compressedProducedBytes & ( COMPRESSED_BUFFER_SIZE - 1 ) ];
size_t ioBytes = nativeFile->Read( dest, COMPRESSED_BLOCK_SIZE );
compressedProducedBytes += ioBytes;
if ( ioBytes != COMPRESSED_BLOCK_SIZE ) {
nativeFileEndHit = true;
}
}
/*
============================
idFile_SaveGamePipelined::PumpCompressedBlock
Reads:
compressed
compressedProducedBytes
Modifies:
dataIO
byteIO
compressedConsumedBytes
============================
*/
void idFile_SaveGamePipelined::PumpCompressedBlock() {
// block until the background thread is done with the last block
if ( readThread != NULL ) {
readThread->WaitForThread();
} else if ( nativeFile == NULL ) {
if ( !nativeFileEndHit ) {
blockAvailable.Wait();
}
}
// fetch the next block read in
dataIO = &compressed[ compressedConsumedBytes & ( COMPRESSED_BUFFER_SIZE - 1 ) ];
bytesIO = compressedProducedBytes - compressedConsumedBytes;
compressedConsumedBytes = compressedProducedBytes;
if ( readThread != NULL ) {
// signal read thread to read another block
readThread->SignalWork();
} else if ( nativeFile != NULL ) {
// run syncronously
ReadBlock();
} else {
// request a new block
blockRequested.Raise();
}
}
/*
============================
idFile_SaveGamePipelined::DecompressBlock
Decompresses the next data block from the memory buffer
Normally this runs in a separate thread when signalled, but
can be called in the main thread for debugging.
This will not exit until a complete block has been decompressed,
unless end-of-file is reached.
This may require additional compressed blocks to be read.
Reads:
nativeFileEndHit
Modifies:
dataIO
bytesIO
uncompressed
uncompressedProducedBytes
zStreamEndHit
zStream
============================
*/
void idFile_SaveGamePipelined::DecompressBlock() {
if ( zStreamEndHit ) {
return;
}
assert( ( uncompressedProducedBytes & ( UNCOMPRESSED_BLOCK_SIZE - 1 ) ) == 0 );
zStream.next_out = (Bytef * )&uncompressed[ uncompressedProducedBytes & ( UNCOMPRESSED_BUFFER_SIZE - 1 ) ];
zStream.avail_out = UNCOMPRESSED_BLOCK_SIZE;
while( zStream.avail_out > 0 ) {
if ( zStream.avail_in == 0 ) {
do {
PumpCompressedBlock();
if ( bytesIO == 0 && nativeFileEndHit ) {
// don't try to decompress any more if there is no more data
zStreamEndHit = true;
return;
}
} while ( bytesIO == 0 );
zStream.next_in = (Bytef *) dataIO;
zStream.avail_in = (uInt) bytesIO;
dataIO = NULL;
bytesIO = 0;
if ( sgf_checksums.GetBool() ) {
if ( sgf_testCorruption.GetInteger() == numChecksums ) {
zStream.next_in[0] ^= 0xFF;
}
zStream.avail_in -= sizeof( uint32 );
uint32 checksum = MD5_BlockChecksum( zStream.next_in, zStream.avail_in );
if ( !verify( zStream.next_in[zStream.avail_in + 0] == ( ( checksum >> 0 ) & 0xFF ) ) ||
!verify( zStream.next_in[zStream.avail_in + 1] == ( ( checksum >> 8 ) & 0xFF ) ) ||
!verify( zStream.next_in[zStream.avail_in + 2] == ( ( checksum >> 16 ) & 0xFF ) ) ||
!verify( zStream.next_in[zStream.avail_in + 3] == ( ( checksum >> 24 ) & 0xFF ) ) ) {
// don't try to decompress any more if the checksum is wrong
zStreamEndHit = true;
return;
}
numChecksums++;
}
}
const int zstat = inflate( &zStream, Z_SYNC_FLUSH );
uncompressedProducedBytes = zStream.total_out;
if ( zstat == Z_STREAM_END ) {
// don't try to decompress any more
zStreamEndHit = true;
return;
}
if ( zstat != Z_OK ) {
idLib::Warning( "idFile_SaveGamePipelined::DecompressBlock: inflate() returned %i", zstat );
zStreamEndHit = true;
return;
}
}
assert( ( uncompressedProducedBytes & ( UNCOMPRESSED_BLOCK_SIZE - 1 ) ) == 0 );
}
/*
============================
idFile_SaveGamePipelined::PumpUncompressedBlock
Called when an uncompressed block is drained.
Reads:
uncompressed
uncompressedProducedBytes
Modifies:
dataZlib
bytesZlib
uncompressedConsumedBytes
============================
*/
void idFile_SaveGamePipelined::PumpUncompressedBlock() {
if ( decompressThread != NULL ) {
// make sure thread has completed the last work
decompressThread->WaitForThread();
}
// fetch the next block produced by Zlib
dataZlib = &uncompressed[ uncompressedConsumedBytes & ( UNCOMPRESSED_BUFFER_SIZE - 1 ) ];
bytesZlib = uncompressedProducedBytes - uncompressedConsumedBytes;
uncompressedConsumedBytes = uncompressedProducedBytes;
if ( decompressThread != NULL ) {
// signal thread for more work
decompressThread->SignalWork();
} else {
// run syncronously
DecompressBlock();
}
}
/*
============================
idFile_SaveGamePipelined::Read
Modifies:
dataZlib
bytesZlib
============================
*/
int idFile_SaveGamePipelined::Read( void * buffer, int length ) {
if ( buffer == NULL || length <= 0 ) {
return 0;
}
assert( mode == READ );
size_t ioCount = 0;
size_t lengthRemaining = length;
byte * buffer_p = (byte *)buffer;
while ( lengthRemaining > 0 ) {
while ( bytesZlib == 0 ) {
PumpUncompressedBlock();
if ( bytesZlib == 0 && zStreamEndHit ) {
return ioCount;
}
}
const size_t copyFromBlock = ( lengthRemaining < bytesZlib ) ? lengthRemaining : bytesZlib;
memcpy( buffer_p, dataZlib, copyFromBlock );
dataZlib += copyFromBlock;
bytesZlib -= copyFromBlock;
buffer_p += copyFromBlock;
ioCount += copyFromBlock;
lengthRemaining -= copyFromBlock;
}
return ioCount;
}
/*
===================================================================================
TEST CODE
===================================================================================
*/
/*
============================
TestProcessFile
============================
*/
static void TestProcessFile( const char * const filename ) {
idLib::Printf( "Processing %s:\n", filename );
// load some test data
void *testData;
const int testDataLength = fileSystem->ReadFile( filename, &testData, NULL );
const char * const outFileName = "junk/savegameTest.bin";
idFile_SaveGamePipelined *saveFile = new (TAG_IDFILE) idFile_SaveGamePipelined;
saveFile->OpenForWriting( outFileName, true );
const uint64 startWriteMicroseconds = Sys_Microseconds();
saveFile->Write( testData, testDataLength );
delete saveFile; // final flush
const int readDataLength = fileSystem->GetFileLength( outFileName );
const uint64 endWriteMicroseconds = Sys_Microseconds();
const uint64 writeMicroseconds = endWriteMicroseconds - startWriteMicroseconds;
idLib::Printf( "%lld microseconds to compress %i bytes to %i written bytes = %4.1f MB/s\n",
writeMicroseconds, testDataLength, readDataLength, (float)readDataLength / writeMicroseconds );
void * readData = (void *)Mem_Alloc( testDataLength, TAG_SAVEGAMES );
const uint64 startReadMicroseconds = Sys_Microseconds();
idFile_SaveGamePipelined *loadFile = new (TAG_IDFILE) idFile_SaveGamePipelined;
loadFile->OpenForReading( outFileName, true );
loadFile->Read( readData, testDataLength );
delete loadFile;
const uint64 endReadMicroseconds = Sys_Microseconds();
const uint64 readMicroseconds = endReadMicroseconds - startReadMicroseconds;
idLib::Printf( "%lld microseconds to decompress = %4.1f MB/s\n", readMicroseconds, (float)testDataLength / readMicroseconds );
int comparePoint;
for ( comparePoint = 0; comparePoint < testDataLength; comparePoint++ ) {
if ( ((byte *)readData)[comparePoint] != ((byte *)testData)[comparePoint] ) {
break;
}
}
if ( comparePoint != testDataLength ) {
idLib::Printf( "Compare failed at %i.\n", comparePoint );
assert( 0 );
} else {
idLib::Printf( "Compare succeeded.\n" );
}
Mem_Free( readData );
Mem_Free( testData );
}
/*
============================
TestSaveGameFile
============================
*/
CONSOLE_COMMAND( TestSaveGameFile, "Exercises the pipelined savegame code", 0 ) {
#if 1
TestProcessFile( "maps/game/wasteland1/wasteland1.map" );
#else
// test every file in base (found a fencepost error >100 files in originally!)
idFileList * fileList = fileSystem->ListFiles( "", "" );
for ( int i = 0; i < fileList->GetNumFiles(); i++ ) {
TestProcessFile( fileList->GetFile( i ) );
common->UpdateConsoleDisplay();
}
delete fileList;
#endif
}
/*
============================
TestCompressionSpeeds
============================
*/
CONSOLE_COMMAND( TestCompressionSpeeds, "Compares zlib and our code", 0 ) {
const char * const filename = "-colorMap.tga";
idLib::Printf( "Processing %s:\n", filename );
// load some test data
void *testData;
const int testDataLength = fileSystem->ReadFile( filename, &testData, NULL );
const int startWriteMicroseconds = Sys_Microseconds();
idCompressor *compressor = idCompressor::AllocLZW();
// idFile *f = fileSystem->OpenFileWrite( "junk/lzwTest.bin" );
idFile_Memory *f = new (TAG_IDFILE) idFile_Memory( "junk/lzwTest.bin" );
compressor->Init( f, true, 8 );
compressor->Write( testData, testDataLength );
const int readDataLength = f->Tell();
delete compressor;
delete f;
const int endWriteMicroseconds = Sys_Microseconds();
const int writeMicroseconds = endWriteMicroseconds - startWriteMicroseconds;
idLib::Printf( "%i microseconds to compress %i bytes to %i written bytes = %4.1f MB/s\n",
writeMicroseconds, testDataLength, readDataLength, (float)readDataLength / writeMicroseconds );
}