#include "BuiltInIcons.h" #include "ImGuiBackendBridge.h" #include "Platform/Win32Utf8.h" #include "StyleTokens.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace XCEngine { namespace Editor { namespace UI { namespace { using Microsoft::WRL::ComPtr; struct BuiltInTexture { ImTextureID textureId = {}; D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle = {}; D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle = {}; ComPtr texture; int width = 0; int height = 0; bool IsValid() const { return textureId != ImTextureID{} && texture != nullptr && width > 0 && height > 0; } }; struct LoadedTexturePixels { std::vector rgbaPixels; int width = 0; int height = 0; }; struct PreviewGpuUpload { ComPtr uploadResource; ComPtr commandAllocator; ComPtr commandList; UINT64 fenceValue = 0; }; struct PreviewDecodeJob { std::string key; std::filesystem::path filePath; }; struct PreviewDecodeResult { std::string key; LoadedTexturePixels pixels; bool success = false; }; struct BuiltInIconState { ImGuiBackendBridge* backend = nullptr; ID3D12Device* device = nullptr; ID3D12CommandQueue* commandQueue = nullptr; BuiltInTexture folder; BuiltInTexture gameObject; BuiltInTexture scene; struct CachedAssetPreview { BuiltInTexture texture; std::unique_ptr decodedPixels; std::unique_ptr pendingUpload; bool decodeQueued = false; bool loadFailed = false; int lastUsedFrame = -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; ComPtr uploadFence; UINT64 nextUploadFenceValue = 1; 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; 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::string MakeAssetPreviewKey(const std::string& filePath) { std::wstring key = std::filesystem::path(Platform::Utf8ToWide(filePath)).lexically_normal().generic_wstring(); std::transform(key.begin(), key.end(), key.begin(), ::towlower); return Platform::WideToUtf8(key); } 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) { if (g_icons.backend && texture.cpuHandle.ptr != 0) { g_icons.backend->FreeTextureDescriptor(texture.cpuHandle, texture.gpuHandle); } texture.texture.Reset(); texture.textureId = {}; texture.cpuHandle = {}; texture.gpuHandle = {}; texture.width = 0; texture.height = 0; } void ResetAssetPreviewCache() { for (auto& entry : g_icons.assetPreviews) { ResetTexture(entry.second.texture); } g_icons.assetPreviews.clear(); } bool EnsureUploadFence(ID3D12Device* device) { if (g_icons.uploadFence) { return true; } return SUCCEEDED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&g_icons.uploadFence))); } bool WaitForQueueIdle(ID3D12Device* device, ID3D12CommandQueue* commandQueue) { if (!device || !commandQueue) { return false; } ComPtr fence; if (FAILED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)))) { return false; } HANDLE eventHandle = CreateEventW(nullptr, FALSE, FALSE, nullptr); if (!eventHandle) { return false; } constexpr UINT64 kFenceValue = 1; const HRESULT signalHr = commandQueue->Signal(fence.Get(), kFenceValue); if (FAILED(signalHr)) { CloseHandle(eventHandle); return false; } if (fence->GetCompletedValue() < kFenceValue) { if (FAILED(fence->SetEventOnCompletion(kFenceValue, eventHandle))) { CloseHandle(eventHandle); return false; } WaitForSingleObject(eventHandle, INFINITE); } CloseHandle(eventHandle); return true; } 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 UploadTexturePixels( ImGuiBackendBridge& backend, ID3D12Device* device, ID3D12CommandQueue* commandQueue, const LoadedTexturePixels& texturePixels, BuiltInTexture& outTexture, std::unique_ptr& outPendingUpload) { if (!device || !commandQueue || texturePixels.width <= 0 || texturePixels.height <= 0 || texturePixels.rgbaPixels.empty()) { return false; } const UINT srcRowPitch = static_cast(texturePixels.width * 4); D3D12_RESOURCE_DESC textureDesc = {}; textureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; textureDesc.Alignment = 0; textureDesc.Width = static_cast(texturePixels.width); textureDesc.Height = static_cast(texturePixels.height); textureDesc.DepthOrArraySize = 1; textureDesc.MipLevels = 1; textureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; textureDesc.SampleDesc.Count = 1; textureDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; D3D12_HEAP_PROPERTIES defaultHeap = {}; defaultHeap.Type = D3D12_HEAP_TYPE_DEFAULT; ComPtr textureResource; if (FAILED(device->CreateCommittedResource( &defaultHeap, D3D12_HEAP_FLAG_NONE, &textureDesc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&textureResource)))) { return false; } D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint = {}; UINT numRows = 0; UINT64 rowSizeInBytes = 0; UINT64 uploadBufferSize = 0; device->GetCopyableFootprints(&textureDesc, 0, 1, 0, &footprint, &numRows, &rowSizeInBytes, &uploadBufferSize); D3D12_RESOURCE_DESC uploadDesc = {}; uploadDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; uploadDesc.Width = uploadBufferSize; uploadDesc.Height = 1; uploadDesc.DepthOrArraySize = 1; uploadDesc.MipLevels = 1; uploadDesc.SampleDesc.Count = 1; uploadDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; D3D12_HEAP_PROPERTIES uploadHeap = {}; uploadHeap.Type = D3D12_HEAP_TYPE_UPLOAD; ComPtr uploadResource; if (FAILED(device->CreateCommittedResource( &uploadHeap, D3D12_HEAP_FLAG_NONE, &uploadDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&uploadResource)))) { return false; } std::uint8_t* mappedData = nullptr; if (FAILED(uploadResource->Map(0, nullptr, reinterpret_cast(&mappedData)))) { return false; } for (UINT row = 0; row < numRows; ++row) { std::memcpy( mappedData + footprint.Offset + static_cast(row) * footprint.Footprint.RowPitch, texturePixels.rgbaPixels.data() + static_cast(row) * srcRowPitch, srcRowPitch); } uploadResource->Unmap(0, nullptr); ComPtr commandAllocator; ComPtr commandList; if (FAILED(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)))) { return false; } if (FAILED(device->CreateCommandList( 0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator.Get(), nullptr, IID_PPV_ARGS(&commandList)))) { return false; } D3D12_TEXTURE_COPY_LOCATION dst = {}; dst.pResource = textureResource.Get(); dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; dst.SubresourceIndex = 0; D3D12_TEXTURE_COPY_LOCATION src = {}; src.pResource = uploadResource.Get(); src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; src.PlacedFootprint = footprint; commandList->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr); D3D12_RESOURCE_BARRIER barrier = {}; barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; barrier.Transition.pResource = textureResource.Get(); barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; commandList->ResourceBarrier(1, &barrier); if (FAILED(commandList->Close())) { return false; } if (!EnsureUploadFence(device)) { return false; } ResetTexture(outTexture); backend.AllocateTextureDescriptor(&outTexture.cpuHandle, &outTexture.gpuHandle); if (outTexture.cpuHandle.ptr == 0 || outTexture.gpuHandle.ptr == 0) { ResetTexture(outTexture); return false; } D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; srvDesc.Texture2D.MipLevels = 1; device->CreateShaderResourceView(textureResource.Get(), &srvDesc, outTexture.cpuHandle); outTexture.texture = textureResource; outTexture.textureId = (ImTextureID)(static_cast(outTexture.gpuHandle.ptr)); outTexture.width = texturePixels.width; outTexture.height = texturePixels.height; ID3D12CommandList* commandLists[] = { commandList.Get() }; commandQueue->ExecuteCommandLists(1, commandLists); const UINT64 fenceValue = g_icons.nextUploadFenceValue++; if (FAILED(commandQueue->Signal(g_icons.uploadFence.Get(), fenceValue))) { ResetTexture(outTexture); return false; } outPendingUpload = std::make_unique(); outPendingUpload->uploadResource = std::move(uploadResource); outPendingUpload->commandAllocator = std::move(commandAllocator); outPendingUpload->commandList = std::move(commandList); outPendingUpload->fenceValue = fenceValue; return true; } 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 UploadTexturePixels( backend, device, commandQueue, texturePixels, 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.success = DecodeTextureFromFile(job.filePath, 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) { std::lock_guard lock(g_icons.previewQueueMutex); if (!g_icons.previewWorkersRunning || g_icons.pendingPreviewDecodeJobs >= kMaxQueuedPreviewDecodeJobs) { return false; } g_icons.previewDecodeQueue.push_back(PreviewDecodeJob{ key, filePath }); ++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 (!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.uploadFence) { return; } const UINT64 completedFenceValue = g_icons.uploadFence->GetCompletedValue(); for (auto& entry : g_icons.assetPreviews) { auto& preview = entry.second; if (preview.pendingUpload && completedFenceValue >= preview.pendingUpload->fenceValue) { preview.pendingUpload.reset(); } } auto eraseIt = std::remove_if( g_icons.pendingIconUploads.begin(), g_icons.pendingIconUploads.end(), [completedFenceValue](const std::unique_ptr& upload) { return upload && completedFenceValue >= upload->fenceValue; }); g_icons.pendingIconUploads.erase(eraseIt, g_icons.pendingIconUploads.end()); } void MaintainIconRuntimeState() { const int frame = ImGui::GetFrameCount(); if (g_icons.lastMaintenanceFrame == frame) { return; } g_icons.lastMaintenanceFrame = frame; DrainPreviewDecodeResults(); PollCompletedUploads(); } ImVec2 ComputeFittedIconSize(const BuiltInTexture& texture, const ImVec2& min, const ImVec2& max) { const float availableWidth = max.x - min.x; const float availableHeight = max.y - min.y; if (availableWidth <= 0.0f || availableHeight <= 0.0f || texture.width <= 0 || texture.height <= 0) { return ImVec2(0.0f, 0.0f); } const float scale = (std::min)( availableWidth / static_cast(texture.width), availableHeight / static_cast(texture.height)); return ImVec2( static_cast(texture.width) * scale, static_cast(texture.height) * scale); } void DrawTextureIcon(ImDrawList* drawList, const BuiltInTexture& texture, const ImVec2& min, const ImVec2& max) { if (!drawList || !texture.IsValid()) { return; } const ImVec2 size = ComputeFittedIconSize(texture, 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) { if (!g_icons.backend || !g_icons.device || !g_icons.commandQueue || filePath.empty()) { return nullptr; } MaintainIconRuntimeState(); const std::string key = MakeAssetPreviewKey(filePath); auto [it, inserted] = g_icons.assetPreviews.try_emplace(key); BuiltInIconState::CachedAssetPreview& preview = it->second; preview.lastUsedFrame = ImGui::GetFrameCount(); if (!inserted && !std::filesystem::exists(std::filesystem::path(Platform::Utf8ToWide(filePath)))) { if (!preview.pendingUpload) { ResetTexture(preview.texture); preview.decodedPixels.reset(); } preview.loadFailed = true; preview.decodeQueued = false; return &preview; } if (preview.texture.IsValid()) { if (preview.pendingUpload && preview.pendingUpload->fenceValue <= g_icons.uploadFence->GetCompletedValue()) { preview.pendingUpload.reset(); } return &preview; } if (preview.decodedPixels) { std::unique_ptr pendingUpload; if (!UploadTexturePixels( *g_icons.backend, g_icons.device, g_icons.commandQueue, *preview.decodedPixels, 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; } const int frame = ImGui::GetFrameCount(); if (g_icons.lastPreviewBudgetFrame != frame) { g_icons.lastPreviewBudgetFrame = frame; g_icons.previewLoadsThisFrame = 0; } if (g_icons.previewLoadsThisFrame >= kMaxPreviewLoadsPerFrame) { return &preview; } if (QueuePreviewDecode(key, std::filesystem::path(Platform::Utf8ToWide(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); } } } // 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)); } } void ShutdownBuiltInIcons() { if (g_icons.device && g_icons.commandQueue) { 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); g_icons.backend = nullptr; g_icons.device = nullptr; g_icons.commandQueue = nullptr; g_icons.uploadFence.Reset(); g_icons.nextUploadFenceValue = 1; 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 DrawTextureAssetPreview( ImDrawList* drawList, const ImVec2& min, const ImVec2& max, const std::string& filePath) { BuiltInIconState::CachedAssetPreview* preview = GetOrCreateAssetPreview(filePath); if (!preview || !preview->texture.IsValid()) { return false; } DrawTextureIcon(drawList, preview->texture, min, max); PruneAssetPreviewCache(); return true; } } // namespace UI } // namespace Editor } // namespace XCEngine