Browse Source

Documentation for background resource loading. Expanded Scene::LoadAsync() to either background load resources only, load scene + resources synchronously, or background load resources first, then load the scene. Closes #406.

Lasse Öörni 11 years ago
parent
commit
ad737e0d6e

+ 27 - 9
Docs/Reference.dox

@@ -310,7 +310,7 @@ Scenes can be loaded and saved in either binary or XML format; see the functions
 
 Nodes and components that are marked temporary will not be saved. See \ref Serializable::SetTemporary "SetTemporary()".
 
-To be able to track the progress of loading a (large) scene without having the program stall for the duration of the loading, a scene can also be loaded asynchronously. This means that on each frame the scene loads child nodes until a certain amount of milliseconds has been exceeded. See \ref Scene::LoadAsync "LoadAsync()" and \ref Scene::LoadAsyncXML "LoadAsyncXML()". Use the functions \ref Scene::IsAsyncLoading "IsAsyncLoading()" and \ref Scene::GetAsyncProgress "GetAsyncProgress()" to track the loading progress; the latter returns a float value between 0 and 1, where 1 is fully loaded. The scene will not update or render before it is fully loaded.
+To be able to track the progress of loading a (large) scene without having the program stall for the duration of the loading, a scene can also be loaded asynchronously. This means that on each frame the scene loads resources and child nodes until a certain amount of milliseconds has been exceeded. See \ref Scene::LoadAsync "LoadAsync()" and \ref Scene::LoadAsyncXML "LoadAsyncXML()". Use the functions \ref Scene::IsAsyncLoading "IsAsyncLoading()" and \ref Scene::GetAsyncProgress "GetAsyncProgress()" to track the loading progress; the latter returns a float value between 0 and 1, where 1 is fully loaded. The scene will not update or render before it is fully loaded.
 
 \section SceneModel_Instantiation Object prefabs
 
@@ -334,11 +334,13 @@ Resources include most things in Urho3D that are loaded from mass storage during
 - Image
 - Model
 - Material
+- ParticleEffect
 - ScriptFile
 - Shader
 - Sound
 - Technique
 - Texture2D
+- Texture3D
 - TextureCube
 - XMLFile
 
@@ -364,6 +366,22 @@ Resources can also be created manually and stored to the resource cache as if th
 
 Memory budgets can be set per resource type: if resources consume more memory than allowed, the oldest resources will be removed from the cache if not in use anymore. By default the memory budgets are set to unlimited.
 
+\section Resources_Background Background loading of resources
+
+Normally, when requesting resources using \ref ResourceCache::GetResource "GetResource()", they are loaded immediately in the main thread, which may take several milliseconds for all the required steps (load file from disk,
+parse data, upload to GPU if necessary) and can therefore result in framerate drops.
+
+If you know in advance what resources you need, you can request them to be loaded in a background thread by calling \ref ResourceCache::BackgroundLoadResource "BackgroundLoadResource()". The event E_RESOURCEBACKGROUNDLOADED will be sent after the loading is complete; it will tell if the loading actually was a success or a failure. Depending on the resource, only a part of the loading process may be moved to a background thread, for example the finishing GPU upload step always needs to happen in the main thread. Note that if you call GetResource() for a resource that is queued for background loading, the main thread will stall until its loading is complete.
+
+The asynchronous scene loading functionality \ref Scene::LoadAsync "LoadAsync()" and \ref Scene::LoadAsyncXML "LoadAsyncXML()" has the option to background load the resources first before proceeding to load the scene content. It can also be used to only load the resources without modifying the scene, by specifying the LOAD_RESOURCES_ONLY mode. This allows to prepare a scene or object prefab file for fast instantiation.
+
+Finally the maximum time (in milliseconds) spent each frame on finishing background loaded resources can be configured, see \ref ResourceCache::SetFinishBackgroundResourcesMs "SetFinishBackgroundResourcesMs()".
+
+\section Resources_BackgroundImplementation Implementing background loading
+
+When writing new resource types, the background loading mechanism requires implementing two functions: \ref Resource::BeginLoad "BeginLoad()" and \ref Resource::EndLoad "EndLoad()". BeginLoad() is potentially called in a background thread and should do as much work (such as file I/O) as possible without violating the \ref Multithreading "multithreading" rules. EndLoad() should perform the main thread finishing step, such as GPU upload. Either step can return false to indicate failure to load the resource.
+
+If a resource depends on other resources, writing efficient threaded loading for it can be hard, as calling GetResource() is not allowed inside BeginLoad() when background loading. There are a few options: it is allowed to queue new background load requests by calling BackgroundLoadResource() within BeginLoad(), or if the needed resource does not need to be permanently stored in the cache and is safe to load outside the main thread (for example Image or XMLFile, which do not possess any GPU-side data), \ref ResourceCache::GetTempResource "GetTempResource()" can be called inside BeginLoad.
 
 \page Scripting Scripting
 
@@ -1969,21 +1987,21 @@ void WorkFunction(const WorkItem* item, unsigned threadIndex)
 
 The thread index ranges from 0 to n, where 0 represents the main thread and n is the number of worker threads created. Its function is to aid in splitting work into per-thread data structures that need no locking. The work item also contains three void pointers: start, end and aux, which can be used to describe a range of sub-work items, and an auxiliary data structure, which may for example be the object that originally queued the work.
 
