Explorar o código

Merge pull request #411 from urho3d/threaded-resource-load

Background loading of resources
Lasse Öörni %!s(int64=11) %!d(string=hai) anos
pai
achega
52661d18dc
Modificáronse 88 ficheiros con 2291 adicións e 646 borrados
  1. 2 2
      Bin/Data/Scripts/Editor/EditorResourceBrowser.as
  2. 29 11
      Docs/Reference.dox
  3. 2 2
      Source/Engine/Audio/Sound.cpp
  4. 2 2
      Source/Engine/Audio/Sound.h
  5. 4 0
      Source/Engine/Core/Context.cpp
  6. 1 1
      Source/Engine/Core/Context.h
  7. 8 0
      Source/Engine/Core/Object.cpp
  8. 18 9
      Source/Engine/Core/Profiler.h
  9. 21 0
      Source/Engine/Core/Thread.cpp
  10. 17 0
      Source/Engine/Core/Thread.h
  11. 3 4
      Source/Engine/Core/WorkQueue.cpp
  12. 6 0
      Source/Engine/Core/WorkQueue.h
  13. 2 4
      Source/Engine/Graphics/Animation.cpp
  14. 2 2
      Source/Engine/Graphics/Animation.h
  15. 25 9
      Source/Engine/Graphics/Direct3D9/D3D9Graphics.cpp
  16. 3 0
      Source/Engine/Graphics/Direct3D9/D3D9Graphics.h
  17. 3 13
      Source/Engine/Graphics/Direct3D9/D3D9Texture.cpp
  18. 5 7
      Source/Engine/Graphics/Direct3D9/D3D9Texture.h
  19. 39 12
      Source/Engine/Graphics/Direct3D9/D3D9Texture2D.cpp
  20. 11 4
      Source/Engine/Graphics/Direct3D9/D3D9Texture2D.h
  21. 48 20
      Source/Engine/Graphics/Direct3D9/D3D9Texture3D.cpp
  22. 10 4
      Source/Engine/Graphics/Direct3D9/D3D9Texture3D.h
  23. 86 58
      Source/Engine/Graphics/Direct3D9/D3D9TextureCube.cpp
  24. 12 6
      Source/Engine/Graphics/Direct3D9/D3D9TextureCube.h
  25. 61 7
      Source/Engine/Graphics/Material.cpp
  26. 6 2
      Source/Engine/Graphics/Material.h
  27. 115 27
      Source/Engine/Graphics/Model.cpp
  28. 52 2
      Source/Engine/Graphics/Model.h
  29. 27 13
      Source/Engine/Graphics/OpenGL/OGLGraphics.cpp
  30. 3 0
      Source/Engine/Graphics/OpenGL/OGLGraphics.h
  31. 3 13
      Source/Engine/Graphics/OpenGL/OGLTexture.cpp
  32. 5 7
      Source/Engine/Graphics/OpenGL/OGLTexture.h
  33. 42 14
      Source/Engine/Graphics/OpenGL/OGLTexture2D.cpp
  34. 13 5
      Source/Engine/Graphics/OpenGL/OGLTexture2D.h
  35. 51 22
      Source/Engine/Graphics/OpenGL/OGLTexture3D.cpp
  36. 10 4
      Source/Engine/Graphics/OpenGL/OGLTexture3D.h
  37. 89 61
      Source/Engine/Graphics/OpenGL/OGLTextureCube.cpp
  38. 12 6
      Source/Engine/Graphics/OpenGL/OGLTextureCube.h
  39. 21 2
      Source/Engine/Graphics/ParticleEffect.cpp
  40. 6 2
      Source/Engine/Graphics/ParticleEffect.h
  41. 7 4
      Source/Engine/Graphics/Shader.cpp
  42. 4 2
      Source/Engine/Graphics/Shader.h
  43. 1 3
      Source/Engine/Graphics/Technique.cpp
  44. 2 2
      Source/Engine/Graphics/Technique.h
  45. 2 2
      Source/Engine/Graphics/View.cpp
  46. 46 0
      Source/Engine/IO/Log.cpp
  47. 35 0
      Source/Engine/IO/Log.h
  48. 1 1
      Source/Engine/LuaScript/LuaFile.cpp
  49. 2 2
      Source/Engine/LuaScript/LuaFile.h
  50. 4 4
      Source/Engine/LuaScript/pkgs/Graphics/Texture2D.pkg
  51. 17 4
      Source/Engine/LuaScript/pkgs/Resource/ResourceCache.pkg
  52. 20 8
      Source/Engine/LuaScript/pkgs/Scene/Scene.pkg
  53. 299 0
      Source/Engine/Resource/BackgroundLoader.cpp
  54. 84 0
      Source/Engine/Resource/BackgroundLoader.h
  55. 35 3
      Source/Engine/Resource/Image.cpp
  56. 6 2
      Source/Engine/Resource/Image.h
  57. 2 4
      Source/Engine/Resource/JSONFile.cpp
  58. 2 2
      Source/Engine/Resource/JSONFile.h
  59. 43 2
      Source/Engine/Resource/Resource.cpp
  60. 27 2
      Source/Engine/Resource/Resource.h
  61. 81 21
      Source/Engine/Resource/ResourceCache.cpp
  62. 31 16
      Source/Engine/Resource/ResourceCache.h
  63. 8 0
      Source/Engine/Resource/ResourceEvents.h
  64. 1 3
      Source/Engine/Resource/XMLFile.cpp
  65. 2 2
      Source/Engine/Resource/XMLFile.h
  66. 1 1
      Source/Engine/Scene/ObjectAnimation.cpp
  67. 2 2
      Source/Engine/Scene/ObjectAnimation.h
  68. 302 58
      Source/Engine/Scene/Scene.cpp
  69. 37 4
      Source/Engine/Scene/Scene.h
  70. 2 0
      Source/Engine/Scene/SceneEvents.h
  71. 9 9
      Source/Engine/Script/GraphicsAPI.cpp
  72. 9 0
      Source/Engine/Script/ResourceAPI.cpp
  73. 10 2
      Source/Engine/Script/SceneAPI.cpp
  74. 5 0
      Source/Engine/Script/Script.h
  75. 68 41
      Source/Engine/Script/ScriptFile.cpp
  76. 9 2
      Source/Engine/Script/ScriptFile.h
  77. 1 1
      Source/Engine/UI/Cursor.cpp
  78. 1 3
      Source/Engine/UI/Font.cpp
  79. 3 2
      Source/Engine/UI/Font.h
  80. 1 1
      Source/Engine/UI/FontFace.cpp
  81. 85 36
      Source/Engine/Urho2D/AnimationSet2D.cpp
  82. 7 2
      Source/Engine/Urho2D/AnimationSet2D.h
  83. 23 8
      Source/Engine/Urho2D/ParticleEffect2D.cpp
  84. 10 6
      Source/Engine/Urho2D/ParticleEffect2D.h
  85. 26 10
      Source/Engine/Urho2D/Sprite2D.cpp
  86. 6 2
      Source/Engine/Urho2D/Sprite2D.h
  87. 27 7
      Source/Engine/Urho2D/SpriteSheet2D.cpp
  88. 10 3
      Source/Engine/Urho2D/SpriteSheet2D.h

+ 2 - 2
Bin/Data/Scripts/Editor/EditorResourceBrowser.as

@@ -1452,7 +1452,7 @@ void CreateResourcePreview(String path, Node@ previewNode)
                 staticModel.model = cache.GetResource("Model", "Models/Editor/ImagePlane.mdl");
                 Material@ material =  cache.GetResource("Material", "Materials/Editor/TexturedUnlit.xml");
                 Texture2D@ texture = Texture2D();
-                texture.Load(@image, true);
+                texture.SetData(@image, true);
                 material.textures[0] = texture;
                 staticModel.material = material;
                 return;
@@ -1482,7 +1482,7 @@ void CreateResourcePreview(String path, Node@ previewNode)
     Material@ material =  cache.GetResource("Material", "Materials/Editor/TexturedUnlit.xml");
     Texture2D@ texture = Texture2D();
     Image@ noPreviewImage = cache.GetResource("Image", "Textures/Editor/NoPreviewAvailable.png");
-    texture.Load(noPreviewImage, false);
+    texture.SetData(noPreviewImage, false);
     material.textures[0] = texture;
     staticModel.material = material;
 

+ 29 - 11
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:
 
-- Sending events
-- Using profiler blocks
-- 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
+
+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.
+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/Audio/Sound.cpp

@@ -81,7 +81,7 @@ void Sound::RegisterObject(Context* context)
     context->RegisterFactory<Sound>();
 }
 
-bool Sound::Load(Deserializer& source)
+bool Sound::BeginLoad(Deserializer& source)
 {
     PROFILE(LoadSound);
     
@@ -343,7 +343,7 @@ void Sound::LoadParameters()
     ResourceCache* cache = GetSubsystem<ResourceCache>();
     String xmlName = ReplaceExtension(GetName(), ".xml");
     
-    XMLFile* file = cache->GetResource<XMLFile>(xmlName, false);
+    SharedPtr<XMLFile> file(cache->GetTempResource<XMLFile>(xmlName, false));
     if (!file)
         return;
     

+ 2 - 2
Source/Engine/Audio/Sound.h

@@ -43,8 +43,8 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
     
     /// Load raw sound data.
     bool LoadRaw(Deserializer& source);

+ 4 - 0
Source/Engine/Core/Context.cpp

@@ -22,6 +22,7 @@
 
 #include "Precompiled.h"
 #include "Context.h"
+#include "Thread.h"
 
 #include "DebugNew.h"
 
@@ -57,6 +58,9 @@ Context::Context() :
     // Always reset the random seed on Android, as the Urho3D library might not be unloaded between runs
     SetRandomSeed(1);
     #endif
+    
+    // Set the main thread ID (assuming the Context is created in it)
+    Thread::SetMainThread();
 }
 
 Context::~Context()

+ 1 - 1
Source/Engine/Core/Context.h

@@ -58,7 +58,7 @@ public:
     void UpdateAttributeDefaultValue(StringHash objectType, const char* name, const Variant& defaultValue);
     /// Return a preallocated map for event data. Used for optimization to avoid constant re-allocation of event data maps.
     VariantMap& GetEventDataMap();
-    
+
     /// Copy base class attributes to derived class.
     void CopyBaseAttributes(StringHash baseType, StringHash derivedType);
     /// Template version of registering an object factory.

+ 8 - 0
Source/Engine/Core/Object.cpp

@@ -22,6 +22,8 @@
 
 #include "Precompiled.h"
 #include "Context.h"
+#include "Log.h"
+#include "Thread.h"
 
 #include "DebugNew.h"
 
@@ -222,6 +224,12 @@ void Object::SendEvent(StringHash eventType)
 
 void Object::SendEvent(StringHash eventType, VariantMap& eventData)
 {
+    if (!Thread::IsMainThread())
+    {
+        LOGERROR("Sending events is only supported from the main thread");
+        return;
+    }
+    
     // Make a weak pointer to self to check for destruction during event handling
     WeakPtr<Object> self(this);
     Context* context = context_;

+ 18 - 9
Source/Engine/Core/Profiler.h

@@ -23,6 +23,7 @@
 #pragma once
 
 #include "Str.h"
+#include "Thread.h"
 #include "Timer.h"
 
 namespace Urho3D
@@ -34,7 +35,7 @@ class URHO3D_API ProfilerBlock
 public:
     /// Construct with the specified parent block and name.
     ProfilerBlock(ProfilerBlock* parent, const char* name) :
-        name_(name),
+        name_(0),
         time_(0),
         maxTime_(0),
         count_(0),
@@ -49,6 +50,12 @@ public:
         totalMaxTime_(0),
         totalCount_(0)
     {
+        if (name)
+        {
+            unsigned nameLength = String::CStringLength(name);
+            name_ = new char[nameLength + 1];
+            memcpy(name_, name, nameLength + 1);
+        }
     }
     
     /// Destruct. Free the child blocks.
@@ -59,6 +66,8 @@ public:
             delete *i;
             *i = 0;
         }
+        
+        delete name_;
     }
     
     /// Begin timing.
@@ -113,13 +122,6 @@ public:
     /// Return child block with the specified name.
     ProfilerBlock* GetChild(const char* name)
     {
-        // First check using string pointers only, then resort to actual strcmp
-        for (PODVector<ProfilerBlock*>::Iterator i = children_.Begin(); i != children_.End(); ++i)
-        {
-            if ((*i)->name_ == name)
-                return *i;
-        }
-        
         for (PODVector<ProfilerBlock*>::Iterator i = children_.Begin(); i != children_.End(); ++i)
         {
             if (!String::Compare((*i)->name_, name, true))
@@ -133,7 +135,7 @@ public:
     }
     
     /// Block name.
-    const char* name_;
+    char* name_;
     /// High-resolution timer for measuring the block duration.
     HiresTimer timer_;
     /// Time on current frame.
@@ -180,6 +182,10 @@ public:
     /// Begin timing a profiling block.
     void BeginBlock(const char* name)
     {
+        // Profiler supports only the main thread currently
+        if (!Thread::IsMainThread())
+            return;
+        
         current_ = current_->GetChild(name);
         current_->Begin();
     }
@@ -187,6 +193,9 @@ public:
     /// End timing the current profiling block.
     void EndBlock()
     {
+        if (!Thread::IsMainThread())
+            return;
+        
         if (current_ != root_)
         {
             current_->End();

+ 21 - 0
Source/Engine/Core/Thread.cpp

@@ -51,6 +51,8 @@ void* ThreadFunctionStatic(void* data)
 }
 #endif
 
+ThreadID Thread::mainThreadID;
+
 Thread::Thread() :
     handle_(0),
     shouldRun_(false)
@@ -113,4 +115,23 @@ void Thread::SetPriority(int priority)
     #endif
 }
 
+void Thread::SetMainThread()
+{
+    mainThreadID = GetCurrentThreadID();
+}
+
+ThreadID Thread::GetCurrentThreadID()
+{
+    #ifdef WIN32
+    return GetCurrentThreadId();
+    #else
+    return pthread_self();
+    #endif
+}
+
+bool Thread::IsMainThread()
+{
+    return GetCurrentThreadID() == mainThreadID;
+}
+
 }

+ 17 - 0
Source/Engine/Core/Thread.h

@@ -24,6 +24,13 @@
 
 #include "Urho3D.h"
 
+#ifndef WIN32
+#include <pthread.h>
+typedef pthread_t ThreadID;
+#else
+typedef unsigned ThreadID;
+#endif
+
 namespace Urho3D
 {
 
@@ -48,12 +55,22 @@ public:
     
     /// Return whether thread exists.
     bool IsStarted() const { return handle_ != 0; }
+
+    /// Set the current thread as the main thread.
+    static void SetMainThread();
+    /// Return the current thread's ID.
+    static ThreadID GetCurrentThreadID();
+    /// Return whether is executing in the main thread.
+    static bool IsMainThread();
     
 protected:
     /// Thread handle.
     void* handle_;
     /// Running flag.
     volatile bool shouldRun_;
+    
+    /// Main thread's thread ID.
+    static ThreadID mainThreadID;
 };
 
 }

+ 3 - 4
Source/Engine/Core/WorkQueue.cpp

@@ -32,8 +32,6 @@
 namespace Urho3D
 {
 
-const unsigned MAX_NONTHREADED_WORK_USEC = 1000;
-
 /// Worker thread managed by the work queue.
 class WorkerThread : public Thread, public RefCounted
 {
@@ -69,7 +67,8 @@ WorkQueue::WorkQueue(Context* context) :
     pausing_(false),
     paused_(false),
     tolerance_(10),
-    lastSize_(0)
+    lastSize_(0),
+    maxNonThreadedWorkMs_(5)
 {
     SubscribeToEvent(E_BEGINFRAME, HANDLER(WorkQueue, HandleBeginFrame));
 }
@@ -343,7 +342,7 @@ void WorkQueue::HandleBeginFrame(StringHash eventType, VariantMap& eventData)
         
         HiresTimer timer;
         
-        while (!queue_.Empty() && timer.GetUSec(false) < MAX_NONTHREADED_WORK_USEC)
+        while (!queue_.Empty() && timer.GetUSec(false) < maxNonThreadedWorkMs_ * 1000)
         {
             WorkItem* item = queue_.Front();
             queue_.PopFront();

+ 6 - 0
Source/Engine/Core/WorkQueue.h

@@ -98,6 +98,8 @@ public:
     void Complete(unsigned priority);
     /// Set the pool telerance before it starts deleting pool items.
     void SetTolerance(int tolerance) { tolerance_ = tolerance; }
+    /// Set how many milliseconds maximum per frame to spend on low-priority work, when there are no worker threads.
+    void SetNonThreadedWorkMs(int ms) { maxNonThreadedWorkMs_ = Max(ms, 1); }
     
     /// Return number of worker threads.
     unsigned GetNumThreads() const { return threads_.Size(); }
@@ -105,6 +107,8 @@ public:
     bool IsCompleted(unsigned priority) const;
     /// Return the pool tolerance.
     int GetTolerance() const { return tolerance_; }
+    /// Return how many milliseconds maximum to spend on non-threaded low-priority work.
+    int GetNonThreadedWorkMs() const { return maxNonThreadedWorkMs_; }
     
 private:
     /// Process work items until shut down. Called by the worker threads.
@@ -136,6 +140,8 @@ private:
     int tolerance_;
     /// Last size of the shared pool.
     unsigned lastSize_;
+    /// Maximum milliseconds per frame to spend on low-priority work, when there are no worker threads.
+    int maxNonThreadedWorkMs_;
 };
 
 }

+ 2 - 4
Source/Engine/Graphics/Animation.cpp

@@ -73,10 +73,8 @@ void Animation::RegisterObject(Context* context)
     context->RegisterFactory<Animation>();
 }
 
-bool Animation::Load(Deserializer& source)
+bool Animation::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadAnimation);
-    
     unsigned memoryUse = sizeof(Animation);
     
     // Check ID
@@ -126,7 +124,7 @@ bool Animation::Load(Deserializer& source)
     ResourceCache* cache = GetSubsystem<ResourceCache>();
     String xmlName = ReplaceExtension(GetName(), ".xml");
     
-    XMLFile* file = cache->GetResource<XMLFile>(xmlName, false);
+    SharedPtr<XMLFile> file(cache->GetTempResource<XMLFile>(xmlName, false));
     if (file)
     {
         XMLElement rootElem = file->GetRoot();

+ 2 - 2
Source/Engine/Graphics/Animation.h

@@ -91,8 +91,8 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
     /// Save resource. Return true if successful.
     virtual bool Save(Serializer& dest) const;
     

+ 25 - 9
Source/Engine/Graphics/Direct3D9/D3D9Graphics.cpp

@@ -252,11 +252,15 @@ Graphics::Graphics(Context* context) :
 
 Graphics::~Graphics()
 {
-    // Release all GPU objects that still exist
-    for (Vector<GPUObject*>::Iterator i = gpuObjects_.Begin(); i != gpuObjects_.End(); ++i)
-        (*i)->Release();
-    gpuObjects_.Clear();
-    
+    {
+        MutexLock lock(gpuObjectMutex_);
+
+        // Release all GPU objects that still exist
+        for (Vector<GPUObject*>::Iterator i = gpuObjects_.Begin(); i != gpuObjects_.End(); ++i)
+            (*i)->Release();
+        gpuObjects_.Clear();
+    }
+
     vertexDeclarations_.Clear();
     
     if (impl_->defaultColorSurface_)
@@ -2218,11 +2222,15 @@ void Graphics::Minimize()
 
 void Graphics::AddGPUObject(GPUObject* object)
 {
+    MutexLock lock(gpuObjectMutex_);
+
     gpuObjects_.Push(object);
 }
 
 void Graphics::RemoveGPUObject(GPUObject* object)
 {
+    MutexLock lock(gpuObjectMutex_);
+
     gpuObjects_.Remove(object);
 }
 
@@ -2665,14 +2673,22 @@ void Graphics::OnDeviceLost()
         impl_->frameQuery_ = 0;
     }
     
-    for (unsigned i = 0; i < gpuObjects_.Size(); ++i)
-        gpuObjects_[i]->OnDeviceLost();
+    {
+        MutexLock lock(gpuObjectMutex_);
+
+        for (Vector<GPUObject*>::Iterator i = gpuObjects_.Begin(); i != gpuObjects_.End(); ++i)
+            (*i)->OnDeviceLost();
+    }
 }
 
 void Graphics::OnDeviceReset()
 {
-    for (unsigned i = 0; i < gpuObjects_.Size(); ++i)
-        gpuObjects_[i]->OnDeviceReset();
+    {
+        MutexLock lock(gpuObjectMutex_);
+
+        for (Vector<GPUObject*>::Iterator i = gpuObjects_.Begin(); i != gpuObjects_.End(); ++i)
+            (*i)->OnDeviceReset();
+    }
     
     // Get default surfaces
     impl_->device_->GetRenderTarget(0, &impl_->defaultColorSurface_);

+ 3 - 0
Source/Engine/Graphics/Direct3D9/D3D9Graphics.h

@@ -26,6 +26,7 @@
 #include "Color.h"
 #include "HashSet.h"
 #include "Image.h"
+#include "Mutex.h"
 #include "Object.h"
 #include "Plane.h"
 #include "Rect.h"
@@ -441,6 +442,8 @@ private:
     /// Initialize texture unit mappings.
     void SetTextureUnitMappings();
     
+    /// Mutex for accessing the GPU objects vector from several threads.
+    Mutex gpuObjectMutex_;
     /// Implementation.
     GraphicsImpl* impl_;
     /// Window title.

+ 3 - 13
Source/Engine/Graphics/Direct3D9/D3D9Texture.cpp

@@ -234,26 +234,16 @@ unsigned Texture::GetRowDataSize(int width) const
     }
 }
 
