Files
XCEngine/engine/src/Core/Asset/AssetDatabase.cpp

1789 lines
64 KiB
C++

#include <XCEngine/Core/Asset/AssetDatabase.h>
#include <XCEngine/Core/Asset/ArtifactFormats.h>
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Resources/Material/MaterialLoader.h>
#include <XCEngine/Resources/Mesh/MeshLoader.h>
#include <XCEngine/Resources/Shader/ShaderLoader.h>
#include <XCEngine/Resources/Texture/TextureLoader.h>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace XCEngine {
namespace Resources {
namespace fs = std::filesystem;
namespace {
std::string ToStdString(const Containers::String& value) {
return std::string(value.CStr());
}
bool ShouldTraceAssetPath(const Containers::String& path) {
const std::string text = ToStdString(path);
return text.rfind("builtin://", 0) == 0 ||
text.find("backpack") != std::string::npos ||
text.find("New Material.mat") != std::string::npos;
}
bool HasVirtualPathScheme(const Containers::String& value) {
return ToStdString(value).find("://") != std::string::npos;
}
Containers::String ToContainersString(const std::string& value) {
return Containers::String(value.c_str());
}
Containers::String NormalizeArtifactPathString(const Containers::String& path) {
if (path.Empty()) {
return Containers::String();
}
return ToContainersString(fs::path(path.CStr()).lexically_normal().generic_string());
}
bool IsProjectRelativePath(const fs::path& path) {
const std::string generic = path.generic_string();
return !generic.empty() &&
generic != "." &&
generic != ".." &&
generic.rfind("../", 0) != 0;
}
void AddUniqueDependencyPath(const fs::path& path,
std::unordered_set<std::string>& seenPaths,
std::vector<fs::path>& outPaths) {
if (path.empty()) {
return;
}
const fs::path normalizedPath = path.lexically_normal();
const std::string key = normalizedPath.generic_string();
if (key.empty()) {
return;
}
if (seenPaths.insert(key).second) {
outPaths.push_back(normalizedPath);
}
}
std::string TrimCopy(const std::string& text) {
const auto begin = std::find_if_not(text.begin(), text.end(), [](unsigned char ch) {
return std::isspace(ch) != 0;
});
if (begin == text.end()) {
return std::string();
}
const auto end = std::find_if_not(text.rbegin(), text.rend(), [](unsigned char ch) {
return std::isspace(ch) != 0;
}).base();
return std::string(begin, end);
}
std::string ToLowerCopy(std::string text) {
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return text;
}
std::vector<std::string> SplitWhitespaceTokens(const std::string& text) {
std::vector<std::string> tokens;
std::istringstream stream(text);
std::string token;
while (stream >> token) {
tokens.push_back(token);
}
return tokens;
}
std::vector<fs::path> CollectObjDeclaredDependencyPaths(const fs::path& sourcePath) {
std::vector<fs::path> dependencies;
std::unordered_set<std::string> seenPaths;
std::ifstream input(sourcePath);
if (!input.is_open()) {
return dependencies;
}
const fs::path sourceDirectory = sourcePath.parent_path();
std::string line;
while (std::getline(input, line)) {
const std::string trimmed = TrimCopy(line);
if (trimmed.empty() || trimmed[0] == '#') {
continue;
}
const std::string lowered = ToLowerCopy(trimmed);
if (lowered.rfind("mtllib", 0) != 0 ||
(trimmed.size() > 6 && std::isspace(static_cast<unsigned char>(trimmed[6])) == 0)) {
continue;
}
const std::string remainder = TrimCopy(trimmed.substr(6));
for (const std::string& token : SplitWhitespaceTokens(remainder)) {
AddUniqueDependencyPath(sourceDirectory / token, seenPaths, dependencies);
}
}
return dependencies;
}
std::vector<fs::path> CollectMtlDeclaredDependencyPaths(const fs::path& mtlPath) {
std::vector<fs::path> dependencies;
std::unordered_set<std::string> seenPaths;
std::ifstream input(mtlPath);
if (!input.is_open()) {
return dependencies;
}
const fs::path sourceDirectory = mtlPath.parent_path();
std::string line;
while (std::getline(input, line)) {
const std::string trimmed = TrimCopy(line);
if (trimmed.empty() || trimmed[0] == '#') {
continue;
}
const std::vector<std::string> tokens = SplitWhitespaceTokens(trimmed);
if (tokens.size() < 2) {
continue;
}
const std::string keyword = ToLowerCopy(tokens.front());
const bool isTextureDirective =
keyword.rfind("map_", 0) == 0 ||
keyword == "bump" ||
keyword == "disp" ||
keyword == "decal" ||
keyword == "refl" ||
keyword == "norm";
if (!isTextureDirective) {
continue;
}
const std::string& textureToken = tokens.back();
if (!textureToken.empty() && textureToken[0] != '-') {
AddUniqueDependencyPath(sourceDirectory / textureToken, seenPaths, dependencies);
}
}
return dependencies;
}
std::string EscapeField(const std::string& value) {
std::string escaped;
escaped.reserve(value.size());
for (const char ch : value) {
if (ch == '\\' || ch == '\t' || ch == '\n' || ch == '\r') {
escaped.push_back('\\');
switch (ch) {
case '\t':
escaped.push_back('t');
break;
case '\n':
escaped.push_back('n');
break;
case '\r':
escaped.push_back('r');
break;
default:
escaped.push_back(ch);
break;
}
} else {
escaped.push_back(ch);
}
}
return escaped;
}
std::string UnescapeField(const std::string& value) {
std::string result;
result.reserve(value.size());
for (size_t index = 0; index < value.size(); ++index) {
if (value[index] == '\\' && index + 1 < value.size()) {
++index;
switch (value[index]) {
case 't':
result.push_back('\t');
break;
case 'n':
result.push_back('\n');
break;
case 'r':
result.push_back('\r');
break;
default:
result.push_back(value[index]);
break;
}
} else {
result.push_back(value[index]);
}
}
return result;
}
std::vector<std::string> SplitFields(const std::string& line) {
std::vector<std::string> fields;
std::string current;
bool escaping = false;
for (const char ch : line) {
if (escaping) {
current.push_back('\\');
current.push_back(ch);
escaping = false;
continue;
}
if (ch == '\\') {
escaping = true;
continue;
}
if (ch == '\t') {
fields.push_back(UnescapeField(current));
current.clear();
continue;
}
current.push_back(ch);
}
if (escaping) {
current.push_back('\\');
}
fields.push_back(UnescapeField(current));
return fields;
}
void WriteString(std::ofstream& stream, const Containers::String& value) {
const Core::uint32 length = static_cast<Core::uint32>(value.Length());
stream.write(reinterpret_cast<const char*>(&length), sizeof(length));
if (length > 0) {
stream.write(value.CStr(), length);
}
}
Containers::String ReadString(std::ifstream& stream) {
Core::uint32 length = 0;
stream.read(reinterpret_cast<char*>(&length), sizeof(length));
if (!stream || length == 0) {
return Containers::String();
}
std::string buffer(length, '\0');
stream.read(buffer.data(), length);
if (!stream) {
return Containers::String();
}
return ToContainersString(buffer);
}
bool WriteTextureArtifactFile(const fs::path& artifactPath, const Texture& texture) {
std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc);
if (!output.is_open()) {
return false;
}
TextureArtifactHeader header;
header.textureType = static_cast<Core::uint32>(texture.GetTextureType());
header.textureFormat = static_cast<Core::uint32>(texture.GetFormat());
header.width = texture.GetWidth();
header.height = texture.GetHeight();
header.depth = texture.GetDepth();
header.mipLevels = texture.GetMipLevels();
header.arraySize = texture.GetArraySize();
header.pixelDataSize = static_cast<Core::uint64>(texture.GetPixelDataSize());
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
if (texture.GetPixelDataSize() > 0) {
output.write(static_cast<const char*>(texture.GetPixelData()), texture.GetPixelDataSize());
}
return static_cast<bool>(output);
}
std::vector<MaterialProperty> GatherMaterialProperties(const Material& material) {
return material.GetProperties();
}
Containers::String ResolveTextureBindingPath(
const Material& material,
Core::uint32 bindingIndex,
const std::unordered_map<const Texture*, Containers::String>& textureArtifactPaths) {
const ResourceHandle<Texture> textureHandle = material.GetTextureBindingTexture(bindingIndex);
const Texture* texture = textureHandle.Get();
if (texture != nullptr) {
const auto textureIt = textureArtifactPaths.find(texture);
if (textureIt != textureArtifactPaths.end()) {
return textureIt->second;
}
if (!texture->GetPath().Empty()) {
return NormalizeArtifactPathString(texture->GetPath());
}
}
return NormalizeArtifactPathString(material.GetTextureBindingPath(bindingIndex));
}
bool WriteMaterialArtifactFile(
const fs::path& artifactPath,
const Material& material,
const std::unordered_map<const Texture*, Containers::String>& textureArtifactPaths = {}) {
std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc);
if (!output.is_open()) {
return false;
}
MaterialArtifactFileHeader fileHeader;
output.write(reinterpret_cast<const char*>(&fileHeader), sizeof(fileHeader));
WriteString(output, material.GetName());
WriteString(output, material.GetPath());
WriteString(
output,
material.GetShader() != nullptr
? material.GetShader()->GetPath()
: Containers::String());
WriteString(output, material.GetShaderPass());
MaterialArtifactHeader header;
header.renderQueue = material.GetRenderQueue();
header.renderState = material.GetRenderState();
header.tagCount = material.GetTagCount();
const std::vector<MaterialProperty> properties = GatherMaterialProperties(material);
std::vector<MaterialProperty> nonTextureProperties;
nonTextureProperties.reserve(properties.size());
for (const MaterialProperty& property : properties) {
if (property.type == MaterialPropertyType::Texture ||
property.type == MaterialPropertyType::Cubemap) {
continue;
}
nonTextureProperties.push_back(property);
}
header.propertyCount = static_cast<Core::uint32>(nonTextureProperties.size());
header.textureBindingCount = material.GetTextureBindingCount();
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
for (Core::uint32 tagIndex = 0; tagIndex < material.GetTagCount(); ++tagIndex) {
WriteString(output, material.GetTagName(tagIndex));
WriteString(output, material.GetTagValue(tagIndex));
}
for (const MaterialProperty& property : nonTextureProperties) {
WriteString(output, property.name);
MaterialPropertyArtifact propertyArtifact;
propertyArtifact.propertyType = static_cast<Core::uint32>(property.type);
propertyArtifact.value = property.value;
output.write(reinterpret_cast<const char*>(&propertyArtifact), sizeof(propertyArtifact));
}
for (Core::uint32 bindingIndex = 0; bindingIndex < material.GetTextureBindingCount(); ++bindingIndex) {
const Containers::String bindingName = material.GetTextureBindingName(bindingIndex);
WriteString(output, bindingName);
WriteString(output, ResolveTextureBindingPath(material, bindingIndex, textureArtifactPaths));
}
return static_cast<bool>(output);
}
bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader) {
std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc);
if (!output.is_open()) {
return false;
}
ShaderArtifactFileHeader fileHeader;
output.write(reinterpret_cast<const char*>(&fileHeader), sizeof(fileHeader));
WriteString(output, shader.GetName());
WriteString(output, NormalizeArtifactPathString(shader.GetPath()));
ShaderArtifactHeader header;
header.propertyCount = static_cast<Core::uint32>(shader.GetProperties().Size());
header.passCount = shader.GetPassCount();
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
for (const ShaderPropertyDesc& property : shader.GetProperties()) {
WriteString(output, property.name);
WriteString(output, property.displayName);
WriteString(output, property.defaultValue);
WriteString(output, property.semantic);
ShaderPropertyArtifact propertyArtifact;
propertyArtifact.propertyType = static_cast<Core::uint32>(property.type);
output.write(reinterpret_cast<const char*>(&propertyArtifact), sizeof(propertyArtifact));
}
for (const ShaderPass& pass : shader.GetPasses()) {
WriteString(output, pass.name);
ShaderPassArtifactHeader passHeader;
passHeader.tagCount = static_cast<Core::uint32>(pass.tags.Size());
passHeader.resourceCount = static_cast<Core::uint32>(pass.resources.Size());
passHeader.variantCount = static_cast<Core::uint32>(pass.variants.Size());
output.write(reinterpret_cast<const char*>(&passHeader), sizeof(passHeader));
for (const ShaderPassTagEntry& tag : pass.tags) {
WriteString(output, tag.name);
WriteString(output, tag.value);
}
for (const ShaderResourceBindingDesc& resource : pass.resources) {
WriteString(output, resource.name);
WriteString(output, resource.semantic);
ShaderResourceArtifact resourceArtifact;
resourceArtifact.resourceType = static_cast<Core::uint32>(resource.type);
resourceArtifact.set = resource.set;
resourceArtifact.binding = resource.binding;
output.write(reinterpret_cast<const char*>(&resourceArtifact), sizeof(resourceArtifact));
}
for (const ShaderStageVariant& variant : pass.variants) {
ShaderVariantArtifactHeader variantHeader;
variantHeader.stage = static_cast<Core::uint32>(variant.stage);
variantHeader.language = static_cast<Core::uint32>(variant.language);
variantHeader.backend = static_cast<Core::uint32>(variant.backend);
variantHeader.compiledBinarySize = static_cast<Core::uint64>(variant.compiledBinary.Size());
output.write(reinterpret_cast<const char*>(&variantHeader), sizeof(variantHeader));
WriteString(output, variant.entryPoint);
WriteString(output, variant.profile);
WriteString(output, variant.sourceCode);
if (!variant.compiledBinary.Empty()) {
output.write(
reinterpret_cast<const char*>(variant.compiledBinary.Data()),
static_cast<std::streamsize>(variant.compiledBinary.Size()));
}
}
}
return static_cast<bool>(output);
}
bool WriteMeshArtifactFile(const fs::path& artifactPath,
const Mesh& mesh,
const std::vector<Containers::String>& materialArtifactPaths) {
std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc);
if (!output.is_open()) {
return false;
}
MeshArtifactHeader header;
header.vertexCount = mesh.GetVertexCount();
header.vertexStride = mesh.GetVertexStride();
header.vertexAttributes = static_cast<Core::uint32>(mesh.GetVertexAttributes());
header.indexCount = mesh.GetIndexCount();
header.use32BitIndex = mesh.IsUse32BitIndex() ? 1u : 0u;
header.sectionCount = static_cast<Core::uint32>(mesh.GetSections().Size());
header.materialCount = static_cast<Core::uint32>(materialArtifactPaths.size());
header.boundsMin = mesh.GetBounds().GetMin();
header.boundsMax = mesh.GetBounds().GetMax();
header.vertexDataSize = static_cast<Core::uint64>(mesh.GetVertexDataSize());
header.indexDataSize = static_cast<Core::uint64>(mesh.GetIndexDataSize());
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
for (const MeshSection& section : mesh.GetSections()) {
output.write(reinterpret_cast<const char*>(&section), sizeof(section));
}
if (mesh.GetVertexDataSize() > 0) {
output.write(static_cast<const char*>(mesh.GetVertexData()), mesh.GetVertexDataSize());
}
if (mesh.GetIndexDataSize() > 0) {
output.write(static_cast<const char*>(mesh.GetIndexData()), mesh.GetIndexDataSize());
}
for (const Containers::String& materialArtifactPath : materialArtifactPaths) {
WriteString(output, NormalizeArtifactPathString(materialArtifactPath));
}
return static_cast<bool>(output);
}
void DestroyImportedMesh(Mesh* mesh) {
if (mesh == nullptr) {
return;
}
std::vector<Material*> materials;
materials.reserve(mesh->GetMaterials().Size());
for (Material* material : mesh->GetMaterials()) {
if (material != nullptr) {
materials.push_back(material);
}
}
std::vector<Texture*> textures;
textures.reserve(mesh->GetTextures().Size());
for (Texture* texture : mesh->GetTextures()) {
if (texture != nullptr) {
textures.push_back(texture);
}
}
delete mesh;
for (Material* material : materials) {
delete material;
}
for (Texture* texture : textures) {
delete texture;
}
}
} // namespace
void AssetDatabase::Initialize(const Containers::String& projectRoot) {
m_projectRoot = NormalizePathString(projectRoot);
m_assetsRoot = NormalizePathString(fs::path(m_projectRoot.CStr()) / "Assets");
m_libraryRoot = NormalizePathString(fs::path(m_projectRoot.CStr()) / "Library");
m_sourceDbPath = NormalizePathString(fs::path(m_libraryRoot.CStr()) / "SourceAssetDB" / "assets.db");
m_artifactDbPath = NormalizePathString(fs::path(m_libraryRoot.CStr()) / "ArtifactDB" / "artifacts.db");
EnsureProjectLayout();
LoadSourceAssetDB();
LoadArtifactDB();
ScanAssets();
SaveArtifactDB();
}
void AssetDatabase::Shutdown() {
SaveSourceAssetDB();
SaveArtifactDB();
m_projectRoot.Clear();
m_assetsRoot.Clear();
m_libraryRoot.Clear();
m_sourceDbPath.Clear();
m_artifactDbPath.Clear();
m_sourcesByPathKey.clear();
m_sourcesByGuid.clear();
m_artifactsByGuid.clear();
}
void AssetDatabase::Refresh() {
ScanAssets();
}
bool AssetDatabase::ResolvePath(const Containers::String& requestPath,
Containers::String& outAbsolutePath,
Containers::String& outRelativePath) const {
if (requestPath.Empty()) {
return false;
}
if (HasVirtualPathScheme(requestPath)) {
return false;
}
fs::path inputPath(requestPath.CStr());
if (inputPath.is_absolute()) {
outAbsolutePath = NormalizePathString(inputPath);
std::error_code ec;
const fs::path projectRootPath(m_projectRoot.CStr());
const fs::path relativePath = fs::relative(inputPath, projectRootPath, ec);
if (!ec) {
const Containers::String normalizedRelative = NormalizePathString(relativePath);
if (normalizedRelative.StartsWith("Assets/") || normalizedRelative == "Assets") {
outRelativePath = normalizedRelative;
} else {
outRelativePath.Clear();
}
} else {
outRelativePath.Clear();
}
return true;
}
const Containers::String normalizedRequest = NormalizePathString(requestPath);
if (normalizedRequest.StartsWith("Assets/") || normalizedRequest == "Assets") {
outRelativePath = normalizedRequest;
outAbsolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / normalizedRequest.CStr());
return true;
}
outAbsolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / requestPath.CStr());
outRelativePath.Clear();
return true;
}
bool AssetDatabase::TryGetAssetGuid(const Containers::String& requestPath, AssetGUID& outGuid) const {
Containers::String absolutePath;
Containers::String relativePath;
if (!ResolvePath(requestPath, absolutePath, relativePath) || relativePath.Empty()) {
return false;
}
const auto sourceIt = m_sourcesByPathKey.find(ToStdString(MakeKey(relativePath)));
if (sourceIt == m_sourcesByPathKey.end()) {
return false;
}
outGuid = sourceIt->second.guid;
return outGuid.IsValid();
}
bool AssetDatabase::TryGetAssetRef(const Containers::String& requestPath,
ResourceType resourceType,
AssetRef& outRef) const {
AssetGUID guid;
if (!TryGetAssetGuid(requestPath, guid)) {
return false;
}
outRef.assetGuid = guid;
outRef.localID = kMainAssetLocalID;
outRef.resourceType = resourceType;
return true;
}
bool AssetDatabase::TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const {
const auto sourceIt = m_sourcesByGuid.find(guid);
if (sourceIt == m_sourcesByGuid.end()) {
return false;
}
outRelativePath = sourceIt->second.relativePath;
return true;
}
void AssetDatabase::BuildLookupSnapshot(std::unordered_map<std::string, AssetGUID>& outPathToGuid,
std::unordered_map<AssetGUID, Containers::String>& outGuidToPath) const {
outPathToGuid.clear();
outGuidToPath.clear();
outPathToGuid.reserve(m_sourcesByPathKey.size());
outGuidToPath.reserve(m_sourcesByGuid.size());
for (const auto& [pathKey, record] : m_sourcesByPathKey) {
if (!record.guid.IsValid() || record.relativePath.Empty()) {
continue;
}
outPathToGuid[pathKey] = record.guid;
}
for (const auto& [guid, record] : m_sourcesByGuid) {
if (!guid.IsValid() || record.relativePath.Empty()) {
continue;
}
outGuidToPath[guid] = record.relativePath;
}
}
void AssetDatabase::EnsureProjectLayout() {
std::error_code ec;
fs::create_directories(fs::path(m_assetsRoot.CStr()), ec);
ec.clear();
fs::create_directories(fs::path(m_libraryRoot.CStr()) / "SourceAssetDB", ec);
ec.clear();
fs::create_directories(fs::path(m_libraryRoot.CStr()) / "ArtifactDB", ec);
ec.clear();
fs::create_directories(fs::path(m_libraryRoot.CStr()) / "Artifacts", ec);
}
void AssetDatabase::LoadSourceAssetDB() {
m_sourcesByPathKey.clear();
m_sourcesByGuid.clear();
std::ifstream input(m_sourceDbPath.CStr());
if (!input.is_open()) {
return;
}
std::string line;
while (std::getline(input, line)) {
if (line.empty() || line[0] == '#') {
continue;
}
const std::vector<std::string> fields = SplitFields(line);
if (fields.size() < 10) {
continue;
}
SourceAssetRecord record;
record.guid = AssetGUID::ParseOrDefault(ToContainersString(fields[0]));
record.relativePath = ToContainersString(fields[1]);
record.metaPath = ToContainersString(fields[2]);
record.isFolder = (fields[3] == "1");
record.importerName = ToContainersString(fields[4]);
record.importerVersion = static_cast<Core::uint32>(std::stoul(fields[5]));
record.metaHash = ToContainersString(fields[6]);
record.sourceHash = ToContainersString(fields[7]);
record.sourceFileSize = static_cast<Core::uint64>(std::stoull(fields[8]));
record.sourceWriteTime = static_cast<Core::uint64>(std::stoull(fields[9]));
if (fields.size() > 10) {
record.lastKnownArtifactKey = ToContainersString(fields[10]);
}
if (!record.guid.IsValid() || record.relativePath.Empty()) {
continue;
}
m_sourcesByGuid[record.guid] = record;
m_sourcesByPathKey[ToStdString(MakeKey(record.relativePath))] = record;
}
}
void AssetDatabase::SaveSourceAssetDB() const {
std::ofstream output(m_sourceDbPath.CStr(), std::ios::out | std::ios::trunc);
if (!output.is_open()) {
return;
}
output << "# guid\trelativePath\tmetaPath\tisFolder\timporter\timporterVersion\tmetaHash\tsourceHash\tsize\twriteTime\tartifactKey\n";
for (const auto& [guid, record] : m_sourcesByGuid) {
output << EscapeField(ToStdString(record.guid.ToString())) << '\t'
<< EscapeField(ToStdString(record.relativePath)) << '\t'
<< EscapeField(ToStdString(record.metaPath)) << '\t'
<< (record.isFolder ? "1" : "0") << '\t'
<< EscapeField(ToStdString(record.importerName)) << '\t'
<< record.importerVersion << '\t'
<< EscapeField(ToStdString(record.metaHash)) << '\t'
<< EscapeField(ToStdString(record.sourceHash)) << '\t'
<< record.sourceFileSize << '\t'
<< record.sourceWriteTime << '\t'
<< EscapeField(ToStdString(record.lastKnownArtifactKey)) << '\n';
}
}
void AssetDatabase::LoadArtifactDB() {
m_artifactsByGuid.clear();
std::ifstream input(m_artifactDbPath.CStr());
if (!input.is_open()) {
return;
}
std::string line;
while (std::getline(input, line)) {
if (line.empty() || line[0] == '#') {
continue;
}
const std::vector<std::string> fields = SplitFields(line);
if (fields.size() < 10) {
continue;
}
ArtifactRecord record;
record.artifactKey = ToContainersString(fields[0]);
record.assetGuid = AssetGUID::ParseOrDefault(ToContainersString(fields[1]));
record.importerName = ToContainersString(fields[2]);
record.importerVersion = static_cast<Core::uint32>(std::stoul(fields[3]));
record.resourceType = static_cast<ResourceType>(std::stoul(fields[4]));
record.artifactDirectory = ToContainersString(fields[5]);
record.mainArtifactPath = ToContainersString(fields[6]);
record.sourceHash = ToContainersString(fields[7]);
record.metaHash = ToContainersString(fields[8]);
record.sourceFileSize = static_cast<Core::uint64>(std::stoull(fields[9]));
record.sourceWriteTime = fields.size() > 10 ? static_cast<Core::uint64>(std::stoull(fields[10])) : 0;
record.mainLocalID = fields.size() > 11 ? static_cast<LocalID>(std::stoull(fields[11])) : kMainAssetLocalID;
for (size_t index = 12; index + 3 < fields.size(); index += 4) {
ArtifactDependencyRecord dependency;
dependency.path = ToContainersString(fields[index + 0]);
dependency.hash = ToContainersString(fields[index + 1]);
dependency.fileSize = static_cast<Core::uint64>(std::stoull(fields[index + 2]));
dependency.writeTime = static_cast<Core::uint64>(std::stoull(fields[index + 3]));
if (!dependency.path.Empty()) {
record.dependencies.push_back(dependency);
}
}
if (!record.assetGuid.IsValid() || record.artifactKey.Empty()) {
continue;
}
m_artifactsByGuid[record.assetGuid] = record;
}
}
void AssetDatabase::SaveArtifactDB() const {
std::ofstream output(m_artifactDbPath.CStr(), std::ios::out | std::ios::trunc);
if (!output.is_open()) {
return;
}
output << "# artifactKey\tassetGuid\timporter\tversion\ttype\tartifactDir\tmainArtifact\tsourceHash\tmetaHash\tsize\twriteTime\tmainLocalID\t(depPath\tdepHash\tdepSize\tdepWriteTime)...\n";
for (const auto& [guid, record] : m_artifactsByGuid) {
output << EscapeField(ToStdString(record.artifactKey)) << '\t'
<< EscapeField(ToStdString(record.assetGuid.ToString())) << '\t'
<< EscapeField(ToStdString(record.importerName)) << '\t'
<< record.importerVersion << '\t'
<< static_cast<Core::uint32>(record.resourceType) << '\t'
<< EscapeField(ToStdString(record.artifactDirectory)) << '\t'
<< EscapeField(ToStdString(record.mainArtifactPath)) << '\t'
<< EscapeField(ToStdString(record.sourceHash)) << '\t'
<< EscapeField(ToStdString(record.metaHash)) << '\t'
<< record.sourceFileSize << '\t'
<< record.sourceWriteTime << '\t'
<< record.mainLocalID;
for (const ArtifactDependencyRecord& dependency : record.dependencies) {
output << '\t' << EscapeField(ToStdString(dependency.path))
<< '\t' << EscapeField(ToStdString(dependency.hash))
<< '\t' << dependency.fileSize
<< '\t' << dependency.writeTime;
}
output << '\n';
}
}
void AssetDatabase::ScanAssets() {
std::unordered_map<std::string, bool> seenPaths;
const fs::path assetsRootPath(m_assetsRoot.CStr());
if (fs::exists(assetsRootPath)) {
ScanAssetPath(assetsRootPath, seenPaths);
}
RemoveMissingRecords(seenPaths);
SaveSourceAssetDB();
}
void AssetDatabase::ScanAssetPath(const fs::path& path,
std::unordered_map<std::string, bool>& seenPaths) {
if (!fs::exists(path)) {
return;
}
if (path.has_extension() && ToLowerCopy(path.extension().string()) == ".meta") {
return;
}
const bool isFolder = fs::is_directory(path);
SourceAssetRecord record;
if (EnsureMetaForPath(path, isFolder, record)) {
seenPaths[ToStdString(MakeKey(record.relativePath))] = true;
}
if (!isFolder) {
return;
}
for (const auto& entry : fs::directory_iterator(path)) {
ScanAssetPath(entry.path(), seenPaths);
}
}
void AssetDatabase::RemoveMissingRecords(const std::unordered_map<std::string, bool>& seenPaths) {
std::vector<std::string> missingPathKeys;
for (const auto& [pathKey, record] : m_sourcesByPathKey) {
if (seenPaths.find(pathKey) == seenPaths.end()) {
missingPathKeys.push_back(pathKey);
}
}
for (const std::string& pathKey : missingPathKeys) {
auto recordIt = m_sourcesByPathKey.find(pathKey);
if (recordIt == m_sourcesByPathKey.end()) {
continue;
}
m_artifactsByGuid.erase(recordIt->second.guid);
m_sourcesByGuid.erase(recordIt->second.guid);
m_sourcesByPathKey.erase(recordIt);
}
if (!missingPathKeys.empty()) {
SaveArtifactDB();
}
}
bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath,
bool isFolder,
SourceAssetRecord& outRecord) {
const Containers::String relativePath = NormalizeRelativePath(sourcePath);
if (relativePath.Empty()) {
return false;
}
const std::string pathKey = ToStdString(MakeKey(relativePath));
auto existingIt = m_sourcesByPathKey.find(pathKey);
if (existingIt != m_sourcesByPathKey.end()) {
outRecord = existingIt->second;
} else {
outRecord = SourceAssetRecord();
outRecord.relativePath = relativePath;
outRecord.importerName = GetImporterNameForPath(relativePath, isFolder);
outRecord.importerVersion = kCurrentImporterVersion;
}
outRecord.relativePath = relativePath;
outRecord.isFolder = isFolder;
outRecord.importerName = GetImporterNameForPath(relativePath, isFolder);
outRecord.importerVersion = kCurrentImporterVersion;
const fs::path metaPath(sourcePath.string() + ".meta");
outRecord.metaPath = NormalizeRelativePath(metaPath);
bool shouldRewriteMeta = false;
if (!fs::exists(metaPath) || !ReadMetaFile(metaPath, outRecord) || !outRecord.guid.IsValid()) {
if (!outRecord.guid.IsValid()) {
outRecord.guid = AssetGUID::Generate();
}
shouldRewriteMeta = true;
}
if (outRecord.importerVersion != kCurrentImporterVersion) {
outRecord.importerVersion = kCurrentImporterVersion;
shouldRewriteMeta = true;
}
const auto duplicateGuidIt = m_sourcesByGuid.find(outRecord.guid);
if (duplicateGuidIt != m_sourcesByGuid.end() &&
duplicateGuidIt->second.relativePath != relativePath) {
outRecord.guid = AssetGUID::Generate();
shouldRewriteMeta = true;
}
if (shouldRewriteMeta) {
WriteMetaFile(metaPath, outRecord);
}
outRecord.metaHash = HashStringToAssetGUID(ReadWholeFileText(metaPath)).ToString();
if (isFolder) {
outRecord.sourceHash.Clear();
outRecord.sourceFileSize = 0;
outRecord.sourceWriteTime = 0;
} else {
const Core::uint64 fileSize = GetFileSizeValue(sourcePath);
const Core::uint64 writeTime = GetFileWriteTimeValue(sourcePath);
if (existingIt != m_sourcesByPathKey.end() &&
existingIt->second.sourceFileSize == fileSize &&
existingIt->second.sourceWriteTime == writeTime &&
!existingIt->second.sourceHash.Empty()) {
outRecord.sourceHash = existingIt->second.sourceHash;
} else {
outRecord.sourceHash = ComputeFileHash(sourcePath);
}
outRecord.sourceFileSize = fileSize;
outRecord.sourceWriteTime = writeTime;
}
m_sourcesByPathKey[pathKey] = outRecord;
m_sourcesByGuid[outRecord.guid] = outRecord;
return true;
}
bool AssetDatabase::ReadMetaFile(const fs::path& metaPath,
SourceAssetRecord& inOutRecord) const {
std::ifstream input(metaPath);
if (!input.is_open()) {
return false;
}
std::string line;
while (std::getline(input, line)) {
const size_t colonPos = line.find(':');
if (colonPos == std::string::npos) {
continue;
}
const std::string key = TrimCopy(line.substr(0, colonPos));
const std::string value = TrimCopy(line.substr(colonPos + 1));
if (key == "guid") {
inOutRecord.guid = AssetGUID::ParseOrDefault(ToContainersString(value));
} else if (key == "folderAsset") {
inOutRecord.isFolder = ToLowerCopy(value) == "true";
} else if (key == "importer") {
inOutRecord.importerName = ToContainersString(value);
} else if (key == "importerVersion") {
inOutRecord.importerVersion = static_cast<Core::uint32>(std::stoul(value));
}
}
if (inOutRecord.importerName.Empty()) {
inOutRecord.importerName = GetImporterNameForPath(inOutRecord.relativePath, inOutRecord.isFolder);
}
if (inOutRecord.importerVersion == 0) {
inOutRecord.importerVersion = kCurrentImporterVersion;
}
return true;
}
void AssetDatabase::WriteMetaFile(const fs::path& metaPath,
const SourceAssetRecord& record) const {
std::ofstream output(metaPath, std::ios::out | std::ios::trunc);
if (!output.is_open()) {
return;
}
output << "fileFormatVersion: 1\n";
output << "guid: " << record.guid.ToString().CStr() << "\n";
output << "folderAsset: " << (record.isFolder ? "true" : "false") << "\n";
output << "importer: " << record.importerName.CStr() << "\n";
output << "importerVersion: " << record.importerVersion << "\n";
}
Containers::String AssetDatabase::NormalizeRelativePath(const fs::path& sourcePath) const {
std::error_code ec;
const fs::path projectRootPath(m_projectRoot.CStr());
const fs::path relativePath = fs::relative(sourcePath, projectRootPath, ec);
if (ec) {
return Containers::String();
}
return NormalizePathString(relativePath);
}
Containers::String AssetDatabase::NormalizePathString(const fs::path& path) {
return ToContainersString(path.lexically_normal().generic_string());
}
Containers::String AssetDatabase::NormalizePathString(const Containers::String& path) {
return NormalizePathString(fs::path(path.CStr()));
}
Containers::String AssetDatabase::MakeKey(const Containers::String& path) {
std::string key = ToStdString(NormalizePathString(path));
std::transform(key.begin(), key.end(), key.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return ToContainersString(key);
}
Containers::String AssetDatabase::GetImporterNameForPath(const Containers::String& relativePath, bool isFolder) {
if (isFolder) {
return Containers::String("FolderImporter");
}
const std::string ext = ToLowerCopy(fs::path(relativePath.CStr()).extension().string());
if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp" || ext == ".tga" || ext == ".gif" || ext == ".hdr") {
return Containers::String("TextureImporter");
}
if (ext == ".obj" || ext == ".fbx" || ext == ".gltf" || ext == ".glb" || ext == ".dae" || ext == ".stl") {
return Containers::String("ModelImporter");
}
if (ext == ".shader" || ext == ".hlsl" || ext == ".glsl" || ext == ".vert" || ext == ".frag" ||
ext == ".geom" || ext == ".comp") {
return Containers::String("ShaderImporter");
}
if (ext == ".mat" || ext == ".material" || ext == ".json") {
return Containers::String("MaterialImporter");
}
return Containers::String("DefaultImporter");
}
ResourceType AssetDatabase::GetPrimaryResourceTypeForImporter(const Containers::String& importerName) {
if (importerName == "TextureImporter") {
return ResourceType::Texture;
}
if (importerName == "ModelImporter") {
return ResourceType::Mesh;
}
if (importerName == "MaterialImporter") {
return ResourceType::Material;
}
if (importerName == "ShaderImporter") {
return ResourceType::Shader;
}
return ResourceType::Unknown;
}
bool AssetDatabase::ShouldReimport(const SourceAssetRecord& sourceRecord,
const ArtifactRecord* artifactRecord) const {
if (artifactRecord == nullptr) {
return true;
}
if (artifactRecord->artifactKey.Empty() ||
artifactRecord->mainArtifactPath.Empty()) {
return true;
}
const fs::path artifactMainPath = fs::path(m_projectRoot.CStr()) / artifactRecord->mainArtifactPath.CStr();
if (!fs::exists(artifactMainPath)) {
return true;
}
return artifactRecord->importerVersion != sourceRecord.importerVersion ||
artifactRecord->sourceHash != sourceRecord.sourceHash ||
artifactRecord->metaHash != sourceRecord.metaHash ||
artifactRecord->sourceFileSize != sourceRecord.sourceFileSize ||
artifactRecord->sourceWriteTime != sourceRecord.sourceWriteTime ||
!AreDependenciesCurrent(artifactRecord->dependencies);
}
bool AssetDatabase::ImportAsset(const SourceAssetRecord& sourceRecord,
ArtifactRecord& outRecord) {
const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName);
switch (primaryType) {
case ResourceType::Texture:
return ImportTextureAsset(sourceRecord, outRecord);
case ResourceType::Material:
return ImportMaterialAsset(sourceRecord, outRecord);
case ResourceType::Mesh:
return ImportModelAsset(sourceRecord, outRecord);
case ResourceType::Shader:
return ImportShaderAsset(sourceRecord, outRecord);
default:
return false;
}
}
bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
ResourceType requestedType,
ResolvedAsset& outAsset) {
outAsset = ResolvedAsset();
Containers::String absolutePath;
Containers::String relativePath;
if (!ResolvePath(requestPath, absolutePath, relativePath) || relativePath.Empty()) {
if (ShouldTraceAssetPath(requestPath)) {
Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem,
Containers::String("[AssetDatabase] EnsureArtifact unresolved path=") + requestPath);
}
return false;
}
const fs::path absoluteFsPath(absolutePath.CStr());
if (!fs::exists(absoluteFsPath)) {
if (ShouldTraceAssetPath(requestPath)) {
Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem,
Containers::String("[AssetDatabase] EnsureArtifact missing source path=") +
requestPath +
" absolute=" +
absolutePath);
}
return false;
}
SourceAssetRecord sourceRecord;
if (!EnsureMetaForPath(absoluteFsPath, fs::is_directory(absoluteFsPath), sourceRecord)) {
return false;
}
if (ShouldTraceAssetPath(requestPath)) {
Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem,
Containers::String("[AssetDatabase] EnsureArtifact source path=") +
requestPath +
" guid=" +
sourceRecord.guid.ToString() +
" importer=" +
sourceRecord.importerName +
" relative=" +
sourceRecord.relativePath);
}
const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName);
if (primaryType == ResourceType::Unknown || primaryType != requestedType) {
if (ShouldTraceAssetPath(requestPath)) {
Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem,
Containers::String("[AssetDatabase] EnsureArtifact type-mismatch path=") +
requestPath +
" requested=" +
GetResourceTypeName(requestedType) +
" importerType=" +
GetResourceTypeName(primaryType));
}
return false;
}
ArtifactRecord* artifactRecord = nullptr;
auto artifactIt = m_artifactsByGuid.find(sourceRecord.guid);
if (artifactIt != m_artifactsByGuid.end()) {
artifactRecord = &artifactIt->second;
}
if (ShouldReimport(sourceRecord, artifactRecord)) {
if (ShouldTraceAssetPath(requestPath)) {
Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem,
Containers::String("[AssetDatabase] EnsureArtifact reimport path=") + requestPath);
}
ArtifactRecord rebuiltRecord;
if (!ImportAsset(sourceRecord, rebuiltRecord)) {
if (ShouldTraceAssetPath(requestPath)) {
Debug::Logger::Get().Error(
Debug::LogCategory::FileSystem,
Containers::String("[AssetDatabase] EnsureArtifact reimport failed path=") + requestPath);
}
return false;
}
m_artifactsByGuid[sourceRecord.guid] = rebuiltRecord;
m_sourcesByGuid[sourceRecord.guid].lastKnownArtifactKey = rebuiltRecord.artifactKey;
m_sourcesByPathKey[ToStdString(MakeKey(sourceRecord.relativePath))].lastKnownArtifactKey = rebuiltRecord.artifactKey;
SaveArtifactDB();
SaveSourceAssetDB();
artifactRecord = &m_artifactsByGuid[sourceRecord.guid];
}
if (artifactRecord == nullptr) {
return false;
}
outAsset.exists = true;
outAsset.artifactReady = true;
outAsset.absolutePath = absolutePath;
outAsset.relativePath = sourceRecord.relativePath;
outAsset.assetGuid = sourceRecord.guid;
outAsset.resourceType = artifactRecord->resourceType;
outAsset.artifactDirectory = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord->artifactDirectory.CStr());
outAsset.artifactMainPath = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord->mainArtifactPath.CStr());
outAsset.mainLocalID = artifactRecord->mainLocalID;
if (ShouldTraceAssetPath(requestPath)) {
Debug::Logger::Get().Info(
Debug::LogCategory::FileSystem,
Containers::String("[AssetDatabase] EnsureArtifact ready path=") +
requestPath +
" artifactKey=" +
artifactRecord->artifactKey +
" artifact=" +
outAsset.artifactMainPath);
}
return true;
}
bool AssetDatabase::ImportTextureAsset(const SourceAssetRecord& sourceRecord,
ArtifactRecord& outRecord) {
TextureLoader loader;
const Containers::String absolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
LoadResult result = loader.Load(absolutePath);
if (!result || result.resource == nullptr) {
return false;
}
Texture* texture = static_cast<Texture*>(result.resource);
const Containers::String artifactKey = BuildArtifactKey(sourceRecord);
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xctex");
std::error_code ec;
fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
ec.clear();
fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
if (ec) {
delete texture;
return false;
}
const bool writeOk = WriteTextureArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *texture);
delete texture;
if (!writeOk) {
return false;
}
outRecord.artifactKey = artifactKey;
outRecord.assetGuid = sourceRecord.guid;
outRecord.importerName = sourceRecord.importerName;
outRecord.importerVersion = sourceRecord.importerVersion;
outRecord.resourceType = ResourceType::Texture;
outRecord.artifactDirectory = artifactDir;
outRecord.mainArtifactPath = mainArtifactPath;
outRecord.sourceHash = sourceRecord.sourceHash;
outRecord.metaHash = sourceRecord.metaHash;
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
outRecord.sourceWriteTime = sourceRecord.sourceWriteTime;
outRecord.mainLocalID = kMainAssetLocalID;
outRecord.dependencies.clear();
return true;
}
bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord,
ArtifactRecord& outRecord) {
MaterialLoader loader;
const Containers::String absolutePath =
NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
LoadResult result = loader.Load(absolutePath);
if (!result || result.resource == nullptr) {
return false;
}
Material* material = static_cast<Material*>(result.resource);
std::vector<ArtifactDependencyRecord> dependencies;
CollectMaterialDependencies(*material, dependencies);
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
const Containers::String mainArtifactPath =
NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmat");
std::error_code ec;
fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
ec.clear();
fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
if (ec) {
delete material;
return false;
}
const bool writeOk =
WriteMaterialArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *material);
delete material;
if (!writeOk) {
return false;
}
outRecord.artifactKey = artifactKey;
outRecord.assetGuid = sourceRecord.guid;
outRecord.importerName = sourceRecord.importerName;
outRecord.importerVersion = sourceRecord.importerVersion;
outRecord.resourceType = ResourceType::Material;
outRecord.artifactDirectory = artifactDir;
outRecord.mainArtifactPath = mainArtifactPath;
outRecord.sourceHash = sourceRecord.sourceHash;
outRecord.metaHash = sourceRecord.metaHash;
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
outRecord.sourceWriteTime = sourceRecord.sourceWriteTime;
outRecord.mainLocalID = kMainAssetLocalID;
outRecord.dependencies = std::move(dependencies);
return true;
}
bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
ArtifactRecord& outRecord) {
MeshLoader loader;
const Containers::String absolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
LoadResult result = loader.Load(absolutePath);
if (!result || result.resource == nullptr) {
return false;
}
Mesh* mesh = static_cast<Mesh*>(result.resource);
std::vector<ArtifactDependencyRecord> dependencies;
CollectModelDependencies(sourceRecord, *mesh, dependencies);
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmesh");
std::error_code ec;
fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
ec.clear();
fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
if (ec) {
DestroyImportedMesh(mesh);
return false;
}
bool writeOk = true;
std::unordered_map<const Texture*, Containers::String> textureArtifactPaths;
for (size_t textureIndex = 0; writeOk && textureIndex < mesh->GetTextures().Size(); ++textureIndex) {
Texture* texture = mesh->GetTextures()[textureIndex];
if (texture == nullptr) {
continue;
}
const Containers::String textureArtifactPath =
NormalizePathString(fs::path(artifactDir.CStr()) / ("texture_" + std::to_string(textureIndex) + ".xctex"));
writeOk = WriteTextureArtifactFile(
fs::path(m_projectRoot.CStr()) / textureArtifactPath.CStr(),
*texture);
if (!writeOk) {
break;
}
textureArtifactPaths.emplace(texture, textureArtifactPath);
}
std::vector<Containers::String> materialArtifactPaths;
materialArtifactPaths.reserve(mesh->GetMaterials().Size());
for (size_t materialIndex = 0; writeOk && materialIndex < mesh->GetMaterials().Size(); ++materialIndex) {
Material* material = mesh->GetMaterials()[materialIndex];
if (material == nullptr) {
materialArtifactPaths.emplace_back();
continue;
}
const Containers::String materialArtifactPath =
NormalizePathString(fs::path(artifactDir.CStr()) / ("material_" + std::to_string(materialIndex) + ".xcmat"));
writeOk = WriteMaterialArtifactFile(
fs::path(m_projectRoot.CStr()) / materialArtifactPath.CStr(),
*material,
textureArtifactPaths);
if (!writeOk) {
break;
}
materialArtifactPaths.push_back(materialArtifactPath);
}
writeOk = writeOk &&
WriteMeshArtifactFile(
fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(),
*mesh,
materialArtifactPaths);
DestroyImportedMesh(mesh);
if (!writeOk) {
return false;
}
outRecord.artifactKey = artifactKey;
outRecord.assetGuid = sourceRecord.guid;
outRecord.importerName = sourceRecord.importerName;
outRecord.importerVersion = sourceRecord.importerVersion;
outRecord.resourceType = ResourceType::Mesh;
outRecord.artifactDirectory = artifactDir;
outRecord.mainArtifactPath = mainArtifactPath;
outRecord.sourceHash = sourceRecord.sourceHash;
outRecord.metaHash = sourceRecord.metaHash;
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
outRecord.sourceWriteTime = sourceRecord.sourceWriteTime;
outRecord.mainLocalID = kMainAssetLocalID;
outRecord.dependencies = std::move(dependencies);
return true;
}
bool AssetDatabase::ImportShaderAsset(const SourceAssetRecord& sourceRecord,
ArtifactRecord& outRecord) {
ShaderLoader loader;
const Containers::String absolutePath =
NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
LoadResult result = loader.Load(absolutePath);
if (!result || result.resource == nullptr) {
return false;
}
Shader* shader = static_cast<Shader*>(result.resource);
std::vector<ArtifactDependencyRecord> dependencies;
if (!CollectShaderDependencies(sourceRecord, dependencies)) {
delete shader;
return false;
}
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
const Containers::String mainArtifactPath =
NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcshader");
std::error_code ec;
fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
ec.clear();
fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
if (ec) {
delete shader;
return false;
}
const bool writeOk =
WriteShaderArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *shader);
delete shader;
if (!writeOk) {
return false;
}
outRecord.artifactKey = artifactKey;
outRecord.assetGuid = sourceRecord.guid;
outRecord.importerName = sourceRecord.importerName;
outRecord.importerVersion = sourceRecord.importerVersion;
outRecord.resourceType = ResourceType::Shader;
outRecord.artifactDirectory = artifactDir;
outRecord.mainArtifactPath = mainArtifactPath;
outRecord.sourceHash = sourceRecord.sourceHash;
outRecord.metaHash = sourceRecord.metaHash;
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
outRecord.sourceWriteTime = sourceRecord.sourceWriteTime;
outRecord.mainLocalID = kMainAssetLocalID;
outRecord.dependencies = std::move(dependencies);
return true;
}
Containers::String AssetDatabase::BuildArtifactKey(
const AssetDatabase::SourceAssetRecord& sourceRecord,
const std::vector<AssetDatabase::ArtifactDependencyRecord>& dependencies) const {
Containers::String signature = sourceRecord.guid.ToString();
signature += ":";
signature += sourceRecord.importerName;
signature += ":";
signature += Containers::String(std::to_string(sourceRecord.importerVersion).c_str());
signature += ":";
signature += sourceRecord.sourceHash;
signature += ":";
signature += sourceRecord.metaHash;
signature += ":";
signature += Containers::String(std::to_string(sourceRecord.sourceFileSize).c_str());
signature += ":";
signature += Containers::String(std::to_string(sourceRecord.sourceWriteTime).c_str());
for (const ArtifactDependencyRecord& dependency : dependencies) {
signature += ":dep:";
signature += dependency.path;
signature += ":";
signature += dependency.hash;
signature += ":";
signature += Containers::String(std::to_string(dependency.fileSize).c_str());
signature += ":";
signature += Containers::String(std::to_string(dependency.writeTime).c_str());
}
return HashStringToAssetGUID(signature).ToString();
}
Containers::String AssetDatabase::NormalizeDependencyPath(const fs::path& path) const {
const fs::path normalizedPath = path.lexically_normal();
if (normalizedPath.empty()) {
return Containers::String();
}
if (normalizedPath.is_absolute() && !m_projectRoot.Empty()) {
std::error_code ec;
const fs::path relativePath = fs::relative(normalizedPath, fs::path(m_projectRoot.CStr()), ec);
if (!ec && IsProjectRelativePath(relativePath)) {
return ToContainersString(relativePath.generic_string());
}
}
return ToContainersString(normalizedPath.generic_string());
}
fs::path AssetDatabase::ResolveDependencyPath(const Containers::String& path) const {
if (path.Empty()) {
return fs::path();
}
fs::path dependencyPath(path.CStr());
if (dependencyPath.is_absolute()) {
return dependencyPath.lexically_normal();
}
return (fs::path(m_projectRoot.CStr()) / dependencyPath).lexically_normal();
}
bool AssetDatabase::CaptureDependencyRecord(const fs::path& path,
AssetDatabase::ArtifactDependencyRecord& outRecord) const {
const Containers::String normalizedPath = NormalizeDependencyPath(path);
if (normalizedPath.Empty()) {
return false;
}
outRecord = ArtifactDependencyRecord();
outRecord.path = normalizedPath;
const fs::path resolvedPath = ResolveDependencyPath(normalizedPath);
if (!resolvedPath.empty() && fs::exists(resolvedPath)) {
outRecord.hash = ComputeFileHash(resolvedPath);
outRecord.fileSize = GetFileSizeValue(resolvedPath);
outRecord.writeTime = GetFileWriteTimeValue(resolvedPath);
}
return true;
}
bool AssetDatabase::AreDependenciesCurrent(
const std::vector<AssetDatabase::ArtifactDependencyRecord>& dependencies) const {
for (const AssetDatabase::ArtifactDependencyRecord& dependency : dependencies) {
if (dependency.path.Empty()) {
return false;
}
const fs::path resolvedPath = ResolveDependencyPath(dependency.path);
if (resolvedPath.empty() || !fs::exists(resolvedPath)) {
if (!dependency.hash.Empty() ||
dependency.fileSize != 0 ||
dependency.writeTime != 0) {
return false;
}
continue;
}
const Core::uint64 currentFileSize = GetFileSizeValue(resolvedPath);
const Core::uint64 currentWriteTime = GetFileWriteTimeValue(resolvedPath);
if (currentFileSize != dependency.fileSize ||
currentWriteTime != dependency.writeTime) {
return false;
}
if (ComputeFileHash(resolvedPath) != dependency.hash) {
return false;
}
}
return true;
}
bool AssetDatabase::CollectModelDependencies(const AssetDatabase::SourceAssetRecord& sourceRecord,
const Mesh& mesh,
std::vector<AssetDatabase::ArtifactDependencyRecord>& outDependencies) const {
outDependencies.clear();
std::unordered_set<std::string> seenDependencyPaths;
std::vector<fs::path> candidatePaths;
const fs::path sourcePath = fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr();
const fs::path normalizedSourcePath = sourcePath.lexically_normal();
if (ToLowerCopy(normalizedSourcePath.extension().string()) == ".obj") {
const std::vector<fs::path> mtlPaths = CollectObjDeclaredDependencyPaths(normalizedSourcePath);
for (const fs::path& mtlPath : mtlPaths) {
AddUniqueDependencyPath(mtlPath, seenDependencyPaths, candidatePaths);
const std::vector<fs::path> texturePaths = CollectMtlDeclaredDependencyPaths(mtlPath);
for (const fs::path& texturePath : texturePaths) {
AddUniqueDependencyPath(texturePath, seenDependencyPaths, candidatePaths);
}
}
}
for (Texture* texture : mesh.GetTextures()) {
if (texture == nullptr || texture->GetPath().Empty()) {
continue;
}
const std::string texturePath = ToStdString(texture->GetPath());
if (texturePath.find('#') != std::string::npos) {
continue;
}
AddUniqueDependencyPath(fs::path(texturePath), seenDependencyPaths, candidatePaths);
}
const std::string sourcePathKey = normalizedSourcePath.generic_string();
for (const fs::path& candidatePath : candidatePaths) {
if (candidatePath.generic_string() == sourcePathKey) {
continue;
}
ArtifactDependencyRecord dependency;
if (CaptureDependencyRecord(candidatePath, dependency)) {
outDependencies.push_back(dependency);
}
}
return true;
}
bool AssetDatabase::CollectMaterialDependencies(
const Material& material,
std::vector<AssetDatabase::ArtifactDependencyRecord>& outDependencies) const {
outDependencies.clear();
std::unordered_set<std::string> seenDependencyPaths;
if (material.GetShader() != nullptr) {
const Containers::String shaderPath = material.GetShader()->GetPath();
if (!shaderPath.Empty() && !HasVirtualPathScheme(shaderPath)) {
ArtifactDependencyRecord dependency;
if (CaptureDependencyRecord(ResolveDependencyPath(shaderPath), dependency)) {
const std::string dependencyKey = ToStdString(dependency.path);
if (seenDependencyPaths.insert(dependencyKey).second) {
outDependencies.push_back(dependency);
}
}
}
}
for (Core::uint32 bindingIndex = 0; bindingIndex < material.GetTextureBindingCount(); ++bindingIndex) {
const Containers::String texturePath = material.GetTextureBindingPath(bindingIndex);
if (texturePath.Empty()) {
continue;
}
ArtifactDependencyRecord dependency;
if (!CaptureDependencyRecord(ResolveDependencyPath(texturePath), dependency)) {
continue;
}
const std::string dependencyKey = ToStdString(dependency.path);
if (seenDependencyPaths.insert(dependencyKey).second) {
outDependencies.push_back(dependency);
}
}
return true;
}
bool AssetDatabase::CollectShaderDependencies(
const SourceAssetRecord& sourceRecord,
std::vector<AssetDatabase::ArtifactDependencyRecord>& outDependencies) const {
outDependencies.clear();
ShaderLoader loader;
const Containers::String absolutePath =
NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
Containers::Array<Containers::String> dependencyPaths;
if (!loader.CollectSourceDependencies(absolutePath, dependencyPaths)) {
return false;
}
std::unordered_set<std::string> seenDependencyPaths;
for (const Containers::String& dependencyPath : dependencyPaths) {
if (dependencyPath.Empty() || HasVirtualPathScheme(dependencyPath)) {
continue;
}
ArtifactDependencyRecord dependency;
if (!CaptureDependencyRecord(ResolveDependencyPath(dependencyPath), dependency)) {
continue;
}
const std::string dependencyKey = ToStdString(dependency.path);
if (seenDependencyPaths.insert(dependencyKey).second) {
outDependencies.push_back(dependency);
}
}
return true;
}
Containers::String AssetDatabase::BuildArtifactDirectory(const Containers::String& artifactKey) const {
if (artifactKey.Length() < 2) {
return Containers::String("Library/Artifacts/00/invalid");
}
const Containers::String shard = artifactKey.Substring(0, 2);
return Containers::String("Library/Artifacts/") + shard + "/" + artifactKey;
}
Containers::String AssetDatabase::ReadWholeFileText(const fs::path& path) {
std::ifstream input(path, std::ios::binary);
if (!input.is_open()) {
return Containers::String();
}
std::ostringstream buffer;
buffer << input.rdbuf();
return ToContainersString(buffer.str());
}
Containers::String AssetDatabase::ComputeFileHash(const fs::path& path) {
std::ifstream input(path, std::ios::binary);
if (!input.is_open()) {
return Containers::String();
}
std::vector<Core::uint8> bytes(
(std::istreambuf_iterator<char>(input)),
std::istreambuf_iterator<char>());
if (bytes.empty()) {
return HashBytesToAssetGUID(nullptr, 0).ToString();
}
return HashBytesToAssetGUID(bytes.data(), bytes.size()).ToString();
}
Core::uint64 AssetDatabase::GetFileSizeValue(const fs::path& path) {
std::error_code ec;
const auto size = fs::file_size(path, ec);
return ec ? 0 : static_cast<Core::uint64>(size);
}
Core::uint64 AssetDatabase::GetFileWriteTimeValue(const fs::path& path) {
std::error_code ec;
const auto writeTime = fs::last_write_time(path, ec);
if (ec) {
return 0;
}
return static_cast<Core::uint64>(writeTime.time_since_epoch().count());
}
} // namespace Resources
} // namespace XCEngine