Browse Source

Added FileWatcher class (for now Windows only) & live resource reloading.

Lasse Öörni 14 years ago
parent
commit
7ec5ec4f6e

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

@@ -22,6 +22,8 @@ void Start()
     SubscribeToEvent("Update", "HandleUpdate");
     // Enable console commands from the editor script
     script.defaultScriptFile = scriptFile;
+    // Enable automatic resource reloading
+    cache.autoReloadResources = true;
 
     CreateScene();
     CreateUI();

+ 1 - 1
Docs/GettingStarted.dox

@@ -586,7 +586,7 @@ To create a user variable into the current node, or delete it, type the variable
 
 While editing, you can execute script files using the "Run script" item in the %File menu. These are AngelScript files that are executed in immediate mode ie. you do not need to define a function. The editor's scene will be accessible to the script as the global property "scene."
 
-Currently, when you edit for example a material or texture, you need to manually reload scene resources (Ctrl+R) to make the changes visible.
+Automatic resource reloading when changed (for example editing a texture in a paint program while the scene editor is running) is currently supported on Windows only. On other platforms you need to manually reload scene resources (Ctrl+R) after editing to make the changes visible.
 
 Components of same type can be multi-edited. Where attribute values differ, the attribute field will be left blank, but editing the attribute will apply the change to all components.
 

+ 1 - 0
Docs/ScriptAPI.dox

@@ -1053,6 +1053,7 @@ Properties:<br>
 - uint totalMemoryUse (readonly)
 - String[]@ resourceDirs (readonly)
 - PackageFile@[]@ packageFiles (readonly)
+- bool autoReloadResources
 
 
 Image

+ 0 - 1
Engine/Core/MiniDump.cpp

@@ -28,7 +28,6 @@
 #include "ProcessUtils.h"
 
 #include <cstdio>
-#include <list>
 #include <io.h>
 #include <fcntl.h>
 #include <time.h>

+ 1 - 1
Engine/Core/Thread.h

@@ -30,7 +30,7 @@ public:
     /// Construct. Does not start the thread yet.
     Thread();
     /// Destruct. If running, stop and wait for thread to finish.
-    ~Thread();
+    virtual ~Thread();
     
     /// The function to run in the thread.
     virtual void ThreadFunction() = 0;

+ 2 - 0
Engine/Engine/ResourceAPI.cpp

@@ -120,6 +120,8 @@ static void RegisterResourceCache(asIScriptEngine* engine)
     engine->RegisterObjectMethod("ResourceCache", "uint get_totalMemoryUse() const", asMETHOD(ResourceCache, GetTotalMemoryUse), asCALL_THISCALL);
     engine->RegisterObjectMethod("ResourceCache", "Array<String>@ get_resourceDirs() const", asFUNCTION(ResourceCacheGetResourceDirs), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("ResourceCache", "Array<PackageFile@>@ get_packageFiles() const", asFUNCTION(ResourceCacheGetPackageFiles), asCALL_CDECL_OBJLAST);
+    engine->RegisterObjectMethod("ResourceCache", "void set_autoReloadResources(bool)", asMETHOD(ResourceCache, SetAutoReloadResources), asCALL_THISCALL);
+    engine->RegisterObjectMethod("ResourceCache", "bool get_autoReloadResources() const", asMETHOD(ResourceCache, GetAutoReloadResources), asCALL_THISCALL);
     engine->RegisterGlobalFunction("ResourceCache@+ get_resourceCache()", asFUNCTION(GetResourceCache), asCALL_CDECL);
     engine->RegisterGlobalFunction("ResourceCache@+ get_cache()", asFUNCTION(GetResourceCache), asCALL_CDECL);
 }

+ 172 - 0
Engine/IO/FileWatcher.cpp

