Bläddra i källkod

Editor: Support saving particle files

Panagiotis Christopoulos Charitos 1 månad sedan
förälder
incheckning
4a665813ac

+ 65 - 1
AnKi/Editor/ParticleEditorUi.cpp

@@ -5,6 +5,7 @@
 
 
 #include <AnKi/Editor/ParticleEditorUi.h>
 #include <AnKi/Editor/ParticleEditorUi.h>
 #include <AnKi/Resource/ResourceFilesystem.h>
 #include <AnKi/Resource/ResourceFilesystem.h>
+#include <AnKi/Resource/ResourceManager.h>
 #include <AnKi/Util/Filesystem.h>
 #include <AnKi/Util/Filesystem.h>
 #include <ThirdParty/ImGui/Extra/IconsMaterialDesignIcons.h> // See all icons in https://pictogrammers.com/library/mdi/
 #include <ThirdParty/ImGui/Extra/IconsMaterialDesignIcons.h> // See all icons in https://pictogrammers.com/library/mdi/
 
 
@@ -18,6 +19,7 @@ void ParticleEditorUi::open(const ParticleEmitterResource2& resource)
 	}
 	}
 
 
 	rebuildCache(resource);
 	rebuildCache(resource);
+	m_filename = ResourceFilesystem::getSingleton().getFileFullPath(resource.getFilename());
 	m_open = true;
 	m_open = true;
 }
 }
 
 
@@ -48,7 +50,11 @@ void ParticleEditorUi::drawWindow([[maybe_unused]] UiCanvas& canvas, Vec2 initia
 		{
 		{
 			if(ImGui::Button(ICON_MDI_CONTENT_SAVE " Save"))
 			if(ImGui::Button(ICON_MDI_CONTENT_SAVE " Save"))
 			{
 			{
-				ANKI_LOGI("TODO");
+				if(saveCache())
+				{
+					ANKI_LOGE("Unnable to save the particles file. Ignoring save");
+				}
+				ResourceManager::getSingleton().refreshFileUpdateTimes();
 			}
 			}
 			ImGui::SameLine();
 			ImGui::SameLine();
 			ImGui::SetNextItemWidth(-1.0f);
 			ImGui::SetNextItemWidth(-1.0f);
@@ -271,4 +277,62 @@ void ParticleEditorUi::rebuildCache(CString particleProgramName)
 	}
 	}
 }
 }
 
 
+Error ParticleEditorUi::saveCache()
+{
+	File file;
+	ANKI_CHECK(file.open(m_filename, FileOpenFlag::kWrite));
+
+	ANKI_CHECK(file.writeText("<particleEmitter>\n"));
+
+	ANKI_CHECK(file.writeTextf("\t<shaderProgram name=\"%s\"/>\n", m_currentlySelectedProgram.cstr()));
+
+	ANKI_CHECK(file.writeTextf("\t<particleCount value=\"%u\"/>\n", m_commonProps.m_particleCount));
+	ANKI_CHECK(file.writeTextf("\t<emissionPeriod value=\"%f\"/>\n", m_commonProps.m_emissionPeriod));
+	ANKI_CHECK(file.writeTextf("\t<particlesPerEmission value=\"%u\"/>\n", m_commonProps.m_particlesPerEmission));
+
+	ANKI_CHECK(file.writeText("\t<inputs>\n"));
+
+	for(const Prop& prop : m_otherProps)
+	{
+		String value;
+		switch(prop.m_type)
+		{
+		case ShaderVariableDataType::kU32:
+			value.toString(prop.m_U32);
+			break;
+		case ShaderVariableDataType::kUVec2:
+			value = prop.m_UVec2.toString();
+			break;
+		case ShaderVariableDataType::kUVec3:
+			value = prop.m_UVec3.toString();
+			break;
+		case ShaderVariableDataType::kUVec4:
+			value = prop.m_UVec4.toString();
+			break;
+		case ShaderVariableDataType::kF32:
+			value.toString(prop.m_F32);
+			break;
+		case ShaderVariableDataType::kVec2:
+			value = prop.m_Vec2.toString();
+			break;
+		case ShaderVariableDataType::kVec3:
+			value = prop.m_Vec3.toString();
+			break;
+		case ShaderVariableDataType::kVec4:
+			value = prop.m_Vec4.toString();
+			break;
+		default:
+			ANKI_ASSERT(!"TODO");
+		}
+
+		ANKI_CHECK(file.writeTextf("\t\t<input name=\"%s\" value=\"%s\"/>\n", prop.m_name.cstr(), value.cstr()));
+	}
+
+	ANKI_CHECK(file.writeText("\t</inputs>\n"));
+
+	ANKI_CHECK(file.writeText("</particleEmitter>\n"));
+
+	return Error::kNone;
+}
+
 } // end namespace anki
 } // end namespace anki