-Multithreading is so far not exposed to scripts, and is currently used only in a limited manner: to speed up the preparation of rendering views, including lit object and shadow caster queries, occlusion tests and particle system, animation and skinning updates. Raycasts into the Octree are also threaded, but physics raycasts are not.
+Multithreading is so far not exposed to scripts, and is currently used only in a limited manner: to speed up the preparation of rendering views, including lit object and shadow caster queries, occlusion tests and particle system, animation and skinning updates. Raycasts into the Octree are also threaded, but physics raycasts are not. Additionally there are dedicated threads for audio mixing and background loading of resources.
 
-When making your own work functions, observe that the following things are (at least currently) unsafe and will result in undefined behavior and crashes, if done outside the main thread:
+When making your own work functions or threads, observe that the following things are unsafe and will result in undefined behavior and crashes, if done outside the main thread:
 
-- Modifying scene or UI content
+- Modifying scene or %UI content
 - Modifying GPU resources
-- Requesting resources from ResourceCache
 - Executing script functions
+- Pointing SharedPtr's or WeakPtr's to the same RefCounted object from multiple threads simultaneously
 
-Sending events and using the Profiler are treated as no-ops when called from outside the main thread.
+Using the Profiler is treated as a no-op when called from outside the main thread. Trying to send an event or get a resource from the ResourceCache when not in the main thread will cause an error to be logged. %Log messages from other threads are collected and handled in the main thread at the end of the frame.
 
 \page AttributeAnimation %Attribute animation
 Attribute animation is a new system for Urho3D, With it user can apply animation to object's attribute. All object derived from Animatable can use attribute animation, currently these classes include Node, Component and UIElement.
 
-These are two way to use use attribute animation. First user can create attribute animation with code, and then apply it to objects attribute. Here is a simple code for light color animation:
+These are two way to use use attribute animation. First user can create attribute animation with code, and then apply it to object's attribute. Here is a simple code for light color animation:
 \code
 SharedPtr<ValueAnimation> colorAnimation(new ValueAnimation(context_));
 colorAnimation->SetKeyFrame(0.0f, Color::WHITE);
@@ -1993,7 +2011,7 @@ colorAnimation->SetKeyFrame(4.0f, Color::WHITE);
 light->SetAttributeAnimation("Color", colorAnimation, WM_LOOP); 
 \endcode
 
-On above code, we first create an ValueAnimation object call colorAnimation, and set it’s key frame value, then apply it to light’s color attribute. (Note here: in order to make animation look correct, the last key frame must equal to the first key frame for loop mode).
+On above code, we first create an ValueAnimation object call colorAnimation, and set its key frame value, then apply it to light's color attribute. (Note here: in order to make animation look correct, the last key frame must equal to the first key frame for loop mode).
 
 Another way is load attribute animation from resource, here is a simple sample:
 \code
@@ -2004,7 +2022,7 @@ light->SetAttributeAnimation("Color", colorAnimation, WM_LOOP);
 These are three kind of wrap mode for attribute animation:
 - WM_LOOP: Loop mode, when the animation arrived to end, it will loop from begin.
 - WM_ONCE: Play once mode, when the animation finished, it will be removed from the object.
-- WM_CLAMP: Clamp mode, then the animation finished, it will keep the last key frames value.
+- WM_CLAMP: Clamp mode, then the animation finished, it will keep the last key frame's value.
 
 These is another argument call speed the animation play speed, the default value is 1.0f, user can change the value to control the animation play speed. User can also change animation’s wrap mode and speed on fly.
 

+ 2 - 2
Source/Engine/Graphics/View.cpp

@@ -375,8 +375,8 @@ bool View::Define(RenderSurface* renderTarget, Viewport* viewport)
         if (!scene_ || !camera_ || !camera_->IsEnabledEffective())
             return false;
         
-        // If scene is loading asynchronously, it is incomplete and should not be rendered
-        if (scene_->IsAsyncLoading())
+        // If scene is loading scene content asynchronously, it is incomplete and should not be rendered
+        if (scene_->IsAsyncLoading() && scene_->GetAsyncLoadMode() > LOAD_RESOURCES_ONLY)
             return false;
         
         octree_ = scene_->GetComponent<Octree>();

+ 20 - 8
Source/Engine/LuaScript/pkgs/Scene/Scene.pkg

@@ -5,6 +5,13 @@ static const unsigned LAST_REPLICATED_ID;
 static const unsigned FIRST_LOCAL_ID;
 static const unsigned LAST_LOCAL_ID;
 