@@ -0,0 +1,172 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2012 Lasse Öörni
+//
+// 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 "File.h"
+#include "FileWatcher.h"
+#include "FileSystem.h"
+#include "Log.h"
+
+#ifdef WIN32
+#include <windows.h>
+
+static const unsigned BUFFERSIZE = 4096;
+#endif
+
+OBJECTTYPESTATIC(FileWatcher);
+
+FileWatcher::FileWatcher(Context* context) :
+    Object(context),
+    watchSubDirs_(false)
+{
+}
+
+FileWatcher::~FileWatcher()
+{
+    StopWatching();
+}
+
+bool FileWatcher::StartWatching(const String& path, bool watchSubDirs)
+{
+    // Stop any previous watching
+    StopWatching();
+    
+#ifdef WIN32
+    String nativePath = GetNativePath(RemoveTrailingSlash(path));
+    
+    dirHandle_ = (void*)CreateFile(
+        nativePath.CString(),
+        FILE_LIST_DIRECTORY,
+        FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
+        0,
+        OPEN_EXISTING,
+        FILE_FLAG_BACKUP_SEMANTICS,
+        0);
+    
+    if (dirHandle_ != INVALID_HANDLE_VALUE)
+    {
+        path_ = AddTrailingSlash(path);
+        watchSubDirs_ = watchSubDirs;
+        Start();
+        
+        LOGDEBUG("Started watching path " + path);
+        return true;
+    }
+    else
+    {
+        LOGERROR("Failed to start watching path " + path);
+        return false;
+    }
+#else
+    LOGERROR("Can not start watching path " + path + ", FileWatcher not implemented yet");
+#endif
+}
+
+void FileWatcher::StopWatching()
+{
+    if (handle_)
+    {
+        shouldRun_ = false;
+        
+        // Create and delete a dummy file to make sure the watcher loop terminates
+        String dummyFileName = path_ + "dummy.tmp";
+        File file(context_, dummyFileName, FILE_WRITE);
+        file.Close();
+        GetSubsystem<FileSystem>()->Delete(dummyFileName);
+        
+        Stop();
+        
+        #ifdef WIN32
+        CloseHandle((HANDLE)dirHandle_);
+        #endif
+        
+        LOGDEBUG("Stopped watching path " + path_);
+        path_.Clear();
+    }
+}
+
+void FileWatcher::ThreadFunction()
+{
+#ifdef WIN32
+    char buffer[BUFFERSIZE];
+    DWORD bytesFilled = 0;
+    
+    while (shouldRun_)
+    {
+        if (ReadDirectoryChangesW((HANDLE)dirHandle_,
+            buffer,
+            BUFFERSIZE,
+            watchSubDirs_,
+            FILE_NOTIFY_CHANGE_FILE_NAME |
+            FILE_NOTIFY_CHANGE_LAST_WRITE,
+            &bytesFilled,
+            0,
+            0))
+        {
+            unsigned offset = 0;
+            
+            for (;;)
+            {
+                FILE_NOTIFY_INFORMATION* record = (FILE_NOTIFY_INFORMATION*)&buffer[offset];
+                
+                if (record->Action == FILE_ACTION_MODIFIED) // Modify
+                {
+                    String fileName;
+                    fileName.Resize(record->FileNameLength / 2);
+                    /// \todo Proper Unicode filename support
+                    for (unsigned i = 0; i < record->FileNameLength / 2; ++i)
+                        fileName[i] = (char)record->FileName[i];
+                    
+                    fileName = GetInternalPath(fileName);
+                    
+                    {
+                        MutexLock lock(changesMutex_);
+                        // If we have 2 unprocessed modifies in a row into the same file, only record the first
+                        if (changes_.Empty() || changes_.Back() != fileName)
+                            changes_.Push(fileName);
+                    }
+                }
+                
+                if (!record->NextEntryOffset)
+                    break;
+                else
+                    offset += record->NextEntryOffset;
+            }
+        }
+    }
+#endif
+}
+
+bool FileWatcher::GetNextChange(String& dest)
+{
+    MutexLock lock(changesMutex_);
+    
+    if (changes_.Empty())
+        return false;
+    else
+    {
+        dest = changes_.Front();
+        changes_.Erase(changes_.Begin());
+        return true;
+    }
+}