+ 4 - 2
AnKi/Editor/ParticleEditorUi.h

@@ -15,8 +15,6 @@ class ParticleEditorUi
 public:
 public:
 	void open(const ParticleEmitterResource2& resource);
 	void open(const ParticleEmitterResource2& resource);
 
 
-	void openNew();
-
 	void drawWindow(UiCanvas& canvas, Vec2 initialPos, Vec2 initialSize, ImGuiWindowFlags windowFlags = 0);
 	void drawWindow(UiCanvas& canvas, Vec2 initialPos, Vec2 initialSize, ImGuiWindowFlags windowFlags = 0);
 
 
 private:
 private:
@@ -63,6 +61,8 @@ private:
 
 
 	DynamicArray<ParticleProgram> m_programs;
 	DynamicArray<ParticleProgram> m_programs;
 
 
+	String m_filename;
+
 	// Cache begin. The UI will manipulate this cache because the resource is immutable
 	// Cache begin. The UI will manipulate this cache because the resource is immutable
 	String m_currentlySelectedProgram;
 	String m_currentlySelectedProgram;
 	ParticleEmitterResourceCommonProperties m_commonProps = {};
 	ParticleEmitterResourceCommonProperties m_commonProps = {};
@@ -75,6 +75,8 @@ private:
 	void rebuildCache(const ParticleEmitterResource2& resource);
 	void rebuildCache(const ParticleEmitterResource2& resource);
 
 
 	void rebuildCache(CString particleProgramName);
 	void rebuildCache(CString particleProgramName);
+
+	Error saveCache();
 };
 };
 
 
 } // end namespace anki
 } // end namespace anki

+ 76 - 13
AnKi/Resource/ResourceFilesystem.cpp

@@ -10,6 +10,7 @@
 #if ANKI_OS_ANDROID
 #if ANKI_OS_ANDROID
 #	include <android_native_app_glue.h>
 #	include <android_native_app_glue.h>
 #endif
 #endif