+enum LoadMode
+{
+    LOAD_RESOURCES_ONLY = 0,
+    LOAD_SCENE,
+    LOAD_SCENE_AND_RESOURCES
+};
+
 class Scene : public Node
 {
     Scene();
@@ -23,10 +30,10 @@ class Scene : public Node
     tolua_outside Node* SceneInstantiateXML @ InstantiateXML(File* source, const Vector3& position, const Quaternion& rotation, CreateMode mode = REPLICATED);
     tolua_outside Node* SceneInstantiateXML @ InstantiateXML(const String fileName, const Vector3& position, const Quaternion& rotation, CreateMode mode = REPLICATED);
 
-    bool LoadAsync(File* file);
-    bool LoadAsyncXML(File* file);
-    tolua_outside bool SceneLoadAsync @ LoadAsync(const String fileName);
-    tolua_outside bool SceneLoadAsyncXML @ LoadAsyncXML(const String fileName);
+    bool LoadAsync(File* file, LoadMode mode = LOAD_SCENE_AND_RESOURCES);
+    bool LoadAsyncXML(File* file, LoadMode mode = LOAD_SCENE_AND_RESOURCES);
+    tolua_outside bool SceneLoadAsync @ LoadAsync(const String fileName, LoadMode mode = LOAD_SCENE_AND_RESOURCES);
+    tolua_outside bool SceneLoadAsyncXML @ LoadAsyncXML(const String fileName, LoadMode mode = LOAD_SCENE_AND_RESOURCES);
     void StopAsyncLoading();
     void Clear(bool clearReplicated = true, bool clearLocal = true);
     void SetUpdateEnabled(bool enable);
@@ -34,6 +41,7 @@ class Scene : public Node
     void SetElapsedTime(float time);
     void SetSmoothingConstant(float constant);
     void SetSnapThreshold(float threshold);
+    void SetAsyncLoadingMs(int ms);
     
     Node* GetNode(unsigned id) const;
     //Component* GetComponent(unsigned id) const;
@@ -41,12 +49,14 @@ class Scene : public Node
     bool IsUpdateEnabled() const;
     bool IsAsyncLoading() const;
     float GetAsyncProgress() const;
+    LoadMode GetAsyncLoadMode() const;
     const String GetFileName() const;
     unsigned GetChecksum() const;
     float GetTimeScale() const;
     float GetElapsedTime() const;
     float GetSmoothingConstant() const;
     float GetSnapThreshold() const;
+    int GetAsyncLoadingMs() const;
     const String GetVarName(StringHash hash) const;
 
     void Update(float timeStep);
@@ -71,12 +81,14 @@ class Scene : public Node
     tolua_property__is_set bool updateEnabled;
     tolua_readonly tolua_property__is_set bool asyncLoading;
     tolua_readonly tolua_property__get_set float asyncProgress;
+    tolua_readonly tolua_property__get_set LoadMode asyncLoadMode;
     tolua_property__get_set const String fileName;
     tolua_readonly tolua_property__get_set unsigned checksum;
     tolua_property__get_set float timeScale;
     tolua_property__get_set float elapsedTime;
     tolua_property__get_set float smoothingConstant;
     tolua_property__get_set float snapThreshold;
+    tolua_property__get_set int asyncLoadingMs;
     tolua_readonly tolua_property__is_set bool threadedUpdate;
     tolua_property__get_set String varNamesAttr;
 };
@@ -140,16 +152,16 @@ static bool SceneSaveXML(const Scene* scene, const String& fileName)
     return scene->SaveXML(file);
 }
 
-static bool SceneLoadAsync(Scene* scene, const String& fileName)
+static bool SceneLoadAsync(Scene* scene, const String& fileName, LoadMode mode)
 {
     SharedPtr<File> file(new File(scene->GetContext(), fileName, FILE_READ));
-    return file->IsOpen() && scene->LoadAsync(file);
+    return file->IsOpen() && scene->LoadAsync(file, mode);
 }
 
-static bool SceneLoadAsyncXML(Scene* scene, const String& fileName)
+static bool SceneLoadAsyncXML(Scene* scene, const String& fileName, LoadMode mode)
 {
     SharedPtr<File> file(new File(scene->GetContext(), fileName, FILE_READ));
-    return file->IsOpen() && scene->LoadAsyncXML(file);
+    return file->IsOpen() && scene->LoadAsyncXML(file, mode);
 }
 
 static Node* SceneInstantiate(Scene* scene, File* file, const Vector3& position, const Quaternion& rotation, CreateMode mode)

+ 1 - 1
Source/Engine/Resource/BackgroundLoader.cpp

@@ -231,7 +231,7 @@ void BackgroundLoader::FinishResources(int maxMs)
                 i = backgroundLoadQueue_.Erase(i);
             }
             
-            // Break when the time limit passed to avoid bogging down the framerate
+            // Break when the time limit passed so that we keep sufficient FPS
             if (timer.GetUSec(false) >= maxMs * 1000)
                 break;
         }

+ 293 - 59
Source/Engine/Scene/Scene.cpp

@@ -30,6 +30,8 @@
 #include "PackageFile.h"
 #include "Profiler.h"
 #include "ReplicationState.h"
+#include "ResourceCache.h"
+#include "ResourceEvents.h"
 #include "Scene.h"
 #include "SceneEvents.h"
 #include "SmoothedTransform.h"
@@ -48,8 +50,6 @@ const char* SCENE_CATEGORY = "Scene";
 const char* LOGIC_CATEGORY = "Logic";
 const char* SUBSYSTEM_CATEGORY = "Subsystem";
 
-static const int ASYNC_LOAD_MIN_FPS = 30;
-static const int ASYNC_LOAD_MAX_MSEC = (int)(1000.0f / ASYNC_LOAD_MIN_FPS);
 static const float DEFAULT_SMOOTHING_CONSTANT = 50.0f;
 static const float DEFAULT_SNAP_THRESHOLD = 5.0f;
 
@@ -64,6 +64,7 @@ Scene::Scene(Context* context) :
     elapsedTime_(0),
     smoothingConstant_(DEFAULT_SMOOTHING_CONSTANT),
     snapThreshold_(DEFAULT_SNAP_THRESHOLD),
+    asyncLoadingMs_(5),
     updateEnabled_(true),
     asyncLoading_(false),
     threadedUpdate_(false)