+ 67 - 0
Engine/IO/FileWatcher.h

@@ -0,0 +1,67 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2012 Lasse Öörni
+//
+// 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 "List.h"
+#include "Mutex.h"
+#include "Object.h"
+#include "Thread.h"
+
+/// Watches a path and its subdirectories for files being modified
+class FileWatcher : public Object, public Thread
+{
+    OBJECT(FileWatcher);
+    
+public:
+    /// Construct.
+    FileWatcher(Context* context);
+    /// Destruct.
+    virtual ~FileWatcher();
+    
+    /// Directory watching loop.
+    virtual void ThreadFunction();
+    
+    /// Start watching a directory. Return true if successful.
+    bool StartWatching(const String& path, bool watchSubDirs);
+    /// Stop watching the directory.
+    void StopWatching();
+    /// Return a file change (true if was found, false if not.)
+    bool GetNextChange(String& dest);
+    
+    /// Return the path being watched, or empty if not watching.
+    const String& GetPath() const { return path_; }
+    
+private:
+    /// The path being watched.
+    String path_;
+    /// Buffered file changes.
+    List<String> changes_;
+    /// Mutex for the change buffer.
+    Mutex changesMutex_;
+    /// Watch subdirectories flag.
+    bool watchSubDirs_;
+    
+    // Directory handle for the path being watched
+    void* dirHandle_;
+};

+ 61 - 3
Engine/Resource/ResourceCache.cpp

@@ -23,7 +23,9 @@
 
 #include "Precompiled.h"
 #include "Context.h"
+#include "CoreEvents.h"
 #include "FileSystem.h"
+#include "FileWatcher.h"
 #include "Image.h"
 #include "Log.h"
 #include "PackageFile.h"
@@ -57,8 +59,10 @@ static const SharedPtr<Resource> noResource;
 OBJECTTYPESTATIC(ResourceCache);
 
 ResourceCache::ResourceCache(Context* context) :
-    Object(context)
+    Object(context),
+    autoReloadResources_(false)
 {
+    SubscribeToEvent(E_BEGINFRAME, HANDLER(ResourceCache, HandleBeginFrame));
 }
 
 ResourceCache::~ResourceCache()
@@ -91,6 +95,14 @@ bool ResourceCache::AddResourceDir(const String& pathName)
     for (unsigned i = 0; i < fileNames.Size(); ++i)
         StoreNameHash(fileNames[i]);
     
+    // If resource auto-reloading active, create a file watcher for the directory
+    if (autoReloadResources_)
+    {
+        SharedPtr<FileWatcher> watcher(new FileWatcher(context_));
+        watcher->StartWatching(fixedPath, true);
+        fileWatchers_.Push(watcher);
+    }
+    
     LOGINFO("Added resource path " + fixedPath);
     return true;
 }
@@ -139,11 +151,13 @@ bool ResourceCache::AddManualResource(Resource* resource)
 void ResourceCache::RemoveResourceDir(const String& path)
 {
     String fixedPath = AddTrailingSlash(path);
-    for (Vector<String>::Iterator i = resourceDirs_.Begin(); i != resourceDirs_.End(); ++i)
+    for (unsigned i = 0; i < resourceDirs_.Size(); ++i)
     {
-        if (!i->Compare(path, false))
+        if (!resourceDirs_[i].Compare(path, false))
         {
             resourceDirs_.Erase(i);
+            if (fileWatchers_.Size() > i)
+                fileWatchers_.Erase(i);
             LOGINFO("Removed resource path " + fixedPath);
             return;
         }
@@ -313,6 +327,26 @@ void ResourceCache::SetMemoryBudget(ShortStringHash type, unsigned budget)
     resourceGroups_[type].memoryBudget_ = budget;
 }
 
+void ResourceCache::SetAutoReloadResources(bool enable)
+{
+    if (enable != autoReloadResources_)
+    {
+        if (enable)
+        {
+            for (unsigned i = 0; i < resourceDirs_.Size(); ++i)
+            {
+                SharedPtr<FileWatcher> watcher(new FileWatcher(context_));
+                watcher->StartWatching(resourceDirs_[i], true);
+                fileWatchers_.Push(watcher);
+            }
+        }
+        else
+            fileWatchers_.Clear();
+        
+        autoReloadResources_ = enable;
+    }
+}
+
 SharedPtr<File> ResourceCache::GetFile(const String& nameIn)
 {
     String name = SanitateResourceName(nameIn);
@@ -622,6 +656,30 @@ void ResourceCache::UpdateResourceGroup(ShortStringHash type)
     }
 }
 
