diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 614f7bd9fe..bad60643ec 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -663,14 +663,6 @@ void MyAvatar::restoreRoleAnimation(const QString& role) { _rig->restoreRoleAnimation(role); } -void MyAvatar::prefetchAnimation(const QString& url) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "prefetchAnimation", Q_ARG(const QString&, url)); - return; - } - _rig->prefetchAnimation(url); -} - void MyAvatar::saveData() { Settings settings; settings.beginGroup("Avatar"); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e320c0e3de..fee1a9add3 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -143,9 +143,6 @@ public: // remove an animation role override and return to the standard animation. Q_INVOKABLE void restoreRoleAnimation(const QString& role); - // prefetch animation - Q_INVOKABLE void prefetchAnimation(const QString& url); - // Adds handler(animStateDictionaryIn) => animStateDictionaryOut, which will be invoked just before each animGraph state update. // The handler will be called with an animStateDictionaryIn that has all those properties specified by the (possibly empty) // propertiesList argument. However for debugging, if the properties argument is null, all internal animGraph state is provided. diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index b3f5e30d40..a7115199a2 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -152,14 +152,6 @@ void Rig::restoreRoleAnimation(const QString& role) { } } -void Rig::prefetchAnimation(const QString& url) { - - // This will begin loading the NetworkGeometry for the given URL. - // which should speed us up if we request it later via overrideAnimation. - auto clipNode = std::make_shared<AnimClip>("prefetch", url, 0, 0, 1.0, false, false); - _prefetchedAnimations.push_back(clipNode); -} - void Rig::destroyAnimGraph() { _animSkeleton.reset(); _animLoader.reset(); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 0ce6f6639e..363006d48c 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -94,7 +94,6 @@ public: QStringList getAnimationRoles() const; void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void restoreRoleAnimation(const QString& role); - void prefetchAnimation(const QString& url); void initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOffset); void reset(const FBXGeometry& geometry); @@ -319,7 +318,6 @@ protected: SimpleMovingAverage _averageLateralSpeed { 10 }; std::map<QString, AnimNode::Pointer> _origRoleAnimations; - std::vector<AnimNode::Pointer> _prefetchedAnimations; bool _lastEnableInverseKinematics { true }; bool _enableInverseKinematics { true }; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 8e46521bfc..013385a169 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -380,7 +380,7 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt _pendingAmbientTexture = false; _ambientTexture.clear(); } else { - _ambientTexture = textureCache->getTexture(zone->getKeyLightProperties().getAmbientURL(), CUBE_TEXTURE); + _ambientTexture = textureCache->getTexture(zone->getKeyLightProperties().getAmbientURL(), NetworkTexture::CUBE_TEXTURE); _pendingAmbientTexture = true; if (_ambientTexture && _ambientTexture->isLoaded()) { @@ -410,7 +410,7 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt _skyboxTexture.clear(); } else { // Update the Texture of the Skybox with the one pointed by this zone - _skyboxTexture = textureCache->getTexture(zone->getSkyboxProperties().getURL(), CUBE_TEXTURE); + _skyboxTexture = textureCache->getTexture(zone->getSkyboxProperties().getURL(), NetworkTexture::CUBE_TEXTURE); _pendingSkyboxTexture = true; if (_skyboxTexture && _skyboxTexture->isLoaded()) { diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 6dd1d97d7f..e4fb5c97f8 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -237,13 +237,14 @@ ModelCache::ModelCache() { QSharedPointer<Resource> ModelCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra) { - const GeometryExtra* geometryExtra = static_cast<const GeometryExtra*>(extra); - Resource* resource = nullptr; if (url.path().toLower().endsWith(".fst")) { resource = new GeometryMappingResource(url); } else { - resource = new GeometryDefinitionResource(url, geometryExtra->mapping, geometryExtra->textureBaseUrl); + const GeometryExtra* geometryExtra = static_cast<const GeometryExtra*>(extra); + auto mapping = geometryExtra ? geometryExtra->mapping : QVariantHash(); + auto textureBaseUrl = geometryExtra ? geometryExtra->textureBaseUrl : QUrl(); + resource = new GeometryDefinitionResource(url, mapping, textureBaseUrl); } return QSharedPointer<Resource>(resource, &Resource::deleter); @@ -424,7 +425,7 @@ NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textur { _textures = Textures(MapChannel::NUM_MAP_CHANNELS); if (!material.albedoTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, NetworkTexture::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); _albedoTransform = material.albedoTexture.transform; map->setTextureTransform(_albedoTransform); @@ -441,39 +442,39 @@ NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textur if (!material.normalTexture.filename.isEmpty()) { - auto type = (material.normalTexture.isBumpmap ? BUMP_TEXTURE : NORMAL_TEXTURE); + auto type = (material.normalTexture.isBumpmap ? NetworkTexture::BUMP_TEXTURE : NetworkTexture::NORMAL_TEXTURE); auto map = fetchTextureMap(textureBaseUrl, material.normalTexture, type, MapChannel::NORMAL_MAP); setTextureMap(MapChannel::NORMAL_MAP, map); } if (!material.roughnessTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.roughnessTexture, ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.roughnessTexture, NetworkTexture::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); setTextureMap(MapChannel::ROUGHNESS_MAP, map); } else if (!material.glossTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.glossTexture, GLOSS_TEXTURE, MapChannel::ROUGHNESS_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.glossTexture, NetworkTexture::GLOSS_TEXTURE, MapChannel::ROUGHNESS_MAP); setTextureMap(MapChannel::ROUGHNESS_MAP, map); } if (!material.metallicTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.metallicTexture, METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.metallicTexture, NetworkTexture::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); setTextureMap(MapChannel::METALLIC_MAP, map); } else if (!material.specularTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.specularTexture, SPECULAR_TEXTURE, MapChannel::METALLIC_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.specularTexture, NetworkTexture::SPECULAR_TEXTURE, MapChannel::METALLIC_MAP); setTextureMap(MapChannel::METALLIC_MAP, map); } if (!material.occlusionTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, NetworkTexture::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); setTextureMap(MapChannel::OCCLUSION_MAP, map); } if (!material.emissiveTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.emissiveTexture, EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.emissiveTexture, NetworkTexture::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); setTextureMap(MapChannel::EMISSIVE_MAP, map); } if (!material.lightmapTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, NetworkTexture::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); _lightmapTransform = material.lightmapTexture.transform; _lightmapParams = material.lightmapParams; map->setTextureTransform(_lightmapTransform); @@ -495,7 +496,7 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { if (!albedoName.isEmpty()) { auto url = textureMap.contains(albedoName) ? textureMap[albedoName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); + auto map = fetchTextureMap(url, NetworkTexture::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); map->setTextureTransform(_albedoTransform); // when reassigning the albedo texture we also check for the alpha channel used as opacity map->setUseAlphaChannel(true); @@ -504,39 +505,39 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { if (!normalName.isEmpty()) { auto url = textureMap.contains(normalName) ? textureMap[normalName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, NORMAL_TEXTURE, MapChannel::NORMAL_MAP); + auto map = fetchTextureMap(url, NetworkTexture::NORMAL_TEXTURE, MapChannel::NORMAL_MAP); setTextureMap(MapChannel::NORMAL_MAP, map); } if (!roughnessName.isEmpty()) { auto url = textureMap.contains(roughnessName) ? textureMap[roughnessName].toUrl() : QUrl(); // FIXME: If passing a gloss map instead of a roughmap how do we know? - auto map = fetchTextureMap(url, ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + auto map = fetchTextureMap(url, NetworkTexture::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); setTextureMap(MapChannel::ROUGHNESS_MAP, map); } if (!metallicName.isEmpty()) { auto url = textureMap.contains(metallicName) ? textureMap[metallicName].toUrl() : QUrl(); // FIXME: If passing a specular map instead of a metallic how do we know? - auto map = fetchTextureMap(url, METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + auto map = fetchTextureMap(url, NetworkTexture::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); setTextureMap(MapChannel::METALLIC_MAP, map); } if (!occlusionName.isEmpty()) { auto url = textureMap.contains(occlusionName) ? textureMap[occlusionName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + auto map = fetchTextureMap(url, NetworkTexture::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); setTextureMap(MapChannel::OCCLUSION_MAP, map); } if (!emissiveName.isEmpty()) { auto url = textureMap.contains(emissiveName) ? textureMap[emissiveName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + auto map = fetchTextureMap(url, NetworkTexture::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); setTextureMap(MapChannel::EMISSIVE_MAP, map); } if (!lightmapName.isEmpty()) { auto url = textureMap.contains(lightmapName) ? textureMap[lightmapName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + auto map = fetchTextureMap(url, NetworkTexture::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); map->setTextureTransform(_lightmapTransform); map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); setTextureMap(MapChannel::LIGHTMAP_MAP, map); diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 88e89a57a9..bf47f293e8 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -170,6 +170,8 @@ protected: const bool& isOriginal() const { return _isOriginal; } private: + using TextureType = NetworkTexture::Type; + // Helpers for the ctors QUrl getTextureUrl(const QUrl& baseUrl, const FBXTexture& fbxTexture); model::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture, diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 7d18151f2c..2aaddace88 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -35,6 +35,16 @@ TextureCache::TextureCache() { const qint64 TEXTURE_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE; setUnusedResourceCacheSize(TEXTURE_DEFAULT_UNUSED_MAX_SIZE); setObjectName("TextureCache"); + + // Expose enum Type to JS/QML via properties + // Despite being one-off, this should be fine, because TextureCache is a SINGLETON_DEPENDENCY + QObject* type = new QObject(this); + type->setObjectName("TextureType"); + setProperty("Type", QVariant::fromValue(type)); + auto metaEnum = QMetaEnum::fromType<Type>(); + for (int i = 0; i < metaEnum.keyCount(); ++i) { + type->setProperty(metaEnum.key(i), metaEnum.value(i)); + } } TextureCache::~TextureCache() { @@ -145,60 +155,68 @@ const gpu::TexturePointer& TextureCache::getNormalFittingTexture() { /// Extra data for creating textures. class TextureExtra { public: - TextureType type; + NetworkTexture::Type type; const QByteArray& content; }; -NetworkTexturePointer TextureCache::getTexture(const QUrl& url, TextureType type, const QByteArray& content) { +ScriptableResource* TextureCache::prefetch(const QUrl& url, int type) { + auto byteArray = QByteArray(); + TextureExtra extra = { (Type)type, byteArray }; + return ResourceCache::prefetch(url, &extra); +} + +NetworkTexturePointer TextureCache::getTexture(const QUrl& url, Type type, const QByteArray& content) { TextureExtra extra = { type, content }; return ResourceCache::getResource(url, QUrl(), content.isEmpty(), &extra).staticCast<NetworkTexture>(); } -TextureCache::TextureLoaderFunc getTextureLoaderForType(TextureType type) { +NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type) { + using Type = NetworkTexture; + switch (type) { - case ALBEDO_TEXTURE: { + case Type::ALBEDO_TEXTURE: { return model::TextureUsage::createAlbedoTextureFromImage; break; } - case EMISSIVE_TEXTURE: { + case Type::EMISSIVE_TEXTURE: { return model::TextureUsage::createEmissiveTextureFromImage; break; } - case LIGHTMAP_TEXTURE: { + case Type::LIGHTMAP_TEXTURE: { return model::TextureUsage::createLightmapTextureFromImage; break; } - case CUBE_TEXTURE: { + case Type::CUBE_TEXTURE: { return model::TextureUsage::createCubeTextureFromImage; break; } - case BUMP_TEXTURE: { + case Type::BUMP_TEXTURE: { return model::TextureUsage::createNormalTextureFromBumpImage; break; } - case NORMAL_TEXTURE: { + case Type::NORMAL_TEXTURE: { return model::TextureUsage::createNormalTextureFromNormalImage; break; } - case ROUGHNESS_TEXTURE: { + case Type::ROUGHNESS_TEXTURE: { return model::TextureUsage::createRoughnessTextureFromImage; break; } - case GLOSS_TEXTURE: { + case Type::GLOSS_TEXTURE: { return model::TextureUsage::createRoughnessTextureFromGlossImage; break; } - case SPECULAR_TEXTURE: { + case Type::SPECULAR_TEXTURE: { return model::TextureUsage::createMetallicTextureFromImage; break; } - case CUSTOM_TEXTURE: { + case Type::CUSTOM_TEXTURE: { Q_ASSERT(false); - return TextureCache::TextureLoaderFunc(); + return NetworkTexture::TextureLoaderFunc(); break; } - case DEFAULT_TEXTURE: + case Type::DEFAULT_TEXTURE: default: { return model::TextureUsage::create2DTextureFromImage; break; @@ -207,7 +225,7 @@ TextureCache::TextureLoaderFunc getTextureLoaderForType(TextureType type) { } /// Returns a texture version of an image file -gpu::TexturePointer TextureCache::getImageTexture(const QString& path, TextureType type) { +gpu::TexturePointer TextureCache::getImageTexture(const QString& path, Type type) { QImage image = QImage(path); auto loader = getTextureLoaderForType(type); return gpu::TexturePointer(loader(image, QUrl::fromLocalFile(path).fileName().toStdString())); @@ -216,11 +234,13 @@ gpu::TexturePointer TextureCache::getImageTexture(const QString& path, TextureTy QSharedPointer<Resource> TextureCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra) { const TextureExtra* textureExtra = static_cast<const TextureExtra*>(extra); - return QSharedPointer<Resource>(new NetworkTexture(url, textureExtra->type, textureExtra->content), + auto type = textureExtra ? textureExtra->type : Type::DEFAULT_TEXTURE; + auto content = textureExtra ? textureExtra->content : QByteArray(); + return QSharedPointer<Resource>(new NetworkTexture(url, type, content), &Resource::deleter); } -NetworkTexture::NetworkTexture(const QUrl& url, TextureType type, const QByteArray& content) : +NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& content) : Resource(url, !content.isEmpty()), _type(type) { diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index c614a7ceb3..8fd0b12369 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -17,6 +17,7 @@ #include <QImage> #include <QMap> #include <QColor> +#include <QMetaEnum> #include <DependencyManager.h> #include <ResourceCache.h> @@ -25,79 +26,6 @@ namespace gpu { class Batch; } -class NetworkTexture; - -typedef QSharedPointer<NetworkTexture> NetworkTexturePointer; - -enum TextureType { - DEFAULT_TEXTURE, - ALBEDO_TEXTURE, - NORMAL_TEXTURE, - BUMP_TEXTURE, - SPECULAR_TEXTURE, - METALLIC_TEXTURE = SPECULAR_TEXTURE, // for now spec and metallic texture are the same, converted to grey - ROUGHNESS_TEXTURE, - GLOSS_TEXTURE, - EMISSIVE_TEXTURE, - CUBE_TEXTURE, - OCCLUSION_TEXTURE, - LIGHTMAP_TEXTURE, - CUSTOM_TEXTURE -}; - -/// Stores cached textures, including render-to-texture targets. -class TextureCache : public ResourceCache, public Dependency { - Q_OBJECT - SINGLETON_DEPENDENCY - -public: - /// Returns the ID of the permutation/normal texture used for Perlin noise shader programs. This texture - /// has two lines: the first, a set of random numbers in [0, 255] to be used as permutation offsets, and - /// the second, a set of random unit vectors to be used as noise gradients. - const gpu::TexturePointer& getPermutationNormalTexture(); - - /// Returns an opaque white texture (useful for a default). - const gpu::TexturePointer& getWhiteTexture(); - - /// Returns an opaque gray texture (useful for a default). - const gpu::TexturePointer& getGrayTexture(); - - /// Returns the a pale blue texture (useful for a normal map). - const gpu::TexturePointer& getBlueTexture(); - - /// Returns the a black texture (useful for a default). - const gpu::TexturePointer& getBlackTexture(); - - // Returns a map used to compress the normals through a fitting scale algorithm - const gpu::TexturePointer& getNormalFittingTexture(); - - /// Returns a texture version of an image file - static gpu::TexturePointer getImageTexture(const QString& path, TextureType type = DEFAULT_TEXTURE); - - /// Loads a texture from the specified URL. - NetworkTexturePointer getTexture(const QUrl& url, TextureType type = DEFAULT_TEXTURE, - const QByteArray& content = QByteArray()); - - typedef gpu::Texture* TextureLoader(const QImage& image, const std::string& srcImageName); - - typedef std::function<TextureLoader> TextureLoaderFunc; -protected: - - virtual QSharedPointer<Resource> createResource(const QUrl& url, - const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra); - -private: - TextureCache(); - virtual ~TextureCache(); - friend class DilatableNetworkTexture; - - gpu::TexturePointer _permutationNormalTexture; - gpu::TexturePointer _whiteTexture; - gpu::TexturePointer _grayTexture; - gpu::TexturePointer _blueTexture; - gpu::TexturePointer _blackTexture; - gpu::TexturePointer _normalFittingTexture; -}; /// A simple object wrapper for an OpenGL texture. class Texture { @@ -107,15 +35,31 @@ public: }; /// A texture loaded from the network. - class NetworkTexture : public Resource, public Texture { Q_OBJECT public: - - typedef TextureCache::TextureLoaderFunc TextureLoaderFunc; - - NetworkTexture(const QUrl& url, TextureType type, const QByteArray& content); + enum Type { + DEFAULT_TEXTURE, + ALBEDO_TEXTURE, + NORMAL_TEXTURE, + BUMP_TEXTURE, + SPECULAR_TEXTURE, + METALLIC_TEXTURE = SPECULAR_TEXTURE, // for now spec and metallic texture are the same, converted to grey + ROUGHNESS_TEXTURE, + GLOSS_TEXTURE, + EMISSIVE_TEXTURE, + CUBE_TEXTURE, + OCCLUSION_TEXTURE, + LIGHTMAP_TEXTURE, + CUSTOM_TEXTURE + }; + Q_ENUM(Type) + + typedef gpu::Texture* TextureLoader(const QImage& image, const std::string& srcImageName); + using TextureLoaderFunc = std::function<TextureLoader>; + + NetworkTexture(const QUrl& url, Type type, const QByteArray& content); NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content); int getOriginalWidth() const { return _originalWidth; } @@ -138,12 +82,69 @@ protected: Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); private: - TextureType _type; - TextureLoaderFunc _textureLoader; + Type _type; + TextureLoaderFunc _textureLoader { [](const QImage&, const std::string&){ return nullptr; } }; int _originalWidth { 0 }; int _originalHeight { 0 }; int _width { 0 }; int _height { 0 }; }; +using NetworkTexturePointer = QSharedPointer<NetworkTexture>; + +/// Stores cached textures, including render-to-texture targets. +class TextureCache : public ResourceCache, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + + using Type = NetworkTexture::Type; + +public: + /// Returns the ID of the permutation/normal texture used for Perlin noise shader programs. This texture + /// has two lines: the first, a set of random numbers in [0, 255] to be used as permutation offsets, and + /// the second, a set of random unit vectors to be used as noise gradients. + const gpu::TexturePointer& getPermutationNormalTexture(); + + /// Returns an opaque white texture (useful for a default). + const gpu::TexturePointer& getWhiteTexture(); + + /// Returns an opaque gray texture (useful for a default). + const gpu::TexturePointer& getGrayTexture(); + + /// Returns the a pale blue texture (useful for a normal map). + const gpu::TexturePointer& getBlueTexture(); + + /// Returns the a black texture (useful for a default). + const gpu::TexturePointer& getBlackTexture(); + + // Returns a map used to compress the normals through a fitting scale algorithm + const gpu::TexturePointer& getNormalFittingTexture(); + + /// Returns a texture version of an image file + static gpu::TexturePointer getImageTexture(const QString& path, Type type = Type::DEFAULT_TEXTURE); + + /// Loads a texture from the specified URL. + NetworkTexturePointer getTexture(const QUrl& url, Type type = Type::DEFAULT_TEXTURE, + const QByteArray& content = QByteArray()); + +protected: + // Overload ResourceCache::prefetch to allow specifying texture type for loads + Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, int type); + + virtual QSharedPointer<Resource> createResource(const QUrl& url, + const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra); + +private: + TextureCache(); + virtual ~TextureCache(); + friend class DilatableNetworkTexture; + + gpu::TexturePointer _permutationNormalTexture; + gpu::TexturePointer _whiteTexture; + gpu::TexturePointer _grayTexture; + gpu::TexturePointer _blueTexture; + gpu::TexturePointer _blackTexture; + gpu::TexturePointer _normalFittingTexture; +}; + #endif // hifi_TextureCache_h diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index c0382a5748..4cc8b1d4f0 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -119,6 +119,92 @@ QSharedPointer<Resource> ResourceCacheSharedItems::getHighestPendingRequest() { return highestResource; } +ScriptableResource::ScriptableResource(const QUrl& url) : + QObject(nullptr), + _url(url) { } + +void ScriptableResource::release() { + disconnectHelper(); + _resource.reset(); +} + +bool ScriptableResource::isInScript() const { + return _resource && _resource->isInScript(); +} + +void ScriptableResource::setInScript(bool isInScript) { + if (_resource) { + _resource->setInScript(isInScript); + } +} + +void ScriptableResource::loadingChanged() { + emit stateChanged(LOADING); +} + +void ScriptableResource::loadedChanged() { + emit stateChanged(LOADED); +} + +void ScriptableResource::finished(bool success) { + disconnectHelper(); + + emit stateChanged(success ? FINISHED : FAILED); +} + +void ScriptableResource::disconnectHelper() { + if (_progressConnection) { + disconnect(_progressConnection); + } + if (_loadingConnection) { + disconnect(_loadingConnection); + } + if (_loadedConnection) { + disconnect(_loadedConnection); + } + if (_finishedConnection) { + disconnect(_finishedConnection); + } +} + +ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra) { + ScriptableResource* result = nullptr; + + if (QThread::currentThread() != thread()) { + // Must be called in thread to ensure getResource returns a valid pointer + QMetaObject::invokeMethod(this, "prefetch", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(ScriptableResource*, result), + Q_ARG(QUrl, url), Q_ARG(void*, extra)); + return result; + } + + result = new ScriptableResource(url); + + auto resource = getResource(url, QUrl(), false, extra); + result->_resource = resource; + result->setObjectName(url.toString()); + + result->_resource = resource; + if (resource->isLoaded()) { + result->finished(!resource->_failedToLoad); + } else { + result->_progressConnection = connect( + resource.data(), &Resource::onProgress, + result, &ScriptableResource::progressChanged); + result->_loadingConnection = connect( + resource.data(), &Resource::loading, + result, &ScriptableResource::loadingChanged); + result->_loadedConnection = connect( + resource.data(), &Resource::loaded, + result, &ScriptableResource::loadedChanged); + result->_finishedConnection = connect( + resource.data(), &Resource::finished, + result, &ScriptableResource::finished); + } + + return result; +} + ResourceCache::ResourceCache(QObject* parent) : QObject(parent) { auto nodeList = DependencyManager::get<NodeList>(); if (nodeList) { @@ -219,7 +305,7 @@ QVariantList ResourceCache::getResourceList() { return list; } - + void ResourceCache::setRequestLimit(int limit) { _requestLimit = limit; @@ -272,6 +358,7 @@ QSharedPointer<Resource> ResourceCache::getResource(const QUrl& url, const QUrl& getResource(fallback, QUrl(), true) : QSharedPointer<Resource>(), delayLoad, extra); resource->setSelf(resource); resource->setCache(this); + connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize); { QWriteLocker locker(&_resourcesLock); _resources.insert(url, resource); @@ -357,8 +444,13 @@ void ResourceCache::removeResource(const QUrl& url, qint64 size) { _totalResourcesSize -= size; } -void ResourceCache::updateTotalSize(const qint64& oldSize, const qint64& newSize) { - _totalResourcesSize += (newSize - oldSize); +void ResourceCache::updateTotalSize(const qint64& deltaSize) { + _totalResourcesSize += deltaSize; + + // Sanity checks + assert(_totalResourcesSize >= 0); + assert(_totalResourcesSize < (1024 * BYTES_PER_GIGABYTES)); + emit dirty(); } @@ -543,7 +635,7 @@ void Resource::finishedLoading(bool success) { } void Resource::setSize(const qint64& bytes) { - QMetaObject::invokeMethod(_cache.data(), "updateTotalSize", Q_ARG(qint64, _bytes), Q_ARG(qint64, bytes)); + emit updateSize(bytes - _bytes); _bytes = bytes; } @@ -569,8 +661,11 @@ void Resource::makeRequest() { } qCDebug(networking).noquote() << "Starting request for:" << _url.toDisplayString(); + emit loading(); + + connect(_request, &ResourceRequest::progress, this, &Resource::onProgress); + connect(this, &Resource::onProgress, this, &Resource::handleDownloadProgress); - connect(_request, &ResourceRequest::progress, this, &Resource::handleDownloadProgress); connect(_request, &ResourceRequest::finished, this, &Resource::handleReplyFinished); _bytesReceived = _bytesTotal = _bytes = 0; diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index ed3dbf69b6..b81c69c079 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -24,9 +24,12 @@ #include <QtCore/QWeakPointer> #include <QtCore/QReadWriteLock> #include <QtCore/QQueue> + #include <QtNetwork/QNetworkReply> #include <QtNetwork/QNetworkRequest> +#include <QScriptEngine> + #include <DependencyManager.h> #include "ResourceManager.h" @@ -50,7 +53,7 @@ static const qint64 DEFAULT_UNUSED_MAX_SIZE = 100 * BYTES_PER_MEGABYTES; static const qint64 DEFAULT_UNUSED_MAX_SIZE = 1024 * BYTES_PER_MEGABYTES; #endif static const qint64 MIN_UNUSED_MAX_SIZE = 0; -static const qint64 MAX_UNUSED_MAX_SIZE = 10 * BYTES_PER_GIGABYTES; +static const qint64 MAX_UNUSED_MAX_SIZE = MAXIMUM_CACHE_SIZE; // We need to make sure that these items are available for all instances of // ResourceCache derived classes. Since we can't count on the ordering of @@ -78,6 +81,61 @@ private: QList<QWeakPointer<Resource>> _loadingRequests; }; +/// Wrapper to expose resources to JS/QML +class ScriptableResource : public QObject { + Q_OBJECT + Q_PROPERTY(QUrl url READ getUrl) + Q_PROPERTY(int state READ getState NOTIFY stateChanged) + +public: + enum State { + QUEUED, + LOADING, + LOADED, + FINISHED, + FAILED, + }; + Q_ENUM(State) + + ScriptableResource(const QUrl& url); + virtual ~ScriptableResource() = default; + + Q_INVOKABLE void release(); + + const QUrl& getUrl() const { return _url; } + int getState() const { return (int)_state; } + const QSharedPointer<Resource>& getResource() const { return _resource; } + + bool isInScript() const; + void setInScript(bool isInScript); + +signals: + void progressChanged(uint64_t bytesReceived, uint64_t bytesTotal); + void stateChanged(int state); + +private slots: + void loadingChanged(); + void loadedChanged(); + void finished(bool success); + +private: + void disconnectHelper(); + + friend class ResourceCache; + + // Holds a ref to the resource to keep it in scope + QSharedPointer<Resource> _resource; + + QMetaObject::Connection _progressConnection; + QMetaObject::Connection _loadingConnection; + QMetaObject::Connection _loadedConnection; + QMetaObject::Connection _finishedConnection; + + QUrl _url; + State _state{ QUEUED }; +}; + +Q_DECLARE_METATYPE(ScriptableResource*); /// Base class for resource caches. class ResourceCache : public QObject { @@ -121,12 +179,23 @@ public slots: void checkAsynchronousGets(); protected slots: - void updateTotalSize(const qint64& oldSize, const qint64& newSize); + void updateTotalSize(const qint64& deltaSize); + + // Prefetches a resource to be held by the QScriptEngine. + // Left as a protected member so subclasses can overload prefetch + // and delegate to it (see TextureCache::prefetch(const QUrl&, int). + ScriptableResource* prefetch(const QUrl& url, void* extra); private slots: void clearATPAssets(); protected: + // Prefetches a resource to be held by the QScriptEngine. + // Pointers created through this method should be owned by the caller, + // which should be a QScriptEngine with ScriptableResource registered, so that + // the QScriptEngine will delete the pointer when it is garbage collected. + Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url) { return prefetch(url, nullptr); } + /// Loads a resource from the specified URL. /// \param fallback a fallback URL to load if the desired one is unavailable /// \param delayLoad if true, don't load the resource immediately; wait until load is first requested @@ -231,6 +300,9 @@ public: const QUrl& getURL() const { return _url; } signals: + /// Fired when the resource begins downloading. + void loading(); + /// Fired when the resource has been downloaded. /// This can be used instead of downloadFinished to access data before it is processed. void loaded(const QByteArray request); @@ -244,6 +316,12 @@ signals: /// Fired when the resource is refreshed. void onRefresh(); + /// Fired on progress updates. + void onProgress(uint64_t bytesReceived, uint64_t bytesTotal); + + /// Fired when the size changes (through setSize). + void updateSize(qint64 deltaSize); + protected slots: void attemptRequest(); @@ -280,21 +358,26 @@ private slots: void handleReplyFinished(); private: + friend class ResourceCache; + friend class ScriptableResource; + void setLRUKey(int lruKey) { _lruKey = lruKey; } void makeRequest(); void retry(); void reinsert(); + + bool isInScript() const { return _isInScript; } + void setInScript(bool isInScript) { _isInScript = isInScript; } - friend class ResourceCache; - - ResourceRequest* _request = nullptr; - int _lruKey = 0; - QTimer* _replyTimer = nullptr; - qint64 _bytesReceived = 0; - qint64 _bytesTotal = 0; - qint64 _bytes = 0; - int _attempts = 0; + ResourceRequest* _request{ nullptr }; + int _lruKey{ 0 }; + QTimer* _replyTimer{ nullptr }; + qint64 _bytesReceived{ 0 }; + qint64 _bytesTotal{ 0 }; + qint64 _bytes{ 0 }; + int _attempts{ 0 }; + bool _isInScript{ false }; }; uint qHash(const QPointer<QObject>& value, uint seed = 0); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 15f3ebb985..fc8b581ffe 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -270,6 +270,48 @@ static void resultHandlerFromScriptValue(const QScriptValue& value, AnimVariantR assert(false); } +// Templated qScriptRegisterMetaType fails to compile with raw pointers +using ScriptableResourceRawPtr = ScriptableResource*; + +static QScriptValue scriptableResourceToScriptValue(QScriptEngine* engine, const ScriptableResourceRawPtr& resource) { + // The first script to encounter this resource will track its memory. + // In this way, it will be more likely to GC. + // This fails in the case that the resource is used across many scripts, but + // in that case it would be too difficult to tell which one should track the memory, and + // this serves the common case (use in a single script). + auto data = resource->getResource(); + if (data && !resource->isInScript()) { + resource->setInScript(true); + QObject::connect(data.data(), SIGNAL(updateSize(qint64)), engine, SLOT(updateMemoryCost(qint64))); + } + + auto object = engine->newQObject( + const_cast<ScriptableResourceRawPtr>(resource), + QScriptEngine::ScriptOwnership); + return object; +} + +static void scriptableResourceFromScriptValue(const QScriptValue& value, ScriptableResourceRawPtr& resource) { + resource = static_cast<ScriptableResourceRawPtr>(value.toQObject()); +} + +static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { + auto prototype = engine->newObject(); + + // Expose enum State to JS/QML via properties + QObject* state = new QObject(engine); + state->setObjectName("ResourceState"); + auto metaEnum = QMetaEnum::fromType<ScriptableResource::State>(); + for (int i = 0; i < metaEnum.keyCount(); ++i) { + state->setProperty(metaEnum.key(i), metaEnum.value(i)); + } + + auto prototypeState = engine->newQObject(state, QScriptEngine::QtOwnership, QScriptEngine::ExcludeSlots | QScriptEngine::ExcludeSuperClassMethods); + prototype.setProperty("State", prototypeState); + + return prototype; +} + void ScriptEngine::init() { if (_isInitialized) { return; // only initialize once @@ -327,11 +369,16 @@ void ScriptEngine::init() { registerGlobalObject("Vec3", &_vec3Library); registerGlobalObject("Mat4", &_mat4Library); registerGlobalObject("Uuid", &_uuidLibrary); - registerGlobalObject("AnimationCache", DependencyManager::get<AnimationCache>().data()); registerGlobalObject("Messages", DependencyManager::get<MessagesClient>().data()); qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue); qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue); + // Scriptable cache access + auto resourcePrototype = createScriptableResourcePrototype(this); + globalObject().setProperty("Resource", resourcePrototype); + setDefaultPrototype(qMetaTypeId<ScriptableResource*>(), resourcePrototype); + qScriptRegisterMetaType(this, scriptableResourceToScriptValue, scriptableResourceFromScriptValue); + // constants globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE))); @@ -793,6 +840,12 @@ void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantM } } +void ScriptEngine::updateMemoryCost(const qint64& deltaSize) { + if (deltaSize > 0) { + reportAdditionalMemoryCost(deltaSize); + } +} + void ScriptEngine::timerFired() { QTimer* callingTimer = reinterpret_cast<QTimer*>(sender()); CallbackData timerData = _timerFunctionMap.value(callingTimer); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index e8ce00c66c..175a3f1f1c 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -131,6 +131,8 @@ public: Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const MouseEvent& event); Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision); + Q_INVOKABLE void requestGarbageCollection() { collectGarbage(); } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts Q_INVOKABLE void stop(); @@ -156,6 +158,7 @@ public: public slots: void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler); + void updateMemoryCost(const qint64&); signals: void scriptLoaded(const QString& scriptFilename); diff --git a/script-archive/theBird.js b/script-archive/theBird.js index 02b2e7fc5d..4adc6e3968 100644 --- a/script-archive/theBird.js +++ b/script-archive/theBird.js @@ -20,8 +20,6 @@ for (i = 0; i < l; i++) { print(roles[i]); } -MyAvatar.prefetchAnimation(THE_BIRD_RIGHT_URL); - // replace point animations with the bird! MyAvatar.overrideRoleAnimation("rightHandPointIntro", THE_BIRD_RIGHT_URL, 30, false, 0, 12); MyAvatar.overrideRoleAnimation("rightHandPointHold", THE_BIRD_RIGHT_URL, 30, false, 12, 12); diff --git a/scripts/developer/tests/scriptableResource/lib.js b/scripts/developer/tests/scriptableResource/lib.js new file mode 100644 index 0000000000..5241d0968e --- /dev/null +++ b/scripts/developer/tests/scriptableResource/lib.js @@ -0,0 +1,99 @@ +// +// lib.js +// scripts/developer/tests/scriptableResource +// +// Created by Zach Pomerantz on 4/20/16. +// Copyright 2016 High Fidelity, Inc. +// +// Preloads textures to play a simple movie, plays it, and frees those textures. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var NUM_FRAMES = 158; // 158 available +var FRAME_RATE = 30; // 30 default + +function getFrame(callback) { + // A model exported from blender with a texture named 'Picture' on one face. + var FRAME_URL = "http://hifi-production.s3.amazonaws.com/tutorials/pictureFrame/finalFrame.fbx"; + + var model = ModelCache.prefetch(FRAME_URL); + if (model.state === Resource.State.FINISHED) { + makeFrame(Resource.State.FINISHED); + } else { + model.stateChanged.connect(makeFrame); + } + + function makeFrame(state) { + if (state == Resource.State.FAILED) { throw "Failed to load frame"; } + if (state != Resource.State.FINISHED) { return; } + + var pictureFrameProperties = { + name: 'scriptableResourceTest Picture Frame', + type: 'Model', + position: getPosition(), + modelURL: FRAME_URL, + dynamic: true, + }; + + callback(Entities.addEntity(pictureFrameProperties)); + } + + function getPosition() { + // Always put it 5 meters in front of you + var position = MyAvatar.position; + var yaw = MyAvatar.bodyYaw + MyAvatar.getHeadFinalYaw(); + var rads = (yaw / 180) * Math.PI; + + position.y += 0.5; + position.x += - 5 * Math.sin(rads); + position.z += - 5 * Math.cos(rads); + + print(JSON.stringify(position)); + return position; + } +} + +function prefetch(callback) { + // A folder full of individual frames. + var MOVIE_URL = "http://hifi-content.s3.amazonaws.com/james/vidtest/"; + + var frames = []; + + var numLoading = 0; + for (var i = 1; i <= NUM_FRAMES; ++i) { + var padded = pad(i, 3); + var filepath = MOVIE_URL + padded + '.jpg'; + var texture = TextureCache.prefetch(filepath); + frames.push(texture); + if (!texture.state == Resource.State.FINISHED) { + numLoading++; + texture.stateChanged.connect(function(state) { + if (state == Resource.State.FAILED || state == Resource.State.FINISHED) { + --numLoading; + if (!numLoading) { callback(frames); } + } + }); + } + } + if (!numLoading) { callback(frames); } + + function pad(num, size) { // left-pad num with zeros until it is size digits + var s = num.toString(); + while (s.length < size) { s = "0" + s; } + return s; + } +} + +function play(model, frames, callback) { + var frame = 0; + var movieInterval = Script.setInterval(function() { + Entities.editEntity(model, { textures: JSON.stringify({ Picture: frames[frame].url }) }); + if (++frame >= frames.length) { + Script.clearInterval(movieInterval); + callback(); + } + }, 1000 / FRAME_RATE); +} + diff --git a/scripts/developer/tests/scriptableResource/movieTest.js b/scripts/developer/tests/scriptableResource/movieTest.js new file mode 100644 index 0000000000..61b2bf7942 --- /dev/null +++ b/scripts/developer/tests/scriptableResource/movieTest.js @@ -0,0 +1,42 @@ +// +// testMovie.js +// scripts/developer/tests/scriptableResource +// +// Created by Zach Pomerantz on 4/27/16. +// Copyright 2016 High Fidelity, Inc. +// +// Preloads textures, plays them on a frame model, and unloads them. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var entity; + +Script.include([ + '../../../developer/utilities/cache/cacheStats.js', + 'lib.js', +], function() { + getFrame(function(frame) { + entity = frame; + prefetch(function(frames) { + play(frame, frames, function() { + // Delete each texture, so the next garbage collection cycle will release them. + + // Setting frames = null breaks the reference, + // but will not delete frames from the calling scope. + // Instead, we must mutate it in-place to free its elements for GC + // (assuming the elements are not held elsewhere). + while (frames.length) { frames.pop(); } + + // Alternatively, forcibly release each texture without relying on GC. + // frames.forEach(function(texture) { texture.release(); }); + + Entities.deleteEntity(entity); + Script.requestGarbageCollection(); + }); + }); + }); +}); + +Script.scriptEnding.connect(function() { entity && Entities.deleteEntity(entity); }); diff --git a/scripts/developer/tests/scriptableResource/prefetchTest.js b/scripts/developer/tests/scriptableResource/prefetchTest.js new file mode 100644 index 0000000000..cda805967e --- /dev/null +++ b/scripts/developer/tests/scriptableResource/prefetchTest.js @@ -0,0 +1,33 @@ +// +// testPrefetch.js +// scripts/developer/tests/scriptableResource +// +// Created by Zach Pomerantz on 4/27/16. +// Copyright 2016 High Fidelity, Inc. +// +// Preloads textures and unloads them. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include([ + '../../../developer/utilities/cache/cacheStats.js', + 'lib.js', +], function() { + prefetch(function(frames) { + // Delete each texture, so the next garbage collection cycle will release them. + + // Setting frames = null breaks the reference, + // but will not delete frames from the calling scope. + // Instead, we must mutate it in-place to free its elements for GC + // (assuming the elements are not held elsewhere). + while (frames.length) { frames.pop(); } + + // Alternatively, forcibly release each texture without relying on GC. + // frames.forEach(function(texture) { texture.release(); }); + + Script.requestGarbageCollection(); + }); +}); + diff --git a/scripts/system/away.js b/scripts/system/away.js index 687345a5e1..932efd6b60 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -46,8 +46,8 @@ var AWAY_INTRO = { endFrame: 83.0 }; -// prefetch the kneel animation so it's resident in memory when we need it. -MyAvatar.prefetchAnimation(AWAY_INTRO.url); +// prefetch the kneel animation and hold a ref so it's always resident in memory when we need it. +var _animation = AnimationCache.prefetch(AWAY_INTRO.url); function playAwayAnimation() { MyAvatar.overrideAnimation(AWAY_INTRO.url, AWAY_INTRO.playbackRate, AWAY_INTRO.loopFlag, AWAY_INTRO.startFrame, AWAY_INTRO.endFrame);