+#include <filesystem>
 
 
 namespace anki {
 namespace anki {
 
 
@@ -291,6 +292,7 @@ Error ResourceFilesystem::addNewPath(CString filepath, const ResourceStringList&
 	ANKI_RESOURCE_LOGV("Adding new resource path: %s", filepath.cstr());
 	ANKI_RESOURCE_LOGV("Adding new resource path: %s", filepath.cstr());
 
 
 	U32 fileCount = 0; // Count files manually because it's slower to get that number from the list
 	U32 fileCount = 0; // Count files manually because it's slower to get that number from the list
+	ResourceStringList filenameList;
 	constexpr CString extension(".ankizip");
 	constexpr CString extension(".ankizip");
 
 
 	auto includePath = [&](CString p) -> Bool {
 	auto includePath = [&](CString p) -> Bool {
@@ -344,7 +346,7 @@ Error ResourceFilesystem::addNewPath(CString filepath, const ResourceStringList&
 
 
 		do
 		do
 		{
 		{
-			Array<char, 1024> filename;
+			Array<Char, 1024> filename;
 
 
 			unz_file_info info;
 			unz_file_info info;
 			if(unzGetCurrentFileInfo(zfile, &info, &filename[0], filename.getSize(), nullptr, 0, nullptr, 0) != UNZ_OK)
 			if(unzGetCurrentFileInfo(zfile, &info, &filename[0], filename.getSize(), nullptr, 0, nullptr, 0) != UNZ_OK)
@@ -357,7 +359,7 @@ Error ResourceFilesystem::addNewPath(CString filepath, const ResourceStringList&
 			const Bool itsADir = info.uncompressed_size == 0;
 			const Bool itsADir = info.uncompressed_size == 0;
 			if(!itsADir && includePath(&filename[0]))
 			if(!itsADir && includePath(&filename[0]))
 			{
 			{
-				path.m_files.pushBackSprintf("%s", &filename[0]);
+				filenameList.pushBack(filename.getBegin());
 				++fileCount;
 				++fileCount;
 			}
 			}
 		} while(unzGoToNextFile(zfile) == UNZ_OK);
 		} while(unzGoToNextFile(zfile) == UNZ_OK);
@@ -382,7 +384,7 @@ Error ResourceFilesystem::addNewPath(CString filepath, const ResourceStringList&
 		{
 		{
 			if(includePath(line))
 			if(includePath(line))
 			{
 			{
-				path.m_files.pushBack(line);
+				filenameList.pushBack(line);
 				++fileCount;
 				++fileCount;
 			}
 			}
 		}
 		}
@@ -397,7 +399,7 @@ Error ResourceFilesystem::addNewPath(CString filepath, const ResourceStringList&
 		ANKI_CHECK(walkDirectoryTree(filepath, [&](WalkDirectoryArgs& args) -> Error {
 		ANKI_CHECK(walkDirectoryTree(filepath, [&](WalkDirectoryArgs& args) -> Error {
 			if(!args.m_isDirectory && includePath(args.m_path))
 			if(!args.m_isDirectory && includePath(args.m_path))
 			{
 			{
-				path.m_files.pushBackSprintf("%s", args.m_path.cstr());
+				filenameList.pushBack(args.m_path);
 				++fileCount;
 				++fileCount;
 			}
 			}
 
 
@@ -405,14 +407,24 @@ Error ResourceFilesystem::addNewPath(CString filepath, const ResourceStringList&
 		}));
 		}));
 	}
 	}
 
 
-	ANKI_ASSERT(path.m_files.getSize() == fileCount);
+	ANKI_ASSERT(filenameList.getSize() == fileCount);
 	if(fileCount == 0)
 	if(fileCount == 0)
 	{
 	{
 		ANKI_RESOURCE_LOGW("Ignoring empty resource path: %s", &filepath[0]);
 		ANKI_RESOURCE_LOGW("Ignoring empty resource path: %s", &filepath[0]);
 	}
 	}
 	else
 	else
 	{
 	{
-		path.m_path.sprintf("%s", &filepath[0]);
+		path.m_path = filepath;
+
+		path.m_files.resize(fileCount);
+		U32 count = 0;
+		for(const ResourceString& str : filenameList)
+		{
+			path.m_files[count].m_filename = str;
+			path.m_files[count].m_filenameHash = str.computeHash();
+			++count;
+		}
+
 		m_paths.emplaceFront(std::move(path));
 		m_paths.emplaceFront(std::move(path));
 
 
 		ANKI_RESOURCE_LOGI("Added new data path \"%s\" that contains %u files", &filepath[0], fileCount);
 		ANKI_RESOURCE_LOGI("Added new data path \"%s\" that contains %u files", &filepath[0], fileCount);
@@ -420,16 +432,16 @@ Error ResourceFilesystem::addNewPath(CString filepath, const ResourceStringList&
 
 
 	if(false)
 	if(false)
 	{
 	{
-		for(const ResourceString& s : m_paths.getFront().m_files)
+		for(const File& s : m_paths.getFront().m_files)
 		{
 		{
-			printf("%s\n", s.cstr());
+			printf("%s\n", s.m_filename.cstr());
 		}
 		}
 	}
 	}
 
 
 	return Error::kNone;
 	return Error::kNone;
 }
 }
 
 
-Error ResourceFilesystem::openFile(const ResourceFilename& filename, ResourceFilePtr& filePtr) const
+Error ResourceFilesystem::openFile(ResourceFilename filename, ResourceFilePtr& filePtr) const
 {
 {
 	ResourceFile* rfile;
 	ResourceFile* rfile;
 	Error err = openFileInternal(filename, rfile);
 	Error err = openFileInternal(filename, rfile);
@@ -453,16 +465,21 @@ Error ResourceFilesystem::openFileInternal(const ResourceFilename& filename, Res
 	ANKI_RESOURCE_LOGV("Opening resource file: %s", filename.cstr());
 	ANKI_RESOURCE_LOGV("Opening resource file: %s", filename.cstr());
 	rfile = nullptr;
 	rfile = nullptr;
 
 
+	const U64 filenameHash = filename.computeHash();
+
 	// Search for the fname in reverse order
 	// Search for the fname in reverse order
 	for(const Path& p : m_paths)
 	for(const Path& p : m_paths)
 	{
 	{
-		for(const ResourceString& pfname : p.m_files)
+		for(const File& fsfile : p.m_files)
 		{
 		{
-			if(pfname != filename)
+			if(filenameHash != fsfile.m_filenameHash)
 			{
 			{
 				continue;
 				continue;
 			}
 			}
 
 
+			CString pfname = fsfile.m_filename;
+			ANKI_ASSERT(pfname == filename);
+
 			// Found
 			// Found
 			if(p.m_isArchive)
 			if(p.m_isArchive)
 			{
 			{
@@ -476,7 +493,7 @@ Error ResourceFilesystem::openFileInternal(const ResourceFilename& filename, Res
 				ResourceString newFname;
 				ResourceString newFname;
 				if(!p.m_isSpecial)
 				if(!p.m_isSpecial)
 				{
 				{
-					newFname.sprintf("%s/%s", &p.m_path[0], &filename[0]);
+					newFname.sprintf("%s/%s", p.m_path.cstr(), filename.cstr());
 				}
 				}
 				else
 				else
 				{
 				{
@@ -495,7 +512,7 @@ Error ResourceFilesystem::openFileInternal(const ResourceFilename& filename, Res
 				ANKI_CHECK(file->m_file.open(newFname, openFlags));
 				ANKI_CHECK(file->m_file.open(newFname, openFlags));
 
 
 #if 0
 #if 0
-				printf("Opening asset %s\n", &newFname[0]);
+				printf("Opening asset %s, update time %llu\n", newFname.cstr(), fsfile.m_fileUpdateTime);
 #endif
 #endif
 			}
 			}
 		}
 		}
@@ -526,4 +543,50 @@ Error ResourceFilesystem::openFileInternal(const ResourceFilename& filename, Res
 	return Error::kNone;
 	return Error::kNone;
 }
 }
 
 
+ResourceString ResourceFilesystem::getFileFullPath(ResourceFilename filename) const
+{
+	ResourceString out;
+	const U64 filenameHash = filename.computeHash();
+	Bool found = false;
+	for(const Path& p : m_paths)
+	{
+		for(const File& fsfile : p.m_files)
+		{
+			if(filenameHash != fsfile.m_filenameHash)
+			{
+				continue;
+			}
+
+			CString pfname = fsfile.m_filename;
+			ANKI_ASSERT(pfname == filename);
+
+			if(!p.m_isArchive && !p.m_isSpecial)
+			{
+				out.sprintf("%s/%s", p.m_path.cstr(), fsfile.m_filename.cstr());
+				found = true;
+				break;
+			}
+		}
+
+		if(found)
+		{
+			break;
+		}
+	}
+
+	ANKI_ASSERT(found);
+	return out;
+}
+
+U64 ResourceFilesystem::getFileUpdateTime(ResourceFilename filename) const
+{
+	const ResourceString fullFilename = getFileFullPath(filename);
+
+	const std::filesystem::path stdpath = fullFilename.cstr();
+	ANKI_ASSERT(std::filesystem::exists(stdpath));
+
+	const auto timeOfUpdate = std::filesystem::last_write_time(stdpath);
+	return std::chrono::time_point_cast<std::chrono::milliseconds>(timeOfUpdate).time_since_epoch().count();
+}
+
 } // end namespace anki
 } // end namespace anki

+ 34 - 26
AnKi/Resource/ResourceFilesystem.h

@@ -14,15 +14,12 @@
 
 
 namespace anki {
 namespace anki {
 
 
-/// @addtogroup resource
-/// @{
-
 ANKI_CVAR(StringCVar, Rsrc, DataPaths, ".",
 ANKI_CVAR(StringCVar, Rsrc, DataPaths, ".",
 		  "The engine loads assets only in from these paths. Separate them with : (it's smart enough to identify drive letters in Windows). After a "
 		  "The engine loads assets only in from these paths. Separate them with : (it's smart enough to identify drive letters in Windows). After a "
 		  "path you can add an optional | and what follows it is a number of words to include or exclude paths. eg. "
 		  "path you can add an optional | and what follows it is a number of words to include or exclude paths. eg. "
 		  "my_path|include_this,include_that,!exclude_this")
 		  "my_path|include_this,include_that,!exclude_this")
 
 
-/// Resource filesystem file. An interface that abstracts the resource file.
+// Resource filesystem file. An interface that abstracts the resource file.
 class ResourceFile
 class ResourceFile
 {
 {
 public:
 public:
@@ -36,24 +33,24 @@ public:
 
 
 	ResourceFile& operator=(const ResourceFile&) = delete; // Non-copyable
 	ResourceFile& operator=(const ResourceFile&) = delete; // Non-copyable
 
 
-	/// Read data from the file
+	// Read data from the file
 	virtual Error read(void* buff, PtrSize size) = 0;
 	virtual Error read(void* buff, PtrSize size) = 0;
 
 
-	/// Read all the contents of a text file. If the file is not rewined it will probably fail
+	// Read all the contents of a text file. If the file is not rewined it will probably fail
 	virtual Error readAllText(ResourceString& out) = 0;
 	virtual Error readAllText(ResourceString& out) = 0;
 
 
-	/// Read 32bit unsigned integer. Set the endianness if the file's endianness is different from the machine's
+	// Read 32bit unsigned integer. Set the endianness if the file's endianness is different from the machine's
 	virtual Error readU32(U32& u) = 0;
 	virtual Error readU32(U32& u) = 0;
 
 
-	/// Read 32bit float. Set the endianness if the file's endianness is different from the machine's
+	// Read 32bit float. Set the endianness if the file's endianness is different from the machine's
 	virtual Error readF32(F32& f) = 0;
 	virtual Error readF32(F32& f) = 0;
 
 
-	/// Set the position indicator to a new position
-	/// @param offset Number of bytes to offset from origin
-	/// @param origin Position used as reference for the offset
+	// Set the position indicator to a new position
+	// offset: Number of bytes to offset from origin
+	// origin: Position used as reference for the offset
 	virtual Error seek(PtrSize offset, FileSeekOrigin origin) = 0;
 	virtual Error seek(PtrSize offset, FileSeekOrigin origin) = 0;
 
 
-	/// Get the size of the file.
+	// Get the size of the file.
 	virtual PtrSize getSize() const = 0;
 	virtual PtrSize getSize() const = 0;
 
 
 	void retain() const
 	void retain() const
@@ -70,7 +67,7 @@ private:
 	mutable Atomic<I32> m_refcount = {0};
 	mutable Atomic<I32> m_refcount = {0};
 };
 };
 
 
-/// Resource file smart pointer.
+// Resource file smart pointer.
 class ResourceFileDeleter
 class ResourceFileDeleter
 {
 {
 public:
 public:
@@ -82,7 +79,7 @@ public:
 
 
 using ResourceFilePtr = IntrusivePtr<ResourceFile, ResourceFileDeleter>;
 using ResourceFilePtr = IntrusivePtr<ResourceFile, ResourceFileDeleter>;
 
 
-/// Resource filesystem.
+// Resource filesystem.
 class ResourceFilesystem : public MakeSingleton<ResourceFilesystem>
 class ResourceFilesystem : public MakeSingleton<ResourceFilesystem>
 {
 {
 public:
 public:
@@ -96,20 +93,25 @@ public:
 
 
 	Error init();
 	Error init();
 
 
-	/// Search the path list to find the file. Then open the file for reading.
-	/// @note Thread-safe.
-	Error openFile(const ResourceFilename& filename, ResourceFilePtr& file) const;
+	// Search the path list to find the file. Then open the file for reading.
+	// Thread-safe.
+	Error openFile(ResourceFilename filename, ResourceFilePtr& file) const;
+
+	// Return some sort of time a file was last updated. This time is opaque and it's increasing with every update. Only works for filesystem files
+	U64 getFileUpdateTime(ResourceFilename filename) const;
 
 
-	/// Iterate all the filenames from all paths provided.
+	// Take the filename (which is relative) and return the full path of the file. Only works for filesystem files
+	ResourceString getFileFullPath(ResourceFilename filename) const;
+
+	// Iterate all the filenames from all paths provided.
 	template<typename TFunc>
 	template<typename TFunc>
 	void iterateAllFilenames(TFunc func) const
 	void iterateAllFilenames(TFunc func) const
 	{
 	{
 		for(const Path& path : m_paths)
 		for(const Path& path : m_paths)
 		{
 		{
-			for(const ResourceString& fname : path.m_files)
+			for(const File& file : path.m_files)
 			{
 			{
-				const FunctorContinue cont_ = func(fname.toCString());
-				if(cont_ == FunctorContinue::kStop)
+				if(func(file.m_filename.toCString()) == FunctorContinue::kStop)
 				{
 				{
 					break;
 					break;
 				}
 				}
@@ -117,7 +119,7 @@ public:
 		}
 		}
 	}
 	}
 
 
-	/// Iterate paths in the DataPaths CVar
+	// Iterate paths in the DataPaths CVar
 	template<typename TFunc>
 	template<typename TFunc>
 	void iterateAllResourceBasePaths(TFunc func) const
 	void iterateAllResourceBasePaths(TFunc func) const
 	{
 	{
@@ -134,11 +136,18 @@ public:
 #if !ANKI_TESTS
 #if !ANKI_TESTS
 private:
 private:
 #endif
 #endif
+	class File
+	{
+	public:
+		ResourceString m_filename;
+		U64 m_filenameHash = 0;
+	};
+
 	class Path
 	class Path
 	{
 	{
 	public:
 	public:
-		ResourceStringList m_files; ///< Files inside the directory.
-		ResourceString m_path; ///< A directory or an archive.
+		ResourceDynamicArray<File> m_files; // Files inside the directory.
+		ResourceString m_path; // A directory or an archive.
 		Bool m_isArchive = false;
 		Bool m_isArchive = false;
 		Bool m_isSpecial = false;
 		Bool m_isSpecial = false;
 
 
@@ -166,11 +175,10 @@ private:
 	ResourceList<Path> m_paths;
 	ResourceList<Path> m_paths;
 	ResourceString m_cacheDir;
 	ResourceString m_cacheDir;
 
 
-	/// Add a filesystem path or an archive. The path is read-only.
+	// Add a filesystem path or an archive. The path is read-only.
 	Error addNewPath(CString path, const ResourceStringList& includeStrings, const ResourceStringList& excludedStrings);
 	Error addNewPath(CString path, const ResourceStringList& includeStrings, const ResourceStringList& excludedStrings);
 
 
 	Error openFileInternal(const ResourceFilename& filename, ResourceFile*& rfile) const;
 	Error openFileInternal(const ResourceFilename& filename, ResourceFile*& rfile) const;
 };
 };
-/// @}
 
 
 } // end namespace anki
 } // end namespace anki

+ 45 - 2
AnKi/Resource/ResourceManager.cpp

@@ -73,11 +73,13 @@ Error ResourceManager::init(AllocAlignedCallback allocCallback, void* allocCallb
 		AccelerationStructureScratchAllocator::allocateSingleton();
 		AccelerationStructureScratchAllocator::allocateSingleton();
 	}
 	}
 
 
+	m_trackFileUpdateTimes = g_cvarRsrcTrackFileUpdates;
+
 	return Error::kNone;
 	return Error::kNone;
 }
 }
 
 
 template<typename T>
 template<typename T>
-Error ResourceManager::loadResource(const CString& filename, ResourcePtr<T>& out, Bool async)
+Error ResourceManager::loadResource(CString filename, ResourcePtr<T>& out, Bool async)
 {
 {
 	ANKI_ASSERT(!out.isCreated() && "Already loaded");
 	ANKI_ASSERT(!out.isCreated() && "Already loaded");
 
 
@@ -141,6 +143,11 @@ Error ResourceManager::loadResource(const CString& filename, ResourcePtr<T>& out
 			{
 			{
 				entry->m_resource = rsrc;
 				entry->m_resource = rsrc;
 			}
 			}
+
+			if(m_trackFileUpdateTimes)
+			{
+				entry->m_fileUpdateTime = ResourceFilesystem::getSingleton().getFileUpdateTime(filename);
+			}
 		}
 		}
 	}
 	}
 
 
@@ -178,8 +185,44 @@ void ResourceManager::freeResource(T* ptr)
 
 
 // Instansiate
 // Instansiate
 #define ANKI_INSTANTIATE_RESOURCE(className) \
 #define ANKI_INSTANTIATE_RESOURCE(className) \
-	template Error ResourceManager::loadResource<className>(const CString& filename, ResourcePtr<className>& out, Bool async); \
+	template Error ResourceManager::loadResource<className>(CString filename, ResourcePtr<className> & out, Bool async); \
 	template void ResourceManager::freeResource<className>(className * ptr);
 	template void ResourceManager::freeResource<className>(className * ptr);
 #include <AnKi/Resource/Resources.def.h>
 #include <AnKi/Resource/Resources.def.h>
 
 
+template<typename T>
+void ResourceManager::refreshFileUpdateTimesInternal()
+{
+	TypeData<T>& type = static_cast<TypeData<T>&>(m_allTypes);
+
+	WLockGuard lock(type.m_mtx);
+
+	for(auto& entry : type.m_entries)
+	{
+		LockGuard lock(entry.m_mtx);
+
+		if(!entry.m_resource)
+		{
+			continue;
+		}
+
+		const U64 newTime = ResourceFilesystem::getSingleton().getFileUpdateTime(entry.m_resource->getFilename());
+		if(newTime != entry.m_fileUpdateTime)
+		{
+			ANKI_RESOURCE_LOGV("File updated: %s", entry.m_resource->getFilename().cstr());
+			entry.m_fileUpdateTime = newTime;
+		}
+	}
+}
+
+void ResourceManager::refreshFileUpdateTimes()
+{
+	if(!m_trackFileUpdateTimes)
+	{
+		return;
+	}
+
+#define ANKI_INSTANTIATE_RESOURCE(className) refreshFileUpdateTimesInternal<className>();
+#include <AnKi/Resource/Resources.def.h>
+}
+
 } // end namespace anki
 } // end namespace anki

+ 16 - 9
AnKi/Resource/ResourceManager.h

@@ -25,12 +25,10 @@ class ShaderCompilerCache;
 class ShaderProgramResourceSystem;
 class ShaderProgramResourceSystem;
 class AccelerationStructureScratchAllocator;
 class AccelerationStructureScratchAllocator;
 
 
-/// @addtogroup resource
-/// @{
-
 ANKI_CVAR(NumericCVar<PtrSize>, Rsrc, TransferScratchMemorySize, 256_MB, 1_MB, 4_GB, "Memory that is used fot texture and buffer uploads")
 ANKI_CVAR(NumericCVar<PtrSize>, Rsrc, TransferScratchMemorySize, 256_MB, 1_MB, 4_GB, "Memory that is used fot texture and buffer uploads")
+ANKI_CVAR(BoolCVar, Rsrc, TrackFileUpdates, false, "If true the resource manager is able to track file update times")
 
 
-/// Resource manager. It holds a few global variables
+// Resource manager. It holds a few global variables
 class ResourceManager : public MakeSingleton<ResourceManager>
 class ResourceManager : public MakeSingleton<ResourceManager>
 {
 {
 	template<typename T>
 	template<typename T>
@@ -43,14 +41,18 @@ class ResourceManager : public MakeSingleton<ResourceManager>
 public:
 public:
 	Error init(AllocAlignedCallback allocCallback, void* allocCallbackData);
 	Error init(AllocAlignedCallback allocCallback, void* allocCallbackData);
 
 
-	/// Load a resource.
-	/// @node Thread-safe against itself and freeResource.
+	// Load a resource.
+	// Note: Thread-safe against itself, freeResource() and refreshFileUpdateTimes()
 	template<typename T>
 	template<typename T>
-	Error loadResource(const CString& filename, ResourcePtr<T>& out, Bool async = true);
+	Error loadResource(CString filename, ResourcePtr<T>& out, Bool async = true);
+
+	// Iterate all loaded resource and check if the files have been updated since they were loaded.
+	// Note: Thread-safe against itself, loadResource() and freeResource()
+	void refreshFileUpdateTimes();
 
 
 	// Internals:
 	// Internals:
 
 
-	/// @node Thread-safe against itself and loadResource.
+	// Note: Thread-safe against itself, loadResource() and refreshFileUpdateTimes()
 	template<typename T>
 	template<typename T>
 	ANKI_INTERNAL void freeResource(T* ptr);
 	ANKI_INTERNAL void freeResource(T* ptr);
 
 
@@ -63,6 +65,7 @@ private:
 		{
 		{
 		public:
 		public:
 			Type* m_resource = nullptr;
 			Type* m_resource = nullptr;
+			U64 m_fileUpdateTime = 0;
 			SpinLock m_mtx;
 			SpinLock m_mtx;
 		};
 		};
 
 
@@ -92,10 +95,14 @@ public \
 
 
 	Atomic<U32> m_uuid = {1};
 	Atomic<U32> m_uuid = {1};
 
 
+	Bool m_trackFileUpdateTimes = false;
+
 	ResourceManager();
 	ResourceManager();
 
 
 	~ResourceManager();
 	~ResourceManager();
+
+	template<typename T>
+	void refreshFileUpdateTimesInternal();
 };
 };
-/// @}
 
 
 } // end namespace anki
 } // end namespace anki

+ 3 - 7
AnKi/Resource/ResourceObject.h

@@ -12,10 +12,7 @@
 
 
 namespace anki {
 namespace anki {
 
 
-/// @addtogroup resource
-/// @{
-
-/// The base of all resource objects.
+// The base of all resource objects.
 class ResourceObject
 class ResourceObject
 {
 {
 	friend class ResourceManager;
 	friend class ResourceManager;
@@ -49,7 +46,7 @@ public:
 		return m_fname.toCString();
 		return m_fname.toCString();
 	}
 	}
 
 
-	/// To check if 2 resource pointers are actually the same resource.
+	// To check if 2 resource pointers are actually the same resource.
 	U32 getUuid() const
 	U32 getUuid() const
 	{
 	{
 		ANKI_ASSERT(m_uuid > 0);
 		ANKI_ASSERT(m_uuid > 0);
@@ -71,8 +68,7 @@ protected:
 private:
 private:
 	mutable Atomic<I32> m_refcount = {0};
 	mutable Atomic<I32> m_refcount = {0};
 	U32 m_uuid = 0;
 	U32 m_uuid = 0;
-	ResourceString m_fname; ///< Unique resource name.
+	ResourceString m_fname; // Unique resource name
 };
 };
-/// @}
 
 
 } // end namespace anki
 } // end namespace anki

+ 1 - 1
AnKi/Util/String.h

@@ -691,7 +691,7 @@ public:
 	U64 computeHash() const
 	U64 computeHash() const
 	{
 	{
 		ANKI_ASSERT(!isEmpty());
 		ANKI_ASSERT(!isEmpty());
-		return anki::computeHash(&m_data[0], m_data.getSize());
+		return toCString().computeHash();
 	}
 	}
 
 
 	/// Replace all occurrences of "from" with "to".
 	/// Replace all occurrences of "from" with "to".

+ 1 - 0
Tools/Editor/EditorMain.cpp

@@ -48,6 +48,7 @@ public:
 		g_cvarWindowFullscreen = false;
 		g_cvarWindowFullscreen = false;
 		g_cvarWindowMaximized = true;
 		g_cvarWindowMaximized = true;
 		g_cvarWindowBorderless = true;
 		g_cvarWindowBorderless = true;
+		g_cvarRsrcTrackFileUpdates = true;
 		ANKI_CHECK(CVarSet::getSingleton().setFromCommandLineArguments(m_argc - 1, m_argv + 1));
 		ANKI_CHECK(CVarSet::getSingleton().setFromCommandLineArguments(m_argc - 1, m_argv + 1));
 
 
 		if(CString(g_cvarEditorScene) != "")
 		if(CString(g_cvarEditorScene) != "")