mirror of
https://github.com/Armored-Dragon/overte.git
synced 2025-03-11 16:13:16 +01:00
Add extended ktx header/high-mip request handling to NetworkTexture
This commit is contained in:
parent
1fec531c68
commit
ccd9c4697b
9 changed files with 257 additions and 47 deletions
|
@ -626,7 +626,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
proxy.setType(QNetworkProxy::HttpProxy);
|
||||
proxy.setHostName("127.0.0.1");
|
||||
proxy.setPort(8888);
|
||||
//QNetworkProxy::setApplicationProxy(proxy);
|
||||
QNetworkProxy::setApplicationProxy(proxy);
|
||||
|
||||
// make sure the debug draw singleton is initialized on the main thread.
|
||||
DebugDraw::getInstance().removeMarker("");
|
||||
|
|
|
@ -270,7 +270,6 @@ public:
|
|||
virtual void reset() = 0;
|
||||
virtual PixelsPointer getMipFace(uint16 level, uint8 face = 0) const = 0;
|
||||
virtual Size getMipFaceSize(uint16 level, uint8 face = 0) const = 0;
|
||||
virtual void assignMipData(uint16 level, const char* data, const size_t length) = 0;
|
||||
virtual void assignMipData(uint16 level, const storage::StoragePointer& storage) = 0;
|
||||
virtual void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) = 0;
|
||||
virtual bool isMipAvailable(uint16 level, uint8 face = 0) const = 0;
|
||||
|
@ -297,7 +296,6 @@ public:
|
|||
void reset() override;
|
||||
PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override;
|
||||
Size getMipFaceSize(uint16 level, uint8 face = 0) const override;
|
||||
void assignMipData(uint16 level, const char* data, const size_t length) override;
|
||||
void assignMipData(uint16 level, const storage::StoragePointer& storage) override;
|
||||
void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override;
|
||||
bool isMipAvailable(uint16 level, uint8 face = 0) const override;
|
||||
|
@ -313,15 +311,12 @@ public:
|
|||
PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override;
|
||||
Size getMipFaceSize(uint16 level, uint8 face = 0) const override;
|
||||
// By convention, all mip levels and faces MUST be populated when using KTX backing
|
||||
bool isMipAvailable(uint16 level, uint8 face = 0) const override { return true; }
|
||||
bool isMipAvailable(uint16 level, uint8 face = 0) const override;
|
||||
|
||||
void assignMipData(uint16 level, const storage::StoragePointer& storage) override {
|
||||
throw std::runtime_error("Invalid call");
|
||||
}
|
||||
void assignMipData(uint16 level, const storage::StoragePointer& storage) override;
|
||||
|
||||
void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override;
|
||||
|
||||
void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override {
|
||||
throw std::runtime_error("Invalid call");
|
||||
}
|
||||
void reset() override { }
|
||||
|
||||
protected:
|
||||
|
|
|
@ -72,6 +72,25 @@ Size KtxStorage::getMipFaceSize(uint16 level, uint8 face) const {
|
|||
return _ktxDescriptor->getMipFaceTexelsSize(level, face);
|
||||
}
|
||||
|
||||
|
||||
bool KtxStorage::isMipAvailable(uint16 level, uint8 face) const {
|
||||
auto numLevels = _ktxDescriptor->header.numberOfMipmapLevels;
|
||||
auto minLevel = 7 > numLevels ? 0 : numLevels - 7;
|
||||
auto avail = level >= minLevel;
|
||||
qDebug() << "isMipAvailable: " << level << " " << face << avail << minLevel << " " << _ktxDescriptor->header.numberOfMipmapLevels;
|
||||
//return true;
|
||||
return level > _ktxDescriptor->header.numberOfMipmapLevels - 7;
|
||||
}
|
||||
|
||||
void KtxStorage::assignMipData(uint16 level, const storage::StoragePointer& storage) {
|
||||
throw std::runtime_error("Invalid call");
|
||||
}
|
||||
|
||||
void KtxStorage::assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) {
|
||||
throw std::runtime_error("Invalid call");
|
||||
}
|
||||
|
||||
|
||||
void Texture::setKtxBacking(const std::string& filename) {
|
||||
// Check the KTX file for validity before using it as backing storage
|
||||
{
|
||||
|
@ -86,6 +105,7 @@ void Texture::setKtxBacking(const std::string& filename) {
|
|||
setStorage(newBacking);
|
||||
}
|
||||
|
||||
|
||||
ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
|
||||
ktx::Header header;
|
||||
|
||||
|
|
|
@ -330,6 +330,166 @@ private:
|
|||
int _maxNumPixels;
|
||||
};
|
||||
|
||||
const uint16_t NetworkTexture::NULL_MIP_LEVEL = std::numeric_limits<uint16_t>::max();
|
||||
void NetworkTexture::makeRequest() {
|
||||
if (!_sourceIsKTX) {
|
||||
Resource::makeRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
// We special-handle ktx requests to run 2 concurrent requests right off the bat
|
||||
PROFILE_ASYNC_BEGIN(resource, "Resource:" + getType(), QString::number(_requestID), { { "url", _url.toString() }, { "activeURL", _activeUrl.toString() } });
|
||||
|
||||
if (!_ktxHeaderLoaded) {
|
||||
qDebug() << ">>> Making request to " << _url << " for header";
|
||||
_ktxHeaderRequest = ResourceManager::createResourceRequest(this, _activeUrl);
|
||||
|
||||
if (!_ktxHeaderRequest) {
|
||||
//qCDebug(networking).noquote() << "Failed to get request for" << _url.toDisplayString();
|
||||
ResourceCache::requestCompleted(_self);
|
||||
finishedLoading(false);
|
||||
PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID));
|
||||
return;
|
||||
}
|
||||
|
||||
ByteRange range;
|
||||
range.fromInclusive = 0;
|
||||
range.toExclusive = 1000;
|
||||
_ktxHeaderRequest->setByteRange(range);
|
||||
|
||||
//qCDebug(resourceLog).noquote() << "Starting request for:" << _url.toDisplayString();
|
||||
emit loading();
|
||||
|
||||
connect(_ktxHeaderRequest, &ResourceRequest::progress, this, &NetworkTexture::ktxHeaderRequestProgress);
|
||||
//connect(this, &Resource::onProgress, this, &NetworkTexture::ktxHeaderRequestFinished);
|
||||
|
||||
connect(_ktxHeaderRequest, &ResourceRequest::finished, this, &NetworkTexture::ktxHeaderRequestFinished);
|
||||
|
||||
_bytesReceived = _bytesTotal = _bytes = 0;
|
||||
|
||||
_ktxHeaderRequest->send();
|
||||
}
|
||||
|
||||
startMipRangeRequest(NULL_MIP_LEVEL, NULL_MIP_LEVEL);
|
||||
}
|
||||
|
||||
// Load mips in the range [low, high] (inclusive)
|
||||
void NetworkTexture::startMipRangeRequest(uint16_t low, uint16_t high) {
|
||||
if (_ktxMipRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool isHighMipRequest = low == NULL_MIP_LEVEL && high == NULL_MIP_LEVEL;
|
||||
|
||||
if (!isHighMipRequest && !_ktxHeaderLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ktxMipRequest = ResourceManager::createResourceRequest(this, _activeUrl);
|
||||
qDebug() << ">>> Making request to " << _url << " for " << low << " to " << high;
|
||||
|
||||
if (isHighMipRequest) {
|
||||
// This is a special case where we load the high 7 mips
|
||||
ByteRange range;
|
||||
range.fromInclusive = -15000;
|
||||
_ktxMipRequest->setByteRange(range);
|
||||
} else {
|
||||
// TODO: Discover range for other mips
|
||||
}
|
||||
|
||||
connect(_ktxMipRequest, &ResourceRequest::progress, this, &NetworkTexture::ktxMipRequestProgress);
|
||||
connect(_ktxMipRequest, &ResourceRequest::finished, this, &NetworkTexture::ktxMipRequestFinished);
|
||||
|
||||
_ktxMipRequest->send();
|
||||
}
|
||||
|
||||
|
||||
void NetworkTexture::ktxHeaderRequestFinished() {
|
||||
assert(!_ktxHeaderLoaded);
|
||||
|
||||
if (_ktxHeaderRequest->getResult() == ResourceRequest::Success) {
|
||||
_ktxHeaderLoaded = true;
|
||||
_ktxHeaderData = _ktxHeaderRequest->getData();
|
||||
maybeCreateKTX();
|
||||
} else {
|
||||
handleFailedRequest(_ktxHeaderRequest->getResult());
|
||||
}
|
||||
_ktxHeaderRequest->deleteLater();
|
||||
_ktxHeaderRequest = nullptr;
|
||||
}
|
||||
|
||||
void NetworkTexture::ktxMipRequestFinished() {
|
||||
bool isHighMipRequest = _ktxMipLevelRangeInFlight.first == NULL_MIP_LEVEL
|
||||
&& _ktxMipLevelRangeInFlight.second == NULL_MIP_LEVEL;
|
||||
|
||||
if (_ktxMipRequest->getResult() == ResourceRequest::Success) {
|
||||
_ktxHighMipData = _ktxMipRequest->getData();
|
||||
maybeCreateKTX();
|
||||
} else {
|
||||
handleFailedRequest(_ktxHeaderRequest->getResult());
|
||||
}
|
||||
_ktxMipRequest->deleteLater();
|
||||
_ktxMipRequest = nullptr;
|
||||
}
|
||||
|
||||
// This is called when the header or top mips have been loaded
|
||||
void NetworkTexture::maybeCreateKTX() {
|
||||
qDebug() << "Maybe create ktx...";
|
||||
if (_ktxHeaderData.size() > 0 && _ktxHighMipData.size() > 0) {
|
||||
// create ktx...
|
||||
auto header = reinterpret_cast<const ktx::Header*>(_ktxHeaderData.data());
|
||||
|
||||
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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
//auto texture = gpu::Texture::serializeHeader("test.ktx", *header, keyValues);
|
||||
gpu::TexturePointer texture;
|
||||
texture.reset(gpu::Texture::unserialize(_file->getFilepath(), memKtx->toDescriptor()));
|
||||
texture->setKtxBacking(file->getFilepath());
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
setImage(texture, header->getPixelWidth(), header->getPixelHeight());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkTexture::downloadFinished(const QByteArray& data) {
|
||||
loadContent(data);
|
||||
}
|
||||
|
|
|
@ -59,7 +59,16 @@ public:
|
|||
signals:
|
||||
void networkTextureCreated(const QWeakPointer<NetworkTexture>& self);
|
||||
|
||||
public slots:
|
||||
void ktxHeaderRequestProgress(uint64_t bytesReceived, uint64_t bytesTotal) { }
|
||||
void ktxHeaderRequestFinished();
|
||||
|
||||
void ktxMipRequestProgress(uint64_t bytesReceived, uint64_t bytesTotal) { }
|
||||
void ktxMipRequestFinished();
|
||||
|
||||
protected:
|
||||
void makeRequest() override;
|
||||
|
||||
virtual bool isCacheable() const override { return _loaded; }
|
||||
|
||||
virtual void downloadFinished(const QByteArray& data) override;
|
||||
|
@ -67,6 +76,9 @@ protected:
|
|||
Q_INVOKABLE void loadContent(const QByteArray& content);
|
||||
Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight);
|
||||
|
||||
void startMipRangeRequest(uint16_t low, uint16_t high);
|
||||
void maybeCreateKTX();
|
||||
|
||||
private:
|
||||
friend class KTXReader;
|
||||
friend class ImageReader;
|
||||
|
@ -79,9 +91,18 @@ private:
|
|||
DONE_LOADING
|
||||
};
|
||||
|
||||
|
||||
KTXLoadState _ktxLoadState { LOADING_HEADER };
|
||||
KTXFilePointer _file;
|
||||
static const uint16_t NULL_MIP_LEVEL;
|
||||
bool _sourceIsKTX { false };
|
||||
bool _ktxHeaderLoaded { false };
|
||||
std::pair<uint16_t, uint16_t> _ktxMipLevelRangeInFlight{ NULL_MIP_LEVEL, NULL_MIP_LEVEL };
|
||||
ResourceRequest* _ktxHeaderRequest { nullptr };
|
||||
ResourceRequest* _ktxMipRequest { nullptr };
|
||||
QByteArray _ktxHeaderData;
|
||||
QByteArray _ktxHighMipData;
|
||||
|
||||
int _originalWidth { 0 };
|
||||
int _originalHeight { 0 };
|
||||
int _width { 0 };
|
||||
|
|
|
@ -60,7 +60,12 @@ void HTTPResourceRequest::doSend() {
|
|||
}
|
||||
|
||||
if (_byteRange.isSet()) {
|
||||
auto byteRange = QString("bytes=%1-%2").arg(_byteRange.fromInclusive).arg(_byteRange.toExclusive);
|
||||
QString byteRange;
|
||||
if (_byteRange.fromInclusive < 0) {
|
||||
auto byteRange = QString("bytes=%1").arg(_byteRange.fromInclusive);
|
||||
} else {
|
||||
auto byteRange = QString("bytes=%1-%2").arg(_byteRange.fromInclusive).arg(_byteRange.toExclusive);
|
||||
}
|
||||
networkRequest.setRawHeader("Range", byteRange.toLatin1());
|
||||
}
|
||||
networkRequest.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
|
||||
|
|
|
@ -725,34 +725,7 @@ void Resource::handleReplyFinished() {
|
|||
emit loaded(data);
|
||||
downloadFinished(data);
|
||||
} else {
|
||||
switch (result) {
|
||||
case ResourceRequest::Result::Timeout: {
|
||||
qCDebug(networking) << "Timed out loading" << _url << "received" << _bytesReceived << "total" << _bytesTotal;
|
||||
// Fall through to other cases
|
||||
}
|
||||
case ResourceRequest::Result::ServerUnavailable: {
|
||||
// retry with increasing delays
|
||||
const int BASE_DELAY_MS = 1000;
|
||||
if (_attempts++ < MAX_ATTEMPTS) {
|
||||
auto waitTime = BASE_DELAY_MS * (int)pow(2.0, _attempts);
|
||||
|
||||
qCDebug(networking).noquote() << "Server unavailable for" << _url << "- may retry in" << waitTime << "ms"
|
||||
<< "if resource is still needed";
|
||||
|
||||
QTimer::singleShot(waitTime, this, &Resource::attemptRequest);
|
||||
break;
|
||||
}
|
||||
// fall through to final failure
|
||||
}
|
||||
default: {
|
||||
qCDebug(networking) << "Error loading " << _url;
|
||||
auto error = (result == ResourceRequest::Timeout) ? QNetworkReply::TimeoutError
|
||||
: QNetworkReply::UnknownNetworkError;
|
||||
emit failed(error);
|
||||
finishedLoading(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
handleFailedRequest(result);
|
||||
}
|
||||
|
||||
_request->disconnect(this);
|
||||
|
@ -760,6 +733,37 @@ void Resource::handleReplyFinished() {
|
|||
_request = nullptr;
|
||||
}
|
||||
|
||||
void Resource::handleFailedRequest(ResourceRequest::Result result) {
|
||||
switch (result) {
|
||||
case ResourceRequest::Result::Timeout: {
|
||||
qCDebug(networking) << "Timed out loading" << _url << "received" << _bytesReceived << "total" << _bytesTotal;
|
||||
// Fall through to other cases
|
||||
}
|
||||
case ResourceRequest::Result::ServerUnavailable: {
|
||||
// retry with increasing delays
|
||||
const int BASE_DELAY_MS = 1000;
|
||||
if (_attempts++ < MAX_ATTEMPTS) {
|
||||
auto waitTime = BASE_DELAY_MS * (int)pow(2.0, _attempts);
|
||||
|
||||
qCDebug(networking).noquote() << "Server unavailable for" << _url << "- may retry in" << waitTime << "ms"
|
||||
<< "if resource is still needed";
|
||||
|
||||
QTimer::singleShot(waitTime, this, &Resource::attemptRequest);
|
||||
break;
|
||||
}
|
||||
// fall through to final failure
|
||||
}
|
||||
default: {
|
||||
qCDebug(networking) << "Error loading " << _url;
|
||||
auto error = (result == ResourceRequest::Timeout) ? QNetworkReply::TimeoutError
|
||||
: QNetworkReply::UnknownNetworkError;
|
||||
emit failed(error);
|
||||
finishedLoading(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint qHash(const QPointer<QObject>& value, uint seed) {
|
||||
return qHash(value.data(), seed);
|
||||
}
|
||||
|
|
|
@ -424,6 +424,8 @@ protected slots:
|
|||
protected:
|
||||
virtual void init();
|
||||
|
||||
virtual void makeRequest();
|
||||
|
||||
/// Checks whether the resource is cacheable.
|
||||
virtual bool isCacheable() const { return true; }
|
||||
|
||||
|
@ -440,6 +442,8 @@ protected:
|
|||
|
||||
Q_INVOKABLE void allReferencesCleared();
|
||||
|
||||
void handleFailedRequest(ResourceRequest::Result result);
|
||||
|
||||
QUrl _url;
|
||||
QUrl _activeUrl;
|
||||
ByteRange _requestByteRange;
|
||||
|
@ -449,8 +453,15 @@ protected:
|
|||
QHash<QPointer<QObject>, float> _loadPriorities;
|
||||
QWeakPointer<Resource> _self;
|
||||
QPointer<ResourceCache> _cache;
|
||||
|
||||
qint64 _bytesReceived{ 0 };
|
||||
qint64 _bytesTotal{ 0 };
|
||||
qint64 _bytes{ 0 };
|
||||
|
||||
int _requestID;
|
||||
ResourceRequest* _request{ nullptr };
|
||||
|
||||
private slots:
|
||||
public slots:
|
||||
void handleDownloadProgress(uint64_t bytesReceived, uint64_t bytesTotal);
|
||||
void handleReplyFinished();
|
||||
|
||||
|
@ -460,20 +471,14 @@ private:
|
|||
|
||||
void setLRUKey(int lruKey) { _lruKey = lruKey; }
|
||||
|
||||
void makeRequest();
|
||||
void retry();
|
||||
void reinsert();
|
||||
|
||||
bool isInScript() const { return _isInScript; }
|
||||
void setInScript(bool isInScript) { _isInScript = isInScript; }
|
||||
|
||||
int _requestID;
|
||||
ResourceRequest* _request{ nullptr };
|
||||
int _lruKey{ 0 };
|
||||
QTimer* _replyTimer{ nullptr };
|
||||
qint64 _bytesReceived{ 0 };
|
||||
qint64 _bytesTotal{ 0 };
|
||||
qint64 _bytes{ 0 };
|
||||
int _attempts{ 0 };
|
||||
bool _isInScript{ false };
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ struct ByteRange {
|
|||
int64_t fromInclusive { 0 };
|
||||
int64_t toExclusive { 0 };
|
||||
|
||||
bool isSet() { return fromInclusive < -1 || fromInclusive < toExclusive; }
|
||||
bool isSet() { return fromInclusive < 0 || fromInclusive < toExclusive; }
|
||||
};
|
||||
|
||||
class ResourceRequest : public QObject {
|
||||
|
|
Loading…
Reference in a new issue