-void Texture::LoadParameters()
-{
-    ResourceCache* cache = GetSubsystem<ResourceCache>();
-    String xmlName = ReplaceExtension(GetName(), ".xml");
-    
-    XMLFile* file = cache->GetResource<XMLFile>(xmlName, false);
-    if (file)
-        LoadParameters(file);
-}
-
-void Texture::LoadParameters(XMLFile* file)
+void Texture::SetParameters(XMLFile* file)
 {
     if (!file)
         return;
     
     XMLElement rootElem = file->GetRoot();
-    LoadParameters(rootElem);
+    SetParameters(rootElem);
 }
 
-void Texture::LoadParameters(const XMLElement& element)
+void Texture::SetParameters(const XMLElement& element)
 {
     XMLElement paramElem = element.GetChild();
     while (paramElem)

+ 5 - 7
Source/Engine/Graphics/Direct3D9/D3D9Texture.h

@@ -97,13 +97,11 @@ public:
     unsigned GetDataSize(int width, int height, int depth) const;
     /// Return data size in bytes for a pixel or block row.
     unsigned GetRowDataSize(int width) const;
-    
-    /// Load parameters.
-    void LoadParameters();
-    /// Load parameters from an XML file.
-    void LoadParameters(XMLFile* xml);
-    /// Load parameters from an XML element.
-    void LoadParameters(const XMLElement& element);
+
+    /// Set additional parameters from an XML file.
+    void SetParameters(XMLFile* xml);
+    /// Set additional parameters from an XML element.
+    void SetParameters(const XMLElement& element);
     
 protected:
     /// Check whether texture memory budget has been exceeded. Free unused materials in that case to release the texture references.

+ 39 - 12
Source/Engine/Graphics/Direct3D9/D3D9Texture2D.cpp

@@ -22,6 +22,7 @@
 
 #include "Precompiled.h"
 #include "Context.h"
+#include "FileSystem.h"
 #include "Graphics.h"
 #include "GraphicsEvents.h"
 #include "GraphicsImpl.h"
@@ -30,6 +31,7 @@
 #include "Profiler.h"
 #include "ResourceCache.h"
 #include "Texture2D.h"
+#include "XMLFile.h"
 
 #include "DebugNew.h"
 
@@ -51,10 +53,8 @@ void Texture2D::RegisterObject(Context* context)
     context->RegisterFactory<Texture2D>();
 }
 
-bool Texture2D::Load(Deserializer& source)
+bool Texture2D::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadTexture2D);
-    
     // In headless mode, do not actually load the texture, just return success
     if (!graphics_)
         return true;
@@ -67,17 +67,42 @@ bool Texture2D::Load(Deserializer& source)
         return true;
     }
     
+    // Load the image data for EndLoad()
+    loadImage_ = new Image(context_);
+    if (!loadImage_->Load(source))
+    {
+        loadImage_.Reset();
+        return false;
+    }
+
+    // Precalculate mip levels if async loading
+    if (GetAsyncLoadState() == ASYNC_LOADING)
+        loadImage_->PrecalculateLevels();
+    
+    // Load the optional parameters file
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    String xmlName = ReplaceExtension(GetName(), ".xml");
+    loadParameters_ = cache->GetTempResource<XMLFile>(xmlName, false);
+    
+    return true;
+}
+
+bool Texture2D::EndLoad()
+{
+    // In headless mode, do not actually load the texture, just return success
+    if (!graphics_ || graphics_->IsDeviceLost())
+        return true;
+    
     // If over the texture budget, see if materials can be freed to allow textures to be freed
     CheckTextureBudget(GetTypeStatic());
+
+    SetParameters(loadParameters_);
+    bool success = SetData(loadImage_);
     
-    SharedPtr<Image> image(new Image(context_));
-    if (!image->Load(source))
-        return false;
-    
-    // Before actually loading the texture, get optional parameters from an XML description file
-    LoadParameters();
+    loadImage_.Reset();
+    loadParameters_.Reset();
     
-    return Load(image);
+    return success;
 }
 
 void Texture2D::OnDeviceLost()
@@ -94,7 +119,7 @@ void Texture2D::OnDeviceReset()
         ResourceCache* cache = GetSubsystem<ResourceCache>();
         if (cache->Exists(GetName()))
             dataLost_ = !cache->ReloadResource(this);
-
+        
         if (!object_)
         {
             Create();
@@ -173,6 +198,8 @@ bool Texture2D::SetSize(int width, int height, unsigned format, TextureUsage usa
 
 bool Texture2D::SetData(unsigned level, int x, int y, int width, int height, const void* data)
 {
+    PROFILE(SetTextureData);
+    
     if (!object_)
     {
         LOGERROR("No texture created, can not set data");
@@ -283,7 +310,7 @@ bool Texture2D::SetData(unsigned level, int x, int y, int width, int height, con
     return true;
 }
 
-bool Texture2D::Load(SharedPtr<Image> image, bool useAlpha)
+bool Texture2D::SetData(SharedPtr<Image> image, bool useAlpha)
 {
     if (!image)
     {

+ 11 - 4
Source/Engine/Graphics/Direct3D9/D3D9Texture2D.h

@@ -30,6 +30,7 @@ namespace Urho3D
 {
 
 class Image;
+class XMLFile;
 
 /// 2D texture resource.
 class URHO3D_API Texture2D : public Texture
@@ -44,8 +45,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     /// Release default pool resources.
     virtual void OnDeviceLost();
     /// Recreate default pool resources.
@@ -57,8 +60,8 @@ public:
     bool SetSize(int width, int height, unsigned format, TextureUsage usage = TEXTURE_STATIC);
     /// Set data either partially or fully on a mip level. Return true if successful.
     bool SetData(unsigned level, int x, int y, int width, int height, const void* data);
-    /// Load from an image. Return true if successful. Optionally make a single channel image alpha-only.
-    bool Load(SharedPtr<Image> image, bool useAlpha = false);
+    /// Set data from an image. Return true if successful. Optionally make a single channel image alpha-only.
+    bool SetData(SharedPtr<Image> image, bool useAlpha = false);
     
     /// Get data from a mip level. The destination buffer must be big enough. Return true if successful.
     bool GetData(unsigned level, void* dest) const;
@@ -73,6 +76,10 @@ private:
     
     /// Render surface.
     SharedPtr<RenderSurface> renderSurface_;
+    /// Image file acquired during BeginLoad.
+    SharedPtr<Image> loadImage_;
+    /// Parameter file acquired during BeginLoad.
+    SharedPtr<XMLFile> loadParameters_;
 };
 
 }

+ 48 - 20
Source/Engine/Graphics/Direct3D9/D3D9Texture3D.cpp

@@ -53,10 +53,10 @@ void Texture3D::RegisterObject(Context* context)
     context->RegisterFactory<Texture3D>();
 }
 
-bool Texture3D::Load(Deserializer& source)
+bool Texture3D::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadTexture3D);
-    
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+
     // In headless mode, do not actually load the texture, just return success
     if (!graphics_)
         return true;
@@ -69,20 +69,19 @@ bool Texture3D::Load(Deserializer& source)
         return true;
     }
     
-    // If over the texture budget, see if materials can be freed to allow textures to be freed
-    CheckTextureBudget(GetTypeStatic());
-
-    // Before actually loading the texture, get optional parameters from an XML description file
-    LoadParameters();
-
     String texPath, texName, texExt;
     SplitPath(GetName(), texPath, texName, texExt);
     
-    SharedPtr<XMLFile> xml(new XMLFile(context_));
-    if (!xml->Load(source))
-        return false;
+    cache->ResetDependencies(this);
 
-    XMLElement textureElem = xml->GetRoot();
+    loadParameters_ = new XMLFile(context_);
+    if (!loadParameters_->Load(source))
+    {
+        loadParameters_.Reset();
+        return false;
+    }
+    
+    XMLElement textureElem = loadParameters_->GetRoot();
     XMLElement volumeElem = textureElem.GetChild("volume");
     XMLElement colorlutElem = textureElem.GetChild("colorlut");
 
@@ -96,8 +95,11 @@ bool Texture3D::Load(Deserializer& source)
         if (volumeTexPath.Empty())
             name = texPath + name;
 
-        SharedPtr<Image> image(GetSubsystem<ResourceCache>()->GetTempResource<Image>(name));
-        return Load(image);
+        loadImage_ = cache->GetTempResource<Image>(name);
+        // Precalculate mip levels if async loading
+        if (loadImage_ && GetAsyncLoadState() == ASYNC_LOADING)
+            loadImage_->PrecalculateLevels();
+        cache->StoreResourceDependency(this, name);
     }
     else if (colorlutElem)
     {
@@ -110,16 +112,40 @@ bool Texture3D::Load(Deserializer& source)
             name = texPath + name;
 
         SharedPtr<File> file = GetSubsystem<ResourceCache>()->GetFile(name);
-        SharedPtr<Image> image(new Image(context_));
-        if (!image->LoadColorLUT(*(file.Get())))
+        loadImage_ = new Image(context_);
+        if (!loadImage_->LoadColorLUT(*(file.Get())))
+        {
+            loadParameters_.Reset();
+            loadImage_.Reset();
             return false;
-
-        return Load(image);
+        }
+        // Precalculate mip levels if async loading
+        if (loadImage_ && GetAsyncLoadState() == ASYNC_LOADING)
+            loadImage_->PrecalculateLevels();
+        cache->StoreResourceDependency(this, name);
     }
 
     return false;
 }
 
