chore: checkpoint current workspace changes

This commit is contained in:
2026-04-11 22:14:02 +08:00
parent 3e55f8c204
commit 8848cfd958
227 changed files with 34027 additions and 6711 deletions

View File

@@ -1,5 +1,6 @@
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
#include <XCEngine/Core/Asset/ArtifactContainer.h>
#include <XCEngine/Core/Asset/ArtifactFormats.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
@@ -35,6 +36,45 @@ std::filesystem::path ResolveArtifactPath(const Containers::String& path) {
return resolvedPath.lexically_normal();
}
bool ReadArtifactFileData(const Containers::String& path,
ResourceType resourceType,
Containers::Array<Core::uint8>& outData) {
outData.Clear();
const std::filesystem::path resolvedPath = ResolveArtifactPath(path);
Containers::String containerError;
if (ReadArtifactContainerPayloadByPath(
Containers::String(resolvedPath.generic_string().c_str()),
resourceType,
outData,
&containerError)) {
return true;
}
std::ifstream input(resolvedPath, std::ios::binary | std::ios::ate);
if (!input.is_open()) {
return false;
}
const std::streamsize size = input.tellg();
if (size < 0) {
return false;
}
input.seekg(0, std::ios::beg);
outData.Resize(static_cast<size_t>(size));
if (size == 0) {
return true;
}
if (!input.read(reinterpret_cast<char*>(outData.Data()), size)) {
outData.Clear();
return false;
}
return true;
}
LoadResult CreateOwnedGaussianSplatResource(const Containers::String& path,
const GaussianSplatMetadata& metadata,
Containers::Array<GaussianSplatSection>&& sections,
@@ -58,6 +98,67 @@ LoadResult CreateOwnedGaussianSplatResource(const Containers::String& path,
} // namespace
bool SerializeGaussianSplatArtifactPayload(const GaussianSplat& gaussianSplat,
Containers::Array<Core::uint8>& outPayload,
Containers::String* outErrorMessage) {
(void)outErrorMessage;
GaussianSplatArtifactFileHeader fileHeader;
const GaussianSplatMetadata& metadata = gaussianSplat.GetMetadata();
GaussianSplatArtifactHeader header;
header.contentVersion = metadata.contentVersion;
header.splatCount = metadata.splatCount;
header.chunkCount = metadata.chunkCount;
header.cameraCount = metadata.cameraCount;
header.boundsMin = metadata.bounds.GetMin();
header.boundsMax = metadata.bounds.GetMax();
header.positionFormat = static_cast<Core::uint32>(metadata.positionFormat);
header.otherFormat = static_cast<Core::uint32>(metadata.otherFormat);
header.colorFormat = static_cast<Core::uint32>(metadata.colorFormat);
header.shFormat = static_cast<Core::uint32>(metadata.shFormat);
header.chunkFormat = static_cast<Core::uint32>(metadata.chunkFormat);
header.cameraFormat = static_cast<Core::uint32>(metadata.cameraFormat);
header.sectionCount = static_cast<Core::uint32>(gaussianSplat.GetSections().Size());
header.payloadSize = static_cast<Core::uint64>(gaussianSplat.GetPayloadSize());
const size_t sectionTableSize =
gaussianSplat.GetSections().Size() * sizeof(GaussianSplatArtifactSectionRecord);
const size_t totalSize =
sizeof(fileHeader) +
sizeof(header) +
sectionTableSize +
gaussianSplat.GetPayloadSize();
outPayload.Resize(totalSize);
size_t writeOffset = 0;
auto appendBytes = [&outPayload, &writeOffset](const void* source, size_t byteCount) {
if (byteCount == 0) {
return;
}
std::memcpy(outPayload.Data() + writeOffset, source, byteCount);
writeOffset += byteCount;
};
appendBytes(&fileHeader, sizeof(fileHeader));
appendBytes(&header, sizeof(header));
for (const GaussianSplatSection& section : gaussianSplat.GetSections()) {
GaussianSplatArtifactSectionRecord sectionRecord;
sectionRecord.sectionType = static_cast<Core::uint32>(section.type);
sectionRecord.format = static_cast<Core::uint32>(section.format);
sectionRecord.payloadOffset = section.dataOffset;
sectionRecord.dataSize = section.dataSize;
sectionRecord.elementCount = section.elementCount;
sectionRecord.elementStride = section.elementStride;
appendBytes(&sectionRecord, sizeof(sectionRecord));
}
appendBytes(gaussianSplat.GetPayloadData(), gaussianSplat.GetPayloadSize());
return true;
}
bool WriteGaussianSplatArtifactFile(const Containers::String& artifactPath,
const GaussianSplat& gaussianSplat,
Containers::String* outErrorMessage) {
@@ -76,6 +177,11 @@ bool WriteGaussianSplatArtifactFile(const Containers::String& artifactPath,
}
}
Containers::Array<Core::uint8> payload;
if (!SerializeGaussianSplatArtifactPayload(gaussianSplat, payload, outErrorMessage)) {
return false;
}
std::ofstream output(resolvedPath, std::ios::binary | std::ios::trunc);
if (!output.is_open()) {
if (outErrorMessage != nullptr) {
@@ -84,40 +190,8 @@ bool WriteGaussianSplatArtifactFile(const Containers::String& artifactPath,
return false;
}
GaussianSplatArtifactFileHeader fileHeader;
output.write(reinterpret_cast<const char*>(&fileHeader), sizeof(fileHeader));
const GaussianSplatMetadata& metadata = gaussianSplat.GetMetadata();
GaussianSplatArtifactHeader header;
header.contentVersion = metadata.contentVersion;
header.splatCount = metadata.splatCount;
header.chunkCount = metadata.chunkCount;
header.cameraCount = metadata.cameraCount;
header.boundsMin = metadata.bounds.GetMin();
header.boundsMax = metadata.bounds.GetMax();
header.positionFormat = static_cast<Core::uint32>(metadata.positionFormat);
header.otherFormat = static_cast<Core::uint32>(metadata.otherFormat);
header.colorFormat = static_cast<Core::uint32>(metadata.colorFormat);
header.shFormat = static_cast<Core::uint32>(metadata.shFormat);
header.chunkFormat = static_cast<Core::uint32>(metadata.chunkFormat);
header.cameraFormat = static_cast<Core::uint32>(metadata.cameraFormat);
header.sectionCount = static_cast<Core::uint32>(gaussianSplat.GetSections().Size());
header.payloadSize = static_cast<Core::uint64>(gaussianSplat.GetPayloadSize());
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
for (const GaussianSplatSection& section : gaussianSplat.GetSections()) {
GaussianSplatArtifactSectionRecord sectionRecord;
sectionRecord.sectionType = static_cast<Core::uint32>(section.type);
sectionRecord.format = static_cast<Core::uint32>(section.format);
sectionRecord.payloadOffset = section.dataOffset;
sectionRecord.dataSize = section.dataSize;
sectionRecord.elementCount = section.elementCount;
sectionRecord.elementStride = section.elementStride;
output.write(reinterpret_cast<const char*>(&sectionRecord), sizeof(sectionRecord));
}
if (gaussianSplat.GetPayloadSize() > 0) {
output.write(reinterpret_cast<const char*>(gaussianSplat.GetPayloadData()), gaussianSplat.GetPayloadSize());
if (!payload.Empty()) {
output.write(reinterpret_cast<const char*>(payload.Data()), static_cast<std::streamsize>(payload.Size()));
}
if (!output && outErrorMessage != nullptr) {
@@ -128,16 +202,26 @@ bool WriteGaussianSplatArtifactFile(const Containers::String& artifactPath,
}
LoadResult LoadGaussianSplatArtifact(const Containers::String& path) {
const std::filesystem::path resolvedPath = ResolveArtifactPath(path);
std::ifstream input(resolvedPath, std::ios::binary);
if (!input.is_open()) {
Containers::Array<Core::uint8> data;
if (!ReadArtifactFileData(path, ResourceType::GaussianSplat, data)) {
return LoadResult(Containers::String("Failed to read GaussianSplat artifact: ") + path);
}
size_t offset = 0;
auto readBytes = [&data, &offset](void* destination, size_t byteCount) -> bool {
if (offset + byteCount > data.Size()) {
return false;
}
if (byteCount > 0) {
std::memcpy(destination, data.Data() + offset, byteCount);
offset += byteCount;
}
return true;
};
GaussianSplatArtifactFileHeader fileHeader;
input.read(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
if (!input) {
if (!readBytes(&fileHeader, sizeof(fileHeader))) {
return LoadResult(Containers::String("Failed to parse GaussianSplat artifact file header: ") + path);
}
@@ -149,8 +233,7 @@ LoadResult LoadGaussianSplatArtifact(const Containers::String& path) {
}
GaussianSplatArtifactHeader header;
input.read(reinterpret_cast<char*>(&header), sizeof(header));
if (!input) {
if (!readBytes(&header, sizeof(header))) {
return LoadResult(Containers::String("Failed to parse GaussianSplat artifact header: ") + path);
}
@@ -158,8 +241,7 @@ LoadResult LoadGaussianSplatArtifact(const Containers::String& path) {
sections.Reserve(header.sectionCount);
for (Core::uint32 index = 0; index < header.sectionCount; ++index) {
GaussianSplatArtifactSectionRecord sectionRecord;
input.read(reinterpret_cast<char*>(&sectionRecord), sizeof(sectionRecord));
if (!input) {
if (!readBytes(&sectionRecord, sizeof(sectionRecord))) {
return LoadResult(Containers::String("Failed to read GaussianSplat artifact section table: ") + path);
}
@@ -175,11 +257,13 @@ LoadResult LoadGaussianSplatArtifact(const Containers::String& path) {
Containers::Array<Core::uint8> payload;
payload.Resize(static_cast<size_t>(header.payloadSize));
if (header.payloadSize > 0) {
input.read(reinterpret_cast<char*>(payload.Data()), static_cast<std::streamsize>(header.payloadSize));
if (!input) {
return LoadResult(Containers::String("Failed to read GaussianSplat artifact payload: ") + path);
}
const size_t remainingBytes = data.Size() - offset;
if (header.payloadSize > static_cast<Core::uint64>(remainingBytes)) {
return LoadResult(Containers::String("Failed to read GaussianSplat artifact payload: ") + path);
}
if (header.payloadSize > 0 &&
!readBytes(payload.Data(), static_cast<size_t>(header.payloadSize))) {
return LoadResult(Containers::String("Failed to read GaussianSplat artifact payload: ") + path);
}
GaussianSplatMetadata metadata;

View File

@@ -1,4 +1,5 @@
#include <XCEngine/Resources/Material/MaterialLoader.h>
#include <XCEngine/Core/Asset/ArtifactContainer.h>
#include <XCEngine/Core/Asset/ArtifactFormats.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
@@ -19,6 +20,32 @@ namespace Resources {
namespace {
Containers::String GetPathExtensionString(const Containers::String& path) {
const std::filesystem::path fsPath(path.CStr());
const std::string extension = fsPath.has_extension()
? fsPath.extension().string()
: std::string();
if (!extension.empty() && extension[0] == '.') {
return Containers::String(extension.substr(1).c_str());
}
return Containers::String(extension.c_str());
}
Containers::String GetMaterialPathExtension(const Containers::String& path) {
Containers::String containerPath;
Containers::String entryName;
if (TryParseArtifactContainerEntryPath(path, containerPath, entryName)) {
const Containers::String entryExtension = GetPathExtensionString(entryName);
if (!entryExtension.Empty()) {
return entryExtension;
}
return GetPathExtensionString(containerPath);
}
return GetPathExtensionString(path);
}
std::string ToStdString(const Containers::Array<Core::uint8>& data) {
return std::string(reinterpret_cast<const char*>(data.Data()), data.Size());
}
@@ -27,6 +54,17 @@ Containers::Array<Core::uint8> ReadMaterialArtifactFileData(const Containers::St
Containers::Array<Core::uint8> data;
auto tryRead = [&data](const std::filesystem::path& filePath, bool& opened) {
opened = false;
Containers::String containerError;
if (ReadArtifactContainerPayloadByPath(
Containers::String(filePath.generic_string().c_str()),
ResourceType::Material,
data,
&containerError)) {
opened = true;
return;
}
std::ifstream file(filePath, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
opened = false;
@@ -143,12 +181,27 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
}
std::filesystem::path dependencyFsPath(dependencyPath.CStr());
Containers::String containerPathText;
Containers::String entryName;
const bool isContainerEntryPath =
TryParseArtifactContainerEntryPath(dependencyPath, containerPathText, entryName);
if (isContainerEntryPath) {
dependencyFsPath = std::filesystem::path(containerPathText.CStr());
}
auto rebuildResolvedPath = [&entryName, isContainerEntryPath](const std::filesystem::path& resolvedPath) {
const Containers::String normalizedPath = NormalizePathString(resolvedPath);
return isContainerEntryPath
? BuildArtifactContainerEntryPath(normalizedPath, entryName)
: normalizedPath;
};
if (dependencyFsPath.is_absolute() && std::filesystem::exists(dependencyFsPath)) {
return NormalizePathString(dependencyFsPath);
return rebuildResolvedPath(dependencyFsPath);
}
if (std::filesystem::exists(dependencyFsPath)) {
return NormalizePathString(dependencyFsPath);
return rebuildResolvedPath(dependencyFsPath);
}
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
@@ -156,7 +209,7 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
const std::filesystem::path projectRelativeCandidate =
std::filesystem::path(resourceRoot.CStr()) / dependencyFsPath;
if (std::filesystem::exists(projectRelativeCandidate)) {
return NormalizePathString(projectRelativeCandidate);
return rebuildResolvedPath(projectRelativeCandidate);
}
}
@@ -168,7 +221,7 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
const std::filesystem::path ownerRelativeCandidate =
ownerArtifactFsPath.parent_path() / dependencyFsPath;
if (std::filesystem::exists(ownerRelativeCandidate)) {
return NormalizePathString(ownerRelativeCandidate);
return rebuildResolvedPath(ownerRelativeCandidate);
}
std::filesystem::path current = ownerArtifactFsPath.parent_path();
@@ -178,7 +231,7 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
if (!projectRoot.empty()) {
const std::filesystem::path projectRelativeCandidate = projectRoot / dependencyFsPath;
if (std::filesystem::exists(projectRelativeCandidate)) {
return NormalizePathString(projectRelativeCandidate);
return rebuildResolvedPath(projectRelativeCandidate);
}
}
break;
@@ -1800,7 +1853,7 @@ bool MaterialLoader::CanLoad(const Containers::String& path) const {
return true;
}
Containers::String ext = GetExtension(path).ToLower();
const Containers::String ext = GetMaterialPathExtension(path).ToLower();
return ext == "mat" || ext == "material" || ext == "json" || ext == "xcmat";
}
@@ -1811,7 +1864,7 @@ LoadResult MaterialLoader::Load(const Containers::String& path, const ImportSett
return CreateBuiltinMaterialResource(path);
}
const Containers::String ext = GetExtension(path).ToLower();
const Containers::String ext = GetMaterialPathExtension(path).ToLower();
if (ext == "xcmat") {
return LoadMaterialArtifact(path);
}

View File

@@ -32,6 +32,32 @@ namespace Resources {
namespace {
Containers::String GetPathExtensionString(const Containers::String& path) {
const std::filesystem::path fsPath(path.CStr());
const std::string extension = fsPath.has_extension()
? fsPath.extension().string()
: std::string();
if (!extension.empty() && extension[0] == '.') {
return Containers::String(extension.substr(1).c_str());
}
return Containers::String(extension.c_str());
}
Containers::String GetMeshPathExtension(const Containers::String& path) {
Containers::String containerPath;
Containers::String entryName;
if (TryParseArtifactContainerEntryPath(path, containerPath, entryName)) {
const Containers::String entryExtension = GetPathExtensionString(entryName);
if (!entryExtension.Empty()) {
return entryExtension;
}
return GetPathExtensionString(containerPath);
}
return GetPathExtensionString(path);
}
struct ImportedMeshData {
std::vector<StaticMeshVertex> vertices;
std::vector<Core::uint32> indices;
@@ -1074,7 +1100,7 @@ bool MeshLoader::CanLoad(const Containers::String& path) const {
return true;
}
Containers::String ext = GetExtension(path).ToLower();
const Containers::String ext = GetMeshPathExtension(path).ToLower();
return ext == "fbx" || ext == "obj" || ext == "gltf" ||
ext == "glb" || ext == "dae" || ext == "stl" ||
@@ -1086,7 +1112,7 @@ LoadResult MeshLoader::Load(const Containers::String& path, const ImportSettings
return CreateBuiltinMeshResource(path);
}
const Containers::String ext = GetExtension(path).ToLower();
const Containers::String ext = GetMeshPathExtension(path).ToLower();
if (!CanLoad(path)) {
return LoadResult(Containers::String("Unsupported mesh format: ") + ext);

View File

@@ -1,5 +1,6 @@
#include <XCEngine/Resources/Model/ModelArtifactIO.h>
#include <XCEngine/Core/Asset/ArtifactContainer.h>
#include <XCEngine/Core/Asset/ArtifactFormats.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Resources/Model/Model.h>
@@ -7,6 +8,7 @@
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
namespace XCEngine {
@@ -35,7 +37,46 @@ std::filesystem::path ResolveArtifactPath(const Containers::String& path) {
return resolvedPath.lexically_normal();
}
void WriteString(std::ofstream& stream, const Containers::String& value) {
bool ReadArtifactFileData(const Containers::String& path,
ResourceType resourceType,
Containers::Array<Core::uint8>& outData) {
outData.Clear();
const std::filesystem::path resolvedPath = ResolveArtifactPath(path);
Containers::String containerError;
if (ReadArtifactContainerPayloadByPath(
Containers::String(resolvedPath.generic_string().c_str()),
resourceType,
outData,
&containerError)) {
return true;
}
std::ifstream input(resolvedPath, std::ios::binary | std::ios::ate);
if (!input.is_open()) {
return false;
}
const std::streamsize size = input.tellg();
if (size < 0) {
return false;
}
input.seekg(0, std::ios::beg);
outData.Resize(static_cast<size_t>(size));
if (size == 0) {
return true;
}
if (!input.read(reinterpret_cast<char*>(outData.Data()), size)) {
outData.Clear();
return false;
}
return true;
}
void WriteString(std::ostream& 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) {
@@ -43,20 +84,65 @@ void WriteString(std::ofstream& stream, const Containers::String& value) {
}
}
Containers::String ReadString(std::ifstream& stream) {
bool ReadString(const Containers::Array<Core::uint8>& data,
size_t& offset,
Containers::String& outValue) {
Core::uint32 length = 0;
stream.read(reinterpret_cast<char*>(&length), sizeof(length));
if (!stream || length == 0) {
return Containers::String();
if (offset + sizeof(length) > data.Size()) {
return false;
}
std::string buffer(length, '\0');
stream.read(buffer.data(), length);
if (!stream) {
return Containers::String();
std::memcpy(&length, data.Data() + offset, sizeof(length));
offset += sizeof(length);
if (length == 0) {
outValue.Clear();
return true;
}
return Containers::String(buffer.c_str());
if (offset + length > data.Size()) {
outValue.Clear();
return false;
}
outValue = Containers::String(reinterpret_cast<const char*>(data.Data() + offset), length);
offset += length;
return true;
}
template<typename T>
bool ReadValue(const Containers::Array<Core::uint8>& data, size_t& offset, T& outValue) {
if (offset + sizeof(T) > data.Size()) {
return false;
}
std::memcpy(&outValue, data.Data() + offset, sizeof(T));
offset += sizeof(T);
return true;
}
Containers::Array<Core::uint8> ToByteArray(const std::string& text) {
Containers::Array<Core::uint8> bytes;
if (text.empty()) {
return bytes;
}
bytes.ResizeUninitialized(text.size());
std::memcpy(bytes.Data(), text.data(), text.size());
return bytes;
}
ModelArtifactHeader BuildModelArtifactHeader(const Model& model) {
ModelArtifactHeader header;
header.nodeCount = static_cast<Core::uint32>(model.GetNodes().Size());
header.meshBindingCount = static_cast<Core::uint32>(model.GetMeshBindings().Size());
header.materialBindingCount = static_cast<Core::uint32>(model.GetMaterialBindings().Size());
header.rootNodeIndex = model.GetRootNodeIndex();
return header;
}
bool ValidateModelArtifactHeader(const ModelArtifactHeader& header) {
return header.rootNodeIndex == kInvalidModelNodeIndex ||
header.rootNodeIndex < header.nodeCount;
}
LoadResult CreateOwnedModelResource(const Containers::String& path,
@@ -94,40 +180,17 @@ LoadResult CreateOwnedModelResource(const Containers::String& path,
} // namespace
bool WriteModelArtifactFile(const Containers::String& artifactPath,
const Model& model,
Containers::String* outErrorMessage) {
const std::filesystem::path resolvedPath = ResolveArtifactPath(artifactPath);
std::error_code ec;
const std::filesystem::path parentPath = resolvedPath.parent_path();
if (!parentPath.empty()) {
std::filesystem::create_directories(parentPath, ec);
if (ec) {
if (outErrorMessage != nullptr) {
*outErrorMessage =
Containers::String("Failed to create model artifact directory: ") +
Containers::String(parentPath.generic_string().c_str());
}
return false;
}
}
bool SerializeModelArtifactPayload(const Model& model,
Containers::Array<Core::uint8>& outPayload,
Containers::String* outErrorMessage) {
(void)outErrorMessage;
std::ofstream output(resolvedPath, std::ios::binary | std::ios::trunc);
if (!output.is_open()) {
if (outErrorMessage != nullptr) {
*outErrorMessage = Containers::String("Failed to open model artifact for write: ") + artifactPath;
}
return false;
}
std::ostringstream output(std::ios::binary | std::ios::out);
ModelArtifactFileHeader fileHeader;
output.write(reinterpret_cast<const char*>(&fileHeader), sizeof(fileHeader));
ModelArtifactHeader header;
header.nodeCount = static_cast<Core::uint32>(model.GetNodes().Size());
header.meshBindingCount = static_cast<Core::uint32>(model.GetMeshBindings().Size());
header.materialBindingCount = static_cast<Core::uint32>(model.GetMaterialBindings().Size());
header.rootNodeIndex = model.GetRootNodeIndex();
const ModelArtifactHeader header = BuildModelArtifactHeader(model);
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
for (const ModelNode& node : model.GetNodes()) {
@@ -158,27 +221,25 @@ bool WriteModelArtifactFile(const Containers::String& artifactPath,
output.write(reinterpret_cast<const char*>(&bindingArtifact), sizeof(bindingArtifact));
}
if (!output && outErrorMessage != nullptr) {
*outErrorMessage = Containers::String("Failed to write model artifact: ") + artifactPath;
if (!output) {
outPayload.Clear();
return false;
}
return static_cast<bool>(output);
outPayload = ToByteArray(output.str());
return true;
}
LoadResult LoadModelArtifact(const Containers::String& path) {
const std::filesystem::path resolvedPath = ResolveArtifactPath(path);
namespace {
std::ifstream input(resolvedPath, std::ios::binary);
if (!input.is_open()) {
return LoadResult(Containers::String("Failed to read model artifact: ") + path);
}
LoadResult ParseModelArtifactPayload(const Containers::String& path,
const Containers::Array<Core::uint8>& data) {
size_t offset = 0;
ModelArtifactFileHeader fileHeader;
input.read(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
if (!input) {
if (!ReadValue(data, offset, fileHeader)) {
return LoadResult(Containers::String("Failed to parse model artifact file header: ") + path);
}
const bool validFileHeader =
std::memcmp(fileHeader.magic, "XCMOD01", 7) == 0 &&
fileHeader.schemaVersion == kModelArtifactSchemaVersion;
@@ -187,13 +248,11 @@ LoadResult LoadModelArtifact(const Containers::String& path) {
}
ModelArtifactHeader header;
input.read(reinterpret_cast<char*>(&header), sizeof(header));
if (!input) {
if (!ReadValue(data, offset, header)) {
return LoadResult(Containers::String("Failed to parse model artifact header: ") + path);
}
if (header.rootNodeIndex != kInvalidModelNodeIndex &&
header.rootNodeIndex >= header.nodeCount) {
if (!ValidateModelArtifactHeader(header)) {
return LoadResult(Containers::String("Invalid model artifact root node index: ") + path);
}
@@ -201,11 +260,12 @@ LoadResult LoadModelArtifact(const Containers::String& path) {
nodes.Reserve(header.nodeCount);
for (Core::uint32 index = 0; index < header.nodeCount; ++index) {
ModelNode node;
node.name = ReadString(input);
if (!ReadString(data, offset, node.name)) {
return LoadResult(Containers::String("Failed to read model node artifact name: ") + path);
}
ModelNodeArtifactHeader nodeHeader;
input.read(reinterpret_cast<char*>(&nodeHeader), sizeof(nodeHeader));
if (!input) {
if (!ReadValue(data, offset, nodeHeader)) {
return LoadResult(Containers::String("Failed to read model node artifact: ") + path);
}
@@ -222,8 +282,7 @@ LoadResult LoadModelArtifact(const Containers::String& path) {
meshBindings.Reserve(header.meshBindingCount);
for (Core::uint32 index = 0; index < header.meshBindingCount; ++index) {
ModelMeshBindingArtifact bindingArtifact;
input.read(reinterpret_cast<char*>(&bindingArtifact), sizeof(bindingArtifact));
if (!input) {
if (!ReadValue(data, offset, bindingArtifact)) {
return LoadResult(Containers::String("Failed to read model mesh binding artifact: ") + path);
}
@@ -238,8 +297,7 @@ LoadResult LoadModelArtifact(const Containers::String& path) {
materialBindings.Reserve(header.materialBindingCount);
for (Core::uint32 index = 0; index < header.materialBindingCount; ++index) {
ModelMaterialBindingArtifact bindingArtifact;
input.read(reinterpret_cast<char*>(&bindingArtifact), sizeof(bindingArtifact));
if (!input) {
if (!ReadValue(data, offset, bindingArtifact)) {
return LoadResult(Containers::String("Failed to read model material binding artifact: ") + path);
}
@@ -257,5 +315,57 @@ LoadResult LoadModelArtifact(const Containers::String& path) {
std::move(materialBindings));
}
} // namespace
bool WriteModelArtifactFile(const Containers::String& artifactPath,
const Model& model,
Containers::String* outErrorMessage) {
const std::filesystem::path resolvedPath = ResolveArtifactPath(artifactPath);
std::error_code ec;
const std::filesystem::path parentPath = resolvedPath.parent_path();
if (!parentPath.empty()) {
std::filesystem::create_directories(parentPath, ec);
if (ec) {
if (outErrorMessage != nullptr) {
*outErrorMessage =
Containers::String("Failed to create model artifact directory: ") +
Containers::String(parentPath.generic_string().c_str());
}
return false;
}
}
Containers::Array<Core::uint8> payload;
if (!SerializeModelArtifactPayload(model, payload, outErrorMessage)) {
return false;
}
std::ofstream output(resolvedPath, std::ios::binary | std::ios::trunc);
if (!output.is_open()) {
if (outErrorMessage != nullptr) {
*outErrorMessage = Containers::String("Failed to open model artifact for write: ") + artifactPath;
}
return false;
}
if (!payload.Empty()) {
output.write(reinterpret_cast<const char*>(payload.Data()), static_cast<std::streamsize>(payload.Size()));
}
if (!output && outErrorMessage != nullptr) {
*outErrorMessage = Containers::String("Failed to write model artifact: ") + artifactPath;
}
return static_cast<bool>(output);
}
LoadResult LoadModelArtifact(const Containers::String& path) {
Containers::Array<Core::uint8> data;
if (!ReadArtifactFileData(path, ResourceType::Model, data)) {
return LoadResult(Containers::String("Failed to read model artifact: ") + path);
}
return ParseModelArtifactPayload(path, data);
}
} // namespace Resources
} // namespace XCEngine

View File

@@ -1,5 +1,6 @@
#include "ShaderFileUtils.h"
#include <XCEngine/Core/Asset/ArtifactContainer.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <filesystem>
@@ -15,6 +16,16 @@ Containers::Array<Core::uint8> TryReadFileData(
bool& opened) {
Containers::Array<Core::uint8> data;
Containers::String payloadError;
if (ReadArtifactContainerPayloadByPath(
Containers::String(filePath.generic_string().c_str()),
ResourceType::Shader,
data,
&payloadError)) {
opened = true;
return data;
}
std::ifstream file(filePath, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
opened = false;
@@ -69,9 +80,16 @@ bool ReadShaderTextFile(const Containers::String& path, Containers::String& outT
}
Containers::String GetShaderPathExtension(const Containers::String& path) {
Containers::String normalizedPath = path;
Containers::String containerPath;
Containers::String entryName;
if (TryParseArtifactContainerEntryPath(path, containerPath, entryName)) {
normalizedPath = containerPath;
}
size_t dotPos = Containers::String::npos;
for (size_t i = path.Length(); i > 0; --i) {
if (path[i - 1] == '.') {
for (size_t i = normalizedPath.Length(); i > 0; --i) {
if (normalizedPath[i - 1] == '.') {
dotPos = i - 1;
break;
}
@@ -81,7 +99,7 @@ Containers::String GetShaderPathExtension(const Containers::String& path) {
return Containers::String();
}
return path.Substring(dotPos + 1);
return normalizedPath.Substring(dotPos + 1);
}
ShaderType DetectShaderTypeFromPath(const Containers::String& path) {

View File

@@ -0,0 +1,388 @@
#include <XCEngine/Resources/Shader/ShaderCompilationCache.h>
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
namespace XCEngine {
namespace Resources {
namespace fs = std::filesystem;
namespace {
struct ShaderCompilationCacheFileHeader {
char magic[8] = { 'X', 'C', 'B', 'C', '0', '1', '\0', '\0' };
Core::uint32 schemaVersion = kShaderCompilationCacheSchemaVersion;
Core::uint32 backend = 0;
Core::uint32 format = 0;
Core::uint32 reserved = 0;
Core::uint64 payloadSize = 0;
Core::uint64 compileKeyHigh = 0;
Core::uint64 compileKeyLow = 0;
};
Containers::String MakeError(const char* message) {
return Containers::String(message == nullptr ? "" : message);
}
std::string ToStdString(const Containers::String& text) {
return std::string(text.CStr(), text.Length());
}
Containers::String ToContainersString(const std::string& text) {
return Containers::String(text.c_str(), text.size());
}
std::vector<std::string> SplitFields(const std::string& line) {
std::vector<std::string> fields;
std::stringstream stream(line);
std::string field;
while (std::getline(stream, field, '\t')) {
fields.push_back(field);
}
return fields;
}
bool IsExpectedMagic(const ShaderCompilationCacheFileHeader& header) {
return std::memcmp(header.magic, "XCBC01", 6) == 0 &&
header.schemaVersion == kShaderCompilationCacheSchemaVersion;
}
} // namespace
void ShaderCompileKey::Normalize() {
shaderPath = shaderPath.Trim();
sourceHash = sourceHash.Trim();
dependencyHash = dependencyHash.Trim();
passName = passName.Trim();
entryPoint = entryPoint.Trim();
profile = profile.Trim();
compilerName = compilerName.Trim();
compilerVersion = compilerVersion.Trim();
optionsSignature = optionsSignature.Trim();
std::vector<std::string> normalizedKeywords;
normalizedKeywords.reserve(keywords.Size());
for (const Containers::String& keyword : keywords) {
const Containers::String trimmedKeyword = keyword.Trim();
if (!trimmedKeyword.Empty()) {
normalizedKeywords.push_back(ToStdString(trimmedKeyword));
}
}
std::sort(normalizedKeywords.begin(), normalizedKeywords.end());
normalizedKeywords.erase(
std::unique(normalizedKeywords.begin(), normalizedKeywords.end()),
normalizedKeywords.end());
Containers::Array<Containers::String> canonicalKeywords;
canonicalKeywords.Reserve(normalizedKeywords.size());
for (const std::string& keyword : normalizedKeywords) {
canonicalKeywords.PushBack(ToContainersString(keyword));
}
keywords = std::move(canonicalKeywords);
}
Containers::String ShaderCompileKey::BuildSignature() const {
ShaderCompileKey normalizedKey = *this;
normalizedKey.Normalize();
Containers::String signature;
signature += "shader=";
signature += normalizedKey.shaderPath;
signature += "\nsourceHash=";
signature += normalizedKey.sourceHash;
signature += "\ndependencyHash=";
signature += normalizedKey.dependencyHash;
signature += "\npass=";
signature += normalizedKey.passName;
signature += "\nentry=";
signature += normalizedKey.entryPoint;
signature += "\nprofile=";
signature += normalizedKey.profile;
signature += "\ncompiler=";
signature += normalizedKey.compilerName;
signature += "\ncompilerVersion=";
signature += normalizedKey.compilerVersion;
signature += "\noptions=";
signature += normalizedKey.optionsSignature;
signature += "\nstage=";
signature += ToContainersString(std::to_string(static_cast<Core::uint32>(normalizedKey.stage)));
signature += "\nsourceLanguage=";
signature += ToContainersString(std::to_string(static_cast<Core::uint32>(normalizedKey.sourceLanguage)));
signature += "\nbackend=";
signature += ToContainersString(std::to_string(static_cast<Core::uint32>(normalizedKey.backend)));
for (const Containers::String& keyword : normalizedKey.keywords) {
signature += "\nkeyword=";
signature += keyword;
}
return signature;
}
Containers::String ShaderCompileKey::BuildCacheKey() const {
return HashStringToAssetGUID(BuildSignature()).ToString();
}
void ShaderCompilationCache::Initialize(const Containers::String& libraryRoot) {
m_libraryRoot = libraryRoot.Trim();
m_databasePath = m_libraryRoot + "/shadercache.db";
LoadDatabase();
}
void ShaderCompilationCache::Shutdown() {
m_libraryRoot.Clear();
m_databasePath.Clear();
m_records.clear();
}
Containers::String ShaderCompilationCache::BuildCacheKey(const ShaderCompileKey& key) const {
return key.BuildCacheKey();
}
Containers::String ShaderCompilationCache::BuildBackendDirectoryName(ShaderBackend backend) {
switch (backend) {
case ShaderBackend::D3D12:
return Containers::String("D3D12");
case ShaderBackend::OpenGL:
return Containers::String("OpenGL");
case ShaderBackend::Vulkan:
return Containers::String("Vulkan");
case ShaderBackend::Generic:
default:
return Containers::String("Generic");
}
}
AssetGUID ShaderCompilationCache::ComputeKeyGuid(const Containers::String& cacheKey) {
AssetGUID guid;
if (AssetGUID::TryParse(cacheKey, guid)) {
return guid;
}
return HashStringToAssetGUID(cacheKey);
}
Containers::String ShaderCompilationCache::BuildCacheRelativePath(const ShaderCompileKey& key) const {
const Containers::String cacheKey = BuildCacheKey(key);
const Containers::String shard =
cacheKey.Length() >= 2 ? cacheKey.Substring(0, 2) : Containers::String("00");
return Containers::String("ShaderCache/") +
BuildBackendDirectoryName(key.backend) +
"/" +
shard +
"/" +
cacheKey +
".xcbc";
}
Containers::String ShaderCompilationCache::BuildCacheAbsolutePath(const ShaderCompileKey& key) const {
if (m_libraryRoot.Empty()) {
return Containers::String();
}
return m_libraryRoot + "/" + BuildCacheRelativePath(key);
}
void ShaderCompilationCache::LoadDatabase() {
m_records.clear();
std::ifstream input(m_databasePath.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() < 5) {
continue;
}
ShaderCacheRecord record;
record.backend = static_cast<ShaderBackend>(std::stoul(fields[1]));
record.relativePath = ToContainersString(fields[2]);
record.format = static_cast<ShaderBytecodeFormat>(std::stoul(fields[3]));
record.payloadSize = static_cast<Core::uint64>(std::stoull(fields[4]));
m_records[fields[0]] = record;
}
}
void ShaderCompilationCache::SaveDatabase() const {
if (m_databasePath.Empty()) {
return;
}
const fs::path dbPath(m_databasePath.CStr());
std::error_code ec;
if (!dbPath.parent_path().empty()) {
fs::create_directories(dbPath.parent_path(), ec);
if (ec) {
return;
}
}
std::ofstream output(dbPath, std::ios::out | std::ios::trunc);
if (!output.is_open()) {
return;
}
output << "# compileKey\tbackend\trelativePath\tformat\tpayloadSize\n";
for (const auto& [compileKey, record] : m_records) {
output << compileKey << '\t'
<< static_cast<Core::uint32>(record.backend) << '\t'
<< ToStdString(record.relativePath) << '\t'
<< static_cast<Core::uint32>(record.format) << '\t'
<< record.payloadSize << '\n';
}
}
bool ShaderCompilationCache::Store(const ShaderCacheEntry& entry,
Containers::String* outErrorMessage) {
if (!IsInitialized()) {
if (outErrorMessage != nullptr) {
*outErrorMessage = MakeError("ShaderCompilationCache is not initialized.");
}
return false;
}
const Containers::String cacheKey = BuildCacheKey(entry.key);
const Containers::String relativePath = BuildCacheRelativePath(entry.key);
const Containers::String absolutePath = BuildCacheAbsolutePath(entry.key);
if (absolutePath.Empty()) {
if (outErrorMessage != nullptr) {
*outErrorMessage = MakeError("ShaderCompilationCache absolute path is empty.");
}
return false;
}
std::error_code ec;
const fs::path cachePath(absolutePath.CStr());
fs::create_directories(cachePath.parent_path(), ec);
if (ec) {
if (outErrorMessage != nullptr) {
*outErrorMessage = MakeError("Failed to create ShaderCompilationCache directory.");
}
return false;
}
std::ofstream output(cachePath, std::ios::binary | std::ios::trunc);
if (!output.is_open()) {
if (outErrorMessage != nullptr) {
*outErrorMessage = MakeError("Failed to open ShaderCompilationCache output file.");
}
return false;
}
const AssetGUID keyGuid = ComputeKeyGuid(cacheKey);
ShaderCompilationCacheFileHeader fileHeader = {};
fileHeader.backend = static_cast<Core::uint32>(entry.key.backend);
fileHeader.format = static_cast<Core::uint32>(entry.format);
fileHeader.payloadSize = static_cast<Core::uint64>(entry.payload.Size());
fileHeader.compileKeyHigh = keyGuid.high;
fileHeader.compileKeyLow = keyGuid.low;
output.write(reinterpret_cast<const char*>(&fileHeader), sizeof(fileHeader));
if (!output) {
if (outErrorMessage != nullptr) {
*outErrorMessage = MakeError("Failed to write ShaderCompilationCache header.");
}
return false;
}
if (!entry.payload.Empty()) {
output.write(reinterpret_cast<const char*>(entry.payload.Data()),
static_cast<std::streamsize>(entry.payload.Size()));
if (!output) {
if (outErrorMessage != nullptr) {
*outErrorMessage = MakeError("Failed to write ShaderCompilationCache payload.");
}
return false;
}
}
ShaderCacheRecord record;
record.backend = entry.key.backend;
record.format = entry.format;
record.relativePath = relativePath;
record.payloadSize = static_cast<Core::uint64>(entry.payload.Size());
m_records[ToStdString(cacheKey)] = record;
SaveDatabase();
return true;
}
bool ShaderCompilationCache::TryLoad(const ShaderCompileKey& key,
ShaderCacheEntry& outEntry,
Containers::String* outErrorMessage) const {
outEntry = {};
if (!IsInitialized()) {
if (outErrorMessage != nullptr) {
*outErrorMessage = MakeError("ShaderCompilationCache is not initialized.");
}
return false;
}
const Containers::String cacheKey = BuildCacheKey(key);
Containers::String relativePath = BuildCacheRelativePath(key);
const auto recordIt = m_records.find(ToStdString(cacheKey));
if (recordIt != m_records.end() && !recordIt->second.relativePath.Empty()) {
relativePath = recordIt->second.relativePath;
}
const Containers::String absolutePath = m_libraryRoot + "/" + relativePath;
std::ifstream input(absolutePath.CStr(), std::ios::binary);
if (!input.is_open()) {
if (outErrorMessage != nullptr) {
*outErrorMessage = MakeError("ShaderCompilationCache entry does not exist.");
}
return false;
}
ShaderCompilationCacheFileHeader fileHeader = {};
input.read(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
if (!input || !IsExpectedMagic(fileHeader)) {
if (outErrorMessage != nullptr) {
*outErrorMessage = MakeError("ShaderCompilationCache header is invalid.");
}
return false;
}
const AssetGUID expectedKeyGuid = ComputeKeyGuid(cacheKey);
const AssetGUID actualKeyGuid(fileHeader.compileKeyHigh, fileHeader.compileKeyLow);
if (expectedKeyGuid != actualKeyGuid ||
fileHeader.backend != static_cast<Core::uint32>(key.backend)) {
if (outErrorMessage != nullptr) {
*outErrorMessage = MakeError("ShaderCompilationCache entry does not match the requested key.");
}
return false;
}
Containers::Array<Core::uint8> payload;
payload.ResizeUninitialized(static_cast<size_t>(fileHeader.payloadSize));
if (fileHeader.payloadSize > 0) {
input.read(reinterpret_cast<char*>(payload.Data()),
static_cast<std::streamsize>(fileHeader.payloadSize));
if (!input) {
if (outErrorMessage != nullptr) {
*outErrorMessage = MakeError("Failed to read ShaderCompilationCache payload.");
}
return false;
}
}
outEntry.key = key;
outEntry.format = static_cast<ShaderBytecodeFormat>(fileHeader.format);
outEntry.payload = std::move(payload);
return true;
}
} // namespace Resources
} // namespace XCEngine

View File

@@ -1,9 +1,11 @@
#include <XCEngine/Resources/Texture/TextureLoader.h>
#include <XCEngine/Core/Asset/ArtifactContainer.h>
#include <XCEngine/Core/Asset/ArtifactFormats.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Resources/Texture/TextureImportSettings.h>
#include <stb_image.h>
#include <cstring>
#include <filesystem>
#include <fstream>
@@ -12,6 +14,32 @@ namespace Resources {
namespace {
Containers::String GetPathExtensionString(const Containers::String& path) {
const std::filesystem::path fsPath(path.CStr());
const std::string extension = fsPath.has_extension()
? fsPath.extension().string()
: std::string();
if (!extension.empty() && extension[0] == '.') {
return Containers::String(extension.substr(1).c_str());
}
return Containers::String(extension.c_str());
}
Containers::String GetTexturePathExtension(const Containers::String& path) {
Containers::String containerPath;
Containers::String entryName;
if (TryParseArtifactContainerEntryPath(path, containerPath, entryName)) {
const Containers::String entryExtension = GetPathExtensionString(entryName);
if (!entryExtension.Empty()) {
return entryExtension;
}
return GetPathExtensionString(containerPath);
}
return GetPathExtensionString(path);
}
Containers::String GetResourceNameFromPath(const Containers::String& path) {
const std::filesystem::path filePath(path.CStr());
const std::string fileName = filePath.filename().string();
@@ -76,37 +104,76 @@ TextureFormat ResolveDecodedTextureFormat(const ImportSettings* settings, bool i
}
LoadResult LoadTextureArtifact(const Containers::String& path) {
auto readFileData = [](const std::filesystem::path& filePath,
Containers::Array<Core::uint8>& outData,
bool& opened) {
opened = false;
outData.Clear();
Containers::String payloadError;
if (ReadArtifactContainerPayloadByPath(
Containers::String(filePath.generic_string().c_str()),
ResourceType::Texture,
outData,
&payloadError)) {
opened = true;
return;
}
std::ifstream input(filePath, std::ios::binary | std::ios::ate);
if (!input.is_open()) {
return;
}
opened = true;
const std::streamsize size = input.tellg();
if (size <= 0) {
return;
}
input.seekg(0, std::ios::beg);
outData.Resize(static_cast<size_t>(size));
if (!input.read(reinterpret_cast<char*>(outData.Data()), size)) {
outData.Clear();
}
};
Containers::Array<Core::uint8> data;
bool opened = false;
std::filesystem::path resolvedPath(path.CStr());
if (!resolvedPath.is_absolute() && !std::filesystem::exists(resolvedPath)) {
readFileData(resolvedPath, data, opened);
if (!opened && !resolvedPath.is_absolute()) {
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
if (!resourceRoot.Empty()) {
resolvedPath = std::filesystem::path(resourceRoot.CStr()) / resolvedPath;
readFileData(resolvedPath, data, opened);
}
}
std::ifstream input(resolvedPath, std::ios::binary);
if (!input.is_open()) {
if (!opened || data.Size() < sizeof(TextureArtifactHeader)) {
return LoadResult(Containers::String("Failed to read texture artifact: ") + path);
}
TextureArtifactHeader header;
input.read(reinterpret_cast<char*>(&header), sizeof(header));
if (!input) {
return LoadResult(Containers::String("Failed to parse texture artifact header: ") + path);
}
std::memcpy(&header, data.Data(), sizeof(header));
const std::string magic(header.magic, header.magic + 7);
if (magic != "XCTEX01") {
return LoadResult(Containers::String("Invalid texture artifact magic: ") + path);
}
const size_t payloadOffset = sizeof(TextureArtifactHeader);
if (header.pixelDataSize > static_cast<Core::uint64>(data.Size() - payloadOffset)) {
return LoadResult(Containers::String("Failed to read texture artifact payload: ") + path);
}
Containers::Array<Core::uint8> pixelData;
pixelData.Resize(static_cast<size_t>(header.pixelDataSize));
if (header.pixelDataSize > 0) {
input.read(reinterpret_cast<char*>(pixelData.Data()), static_cast<std::streamsize>(header.pixelDataSize));
if (!input) {
return LoadResult(Containers::String("Failed to read texture artifact payload: ") + path);
}
std::memcpy(
pixelData.Data(),
data.Data() + payloadOffset,
static_cast<size_t>(header.pixelDataSize));
}
return CreateTextureResource(path,
@@ -150,7 +217,7 @@ bool TextureLoader::CanLoad(const Containers::String& path) const {
return true;
}
Containers::String ext = GetExtension(path).ToLower();
Containers::String ext = GetTexturePathExtension(path).ToLower();
return ext == "png" || ext == "jpg" || ext == "jpeg" ||
ext == "tga" || ext == "bmp" || ext == "gif" ||
@@ -162,7 +229,7 @@ LoadResult TextureLoader::Load(const Containers::String& path, const ImportSetti
return CreateBuiltinTextureResource(path);
}
Containers::String ext = GetExtension(path).ToLower();
Containers::String ext = GetTexturePathExtension(path).ToLower();
if (!CanLoad(path)) {
return LoadResult(Containers::String("Unsupported texture format: ") + ext);

View File

@@ -1,5 +1,6 @@
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
#include <XCEngine/Core/Asset/ArtifactContainer.h>
#include <XCEngine/Core/Asset/ArtifactFormats.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
@@ -53,7 +54,15 @@ Containers::String FormatDiagnosticMessage(const Containers::String& path,
message;
}
void WriteString(std::ofstream& stream, const Containers::String& value) {
bool BuildSchemaDefinition(
const Containers::String& sourcePath,
const UIDocumentNode& rootNode,
const Containers::String& displayName,
UISchemaDefinition& outDefinition,
Containers::Array<UIDocumentDiagnostic>& diagnostics,
Containers::String& outErrorMessage);
void WriteString(std::ostream& 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) {
@@ -61,7 +70,7 @@ void WriteString(std::ofstream& stream, const Containers::String& value) {
}
}
bool ReadString(std::ifstream& stream, Containers::String& outValue) {
bool ReadString(std::istream& stream, Containers::String& outValue) {
Core::uint32 length = 0;
stream.read(reinterpret_cast<char*>(&length), sizeof(length));
if (!stream) {
@@ -83,7 +92,7 @@ bool ReadString(std::ifstream& stream, Containers::String& outValue) {
return true;
}
bool WriteNode(std::ofstream& stream, const UIDocumentNode& node) {
bool WriteNode(std::ostream& stream, const UIDocumentNode& node) {
WriteString(stream, node.tagName);
UIDocumentArtifactNodeHeader header;
@@ -114,7 +123,7 @@ bool WriteNode(std::ofstream& stream, const UIDocumentNode& node) {
return static_cast<bool>(stream);
}
bool ReadNode(std::ifstream& stream, UIDocumentNode& outNode) {
bool ReadNode(std::istream& stream, UIDocumentNode& outNode) {
if (!ReadString(stream, outNode.tagName)) {
return false;
}
@@ -178,7 +187,7 @@ struct UIDocumentArtifactSchemaAttributeHeader {
Core::uint32 restrictDocumentKind = 0;
};
bool WriteSchemaAttribute(std::ofstream& stream, const UISchemaAttributeDefinition& attribute) {
bool WriteSchemaAttribute(std::ostream& stream, const UISchemaAttributeDefinition& attribute) {
WriteString(stream, attribute.name);
UIDocumentArtifactSchemaAttributeHeader header;
@@ -204,7 +213,7 @@ bool WriteSchemaAttribute(std::ofstream& stream, const UISchemaAttributeDefiniti
return true;
}
bool ReadSchemaAttribute(std::ifstream& stream, UISchemaAttributeDefinition& outAttribute) {
bool ReadSchemaAttribute(std::istream& stream, UISchemaAttributeDefinition& outAttribute) {
if (!ReadString(stream, outAttribute.name)) {
return false;
}
@@ -234,7 +243,7 @@ bool ReadSchemaAttribute(std::ifstream& stream, UISchemaAttributeDefinition& out
return true;
}
bool WriteSchemaElement(std::ofstream& stream, const UISchemaElementDefinition& element) {
bool WriteSchemaElement(std::ostream& stream, const UISchemaElementDefinition& element) {
WriteString(stream, element.tagName);
UIDocumentArtifactSchemaElementHeader header;
@@ -264,7 +273,7 @@ bool WriteSchemaElement(std::ofstream& stream, const UISchemaElementDefinition&
return true;
}
bool ReadSchemaElement(std::ifstream& stream, UISchemaElementDefinition& outElement) {
bool ReadSchemaElement(std::istream& stream, UISchemaElementDefinition& outElement) {
if (!ReadString(stream, outElement.tagName)) {
return false;
}
@@ -302,7 +311,7 @@ bool ReadSchemaElement(std::ifstream& stream, UISchemaElementDefinition& outElem
return true;
}
bool WriteSchemaDefinition(std::ofstream& stream, const UISchemaDefinition& schemaDefinition) {
bool WriteSchemaDefinition(std::ostream& stream, const UISchemaDefinition& schemaDefinition) {
WriteString(stream, schemaDefinition.name);
UIDocumentArtifactSchemaDefinitionHeader header;
@@ -322,7 +331,7 @@ bool WriteSchemaDefinition(std::ofstream& stream, const UISchemaDefinition& sche
return true;
}
bool ReadSchemaDefinition(std::ifstream& stream, UISchemaDefinition& outSchemaDefinition) {
bool ReadSchemaDefinition(std::istream& stream, UISchemaDefinition& outSchemaDefinition) {
if (!ReadString(stream, outSchemaDefinition.name)) {
return false;
}
@@ -347,6 +356,193 @@ bool ReadSchemaDefinition(std::ifstream& stream, UISchemaDefinition& outSchemaDe
return true;
}
ResourceType GetUIDocumentResourceType(UIDocumentKind kind) {
switch (kind) {
case UIDocumentKind::View:
return ResourceType::UIView;
case UIDocumentKind::Theme:
return ResourceType::UITheme;
case UIDocumentKind::Schema:
return ResourceType::UISchema;
default:
return ResourceType::Unknown;
}
}
bool SerializeUIDocumentArtifactPayload(const UIDocumentCompileResult& compileResult,
Containers::Array<Core::uint8>& outPayload,
Containers::String* outErrorMessage) {
outPayload.Clear();
if (!compileResult.succeeded || !compileResult.document.valid) {
if (outErrorMessage != nullptr) {
*outErrorMessage = "UI document compile result is invalid.";
}
return false;
}
std::ostringstream output(std::ios::binary | std::ios::out);
UIDocumentArtifactFileHeader header;
header.kind = static_cast<Core::uint32>(compileResult.document.kind);
header.dependencyCount = static_cast<Core::uint32>(compileResult.document.dependencies.Size());
header.diagnosticCount = static_cast<Core::uint32>(compileResult.document.diagnostics.Size());
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
if (!output) {
if (outErrorMessage != nullptr) {
*outErrorMessage = "Failed to serialize UI document artifact header.";
}
return false;
}
WriteString(output, compileResult.document.sourcePath);
WriteString(output, compileResult.document.displayName);
if (!WriteNode(output, compileResult.document.rootNode)) {
if (outErrorMessage != nullptr) {
*outErrorMessage = "Failed to serialize UI document node tree.";
}
return false;
}
if (!WriteSchemaDefinition(output, compileResult.document.schemaDefinition)) {
if (outErrorMessage != nullptr) {
*outErrorMessage = "Failed to serialize UI document schema definition.";
}
return false;
}
for (const Containers::String& dependency : compileResult.document.dependencies) {
WriteString(output, dependency);
if (!output) {
if (outErrorMessage != nullptr) {
*outErrorMessage = "Failed to serialize UI document dependencies.";
}
return false;
}
}
for (const UIDocumentDiagnostic& diagnostic : compileResult.document.diagnostics) {
UIDocumentArtifactDiagnosticHeader diagnosticHeader;
diagnosticHeader.severity = static_cast<Core::uint32>(diagnostic.severity);
diagnosticHeader.line = diagnostic.location.line;
diagnosticHeader.column = diagnostic.location.column;
output.write(reinterpret_cast<const char*>(&diagnosticHeader), sizeof(diagnosticHeader));
WriteString(output, diagnostic.message);
if (!output) {
if (outErrorMessage != nullptr) {
*outErrorMessage = "Failed to serialize UI document diagnostics.";
}
return false;
}
}
const std::string serialized = output.str();
outPayload.Resize(serialized.size());
if (!serialized.empty()) {
std::memcpy(outPayload.Data(), serialized.data(), serialized.size());
}
return true;
}
bool ParseUIDocumentArtifactStream(std::istream& input,
const Containers::String& artifactPath,
UIDocumentKind expectedKind,
UIDocumentCompileResult& outResult) {
UIDocumentArtifactFileHeader header;
input.read(reinterpret_cast<char*>(&header), sizeof(header));
if (!input) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact header: ") + artifactPath;
return false;
}
const UIDocumentArtifactFileHeader expectedHeader;
if (std::memcmp(header.magic, expectedHeader.magic, sizeof(header.magic)) != 0) {
outResult.errorMessage = Containers::String("Invalid UI document artifact magic: ") + artifactPath;
return false;
}
if (header.schemaVersion != 1u &&
header.schemaVersion != kUIDocumentArtifactSchemaVersion) {
outResult.errorMessage = Containers::String("Unsupported UI document artifact schema version: ") + artifactPath;
return false;
}
if (header.kind != static_cast<Core::uint32>(expectedKind)) {
outResult.errorMessage = Containers::String("UI document artifact kind mismatch: ") + artifactPath;
return false;
}
outResult.document.kind = expectedKind;
if (!ReadString(input, outResult.document.sourcePath) ||
!ReadString(input, outResult.document.displayName) ||
!ReadNode(input, outResult.document.rootNode)) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact body: ") + artifactPath;
return false;
}
outResult.document.schemaDefinition.Clear();
if (header.schemaVersion >= 2u) {
if (!ReadSchemaDefinition(input, outResult.document.schemaDefinition)) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact schema definition: ") + artifactPath;
return false;
}
}
outResult.document.dependencies.Clear();
outResult.document.dependencies.Reserve(header.dependencyCount);
for (Core::uint32 index = 0; index < header.dependencyCount; ++index) {
Containers::String dependency;
if (!ReadString(input, dependency)) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact dependencies: ") + artifactPath;
return false;
}
outResult.document.dependencies.PushBack(std::move(dependency));
}
outResult.document.diagnostics.Clear();
outResult.document.diagnostics.Reserve(header.diagnosticCount);
for (Core::uint32 index = 0; index < header.diagnosticCount; ++index) {
UIDocumentArtifactDiagnosticHeader diagnosticHeader;
input.read(reinterpret_cast<char*>(&diagnosticHeader), sizeof(diagnosticHeader));
if (!input) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact diagnostic header: ") + artifactPath;
return false;
}
UIDocumentDiagnostic diagnostic;
diagnostic.severity = static_cast<UIDocumentDiagnosticSeverity>(diagnosticHeader.severity);
diagnostic.location.line = diagnosticHeader.line;
diagnostic.location.column = diagnosticHeader.column;
if (!ReadString(input, diagnostic.message)) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact diagnostic message: ") + artifactPath;
return false;
}
outResult.document.diagnostics.PushBack(std::move(diagnostic));
}
if (expectedKind == UIDocumentKind::Schema) {
const Containers::String sourcePath =
outResult.document.sourcePath.Empty()
? artifactPath
: outResult.document.sourcePath;
if (!outResult.document.schemaDefinition.valid &&
!BuildSchemaDefinition(
sourcePath,
outResult.document.rootNode,
outResult.document.displayName,
outResult.document.schemaDefinition,
outResult.document.diagnostics,
outResult.errorMessage)) {
outResult.succeeded = false;
outResult.document.valid = false;
return false;
}
}
outResult.document.valid = true;
outResult.succeeded = true;
return true;
}
bool IsNameStartChar(char ch) {
return std::isalpha(static_cast<unsigned char>(ch)) != 0 ||
ch == '_' ||
@@ -1548,74 +1744,27 @@ bool CompileUIDocument(const UIDocumentCompileRequest& request,
bool WriteUIDocumentArtifact(const Containers::String& artifactPath,
const UIDocumentCompileResult& compileResult,
Containers::String* outErrorMessage) {
if (!compileResult.succeeded || !compileResult.document.valid) {
Containers::Array<Core::uint8> payload;
if (!SerializeUIDocumentArtifactPayload(compileResult, payload, outErrorMessage)) {
return false;
}
const ResourceType resourceType = GetUIDocumentResourceType(compileResult.document.kind);
if (resourceType == ResourceType::Unknown) {
if (outErrorMessage != nullptr) {
*outErrorMessage = "UI document compile result is invalid.";
*outErrorMessage = "UI document kind is not importable.";
}
return false;
}
std::ofstream output(artifactPath.CStr(), std::ios::binary | std::ios::trunc);
if (!output.is_open()) {
if (outErrorMessage != nullptr) {
*outErrorMessage = Containers::String("Unable to write UI document artifact: ") + artifactPath;
}
return false;
}
UIDocumentArtifactFileHeader header;
header.kind = static_cast<Core::uint32>(compileResult.document.kind);
header.dependencyCount = static_cast<Core::uint32>(compileResult.document.dependencies.Size());
header.diagnosticCount = static_cast<Core::uint32>(compileResult.document.diagnostics.Size());
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
if (!output) {
if (outErrorMessage != nullptr) {
*outErrorMessage = Containers::String("Failed to write UI document artifact header: ") + artifactPath;
}
return false;
}
WriteString(output, compileResult.document.sourcePath);
WriteString(output, compileResult.document.displayName);
if (!WriteNode(output, compileResult.document.rootNode)) {
if (outErrorMessage != nullptr) {
*outErrorMessage = Containers::String("Failed to write UI document node tree: ") + artifactPath;
}
return false;
}
if (!WriteSchemaDefinition(output, compileResult.document.schemaDefinition)) {
if (outErrorMessage != nullptr) {
*outErrorMessage = Containers::String("Failed to write UI document schema definition: ") + artifactPath;
}
return false;
}
for (const Containers::String& dependency : compileResult.document.dependencies) {
WriteString(output, dependency);
if (!output) {
if (outErrorMessage != nullptr) {
*outErrorMessage = Containers::String("Failed to write UI document dependencies: ") + artifactPath;
}
return false;
}
}
for (const UIDocumentDiagnostic& diagnostic : compileResult.document.diagnostics) {
UIDocumentArtifactDiagnosticHeader diagnosticHeader;
diagnosticHeader.severity = static_cast<Core::uint32>(diagnostic.severity);
diagnosticHeader.line = diagnostic.location.line;
diagnosticHeader.column = diagnostic.location.column;
output.write(reinterpret_cast<const char*>(&diagnosticHeader), sizeof(diagnosticHeader));
WriteString(output, diagnostic.message);
if (!output) {
if (outErrorMessage != nullptr) {
*outErrorMessage = Containers::String("Failed to write UI document diagnostics: ") + artifactPath;
}
return false;
}
}
return true;
ArtifactContainerWriter writer;
ArtifactContainerEntry mainEntry;
mainEntry.name = "main";
mainEntry.resourceType = resourceType;
mainEntry.localID = kMainAssetLocalID;
mainEntry.payload = std::move(payload);
writer.AddEntry(std::move(mainEntry));
return writer.WriteToFile(artifactPath, outErrorMessage);
}
bool LoadUIDocumentArtifact(const Containers::String& artifactPath,
@@ -1623,106 +1772,30 @@ bool LoadUIDocumentArtifact(const Containers::String& artifactPath,
UIDocumentCompileResult& outResult) {
outResult = UIDocumentCompileResult();
Containers::Array<Core::uint8> payload;
Containers::String containerError;
if (ReadArtifactContainerPayloadByPath(
artifactPath,
GetUIDocumentResourceType(expectedKind),
payload,
&containerError)) {
std::string inputBytes;
inputBytes.resize(payload.Size());
if (!payload.Empty()) {
std::memcpy(inputBytes.data(), payload.Data(), payload.Size());
}
std::istringstream input(inputBytes, std::ios::binary | std::ios::in);
return ParseUIDocumentArtifactStream(input, artifactPath, expectedKind, outResult);
}
std::ifstream input(artifactPath.CStr(), std::ios::binary);
if (!input.is_open()) {
outResult.errorMessage = Containers::String("Unable to open UI document artifact: ") + artifactPath;
return false;
}
UIDocumentArtifactFileHeader header;
input.read(reinterpret_cast<char*>(&header), sizeof(header));
if (!input) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact header: ") + artifactPath;
return false;
}
const UIDocumentArtifactFileHeader expectedHeader;
if (std::memcmp(header.magic, expectedHeader.magic, sizeof(header.magic)) != 0) {
outResult.errorMessage = Containers::String("Invalid UI document artifact magic: ") + artifactPath;
return false;
}
if (header.schemaVersion != 1u &&
header.schemaVersion != kUIDocumentArtifactSchemaVersion) {
outResult.errorMessage = Containers::String("Unsupported UI document artifact schema version: ") + artifactPath;
return false;
}
if (header.kind != static_cast<Core::uint32>(expectedKind)) {
outResult.errorMessage = Containers::String("UI document artifact kind mismatch: ") + artifactPath;
return false;
}
outResult.document.kind = expectedKind;
if (!ReadString(input, outResult.document.sourcePath) ||
!ReadString(input, outResult.document.displayName) ||
!ReadNode(input, outResult.document.rootNode)) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact body: ") + artifactPath;
return false;
}
outResult.document.schemaDefinition.Clear();
if (header.schemaVersion >= 2u) {
if (!ReadSchemaDefinition(input, outResult.document.schemaDefinition)) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact schema definition: ") + artifactPath;
return false;
}
}
outResult.document.dependencies.Clear();
outResult.document.dependencies.Reserve(header.dependencyCount);
for (Core::uint32 index = 0; index < header.dependencyCount; ++index) {
Containers::String dependency;
if (!ReadString(input, dependency)) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact dependencies: ") + artifactPath;
return false;
}
outResult.document.dependencies.PushBack(std::move(dependency));
}
outResult.document.diagnostics.Clear();
outResult.document.diagnostics.Reserve(header.diagnosticCount);
for (Core::uint32 index = 0; index < header.diagnosticCount; ++index) {
UIDocumentArtifactDiagnosticHeader diagnosticHeader;
input.read(reinterpret_cast<char*>(&diagnosticHeader), sizeof(diagnosticHeader));
if (!input) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact diagnostic header: ") + artifactPath;
return false;
}
UIDocumentDiagnostic diagnostic;
diagnostic.severity = static_cast<UIDocumentDiagnosticSeverity>(diagnosticHeader.severity);
diagnostic.location.line = diagnosticHeader.line;
diagnostic.location.column = diagnosticHeader.column;
if (!ReadString(input, diagnostic.message)) {
outResult.errorMessage = Containers::String("Failed to read UI document artifact diagnostic message: ") + artifactPath;
return false;
}
outResult.document.diagnostics.PushBack(std::move(diagnostic));
}
if (expectedKind == UIDocumentKind::Schema) {
const Containers::String sourcePath =
outResult.document.sourcePath.Empty()
? artifactPath
: outResult.document.sourcePath;
if (!outResult.document.schemaDefinition.valid &&
!BuildSchemaDefinition(
sourcePath,
outResult.document.rootNode,
outResult.document.displayName,
outResult.document.schemaDefinition,
outResult.document.diagnostics,
outResult.errorMessage)) {
outResult.succeeded = false;
outResult.document.valid = false;
return false;
}
}
outResult.document.valid = true;
outResult.succeeded = true;
return true;
return ParseUIDocumentArtifactStream(input, artifactPath, expectedKind, outResult);
}
Containers::String GetUIDocumentDefaultRootTag(UIDocumentKind kind) {

View File

@@ -1,4 +1,5 @@
#include <XCEngine/Resources/UI/UIDocumentLoaders.h>
#include <XCEngine/Core/Asset/ArtifactContainer.h>
#include <filesystem>
@@ -8,7 +9,14 @@ namespace Resources {
namespace {
Containers::String GetPathExtension(const Containers::String& path) {
const std::filesystem::path fsPath(path.CStr());
Containers::String normalizedPath = path;
Containers::String containerPath;
Containers::String entryName;
if (TryParseArtifactContainerEntryPath(path, containerPath, entryName)) {
normalizedPath = containerPath;
}
const std::filesystem::path fsPath(normalizedPath.CStr());
const std::string extension = fsPath.has_extension()
? fsPath.extension().generic_string()
: std::string();

View File

@@ -1,5 +1,6 @@
#include <XCEngine/Resources/Volume/VolumeFieldLoader.h>
#include <XCEngine/Core/Asset/ArtifactContainer.h>
#include <XCEngine/Core/Asset/ArtifactFormats.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Debug/Logger.h>
@@ -82,25 +83,66 @@ std::filesystem::path ResolveVolumeFieldPath(const Containers::String& path) {
return resolvedPath.lexically_normal();
}
LoadResult LoadVolumeFieldArtifact(const Containers::String& path) {
const std::filesystem::path resolvedPath = ResolveVolumeFieldPath(path);
const auto totalStart = std::chrono::steady_clock::now();
bool ReadVolumeArtifactData(const Containers::String& path,
Containers::Array<Core::uint8>& outData,
bool& outReadFromContainer,
std::filesystem::path& outResolvedPath) {
outData.Clear();
outReadFromContainer = false;
outResolvedPath = ResolveVolumeFieldPath(path);
const auto openStart = std::chrono::steady_clock::now();
std::ifstream input(resolvedPath, std::ios::binary);
const auto openEnd = std::chrono::steady_clock::now();
Containers::String containerError;
if (ReadArtifactContainerPayloadByPath(
Containers::String(outResolvedPath.generic_string().c_str()),
ResourceType::VolumeField,
outData,
&containerError)) {
outReadFromContainer = true;
return true;
}
std::ifstream input(outResolvedPath, std::ios::binary | std::ios::ate);
if (!input.is_open()) {
return false;
}
const std::streamsize size = input.tellg();
if (size < 0) {
return false;
}
input.seekg(0, std::ios::beg);
outData.Resize(static_cast<size_t>(size));
if (size == 0) {
return true;
}
if (!input.read(reinterpret_cast<char*>(outData.Data()), size)) {
outData.Clear();
return false;
}
return true;
}
LoadResult LoadVolumeFieldArtifact(const Containers::String& path) {
const auto totalStart = std::chrono::steady_clock::now();
Containers::Array<Core::uint8> data;
bool readFromContainer = false;
std::filesystem::path resolvedPath;
const auto readStart = std::chrono::steady_clock::now();
if (!ReadVolumeArtifactData(path, data, readFromContainer, resolvedPath)) {
return LoadResult(Containers::String("Failed to read volume artifact: ") + path);
}
const auto readEnd = std::chrono::steady_clock::now();
const auto headerStart = std::chrono::steady_clock::now();
VolumeFieldArtifactHeader header;
input.read(reinterpret_cast<char*>(&header), sizeof(header));
const auto headerEnd = std::chrono::steady_clock::now();
if (!input) {
if (data.Size() < sizeof(VolumeFieldArtifactHeader)) {
return LoadResult(Containers::String("Failed to parse volume artifact header: ") + path);
}
VolumeFieldArtifactHeader header;
std::memcpy(&header, data.Data(), sizeof(header));
const bool validHeader =
std::memcmp(header.magic, "XCVOL02", 7) == 0 &&
header.schemaVersion == kVolumeFieldArtifactSchemaVersion &&
@@ -110,23 +152,26 @@ LoadResult LoadVolumeFieldArtifact(const Containers::String& path) {
}
Containers::Array<Core::uint8> payload;
payload.ResizeUninitialized(static_cast<size_t>(header.payloadSize));
const auto payloadReadStart = std::chrono::steady_clock::now();
input.read(reinterpret_cast<char*>(payload.Data()), static_cast<std::streamsize>(header.payloadSize));
const auto payloadReadEnd = std::chrono::steady_clock::now();
if (!input) {
const size_t payloadOffset = sizeof(VolumeFieldArtifactHeader);
if (header.payloadSize > static_cast<Core::uint64>(data.Size() - payloadOffset)) {
return LoadResult(Containers::String("Failed to read volume artifact payload: ") + path);
}
payload.ResizeUninitialized(static_cast<size_t>(header.payloadSize));
if (header.payloadSize > 0) {
std::memcpy(
payload.Data(),
data.Data() + payloadOffset,
static_cast<size_t>(header.payloadSize));
}
const auto totalEnd = std::chrono::steady_clock::now();
LogVolumeTraceFileSystem(
"VolumeFieldLoader Artifact path=" + std::string(path.CStr()) +
" resolved=" + resolvedPath.generic_string() +
" open_ms=" +
std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(openEnd - openStart).count()) +
" header_ms=" +
std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(headerEnd - headerStart).count()) +
" payload_read_ms=" +
std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(payloadReadEnd - payloadReadStart).count()) +
" mode=" +
std::string(readFromContainer ? "container" : "raw") +
" read_ms=" +
std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(readEnd - readStart).count()) +
" total_ms=" +
std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(totalEnd - totalStart).count()) +
" payload_bytes=" +