@@ -73,6 +74,7 @@ Scene::Scene(Context* context) :
     NodeAdded(this);
 
     SubscribeToEvent(E_UPDATE, HANDLER(Scene, HandleUpdate));
+    SubscribeToEvent(E_RESOURCEBACKGROUNDLOADED, HANDLER(Scene, HandleResourceBackgroundLoaded));
 }
 
 Scene::~Scene()
@@ -237,7 +239,7 @@ bool Scene::SaveXML(Serializer& dest) const
         return false;
 }
 
-bool Scene::LoadAsync(File* file)
+bool Scene::LoadAsync(File* file, LoadMode mode)
 {
     if (!file)
     {
@@ -248,34 +250,65 @@ bool Scene::LoadAsync(File* file)
     StopAsyncLoading();
 
     // Check ID
-    if (file->ReadFileID() != "USCN")
+    bool isSceneFile = file->ReadFileID() == "USCN";
+    if (!isSceneFile)
     {
-        LOGERROR(file->GetName() + " is not a valid scene file");
-        return false;
+        // In resource load mode can load also object prefabs, which have no identifier
+        if (mode > LOAD_RESOURCES_ONLY)
+        {
+            LOGERROR(file->GetName() + " is not a valid scene file");
+            return false;
+        }
+        else
+            file->Seek(0);
     }
 
-    LOGINFO("Loading scene from " + file->GetName());
-
-    Clear();
-
-    // Store own old ID for resolving possible root node references
-    unsigned nodeID = file->ReadUInt();
-    resolver_.AddNode(nodeID, this);
-
-    // Load root level components first
-    if (!Node::Load(*file, resolver_, false))
-        return false;
-
-    // Then prepare for loading all root level child nodes in the async update
+    if (mode > LOAD_RESOURCES_ONLY)
+    {
+        LOGINFO("Loading scene from " + file->GetName());
+        Clear();
+    }
+    
     asyncLoading_ = true;
     asyncProgress_.file_ = file;
-    asyncProgress_.loadedNodes_ = 0;
-    asyncProgress_.totalNodes_ = file->ReadVLE();
+    asyncProgress_.mode_ = mode;
+    asyncProgress_.loadedNodes_ = asyncProgress_.totalNodes_ = asyncProgress_.loadedResources_ = asyncProgress_.totalResources_ = 0;
+    asyncProgress_.resources_.Clear();
+    
+    if (mode > LOAD_RESOURCES_ONLY)
+    {
+        // Preload resources if appropriate, then return to the original position for loading the scene content
+        if (mode != LOAD_SCENE)
+        {
+            unsigned currentPos = file->GetPosition();
+            PreloadResources(file, isSceneFile);
+            file->Seek(currentPos);
+        }
+        
+        // Store own old ID for resolving possible root node references
+        unsigned nodeID = file->ReadUInt();
+        resolver_.AddNode(nodeID, this);
+
+        // Load root level components first
+        if (!Node::Load(*file, resolver_, false))
+        {
+            StopAsyncLoading();
+            return false;
+        }
+        
+        // Then prepare to load child nodes in the async updates
+        asyncProgress_.totalNodes_ = file->ReadVLE();
+    }
+    else
+    {
+        LOGINFO("Preloading resources from " + file->GetName());
+        PreloadResources(file, isSceneFile);
+    }
 
     return true;
 }
 
-bool Scene::LoadAsyncXML(File* file)
+bool Scene::LoadAsyncXML(File* file, LoadMode mode)
 {
     if (!file)
     {
@@ -289,36 +322,52 @@ bool Scene::LoadAsyncXML(File* file)
     if (!xml->Load(*file))
         return false;
 
-    LOGINFO("Loading scene from " + file->GetName());
-
-    Clear();
-
-    XMLElement rootElement = xml->GetRoot();
-
-    // Store own old ID for resolving possible root node references
-    unsigned nodeID = rootElement.GetInt("id");
-    resolver_.AddNode(nodeID, this);
-
-    // Load the root level components first
-    if (!Node::LoadXML(rootElement, resolver_, false))
-        return false;
-
-    // Then prepare for loading all root level child nodes in the async update
-    XMLElement childNodeElement = rootElement.GetChild("node");
+    if (mode > LOAD_RESOURCES_ONLY)
+    {
+        LOGINFO("Loading scene from " + file->GetName());
+        Clear();
+    }
+    
     asyncLoading_ = true;
-    asyncProgress_.file_ = file;
     asyncProgress_.xmlFile_ = xml;
-    asyncProgress_.xmlElement_ = childNodeElement;
-    asyncProgress_.loadedNodes_ = 0;
-    asyncProgress_.totalNodes_ = 0;
-
-    // Count the amount of child nodes
-    while (childNodeElement)
+    asyncProgress_.file_ = file;
+    asyncProgress_.mode_ = mode;
+    asyncProgress_.loadedNodes_ = asyncProgress_.totalNodes_ = asyncProgress_.loadedResources_ = asyncProgress_.totalResources_ = 0;
+    asyncProgress_.resources_.Clear();
+    
+    if (mode > LOAD_RESOURCES_ONLY)
+    {
+        XMLElement rootElement = xml->GetRoot();
+        
+        // Preload resources if appropriate
+        if (mode != LOAD_SCENE)
+            PreloadResourcesXML(rootElement);
+
+        // Store own old ID for resolving possible root node references
+        unsigned nodeID = rootElement.GetInt("id");
+        resolver_.AddNode(nodeID, this);
+
+        // Load the root level components first
+        if (!Node::LoadXML(rootElement, resolver_, false))
+            return false;
+
+        // Then prepare for loading all root level child nodes in the async update
+        XMLElement childNodeElement = rootElement.GetChild("node");
+        asyncProgress_.xmlElement_ = childNodeElement;
+
+        // Count the amount of child nodes
+        while (childNodeElement)
+        {
+            ++asyncProgress_.totalNodes_;
+            childNodeElement = childNodeElement.GetNext("node");
+        }
+    }
+    else
     {
-        ++asyncProgress_.totalNodes_;
-        childNodeElement = childNodeElement.GetNext("node");
+        LOGINFO("Preloading resources from " + file->GetName());
+        PreloadResourcesXML(xml->GetRoot());
     }
-
+    
     return true;
 }
 
@@ -328,6 +377,7 @@ void Scene::StopAsyncLoading()
     asyncProgress_.file_.Reset();
     asyncProgress_.xmlFile_.Reset();
     asyncProgress_.xmlElement_ = XMLElement::EMPTY;
+    asyncProgress_.resources_.Clear();
     resolver_.Reset();
 }
 
@@ -438,6 +488,11 @@ void Scene::SetSnapThreshold(float threshold)
     Node::MarkNetworkUpdate();
 }
 
