#include "BuiltInIcons.h" #include "ImGuiBackendBridge.h" #include "BuiltInIconLayoutUtils.h" #include "Platform/Win32Utf8.h" #include "StyleTokens.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace XCEngine { namespace Editor { namespace UI { namespace { using BuiltInTexture = ImGuiBackendBridge::UploadedTexture; using PreviewGpuUpload = ImGuiBackendBridge::PendingTextureUpload; struct LoadedTexturePixels { std::vector rgbaPixels; int width = 0; int height = 0; }; struct PreviewDecodeJob { std::string key; std::filesystem::path filePath; std::filesystem::path cacheFilePath; std::string relativePathKey; std::uint64_t fileSize = 0; std::int64_t lastWriteTimeTicks = 0; bool useDiskCache = false; }; struct PreviewDecodeResult { std::string key; LoadedTexturePixels pixels; std::uint64_t fileSize = 0; std::int64_t lastWriteTimeTicks = 0; bool success = false; }; struct SourceFileFingerprint { std::uint64_t fileSize = 0; std::int64_t lastWriteTimeTicks = 0; bool IsValid() const { return fileSize > 0 && lastWriteTimeTicks != 0; } }; bool operator==(const SourceFileFingerprint& lhs, const SourceFileFingerprint& rhs) { return lhs.fileSize == rhs.fileSize && lhs.lastWriteTimeTicks == rhs.lastWriteTimeTicks; } bool operator!=(const SourceFileFingerprint& lhs, const SourceFileFingerprint& rhs) { return !(lhs == rhs); } struct PreviewDiskCacheEntry { std::filesystem::path filePath; std::string relativePathKey; }; #pragma pack(push, 1) struct ThumbnailCacheFileHeader { char magic[8]; std::uint32_t version = 1; std::uint64_t fileSize = 0; std::int64_t lastWriteTimeTicks = 0; std::uint32_t width = 0; std::uint32_t height = 0; std::uint32_t relativePathSize = 0; std::uint32_t pixelDataSize = 0; }; #pragma pack(pop) struct BuiltInIconState { ImGuiBackendBridge* backend = nullptr; ID3D12Device* device = nullptr; ID3D12CommandQueue* commandQueue = nullptr; BuiltInTexture folder; BuiltInTexture gameObject; BuiltInTexture scene; BuiltInTexture cameraGizmo; BuiltInTexture directionalLightGizmo; BuiltInTexture pointLightGizmo; BuiltInTexture spotLightGizmo; struct CachedAssetPreview { BuiltInTexture texture; std::unique_ptr decodedPixels; std::unique_ptr pendingUpload; SourceFileFingerprint sourceFingerprint; bool decodeQueued = false; bool hasStaleTexture = false; bool loadFailed = false; int lastUsedFrame = -1; int lastSourceValidationFrame = -1; }; std::unordered_map assetPreviews; std::vector> pendingIconUploads; std::vector previewWorkers; std::deque previewDecodeQueue; std::deque previewDecodeResults; std::mutex previewQueueMutex; std::condition_variable previewQueueEvent; bool previewWorkersRunning = false; size_t pendingPreviewDecodeJobs = 0; int lastPreviewBudgetFrame = -1; int previewLoadsThisFrame = 0; int lastMaintenanceFrame = -1; }; BuiltInIconState g_icons; constexpr size_t kMaxCachedAssetPreviews = 40; constexpr int kMaxPreviewLoadsPerFrame = 64; constexpr size_t kMaxQueuedPreviewDecodeJobs = 64; constexpr int kMaxPreviewThumbnailExtent = 192; constexpr size_t kPreviewWorkerCount = 2; constexpr int kPreviewSourceValidationIntervalFrames = 30; constexpr std::uint32_t kThumbnailCacheVersion = 1; constexpr std::array kThumbnailCacheMagic = { 'X', 'C', 'T', 'H', 'M', 'B', '1', '\0' }; constexpr std::uint32_t kMaxThumbnailCacheRelativePathBytes = 16u * 1024u; std::filesystem::path ResolveFolderIconPath() { const std::filesystem::path exeDir(Platform::GetExecutableDirectoryUtf8()); return (exeDir / ".." / ".." / "resources" / "Icons" / "folder_icon.png").lexically_normal(); } std::filesystem::path ResolveGameObjectIconPath() { const std::filesystem::path exeDir(Platform::GetExecutableDirectoryUtf8()); return (exeDir / ".." / ".." / "resources" / "Icons" / "gameobject_icon.png").lexically_normal(); } std::filesystem::path ResolveSceneIconPath() { const std::filesystem::path exeDir(Platform::GetExecutableDirectoryUtf8()); return (exeDir / ".." / ".." / "resources" / "Icons" / "scene_icon.png").lexically_normal(); } std::filesystem::path ResolveCameraGizmoIconPath() { const std::filesystem::path exeDir(Platform::GetExecutableDirectoryUtf8()); return (exeDir / ".." / ".." / "resources" / "Icons" / "camera_gizmo.png").lexically_normal(); } std::filesystem::path ResolveDirectionalLightGizmoIconPath() { const std::filesystem::path exeDir(Platform::GetExecutableDirectoryUtf8()); return (exeDir / ".." / ".." / "resources" / "Icons" / "directional_light_gizmo.png").lexically_normal(); } std::filesystem::path ResolvePointLightGizmoIconPath() { const std::filesystem::path exeDir(Platform::GetExecutableDirectoryUtf8()); return (exeDir / ".." / ".." / "resources" / "Icons" / "point_light_gizmo.png").lexically_normal(); } std::filesystem::path ResolveSpotLightGizmoIconPath() { const std::filesystem::path exeDir(Platform::GetExecutableDirectoryUtf8()); return (exeDir / ".." / ".." / "resources" / "Icons" / "spot_light_gizmo.png").lexically_normal(); } std::string NormalizePathKey(const std::filesystem::path& path) { std::wstring key = path.lexically_normal().generic_wstring(); std::transform(key.begin(), key.end(), key.begin(), ::towlower); return Platform::WideToUtf8(key); } std::string MakeAssetPreviewKey(const std::string& filePath) { return NormalizePathKey(std::filesystem::path(Platform::Utf8ToWide(filePath))); } SourceFileFingerprint MakeSourceFingerprint( std::uint64_t fileSize, std::int64_t lastWriteTimeTicks) { SourceFileFingerprint fingerprint; fingerprint.fileSize = fileSize; fingerprint.lastWriteTimeTicks = lastWriteTimeTicks; return fingerprint; } bool QuerySourceFileFingerprint( const std::filesystem::path& filePath, SourceFileFingerprint& outFingerprint) { std::error_code ec; if (!std::filesystem::exists(filePath, ec) || ec) { return false; } ec.clear(); if (!std::filesystem::is_regular_file(filePath, ec) || ec) { return false; } ec.clear(); const auto fileSize = std::filesystem::file_size(filePath, ec); if (ec || fileSize == static_cast(-1)) { return false; } ec.clear(); const auto lastWriteTime = std::filesystem::last_write_time(filePath, ec); if (ec) { return false; } outFingerprint.fileSize = static_cast(fileSize); outFingerprint.lastWriteTimeTicks = static_cast(lastWriteTime.time_since_epoch().count()); return outFingerprint.IsValid(); } std::uint64_t ComputeFnv1a64(const std::string& value) { constexpr std::uint64_t kOffsetBasis = 14695981039346656037ull; constexpr std::uint64_t kPrime = 1099511628211ull; std::uint64_t hash = kOffsetBasis; for (unsigned char ch : value) { hash ^= static_cast(ch); hash *= kPrime; } return hash; } std::string FormatHashHex(std::uint64_t value) { static constexpr char kHexDigits[] = "0123456789abcdef"; std::string result(16, '0'); for (int index = 15; index >= 0; --index) { result[static_cast(index)] = kHexDigits[value & 0xFu]; value >>= 4u; } return result; } bool IsProjectRelativePath(const std::filesystem::path& relativePath) { if (relativePath.empty()) { return false; } const std::wstring generic = relativePath.generic_wstring(); return generic != L"." && generic != L".." && generic.rfind(L"../", 0) != 0; } bool ResolvePreviewDiskCacheEntry( const std::string& projectPath, const std::string& filePath, PreviewDiskCacheEntry& outEntry) { if (projectPath.empty() || filePath.empty()) { return false; } std::error_code ec; const std::filesystem::path rootPath = std::filesystem::weakly_canonical( std::filesystem::path(Platform::Utf8ToWide(projectPath)), ec); if (ec || rootPath.empty()) { return false; } ec.clear(); const std::filesystem::path assetPath = std::filesystem::weakly_canonical( std::filesystem::path(Platform::Utf8ToWide(filePath)), ec); if (ec || assetPath.empty()) { return false; } ec.clear(); std::filesystem::path relativePath = std::filesystem::relative(assetPath, rootPath, ec); if (ec) { return false; } relativePath = relativePath.lexically_normal(); if (!IsProjectRelativePath(relativePath)) { return false; } outEntry.relativePathKey = NormalizePathKey(relativePath); const std::string hashFileName = FormatHashHex(ComputeFnv1a64(outEntry.relativePathKey)) + ".thumb"; outEntry.filePath = (rootPath / L".xceditor" / L"thumbs" / std::filesystem::path(Platform::Utf8ToWide(hashFileName))) .lexically_normal(); return true; } bool ReadThumbnailCacheFile( const std::filesystem::path& cacheFilePath, const std::string& relativePathKey, const SourceFileFingerprint& sourceFingerprint, LoadedTexturePixels& outTexturePixels) { std::ifstream stream(cacheFilePath, std::ios::binary | std::ios::in); if (!stream.is_open()) { return false; } ThumbnailCacheFileHeader header = {}; if (!stream.read(reinterpret_cast(&header), sizeof(header)).good()) { return false; } if (std::memcmp(header.magic, kThumbnailCacheMagic.data(), kThumbnailCacheMagic.size()) != 0 || header.version != kThumbnailCacheVersion || header.fileSize != sourceFingerprint.fileSize || header.lastWriteTimeTicks != sourceFingerprint.lastWriteTimeTicks || header.relativePathSize == 0 || header.relativePathSize > kMaxThumbnailCacheRelativePathBytes || header.width == 0 || header.height == 0 || header.width > static_cast(kMaxPreviewThumbnailExtent) || header.height > static_cast(kMaxPreviewThumbnailExtent)) { return false; } const std::uint64_t expectedPixelBytes = static_cast(header.width) * static_cast(header.height) * 4ull; if (expectedPixelBytes == 0 || expectedPixelBytes > static_cast((std::numeric_limits::max)()) || header.pixelDataSize != expectedPixelBytes) { return false; } std::string cachedRelativePath(header.relativePathSize, '\0'); if (!stream.read(cachedRelativePath.data(), static_cast(cachedRelativePath.size())).good()) { return false; } if (cachedRelativePath != relativePathKey) { return false; } outTexturePixels.width = static_cast(header.width); outTexturePixels.height = static_cast(header.height); outTexturePixels.rgbaPixels.resize(static_cast(header.pixelDataSize)); if (!stream.read( reinterpret_cast(outTexturePixels.rgbaPixels.data()), static_cast(outTexturePixels.rgbaPixels.size())).good()) { outTexturePixels.rgbaPixels.clear(); outTexturePixels.width = 0; outTexturePixels.height = 0; return false; } return true; } void RemoveDiskCacheFile(const std::filesystem::path& cacheFilePath) { std::error_code ec; std::filesystem::remove(cacheFilePath, ec); } bool WriteThumbnailCacheFile( const std::filesystem::path& cacheFilePath, const std::string& relativePathKey, const SourceFileFingerprint& sourceFingerprint, const LoadedTexturePixels& texturePixels) { if (cacheFilePath.empty() || relativePathKey.empty() || relativePathKey.size() > kMaxThumbnailCacheRelativePathBytes || !sourceFingerprint.IsValid() || texturePixels.width <= 0 || texturePixels.height <= 0 || texturePixels.rgbaPixels.empty()) { return false; } const std::uint64_t pixelDataSize = static_cast(texturePixels.rgbaPixels.size()); if (pixelDataSize != static_cast(texturePixels.width) * static_cast(texturePixels.height) * 4ull || pixelDataSize > static_cast((std::numeric_limits::max)())) { return false; } std::error_code ec; std::filesystem::create_directories(cacheFilePath.parent_path(), ec); if (ec) { return false; } const std::filesystem::path tempPath = cacheFilePath.parent_path() / (cacheFilePath.filename().wstring() + L".tmp"); std::ofstream stream(tempPath, std::ios::binary | std::ios::out | std::ios::trunc); if (!stream.is_open()) { return false; } ThumbnailCacheFileHeader header = {}; std::memcpy(header.magic, kThumbnailCacheMagic.data(), kThumbnailCacheMagic.size()); header.version = kThumbnailCacheVersion; header.fileSize = sourceFingerprint.fileSize; header.lastWriteTimeTicks = sourceFingerprint.lastWriteTimeTicks; header.width = static_cast(texturePixels.width); header.height = static_cast(texturePixels.height); header.relativePathSize = static_cast(relativePathKey.size()); header.pixelDataSize = static_cast(pixelDataSize); const bool writeSucceeded = stream.write(reinterpret_cast(&header), sizeof(header)).good() && stream.write(relativePathKey.data(), static_cast(relativePathKey.size())).good() && stream.write( reinterpret_cast(texturePixels.rgbaPixels.data()), static_cast(texturePixels.rgbaPixels.size())).good(); stream.close(); if (!writeSucceeded) { RemoveDiskCacheFile(tempPath); return false; } ec.clear(); std::filesystem::remove(cacheFilePath, ec); ec.clear(); std::filesystem::rename(tempPath, cacheFilePath, ec); if (ec) { RemoveDiskCacheFile(tempPath); return false; } return true; } bool ReadFileBytes(const std::filesystem::path& filePath, std::vector& bytes) { std::ifstream stream(filePath, std::ios::binary | std::ios::ate); if (!stream.is_open()) { return false; } const std::streamsize size = stream.tellg(); if (size <= 0) { return false; } bytes.resize(static_cast(size)); stream.seekg(0, std::ios::beg); return stream.read(reinterpret_cast(bytes.data()), size).good(); } void ResizeRgbaImageBilinear( const stbi_uc* srcPixels, int srcWidth, int srcHeight, int dstWidth, int dstHeight, std::vector& dstPixels) { dstPixels.resize(static_cast(dstWidth) * static_cast(dstHeight) * 4u); if (!srcPixels || srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) { return; } for (int dstY = 0; dstY < dstHeight; ++dstY) { const float srcY = dstHeight > 1 ? static_cast(dstY) * static_cast(srcHeight - 1) / static_cast(dstHeight - 1) : 0.0f; const int y0 = static_cast(srcY); const int y1 = (std::min)(y0 + 1, srcHeight - 1); const float ty = srcY - static_cast(y0); for (int dstX = 0; dstX < dstWidth; ++dstX) { const float srcX = dstWidth > 1 ? static_cast(dstX) * static_cast(srcWidth - 1) / static_cast(dstWidth - 1) : 0.0f; const int x0 = static_cast(srcX); const int x1 = (std::min)(x0 + 1, srcWidth - 1); const float tx = srcX - static_cast(x0); const size_t dstOffset = (static_cast(dstY) * static_cast(dstWidth) + static_cast(dstX)) * 4u; const size_t srcOffset00 = (static_cast(y0) * static_cast(srcWidth) + static_cast(x0)) * 4u; const size_t srcOffset10 = (static_cast(y0) * static_cast(srcWidth) + static_cast(x1)) * 4u; const size_t srcOffset01 = (static_cast(y1) * static_cast(srcWidth) + static_cast(x0)) * 4u; const size_t srcOffset11 = (static_cast(y1) * static_cast(srcWidth) + static_cast(x1)) * 4u; for (size_t channel = 0; channel < 4u; ++channel) { const float top = static_cast(srcPixels[srcOffset00 + channel]) * (1.0f - tx) + static_cast(srcPixels[srcOffset10 + channel]) * tx; const float bottom = static_cast(srcPixels[srcOffset01 + channel]) * (1.0f - tx) + static_cast(srcPixels[srcOffset11 + channel]) * tx; dstPixels[dstOffset + channel] = static_cast(top * (1.0f - ty) + bottom * ty + 0.5f); } } } } bool DownscalePreviewTextureIfNeeded(LoadedTexturePixels& texturePixels) { if (texturePixels.width <= 0 || texturePixels.height <= 0 || texturePixels.rgbaPixels.empty()) { return false; } const int maxExtent = (std::max)(texturePixels.width, texturePixels.height); if (maxExtent <= kMaxPreviewThumbnailExtent) { return true; } const float scale = static_cast(kMaxPreviewThumbnailExtent) / static_cast(maxExtent); const int dstWidth = (std::max)(1, static_cast(static_cast(texturePixels.width) * scale + 0.5f)); const int dstHeight = (std::max)(1, static_cast(static_cast(texturePixels.height) * scale + 0.5f)); std::vector resizedPixels; ResizeRgbaImageBilinear( texturePixels.rgbaPixels.data(), texturePixels.width, texturePixels.height, dstWidth, dstHeight, resizedPixels); if (resizedPixels.empty()) { return false; } texturePixels.rgbaPixels = std::move(resizedPixels); texturePixels.width = dstWidth; texturePixels.height = dstHeight; return true; } void ResetTexture(BuiltInTexture& texture); void InvalidateAssetPreview(BuiltInIconState::CachedAssetPreview& preview) { if (preview.pendingUpload) { preview.hasStaleTexture = true; } else { ResetTexture(preview.texture); preview.hasStaleTexture = false; } preview.decodedPixels.reset(); preview.decodeQueued = false; preview.loadFailed = false; } bool RefreshAssetPreviewSourceFingerprint( const std::filesystem::path& filePath, BuiltInIconState::CachedAssetPreview& preview) { SourceFileFingerprint currentFingerprint; if (!QuerySourceFileFingerprint(filePath, currentFingerprint)) { InvalidateAssetPreview(preview); preview.sourceFingerprint = {}; preview.loadFailed = true; return false; } if (preview.sourceFingerprint != currentFingerprint) { InvalidateAssetPreview(preview); preview.sourceFingerprint = currentFingerprint; } return true; } void ResetTexture(BuiltInTexture& texture) { if (g_icons.backend != nullptr) { g_icons.backend->ResetUploadedTexture(&texture); return; } texture = {}; } void ResetAssetPreviewCache() { for (auto& entry : g_icons.assetPreviews) { ResetTexture(entry.second.texture); } g_icons.assetPreviews.clear(); } bool DecodeTextureFromFile( const std::filesystem::path& filePath, LoadedTexturePixels& outTexturePixels) { if (!std::filesystem::exists(filePath)) { return false; } std::vector fileData; if (!ReadFileBytes(filePath, fileData)) { return false; } int width = 0; int height = 0; int channels = 0; stbi_uc* pixels = stbi_load_from_memory( fileData.data(), static_cast(fileData.size()), &width, &height, &channels, STBI_rgb_alpha); if (!pixels || width <= 0 || height <= 0) { if (pixels) { stbi_image_free(pixels); } return false; } outTexturePixels.width = width; outTexturePixels.height = height; outTexturePixels.rgbaPixels.assign( pixels, pixels + static_cast(width) * static_cast(height) * 4u); stbi_image_free(pixels); return DownscalePreviewTextureIfNeeded(outTexturePixels); } bool LoadTextureFromFile( ImGuiBackendBridge& backend, ID3D12Device* device, ID3D12CommandQueue* commandQueue, const std::filesystem::path& filePath, BuiltInTexture& outTexture, std::unique_ptr& outPendingUpload) { LoadedTexturePixels texturePixels; if (!DecodeTextureFromFile(filePath, texturePixels)) { return false; } return backend.UploadRgbaTexture( device, commandQueue, texturePixels.rgbaPixels.data(), texturePixels.width, texturePixels.height, &outTexture, &outPendingUpload); } void PreviewWorkerMain() { while (true) { PreviewDecodeJob job; { std::unique_lock lock(g_icons.previewQueueMutex); g_icons.previewQueueEvent.wait(lock, [] { return !g_icons.previewWorkersRunning || !g_icons.previewDecodeQueue.empty(); }); if (!g_icons.previewWorkersRunning && g_icons.previewDecodeQueue.empty()) { return; } job = std::move(g_icons.previewDecodeQueue.front()); g_icons.previewDecodeQueue.pop_front(); } PreviewDecodeResult result; result.key = std::move(job.key); result.fileSize = job.fileSize; result.lastWriteTimeTicks = job.lastWriteTimeTicks; if (job.useDiskCache) { result.success = ReadThumbnailCacheFile( job.cacheFilePath, job.relativePathKey, MakeSourceFingerprint(job.fileSize, job.lastWriteTimeTicks), result.pixels); if (!result.success) { RemoveDiskCacheFile(job.cacheFilePath); } } if (!result.success) { result.success = DecodeTextureFromFile(job.filePath, result.pixels); if (result.success && job.useDiskCache) { WriteThumbnailCacheFile( job.cacheFilePath, job.relativePathKey, MakeSourceFingerprint(job.fileSize, job.lastWriteTimeTicks), result.pixels); } } { std::lock_guard lock(g_icons.previewQueueMutex); g_icons.previewDecodeResults.push_back(std::move(result)); } } } void StartPreviewWorkers() { if (g_icons.previewWorkersRunning) { return; } g_icons.previewWorkersRunning = true; g_icons.previewWorkers.reserve(kPreviewWorkerCount); for (size_t workerIndex = 0; workerIndex < kPreviewWorkerCount; ++workerIndex) { g_icons.previewWorkers.emplace_back(PreviewWorkerMain); } } void StopPreviewWorkers() { { std::lock_guard lock(g_icons.previewQueueMutex); g_icons.previewWorkersRunning = false; g_icons.previewDecodeQueue.clear(); g_icons.previewDecodeResults.clear(); g_icons.pendingPreviewDecodeJobs = 0; } g_icons.previewQueueEvent.notify_all(); for (std::thread& worker : g_icons.previewWorkers) { if (worker.joinable()) { worker.join(); } } g_icons.previewWorkers.clear(); } bool QueuePreviewDecode( const std::string& key, const std::filesystem::path& filePath, const SourceFileFingerprint& sourceFingerprint, const std::string& projectPath, const std::string& filePathUtf8) { PreviewDiskCacheEntry diskCacheEntry; const bool useDiskCache = ResolvePreviewDiskCacheEntry(projectPath, filePathUtf8, diskCacheEntry); std::lock_guard lock(g_icons.previewQueueMutex); if (!g_icons.previewWorkersRunning || g_icons.pendingPreviewDecodeJobs >= kMaxQueuedPreviewDecodeJobs) { return false; } PreviewDecodeJob job; job.key = key; job.filePath = filePath; job.fileSize = sourceFingerprint.fileSize; job.lastWriteTimeTicks = sourceFingerprint.lastWriteTimeTicks; if (useDiskCache) { job.cacheFilePath = std::move(diskCacheEntry.filePath); job.relativePathKey = std::move(diskCacheEntry.relativePathKey); job.useDiskCache = true; } g_icons.previewDecodeQueue.push_back(std::move(job)); ++g_icons.pendingPreviewDecodeJobs; g_icons.previewQueueEvent.notify_one(); return true; } void DrainPreviewDecodeResults() { std::deque completedResults; { std::lock_guard lock(g_icons.previewQueueMutex); completedResults.swap(g_icons.previewDecodeResults); } for (PreviewDecodeResult& result : completedResults) { if (g_icons.pendingPreviewDecodeJobs > 0) { --g_icons.pendingPreviewDecodeJobs; } auto it = g_icons.assetPreviews.find(result.key); if (it == g_icons.assetPreviews.end()) { continue; } BuiltInIconState::CachedAssetPreview& preview = it->second; preview.decodeQueued = false; if (preview.sourceFingerprint != MakeSourceFingerprint(result.fileSize, result.lastWriteTimeTicks)) { continue; } if (!result.success) { preview.loadFailed = true; preview.decodedPixels.reset(); continue; } preview.loadFailed = false; preview.decodedPixels = std::make_unique(std::move(result.pixels)); } } void PollCompletedUploads() { if (g_icons.backend == nullptr) { return; } for (auto& entry : g_icons.assetPreviews) { auto& preview = entry.second; if (preview.pendingUpload && g_icons.backend->IsTextureUploadComplete(*preview.pendingUpload)) { preview.pendingUpload.reset(); if (preview.hasStaleTexture) { ResetTexture(preview.texture); preview.hasStaleTexture = false; } } } g_icons.backend->RemoveCompletedTextureUploads(&g_icons.pendingIconUploads); } void MaintainIconRuntimeState() { const int frame = ImGui::GetFrameCount(); if (g_icons.lastMaintenanceFrame == frame) { return; } g_icons.lastMaintenanceFrame = frame; DrainPreviewDecodeResults(); PollCompletedUploads(); } void DrawTextureIcon(ImDrawList* drawList, const BuiltInTexture& texture, const ImVec2& min, const ImVec2& max) { if (!drawList || !texture.IsValid()) { return; } const ImVec2 size = ComputeFittedIconSize(texture.width, texture.height, min, max); const float x = min.x + ((max.x - min.x) - size.x) * 0.5f; const float y = min.y + ((max.y - min.y) - size.y) * 0.5f; drawList->AddImage(texture.textureId, ImVec2(x, y), ImVec2(x + size.x, y + size.y)); } void DrawBuiltInFolderFallback(ImDrawList* drawList, const ImVec2& min, const ImVec2& max) { if (!drawList) { return; } const float width = max.x - min.x; const float height = max.y - min.y; if (width <= 0.0f || height <= 0.0f) { return; } const float rounding = (std::max)(1.0f, (std::min)(width, height) * 0.18f); const ImU32 tabColor = ImGui::GetColorU32(BuiltInFolderIconTabColor()); const ImU32 topColor = ImGui::GetColorU32(BuiltInFolderIconTopColor()); const ImU32 bodyColor = ImGui::GetColorU32(BuiltInFolderIconBodyColor()); const ImVec2 tabMin(min.x + width * 0.08f, min.y + height * 0.14f); const ImVec2 tabMax(min.x + width * 0.48f, min.y + height * 0.38f); const ImVec2 topMin(min.x + width * 0.24f, min.y + height * 0.22f); const ImVec2 topMax(min.x + width * 0.90f, min.y + height * 0.42f); const ImVec2 bodyMin(min.x + width * 0.06f, min.y + height * 0.32f); const ImVec2 bodyMax(min.x + width * 0.94f, min.y + height * 0.88f); drawList->AddRectFilled(tabMin, tabMax, tabColor, rounding); drawList->AddRectFilled( topMin, topMax, topColor, rounding, ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight); drawList->AddRectFilled(bodyMin, bodyMax, bodyColor, rounding); } void DrawBuiltInFileIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max) { if (!drawList) { return; } const ImU32 fillColor = ImGui::GetColorU32(AssetFileIconFillColor()); const ImU32 lineColor = ImGui::GetColorU32(AssetFileIconLineColor()); const ImVec2 foldA(max.x - 8.0f, min.y); const ImVec2 foldB(max.x, min.y + 8.0f); drawList->AddRectFilled(min, max, fillColor, 2.0f); drawList->AddRect(min, max, lineColor, 2.0f); drawList->AddTriangleFilled(foldA, ImVec2(max.x, min.y), foldB, ImGui::GetColorU32(AssetFileFoldColor())); drawList->AddLine(foldA, foldB, lineColor); } BuiltInIconState::CachedAssetPreview* GetOrCreateAssetPreview( const std::string& filePath, const std::string& projectPath) { if (!g_icons.backend || !g_icons.device || !g_icons.commandQueue || filePath.empty()) { return nullptr; } MaintainIconRuntimeState(); const std::filesystem::path filePathW(Platform::Utf8ToWide(filePath)); const std::string key = MakeAssetPreviewKey(filePath); auto [it, inserted] = g_icons.assetPreviews.try_emplace(key); BuiltInIconState::CachedAssetPreview& preview = it->second; preview.lastUsedFrame = ImGui::GetFrameCount(); const int frame = ImGui::GetFrameCount(); if (inserted || preview.lastSourceValidationFrame < 0 || frame - preview.lastSourceValidationFrame >= kPreviewSourceValidationIntervalFrames) { preview.lastSourceValidationFrame = frame; if (!RefreshAssetPreviewSourceFingerprint(filePathW, preview)) { return &preview; } } if (!preview.sourceFingerprint.IsValid()) { return &preview; } if (preview.texture.IsValid() && !preview.hasStaleTexture) { if (preview.pendingUpload && g_icons.backend->IsTextureUploadComplete(*preview.pendingUpload)) { preview.pendingUpload.reset(); } return &preview; } if (preview.decodedPixels && !preview.pendingUpload) { std::unique_ptr pendingUpload; if (!g_icons.backend->UploadRgbaTexture( g_icons.device, g_icons.commandQueue, preview.decodedPixels->rgbaPixels.data(), preview.decodedPixels->width, preview.decodedPixels->height, &preview.texture, &pendingUpload)) { preview.decodedPixels.reset(); preview.loadFailed = true; return &preview; } preview.decodedPixels.reset(); preview.loadFailed = false; preview.pendingUpload = std::move(pendingUpload); return &preview; } if (preview.decodeQueued || preview.loadFailed) { return &preview; } if (g_icons.lastPreviewBudgetFrame != frame) { g_icons.lastPreviewBudgetFrame = frame; g_icons.previewLoadsThisFrame = 0; } if (g_icons.previewLoadsThisFrame >= kMaxPreviewLoadsPerFrame) { return &preview; } if (QueuePreviewDecode(key, filePathW, preview.sourceFingerprint, projectPath, filePath)) { preview.decodeQueued = true; ++g_icons.previewLoadsThisFrame; } return &preview; } void PruneAssetPreviewCache() { if (g_icons.assetPreviews.size() <= kMaxCachedAssetPreviews) { return; } std::vector> candidates; candidates.reserve(g_icons.assetPreviews.size()); for (const auto& entry : g_icons.assetPreviews) { if (entry.second.pendingUpload || entry.second.decodeQueued) { continue; } candidates.emplace_back(entry.first, entry.second.lastUsedFrame); } std::sort( candidates.begin(), candidates.end(), [](const auto& lhs, const auto& rhs) { return lhs.second < rhs.second; }); size_t removeCount = g_icons.assetPreviews.size() - kMaxCachedAssetPreviews; for (size_t i = 0; i < removeCount && i < candidates.size(); ++i) { auto it = g_icons.assetPreviews.find(candidates[i].first); if (it == g_icons.assetPreviews.end()) { continue; } ResetTexture(it->second.texture); g_icons.assetPreviews.erase(it); } } BuiltInTexture* ResolveEditorTexture(EditorTextureIconKind kind) { switch (kind) { case EditorTextureIconKind::CameraGizmo: return &g_icons.cameraGizmo; case EditorTextureIconKind::DirectionalLightGizmo: return &g_icons.directionalLightGizmo; case EditorTextureIconKind::PointLightGizmo: return &g_icons.pointLightGizmo; case EditorTextureIconKind::SpotLightGizmo: return &g_icons.spotLightGizmo; default: return nullptr; } } } // namespace void InitializeBuiltInIcons( ImGuiBackendBridge& backend, ID3D12Device* device, ID3D12CommandQueue* commandQueue) { ShutdownBuiltInIcons(); g_icons.backend = &backend; g_icons.device = device; g_icons.commandQueue = commandQueue; StartPreviewWorkers(); std::unique_ptr pendingUpload; if (LoadTextureFromFile(backend, device, commandQueue, ResolveFolderIconPath(), g_icons.folder, pendingUpload)) { g_icons.pendingIconUploads.push_back(std::move(pendingUpload)); } pendingUpload.reset(); if (LoadTextureFromFile(backend, device, commandQueue, ResolveGameObjectIconPath(), g_icons.gameObject, pendingUpload)) { g_icons.pendingIconUploads.push_back(std::move(pendingUpload)); } pendingUpload.reset(); if (LoadTextureFromFile(backend, device, commandQueue, ResolveSceneIconPath(), g_icons.scene, pendingUpload)) { g_icons.pendingIconUploads.push_back(std::move(pendingUpload)); } pendingUpload.reset(); if (LoadTextureFromFile( backend, device, commandQueue, ResolveCameraGizmoIconPath(), g_icons.cameraGizmo, pendingUpload)) { g_icons.pendingIconUploads.push_back(std::move(pendingUpload)); } pendingUpload.reset(); if (LoadTextureFromFile( backend, device, commandQueue, ResolveDirectionalLightGizmoIconPath(), g_icons.directionalLightGizmo, pendingUpload)) { g_icons.pendingIconUploads.push_back(std::move(pendingUpload)); } pendingUpload.reset(); if (LoadTextureFromFile( backend, device, commandQueue, ResolvePointLightGizmoIconPath(), g_icons.pointLightGizmo, pendingUpload)) { g_icons.pendingIconUploads.push_back(std::move(pendingUpload)); } pendingUpload.reset(); if (LoadTextureFromFile( backend, device, commandQueue, ResolveSpotLightGizmoIconPath(), g_icons.spotLightGizmo, pendingUpload)) { g_icons.pendingIconUploads.push_back(std::move(pendingUpload)); } } void ShutdownBuiltInIcons() { if (g_icons.backend && g_icons.device && g_icons.commandQueue) { g_icons.backend->WaitForQueueIdle(g_icons.device, g_icons.commandQueue); } StopPreviewWorkers(); ResetAssetPreviewCache(); g_icons.pendingIconUploads.clear(); ResetTexture(g_icons.folder); ResetTexture(g_icons.gameObject); ResetTexture(g_icons.scene); ResetTexture(g_icons.cameraGizmo); ResetTexture(g_icons.directionalLightGizmo); ResetTexture(g_icons.pointLightGizmo); ResetTexture(g_icons.spotLightGizmo); g_icons.backend = nullptr; g_icons.device = nullptr; g_icons.commandQueue = nullptr; g_icons.lastMaintenanceFrame = -1; g_icons.lastPreviewBudgetFrame = -1; g_icons.previewLoadsThisFrame = 0; } void DrawAssetIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, AssetIconKind kind) { MaintainIconRuntimeState(); if (kind == AssetIconKind::Folder) { if (g_icons.folder.IsValid()) { DrawTextureIcon(drawList, g_icons.folder, min, max); return; } DrawBuiltInFolderFallback(drawList, min, max); return; } if (kind == AssetIconKind::GameObject) { if (g_icons.gameObject.IsValid()) { DrawTextureIcon(drawList, g_icons.gameObject, min, max); return; } DrawBuiltInFileIcon(drawList, min, max); return; } if (kind == AssetIconKind::Scene) { if (g_icons.scene.IsValid()) { DrawTextureIcon(drawList, g_icons.scene, min, max); return; } DrawBuiltInFileIcon(drawList, min, max); return; } DrawBuiltInFileIcon(drawList, min, max); } bool DrawEditorTextureIcon( ImDrawList* drawList, const ImVec2& min, const ImVec2& max, EditorTextureIconKind kind) { MaintainIconRuntimeState(); const BuiltInTexture* texture = ResolveEditorTexture(kind); if (texture == nullptr || !texture->IsValid()) { return false; } DrawTextureIcon(drawList, *texture, min, max); return true; } bool DrawTextureAssetPreview( ImDrawList* drawList, const ImVec2& min, const ImVec2& max, const std::string& filePath, const std::string& projectPath) { BuiltInIconState::CachedAssetPreview* preview = GetOrCreateAssetPreview(filePath, projectPath); if (!preview || !preview->texture.IsValid() || preview->hasStaleTexture) { return false; } DrawTextureIcon(drawList, preview->texture, min, max); PruneAssetPreviewCache(); return true; } } // namespace UI } // namespace Editor } // namespace XCEngine