+bool Texture3D::EndLoad()
+{
+    // In headless mode, do not actually load the texture, just return success
+    if (!graphics_ || graphics_->IsDeviceLost())
+        return true;
+    
+    // If over the texture budget, see if materials can be freed to allow textures to be freed
+    CheckTextureBudget(GetTypeStatic());
+
+    SetParameters(loadParameters_);
+    bool success = SetData(loadImage_);
+    
+    loadImage_.Reset();
+    loadParameters_.Reset();
+    
+    return success;
+}
+
 void Texture3D::OnDeviceLost()
 {
     if (pool_ == D3DPOOL_DEFAULT)
@@ -211,6 +237,8 @@ bool Texture3D::SetSize(int width, int height, int depth, unsigned format, Textu
 
 bool Texture3D::SetData(unsigned level, int x, int y, int z, int width, int height, int depth, const void* data)
 {
+    PROFILE(SetTextureData);
+    
     if (!object_)
     {
         LOGERROR("No texture created, can not set data");
@@ -333,7 +361,7 @@ bool Texture3D::SetData(unsigned level, int x, int y, int z, int width, int heig
     return true;
 }
 
-bool Texture3D::Load(SharedPtr<Image> image, bool useAlpha)
+bool Texture3D::SetData(SharedPtr<Image> image, bool useAlpha)
 {
     if (!image)
     {

+ 10 - 4
Source/Engine/Graphics/Direct3D9/D3D9Texture3D.h

@@ -44,8 +44,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     /// Release default pool resources.
     virtual void OnDeviceLost();
     /// Recreate default pool resources.
@@ -57,8 +59,8 @@ public:
     bool SetSize(int width, int height, int depth, unsigned format, TextureUsage usage = TEXTURE_STATIC);
     /// Set data either partially or fully on a mip level. Return true if successful.
     bool SetData(unsigned level, int x, int y, int z, int width, int height, int depth, const void* data);
-    /// Load from an image. Return true if successful. Optionally make a single channel image alpha-only.
-    bool Load(SharedPtr<Image> image, bool useAlpha = false);
+    /// Set data from an image. Return true if successful. Optionally make a single channel image alpha-only.
+    bool SetData(SharedPtr<Image> image, bool useAlpha = false);
     
     /// Get data from a mip level. The destination buffer must be big enough. Return true if successful.
     bool GetData(unsigned level, void* dest) const;
@@ -73,6 +75,10 @@ private:
     
     /// Render surface.
     SharedPtr<RenderSurface> renderSurface_;
+    /// Image file acquired during BeginLoad.
+    SharedPtr<Image> loadImage_;
+    /// Parameter file acquired during BeginLoad.
+    SharedPtr<XMLFile> loadParameters_;
 };
 
 }

+ 86 - 58
Source/Engine/Graphics/Direct3D9/D3D9TextureCube.cpp

@@ -65,6 +65,87 @@ void TextureCube::RegisterObject(Context* context)
     context->RegisterFactory<TextureCube>();
 }
 
+bool TextureCube::BeginLoad(Deserializer& source)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    
+    // In headless mode, do not actually load the texture, just return success
+    if (!graphics_)
+        return true;
+    
+    // If device is lost, retry later
+    if (graphics_->IsDeviceLost())
+    {
+        LOGWARNING("Texture load while device is lost");
+        dataPending_ = true;
+        return true;
+    }
+    
+    cache->ResetDependencies(this);
+
+    String texPath, texName, texExt;
+    SplitPath(GetName(), texPath, texName, texExt);
+    
+    loadParameters_ = (new XMLFile(context_));
+    if (!loadParameters_->Load(source))
+    {
+        loadParameters_.Reset();
+        return false;
+    }
+    
+    loadImages_.Clear();
+
+    XMLElement textureElem = loadParameters_->GetRoot();
+    XMLElement faceElem = textureElem.GetChild("face");
+    while (faceElem)
+    {
+        String name = faceElem.GetAttribute("name");
+        
+        String faceTexPath, faceTexName, faceTexExt;
+        SplitPath(name, faceTexPath, faceTexName, faceTexExt);
+        // If path is empty, add the XML file path
+        if (faceTexPath.Empty())
+            name = texPath + name;
+        
+        loadImages_.Push(cache->GetTempResource<Image>(name));
+        cache->StoreResourceDependency(this, name);
+        
+        faceElem = faceElem.GetNext("face");
+    }
+
+    // Precalculate mip levels if async loading
+    if (GetAsyncLoadState() == ASYNC_LOADING)
+    {
+        for (unsigned i = 0; i < loadImages_.Size(); ++i)
+        {
+            if (loadImages_[i])
+                loadImages_[i]->PrecalculateLevels();
+        }
+    }
+
+    return true;
+}
+
+bool TextureCube::EndLoad()
+{
+    // In headless mode, do not actually load the texture, just return success
+    if (!graphics_ || graphics_->IsDeviceLost())
+        return true;
+    
+    // If over the texture budget, see if materials can be freed to allow textures to be freed
+    CheckTextureBudget(GetTypeStatic());
+
+    SetParameters(loadParameters_);
+    
+    for (unsigned i = 0; i < loadImages_.Size() && i < MAX_CUBEMAP_FACES; ++i)
+        SetData((CubeMapFace)i, loadImages_[i]);
+    
+    loadImages_.Clear();
+    loadParameters_.Reset();
+    
+    return true;
+}
+
 void TextureCube::OnDeviceLost()
 {
     if (pool_ == D3DPOOL_DEFAULT)
@@ -169,6 +250,8 @@ bool TextureCube::SetSize(int size, unsigned format, TextureUsage usage)
 
 bool TextureCube::SetData(CubeMapFace face, unsigned level, int x, int y, int width, int height, const void* data)
 {
+    PROFILE(SetTextureData);
+    
     if (!object_)
     {
         LOGERROR("No texture created, can not set data");
@@ -280,71 +363,16 @@ bool TextureCube::SetData(CubeMapFace face, unsigned level, int x, int y, int wi
     return true;
 }
 
-bool TextureCube::Load(Deserializer& source)
+bool TextureCube::SetData(CubeMapFace face, Deserializer& source)
 {
-    PROFILE(LoadTextureCube);
-    
-    ResourceCache* cache = GetSubsystem<ResourceCache>();
-    
-    // In headless mode, do not actually load the texture, just return success
-    if (!graphics_)
-        return true;
-    
-    // If device is lost, retry later
-    if (graphics_->IsDeviceLost())
-    {
-        LOGWARNING("Texture load while device is lost");
-        dataPending_ = true;
-        return true;
-    }
-    
-    // If over the texture budget, see if materials can be freed to allow textures to be freed
-    CheckTextureBudget(GetTypeStatic());
-    
-    String texPath, texName, texExt;
-    SplitPath(GetName(), texPath, texName, texExt);
-    
-    SharedPtr<XMLFile> xml(new XMLFile(context_));
-    if (!xml->Load(source))
-        return false;
-    
-    LoadParameters(xml);
-    
-    XMLElement textureElem = xml->GetRoot();
-    XMLElement faceElem = textureElem.GetChild("face");
-    unsigned faces = 0;
-    while (faceElem && faces < MAX_CUBEMAP_FACES)
-    {
-        String name = faceElem.GetAttribute("name");
-        
-        String faceTexPath, faceTexName, faceTexExt;
-        SplitPath(name, faceTexPath, faceTexName, faceTexExt);
-        // If path is empty, add the XML file path
-        if (faceTexPath.Empty())
-            name = texPath + name;
-        
-        SharedPtr<Image> image(cache->GetTempResource<Image>(name));
-        Load((CubeMapFace)faces, image);
-        faces++;
-        
-        faceElem = faceElem.GetNext("face");
-    }
-    
-    return true;
-}
-
-bool TextureCube::Load(CubeMapFace face, Deserializer& source)
-{
-    PROFILE(LoadTextureCube);
-    
     SharedPtr<Image> image(new Image(context_));
     if (!image->Load(source))
         return false;
     
-    return Load(face, image);
+    return SetData(face, image);
 }
 
-bool TextureCube::Load(CubeMapFace face, SharedPtr<Image> image, bool useAlpha)
+bool TextureCube::SetData(CubeMapFace face, SharedPtr<Image> image, bool useAlpha)
 {
     if (!image)
     {

+ 12 - 6
Source/Engine/Graphics/Direct3D9/D3D9TextureCube.h

@@ -45,8 +45,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     /// Release default pool resources.
     virtual void OnDeviceLost();
     /// ReCreate default pool resources.
@@ -58,10 +60,10 @@ public:
     bool SetSize(int size, unsigned format, TextureUsage usage = TEXTURE_STATIC);
     /// Set data either partially or fully on a face's mip level. Return true if successful.
     bool SetData(CubeMapFace face, unsigned level, int x, int y, int width, int height, const void* data);
-    /// Load one face from a stream. Return true if successful.
-    bool Load(CubeMapFace face, Deserializer& source);
-    /// Load one face from an image. Return true if successful. Optionally make a single channel image alpha-only.
-    bool Load(CubeMapFace face, SharedPtr<Image> image, bool useAlpha = false);
+    /// Set data of one face from a stream. Return true if successful.
+    bool SetData(CubeMapFace face, Deserializer& source);
+    /// Set data of one face from an image. Return true if successful. Optionally make a single channel image alpha-only.
+    bool SetData(CubeMapFace face, SharedPtr<Image> image, bool useAlpha = false);
     
     /// Get data from a face's mip level. The destination buffer must be big enough. Return true if successful.
     bool GetData(CubeMapFace face, unsigned level, void* dest) const;
@@ -82,6 +84,10 @@ private:
     int lockedLevel_;
     /// Currently locked face.
     CubeMapFace lockedFace_;
+    /// Face image files acquired during BeginLoad.
+    Vector<SharedPtr<Image> > loadImages_;
+    /// Parameter file acquired during BeginLoad.
+    SharedPtr<XMLFile> loadParameters_;
 };
 
 }

+ 61 - 7
Source/Engine/Graphics/Material.cpp

@@ -165,26 +165,73 @@ void Material::RegisterObject(Context* context)
     context->RegisterFactory<Material>();
 }
 
-bool Material::Load(Deserializer& source)
+bool Material::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadMaterial);
-
     // In headless mode, do not actually load the material, just return success
     Graphics* graphics = GetSubsystem<Graphics>();
     if (!graphics)
         return true;
 
-    SharedPtr<XMLFile> xml(new XMLFile(context_));
-    if (!xml->Load(source))
+    loadXMLFile_ = new XMLFile(context_);
+    if (loadXMLFile_->Load(source))
+    {
+        // If async loading, scan the XML content beforehand for technique & texture resources
+        // and request them to also be loaded. Can not do anything else at this point
+        if (GetAsyncLoadState() == ASYNC_LOADING)
+        {
+            ResourceCache* cache = GetSubsystem<ResourceCache>();
+            XMLElement rootElem = loadXMLFile_->GetRoot();
+            XMLElement techniqueElem = rootElem.GetChild("technique");
+            while (techniqueElem)
+            {
+                cache->BackgroundLoadResource<Technique>(techniqueElem.GetAttribute("name"), true, this);
+                techniqueElem = techniqueElem.GetNext("technique");
+            }
+
+            XMLElement textureElem = rootElem.GetChild("texture");
+            while (textureElem)
+            {
+                String name = textureElem.GetAttribute("name");
+                // Detect cube maps by file extension: they are defined by an XML file
+                /// \todo Differentiate with 3D textures by actually reading the XML content
+                if (GetExtension(name) == ".xml")
+                    cache->BackgroundLoadResource<TextureCube>(name, true, this);
+                else
+                    cache->BackgroundLoadResource<Texture2D>(name, true, this);
+                textureElem = textureElem.GetNext("texture");
+            }
+        }
+
+        return true;
+    }
+    else
     {
         ResetToDefaults();
+        loadXMLFile_.Reset();
         return false;
     }
+}
+
+bool Material::EndLoad()
+{
+    // In headless mode, do not actually load the material, just return success
+    Graphics* graphics = GetSubsystem<Graphics>();
+    if (!graphics)
+        return true;
 
-    XMLElement rootElem = xml->GetRoot();
-    return Load(rootElem);
+    bool success = false;
+    if (loadXMLFile_)
+    {
+        // If async loading, get the techniques / textures which should be ready now
+        XMLElement rootElem = loadXMLFile_->GetRoot();
+        success = Load(rootElem);
+    }
+
+    loadXMLFile_.Reset();
+    return success;
 }
 
+
 bool Material::Save(Serializer& dest) const
 {
     SharedPtr<XMLFile> xml(new XMLFile(context_));
@@ -208,6 +255,7 @@ bool Material::Load(const XMLElement& source)
 
     XMLElement techniqueElem = source.GetChild("technique");
     techniques_.Clear();
+    
     while (techniqueElem)
     {
         Technique* tech = cache->GetResource<Technique>(techniqueElem.GetAttribute("name"));
@@ -221,6 +269,7 @@ bool Material::Load(const XMLElement& source)
                 newTechnique.lodDistance_ = techniqueElem.GetFloat("loddistance");
             techniques_.Push(newTechnique);
         }
+
         techniqueElem = techniqueElem.GetNext("technique");
     }
 
@@ -236,6 +285,7 @@ bool Material::Load(const XMLElement& source)
         {
             String name = textureElem.GetAttribute("name");
             // Detect cube maps by file extension: they are defined by an XML file
+            /// \todo Differentiate with 3D textures by actually reading the XML content
             if (GetExtension(name) == ".xml")
                 SetTexture(unit, cache->GetResource<TextureCube>(name));
             else
@@ -662,6 +712,10 @@ void Material::CheckOcclusion()
 
 void Material::ResetToDefaults()
 {
+    // Needs to be a no-op when async loading, as this does a GetResource() which is not allowed from worker threads
+    if (!Thread::IsMainThread())
+        return;
+
     SetNumTechniques(1);
     SetTechnique(0, GetSubsystem<ResourceCache>()->GetResource<Technique>("Techniques/NoTexture.xml"));
 

+ 6 - 2
Source/Engine/Graphics/Material.h

@@ -102,8 +102,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
 
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     /// Save resource. Return true if successful.
     virtual bool Save(Serializer& dest) const;
 
@@ -222,6 +224,8 @@ private:
     bool specular_;
     /// Last animation update frame number.
     unsigned animationFrameNumber_;
+    /// XML file used while loading.
+    SharedPtr<XMLFile> loadXMLFile_;
 };
 
 }

+ 115 - 27
Source/Engine/Graphics/Model.cpp

@@ -73,10 +73,8 @@ void Model::RegisterObject(Context* context)
     context->RegisterFactory<Model>();
 }
 
-bool Model::Load(Deserializer& source)
+bool Model::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadModel);
-    
     // Check ID
     if (source.ReadFileID() != "UMDL")
     {
@@ -92,12 +90,14 @@ bool Model::Load(Deserializer& source)
     indexBuffers_.Clear();
     
     unsigned memoryUse = sizeof(Model);
-    
+    bool async = GetAsyncLoadState() == ASYNC_LOADING;
+
     // Read vertex buffers
     unsigned numVertexBuffers = source.ReadUInt();
     vertexBuffers_.Reserve(numVertexBuffers);
     morphRangeStarts_.Resize(numVertexBuffers);
     morphRangeCounts_.Resize(numVertexBuffers);
+    loadVBData_.Resize(numVertexBuffers);
     for (unsigned i = 0; i < numVertexBuffers; ++i)
     {
         unsigned vertexCount = source.ReadUInt();
@@ -106,14 +106,28 @@ bool Model::Load(Deserializer& source)
         morphRangeCounts_[i] = source.ReadUInt();
         
         SharedPtr<VertexBuffer> buffer(new VertexBuffer(context_));
-        buffer->SetShadowed(true);
-        buffer->SetSize(vertexCount, elementMask);
-        
-        void* dest = buffer->Lock(0, vertexCount);
-        unsigned vertexSize = buffer->GetVertexSize();
-        source.Read(dest, vertexCount * vertexSize);
-        buffer->Unlock();
-        
+        unsigned vertexSize = VertexBuffer::GetVertexSize(elementMask);
+
+        // Prepare vertex buffer data to be uploaded during EndLoad()
+        if (async)
+        {
+            loadVBData_[i].vertexCount_ = vertexCount;
+            loadVBData_[i].elementMask_ = elementMask;
+            loadVBData_[i].dataSize_ = vertexCount * vertexSize;
+            loadVBData_[i].data_ = new unsigned char[loadVBData_[i].dataSize_];
+            source.Read(loadVBData_[i].data_.Get(), loadVBData_[i].dataSize_);
+        }
+        else
+        {
+            // If not async loading, use locking to avoid extra allocation & copy
+            loadVBData_[i].data_.Reset(); // Make sure no previous data
+            buffer->SetShadowed(true);
+            buffer->SetSize(vertexCount, elementMask);
+            void* dest = buffer->Lock(0, vertexCount);
+            source.Read(dest, vertexCount * vertexSize);
+            buffer->Unlock();
+        }
+
         memoryUse += sizeof(VertexBuffer) + vertexCount * vertexSize;
         vertexBuffers_.Push(buffer);
     }
@@ -121,19 +135,34 @@ bool Model::Load(Deserializer& source)
     // Read index buffers
     unsigned numIndexBuffers = source.ReadUInt();
     indexBuffers_.Reserve(numIndexBuffers);
+    loadIBData_.Resize(numIndexBuffers);
     for (unsigned i = 0; i < numIndexBuffers; ++i)
     {
         unsigned indexCount = source.ReadUInt();
         unsigned indexSize = source.ReadUInt();
         
         SharedPtr<IndexBuffer> buffer(new IndexBuffer(context_));
-        buffer->SetShadowed(true);
-        buffer->SetSize(indexCount, indexSize > sizeof(unsigned short));
-        
-        void* dest = buffer->Lock(0, indexCount);
-        source.Read(dest, indexCount * indexSize);
-        buffer->Unlock();
-        
+
+        // Prepare index buffer data to be uploaded during EndLoad()
+        if (async)
+        {
+            loadIBData_[i].indexCount_ = indexCount;
+            loadIBData_[i].indexSize_ = indexSize;
+            loadIBData_[i].dataSize_ = indexCount * indexSize;
+            loadIBData_[i].data_ = new unsigned char[loadIBData_[i].dataSize_];
+            source.Read(loadIBData_[i].data_.Get(), loadIBData_[i].dataSize_);
+        }
+        else
+        {
+            // If not async loading, use locking to avoid extra allocation & copy
+            loadIBData_[i].data_.Reset(); // Make sure no previous data
+            buffer->SetShadowed(true);
+            buffer->SetSize(indexCount, indexSize > sizeof(unsigned short));
+            void* dest = buffer->Lock(0, indexCount);
+            source.Read(dest, indexCount * indexSize);
+            buffer->Unlock();
+        }
+
         memoryUse += sizeof(IndexBuffer) + indexCount * indexSize;
         indexBuffers_.Push(buffer);
     }
@@ -143,6 +172,7 @@ bool Model::Load(Deserializer& source)
     geometries_.Reserve(numGeometries);
     geometryBoneMappings_.Reserve(numGeometries);
     geometryCenters_.Reserve(numGeometries);
+    loadGeometries_.Resize(numGeometries);
     for (unsigned i = 0; i < numGeometries; ++i)
     {
         // Read bone mappings
@@ -155,34 +185,45 @@ bool Model::Load(Deserializer& source)
         unsigned numLodLevels = source.ReadUInt();
         Vector<SharedPtr<Geometry> > geometryLodLevels;
         geometryLodLevels.Reserve(numLodLevels);
+        loadGeometries_[i].Resize(numLodLevels);
         
         for (unsigned j = 0; j < numLodLevels; ++j)
         {
             float distance = source.ReadFloat();
             PrimitiveType type = (PrimitiveType)source.ReadUInt();
             
-            unsigned vertexBufferRef = source.ReadUInt();
-            unsigned indexBufferRef = source.ReadUInt();
+            unsigned vbRef = source.ReadUInt();
+            unsigned ibRef = source.ReadUInt();
             unsigned indexStart = source.ReadUInt();
             unsigned indexCount = source.ReadUInt();
             
-            if (vertexBufferRef >= vertexBuffers_.Size())
+            if (vbRef >= vertexBuffers_.Size())
             {
                 LOGERROR("Vertex buffer index out of bounds");
+                loadVBData_.Clear();
+                loadIBData_.Clear();
+                loadGeometries_.Clear();
                 return false;
             }
-            if (indexBufferRef >= indexBuffers_.Size())
+            if (ibRef >= indexBuffers_.Size())
             {
                 LOGERROR("Index buffer index out of bounds");
+                loadVBData_.Clear();
+                loadIBData_.Clear();
+                loadGeometries_.Clear();
                 return false;
             }
             
             SharedPtr<Geometry> geometry(new Geometry(context_));
-            geometry->SetVertexBuffer(0, vertexBuffers_[vertexBufferRef]);
-            geometry->SetIndexBuffer(indexBuffers_[indexBufferRef]);
-            geometry->SetDrawRange(type, indexStart, indexCount);
             geometry->SetLodDistance(distance);
-            
+
+            // Prepare geometry to be defined during EndLoad()
+            loadGeometries_[i][j].type_ = type;
+            loadGeometries_[i][j].vbRef_ = vbRef;
+            loadGeometries_[i][j].ibRef_ = ibRef;
+            loadGeometries_[i][j].indexStart_ = indexStart;
+            loadGeometries_[i][j].indexCount_ = indexCount;
+
             geometryLodLevels.Push(geometry);
             memoryUse += sizeof(Geometry);
         }
@@ -250,6 +291,53 @@ bool Model::Load(Deserializer& source)
     return true;
 }
 
+bool Model::EndLoad()
+{
+    // Upload vertex buffer data
+    for (unsigned i = 0; i < vertexBuffers_.Size(); ++i)
+    {
+        VertexBuffer* buffer = vertexBuffers_[i];
+        VertexBufferDesc& desc = loadVBData_[i];
+        if (desc.data_)
+        {
+            buffer->SetShadowed(true);
+            buffer->SetSize(desc.vertexCount_, desc.elementMask_);
+            buffer->SetData(desc.data_.Get());
+        }
+    }
+
+    // Upload index buffer data
+    for (unsigned i = 0; i < indexBuffers_.Size(); ++i)
+    {
+        IndexBuffer* buffer = indexBuffers_[i];
+        IndexBufferDesc& desc = loadIBData_[i];
+        if (desc.data_)
+        {
+            buffer->SetShadowed(true);
+            buffer->SetSize(desc.indexCount_, desc.indexSize_ > sizeof(unsigned short));
+            buffer->SetData(desc.data_.Get());
+        }
+    }
+
+    // Set up geometries
+    for (unsigned i = 0; i < geometries_.Size(); ++i)
+    {
+        for (unsigned j = 0; j < geometries_[i].Size(); ++j)
+        {
+            Geometry* geometry = geometries_[i][j];
+            GeometryDesc& desc = loadGeometries_[i][j];
+            geometry->SetVertexBuffer(0, vertexBuffers_[desc.vbRef_]);
+            geometry->SetIndexBuffer(indexBuffers_[desc.ibRef_]);
+            geometry->SetDrawRange(desc.type_, desc.indexStart_, desc.indexCount_);
+        }
+    }
+
+    loadVBData_.Clear();
+    loadIBData_.Clear();
+    loadGeometries_.Clear();
+    return true;
+}
+
 bool Model::Save(Serializer& dest) const
 {
     // Write ID

+ 52 - 2
Source/Engine/Graphics/Model.h

@@ -24,6 +24,7 @@
 
 #include "ArrayPtr.h"
 #include "BoundingBox.h"
+#include "GraphicsDefs.h"
 #include "Skeleton.h"
 #include "Resource.h"
 #include "Ptr.h"
@@ -62,6 +63,47 @@ struct ModelMorph
     HashMap<unsigned, VertexBufferMorph> buffers_;
 };
 
+/// Description of vertex buffer data for asynchronous loading.
+struct VertexBufferDesc
+{
+    /// Vertex count.
+    unsigned vertexCount_;
+    /// Element mask.
+    unsigned elementMask_;
+    /// Vertex data size.
+    unsigned dataSize_;
+    /// Vertex data.
+    SharedArrayPtr<unsigned char> data_;
+};
+
+/// Description of index buffer data for asynchronous loading.
+struct IndexBufferDesc
+{
+    /// Index count.
+    unsigned indexCount_;
+    /// Index size.
+    unsigned indexSize_;
+    /// Index data size.
+    unsigned dataSize_;
+    /// Index data.
+    SharedArrayPtr<unsigned char> data_;
+};
+
+/// Description of a geometry for asynchronous loading.
+struct GeometryDesc
+{
+    /// Primitive type.
+    PrimitiveType type_;
+    /// Vertex buffer ref.
+    unsigned vbRef_;
+    /// Index buffer ref.
+    unsigned ibRef_;
+    /// Index start.
+    unsigned indexStart_;
+    /// Index count.
+    unsigned indexCount_;
+};
+
 /// 3D model resource.
 class URHO3D_API Model : public Resource
 {
@@ -75,8 +117,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     /// Save resource. Return true if successful.
     virtual bool Save(Serializer& dest) const;
     
@@ -159,6 +203,12 @@ private:
     PODVector<unsigned> morphRangeStarts_;
     /// Vertex buffer morph range vertex count.
     PODVector<unsigned> morphRangeCounts_;
+    /// Vertex buffer data for asynchronous loading.
+    Vector<VertexBufferDesc> loadVBData_;
+    /// Index buffer data for asynchronous loading.
+    Vector<IndexBufferDesc> loadIBData_;
+    /// Geometry definitions for asynchronous loading.
+    Vector<Vector<GeometryDesc> > loadGeometries_;
 };
 
 }

+ 27 - 13
Source/Engine/Graphics/OpenGL/OGLGraphics.cpp

@@ -1537,6 +1537,8 @@ void Graphics::SetTextureAnisotropy(unsigned level)
 
 void Graphics::SetTextureParametersDirty()
 {
+    MutexLock lock(gpuObjectMutex_);
+    
     for (Vector<GPUObject*>::Iterator i = gpuObjects_.Begin(); i != gpuObjects_.End(); ++i)
     {
         Texture* texture = dynamic_cast<Texture*>(*i);
@@ -2172,11 +2174,15 @@ void Graphics::WindowResized()
 
 void Graphics::AddGPUObject(GPUObject* object)
 {
+    MutexLock lock(gpuObjectMutex_);
+    
     gpuObjects_.Push(object);
 }
 
 void Graphics::RemoveGPUObject(GPUObject* object)
 {
+    MutexLock lock(gpuObjectMutex_);
+    
     gpuObjects_.Remove(object);
 }
 
@@ -2257,18 +2263,22 @@ void Graphics::Release(bool clearGPUObjects, bool closeWindow)
     
     releasingGPUObjects_ = true;
     
-    if (clearGPUObjects)
-    {
-        // Shutting down: release all GPU objects that still exist
-        for (Vector<GPUObject*>::Iterator i = gpuObjects_.Begin(); i != gpuObjects_.End(); ++i)
-            (*i)->Release();
-        gpuObjects_.Clear();
-    }
-    else
     {
-        // We are not shutting down, but recreating the context: mark GPU objects lost
-        for (Vector<GPUObject*>::Iterator i = gpuObjects_.Begin(); i != gpuObjects_.End(); ++i)
-            (*i)->OnDeviceLost();
+        MutexLock lock(gpuObjectMutex_);
+        
+        if (clearGPUObjects)
+        {
+            // Shutting down: release all GPU objects that still exist
+            for (Vector<GPUObject*>::Iterator i = gpuObjects_.Begin(); i != gpuObjects_.End(); ++i)
+                (*i)->Release();
+            gpuObjects_.Clear();
+        }
+        else
+        {
+            // We are not shutting down, but recreating the context: mark GPU objects lost
+            for (Vector<GPUObject*>::Iterator i = gpuObjects_.Begin(); i != gpuObjects_.End(); ++i)
+                (*i)->OnDeviceLost();
+        }
     }
     
     releasingGPUObjects_ = false;
@@ -2339,8 +2349,12 @@ void Graphics::Restore()
     glPixelStorei(GL_PACK_ALIGNMENT, 1);
     glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
     
-    for (Vector<GPUObject*>::Iterator i = gpuObjects_.Begin(); i != gpuObjects_.End(); ++i)
-        (*i)->OnDeviceReset();
+    {
+        MutexLock lock(gpuObjectMutex_);
+        
+        for (Vector<GPUObject*>::Iterator i = gpuObjects_.Begin(); i != gpuObjects_.End(); ++i)
+            (*i)->OnDeviceReset();
+    }
 }
 
 void Graphics::Maximize()

+ 3 - 0
Source/Engine/Graphics/OpenGL/OGLGraphics.h

@@ -26,6 +26,7 @@
 #include "Color.h"
 #include "GraphicsDefs.h"
 #include "Image.h"
+#include "Mutex.h"
 #include "Object.h"
 #include "Plane.h"
 #include "Rect.h"
@@ -449,6 +450,8 @@ private:
     /// Initialize texture unit mappings.
     void SetTextureUnitMappings();
     
+    /// Mutex for accessing the GPU objects vector from several threads.
+    Mutex gpuObjectMutex_;
     /// Implementation.
     GraphicsImpl* impl_;
     /// Window title.

+ 3 - 13
Source/Engine/Graphics/OpenGL/OGLTexture.cpp

@@ -393,26 +393,16 @@ unsigned Texture::GetDataType(unsigned format)
     #endif
 }
 
-void Texture::LoadParameters()
-{
-    ResourceCache* cache = GetSubsystem<ResourceCache>();
-    String xmlName = ReplaceExtension(GetName(), ".xml");
-    
-    XMLFile* file = cache->GetResource<XMLFile>(xmlName, false);
-    if (file)
-        LoadParameters(file);
-}
-
-void Texture::LoadParameters(XMLFile* file)
+void Texture::SetParameters(XMLFile* file)
 {
     if (!file)
         return;
     
     XMLElement rootElem = file->GetRoot();
-    LoadParameters(rootElem);
+    SetParameters(rootElem);
 }
 
-void Texture::LoadParameters(const XMLElement& elem)
+void Texture::SetParameters(const XMLElement& elem)
 {
     XMLElement paramElem = elem.GetChild();
     while (paramElem)

+ 5 - 7
Source/Engine/Graphics/OpenGL/OGLTexture.h

@@ -113,13 +113,11 @@ public:
     static unsigned GetExternalFormat(unsigned format);
     /// Return the data type corresponding to an OpenGL internal format.
     static unsigned GetDataType(unsigned format);
-    
-    /// Load parameters.
-    void LoadParameters();
-    /// Load parameters from an XML file.
-    void LoadParameters(XMLFile* xml);
-    /// Load parameters from an XML element.
-    void LoadParameters(const XMLElement& element);
+
+    /// Set additional parameters from an XML file.
+    void SetParameters(XMLFile* xml);
+    /// Set additional parameters from an XML element.
+    void SetParameters(const XMLElement& element);
     /// Return the corresponding SRGB texture format if supported. If not supported, return format unchanged.
     unsigned GetSRGBFormat(unsigned format);
     

+ 42 - 14
Source/Engine/Graphics/OpenGL/OGLTexture2D.cpp

@@ -3,7 +3,7 @@
 //
 // Permission is hereby granted, free of charge, to any person obtaining a copy
 // of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
+// in the Software without restriction, including without limitation the rightsR
 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 // copies of the Software, and to permit persons to whom the Software is
 // furnished to do so, subject to the following conditions:
@@ -22,14 +22,17 @@
 
 #include "Precompiled.h"
 #include "Context.h"
+#include "FileSystem.h"
 #include "Graphics.h"
 #include "GraphicsEvents.h"
 #include "GraphicsImpl.h"
+#include "Image.h"
 #include "Log.h"
 #include "Profiler.h"
 #include "Renderer.h"
 #include "ResourceCache.h"
 #include "Texture2D.h"
+#include "XMLFile.h"
 
 #include "DebugNew.h"
 
@@ -52,10 +55,8 @@ void Texture2D::RegisterObject(Context* context)
     context->RegisterFactory<Texture2D>();
 }
 
-bool Texture2D::Load(Deserializer& source)
+bool Texture2D::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadTexture2D);
-    
     // In headless mode, do not actually load the texture, just return success
     if (!graphics_)
         return true;
@@ -68,17 +69,42 @@ bool Texture2D::Load(Deserializer& source)
         return true;
     }
     