+void Scene::SetAsyncLoadingMs(int ms)
+{
+    asyncLoadingMs_ = Max(ms, 1);
+}
+
 void Scene::SetElapsedTime(float time)
 {
     elapsedTime_ = time;
@@ -514,10 +569,13 @@ Component* Scene::GetComponent(unsigned id) const
 
 float Scene::GetAsyncProgress() const
 {
-    if (!asyncLoading_ || !asyncProgress_.totalNodes_)
+    if (!asyncLoading_ || asyncProgress_.totalNodes_ + asyncProgress_.totalResources_ == 0)
         return 1.0f;
     else
-        return (float)asyncProgress_.loadedNodes_ / (float)asyncProgress_.totalNodes_;
+    {
+        return (float)(asyncProgress_.loadedNodes_ + asyncProgress_.loadedResources_) / (float)(asyncProgress_.totalNodes_ + 
+            asyncProgress_.totalResources_);
+    }
 }
 
 const String& Scene::GetVarName(StringHash hash) const
@@ -531,7 +589,9 @@ void Scene::Update(float timeStep)
     if (asyncLoading_)
     {
         UpdateAsyncLoading();
-        return;
+        // If only preloading resources, scene update can continue
+        if (asyncProgress_.mode_ > LOAD_RESOURCES_ONLY)
+            return;
     }
 
     PROFILE(UpdateScene);
@@ -890,11 +950,30 @@ void Scene::HandleUpdate(StringHash eventType, VariantMap& eventData)
         Update(eventData[P_TIMESTEP].GetFloat());
 }
 
+void Scene::HandleResourceBackgroundLoaded(StringHash eventType, VariantMap& eventData)
+{
+    using namespace ResourceBackgroundLoaded;
+    
+    if (asyncLoading_)
+    {
+        Resource* resource = static_cast<Resource*>(eventData[P_RESOURCE].GetPtr());
+        if (asyncProgress_.resources_.Contains(resource->GetNameHash()))
+        {
+            asyncProgress_.resources_.Erase(resource->GetNameHash());
+            ++asyncProgress_.loadedResources_;
+        }
+    }
+}
+
 void Scene::UpdateAsyncLoading()
 {
     PROFILE(UpdateAsyncLoading);
 
-    Timer asyncLoadTimer;
+    // If resources left to load, do not load nodes yet
+    if (asyncProgress_.loadedResources_ < asyncProgress_.totalResources_)
+        return;
+    
+    HiresTimer asyncLoadTimer;
 
     for (;;)
     {
@@ -925,7 +1004,7 @@ void Scene::UpdateAsyncLoading()
         ++asyncProgress_.loadedNodes_;
 
         // Break if time limit exceeded, so that we keep sufficient FPS
-        if (asyncLoadTimer.GetMSec(false) >= ASYNC_LOAD_MAX_MSEC)
+        if (asyncLoadTimer.GetUSec(false) >= asyncLoadingMs_ * 1000)
             break;
     }
 
@@ -933,17 +1012,23 @@ void Scene::UpdateAsyncLoading()
 
     VariantMap& eventData = GetEventDataMap();
     eventData[P_SCENE] = this;
-    eventData[P_PROGRESS] = (float)asyncProgress_.loadedNodes_ / (float)asyncProgress_.totalNodes_;
-    eventData[P_LOADEDNODES]  = asyncProgress_.loadedNodes_;
-    eventData[P_TOTALNODES]  = asyncProgress_.totalNodes_;
+    eventData[P_PROGRESS] = GetAsyncProgress();
+    eventData[P_LOADEDNODES] = asyncProgress_.loadedNodes_;
+    eventData[P_TOTALNODES] = asyncProgress_.totalNodes_;
+    eventData[P_LOADEDRESOURCES]  = asyncProgress_.loadedResources_;
+    eventData[P_TOTALRESOURCES] = asyncProgress_.totalResources_;
     SendEvent(E_ASYNCLOADPROGRESS, eventData);
 }
 
 void Scene::FinishAsyncLoading()
 {
-    resolver_.Resolve();
-    ApplyAttributes();
-    FinishLoading(asyncProgress_.file_);
+    if (asyncProgress_.mode_ > LOAD_RESOURCES_ONLY)
+    {
+        resolver_.Resolve();
+        ApplyAttributes();
+        FinishLoading(asyncProgress_.file_);
+    }
+
     StopAsyncLoading();
 
     using namespace AsyncLoadFinished;
@@ -972,6 +1057,155 @@ void Scene::FinishSaving(Serializer* dest) const
     }
 }
 