+void ResourceCache::HandleBeginFrame(StringHash eventType, VariantMap& eventData)
+{
+    if (!autoReloadResources_)
+        return;
+    
+    for (unsigned i = 0; i < fileWatchers_.Size(); ++i)
+    {
+        String fileName;
+        while (fileWatchers_[i]->GetNextChange(fileName))
+        {
+            // If the filename is a resource we keep track of, reload it
+            for (Map<ShortStringHash, ResourceGroup>::Iterator j = resourceGroups_.Begin(); j != resourceGroups_.End(); ++j)
+            {
+                Map<StringHash, SharedPtr<Resource> >::Iterator k = j->second_.resources_.Find(StringHash(fileName));
+                if (k != j->second_.resources_.End())
+                {
+                    LOGDEBUG("Reloading changed resource " + fileName);
+                    ReloadResource(k->second_);
+                }
+            }
+        }
+    }
+}
+
 void RegisterResourceLibrary(Context* context)
 {
     Image::RegisterObject(context);

+ 12 - 0
Engine/Resource/ResourceCache.h

@@ -26,6 +26,7 @@
 #include "File.h"
 #include "Resource.h"
 
+class FileWatcher;
 class PackageFile;
 
 /// Container of resources with specific type.
@@ -83,6 +84,8 @@ public:
     bool ReloadResource(Resource* resource);
     /// %Set memory budget for a specific resource type, default 0 is unlimited.
     void SetMemoryBudget(ShortStringHash type, unsigned budget);
+    /// Enable or disable automatic reloading of resources as files are modified.
+    void SetAutoReloadResources(bool enable);
     
     /// Open and return a file from either the resource load paths or from inside a package file. Return null if fails.
     SharedPtr<File> GetFile(const String& name);
@@ -116,6 +119,9 @@ public:
     unsigned GetTotalMemoryUse() const;
     /// Return resource name from hash, or empty if not found.
     const String& GetResourceName(StringHash nameHash) const;
+    /// Return whether automatic resource reloading is enabled.
+    bool GetAutoReloadResources() const { return autoReloadResources_; }
+    
     /// Return either the path itself or its parent, based on which of them has recognized resource subdirectories.
     String GetPreferredResourceDir(const String& path);
     /// Remove unsupported constructs from the resource name to prevent ambiguity.
@@ -130,15 +136,21 @@ private:
     void ReleasePackageResources(PackageFile* package, bool force = false);
     /// Update a resource group. Recalculate memory use and release resources if over memory budget.
     void UpdateResourceGroup(ShortStringHash type);
+    /// Handle begin frame event. Automatic resource reloads are processed here.
+    void HandleBeginFrame(StringHash eventType, VariantMap& eventData);
     
     /// Resources by type.
     Map<ShortStringHash, ResourceGroup> resourceGroups_;
     /// Resource load directories.
     Vector<String> resourceDirs_;
+    /// File watchers for resource directories, if automatic reloading enabled.
+    Vector<SharedPtr<FileWatcher> > fileWatchers_;
     /// Package files.
     Vector<SharedPtr<PackageFile> > packages_;
     /// Mapping of hashes to filenames.
     Map<StringHash, String> hashToName_;
+    /// Automatic resource reloading flag.
+    bool autoReloadResources_;
 };
 
 template <class T> T* ResourceCache::GetResource(const String& name)