+    // Load the image data for EndLoad()
+    loadImage_ = new Image(context_);
+    if (!loadImage_->Load(source))
+    {
+        loadImage_.Reset();
+        return false;
+    }
+
+    // Precalculate mip levels if async loading
+    if (GetAsyncLoadState() == ASYNC_LOADING)
+        loadImage_->PrecalculateLevels();
+    
+    // Load the optional parameters file
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    String xmlName = ReplaceExtension(GetName(), ".xml");
+    loadParameters_ = cache->GetTempResource<XMLFile>(xmlName, false);
+    
+    return true;
+}
+
+bool Texture2D::EndLoad()
+{
+    // In headless mode, do not actually load the texture, just return success
+     if (!graphics_ || graphics_->IsDeviceLost())
+        return true;
+    
     // If over the texture budget, see if materials can be freed to allow textures to be freed
     CheckTextureBudget(GetTypeStatic());
+
+    SetParameters(loadParameters_);
+    bool success = SetData(loadImage_);
     
-    SharedPtr<Image> image(new Image(context_));
-    if (!image->Load(source))
-        return false;
-    
-    // Before actually loading the texture, get optional parameters from an XML description file
-    LoadParameters();
+    loadImage_.Reset();
+    loadParameters_.Reset();
     
-    return Load(image);
+    return success;
 }
 
 void Texture2D::OnDeviceLost()
@@ -170,6 +196,8 @@ bool Texture2D::SetSize(int width, int height, unsigned format, TextureUsage usa
 
 bool Texture2D::SetData(unsigned level, int x, int y, int width, int height, const void* data)
 {
+    PROFILE(SetTextureData);
+
     if (!object_ || !graphics_)
     {
         LOGERROR("No texture created, can not set data");
@@ -233,14 +261,14 @@ bool Texture2D::SetData(unsigned level, int x, int y, int width, int height, con
     return true;
 }
 
-bool Texture2D::Load(SharedPtr<Image> image, bool useAlpha)
+bool Texture2D::SetData(SharedPtr<Image> image, bool useAlpha)
 {
     if (!image)
     {
-        LOGERROR("Null image, can not load texture");
+        LOGERROR("Null image, can not set data");
         return false;
     }
-    
+
     unsigned memoryUse = sizeof(Texture2D);
     
     int quality = QUALITY_HIGH;

+ 13 - 5
Source/Engine/Graphics/OpenGL/OGLTexture2D.h

@@ -22,7 +22,6 @@
 
 #pragma once
 
-#include "Image.h"
 #include "RenderSurface.h"
 #include "Ptr.h"
 #include "Texture.h"
@@ -30,6 +29,9 @@
 namespace Urho3D
 {
 
+class Image;
+class XMLFile;
+
 /// 2D texture resource.
 class URHO3D_API Texture2D : public Texture
 {
@@ -43,8 +45,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     /// Mark the GPU resource destroyed on context destruction.
     virtual void OnDeviceLost();
     /// Recreate the GPU resource and restore data if applicable.
@@ -56,8 +60,8 @@ public:
     bool SetSize(int width, int height, unsigned format, TextureUsage usage = TEXTURE_STATIC);
     /// Set data either partially or fully on a mip level. Return true if successful.
     bool SetData(unsigned level, int x, int y, int width, int height, const void* data);
-    /// Load from an image. Return true if successful. Optionally make a single channel image alpha-only.
-    bool Load(SharedPtr<Image> image, bool useAlpha = false);
+    /// Set data from an image. Return true if successful. Optionally make a single channel image alpha-only.
+    bool SetData(SharedPtr<Image> image, bool useAlpha = false);
     
     /// Get data from a mip level. The destination buffer must be big enough. Return true if successful.
     bool GetData(unsigned level, void* dest) const;
@@ -74,6 +78,10 @@ private:
     
     /// Render surface.
     SharedPtr<RenderSurface> renderSurface_;
+    /// Image file acquired during BeginLoad.
+    SharedPtr<Image> loadImage_;
+    /// Parameter file acquired during BeginLoad.
+    SharedPtr<XMLFile> loadParameters_;
 };
 
 }

+ 51 - 22
Source/Engine/Graphics/OpenGL/OGLTexture3D.cpp

@@ -58,10 +58,10 @@ void Texture3D::RegisterObject(Context* context)
     context->RegisterFactory<Texture3D>();
 }
 
-bool Texture3D::Load(Deserializer& source)
+bool Texture3D::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadTexture3D);
-    
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+
     // In headless mode, do not actually load the texture, just return success
     if (!graphics_)
         return true;
@@ -74,20 +74,19 @@ bool Texture3D::Load(Deserializer& source)
         return true;
     }
     
-    // If over the texture budget, see if materials can be freed to allow textures to be freed
-    CheckTextureBudget(GetTypeStatic());
-
-    // Before actually loading the texture, get optional parameters from an XML description file
-    LoadParameters();
-
     String texPath, texName, texExt;
     SplitPath(GetName(), texPath, texName, texExt);
     
-    SharedPtr<XMLFile> xml(new XMLFile(context_));
-    if (!xml->Load(source))
-        return false;
+    cache->ResetDependencies(this);
 
-    XMLElement textureElem = xml->GetRoot();
+    loadParameters_ = new XMLFile(context_);
+    if (!loadParameters_->Load(source))
+    {
+        loadParameters_.Reset();
+        return false;
+    }
+    
+    XMLElement textureElem = loadParameters_->GetRoot();
     XMLElement volumeElem = textureElem.GetChild("volume");
     XMLElement colorlutElem = textureElem.GetChild("colorlut");
 
@@ -101,8 +100,11 @@ bool Texture3D::Load(Deserializer& source)
         if (volumeTexPath.Empty())
             name = texPath + name;
 
-        SharedPtr<Image> image(GetSubsystem<ResourceCache>()->GetTempResource<Image>(name));
-        return Load(image);
+        loadImage_ = cache->GetTempResource<Image>(name);
+        // Precalculate mip levels if async loading
+        if (loadImage_ && GetAsyncLoadState() == ASYNC_LOADING)
+            loadImage_->PrecalculateLevels();
+        cache->StoreResourceDependency(this, name);
     }
     else if (colorlutElem)
     {
@@ -115,16 +117,41 @@ bool Texture3D::Load(Deserializer& source)
             name = texPath + name;
 
         SharedPtr<File> file = GetSubsystem<ResourceCache>()->GetFile(name);
-        SharedPtr<Image> image(new Image(context_));
-        if (!image->LoadColorLUT(*(file.Get())))
+        loadImage_ = new Image(context_);
+        if (!loadImage_->LoadColorLUT(*(file.Get())))
+        {
+            loadParameters_.Reset();
+            loadImage_.Reset();
             return false;
-
-        return Load(image);
+        }
+        // Precalculate mip levels if async loading
+        if (loadImage_ && GetAsyncLoadState() == ASYNC_LOADING)
+            loadImage_->PrecalculateLevels();
+        cache->StoreResourceDependency(this, name);
     }
 
     return false;
 }
 
+
+bool Texture3D::EndLoad()
+{
+    // In headless mode, do not actually load the texture, just return success
+    if (!graphics_ || graphics_->IsDeviceLost())
+        return true;
+    
+    // If over the texture budget, see if materials can be freed to allow textures to be freed
+    CheckTextureBudget(GetTypeStatic());
+
+    SetParameters(loadParameters_);
+    bool success = SetData(loadImage_);
+    
+    loadImage_.Reset();
+    loadParameters_.Reset();
+    
+    return success;
+}
+
 void Texture3D::OnDeviceLost()
 {
     GPUObject::OnDeviceLost();
@@ -211,6 +238,8 @@ bool Texture3D::SetSize(int width, int height, int depth, unsigned format, Textu
 
 bool Texture3D::SetData(unsigned level, int x, int y, int z, int width, int height, int depth, const void* data)
 {
+    PROFILE(SetTextureData);
+
     if (!object_ || !graphics_)
     {
         LOGERROR("No texture created, can not set data");
@@ -277,14 +306,14 @@ bool Texture3D::SetData(unsigned level, int x, int y, int z, int width, int heig
     return true;
 }
 
-bool Texture3D::Load(SharedPtr<Image> image, bool useAlpha)
+bool Texture3D::SetData(SharedPtr<Image> image, bool useAlpha)
 {
     if (!image)
     {
-        LOGERROR("Null image, can not load texture");
+        LOGERROR("Null image, can not set data");
         return false;
     }
-    
+
     unsigned memoryUse = sizeof(Texture3D);
     
     int quality = QUALITY_HIGH;

+ 10 - 4
Source/Engine/Graphics/OpenGL/OGLTexture3D.h

@@ -43,8 +43,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     /// Mark the GPU resource destroyed on context destruction.
     virtual void OnDeviceLost();
     /// Recreate the GPU resource and restore data if applicable.
@@ -56,8 +58,8 @@ public:
     bool SetSize(int width, int height, int depth, unsigned format, TextureUsage usage = TEXTURE_STATIC);
     /// Set data either partially or fully on a mip level. Return true if successful.
     bool SetData(unsigned level, int x, int y, int z, int width, int height, int depth, const void* data);
-    /// Load from an image. Return true if successful. Optionally make a single channel image alpha-only.
-    bool Load(SharedPtr<Image> image, bool useAlpha = false);
+    /// Set data from an image. Return true if successful. Optionally make a single channel image alpha-only.
+    bool SetData(SharedPtr<Image> image, bool useAlpha = false);
     
     /// Get data from a mip level. The destination buffer must be big enough. Return true if successful.
     bool GetData(unsigned level, void* dest) const;
@@ -74,6 +76,10 @@ private:
     
     /// Render surface.
     SharedPtr<RenderSurface> renderSurface_;
+    /// Image file acquired during BeginLoad.
+    SharedPtr<Image> loadImage_;
+    /// Parameter file acquired during BeginLoad.
+    SharedPtr<XMLFile> loadParameters_;
 };
 
 }

+ 89 - 61
Source/Engine/Graphics/OpenGL/OGLTextureCube.cpp

@@ -66,10 +66,91 @@ void TextureCube::RegisterObject(Context* context)
     context->RegisterFactory<TextureCube>();
 }
 
+bool TextureCube::BeginLoad(Deserializer& source)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    
+    // In headless mode, do not actually load the texture, just return success
+    if (!graphics_)
+        return true;
+    
+    // If device is lost, retry later
+    if (graphics_->IsDeviceLost())
+    {
+        LOGWARNING("Texture load while device is lost");
+        dataPending_ = true;
+        return true;
+    }
+    
+    cache->ResetDependencies(this);
+
+    String texPath, texName, texExt;
+    SplitPath(GetName(), texPath, texName, texExt);
+    
+    loadParameters_ = (new XMLFile(context_));
+    if (!loadParameters_->Load(source))
+    {
+        loadParameters_.Reset();
+        return false;
+    }
+    
+    loadImages_.Clear();
+
+    XMLElement textureElem = loadParameters_->GetRoot();
+    XMLElement faceElem = textureElem.GetChild("face");
+    while (faceElem)
+    {
+        String name = faceElem.GetAttribute("name");
+        
+        String faceTexPath, faceTexName, faceTexExt;
+        SplitPath(name, faceTexPath, faceTexName, faceTexExt);
+        // If path is empty, add the XML file path
+        if (faceTexPath.Empty())
+            name = texPath + name;
+        
+        loadImages_.Push(cache->GetTempResource<Image>(name));
+        cache->StoreResourceDependency(this, name);
+        
+        faceElem = faceElem.GetNext("face");
+    }
+
+    // Precalculate mip levels if async loading
+    if (GetAsyncLoadState() == ASYNC_LOADING)
+    {
+        for (unsigned i = 0; i < loadImages_.Size(); ++i)
+        {
+            if (loadImages_[i])
+                loadImages_[i]->PrecalculateLevels();
+        }
+    }
+
+    return true;
+}
+
+bool TextureCube::EndLoad()
+{
+    // In headless mode, do not actually load the texture, just return success
+    if (!graphics_ || graphics_->IsDeviceLost())
+        return true;
+    
+    // If over the texture budget, see if materials can be freed to allow textures to be freed
+    CheckTextureBudget(GetTypeStatic());
+
+    SetParameters(loadParameters_);
+    
+    for (unsigned i = 0; i < loadImages_.Size() && i < MAX_CUBEMAP_FACES; ++i)
+        SetData((CubeMapFace)i, loadImages_[i]);
+    
+    loadImages_.Clear();
+    loadParameters_.Reset();
+    
+    return true;
+}
+
 void TextureCube::OnDeviceLost()
 {
     GPUObject::OnDeviceLost();
-    
+
     for (unsigned i = 0; i < MAX_CUBEMAP_FACES; ++i)
     {
         if (renderSurfaces_[i])
@@ -173,6 +254,8 @@ bool TextureCube::SetSize(int size, unsigned format, TextureUsage usage)
 
 bool TextureCube::SetData(CubeMapFace face, unsigned level, int x, int y, int width, int height, const void* data)
 {
+    PROFILE(SetTextureData);
+    
     if (!object_ || !graphics_)
     {
         LOGERROR("No texture created, can not set data");
@@ -240,78 +323,23 @@ bool TextureCube::SetData(CubeMapFace face, unsigned level, int x, int y, int wi
     return true;
 }
 
-bool TextureCube::Load(Deserializer& source)
+bool TextureCube::SetData(CubeMapFace face, Deserializer& source)
 {
-    PROFILE(LoadTextureCube);
-    
-    ResourceCache* cache = GetSubsystem<ResourceCache>();
-    
-    // In headless mode, do not actually load the texture, just return success
-    if (!graphics_)
-        return true;
-    
-    // If device is lost, retry later
-    if (graphics_->IsDeviceLost())
-    {
-        LOGWARNING("Texture load while device is lost");
-        dataPending_ = true;
-        return true;
-    }
-    
-    // If over the texture budget, see if materials can be freed to allow textures to be freed
-    CheckTextureBudget(GetTypeStatic());
-    
-    String texPath, texName, texExt;
-    SplitPath(GetName(), texPath, texName, texExt);
-    
-    SharedPtr<XMLFile> xml(new XMLFile(context_));
-    if (!xml->Load(source))
-        return false;
-    
-    LoadParameters(xml);
-    
-    XMLElement textureElem = xml->GetRoot();
-    XMLElement faceElem = textureElem.GetChild("face");
-    unsigned faces = 0;
-    while (faceElem && faces < MAX_CUBEMAP_FACES)
-    {
-        String name = faceElem.GetAttribute("name");
-        
-        String faceTexPath, faceTexName, faceTexExt;
-        SplitPath(name, faceTexPath, faceTexName, faceTexExt);
-        // If path is empty, add the XML file path
-        if (faceTexPath.Empty())
-            name = texPath + name;
-        
-        SharedPtr<Image> image(cache->GetTempResource<Image>(name));
-        Load((CubeMapFace)faces, image);
-        ++faces;
-        
-        faceElem = faceElem.GetNext("face");
-    }
-    
-    return true;
-}
-
-bool TextureCube::Load(CubeMapFace face, Deserializer& source)
-{
-    PROFILE(LoadTextureCube);
-    
     SharedPtr<Image> image(new Image(context_));
     if (!image->Load(source))
         return false;
     
-    return Load(face, image);
+    return SetData(face, image);
 }
 
-bool TextureCube::Load(CubeMapFace face, SharedPtr<Image> image, bool useAlpha)
+bool TextureCube::SetData(CubeMapFace face, SharedPtr<Image> image, bool useAlpha)
 {
     if (!image)
     {
-        LOGERROR("Null image, can not load texture");
+        LOGERROR("Null image, can not set face data");
         return false;
     }
-    
+
     unsigned memoryUse = 0;
     
     int quality = QUALITY_HIGH;

+ 12 - 6
Source/Engine/Graphics/OpenGL/OGLTextureCube.h

@@ -45,8 +45,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     /// Mark the GPU resource destroyed on context destruction.
     virtual void OnDeviceLost();
     /// Recreate the GPU resource and restore data if applicable.
@@ -58,10 +60,10 @@ public:
     bool SetSize(int size, unsigned format, TextureUsage usage = TEXTURE_STATIC);
     /// Set data either partially or fully on a face's mip level. Return true if successful.
     bool SetData(CubeMapFace face, unsigned level, int x, int y, int width, int height, const void* data);
-    /// Load one face from a stream. Return true if successful.
-    bool Load(CubeMapFace face, Deserializer& source);
-    /// Load one face from an image. Return true if successful. Optionally make a single channel image alpha-only.
-    bool Load(CubeMapFace face, SharedPtr<Image> image, bool useAlpha = false);
+    /// Set data of one face from a stream. Return true if successful.
+    bool SetData(CubeMapFace face, Deserializer& source);
+    /// Set data of one face from an image. Return true if successful. Optionally make a single channel image alpha-only.
+    bool SetData(CubeMapFace face, SharedPtr<Image> image, bool useAlpha = false);
     
     /// Get data from a face's mip level. The destination buffer must be big enough. Return true if successful.
     bool GetData(CubeMapFace face, unsigned level, void* dest) const;
@@ -80,6 +82,10 @@ private:
     SharedPtr<RenderSurface> renderSurfaces_[MAX_CUBEMAP_FACES];
     /// Memory use per face.
     unsigned faceMemoryUse_[MAX_CUBEMAP_FACES];
+    /// Face image files acquired during BeginLoad.
+    Vector<SharedPtr<Image> > loadImages_;
+    /// Parameter file acquired during BeginLoad.
+    SharedPtr<XMLFile> loadParameters_;
 };
 
 }

+ 21 - 2
Source/Engine/Graphics/ParticleEffect.cpp

@@ -90,8 +90,10 @@ void ParticleEffect::RegisterObject(Context* context)
     context->RegisterFactory<ParticleEffect>();
 }
 
-bool ParticleEffect::Load(Deserializer& source)
+bool ParticleEffect::BeginLoad(Deserializer& source)
 {
+    loadMaterialName_.Clear();
+
     XMLFile file(context_);
     if (!file.Load(source))
     {
@@ -140,7 +142,12 @@ bool ParticleEffect::Load(Deserializer& source)
     textureFrames_.Clear();
 
     if (rootElem.HasChild("material"))
-        SetMaterial(GetSubsystem<ResourceCache>()->GetResource<Material>(rootElem.GetChild("material").GetAttribute("name")));
+    {
+        loadMaterialName_ = rootElem.GetChild("material").GetAttribute("name");
+        // If async loading, can not GetResource() the material. But can do a background request for it
+        if (GetAsyncLoadState() == ASYNC_LOADING)
+            GetSubsystem<ResourceCache>()->BackgroundLoadResource<Material>(loadMaterialName_, true, this);
+    }
 
     if (rootElem.HasChild("numparticles"))
         SetNumParticles(rootElem.GetChild("numparticles").GetInt("value"));
@@ -273,6 +280,18 @@ bool ParticleEffect::Load(Deserializer& source)
     return true;
 }
 
+bool ParticleEffect::EndLoad()
+{
+    // Apply the material now
+    if (!loadMaterialName_.Empty())
+    {
+        SetMaterial(GetSubsystem<ResourceCache>()->GetResource<Material>(loadMaterialName_));
+        loadMaterialName_.Clear();
+    }
+
+    return true;
+}
+
 bool ParticleEffect::Save(Serializer& dest) const
 {
     XMLFile file(context_);

+ 6 - 2
Source/Engine/Graphics/ParticleEffect.h

@@ -111,8 +111,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
 
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     /// Save resource. Return true if successful.
     virtual bool Save(Serializer& dest) const;
 
@@ -337,6 +339,8 @@ private:
     Vector<ColorFrame> colorFrames_;
     /// Texture animation frames.
     Vector<TextureFrame> textureFrames_;
+    /// Material name acquired during BeginLoad().
+    String loadMaterialName_;
 };
 
 }

+ 7 - 4
Source/Engine/Graphics/Shader.cpp

@@ -81,10 +81,8 @@ void Shader::RegisterObject(Context* context)
     context->RegisterFactory<Shader>();
 }
 
-bool Shader::Load(Deserializer& source)
+bool Shader::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadShader);
-    
     Graphics* graphics = GetSubsystem<Graphics>();
     if (!graphics)
         return false;
@@ -108,13 +106,18 @@ bool Shader::Load(Deserializer& source)
     psSourceCode_.Replace("attribute ", "// attribute ");
     #endif
     
+    RefreshMemoryUse();
+    return true;
+}
+
+bool Shader::EndLoad()
+{
     // If variations had already been created, release them and require recompile
     for (HashMap<StringHash, SharedPtr<ShaderVariation> >::Iterator i = vsVariations_.Begin(); i != vsVariations_.End(); ++i)
         i->second_->Release();
     for (HashMap<StringHash, SharedPtr<ShaderVariation> >::Iterator i = psVariations_.Begin(); i != psVariations_.End(); ++i)
         i->second_->Release();
     
-    RefreshMemoryUse();
     return true;
 }
 

+ 4 - 2
Source/Engine/Graphics/Shader.h

@@ -43,8 +43,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     
     /// Return a variation with defines.
     ShaderVariation* GetVariation(ShaderType type, const String& defines);

+ 1 - 3
Source/Engine/Graphics/Technique.cpp