+void Scene::PreloadResources(File* file, bool isSceneFile)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    
+    // Read node ID (not needed)
+    unsigned nodeID = file->ReadUInt();
+
+    // Read Node or Scene attributes; these do not include any resources
+    const Vector<AttributeInfo>* attributes = context_->GetAttributes(isSceneFile ? Scene::GetTypeStatic() : Node::GetTypeStatic());
+    assert(attributes);
+    
+    for (unsigned i = 0; i < attributes->Size(); ++i)
+    {
+        const AttributeInfo& attr = attributes->At(i);
+        if (!(attr.mode_ & AM_FILE))
+            continue;
+        Variant varValue = file->ReadVariant(attr.type_);
+    }
+    
+    // Read component attributes
+    unsigned numComponents = file->ReadVLE();
+    for (unsigned i = 0; i < numComponents; ++i)
+    {
+        VectorBuffer compBuffer(*file, file->ReadVLE());
+        StringHash compType = compBuffer.ReadStringHash();
+        unsigned compID = compBuffer.ReadUInt();
+        
+        attributes = context_->GetAttributes(compType);
+        if (attributes)
+        {
+            for (unsigned j = 0; j < attributes->Size(); ++j)
+            {
+                const AttributeInfo& attr = attributes->At(j);
+                if (!(attr.mode_ & AM_FILE))
+                    continue;
+                Variant varValue = compBuffer.ReadVariant(attr.type_);
+                if (attr.type_ == VAR_RESOURCEREF)
+                {
+                    const ResourceRef& ref = varValue.GetResourceRef();
+                    // Sanitate resource name beforehand so that when we get the background load event, the name matches exactly
+                    String name = cache->SanitateResourceName(ref.name_);
+                    bool success = cache->BackgroundLoadResource(ref.type_, name);
+                    if (success)
+                    {
+                        ++asyncProgress_.totalResources_;
+                        asyncProgress_.resources_.Insert(StringHash(name));
+                    }
+                }
+                else if (attr.type_ == VAR_RESOURCEREFLIST)
+                {
+                    const ResourceRefList& refList = varValue.GetResourceRefList();
+                    for (unsigned k = 0; k < refList.names_.Size(); ++k)
+                    {
+                        String name = cache->SanitateResourceName(refList.names_[k]);
+                        bool success = cache->BackgroundLoadResource(refList.type_, name);
+                        if (success)
+                        {
+                            ++asyncProgress_.totalResources_;
+                            asyncProgress_.resources_.Insert(StringHash(name));
+                        }
+                    }
+                }
+             }
+        }
+    }
+    
+    // Read child nodes
+    unsigned numChildren = file->ReadVLE();
+    for (unsigned i = 0; i < numChildren; ++i)
+        PreloadResources(file, false);
+}
+
+void Scene::PreloadResourcesXML(const XMLElement& element)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    
+    // Node or Scene attributes do not include any resources; therefore skip to the components
+    XMLElement compElem = element.GetChild("component");
+    while (compElem)
+    {
+        String typeName = compElem.GetAttribute("type");
+        const Vector<AttributeInfo>* attributes = context_->GetAttributes(StringHash(typeName));
+        if (attributes)
+        {
+            XMLElement attrElem = compElem.GetChild("attribute");
+            unsigned startIndex = 0;
+
+            while (attrElem)
+            {
+                String name = attrElem.GetAttribute("name");
+                unsigned i = startIndex;
+                unsigned attempts = attributes->Size();
+
+                while (attempts)
+                {
+                    const AttributeInfo& attr = attributes->At(i);
+                    if ((attr.mode_ & AM_FILE) && !attr.name_.Compare(name, true))
+                    {
+                        if (attr.type_ == VAR_RESOURCEREF)
+                        {
+                            ResourceRef ref = attrElem.GetVariantValue(attr.type_).GetResourceRef();
+                            String name = cache->SanitateResourceName(ref.name_);
+                            bool success = cache->BackgroundLoadResource(ref.type_, name);
+                            if (success)
+                            {
+                                ++asyncProgress_.totalResources_;
+                                asyncProgress_.resources_.Insert(StringHash(name));
+                            }
+                        }
+                        else if (attr.type_ == VAR_RESOURCEREFLIST)
+                        {
+                            ResourceRefList refList = attrElem.GetVariantValue(attr.type_).GetResourceRefList();
+                            for (unsigned k = 0; k < refList.names_.Size(); ++k)
+                            {
+                                String name = cache->SanitateResourceName(refList.names_[k]);
+                                bool success = cache->BackgroundLoadResource(refList.type_, name);
+                                if (success)
+                                {
+                                    ++asyncProgress_.totalResources_;
+                                    asyncProgress_.resources_.Insert(StringHash(name));
+                                }
+                            }
+                        }
+                        
+                        startIndex = (i + 1) % attributes->Size();
+                        break;
+                    }
+                    else
+                    {
+                        i = (i + 1) % attributes->Size();
+                        --attempts;
+                    }
+                }
+                
+                attrElem = attrElem.GetNext("attribute");
+            }
+        }
+
+        compElem = compElem.GetNext("component");
+    }
+
+    XMLElement childElem = element.GetChild("node");
+    while (childElem)
+    {
+        PreloadResourcesXML(childElem);
+        childElem = childElem.GetNext("node");
+    }
+}
+
 void RegisterSceneLibrary(Context* context)
 {
     ValueAnimation::RegisterObject(context);

+ 37 - 4
Source/Engine/Scene/Scene.h

@@ -39,6 +39,17 @@ static const unsigned LAST_REPLICATED_ID = 0xffffff;
 static const unsigned FIRST_LOCAL_ID = 0x01000000;
 static const unsigned LAST_LOCAL_ID = 0xffffffff;
 
+/// Asynchronous scene loading mode.
+enum LoadMode
+{
+    /// Preload resources used by a scene or object prefab file, but do not load any scene content.
+    LOAD_RESOURCES_ONLY = 0,
+    /// Load scene content without preloading. Resources will be requested synchronously when encountered.
+    LOAD_SCENE,
+    /// Default mode: reload resources used by the scene on the background first, then load the scene content.
+    LOAD_SCENE_AND_RESOURCES
+};
+
 /// Asynchronous loading progress of a scene.
 struct AsyncProgress
 {
@@ -48,6 +59,14 @@ struct AsyncProgress
     SharedPtr<XMLFile> xmlFile_;
     /// Current XML element for XML mode.
     XMLElement xmlElement_;
+    /// Current load mode.
+    LoadMode mode_;
+    /// Resource name hashes left to load.
+    HashSet<StringHash> resources_;
+    /// Loaded resources.
+    unsigned loadedResources_;
+    /// Total resources.
+    unsigned totalResources_;
     /// Loaded root-level nodes.
     unsigned loadedNodes_;
     /// Total root-level nodes.
@@ -85,10 +104,10 @@ public:
     bool LoadXML(Deserializer& source);
     /// Save to an XML file. Return true if successful.
     bool SaveXML(Serializer& dest) const;
-    /// Load from a binary file asynchronously. Return true if started successfully.
-    bool LoadAsync(File* file);
-    /// Load from an XML file asynchronously. Return true if started successfully.
-    bool LoadAsyncXML(File* file);
+    /// Load from a binary file asynchronously. Return true if started successfully. The LOAD_RESOURCES_ONLY mode can also be used to preload resources from object prefab files.
+    bool LoadAsync(File* file, LoadMode mode = LOAD_SCENE_AND_RESOURCES);
+    /// Load from an XML file asynchronously. Return true if started successfully. The LOAD_RESOURCES_ONLY mode can also be used to preload resources from object prefab files.
+    bool LoadAsyncXML(File* file, LoadMode mode = LOAD_SCENE_AND_RESOURCES);
     /// Stop asynchronous loading.
     void StopAsyncLoading();
     /// Instantiate scene content from binary data. Return root node if successful.
@@ -109,6 +128,8 @@ public:
     void SetSmoothingConstant(float constant);
     /// Set network client motion smoothing snap threshold.
     void SetSnapThreshold(float threshold);
+    /// Set maximum milliseconds per frame to spend on async scene loading.
+    void SetAsyncLoadingMs(int ms);
     /// Add a required package file for networking. To be called on the server.
     void AddRequiredPackageFile(PackageFile* package);
     /// Clear required package files.
@@ -130,6 +151,8 @@ public:
     bool IsAsyncLoading() const { return asyncLoading_; }
     /// Return asynchronous loading progress between 0.0 and 1.0, or 1.0 if not in progress.
     float GetAsyncProgress() const;
+    /// Return the load mode of the current asynchronous loading operation.
+    LoadMode GetAsyncLoadMode() const { return asyncProgress_.mode_; }
     /// Return source file name.
     const String& GetFileName() const { return fileName_; }
     /// Return source file checksum.
@@ -142,6 +165,8 @@ public:
     float GetSmoothingConstant() const { return smoothingConstant_; }
     /// Return motion smoothing snap threshold.
     float GetSnapThreshold() const { return snapThreshold_; }
+    /// Return maximum milliseconds per frame to spend on async loading.
+    int GetAsyncLoadingMs() const { return asyncLoadingMs_; }
     /// Return required package files.
     const Vector<SharedPtr<PackageFile> >& GetRequiredPackageFiles() const { return requiredPackageFiles_; }
     /// Return a node user variable name, or empty if not registered.
@@ -187,6 +212,8 @@ public:
 private:
     /// Handle the logic update event to update the scene, if active.
     void HandleUpdate(StringHash eventType, VariantMap& eventData);
+    /// Handle a background loaded resource completing.
+    void HandleResourceBackgroundLoaded(StringHash eventType, VariantMap& eventData);
     /// Update asynchronous loading.
     void UpdateAsyncLoading();
     /// Finish asynchronous loading.
@@ -195,6 +222,10 @@ private:
     void FinishLoading(Deserializer* source);
     /// Finish saving. Sets the scene filename and checksum.
     void FinishSaving(Serializer* dest) const;
+    /// Preload resources from a binary scene or object prefab file.
+    void PreloadResources(File* file, bool isSceneFile);
+    /// Preload resources from an XML scene or object prefab file.
+    void PreloadResourcesXML(const XMLElement& element);
 
     /// Replicated scene nodes by ID.
     HashMap<unsigned, Node*> replicatedNodes_;
@@ -234,6 +265,8 @@ private:
     unsigned localComponentID_;
     /// Scene source file checksum.
     mutable unsigned checksum_;
+    /// Maximum milliseconds per frame to spend on async scene loading.
+    int asyncLoadingMs_;
     /// Scene update time scale.
     float timeScale_;
     /// Elapsed time accumulator.

+ 2 - 0
Source/Engine/Scene/SceneEvents.h

@@ -86,6 +86,8 @@ EVENT(E_ASYNCLOADPROGRESS, AsyncLoadProgress)
     PARAM(P_PROGRESS, Progress);            // float
     PARAM(P_LOADEDNODES, LoadedNodes);      // int
     PARAM(P_TOTALNODES, TotalNodes);        // int
+    PARAM(P_LOADEDRESOURCES, LoadedResources); // int
+    PARAM(P_TOTALRESOURCES, TotalResources);   // int
 };
 
 /// Asynchronous scene loading finished.

+ 10 - 2
Source/Engine/Script/SceneAPI.cpp

@@ -253,6 +253,11 @@ static void RegisterSplinePath(asIScriptEngine* engine)
 
 static void RegisterScene(asIScriptEngine* engine)
 {
+    engine->RegisterEnum("LoadMode");
+    engine->RegisterEnumValue("LoadMode", "LOAD_RESOURCES_ONLY", LOAD_RESOURCES_ONLY);
+    engine->RegisterEnumValue("LoadMode", "LOAD_SCENE", LOAD_SCENE);
+    engine->RegisterEnumValue("LoadMode", "LOAD_SCENE_AND_RESOURCES", LOAD_SCENE_AND_RESOURCES);
+    
     engine->RegisterGlobalProperty("const uint FIRST_REPLICATED_ID", (void*)&FIRST_REPLICATED_ID);
     engine->RegisterGlobalProperty("const uint LAST_REPLICATED_ID", (void*)&LAST_REPLICATED_ID);
     engine->RegisterGlobalProperty("const uint FIRST_LOCAL_ID", (void*)&FIRST_LOCAL_ID);
@@ -265,8 +270,8 @@ static void RegisterScene(asIScriptEngine* engine)
     engine->RegisterObjectMethod("Scene", "bool LoadXML(VectorBuffer&)", asFUNCTION(SceneLoadXMLVectorBuffer), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("Scene", "bool SaveXML(File@+)", asFUNCTION(SceneSaveXML), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("Scene", "bool SaveXML(VectorBuffer&)", asFUNCTION(SceneSaveXMLVectorBuffer), asCALL_CDECL_OBJLAST);
-    engine->RegisterObjectMethod("Scene", "bool LoadAsync(File@+)", asMETHOD(Scene, LoadAsync), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Scene", "bool LoadAsyncXML(File@+)", asMETHOD(Scene, LoadAsyncXML), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Scene", "bool LoadAsync(File@+, LoadMode mode = LOAD_SCENE_AND_RESOURCES)", asMETHOD(Scene, LoadAsync), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Scene", "bool LoadAsyncXML(File@+, LoadMode mode = LOAD_SCENE_AND_RESOURCES)", asMETHOD(Scene, LoadAsyncXML), asCALL_THISCALL);
     engine->RegisterObjectMethod("Scene", "void StopAsyncLoading()", asMETHOD(Scene, StopAsyncLoading), asCALL_THISCALL);
     engine->RegisterObjectMethod("Scene", "Node@+ Instantiate(File@+, const Vector3&in, const Quaternion&in, CreateMode mode = REPLICATED)", asFUNCTION(SceneInstantiate), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("Scene", "Node@+ Instantiate(VectorBuffer&, const Vector3&in, const Quaternion&in, CreateMode mode = REPLICATED)", asFUNCTION(SceneInstantiateVectorBuffer), asCALL_CDECL_OBJLAST);
@@ -296,6 +301,9 @@ static void RegisterScene(asIScriptEngine* engine)
     engine->RegisterObjectMethod("Scene", "float get_snapThreshold() const", asMETHOD(Scene, GetSnapThreshold), asCALL_THISCALL);
     engine->RegisterObjectMethod("Scene", "bool get_asyncLoading() const", asMETHOD(Scene, IsAsyncLoading), asCALL_THISCALL);
     engine->RegisterObjectMethod("Scene", "float get_asyncProgress() const", asMETHOD(Scene, GetAsyncProgress), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Scene", "LoadMode get_asyncLoadMode() const", asMETHOD(Scene, GetAsyncLoadMode), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Scene", "void set_asyncLoadingMs(int)", asMETHOD(Scene, SetAsyncLoadingMs), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Scene", "int get_asyncLoadingMs() const", asMETHOD(Scene, GetAsyncLoadingMs), asCALL_THISCALL);
     engine->RegisterObjectMethod("Scene", "uint get_checksum() const", asMETHOD(Scene, GetChecksum), asCALL_THISCALL);
     engine->RegisterObjectMethod("Scene", "const String& get_fileName() const", asMETHOD(Scene, GetFileName), asCALL_THISCALL);
     engine->RegisterObjectMethod("Scene", "Array<PackageFile@>@ get_requiredPackageFiles() const", asFUNCTION(SceneGetRequiredPackageFiles), asCALL_CDECL_OBJLAST);