diff --git a/source/lib/file/common/real_directory.cpp b/source/lib/file/common/real_directory.cpp index 5a9f29add6..ebe3bb0c3f 100644 --- a/source/lib/file/common/real_directory.cpp +++ b/source/lib/file/common/real_directory.cpp @@ -76,7 +76,8 @@ LibError RealDirectory::Store(const std::wstring& name, const shared_ptr& fi void RealDirectory::Watch() { - (void)dir_watch_Add(m_path, m_watch); + if(!m_watch) + (void)dir_watch_Add(m_path, m_watch); } diff --git a/source/lib/file/io/io.h b/source/lib/file/io/io.h index bd7bf32d27..7bec793286 100644 --- a/source/lib/file/io/io.h +++ b/source/lib/file/io/io.h @@ -36,7 +36,8 @@ LIB_API shared_ptr io_Allocate(size_t size, off_t ofs = 0); * @return INFO::CB_CONTINUE to continue; any other value will cause the * IO splitter to abort immediately and return that. * - * this is useful for interleaving e.g. decompression with IOs. + * this is useful for user progress notification or processing data while + * waiting for the next I/O to complete (without the complexity of threads). **/ typedef LibError (*IoCallback)(uintptr_t cbData, const u8* block, size_t blockSize); diff --git a/source/lib/file/vfs/vfs.cpp b/source/lib/file/vfs/vfs.cpp index e40d02db25..69dc0c1a20 100644 --- a/source/lib/file/vfs/vfs.cpp +++ b/source/lib/file/vfs/vfs.cpp @@ -18,12 +18,15 @@ #include "precompiled.h" #include "vfs.h" +#include "lib/posix/posix_time.h" // usleep #include "lib/allocators/shared_ptr.h" #include "lib/path_util.h" #include "lib/file/common/file_stats.h" #include "lib/file/common/trace.h" #include "lib/file/archive/archive.h" #include "lib/file/io/io.h" +#include "lib/res/h_mgr.h" // h_reload +#include "lib/sysdep/dir_watch.h" #include "vfs_tree.h" #include "vfs_lookup.h" #include "vfs_populate.h" @@ -92,8 +95,6 @@ public: return INFO::OK; } - // note: only allowing either reads or writes simplifies file cache coherency - // (we need only invalidate when closing a newly written file). virtual LibError CreateFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) { VfsDirectory* directory; @@ -114,15 +115,6 @@ public: return INFO::OK; } - // read the entire file. - // return number of bytes transferred (see above), or a negative error code. - // - // if non-NULL, is called for each block transferred, passing . - // it returns how much data was actually transferred, or a negative error - // code (in which case we abort the transfer and return that value). - // the callback mechanism is useful for user progress notification or - // processing data while waiting for the next I/O to complete - // (quasi-parallel, without the complexity of threads). virtual LibError LoadFile(const VfsPath& pathname, shared_ptr& fileContents, size_t& size) { const bool isCacheHit = m_fileCache.Retrieve(pathname, fileContents, size); @@ -155,21 +147,15 @@ public: return INFO::OK; } - // rebuild the VFS, i.e. re-mount everything. open files are not affected. - // necessary after loose files or directories change, so that the VFS - // "notices" the changes and updates file locations. res calls this after - // dir_watch reports changes; can also be called from the console after a - // rebuild command. there is no provision for updating single VFS dirs - - // it's not worth the trouble. virtual void Clear() { - ClearR(m_rootDirectory); + m_rootDirectory.Clear(); } virtual std::wstring TextRepresentation() const { std::wstring textRepresentation; - textRepresentation.reserve(100000); + textRepresentation.reserve(100*KiB); DirectoryDescriptionR(textRepresentation, m_rootDirectory, 0); return textRepresentation; } @@ -183,34 +169,81 @@ public: return INFO::OK; } -private: - void ClearR(VfsDirectory& directory) + virtual LibError ReloadChangedFiles() { + std::vector changedPathnames; + for(;;) + { + DirWatchNotification notification; + LibError ret = dir_watch_Poll(notification); + if(ret == ERR::AGAIN) // none available; done. + break; + RETURN_ERR(ret); + if(!CanIgnore(notification)) + changedPathnames.push_back(notification.Pathname()); + } + if(changedPathnames.empty()) + return INFO::OK; + usleep(500*1000); + for(size_t i = 0; i < changedPathnames.size(); i++) + { + LibError ret = NotifyChangedR(m_rootDirectory, L"", changedPathnames[i]); + RETURN_ERR(ret); + if(ret == INFO::SKIPPED) + { + debug_printf(L"NotifyChangedR: no match found, ignored\n"); + return ERR::VFS_FILE_NOT_FOUND; // NOWARN (happens if a new file was created) + } + } + + return INFO::OK; + } + +private: + // try to skip unnecessary work by ignoring uninteresting notifications. + static bool CanIgnore(const DirWatchNotification& notification) + { + // ignore directories + const fs::wpath& pathname = notification.Pathname(); + if(pathname.leaf() == L".") + return true; + + // ignore uninteresting file types (e.g. temp files, or the + // hundreds of XMB files that are generated from XML) + const std::wstring extension = fs::extension(pathname); + const wchar_t* extensionsToIgnore[] = { L".xmb", L".tmp" }; + for(size_t i = 0; i < ARRAY_SIZE(extensionsToIgnore); i++) + { + if(!wcscasecmp(extension.c_str(), extensionsToIgnore[i])) + return true; + } + + return false; + } + + LibError NotifyChangedR(VfsDirectory& directory, const VfsPath& path, const fs::wpath& realPathname) + { + LibError ret = directory.NotifyChanged(realPathname); + if(ret == INFO::OK) + { + const VfsPath pathname(path/realPathname.leaf()); + m_fileCache.Remove(pathname); // invalidate cached data + debug_printf(L"NotifyChangedR: reloading %ls\n", pathname.string().c_str()); + RETURN_ERR(h_reload(pathname)); + return INFO::OK; + } + VfsDirectory::VfsSubdirectories& subdirectories = directory.Subdirectories(); for(VfsDirectory::VfsSubdirectories::iterator it = subdirectories.begin(); it != subdirectories.end(); ++it) { + const std::wstring& subdirectoryName = it->first; VfsDirectory& subdirectory = it->second; - ClearR(subdirectory); + ret = NotifyChangedR(subdirectory, path/subdirectoryName, realPathname); + if(ret != INFO::SKIPPED) + return ret; } - directory.Clear(); - } - - void DirectoryDescriptionR(std::wstring& descriptions, const VfsDirectory& directory, size_t indentLevel) const - { - const std::wstring indentation(4*indentLevel, ' '); - - const VfsDirectory::VfsSubdirectories& subdirectories = directory.Subdirectories(); - for(VfsDirectory::VfsSubdirectories::const_iterator it = subdirectories.begin(); it != subdirectories.end(); ++it) - { - const std::wstring& name = it->first; - const VfsDirectory& subdirectory = it->second; - descriptions += indentation; - descriptions += std::wstring(L"[") + name + L"]\n"; - descriptions += FileDescriptions(subdirectory, indentLevel+1); - - DirectoryDescriptionR(descriptions, subdirectory, indentLevel+1); - } + return INFO::SKIPPED; } size_t m_cacheSize; diff --git a/source/lib/file/vfs/vfs.h b/source/lib/file/vfs/vfs.h index 2ce8c142a3..ad4de43275 100644 --- a/source/lib/file/vfs/vfs.h +++ b/source/lib/file/vfs/vfs.h @@ -17,7 +17,7 @@ /* * Virtual File System API - allows transparent access to files in - * archives and modding via multiple mount points. + * archives, modding via multiple mount points and hotloading. */ #ifndef INCLUDED_VFS @@ -95,7 +95,7 @@ struct IVFS * @return LibError. * * rationale: disallowing partial writes simplifies file cache coherency - * (need only be invalidated when closing a FILE_WRITE file). + * (we need only invalidate cached data when closing a newly written file). **/ virtual LibError CreateFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) = 0; @@ -113,14 +113,15 @@ struct IVFS virtual LibError LoadFile(const VfsPath& pathname, shared_ptr& fileContents, size_t& size) = 0; /** - * dump a text representation of the filesystem to debug output. + * @return a text representation of all files and directories. **/ virtual std::wstring TextRepresentation() const = 0; /** * empty the contents of the filesystem. - * - * the effect is as if nothing had been mounted. + * this is typically only necessary when changing the set of + * mounted directories, e.g. when switching mods. + * NB: open files are not affected. **/ virtual void Clear() = 0; @@ -130,6 +131,14 @@ struct IVFS * this is useful when passing paths to external libraries. **/ virtual LibError GetRealPath(const VfsPath& pathname, fs::wpath& path) = 0; + + /** + * poll for directory change notifications and reload all affected files. + * must be called regularly (e.g. once a frame), else notifications + * may be lost. + * note: polling is much simpler than asynchronous notifications. + **/ + virtual LibError ReloadChangedFiles() = 0; }; typedef shared_ptr PIVFS; diff --git a/source/lib/file/vfs/vfs_tree.cpp b/source/lib/file/vfs/vfs_tree.cpp index e8f5bd507a..078032d573 100644 --- a/source/lib/file/vfs/vfs_tree.cpp +++ b/source/lib/file/vfs/vfs_tree.cpp @@ -131,6 +131,21 @@ void VfsDirectory::Clear() } +LibError VfsDirectory::NotifyChanged(const fs::wpath& realPathname) +{ + // we're not associated with the directory that changed + if(!m_realDirectory || m_realDirectory->Path() != realPathname.branch_path()) + return INFO::SKIPPED; + + // one of our files changed; ensure its new version supersedes the + // old by removing it and marking the directory for re-population. + const std::wstring name = realPathname.leaf(); + m_files.erase(name); + m_shouldPopulate = 1; + return INFO::OK; +} + + void VfsDirectory::SetAssociatedDirectory(const PRealDirectory& realDirectory) { if(!cpu_CAS(&m_shouldPopulate, 0, 1)) @@ -145,11 +160,6 @@ bool VfsDirectory::ShouldPopulate() } -void VfsDirectory::Invalidate() -{ - m_shouldPopulate = 1; -} - //----------------------------------------------------------------------------- @@ -183,3 +193,21 @@ std::wstring FileDescriptions(const VfsDirectory& directory, size_t indentLevel) return descriptions; } + + +void DirectoryDescriptionR(std::wstring& descriptions, const VfsDirectory& directory, size_t indentLevel) +{ + const std::wstring indentation(4*indentLevel, ' '); + + const VfsDirectory::VfsSubdirectories& subdirectories = directory.Subdirectories(); + for(VfsDirectory::VfsSubdirectories::const_iterator it = subdirectories.begin(); it != subdirectories.end(); ++it) + { + const std::wstring& name = it->first; + const VfsDirectory& subdirectory = it->second; + descriptions += indentation; + descriptions += std::wstring(L"[") + name + L"]\n"; + descriptions += FileDescriptions(subdirectory, indentLevel+1); + + DirectoryDescriptionR(descriptions, subdirectory, indentLevel+1); + } +} diff --git a/source/lib/file/vfs/vfs_tree.h b/source/lib/file/vfs/vfs_tree.h index 356ff284d3..70f5a5da11 100644 --- a/source/lib/file/vfs/vfs_tree.h +++ b/source/lib/file/vfs/vfs_tree.h @@ -121,6 +121,14 @@ public: **/ void Clear(); + /** + * check if this directory is affected by changes to . + * @return INFO::OK if this directory was affected, + * INFO::CB_CONTINUE if this is a parent directory, or + * INFO::SKIPPED if this directory is unaffected. + **/ + LibError NotifyChanged(const fs::wpath& path); + /** * side effect: the next ShouldPopulate() will return true. **/ @@ -138,13 +146,6 @@ public: **/ bool ShouldPopulate(); - /** - * cause the directory to be (re)populated when next accessed - * by having ShouldPopulate return true. - * this is typically called in response to file change notifications. - **/ - void Invalidate(); - private: VfsFiles m_files; VfsSubdirectories m_subdirectories; @@ -164,4 +165,9 @@ extern std::wstring FileDescription(const VfsFile& file); **/ extern std::wstring FileDescriptions(const VfsDirectory& directory, size_t indentLevel); +/** + * append each directory's files' description to the given string. + **/ +void DirectoryDescriptionR(std::wstring& descriptions, const VfsDirectory& directory, size_t indentLevel); + #endif // #ifndef INCLUDED_VFS_TREE diff --git a/source/lib/res/graphics/ogl_tex.cpp b/source/lib/res/graphics/ogl_tex.cpp index c126e20de7..aaf8648f83 100644 --- a/source/lib/res/graphics/ogl_tex.cpp +++ b/source/lib/res/graphics/ogl_tex.cpp @@ -421,11 +421,12 @@ static LibError OglTex_reload(OglTex* ot, const VfsPath& pathname, Handle h) if(!(ot->flags & OT_TEX_VALID)) { shared_ptr file; size_t fileSize; - RETURN_ERR(g_VFS->LoadFile(pathname, file, fileSize)); - if(tex_decode(file, fileSize, &ot->t) < 0) - return 0; + if(g_VFS->LoadFile(pathname, file, fileSize) >= 0) + { + if(tex_decode(file, fileSize, &ot->t) >= 0) + ot->flags |= OT_TEX_VALID; + } } - ot->flags |= OT_TEX_VALID; glGenTextures(1, &ot->id); @@ -439,26 +440,29 @@ static LibError OglTex_reload(OglTex* ot, const VfsPath& pathname, Handle h) static LibError OglTex_validate(const OglTex* ot) { - RETURN_ERR(tex_validate(&ot->t)); + if(ot->flags & OT_TEX_VALID) + { + RETURN_ERR(tex_validate(&ot->t)); - // width, height - // (note: this is done here because tex.cpp doesn't impose any - // restrictions on dimensions, while OpenGL does). - GLsizei w = (GLsizei)ot->t.w; - GLsizei h = (GLsizei)ot->t.h; - // .. == 0; texture file probably not loaded successfully. - if(w == 0 || h == 0) - WARN_RETURN(ERR::_11); - // .. greater than max supported tex dimension. - // no-op if ogl_Init not yet called - if(w > (GLsizei)ogl_max_tex_size || h > (GLsizei)ogl_max_tex_size) - WARN_RETURN(ERR::_12); - // .. not power-of-2. - // note: we can't work around this because both NV_texture_rectangle - // and subtexture require work for the client (changing tex coords). - // TODO: ARB_texture_non_power_of_two - if(!is_pow2(w) || !is_pow2(h)) - WARN_RETURN(ERR::_13); + // width, height + // (note: this is done here because tex.cpp doesn't impose any + // restrictions on dimensions, while OpenGL does). + GLsizei w = (GLsizei)ot->t.w; + GLsizei h = (GLsizei)ot->t.h; + // .. == 0; texture file probably not loaded successfully. + if(w == 0 || h == 0) + WARN_RETURN(ERR::_11); + // .. greater than max supported tex dimension. + // no-op if ogl_Init not yet called + if(w > (GLsizei)ogl_max_tex_size || h > (GLsizei)ogl_max_tex_size) + WARN_RETURN(ERR::_12); + // .. not power-of-2. + // note: we can't work around this because both NV_texture_rectangle + // and subtexture require work for the client (changing tex coords). + // TODO: ARB_texture_non_power_of_two + if(!is_pow2(w) || !is_pow2(h)) + WARN_RETURN(ERR::_13); + } // texture state if(!filter_valid(ot->state.filter)) @@ -481,7 +485,7 @@ static LibError OglTex_validate(const OglTex* ot) static LibError OglTex_to_string(const OglTex* ot, char* buf) { - snprintf(buf, H_STRING_LEN, "id=%d", ot->id); + snprintf(buf, H_STRING_LEN, "OglTex id=%d flags=%x", ot->id, ot->flags); return INFO::OK; } @@ -831,49 +835,52 @@ LibError ogl_tex_upload(const Handle ht, GLenum fmt_ovr, int q_flags_ovr, GLint if(ot->flags & OT_IS_UPLOADED) return INFO::OK; - debug_assert(ot->flags & OT_TEX_VALID); - - // decompress S3TC if that's not supported by OpenGL. - if((t->flags & TEX_DXT) && !have_s3tc) - (void)tex_transform_to(t, t->flags & ~TEX_DXT); - - // determine fmt and int_fmt, allowing for user override. - ot->fmt = choose_fmt(t->bpp, t->flags); - if(fmt_ovr) ot->fmt = fmt_ovr; - if(q_flags_ovr) ot->q_flags = q_flags_ovr; - ot->int_fmt = choose_int_fmt(ot->fmt, ot->q_flags); - if(int_fmt_ovr) ot->int_fmt = int_fmt_ovr; - - // now actually send to OpenGL: - ogl_WarnIfError(); + if(ot->flags & OT_TEX_VALID) { - // (note: we know ht is valid due to H_DEREF, but ogl_tex_bind can - // fail in debug builds if OglTex.id isn't a valid texture name) - RETURN_ERR(ogl_tex_bind(ht, ot->tmu)); - int levels_to_skip; - if(get_mipmaps(t, ot->state.filter, ot->q_flags, &levels_to_skip) < 0) - // error => disable mipmapping - ot->state.filter = GL_LINEAR; - // (note: if first time, applies our defaults/previous overrides; - // otherwise, replays all state changes) - state_latch(&ot->state); - upload_impl(t, ot->fmt, ot->int_fmt, levels_to_skip); - } - ogl_WarnIfError(); + // decompress S3TC if that's not supported by OpenGL. + if((t->flags & TEX_DXT) && !have_s3tc) + (void)tex_transform_to(t, t->flags & ~TEX_DXT); - ot->flags |= OT_NEED_AUTO_UPLOAD|OT_IS_UPLOADED; + // determine fmt and int_fmt, allowing for user override. + ot->fmt = choose_fmt(t->bpp, t->flags); + if(fmt_ovr) ot->fmt = fmt_ovr; + if(q_flags_ovr) ot->q_flags = q_flags_ovr; + ot->int_fmt = choose_int_fmt(ot->fmt, ot->q_flags); + if(int_fmt_ovr) ot->int_fmt = int_fmt_ovr; - // see rationale for at declaration of OglTex. - // note: tex_free is safe even if this OglTex was wrapped - - // the Tex contains a mem handle. - int refs = h_get_refcnt(ht); - if(refs == 1) - { - // note: we verify above that OT_TEX_VALID is set - tex_free(t); - ot->flags &= ~OT_TEX_VALID; + // now actually send to OpenGL: + ogl_WarnIfError(); + { + // (note: we know ht is valid due to H_DEREF, but ogl_tex_bind can + // fail in debug builds if OglTex.id isn't a valid texture name) + RETURN_ERR(ogl_tex_bind(ht, ot->tmu)); + int levels_to_skip; + if(get_mipmaps(t, ot->state.filter, ot->q_flags, &levels_to_skip) < 0) + // error => disable mipmapping + ot->state.filter = GL_LINEAR; + // (note: if first time, applies our defaults/previous overrides; + // otherwise, replays all state changes) + state_latch(&ot->state); + upload_impl(t, ot->fmt, ot->int_fmt, levels_to_skip); + } + ogl_WarnIfError(); + + ot->flags |= OT_IS_UPLOADED; + + // see rationale for at declaration of OglTex. + // note: tex_free is safe even if this OglTex was wrapped - + // the Tex contains a mem handle. + int refs = h_get_refcnt(ht); + if(refs == 1) + { + // note: we verify above that OT_TEX_VALID is set + tex_free(t); + ot->flags &= ~OT_TEX_VALID; + } } + ot->flags |= OT_NEED_AUTO_UPLOAD; + return INFO::OK; } diff --git a/source/main.cpp b/source/main.cpp index a2a431e592..2b9694be95 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -41,6 +41,7 @@ that of Atlas depending on commandline parameters. #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/Loader.h" +#include "ps/Filesystem.h" #include "ps/CConsole.h" #include "ps/Profile.h" #include "ps/Util.h" @@ -238,7 +239,7 @@ static void Frame() { PROFILE_START("reload changed files"); MICROLOG(L"reload changed files"); -// vfs_reload_changed_files(); + g_VFS->ReloadChangedFiles(); PROFILE_END( "reload changed files"); }