@@ -172,10 +172,8 @@ void Technique::RegisterObject(Context* context)
     context->RegisterFactory<Technique>();
 }
 
-bool Technique::Load(Deserializer& source)
+bool Technique::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadTechnique);
-    
     passes_.Clear();
     SetMemoryUse(sizeof(Technique));
     

+ 2 - 2
Source/Engine/Graphics/Technique.h

@@ -148,8 +148,8 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
     
     /// Set whether requires %Shader %Model 3.
     void SetIsSM3(bool enable);

+ 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>();

+ 46 - 0
Source/Engine/IO/Log.cpp

@@ -22,11 +22,13 @@
 
 #include "Precompiled.h"
 #include "Context.h"
+#include "CoreEvents.h"
 #include "File.h"
 #include "IOEvents.h"
 #include "Log.h"
 #include "Mutex.h"
 #include "ProcessUtils.h"
+#include "Thread.h"
 #include "Timer.h"
 
 #include <cstdio>
@@ -66,6 +68,8 @@ Log::Log(Context* context) :
     quiet_(false)
 {
     logInstance = this;
+    
+    SubscribeToEvent(E_ENDFRAME, HANDLER(Log, HandleEndFrame));
 }
 
 Log::~Log()
@@ -129,6 +133,18 @@ void Log::Write(int level, const String& message)
 {
     assert(level >= LOG_DEBUG && level < LOG_NONE);
 
+    // If not in the main thread, store message for later processing
+    if (!Thread::IsMainThread())
+    {
+        if (logInstance)
+        {
+            MutexLock lock(logInstance->logMutex_);
+            logInstance->threadMessages_.Push(StoredLogMessage(message, level, false));
+        }
+        
+        return;
+    }
+
     // Do not log if message level excluded or if currently sending a log event
     if (!logInstance || logInstance->level_ > level || logInstance->inWrite_)
         return;
@@ -176,6 +192,18 @@ void Log::Write(int level, const String& message)
 
 void Log::WriteRaw(const String& message, bool error)
 {
+    // If not in the main thread, store message for later processing
+    if (!Thread::IsMainThread())
+    {
+        if (logInstance)
+        {
+            MutexLock lock(logInstance->logMutex_);
+            logInstance->threadMessages_.Push(StoredLogMessage(message, LOG_RAW, error));
+        }
+        
+        return;
+    }
+    
     // Prevent recursion during log event
     if (!logInstance || logInstance->inWrite_)
         return;
@@ -221,4 +249,22 @@ void Log::WriteRaw(const String& message, bool error)
     logInstance->inWrite_ = false;
 }
 
+void Log::HandleEndFrame(StringHash eventType, VariantMap& eventData)
+{
+    MutexLock lock(logMutex_);
+    
+    // Process messages accumulated from other threads (if any)
+    while (!threadMessages_.Empty())
+    {
+        const StoredLogMessage& stored = threadMessages_.Front();
+        
+        if (stored.level_ != LOG_RAW)
+            Write(stored.level_, stored.message_);
+        else
+            WriteRaw(stored.message_, stored.error_);
+        
+        threadMessages_.PopFront();
+    }
+}
+
 }

+ 35 - 0
Source/Engine/IO/Log.h

@@ -22,12 +22,16 @@
 
 #pragma once
 
+#include "List.h"
+#include "Mutex.h"
 #include "Object.h"
 #include "StringUtils.h"
 
 namespace Urho3D
 {
 
+/// Fictional message level to indicate a stored raw message.
+static const int LOG_RAW = -1;
 /// Debug message level. By default only shown in debug mode.
 static const int LOG_DEBUG = 0;
 /// Informative message level.
@@ -41,6 +45,30 @@ static const int LOG_NONE = 4;
 
 class File;
 
+/// Stored log message from another thread.
+struct StoredLogMessage
+{
+    /// Construct undefined.
+    StoredLogMessage()
+    {
+    }
+    
+    /// Construct with parameters.
+    StoredLogMessage(const String& message, int level, bool error) :
+        message_(message),
+        level_(level),
+        error_(error)
+    {
+    }
+    
+    /// Message text.
+    String message_;
+    /// Message level. -1 for raw messages.
+    int level_;
+    /// Error flag for raw messages.
+    bool error_;
+};
+
 /// Logging subsystem.
 class URHO3D_API Log : public Object
 {
@@ -78,6 +106,13 @@ public:
     static void WriteRaw(const String& message, bool error = false);
 
 private:
+    /// Handle end of frame. Process the threaded log messages.
+    void HandleEndFrame(StringHash eventType, VariantMap& eventData);
+    
+    /// Mutex for threaded operation.
+    Mutex logMutex_;
+    /// Log messages from other threads.
+    List<StoredLogMessage> threadMessages_;
     /// Log file.
     SharedPtr<File> logFile_;
     /// Last log message.

+ 1 - 1
Source/Engine/LuaScript/LuaFile.cpp

@@ -60,7 +60,7 @@ void LuaFile::RegisterObject(Context* context)
     context->RegisterFactory<LuaFile>();
 }
 
-bool LuaFile::Load(Deserializer& source)
+bool LuaFile::BeginLoad(Deserializer& source)
 {
     size_ = source.GetSize();
 

+ 2 - 2
Source/Engine/LuaScript/LuaFile.h

@@ -43,8 +43,8 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
 
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
     /// Save resource. Return true if successful.
     virtual bool Save(Serializer& dest) const;
 

+ 4 - 4
Source/Engine/LuaScript/pkgs/Graphics/Texture2D.pkg

@@ -9,8 +9,8 @@ class Texture2D : public Texture
 
     bool SetSize(int width, int height, unsigned format, TextureUsage usage = TEXTURE_STATIC);
 
-    // bool Load(SharedPtr<Image> image, bool useAlpha = false);
-    tolua_outside bool Texture2DLoad @ Load(Image* image, bool useAlpha = false);
+    // bool SetData(SharedPtr<Image> image, bool useAlpha = false);
+    tolua_outside bool Texture2DSetData @ SetData(Image* image, bool useAlpha = false);
 
     RenderSurface* GetRenderSurface() const;
     
@@ -30,10 +30,10 @@ static int tolua_GraphicsLuaAPI_Texture2D_new00_local(lua_State* tolua_S)
     return ToluaNewObjectGC<Texture2D>(tolua_S);
 }
 
-static bool Texture2DLoad(Texture2D* texture, Image* image, bool useAlpha)
+static bool Texture2DSetData(Texture2D* texture, Image* image, bool useAlpha)
 {
     SharedPtr<Image> imagePtr(image);
-    bool ret = texture->Load(imagePtr, useAlpha);
+    bool ret = texture->SetData(imagePtr, useAlpha);
     // Need to safely detach the object from the shared pointer so that the Lua script can manually
     // delete the object once done
     imagePtr.Detach();

+ 17 - 4
Source/Engine/LuaScript/pkgs/Resource/ResourceCache.pkg

@@ -11,10 +11,13 @@ class ResourceCache
     void SetAutoReloadResources(bool enable);
     void SetReturnFailedResources(bool enable);
     void SetSearchPackagesFirst(bool value);
+    void SetFinishBackgroundResourcesMs(int ms);
 
     tolua_outside File* ResourceCacheGetFile @ GetFile(const String name);
 
-    Resource* GetResource(const String type, const String name, bool SendEventOnFailure = true);
+    Resource* GetResource(const String type, const String name, bool sendEventOnFailure = true);
+    tolua_outside bool ResourceCacheBackgroundLoadResource @ BackgroundLoadResource(const String type, const String name, bool sendEventOnFailure = true);
+    unsigned GetNumBackgroundLoadResources() const;
 
     bool Exists(const String name) const;
     unsigned GetMemoryBudget(StringHash type) const;
@@ -25,15 +28,18 @@ class ResourceCache
     bool GetAutoReloadResources() const;
     bool GetReturnFailedResources() const;
     bool GetSearchPackagesFirst() const;
+    int GetFinishBackgroundResourcesMs() const;
 
     String GetPreferredResourceDir(const String path) const;
     String SanitateResourceName(const String name) const;
     String SanitateResourceDirName(const String name) const;
 
     tolua_readonly tolua_property__get_set unsigned totalMemoryUse;
-    tolua_readonly tolua_property__get_set bool autoReloadResources;
-    tolua_readonly tolua_property__get_set bool returnFailedResources;
-    tolua_readonly tolua_property__get_set bool searchPackagesFirst;
+    tolua_property__get_set bool autoReloadResources;
+    tolua_property__get_set bool returnFailedResources;
+    tolua_property__get_set bool searchPackagesFirst;
+    tolua_readonly tolua_property__get_set unsigned numBackgroundLoadResources;
+    tolua_property__get_set int finishBackgroundResourcesMs;
 };
 
 ResourceCache* GetCache();
@@ -60,4 +66,11 @@ static File* ResourceCacheGetFile(ResourceCache* cache, const String& fileName)
 
     return file;
 }
+
+static bool ResourceCacheBackgroundLoadResource(ResourceCache* cache, StringHash type, const String& fileName, bool sendEventOnFailure)
+{
+    return cache->BackgroundLoadResource(type, fileName, sendEventOnFailure);
+}
+
+
 $}

+ 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)

+ 299 - 0
Source/Engine/Resource/BackgroundLoader.cpp

@@ -0,0 +1,299 @@
+//
+// Copyright (c) 2008-2014 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "Precompiled.h"
+#include "BackgroundLoader.h"
+#include "Context.h"
+#include "Log.h"
+#include "Profiler.h"
+#include "ResourceCache.h"
+#include "ResourceEvents.h"
+#include "Timer.h"
+
+#include "DebugNew.h"
+
+namespace Urho3D
+{
+
+BackgroundLoader::BackgroundLoader(ResourceCache* owner) :
+    owner_(owner)
+{
+}
+
+void BackgroundLoader::ThreadFunction()
+{
+    while (shouldRun_)
+    {
+        backgroundLoadMutex_.Acquire();
+        
+        // Search for a queued resource that has not been loaded yet
+        HashMap<Pair<StringHash, StringHash>, BackgroundLoadItem>::Iterator i = backgroundLoadQueue_.Begin();
+        while (i != backgroundLoadQueue_.End())
+        {
+            if (i->second_.resource_->GetAsyncLoadState() == ASYNC_QUEUED)
+                break;
+            else
+                ++i;
+        }
+        
+        if (i == backgroundLoadQueue_.End())
+        {
+            // No resources to load found
+            backgroundLoadMutex_.Release();
+            Time::Sleep(5);
+        }
+        else
+        {
+            BackgroundLoadItem& item = i->second_;
+            Resource* resource = item.resource_;
+            // We can be sure that the item is not removed from the queue as long as it is in the
+            // "queued" or "loading" state
+            backgroundLoadMutex_.Release();
+            
+            bool success = false;
+            SharedPtr<File> file = owner_->GetFile(resource->GetName(), item.sendEventOnFailure_);
+            if (file)
+            {
+                resource->SetAsyncLoadState(ASYNC_LOADING);
+                success = resource->BeginLoad(*file);
+            }
+            
+            // Process dependencies now
+            // Need to lock the queue again when manipulating other entries
+            Pair<StringHash, StringHash> key = MakePair(resource->GetType(), resource->GetNameHash());
+            backgroundLoadMutex_.Acquire();
+            if (item.dependents_.Size())
+            {
+                for (HashSet<Pair<StringHash, StringHash> >::Iterator i = item.dependents_.Begin(); i != item.dependents_.End();
+                    ++i)
+                {
+                    HashMap<Pair<StringHash, StringHash>, BackgroundLoadItem>::Iterator j =
+                        backgroundLoadQueue_.Find(*i);
+                    if (j != backgroundLoadQueue_.End())
+                        j->second_.dependencies_.Erase(key);
+                }
+                
+                item.dependents_.Clear();
+            }
+            
+            resource->SetAsyncLoadState(success ? ASYNC_SUCCESS : ASYNC_FAIL);
+            backgroundLoadMutex_.Release();
+        }
+    }
+}
+
+bool BackgroundLoader::QueueResource(StringHash type, const String& name, bool sendEventOnFailure, Resource* caller)
+{
+    StringHash nameHash(name);
+    Pair<StringHash, StringHash> key = MakePair(type, nameHash);
+    
+    MutexLock lock(backgroundLoadMutex_);
+    
+    // Check if already exists in the queue
+    if (backgroundLoadQueue_.Find(key) != backgroundLoadQueue_.End())
+        return false;
+    
+    BackgroundLoadItem& item = backgroundLoadQueue_[key];
+    item.sendEventOnFailure_ = sendEventOnFailure;
+    
+    // Make sure the pointer is non-null and is a Resource subclass
+    item.resource_ = DynamicCast<Resource>(owner_->GetContext()->CreateObject(type));
+    if (!item.resource_)
+    {
+        LOGERROR("Could not load unknown resource type " + String(type));
+
+        if (sendEventOnFailure && Thread::IsMainThread())
+        {
+            using namespace UnknownResourceType;
+            
+            VariantMap& eventData = owner_->GetEventDataMap();
+            eventData[P_RESOURCETYPE] = type;
+            owner_->SendEvent(E_UNKNOWNRESOURCETYPE, eventData);
+        }
+        
+        backgroundLoadQueue_.Erase(key);
+        return false;
+    }
+    
+    LOGDEBUG("Background loading resource " + name);
+
+    item.resource_->SetName(name);
+    item.resource_->SetAsyncLoadState(ASYNC_QUEUED);
+    
+    // If this is a resource calling for the background load of more resources, mark the dependency as necessary
+    if (caller)
+    {
+        Pair<StringHash, StringHash> callerKey = MakePair(caller->GetType(), caller->GetNameHash());
+        HashMap<Pair<StringHash, StringHash>, BackgroundLoadItem>::Iterator j = backgroundLoadQueue_.Find(callerKey);
+        if (j != backgroundLoadQueue_.End())
+        {
+            BackgroundLoadItem& callerItem = j->second_;
+            item.dependents_.Insert(callerKey);
+            callerItem.dependencies_.Insert(key);
+        }
+        else
+            LOGWARNING("Resource " + caller->GetName() + " requested for a background loaded resource but was not in the background load queue");
+    }
+    
+    // Start the background loader thread now
+    if (!IsStarted())
+        Run();
+    
+    return true;
+}
+
+void BackgroundLoader::WaitForResource(StringHash type, StringHash nameHash)
+{
+    backgroundLoadMutex_.Acquire();
+    
+    // Check if the resource in question is being background loaded
+    Pair<StringHash, StringHash> key = MakePair(type, nameHash);
+    HashMap<Pair<StringHash, StringHash>, BackgroundLoadItem>::Iterator i = backgroundLoadQueue_.Find(key);
+    if (i != backgroundLoadQueue_.End())
+    {
+        backgroundLoadMutex_.Release();
+        
+        {
+            Resource* resource = i->second_.resource_;
+            HiresTimer waitTimer;
+            bool didWait = false;
+            
+            for (;;)
+            {
+                unsigned numDeps = i->second_.dependencies_.Size();
+                AsyncLoadState state = resource->GetAsyncLoadState();
+                if (numDeps > 0 || state == ASYNC_QUEUED || state == ASYNC_LOADING)
+                {
+                    didWait = true;
+                    Time::Sleep(1);
+                }
+                else
+                    break;
+            }
+            
+            if (didWait)
+                LOGDEBUG("Waited " + String(waitTimer.GetUSec(false) / 1000) + " ms for background loaded resource " + resource->GetName());
+        }
+        
+        // This may take a long time and may potentially wait on other resources, so it is important we do not hold the mutex during this
+        FinishBackgroundLoading(i->second_);
+        
+        backgroundLoadMutex_.Acquire();
+        backgroundLoadQueue_.Erase(i);
+        backgroundLoadMutex_.Release();
+    }
+    else
+        backgroundLoadMutex_.Release();
+}
+
+void BackgroundLoader::FinishResources(int maxMs)
+{
+    if (IsStarted())
+    {
+        HiresTimer timer;
+
+        backgroundLoadMutex_.Acquire();
+        
+        for (HashMap<Pair<StringHash, StringHash>, BackgroundLoadItem>::Iterator i = backgroundLoadQueue_.Begin();
+            i != backgroundLoadQueue_.End();)
+        {
+            Resource* resource = i->second_.resource_;
+            unsigned numDeps = i->second_.dependencies_.Size();
+            AsyncLoadState state = resource->GetAsyncLoadState();
+            if (numDeps > 0 || state == ASYNC_QUEUED || state == ASYNC_LOADING)
+                ++i;
+            else
+            {
+                // Finishing a resource may need it to wait for other resources to load, in which case we can not
+                // hold on to the mutex
+                backgroundLoadMutex_.Release();
+                FinishBackgroundLoading(i->second_);
+                backgroundLoadMutex_.Acquire();
+                i = backgroundLoadQueue_.Erase(i);
+            }
+            
+            // Break when the time limit passed so that we keep sufficient FPS
+            if (timer.GetUSec(false) >= maxMs * 1000)
+                break;
+        }
+        
+        backgroundLoadMutex_.Release();
+    }
+}
+
+unsigned BackgroundLoader::GetNumQueuedResources() const
+{
+    MutexLock lock(backgroundLoadMutex_);
+    return backgroundLoadQueue_.Size();
+}
+
+void BackgroundLoader::FinishBackgroundLoading(BackgroundLoadItem& item)
+{
+    Resource* resource = item.resource_;
+    
+    bool success = resource->GetAsyncLoadState() == ASYNC_SUCCESS;
+    // If BeginLoad() phase was successful, call EndLoad() and get the final success/failure result
+    if (success)
+    {
+#ifdef URHO3D_PROFILING
+        String profileBlockName("Finish" + resource->GetTypeName());
+        
+        Profiler* profiler = owner_->GetSubsystem<Profiler>();
+        if (profiler)
+            profiler->BeginBlock(profileBlockName.CString());
+#endif
+        LOGDEBUG("Finishing background loaded resource " + resource->GetName());
+        success = resource->EndLoad();
+        
+#ifdef URHO3D_PROFILING
+        if (profiler)
+            profiler->EndBlock();
+#endif
+    }
+    resource->SetAsyncLoadState(ASYNC_DONE);
+    
+    if (!success && item.sendEventOnFailure_)
+    {
+        using namespace LoadFailed;
+
+        VariantMap& eventData = owner_->GetEventDataMap();
+        eventData[P_RESOURCENAME] = resource->GetName();
+        owner_->SendEvent(E_LOADFAILED, eventData);
+    }
+    
+    // Send event, either success or failure
+    {
+        using namespace ResourceBackgroundLoaded;
+        
+        VariantMap& eventData = owner_->GetEventDataMap();
+        eventData[P_RESOURCENAME] = resource->GetName();
+        eventData[P_SUCCESS] = success;
+        eventData[P_RESOURCE] = resource;
+        owner_->SendEvent(E_RESOURCEBACKGROUNDLOADED, eventData);
+    }
+    
+    // Store to the cache; use same mechanism as for manual resources
+    if (success || owner_->GetReturnFailedResources())
+        owner_->AddManualResource(resource);
+}
+
+}

+ 84 - 0
Source/Engine/Resource/BackgroundLoader.h

@@ -0,0 +1,84 @@
+//
+// Copyright (c) 2008-2014 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "HashMap.h"
+#include "HashSet.h"
+#include "Mutex.h"
+#include "Ptr.h"
+#include "RefCounted.h"
+#include "StringHash.h"
+#include "Thread.h"
+
+namespace Urho3D
+{
+
+class Resource;
+class ResourceCache;
+
+/// Queue item for background loading of a resource.
+struct BackgroundLoadItem
+{
+    /// Resource.
+    SharedPtr<Resource> resource_;
+    /// Resources depended on for loading.
+    HashSet<Pair<StringHash, StringHash> > dependencies_;
+    /// Resources that depend on this resource's loading.
+    HashSet<Pair<StringHash, StringHash> > dependents_;
+    /// Whether to send failure event.
+    bool sendEventOnFailure_;
+};
+
+/// Background loader of resources. Owned by the ResourceCache.
+class BackgroundLoader : public RefCounted, public Thread
+{
+public:
+    /// Construct.
+    BackgroundLoader(ResourceCache* owner);
+    
+    /// Resource background loading loop.
+    virtual void ThreadFunction();
+    
+    /// Queue loading of a resource. The name must be sanitated to ensure consistent format. Return true if queued (not a duplicate and resource was a known type).
+    bool QueueResource(StringHash type, const String& name, bool sendEventOnFailure, Resource* caller);
+    /// Wait and finish possible loading of a resource when being requested from the cache.
+    void WaitForResource(StringHash type, StringHash nameHash);
+    /// Process resources that are ready to finish.
+    void FinishResources(int maxMs);
+    
+    /// Return amount of resources in the load queue.
+    unsigned GetNumQueuedResources() const;
+    
+private:
+    /// Finish one background loaded resource.
+    void FinishBackgroundLoading(BackgroundLoadItem& item);
+    
+    /// Resource cache.
+    ResourceCache* owner_;
+    /// Mutex for thread-safe access to the background load queue.
+    mutable Mutex backgroundLoadMutex_;
+    /// Resources that are queued for background loading.
+    HashMap<Pair<StringHash, StringHash>, BackgroundLoadItem> backgroundLoadQueue_;
+};
+
+}

