#include "BuiltInIcons.h" #include "Bootstrap/EditorResources.h" #include "Ports/TexturePort.h" #include "Support/EmbeddedPngLoader.h" #include "Support/StringEncoding.h" #include #include #include #include #include #include #include #include #include namespace XCEngine::UI::Editor::App { namespace { struct PreviewDiskCacheEntry { std::filesystem::path filePath = {}; std::string relativePathKey = {}; }; #pragma pack(push, 1) struct ThumbnailCacheFileHeader { char magic[8] = {}; std::uint32_t version = 1u; std::uint64_t fileSize = 0u; std::int64_t lastWriteTimeTicks = 0; std::uint32_t width = 0u; std::uint32_t height = 0u; std::uint32_t relativePathSize = 0u; std::uint32_t pixelDataSize = 0u; }; #pragma pack(pop) constexpr std::size_t kMaxCachedAssetPreviews = 40u; constexpr int kMaxPreviewLoadsPerFrame = 64; constexpr std::size_t kMaxQueuedPreviewDecodeJobs = 64u; constexpr int kMaxPreviewThumbnailExtent = 192; constexpr std::size_t kPreviewWorkerCount = 2u; constexpr int kPreviewSourceValidationIntervalFrames = 30; constexpr std::uint32_t kThumbnailCacheVersion = 1u; constexpr std::array kThumbnailCacheMagic = { 'X', 'C', 'T', 'H', 'M', 'B', '1', '\0' }; constexpr std::uint32_t kMaxThumbnailCacheRelativePathBytes = 16u * 1024u; void AppendLoadError( std::ostringstream& stream, std::string_view label, const std::string& error) { if (error.empty()) { return; } if (stream.tellp() > 0) { stream << '\n'; } stream << label << ": " << error; } void LoadEmbeddedIconTexture( Ports::TexturePort& renderer, UINT resourceId, std::string_view label, ::XCEngine::UI::UITextureHandle& outTexture, std::ostringstream& errorStream) { std::string error = {}; if (!LoadEmbeddedPngTexture(renderer, resourceId, outTexture, error)) { AppendLoadError(errorStream, label, error); } } BuiltInIcons::SourceFileFingerprint MakeSourceFingerprint( std::uint64_t fileSize, std::int64_t lastWriteTimeTicks) { BuiltInIcons::SourceFileFingerprint fingerprint = {}; fingerprint.fileSize = fileSize; fingerprint.lastWriteTimeTicks = lastWriteTimeTicks; return fingerprint; } bool QuerySourceFileFingerprint( const std::filesystem::path& filePath, BuiltInIcons::SourceFileFingerprint& outFingerprint) { std::error_code errorCode = {}; if (!std::filesystem::exists(filePath, errorCode) || errorCode) { return false; } errorCode.clear(); if (!std::filesystem::is_regular_file(filePath, errorCode) || errorCode) { return false; } errorCode.clear(); const auto fileSize = std::filesystem::file_size(filePath, errorCode); if (errorCode || fileSize == static_cast(-1)) { return false; } errorCode.clear(); const auto lastWriteTime = std::filesystem::last_write_time(filePath, errorCode); if (errorCode) { return false; } outFingerprint.fileSize = static_cast(fileSize); outFingerprint.lastWriteTimeTicks = static_cast(lastWriteTime.time_since_epoch().count()); return outFingerprint.IsValid(); } 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 WideToUtf8(key); } 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 character : value) { hash ^= static_cast(character); hash *= kPrime; } return hash; } std::string FormatHashHex(std::uint64_t value) { static constexpr char kHexDigits[] = "0123456789abcdef"; std::string result(16u, '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::filesystem::path& projectRoot, const std::filesystem::path& filePath, PreviewDiskCacheEntry& outEntry) { if (projectRoot.empty() || filePath.empty()) { return false; } std::error_code errorCode = {}; const std::filesystem::path canonicalRoot = std::filesystem::weakly_canonical(projectRoot, errorCode); if (errorCode || canonicalRoot.empty()) { return false; } errorCode.clear(); const std::filesystem::path canonicalAsset = std::filesystem::weakly_canonical(filePath, errorCode); if (errorCode || canonicalAsset.empty()) { return false; } errorCode.clear(); std::filesystem::path relativePath = std::filesystem::relative(canonicalAsset, canonicalRoot, errorCode); if (errorCode) { 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 = (canonicalRoot / L".xceditor" / L"thumbs" / std::filesystem::path(Utf8ToWide(hashFileName))).lexically_normal(); return true; } bool ReadThumbnailCacheFile( const std::filesystem::path& cacheFilePath, const std::string& relativePathKey, const BuiltInIcons::SourceFileFingerprint& sourceFingerprint, BuiltInIcons::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 == 0u || header.relativePathSize > kMaxThumbnailCacheRelativePathBytes || header.width == 0u || header.height == 0u || 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 == 0u || 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 = {}; return false; } return true; } void RemoveDiskCacheFile(const std::filesystem::path& cacheFilePath) { std::error_code errorCode = {}; std::filesystem::remove(cacheFilePath, errorCode); } bool WriteThumbnailCacheFile( const std::filesystem::path& cacheFilePath, const std::string& relativePathKey, const BuiltInIcons::SourceFileFingerprint& sourceFingerprint, const BuiltInIcons::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 errorCode = {}; std::filesystem::create_directories(cacheFilePath.parent_path(), errorCode); if (errorCode) { 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; } errorCode.clear(); std::filesystem::remove(cacheFilePath, errorCode); errorCode.clear(); std::filesystem::rename(tempPath, cacheFilePath, errorCode); if (errorCode) { 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 std::uint8_t* 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 std::size_t dstOffset = (static_cast(dstY) * static_cast(dstWidth) + static_cast(dstX)) * 4u; const std::size_t srcOffset00 = (static_cast(y0) * static_cast(srcWidth) + static_cast(x0)) * 4u; const std::size_t srcOffset10 = (static_cast(y0) * static_cast(srcWidth) + static_cast(x1)) * 4u; const std::size_t srcOffset01 = (static_cast(y1) * static_cast(srcWidth) + static_cast(x0)) * 4u; const std::size_t srcOffset11 = (static_cast(y1) * static_cast(srcWidth) + static_cast(x1)) * 4u; for (std::size_t channel = 0u; 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(BuiltInIcons::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; } bool DecodeTextureFromFile( const std::filesystem::path& filePath, BuiltInIcons::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( reinterpret_cast(fileData.data()), static_cast(fileData.size()), &width, &height, &channels, STBI_rgb_alpha); if (!pixels || width <= 0 || height <= 0) { if (pixels != nullptr) { 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); } } // namespace void BuiltInIcons::Initialize(Ports::TexturePort& renderer) { Shutdown(); m_renderer = &renderer; StartPreviewWorkers(); std::ostringstream errorStream = {}; LoadEmbeddedIconTexture( renderer, IDR_PNG_FOLDER_ICON, "folder_icon.png", m_folderIcon, errorStream); LoadEmbeddedIconTexture( renderer, IDR_PNG_GAMEOBJECT_ICON, "gameobject_icon.png", m_gameObjectIcon, errorStream); LoadEmbeddedIconTexture( renderer, IDR_PNG_SCENE_ICON, "scene_icon.png", m_sceneIcon, errorStream); LoadEmbeddedIconTexture( renderer, IDR_PNG_CAMERA_GIZMO_ICON, "camera_gizmo.png", m_cameraGizmoIcon, errorStream); LoadEmbeddedIconTexture( renderer, IDR_PNG_DIRECTIONAL_LIGHT_GIZMO_ICON, "directional_light_gizmo.png", m_directionalLightGizmoIcon, errorStream); LoadEmbeddedIconTexture( renderer, IDR_PNG_POINT_LIGHT_GIZMO_ICON, "point_light_gizmo.png", m_pointLightGizmoIcon, errorStream); LoadEmbeddedIconTexture( renderer, IDR_PNG_SPOT_LIGHT_GIZMO_ICON, "spot_light_gizmo.png", m_spotLightGizmoIcon, errorStream); LoadEmbeddedIconTexture( renderer, IDR_PNG_PLAY_BUTTON_ICON, "play_button.png", m_playButtonIcon, errorStream); LoadEmbeddedIconTexture( renderer, IDR_PNG_PAUSE_BUTTON_ICON, "pause_button.png", m_pauseButtonIcon, errorStream); LoadEmbeddedIconTexture( renderer, IDR_PNG_STEP_BUTTON_ICON, "step_button.png", m_stepButtonIcon, errorStream); m_frameIndex = 0; m_previewLoadsThisFrame = 0; m_lastError = errorStream.str(); } void BuiltInIcons::Shutdown() { StopPreviewWorkers(); ResetAssetPreviewCache(); if (m_renderer != nullptr) { m_renderer->ReleaseTexture(m_folderIcon); m_renderer->ReleaseTexture(m_gameObjectIcon); m_renderer->ReleaseTexture(m_sceneIcon); m_renderer->ReleaseTexture(m_cameraGizmoIcon); m_renderer->ReleaseTexture(m_directionalLightGizmoIcon); m_renderer->ReleaseTexture(m_pointLightGizmoIcon); m_renderer->ReleaseTexture(m_spotLightGizmoIcon); m_renderer->ReleaseTexture(m_playButtonIcon); m_renderer->ReleaseTexture(m_pauseButtonIcon); m_renderer->ReleaseTexture(m_stepButtonIcon); } m_renderer = nullptr; m_frameIndex = 0; m_previewLoadsThisFrame = 0; m_lastError.clear(); } void BuiltInIcons::BeginFrame() { ++m_frameIndex; m_previewLoadsThisFrame = 0; MaintainPreviewRuntimeState(); PruneAssetPreviewCache(); } void BuiltInIcons::ResetAssetPreviewCache() { if (m_renderer != nullptr) { for (auto& entry : m_assetPreviews) { m_renderer->ReleaseTexture(entry.second.texture); } } m_assetPreviews.clear(); } void BuiltInIcons::StartPreviewWorkers() { if (m_previewWorkersRunning) { return; } m_previewWorkersRunning = true; m_previewWorkers.reserve(kPreviewWorkerCount); for (std::size_t workerIndex = 0u; workerIndex < kPreviewWorkerCount; ++workerIndex) { m_previewWorkers.emplace_back([this]() { PreviewWorkerMain(); }); } } void BuiltInIcons::StopPreviewWorkers() { { std::lock_guard lock(m_previewQueueMutex); m_previewWorkersRunning = false; m_previewDecodeQueue.clear(); m_previewDecodeResults.clear(); m_pendingPreviewDecodeJobs = 0u; } m_previewQueueEvent.notify_all(); for (std::thread& worker : m_previewWorkers) { if (worker.joinable()) { worker.join(); } } m_previewWorkers.clear(); } void BuiltInIcons::PreviewWorkerMain() { while (true) { PreviewDecodeJob job = {}; { std::unique_lock lock(m_previewQueueMutex); m_previewQueueEvent.wait(lock, [this]() { return !m_previewWorkersRunning || !m_previewDecodeQueue.empty(); }); if (!m_previewWorkersRunning && m_previewDecodeQueue.empty()) { return; } job = std::move(m_previewDecodeQueue.front()); m_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(m_previewQueueMutex); m_previewDecodeResults.push_back(std::move(result)); } } } void BuiltInIcons::DrainPreviewDecodeResults() { std::deque completedResults = {}; { std::lock_guard lock(m_previewQueueMutex); completedResults.swap(m_previewDecodeResults); } for (PreviewDecodeResult& result : completedResults) { if (m_pendingPreviewDecodeJobs > 0u) { --m_pendingPreviewDecodeJobs; } auto iterator = m_assetPreviews.find(result.key); if (iterator == m_assetPreviews.end()) { continue; } CachedAssetPreview& preview = iterator->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 BuiltInIcons::MaintainPreviewRuntimeState() { DrainPreviewDecodeResults(); } void BuiltInIcons::InvalidateAssetPreview(CachedAssetPreview& preview) { if (m_renderer != nullptr) { m_renderer->ReleaseTexture(preview.texture); } else { preview.texture = {}; } preview.decodedPixels.reset(); preview.decodeQueued = false; preview.loadFailed = false; } bool BuiltInIcons::RefreshAssetPreviewSourceFingerprint( const std::filesystem::path& filePath, 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; } bool BuiltInIcons::QueuePreviewDecode( const std::string& key, const std::filesystem::path& filePath, const SourceFileFingerprint& sourceFingerprint, const std::filesystem::path& projectRoot) { PreviewDiskCacheEntry diskCacheEntry = {}; const bool useDiskCache = ResolvePreviewDiskCacheEntry(projectRoot, filePath, diskCacheEntry); std::lock_guard lock(m_previewQueueMutex); if (!m_previewWorkersRunning || m_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; } m_previewDecodeQueue.push_back(std::move(job)); ++m_pendingPreviewDecodeJobs; m_previewQueueEvent.notify_one(); return true; } BuiltInIcons::CachedAssetPreview* BuiltInIcons::GetOrCreateAssetPreview( const std::filesystem::path& assetPath, const std::filesystem::path& projectRoot) { if (m_renderer == nullptr || assetPath.empty()) { return nullptr; } if (m_frameIndex <= 0) { BeginFrame(); } else { MaintainPreviewRuntimeState(); } const std::filesystem::path normalizedAssetPath = assetPath.lexically_normal(); const std::string key = NormalizePathKey(normalizedAssetPath); auto [iterator, inserted] = m_assetPreviews.try_emplace(key); CachedAssetPreview& preview = iterator->second; preview.lastUsedFrame = m_frameIndex; if (inserted || preview.lastSourceValidationFrame < 0 || m_frameIndex - preview.lastSourceValidationFrame >= kPreviewSourceValidationIntervalFrames) { preview.lastSourceValidationFrame = m_frameIndex; if (!RefreshAssetPreviewSourceFingerprint(normalizedAssetPath, preview)) { return &preview; } } if (!preview.sourceFingerprint.IsValid()) { return &preview; } if (preview.texture.IsValid()) { return &preview; } if (preview.decodedPixels) { std::string error = {}; if (!m_renderer->LoadTextureFromRgba( preview.decodedPixels->rgbaPixels.data(), static_cast(preview.decodedPixels->width), static_cast(preview.decodedPixels->height), preview.texture, error)) { preview.decodedPixels.reset(); preview.loadFailed = true; return &preview; } preview.decodedPixels.reset(); preview.loadFailed = false; return &preview; } if (preview.decodeQueued || preview.loadFailed) { return &preview; } if (m_previewLoadsThisFrame >= kMaxPreviewLoadsPerFrame) { return &preview; } if (QueuePreviewDecode(key, normalizedAssetPath, preview.sourceFingerprint, projectRoot)) { preview.decodeQueued = true; ++m_previewLoadsThisFrame; } return &preview; } void BuiltInIcons::PruneAssetPreviewCache() { if (m_assetPreviews.size() <= kMaxCachedAssetPreviews) { return; } std::vector> candidates = {}; candidates.reserve(m_assetPreviews.size()); for (const auto& entry : m_assetPreviews) { if (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; }); const std::size_t removeCount = m_assetPreviews.size() - kMaxCachedAssetPreviews; for (std::size_t index = 0u; index < removeCount && index < candidates.size(); ++index) { auto iterator = m_assetPreviews.find(candidates[index].first); if (iterator == m_assetPreviews.end()) { continue; } if (m_renderer != nullptr) { m_renderer->ReleaseTexture(iterator->second.texture); } m_assetPreviews.erase(iterator); } } const ::XCEngine::UI::UITextureHandle& BuiltInIcons::Resolve( BuiltInIconKind kind) const { switch (kind) { case BuiltInIconKind::Folder: return m_folderIcon; case BuiltInIconKind::GameObject: return m_gameObjectIcon; case BuiltInIconKind::Scene: return m_sceneIcon; case BuiltInIconKind::CameraGizmo: return m_cameraGizmoIcon; case BuiltInIconKind::DirectionalLightGizmo: return m_directionalLightGizmoIcon; case BuiltInIconKind::PointLightGizmo: return m_pointLightGizmoIcon; case BuiltInIconKind::SpotLightGizmo: return m_spotLightGizmoIcon; case BuiltInIconKind::PlayButton: return m_playButtonIcon; case BuiltInIconKind::PauseButton: return m_pauseButtonIcon; case BuiltInIconKind::StepButton: return m_stepButtonIcon; default: return m_folderIcon; } } const ::XCEngine::UI::UITextureHandle* BuiltInIcons::ResolveAssetPreview( const std::filesystem::path& assetPath, const std::filesystem::path& projectRoot) { CachedAssetPreview* preview = GetOrCreateAssetPreview(assetPath, projectRoot); if (preview == nullptr || !preview->texture.IsValid()) { return nullptr; } PruneAssetPreviewCache(); return &preview->texture; } const std::string& BuiltInIcons::GetLastError() const { return m_lastError; } } // namespace XCEngine::UI::Editor::App