关键节点
This commit is contained in:
945
editor/app/Rendering/Assets/BuiltInIcons.cpp
Normal file
945
editor/app/Rendering/Assets/BuiltInIcons.cpp
Normal file
@@ -0,0 +1,945 @@
|
||||
#include "BuiltInIcons.h"
|
||||
|
||||
#include "Bootstrap/EditorResources.h"
|
||||
#include "Rendering/Host/UiTextureHost.h"
|
||||
#include "Support/EmbeddedPngLoader.h"
|
||||
#include "Support/StringEncoding.h"
|
||||
|
||||
#include <stb_image.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
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<char, 8> 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(
|
||||
Rendering::Host::UiTextureHost& 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<std::uintmax_t>(-1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
errorCode.clear();
|
||||
const auto lastWriteTime = std::filesystem::last_write_time(filePath, errorCode);
|
||||
if (errorCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outFingerprint.fileSize = static_cast<std::uint64_t>(fileSize);
|
||||
outFingerprint.lastWriteTimeTicks =
|
||||
static_cast<std::int64_t>(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<std::uint64_t>(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<std::size_t>(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<char*>(&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<std::uint32_t>(kMaxPreviewThumbnailExtent) ||
|
||||
header.height > static_cast<std::uint32_t>(kMaxPreviewThumbnailExtent)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::uint64_t expectedPixelBytes =
|
||||
static_cast<std::uint64_t>(header.width) * static_cast<std::uint64_t>(header.height) * 4ull;
|
||||
if (expectedPixelBytes == 0u ||
|
||||
expectedPixelBytes > static_cast<std::uint64_t>((std::numeric_limits<std::size_t>::max)()) ||
|
||||
header.pixelDataSize != expectedPixelBytes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string cachedRelativePath(header.relativePathSize, '\0');
|
||||
if (!stream.read(
|
||||
cachedRelativePath.data(),
|
||||
static_cast<std::streamsize>(cachedRelativePath.size())).good()) {
|
||||
return false;
|
||||
}
|
||||
if (cachedRelativePath != relativePathKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outTexturePixels.width = static_cast<int>(header.width);
|
||||
outTexturePixels.height = static_cast<int>(header.height);
|
||||
outTexturePixels.rgbaPixels.resize(static_cast<std::size_t>(header.pixelDataSize));
|
||||
if (!stream.read(
|
||||
reinterpret_cast<char*>(outTexturePixels.rgbaPixels.data()),
|
||||
static_cast<std::streamsize>(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<std::uint64_t>(texturePixels.rgbaPixels.size());
|
||||
if (pixelDataSize !=
|
||||
static_cast<std::uint64_t>(texturePixels.width) *
|
||||
static_cast<std::uint64_t>(texturePixels.height) * 4ull ||
|
||||
pixelDataSize >
|
||||
static_cast<std::uint64_t>((std::numeric_limits<std::uint32_t>::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<std::uint32_t>(texturePixels.width);
|
||||
header.height = static_cast<std::uint32_t>(texturePixels.height);
|
||||
header.relativePathSize = static_cast<std::uint32_t>(relativePathKey.size());
|
||||
header.pixelDataSize = static_cast<std::uint32_t>(pixelDataSize);
|
||||
|
||||
const bool writeSucceeded =
|
||||
stream.write(reinterpret_cast<const char*>(&header), sizeof(header)).good() &&
|
||||
stream.write(
|
||||
relativePathKey.data(),
|
||||
static_cast<std::streamsize>(relativePathKey.size())).good() &&
|
||||
stream.write(
|
||||
reinterpret_cast<const char*>(texturePixels.rgbaPixels.data()),
|
||||
static_cast<std::streamsize>(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<std::uint8_t>& 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<std::size_t>(size));
|
||||
stream.seekg(0, std::ios::beg);
|
||||
return stream.read(reinterpret_cast<char*>(bytes.data()), size).good();
|
||||
}
|
||||
|
||||
void ResizeRgbaImageBilinear(
|
||||
const std::uint8_t* srcPixels,
|
||||
int srcWidth,
|
||||
int srcHeight,
|
||||
int dstWidth,
|
||||
int dstHeight,
|
||||
std::vector<std::uint8_t>& dstPixels) {
|
||||
dstPixels.resize(
|
||||
static_cast<std::size_t>(dstWidth) * static_cast<std::size_t>(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<float>(dstY) * static_cast<float>(srcHeight - 1) /
|
||||
static_cast<float>(dstHeight - 1)
|
||||
: 0.0f;
|
||||
const int y0 = static_cast<int>(srcY);
|
||||
const int y1 = (std::min)(y0 + 1, srcHeight - 1);
|
||||
const float ty = srcY - static_cast<float>(y0);
|
||||
|
||||
for (int dstX = 0; dstX < dstWidth; ++dstX) {
|
||||
const float srcX = dstWidth > 1
|
||||
? static_cast<float>(dstX) * static_cast<float>(srcWidth - 1) /
|
||||
static_cast<float>(dstWidth - 1)
|
||||
: 0.0f;
|
||||
const int x0 = static_cast<int>(srcX);
|
||||
const int x1 = (std::min)(x0 + 1, srcWidth - 1);
|
||||
const float tx = srcX - static_cast<float>(x0);
|
||||
|
||||
const std::size_t dstOffset =
|
||||
(static_cast<std::size_t>(dstY) * static_cast<std::size_t>(dstWidth) +
|
||||
static_cast<std::size_t>(dstX)) * 4u;
|
||||
const std::size_t srcOffset00 =
|
||||
(static_cast<std::size_t>(y0) * static_cast<std::size_t>(srcWidth) +
|
||||
static_cast<std::size_t>(x0)) * 4u;
|
||||
const std::size_t srcOffset10 =
|
||||
(static_cast<std::size_t>(y0) * static_cast<std::size_t>(srcWidth) +
|
||||
static_cast<std::size_t>(x1)) * 4u;
|
||||
const std::size_t srcOffset01 =
|
||||
(static_cast<std::size_t>(y1) * static_cast<std::size_t>(srcWidth) +
|
||||
static_cast<std::size_t>(x0)) * 4u;
|
||||
const std::size_t srcOffset11 =
|
||||
(static_cast<std::size_t>(y1) * static_cast<std::size_t>(srcWidth) +
|
||||
static_cast<std::size_t>(x1)) * 4u;
|
||||
|
||||
for (std::size_t channel = 0u; channel < 4u; ++channel) {
|
||||
const float top =
|
||||
static_cast<float>(srcPixels[srcOffset00 + channel]) * (1.0f - tx) +
|
||||
static_cast<float>(srcPixels[srcOffset10 + channel]) * tx;
|
||||
const float bottom =
|
||||
static_cast<float>(srcPixels[srcOffset01 + channel]) * (1.0f - tx) +
|
||||
static_cast<float>(srcPixels[srcOffset11 + channel]) * tx;
|
||||
dstPixels[dstOffset + channel] =
|
||||
static_cast<std::uint8_t>(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<float>(kMaxPreviewThumbnailExtent) / static_cast<float>(maxExtent);
|
||||
const int dstWidth =
|
||||
(std::max)(1, static_cast<int>(static_cast<float>(texturePixels.width) * scale + 0.5f));
|
||||
const int dstHeight =
|
||||
(std::max)(1, static_cast<int>(static_cast<float>(texturePixels.height) * scale + 0.5f));
|
||||
|
||||
std::vector<std::uint8_t> 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<std::uint8_t> 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<const stbi_uc*>(fileData.data()),
|
||||
static_cast<int>(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<std::size_t>(width) * static_cast<std::size_t>(height) * 4u);
|
||||
stbi_image_free(pixels);
|
||||
|
||||
return DownscalePreviewTextureIfNeeded(outTexturePixels);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void BuiltInIcons::Initialize(Rendering::Host::UiTextureHost& 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(m_previewQueueMutex);
|
||||
m_previewDecodeResults.push_back(std::move(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BuiltInIcons::DrainPreviewDecodeResults() {
|
||||
std::deque<PreviewDecodeResult> completedResults = {};
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<LoadedTexturePixels>(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<std::mutex> 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<std::uint32_t>(preview.decodedPixels->width),
|
||||
static_cast<std::uint32_t>(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<std::pair<std::string, std::int64_t>> 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
|
||||
Reference in New Issue
Block a user