+ 35 - 3
Source/Engine/Resource/Image.cpp

@@ -210,10 +210,8 @@ void Image::RegisterObject(Context* context)
     context->RegisterFactory<Image>();
 }
 
-bool Image::Load(Deserializer& source)
+bool Image::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadImage);
-
     // Check for DDS, KTX or PVR compressed format
     String fileID = source.ReadFileID();
 
@@ -508,6 +506,7 @@ bool Image::SetSize(int width, int height, int depth, unsigned components)
     components_ = components;
     compressedFormat_ = CF_NONE;
     numCompressedLevels_ = 0;
+    nextLevel_.Reset();
 
     SetMemoryUse(width * height * depth * components);
     return true;
@@ -558,7 +557,14 @@ void Image::SetData(const unsigned char* pixelData)
     if (!data_)
         return;
 
+    if (IsCompressed())
+    {
+        LOGERROR("Can not set new pixel data for a compressed image");
+        return;
+    }
+
     memcpy(data_.Get(), pixelData, width_ * height_ * depth_ * components_);
+    nextLevel_.Reset();
 }
 
 bool Image::LoadColorLUT(Deserializer& source)
@@ -939,6 +945,11 @@ SharedPtr<Image> Image::GetNextLevel() const
         return SharedPtr<Image>();
     }
 
+    if (nextLevel_)
+        return nextLevel_;
+
+    PROFILE(CalculateImageMipLevel);
+
     int widthOut = width_ / 2;
     int heightOut = height_ / 2;
     int depthOut = depth_ / 2;
@@ -1376,6 +1387,27 @@ SDL_Surface* Image::GetSDLSurface(const IntRect& rect) const
     return surface;
 }
 
+void Image::PrecalculateLevels()
+{
+    if (!data_ || IsCompressed())
+        return;
+
+    PROFILE(PrecalculateImageMipLevels);
+
+    nextLevel_.Reset();
+
+    if (width_ > 1 || height_ > 1)
+    {
+        SharedPtr<Image> current = GetNextLevel();
+        nextLevel_ = current;
+        while (current && (current->width_ > 1 || current->height_ > 1))
+        {
+            current->nextLevel_ = current->GetNextLevel();
+            current = current->nextLevel_;
+        }
+    }
+}
+
 unsigned char* Image::GetImageData(Deserializer& source, int& width, int& height, unsigned& components)
 {
     unsigned dataSize = source.GetSize();

+ 6 - 2
Source/Engine/Resource/Image.h

@@ -98,8 +98,8 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
 
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
 
     /// Set 2D size and number of color components. Old image data will be destroyed and new data is undefined. Return true if successful.
     bool SetSize(int width, int height, unsigned components);
@@ -170,6 +170,8 @@ public:
     Image* GetSubimage(const IntRect& rect) const;
     /// Return an SDL surface from the image, or null if failed. Only RGB images are supported. Specify rect to only return partial image. You must free the surface yourself.
     SDL_Surface* GetSDLSurface(const IntRect& rect = IntRect::ZERO) const;
+    /// Precalculate the mip levels. Used by asynchronous texture loading.
+    void PrecalculateLevels();
 
 private:
     /// Decode an image using stb_image.
@@ -191,6 +193,8 @@ private:
     CompressedFormat compressedFormat_;
     /// Pixel data.
     SharedArrayPtr<unsigned char> data_;
+    /// Precalculated mip level image.
+    SharedPtr<Image> nextLevel_;
 };
 
 }

+ 2 - 4
Source/Engine/Resource/JSONFile.cpp

@@ -57,10 +57,8 @@ void JSONFile::RegisterObject(Context* context)
     context->RegisterFactory<JSONFile>();
 }
 
