|
|
|
|
@@ -5,6 +5,7 @@
|
|
|
|
|
#include "StyleTokens.h"
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <condition_variable>
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <cwctype>
|
|
|
|
|
@@ -12,6 +13,7 @@
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <limits>
|
|
|
|
|
#include <memory>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <thread>
|
|
|
|
|
@@ -58,14 +60,56 @@ struct PreviewGpuUpload {
|
|
|
|
|
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;
|
|
|
|
|
@@ -77,9 +121,12 @@ struct BuiltInIconState {
|
|
|
|
|
BuiltInTexture texture;
|
|
|
|
|
std::unique_ptr<LoadedTexturePixels> decodedPixels;
|
|
|
|
|
std::unique_ptr<PreviewGpuUpload> pendingUpload;
|
|
|
|
|
SourceFileFingerprint sourceFingerprint;
|
|
|
|
|
bool decodeQueued = false;
|
|
|
|
|
bool hasStaleTexture = false;
|
|
|
|
|
bool loadFailed = false;
|
|
|
|
|
int lastUsedFrame = -1;
|
|
|
|
|
int lastSourceValidationFrame = -1;
|
|
|
|
|
};
|
|
|
|
|
std::unordered_map<std::string, CachedAssetPreview> assetPreviews;
|
|
|
|
|
std::vector<std::unique_ptr<PreviewGpuUpload>> pendingIconUploads;
|
|
|
|
|
@@ -104,6 +151,10 @@ 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<char, 8> 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());
|
|
|
|
|
@@ -120,12 +171,261 @@ std::filesystem::path ResolveSceneIconPath() {
|
|
|
|
|
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::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<std::uintmax_t>(-1)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ec.clear();
|
|
|
|
|
const auto lastWriteTime = std::filesystem::last_write_time(filePath, ec);
|
|
|
|
|
if (ec) {
|
|
|
|
|
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::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<std::uint64_t>(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<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::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<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 == 0 ||
|
|
|
|
|
header.relativePathSize > kMaxThumbnailCacheRelativePathBytes ||
|
|
|
|
|
header.width == 0 ||
|
|
|
|
|
header.height == 0 ||
|
|
|
|
|
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 == 0 ||
|
|
|
|
|
expectedPixelBytes > static_cast<std::uint64_t>((std::numeric_limits<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<size_t>(header.pixelDataSize));
|
|
|
|
|
if (!stream.read(
|
|
|
|
|
reinterpret_cast<char*>(outTexturePixels.rgbaPixels.data()),
|
|
|
|
|
static_cast<std::streamsize>(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<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 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<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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<stbi_uc>& bytes) {
|
|
|
|
|
std::ifstream stream(filePath, std::ios::binary | std::ios::ate);
|
|
|
|
|
if (!stream.is_open()) {
|
|
|
|
|
@@ -226,6 +526,39 @@ bool DownscalePreviewTextureIfNeeded(LoadedTexturePixels& texturePixels) {
|
|
|
|
|
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 && texture.cpuHandle.ptr != 0) {
|
|
|
|
|
g_icons.backend->FreeTextureDescriptor(texture.cpuHandle, texture.gpuHandle);
|
|
|
|
|
@@ -524,7 +857,30 @@ void PreviewWorkerMain() {
|
|
|
|
|
|
|
|
|
|
PreviewDecodeResult result;
|
|
|
|
|
result.key = std::move(job.key);
|
|
|
|
|
result.success = DecodeTextureFromFile(job.filePath, result.pixels);
|
|
|
|
|
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(g_icons.previewQueueMutex);
|
|
|
|
|
@@ -563,13 +919,32 @@ void StopPreviewWorkers() {
|
|
|
|
|
g_icons.previewWorkers.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool QueuePreviewDecode(const std::string& key, const std::filesystem::path& filePath) {
|
|
|
|
|
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<std::mutex> lock(g_icons.previewQueueMutex);
|
|
|
|
|
if (!g_icons.previewWorkersRunning || g_icons.pendingPreviewDecodeJobs >= kMaxQueuedPreviewDecodeJobs) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_icons.previewDecodeQueue.push_back(PreviewDecodeJob{ key, filePath });
|
|
|
|
|
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;
|
|
|
|
|
@@ -594,6 +969,10 @@ void DrainPreviewDecodeResults() {
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
@@ -616,6 +995,10 @@ void PollCompletedUploads() {
|
|
|
|
|
auto& preview = entry.second;
|
|
|
|
|
if (preview.pendingUpload && completedFenceValue >= preview.pendingUpload->fenceValue) {
|
|
|
|
|
preview.pendingUpload.reset();
|
|
|
|
|
if (preview.hasStaleTexture) {
|
|
|
|
|
ResetTexture(preview.texture);
|
|
|
|
|
preview.hasStaleTexture = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -713,36 +1096,43 @@ void DrawBuiltInFileIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2&
|
|
|
|
|
drawList->AddLine(foldA, foldB, lineColor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BuiltInIconState::CachedAssetPreview* GetOrCreateAssetPreview(const std::string& filePath) {
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
if (!inserted && !std::filesystem::exists(std::filesystem::path(Platform::Utf8ToWide(filePath)))) {
|
|
|
|
|
if (!preview.pendingUpload) {
|
|
|
|
|
ResetTexture(preview.texture);
|
|
|
|
|
preview.decodedPixels.reset();
|
|
|
|
|
const int frame = ImGui::GetFrameCount();
|
|
|
|
|
if (inserted ||
|
|
|
|
|
preview.lastSourceValidationFrame < 0 ||
|
|
|
|
|
frame - preview.lastSourceValidationFrame >= kPreviewSourceValidationIntervalFrames) {
|
|
|
|
|
preview.lastSourceValidationFrame = frame;
|
|
|
|
|
if (!RefreshAssetPreviewSourceFingerprint(filePathW, preview)) {
|
|
|
|
|
return &preview;
|
|
|
|
|
}
|
|
|
|
|
preview.loadFailed = true;
|
|
|
|
|
preview.decodeQueued = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!preview.sourceFingerprint.IsValid()) {
|
|
|
|
|
return &preview;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (preview.texture.IsValid()) {
|
|
|
|
|
if (preview.texture.IsValid() && !preview.hasStaleTexture) {
|
|
|
|
|
if (preview.pendingUpload && preview.pendingUpload->fenceValue <= g_icons.uploadFence->GetCompletedValue()) {
|
|
|
|
|
preview.pendingUpload.reset();
|
|
|
|
|
}
|
|
|
|
|
return &preview;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (preview.decodedPixels) {
|
|
|
|
|
if (preview.decodedPixels && !preview.pendingUpload) {
|
|
|
|
|
std::unique_ptr<PreviewGpuUpload> pendingUpload;
|
|
|
|
|
if (!UploadTexturePixels(
|
|
|
|
|
*g_icons.backend,
|
|
|
|
|
@@ -766,7 +1156,6 @@ BuiltInIconState::CachedAssetPreview* GetOrCreateAssetPreview(const std::string&
|
|
|
|
|
return &preview;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const int frame = ImGui::GetFrameCount();
|
|
|
|
|
if (g_icons.lastPreviewBudgetFrame != frame) {
|
|
|
|
|
g_icons.lastPreviewBudgetFrame = frame;
|
|
|
|
|
g_icons.previewLoadsThisFrame = 0;
|
|
|
|
|
@@ -775,7 +1164,7 @@ BuiltInIconState::CachedAssetPreview* GetOrCreateAssetPreview(const std::string&
|
|
|
|
|
return &preview;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (QueuePreviewDecode(key, std::filesystem::path(Platform::Utf8ToWide(filePath)))) {
|
|
|
|
|
if (QueuePreviewDecode(key, filePathW, preview.sourceFingerprint, projectPath, filePath)) {
|
|
|
|
|
preview.decodeQueued = true;
|
|
|
|
|
++g_icons.previewLoadsThisFrame;
|
|
|
|
|
}
|
|
|
|
|
@@ -903,9 +1292,10 @@ bool DrawTextureAssetPreview(
|
|
|
|
|
ImDrawList* drawList,
|
|
|
|
|
const ImVec2& min,
|
|
|
|
|
const ImVec2& max,
|
|
|
|
|
const std::string& filePath) {
|
|
|
|
|
BuiltInIconState::CachedAssetPreview* preview = GetOrCreateAssetPreview(filePath);
|
|
|
|
|
if (!preview || !preview->texture.IsValid()) {
|
|
|
|
|
const std::string& filePath,
|
|
|
|
|
const std::string& projectPath) {
|
|
|
|
|
BuiltInIconState::CachedAssetPreview* preview = GetOrCreateAssetPreview(filePath, projectPath);
|
|
|
|
|
if (!preview || !preview->texture.IsValid() || preview->hasStaleTexture) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|