From 1119af2e38be3a71b7313ea441f06105f119a6cf Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 10 Apr 2026 21:36:53 +0800 Subject: [PATCH] Upgrade project asset watcher to native Win32 notifications --- editor/src/Core/ProjectAssetWatcher.cpp | 307 +++++++++++++++++++++++- editor/src/Core/ProjectAssetWatcher.h | 17 ++ 2 files changed, 314 insertions(+), 10 deletions(-) diff --git a/editor/src/Core/ProjectAssetWatcher.cpp b/editor/src/Core/ProjectAssetWatcher.cpp index 794e5870..87d9916b 100644 --- a/editor/src/Core/ProjectAssetWatcher.cpp +++ b/editor/src/Core/ProjectAssetWatcher.cpp @@ -1,10 +1,15 @@ +#define WIN32_LEAN_AND_MEAN #include "Core/ProjectAssetWatcher.h" #include "Core/IProjectManager.h" +#include "Platform/Win32Utf8.h" + +#include #include #include #include +#include #include #include @@ -17,6 +22,14 @@ namespace { constexpr float kScanIntervalSeconds = 0.75f; constexpr float kDebounceWindowSeconds = 0.35f; +constexpr float kNativeRestartIntervalSeconds = 2.0f; +constexpr DWORD kNativeWatchBufferSize = 64 * 1024; +constexpr DWORD kNativeNotifyFilter = + FILE_NOTIFY_CHANGE_FILE_NAME | + FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_SIZE | + FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_CREATION; std::string NormalizePathKey(const fs::path& path) { std::string key = path.lexically_normal().generic_string(); @@ -46,20 +59,44 @@ std::uint64_t GetFileWriteTimeValue(const fs::path& path) { return static_cast(writeTime.time_since_epoch().count()); } +void CloseHandleIfValid(void*& handle) { + if (handle == nullptr) { + return; + } + + CloseHandle(static_cast(handle)); + handle = nullptr; +} + +std::string PathToUtf8String(const fs::path& path) { + return Platform::WideToUtf8(path.wstring()); +} + } // namespace +ProjectAssetWatcher::~ProjectAssetWatcher() { + Detach(); +} + void ProjectAssetWatcher::Attach(const std::string& projectPath) { RefreshProjectBinding(projectPath); } void ProjectAssetWatcher::Detach() { + StopNativeWatcher(); m_projectPath.clear(); m_assetsRoot.clear(); m_snapshot.clear(); m_pendingChanges.Clear(); + { + std::lock_guard lock(m_nativeChangeMutex); + m_threadPendingChanges.Clear(); + } m_scanCooldownSeconds = 0.0f; + m_nativeRestartCooldownSeconds = 0.0f; m_debounceSeconds = 0.0f; m_hasPendingRefresh = false; + m_pollingResyncRequested.store(false); } void ProjectAssetWatcher::Update(IEditorContext& context, float dt) { @@ -68,10 +105,23 @@ void ProjectAssetWatcher::Update(IEditorContext& context, float dt) { return; } - m_scanCooldownSeconds -= dt; - if (m_scanCooldownSeconds <= 0.0f) { + ConsumeNativeChanges(); + + if (m_pollingResyncRequested.exchange(false)) { ScanForChanges(); - m_scanCooldownSeconds = kScanIntervalSeconds; + } + + if (!m_nativeWatcherHealthy.load()) { + m_nativeRestartCooldownSeconds -= dt; + if (m_nativeRestartCooldownSeconds <= 0.0f) { + StartNativeWatcher(); + } + + m_scanCooldownSeconds -= dt; + if (m_scanCooldownSeconds <= 0.0f) { + ScanForChanges(); + m_scanCooldownSeconds = kScanIntervalSeconds; + } } if (!m_hasPendingRefresh) { @@ -89,25 +139,250 @@ void ProjectAssetWatcher::RefreshProjectBinding(const std::string& projectPath) return; } + StopNativeWatcher(); m_projectPath = projectPath; - m_assetsRoot = projectPath.empty() ? fs::path() : (fs::path(projectPath) / "Assets"); + m_assetsRoot = projectPath.empty() ? fs::path() : (fs::path(Platform::Utf8ToWide(projectPath)) / L"Assets"); m_snapshot.clear(); m_pendingChanges.Clear(); m_scanCooldownSeconds = kScanIntervalSeconds; + m_nativeRestartCooldownSeconds = 0.0f; m_debounceSeconds = 0.0f; m_hasPendingRefresh = false; + { + std::lock_guard lock(m_nativeChangeMutex); + m_threadPendingChanges.Clear(); + } + m_pollingResyncRequested.store(false); if (!m_assetsRoot.empty()) { BuildSnapshot(m_assetsRoot, m_snapshot); + StartNativeWatcher(); } } -void ProjectAssetWatcher::ScanForChanges() { - Snapshot currentSnapshot; - BuildSnapshot(m_assetsRoot, currentSnapshot); +void ProjectAssetWatcher::StartNativeWatcher() { + StopNativeWatcher(); + m_nativeRestartCooldownSeconds = kNativeRestartIntervalSeconds; - const ChangeStats changes = DiffSnapshots(m_snapshot, currentSnapshot); - m_snapshot = std::move(currentSnapshot); + std::error_code ec; + if (m_assetsRoot.empty() || !fs::exists(m_assetsRoot, ec) || !fs::is_directory(m_assetsRoot, ec)) { + return; + } + + HANDLE stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (stopEvent == nullptr) { + Debug::Logger::Get().Warning( + Debug::LogCategory::FileSystem, + (std::string("[ProjectAssetWatcher] Failed to create native watcher stop event. error=") + + std::to_string(GetLastError()) + + " fallback=polling").c_str()); + return; + } + + HANDLE directoryHandle = CreateFileW( + m_assetsRoot.c_str(), + FILE_LIST_DIRECTORY, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, + nullptr); + if (directoryHandle == INVALID_HANDLE_VALUE) { + const DWORD error = GetLastError(); + CloseHandle(stopEvent); + Debug::Logger::Get().Warning( + Debug::LogCategory::FileSystem, + (std::string("[ProjectAssetWatcher] Failed to start native watcher for ") + + PathToUtf8String(m_assetsRoot) + + " error=" + + std::to_string(error) + + " fallback=polling").c_str()); + return; + } + + m_nativeStopEvent = stopEvent; + m_nativeWatcherHealthy.store(true); + m_nativeWatcherThread = + std::thread(&ProjectAssetWatcher::RunNativeWatcher, this, static_cast(directoryHandle), m_assetsRoot); + + Debug::Logger::Get().Info( + Debug::LogCategory::FileSystem, + (std::string("[ProjectAssetWatcher] Started native watcher for ") + PathToUtf8String(m_assetsRoot)).c_str()); +} + +void ProjectAssetWatcher::StopNativeWatcher() { + m_nativeWatcherHealthy.store(false); + + if (m_nativeStopEvent != nullptr) { + SetEvent(static_cast(m_nativeStopEvent)); + } + + if (m_nativeWatcherThread.joinable()) { + m_nativeWatcherThread.join(); + } + + CloseHandleIfValid(m_nativeStopEvent); +} + +void ProjectAssetWatcher::RunNativeWatcher(void* directoryHandleRaw, fs::path watchedRoot) { + HANDLE directoryHandle = static_cast(directoryHandleRaw); + HANDLE stopEvent = static_cast(m_nativeStopEvent); + HANDLE readEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (readEvent == nullptr) { + Debug::Logger::Get().Warning( + Debug::LogCategory::FileSystem, + (std::string("[ProjectAssetWatcher] Failed to create native watcher read event for ") + + PathToUtf8String(watchedRoot) + + " error=" + + std::to_string(GetLastError()) + + " fallback=polling").c_str()); + CloseHandle(directoryHandle); + m_nativeWatcherHealthy.store(false); + m_pollingResyncRequested.store(true); + return; + } + + std::array buffer{}; + while (WaitForSingleObject(stopEvent, 0) != WAIT_OBJECT_0) { + ResetEvent(readEvent); + + OVERLAPPED overlapped{}; + overlapped.hEvent = readEvent; + if (!ReadDirectoryChangesW( + directoryHandle, + buffer.data(), + static_cast(buffer.size()), + TRUE, + kNativeNotifyFilter, + nullptr, + &overlapped, + nullptr)) { + const DWORD error = GetLastError(); + if (error != ERROR_OPERATION_ABORTED) { + Debug::Logger::Get().Warning( + Debug::LogCategory::FileSystem, + (std::string("[ProjectAssetWatcher] Native watcher failed for ") + + PathToUtf8String(watchedRoot) + + " error=" + + std::to_string(error) + + " fallback=polling").c_str()); + m_pollingResyncRequested.store(true); + } + m_nativeWatcherHealthy.store(false); + break; + } + + HANDLE waitHandles[] = { stopEvent, readEvent }; + const DWORD waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE); + if (waitResult == WAIT_OBJECT_0) { + CancelIoEx(directoryHandle, &overlapped); + DWORD ignoredBytes = 0; + GetOverlappedResult(directoryHandle, &overlapped, &ignoredBytes, TRUE); + break; + } + + if (waitResult != (WAIT_OBJECT_0 + 1)) { + CancelIoEx(directoryHandle, &overlapped); + Debug::Logger::Get().Warning( + Debug::LogCategory::FileSystem, + (std::string("[ProjectAssetWatcher] Native watcher wait failed for ") + + PathToUtf8String(watchedRoot) + + " error=" + + std::to_string(GetLastError()) + + " fallback=polling").c_str()); + m_nativeWatcherHealthy.store(false); + m_pollingResyncRequested.store(true); + break; + } + + DWORD bytesReturned = 0; + if (!GetOverlappedResult(directoryHandle, &overlapped, &bytesReturned, FALSE)) { + const DWORD error = GetLastError(); + if (error == ERROR_NOTIFY_ENUM_DIR) { + m_pollingResyncRequested.store(true); + continue; + } + if (error == ERROR_OPERATION_ABORTED) { + break; + } + + Debug::Logger::Get().Warning( + Debug::LogCategory::FileSystem, + (std::string("[ProjectAssetWatcher] Native watcher overlapped result failed for ") + + PathToUtf8String(watchedRoot) + + " error=" + + std::to_string(error) + + " fallback=polling").c_str()); + m_nativeWatcherHealthy.store(false); + m_pollingResyncRequested.store(true); + break; + } + + if (bytesReturned == 0) { + m_pollingResyncRequested.store(true); + continue; + } + + ChangeStats changes; + bool requiresResync = false; + for (DWORD offset = 0; offset < bytesReturned;) { + const auto* notify = reinterpret_cast( + reinterpret_cast(buffer.data()) + offset); + + switch (notify->Action) { + case FILE_ACTION_ADDED: + ++changes.added; + break; + case FILE_ACTION_REMOVED: + ++changes.removed; + break; + case FILE_ACTION_MODIFIED: + ++changes.modified; + break; + case FILE_ACTION_RENAMED_OLD_NAME: + ++changes.removed; + break; + case FILE_ACTION_RENAMED_NEW_NAME: + ++changes.added; + break; + default: + requiresResync = true; + break; + } + + if (notify->NextEntryOffset == 0) { + break; + } + + offset += notify->NextEntryOffset; + } + + if (requiresResync) { + m_pollingResyncRequested.store(true); + } + + if (changes.HasChanges()) { + std::lock_guard lock(m_nativeChangeMutex); + m_threadPendingChanges.Accumulate(changes); + } + } + + CloseHandle(readEvent); + CloseHandle(directoryHandle); +} + +void ProjectAssetWatcher::ConsumeNativeChanges() { + ChangeStats changes; + { + std::lock_guard lock(m_nativeChangeMutex); + changes = m_threadPendingChanges; + m_threadPendingChanges.Clear(); + } + + ScheduleRefresh(changes); +} + +void ProjectAssetWatcher::ScheduleRefresh(const ChangeStats& changes) { if (!changes.HasChanges()) { return; } @@ -117,6 +392,15 @@ void ProjectAssetWatcher::ScanForChanges() { m_debounceSeconds = kDebounceWindowSeconds; } +void ProjectAssetWatcher::ScanForChanges() { + Snapshot currentSnapshot; + BuildSnapshot(m_assetsRoot, currentSnapshot); + + const ChangeStats changes = DiffSnapshots(m_snapshot, currentSnapshot); + m_snapshot = std::move(currentSnapshot); + ScheduleRefresh(changes); +} + void ProjectAssetWatcher::FlushPendingRefresh(IEditorContext& context) { if (!m_hasPendingRefresh) { return; @@ -124,6 +408,7 @@ void ProjectAssetWatcher::FlushPendingRefresh(IEditorContext& context) { Resources::ResourceManager::Get().RefreshProjectAssets(); context.GetProjectManager().RefreshCurrentFolder(); + BuildSnapshot(m_assetsRoot, m_snapshot); Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, @@ -132,7 +417,9 @@ void ProjectAssetWatcher::FlushPendingRefresh(IEditorContext& context) { " modified=" + std::to_string(m_pendingChanges.modified) + " removed=" + - std::to_string(m_pendingChanges.removed)).c_str()); + std::to_string(m_pendingChanges.removed) + + " backend=" + + (m_nativeWatcherHealthy.load() ? "native" : "polling")).c_str()); m_pendingChanges.Clear(); m_debounceSeconds = 0.0f; diff --git a/editor/src/Core/ProjectAssetWatcher.h b/editor/src/Core/ProjectAssetWatcher.h index 7c8f2ebb..33022b15 100644 --- a/editor/src/Core/ProjectAssetWatcher.h +++ b/editor/src/Core/ProjectAssetWatcher.h @@ -2,9 +2,12 @@ #include "Core/IEditorContext.h" +#include #include #include +#include #include +#include #include namespace XCEngine { @@ -12,6 +15,8 @@ namespace Editor { class ProjectAssetWatcher { public: + ~ProjectAssetWatcher(); + void Attach(const std::string& projectPath); void Detach(); void Update(IEditorContext& context, float dt); @@ -54,6 +59,11 @@ private: using Snapshot = std::unordered_map; void RefreshProjectBinding(const std::string& projectPath); + void StartNativeWatcher(); + void StopNativeWatcher(); + void RunNativeWatcher(void* directoryHandle, std::filesystem::path watchedRoot); + void ConsumeNativeChanges(); + void ScheduleRefresh(const ChangeStats& changes); void ScanForChanges(); void FlushPendingRefresh(IEditorContext& context); static void BuildSnapshot(const std::filesystem::path& assetsRoot, Snapshot& outSnapshot); @@ -64,8 +74,15 @@ private: Snapshot m_snapshot; ChangeStats m_pendingChanges; float m_scanCooldownSeconds = 0.0f; + float m_nativeRestartCooldownSeconds = 0.0f; float m_debounceSeconds = 0.0f; bool m_hasPendingRefresh = false; + std::thread m_nativeWatcherThread; + std::mutex m_nativeChangeMutex; + ChangeStats m_threadPendingChanges; + std::atomic m_nativeWatcherHealthy{false}; + std::atomic m_pollingResyncRequested{false}; + void* m_nativeStopEvent = nullptr; }; } // namespace Editor