-bool JSONFile::Load(Deserializer& source)
+bool JSONFile::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadJSONFile);
-
     unsigned dataSize = source.GetSize();
     if (!dataSize && !source.GetName().Empty())
     {
@@ -75,7 +73,7 @@ bool JSONFile::Load(Deserializer& source)
 
     if (document_->Parse<0>(buffer).HasParseError())
     {
-        LOGERROR("Could not parse JOSO data from " + source.GetName());
+        LOGERROR("Could not parse JSON data from " + source.GetName());
         return false;
     }
 

+ 2 - 2
Source/Engine/Resource/JSONFile.h

@@ -47,8 +47,8 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
 
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
     /// Save resource. Return true if successful. Only supports saving to a File.
     virtual bool Save(Serializer& dest) const;
 

+ 43 - 2
Source/Engine/Resource/Resource.cpp

@@ -22,6 +22,7 @@
 
 #include "Precompiled.h"
 #include "Log.h"
+#include "Profiler.h"
 #include "Resource.h"
 
 namespace Urho3D
@@ -29,15 +30,50 @@ namespace Urho3D
 
 Resource::Resource(Context* context) :
     Object(context),
-    memoryUse_(0)
+    memoryUse_(0),
+    asyncLoadState_(ASYNC_DONE)
 {
 }
 
-bool Resource::Load(Deserializer& src)
+bool Resource::Load(Deserializer& source)
 {
+    // Because BeginLoad() / EndLoad() can be called from worker threads, where profiling would be a no-op,
+    // create a type name -based profile block here
+#ifdef URHO3D_PROFILING
+    String profileBlockName("Load" + GetTypeName());
+    
+    Profiler* profiler = GetSubsystem<Profiler>();
+    if (profiler)
+        profiler->BeginBlock(profileBlockName.CString());
+#endif
+
+    // Make sure any previous async state is cancelled
+    SetAsyncLoadState(ASYNC_DONE);
+
+    bool success = BeginLoad(source);
+    if (success)
+        success &= EndLoad();
+
+#ifdef URHO3D_PROFILING
+    if (profiler)
+        profiler->EndBlock();
+#endif
+
+    return success;
+}
+
+bool Resource::BeginLoad(Deserializer& source)
+{
+    // This always needs to be overridden by subclasses
     return false;
 }
 
+bool Resource::EndLoad()
+{
+    // If no GPU upload step is necessary, no override is necessary
+    return true;
+}
+
 bool Resource::Save(Serializer& dest) const
 {
     LOGERROR("Save not supported for " + GetTypeName());
@@ -60,6 +96,11 @@ void Resource::ResetUseTimer()
     useTimer_.Reset();
 }
 
+void Resource::SetAsyncLoadState(AsyncLoadState newState)
+{
+    asyncLoadState_ = newState;
+}
+
 unsigned Resource::GetUseTimer()
 {
     // If more references than the resource cache, return always 0 & reset the timer

+ 27 - 2
Source/Engine/Resource/Resource.h

@@ -31,6 +31,21 @@ namespace Urho3D
 class Deserializer;
 class Serializer;
 
+/// Asynchronous loading state of a resource.
+enum AsyncLoadState
+{
+    /// No async operation in progress.
+    ASYNC_DONE = 0,
+    /// Queued for asynchronous loading.
+    ASYNC_QUEUED = 1,
+    /// In progress of calling BeginLoad() in a worker thread.
+    ASYNC_LOADING = 2,
+    /// BeginLoad() succeeded. EndLoad() can be called in the main thread.
+    ASYNC_SUCCESS = 3,
+    /// BeginLoad() failed.
+    ASYNC_FAIL = 4
+};
+
 /// Base class for resources.
 class URHO3D_API Resource : public Object
 {
@@ -41,8 +56,12 @@ public:
     /// Construct.
     Resource(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource synchronously. Call both BeginLoad() & EndLoad() and return true if both succeeded.
+    bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     /// Save resource. Return true if successful.
     virtual bool Save(Serializer& dest) const;
     
@@ -52,6 +71,8 @@ public:
     void SetMemoryUse(unsigned size);
     /// Reset last used timer.
     void ResetUseTimer();
+    /// Set the asynchronous loading state. Called by ResourceCache. Resources in the middle of asynchronous loading are not normally returned to user.
+    void SetAsyncLoadState(AsyncLoadState newState);
     
     /// Return name.
     const String& GetName() const { return name_; }
@@ -61,6 +82,8 @@ public:
     unsigned GetMemoryUse() const { return memoryUse_; }
     /// Return time since last use in milliseconds. If referred to elsewhere than in the resource cache, returns always zero.
     unsigned GetUseTimer();
+    /// Return the asynchronous loading state.
+    AsyncLoadState GetAsyncLoadState() const { return asyncLoadState_; }
     
 private:
     /// Name.
@@ -71,6 +94,8 @@ private:
     Timer useTimer_;
     /// Memory use in bytes.
     unsigned memoryUse_;
+    /// Asynchronous loading state.
+    AsyncLoadState asyncLoadState_;
 };
 
 inline const String& GetResourceName(Resource* resource)

+ 81 - 21
Source/Engine/Resource/ResourceCache.cpp

@@ -21,6 +21,7 @@
 //
 
 #include "Precompiled.h"
+#include "BackgroundLoader.h"
 #include "Context.h"
 #include "CoreEvents.h"
 #include "FileSystem.h"
@@ -29,8 +30,10 @@
 #include "JSONFile.h"
 #include "Log.h"
 #include "PackageFile.h"
+#include "Profiler.h"
 #include "ResourceCache.h"
 #include "ResourceEvents.h"
+#include "WorkQueue.h"
 #include "XMLFile.h"
 
 #include "DebugNew.h"
@@ -63,18 +66,29 @@ ResourceCache::ResourceCache(Context* context) :
     Object(context),
     autoReloadResources_(false),
     returnFailedResources_(false),
-    searchPackagesFirst_(true)
+    searchPackagesFirst_(true),
+    finishBackgroundResourcesMs_(5)
 {
     // Register Resource library object factories
     RegisterResourceLibrary(context_);
+    
+    // Create resource background loader. Its thread will start on the first background request
+    backgroundLoader_ = new BackgroundLoader(this);
+    
+    // Subscribe BeginFrame for handling directory watchers and background loaded resource finalization
+    SubscribeToEvent(E_BEGINFRAME, HANDLER(ResourceCache, HandleBeginFrame));
 }
 
 ResourceCache::~ResourceCache()
 {
+    // Shut down the background loader first
+    backgroundLoader_.Reset();
 }
 
 bool ResourceCache::AddResourceDir(const String& pathName, unsigned int priority)
 {
+    MutexLock lock(resourceMutex_);
+    
     FileSystem* fileSystem = GetSubsystem<FileSystem>();
     if (!fileSystem || !fileSystem->DirExists(pathName))
     {
@@ -112,6 +126,8 @@ bool ResourceCache::AddResourceDir(const String& pathName, unsigned int priority
 
 void ResourceCache::AddPackageFile(PackageFile* package, unsigned int priority)
 {
+    MutexLock lock(resourceMutex_);
+    
     // Do not add packages that failed to load
     if (!package || !package->GetNumFiles())
         return;
@@ -148,6 +164,8 @@ bool ResourceCache::AddManualResource(Resource* resource)
 
 void ResourceCache::RemoveResourceDir(const String& pathName)
 {
+    MutexLock lock(resourceMutex_);
+    
     String fixedPath = SanitateResourceDirName(pathName);
     
     for (unsigned i = 0; i < resourceDirs_.Size(); ++i)
@@ -172,6 +190,8 @@ void ResourceCache::RemoveResourceDir(const String& pathName)
 
 void ResourceCache::RemovePackageFile(PackageFile* package, bool releaseResources, bool forceRelease)
 {
+    MutexLock lock(resourceMutex_);
+    
     for (Vector<SharedPtr<PackageFile> >::Iterator i = packages_.Begin(); i != packages_.End(); ++i)
     {
         if (*i == package)
@@ -187,6 +207,8 @@ void ResourceCache::RemovePackageFile(PackageFile* package, bool releaseResource
 
 void ResourceCache::RemovePackageFile(const String& fileName, bool releaseResources, bool forceRelease)
 {
+    MutexLock lock(resourceMutex_);
+
     // Compare the name and extension only, not the path
     String fileNameNoPath = GetFileNameAndExtension(fileName);
     
@@ -372,14 +394,9 @@ void ResourceCache::SetAutoReloadResources(bool enable)
                 watcher->StartWatching(resourceDirs_[i], true);
                 fileWatchers_.Push(watcher);
             }
-
-            SubscribeToEvent(E_BEGINFRAME, HANDLER(ResourceCache, HandleBeginFrame));
         }
         else
-        {
-            UnsubscribeFromEvent(E_BEGINFRAME);
             fileWatchers_.Clear();
-        }
         
         autoReloadResources_ = enable;
     }
@@ -392,6 +409,8 @@ void ResourceCache::SetReturnFailedResources(bool enable)
 
 SharedPtr<File> ResourceCache::GetFile(const String& nameIn, bool sendEventOnFailure)
 {
+    MutexLock lock(resourceMutex_);
+    
     String name = SanitateResourceName(nameIn);
     File* file = 0;
 
@@ -415,30 +434,38 @@ SharedPtr<File> ResourceCache::GetFile(const String& nameIn, bool sendEventOnFai
     {
         LOGERROR("Could not find resource " + name);
 
-        using namespace ResourceNotFound;
+        if (Thread::IsMainThread())
+        {
+            using namespace ResourceNotFound;
 
-        VariantMap& eventData = GetEventDataMap();
-        eventData[P_RESOURCENAME] = name;
-        SendEvent(E_RESOURCENOTFOUND, eventData);
+            VariantMap& eventData = GetEventDataMap();
+            eventData[P_RESOURCENAME] = name;
+            SendEvent(E_RESOURCENOTFOUND, eventData);
+        }
     }
 
     return SharedPtr<File>();
 }
 
-Resource* ResourceCache::GetResource(StringHash type, const String& name, bool sendEventOnFailure)
-{
-    return GetResource(type, name.CString(), sendEventOnFailure);
-}
-
-Resource* ResourceCache::GetResource(StringHash type, const char* nameIn, bool sendEventOnFailure)
+Resource* ResourceCache::GetResource(StringHash type, const String& nameIn, bool sendEventOnFailure)
 {
     String name = SanitateResourceName(nameIn);
     
+    if (!Thread::IsMainThread())
+    {
+        LOGERROR("Attempted to get resource " + name + " from outside the main thread");
+        return 0;
+    }
+    
     // If empty name, return null pointer immediately
     if (name.Empty())
         return 0;
     
     StringHash nameHash(name);
+
+    // Check if the resource is being background loaded but is now needed immediately
+    backgroundLoader_->WaitForResource(type, nameHash);
+
     const SharedPtr<Resource>& existing = FindResource(type, nameHash);
     if (existing)
         return existing;
@@ -494,6 +521,21 @@ Resource* ResourceCache::GetResource(StringHash type, const char* nameIn, bool s
     return resource;
 }
 
+bool ResourceCache::BackgroundLoadResource(StringHash type, const String& nameIn, bool sendEventOnFailure, Resource* caller)
+{
+    // If empty name, fail immediately
+    String name = SanitateResourceName(nameIn);
+    if (name.Empty())
+        return false;
+    
+    // First check if already exists as a loaded resource
+    StringHash nameHash(name);
+    if (FindResource(type, nameHash) != noResource)
+        return false;
+    
+    return backgroundLoader_->QueueResource(type, name, sendEventOnFailure, caller);
+}
+
 SharedPtr<Resource> ResourceCache::GetTempResource(StringHash type, const String& nameIn, bool sendEventOnFailure)
 {
     String name = SanitateResourceName(nameIn);
@@ -502,11 +544,6 @@ SharedPtr<Resource> ResourceCache::GetTempResource(StringHash type, const String
     if (name.Empty())
         return SharedPtr<Resource>();
     
-    StringHash nameHash(name);
-    const SharedPtr<Resource>& existing = FindResource(type, nameHash);
-    if (existing)
-        return existing;
-    
     SharedPtr<Resource> resource;
     // Make sure the pointer is non-null and is a Resource subclass
     resource = DynamicCast<Resource>(context_->CreateObject(type));
@@ -552,6 +589,11 @@ SharedPtr<Resource> ResourceCache::GetTempResource(StringHash type, const String
     return resource;
 }
 
+unsigned ResourceCache::GetNumBackgroundLoadResources() const
+{
+    return backgroundLoader_->GetNumQueuedResources();
+}
+
 void ResourceCache::GetResources(PODVector<Resource*>& result, StringHash type) const
 {
     result.Clear();
@@ -566,6 +608,8 @@ void ResourceCache::GetResources(PODVector<Resource*>& result, StringHash type)
 
 bool ResourceCache::Exists(const String& nameIn) const
 {
+    MutexLock lock(resourceMutex_);
+    
     String name = SanitateResourceName(nameIn);
     if (name.Empty())
         return false;
@@ -618,6 +662,8 @@ unsigned ResourceCache::GetTotalMemoryUse() const
 
 String ResourceCache::GetResourceFileName(const String& name) const
 {
+    MutexLock lock(resourceMutex_);
+    
     FileSystem* fileSystem = GetSubsystem<FileSystem>();
     for (unsigned i = 0; i < resourceDirs_.Size(); ++i)
     {
@@ -713,6 +759,8 @@ void ResourceCache::StoreResourceDependency(Resource* resource, const String& de
     if (!resource || !autoReloadResources_)
         return;
     
+    MutexLock lock(resourceMutex_);
+    
     StringHash nameHash(resource->GetName());
     HashSet<StringHash>& dependents = dependentResources_[dependency];
     dependents.Insert(nameHash);
@@ -723,6 +771,8 @@ void ResourceCache::ResetDependencies(Resource* resource)
     if (!resource || !autoReloadResources_)
         return;
     
+    MutexLock lock(resourceMutex_);
+    
     StringHash nameHash(resource->GetName());
     
     for (HashMap<StringHash, HashSet<StringHash> >::Iterator i = dependentResources_.Begin(); i !=
@@ -739,6 +789,8 @@ void ResourceCache::ResetDependencies(Resource* resource)
 
 const SharedPtr<Resource>& ResourceCache::FindResource(StringHash type, StringHash nameHash)
 {
+    MutexLock lock(resourceMutex_);
+
     HashMap<StringHash, ResourceGroup>::Iterator i = resourceGroups_.Find(type);
     if (i == resourceGroups_.End())
         return noResource;
@@ -751,6 +803,8 @@ const SharedPtr<Resource>& ResourceCache::FindResource(StringHash type, StringHa
 
 const SharedPtr<Resource>& ResourceCache::FindResource(StringHash nameHash)
 {
+    MutexLock lock(resourceMutex_);
+
     for (HashMap<StringHash, ResourceGroup>::Iterator i = resourceGroups_.Begin(); i != resourceGroups_.End(); ++i)
     {
         HashMap<StringHash, SharedPtr<Resource> >::Iterator j = i->second_.resources_.Find(nameHash);
@@ -883,6 +937,12 @@ void ResourceCache::HandleBeginFrame(StringHash eventType, VariantMap& eventData
             SendEvent(E_FILECHANGED, eventData);
         }
     }
+    
+    // Check for background loaded resources that can be finished
+    {
+        PROFILE(FinishBackgroundResources);
+        backgroundLoader_->FinishResources(finishBackgroundResourcesMs_);
+    }
 }
 
 File* ResourceCache::SearchResourceDirs(const String& nameIn)

+ 31 - 16
Source/Engine/Resource/ResourceCache.h

@@ -24,11 +24,14 @@
 
 #include "File.h"
 #include "HashSet.h"
+#include "List.h"
+#include "Mutex.h"
 #include "Resource.h"
 
 namespace Urho3D
 {
 
+class BackgroundLoader;
 class FileWatcher;
 class PackageFile;
 
@@ -96,15 +99,19 @@ public:
     void SetReturnFailedResources(bool enable);
     /// Define whether when getting resources should check package files or directories first. True for packages, false for directories.
     void SetSearchPackagesFirst(bool value) { searchPackagesFirst_ = value; }
+    /// Set how many milliseconds maximum per frame to spend on finishing background loaded resources.
+    void SetFinishBackgroundResourcesMs(int ms) { finishBackgroundResourcesMs_ = Max(ms, 1); }
 
-    /// Open and return a file from the resource load paths or from inside a package file. If not found, use a fallback search with absolute path. Return null if fails.
+    /// Open and return a file from the resource load paths or from inside a package file. If not found, use a fallback search with absolute path. Return null if fails. Can be called from outside the main thread.
     SharedPtr<File> GetFile(const String& name, bool sendEventOnFailure = true);
-    /// Return a resource by type and name. Load if not loaded yet. Return null if not found or if fails, unless SetReturnFailedResources(true) has been called.
+    /// Return a resource by type and name. Load if not loaded yet. Return null if not found or if fails, unless SetReturnFailedResources(true) has been called. Can be called only from the main thread.
     Resource* GetResource(StringHash type, const String& name, bool sendEventOnFailure = true);
-    /// Return a resource by type and name. Load if not loaded yet. Return null if not found or if fails, unless SetReturnFailedResources(true) has been called.
-    Resource* GetResource(StringHash type, const char* name, bool sendEventOnFailure = true);
-    /// Load a resource without storing it in the resource cache. Return null if not found or if fails.
+    /// Load a resource without storing it in the resource cache. Return null if not found or if fails. Can be called from outside the main thread if the resource itself is safe to load completely (it does not possess for example GPU data.)
     SharedPtr<Resource> GetTempResource(StringHash type, const String& name, bool sendEventOnFailure = true);
+    /// Background load a resource. An event will be sent when complete. Return true if successfully stored to the load queue, false if eg. already exists. Can be called from outside the main thread.
+    bool BackgroundLoadResource(StringHash type, const String& name, bool sendEventOnFailure = true, Resource* caller = 0);
+    /// Return number of pending background-loaded resources.
+    unsigned GetNumBackgroundLoadResources() const;
     /// Return all loaded resources of a specific type.
     void GetResources(PODVector<Resource*>& result, StringHash type) const;
     /// Return all loaded resources.
@@ -115,10 +122,10 @@ public:
     const Vector<SharedPtr<PackageFile> >& GetPackageFiles() const { return packages_; }
     /// Template version of returning a resource by name.
     template <class T> T* GetResource(const String& name, bool sendEventOnFailure = true);
-    /// Template version of returning a resource by name.
-    template <class T> T* GetResource(const char* name, bool sendEventOnFailure = true);
     /// Template version of loading a resource without storing it to the cache.
     template <class T> SharedPtr<T> GetTempResource(const String& name, bool sendEventOnFailure = true);
+    /// Template version of queueing a resource background load.
+    template <class T> bool BackgroundLoadResource(const String& name, bool sendEventOnFailure = true, Resource* caller = 0);
     /// Template version of returning loaded resources of a specific type.
     template <class T> void GetResources(PODVector<T*>& result) const;
     /// Return whether a file exists by name.
@@ -135,8 +142,10 @@ public:
     bool GetAutoReloadResources() const { return autoReloadResources_; }
     /// Return whether resources that failed to load are returned.
     bool GetReturnFailedResources() const { return returnFailedResources_; }
-    /// Define whether when getting resources should check package files or directories first.
+    /// Return whether when getting resources should check package files or directories first.
     bool GetSearchPackagesFirst() const { return searchPackagesFirst_; }
+    /// Return how many milliseconds maximum to spend on finishing background loaded resources.
+    int GetFinishBackgroundResourcesMs() const { return finishBackgroundResourcesMs_; }
 
     /// Return either the path itself or its parent, based on which of them has recognized resource subdirectories.
     String GetPreferredResourceDir(const String& path) const;
@@ -158,13 +167,15 @@ private:
     void ReleasePackageResources(PackageFile* package, bool force = false);
     /// Update a resource group. Recalculate memory use and release resources if over memory budget.
     void UpdateResourceGroup(StringHash type);
-    /// Handle begin frame event. Automatic resource reloads are processed here.
+    /// Handle begin frame event. Automatic resource reloads and the finalization of background loaded resources are processed here.
     void HandleBeginFrame(StringHash eventType, VariantMap& eventData);
-    /// Search FileSystem for File.
+    /// Search FileSystem for file.
     File* SearchResourceDirs(const String& nameIn);
-    /// Search Packages for File.
+    /// Search resource packages for file.
     File* SearchPackages(const String& nameIn);
     
+    /// Mutex for thread-safe access to the resource directories, resource packages and resource dependencies.
+    mutable Mutex resourceMutex_;
     /// Resources by type.
     HashMap<StringHash, ResourceGroup> resourceGroups_;
     /// Resource load directories.
@@ -173,14 +184,18 @@ private:
     Vector<SharedPtr<FileWatcher> > fileWatchers_;
     /// Package files.
     Vector<SharedPtr<PackageFile> > packages_;
-    /// Dependent resources.
+    /// Dependent resources. Only used with automatic reload to eg. trigger reload of a cube texture when any of its faces change.
     HashMap<StringHash, HashSet<StringHash> > dependentResources_;
+    /// Resource background loader.
+    SharedPtr<BackgroundLoader> backgroundLoader_;
     /// Automatic resource reloading flag.
     bool autoReloadResources_;
     /// Return failed resources flag.
     bool returnFailedResources_;
     /// Search priority flag.
     bool searchPackagesFirst_;
+    /// How many milliseconds maximum per frame to spend on finishing background loaded resources.
+    int finishBackgroundResourcesMs_;
 };
 
 template <class T> T* ResourceCache::GetResource(const String& name, bool sendEventOnFailure)
@@ -189,16 +204,16 @@ template <class T> T* ResourceCache::GetResource(const String& name, bool sendEv
     return static_cast<T*>(GetResource(type, name, sendEventOnFailure));
 }
 
-template <class T> T* ResourceCache::GetResource(const char* name, bool sendEventOnFailure)
+template <class T> SharedPtr<T> ResourceCache::GetTempResource(const String& name, bool sendEventOnFailure)
 {
     StringHash type = T::GetTypeStatic();
-    return static_cast<T*>(GetResource(type, name, sendEventOnFailure));
+    return StaticCast<T>(GetTempResource(type, name, sendEventOnFailure));
 }
 
-template <class T> SharedPtr<T> ResourceCache::GetTempResource(const String& name, bool sendEventOnFailure)
+template <class T> bool ResourceCache::BackgroundLoadResource(const String& name, bool sendEventOnFailure, Resource* caller)
 {
     StringHash type = T::GetTypeStatic();
-    return StaticCast<T>(GetTempResource(type, name, sendEventOnFailure));
+    return BackgroundLoadResource(type, name, sendEventOnFailure, caller);
 }
 
 template <class T> void ResourceCache::GetResources(PODVector<T*>& result) const

+ 8 - 0
Source/Engine/Resource/ResourceEvents.h

@@ -67,4 +67,12 @@ EVENT(E_UNKNOWNRESOURCETYPE, UnknownResourceType)
     PARAM(P_RESOURCETYPE, ResourceType);            // StringHash
 }
 
+/// Resource background loading finished.
+EVENT(E_RESOURCEBACKGROUNDLOADED, ResourceBackgroundLoaded)
+{
+    PARAM(P_RESOURCENAME, ResourceName);            // String
+    PARAM(P_SUCCESS, Success);                      // bool
+    PARAM(P_RESOURCE, Resource);                    // Resource pointer
+}
+
 }

+ 1 - 3
Source/Engine/Resource/XMLFile.cpp

@@ -79,10 +79,8 @@ void XMLFile::RegisterObject(Context* context)
     context->RegisterFactory<XMLFile>();
 }
 
-bool XMLFile::Load(Deserializer& source)
+bool XMLFile::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadXMLFile);
-
     unsigned dataSize = source.GetSize();
     if (!dataSize && !source.GetName().Empty())
     {

+ 2 - 2
Source/Engine/Resource/XMLFile.h

@@ -48,8 +48,8 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
     /// Save resource. Return true if successful. Only supports saving to a File.
     virtual bool Save(Serializer& dest) const;
     

+ 1 - 1
Source/Engine/Scene/ObjectAnimation.cpp

@@ -54,7 +54,7 @@ void ObjectAnimation::RegisterObject(Context* context)
     context->RegisterFactory<ObjectAnimation>();
 }
 
-bool ObjectAnimation::Load(Deserializer& source)
+bool ObjectAnimation::BeginLoad(Deserializer& source)
 {
     XMLFile xmlFile(context_);
     if (!xmlFile.Load(source))

+ 2 - 2
Source/Engine/Scene/ObjectAnimation.h

@@ -45,8 +45,8 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
 
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
     /// Save resource. Return true if successful.
     virtual bool Save(Serializer& dest) const;
     /// Load from XML data. Return true if successful.

+ 302 - 58
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,69 @@ 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)
+        {
+            PROFILE(FindResourcesToPreload);
+            
+            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
+    {
+        PROFILE(FindResourcesToPreload);
+        
+        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 +326,58 @@ 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;
+    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)
+        {
+            PROFILE(FindResourcesToPreload);
+            
+            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;
 
-    // Count the amount of child nodes
-    while (childNodeElement)
+        // 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");
+        PROFILE(FindResourcesToPreload);
+        
+        LOGINFO("Preloading resources from " + file->GetName());
+        PreloadResourcesXML(xml->GetRoot());
     }
-
+    
     return true;
 }
 
@@ -328,6 +387,7 @@ void Scene::StopAsyncLoading()
     asyncProgress_.file_.Reset();
     asyncProgress_.xmlFile_.Reset();
     asyncProgress_.xmlElement_ = XMLElement::EMPTY;
+    asyncProgress_.resources_.Clear();
     resolver_.Reset();
 }
 
@@ -438,6 +498,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 +579,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 +599,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 +960,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 +1014,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 +1022,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 +1067,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: preload resources used by the scene 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.

+ 9 - 9
Source/Engine/Script/GraphicsAPI.cpp

@@ -172,19 +172,19 @@ static Viewport* ConstructViewportSceneCameraRect(Scene* scene, Camera* camera,
     return new Viewport(GetScriptContext(), scene, camera, rect, renderPath);
 }
 
-static bool Texture2DLoad(Image* image, bool useAlpha, Texture2D* ptr)
+static bool Texture2DSetData(Image* image, bool useAlpha, Texture2D* ptr)
 {
-    return ptr->Load(SharedPtr<Image>(image), useAlpha);
+    return ptr->SetData(SharedPtr<Image>(image), useAlpha);
 }
 
-static bool Texture3DLoad(Image* image, bool useAlpha, Texture3D* ptr)
+static bool Texture3DSetData(Image* image, bool useAlpha, Texture3D* ptr)
 {
-    return ptr->Load(SharedPtr<Image>(image), useAlpha);
+    return ptr->SetData(SharedPtr<Image>(image), useAlpha);
 }
 
-static bool TextureCubeLoad(CubeMapFace face, Image* image, bool useAlpha, TextureCube* ptr)
+static bool TextureCubeSetData(CubeMapFace face, Image* image, bool useAlpha, TextureCube* ptr)
 {
-    return ptr->Load(face, SharedPtr<Image>(image), useAlpha);
+    return ptr->SetData(face, SharedPtr<Image>(image), useAlpha);
 }
 
 static void ConstructRenderTargetInfo(RenderTargetInfo* ptr)
@@ -446,17 +446,17 @@ static void RegisterTextures(asIScriptEngine* engine)
     
     RegisterTexture<Texture2D>(engine, "Texture2D");
     engine->RegisterObjectMethod("Texture2D", "bool SetSize(int, int, uint, TextureUsage usage = TEXTURE_STATIC)", asMETHOD(Texture2D, SetSize), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Texture2D", "bool Load(Image@+, bool useAlpha = false)", asFUNCTION(Texture2DLoad), asCALL_CDECL_OBJLAST);
+    engine->RegisterObjectMethod("Texture2D", "bool SetData(Image@+, bool useAlpha = false)", asFUNCTION(Texture2DSetData), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("Texture2D", "RenderSurface@+ get_renderSurface() const", asMETHOD(Texture2D, GetRenderSurface), asCALL_THISCALL);
 
     RegisterTexture<Texture3D>(engine, "Texture3D");
     engine->RegisterObjectMethod("Texture3D", "bool SetSize(int, int, uint, TextureUsage usage = TEXTURE_STATIC)", asMETHOD(Texture3D, SetSize), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Texture3D", "bool Load(Image@+, bool useAlpha = false)", asFUNCTION(Texture3DLoad), asCALL_CDECL_OBJLAST);
+    engine->RegisterObjectMethod("Texture3D", "bool SetData(Image@+, bool useAlpha = false)", asFUNCTION(Texture3DSetData), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("Texture3D", "RenderSurface@+ get_renderSurface() const", asMETHOD(Texture3D, GetRenderSurface), asCALL_THISCALL);
     
     RegisterTexture<TextureCube>(engine, "TextureCube");
     engine->RegisterObjectMethod("TextureCube", "bool SetSize(int, uint, TextureUsage usage = TEXTURE_STATIC)", asMETHOD(TextureCube, SetSize), asCALL_THISCALL);
-    engine->RegisterObjectMethod("TextureCube", "bool Load(CubeMapFace, Image@+, bool useAlpha = false)", asFUNCTION(TextureCubeLoad), asCALL_CDECL_OBJLAST);
+    engine->RegisterObjectMethod("TextureCube", "bool SetData(CubeMapFace, Image@+, bool useAlpha = false)", asFUNCTION(TextureCubeSetData), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("TextureCube", "RenderSurface@+ get_renderSurfaces(CubeMapFace) const", asMETHOD(TextureCube, GetRenderSurface), asCALL_THISCALL);
     
     engine->RegisterGlobalFunction("uint GetAlphaFormat()", asFUNCTION(Graphics::GetAlphaFormat), asCALL_CDECL);

+ 9 - 0
Source/Engine/Script/ResourceAPI.cpp

@@ -93,6 +93,11 @@ static CScriptArray* ResourceCacheGetPackageFiles(ResourceCache* ptr)
     return VectorToHandleArray<PackageFile>(ptr->GetPackageFiles(), "Array<PackageFile@>");
 }
 
+static bool ResourceCacheBackgroundLoadResource(const String& type, const String& name, bool sendEventOnFailure, ResourceCache* ptr)
+{
+    return ptr->BackgroundLoadResource(type, name, sendEventOnFailure);
+}
+
 static void RegisterResourceCache(asIScriptEngine* engine)
 {
     RegisterObject<ResourceCache>(engine, "ResourceCache");
@@ -116,6 +121,7 @@ static void RegisterResourceCache(asIScriptEngine* engine)
     engine->RegisterObjectMethod("ResourceCache", "String GetResourceFileName(const String&in) const", asMETHOD(ResourceCache, GetResourceFileName), asCALL_THISCALL);
     engine->RegisterObjectMethod("ResourceCache", "Resource@+ GetResource(const String&in, const String&in, bool sendEventOnFailure = true)", asFUNCTION(ResourceCacheGetResource), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("ResourceCache", "Resource@+ GetResource(StringHash, const String&in, bool sendEventOnFailure = true)", asMETHODPR(ResourceCache, GetResource, (StringHash, const String&, bool), Resource*), asCALL_THISCALL);
+    engine->RegisterObjectMethod("ResourceCache", "bool BackgroundLoadResource(const String&in, const String&in, bool sendEventOnFailure = true)", asFUNCTION(ResourceCacheBackgroundLoadResource), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("ResourceCache", "void set_memoryBudget(const String&in, uint)", asFUNCTION(ResourceCacheSetMemoryBudget), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("ResourceCache", "uint get_memoryBudget(const String&in) const", asFUNCTION(ResourceCacheGetMemoryBudget), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("ResourceCache", "uint get_memoryUse(const String&in) const", asFUNCTION(ResourceCacheGetMemoryUse), asCALL_CDECL_OBJLAST);
@@ -128,6 +134,9 @@ static void RegisterResourceCache(asIScriptEngine* engine)
     engine->RegisterObjectMethod("ResourceCache", "bool get_autoReloadResources() const", asMETHOD(ResourceCache, GetAutoReloadResources), asCALL_THISCALL);
     engine->RegisterObjectMethod("ResourceCache", "void set_returnFailedResources(bool)", asMETHOD(ResourceCache, SetReturnFailedResources), asCALL_THISCALL);
     engine->RegisterObjectMethod("ResourceCache", "bool get_returnFailedResources() const", asMETHOD(ResourceCache, GetReturnFailedResources), asCALL_THISCALL);
+    engine->RegisterObjectMethod("ResourceCache", "void set_finishBackgroundResourcesMs(int)", asMETHOD(ResourceCache, SetFinishBackgroundResourcesMs), asCALL_THISCALL);
+    engine->RegisterObjectMethod("ResourceCache", "int get_finishBackgroundResourcesMs() const", asMETHOD(ResourceCache, GetFinishBackgroundResourcesMs), asCALL_THISCALL);
+    engine->RegisterObjectMethod("ResourceCache", "uint get_numBackgroundLoadResources() const", asMETHOD(ResourceCache, GetNumBackgroundLoadResources), asCALL_THISCALL);
     engine->RegisterGlobalFunction("ResourceCache@+ get_resourceCache()", asFUNCTION(GetResourceCache), asCALL_CDECL);
     engine->RegisterGlobalFunction("ResourceCache@+ get_cache()", asFUNCTION(GetResourceCache), asCALL_CDECL);
 }

+ 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);

+ 5 - 0
Source/Engine/Script/Script.h

@@ -22,6 +22,7 @@
 
 #pragma once
 
+#include "Mutex.h"
 #include "Object.h"
 
 class asIObjectType;
@@ -92,6 +93,8 @@ public:
     void ClearObjectTypeCache();
     /// Query for an inbuilt object type by constant declaration. Can not be used for script types.
     asIObjectType* GetObjectType(const char* declaration);
+    /// Return the script module create/delete mutex.
+    Mutex& GetModuleMutex() { return moduleMutex_; }
 
 private:
     /// Increase script nesting level.
@@ -119,6 +122,8 @@ private:
     Vector<asIScriptContext*> scriptFileContexts_;
     /// Search cache for inbuilt object types.
     HashMap<const char*, asIObjectType*> objectTypes_;
+    /// Script module create/delete mutex.
+    Mutex moduleMutex_;
     /// Current script execution nesting level.
     unsigned scriptNestingLevel_;
     /// Flag for executing engine console commands as script code. Default to true.

+ 68 - 41
Source/Engine/Script/ScriptFile.cpp

@@ -21,11 +21,11 @@
 //
 
 #include "Precompiled.h"
-#include "ArrayPtr.h"
 #include "Context.h"
 #include "CoreEvents.h"
 #include "FileSystem.h"
 #include "Log.h"
+#include "MemoryBuffer.h"
 #include "Profiler.h"
 #include "ResourceCache.h"
 #include "Script.h"
@@ -72,7 +72,7 @@ class ByteCodeDeserializer : public asIBinaryStream
 {
 public:
     /// Construct.
-    ByteCodeDeserializer(Deserializer& source) :
+    ByteCodeDeserializer(MemoryBuffer& source) :
         source_(source)
     {
     }
@@ -90,7 +90,7 @@ public:
     
 private:
     /// Source stream.
-    Deserializer& source_;
+    MemoryBuffer& source_;
 };
 
 ScriptFile::ScriptFile(Context* context) :
@@ -112,58 +112,80 @@ void ScriptFile::RegisterObject(Context* context)
     context->RegisterFactory<ScriptFile>();
 }
 
-bool ScriptFile::Load(Deserializer& source)
+bool ScriptFile::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadScript);
-    
     ReleaseModule();
+    loadByteCode_.Reset();
     
-    // Create the module. Discard previous module if there was one
     asIScriptEngine* engine = script_->GetScriptEngine();
-    scriptModule_ = engine->GetModule(GetName().CString(), asGM_ALWAYS_CREATE);
-    if (!scriptModule_)
+    
     {
-        LOGERROR("Failed to create script module " + GetName());
-        return false;
+        MutexLock lock(script_->GetModuleMutex());
+    
+        // Create the module. Discard previous module if there was one
+        scriptModule_ = engine->GetModule(GetName().CString(), asGM_ALWAYS_CREATE);
+        if (!scriptModule_)
+        {
+            LOGERROR("Failed to create script module " + GetName());
+            return false;
+        }
     }
     
     // Check if this file is precompiled bytecode
     if (source.ReadFileID() == "ASBC")
     {
-        ByteCodeDeserializer deserializer = ByteCodeDeserializer(source);
+        // Perform actual parsing in EndLoad(); read data now
+        loadByteCodeSize_ = source.GetSize() - source.GetPosition();
+        loadByteCode_ = new unsigned char[loadByteCodeSize_];
+        source.Read(loadByteCode_.Get(), loadByteCodeSize_);
+        return true;
+    }
+    else
+        source.Seek(0);
+    
+    // Not bytecode: add the initial section and check for includes.
+    // Perform actual building during EndLoad(), as AngelScript can not multithread module compilation,
+    // and static initializers may access arbitrary engine functionality which may not be thread-safe
+    return AddScriptSection(engine, source);
+}
+
+bool ScriptFile::EndLoad()
+{
+    bool success = false;
+
+    // Load from bytecode if available, else compile
+    if (loadByteCode_)
+    {
+        MemoryBuffer buffer(loadByteCode_.Get(), loadByteCodeSize_);
+        ByteCodeDeserializer deserializer = ByteCodeDeserializer(buffer);
+
         if (scriptModule_->LoadByteCode(&deserializer) >= 0)
         {
             LOGINFO("Loaded script module " + GetName() + " from bytecode");
-            compiled_ = true;
-            // Map script module to script resource with userdata
-            scriptModule_->SetUserData(this);
-            
-            return true;
+            success = true;
         }
-        else
-            return false;
     }
     else
-        source.Seek(0);
-    
-    // Not bytecode: add the initial section and check for includes
-    if (!AddScriptSection(engine, source))
-        return false;
-    
-    // Compile
-    int result = scriptModule_->Build();
-    if (result < 0)
     {
-        LOGERROR("Failed to compile script module " + GetName());
-        return false;
+        int result = scriptModule_->Build();
+        if (result >= 0)
+        {
+            LOGINFO("Compiled script module " + GetName());
+            success = true;
+        }
+        else
+            LOGERROR("Failed to compile script module " + GetName());
     }
     
-    LOGINFO("Compiled script module " + GetName());
-    compiled_ = true;
-    // Map script module to script resource with userdata
-    scriptModule_->SetUserData(this);
-    
-    return true;
+    if (success)
+    {
+        compiled_ = true;
+        // Map script module to script resource with userdata
+        scriptModule_->SetUserData(this);
+    }
+
+    loadByteCode_.Reset();
+    return success;
 }
 
 void ScriptFile::AddEventHandler(StringHash eventType, const String& handlerName)
@@ -719,8 +741,6 @@ void ScriptFile::ReleaseModule()
 {
     if (scriptModule_)
     {
-        script_->ClearObjectTypeCache();
-        
         // Clear search caches and event handlers
         includeFiles_.Clear();
         validClasses_.Clear();
@@ -729,10 +749,17 @@ void ScriptFile::ReleaseModule()
         delayedCalls_.Clear();
         eventInvokers_.Clear();
         
-        // Remove the module
-        scriptModule_->SetUserData(0);
         asIScriptEngine* engine = script_->GetScriptEngine();
-        engine->DiscardModule(GetName().CString());
+        scriptModule_->SetUserData(0);
+        
+        // Remove the module
+        {
+            MutexLock lock(script_->GetModuleMutex());
+            
+            script_->ClearObjectTypeCache();
+            engine->DiscardModule(GetName().CString());
+        }
+        
         scriptModule_ = 0;
         compiled_ = false;
         SetMemoryUse(0);

+ 9 - 2
Source/Engine/Script/ScriptFile.h

@@ -22,6 +22,7 @@
 
 #pragma once
 
+#include "ArrayPtr.h"
 #include "HashSet.h"
 #include "Resource.h"
 #include "ScriptEventListener.h"
@@ -54,8 +55,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
     
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
 
     /// Add a scripted event handler.
     virtual void AddEventHandler(StringHash eventType, const String& handlerName);
@@ -132,6 +135,10 @@ private:
     Vector<DelayedCall> delayedCalls_;
     /// Event helper objects for handling procedural or non-ScriptInstance script events
     HashMap<asIScriptObject*, SharedPtr<ScriptEventInvoker> > eventInvokers_;
+    /// Byte code for asynchronous loading.
+    SharedArrayPtr<unsigned char> loadByteCode_;
+    /// Byte code size for asynchronous loading.
+    unsigned loadByteCodeSize_;
 };
 
 /// Helper class for forwarding events to script objects that are not part of a scene.

+ 1 - 1
Source/Engine/UI/Cursor.cpp

@@ -130,7 +130,7 @@ void Cursor::DefineShape(CursorShape shape, Image* image, const IntRect& imageRe
     if (!info.texture_)
     {
         Texture2D* texture = new Texture2D(context_);
-        texture->Load(SharedPtr<Image>(image));
+        texture->SetData(SharedPtr<Image>(image));
         info.texture_ = texture;
     }
 

+ 1 - 3
Source/Engine/UI/Font.cpp

@@ -57,10 +57,8 @@ void Font::RegisterObject(Context* context)
     context->RegisterFactory<Font>();
 }
 
-bool Font::Load(Deserializer& source)
+bool Font::BeginLoad(Deserializer& source)
 {
-    PROFILE(LoadFont);
-
     // In headless mode, do not actually load, just return success
     Graphics* graphics = GetSubsystem<Graphics>();
     if (!graphics)

+ 3 - 2
Source/Engine/UI/Font.h

@@ -54,8 +54,9 @@ public:
     virtual ~Font();
     /// Register object factory.
     static void RegisterObject(Context* context);
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
     /// Save resource as a new bitmap font type in XML format. Return true if successful.
     bool SaveXML(Serializer& dest, int pointSize, bool usedGlyphs = false);
     /// Return font face. Pack and render to a texture if not rendered yet. Return null on error.

+ 1 - 1
Source/Engine/UI/FontFace.cpp

@@ -114,7 +114,7 @@ SharedPtr<Texture2D> FontFace::CreateFaceTexture()
 SharedPtr<Texture2D> FontFace::LoadFaceTexture(SharedPtr<Image> image)
 {
     SharedPtr<Texture2D> texture = CreateFaceTexture();
-    if (!texture->Load(image, true))
+    if (!texture->SetData(image, true))
     {
         LOGERROR("Could not load texture from image resource");
         return SharedPtr<Texture2D>();

+ 85 - 36
Source/Engine/Urho2D/AnimationSet2D.cpp

@@ -50,38 +50,68 @@ void AnimationSet2D::RegisterObject(Context* context)
     context->RegisterFactory<AnimationSet2D>();
 }
 
-bool AnimationSet2D::Load(Deserializer& source)
+bool AnimationSet2D::BeginLoad(Deserializer& source)
 {
-    XMLFile xmlFile(context_);
-    if (!xmlFile.Load(source))
+    loadXMLFile_ = new XMLFile(context_);
+    if (!loadXMLFile_->Load(source))
     {
-        LOGERROR("Load XML filed " + source.GetName());
+        LOGERROR("Load XML failed " + source.GetName());
+        loadXMLFile_.Reset();
         return false;
     }
 
-    XMLElement rootElem = xmlFile.GetRoot("spriter_data");
+    XMLElement rootElem = loadXMLFile_->GetRoot("spriter_data");
     if (!rootElem)
     {
         LOGERROR("Invalid spriter file " + source.GetName());
+        loadXMLFile_.Reset();
         return false;
     }
     
+    // When async loading, preprocess folders for spritesheet / sprite files and request them for background loading
+    if (GetAsyncLoadState() == ASYNC_LOADING)
+    {
+        if (!LoadFolders(rootElem))
+        {
+            loadXMLFile_.Reset();
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool AnimationSet2D::EndLoad()
+{
+    // Actually load the folders and animations now
+    if (!loadXMLFile_)
+        return false;
+
+    XMLElement rootElem = loadXMLFile_->GetRoot("spriter_data");
     if (!LoadFolders(rootElem))
+    {
+        loadXMLFile_.Reset();
         return false;
+    }
 
     XMLElement entityElem = rootElem.GetChild("entity");
     if (!entityElem)
     {
         LOGERROR("Could not find entity");
+        loadXMLFile_.Reset();
         return false;
     }
     
     for (XMLElement animationElem = entityElem.GetChild("animation"); animationElem; animationElem = animationElem.GetNext("animation"))
     {
         if (!LoadAnimation(animationElem))
+        {
+            loadXMLFile_.Reset();
             return false;
+        }
     }
 
+    loadXMLFile_.Reset();
     return true;
 }
 
@@ -111,9 +141,22 @@ bool AnimationSet2D::LoadFolders(const XMLElement& rootElem)
 {
     ResourceCache* cache = GetSubsystem<ResourceCache>();
 
+    bool async = GetAsyncLoadState() == ASYNC_LOADING;
+
     String parentPath = GetParentPath(GetName());
     String spriteSheetFilePath = parentPath + GetFileName(GetName()) + ".xml";
-    SpriteSheet2D* spriterSheet = cache->GetResource<SpriteSheet2D>(spriteSheetFilePath, false);
+    SpriteSheet2D* spriteSheet = 0;
+    bool hasSpriteSheet = false;
+
+    // When async loading, request the sprite sheet for background loading but do not actually get it
+    if (!async)
+        spriteSheet = cache->GetResource<SpriteSheet2D>(spriteSheetFilePath, false);
+    else
+    {
+        hasSpriteSheet = cache->Exists(spriteSheetFilePath);
+        if (hasSpriteSheet)
+            cache->BackgroundLoadResource<SpriteSheet2D>(spriteSheetFilePath, false, this);
+    }
 
     for (XMLElement folderElem = rootElem.GetChild("folder"); folderElem; folderElem = folderElem.GetNext("folder"))
     {
@@ -124,43 +167,49 @@ bool AnimationSet2D::LoadFolders(const XMLElement& rootElem)
             unsigned fileId = fileElem.GetUInt("id");
             String fileName = fileElem.GetAttribute("name");
 
-            SharedPtr<Sprite2D> sprite;
-            
-            if (spriterSheet)
-                sprite = spriterSheet->GetSprite(GetFileName(fileName));
-            else
-                sprite = (cache->GetResource<Sprite2D>(parentPath + fileName));
-
-            if (!sprite)
+            // When async loading, request the sprites for background loading but do not actually get them
+            if (!async)
             {
-                LOGERROR("Could not load sprite " + fileName);
-                return false;
-            }
+                SharedPtr<Sprite2D> sprite;
+                
+                if (spriteSheet)
+                    sprite = spriteSheet->GetSprite(GetFileName(fileName));
+                else
+                    sprite = (cache->GetResource<Sprite2D>(parentPath + fileName));
 
-            Vector2 hotSpot(0.0f, 1.0f);
-            if (fileElem.HasAttribute("pivot_x"))
-                hotSpot.x_ = fileElem.GetFloat("pivot_x");
-            if (fileElem.HasAttribute("pivot_y"))
-                hotSpot.y_ = fileElem.GetFloat("pivot_y");
+                if (!sprite)
+                {
+                    LOGERROR("Could not load sprite " + fileName);
+                    return false;
+                }
 
-            // If sprite is trimmed, recalculate hot spot
-            const IntVector2& offset = sprite->GetOffset();
-            if (offset != IntVector2::ZERO)
-            {
-                int width = fileElem.GetInt("width");
-                int height = fileElem.GetInt("height");
+                Vector2 hotSpot(0.0f, 1.0f);
+                if (fileElem.HasAttribute("pivot_x"))
+                    hotSpot.x_ = fileElem.GetFloat("pivot_x");
+                if (fileElem.HasAttribute("pivot_y"))
+                    hotSpot.y_ = fileElem.GetFloat("pivot_y");
+
+                // If sprite is trimmed, recalculate hot spot
+                const IntVector2& offset = sprite->GetOffset();
+                if (offset != IntVector2::ZERO)
+                {
+                    int width = fileElem.GetInt("width");
+                    int height = fileElem.GetInt("height");
 
-                float pivotX = width * hotSpot.x_;
-                float pivotY = height * (1.0f - hotSpot.y_);
+                    float pivotX = width * hotSpot.x_;
+                    float pivotY = height * (1.0f - hotSpot.y_);
 
-                const IntRect& rectangle = sprite->GetRectangle();
-                hotSpot.x_ = (offset.x_ + pivotX) / rectangle.Width();
-                hotSpot.y_ = 1.0f - (offset.y_ + pivotY) / rectangle.Height();
-            }
+                    const IntRect& rectangle = sprite->GetRectangle();
+                    hotSpot.x_ = (offset.x_ + pivotX) / rectangle.Width();
+                    hotSpot.y_ = 1.0f - (offset.y_ + pivotY) / rectangle.Height();
+                }
 
-            sprite->SetHotSpot(hotSpot);
+                sprite->SetHotSpot(hotSpot);
 
-            sprites_[(folderId << 16) + fileId] = sprite;
+                sprites_[(folderId << 16) + fileId] = sprite;
+            }
+            else if (!hasSpriteSheet)
+                cache->BackgroundLoadResource<Sprite2D>(parentPath + fileName, true, this);
         }
     }
 

+ 7 - 2
Source/Engine/Urho2D/AnimationSet2D.h

@@ -30,6 +30,7 @@ namespace Urho3D
 class Animation2D;
 class Sprite2D;
 class XMLElement;
+class XMLFile;
 
 /// Spriter animation set, it includes one or more animations, for more information please refer to http://www.brashmonkey.com/spriter.htm.
 class URHO3D_API AnimationSet2D : public Resource
@@ -44,8 +45,10 @@ public:
     /// Register object factory. 
     static void RegisterObject(Context* context);
 
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
 
     /// Get number of animations.
     unsigned GetNumAnimations() const;
@@ -66,6 +69,8 @@ private:
     HashMap<unsigned, SharedPtr<Sprite2D> > sprites_;
     /// Animations.
     Vector<SharedPtr<Animation2D> > animations_;
+    /// XML file used during loading.
+    SharedPtr<XMLFile> loadXMLFile_;
 };
 
 }

+ 23 - 8
Source/Engine/Urho2D/ParticleEffect2D.cpp

@@ -105,8 +105,10 @@ void ParticleEffect2D::RegisterObject(Context* context)
     context->RegisterFactory<ParticleEffect2D>();
 }
 
-bool ParticleEffect2D::Load(Deserializer& source)
+bool ParticleEffect2D::BeginLoad(Deserializer& source)
 {
+    loadSpriteName_.Clear();
+
     XMLFile xmlFile(context_);
     if (!xmlFile.Load(source))
         return false;
@@ -116,13 +118,10 @@ bool ParticleEffect2D::Load(Deserializer& source)
         return false;
 
     String texture = rootElem.GetChild("texture").GetAttribute("name");
-    ResourceCache* cache = GetSubsystem<ResourceCache>();
-    sprite_= cache->GetResource<Sprite2D>(GetParentPath(GetName()) + texture);
-    if (!sprite_)
-    {
-        LOGERROR("Could not load sprite " + GetParentPath(GetName()) + texture);
-        return false;
-    }
+    loadSpriteName_ = GetParentPath(GetName()) + texture;
+    // If async loading, request the sprite beforehand
+    if (GetAsyncLoadState() == ASYNC_LOADING)
+        GetSubsystem<ResourceCache>()->BackgroundLoadResource<Sprite2D>(loadSpriteName_, true, this);
 
     sourcePositionVariance_ = ReadVector2(rootElem, "sourcePositionVariance");
 
@@ -198,6 +197,22 @@ bool ParticleEffect2D::Load(Deserializer& source)
     return true;
 }
 
+bool ParticleEffect2D::EndLoad()
+{
+    // Apply the sprite now
+    if (!loadSpriteName_.Empty())
+    {
+        ResourceCache* cache = GetSubsystem<ResourceCache>();
+        sprite_ = cache->GetResource<Sprite2D>(loadSpriteName_);
+        if (!sprite_)
+            LOGERROR("Could not load sprite " + loadSpriteName_ + " for particle effect");
+
+        loadSpriteName_.Clear();
+    }
+    
+    return true;
+}
+
 bool ParticleEffect2D::Save(Serializer& dest) const
 {
     if (!sprite_)

+ 10 - 6
Source/Engine/Urho2D/ParticleEffect2D.h

@@ -48,11 +48,13 @@ public:
     ParticleEffect2D(Context* context);
     /// Destruct.
     ~ParticleEffect2D();
-    /// Register object factory. drawable2d must be registered first.
+    /// Register object factory. Drawable2D must be registered first.
     static void RegisterObject(Context* context);
 
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
     /// Save resource. Return true if successful.
     virtual bool Save(Serializer& dest) const;
 
@@ -208,11 +210,11 @@ private:
     /// Read Vector2.
     Vector2 ReadVector2(const XMLElement& element, const String& name) const;
     /// Write integer.
-	void WriteInt(XMLElement& element, const String& name, int value) const;
+    void WriteInt(XMLElement& element, const String& name, int value) const;
     /// Write float.
-	void WriteFloat(XMLElement& element, const String& name, float value) const;
+    void WriteFloat(XMLElement& element, const String& name, float value) const;
     /// Write Color.
-	void WriteColor(XMLElement& element, const String& name, const Color& color) const;
+    void WriteColor(XMLElement& element, const String& name, const Color& color) const;
     /// Write Vector2.
     void WriteVector2(XMLElement& element, const String& name, const Vector2& value) const;
 
@@ -286,6 +288,8 @@ private:
     float rotationEnd_;
     /// Rotation end variance.
     float rotationEndVariance_;
+    /// Sprite name acquired during BeginLoad().
+    String loadSpriteName_;
 };
 
 }

+ 26 - 10
Source/Engine/Urho2D/Sprite2D.cpp

@@ -49,21 +49,37 @@ void Sprite2D::RegisterObject(Context* context)
     context->RegisterFactory<Sprite2D>();
 }
 
-bool Sprite2D::Load(Deserializer& source)
+bool Sprite2D::BeginLoad(Deserializer& source)
 {
-    SharedPtr<Texture2D> texture(new Texture2D(context_));
-    texture->SetName(GetName());
-    if (!texture->Load(source))
+    loadTexture_ = new Texture2D(context_);
+    loadTexture_->SetName(GetName());
+    // In case we're async loading, only call BeginLoad() for the texture (load image but do not upload to GPU)
+    if (!loadTexture_->BeginLoad(source))
+    {
+        loadTexture_.Reset();
         return false;
-
-    SetTexture(texture);
-
-    if (texture_)
-        SetRectangle(IntRect(0, 0, texture_->GetWidth(), texture_->GetHeight()));
-
+    }
+    
     return true;
 }
 
+bool Sprite2D::EndLoad()
+{
+    // Finish loading of the texture in the main thread
+    bool success = false;
+    if (loadTexture_ && loadTexture_->EndLoad())
+    {
+        success = true;
+        SetTexture(loadTexture_);
+        
+        if (texture_)
+            SetRectangle(IntRect(0, 0, texture_->GetWidth(), texture_->GetHeight()));
+    }
+
+    loadTexture_.Reset();
+    return success;
+}
+
 void Sprite2D::SetTexture(Texture2D* texture)
 {
     texture_ = texture;

+ 6 - 2
Source/Engine/Urho2D/Sprite2D.h

@@ -57,8 +57,10 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
 
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
 
     /// Set texture.
     void SetTexture(Texture2D* texture);
@@ -93,6 +95,8 @@ private:
     IntVector2 offset_;
     /// Sprite sheet.
     WeakPtr<SpriteSheet2D> spriteSheet_;
+    /// Texture used while loading.
+    SharedPtr<Texture2D> loadTexture_;
 };
 
 }

+ 27 - 7
Source/Engine/Urho2D/SpriteSheet2D.cpp

@@ -54,35 +54,53 @@ void SpriteSheet2D::RegisterObject(Context* context)
     context->RegisterFactory<SpriteSheet2D>();
 }
 
-bool SpriteSheet2D::Load(Deserializer& source)
+bool SpriteSheet2D::BeginLoad(Deserializer& source)
 {
+    loadTextureName_.Clear();
     spriteMapping_.Clear();
 
-    SharedPtr<XMLFile> xmlFile(new XMLFile(context_));
-    if(!xmlFile->Load(source))
+    loadXMLFile_ = new XMLFile(context_);
+    if(!loadXMLFile_->Load(source))
     {
         LOGERROR("Could not load sprite sheet");
+        loadXMLFile_.Reset();
         return false;
     }
 
     SetMemoryUse(source.GetSize());
 
-    XMLElement rootElem = xmlFile->GetRoot("TextureAtlas");
+    XMLElement rootElem = loadXMLFile_->GetRoot("TextureAtlas");
     if (!rootElem)
     {
         LOGERROR("Invalid sprite sheet");
+        loadXMLFile_.Reset();
         return false;
     }
 
-    String textureFileName = rootElem.GetAttribute("imagePath");
+    // If we're async loading, request the texture now. Finish during EndLoad().
+    loadTextureName_ = GetParentPath(GetName()) + rootElem.GetAttribute("imagePath");
+    if (GetAsyncLoadState() == ASYNC_LOADING)
+        GetSubsystem<ResourceCache>()->BackgroundLoadResource<Texture2D>(loadTextureName_, true, this);
+
+    return true;
+}
+
+bool SpriteSheet2D::EndLoad()
+{
+    if (!loadXMLFile_)
+        return false;
+
     ResourceCache* cache = GetSubsystem<ResourceCache>();
-    texture_ = cache->GetResource<Texture2D>(GetParentPath(GetName()) + textureFileName);
+    texture_ = cache->GetResource<Texture2D>(loadTextureName_);
     if (!texture_)
     {
-        LOGERROR("Could not load texture " + GetParentPath(GetName()) + textureFileName);
+        LOGERROR("Could not load texture " + loadTextureName_);
+        loadXMLFile_.Reset();
+        loadTextureName_.Clear();
         return false;
     }
 
+    XMLElement rootElem = loadXMLFile_->GetRoot("TextureAtlas");
     XMLElement subTextureElem = rootElem.GetChild("SubTexture");
     while (subTextureElem)
     {
@@ -111,6 +129,8 @@ bool SpriteSheet2D::Load(Deserializer& source)
         subTextureElem = subTextureElem.GetNext("SubTexture");
     }
 
+    loadXMLFile_.Reset();
+    loadTextureName_.Clear();
     return true;
 }
 

+ 10 - 3
Source/Engine/Urho2D/SpriteSheet2D.h

@@ -29,6 +29,7 @@ namespace Urho3D
 
 class Sprite2D;
 class Texture2D;
+class XMLFile;
 
 /// Sprite sheet.
 class URHO3D_API SpriteSheet2D : public Resource
@@ -43,9 +44,11 @@ public:
     /// Register object factory.
     static void RegisterObject(Context* context);
 
-    /// Load resource. Return true if successful.
-    virtual bool Load(Deserializer& source);
-    
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Finish resource loading. Always called from the main thread. Return true if successful.
+    virtual bool EndLoad();
+
     /// Return texture.
     Texture2D* GetTexture() const { return texture_; }
     /// Return sprite.
@@ -61,6 +64,10 @@ private:
     SharedPtr<Texture2D> texture_;
     /// Sprite mapping.
     HashMap<String, SharedPtr<Sprite2D> > spriteMapping_;
+    /// XML file used while loading.
+    SharedPtr<XMLFile> loadXMLFile_;
+    /// Texture name used while loading.
+    String loadTextureName_;
 };
 
 }