550 lines
14 KiB
C++
550 lines
14 KiB
C++
|
/**
|
||
|
@file BinaryOutput.cpp
|
||
|
|
||
|
@author Morgan McGuire, http://graphics.cs.williams.edu
|
||
|
Copyright 2002-2011, Morgan McGuire, All rights reserved.
|
||
|
|
||
|
@created 2002-02-20
|
||
|
@edited 2010-03-17
|
||
|
*/
|
||
|
|
||
|
#include "G3D/platform.h"
|
||
|
#include "G3D/BinaryOutput.h"
|
||
|
#include "G3D/fileutils.h"
|
||
|
#include "G3D/FileSystem.h"
|
||
|
#include "G3D/stringutils.h"
|
||
|
#include "G3D/Array.h"
|
||
|
#include <zlib.h>
|
||
|
#include "G3D/Log.h"
|
||
|
#include <cstring>
|
||
|
|
||
|
#ifdef G3D_LINUX
|
||
|
# include <errno.h>
|
||
|
#endif
|
||
|
|
||
|
// Largest memory buffer that the system will use for writing to
|
||
|
// disk. After this (or if the system runs out of memory)
|
||
|
// chunks of the file will be dumped to disk.
|
||
|
//
|
||
|
// Currently 400 MB
|
||
|
#define MAX_BINARYOUTPUT_BUFFER_SIZE 400000000
|
||
|
|
||
|
namespace G3D {
|
||
|
|
||
|
void BinaryOutput::writeBool8(const std::vector<bool>& out, int n) {
|
||
|
for (int i = 0; i < n; ++i) {
|
||
|
writeBool8(out[i]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeBool8(const Array<bool>& out, int n) {
|
||
|
writeBool8(out.getCArray(), n);
|
||
|
}
|
||
|
|
||
|
#define IMPLEMENT_WRITER(ucase, lcase)\
|
||
|
void BinaryOutput::write##ucase(const std::vector<lcase>& out, int n) {\
|
||
|
write##ucase(&out[0], n);\
|
||
|
}\
|
||
|
\
|
||
|
\
|
||
|
void BinaryOutput::write##ucase(const Array<lcase>& out, int n) {\
|
||
|
write##ucase(out.getCArray(), n);\
|
||
|
}
|
||
|
|
||
|
|
||
|
IMPLEMENT_WRITER(UInt8, uint8)
|
||
|
IMPLEMENT_WRITER(Int8, int8)
|
||
|
IMPLEMENT_WRITER(UInt16, uint16)
|
||
|
IMPLEMENT_WRITER(Int16, int16)
|
||
|
IMPLEMENT_WRITER(UInt32, uint32)
|
||
|
IMPLEMENT_WRITER(Int32, int32)
|
||
|
IMPLEMENT_WRITER(UInt64, uint64)
|
||
|
IMPLEMENT_WRITER(Int64, int64)
|
||
|
IMPLEMENT_WRITER(Float32, float32)
|
||
|
IMPLEMENT_WRITER(Float64, float64)
|
||
|
|
||
|
#undef IMPLEMENT_WRITER
|
||
|
|
||
|
// Data structures that are one byte per element can be
|
||
|
// directly copied, regardles of endian-ness.
|
||
|
#define IMPLEMENT_WRITER(ucase, lcase)\
|
||
|
void BinaryOutput::write##ucase(const lcase* out, int n) {\
|
||
|
if (sizeof(lcase) == 1) {\
|
||
|
writeBytes((void*)out, n);\
|
||
|
} else {\
|
||
|
for (int i = 0; i < n ; ++i) {\
|
||
|
write##ucase(out[i]);\
|
||
|
}\
|
||
|
}\
|
||
|
}
|
||
|
|
||
|
IMPLEMENT_WRITER(Bool8, bool)
|
||
|
IMPLEMENT_WRITER(UInt8, uint8)
|
||
|
IMPLEMENT_WRITER(Int8, int8)
|
||
|
|
||
|
#undef IMPLEMENT_WRITER
|
||
|
|
||
|
|
||
|
#define IMPLEMENT_WRITER(ucase, lcase)\
|
||
|
void BinaryOutput::write##ucase(const lcase* out, int n) {\
|
||
|
if (m_swapBytes) {\
|
||
|
for (int i = 0; i < n; ++i) {\
|
||
|
write##ucase(out[i]);\
|
||
|
}\
|
||
|
} else {\
|
||
|
writeBytes((const void*)out, sizeof(lcase) * n);\
|
||
|
}\
|
||
|
}
|
||
|
|
||
|
|
||
|
IMPLEMENT_WRITER(UInt16, uint16)
|
||
|
IMPLEMENT_WRITER(Int16, int16)
|
||
|
IMPLEMENT_WRITER(UInt32, uint32)
|
||
|
IMPLEMENT_WRITER(Int32, int32)
|
||
|
IMPLEMENT_WRITER(UInt64, uint64)
|
||
|
IMPLEMENT_WRITER(Int64, int64)
|
||
|
IMPLEMENT_WRITER(Float32, float32)
|
||
|
IMPLEMENT_WRITER(Float64, float64)
|
||
|
|
||
|
#undef IMPLEMENT_WRITER
|
||
|
|
||
|
|
||
|
void BinaryOutput::reallocBuffer(size_t bytes, size_t oldBufferLen) {
|
||
|
//debugPrintf("reallocBuffer(%d, %d)\n", bytes, oldBufferLen);
|
||
|
|
||
|
size_t newBufferLen = (int)(m_bufferLen * 1.5) + 100;
|
||
|
uint8* newBuffer = NULL;
|
||
|
|
||
|
if ((m_filename == "<memory>") || (newBufferLen < MAX_BINARYOUTPUT_BUFFER_SIZE)) {
|
||
|
// We're either writing to memory (in which case we *have* to
|
||
|
// try and allocate) or we've been asked to allocate a
|
||
|
// reasonable size buffer.
|
||
|
|
||
|
// debugPrintf(" realloc(%d)\n", newBufferLen);
|
||
|
newBuffer = (uint8*)System::realloc(m_buffer, newBufferLen);
|
||
|
if (newBuffer != NULL) {
|
||
|
m_maxBufferLen = newBufferLen;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ((newBuffer == NULL) && (bytes > 0)) {
|
||
|
// Realloc failed; we're probably out of memory. Back out
|
||
|
// the entire call and try to dump some data to disk.
|
||
|
alwaysAssertM(m_filename != "<memory>", "Realloc failed while writing to memory.");
|
||
|
m_bufferLen = oldBufferLen;
|
||
|
reserveBytesWhenOutOfMemory(bytes);
|
||
|
} else {
|
||
|
// Realloc succeeded
|
||
|
m_buffer = newBuffer;
|
||
|
debugAssert(isValidHeapPointer(m_buffer));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::reserveBytesWhenOutOfMemory(size_t bytes) {
|
||
|
if (m_filename == "<memory>") {
|
||
|
throw "Out of memory while writing to memory in BinaryOutput (no RAM left).";
|
||
|
} else if ((int)bytes > (int)m_maxBufferLen) {
|
||
|
throw "Out of memory while writing to disk in BinaryOutput (could not create a large enough buffer).";
|
||
|
} else {
|
||
|
|
||
|
// Dump the contents to disk. In order to enable seeking backwards,
|
||
|
// we keep the last 10 MB in memory.
|
||
|
size_t writeBytes = m_bufferLen - 10 * 1024 * 1024;
|
||
|
|
||
|
if (writeBytes < m_bufferLen / 3) {
|
||
|
// We're going to write less than 1/3 of the file;
|
||
|
// give up and just write the whole thing.
|
||
|
writeBytes = m_bufferLen;
|
||
|
}
|
||
|
debugAssert(writeBytes > 0);
|
||
|
|
||
|
//debugPrintf("Writing %d bytes to disk\n", writeBytes);
|
||
|
|
||
|
const char* mode = (m_alreadyWritten > 0) ? "ab" : "wb";
|
||
|
alwaysAssertM(m_filename != "<memory>", "Writing memory file");
|
||
|
FILE* file = FileSystem::fopen(m_filename.c_str(), mode);
|
||
|
debugAssert(file);
|
||
|
|
||
|
size_t count = fwrite(m_buffer, 1, writeBytes, file);
|
||
|
debugAssert(count == writeBytes); (void)count;
|
||
|
|
||
|
fclose(file);
|
||
|
file = NULL;
|
||
|
|
||
|
// Record that we saved this data.
|
||
|
m_alreadyWritten += writeBytes;
|
||
|
m_bufferLen -= writeBytes;
|
||
|
m_pos -= writeBytes;
|
||
|
|
||
|
debugAssert(m_bufferLen < m_maxBufferLen);
|
||
|
debugAssert(m_bufferLen >= 0);
|
||
|
debugAssert(m_pos >= 0);
|
||
|
debugAssert(m_pos <= (int64)m_bufferLen);
|
||
|
|
||
|
// Shift the unwritten data back appropriately in the buffer.
|
||
|
debugAssert(isValidHeapPointer(m_buffer));
|
||
|
System::memcpy(m_buffer, m_buffer + writeBytes, m_bufferLen);
|
||
|
debugAssert(isValidHeapPointer(m_buffer));
|
||
|
|
||
|
// *now* we allocate bytes (there should presumably be enough
|
||
|
// space in the buffer; if not, we'll come back through this
|
||
|
// code and dump the last 10MB to disk as well. Note that the
|
||
|
// bytes > maxBufferLen case above would already have triggered
|
||
|
// if this call couldn't succeed.
|
||
|
reserveBytes(bytes);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
BinaryOutput::BinaryOutput() {
|
||
|
m_alreadyWritten = 0;
|
||
|
m_swapBytes = false;
|
||
|
m_pos = 0;
|
||
|
m_filename = "<memory>";
|
||
|
m_buffer = NULL;
|
||
|
m_bufferLen = 0;
|
||
|
m_maxBufferLen = 0;
|
||
|
m_beginEndBits = 0;
|
||
|
m_bitString = 0;
|
||
|
m_bitPos = 0;
|
||
|
m_ok = true;
|
||
|
m_committed = false;
|
||
|
}
|
||
|
|
||
|
|
||
|
BinaryOutput::BinaryOutput(
|
||
|
const std::string& filename,
|
||
|
G3DEndian fileEndian) {
|
||
|
|
||
|
m_pos = 0;
|
||
|
m_alreadyWritten = 0;
|
||
|
setEndian(fileEndian);
|
||
|
m_filename = filename;
|
||
|
m_buffer = NULL;
|
||
|
m_bufferLen = 0;
|
||
|
m_maxBufferLen = 0;
|
||
|
m_beginEndBits = 0;
|
||
|
m_bitString = 0;
|
||
|
m_bitPos = 0;
|
||
|
m_committed = false;
|
||
|
|
||
|
m_ok = true;
|
||
|
/** Verify ability to write to disk */
|
||
|
commit(false);
|
||
|
m_committed = false;
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::reset() {
|
||
|
debugAssert(m_beginEndBits == 0);
|
||
|
alwaysAssertM(m_filename == "<memory>",
|
||
|
"Can only reset a BinaryOutput that writes to memory.");
|
||
|
|
||
|
// Do not reallocate, just clear the size of the buffer.
|
||
|
m_pos = 0;
|
||
|
m_alreadyWritten = 0;
|
||
|
m_bufferLen = 0;
|
||
|
m_beginEndBits = 0;
|
||
|
m_bitString = 0;
|
||
|
m_bitPos = 0;
|
||
|
m_committed = false;
|
||
|
}
|
||
|
|
||
|
|
||
|
BinaryOutput::~BinaryOutput() {
|
||
|
debugAssert((m_buffer == NULL) || isValidHeapPointer(m_buffer));
|
||
|
System::free(m_buffer);
|
||
|
m_buffer = NULL;
|
||
|
m_bufferLen = 0;
|
||
|
m_maxBufferLen = 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::setEndian(G3DEndian fileEndian) {
|
||
|
m_fileEndian = fileEndian;
|
||
|
m_swapBytes = (fileEndian != System::machineEndian());
|
||
|
}
|
||
|
|
||
|
|
||
|
bool BinaryOutput::ok() const {
|
||
|
return m_ok;
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::compress(int level) {
|
||
|
if (m_alreadyWritten > 0) {
|
||
|
throw "Cannot compress huge files (part of this file has already been written to disk).";
|
||
|
}
|
||
|
debugAssertM(! m_committed, "Cannot compress after committing.");
|
||
|
alwaysAssertM(m_bufferLen < 0xFFFFFFFF, "Compress only works for 32-bit files.");
|
||
|
|
||
|
// This is the worst-case size, as mandated by zlib
|
||
|
unsigned long compressedSize = iCeil(m_bufferLen * 1.001) + 12;
|
||
|
|
||
|
// Save the old buffer and reallocate to the worst-case size
|
||
|
const uint8* src = m_buffer;
|
||
|
const uint32 srcSize = (uint32)m_bufferLen;
|
||
|
|
||
|
// add space for the 4-byte header
|
||
|
m_maxBufferLen = compressedSize + 4;
|
||
|
m_buffer = (uint8*)System::malloc(m_maxBufferLen);
|
||
|
|
||
|
// Write the header containing the old buffer size, which is needed for decompression
|
||
|
{
|
||
|
const uint8* convert = (const uint8*)&srcSize;
|
||
|
if (m_swapBytes) {
|
||
|
m_buffer[0] = convert[3];
|
||
|
m_buffer[1] = convert[2];
|
||
|
m_buffer[2] = convert[1];
|
||
|
m_buffer[3] = convert[0];
|
||
|
} else {
|
||
|
m_buffer[0] = convert[0];
|
||
|
m_buffer[1] = convert[1];
|
||
|
m_buffer[2] = convert[2];
|
||
|
m_buffer[3] = convert[3];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Compress and write after the header
|
||
|
int result = compress2(m_buffer + 4, &compressedSize, src, srcSize, iClamp(level, 0, 9));
|
||
|
|
||
|
debugAssert(result == Z_OK); (void)result;
|
||
|
m_bufferLen = compressedSize + 4;
|
||
|
m_pos = m_bufferLen;
|
||
|
|
||
|
// Free the old data
|
||
|
System::free((void*)src);
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::commit(bool flush) {
|
||
|
debugAssertM(! m_committed, "Cannot commit twice");
|
||
|
m_committed = true;
|
||
|
debugAssertM(m_beginEndBits == 0, "Missing endBits before commit");
|
||
|
|
||
|
if (m_filename == "<memory>") {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Make sure the directory exists.
|
||
|
std::string root, base, ext, path;
|
||
|
Array<std::string> pathArray;
|
||
|
parseFilename(m_filename, root, pathArray, base, ext);
|
||
|
|
||
|
path = root + stringJoin(pathArray, '/');
|
||
|
if (! FileSystem::exists(path, false)) {
|
||
|
FileSystem::createDirectory(path);
|
||
|
}
|
||
|
|
||
|
const char* mode = (m_alreadyWritten > 0) ? "ab" : "wb";
|
||
|
|
||
|
alwaysAssertM(m_filename != "<memory>", "Writing to memory file");
|
||
|
FILE* file = FileSystem::fopen(m_filename.c_str(), mode);
|
||
|
|
||
|
if (! file) {
|
||
|
logPrintf("Error %d while trying to open \"%s\"\n", errno, m_filename.c_str());
|
||
|
}
|
||
|
m_ok = (file != NULL) && m_ok;
|
||
|
|
||
|
if (m_ok) {
|
||
|
debugAssertM(file, std::string("Could not open '") + m_filename + "'");
|
||
|
|
||
|
if (m_buffer != NULL) {
|
||
|
m_alreadyWritten += m_bufferLen;
|
||
|
|
||
|
size_t success = fwrite(m_buffer, m_bufferLen, 1, file);
|
||
|
(void)success;
|
||
|
debugAssertM(success == 1, std::string("Could not write to '") + m_filename + "'");
|
||
|
}
|
||
|
if (flush) {
|
||
|
fflush(file);
|
||
|
}
|
||
|
FileSystem::fclose(file);
|
||
|
file = NULL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::commit(
|
||
|
uint8* out) {
|
||
|
debugAssertM(! m_committed, "Cannot commit twice");
|
||
|
m_committed = true;
|
||
|
|
||
|
System::memcpy(out, m_buffer, m_bufferLen);
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeUInt16(uint16 u) {
|
||
|
reserveBytes(2);
|
||
|
|
||
|
uint8* convert = (uint8*)&u;
|
||
|
|
||
|
if (m_swapBytes) {
|
||
|
m_buffer[m_pos] = convert[1];
|
||
|
m_buffer[m_pos + 1] = convert[0];
|
||
|
} else {
|
||
|
*(uint16*)(m_buffer + m_pos) = u;
|
||
|
}
|
||
|
|
||
|
m_pos += 2;
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeUInt32(uint32 u) {
|
||
|
reserveBytes(4);
|
||
|
|
||
|
uint8* convert = (uint8*)&u;
|
||
|
|
||
|
debugAssert(m_beginEndBits == 0);
|
||
|
|
||
|
if (m_swapBytes) {
|
||
|
m_buffer[m_pos] = convert[3];
|
||
|
m_buffer[m_pos + 1] = convert[2];
|
||
|
m_buffer[m_pos + 2] = convert[1];
|
||
|
m_buffer[m_pos + 3] = convert[0];
|
||
|
} else {
|
||
|
*(uint32*)(m_buffer + m_pos) = u;
|
||
|
}
|
||
|
|
||
|
m_pos += 4;
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeUInt64(uint64 u) {
|
||
|
reserveBytes(8);
|
||
|
|
||
|
uint8* convert = (uint8*)&u;
|
||
|
|
||
|
if (m_swapBytes) {
|
||
|
m_buffer[m_pos] = convert[7];
|
||
|
m_buffer[m_pos + 1] = convert[6];
|
||
|
m_buffer[m_pos + 2] = convert[5];
|
||
|
m_buffer[m_pos + 3] = convert[4];
|
||
|
m_buffer[m_pos + 4] = convert[3];
|
||
|
m_buffer[m_pos + 5] = convert[2];
|
||
|
m_buffer[m_pos + 6] = convert[1];
|
||
|
m_buffer[m_pos + 7] = convert[0];
|
||
|
} else {
|
||
|
*(uint64*)(m_buffer + m_pos) = u;
|
||
|
}
|
||
|
|
||
|
m_pos += 8;
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeString(const char* s) {
|
||
|
// +1 is because strlen doesn't count the null
|
||
|
size_t len = strlen(s) + 1;
|
||
|
|
||
|
debugAssert(m_beginEndBits == 0);
|
||
|
reserveBytes(len);
|
||
|
System::memcpy(m_buffer + m_pos, s, len);
|
||
|
m_pos += len;
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeStringEven(const char* s) {
|
||
|
// +1 is because strlen doesn't count the null
|
||
|
size_t len = strlen(s) + 1;
|
||
|
|
||
|
reserveBytes(len);
|
||
|
System::memcpy(m_buffer + m_pos, s, len);
|
||
|
m_pos += len;
|
||
|
|
||
|
// Pad with another NULL
|
||
|
if ((len % 2) == 1) {
|
||
|
writeUInt8(0);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeString32(const char* s) {
|
||
|
// Write the NULL and count it
|
||
|
size_t len = strlen(s) + 1;
|
||
|
writeUInt32((uint32)len);
|
||
|
|
||
|
debugAssert(m_beginEndBits == 0);
|
||
|
reserveBytes(len);
|
||
|
System::memcpy(m_buffer + m_pos, s, len);
|
||
|
m_pos += len;
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeVector4(const Vector4& v) {
|
||
|
writeFloat32(v.x);
|
||
|
writeFloat32(v.y);
|
||
|
writeFloat32(v.z);
|
||
|
writeFloat32(v.w);
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeVector3(const Vector3& v) {
|
||
|
writeFloat32(v.x);
|
||
|
writeFloat32(v.y);
|
||
|
writeFloat32(v.z);
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeVector2(const Vector2& v) {
|
||
|
writeFloat32(v.x);
|
||
|
writeFloat32(v.y);
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeColor4(const Color4& v) {
|
||
|
writeFloat32(v.r);
|
||
|
writeFloat32(v.g);
|
||
|
writeFloat32(v.b);
|
||
|
writeFloat32(v.a);
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeColor3(const Color3& v) {
|
||
|
writeFloat32(v.r);
|
||
|
writeFloat32(v.g);
|
||
|
writeFloat32(v.b);
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::beginBits() {
|
||
|
debugAssertM(m_beginEndBits == 0, "Already in beginBits...endBits");
|
||
|
m_bitString = 0x00;
|
||
|
m_bitPos = 0;
|
||
|
m_beginEndBits = 1;
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::writeBits(uint32 value, int numBits) {
|
||
|
|
||
|
while (numBits > 0) {
|
||
|
// Extract the current bit of value and
|
||
|
// insert it into the current byte
|
||
|
m_bitString |= (value & 1) << m_bitPos;
|
||
|
++m_bitPos;
|
||
|
value = value >> 1;
|
||
|
--numBits;
|
||
|
|
||
|
if (m_bitPos > 7) {
|
||
|
// We've reached the end of this byte
|
||
|
writeUInt8(m_bitString);
|
||
|
m_bitString = 0x00;
|
||
|
m_bitPos = 0;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void BinaryOutput::endBits() {
|
||
|
debugAssertM(m_beginEndBits == 1, "Not in beginBits...endBits");
|
||
|
if (m_bitPos > 0) {
|
||
|
writeUInt8(m_bitString);
|
||
|
}
|
||
|
m_bitString = 0;
|
||
|
m_bitPos = 0;
|
||
|
m_beginEndBits = 0;
|
||
|
}
|
||
|
|
||
|
}
|