Add compressed KTX size evaluation

This commit is contained in:
Ryan Huffman 2017-04-17 17:45:02 -07:00 committed by Atlante45
parent b21dc12cc6
commit 20f4d14e07
10 changed files with 220 additions and 143 deletions

View file

@ -31,7 +31,7 @@
const float METERS_TO_INCHES = 39.3701f;
static uint32_t _currentWebCount { 0 };
// Don't allow more than 100 concurrent web views
static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 20;
static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 0;
// If a web-view hasn't been rendered for 30 seconds, de-allocate the framebuffer
static uint64_t MAX_NO_RENDER_INTERVAL = 30 * USECS_PER_SECOND;
@ -71,7 +71,7 @@ RenderableWebEntityItem::~RenderableWebEntityItem() {
bool RenderableWebEntityItem::buildWebSurface(QSharedPointer<EntityTreeRenderer> renderer) {
if (_currentWebCount >= MAX_CONCURRENT_WEB_VIEWS) {
qWarning() << "Too many concurrent web views to create new view";
//qWarning() << "Too many concurrent web views to create new view";
return false;
}
QString javaScriptToInject;

View file

@ -181,13 +181,13 @@ void GL45ResourceTexture::populateTransferQueue() {
const uint8_t maxFace = GLTexture::getFaceCount(_target);
uint16_t sourceMip = _populatedMip;
qDebug() << "populateTransferQueue info : " << _populatedMip << " " << _maxAllocatedMip << " " << _allocatedMip;
//qDebug() << "populateTransferQueue info : " << _populatedMip << " " << _maxAllocatedMip << " " << _allocatedMip;
do {
--sourceMip;
auto targetMip = sourceMip - _allocatedMip;
auto mipDimensions = _gpuObject.evalMipDimensions(sourceMip);
bool transferQueued = false;
qDebug() << "populateTransferQueue " << QString::fromStdString(_gpuObject.source()) << sourceMip << " " << targetMip;
//qDebug() << "populateTransferQueue " << QString::fromStdString(_gpuObject.source()) << sourceMip << " " << targetMip;
for (uint8_t face = 0; face < maxFace; ++face) {
if (!_gpuObject.isStoredMipFaceAvailable(sourceMip, face)) {
const_cast<gpu::Texture&>(_gpuObject).requestInterestInMip(sourceMip);

View file

@ -34,6 +34,8 @@ namespace ktx {
namespace gpu {
extern const std::string SOURCE_HASH_KEY;
// THe spherical harmonics is a nice tool for cubemap, so if required, the irradiance SH can be automatically generated
// with the cube texture
class Texture;

View file

@ -11,6 +11,7 @@
#include "Texture.h"
#include <qdebug.h>
#include <ktx/KTX.h>
using namespace gpu;
@ -41,6 +42,7 @@ struct GPUKTXPayload {
}
};
const std::string gpu::SOURCE_HASH_KEY { "hifi.sourceHash" };
std::string GPUKTXPayload::KEY{ "hifi.gpu" };
KtxStorage::KtxStorage(const std::string& filename) : _filename(filename) {
@ -91,7 +93,7 @@ std::shared_ptr<storage::FileStorage> KtxStorage::maybeOpenFile() {
}
PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const {
qDebug() << "getMipFace: " << QString::fromStdString(_filename) << ": " << level << " " << face;
//qDebug() << "getMipFace: " << QString::fromStdString(_filename) << ": " << level << " " << face;
storage::StoragePointer result;
auto faceOffset = _ktxDescriptor->getMipFaceTexelsOffset(level, face);
auto faceSize = _ktxDescriptor->getMipFaceTexelsSize(level, face);
@ -108,14 +110,14 @@ Size KtxStorage::getMipFaceSize(uint16 level, uint8 face) const {
bool KtxStorage::isMipAvailable(uint16 level, uint8 face) const {
auto avail = level >= _minMipLevelAvailable;
qDebug() << "isMipAvailable: " << QString::fromStdString(_filename) << ": " << level << " " << face << avail << _minMipLevelAvailable << " " << _ktxDescriptor->header.numberOfMipmapLevels;
//qDebug() << "isMipAvailable: " << QString::fromStdString(_filename) << ": " << level << " " << face << avail << _minMipLevelAvailable << " " << _ktxDescriptor->header.numberOfMipmapLevels;
//return true;
return avail;
}
void KtxStorage::assignMipData(uint16 level, const storage::StoragePointer& storage) {
if (level != _minMipLevelAvailable - 1) {
qWarning() << "Invalid level to be stored";
qWarning() << "Invalid level to be stored, expected: " << (_minMipLevelAvailable - 1) << ", got: " << level;
return;
}
@ -124,7 +126,10 @@ void KtxStorage::assignMipData(uint16 level, const storage::StoragePointer& stor
}
if (storage->size() != _ktxDescriptor->images[level]._imageSize) {
throw std::runtime_error("Invalid image size for level");
qDebug() << "Invalid image size: " << storage->size() << ", expected: " << _ktxDescriptor->images[level]._imageSize
<< ", filename: " << QString::fromStdString(_filename);
//throw std::runtime_error("Invalid image size for level");
return;
}
@ -258,7 +263,6 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
ktx::KeyValues keyValues;
keyValues.emplace_back(ktx::KeyValue(GPUKTXPayload::KEY, sizeof(GPUKTXPayload), (ktx::Byte*) &keyval));
static const std::string SOURCE_HASH_KEY = "hifi.sourceHash";
auto hash = texture.sourceHash();
if (!hash.empty()) {
keyValues.emplace_back(ktx::KeyValue(SOURCE_HASH_KEY, static_cast<uint32>(hash.size()), (ktx::Byte*) hash.c_str()));

View file

@ -34,30 +34,75 @@ uint32_t Header::evalMaxDimension() const {
return std::max(getPixelWidth(), std::max(getPixelHeight(), getPixelDepth()));
}
uint32_t Header::evalPixelWidth(uint32_t level) const {
return std::max(getPixelWidth() >> level, 1U);
uint32_t Header::evalPixelOrBlockWidth(uint32_t level) const {
auto pixelWidth = std::max(getPixelWidth() >> level, 1U);
if (getGLType() == GLType::COMPRESSED_TYPE) {
return (pixelWidth + 3) / 4;
} else {
return pixelWidth;
}
}
uint32_t Header::evalPixelHeight(uint32_t level) const {
return std::max(getPixelHeight() >> level, 1U);
uint32_t Header::evalPixelOrBlockHeight(uint32_t level) const {
auto pixelWidth = std::max(getPixelHeight() >> level, 1U);
if (getGLType() == GLType::COMPRESSED_TYPE) {
auto format = getGLInternaFormat_Compressed();
switch (format) {
case GLInternalFormat_Compressed::COMPRESSED_SRGB_S3TC_DXT1_EXT: // BC1
case GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT: // BC1A
case GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: // BC3
case GLInternalFormat_Compressed::COMPRESSED_RED_RGTC1: // BC4
case GLInternalFormat_Compressed::COMPRESSED_RG_RGTC2: // BC5
return (pixelWidth + 3) / 4;
default:
throw std::runtime_error("Unknown format");
}
} else {
return pixelWidth;
}
}
uint32_t Header::evalPixelDepth(uint32_t level) const {
uint32_t Header::evalPixelOrBlockDepth(uint32_t level) const {
return std::max(getPixelDepth() >> level, 1U);
}
size_t Header::evalPixelSize() const {
return 4;//glTypeSize; // Really we should generate the size from the FOrmat etc
size_t Header::evalPixelOrBlockSize() const {
if (getGLType() == GLType::COMPRESSED_TYPE) {
auto format = getGLInternaFormat_Compressed();
if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_S3TC_DXT1_EXT) {
return 8;
} else if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT) {
return 8;
} else if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT) {
return 16;
} else if (format == GLInternalFormat_Compressed::COMPRESSED_RED_RGTC1) {
return 8;
} else if (format == GLInternalFormat_Compressed::COMPRESSED_RG_RGTC2) {
return 16;
}
} else {
auto baseFormat = getGLBaseInternalFormat();
if (baseFormat == GLBaseInternalFormat::RED) {
return 1;
} else if (baseFormat == GLBaseInternalFormat::RG) {
return 2;
} else if (baseFormat == GLBaseInternalFormat::RGB) {
return 3;
} else if (baseFormat == GLBaseInternalFormat::RGBA) {
return 4;
}
}
throw std::runtime_error("Unknown format");
}
size_t Header::evalRowSize(uint32_t level) const {
auto pixWidth = evalPixelWidth(level);
auto pixSize = evalPixelSize();
auto pixWidth = evalPixelOrBlockWidth(level);
auto pixSize = evalPixelOrBlockSize();
auto netSize = pixWidth * pixSize;
auto padding = evalPadding(netSize);
return netSize + padding;
}
size_t Header::evalFaceSize(uint32_t level) const {
auto pixHeight = evalPixelHeight(level);
auto pixDepth = evalPixelDepth(level);
auto pixHeight = evalPixelOrBlockHeight(level);
auto pixDepth = evalPixelOrBlockDepth(level);
auto rowSize = evalRowSize(level);
return pixDepth * pixHeight * rowSize;
}

View file

@ -336,11 +336,11 @@ namespace ktx {
uint32_t getNumberOfLevels() const { return (numberOfMipmapLevels ? numberOfMipmapLevels : 1); }
uint32_t evalMaxDimension() const;
uint32_t evalPixelWidth(uint32_t level) const;
uint32_t evalPixelHeight(uint32_t level) const;
uint32_t evalPixelDepth(uint32_t level) const;
uint32_t evalPixelOrBlockWidth(uint32_t level) const;
uint32_t evalPixelOrBlockHeight(uint32_t level) const;
uint32_t evalPixelOrBlockDepth(uint32_t level) const;
size_t evalPixelSize() const;
size_t evalPixelOrBlockSize() const;
size_t evalRowSize(uint32_t level) const;
size_t evalFaceSize(uint32_t level) const;
size_t evalImageSize(uint32_t level) const;

View file

@ -155,7 +155,7 @@ namespace ktx {
ptr++;
#ifdef DEBUG
for (size_t k = 0; k < descriptors[i]._imageSize/4; k++) {
*(ptr + k) = 0xFFFF00FF;
*(ptr + k) = 0xFFFFFFFF;
}
#endif
currentDestPtr += descriptors[i]._imageSize + sizeof(uint32_t);

View file

@ -245,7 +245,10 @@ gpu::TexturePointer getFallbackTextureForType(image::TextureUsage::Type type) {
}
NetworkTexture::~NetworkTexture() {
_textureSource->getGPUTexture()->unregisterMipInterestListener(this);
auto texture = _textureSource->getGPUTexture();
if (texture) {
texture->unregisterMipInterestListener(this);
}
}
/// Returns a texture version of an image file
@ -412,7 +415,6 @@ void NetworkTexture::startMipRangeRequest(uint16_t low, uint16_t high) {
range.fromInclusive = -15000;
_ktxMipRequest->setByteRange(range);
} else {
// TODO: Discover range for other mips
ByteRange range;
range.fromInclusive = ktx::KTX_HEADER_SIZE + _originalKtxDescriptor->header.bytesOfKeyValueData
+ _originalKtxDescriptor->images[low]._imageOffset + 4;
@ -431,6 +433,7 @@ void NetworkTexture::startMipRangeRequest(uint16_t low, uint16_t high) {
void NetworkTexture::ktxHeaderRequestFinished() {
assert(!_ktxHeaderLoaded);
_headerRequestFinished = true;
if (_ktxHeaderRequest->getResult() == ResourceRequest::Success) {
_ktxHeaderLoaded = true;
_ktxHeaderData = _ktxHeaderRequest->getData();
@ -447,7 +450,7 @@ void NetworkTexture::ktxMipRequestFinished() {
&& _ktxMipLevelRangeInFlight.second == NULL_MIP_LEVEL;
if (_ktxMipRequest->getResult() == ResourceRequest::Success) {
if (_initialKtxLoaded) {
if (_highMipRequestFinished) {
assert(_ktxMipLevelRangeInFlight.second - _ktxMipLevelRangeInFlight.first == 0);
_lowestKnownPopulatedMip = _ktxMipLevelRangeInFlight.first;
@ -456,6 +459,7 @@ void NetworkTexture::ktxMipRequestFinished() {
_ktxMipRequest->getData().size(), reinterpret_cast<uint8_t*>(_ktxMipRequest->getData().data()));
//texture->assignStoredMip(level, image._imageSize, ktxData);
} else {
_highMipRequestFinished = true;
_ktxHighMipData = _ktxMipRequest->getData();
maybeCreateKTX();
}
@ -469,118 +473,144 @@ void NetworkTexture::ktxMipRequestFinished() {
// This is called when the header or top mips have been loaded
void NetworkTexture::maybeCreateKTX() {
if (_ktxHeaderData.size() > 0 && _ktxHighMipData.size() > 0) {
// create ktx...
auto header = reinterpret_cast<const ktx::Header*>(_ktxHeaderData.data());
if (_headerRequestFinished && _highMipRequestFinished) {
ResourceCache::requestCompleted(_self);
if (_ktxHeaderData.size() > 0 && _ktxHighMipData.size() > 0) {
// create ktx...
auto header = reinterpret_cast<const ktx::Header*>(_ktxHeaderData.data());
qDebug() << "Creating KTX";
qDebug() << "Identifier:" << QString(QByteArray((char*)header->identifier, 12));
qDebug() << "Type:" << header->glType;
qDebug() << "TypeSize:" << header->glTypeSize;
qDebug() << "numberOfArrayElements:" << header->numberOfArrayElements;
qDebug() << "numberOfFaces:" << header->numberOfFaces;
qDebug() << "numberOfMipmapLevels:" << header->numberOfMipmapLevels;
auto kvSize = header->bytesOfKeyValueData;
if (kvSize > _ktxHeaderData.size() - ktx::KTX_HEADER_SIZE) {
qWarning() << "Cannot load " << _url << ", did not receive all kv data with initial request";
return;
}
auto keyValues = ktx::KTX::parseKeyValues(header->bytesOfKeyValueData, reinterpret_cast<const ktx::Byte*>(_ktxHeaderData.data()) + ktx::KTX_HEADER_SIZE);
auto imageDescriptors = header->generateImageDescriptors();
_originalKtxDescriptor.reset(new ktx::KTXDescriptor(*header, keyValues, imageDescriptors));
// Create bare ktx in memory
auto found = std::find_if(keyValues.begin(), keyValues.end(), [](const ktx::KeyValue& val) -> bool {
return val._key.compare(gpu::SOURCE_HASH_KEY) == 0;
});
std::string filename;
if (found == keyValues.end()) {
qWarning("Source hash key not found, bailing");
filename = "test";
//return;
}
else {
if (found->_value.size() < 16) {
filename = _activeUrl.fileName().toStdString();
}
else {
filename = std::string(reinterpret_cast<char*>(found->_value.data()), 32);
}
}
auto memKtx = ktx::KTX::createBare(*header, keyValues);
auto d = const_cast<uint8_t*>(memKtx->getStorage()->data());
///memcpy(d + memKtx->_storage->size() - _ktxHighMipData.size(), _ktxHighMipData.data(), _ktxHighMipData.size());
auto textureCache = DependencyManager::get<TextureCache>();
// Move ktx to file
const char* data = reinterpret_cast<const char*>(memKtx->_storage->data());
size_t length = memKtx->_storage->size();
KTXFilePointer file;
auto& ktxCache = textureCache->_ktxCache;
if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(filename, length)))) {
qCWarning(modelnetworking) << _url << "file cache failed";
return;
}
else {
_file = file;
}
//_ktxDescriptor.reset(new ktx::KTXDescriptor(memKtx->toDescriptor()));
auto newKtxDescriptor = memKtx->toDescriptor();
//auto texture = gpu::Texture::serializeHeader("test.ktx", *header, keyValues);
gpu::TexturePointer texture;
texture.reset(gpu::Texture::unserialize(_file->getFilepath(), newKtxDescriptor));
texture->setKtxBacking(file->getFilepath());
texture->setSource(filename);
texture->registerMipInterestListener(this);
auto& images = _originalKtxDescriptor->images;
size_t imageSizeRemaining = _ktxHighMipData.size();
uint8_t* ktxData = reinterpret_cast<uint8_t*>(_ktxHighMipData.data());
ktxData += _ktxHighMipData.size();
// TODO Move image offset calculation to ktx ImageDescriptor
int level;
for (level = images.size() - 1; level >= 0; --level) {
auto& image = images[level];
if (image._imageSize > imageSizeRemaining) {
break;
}
qDebug() << "Transferring " << level;
ktxData -= image._imageSize;
texture->assignStoredMip(level, image._imageSize, ktxData);
ktxData -= 4;
imageSizeRemaining - image._imageSize - 4;
}
// We replace the texture with the one stored in the cache. This deals with the possible race condition of two different
// images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will
// be the winner
if (textureCache) {
texture = textureCache->cacheTextureByHash(filename, texture);
}
_lowestKnownPopulatedMip = _originalKtxDescriptor->header.numberOfMipmapLevels;
for (uint16_t l = 0; l < 200; l++) {
if (texture->isStoredMipFaceAvailable(l)) {
_lowestKnownPopulatedMip = l;
break;
}
}
ResourceCache::requestCompleted(_self);
setImage(texture, header->getPixelWidth(), header->getPixelHeight());
qDebug() << "Identifier:" << QString(QByteArray((char*)header->identifier, 12));
qDebug() << "Type:" << header->glType;
qDebug() << "TypeSize:" << header->glTypeSize;
qDebug() << "numberOfArrayElements:" << header->numberOfArrayElements;
qDebug() << "numberOfFaces:" << header->numberOfFaces;
qDebug() << "numberOfMipmapLevels:" << header->numberOfMipmapLevels;
auto kvSize = header->bytesOfKeyValueData;
if (kvSize > _ktxHeaderData.size() - ktx::KTX_HEADER_SIZE) {
qWarning() << "Cannot load " << _url << ", did not receive all kv data with initial request";
return;
}
auto keyValues = ktx::KTX::parseKeyValues(header->bytesOfKeyValueData, reinterpret_cast<const ktx::Byte*>(_ktxHeaderData.data()) + ktx::KTX_HEADER_SIZE);
auto imageDescriptors = header->generateImageDescriptors();
_originalKtxDescriptor.reset(new ktx::KTXDescriptor(*header, keyValues, imageDescriptors));
// Create bare ktx in memory
std::string filename = "test";
auto memKtx = ktx::KTX::createBare(*header, keyValues);
auto d = const_cast<uint8_t*>(memKtx->getStorage()->data());
///memcpy(d + memKtx->_storage->size() - _ktxHighMipData.size(), _ktxHighMipData.data(), _ktxHighMipData.size());
auto textureCache = DependencyManager::get<TextureCache>();
// Move ktx to file
const char* data = reinterpret_cast<const char*>(memKtx->_storage->data());
size_t length = memKtx->_storage->size();
KTXFilePointer file;
auto& ktxCache = textureCache->_ktxCache;
if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(filename, length)))) {
qCWarning(modelnetworking) << _url << "file cache failed";
} else {
_file = file;
}
//_ktxDescriptor.reset(new ktx::KTXDescriptor(memKtx->toDescriptor()));
auto newKtxDescriptor = memKtx->toDescriptor();
//auto texture = gpu::Texture::serializeHeader("test.ktx", *header, keyValues);
gpu::TexturePointer texture;
texture.reset(gpu::Texture::unserialize(_file->getFilepath(), newKtxDescriptor));
texture->setKtxBacking(file->getFilepath());
texture->setSource(filename);
texture->registerMipInterestListener(this);
auto& images = _originalKtxDescriptor->images;
size_t imageSizeRemaining = _ktxHighMipData.size();
uint8_t* ktxData = reinterpret_cast<uint8_t*>(_ktxHighMipData.data());
ktxData += _ktxHighMipData.size();
// TODO Move image offset calculation to ktx ImageDescriptor
uint16_t level;
for (level = images.size() - 1; level >= 0; --level) {
auto& image = images[level];
if (image._imageSize > imageSizeRemaining) {
break;
/*
// Force load the next two levels
{
QTimer* timer = new QTimer();
connect(timer, &QTimer::timeout, this, [=]() {
//startMipRangeRequest(level, level);
startRequestForNextMipLevel();
});
timer->setSingleShot(true);
timer->setInterval(4000);
timer->start();
}
qDebug() << "Transferring " << level;
ktxData -= image._imageSize;
texture->assignStoredMip(level, image._imageSize, ktxData);
ktxData -= 4;
imageSizeRemaining - image._imageSize - 4;
}
_initialKtxLoaded = true;
// We replace the texture with the one stored in the cache. This deals with the possible race condition of two different
// images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will
// be the winner
if (textureCache) {
texture = textureCache->cacheTextureByHash(filename, texture);
}
_lowestKnownPopulatedMip = _originalKtxDescriptor->header.numberOfMipmapLevels;
for (uint16_t l = 0; l < 200; l++) {
if (texture->isStoredMipFaceAvailable(l)) {
_lowestKnownPopulatedMip = l;
break;
{
QTimer* timer = new QTimer();
connect(timer, &QTimer::timeout, this, [=]() {
//startMipRangeRequest(level - 1, level - 1);
startRequestForNextMipLevel();
});
timer->setSingleShot(true);
timer->setInterval(6000);
timer->start();
}
}
setImage(texture, header->getPixelWidth(), header->getPixelHeight());
return;
// Force load the next two levels
{
QTimer* timer = new QTimer();
connect(timer, &QTimer::timeout, this, [=]() {
//startMipRangeRequest(level, level);
startRequestForNextMipLevel();
});
timer->setSingleShot(true);
timer->setInterval(4000);
timer->start();
}
{
QTimer* timer = new QTimer();
connect(timer, &QTimer::timeout, this, [=]() {
//startMipRangeRequest(level - 1, level - 1);
startRequestForNextMipLevel();
});
timer->setSingleShot(true);
timer->setInterval(6000);
timer->start();
*/
}
}
}

View file

@ -46,6 +46,7 @@ class NetworkTexture : public Resource, public Texture, public gpu::Texture::Mip
public:
NetworkTexture(const QUrl& url, image::TextureUsage::Type type, const QByteArray& content, int maxNumPixels);
NetworkTexture::~NetworkTexture() override;
QString getType() const override { return "NetworkTexture"; }
@ -91,13 +92,6 @@ private:
image::TextureUsage::Type _type;
enum KTXLoadState {
LOADING_HEADER,
LOADING_LOWEST_SIX,
DONE_LOADING
};
bool _initialKtxLoaded { false };
KTXFilePointer _file;
static const uint16_t NULL_MIP_LEVEL;
bool _sourceIsKTX { false };
@ -105,6 +99,8 @@ private:
std::pair<uint16_t, uint16_t> _ktxMipLevelRangeInFlight{ NULL_MIP_LEVEL, NULL_MIP_LEVEL };
ResourceRequest* _ktxHeaderRequest { nullptr };
ResourceRequest* _ktxMipRequest { nullptr };
bool _headerRequestFinished{ false };
bool _highMipRequestFinished{ false };
uint16_t _lowestRequestedMipLevel { NULL_MIP_LEVEL };
uint16_t _lowestKnownPopulatedMip { NULL_MIP_LEVEL };
QByteArray _ktxHeaderData;

View file

@ -70,7 +70,7 @@ StoragePointer FileStorage::create(const QString& filename, size_t size, const u
// Represents a memory mapped file
FileStorage::FileStorage(const QString& filename) : _file(filename) {
if (_file.open(QFile::ReadWrite)) {
qDebug() << ">>> Opening mmapped file: " << filename;
//qDebug() << ">>> Opening mmapped file: " << filename;
_mapped = _file.map(0, _file.size());
if (_mapped) {
_valid = true;
@ -83,7 +83,7 @@ FileStorage::FileStorage(const QString& filename) : _file(filename) {
}
FileStorage::~FileStorage() {
qDebug() << ">>> Closing mmapped file: " << _file.fileName();
//qDebug() << ">>> Closing mmapped file: " << _file.fileName();
if (_mapped) {
if (!_file.unmap(_mapped)) {
throw std::runtime_error("Unable to unmap file");